summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Héctor Ramón Jiménez <hector0193@gmail.com>2024-05-23 13:29:45 +0200
committerLibravatar Héctor Ramón Jiménez <hector0193@gmail.com>2024-05-23 13:29:45 +0200
commitd8ba6b0673a33724a177f3a1ba59705527280142 (patch)
tree89482c8d1e3a03e00b3a8151abbb81e30ae5898c
parent72ed8bcc8def9956e25f3720a3095fc96bb2eef0 (diff)
parent468794d918eb06c1dbebb33c32b10017ad335f05 (diff)
downloadiced-d8ba6b0673a33724a177f3a1ba59705527280142.tar.gz
iced-d8ba6b0673a33724a177f3a1ba59705527280142.tar.bz2
iced-d8ba6b0673a33724a177f3a1ba59705527280142.zip
Merge branch 'master' into feat/text-macro
-rw-r--r--.cargo/config.toml53
-rw-r--r--.github/workflows/check.yml18
-rw-r--r--.github/workflows/document.yml2
-rw-r--r--.github/workflows/lint.yml7
-rw-r--r--Cargo.toml55
-rw-r--r--DEPENDENCIES.md2
-rw-r--r--benches/ipsum.txt9
-rw-r--r--benches/wgpu.rs228
-rw-r--r--core/Cargo.toml13
-rw-r--r--core/src/angle.rs75
-rw-r--r--core/src/content_fit.rs17
-rw-r--r--core/src/element.rs4
-rw-r--r--core/src/hasher.rs2
-rw-r--r--core/src/image.rs216
-rw-r--r--core/src/layout.rs2
-rw-r--r--core/src/lib.rs11
-rw-r--r--core/src/mouse/interaction.rs1
-rw-r--r--core/src/overlay.rs2
-rw-r--r--core/src/rectangle.rs79
-rw-r--r--core/src/renderer.rs33
-rw-r--r--core/src/renderer/null.rs75
-rw-r--r--core/src/rotation.rs72
-rw-r--r--core/src/size.rs45
-rw-r--r--core/src/svg.rs16
-rw-r--r--core/src/text.rs10
-rw-r--r--core/src/text/paragraph.rs6
-rw-r--r--core/src/theme.rs25
-rw-r--r--core/src/theme/palette.rs18
-rw-r--r--core/src/transformation.rs6
-rw-r--r--core/src/vector.rs3
-rw-r--r--core/src/widget.rs2
-rw-r--r--core/src/widget/text.rs199
-rw-r--r--core/src/window/position.rs8
-rw-r--r--core/src/window/redraw_request.rs2
-rw-r--r--core/src/window/settings/wasm.rs12
-rw-r--r--docs/redirect.html13
-rw-r--r--examples/bezier_tool/src/main.rs68
-rw-r--r--examples/checkbox/src/main.rs11
-rw-r--r--examples/clock/Cargo.toml4
-rw-r--r--examples/clock/src/main.rs24
-rw-r--r--examples/color_palette/src/main.rs4
-rw-r--r--examples/combo_box/src/main.rs9
-rw-r--r--examples/component/src/main.rs18
-rw-r--r--examples/custom_quad/src/main.rs11
-rw-r--r--examples/custom_shader/src/main.rs9
-rw-r--r--examples/custom_shader/src/scene.rs33
-rw-r--r--examples/custom_widget/src/main.rs11
-rw-r--r--examples/download_progress/src/download.rs6
-rw-r--r--examples/download_progress/src/main.rs12
-rw-r--r--examples/editor/src/main.rs2
-rw-r--r--examples/events/src/main.rs9
-rw-r--r--examples/exit/src/main.rs12
-rw-r--r--examples/ferris/Cargo.toml10
-rw-r--r--examples/ferris/src/main.rs211
-rw-r--r--examples/game_of_life/src/main.rs6
-rw-r--r--examples/geometry/src/main.rs21
-rw-r--r--examples/gradient/src/main.rs2
-rw-r--r--examples/integration/Cargo.toml2
-rw-r--r--examples/integration/README.md19
-rw-r--r--examples/integration/index.html21
-rw-r--r--examples/integration/src/main.rs571
-rw-r--r--examples/layout/src/main.rs30
-rw-r--r--examples/lazy/src/main.rs2
-rw-r--r--examples/loading_spinners/src/circular.rs2
-rw-r--r--examples/loading_spinners/src/main.rs10
-rw-r--r--examples/loupe/src/main.rs12
-rw-r--r--examples/modal/src/main.rs368
-rw-r--r--examples/multi_window/src/main.rs13
-rw-r--r--examples/pane_grid/src/main.rs32
-rw-r--r--examples/pokedex/src/main.rs11
-rw-r--r--examples/qr_code/src/main.rs14
-rw-r--r--examples/screenshot/src/main.rs24
-rw-r--r--examples/scrollable/src/main.rs6
-rw-r--r--examples/sierpinski_triangle/src/main.rs4
-rw-r--r--examples/slider/src/main.rs14
-rw-r--r--examples/solar_system/src/main.rs4
-rw-r--r--examples/stopwatch/src/main.rs11
-rw-r--r--examples/styling/src/main.rs13
-rw-r--r--examples/svg/src/main.rs19
-rw-r--r--examples/system_information/src/main.rs11
-rw-r--r--examples/the_matrix/Cargo.toml13
-rw-r--r--examples/the_matrix/src/main.rs115
-rw-r--r--examples/toast/src/main.rs34
-rw-r--r--examples/todos/src/main.rs17
-rw-r--r--examples/tooltip/src/main.rs11
-rw-r--r--examples/tour/src/main.rs14
-rw-r--r--examples/url_handler/src/main.rs11
-rw-r--r--examples/websocket/src/echo.rs23
-rw-r--r--examples/websocket/src/main.rs19
-rw-r--r--futures/Cargo.toml4
-rw-r--r--futures/src/backend/native/async_std.rs3
-rw-r--r--futures/src/backend/native/smol.rs3
-rw-r--r--futures/src/backend/native/tokio.rs17
-rw-r--r--futures/src/backend/wasm/wasm_bindgen.rs3
-rw-r--r--futures/src/lib.rs7
-rw-r--r--futures/src/subscription.rs4
-rw-r--r--futures/src/subscription/tracker.rs9
-rw-r--r--graphics/Cargo.toml4
-rw-r--r--graphics/src/backend.rs32
-rw-r--r--graphics/src/cache.rs189
-rw-r--r--graphics/src/compositor.rs108
-rw-r--r--graphics/src/damage.rs240
-rw-r--r--graphics/src/error.rs27
-rw-r--r--graphics/src/geometry.rs31
-rw-r--r--graphics/src/geometry/cache.rs116
-rw-r--r--graphics/src/geometry/frame.rs249
-rw-r--r--graphics/src/image.rs208
-rw-r--r--graphics/src/layer.rs144
-rw-r--r--graphics/src/lib.rs29
-rw-r--r--graphics/src/mesh.rs100
-rw-r--r--graphics/src/primitive.rs160
-rw-r--r--graphics/src/renderer.rs252
-rw-r--r--graphics/src/settings.rs (renamed from renderer/src/settings.rs)4
-rw-r--r--graphics/src/text.rs129
-rw-r--r--graphics/src/text/cache.rs16
-rw-r--r--graphics/src/text/editor.rs12
-rw-r--r--graphics/src/text/paragraph.rs4
-rw-r--r--highlighter/Cargo.toml3
-rw-r--r--highlighter/src/lib.rs31
-rw-r--r--renderer/Cargo.toml13
-rw-r--r--renderer/src/compositor.rs269
-rw-r--r--renderer/src/fallback.rs637
-rw-r--r--renderer/src/geometry.rs210
-rw-r--r--renderer/src/geometry/cache.rs125
-rw-r--r--renderer/src/lib.rs314
-rw-r--r--runtime/Cargo.toml4
-rw-r--r--runtime/src/lib.rs7
-rw-r--r--runtime/src/multi_window/state.rs2
-rw-r--r--runtime/src/program.rs4
-rw-r--r--runtime/src/program/state.rs2
-rw-r--r--runtime/src/user_interface.rs12
-rw-r--r--runtime/src/window.rs2
-rw-r--r--runtime/src/window/action.rs2
-rw-r--r--runtime/src/window/screenshot.rs16
-rw-r--r--src/advanced.rs6
-rw-r--r--src/application.rs37
-rw-r--r--src/lib.rs17
-rw-r--r--src/multi_window.rs4
-rw-r--r--src/program.rs66
-rw-r--r--src/window/icon.rs2
-rw-r--r--tiny_skia/Cargo.toml4
-rw-r--r--tiny_skia/src/backend.rs1020
-rw-r--r--tiny_skia/src/engine.rs856
-rw-r--r--tiny_skia/src/geometry.rs171
-rw-r--r--tiny_skia/src/layer.rs341
-rw-r--r--tiny_skia/src/lib.rs405
-rw-r--r--tiny_skia/src/primitive.rs30
-rw-r--r--tiny_skia/src/raster.rs12
-rw-r--r--tiny_skia/src/settings.rs14
-rw-r--r--tiny_skia/src/text.rs31
-rw-r--r--tiny_skia/src/vector.rs21
-rw-r--r--tiny_skia/src/window/compositor.rs95
-rw-r--r--wgpu/Cargo.toml8
-rw-r--r--wgpu/src/backend.rs399
-rw-r--r--wgpu/src/buffer.rs45
-rw-r--r--wgpu/src/color.rs68
-rw-r--r--wgpu/src/engine.rs87
-rw-r--r--wgpu/src/geometry.rs578
-rw-r--r--wgpu/src/image/atlas.rs53
-rw-r--r--wgpu/src/image/atlas/allocator.rs4
-rw-r--r--wgpu/src/image/atlas/layer.rs8
-rw-r--r--wgpu/src/image/cache.rs86
-rw-r--r--wgpu/src/image/mod.rs (renamed from wgpu/src/image.rs)489
-rw-r--r--wgpu/src/image/null.rs10
-rw-r--r--wgpu/src/image/raster.rs19
-rw-r--r--wgpu/src/image/vector.rs18
-rw-r--r--wgpu/src/layer.rs581
-rw-r--r--wgpu/src/layer/image.rs30
-rw-r--r--wgpu/src/layer/mesh.rs97
-rw-r--r--wgpu/src/layer/pipeline.rs17
-rw-r--r--wgpu/src/layer/text.rs70
-rw-r--r--wgpu/src/lib.rs582
-rw-r--r--wgpu/src/primitive.rs105
-rw-r--r--wgpu/src/primitive/pipeline.rs116
-rw-r--r--wgpu/src/quad.rs118
-rw-r--r--wgpu/src/quad/gradient.rs5
-rw-r--r--wgpu/src/quad/solid.rs5
-rw-r--r--wgpu/src/settings.rs77
-rw-r--r--wgpu/src/shader/blit.wgsl20
-rw-r--r--wgpu/src/shader/image.wgsl37
-rw-r--r--wgpu/src/shader/quad/solid.wgsl12
-rw-r--r--wgpu/src/text.rs836
-rw-r--r--wgpu/src/triangle.rs496
-rw-r--r--wgpu/src/triangle/msaa.rs130
-rw-r--r--wgpu/src/window/compositor.rs250
-rw-r--r--widget/Cargo.toml5
-rw-r--r--widget/src/button.rs143
-rw-r--r--widget/src/canvas.rs37
-rw-r--r--widget/src/canvas/program.rs5
-rw-r--r--widget/src/checkbox.rs101
-rw-r--r--widget/src/column.rs9
-rw-r--r--widget/src/combo_box.rs193
-rw-r--r--widget/src/container.rs233
-rw-r--r--widget/src/helpers.rs517
-rw-r--r--widget/src/image.rs93
-rw-r--r--widget/src/image/viewer.rs23
-rw-r--r--widget/src/keyed/column.rs48
-rw-r--r--widget/src/lazy.rs21
-rw-r--r--widget/src/lazy/component.rs2
-rw-r--r--widget/src/lazy/responsive.rs45
-rw-r--r--widget/src/lib.rs10
-rw-r--r--widget/src/mouse_area.rs2
-rw-r--r--widget/src/overlay/menu.rs179
-rw-r--r--widget/src/pane_grid.rs108
-rw-r--r--widget/src/pane_grid/content.rs59
-rw-r--r--widget/src/pane_grid/state.rs8
-rw-r--r--widget/src/pane_grid/title_bar.rs64
-rw-r--r--widget/src/pick_list.rs110
-rw-r--r--widget/src/progress_bar.rs103
-rw-r--r--widget/src/qr_code.rs101
-rw-r--r--widget/src/radio.rs87
-rw-r--r--widget/src/row.rs9
-rw-r--r--widget/src/rule.rs109
-rw-r--r--widget/src/scrollable.rs511
-rw-r--r--widget/src/shader.rs11
-rw-r--r--widget/src/shader/program.rs4
-rw-r--r--widget/src/slider.rs93
-rw-r--r--widget/src/stack.rs333
-rw-r--r--widget/src/svg.rs175
-rw-r--r--widget/src/text_editor.rs150
-rw-r--r--widget/src/text_input.rs127
-rw-r--r--widget/src/themer.rs4
-rw-r--r--widget/src/toggler.rs85
-rw-r--r--widget/src/tooltip.rs41
-rw-r--r--widget/src/vertical_slider.rs52
-rw-r--r--winit/Cargo.toml7
-rw-r--r--winit/src/application.rs484
-rw-r--r--winit/src/conversion.rs91
-rw-r--r--winit/src/lib.rs8
-rw-r--r--winit/src/multi_window.rs546
-rw-r--r--winit/src/multi_window/window_manager.rs5
-rw-r--r--winit/src/proxy.rs101
232 files changed, 12506 insertions, 8395 deletions
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/workflows/check.yml b/.github/workflows/check.yml
index 4107e618..8f5e65d6 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -1,14 +1,6 @@
name: Check
on: [push, pull_request]
jobs:
- 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
-
wasm:
runs-on: ubuntu-latest
env:
@@ -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 ba482215..827a2ca8 100644
--- a/.github/workflows/document.yml
+++ b/.github/workflows/document.yml
@@ -27,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/lint.yml b/.github/workflows/lint.yml
index ccf79cb7..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@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/Cargo.toml b/Cargo.toml
index f446e2af..fc35fee8 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
@@ -50,11 +55,14 @@ 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
iced_futures.workspace = true
iced_renderer.workspace = true
iced_widget.workspace = true
@@ -69,6 +77,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
@@ -120,10 +137,12 @@ iced_winit = { version = "0.13.0-dev", path = "winit" }
async-std = "1.0"
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"
@@ -149,13 +168,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 = "forbid"
+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/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..0e407253
--- /dev/null
+++ b/benches/wgpu.rs
@@ -0,0 +1,228 @@
+#![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::<&str>(
+ &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<Message, Theme> canvas::Program<Message, Theme, Renderer> for Scene {
+ type State = canvas::Cache<Renderer>;
+
+ fn draw(
+ &self,
+ cache: &Self::State,
+ renderer: &Renderer,
+ _theme: &Theme,
+ bounds: Rectangle,
+ _cursor: mouse::Cursor,
+ ) -> Vec<canvas::Geometry<Renderer>> {
+ 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::<String>(),
+ ))
+ .size(10),
+ )
+ .into()
+}
diff --git a/core/Cargo.toml b/core/Cargo.toml
index c273fcb4..3c557bca 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -10,17 +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
+
+dark-light.workspace = true
+dark-light.optional = true
[target.'cfg(windows)'.dependencies]
raw-window-handle.workspace = true
diff --git a/core/src/angle.rs b/core/src/angle.rs
index dc3c0e93..9c8a9b24 100644
--- a/core/src/angle.rs
+++ b/core/src/angle.rs
@@ -1,12 +1,17 @@
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> = Self(0.0)..=Self(360.0);
+}
+
impl PartialEq<f32> for Degrees {
fn eq(&self, other: &f32) -> bool {
self.0.eq(other)
@@ -19,6 +24,52 @@ impl PartialOrd<f32> for Degrees {
}
}
+impl From<f32> for Degrees {
+ fn from(degrees: f32) -> Self {
+ Self(degrees)
+ }
+}
+
+impl From<u8> for Degrees {
+ fn from(degrees: u8) -> Self {
+ Self(f32::from(degrees))
+ }
+}
+
+impl From<Degrees> for f32 {
+ fn from(degrees: Degrees) -> Self {
+ degrees.0
+ }
+}
+
+impl From<Degrees> for f64 {
+ fn from(degrees: Degrees) -> Self {
+ Self::from(degrees.0)
+ }
+}
+
+impl Mul<f32> 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<Self> {
+ Some(Self(n as f32))
+ }
+
+ fn from_u64(n: u64) -> Option<Self> {
+ Some(Self(n as f32))
+ }
+
+ fn from_f64(n: f64) -> Option<Self> {
+ Some(Self(n as f32))
+ }
+}
+
/// Radians
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
pub struct Radians(pub f32);
@@ -65,6 +116,12 @@ impl From<u8> for Radians {
}
}
+impl From<Radians> for f32 {
+ fn from(radians: Radians) -> Self {
+ radians.0
+ }
+}
+
impl From<Radians> for f64 {
fn from(radians: Radians) -> Self {
Self::from(radians.0)
@@ -107,6 +164,14 @@ impl Add for Radians {
}
}
+impl Add<Degrees> 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;
@@ -153,6 +218,14 @@ impl Div for Radians {
}
}
+impl Rem for Radians {
+ type Output = Self;
+
+ fn rem(self, rhs: Self) -> Self::Output {
+ Self(self.0 % rhs.0)
+ }
+}
+
impl PartialEq<f32> for Radians {
fn eq(&self, other: &f32) -> bool {
self.0.eq(other)
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 989eaa3b..7d918a2e 100644
--- a/core/src/element.rs
+++ b/core/src/element.rs
@@ -95,7 +95,7 @@ impl<'a, Message, Theme, Renderer> Element<'a, Message, Theme, Renderer> {
///
/// ```no_run
/// # mod iced {
- /// # pub type Element<'a, Message> = iced_core::Element<'a, Message, iced_core::Theme, iced_core::renderer::Null>;
+ /// # pub type Element<'a, Message> = iced_core::Element<'a, Message, iced_core::Theme, ()>;
/// #
/// # pub mod widget {
/// # pub fn row<'a, Message>(iter: impl IntoIterator<Item = super::Element<'a, Message>>) -> super::Element<'a, Message> {
@@ -109,7 +109,7 @@ impl<'a, Message, Theme, Renderer> Element<'a, Message, Theme, Renderer> {
/// # pub enum Message {}
/// # pub struct Counter;
/// #
- /// # pub type Element<'a, Message> = iced_core::Element<'a, Message, iced_core::Theme, iced_core::renderer::Null>;
+ /// # pub type Element<'a, Message> = iced_core::Element<'a, Message, iced_core::Theme, ()>;
/// #
/// # impl Counter {
/// # pub fn view(&self) -> Element<Message> {
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<T: Into<PathBuf>>(path: T) -> Handle {
- Self::from_data(Data::Path(path.into()))
- }
+ let path = path.into();
- /// Creates an image [`Handle`] containing the image pixels directly. This
- /// function expects the input data to be provided as a `Vec<u8>` 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),
- })
+ Self::Path(Id::path(&path), path)
}
- /// 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<Bytes>) -> 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<Bytes>,
+ ) -> 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,90 +101,46 @@ where
}
}
-impl Hash for Handle {
- fn hash<H: std::hash::Hasher>(&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<dyn AsRef<[u8]> + 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 {
+impl std::fmt::Debug for Handle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- self.0.as_ref().as_ref().fmt(f)
+ match self {
+ Self::Path(_, path) => write!(f, "Path({path:?})"),
+ Self::Bytes(_, _) => write!(f, "Bytes(...)"),
+ Self::Rgba { width, height, .. } => {
+ write!(f, "Pixels({width} * {height})")
+ }
+ }
}
}
-impl std::hash::Hash for Bytes {
- fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
- self.0.as_ref().as_ref().hash(state);
- }
-}
+/// The unique identifier of some [`Handle`] data.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Id(_Id);
-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
- }
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+enum _Id {
+ Unique(u64),
+ Hash(u64),
}
-impl Eq for Bytes {}
-
-impl AsRef<[u8]> for Bytes {
- fn as_ref(&self) -> &[u8] {
- self.0.as_ref().as_ref()
- }
-}
+impl Id {
+ fn unique() -> Self {
+ use std::sync::atomic::{self, AtomicU64};
-impl std::ops::Deref for Bytes {
- type Target = [u8];
+ static NEXT_ID: AtomicU64 = AtomicU64::new(0);
- fn deref(&self) -> &[u8] {
- self.0.as_ref().as_ref()
+ Self(_Id::Unique(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)))
}
-}
-/// The data of a raster image.
-#[derive(Clone, PartialEq, Eq, Hash)]
-pub enum Data {
- /// File data
- Path(PathBuf),
+ fn path(path: impl AsRef<Path>) -> Self {
+ let hash = {
+ let mut hasher = FxHasher::default();
+ path.as_ref().hash(&mut hasher);
- /// 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,
- },
-}
+ hasher.finish()
+ };
-impl std::fmt::Debug for Data {
- 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, .. } => {
- write!(f, "Pixels({width} * {height})")
- }
- }
+ Self(_Id::Hash(hash))
}
}
@@ -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<u32>;
+ fn measure_image(&self, handle: &Self::Handle) -> Size<u32>;
/// 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<Item = Layout<'a>> {
+ pub fn children(self) -> impl DoubleEndedIterator<Item = Layout<'a>> {
self.node.children().iter().map(move |node| {
Layout::with_offset(
Vector::new(self.position.x, self.position.y),
diff --git a/core/src/lib.rs b/core/src/lib.rs
index d076413e..32156441 100644
--- a/core/src/lib.rs
+++ b/core/src/lib.rs
@@ -9,13 +9,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
-)]
pub mod alignment;
pub mod border;
pub mod clipboard;
@@ -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,6 +65,7 @@ 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;
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<T = f32> {
/// X coordinate of the top-left corner.
@@ -16,24 +16,32 @@ pub struct Rectangle<T = f32> {
pub height: T,
}
-impl Rectangle<f32> {
- /// 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<T> Rectangle<T>
+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<T>) -> 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<f32> {
+ /// 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<f32> {
}
/// Snaps the [`Rectangle`] to __unsigned__ integer coordinates.
- pub fn snap(self) -> Rectangle<u32> {
- Rectangle {
+ pub fn snap(self) -> Option<Rectangle<u32>> {
+ 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<f32> {
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<f32> for Rectangle<f32> {
@@ -212,3 +239,19 @@ where
}
}
}
+
+impl<T> std::ops::Mul<Vector<T>> for Rectangle<T>
+where
+ T: std::ops::Mul<Output = T> + Copy,
+{
+ type Output = Rectangle<T>;
+
+ fn mul(self, scale: Vector<T>) -> 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
- }
-}
-
-impl Renderer for Null {
- fn with_layer(&mut self, _bounds: Rectangle, _f: impl FnOnce(&mut Self)) {}
+ fn start_transformation(&mut self, _transformation: Transformation) {}
- 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<u32> {
+ 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<u32> {
+ Size::default()
+ }
+
+ fn draw_svg(
+ &mut self,
+ _handle: svg::Handle,
+ _color: Option<Color>,
+ _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<Radians> for Rotation {
+ fn from(radians: Radians) -> Self {
+ Self::Floating(radians)
+ }
+}
+
+impl From<f32> for Rotation {
+ fn from(radians: f32) -> Self {
+ Self::Floating(Radians(radians))
+ }
+}
diff --git a/core/src/size.rs b/core/src/size.rs
index 267fc90e..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<T = f32> {
/// The width.
pub width: T,
@@ -51,6 +51,19 @@ 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<T> From<[T; 2]> for Size<T> {
@@ -99,3 +112,31 @@ where
}
}
}
+
+impl<T> std::ops::Mul<T> for Size<T>
+where
+ T: std::ops::Mul<Output = T> + Copy,
+{
+ type Output = Size<T>;
+
+ fn mul(self, rhs: T) -> Self::Output {
+ Size {
+ width: self.width * rhs,
+ height: self.height * rhs,
+ }
+ }
+}
+
+impl<T> std::ops::Mul<Vector<T>> for Size<T>
+where
+ T: std::ops::Mul<Output = T> + Copy,
+{
+ type Output = Size<T>;
+
+ fn mul(self, scale: Vector<T>) -> 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<u32>;
+ fn measure_svg(&self, handle: &Handle) -> Size<u32>;
/// Draws an SVG with the given [`Handle`], an optional [`Color`] filter, and inside the provided `bounds`.
- fn draw(&mut self, handle: Handle, color: Option<Color>, bounds: Rectangle);
+ fn draw_svg(
+ &mut self,
+ handle: Handle,
+ color: Option<Color>,
+ 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<Content = String, Font = crate::Font> {
/// 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<String, Self::Font>,
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<Point>;
/// 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
index 948aaf83..6b2c04da 100644
--- a/core/src/theme.rs
+++ b/core/src/theme.rs
@@ -7,10 +7,9 @@ use std::fmt;
use std::sync::Arc;
/// A built-in theme.
-#[derive(Debug, Clone, PartialEq, Default)]
+#[derive(Debug, Clone, PartialEq)]
pub enum Theme {
/// The built-in light variant.
- #[default]
Light,
/// The built-in dark variant.
Dark,
@@ -161,6 +160,28 @@ impl Theme {
}
}
+impl Default for Theme {
+ fn default() -> Self {
+ #[cfg(feature = "auto-detect-theme")]
+ {
+ use once_cell::sync::Lazy;
+
+ static DEFAULT: Lazy<Theme> =
+ 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 {
diff --git a/core/src/theme/palette.rs b/core/src/theme/palette.rs
index ca91c248..e0ff397a 100644
--- a/core/src/theme/palette.rs
+++ b/core/src/theme/palette.rs
@@ -613,10 +613,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
+ }
}
}
@@ -631,6 +636,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<T> Vector<T> {
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<T> std::ops::Add for Vector<T>
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 66e2d066..f1f0b345 100644
--- a/core/src/widget/text.rs
+++ b/core/src/widget/text.rs
@@ -18,9 +18,10 @@ pub use text::{LineHeight, Shaping};
#[allow(missing_debug_implementations)]
pub struct Text<'a, Theme, Renderer>
where
+ Theme: Catalog,
Renderer: text::Renderer,
{
- content: Cow<'a, str>,
+ fragment: Fragment<'a>,
size: Option<Pixels>,
line_height: LineHeight,
width: Length,
@@ -29,20 +30,18 @@ where
vertical_alignment: alignment::Vertical,
font: Option<Renderer::Font>,
shaping: Shaping,
- style: Style<'a, Theme>,
+ class: Theme::Class<'a>,
}
impl<'a, Theme, Renderer> Text<'a, Theme, Renderer>
where
+ Theme: Catalog,
Renderer: text::Renderer,
{
/// Create a new fragment of [`Text`] with the given contents.
- pub fn new(content: impl Into<Cow<'a, str>>) -> Self
- where
- Theme: DefaultStyle + 'a,
- {
+ pub fn new(fragment: impl IntoFragment<'a>) -> Self {
Text {
- content: content.into(),
+ fragment: fragment.into_fragment(),
size: None,
line_height: LineHeight::default(),
font: None,
@@ -51,7 +50,7 @@ where
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
shaping: Shaping::Basic,
- style: Box::new(Theme::default_style),
+ class: Theme::default(),
}
}
@@ -75,25 +74,6 @@ where
self
}
- /// Sets the style of the [`Text`].
- pub fn style(mut self, style: impl Fn(&Theme) -> Appearance + 'a) -> Self {
- self.style = Box::new(style);
- self
- }
-
- /// Sets the [`Color`] of the [`Text`].
- pub fn color(self, color: impl Into<Color>) -> Self {
- self.color_maybe(Some(color))
- }
-
- /// Sets the [`Color`] of the [`Text`], if `Some`.
- pub fn color_maybe(mut self, color: Option<impl Into<Color>>) -> Self {
- let color = color.map(Into::into);
-
- self.style = Box::new(move |_theme| Appearance { color });
- self
- }
-
/// Sets the width of the [`Text`] boundaries.
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
@@ -129,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<StyleFn<'a, Theme>>,
+ {
+ self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
+ self
+ }
+
+ /// Sets the [`Color`] of the [`Text`].
+ pub fn color(self, color: impl Into<Color>) -> Self
+ where
+ Theme::Class<'a>: From<StyleFn<'a, Theme>>,
+ {
+ self.color_maybe(Some(color))
+ }
+
+ /// Sets the [`Color`] of the [`Text`], if `Some`.
+ pub fn color_maybe(self, color: Option<impl Into<Color>>) -> Self
+ where
+ Theme::Class<'a>: From<StyleFn<'a, Theme>>,
+ {
+ 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<Theme::Class<'a>>) -> Self {
+ self.class = class.into();
+ self
+ }
}
/// The internal state of a [`Text`] widget.
@@ -138,6 +154,7 @@ pub struct State<P: Paragraph>(P);
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Text<'a, Theme, Renderer>
where
+ Theme: Catalog,
Renderer: text::Renderer,
{
fn tag(&self) -> tree::Tag {
@@ -167,7 +184,7 @@ where
limits,
self.width,
self.height,
- &self.content,
+ &self.fragment,
self.line_height,
self.size,
self.font,
@@ -182,15 +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::<State<Renderer::Paragraph>>();
- let appearance = (self.style)(theme);
+ let style = theme.style(&self.class);
- draw(renderer, style, layout, state, appearance, viewport);
+ draw(renderer, defaults, layout, state, style, viewport);
}
}
@@ -250,7 +267,7 @@ pub fn draw<Renderer>(
style: &renderer::Style,
layout: Layout<'_>,
state: &State<Renderer::Paragraph>,
- appearance: Appearance,
+ appearance: Style,
viewport: &Rectangle,
) where
Renderer: text::Renderer,
@@ -281,7 +298,7 @@ pub fn draw<Renderer>(
impl<'a, Message, Theme, Renderer> From<Text<'a, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
- Theme: 'a,
+ Theme: Catalog + 'a,
Renderer: text::Renderer + 'a,
{
fn from(
@@ -293,7 +310,7 @@ where
impl<'a, Theme, Renderer> From<&'a str> for Text<'a, Theme, Renderer>
where
- Theme: DefaultStyle + 'a,
+ Theme: Catalog + 'a,
Renderer: text::Renderer,
{
fn from(content: &'a str) -> Self {
@@ -304,7 +321,7 @@ where
impl<'a, Message, Theme, Renderer> From<&'a str>
for Element<'a, Message, Theme, Renderer>
where
- Theme: DefaultStyle + 'a,
+ Theme: Catalog + 'a,
Renderer: text::Renderer + 'a,
{
fn from(content: &'a str) -> Self {
@@ -314,30 +331,116 @@ where
/// 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<Color>,
}
-/// The style of some [`Text`].
-pub type Style<'a, Theme> = Box<dyn Fn(&Theme) -> Appearance + 'a>;
+/// 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 default style of some [`Text`].
-pub trait DefaultStyle {
- /// Returns the default style of some [`Text`].
- fn default_style(&self) -> Appearance;
+ /// The [`Style`] of a class with the given status.
+ fn style(&self, item: &Self::Class<'_>) -> Style;
}
-impl DefaultStyle for Theme {
- fn default_style(&self) -> Appearance {
- Appearance::default()
+/// A styling function for a [`Text`].
+///
+/// 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(|_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 DefaultStyle for Color {
- fn default_style(&self) -> Appearance {
- Appearance { color: Some(*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/position.rs b/core/src/window/position.rs
index 73391e75..1c8e86b6 100644
--- a/core/src/window/position.rs
+++ b/core/src/window/position.rs
@@ -1,4 +1,4 @@
-use crate::Point;
+use crate::{Point, Size};
/// The position of a window in a given screen.
#[derive(Debug, Clone, Copy, PartialEq)]
@@ -15,6 +15,12 @@ pub enum Position {
/// at (0, 0) you would have to set the position to
/// `(PADDING_X, PADDING_Y)`.
Specific(Point),
+ /// Like [`Specific`], but the window is positioned with the specific coordinates returned by the function.
+ ///
+ /// The function receives the window size and the monitor's resolution as input.
+ ///
+ /// [`Specific`]: Self::Specific
+ SpecificWith(fn(Size, Size) -> Point),
}
impl Default for Position {
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<String>,
}
+
+impl Default for PlatformSpecific {
+ fn default() -> Self {
+ Self {
+ target: Some(String::from("iced")),
+ }
+ }
+}
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 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Redirecting...</title>
+ <meta http-equiv="refresh" content="0; URL='/iced/'" />
+</head>
+<body>
+ <p>If you are not redirected automatically, follow this <a href="/iced/">link</a>.</p>
+</body>
+</html>
diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs
index cf70bd40..29df3eeb 100644
--- a/examples/bezier_tool/src/main.rs
+++ b/examples/bezier_tool/src/main.rs
@@ -1,9 +1,11 @@
//! This example showcases an interactive `Canvas` for drawing Bézier curves.
-use iced::widget::{button, column, text};
-use iced::{Alignment, Element, Length};
+use iced::alignment;
+use iced::widget::{button, container, horizontal_space, hover};
+use iced::{Element, Length, Theme};
pub fn main() -> iced::Result {
iced::program("Bezier Tool - Iced", Example::update, Example::view)
+ .theme(|_| Theme::CatppuccinMocha)
.antialiasing(true)
.run()
}
@@ -35,16 +37,22 @@ impl Example {
}
fn view(&self) -> Element<Message> {
- column![
- text("Bezier tool example").width(Length::Shrink).size(50),
+ container(hover(
self.bezier.view(&self.curves).map(Message::AddCurve),
- button("Clear")
- .style(button::danger)
- .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()
}
}
@@ -139,27 +147,24 @@ mod bezier {
&self,
state: &Self::State,
renderer: &Renderer,
- _theme: &Theme,
+ theme: &Theme,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Vec<Geometry> {
- 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]
}
@@ -187,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);
@@ -195,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),
+ );
}
}
@@ -209,6 +219,7 @@ mod bezier {
fn draw(
&self,
renderer: &Renderer,
+ theme: &Theme,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Geometry {
@@ -218,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 {
@@ -227,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 38949336..bec4a954 100644
--- a/examples/checkbox/src/main.rs
+++ b/examples/checkbox/src/main.rs
@@ -1,5 +1,5 @@
-use iced::widget::{checkbox, column, container, row, text};
-use iced::{Element, Font, Length};
+use iced::widget::{center, checkbox, column, row, text};
+use iced::{Element, Font};
const ICON_FONT: Font = Font::with_name("icons");
@@ -68,11 +68,6 @@ impl 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..bc6c202b 100644
--- a/examples/clock/Cargo.toml
+++ b/examples/clock/Cargo.toml
@@ -8,5 +8,5 @@ publish = false
[dependencies]
iced.workspace = true
iced.features = ["canvas", "tokio", "debug"]
-
-time = { version = "0.3", features = ["local-offset"] }
+chrono = "0.4"
+tracing-subscriber = "0.3"
diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs
index 897f8f1b..7c4685c4 100644
--- a/examples/clock/src/main.rs
+++ b/examples/clock/src/main.rs
@@ -1,5 +1,6 @@
use iced::alignment;
use iced::mouse;
+use iced::time;
use iced::widget::canvas::{stroke, Cache, Geometry, LineCap, Path, Stroke};
use iced::widget::{canvas, container};
use iced::{
@@ -8,6 +9,8 @@ use iced::{
};
pub fn main() -> iced::Result {
+ tracing_subscriber::fmt::init();
+
iced::program("Clock - Iced", Clock::update, Clock::view)
.subscription(Clock::subscription)
.theme(Clock::theme)
@@ -16,13 +19,13 @@ pub fn main() -> iced::Result {
}
struct Clock {
- now: time::OffsetDateTime,
+ now: chrono::DateTime<chrono::Local>,
clock: Cache,
}
#[derive(Debug, Clone, Copy)]
enum Message {
- Tick(time::OffsetDateTime),
+ Tick(chrono::DateTime<chrono::Local>),
}
impl Clock {
@@ -52,16 +55,12 @@ impl Clock {
}
fn subscription(&self) -> Subscription<Message> {
- iced::time::every(std::time::Duration::from_millis(500)).map(|_| {
- Message::Tick(
- time::OffsetDateTime::now_local()
- .unwrap_or_else(|_| time::OffsetDateTime::now_utc()),
- )
- })
+ time::every(time::Duration::from_millis(500))
+ .map(|_| Message::Tick(chrono::offset::Local::now()))
}
fn theme(&self) -> Theme {
- Theme::ALL[(self.now.unix_timestamp() as usize / 10) % Theme::ALL.len()]
+ Theme::ALL[(self.now.timestamp() as usize / 10) % Theme::ALL.len()]
.clone()
}
}
@@ -69,8 +68,7 @@ impl Clock {
impl Default for Clock {
fn default() -> Self {
Self {
- now: time::OffsetDateTime::now_local()
- .unwrap_or_else(|_| time::OffsetDateTime::now_utc()),
+ now: chrono::offset::Local::now(),
clock: Cache::default(),
}
}
@@ -87,6 +85,8 @@ impl<Message> canvas::Program<Message> for Clock {
bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Vec<Geometry> {
+ use chrono::Timelike;
+
let clock = self.clock.draw(renderer, bounds.size(), |frame| {
let palette = theme.extended_palette();
@@ -167,7 +167,7 @@ impl<Message> canvas::Program<Message> for Clock {
}
}
-fn hand_rotation(n: u8, total: u8) -> Degrees {
+fn hand_rotation(n: u32, total: u32) -> Degrees {
let turns = n as f32 / total as f32;
Degrees(360.0 * turns)
diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs
index 29337508..d9325edb 100644
--- a/examples/color_palette/src/main.rs
+++ b/examples/color_palette/src/main.rs
@@ -6,9 +6,7 @@ use iced::{
Color, Element, Font, Length, Pixels, Point, Rectangle, Renderer, Size,
Vector,
};
-use palette::{
- self, convert::FromColor, rgb::Rgb, Darken, Hsl, Lighten, ShiftHue,
-};
+use palette::{convert::FromColor, rgb::Rgb, Darken, Hsl, Lighten, ShiftHue};
use std::marker::PhantomData;
use std::ops::RangeInclusive;
diff --git a/examples/combo_box/src/main.rs b/examples/combo_box/src/main.rs
index 2feb4522..ff759ab4 100644
--- a/examples/combo_box/src/main.rs
+++ b/examples/combo_box/src/main.rs
@@ -1,5 +1,5 @@
use iced::widget::{
- column, combo_box, container, scrollable, text, vertical_space,
+ center, column, combo_box, scrollable, text, vertical_space,
};
use iced::{Alignment, Element, Length};
@@ -68,12 +68,7 @@ impl 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()
}
}
diff --git a/examples/component/src/main.rs b/examples/component/src/main.rs
index 43ba3187..5625f12a 100644
--- a/examples/component/src/main.rs
+++ b/examples/component/src/main.rs
@@ -1,5 +1,5 @@
-use iced::widget::container;
-use iced::{Element, Length};
+use iced::widget::center;
+use iced::Element;
use numeric_input::numeric_input;
@@ -27,10 +27,8 @@ impl Component {
}
fn view(&self) -> Element<Message> {
- container(numeric_input(self.value, Message::NumericInputChanged))
+ center(numeric_input(self.value, Message::NumericInputChanged))
.padding(20)
- .height(Length::Fill)
- .center_y()
.into()
}
}
@@ -73,10 +71,7 @@ mod numeric_input {
impl<Message, Theme> Component<Message, Theme> for NumericInput<Message>
where
- Theme: text::DefaultStyle
- + button::DefaultStyle
- + text_input::DefaultStyle
- + 'static,
+ Theme: text::Catalog + button::Catalog + text_input::Catalog + 'static,
{
type State = ();
type Event = Event;
@@ -151,10 +146,7 @@ mod numeric_input {
impl<'a, Message, Theme> From<NumericInput<Message>>
for Element<'a, Message, Theme>
where
- Theme: text::DefaultStyle
- + button::DefaultStyle
- + text_input::DefaultStyle
- + 'static,
+ Theme: text::Catalog + button::Catalog + text_input::Catalog + 'static,
Message: 'a,
{
fn from(numeric_input: NumericInput<Message>) -> Self {
diff --git a/examples/custom_quad/src/main.rs b/examples/custom_quad/src/main.rs
index d8aac1d0..b53a40d6 100644
--- a/examples/custom_quad/src/main.rs
+++ b/examples/custom_quad/src/main.rs
@@ -81,8 +81,8 @@ mod quad {
}
}
-use iced::widget::{column, container, slider, text};
-use iced::{Alignment, Color, Element, Length, Shadow, Vector};
+use iced::widget::{center, column, slider, text};
+use iced::{Alignment, Color, Element, Shadow, Vector};
pub fn main() -> iced::Result {
iced::run("Custom Quad - Iced", Example::update, Example::view)
@@ -187,12 +187,7 @@ impl Example {
.max_width(500)
.align_items(Alignment::Center);
- container(content)
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y()
- .into()
+ center(content).into()
}
}
diff --git a/examples/custom_shader/src/main.rs b/examples/custom_shader/src/main.rs
index aa3dafe9..463b2df9 100644
--- a/examples/custom_shader/src/main.rs
+++ b/examples/custom_shader/src/main.rs
@@ -4,7 +4,7 @@ use scene::Scene;
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, Color, Element, Length, Subscription};
@@ -123,12 +123,7 @@ impl 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<Message> {
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<u32>,
- _scale_factor: f32,
+ format: wgpu::TextureFormat,
storage: &mut shader::Storage,
+ _bounds: &Rectangle,
+ viewport: &Viewport,
) {
if !storage.has::<Pipeline>() {
- storage.store(Pipeline::new(device, queue, format, target_size));
+ storage.store(Pipeline::new(
+ device,
+ queue,
+ format,
+ viewport.physical_size(),
+ ));
}
let pipeline = storage.get_mut::<Pipeline>().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<u32>,
- viewport: Rectangle<u32>,
- encoder: &mut wgpu::CommandEncoder,
+ clip_bounds: &Rectangle<u32>,
) {
- //at this point our pipeline should always be initialized
+ // At this point our pipeline should always be initialized
let pipeline = storage.get::<Pipeline>().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 0c9e774d..3cf10e22 100644
--- a/examples/custom_widget/src/main.rs
+++ b/examples/custom_widget/src/main.rs
@@ -82,8 +82,8 @@ mod circle {
}
use circle::circle;
-use iced::widget::{column, container, slider, text};
-use iced::{Alignment, Element, Length};
+use iced::widget::{center, column, slider, text};
+use iced::{Alignment, Element};
pub fn main() -> iced::Result {
iced::run("Custom Widget - Iced", Example::update, Example::view)
@@ -122,12 +122,7 @@ impl Example {
.max_width(500)
.align_items(Alignment::Center);
- container(content)
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y()
- .into()
+ center(content).into()
}
}
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<I: 'static + Hash + Copy + Send + Sync, T: ToString>(
})
}
-#[derive(Debug, Hash, Clone)]
-pub struct Download<I> {
- id: I,
- url: String,
-}
-
async fn download<I: Copy>(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 a4136415..7974d5a0 100644
--- a/examples/download_progress/src/main.rs
+++ b/examples/download_progress/src/main.rs
@@ -1,7 +1,7 @@
mod download;
-use iced::widget::{button, column, container, progress_bar, text, Column};
-use iced::{Alignment, Element, Length, Subscription};
+use iced::widget::{button, center, column, progress_bar, text, Column};
+use iced::{Alignment, Element, Subscription};
pub fn main() -> iced::Result {
iced::program("Download Progress - Iced", Example::update, Example::view)
@@ -67,13 +67,7 @@ impl 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()
}
}
diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs
index ed16018a..c20a7d67 100644
--- a/examples/editor/src/main.rs
+++ b/examples/editor/src/main.rs
@@ -277,7 +277,7 @@ fn action<'a, Message: Clone + 'a>(
label: &'a str,
on_press: Option<Message>,
) -> Element<'a, Message> {
- let action = button(container(content).width(30).center_x());
+ let action = button(container(content).center_x(30));
if let Some(on_press) = on_press {
tooltip(
diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs
index 4734e20c..9d1c502a 100644
--- a/examples/events/src/main.rs
+++ b/examples/events/src/main.rs
@@ -1,6 +1,6 @@
use iced::alignment;
use iced::event::{self, Event};
-use iced::widget::{button, checkbox, container, text, Column};
+use iced::widget::{button, center, checkbox, text, Column};
use iced::window;
use iced::{Alignment, Command, Element, Length, Subscription};
@@ -84,11 +84,6 @@ impl 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 7bed272d..2de97e20 100644
--- a/examples/exit/src/main.rs
+++ b/examples/exit/src/main.rs
@@ -1,6 +1,6 @@
-use iced::widget::{button, column, container};
+use iced::widget::{button, center, column};
use iced::window;
-use iced::{Alignment, Command, Element, Length};
+use iced::{Alignment, Command, Element};
pub fn main() -> iced::Result {
iced::program("Exit - Iced", Exit::update, Exit::view).run()
@@ -46,12 +46,6 @@ impl 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 <hector0193@gmail.com>"]
+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<Message> {
+ if self.spin {
+ window::frames().map(Message::RedrawRequested)
+ } else {
+ Subscription::none()
+ }
+ }
+
+ fn view(&self) -> Element<Message> {
+ 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<Element<'a, Message>>,
+ 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 48574247..8f1f7a54 100644
--- a/examples/game_of_life/src/main.rs
+++ b/examples/game_of_life/src/main.rs
@@ -602,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);
@@ -641,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 63efcbdd..3c7969c5 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(
@@ -170,12 +176,7 @@ fn view(_state: &()) -> Element<'_, ()> {
.spacing(20)
.max_width(500);
- let scrollable =
- scrollable(container(content).width(Length::Fill).center_x());
+ let scrollable = scrollable(container(content).center_x(Length::Fill));
- container(scrollable)
- .width(Length::Fill)
- .height(Length::Fill)
- .center_y()
- .into()
+ container(scrollable).center_y(Length::Fill).into()
}
diff --git a/examples/gradient/src/main.rs b/examples/gradient/src/main.rs
index 22c21cdd..2b906c32 100644
--- a/examples/gradient/src/main.rs
+++ b/examples/gradient/src/main.rs
@@ -60,7 +60,7 @@ impl Gradient {
} = *self;
let gradient_box = container(horizontal_space())
- .style(move |_theme, _status| {
+ .style(move |_theme| {
let gradient = gradient::Linear::new(angle)
.add_stop(0.0, start)
.add_stop(1.0, end);
diff --git a/examples/integration/Cargo.toml b/examples/integration/Cargo.toml
index a4a961f8..7f8feb3f 100644
--- a/examples/integration/Cargo.toml
+++ b/examples/integration/Cargo.toml
@@ -8,7 +8,9 @@ publish = false
[dependencies]
iced_winit.workspace = true
iced_wgpu.workspace = true
+
iced_widget.workspace = true
+iced_widget.features = ["wgpu"]
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tracing-subscriber = "0.3"
diff --git a/examples/integration/README.md b/examples/integration/README.md
index aa3a6e94..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 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 @@
-<!DOCTYPE html>
-<html>
- <head>
- <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
- <title>Iced - wgpu + WebGL integration</title>
- </head>
- <body>
- <h1>integration_wgpu</h1>
- <canvas id="iced_canvas"></canvas>
- <script type="module">
- import init from "./integration.js";
- init('./integration_bg.wasm');
- </script>
- <style>
- body {
- width: 100%;
- text-align: center;
- }
- </style>
- </body>
-</html>
diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs
index 9cd801b2..e1c7d62f 100644
--- a/examples/integration/src/main.rs
+++ b/examples/integration/src/main.rs
@@ -5,7 +5,7 @@ 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;
@@ -18,309 +18,342 @@ 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<dyn std::error::Error>> {
- #[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::<HtmlCanvasElement>().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)?;
-
- #[cfg(not(target_arch = "wasm32"))]
- let window = winit::window::Window::new(&event_loop)?;
-
- let window = Arc::new(window);
-
- let physical_size = window.inner_size();
- let mut viewport = Viewport::with_physical_size(
- Size::new(physical_size.width, physical_size.height),
- window.scale_factor(),
- );
- let mut cursor_position = None;
- let mut modifiers = ModifiersState::default();
- let mut clipboard = Clipboard::connect(&window);
-
- // Initialize wgpu
- #[cfg(target_arch = "wasm32")]
- let default_backend = wgpu::Backends::GL;
- #[cfg(not(target_arch = "wasm32"))]
- let default_backend = wgpu::Backends::PRIMARY;
-
- let backend =
- wgpu::util::backend_bits_from_env().unwrap_or(default_backend);
-
- let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
- backends: backend,
- ..Default::default()
- });
- let surface = instance.create_surface(window.clone())?;
-
- 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 adapter_features = adapter.features();
-
- #[cfg(target_arch = "wasm32")]
- let needed_limits = wgpu::Limits::downlevel_webgl2_defaults()
- .using_resolution(adapter.limits());
-
- #[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,
- },
- 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,
- device,
- queue,
- )
- });
-
- 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,
+ #[allow(clippy::large_enum_variant)]
+ enum Runner {
+ Loading,
+ Ready {
+ window: Arc<winit::window::Window>,
+ device: wgpu::Device,
+ queue: wgpu::Queue,
+ surface: wgpu::Surface<'static>,
+ format: wgpu::TextureFormat,
+ engine: Engine,
+ renderer: Renderer,
+ scene: Scene,
+ state: program::State<Controls>,
+ cursor_position: Option<winit::dpi::PhysicalPosition<f64>>,
+ clipboard: Clipboard,
+ viewport: Viewport,
+ modifiers: ModifiersState,
+ resized: bool,
+ debug: Debug,
},
- );
-
- let mut resized = false;
-
- // Initialize scene and GUI controls
- let scene = Scene::new(&device, format);
- let controls = Controls::new();
-
- // Initialize iced
- let mut debug = Debug::new();
- 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,
- &mut debug,
- );
-
- // Run event loop
- event_loop.run(move |event, window_target| {
- // You should change this if you want to render continuously
- 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,
- },
- );
+ }
+
+ 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 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 backend =
+ wgpu::util::backend_bits_from_env().unwrap_or_default();
+
+ let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
+ backends: backend,
+ ..Default::default()
+ });
+ let surface = instance
+ .create_surface(window.clone())
+ .expect("Create window surface");
+
+ 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");
- resized = false;
- }
+ let adapter_features = adapter.features();
- match surface.get_current_texture() {
- Ok(frame) => {
- let mut encoder = device.create_command_encoder(
- &wgpu::CommandEncoderDescriptor { label: None },
- );
+ let capabilities = surface.get_capabilities(&adapter);
- let program = state.program();
+ 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");
+
+ (
+ capabilities
+ .formats
+ .iter()
+ .copied()
+ .find(wgpu::TextureFormat::is_srgb)
+ .or_else(|| {
+ capabilities.formats.first().copied()
+ })
+ .expect("Get preferred format"),
+ adapter,
+ device,
+ queue,
+ )
+ });
+
+ 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,
+ },
+ );
+
+ // Initialize scene and GUI controls
+ let scene = Scene::new(&device, format);
+ let controls = Controls::new();
+
+ // Initialize iced
+ let mut debug = Debug::new();
+ 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,
+ &mut debug,
+ );
+
+ // 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,
+ debug,
+ };
+ }
+ }
- let view = frame.texture.create_view(
- &wgpu::TextureViewDescriptor::default(),
+ 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,
+ debug,
+ } = self
+ else {
+ return;
+ };
+
+ match event {
+ WindowEvent::RedrawRequested => {
+ if *resized {
+ let size = window.inner_size();
+
+ *viewport = Viewport::with_physical_size(
+ Size::new(size.width, size.height),
+ window.scale_factor(),
);
- {
- // We clear the frame
- let mut render_pass = Scene::clear(
- &view,
- &mut encoder,
- program.background_color(),
+ 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,
+ },
+ );
+
+ *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();
+
+ let view = frame.texture.create_view(
+ &wgpu::TextureViewDescriptor::default(),
+ );
- // And then iced on top
- renderer.with_primitives(|backend, primitive| {
- backend.present(
- &device,
- &queue,
+ {
+ // 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,
&debug.overlay(),
);
- });
- // 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}. \
- Rendering cannot continue."
- )
- }
- _ => {
- // Try rendering again next frame.
- window.request_redraw();
+ // Update the mouse cursor
+ window.set_cursor(
+ iced_winit::conversion::mouse_interaction(
+ state.mouse_interaction(),
+ ),
+ );
}
- },
- }
- }
- 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();
+ Err(error) => match error {
+ wgpu::SurfaceError::OutOfMemory => {
+ panic!(
+ "Swapchain error: {error}. \
+ Rendering cannot continue."
+ )
+ }
+ _ => {
+ // Try rendering again next frame.
+ window.request_redraw();
+ }
+ },
}
- _ => {}
}
-
- // 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);
+ WindowEvent::CursorMoved { position, .. } => {
+ *cursor_position = Some(position);
+ }
+ WindowEvent::ModifiersChanged(new_modifiers) => {
+ *modifiers = new_modifiers.state();
}
+ WindowEvent::Resized(_) => {
+ *resized = true;
+ }
+ WindowEvent::CloseRequested => {
+ event_loop.exit();
+ }
+ _ => {}
}
- _ => {}
- }
- // If there are events pending
- if !state.is_queue_empty() {
- // We update iced
- let _ = state.update(
- viewport.logical_size(),
- cursor_position
- .map(|p| {
- conversion::cursor_position(p, viewport.scale_factor())
- })
- .map(mouse::Cursor::Available)
- .unwrap_or(mouse::Cursor::Unavailable),
- &mut renderer,
- &Theme::Dark,
- &renderer::Style {
- text_color: Color::WHITE,
- },
- &mut clipboard,
- &mut debug,
- );
-
- // and request a redraw
- window.request_redraw();
+ // 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),
+ renderer,
+ &Theme::Dark,
+ &renderer::Style {
+ text_color: Color::WHITE,
+ },
+ clipboard,
+ debug,
+ );
+
+ // 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 713e2b70..c40ac820 100644
--- a/examples/layout/src/main.rs
+++ b/examples/layout/src/main.rs
@@ -1,8 +1,8 @@
use iced::keyboard;
use iced::mouse;
use iced::widget::{
- button, canvas, checkbox, column, container, horizontal_space, pick_list,
- row, scrollable, text,
+ button, canvas, center, checkbox, column, container, horizontal_space,
+ pick_list, row, scrollable, text,
};
use iced::{
color, Alignment, Element, Font, Length, Point, Rectangle, Renderer,
@@ -76,22 +76,18 @@ impl 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, _status| {
+ .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(
@@ -195,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> {
@@ -245,10 +236,10 @@ fn application<'a>() -> Element<'a, Message> {
.padding(10)
.align_items(Alignment::Center),
)
- .style(|theme, _status| {
+ .style(|theme| {
let palette = theme.extended_palette();
- container::Appearance::default()
+ container::Style::default()
.with_border(palette.background.strong.color, 1)
});
@@ -260,8 +251,7 @@ fn application<'a>() -> Element<'a, Message> {
.align_items(Alignment::Center),
)
.style(container::rounded_box)
- .height(Length::Fill)
- .center_y();
+ .center_y(Length::Fill);
let content = container(
scrollable(
diff --git a/examples/lazy/src/main.rs b/examples/lazy/src/main.rs
index 627aba23..f24c0d62 100644
--- a/examples/lazy/src/main.rs
+++ b/examples/lazy/src/main.rs
@@ -173,7 +173,7 @@ impl App {
.style(button::danger);
row![
- text(&item.name).color(item.color),
+ 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 2b2abad5..a63c51d4 100644
--- a/examples/loading_spinners/src/main.rs
+++ b/examples/loading_spinners/src/main.rs
@@ -1,5 +1,5 @@
-use iced::widget::{column, container, row, slider, text};
-use iced::{Element, Length};
+use iced::widget::{center, column, row, slider, text};
+use iced::Element;
use std::time::Duration;
@@ -73,7 +73,7 @@ impl LoadingSpinners {
})
.spacing(20);
- container(
+ center(
column.push(
row![
text("Cycle duration:"),
@@ -87,10 +87,6 @@ impl LoadingSpinners {
.spacing(20.0),
),
)
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y()
.into()
}
}
diff --git a/examples/loupe/src/main.rs b/examples/loupe/src/main.rs
index 6a5ff123..c4d3b449 100644
--- a/examples/loupe/src/main.rs
+++ b/examples/loupe/src/main.rs
@@ -1,5 +1,5 @@
-use iced::widget::{button, column, container, text};
-use iced::{Alignment, Element, Length};
+use iced::widget::{button, center, column, text};
+use iced::{Alignment, Element};
use loupe::loupe;
@@ -31,7 +31,7 @@ impl Loupe {
}
fn view(&self) -> Element<Message> {
- container(loupe(
+ center(loupe(
3.0,
column![
button("Increment").on_press(Message::Increment),
@@ -41,10 +41,6 @@ impl Loupe {
.padding(20)
.align_items(Alignment::Center),
))
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y()
.into()
}
}
@@ -159,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 398728e0..a012c310 100644
--- a/examples/modal/src/main.rs
+++ b/examples/modal/src/main.rs
@@ -2,12 +2,11 @@ use iced::event::{self, Event};
use iced::keyboard;
use iced::keyboard::key;
use iced::widget::{
- self, button, column, container, horizontal_space, pick_list, row, text,
- text_input,
+ self, button, center, column, container, horizontal_space, mouse_area,
+ opaque, pick_list, row, stack, text, text_input,
};
-use iced::{Alignment, Command, Element, Length, Subscription};
+use iced::{Alignment, Color, Command, Element, Length, Subscription};
-use modal::Modal;
use std::fmt;
pub fn main() -> iced::Result {
@@ -99,13 +98,7 @@ impl 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(),
@@ -116,12 +109,10 @@ impl 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![
@@ -162,9 +153,7 @@ impl App {
.padding(10)
.style(container::rounded_box);
- Modal::new(content, modal)
- .on_blur(Message::HideModal)
- .into()
+ modal(content, signup, Message::HideModal)
} else {
content.into()
}
@@ -203,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<Message>,
- }
-
- impl<'a, Message, Theme, Renderer> Modal<'a, Message, Theme, Renderer> {
- /// Returns a new [`Modal`]
- pub fn new(
- base: impl Into<Element<'a, Message, Theme, Renderer>>,
- modal: impl Into<Element<'a, Message, Theme, Renderer>>,
- ) -> 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<Message, Theme, Renderer>
- for Modal<'a, Message, Theme, Renderer>
- where
- Renderer: advanced::Renderer,
- Message: Clone,
- {
- fn children(&self) -> Vec<widget::Tree> {
- 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<Length> {
- 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<overlay::Element<'b, Message, Theme, Renderer>> {
- 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<Message>,
- ) {
- 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<Message>,
- }
-
- impl<'a, 'b, Message, Theme, Renderer>
- overlay::Overlay<Message, Theme, Renderer>
- 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<Element<'a, Message>>,
+ content: impl Into<Element<'a, Message>>,
+ 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<Message>,
- ) {
- 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<overlay::Element<'c, Message, Theme, Renderer>> {
- self.content.as_widget_mut().overlay(
- self.tree,
- layout.children().next().unwrap(),
- renderer,
- Vector::ZERO,
- )
- }
- }
-
- impl<'a, Message, Theme, Renderer> From<Modal<'a, Message, Theme, Renderer>>
- 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..31c2e4f6 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<Message> {
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(200).into()
}
}
diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs
index 9e78ad0b..6b5bd332 100644
--- a/examples/pane_grid/src/main.rs
+++ b/examples/pane_grid/src/main.rs
@@ -290,10 +290,8 @@ fn view_content<'a>(
.align_items(Alignment::Center);
container(scrollable(content))
- .width(Length::Fill)
- .height(Length::Fill)
+ .center_y(Length::Fill)
.padding(5)
- .center_y()
.into()
}
@@ -336,39 +334,30 @@ mod style {
use iced::widget::container;
use iced::{Border, Theme};
- pub fn title_bar_active(
- theme: &Theme,
- _status: container::Status,
- ) -> 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,
- _status: container::Status,
- ) -> 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,
- _status: container::Status,
- ) -> 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,
@@ -379,13 +368,10 @@ mod style {
}
}
- pub fn pane_focused(
- theme: &Theme,
- _status: container::Status,
- ) -> 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/pokedex/src/main.rs b/examples/pokedex/src/main.rs
index 6ba6fe66..cffa3727 100644
--- a/examples/pokedex/src/main.rs
+++ b/examples/pokedex/src/main.rs
@@ -1,5 +1,5 @@
use iced::futures;
-use iced::widget::{self, column, container, image, row, text};
+use iced::widget::{self, center, column, image, row, text};
use iced::{Alignment, Command, Element, Length};
pub fn main() -> iced::Result {
@@ -83,12 +83,7 @@ impl Pokedex {
.align_items(Alignment::End),
};
- container(content)
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y()
- .into()
+ center(content).into()
}
}
@@ -186,7 +181,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/qr_code/src/main.rs b/examples/qr_code/src/main.rs
index b93adf04..c6a90458 100644
--- a/examples/qr_code/src/main.rs
+++ b/examples/qr_code/src/main.rs
@@ -1,7 +1,5 @@
-use iced::widget::{
- column, container, pick_list, qr_code, row, text, text_input,
-};
-use iced::{Alignment, Element, Length, Theme};
+use iced::widget::{center, column, pick_list, qr_code, row, text, text_input};
+use iced::{Alignment, Element, Theme};
pub fn main() -> iced::Result {
iced::program(
@@ -72,13 +70,7 @@ impl 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 c73d8dfd..fb19e556 100644
--- a/examples/screenshot/src/main.rs
+++ b/examples/screenshot/src/main.rs
@@ -109,7 +109,7 @@ impl Example {
fn view(&self) -> Element<'_, Message> {
let image: Element<Message> = if let Some(screenshot) = &self.screenshot
{
- image(image::Handle::from_pixels(
+ image(image::Handle::from_rgba(
screenshot.size.width,
screenshot.size.height,
screenshot.clone(),
@@ -123,12 +123,9 @@ impl Example {
};
let image = container(image)
+ .center_y(Length::FillPortion(2))
.padding(10)
- .style(container::rounded_box)
- .width(Length::FillPortion(2))
- .height(Length::Fill)
- .center_x()
- .center_y();
+ .style(container::rounded_box);
let crop_origin_controls = row![
text("X:")
@@ -213,12 +210,7 @@ impl 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(Length::Fill);
let content = row![side_content, image]
.spacing(10)
@@ -226,13 +218,7 @@ impl 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<Message> {
diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs
index 240ae908..bbb6497f 100644
--- a/examples/scrollable/src/main.rs
+++ b/examples/scrollable/src/main.rs
@@ -327,7 +327,7 @@ impl ScrollableDemo {
.spacing(10)
.into();
- container(content).padding(20).center_x().center_y().into()
+ container(content).padding(20).into()
}
fn theme(&self) -> Theme {
@@ -341,8 +341,8 @@ impl Default for ScrollableDemo {
}
}
-fn progress_bar_custom_style(theme: &Theme) -> progress_bar::Appearance {
- progress_bar::Appearance {
+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 b805e7d5..7dd7be5e 100644
--- a/examples/sierpinski_triangle/src/main.rs
+++ b/examples/sierpinski_triangle/src/main.rs
@@ -1,6 +1,6 @@
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::{Color, Length, Point, Rectangle, Renderer, Size, Theme};
@@ -111,7 +111,7 @@ impl canvas::Program<Message> for SierpinskiGraph {
_theme: &Theme,
bounds: Rectangle,
_cursor: mouse::Cursor,
- ) -> Vec<canvas::Geometry> {
+ ) -> Vec<Geometry> {
let geom = self.cache.draw(renderer, bounds.size(), |frame| {
frame.stroke(
&canvas::Path::rectangle(Point::ORIGIN, frame.size()),
diff --git a/examples/slider/src/main.rs b/examples/slider/src/main.rs
index b3a47614..0b4c29aa 100644
--- a/examples/slider/src/main.rs
+++ b/examples/slider/src/main.rs
@@ -1,4 +1,4 @@
-use iced::widget::{column, container, slider, text, vertical_slider};
+use iced::widget::{center, column, container, slider, text, vertical_slider};
use iced::{Element, Length};
pub fn main() -> iced::Result {
@@ -54,18 +54,14 @@ impl 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(Length::Fill),
+ container(h_slider).center_x(Length::Fill),
+ container(text).center_x(Length::Fill)
]
.spacing(25),
)
- .height(Length::Fill)
- .width(Length::Fill)
- .center_x()
- .center_y()
.into()
}
}
diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs
index b5228f09..deb211d8 100644
--- a/examples/solar_system/src/main.rs
+++ b/examples/solar_system/src/main.rs
@@ -10,7 +10,7 @@ use iced::mouse;
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::{
Color, Element, Length, Point, Rectangle, Renderer, Size, Subscription,
@@ -130,7 +130,7 @@ impl<Message> canvas::Program<Message> for State {
_theme: &Theme,
bounds: Rectangle,
_cursor: mouse::Cursor,
- ) -> Vec<canvas::Geometry> {
+ ) -> Vec<Geometry> {
use std::f32::consts::PI;
let background =
diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs
index 6bd5ce3e..a8149753 100644
--- a/examples/stopwatch/src/main.rs
+++ b/examples/stopwatch/src/main.rs
@@ -1,8 +1,8 @@
use iced::alignment;
use iced::keyboard;
use iced::time;
-use iced::widget::{button, column, container, row, text};
-use iced::{Alignment, Element, Length, Subscription, Theme};
+use iced::widget::{button, center, column, row, text};
+use iced::{Alignment, Element, Subscription, Theme};
use std::time::{Duration, Instant};
@@ -128,12 +128,7 @@ impl 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 73268da0..57e8f47e 100644
--- a/examples/styling/src/main.rs
+++ b/examples/styling/src/main.rs
@@ -1,7 +1,7 @@
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, Theme};
@@ -106,12 +106,7 @@ impl 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 cc686dca..e071c3af 100644
--- a/examples/svg/src/main.rs
+++ b/examples/svg/src/main.rs
@@ -1,4 +1,4 @@
-use iced::widget::{checkbox, column, container, svg};
+use iced::widget::{center, checkbox, column, container, svg};
use iced::{color, Element, Length};
pub fn main() -> iced::Result {
@@ -31,7 +31,7 @@ impl Tiger {
));
let svg = svg(handle).width(Length::Fill).height(Length::Fill).style(
- |_theme, _status| svg::Appearance {
+ |_theme, _status| svg::Style {
color: if self.apply_color_filter {
Some(color!(0x0000ff))
} else {
@@ -44,19 +44,12 @@ impl 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(Length::Fill)]
+ .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 cae764dc..8ce12e1c 100644
--- a/examples/system_information/src/main.rs
+++ b/examples/system_information/src/main.rs
@@ -1,5 +1,5 @@
-use iced::widget::{button, column, container, text};
-use iced::{system, Command, Element, Length};
+use iced::widget::{button, center, column, text};
+use iced::{system, Command, Element};
pub fn main() -> iced::Result {
iced::program("System Information - Iced", Example::update, Example::view)
@@ -132,11 +132,6 @@ impl Example {
}
};
- container(content)
- .center_x()
- .center_y()
- .width(Length::Fill)
- .height(Length::Fill)
- .into()
+ center(content).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 <hector0193@gmail.com>"]
+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<Message> {
+ canvas(self as &Self)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .into()
+ }
+
+ fn subscription(&self) -> Subscription<Message> {
+ time::every(std::time::Duration::from_millis(50)).map(Message::Tick)
+ }
+}
+
+impl<Message> canvas::Program<Message> for TheMatrix {
+ type State = RefCell<Vec<canvas::Cache>>;
+
+ fn draw(
+ &self,
+ state: &Self::State,
+ renderer: &Renderer,
+ _theme: &Theme,
+ bounds: Rectangle,
+ _cursor: mouse::Cursor,
+ ) -> Vec<canvas::Geometry> {
+ 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 4916ceb6..355c40b8 100644
--- a/examples/toast/src/main.rs
+++ b/examples/toast/src/main.rs
@@ -2,7 +2,7 @@ use iced::event::{self, Event};
use iced::keyboard;
use iced::keyboard::key;
use iced::widget::{
- self, button, column, container, pick_list, row, slider, text, text_input,
+ self, button, center, column, pick_list, row, slider, text, text_input,
};
use iced::{Alignment, Command, Element, Length, Subscription};
@@ -102,7 +102,7 @@ impl App {
.then_some(Message::Add),
);
- let content = container(
+ let content = center(
column![
subtitle(
"Title",
@@ -146,11 +146,7 @@ impl 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)
@@ -651,45 +647,33 @@ mod toast {
}
}
- fn styled(pair: theme::palette::Pair) -> container::Appearance {
- container::Appearance {
+ 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,
- _status: container::Status,
- ) -> container::Appearance {
+ fn primary(theme: &Theme) -> container::Style {
let palette = theme.extended_palette();
styled(palette.primary.weak)
}
- fn secondary(
- theme: &Theme,
- _status: container::Status,
- ) -> container::Appearance {
+ fn secondary(theme: &Theme) -> container::Style {
let palette = theme.extended_palette();
styled(palette.secondary.weak)
}
- fn success(
- theme: &Theme,
- _status: container::Status,
- ) -> container::Appearance {
+ fn success(theme: &Theme) -> container::Style {
let palette = theme.extended_palette();
styled(palette.success.weak)
}
- fn danger(
- theme: &Theme,
- _status: container::Status,
- ) -> container::Appearance {
+ 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 f5fb94c9..dd1e5213 100644
--- a/examples/todos/src/main.rs
+++ b/examples/todos/src/main.rs
@@ -1,8 +1,8 @@
use iced::alignment::{self, Alignment};
use iced::keyboard;
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::{Command, Element, Font, Length, Subscription};
@@ -238,7 +238,10 @@ impl Todos {
.spacing(20)
.max_width(800);
- scrollable(container(content).padding(40).center_x()).into()
+ scrollable(
+ container(content).center_x(Length::Fill).padding(40),
+ )
+ .into()
}
}
}
@@ -435,19 +438,16 @@ 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)
@@ -455,7 +455,6 @@ fn empty_message(message: &str) -> Element<'_, Message> {
.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 b6603068..f48f688a 100644
--- a/examples/tooltip/src/main.rs
+++ b/examples/tooltip/src/main.rs
@@ -1,6 +1,6 @@
use iced::widget::tooltip::Position;
-use iced::widget::{button, container, tooltip};
-use iced::{Element, Length};
+use iced::widget::{button, center, container, tooltip};
+use iced::Element;
pub fn main() -> iced::Result {
iced::run("Tooltip - Iced", Tooltip::update, Tooltip::view)
@@ -43,12 +43,7 @@ impl Tooltip {
.gap(10)
.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 3fb8b460..f624053c 100644
--- a/examples/tour/src/main.rs
+++ b/examples/tour/src/main.rs
@@ -76,11 +76,10 @@ impl Tour {
} else {
content
})
- .width(Length::Fill)
- .center_x(),
+ .center_x(Length::Fill),
);
- container(scrollable).height(Length::Fill).center_y().into()
+ container(scrollable).center_y(Length::Fill).into()
}
}
@@ -357,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)
}
@@ -589,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)
@@ -670,11 +669,10 @@ fn ferris<'a>(
.filter_method(filter_method)
.width(width),
)
- .width(Length::Fill)
- .center_x()
+ .center_x(Length::Fill)
}
-fn padded_button<'a, Message: Clone>(label: &str) -> Button<'a, Message> {
+fn padded_button<Message: Clone>(label: &str) -> Button<'_, Message> {
button(text(label)).padding([12, 24])
}
diff --git a/examples/url_handler/src/main.rs b/examples/url_handler/src/main.rs
index df705b6c..800a188b 100644
--- a/examples/url_handler/src/main.rs
+++ b/examples/url_handler/src/main.rs
@@ -1,6 +1,6 @@
use iced::event::{self, Event};
-use iced::widget::{container, text};
-use iced::{Element, Length, Subscription};
+use iced::widget::{center, text};
+use iced::{Element, Subscription};
pub fn main() -> iced::Result {
iced::program("URL Handler - Iced", App::update, App::view)
@@ -44,11 +44,6 @@ impl 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/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 460d9a08..ba1e1029 100644
--- a/examples/websocket/src/main.rs
+++ b/examples/websocket/src/main.rs
@@ -2,7 +2,7 @@ mod echo;
use iced::alignment::{self, Alignment};
use iced::widget::{
- button, column, container, row, scrollable, text, text_input,
+ self, button, center, column, row, scrollable, text, text_input,
};
use iced::{color, Command, Element, Length, Subscription};
use once_cell::sync::Lazy;
@@ -31,7 +31,10 @@ enum Message {
impl WebSocket {
fn load() -> Command<Message> {
- Command::perform(echo::server::run(), |_| Message::Server)
+ Command::batch([
+ Command::perform(echo::server::run(), |_| Message::Server),
+ widget::focus_next(),
+ ])
}
fn update(&mut self, message: Message) -> Command<Message> {
@@ -85,21 +88,15 @@ impl WebSocket {
fn view(&self) -> Element<Message> {
let message_log: Element<_> = if self.messages.is_empty() {
- container(
+ center(
text("Your messages will appear here...")
.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 ff7ea0f6..3228dd18 100644
--- a/futures/src/backend/wasm/wasm_bindgen.rs
+++ b/futures/src/backend/wasm/wasm_bindgen.rs
@@ -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<u64, Execution>,
+ subscriptions: FxHashMap<u64, Execution>,
}
#[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<u32>;
-}
-
-/// 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<u32>;
-}
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<T> {
+ group: Group,
+ state: RefCell<State<T>>,
+}
+
+impl<T> Cache<T> {
+ /// 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<State<T>> {
+ &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<T> fmt::Debug for Cache<T>
+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<T> Default for Cache<T> {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+/// The state of a [`Cache`].
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum State<T> {
+ /// The [`Cache`] is empty.
+ Empty {
+ /// The previous value of the [`Cache`].
+ previous: Option<T>,
+ },
+ /// 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>) -> 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>,
+ ) -> Self::Cache {
+ }
+}
diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs
index 91951a8e..47521eb0 100644
--- a/graphics/src/compositor.rs
+++ b/graphics/src/compositor.rs
@@ -1,29 +1,39 @@
//! 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 std::future::Future;
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<W: Window + Clone>(
- settings: Self::Settings,
+ settings: Settings,
compatible_window: W,
+ ) -> impl Future<Output = Result<Self, Error>> {
+ 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<W: Window + Clone>(
+ _settings: Settings,
+ _compatible_window: W,
+ _backend: Option<&str>,
) -> impl Future<Output = Result<Self, Error>>;
/// Creates a [`Self::Renderer`] for the [`Compositor`].
@@ -52,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
@@ -93,6 +111,12 @@ impl<T> Window for T where
{
}
+/// Defines the default compositor of a renderer.
+pub trait Default {
+ /// The compositor of the renderer.
+ type Compositor: Compositor<Renderer = Self>;
+}
+
/// Result of an unsuccessful call to [`Compositor::present`].
#[derive(Clone, PartialEq, Eq, Debug, Error)]
pub enum SurfaceError {
@@ -122,3 +146,71 @@ pub struct Information {
/// Contains the graphics backend.
pub backend: String,
}
+
+#[cfg(debug_assertions)]
+impl Compositor for () {
+ type Renderer = ();
+ type Surface = ();
+
+ async fn with_backend<W: Window + Clone>(
+ _settings: Settings,
+ _compatible_window: W,
+ _preffered_backend: Option<&str>,
+ ) -> Result<Self, Error> {
+ Ok(())
+ }
+
+ fn create_renderer(&self) -> Self::Renderer {}
+
+ fn create_surface<W: Window + Clone>(
+ &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<T: AsRef<str>>(
+ &mut self,
+ _renderer: &mut Self::Renderer,
+ _surface: &mut Self::Surface,
+ _viewport: &Viewport,
+ _background_color: Color,
+ _overlay: &[T],
+ ) -> Result<(), SurfaceError> {
+ Ok(())
+ }
+
+ fn screenshot<T: AsRef<str>>(
+ &mut self,
+ _renderer: &mut Self::Renderer,
+ _surface: &mut Self::Surface,
+ _viewport: &Viewport,
+ _background_color: Color,
+ _overlay: &[T],
+ ) -> Vec<u8> {
+ 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;
-
-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<T: Damage> Damage for Primitive<T> {
- 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<T: Damage>(a: &Primitive<T>, b: &Primitive<T>) -> Vec<Rectangle> {
- 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<T: Damage>(
- previous: &[Primitive<T>],
- current: &[Primitive<T>],
+//! Compute the damage between frames.
+use crate::core::{Point, Rectangle};
+
+/// Diffs the damage regions given some previous and current primitives.
+pub fn diff<T>(
+ previous: &[T],
+ current: &[T],
+ bounds: impl Fn(&T) -> Vec<Rectangle>,
+ diff: impl Fn(&T, &T) -> Vec<Rectangle>,
) -> Vec<Rectangle> {
- 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<T: Damage>(
// 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<T>(
+ previous: &[T],
+ current: &[T],
+ bounds: impl Fn(&T) -> Vec<Rectangle>,
+ are_equal: impl Fn(&T, &T) -> bool,
+) -> Vec<Rectangle> {
+ 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<Rectangle>,
- scale_factor: f32,
- bounds: Size<u32>,
-) -> Vec<Rectangle> {
+pub fn group(mut damage: Vec<Rectangle>, bounds: Rectangle) -> Vec<Rectangle> {
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 c6ea98a3..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 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<Self>),
+}
+
+/// 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};
+use crate::cache::Cached;
+use crate::core::{self, Size};
+
/// A renderer capable of drawing some [`Self::Geometry`].
-pub trait Renderer: crate::core::Renderer {
+pub trait Renderer: core::Renderer {
/// The kind of geometry this renderer can draw.
- type Geometry;
+ type Geometry: Cached;
+
+ /// The kind of [`Frame`] this renderer supports.
+ type Frame: frame::Backend<Geometry = Self::Geometry>;
+
+ /// 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 {}
- /// Draws the given layers of [`Self::Geometry`].
- fn draw(&mut self, layers: Vec<Self::Geometry>);
+ 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<Renderer>
+where
+ Renderer: geometry::Renderer,
+{
+ raw: crate::Cache<Data<<Renderer::Geometry as Cached>::Cache>>,
+}
+
+#[derive(Debug, Clone)]
+struct Data<T> {
+ bounds: Size,
+ geometry: T,
+}
+
+impl<Renderer> Cache<Renderer>
+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>),
+ ) -> 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(&current.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<Renderer> std::fmt::Debug for Cache<Renderer>
+where
+ Renderer: geometry::Renderer,
+ <Renderer::Geometry as Cached>::Cache: std::fmt::Debug,
+{
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{:?}", &self.raw)
+ }
+}
+
+impl<Renderer> Default for Cache<Renderer>
+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<Renderer>
+where
+ Renderer: geometry::Renderer,
+{
+ raw: Renderer::Frame,
+}
+
+impl<Renderer> Frame<Renderer>
+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<Fill>) {
+ 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<Fill>,
+ ) {
+ 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<Stroke<'a>>) {
+ 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<Text>) {
+ 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<R>(&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<R>(
+ &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<Radians>) {
+ self.raw.rotate(angle);
+ }
+
+ /// Applies a uniform scaling to the current transform of the [`Frame`].
+ pub fn scale(&mut self, scale: impl Into<f32>) {
+ 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<Vector>) {
+ 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<Radians>);
+ fn scale(&mut self, scale: impl Into<f32>);
+ fn scale_nonuniform(&mut self, scale: impl Into<Vector>);
+
+ 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<Stroke<'a>>);
+
+ fn fill(&mut self, path: &Path, fill: impl Into<Fill>);
+ fn fill_text(&mut self, text: impl Into<Text>);
+ fn fill_rectangle(
+ &mut self,
+ top_left: Point,
+ size: Size,
+ fill: impl Into<Fill>,
+ );
+
+ 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<Radians>) {}
+ fn scale(&mut self, _scale: impl Into<f32>) {}
+ fn scale_nonuniform(&mut self, _scale: impl Into<Vector>) {}
+
+ 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<Stroke<'a>>) {}
+
+ fn fill(&mut self, _path: &Path, _fill: impl Into<Fill>) {}
+ fn fill_text(&mut self, _text: impl Into<Text>) {}
+ fn fill_rectangle(
+ &mut self,
+ _top_left: Point,
+ _size: Size,
+ _fill: impl Into<Fill>,
+ ) {
+ }
+
+ 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};
+#[cfg(feature = "image")]
+pub use ::image as image_rs;
-use bitflags::bitflags;
+use crate::core::{image, svg, Color, Radians, Rectangle};
-pub use ::image as image_rs;
+/// 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<Color>,
+
+ /// 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<image_rs::DynamicImage> {
- match handle.data() {
- Data::Path(path) => {
+///
+/// [`Handle`]: image::Handle
+pub fn load(
+ handle: &image::Handle,
+) -> ::image::ImageResult<::image::ImageBuffer<::image::Rgba<u8>, 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<R>(reader: &mut R) -> Result<Self, exif::Error>
+ 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<image_rs::DynamicImage> {
.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<R>(reader: &mut R) -> Result<Self, exif::Error>
- 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);
- }
+ ..
+ } => (*width, *height, pixels.clone()),
+ };
- image
+ 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<T: Layer> {
+ layers: Vec<T>,
+ transformations: Vec<Transformation>,
+ previous: Vec<usize>,
+ current: usize,
+ active_count: usize,
+}
+
+impl<T: Layer> Stack<T> {
+ /// 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<Item = &mut T> {
+ 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<Item = &T> {
+ 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<T: Layer> Default for Stack<T> {
+ 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<SolidVertex2D>,
- /// 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<GradientVertex2D>,
- /// 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 { buffers, .. } => &buffers.indices,
+ Self::Gradient { buffers, .. } => &buffers.indices,
+ }
+ }
+
+ /// Returns the [`Transformation`] of the [`Mesh`].
+ pub fn transformation(&self) -> Transformation {
match self {
- Self::Solid { size, .. } | Self::Gradient { size, .. } => {
- Rectangle::with_size(*size)
+ 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<T> {
- /// 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<Color>,
-
- /// The bounds of the viewport
- bounds: Rectangle,
- },
- /// A group of primitives
- Group {
- /// The primitives of the group
- primitives: Vec<Primitive<T>>,
- },
- /// A clip primitive
- Clip {
- /// The bounds of the clip
- bounds: Rectangle,
- /// The content of the clip
- content: Box<Primitive<T>>,
- },
- /// A primitive that applies a [`Transformation`]
- Transform {
- /// The [`Transformation`]
- transformation: Transformation,
-
- /// The primitive to transform
- content: Box<Primitive<T>>,
- },
- /// A cached primitive.
- ///
- /// This can be useful if you are implementing a widget where primitive
- /// generation is expensive.
- Cache {
- /// The cached primitive
- content: Arc<Primitive<T>>,
- },
- /// A backend-specific primitive.
- Custom(T),
-}
-
-impl<T> Primitive<T> {
- /// Groups the current [`Primitive`].
- pub fn group(primitives: Vec<Self>) -> 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<B: Backend> {
- backend: B,
- default_font: Font,
- default_text_size: Pixels,
- primitives: Vec<Primitive<B::Primitive>>,
-}
-
-impl<B: Backend> Renderer<B> {
- /// 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<B::Primitive>) {
- self.primitives.push(primitive);
- }
-
- /// Runs the given closure with the [`Backend`] and the recorded primitives
- /// of the [`Renderer`].
- pub fn with_primitives<O>(
- &mut self,
- f: impl FnOnce(&mut B, &[Primitive<B::Primitive>]) -> O,
- ) -> O {
- f(&mut self.backend, &self.primitives)
- }
-
- /// Starts recording a new layer.
- pub fn start_layer(&mut self) -> Vec<Primitive<B::Primitive>> {
- std::mem::take(&mut self.primitives)
- }
-
- /// Ends the recording of a layer.
- pub fn end_layer(
- &mut self,
- primitives: Vec<Primitive<B::Primitive>>,
- 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<Primitive<B::Primitive>> {
- std::mem::take(&mut self.primitives)
- }
-
- /// Ends the recording of a translation.
- pub fn end_transformation(
- &mut self,
- primitives: Vec<Primitive<B::Primitive>>,
- transformation: Transformation,
- ) {
- let layer = std::mem::replace(&mut self.primitives, primitives);
-
- self.primitives
- .push(Primitive::group(layer).transform(transformation));
- }
-}
-
-impl<B: Backend> iced_core::Renderer for Renderer<B> {
- 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<Background>,
- ) {
- 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<B> core::text::Renderer for Renderer<B>
-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<B> image::Renderer for Renderer<B>
-where
- B: Backend + backend::Image,
-{
- type Handle = image::Handle;
-
- fn dimensions(&self, handle: &image::Handle) -> Size<u32> {
- 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<B> svg::Renderer for Renderer<B>
-where
- B: Backend + backend::Svg,
-{
- fn dimensions(&self, handle: &svg::Handle) -> Size<u32> {
- self.backend().viewport_dimensions(handle)
- }
-
- fn draw(
- &mut self,
- handle: svg::Handle,
- color: Option<Color>,
- bounds: Rectangle,
- ) {
- self.primitives.push(Primitive::Svg {
- handle,
- color,
- bounds,
- });
- }
-}
diff --git a/renderer/src/settings.rs b/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<Rectangle> {
+ 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<KeyHash, Entry>,
aliases: FxHashMap<KeyHash, KeyHash>,
recently_used: FxHashSet<KeyHash>,
- 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<highlighting::ThemeSet> =
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<Color> {
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<Font> {
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<Font> {
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
index c23a814c..8b137891 100644
--- a/renderer/src/compositor.rs
+++ b/renderer/src/compositor.rs
@@ -1,270 +1 @@
-use crate::core::Color;
-use crate::graphics::compositor::{Information, SurfaceError, Window};
-use crate::graphics::{Error, Viewport};
-use crate::{Renderer, Settings};
-use std::env;
-use std::future::Future;
-
-pub enum Compositor {
- TinySkia(iced_tiny_skia::window::Compositor),
- #[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<W: Window + Clone>(
- settings: Self::Settings,
- compatible_window: W,
- ) -> impl Future<Output = Result<Self, Error>> {
- let candidates =
- Candidate::list_from_env().unwrap_or(Candidate::default_list());
-
- async move {
- let mut error = Error::GraphicsAdapterNotFound;
-
- for candidate in candidates {
- match candidate.build(settings, compatible_window.clone()).await
- {
- Ok(compositor) => return Ok(compositor),
- Err(new_error) => {
- error = new_error;
- }
- }
- }
-
- Err(error)
- }
- }
-
- fn create_renderer(&self) -> Self::Renderer {
- match self {
- Compositor::TinySkia(compositor) => {
- Renderer::TinySkia(compositor.create_renderer())
- }
- #[cfg(feature = "wgpu")]
- Compositor::Wgpu(compositor) => {
- Renderer::Wgpu(compositor.create_renderer())
- }
- }
- }
-
- fn create_surface<W: Window + Clone>(
- &mut self,
- window: W,
- 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<T: AsRef<str>>(
- &mut self,
- renderer: &mut Self::Renderer,
- surface: &mut Self::Surface,
- viewport: &Viewport,
- background_color: Color,
- overlay: &[T],
- ) -> 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,
- overlay,
- )
- }),
- #[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,
- overlay,
- )
- }),
- #[allow(unreachable_patterns)]
- _ => panic!(
- "The provided renderer or surface are not compatible \
- with the compositor."
- ),
- }
- }
-
- fn screenshot<T: AsRef<str>>(
- &mut self,
- renderer: &mut Self::Renderer,
- surface: &mut Self::Surface,
- viewport: &Viewport,
- background_color: Color,
- overlay: &[T],
- ) -> Vec<u8> {
- 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,
- overlay,
- )
- }),
- #[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,
- overlay,
- )
- }),
- #[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<Self> {
- vec![
- #[cfg(feature = "wgpu")]
- Self::Wgpu,
- Self::TinySkia,
- ]
- }
-
- fn list_from_env() -> Option<Vec<Self>> {
- 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(),
- )
- }
-
- async fn build<W: Window>(
- self,
- settings: Settings,
- _compatible_window: W,
- ) -> Result<Compositor, Error> {
- 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,
- )
- .await?;
-
- 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..6a169692
--- /dev/null
+++ b/renderer/src/fallback.rs
@@ -0,0 +1,637 @@
+//! 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<A, B> {
+ /// 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<A, B> core::Renderer for Renderer<A, B>
+where
+ A: core::Renderer,
+ B: core::Renderer,
+{
+ fn fill_quad(
+ &mut self,
+ quad: renderer::Quad,
+ background: impl Into<Background>,
+ ) {
+ 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<A, B> core::text::Renderer for Renderer<A, B>
+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<String, Self::Font>,
+ position: Point,
+ color: Color,
+ clip_bounds: Rectangle,
+ ) {
+ delegate!(
+ self,
+ renderer,
+ renderer.fill_text(text, position, color, clip_bounds)
+ );
+ }
+}
+
+impl<A, B> image::Renderer for Renderer<A, B>
+where
+ A: image::Renderer,
+ B: image::Renderer<Handle = A::Handle>,
+{
+ type Handle = A::Handle;
+
+ fn measure_image(&self, handle: &Self::Handle) -> Size<u32> {
+ 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<A, B> svg::Renderer for Renderer<A, B>
+where
+ A: svg::Renderer,
+ B: svg::Renderer,
+{
+ fn measure_svg(&self, handle: &svg::Handle) -> Size<u32> {
+ delegate!(self, renderer, renderer.measure_svg(handle))
+ }
+
+ fn draw_svg(
+ &mut self,
+ handle: svg::Handle,
+ color: Option<Color>,
+ bounds: Rectangle,
+ rotation: Radians,
+ opacity: f32,
+ ) {
+ delegate!(
+ self,
+ renderer,
+ renderer.draw_svg(handle, color, bounds, rotation, opacity)
+ );
+ }
+}
+
+impl<A, B> mesh::Renderer for Renderer<A, B>
+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<A, B>
+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<A, B> {
+ /// The primary surface option.
+ Primary(A),
+ /// The secondary (or fallback) surface option.
+ Secondary(B),
+}
+
+impl<A, B> graphics::Compositor for Compositor<A, B>
+where
+ A: graphics::Compositor,
+ B: graphics::Compositor,
+{
+ type Renderer = Renderer<A::Renderer, B::Renderer>;
+ type Surface = Surface<A::Surface, B::Surface>;
+
+ async fn with_backend<W: compositor::Window + Clone>(
+ settings: graphics::Settings,
+ compatible_window: W,
+ backend: Option<&str>,
+ ) -> Result<Self, graphics::Error> {
+ 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<W: compositor::Window + Clone>(
+ &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<T: AsRef<str>>(
+ &mut self,
+ renderer: &mut Self::Renderer,
+ surface: &mut Self::Surface,
+ viewport: &graphics::Viewport,
+ background_color: Color,
+ overlay: &[T],
+ ) -> Result<(), compositor::SurfaceError> {
+ match (self, renderer, surface) {
+ (
+ Self::Primary(compositor),
+ Renderer::Primary(renderer),
+ Surface::Primary(surface),
+ ) => compositor.present(
+ renderer,
+ surface,
+ viewport,
+ background_color,
+ overlay,
+ ),
+ (
+ Self::Secondary(compositor),
+ Renderer::Secondary(renderer),
+ Surface::Secondary(surface),
+ ) => compositor.present(
+ renderer,
+ surface,
+ viewport,
+ background_color,
+ overlay,
+ ),
+ _ => unreachable!(),
+ }
+ }
+
+ fn screenshot<T: AsRef<str>>(
+ &mut self,
+ renderer: &mut Self::Renderer,
+ surface: &mut Self::Surface,
+ viewport: &graphics::Viewport,
+ background_color: Color,
+ overlay: &[T],
+ ) -> Vec<u8> {
+ match (self, renderer, surface) {
+ (
+ Self::Primary(compositor),
+ Renderer::Primary(renderer),
+ Surface::Primary(surface),
+ ) => compositor.screenshot(
+ renderer,
+ surface,
+ viewport,
+ background_color,
+ overlay,
+ ),
+ (
+ Self::Secondary(compositor),
+ Renderer::Secondary(renderer),
+ Surface::Secondary(surface),
+ ) => compositor.screenshot(
+ renderer,
+ surface,
+ viewport,
+ background_color,
+ overlay,
+ ),
+ _ => unreachable!(),
+ }
+ }
+}
+
+#[cfg(feature = "wgpu")]
+impl<A, B> iced_wgpu::primitive::Renderer for Renderer<A, B>
+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<A, B> geometry::Renderer for Renderer<A, B>
+ where
+ A: geometry::Renderer,
+ B: geometry::Renderer,
+ {
+ type Geometry = Geometry<A::Geometry, B::Geometry>;
+ type Frame = Frame<A::Frame, B::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<A, B> {
+ Primary(A),
+ Secondary(B),
+ }
+
+ impl<A, B> Cached for Geometry<A, B>
+ where
+ A: Cached,
+ B: Cached,
+ {
+ type Cache = Geometry<A::Cache, B::Cache>;
+
+ 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>,
+ ) -> 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<A, B> {
+ Primary(A),
+ Secondary(B),
+ }
+
+ impl<A, B> geometry::frame::Backend for Frame<A, B>
+ where
+ A: geometry::frame::Backend,
+ B: geometry::frame::Backend,
+ {
+ type Geometry = Geometry<A::Geometry, B::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<Fill>) {
+ delegate!(self, frame, frame.fill(path, fill));
+ }
+
+ fn fill_rectangle(
+ &mut self,
+ top_left: Point,
+ size: Size,
+ fill: impl Into<Fill>,
+ ) {
+ delegate!(self, frame, frame.fill_rectangle(top_left, size, fill));
+ }
+
+ fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) {
+ delegate!(self, frame, frame.stroke(path, stroke));
+ }
+
+ fn fill_text(&mut self, text: impl Into<Text>) {
+ 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<Radians>) {
+ delegate!(self, frame, frame.rotate(angle));
+ }
+
+ fn scale(&mut self, scale: impl Into<f32>) {
+ delegate!(self, frame, frame.scale(scale));
+ }
+
+ fn scale_nonuniform(&mut self, scale: impl Into<Vector>) {
+ 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<A, B> compositor::Default for Renderer<A, B>
+where
+ A: compositor::Default,
+ B: compositor::Default,
+{
+ type Compositor = Compositor<A::Compositor, B::Compositor>;
+}
diff --git a/renderer/src/geometry.rs b/renderer/src/geometry.rs
deleted file mode 100644
index 36435148..00000000
--- a/renderer/src/geometry.rs
+++ /dev/null
@@ -1,210 +0,0 @@
-mod cache;
-
-pub use cache::Cache;
-
-use crate::core::{Point, Radians, 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<Fill>) {
- 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<Fill>,
- ) {
- 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<Stroke<'a>>) {
- 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<Text>) {
- 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<R>(&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<R>(
- &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: impl Into<Radians>) {
- 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<f32>) {
- 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<Vector>) {
- 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<State>,
-}
-
-#[derive(Debug, Default)]
-enum State {
- #[default]
- Empty,
- Filled {
- bounds: Size,
- primitive: Internal,
- },
-}
-
-#[derive(Debug, Clone)]
-enum Internal {
- TinySkia(Arc<iced_tiny_skia::Primitive>),
- #[cfg(feature = "wgpu")]
- Wgpu(Arc<iced_wgpu::Primitive>),
-}
-
-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..220542e1 100644
--- a/renderer/src/lib.rs
+++ b/renderer/src/lib.rs
@@ -1,302 +1,60 @@
-#![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),
-}
-
-macro_rules! delegate {
- ($renderer:expr, $name:ident, $body:expr) => {
- match $renderer {
- Self::TinySkia($name) => $body,
- #[cfg(feature = "wgpu")]
- Self::Wgpu($name) => $body,
- }
- };
-}
-
-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),
- ));
- }
- }
- }
-}
-
-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();
+pub type Renderer = renderer::Renderer;
- 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<Background>,
- ) {
- 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)
- );
- }
+/// The default graphics compositor for [`iced`].
+///
+/// [`iced`]: https://github.com/iced-rs/iced
+pub type Compositor = renderer::Compositor;
- 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)
- );
- }
+#[cfg(all(feature = "wgpu", feature = "tiny-skia"))]
+mod renderer {
+ pub type Renderer = crate::fallback::Renderer<
+ iced_wgpu::Renderer,
+ iced_tiny_skia::Renderer,
+ >;
- 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)
- );
- }
+ pub type Compositor = crate::fallback::Compositor<
+ iced_wgpu::window::Compositor,
+ iced_tiny_skia::window::Compositor,
+ >;
}
-#[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<u32> {
- 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(all(feature = "wgpu", not(feature = "tiny-skia")))]
+mod renderer {
+ pub type Renderer = iced_wgpu::Renderer;
+ pub type Compositor = iced_wgpu::window::Compositor;
}
-#[cfg(feature = "svg")]
-impl crate::core::svg::Renderer for Renderer {
- fn dimensions(&self, handle: &crate::core::svg::Handle) -> core::Size<u32> {
- delegate!(self, renderer, renderer.dimensions(handle))
- }
-
- fn draw(
- &mut self,
- handle: crate::core::svg::Handle,
- color: Option<crate::core::Color>,
- bounds: Rectangle,
- ) {
- delegate!(self, renderer, renderer.draw(handle, color, bounds));
- }
+#[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;
}
-#[cfg(feature = "geometry")]
-impl crate::graphics::geometry::Renderer for Renderer {
- type Geometry = crate::Geometry;
-
- fn draw(&mut self, layers: Vec<Self::Geometry>) {
- 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(not(any(feature = "wgpu", feature = "tiny-skia")))]
+mod renderer {
+ #[cfg(not(debug_assertions))]
+ compile_error!(
+ "Cannot compile `iced_renderer` in release mode \
+ without a renderer feature enabled. \
+ Enable either the `wgpu` or `tiny-skia` feature, or both."
+ );
-#[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);
- }
- }
- }
+ pub type Renderer = ();
+ pub type Compositor = ();
}
diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml
index 3a47a971..703c3ed9 100644
--- a/runtime/Cargo.toml
+++ b/runtime/Cargo.toml
@@ -10,11 +10,15 @@ homepage.workspace = true
categories.workspace = true
keywords.workspace = true
+[lints]
+workspace = true
+
[features]
debug = []
multi-window = []
[dependencies]
+bytes.workspace = true
iced_core.workspace = true
iced_futures.workspace = true
iced_futures.features = ["thread-pool"]
diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs
index 5c2836a5..5f054c46 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 afd04519..10366ec0 100644
--- a/runtime/src/multi_window/state.rs
+++ b/runtime/src/multi_window/state.rs
@@ -48,7 +48,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 d685b07c..c6589c22 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<Message>(id: Id, icon: Icon) -> Command<Message> {
/// Note that if the window closes before this call is processed the callback will not be run.
pub fn run_with_handle<Message>(
id: Id,
- f: impl FnOnce(&WindowHandle<'_>) -> Message + 'static,
+ f: impl FnOnce(WindowHandle<'_>) -> Message + 'static,
) -> Command<Message> {
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<T> {
/// 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<dyn FnOnce(&WindowHandle<'_>) -> T + 'static>),
+ RunWithHandle(Id, Box<dyn FnOnce(WindowHandle<'_>) -> T + 'static>),
/// Screenshot the viewport of the window.
Screenshot(Id, Box<dyn FnOnce(Screenshot) -> 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<Vec<u8>>,
+ pub bytes: Bytes,
/// The size of the [`Screenshot`].
pub size: Size<u32>,
}
@@ -28,9 +28,9 @@ impl Debug for Screenshot {
impl Screenshot {
/// Creates a new [`Screenshot`].
- pub fn new(bytes: Vec<u8>, size: Size<u32>) -> Self {
+ pub fn new(bytes: impl Into<Bytes>, size: Size<u32>) -> 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<Screenshot> 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/src/advanced.rs b/src/advanced.rs
index 306c3559..5826ba0f 100644
--- a/src/advanced.rs
+++ b/src/advanced.rs
@@ -9,10 +9,12 @@ 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 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 8317abcb..d12ba73d 100644
--- a/src/application.rs
+++ b/src/application.rs
@@ -1,4 +1,6 @@
//! Build interactive cross-platform applications.
+use crate::core::text;
+use crate::graphics::compositor;
use crate::shell::application;
use crate::{Command, Element, Executor, Settings, Subscription};
@@ -60,7 +62,7 @@ pub use application::{Appearance, DefaultStyle};
/// ```no_run
/// use iced::advanced::Application;
/// use iced::executor;
-/// use iced::{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 application::{Appearance, DefaultStyle};
/// type Flags = ();
/// type Message = ();
/// type Theme = Theme;
+/// type Renderer = Renderer;
///
/// fn new(_flags: ()) -> (Hello, Command<Self::Message>) {
/// (Hello, Command::none())
@@ -109,6 +112,9 @@ where
/// The theme of your [`Application`].
type Theme: Default;
+ /// The renderer of your [`Application`].
+ type Renderer: text::Renderer + compositor::Default;
+
/// The data needed to initialize your [`Application`].
type Flags;
@@ -142,7 +148,7 @@ where
/// 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`].
///
@@ -195,7 +201,7 @@ where
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 {
@@ -203,29 +209,14 @@ where
} else {
None
},
- ..crate::renderer::Settings::default()
+ ..crate::graphics::Settings::default()
};
- let run = crate::shell::application::run::<
+ Ok(crate::shell::application::run::<
Instance<Self>,
Self::Executor,
- crate::renderer::Compositor,
- >(settings.into(), renderer_settings);
-
- #[cfg(target_arch = "wasm32")]
- {
- use crate::futures::FutureExt;
- use iced_futures::backend::wasm::wasm_bindgen::Executor;
-
- Executor::new()
- .expect("Create Wasm executor")
- .spawn(run.map(|_| ()));
-
- Ok(())
- }
-
- #[cfg(not(target_arch = "wasm32"))]
- Ok(crate::futures::executor::block_on(run)?)
+ <Self::Renderer as compositor::Default>::Compositor,
+ >(settings.into(), renderer_settings)?)
}
}
@@ -241,7 +232,7 @@ where
{
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::Message> {
self.0.update(message)
diff --git a/src/lib.rs b/src/lib.rs
index 0e9566e2..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,
@@ -165,13 +166,6 @@
#![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;
@@ -206,8 +200,8 @@ 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, Theme,
- Transformation, Vector,
+ Length, Padding, Pixels, Point, Radians, Rectangle, Rotation, Shadow, Size,
+ Theme, Transformation, Vector,
};
pub mod clipboard {
@@ -372,15 +366,16 @@ pub type Result = std::result::Result<(), Error>;
/// ]
/// }
/// ```
-pub fn run<State, Message, Theme>(
+pub fn run<State, Message, Theme, Renderer>(
title: impl program::Title<State> + 'static,
update: impl program::Update<State, Message> + 'static,
- view: impl for<'a> program::View<'a, State, Message, Theme> + '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()
}
diff --git a/src/multi_window.rs b/src/multi_window.rs
index fca0be46..b81297dc 100644
--- a/src/multi_window.rs
+++ b/src/multi_window.rs
@@ -174,7 +174,7 @@ where
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 {
@@ -182,7 +182,7 @@ where
} else {
None
},
- ..crate::renderer::Settings::default()
+ ..crate::graphics::Settings::default()
};
Ok(crate::shell::multi_window::run::<
diff --git a/src/program.rs b/src/program.rs
index 7a366585..d4c2a266 100644
--- a/src/program.rs
+++ b/src/program.rs
@@ -31,7 +31,9 @@
//! }
//! ```
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};
@@ -67,37 +69,41 @@ use std::borrow::Cow;
/// ]
/// }
/// ```
-pub fn program<State, Message, Theme>(
+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>,
+ 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, Update, View> {
+ 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, Update, View> Definition
- for Application<State, Message, Theme, Update, View>
+ 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>,
+ 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> {
@@ -115,7 +121,7 @@ where
fn view<'a>(
&self,
state: &'a Self::State,
- ) -> Element<'a, Self::Message, Self::Theme> {
+ ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> {
self.view.view(state).into()
}
}
@@ -127,6 +133,7 @@ where
_state: PhantomData,
_message: PhantomData,
_theme: PhantomData,
+ _renderer: PhantomData,
},
settings: Settings::default(),
}
@@ -184,6 +191,7 @@ impl<P: Definition> Program<P> {
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;
@@ -216,7 +224,7 @@ impl<P: Definition> Program<P> {
fn view(
&self,
- ) -> crate::Element<'_, Self::Message, Self::Theme, crate::Renderer>
+ ) -> crate::Element<'_, Self::Message, Self::Theme, Self::Renderer>
{
self.program.view(&self.state)
}
@@ -417,6 +425,9 @@ pub trait Definition: Sized {
/// 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;
@@ -431,7 +442,7 @@ pub trait Definition: Sized {
fn view<'a>(
&self,
state: &'a Self::State,
- ) -> Element<'a, Self::Message, Self::Theme>;
+ ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer>;
fn title(&self, _state: &Self::State) -> String {
String::from("A cool iced application!")
@@ -470,6 +481,7 @@ fn with_title<P: Definition>(
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> {
@@ -491,7 +503,7 @@ fn with_title<P: Definition>(
fn view<'a>(
&self,
state: &'a Self::State,
- ) -> Element<'a, Self::Message, Self::Theme> {
+ ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> {
self.program.view(state)
}
@@ -534,6 +546,7 @@ fn with_load<P: Definition>(
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> {
@@ -551,7 +564,7 @@ fn with_load<P: Definition>(
fn view<'a>(
&self,
state: &'a Self::State,
- ) -> Element<'a, Self::Message, Self::Theme> {
+ ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> {
self.program.view(state)
}
@@ -598,6 +611,7 @@ fn with_subscription<P: Definition>(
type State = P::State;
type Message = P::Message;
type Theme = P::Theme;
+ type Renderer = P::Renderer;
type Executor = executor::Default;
fn subscription(
@@ -622,7 +636,7 @@ fn with_subscription<P: Definition>(
fn view<'a>(
&self,
state: &'a Self::State,
- ) -> Element<'a, Self::Message, Self::Theme> {
+ ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> {
self.program.view(state)
}
@@ -665,6 +679,7 @@ fn with_theme<P: Definition>(
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 {
@@ -690,7 +705,7 @@ fn with_theme<P: Definition>(
fn view<'a>(
&self,
state: &'a Self::State,
- ) -> Element<'a, Self::Message, Self::Theme> {
+ ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> {
self.program.view(state)
}
@@ -729,6 +744,7 @@ fn with_style<P: Definition>(
type State = P::State;
type Message = P::Message;
type Theme = P::Theme;
+ type Renderer = P::Renderer;
type Executor = P::Executor;
fn style(
@@ -758,7 +774,7 @@ fn with_style<P: Definition>(
fn view<'a>(
&self,
state: &'a Self::State,
- ) -> Element<'a, Self::Message, Self::Theme> {
+ ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> {
self.program.view(state)
}
@@ -834,18 +850,30 @@ where
///
/// This trait allows the [`program`] builder to take any closure that
/// returns any `Into<Element<'_, Message>>`.
-pub trait View<'a, State, Message, Theme> {
+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>>;
+ fn view(
+ &self,
+ state: &'a State,
+ ) -> impl Into<Element<'a, Message, Theme, Renderer>>;
}
-impl<'a, T, State, Message, Theme, Widget> View<'a, State, Message, Theme> for T
+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>>,
+ Widget: Into<Element<'a, Message, Theme, Renderer>>,
{
- fn view(&self, state: &'a State) -> impl Into<Element<'a, Message, Theme>> {
+ 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/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/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 b6487b38..00000000
--- a/tiny_skia/src/backend.rs
+++ /dev/null
@@ -1,1020 +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<T: AsRef<str>>(
- &mut self,
- pixels: &mut tiny_skia::PixmapMut<'_>,
- clip_mask: &mut tiny_skia::Mask,
- primitives: &[Primitive],
- viewport: &Viewport,
- damage: &[Rectangle],
- background_color: Color,
- overlay: &[T],
- ) {
- let physical_size = viewport.physical_size();
- let scale_factor = viewport.scale_factor() as f32;
-
- if !overlay.is_empty() {
- let path = tiny_skia::PathBuilder::from_rect(
- tiny_skia::Rect::from_xywh(
- 0.0,
- 0.0,
- physical_size.width as f32,
- physical_size.height as f32,
- )
- .expect("Create damage rectangle"),
- );
-
- pixels.fill_path(
- &path,
- &tiny_skia::Paint {
- shader: tiny_skia::Shader::SolidColor(into_color(Color {
- a: 0.1,
- ..background_color
- })),
- anti_alias: false,
- ..Default::default()
- },
- tiny_skia::FillRule::default(),
- tiny_skia::Transform::identity(),
- None,
- );
- }
-
- for &region 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,
- );
- }
-
- if !overlay.is_empty() {
- pixels.stroke_path(
- &path,
- &tiny_skia::Paint {
- shader: tiny_skia::Shader::SolidColor(into_color(
- Color::from_rgb(1.0, 0.0, 0.0),
- )),
- anti_alias: false,
- ..tiny_skia::Paint::default()
- },
- &tiny_skia::Stroke {
- width: 1.0,
- ..tiny_skia::Stroke::default()
- },
- tiny_skia::Transform::identity(),
- None,
- );
- }
- }
-
- 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 16787f89..02b6e1b9 100644
--- a/tiny_skia/src/geometry.rs
+++ b/tiny_skia/src/geometry.rs
@@ -1,47 +1,102 @@
use crate::core::text::LineHeight;
-use crate::core::{
- Pixels, Point, Radians, 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,
+ ),
}
}
+}
+
+impl geometry::frame::Backend for Frame {
+ type Geometry = Geometry;
- pub fn width(&self) -> f32 {
- self.size.width
+ 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 {
@@ -53,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,
@@ -81,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 {
@@ -102,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
@@ -157,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,
@@ -174,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)
+ }
+
+ fn paste(&mut self, frame: Self, _at: Point) {
+ self.primitives.extend(frame.primitives);
+ self.text.extend(frame.text);
}
- pub fn translate(&mut self, translation: Vector) {
+ fn translate(&mut self, translation: Vector) {
self.transform =
self.transform.pre_translate(translation.x, translation.y);
}
- pub fn rotate(&mut self, angle: impl Into<Radians>) {
+ 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,
+ &current.quads,
+ |(quad, _)| {
+ quad.bounds
+ .expand(1.0)
+ .intersection(&current.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,
+ &current.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,
+ &current.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,
+ &current.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..1aabff00 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,407 @@ 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<T: AsRef<str>>(
+ &mut self,
+ pixels: &mut tiny_skia::PixmapMut<'_>,
+ clip_mask: &mut tiny_skia::Mask,
+ viewport: &Viewport,
+ damage: &[Rectangle],
+ background_color: Color,
+ overlay: &[T],
+ ) {
+ let physical_size = viewport.physical_size();
+ let scale_factor = viewport.scale_factor() as f32;
+
+ if !overlay.is_empty() {
+ let path = tiny_skia::PathBuilder::from_rect(
+ tiny_skia::Rect::from_xywh(
+ 0.0,
+ 0.0,
+ physical_size.width as f32,
+ physical_size.height as f32,
+ )
+ .expect("Create damage rectangle"),
+ );
+
+ pixels.fill_path(
+ &path,
+ &tiny_skia::Paint {
+ shader: tiny_skia::Shader::SolidColor(engine::into_color(
+ Color {
+ a: 0.1,
+ ..background_color
+ },
+ )),
+ anti_alias: false,
+ ..Default::default()
+ },
+ tiny_skia::FillRule::default(),
+ tiny_skia::Transform::identity(),
+ None,
+ );
+ }
+
+ self.layers.flush();
+
+ for &region 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,
+ );
+ }
+ }
+
+ if !overlay.is_empty() {
+ pixels.stroke_path(
+ &path,
+ &tiny_skia::Paint {
+ shader: tiny_skia::Shader::SolidColor(
+ engine::into_color(Color::from_rgb(1.0, 0.0, 0.0)),
+ ),
+ anti_alias: false,
+ ..tiny_skia::Paint::default()
+ },
+ &tiny_skia::Stroke {
+ width: 1.0,
+ ..tiny_skia::Stroke::default()
+ },
+ tiny_skia::Transform::identity(),
+ None,
+ );
+ }
+ }
+
+ 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: &paragraph::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 a98825f1..153af6d5 100644
--- a/tiny_skia/src/window/compositor.rs
+++ b/tiny_skia/src/window/compositor.rs
@@ -1,44 +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::future::{self, Future};
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,
- ) -> impl Future<Output = Result<Self, Error>> {
- future::ready(Ok(new(settings, compatible_window)))
+ backend: Option<&str>,
+ ) -> Result<Self, Error> {
+ 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,
)
@@ -60,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,
};
@@ -86,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 {
@@ -104,16 +115,7 @@ impl crate::graphics::Compositor for Compositor {
background_color: Color,
overlay: &[T],
) -> Result<(), compositor::SurfaceError> {
- renderer.with_primitives(|backend, primitives| {
- present(
- backend,
- surface,
- primitives,
- viewport,
- background_color,
- overlay,
- )
- })
+ present(renderer, surface, viewport, background_color, overlay)
}
fn screenshot<T: AsRef<str>>(
@@ -124,16 +126,7 @@ impl crate::graphics::Compositor for Compositor {
background_color: Color,
overlay: &[T],
) -> Vec<u8> {
- renderer.with_primitives(|backend, primitives| {
- screenshot(
- surface,
- backend,
- primitives,
- viewport,
- background_color,
- overlay,
- )
- })
+ screenshot(renderer, surface, viewport, background_color, overlay)
}
}
@@ -149,38 +142,42 @@ pub fn new<W: compositor::Window>(
}
pub fn present<T: AsRef<str>>(
- backend: &mut Backend,
+ renderer: &mut Renderer,
surface: &mut Surface,
- primitives: &[Primitive],
viewport: &Viewport,
background_color: Color,
overlay: &[T],
) -> 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())]);
@@ -188,10 +185,11 @@ pub fn present<T: AsRef<str>>(
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),
@@ -200,10 +198,9 @@ pub fn present<T: AsRef<str>>(
)
.expect("Create pixel map");
- backend.draw(
+ renderer.draw(
&mut pixels,
&mut surface.clip_mask,
- primitives,
viewport,
&damage,
background_color,
@@ -214,9 +211,8 @@ pub fn present<T: AsRef<str>>(
}
pub fn screenshot<T: AsRef<str>>(
+ renderer: &mut Renderer,
surface: &mut Surface,
- backend: &mut Backend,
- primitives: &[Primitive],
viewport: &Viewport,
background_color: Color,
overlay: &[T],
@@ -226,7 +222,7 @@ pub fn screenshot<T: AsRef<str>>(
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,
@@ -234,7 +230,6 @@ pub fn screenshot<T: AsRef<str>>(
)
.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 09ddbe4d..00000000
--- a/wgpu/src/backend.rs
+++ /dev/null
@@ -1,399 +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<T: AsRef<str>>(
- &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,
- overlay_text: &[T],
- ) {
- 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 mut layers = Layer::generate(primitives, viewport);
-
- if !overlay_text.is_empty() {
- layers.push(Layer::overlay(overlay_text, 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 890f3f89..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,7 +184,7 @@ 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);
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 f4e0fbda..f6213e1d 100644
--- a/wgpu/src/geometry.rs
+++ b/wgpu/src/geometry.rs
@@ -3,215 +3,136 @@ use crate::core::text::LineHeight;
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;
-/// A frame for drawing some geometry.
-#[allow(missing_debug_implementations)]
-pub struct Frame {
- size: Size,
- buffers: BufferStack,
- primitives: Vec<Primitive>,
- transforms: Transforms,
- fill_tessellator: tessellation::FillTessellator,
- stroke_tessellator: tessellation::StrokeTessellator,
+#[derive(Debug)]
+pub enum Geometry {
+ Live { meshes: Vec<Mesh>, text: Vec<Text> },
+ Cached(Cache),
}
-enum Buffer {
- Solid(tessellation::VertexBuffers<mesh::SolidVertex2D, u32>),
- Gradient(tessellation::VertexBuffers<mesh::GradientVertex2D, u32>),
+#[derive(Debug, Clone)]
+pub struct Cache {
+ pub meshes: Option<triangle::Cache>,
+ pub text: Option<text::Cache>,
}
-struct BufferStack {
- stack: Vec<Buffer>,
-}
+impl Cached for Geometry {
+ type Cache = Cache;
-impl BufferStack {
- fn new() -> Self {
- Self { stack: Vec::new() }
+ fn load(cache: &Self::Cache) -> Self {
+ Geometry::Cached(cache.clone())
}
- fn get_mut(&mut self, style: &Style) -> &mut Buffer {
- match style {
- Style::Solid(_) => match self.stack.last() {
- Some(Buffer::Solid(_)) => {}
- _ => {
- self.stack.push(Buffer::Solid(
- tessellation::VertexBuffers::new(),
- ));
- }
- },
- Style::Gradient(_) => match self.stack.last() {
- Some(Buffer::Gradient(_)) => {}
- _ => {
- self.stack.push(Buffer::Gradient(
- tessellation::VertexBuffers::new(),
- ));
- }
- },
- }
-
- self.stack.last_mut().unwrap()
- }
+ 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);
+ }
- fn get_fill<'a>(
- &'a mut self,
- style: &Style,
- ) -> Box<dyn tessellation::FillGeometryBuilder + 'a> {
- match (style, self.get_mut(style)) {
- (Style::Solid(color), Buffer::Solid(buffer)) => {
- Box::new(tessellation::BuffersBuilder::new(
- buffer,
- TriangleVertex2DBuilder(color::pack(*color)),
- ))
- }
- (Style::Gradient(gradient), Buffer::Gradient(buffer)) => {
- Box::new(tessellation::BuffersBuilder::new(
- buffer,
- GradientVertex2DBuilder {
- gradient: gradient.pack(),
- },
- ))
- }
- _ => unreachable!(),
- }
- }
+ if let Some(cache) = &mut previous.text {
+ cache.update(text);
+ } else {
+ previous.text = text::Cache::new(group, text);
+ }
- fn get_stroke<'a>(
- &'a mut self,
- style: &Style,
- ) -> Box<dyn tessellation::StrokeGeometryBuilder + 'a> {
- match (style, self.get_mut(style)) {
- (Style::Solid(color), Buffer::Solid(buffer)) => {
- Box::new(tessellation::BuffersBuilder::new(
- buffer,
- TriangleVertex2DBuilder(color::pack(*color)),
- ))
- }
- (Style::Gradient(gradient), Buffer::Gradient(buffer)) => {
- Box::new(tessellation::BuffersBuilder::new(
- buffer,
- GradientVertex2DBuilder {
- gradient: gradient.pack(),
- },
- ))
+ previous
+ } else {
+ Cache {
+ meshes: triangle::Cache::new(meshes),
+ text: text::Cache::new(group, text),
+ }
+ }
}
- _ => unreachable!(),
+ Self::Cached(cache) => cache,
}
}
}
-#[derive(Debug)]
-struct Transforms {
- previous: Vec<Transform>,
- current: Transform,
-}
-
-#[derive(Debug, Clone, Copy)]
-struct Transform(lyon::math::Transform);
-
-impl Transform {
- fn is_identity(&self) -> bool {
- self.0 == lyon::math::Transform::identity()
- }
-
- fn is_scale_translation(&self) -> bool {
- self.0.m12.abs() < 2.0 * f32::EPSILON
- && self.0.m21.abs() < 2.0 * f32::EPSILON
- }
-
- fn scale(&self) -> (f32, f32) {
- (self.0.m11, self.0.m22)
- }
-
- fn transform_point(&self, point: Point) -> Point {
- let transformed = self
- .0
- .transform_point(euclid::Point2D::new(point.x, point.y));
-
- Point {
- x: transformed.x,
- y: transformed.y,
- }
- }
-
- fn transform_style(&self, style: Style) -> Style {
- match style {
- Style::Solid(color) => Style::Solid(color),
- Style::Gradient(gradient) => {
- Style::Gradient(self.transform_gradient(gradient))
- }
- }
- }
-
- fn transform_gradient(&self, mut gradient: Gradient) -> Gradient {
- match &mut gradient {
- Gradient::Linear(linear) => {
- linear.start = self.transform_point(linear.start);
- linear.end = self.transform_point(linear.end);
- }
- }
-
- gradient
- }
+/// A frame for drawing some geometry.
+#[allow(missing_debug_implementations)]
+pub struct Frame {
+ clip_bounds: Rectangle,
+ buffers: BufferStack,
+ meshes: Vec<Mesh>,
+ text: Vec<Text>,
+ transforms: Transforms,
+ fill_tessellator: tessellation::FillTessellator,
+ stroke_tessellator: tessellation::StrokeTessellator,
}
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.
+ /// 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 {
- size,
+ clip_bounds: bounds,
buffers: BufferStack::new(),
- primitives: Vec::new(),
+ meshes: Vec::new(),
+ text: Vec::new(),
transforms: Transforms {
previous: Vec::new(),
- current: Transform(lyon::math::Transform::identity()),
+ 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;
- /// Returns the width of the [`Frame`].
#[inline]
- pub fn width(&self) -> f32 {
- self.size.width
+ fn width(&self) -> f32 {
+ self.clip_bounds.width
}
- /// Returns the height of the [`Frame`].
#[inline]
- pub fn height(&self) -> f32 {
- self.size.height
+ fn height(&self) -> f32 {
+ self.clip_bounds.height
}
- /// Returns the dimensions of the [`Frame`].
#[inline]
- pub fn size(&self) -> Size {
- self.size
+ fn size(&self) -> Size {
+ self.clip_bounds.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)
+ fn center(&self) -> Point {
+ Point::new(self.clip_bounds.width / 2.0, self.clip_bounds.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>) {
+ fn fill(&mut self, path: &Path, fill: impl Into<Fill>) {
let Fill { style, rule } = fill.into();
let mut buffer = self
@@ -239,9 +160,7 @@ impl Frame {
.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(
+ fn fill_rectangle(
&mut self,
top_left: Point,
size: Size,
@@ -276,9 +195,7 @@ impl Frame {
.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>>) {
+ fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) {
let stroke = stroke.into();
let mut buffer = self
@@ -315,20 +232,7 @@ impl Frame {
.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>) {
+ fn fill_text(&mut self, text: impl Into<geometry::Text>) {
let text = text.into();
let (scale_x, scale_y) = self.transforms.current.scale();
@@ -366,105 +270,25 @@ impl Frame {
height: f32::INFINITY,
};
- // TODO: Honor layering!
- self.primitives.push(Primitive::Text {
+ self.text.push(graphics::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,
shaping: text.shaping,
- clip_bounds: Rectangle::with_size(Size::INFINITY),
+ clip_bounds: self.clip_bounds,
});
} 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) {
+ fn translate(&mut self, translation: Vector) {
self.transforms.current.0 =
self.transforms
.current
@@ -475,9 +299,8 @@ impl Frame {
));
}
- /// Applies a rotation in radians to the current transform of the [`Frame`].
#[inline]
- pub fn rotate(&mut self, angle: impl Into<Radians>) {
+ fn rotate(&mut self, angle: impl Into<Radians>) {
self.transforms.current.0 = self
.transforms
.current
@@ -485,66 +308,217 @@ impl Frame {
.pre_rotate(lyon::math::Angle::radians(angle.into().0));
}
- /// Applies a uniform scaling to the current transform of the [`Frame`].
#[inline]
- 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 });
}
- /// Applies a non-uniform scaling to the current transform of the [`Frame`].
#[inline]
- pub fn scale_nonuniform(&mut self, scale: impl Into<Vector>) {
+ 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 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,
}
}
+}
- 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,
- }),
- ));
- }
+enum Buffer {
+ Solid(tessellation::VertexBuffers<mesh::SolidVertex2D, u32>),
+ Gradient(tessellation::VertexBuffers<mesh::GradientVertex2D, u32>),
+}
+
+struct BufferStack {
+ stack: Vec<Buffer>,
+}
+
+impl BufferStack {
+ fn new() -> Self {
+ Self { stack: Vec::new() }
+ }
+
+ fn get_mut(&mut self, style: &Style) -> &mut Buffer {
+ match style {
+ Style::Solid(_) => match self.stack.last() {
+ Some(Buffer::Solid(_)) => {}
+ _ => {
+ self.stack.push(Buffer::Solid(
+ tessellation::VertexBuffers::new(),
+ ));
}
- 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,
- }),
- ));
- }
+ },
+ Style::Gradient(_) => match self.stack.last() {
+ Some(Buffer::Gradient(_)) => {}
+ _ => {
+ self.stack.push(Buffer::Gradient(
+ tessellation::VertexBuffers::new(),
+ ));
}
+ },
+ }
+
+ self.stack.last_mut().unwrap()
+ }
+
+ fn get_fill<'a>(
+ &'a mut self,
+ style: &Style,
+ ) -> Box<dyn tessellation::FillGeometryBuilder + 'a> {
+ match (style, self.get_mut(style)) {
+ (Style::Solid(color), Buffer::Solid(buffer)) => {
+ Box::new(tessellation::BuffersBuilder::new(
+ buffer,
+ TriangleVertex2DBuilder(color::pack(*color)),
+ ))
+ }
+ (Style::Gradient(gradient), Buffer::Gradient(buffer)) => {
+ Box::new(tessellation::BuffersBuilder::new(
+ buffer,
+ GradientVertex2DBuilder {
+ gradient: gradient.pack(),
+ },
+ ))
}
+ _ => unreachable!(),
}
+ }
- self.primitives
+ fn get_stroke<'a>(
+ &'a mut self,
+ style: &Style,
+ ) -> Box<dyn tessellation::StrokeGeometryBuilder + 'a> {
+ match (style, self.get_mut(style)) {
+ (Style::Solid(color), Buffer::Solid(buffer)) => {
+ Box::new(tessellation::BuffersBuilder::new(
+ buffer,
+ TriangleVertex2DBuilder(color::pack(*color)),
+ ))
+ }
+ (Style::Gradient(gradient), Buffer::Gradient(buffer)) => {
+ Box::new(tessellation::BuffersBuilder::new(
+ buffer,
+ GradientVertex2DBuilder {
+ gradient: gradient.pack(),
+ },
+ ))
+ }
+ _ => 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)]
+struct Transforms {
+ previous: Vec<Transform>,
+ current: Transform,
+}
+
+#[derive(Debug, Clone, Copy)]
+struct Transform(lyon::math::Transform);
+
+impl Transform {
+ fn is_identity(&self) -> bool {
+ self.0 == lyon::math::Transform::identity()
+ }
+
+ fn is_scale_translation(&self) -> bool {
+ self.0.m12.abs() < 2.0 * f32::EPSILON
+ && self.0.m21.abs() < 2.0 * f32::EPSILON
+ }
+
+ fn scale(&self) -> (f32, f32) {
+ (self.0.m11, self.0.m22)
+ }
+
+ fn transform_point(&self, point: Point) -> Point {
+ let transformed = self
+ .0
+ .transform_point(euclid::Point2D::new(point.x, point.y));
+
+ Point {
+ x: transformed.x,
+ y: transformed.y,
+ }
+ }
+
+ fn transform_style(&self, style: Style) -> Style {
+ match style {
+ Style::Solid(color) => Style::Solid(color),
+ Style::Gradient(gradient) => {
+ Style::Gradient(self.transform_gradient(gradient))
+ }
+ }
+ }
+
+ fn transform_gradient(&self, mut gradient: Gradient) -> Gradient {
+ match &mut gradient {
+ Gradient::Linear(linear) => {
+ linear.start = self.transform_point(linear.start);
+ linear.end = self.transform_point(linear.end);
+ }
+ }
+
+ gradient
+ }
+}
struct GradientVertex2DBuilder {
gradient: gradient::Packed,
}
@@ -651,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
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,20 +322,162 @@ 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);
+ self.prepare_layer = 0;
+ }
+}
+
+#[derive(Debug)]
+struct Layer {
+ uniforms: wgpu::Buffer,
+ nearest: Data,
+ linear: Data,
+}
- #[cfg(feature = "svg")]
- self.vector_cache.borrow_mut().trim(&mut self.texture_atlas);
+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,
+ });
- self.prepare_layer = 0;
+ 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);
}
}
@@ -539,7 +485,10 @@ impl Pipeline {
#[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,
+ pub triangles: triangle::Batch,
+ pub primitives: primitive::Batch,
+ pub text: text::Batch,
+ pub images: image::Batch,
+ pending_meshes: Vec<Mesh>,
+ pending_text: Vec<Text>,
+}
- /// The triangle meshes of the [`Layer`].
- pub meshes: Vec<Mesh<'a>>,
+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);
+ }
- /// The text of the [`Layer`].
- pub text: Vec<Text<'a>>,
+ 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);
+ }
- /// The images of the [`Layer`].
- pub images: Vec<Image>,
+ 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);
+ }
- /// The custom pipelines of this [`Layer`].
- pub pipelines: Vec<Pipeline>,
-}
+ 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);
+ }
-impl<'a> Layer<'a> {
- /// Creates a new [`Layer`] with the given clipping bounds.
- pub fn new(bounds: Rectangle) -> Self {
- Self {
- bounds,
- quads: quad::Batch::default(),
- meshes: Vec::new(),
- text: Vec::new(),
- images: Vec::new(),
- pipelines: Vec::new(),
- }
+ 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);
}
- /// 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
- }));
+ 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;
+ }
}
- overlay
+ self.pending_meshes.push(mesh);
}
- /// 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,
- );
- }
+ pub fn draw_mesh_group(
+ &mut self,
+ meshes: Vec<Mesh>,
+ transformation: Transformation,
+ ) {
+ self.flush_meshes();
- layers
+ self.triangles.push(triangle::Item::Group {
+ meshes,
+ transformation,
+ });
}
- fn process_primitive(
- layers: &mut Vec<Self>,
+ pub fn draw_mesh_cache(
+ &mut self,
+ cache: triangle::Cache,
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];
+ self.flush_meshes();
- 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];
+ self.triangles.push(triangle::Item::Cached {
+ cache,
+ transformation,
+ });
+ }
- 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];
+ pub fn draw_text_group(
+ &mut self,
+ text: Vec<Text>,
+ transformation: Transformation,
+ ) {
+ self.flush_text();
- 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(),
- });
- }
- }
- },
+ 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(),
+ 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..ad88ce3e 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,571 @@ 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,
+ Vector,
+};
+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<T: AsRef<str>>(
+ &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,
+ overlay: &[T],
+ ) {
+ self.draw_overlay(overlay, 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);
+ }
+
+ fn draw_overlay(
+ &mut self,
+ overlay: &[impl AsRef<str>],
+ viewport: &Viewport,
+ ) {
+ use crate::core::alignment;
+ use crate::core::text::Renderer as _;
+ use crate::core::Renderer as _;
+
+ self.with_layer(
+ Rectangle::with_size(viewport.logical_size()),
+ |renderer| {
+ for (i, line) in overlay.iter().enumerate() {
+ let text = crate::core::Text {
+ content: line.as_ref().to_owned(),
+ bounds: viewport.logical_size(),
+ 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,
+ };
+
+ renderer.fill_text(
+ text.clone(),
+ Point::new(11.0, 11.0 + 25.0 * i as f32),
+ Color::new(0.9, 0.9, 0.9, 1.0),
+ Rectangle::with_size(Size::INFINITY),
+ );
+
+ renderer.fill_text(
+ text,
+ Point::new(11.0, 11.0 + 25.0 * i as f32)
+ + Vector::new(-1.0, -1.0),
+ Color::BLACK,
+ Rectangle::with_size(Size::INFINITY),
+ );
+ }
+ },
+ );
+ }
+}
+
+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;
+//! Draw custom primitives.
+use crate::core::{self, Rectangle};
+use crate::graphics::Viewport;
-pub use pipeline::Pipeline;
+use rustc_hash::FxHashMap;
+use std::any::{Any, TypeId};
+use std::fmt::Debug;
-use crate::core::Rectangle;
-use crate::graphics::{Damage, Mesh};
+/// A batch of primitives.
+pub type Batch = Vec<Instance>;
-use std::fmt::Debug;
+/// 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,
+ );
-/// The graphical primitives supported by `iced_wgpu`.
-pub type Primitive = crate::graphics::Primitive<Custom>;
+ /// Renders the [`Primitive`].
+ fn render(
+ &self,
+ encoder: &mut wgpu::CommandEncoder,
+ storage: &Storage,
+ target: &wgpu::TextureView,
+ clip_bounds: &Rectangle<u32>,
+ );
+}
-/// 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),
+#[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 Damage for Custom {
- fn bounds(&self) -> Rectangle {
- match self {
- Self::Mesh(mesh) => mesh.bounds(),
- Self::Pipeline(pipeline) => pipeline.bounds,
+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..0eeb100f 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 = round(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/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,296 +1,389 @@
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;
-#[allow(missing_debug_implementations)]
-pub struct Pipeline {
- renderers: Vec<glyphon::TextRenderer>,
- atlas: glyphon::TextAtlas,
- prepare_layer: usize,
- cache: RefCell<Cache>,
+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,
+ },
}
-impl Pipeline {
- pub fn new(
- device: &wgpu::Device,
- queue: &wgpu::Queue,
- format: wgpu::TextureFormat,
- ) -> Self {
- Pipeline {
- renderers: Vec::new(),
- atlas: glyphon::TextAtlas::with_color_mode(
- device,
- queue,
- format,
- if color::GAMMA_CORRECTION {
- glyphon::ColorMode::Accurate
- } else {
- glyphon::ColorMode::Web
- },
- ),
- prepare_layer: 0,
- cache: RefCell::new(Cache::new()),
+#[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()
}
- pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
- font_system()
- .write()
- .expect("Write font system")
- .load_font(bytes);
+ fn get(&self, cache: &Cache) -> Option<(&glyphon::TextAtlas, &Upload)> {
+ if cache.text.is_empty() {
+ return None;
+ }
- self.cache = RefCell::new(Cache::new());
+ self.groups
+ .get(&cache.group)
+ .map(|group| &group.atlas)
+ .zip(self.uploads.get(&cache.id))
}
- pub fn prepare(
+ fn prepare(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
- sections: &[Text<'_>],
- layer_bounds: Rectangle,
- scale_factor: f32,
- target_size: Size<u32>,
+ viewport: &glyphon::Viewport,
+ encoder: &mut wgpu::CommandEncoder,
+ format: wgpu::TextureFormat,
+ state: &glyphon::Cache,
+ cache: &Cache,
+ new_transformation: Transformation,
+ bounds: Rectangle,
) {
- 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 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,
+ );
+ }
- let renderer = &mut self.renderers[self.prepare_layer];
- let cache = self.cache.get_mut();
+ // Only trim if glyphs have changed
+ group.should_trim =
+ group.should_trim || upload.version != cache.version;
- enum Allocation {
- Paragraph(Paragraph),
- Editor(Editor),
- Cache(cache::KeyHash),
- Raw(Arc<glyphon::Buffer>),
- }
+ upload.text = Rc::downgrade(&cache.text);
+ upload.version = cache.version;
+ upload.group_version = group.version;
+ upload.transformation = new_transformation;
- let allocations: Vec<_> = sections
- .iter()
- .map(|section| match section {
- Text::Paragraph { paragraph, .. } => {
- paragraph.upgrade().map(Allocation::Paragraph)
+ upload.buffer_cache.trim();
}
- 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,
- },
+ }
+ 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,
);
-
- Some(Allocation::Cache(key))
- }
- Text::Raw { raw, .. } => {
- raw.buffer.upgrade().map(Allocation::Raw)
}
- })
- .collect();
- let layer_bounds = layer_bounds * scale_factor;
+ 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()
+ );
+ }
+ }
+ }
- 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(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,
- )
- }
- };
+ pub fn trim(&mut self) {
+ self.uploads
+ .retain(|_id, upload| upload.text.strong_count() > 0);
- let bounds = bounds * transformation * scale_factor;
+ self.groups.retain(|id, group| {
+ let active_uploads = Rc::weak_count(&group.handle);
- 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,
- };
+ if active_uploads == 0 {
+ log::debug!("Dropping text atlas: {id:?}");
- 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),
- })
- },
- );
+ 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);
- let result = renderer.prepare(
- device,
+impl Viewport {
+ pub fn update(&mut self, queue: &wgpu::Queue, resolution: Size<u32>) {
+ self.0.update(
queue,
- font_system,
- &mut self.atlas,
glyphon::Resolution {
- width: target_size.width,
- height: target_size.height,
+ width: resolution.width,
+ height: resolution.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...
+#[allow(missing_debug_implementations)]
+pub struct Pipeline {
+ state: glyphon::Cache,
+ format: wgpu::TextureFormat,
+ atlas: glyphon::TextAtlas,
+ renderers: Vec<glyphon::TextRenderer>,
+ prepare_layer: usize,
+ cache: BufferCache,
+}
+
+impl Pipeline {
+ pub fn new(
+ device: &wgpu::Device,
+ 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,
+ prepare_layer: 0,
+ cache: BufferCache::new(),
+ }
+ }
+
+ pub fn prepare(
+ &mut self,
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ viewport: &Viewport,
+ encoder: &mut wgpu::CommandEncoder,
+ storage: &mut Storage,
+ batch: &Batch,
+ layer_bounds: Rectangle,
+ layer_transformation: Transformation,
+ ) {
+ 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 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,
+ );
+
+ 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...
+ }
+ }
+ }
+ Item::Cached {
+ transformation,
+ 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 328ad781..2e938c77 100644
--- a/wgpu/src/window/compositor.rs
+++ b/wgpu/src/window/compositor.rs
@@ -1,23 +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 std::future::Future;
+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 {
@@ -27,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()
});
@@ -38,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();
@@ -49,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();
@@ -92,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:?}"
@@ -111,70 +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();
-
- if let Some(device) = device {
- break Some(device);
+ )
+ .await;
+
+ 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,
+ });
}
- }?;
-
- Some(Compositor {
- instance,
- settings,
- adapter,
- device,
- queue,
- format,
- alpha_mode,
- })
- }
+ Err(error) => {
+ errors.push((required_limits, error));
+ }
+ }
+ }
- /// 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.
+/// Creates a [`Compositor`] with the given [`Settings`] and window.
pub async fn new<W: compositor::Window>(
settings: Settings,
compatible_window: W,
) -> Result<Compositor, Error> {
- Compositor::request(settings, Some(compatible_window))
- .await
- .ok_or(Error::GraphicsAdapterNotFound)
+ 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<T: AsRef<str>>(
compositor: &mut Compositor,
- backend: &mut Backend,
+ renderer: &mut Renderer,
surface: &mut wgpu::Surface<'static>,
- primitives: &[Primitive],
viewport: &Viewport,
background_color: Color,
overlay: &[T],
@@ -191,20 +227,21 @@ pub fn present<T: AsRef<str>>(
.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,
overlay,
);
- // 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 +262,41 @@ pub fn present<T: AsRef<str>>(
}
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,
- ) -> impl Future<Output = 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 +336,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,
},
);
}
@@ -300,17 +358,7 @@ impl graphics::Compositor for Compositor {
background_color: Color,
overlay: &[T],
) -> Result<(), compositor::SurfaceError> {
- renderer.with_primitives(|backend, primitives| {
- present(
- self,
- backend,
- surface,
- primitives,
- viewport,
- background_color,
- overlay,
- )
- })
+ present(self, renderer, surface, viewport, background_color, overlay)
}
fn screenshot<T: AsRef<str>>(
@@ -321,16 +369,7 @@ impl graphics::Compositor for Compositor {
background_color: Color,
overlay: &[T],
) -> Vec<u8> {
- renderer.with_primitives(|backend, primitives| {
- screenshot(
- self,
- backend,
- primitives,
- viewport,
- background_color,
- overlay,
- )
- })
+ screenshot(self, renderer, viewport, background_color, overlay)
}
}
@@ -338,19 +377,12 @@ impl graphics::Compositor for Compositor {
///
/// Returns RGBA bytes of the texture data.
pub fn screenshot<T: AsRef<str>>(
- compositor: &Compositor,
- backend: &mut Backend,
- primitives: &[Primitive],
+ compositor: &mut Compositor,
+ renderer: &mut Renderer,
viewport: &Viewport,
background_color: Color,
overlay: &[T],
) -> 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 {
@@ -374,14 +406,20 @@ pub fn screenshot<T: AsRef<str>>(
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,
overlay,
);
@@ -419,7 +457,7 @@ pub fn screenshot<T: AsRef<str>>(
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 3c9ffddb..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,12 +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
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 5790f811..dc949671 100644
--- a/widget/src/button.rs
+++ b/widget/src/button.rs
@@ -49,6 +49,7 @@ use crate::core::{
pub struct Button<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
where
Renderer: crate::core::Renderer,
+ Theme: Catalog,
{
content: Element<'a, Message, Theme, Renderer>,
on_press: Option<Message>,
@@ -56,20 +57,18 @@ where
height: Length,
padding: Padding,
clip: bool,
- style: Style<'a, Theme>,
+ class: Theme::Class<'a>,
}
impl<'a, Message, Theme, Renderer> Button<'a, Message, Theme, Renderer>
where
Renderer: crate::core::Renderer,
+ Theme: Catalog,
{
/// Creates a new [`Button`] with the given content.
pub fn new(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
- ) -> Self
- where
- Theme: DefaultStyle + 'a,
- {
+ ) -> Self {
let content = content.into();
let size = content.as_widget().size_hint();
@@ -80,7 +79,7 @@ where
height: size.height.fluid(),
padding: DEFAULT_PADDING,
clip: false,
- style: Box::new(Theme::default_style),
+ class: Theme::default(),
}
}
@@ -119,21 +118,30 @@ where
self
}
- /// Sets the style variant of this [`Button`].
- pub fn style(
- mut self,
- style: impl Fn(&Theme, Status) -> Appearance + 'a,
- ) -> Self {
- self.style = Box::new(style);
- self
- }
-
/// Sets whether the contents of the [`Button`] should be clipped on
/// overflow.
pub fn clip(mut self, clip: bool) -> Self {
self.clip = clip;
self
}
+
+ /// 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)]
@@ -146,6 +154,7 @@ impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
where
Message: 'a + Clone,
Renderer: 'a + crate::core::Renderer,
+ Theme: Catalog,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
@@ -304,19 +313,19 @@ where
Status::Active
};
- let styling = (self.style)(theme, status);
+ let style = theme.style(&self.class, status);
- if styling.background.is_some()
- || styling.border.width > 0.0
- || styling.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: styling.border,
- shadow: styling.shadow,
+ border: style.border,
+ shadow: style.shadow,
},
- styling
+ style
.background
.unwrap_or(Background::Color(Color::TRANSPARENT)),
);
@@ -333,7 +342,7 @@ where
renderer,
theme,
&renderer::Style {
- text_color: styling.text_color,
+ text_color: style.text_color,
},
content_layout,
cursor,
@@ -378,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: 'a,
+ Theme: Catalog + 'a,
Renderer: crate::core::Renderer + 'a,
{
fn from(button: Button<'a, Message, Theme, Renderer>) -> Self {
@@ -407,9 +416,9 @@ pub enum Status {
Disabled,
}
-/// The appearance of a button.
+/// The style of a button.
#[derive(Debug, Clone, Copy, PartialEq)]
-pub struct Appearance {
+pub struct Style {
/// The [`Background`] of the button.
pub background: Option<Background>,
/// The text [`Color`] of the button.
@@ -420,8 +429,8 @@ pub struct Appearance {
pub shadow: Shadow,
}
-impl Appearance {
- /// Updates the [`Appearance`] with the given [`Background`].
+impl Style {
+ /// Updates the [`Style`] with the given [`Background`].
pub fn with_background(self, background: impl Into<Background>) -> Self {
Self {
background: Some(background.into()),
@@ -430,7 +439,7 @@ impl Appearance {
}
}
-impl std::default::Default for Appearance {
+impl Default for Style {
fn default() -> Self {
Self {
background: None,
@@ -441,41 +450,41 @@ impl std::default::Default for Appearance {
}
}
-/// The style of a [`Button`].
-pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>;
+/// The theme catalog of a [`Button`].
+pub trait Catalog {
+ /// The item class of the [`Catalog`].
+ type Class<'a>;
-/// The default style of a [`Button`].
-pub trait DefaultStyle {
- /// Returns the default style of a [`Button`].
- fn default_style(&self, status: Status) -> Appearance;
-}
+ /// The default class produced by the [`Catalog`].
+ fn default<'a>() -> Self::Class<'a>;
-impl DefaultStyle for Theme {
- fn default_style(&self, status: Status) -> Appearance {
- primary(self, status)
- }
+ /// The [`Style`] of a class with the given status.
+ fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
}
-impl DefaultStyle for Appearance {
- fn default_style(&self, _status: Status) -> Appearance {
- *self
+/// 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)
}
-}
-impl DefaultStyle for Color {
- fn default_style(&self, _status: Status) -> Appearance {
- Appearance::default().with_background(*self)
+ 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) -> Appearance {
+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 => Appearance {
+ Status::Hovered => Style {
background: Some(Background::Color(palette.primary.base.color)),
..base
},
@@ -484,13 +493,13 @@ pub fn primary(theme: &Theme, status: Status) -> Appearance {
}
/// A secondary button; denoting a complementary action.
-pub fn secondary(theme: &Theme, status: Status) -> Appearance {
+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 => Appearance {
+ Status::Hovered => Style {
background: Some(Background::Color(palette.secondary.strong.color)),
..base
},
@@ -499,13 +508,13 @@ pub fn secondary(theme: &Theme, status: Status) -> Appearance {
}
/// A success button; denoting a good outcome.
-pub fn success(theme: &Theme, status: Status) -> Appearance {
+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 => Appearance {
+ Status::Hovered => Style {
background: Some(Background::Color(palette.success.strong.color)),
..base
},
@@ -514,13 +523,13 @@ pub fn success(theme: &Theme, status: Status) -> Appearance {
}
/// A danger button; denoting a destructive action.
-pub fn danger(theme: &Theme, status: Status) -> Appearance {
+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 => Appearance {
+ Status::Hovered => Style {
background: Some(Background::Color(palette.danger.strong.color)),
..base
},
@@ -529,17 +538,17 @@ pub fn danger(theme: &Theme, status: Status) -> Appearance {
}
/// A text button; useful for links.
-pub fn text(theme: &Theme, status: Status) -> Appearance {
+pub fn text(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
- let base = Appearance {
+ let base = Style {
text_color: palette.background.base.text,
- ..Appearance::default()
+ ..Style::default()
};
match status {
Status::Active | Status::Pressed => base,
- Status::Hovered => Appearance {
+ Status::Hovered => Style {
text_color: palette.background.base.text.scale_alpha(0.8),
..base
},
@@ -547,21 +556,21 @@ pub fn text(theme: &Theme, status: Status) -> Appearance {
}
}
-fn styled(pair: palette::Pair) -> Appearance {
- Appearance {
+fn styled(pair: palette::Pair) -> Style {
+ Style {
background: Some(Background::Color(pair.color)),
text_color: pair.text,
border: Border::rounded(2),
- ..Appearance::default()
+ ..Style::default()
}
}
-fn disabled(appearance: Appearance) -> Appearance {
- Appearance {
- background: appearance
+fn disabled(style: Style) -> Style {
+ Style {
+ background: style
.background
.map(|background| background.scale_alpha(0.5)),
- text_color: appearance.text_color.scale_alpha(0.5),
- ..appearance
+ 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 15fb8f58..225c316d 100644
--- a/widget/src/checkbox.rs
+++ b/widget/src/checkbox.rs
@@ -39,6 +39,7 @@ pub struct Checkbox<
Renderer = crate::Renderer,
> where
Renderer: text::Renderer,
+ Theme: Catalog,
{
is_checked: bool,
on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
@@ -51,12 +52,13 @@ pub struct Checkbox<
text_shaping: text::Shaping,
font: Option<Renderer::Font>,
icon: Icon<Renderer::Font>,
- style: Style<'a, Theme>,
+ class: Theme::Class<'a>,
}
impl<'a, Message, Theme, Renderer> Checkbox<'a, Message, Theme, Renderer>
where
Renderer: text::Renderer,
+ Theme: Catalog,
{
/// The default size of a [`Checkbox`].
const DEFAULT_SIZE: f32 = 16.0;
@@ -69,10 +71,7 @@ where
/// It expects:
/// * the label of the [`Checkbox`]
/// * a boolean describing whether the [`Checkbox`] is checked or not
- pub fn new(label: impl Into<String>, is_checked: bool) -> Self
- where
- Theme: DefaultStyle + 'a,
- {
+ pub fn new(label: impl Into<String>, is_checked: bool) -> Self {
Checkbox {
is_checked,
on_toggle: None,
@@ -91,7 +90,7 @@ where
line_height: text::LineHeight::default(),
shaping: text::Shaping::Basic,
},
- style: Box::new(Theme::default_style),
+ class: Theme::default(),
}
}
@@ -174,11 +173,20 @@ where
}
/// Sets the style of the [`Checkbox`].
- pub fn style(
- mut self,
- style: impl Fn(&Theme, Status) -> Appearance + 'a,
- ) -> Self {
- self.style = Box::new(style);
+ #[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
}
}
@@ -187,6 +195,7 @@ impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Checkbox<'a, Message, Theme, Renderer>
where
Renderer: text::Renderer,
+ Theme: Catalog,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<widget::text::State<Renderer::Paragraph>>()
@@ -285,7 +294,7 @@ where
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
- style: &renderer::Style,
+ defaults: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
@@ -304,7 +313,7 @@ where
Status::Active { is_checked }
};
- let appearance = (self.style)(theme, status);
+ let style = theme.style(&self.class, status);
{
let layout = children.next().unwrap();
@@ -313,10 +322,10 @@ where
renderer.fill_quad(
renderer::Quad {
bounds,
- border: appearance.border,
+ border: style.border,
..renderer::Quad::default()
},
- appearance.background,
+ style.background,
);
let Icon {
@@ -331,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,
@@ -341,7 +350,7 @@ where
shaping: *shaping,
},
bounds.center(),
- appearance.icon_color,
+ style.icon_color,
*viewport,
);
}
@@ -352,11 +361,11 @@ where
crate::text::draw(
renderer,
- style,
+ defaults,
label_layout,
tree.state.downcast_ref(),
- crate::text::Appearance {
- color: appearance.text_color,
+ crate::text::Style {
+ color: style.text_color,
},
viewport,
);
@@ -368,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,
+ Theme: 'a + Catalog,
Renderer: 'a + text::Renderer,
{
fn from(
@@ -413,9 +422,9 @@ pub enum Status {
},
}
-/// The appearance of a checkbox.
+/// The style of a checkbox.
#[derive(Debug, Clone, Copy)]
-pub struct Appearance {
+pub struct Style {
/// The [`Background`] of the checkbox.
pub background: Background,
/// The icon [`Color`] of the checkbox.
@@ -426,29 +435,37 @@ pub struct Appearance {
pub text_color: Option<Color>,
}
-/// The style of a [`Checkbox`].
-pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>;
+/// The theme catalog of a [`Checkbox`].
+pub trait Catalog: Sized {
+ /// The item class of the [`Catalog`].
+ type Class<'a>;
-/// The default style of a [`Checkbox`].
-pub trait DefaultStyle {
- /// Returns the default style of a [`Checkbox`].
- fn default_style(&self, status: Status) -> Appearance;
+ /// 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 DefaultStyle for Theme {
- fn default_style(&self, status: Status) -> Appearance {
- primary(self, status)
+/// 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)
}
-}
-impl DefaultStyle for Appearance {
- fn default_style(&self, _status: Status) -> Appearance {
- *self
+ 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) -> Appearance {
+pub fn primary(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
match status {
@@ -474,7 +491,7 @@ pub fn primary(theme: &Theme, status: Status) -> Appearance {
}
/// A secondary checkbox; denoting a complementary toggle.
-pub fn secondary(theme: &Theme, status: Status) -> Appearance {
+pub fn secondary(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
match status {
@@ -500,7 +517,7 @@ pub fn secondary(theme: &Theme, status: Status) -> Appearance {
}
/// A success checkbox; denoting a positive toggle.
-pub fn success(theme: &Theme, status: Status) -> Appearance {
+pub fn success(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
match status {
@@ -526,7 +543,7 @@ pub fn success(theme: &Theme, status: Status) -> Appearance {
}
/// A danger checkbox; denoting a negaive toggle.
-pub fn danger(theme: &Theme, status: Status) -> Appearance {
+pub fn danger(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
match status {
@@ -556,8 +573,8 @@ fn styled(
base: palette::Pair,
accent: palette::Pair,
is_checked: bool,
-) -> Appearance {
- Appearance {
+) -> Style {
+ Style {
background: Background::Color(if is_checked {
accent.color
} else {
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 ee24d742..253850df 100644
--- a/widget/src/combo_box.rs
+++ b/widget/src/combo_box.rs
@@ -32,6 +32,7 @@ pub struct ComboBox<
Theme = crate::Theme,
Renderer = crate::Renderer,
> where
+ Theme: Catalog,
Renderer: text::Renderer,
{
state: &'a State<T>,
@@ -42,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: menu::Style<'a, Theme>,
+ menu_class: <Theme as menu::Catalog>::Class<'a>,
padding: Padding,
size: Option<f32>,
}
@@ -50,6 +51,7 @@ pub struct ComboBox<
impl<'a, T, Message, Theme, Renderer> ComboBox<'a, T, Message, Theme, Renderer>
where
T: std::fmt::Display + Clone,
+ Theme: Catalog,
Renderer: text::Renderer,
{
/// Creates a new [`ComboBox`] with the given list of options, a placeholder,
@@ -60,18 +62,10 @@ where
placeholder: &str,
selection: Option<&T>,
on_selected: impl Fn(T) -> Message + 'static,
- ) -> Self
- where
- Theme: DefaultStyle + 'a,
- {
- let style = Theme::default_style();
-
- let text_input = TextInput::with_style(
- placeholder,
- &state.value(),
- style.text_input,
- )
- .on_input(TextInputEvent::TextChanged);
+ ) -> Self {
+ let text_input = TextInput::new(placeholder, &state.value())
+ .on_input(TextInputEvent::TextChanged)
+ .class(Theme::default_input());
let selection = selection.map(T::to_string).unwrap_or_default();
@@ -84,7 +78,7 @@ where
on_option_hovered: None,
on_input: None,
on_close: None,
- menu_style: style.menu,
+ menu_class: <Theme as Catalog>::default_menu(),
padding: text_input::DEFAULT_PADDING,
size: None,
}
@@ -124,18 +118,6 @@ where
self
}
- /// Sets the style of the [`ComboBox`].
- pub fn style(mut self, style: impl Into<Style<'a, Theme>>) -> Self
- where
- Theme: 'a,
- {
- let style = style.into();
-
- self.text_input = self.text_input.style(style.text_input);
- self.menu_style = style.menu;
- self
- }
-
/// Sets the [`Renderer::Font`] of the [`ComboBox`].
///
/// [`Renderer::Font`]: text::Renderer
@@ -173,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`].
@@ -296,6 +327,7 @@ impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
where
T: Display + Clone + 'static,
Message: Clone,
+ Theme: Catalog,
Renderer: text::Renderer,
{
fn size(&self) -> Size<Length> {
@@ -668,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();
-
- (self.on_selected)(x)
- },
- self.on_option_hovered.as_deref(),
- &self.menu_style,
- )
- .width(bounds.width)
- .padding(self.padding);
-
- if let Some(font) = self.font {
- menu = menu.font(font);
- }
+ if filtered_options.options.is_empty() {
+ None
+ } else {
+ let bounds = layout.bounds();
+
+ 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();
+
+ (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);
- }
+ if let Some(size) = self.size {
+ menu = menu.text_size(size);
+ }
- Some(menu.overlay(layout.position() + translation, bounds.height))
+ Some(
+ menu.overlay(
+ layout.position() + translation,
+ bounds.height,
+ ),
+ )
+ }
} else {
None
}
@@ -712,7 +753,7 @@ impl<'a, T, Message, Theme, Renderer>
where
T: Display + Clone + 'static,
Message: Clone + 'a,
- Theme: 'a,
+ Theme: Catalog + 'a,
Renderer: text::Renderer + 'a,
{
fn from(combo_box: ComboBox<'a, T, Message, Theme, Renderer>) -> Self {
@@ -720,6 +761,21 @@ where
}
}
+/// 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,
@@ -762,30 +818,3 @@ where
})
.collect()
}
-
-/// The style of a [`ComboBox`].
-#[allow(missing_debug_implementations)]
-pub struct Style<'a, Theme> {
- /// The style of the [`TextInput`] of the [`ComboBox`].
- pub text_input: text_input::Style<'a, Theme>,
-
- /// The style of the [`Menu`] of the [`ComboBox`].
- ///
- /// [`Menu`]: menu::Menu
- pub menu: menu::Style<'a, Theme>,
-}
-
-/// The default style of a [`ComboBox`].
-pub trait DefaultStyle: Sized {
- /// Returns the default style of a [`ComboBox`].
- fn default_style() -> Style<'static, Self>;
-}
-
-impl DefaultStyle for Theme {
- fn default_style() -> Style<'static, Self> {
- Style {
- text_input: Box::new(text_input::default),
- menu: menu::DefaultStyle::default_style(),
- }
- }
-}
diff --git a/widget/src/container.rs b/widget/src/container.rs
index 7c133588..51967707 100644
--- a/widget/src/container.rs
+++ b/widget/src/container.rs
@@ -9,8 +9,9 @@ use crate::core::renderer;
use crate::core::widget::tree::{self, Tree};
use crate::core::widget::{self, Operation};
use crate::core::{
- Background, Border, Clipboard, Color, Element, Layout, Length, Padding,
- Pixels, Point, Rectangle, Shadow, Shell, Size, Theme, Vector, Widget,
+ self, Background, Border, Clipboard, Color, Element, Layout, Length,
+ Padding, Pixels, Point, Rectangle, Shadow, Shell, Size, Theme, Vector,
+ Widget,
};
use crate::runtime::Command;
@@ -24,7 +25,8 @@ pub struct Container<
Theme = crate::Theme,
Renderer = crate::Renderer,
> where
- Renderer: crate::core::Renderer,
+ Theme: Catalog,
+ Renderer: core::Renderer,
{
id: Option<Id>,
padding: Padding,
@@ -36,27 +38,17 @@ pub struct Container<
vertical_alignment: alignment::Vertical,
clip: bool,
content: Element<'a, Message, Theme, Renderer>,
- style: Style<'a, Theme>,
+ class: Theme::Class<'a>,
}
impl<'a, Message, Theme, Renderer> Container<'a, Message, Theme, Renderer>
where
- Renderer: crate::core::Renderer,
+ Theme: Catalog,
+ Renderer: core::Renderer,
{
/// Creates a [`Container`] with the given content.
pub fn new(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
- ) -> Self
- where
- Theme: DefaultStyle + 'a,
- {
- Self::with_style(content, Theme::default_style)
- }
-
- /// Creates a [`Container`] with the given content and style.
- pub fn with_style(
- content: impl Into<Element<'a, Message, Theme, Renderer>>,
- style: impl Fn(&Theme, Status) -> Appearance + 'a,
) -> Self {
let content = content.into();
let size = content.as_widget().size_hint();
@@ -71,7 +63,7 @@ where
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
clip: false,
- style: Box::new(style),
+ class: Theme::default(),
content,
}
}
@@ -100,6 +92,46 @@ 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.
+ ///
+ /// 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;
@@ -124,25 +156,27 @@ where
self
}
- /// Centers the contents in the horizontal axis of the [`Container`].
- pub fn center_x(mut self) -> Self {
- self.horizontal_alignment = alignment::Horizontal::Center;
- self
+ /// Sets the width of the [`Container`] and centers its contents horizontally.
+ pub fn center_x(self, width: impl Into<Length>) -> Self {
+ self.width(width).align_x(alignment::Horizontal::Center)
}
- /// Centers the contents in the vertical axis of the [`Container`].
- pub fn center_y(mut self) -> Self {
- self.vertical_alignment = alignment::Vertical::Center;
- self
+ /// Sets the height of the [`Container`] and centers its contents vertically.
+ pub fn center_y(self, height: impl Into<Length>) -> Self {
+ self.height(height).align_y(alignment::Vertical::Center)
}
- /// Sets the style of the [`Container`].
- pub fn style(
- mut self,
- style: impl Fn(&Theme, Status) -> Appearance + 'a,
- ) -> Self {
- self.style = Box::new(style);
- 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, length: impl Into<Length>) -> Self {
+ let length = length.into();
+
+ self.center_x(length).center_y(length)
}
/// Sets whether the contents of the [`Container`] should be clipped on
@@ -151,12 +185,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
- Renderer: crate::core::Renderer,
+ Theme: Catalog,
+ Renderer: core::Renderer,
{
fn tag(&self) -> tree::Tag {
self.content.as_widget().tag()
@@ -272,14 +325,7 @@ where
viewport: &Rectangle,
) {
let bounds = layout.bounds();
-
- let status = if cursor.is_over(bounds) {
- Status::Hovered
- } else {
- Status::Idle
- };
-
- let style = (self.style)(theme, status);
+ let style = theme.style(&self.class);
if let Some(clipped_viewport) = bounds.intersection(viewport) {
draw_background(renderer, &style, bounds);
@@ -324,8 +370,8 @@ impl<'a, Message, Theme, Renderer> From<Container<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
- Theme: 'a,
- Renderer: 'a + crate::core::Renderer,
+ Theme: Catalog + 'a,
+ Renderer: core::Renderer + 'a,
{
fn from(
column: Container<'a, Message, Theme, Renderer>,
@@ -362,25 +408,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)),
);
@@ -502,7 +548,7 @@ pub fn visible_bounds(id: Id) -> Command<Option<Rectangle>> {
/// The appearance of a container.
#[derive(Debug, Clone, Copy, Default)]
-pub struct Appearance {
+pub struct Style {
/// The text [`Color`] of the container.
pub text_color: Option<Color>,
/// The [`Background`] of the container.
@@ -513,8 +559,8 @@ pub struct Appearance {
pub shadow: Shadow,
}
-impl Appearance {
- /// Updates the border of the [`Appearance`] with the given [`Color`] and `width`.
+impl Style {
+ /// Updates the border of the [`Style`] with the given [`Color`] and `width`.
pub fn with_border(
self,
color: impl Into<Color>,
@@ -530,7 +576,7 @@ impl Appearance {
}
}
- /// Updates the background of the [`Appearance`].
+ /// Updates the background of the [`Style`].
pub fn with_background(self, background: impl Into<Background>) -> Self {
Self {
background: Some(background.into()),
@@ -539,99 +585,78 @@ impl Appearance {
}
}
-impl From<Color> for Appearance {
+impl From<Color> for Style {
fn from(color: Color) -> Self {
Self::default().with_background(color)
}
}
-impl From<Gradient> for Appearance {
+impl From<Gradient> for Style {
fn from(gradient: Gradient) -> Self {
Self::default().with_background(gradient)
}
}
-impl From<gradient::Linear> for Appearance {
+impl From<gradient::Linear> for Style {
fn from(gradient: gradient::Linear) -> Self {
Self::default().with_background(gradient)
}
}
-/// The possible status of a [`Container`].
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum Status {
- /// The [`Container`] is idle.
- Idle,
- /// The [`Container`] is being hovered.
- Hovered,
-}
+/// The theme catalog of a [`Container`].
+pub trait Catalog {
+ /// The item class of the [`Catalog`].
+ type Class<'a>;
-/// The style of a [`Container`].
-pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>;
+ /// The default class produced by the [`Catalog`].
+ fn default<'a>() -> Self::Class<'a>;
-/// The default style of a [`Container`].
-pub trait DefaultStyle {
- /// Returns the default style of a [`Container`].
- fn default_style(&self, status: Status) -> Appearance;
+ /// The [`Style`] of a class with the given status.
+ fn style(&self, class: &Self::Class<'_>) -> Style;
}
-impl DefaultStyle for Theme {
- fn default_style(&self, status: Status) -> Appearance {
- transparent(self, status)
- }
-}
+/// A styling function for a [`Container`].
+pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
-impl DefaultStyle for Appearance {
- fn default_style(&self, _status: Status) -> Appearance {
- *self
- }
-}
+impl Catalog for Theme {
+ type Class<'a> = StyleFn<'a, Self>;
-impl DefaultStyle for Color {
- fn default_style(&self, _status: Status) -> Appearance {
- Appearance::from(*self)
+ fn default<'a>() -> Self::Class<'a> {
+ Box::new(transparent)
}
-}
-
-impl DefaultStyle for Gradient {
- fn default_style(&self, _status: Status) -> Appearance {
- Appearance::from(*self)
- }
-}
-impl DefaultStyle for gradient::Linear {
- fn default_style(&self, _status: Status) -> Appearance {
- Appearance::from(*self)
+ fn style(&self, class: &Self::Class<'_>) -> Style {
+ class(self)
}
}
/// A transparent [`Container`].
-pub fn transparent<Theme>(_theme: &Theme, _status: Status) -> Appearance {
- Appearance::default()
+pub fn transparent<Theme>(_theme: &Theme) -> Style {
+ Style::default()
}
/// A rounded [`Container`] with a background.
-pub fn rounded_box(theme: &Theme, _status: Status) -> Appearance {
+pub fn rounded_box(theme: &Theme) -> Style {
let palette = theme.extended_palette();
- Appearance {
+ Style {
background: Some(palette.background.weak.color.into()),
border: Border::rounded(2),
- ..Appearance::default()
+ ..Style::default()
}
}
/// A bordered [`Container`] with a background.
-pub fn bordered_box(theme: &Theme, _status: Status) -> Appearance {
+pub fn bordered_box(theme: &Theme) -> Style {
let palette = theme.extended_palette();
- Appearance {
+ Style {
background: Some(palette.background.weak.color.into()),
border: Border {
width: 1.0,
radius: 0.0.into(),
color: palette.background.strong.color,
},
- ..Appearance::default()
+ ..Style::default()
}
}
diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs
index b294a1d4..016bafbb 100644
--- a/widget/src/helpers.rs
+++ b/widget/src/helpers.rs
@@ -5,8 +5,9 @@ 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::pick_list::{self, PickList};
use crate::progress_bar::{self, ProgressBar};
use crate::radio::{self, Radio};
@@ -20,7 +21,7 @@ use crate::text_input::{self, TextInput};
use crate::toggler::{self, Toggler};
use crate::tooltip::{self, Tooltip};
use crate::vertical_slider::{self, VerticalSlider};
-use crate::{Column, MouseArea, Row, Space, Themer};
+use crate::{Column, MouseArea, Row, Space, Stack, Themer};
use std::borrow::Borrow;
use std::ops::RangeInclusive;
@@ -51,6 +52,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 [`Text`] widget with the provided content.
///
/// [`Text`]: core::widget::Text
@@ -104,12 +118,34 @@ pub fn container<'a, Message, Theme, Renderer>(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Container<'a, Message, Theme, Renderer>
where
- Theme: container::DefaultStyle + 'a,
+ 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::core::Length;
+/// # use iced_widget::Container;
+/// # fn container<A>(x: A) -> Container<'static, ()> { unreachable!() }
+/// let centered = container("Centered!").center(Length::Fill);
+/// ```
+///
+/// [`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).center(Length::Fill)
+}
+
/// Creates a new [`Column`] with the given children.
pub fn column<'a, Message, Theme, Renderer>(
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
@@ -143,6 +179,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
@@ -150,7 +608,7 @@ pub fn scrollable<'a, Message, Theme, Renderer>(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Scrollable<'a, Message, Theme, Renderer>
where
- Theme: scrollable::DefaultStyle + 'a,
+ Theme: scrollable::Catalog + 'a,
Renderer: core::Renderer,
{
Scrollable::new(content)
@@ -163,7 +621,7 @@ pub fn button<'a, Message, Theme, Renderer>(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Button<'a, Message, Theme, Renderer>
where
- Theme: button::DefaultStyle + 'a,
+ Theme: button::Catalog + 'a,
Renderer: core::Renderer,
{
Button::new(content)
@@ -180,7 +638,7 @@ pub fn tooltip<'a, Message, Theme, Renderer>(
position: tooltip::Position,
) -> crate::Tooltip<'a, Message, Theme, Renderer>
where
- Theme: container::DefaultStyle + 'a,
+ Theme: container::Catalog + 'a,
Renderer: core::text::Renderer,
{
Tooltip::new(content, tooltip, position)
@@ -190,13 +648,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::Catalog + 'a,
+ Renderer: core::text::Renderer,
+{
+ 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::DefaultStyle + 'a,
+ Theme: text::Catalog + 'a,
Renderer: core::text::Renderer,
{
- Text::new(text.to_string())
+ Text::new(value.to_string())
}
/// Creates a new [`Checkbox`].
@@ -207,7 +678,7 @@ pub fn checkbox<'a, Message, Theme, Renderer>(
is_checked: bool,
) -> Checkbox<'a, Message, Theme, Renderer>
where
- Theme: checkbox::DefaultStyle + 'a,
+ Theme: checkbox::Catalog + 'a,
Renderer: core::text::Renderer,
{
Checkbox::new(label, is_checked)
@@ -224,7 +695,7 @@ pub fn radio<'a, Message, Theme, Renderer, V>(
) -> Radio<'a, Message, Theme, Renderer>
where
Message: Clone,
- Theme: radio::DefaultStyle + 'a,
+ Theme: radio::Catalog + 'a,
Renderer: core::text::Renderer,
V: Copy + Eq,
{
@@ -240,7 +711,7 @@ pub fn toggler<'a, Message, Theme, Renderer>(
f: impl Fn(bool) -> Message + 'a,
) -> Toggler<'a, Message, Theme, Renderer>
where
- Theme: toggler::DefaultStyle + 'a,
+ Theme: toggler::Catalog + 'a,
Renderer: core::text::Renderer,
{
Toggler::new(label, is_checked, f)
@@ -255,7 +726,7 @@ pub fn text_input<'a, Message, Theme, Renderer>(
) -> TextInput<'a, Message, Theme, Renderer>
where
Message: Clone,
- Theme: text_input::DefaultStyle + 'a,
+ Theme: text_input::Catalog + 'a,
Renderer: core::text::Renderer,
{
TextInput::new(placeholder, value)
@@ -269,7 +740,7 @@ pub fn text_editor<'a, Message, Theme, Renderer>(
) -> TextEditor<'a, core::text::highlighter::PlainText, Message, Theme, Renderer>
where
Message: Clone,
- Theme: text_editor::DefaultStyle + 'a,
+ Theme: text_editor::Catalog + 'a,
Renderer: core::text::Renderer,
{
TextEditor::new(content)
@@ -286,7 +757,7 @@ pub fn slider<'a, T, Message, Theme>(
where
T: Copy + From<u8> + std::cmp::PartialOrd,
Message: Clone,
- Theme: slider::DefaultStyle + 'a,
+ Theme: slider::Catalog + 'a,
{
Slider::new(range, value, on_change)
}
@@ -302,7 +773,7 @@ pub fn vertical_slider<'a, T, Message, Theme>(
where
T: Copy + From<u8> + std::cmp::PartialOrd,
Message: Clone,
- Theme: vertical_slider::DefaultStyle + 'a,
+ Theme: vertical_slider::Catalog + 'a,
{
VerticalSlider::new(range, value, on_change)
}
@@ -320,7 +791,7 @@ where
L: Borrow<[T]> + 'a,
V: Borrow<T> + 'a,
Message: Clone,
- Theme: pick_list::DefaultStyle,
+ Theme: pick_list::Catalog + overlay::menu::Catalog,
Renderer: core::text::Renderer,
{
PickList::new(options, selected, on_selected)
@@ -337,7 +808,7 @@ pub fn combo_box<'a, T, Message, Theme, Renderer>(
) -> ComboBox<'a, T, Message, Theme, Renderer>
where
T: std::fmt::Display + Clone,
- Theme: combo_box::DefaultStyle + 'a,
+ Theme: combo_box::Catalog + 'a,
Renderer: core::text::Renderer,
{
ComboBox::new(state, placeholder, selection, on_selected)
@@ -364,7 +835,7 @@ pub fn vertical_space() -> Space {
/// [`Rule`]: crate::Rule
pub fn horizontal_rule<'a, Theme>(height: impl Into<Pixels>) -> Rule<'a, Theme>
where
- Theme: rule::DefaultStyle + 'a,
+ Theme: rule::Catalog + 'a,
{
Rule::horizontal(height)
}
@@ -374,7 +845,7 @@ where
/// [`Rule`]: crate::Rule
pub fn vertical_rule<'a, Theme>(width: impl Into<Pixels>) -> Rule<'a, Theme>
where
- Theme: rule::DefaultStyle + 'a,
+ Theme: rule::Catalog + 'a,
{
Rule::vertical(width)
}
@@ -391,7 +862,7 @@ pub fn progress_bar<'a, Theme>(
value: f32,
) -> ProgressBar<'a, Theme>
where
- Theme: progress_bar::DefaultStyle + 'a,
+ Theme: progress_bar::Catalog + 'a,
{
ProgressBar::new(range, value)
}
@@ -413,7 +884,7 @@ pub fn svg<'a, Theme>(
handle: impl Into<core::svg::Handle>,
) -> crate::Svg<'a, Theme>
where
- Theme: crate::svg::DefaultStyle + 'a,
+ Theme: crate::svg::Catalog,
{
crate::Svg::new(handle)
}
@@ -441,7 +912,7 @@ pub fn qr_code<'a, Theme>(
data: &'a crate::qr_code::Data,
) -> crate::QRCode<'a, Theme>
where
- Theme: crate::qr_code::DefaultStyle + 'a,
+ Theme: crate::qr_code::Catalog + 'a,
{
crate::QRCode::new(data)
}
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 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 scale = Vector::new(
+ adjusted_fit.width / rotated_size.width,
+ adjusted_fit.height / rotated_size.height,
+ );
+
+ let final_size = image_size * scale;
- let drawing_bounds = Rectangle {
- width: adjusted_fit.width,
- height: adjusted_fit.height,
- ..bounds
- };
+ 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);
- renderer.draw(handle.clone(), filter_method, drawing_bounds + offset);
+ let render = |renderer: &mut Renderer| {
+ renderer.draw_image(
+ handle.clone(),
+ filter_method,
+ drawing_bounds,
+ rotation.radians(),
+ opacity,
+ );
};
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 2e3fd713..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> {
@@ -94,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>()
@@ -117,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,
@@ -218,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;
};
@@ -304,7 +302,7 @@ where
} else if is_mouse_over {
mouse::Interaction::Grab
} else {
- mouse::Interaction::Idle
+ mouse::Interaction::None
}
}
@@ -335,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 {
@@ -344,6 +341,8 @@ where
y: bounds.y,
..Rectangle::with_size(image_size)
},
+ Radians(0.0),
+ 1.0,
);
});
});
@@ -402,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)
@@ -421,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 209dfad9..00e9aaa4 100644
--- a/widget/src/lib.rs
+++ b/widget/src/lib.rs
@@ -2,13 +2,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 iced_renderer as renderer;
pub use iced_renderer::graphics;
@@ -19,6 +12,7 @@ mod column;
mod mouse_area;
mod row;
mod space;
+mod stack;
mod themer;
pub mod button;
@@ -85,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;
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 0364f980..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};
@@ -20,12 +19,15 @@ use crate::scrollable::{self, Scrollable};
#[allow(missing_debug_implementations)]
pub struct Menu<
'a,
+ 'b,
T,
Message,
Theme = crate::Theme,
Renderer = crate::Renderer,
> where
+ Theme: Catalog,
Renderer: text::Renderer,
+ 'b: 'a,
{
state: &'a mut State,
options: &'a [T],
@@ -38,15 +40,17 @@ pub struct Menu<
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
font: Option<Renderer::Font>,
- style: &'a Style<'a, Theme>,
+ 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: 'a,
+ Theme: Catalog + 'a,
Renderer: text::Renderer + 'a,
+ 'b: 'a,
{
/// Creates a new [`Menu`] with the given [`State`], a list of options,
/// the message to produced when an option is selected, and its [`Style`].
@@ -56,7 +60,7 @@ where
hovered_option: &'a mut Option<usize>,
on_selected: impl FnMut(T) -> Message + 'a,
on_option_hovered: Option<&'a dyn Fn(T) -> Message>,
- style: &'a Style<'a, Theme>,
+ class: &'a <Theme as Catalog>::Class<'b>,
) -> Self {
Menu {
state,
@@ -70,7 +74,7 @@ where
text_line_height: text::LineHeight::default(),
text_shaping: text::Shaping::Basic,
font: None,
- style,
+ class,
}
}
@@ -153,27 +157,29 @@ impl Default for State {
}
}
-struct Overlay<'a, Message, Theme, Renderer>
+struct Overlay<'a, 'b, Message, Theme, Renderer>
where
+ 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: &'a Style<'a, Theme>,
+ 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: '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
@@ -191,46 +197,43 @@ where
text_size,
text_line_height,
text_shaping,
- style,
+ class,
} = menu;
- let container = Container::with_style(
- Scrollable::with_direction_and_style(
- List {
- options,
- hovered_option,
- on_selected,
- on_option_hovered,
- font,
- text_size,
- text_line_height,
- text_shaping,
- padding,
- style: &style.list,
- },
- scrollable::Direction::default(),
- &style.scrollable,
- ),
- container::transparent,
+ 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: Catalog,
Renderer: text::Renderer,
{
fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
@@ -251,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 {
@@ -272,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,
)
@@ -285,7 +288,7 @@ where
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
- self.container
+ self.list
.mouse_interaction(self.state, layout, cursor, viewport, renderer)
}
@@ -293,30 +296,32 @@ where
&self,
renderer: &mut Renderer,
theme: &Theme,
- style: &renderer::Style,
+ defaults: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
) {
let bounds = layout.bounds();
- let appearance = (self.style.list)(theme);
+ 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: Catalog,
Renderer: text::Renderer,
{
options: &'a [T],
@@ -328,13 +333,14 @@ where
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
font: Option<Renderer::Font>,
- style: &'a dyn Fn(&Theme) -> Appearance,
+ 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: Catalog,
Renderer: text::Renderer,
{
fn size(&self) -> Size<Length> {
@@ -477,7 +483,7 @@ where
_cursor: mouse::Cursor,
viewport: &Rectangle,
) {
- let appearance = (self.style)(theme);
+ let style = Catalog::style(theme, self.class);
let bounds = layout.bounds();
let text_size =
@@ -507,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::rounded(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,
@@ -531,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,
);
@@ -541,23 +547,24 @@ 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: '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 Appearance {
+pub struct Style {
/// The [`Background`] of the menu.
pub background: Background,
/// The [`Border`] of the menu.
@@ -570,35 +577,43 @@ pub struct Appearance {
pub selected_background: Background,
}
-/// The style of the different parts of a [`Menu`].
-#[allow(missing_debug_implementations)]
-pub struct Style<'a, Theme> {
- /// The style of the list of the [`Menu`].
- pub list: Box<dyn Fn(&Theme) -> Appearance + 'a>,
- /// The style of the [`Scrollable`] of the [`Menu`].
- pub scrollable: scrollable::Style<'a, Theme>,
-}
+/// The 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 style of a [`Menu`].
-pub trait DefaultStyle: Sized {
- /// Returns the default style of a [`Menu`].
- fn default_style() -> Style<'static, Self>;
+ /// 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;
}
-impl DefaultStyle for Theme {
- fn default_style() -> Style<'static, Self> {
- Style {
- list: Box::new(default),
- scrollable: Box::new(scrollable::default),
- }
+/// 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) -> Appearance {
+pub fn default(theme: &Theme) -> Style {
let palette = theme.extended_palette();
- Appearance {
+ Style {
background: palette.background.weak.color.into(),
border: Border {
width: 1.0,
diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs
index beac0bd8..acfa9d44 100644
--- a/widget/src/pane_grid.rs
+++ b/widget/src/pane_grid.rs
@@ -30,6 +30,7 @@ pub use split::Split;
pub use state::State;
pub use title_bar::TitleBar;
+use crate::container;
use crate::core::event::{self, Event};
use crate::core::layout;
use crate::core::mouse;
@@ -39,8 +40,8 @@ use crate::core::touch;
use crate::core::widget;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
- Background, Border, Clipboard, Color, Element, Layout, Length, Pixels,
- Point, Rectangle, Shell, Size, Theme, 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;
@@ -101,7 +102,8 @@ pub struct PaneGrid<
Theme = crate::Theme,
Renderer = crate::Renderer,
> where
- Renderer: crate::core::Renderer,
+ Theme: Catalog,
+ Renderer: core::Renderer,
{
contents: Contents<'a, Content<'a, Message, Theme, Renderer>>,
width: Length,
@@ -110,12 +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: Style<'a, Theme>,
+ class: <Theme as Catalog>::Class<'a>,
}
impl<'a, Message, Theme, Renderer> PaneGrid<'a, Message, Theme, Renderer>
where
- Renderer: crate::core::Renderer,
+ Theme: Catalog,
+ Renderer: core::Renderer,
{
/// Creates a [`PaneGrid`] with the given [`State`] and view function.
///
@@ -124,10 +127,7 @@ where
pub fn new<T>(
state: &'a State<T>,
view: impl Fn(Pane, &'a T, bool) -> Content<'a, Message, Theme, Renderer>,
- ) -> Self
- where
- Theme: DefaultStyle + 'a,
- {
+ ) -> Self {
let contents = if let Some((pane, pane_state)) =
state.maximized.and_then(|pane| {
state.panes.get(&pane).map(|pane_state| (pane, pane_state))
@@ -158,7 +158,7 @@ where
on_click: None,
on_drag: None,
on_resize: None,
- style: Box::new(Theme::default_style),
+ class: <Theme as Catalog>::default(),
}
}
@@ -218,8 +218,23 @@ where
}
/// Sets the style of the [`PaneGrid`].
- pub fn style(mut self, style: impl Fn(&Theme) -> Appearance + 'a) -> Self {
- self.style = Box::new(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,
+ class: impl Into<<Theme as Catalog>::Class<'a>>,
+ ) -> Self {
+ self.class = class.into();
self
}
@@ -233,7 +248,8 @@ where
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for PaneGrid<'a, Message, Theme, Renderer>
where
- Renderer: crate::core::Renderer,
+ Theme: Catalog,
+ Renderer: core::Renderer,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<state::Action>()
@@ -596,7 +612,7 @@ where
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
- style: &renderer::Style,
+ defaults: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
@@ -677,7 +693,7 @@ where
None
};
- let appearance = (self.style)(theme);
+ let style = Catalog::style(theme, &self.class);
for ((id, (content, tree)), pane_layout) in
contents.zip(layout.children())
@@ -692,7 +708,7 @@ where
tree,
renderer,
theme,
- style,
+ defaults,
pane_layout,
pane_cursor,
viewport,
@@ -710,10 +726,10 @@ where
renderer.fill_quad(
renderer::Quad {
bounds,
- border: appearance.hovered_region.border,
+ border: style.hovered_region.border,
..renderer::Quad::default()
},
- appearance.hovered_region.background,
+ style.hovered_region.background,
);
}
}
@@ -723,7 +739,7 @@ where
tree,
renderer,
theme,
- style,
+ defaults,
pane_layout,
pane_cursor,
viewport,
@@ -738,10 +754,10 @@ where
renderer.fill_quad(
renderer::Quad {
bounds,
- border: appearance.hovered_region.border,
+ border: style.hovered_region.border,
..renderer::Quad::default()
},
- appearance.hovered_region.background,
+ style.hovered_region.background,
);
}
@@ -759,7 +775,7 @@ where
tree,
renderer,
theme,
- style,
+ defaults,
layout,
pane_cursor,
viewport,
@@ -772,9 +788,9 @@ where
if picked_pane.is_none() {
if let Some((axis, split_region, is_picked)) = picked_split {
let highlight = if is_picked {
- appearance.picked_split
+ style.picked_split
} else {
- appearance.hovered_split
+ style.hovered_split
};
renderer.fill_quad(
@@ -832,8 +848,8 @@ impl<'a, Message, Theme, Renderer> From<PaneGrid<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
- Theme: 'a,
- Renderer: crate::core::Renderer + 'a,
+ Theme: Catalog + 'a,
+ Renderer: core::Renderer + 'a,
{
fn from(
pane_grid: PaneGrid<'a, Message, Theme, Renderer>,
@@ -1116,7 +1132,7 @@ impl<'a, T> Contents<'a, T> {
/// The appearance of a [`PaneGrid`].
#[derive(Debug, Clone, Copy, PartialEq)]
-pub struct Appearance {
+pub struct Style {
/// The appearance of a hovered region highlight.
pub hovered_region: Highlight,
/// The appearance of a picked split.
@@ -1145,32 +1161,40 @@ pub struct Line {
pub width: f32,
}
-/// The style of a [`PaneGrid`].
-pub type Style<'a, Theme> = Box<dyn Fn(&Theme) -> Appearance + 'a>;
+/// The theme catalog of a [`PaneGrid`].
+pub trait Catalog: container::Catalog {
+ /// The item class of this [`Catalog`].
+ type Class<'a>;
-/// The default style of a [`PaneGrid`].
-pub trait DefaultStyle {
- /// Returns the default style of a [`PaneGrid`].
- fn default_style(&self) -> Appearance;
+ /// 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;
}
-impl DefaultStyle for Theme {
- fn default_style(&self) -> Appearance {
- default(self)
+/// 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)
}
-}
-impl DefaultStyle for Appearance {
- fn default_style(&self) -> Appearance {
- *self
+ fn style(&self, class: &StyleFn<'_, Self>) -> Style {
+ class(self)
}
}
/// The default style of a [`PaneGrid`].
-pub fn default(theme: &Theme) -> Appearance {
+pub fn default(theme: &Theme) -> Style {
let palette = theme.extended_palette();
- Appearance {
+ Style {
hovered_region: Highlight {
background: Background::Color(Color {
a: 0.5,
diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs
index 98f4f99a..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,30 +20,29 @@ pub struct Content<
Theme = crate::Theme,
Renderer = crate::Renderer,
> where
- 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: container::Style<'a, Theme>,
+ class: Theme::Class<'a>,
}
impl<'a, Message, Theme, Renderer> Content<'a, Message, Theme, Renderer>
where
- 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
- where
- Theme: container::DefaultStyle + 'a,
- {
+ pub fn new(body: impl Into<Element<'a, Message, Theme, Renderer>>) -> Self {
Self {
title_bar: None,
body: body.into(),
- style: Box::new(Theme::default_style),
+ 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>,
@@ -53,18 +52,31 @@ where
}
/// Sets the style of the [`Content`].
+ #[must_use]
pub fn style(
mut self,
- style: impl Fn(&Theme, container::Status) -> container::Appearance + 'a,
- ) -> Self {
- self.style = Box::new(style);
+ 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
- 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() {
@@ -93,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,
@@ -107,15 +119,7 @@ where
let bounds = layout.bounds();
{
- let style = {
- let status = if cursor.is_over(bounds) {
- container::Status::Hovered
- } else {
- container::Status::Idle
- };
-
- (self.style)(theme, status)
- };
+ let style = theme.style(&self.class);
container::draw_background(renderer, &style, bounds);
}
@@ -381,7 +385,8 @@ where
impl<'a, Message, Theme, Renderer> Draggable
for &Content<'a, Message, Theme, Renderer>
where
- Renderer: crate::core::Renderer,
+ Theme: container::Catalog,
+ Renderer: core::Renderer,
{
fn can_be_dragged_at(
&self,
@@ -403,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::DefaultStyle + 'a,
- 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 8dfea6e3..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
- 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: container::Style<'a, Theme>,
+ class: Theme::Class<'a>,
}
impl<'a, Message, Theme, Renderer> TitleBar<'a, Message, Theme, Renderer>
where
- Renderer: crate::core::Renderer,
+ Theme: container::Catalog,
+ Renderer: core::Renderer,
{
/// Creates a new [`TitleBar`] with the given content.
pub fn new(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
- ) -> Self
- where
- Theme: container::DefaultStyle + 'a,
- {
+ ) -> Self {
Self {
content: content.into(),
controls: None,
padding: Padding::ZERO,
always_show_controls: false,
- style: Box::new(Theme::default_style),
+ class: Theme::default(),
}
}
@@ -63,15 +63,6 @@ where
self
}
- /// Sets the style of the [`TitleBar`].
- pub fn style(
- mut self,
- style: impl Fn(&Theme, container::Status) -> container::Appearance + 'a,
- ) -> Self {
- self.style = Box::new(style);
- self
- }
-
/// Sets whether or not the [`controls`] attached to this [`TitleBar`] are
/// always visible.
///
@@ -84,11 +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
- 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() {
@@ -117,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,
@@ -130,16 +143,7 @@ where
show_controls: bool,
) {
let bounds = layout.bounds();
-
- let style = {
- let status = if cursor.is_over(bounds) {
- container::Status::Hovered
- } else {
- container::Status::Idle
- };
-
- (self.style)(theme, status)
- };
+ 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 52d54397..edccfdaa 100644
--- a/widget/src/pick_list.rs
+++ b/widget/src/pick_list.rs
@@ -32,6 +32,7 @@ pub struct PickList<
T: ToString + PartialEq + Clone,
L: Borrow<[T]> + 'a,
V: Borrow<T> + 'a,
+ Theme: Catalog,
Renderer: text::Renderer,
{
on_select: Box<dyn Fn(T) -> Message + 'a>,
@@ -47,7 +48,8 @@ pub struct PickList<
text_shaping: text::Shaping,
font: Option<Renderer::Font>,
handle: Handle<Renderer::Font>,
- style: Style<'a, Theme>,
+ class: <Theme as Catalog>::Class<'a>,
+ menu_class: <Theme as menu::Catalog>::Class<'a>,
}
impl<'a, T, L, V, Message, Theme, Renderer>
@@ -57,6 +59,7 @@ where
L: Borrow<[T]> + 'a,
V: Borrow<T> + 'a,
Message: Clone,
+ Theme: Catalog,
Renderer: text::Renderer,
{
/// Creates a new [`PickList`] with the given list of options, the current
@@ -65,10 +68,7 @@ where
options: L,
selected: Option<V>,
on_select: impl Fn(T) -> Message + 'a,
- ) -> Self
- where
- Theme: DefaultStyle,
- {
+ ) -> Self {
Self {
on_select: Box::new(on_select),
on_open: None,
@@ -83,7 +83,8 @@ where
text_shaping: text::Shaping::Basic,
font: None,
handle: Handle::default(),
- style: Theme::default_style(),
+ class: <Theme as Catalog>::default(),
+ menu_class: <Theme as Catalog>::default_menu(),
}
}
@@ -151,8 +152,23 @@ where
}
/// Sets the style of the [`PickList`].
- pub fn style(mut self, style: impl Into<Style<'a, Theme>>) -> Self {
- self.style = style.into();
+ #[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,
+ class: impl Into<<Theme as Catalog>::Class<'a>>,
+ ) -> Self {
+ self.class = class.into();
self
}
}
@@ -164,6 +180,7 @@ where
L: Borrow<[T]>,
V: Borrow<T>,
Message: Clone + 'a,
+ Theme: Catalog + 'a,
Renderer: text::Renderer + 'a,
{
fn tag(&self) -> tree::Tag {
@@ -409,15 +426,15 @@ where
Status::Active
};
- let appearance = (self.style.field)(theme, status);
+ let style = Catalog::style(theme, &self.class, status);
renderer.fill_quad(
renderer::Quad {
bounds,
- border: appearance.border,
+ border: style.border,
..renderer::Quad::default()
},
- appearance.background,
+ style.background,
);
let handle = match &self.handle {
@@ -462,7 +479,7 @@ where
renderer.fill_text(
Text {
- content: &code_point.to_string(),
+ content: code_point.to_string(),
size,
line_height,
font,
@@ -478,14 +495,14 @@ where
bounds.x + bounds.width - self.padding.right,
bounds.center_y(),
),
- appearance.handle_color,
+ style.handle_color,
*viewport,
);
}
let label = selected.map(ToString::to_string);
- if let Some(label) = label.as_deref().or(self.placeholder.as_deref()) {
+ if let Some(label) = label.or_else(|| self.placeholder.clone()) {
let text_size =
self.text_size.unwrap_or_else(|| renderer.default_size());
@@ -505,9 +522,9 @@ where
},
Point::new(bounds.x + self.padding.left, bounds.center_y()),
if is_selected {
- appearance.text_color
+ style.text_color
} else {
- appearance.placeholder_color
+ style.placeholder_color
},
*viewport,
);
@@ -539,7 +556,7 @@ where
(on_select)(option)
},
None,
- &self.style.menu,
+ &self.menu_class,
)
.width(bounds.width)
.padding(self.padding)
@@ -565,7 +582,7 @@ where
L: Borrow<[T]> + 'a,
V: Borrow<T> + 'a,
Message: Clone + 'a,
- Theme: 'a,
+ Theme: Catalog + 'a,
Renderer: text::Renderer + 'a,
{
fn from(
@@ -662,7 +679,7 @@ pub enum Status {
/// The appearance of a pick list.
#[derive(Debug, Clone, Copy)]
-pub struct Appearance {
+pub struct Style {
/// The text [`Color`] of the pick list.
pub text_color: Color,
/// The placeholder [`Color`] of the pick list.
@@ -675,36 +692,49 @@ pub struct Appearance {
pub border: Border,
}
-/// The styles of the different parts of a [`PickList`].
-#[allow(missing_debug_implementations)]
-pub struct Style<'a, Theme> {
- /// The style of the [`PickList`] itself.
- pub field: Box<dyn Fn(&Theme, Status) -> Appearance + 'a>,
+/// The theme catalog of a [`PickList`].
+pub trait Catalog: menu::Catalog {
+ /// The item class of the [`Catalog`].
+ type Class<'a>;
- /// The style of the [`Menu`] of the pick list.
- pub menu: menu::Style<'a, Theme>,
-}
+ /// The default class produced by the [`Catalog`].
+ fn default<'a>() -> <Self as Catalog>::Class<'a>;
-/// The default style of a [`PickList`].
-pub trait DefaultStyle: Sized {
- /// Returns the default style of a [`PickList`].
- fn default_style() -> Style<'static, Self>;
+ /// The default class for the menu of the [`PickList`].
+ fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> {
+ <Self as menu::Catalog>::default()
+ }
+
+ /// The [`Style`] of a class with the given status.
+ fn style(
+ &self,
+ class: &<Self as Catalog>::Class<'_>,
+ status: Status,
+ ) -> Style;
}
-impl DefaultStyle for Theme {
- fn default_style() -> Style<'static, Self> {
- Style {
- field: Box::new(default),
- menu: menu::DefaultStyle::default_style(),
- }
+/// 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>;
+
+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>, status: Status) -> Style {
+ class(self, status)
}
}
/// The default style of the field of a [`PickList`].
-pub fn default(theme: &Theme, status: Status) -> Appearance {
+pub fn default(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
- let active = Appearance {
+ let active = Style {
text_color: palette.background.weak.text,
background: palette.background.weak.color.into(),
placeholder_color: palette.background.strong.color,
@@ -718,7 +748,7 @@ pub fn default(theme: &Theme, status: Status) -> Appearance {
match status {
Status::Active => active,
- Status::Hovered | Status::Opened => Appearance {
+ Status::Hovered | Status::Opened => Style {
border: Border {
color: palette.primary.strong.color,
..active.border
diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs
index 38d8da85..e7821b43 100644
--- a/widget/src/progress_bar.rs
+++ b/widget/src/progress_bar.rs
@@ -4,7 +4,8 @@ use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget::Tree;
use crate::core::{
- Background, Border, Element, Layout, Length, Rectangle, Size, Theme, Widget,
+ self, Background, Border, Element, Layout, Length, Rectangle, Size, Theme,
+ Widget,
};
use std::ops::RangeInclusive;
@@ -22,15 +23,21 @@ use std::ops::RangeInclusive;
///
/// ![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<'a, Theme = crate::Theme> {
+pub struct ProgressBar<'a, Theme = crate::Theme>
+where
+ Theme: Catalog,
+{
range: RangeInclusive<f32>,
value: f32,
width: Length,
height: Option<Length>,
- style: Style<'a, Theme>,
+ class: Theme::Class<'a>,
}
-impl<'a, Theme> ProgressBar<'a, Theme> {
+impl<'a, Theme> ProgressBar<'a, Theme>
+where
+ Theme: Catalog,
+{
/// The default height of a [`ProgressBar`].
pub const DEFAULT_HEIGHT: f32 = 30.0;
@@ -39,16 +46,13 @@ impl<'a, Theme> ProgressBar<'a, Theme> {
/// It expects:
/// * an inclusive range of possible values
/// * the current value of the [`ProgressBar`]
- pub fn new(range: RangeInclusive<f32>, value: f32) -> Self
- where
- Theme: DefaultStyle + 'a,
- {
+ pub fn new(range: RangeInclusive<f32>, value: f32) -> Self {
ProgressBar {
value: value.clamp(*range.start(), *range.end()),
range,
width: Length::Fill,
height: None,
- style: Box::new(Theme::default_style),
+ class: Theme::default(),
}
}
@@ -65,8 +69,20 @@ impl<'a, Theme> ProgressBar<'a, Theme> {
}
/// Sets the style of the [`ProgressBar`].
- pub fn style(mut self, style: impl Fn(&Theme) -> Appearance + 'a) -> Self {
- self.style = Box::new(style);
+ #[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
}
}
@@ -74,7 +90,8 @@ impl<'a, Theme> ProgressBar<'a, Theme> {
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for ProgressBar<'a, Theme>
where
- Renderer: crate::core::Renderer,
+ Theme: Catalog,
+ Renderer: core::Renderer,
{
fn size(&self) -> Size<Length> {
Size {
@@ -116,15 +133,15 @@ where
/ (range_end - range_start)
};
- let appearance = (self.style)(theme);
+ let style = theme.style(&self.class);
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle { ..bounds },
- border: appearance.border,
+ border: style.border,
..renderer::Quad::default()
},
- appearance.background,
+ style.background,
);
if active_progress_width > 0.0 {
@@ -134,10 +151,10 @@ where
width: active_progress_width,
..bounds
},
- border: Border::rounded(appearance.border.radius),
+ border: Border::rounded(style.border.radius),
..renderer::Quad::default()
},
- appearance.bar,
+ style.bar,
);
}
}
@@ -147,8 +164,8 @@ impl<'a, Message, Theme, Renderer> From<ProgressBar<'a, Theme>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
- Theme: 'a,
- Renderer: 'a + crate::core::Renderer,
+ Theme: 'a + Catalog,
+ Renderer: 'a + core::Renderer,
{
fn from(
progress_bar: ProgressBar<'a, Theme>,
@@ -159,7 +176,7 @@ where
/// The appearance of a progress bar.
#[derive(Debug, Clone, Copy)]
-pub struct Appearance {
+pub struct Style {
/// The [`Background`] of the progress bar.
pub background: Background,
/// The [`Background`] of the bar of the progress bar.
@@ -168,29 +185,37 @@ pub struct Appearance {
pub border: Border,
}
-/// The style of a [`ProgressBar`].
-pub type Style<'a, Theme> = Box<dyn Fn(&Theme) -> Appearance + 'a>;
+/// 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 default style of a [`ProgressBar`].
-pub trait DefaultStyle {
- /// Returns the default style of a [`ProgressBar`].
- fn default_style(&self) -> Appearance;
+ /// The [`Style`] of a class with the given status.
+ fn style(&self, class: &Self::Class<'_>) -> Style;
}
-impl DefaultStyle for Theme {
- fn default_style(&self) -> Appearance {
- primary(self)
+/// 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)
}
-}
-impl DefaultStyle for Appearance {
- fn default_style(&self) -> Appearance {
- *self
+ fn style(&self, class: &Self::Class<'_>) -> Style {
+ class(self)
}
}
/// The primary style of a [`ProgressBar`].
-pub fn primary(theme: &Theme) -> Appearance {
+pub fn primary(theme: &Theme) -> Style {
let palette = theme.extended_palette();
styled(
@@ -200,7 +225,7 @@ pub fn primary(theme: &Theme) -> Appearance {
}
/// The secondary style of a [`ProgressBar`].
-pub fn secondary(theme: &Theme) -> Appearance {
+pub fn secondary(theme: &Theme) -> Style {
let palette = theme.extended_palette();
styled(
@@ -210,14 +235,14 @@ pub fn secondary(theme: &Theme) -> Appearance {
}
/// The success style of a [`ProgressBar`].
-pub fn success(theme: &Theme) -> Appearance {
+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) -> Appearance {
+pub fn danger(theme: &Theme) -> Style {
let palette = theme.extended_palette();
styled(palette.background.strong.color, palette.danger.base.color)
@@ -226,8 +251,8 @@ pub fn danger(theme: &Theme) -> Appearance {
fn styled(
background: impl Into<Background>,
bar: impl Into<Background>,
-) -> Appearance {
- Appearance {
+) -> 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 90c0c970..e064aada 100644
--- a/widget/src/qr_code.rs
+++ b/widget/src/qr_code.rs
@@ -8,7 +8,6 @@ use crate::core::{
Color, Element, Layout, Length, Point, Rectangle, Size, Theme, Vector,
Widget,
};
-use crate::graphics::geometry::Renderer as _;
use crate::Renderer;
use std::cell::RefCell;
@@ -20,22 +19,25 @@ 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.
#[allow(missing_debug_implementations)]
-pub struct QRCode<'a, Theme = crate::Theme> {
+pub struct QRCode<'a, Theme = crate::Theme>
+where
+ Theme: Catalog,
+{
data: &'a Data,
cell_size: u16,
- style: Style<'a, Theme>,
+ class: Theme::Class<'a>,
}
-impl<'a, Theme> QRCode<'a, Theme> {
+impl<'a, Theme> QRCode<'a, Theme>
+where
+ Theme: Catalog,
+{
/// Creates a new [`QRCode`] with the provided [`Data`].
- pub fn new(data: &'a Data) -> Self
- where
- Theme: DefaultStyle + 'a,
- {
+ pub fn new(data: &'a Data) -> Self {
Self {
data,
cell_size: DEFAULT_CELL_SIZE,
- style: Box::new(Theme::default_style),
+ class: Theme::default(),
}
}
@@ -46,14 +48,27 @@ impl<'a, Theme> QRCode<'a, Theme> {
}
/// Sets the style of the [`QRCode`].
- pub fn style(mut self, style: impl Fn(&Theme) -> Appearance + 'a) -> Self {
- self.style = Box::new(style);
+ #[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>
+impl<'a, Message, Theme> Widget<Message, Theme, Renderer> for QRCode<'a, Theme>
+where
+ Theme: Catalog,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
@@ -97,13 +112,13 @@ impl<'a, Message, Theme> Widget<Message, Theme, Renderer>
let bounds = layout.bounds();
let side_length = self.data.width + 2 * QUIET_ZONE;
- let appearance = (self.style)(theme);
- 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
@@ -115,7 +130,7 @@ impl<'a, Message, Theme> Widget<Message, Theme, Renderer>
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
@@ -134,7 +149,7 @@ impl<'a, Message, Theme> Widget<Message, Theme, Renderer>
frame.fill_rectangle(
Point::new(column as f32, row as f32),
Size::UNIT,
- appearance.cell,
+ style.cell,
);
});
});
@@ -142,7 +157,9 @@ impl<'a, Message, Theme> Widget<Message, Theme, Renderer>
renderer.with_translation(
bounds.position() - Point::ORIGIN,
|renderer| {
- renderer.draw(vec![geometry]);
+ use crate::graphics::geometry::Renderer as _;
+
+ renderer.draw_geometry(geometry);
},
);
}
@@ -151,7 +168,7 @@ impl<'a, Message, Theme> Widget<Message, Theme, Renderer>
impl<'a, Message, Theme> From<QRCode<'a, Theme>>
for Element<'a, Message, Theme, Renderer>
where
- Theme: 'a,
+ Theme: Catalog + 'a,
{
fn from(qr_code: QRCode<'a, Theme>) -> Self {
Self::new(qr_code)
@@ -165,7 +182,7 @@ where
pub struct Data {
contents: Vec<qrcode::Color>,
width: usize,
- cache: canvas::Cache,
+ cache: canvas::Cache<Renderer>,
}
impl Data {
@@ -323,44 +340,50 @@ 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 Appearance {
+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 style of a [`QRCode`].
-pub type Style<'a, Theme> = Box<dyn Fn(&Theme) -> Appearance + 'a>;
+/// The theme catalog of a [`QRCode`].
+pub trait Catalog {
+ /// The item class of the [`Catalog`].
+ type Class<'a>;
-/// The default style of a [`QRCode`].
-pub trait DefaultStyle {
- /// Returns the default style of a [`QRCode`].
- fn default_style(&self) -> Appearance;
+ /// 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;
}
-impl DefaultStyle for Theme {
- fn default_style(&self) -> Appearance {
- default(self)
+/// 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)
}
-}
-impl DefaultStyle for Appearance {
- fn default_style(&self) -> Appearance {
- *self
+ fn style(&self, class: &Self::Class<'_>) -> Style {
+ class(self)
}
}
/// The default style of a [`QRCode`].
-pub fn default(theme: &Theme) -> Appearance {
+pub fn default(theme: &Theme) -> Style {
let palette = theme.palette();
- Appearance {
+ Style {
cell: palette.text,
background: palette.background,
}
diff --git a/widget/src/radio.rs b/widget/src/radio.rs
index a7b7dd03..6b22961d 100644
--- a/widget/src/radio.rs
+++ b/widget/src/radio.rs
@@ -69,6 +69,7 @@ use crate::core::{
#[allow(missing_debug_implementations)]
pub struct Radio<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
where
+ Theme: Catalog,
Renderer: text::Renderer,
{
is_selected: bool,
@@ -81,12 +82,13 @@ where
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
font: Option<Renderer::Font>,
- style: Style<'a, Theme>,
+ class: Theme::Class<'a>,
}
impl<'a, Message, Theme, Renderer> Radio<'a, Message, Theme, Renderer>
where
Message: Clone,
+ Theme: Catalog,
Renderer: text::Renderer,
{
/// The default size of a [`Radio`] button.
@@ -110,7 +112,6 @@ where
f: F,
) -> Self
where
- Theme: DefaultStyle + 'a,
V: Eq + Copy,
F: FnOnce(V) -> Message,
{
@@ -125,7 +126,7 @@ where
text_line_height: text::LineHeight::default(),
text_shaping: text::Shaping::Basic,
font: None,
- style: Box::new(Theme::default_style),
+ class: Theme::default(),
}
}
@@ -175,11 +176,20 @@ where
}
/// Sets the style of the [`Radio`] button.
- pub fn style(
- mut self,
- style: impl Fn(&Theme, Status) -> Appearance + 'a,
- ) -> Self {
- self.style = Box::new(style);
+ #[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
}
}
@@ -188,6 +198,7 @@ impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Radio<'a, Message, Theme, Renderer>
where
Message: Clone,
+ Theme: Catalog,
Renderer: text::Renderer,
{
fn tag(&self) -> tree::Tag {
@@ -284,7 +295,7 @@ where
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
- style: &renderer::Style,
+ defaults: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
@@ -300,7 +311,7 @@ where
Status::Active { is_selected }
};
- let appearance = (self.style)(theme, status);
+ let style = theme.style(&self.class, status);
{
let layout = children.next().unwrap();
@@ -314,12 +325,12 @@ where
bounds,
border: Border {
radius: (size / 2.0).into(),
- width: appearance.border_width,
- color: appearance.border_color,
+ width: style.border_width,
+ color: style.border_color,
},
..renderer::Quad::default()
},
- appearance.background,
+ style.background,
);
if self.is_selected {
@@ -334,7 +345,7 @@ where
border: Border::rounded(dot_size / 2.0),
..renderer::Quad::default()
},
- appearance.dot_color,
+ style.dot_color,
);
}
}
@@ -344,11 +355,11 @@ where
crate::text::draw(
renderer,
- style,
+ defaults,
label_layout,
tree.state.downcast_ref(),
- crate::text::Appearance {
- color: appearance.text_color,
+ crate::text::Style {
+ color: style.text_color,
},
viewport,
);
@@ -360,7 +371,7 @@ impl<'a, Message, Theme, Renderer> From<Radio<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a + Clone,
- Theme: 'a,
+ Theme: 'a + Catalog,
Renderer: 'a + text::Renderer,
{
fn from(
@@ -387,7 +398,7 @@ pub enum Status {
/// The appearance of a radio button.
#[derive(Debug, Clone, Copy)]
-pub struct Appearance {
+pub struct Style {
/// The [`Background`] of the radio button.
pub background: Background,
/// The [`Color`] of the dot of the radio button.
@@ -400,32 +411,38 @@ pub struct Appearance {
pub text_color: Option<Color>,
}
-/// The style of a [`Radio`] button.
-pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>;
+/// The theme catalog of a [`Radio`].
+pub trait Catalog {
+ /// The item class of the [`Catalog`].
+ type Class<'a>;
-/// The default style of a [`Radio`] button.
-pub trait DefaultStyle {
- /// Returns the default style of a [`Radio`] button.
- fn default_style(&self, status: Status) -> Appearance;
+ /// 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 DefaultStyle for Theme {
- fn default_style(&self, status: Status) -> Appearance {
- default(self, status)
+/// 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)
}
-}
-impl DefaultStyle for Appearance {
- fn default_style(&self, _status: Status) -> Appearance {
- *self
+ 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) -> Appearance {
+pub fn default(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
- let active = Appearance {
+ let active = Style {
background: Color::TRANSPARENT.into(),
dot_color: palette.primary.strong.color,
border_width: 1.0,
@@ -435,7 +452,7 @@ pub fn default(theme: &Theme, status: Status) -> Appearance {
match status {
Status::Active { .. } => active,
- Status::Hovered { .. } => Appearance {
+ 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 9fa5f74f..1a536d2f 100644
--- a/widget/src/rule.rs
+++ b/widget/src/rule.rs
@@ -1,4 +1,5 @@
//! 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;
@@ -10,43 +11,55 @@ use crate::core::{
/// Display a horizontal or vertical rule for dividing content.
#[allow(missing_debug_implementations)]
-pub struct Rule<'a, Theme = crate::Theme> {
+pub struct Rule<'a, Theme = crate::Theme>
+where
+ Theme: Catalog,
+{
width: Length,
height: Length,
is_horizontal: bool,
- style: Style<'a, Theme>,
+ class: Theme::Class<'a>,
}
-impl<'a, Theme> Rule<'a, Theme> {
+impl<'a, Theme> Rule<'a, Theme>
+where
+ Theme: Catalog,
+{
/// Creates a horizontal [`Rule`] with the given height.
- pub fn horizontal(height: impl Into<Pixels>) -> Self
- where
- Theme: DefaultStyle + 'a,
- {
+ pub fn horizontal(height: impl Into<Pixels>) -> Self {
Rule {
width: Length::Fill,
height: Length::Fixed(height.into().0),
is_horizontal: true,
- style: Box::new(Theme::default_style),
+ class: Theme::default(),
}
}
/// Creates a vertical [`Rule`] with the given width.
- pub fn vertical(width: impl Into<Pixels>) -> Self
- where
- Theme: DefaultStyle + 'a,
- {
+ pub fn vertical(width: impl Into<Pixels>) -> Self {
Rule {
width: Length::Fixed(width.into().0),
height: Length::Fill,
is_horizontal: false,
- style: Box::new(Theme::default_style),
+ class: Theme::default(),
}
}
/// Sets the style of the [`Rule`].
- pub fn style(mut self, style: impl Fn(&Theme) -> Appearance + 'a) -> Self {
- self.style = Box::new(style);
+ #[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
}
}
@@ -54,7 +67,8 @@ impl<'a, Theme> Rule<'a, Theme> {
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Rule<'a, Theme>
where
- Renderer: crate::core::Renderer,
+ Renderer: core::Renderer,
+ Theme: Catalog,
{
fn size(&self) -> Size<Length> {
Size {
@@ -83,35 +97,34 @@ where
_viewport: &Rectangle,
) {
let bounds = layout.bounds();
- let appearance = (self.style)(theme);
+ let style = theme.style(&self.class);
let bounds = if self.is_horizontal {
let line_y = (bounds.y + (bounds.height / 2.0)
- - (appearance.width as f32 / 2.0))
+ - (style.width as f32 / 2.0))
.round();
- let (offset, line_width) = appearance.fill_mode.fill(bounds.width);
+ let (offset, line_width) = style.fill_mode.fill(bounds.width);
let line_x = bounds.x + offset;
Rectangle {
x: line_x,
y: line_y,
width: line_width,
- height: appearance.width as f32,
+ height: style.width as f32,
}
} else {
let line_x = (bounds.x + (bounds.width / 2.0)
- - (appearance.width as f32 / 2.0))
+ - (style.width as f32 / 2.0))
.round();
- let (offset, line_height) =
- appearance.fill_mode.fill(bounds.height);
+ let (offset, line_height) = style.fill_mode.fill(bounds.height);
let line_y = bounds.y + offset;
Rectangle {
x: line_x,
y: line_y,
- width: appearance.width as f32,
+ width: style.width as f32,
height: line_height,
}
};
@@ -119,10 +132,10 @@ where
renderer.fill_quad(
renderer::Quad {
bounds,
- border: Border::rounded(appearance.radius),
+ border: Border::rounded(style.radius),
..renderer::Quad::default()
},
- appearance.color,
+ style.color,
);
}
}
@@ -131,8 +144,8 @@ impl<'a, Message, Theme, Renderer> From<Rule<'a, Theme>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
- Theme: 'a,
- Renderer: 'a + crate::core::Renderer,
+ Theme: 'a + Catalog,
+ Renderer: 'a + core::Renderer,
{
fn from(rule: Rule<'a, Theme>) -> Element<'a, Message, Theme, Renderer> {
Element::new(rule)
@@ -141,7 +154,7 @@ where
/// The appearance of a rule.
#[derive(Debug, Clone, Copy)]
-pub struct Appearance {
+pub struct Style {
/// The color of the rule.
pub color: Color,
/// The width (thickness) of the rule line.
@@ -216,32 +229,40 @@ impl FillMode {
}
}
-/// The style of a [`Rule`].
-pub type Style<'a, Theme> = Box<dyn Fn(&Theme) -> Appearance + 'a>;
+/// The theme catalog of a [`Rule`].
+pub trait Catalog: Sized {
+ /// The item class of the [`Catalog`].
+ type Class<'a>;
-/// The default style of a [`Rule`].
-pub trait DefaultStyle {
- /// Returns the default style of a [`Rule`].
- fn default_style(&self) -> Appearance;
+ /// 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;
}
-impl DefaultStyle for Theme {
- fn default_style(&self) -> Appearance {
- default(self)
+/// 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)
}
-}
-impl DefaultStyle for Appearance {
- fn default_style(&self) -> Appearance {
- *self
+ fn style(&self, class: &Self::Class<'_>) -> Style {
+ class(self)
}
}
/// The default styling of a [`Rule`].
-pub fn default(theme: &Theme) -> Appearance {
+pub fn default(theme: &Theme) -> Style {
let palette = theme.extended_palette();
- Appearance {
+ Style {
color: palette.background.strong.color,
width: 1,
radius: 0.0.into(),
diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs
index c03bbb7d..6fc00f87 100644
--- a/widget/src/scrollable.rs
+++ b/widget/src/scrollable.rs
@@ -12,8 +12,8 @@ use crate::core::widget;
use crate::core::widget::operation::{self, Operation};
use crate::core::widget::tree::{self, Tree};
use crate::core::{
- Background, Border, Clipboard, Color, Element, Layout, Length, Pixels,
- Point, Rectangle, Shell, Size, Theme, Vector, Widget,
+ self, Background, Border, Clipboard, Color, Element, Layout, Length,
+ Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget,
};
use crate::runtime::Command;
@@ -28,7 +28,8 @@ pub struct Scrollable<
Theme = crate::Theme,
Renderer = crate::Renderer,
> where
- Renderer: crate::core::Renderer,
+ Theme: Catalog,
+ Renderer: core::Renderer,
{
id: Option<Id>,
width: Length,
@@ -36,20 +37,18 @@ pub struct Scrollable<
direction: Direction,
content: Element<'a, Message, Theme, Renderer>,
on_scroll: Option<Box<dyn Fn(Viewport) -> Message + 'a>>,
- style: Style<'a, Theme>,
+ class: Theme::Class<'a>,
}
impl<'a, Message, Theme, Renderer> Scrollable<'a, Message, Theme, Renderer>
where
- Renderer: crate::core::Renderer,
+ Theme: Catalog,
+ Renderer: core::Renderer,
{
/// Creates a new vertical [`Scrollable`].
pub fn new(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
- ) -> Self
- where
- Theme: DefaultStyle + 'a,
- {
+ ) -> Self {
Self::with_direction(content, Direction::default())
}
@@ -57,18 +56,6 @@ where
pub fn with_direction(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
direction: Direction,
- ) -> Self
- where
- Theme: DefaultStyle + 'a,
- {
- Self::with_direction_and_style(content, direction, Theme::default_style)
- }
-
- /// Creates a new [`Scrollable`] with the given [`Direction`] and style.
- pub fn with_direction_and_style(
- content: impl Into<Element<'a, Message, Theme, Renderer>>,
- direction: Direction,
- style: impl Fn(&Theme, Status) -> Appearance + 'a,
) -> Self {
let content = content.into();
@@ -91,7 +78,7 @@ where
direction,
content,
on_scroll: None,
- style: Box::new(style),
+ class: Theme::default(),
}
}
@@ -121,12 +108,21 @@ where
self
}
- /// Sets the style of the [`Scrollable`] .
- pub fn style(
- mut self,
- style: impl Fn(&Theme, Status) -> Appearance + 'a,
- ) -> Self {
- self.style = Box::new(style);
+ /// 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
}
}
@@ -237,7 +233,8 @@ pub enum Alignment {
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Scrollable<'a, Message, Theme, Renderer>
where
- Renderer: crate::core::Renderer,
+ Theme: Catalog,
+ Renderer: core::Renderer,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
@@ -353,6 +350,148 @@ where
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)
@@ -425,7 +564,9 @@ where
let delta = match delta {
mouse::ScrollDelta::Lines { x, y } => {
// TODO: Configurable speed/friction (?)
- let movement = if state.keyboard_modifiers.shift() {
+ 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)
@@ -438,15 +579,17 @@ where
state.scroll(delta, self.direction, bounds, content_bounds);
- notify_on_scroll(
+ event_status = if notify_on_scroll(
state,
&self.on_scroll,
bounds,
content_bounds,
shell,
- );
-
- event_status = event::Status::Captured;
+ ) {
+ event::Status::Captured
+ } else {
+ event::Status::Ignored
+ };
}
Event::Touch(event)
if state.scroll_area_touched_at.is_some()
@@ -484,7 +627,8 @@ where
state.scroll_area_touched_at =
Some(cursor_position);
- notify_on_scroll(
+ // TODO: bubble up touch movements if not consumed.
+ let _ = notify_on_scroll(
state,
&self.on_scroll,
bounds,
@@ -501,148 +645,6 @@ where
_ => {}
}
- if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at {
- match event {
- Event::Mouse(mouse::Event::CursorMoved { .. })
- | Event::Touch(touch::Event::FingerMoved { .. }) => {
- if let Some(scrollbar) = scrollbars.y {
- let Some(cursor_position) = cursor.position() else {
- return event::Status::Ignored;
- };
-
- state.scroll_y_to(
- scrollbar.scroll_percentage_y(
- scroller_grabbed_at,
- cursor_position,
- ),
- bounds,
- content_bounds,
- );
-
- notify_on_scroll(
- state,
- &self.on_scroll,
- bounds,
- content_bounds,
- shell,
- );
-
- event_status = event::Status::Captured;
- }
- }
- _ => {}
- }
- } else if mouse_over_y_scrollbar {
- match event {
- Event::Mouse(mouse::Event::ButtonPressed(
- mouse::Button::Left,
- ))
- | Event::Touch(touch::Event::FingerPressed { .. }) => {
- let Some(cursor_position) = cursor.position() else {
- return event::Status::Ignored;
- };
-
- if let (Some(scroller_grabbed_at), Some(scrollbar)) = (
- scrollbars.grab_y_scroller(cursor_position),
- scrollbars.y,
- ) {
- state.scroll_y_to(
- scrollbar.scroll_percentage_y(
- scroller_grabbed_at,
- cursor_position,
- ),
- bounds,
- content_bounds,
- );
-
- state.y_scroller_grabbed_at = Some(scroller_grabbed_at);
-
- notify_on_scroll(
- state,
- &self.on_scroll,
- bounds,
- content_bounds,
- shell,
- );
- }
-
- event_status = event::Status::Captured;
- }
- _ => {}
- }
- }
-
- if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at {
- match event {
- Event::Mouse(mouse::Event::CursorMoved { .. })
- | Event::Touch(touch::Event::FingerMoved { .. }) => {
- let Some(cursor_position) = cursor.position() else {
- return event::Status::Ignored;
- };
-
- if let Some(scrollbar) = scrollbars.x {
- state.scroll_x_to(
- scrollbar.scroll_percentage_x(
- scroller_grabbed_at,
- cursor_position,
- ),
- bounds,
- content_bounds,
- );
-
- notify_on_scroll(
- state,
- &self.on_scroll,
- bounds,
- content_bounds,
- shell,
- );
- }
-
- event_status = event::Status::Captured;
- }
- _ => {}
- }
- } else if mouse_over_x_scrollbar {
- match event {
- Event::Mouse(mouse::Event::ButtonPressed(
- mouse::Button::Left,
- ))
- | Event::Touch(touch::Event::FingerPressed { .. }) => {
- let Some(cursor_position) = cursor.position() else {
- return event::Status::Ignored;
- };
-
- if let (Some(scroller_grabbed_at), Some(scrollbar)) = (
- scrollbars.grab_x_scroller(cursor_position),
- scrollbars.x,
- ) {
- state.scroll_x_to(
- scrollbar.scroll_percentage_x(
- scroller_grabbed_at,
- cursor_position,
- ),
- bounds,
- content_bounds,
- );
-
- state.x_scroller_grabbed_at = Some(scroller_grabbed_at);
-
- notify_on_scroll(
- state,
- &self.on_scroll,
- bounds,
- content_bounds,
- shell,
- );
-
- event_status = event::Status::Captured;
- }
- }
- _ => {}
- }
- }
-
event_status
}
@@ -651,10 +653,10 @@ where
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
- style: &renderer::Style,
+ defaults: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
- _viewport: &Rectangle,
+ viewport: &Rectangle,
) {
let state = tree.state.downcast_ref::<State>();
@@ -662,6 +664,10 @@ where
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);
@@ -701,17 +707,13 @@ where
Status::Active
};
- let appearance = (self.style)(theme, status);
+ let style = theme.style(&self.class, status);
- container::draw_background(
- renderer,
- &appearance.container,
- layout.bounds(),
- );
+ container::draw_background(renderer, &style.container, layout.bounds());
// Draw inner content
if scrollbars.active() {
- renderer.with_layer(bounds, |renderer| {
+ renderer.with_layer(visible_bounds, |renderer| {
renderer.with_translation(
Vector::new(-translation.x, -translation.y),
|renderer| {
@@ -719,7 +721,7 @@ where
&tree.children[0],
renderer,
theme,
- style,
+ defaults,
content_layout,
cursor,
&Rectangle {
@@ -774,15 +776,15 @@ where
renderer.with_layer(
Rectangle {
- width: bounds.width + 2.0,
- height: bounds.height + 2.0,
- ..bounds
+ 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,
- appearance.vertical_scrollbar,
+ style.vertical_scrollbar,
&scrollbar,
);
}
@@ -790,14 +792,14 @@ where
if let Some(scrollbar) = scrollbars.x {
draw_scrollbar(
renderer,
- appearance.horizontal_scrollbar,
+ style.horizontal_scrollbar,
&scrollbar,
);
}
if let (Some(x), Some(y)) = (scrollbars.x, scrollbars.y) {
let background =
- appearance.gap.or(appearance.container.background);
+ style.gap.or(style.container.background);
if let Some(background) = background {
renderer.fill_quad(
@@ -821,7 +823,7 @@ where
&tree.children[0],
renderer,
theme,
- style,
+ defaults,
content_layout,
cursor,
&Rectangle {
@@ -857,7 +859,7 @@ where
if (mouse_over_x_scrollbar || mouse_over_y_scrollbar)
|| state.scrollers_grabbed()
{
- mouse::Interaction::Idle
+ mouse::Interaction::None
} else {
let translation =
state.translation(self.direction, bounds, content_bounds);
@@ -916,8 +918,8 @@ impl<'a, Message, Theme, Renderer>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
- Theme: 'a,
- Renderer: 'a + crate::core::Renderer,
+ Theme: 'a + Catalog,
+ Renderer: 'a + core::Renderer,
{
fn from(
text_input: Scrollable<'a, Message, Theme, Renderer>,
@@ -968,51 +970,54 @@ pub fn scroll_to<Message: 'static>(
Command::widget(operation::scrollable::scroll_to(id.0, offset))
}
+/// 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();
+ // 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 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())
- };
+ 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;
}
+ }
+ if let Some(on_scroll) = on_scroll {
shell.publish(on_scroll(viewport));
- state.last_notified = Some(viewport);
}
+ state.last_notified = Some(viewport);
+
+ true
}
#[derive(Debug, Clone, Copy)]
@@ -1570,9 +1575,9 @@ pub enum Status {
/// The appearance of a scrolable.
#[derive(Debug, Clone, Copy)]
-pub struct Appearance {
- /// The [`container::Appearance`] of a scrollable.
- pub container: container::Appearance,
+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.
@@ -1601,29 +1606,35 @@ pub struct Scroller {
pub border: Border,
}
-/// The style of a [`Scrollable`].
-pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>;
+/// The theme catalog of a [`Scrollable`].
+pub trait Catalog {
+ /// The item class of the [`Catalog`].
+ type Class<'a>;
-/// The default style of a [`Scrollable`].
-pub trait DefaultStyle {
- /// Returns the default style of a [`Scrollable`].
- fn default_style(&self, status: Status) -> Appearance;
+ /// 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 DefaultStyle for Theme {
- fn default_style(&self, status: Status) -> Appearance {
- default(self, status)
+/// 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)
}
-}
-impl DefaultStyle for Appearance {
- fn default_style(&self, _status: Status) -> Appearance {
- *self
+ 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) -> Appearance {
+pub fn default(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
let scrollbar = Scrollbar {
@@ -1636,8 +1647,8 @@ pub fn default(theme: &Theme, status: Status) -> Appearance {
};
match status {
- Status::Active => Appearance {
- container: container::Appearance::default(),
+ Status::Active => Style {
+ container: container::Style::default(),
vertical_scrollbar: scrollbar,
horizontal_scrollbar: scrollbar,
gap: None,
@@ -1654,8 +1665,8 @@ pub fn default(theme: &Theme, status: Status) -> Appearance {
..scrollbar
};
- Appearance {
- container: container::Appearance::default(),
+ Style {
+ container: container::Style::default(),
vertical_scrollbar: if is_vertical_scrollbar_hovered {
hovered_scrollbar
} else {
@@ -1681,8 +1692,8 @@ pub fn default(theme: &Theme, status: Status) -> Appearance {
..scrollbar
};
- Appearance {
- container: container::Appearance::default(),
+ Style {
+ container: container::Style::default(),
vertical_scrollbar: if is_vertical_scrollbar_dragged {
dragged_scrollbar
} else {
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 d3b46a98..a8f1d192 100644
--- a/widget/src/slider.rs
+++ b/widget/src/slider.rs
@@ -9,7 +9,7 @@ use crate::core::renderer;
use crate::core::touch;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
- Border, Clipboard, Color, Element, Layout, Length, Pixels, Point,
+ self, Border, Clipboard, Color, Element, Layout, Length, Pixels, Point,
Rectangle, Shell, Size, Theme, Widget,
};
@@ -39,7 +39,10 @@ use std::ops::RangeInclusive;
///
/// ![Slider drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/slider.png?raw=true)
#[allow(missing_debug_implementations)]
-pub struct Slider<'a, T, Message, Theme = crate::Theme> {
+pub struct Slider<'a, T, Message, Theme = crate::Theme>
+where
+ Theme: Catalog,
+{
range: RangeInclusive<T>,
step: T,
shift_step: Option<T>,
@@ -49,13 +52,14 @@ pub struct Slider<'a, T, Message, Theme = crate::Theme> {
on_release: Option<Message>,
width: Length,
height: f32,
- style: Style<'a, Theme>,
+ class: Theme::Class<'a>,
}
impl<'a, T, Message, Theme> Slider<'a, T, Message, Theme>
where
T: Copy + From<u8> + PartialOrd,
Message: Clone,
+ Theme: Catalog,
{
/// The default height of a [`Slider`].
pub const DEFAULT_HEIGHT: f32 = 16.0;
@@ -70,7 +74,6 @@ where
/// `Message`.
pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self
where
- Theme: DefaultStyle + 'a,
F: 'a + Fn(T) -> Message,
{
let value = if value >= *range.start() {
@@ -95,7 +98,7 @@ where
on_release: None,
width: Length::Fill,
height: Self::DEFAULT_HEIGHT,
- style: Box::new(Theme::default_style),
+ class: Theme::default(),
}
}
@@ -130,15 +133,6 @@ where
self
}
- /// Sets the style of the [`Slider`].
- pub fn style(
- mut self,
- style: impl Fn(&Theme, Status) -> Appearance + 'a,
- ) -> Self {
- self.style = Box::new(style);
- self
- }
-
/// Sets the step size of the [`Slider`].
pub fn step(mut self, step: impl Into<T>) -> Self {
self.step = step.into();
@@ -152,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>
@@ -159,7 +171,8 @@ impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
where
T: Copy + Into<f64> + num_traits::FromPrimitive,
Message: Clone,
- Renderer: crate::core::Renderer,
+ Theme: Catalog,
+ Renderer: core::Renderer,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
@@ -349,8 +362,8 @@ where
let bounds = layout.bounds();
let is_mouse_over = cursor.is_over(bounds);
- let style = (self.style)(
- theme,
+ let style = theme.style(
+ &self.class,
if state.is_dragging {
Status::Dragged
} else if is_mouse_over {
@@ -461,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: 'a,
- Renderer: crate::core::Renderer + 'a,
+ Theme: Catalog + 'a,
+ Renderer: core::Renderer + 'a,
{
fn from(
slider: Slider<'a, T, Message, Theme>,
@@ -490,15 +503,15 @@ pub enum Status {
/// The appearance of a slider.
#[derive(Debug, Clone, Copy)]
-pub struct Appearance {
+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 Appearance {
- /// Changes the [`HandleShape`] of the [`Appearance`] to a circle
+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 {
@@ -549,29 +562,35 @@ pub enum HandleShape {
},
}
-/// The style of a [`Slider`].
-pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>;
+/// The theme catalog of a [`Slider`].
+pub trait Catalog: Sized {
+ /// The item class of the [`Catalog`].
+ type Class<'a>;
-/// The default style of a [`Slider`].
-pub trait DefaultStyle {
- /// Returns the default style of a [`Slider`].
- fn default_style(&self, status: Status) -> Appearance;
+ /// 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 DefaultStyle for Theme {
- fn default_style(&self, status: Status) -> Appearance {
- default(self, status)
+/// 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)
}
-}
-impl DefaultStyle for Appearance {
- fn default_style(&self, _status: Status) -> Appearance {
- *self
+ 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) -> Appearance {
+pub fn default(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
let color = match status {
@@ -580,7 +599,7 @@ pub fn default(theme: &Theme, status: Status) -> Appearance {
Status::Dragged => palette.primary.strong.color,
};
- Appearance {
+ Style {
rail: Rail {
colors: (color, palette.secondary.base.color),
width: 4.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 1ac07ade..4551bcad 100644
--- a/widget/src/svg.rs
+++ b/widget/src/svg.rs
@@ -5,8 +5,8 @@ use crate::core::renderer;
use crate::core::svg;
use crate::core::widget::Tree;
use crate::core::{
- Color, ContentFit, Element, Layout, Length, Rectangle, Size, Theme, Vector,
- Widget,
+ Color, ContentFit, Element, Layout, Length, Point, Rectangle, Rotation,
+ Size, Theme, Vector, Widget,
};
use std::path::PathBuf;
@@ -20,36 +20,40 @@ pub use crate::core::svg::Handle;
/// [`Svg`] images can have a considerable rendering cost when resized,
/// specially when they are complex.
#[allow(missing_debug_implementations)]
-pub struct Svg<'a, Theme = crate::Theme> {
+pub struct Svg<'a, Theme = crate::Theme>
+where
+ Theme: Catalog,
+{
handle: Handle,
width: Length,
height: Length,
content_fit: ContentFit,
- style: Style<'a, Theme>,
+ class: Theme::Class<'a>,
+ rotation: Rotation,
+ opacity: f32,
}
-impl<'a, Theme> Svg<'a, Theme> {
+impl<'a, Theme> Svg<'a, Theme>
+where
+ Theme: Catalog,
+{
/// Creates a new [`Svg`] from the given [`Handle`].
- pub fn new(handle: impl Into<Handle>) -> Self
- where
- Theme: DefaultStyle + 'a,
- {
+ pub fn new(handle: impl Into<Handle>) -> Self {
Svg {
handle: handle.into(),
width: Length::Fill,
height: Length::Shrink,
content_fit: ContentFit::Contain,
- style: Box::new(Theme::default_style),
+ class: Theme::default(),
+ rotation: Rotation::default(),
+ opacity: 1.0,
}
}
/// Creates a new [`Svg`] that will display the contents of the file at the
/// provided path.
#[must_use]
- pub fn from_path(path: impl Into<PathBuf>) -> Self
- where
- Theme: DefaultStyle + 'a,
- {
+ pub fn from_path(path: impl Into<PathBuf>) -> Self {
Self::new(Handle::from_path(path))
}
@@ -78,13 +82,36 @@ impl<'a, Theme> Svg<'a, Theme> {
}
}
- /// Sets the style variant of this [`Svg`].
+ /// Sets the style of the [`Svg`].
#[must_use]
- pub fn style(
- mut self,
- style: impl Fn(&Theme, Status) -> Appearance + 'a,
- ) -> Self {
- self.style = Box::new(style);
+ 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
}
}
@@ -93,6 +120,7 @@ impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Svg<'a, Theme>
where
Renderer: svg::Renderer,
+ Theme: Catalog,
{
fn size(&self) -> Size<Length> {
Size {
@@ -108,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 {
@@ -142,37 +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 is_mouse_over = cursor.is_over(bounds);
+ 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 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 final_size = image_size * scale;
- let drawing_bounds = Rectangle {
- width: adjusted_fit.width,
- height: adjusted_fit.height,
- ..bounds
- };
+ 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 status = if is_mouse_over {
- Status::Hovered
- } else {
- Status::Idle
- };
+ let drawing_bounds = Rectangle::new(position, final_size);
- let appearance = (self.style)(theme, status);
+ let is_mouse_over = cursor.is_over(bounds);
- renderer.draw(
+ let status = if is_mouse_over {
+ Status::Hovered
+ } else {
+ Status::Idle
+ };
+
+ let style = theme.style(&self.class, status);
+
+ let render = |renderer: &mut Renderer| {
+ renderer.draw_svg(
self.handle.clone(),
- appearance.color,
- drawing_bounds + offset,
+ style.color,
+ drawing_bounds,
+ self.rotation.radians(),
+ self.opacity,
);
};
@@ -189,7 +232,7 @@ where
impl<'a, Message, Theme, Renderer> From<Svg<'a, Theme>>
for Element<'a, Message, Theme, Renderer>
where
- Theme: 'a,
+ Theme: Catalog + 'a,
Renderer: svg::Renderer + 'a,
{
fn from(icon: Svg<'a, Theme>) -> Element<'a, Message, Theme, Renderer> {
@@ -208,7 +251,7 @@ pub enum Status {
/// The appearance of an [`Svg`].
#[derive(Debug, Clone, Copy, PartialEq, Default)]
-pub struct Appearance {
+pub struct Style {
/// The [`Color`] filter of an [`Svg`].
///
/// Useful for coloring a symbolic icon.
@@ -217,23 +260,37 @@ pub struct Appearance {
pub color: Option<Color>,
}
-/// The style of an [`Svg`].
-pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>;
+/// The theme catalog of an [`Svg`].
+pub trait Catalog {
+ /// The item class of the [`Catalog`].
+ type Class<'a>;
-/// The default style of an [`Svg`].
-pub trait DefaultStyle {
- /// Returns the default style of an [`Svg`].
- fn default_style(&self, status: Status) -> Appearance;
+ /// 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 DefaultStyle for Theme {
- fn default_style(&self, _status: Status) -> Appearance {
- Appearance::default()
+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)
}
}
-impl DefaultStyle for Appearance {
- fn default_style(&self, _status: Status) -> Appearance {
- *self
+/// 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 5b8f6a1b..7c0b98ea 100644
--- a/widget/src/text_editor.rs
+++ b/widget/src/text_editor.rs
@@ -32,6 +32,7 @@ pub struct TextEditor<
Renderer = crate::Renderer,
> where
Highlighter: text::Highlighter,
+ Theme: Catalog,
Renderer: text::Renderer,
{
content: &'a Content<Renderer>,
@@ -41,7 +42,7 @@ pub struct TextEditor<
width: Length,
height: Length,
padding: Padding,
- style: Style<'a, Theme>,
+ class: Theme::Class<'a>,
on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>,
highlighter_settings: Highlighter::Settings,
highlighter_format: fn(
@@ -53,13 +54,11 @@ pub struct TextEditor<
impl<'a, Message, Theme, Renderer>
TextEditor<'a, highlighter::PlainText, Message, Theme, Renderer>
where
+ Theme: Catalog,
Renderer: text::Renderer,
{
/// Creates new [`TextEditor`] with the given [`Content`].
- pub fn new(content: &'a Content<Renderer>) -> Self
- where
- Theme: DefaultStyle + 'a,
- {
+ pub fn new(content: &'a Content<Renderer>) -> Self {
Self {
content,
font: None,
@@ -68,7 +67,7 @@ where
width: Length::Fill,
height: Length::Shrink,
padding: Padding::new(5.0),
- style: Box::new(Theme::default_style),
+ class: Theme::default(),
on_edit: None,
highlighter_settings: (),
highlighter_format: |_highlight, _theme| {
@@ -82,6 +81,7 @@ impl<'a, Highlighter, Message, Theme, Renderer>
TextEditor<'a, Highlighter, Message, Theme, Renderer>
where
Highlighter: text::Highlighter,
+ 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,11 +157,20 @@ where
}
/// Sets the style of the [`TextEditor`].
- pub fn style(
- mut self,
- style: impl Fn(&Theme, Status) -> Appearance + 'a,
- ) -> Self {
- self.style = Box::new(style);
+ #[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
}
}
@@ -295,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>,
@@ -305,10 +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: Catalog,
Renderer: text::Renderer,
{
fn tag(&self) -> widget::tree::Tag {
@@ -479,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,
@@ -508,22 +542,22 @@ where
Status::Active
};
- let appearance = (self.style)(theme, status);
+ 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,
);
@@ -535,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.floor(),
- 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()
},
- appearance.value,
+ style.value,
);
}
}
@@ -568,7 +606,7 @@ where
bounds: range,
..renderer::Quad::default()
},
- appearance.selection,
+ style.selection,
);
}
}
@@ -604,7 +642,7 @@ impl<'a, Highlighter, Message, Theme, Renderer>
where
Highlighter: text::Highlighter,
Message: 'a,
- Theme: 'a,
+ Theme: Catalog + 'a,
Renderer: text::Renderer,
{
fn from(
@@ -796,7 +834,7 @@ pub enum Status {
/// The appearance of a text input.
#[derive(Debug, Clone, Copy)]
-pub struct Appearance {
+pub struct Style {
/// The [`Background`] of the text input.
pub background: Background,
/// The [`Border`] of the text input.
@@ -811,32 +849,38 @@ pub struct Appearance {
pub selection: Color,
}
-/// The style of a [`TextEditor`].
-pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>;
+/// The theme catalog of a [`TextEditor`].
+pub trait Catalog {
+ /// The item class of the [`Catalog`].
+ type Class<'a>;
-/// The default style of a [`TextEditor`].
-pub trait DefaultStyle {
- /// Returns the default style of a [`TextEditor`].
- fn default_style(&self, status: Status) -> Appearance;
+ /// 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 DefaultStyle for Theme {
- fn default_style(&self, status: Status) -> Appearance {
- default(self, status)
+/// 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)
}
-}
-impl DefaultStyle for Appearance {
- fn default_style(&self, _status: Status) -> Appearance {
- *self
+ 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) -> Appearance {
+pub fn default(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
- let active = Appearance {
+ let active = Style {
background: Background::Color(palette.background.base.color),
border: Border {
radius: 2.0.into(),
@@ -851,21 +895,21 @@ pub fn default(theme: &Theme, status: Status) -> Appearance {
match status {
Status::Active => active,
- Status::Hovered => Appearance {
+ Status::Hovered => Style {
border: Border {
color: palette.background.base.text,
..active.border
},
..active
},
- Status::Focused => Appearance {
+ Status::Focused => Style {
border: Border {
color: palette.primary.strong.color,
..active.border
},
..active
},
- Status::Disabled => Appearance {
+ 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 b161ec74..e9f07838 100644
--- a/widget/src/text_input.rs
+++ b/widget/src/text_input.rs
@@ -60,6 +60,7 @@ pub struct TextInput<
Theme = crate::Theme,
Renderer = crate::Renderer,
> where
+ Theme: Catalog,
Renderer: text::Renderer,
{
id: Option<Id>,
@@ -75,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: Style<'a, Theme>,
+ class: Theme::Class<'a>,
}
/// The default [`Padding`] of a [`TextInput`].
@@ -84,24 +85,12 @@ pub const DEFAULT_PADDING: Padding = Padding::new(5.0);
impl<'a, Message, Theme, Renderer> TextInput<'a, Message, Theme, Renderer>
where
Message: Clone,
+ Theme: Catalog,
Renderer: text::Renderer,
{
/// Creates a new [`TextInput`] with the given placeholder and
/// its current value.
- pub fn new(placeholder: &str, value: &str) -> Self
- where
- Theme: DefaultStyle + 'a,
- {
- Self::with_style(placeholder, value, Theme::default_style)
- }
-
- /// Creates a new [`TextInput`] with the given placeholder,
- /// its current value, and its style.
- pub fn with_style(
- placeholder: &str,
- value: &str,
- style: impl Fn(&Theme, Status) -> Appearance + 'a,
- ) -> Self {
+ pub fn new(placeholder: &str, value: &str) -> Self {
TextInput {
id: None,
placeholder: String::from(placeholder),
@@ -116,7 +105,7 @@ where
on_paste: None,
on_submit: None,
icon: None,
- style: Box::new(style),
+ class: Theme::default(),
}
}
@@ -203,11 +192,19 @@ where
}
/// Sets the style of the [`TextInput`].
- pub fn style(
- mut self,
- style: impl Fn(&Theme, Status) -> Appearance + 'a,
- ) -> Self {
- self.style = Box::new(style);
+ #[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
}
@@ -235,7 +232,7 @@ where
let placeholder_text = Text {
font,
line_height: self.line_height,
- content: &self.placeholder,
+ content: self.placeholder.as_str(),
bounds: Size::new(f32::INFINITY, text_bounds.height),
size: text_size,
horizontal_alignment: alignment::Horizontal::Left,
@@ -254,9 +251,11 @@ where
});
if let Some(icon) = &self.icon {
+ let mut content = [0; 4];
+
let icon_text = Text {
line_height: self.line_height,
- content: &icon.code_point.to_string(),
+ 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),
@@ -345,15 +344,15 @@ where
Status::Active
};
- let appearance = (self.style)(theme, status);
+ 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,
);
if self.icon.is_some() {
@@ -362,14 +361,14 @@ where
renderer.fill_paragraph(
&state.icon,
icon_layout.bounds().center(),
- appearance.icon,
+ style.icon,
*viewport,
);
}
let text = value.to_string();
- let (cursor, offset) = if let Some(focus) = state
+ let (cursor, offset, is_selecting) = if let Some(focus) = state
.is_focused
.as_ref()
.filter(|focus| focus.is_window_focused)
@@ -401,13 +400,13 @@ where
},
..renderer::Quad::default()
},
- appearance.value,
+ style.value,
))
} else {
None
};
- (cursor, offset)
+ (cursor, offset, false)
}
cursor::State::Selection { start, end } => {
let left = start.min(end);
@@ -440,18 +439,19 @@ where
},
..renderer::Quad::default()
},
- appearance.selection,
+ style.selection,
)),
if end == right {
right_offset
} else {
left_offset
},
+ true,
)
}
}
} else {
- (None, 0.0)
+ (None, 0.0, false)
};
let draw = |renderer: &mut Renderer, viewport| {
@@ -475,15 +475,15 @@ where
Point::new(text_bounds.x, text_bounds.center_y())
- Vector::new(offset, 0.0),
if text.is_empty() {
- appearance.placeholder
+ style.placeholder
} else {
- appearance.value
+ style.value
},
viewport,
);
};
- if cursor.is_some() {
+ if is_selecting {
renderer
.with_layer(text_bounds, |renderer| draw(renderer, *viewport));
} else {
@@ -496,6 +496,7 @@ impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for TextInput<'a, Message, Theme, Renderer>
where
Message: Clone,
+ Theme: Catalog,
Renderer: text::Renderer,
{
fn tag(&self) -> tree::Tag {
@@ -712,7 +713,8 @@ where
match key.as_ref() {
keyboard::Key::Character("c")
- if state.keyboard_modifiers.command() =>
+ if state.keyboard_modifiers.command()
+ && !self.is_secure =>
{
if let Some((start, end)) =
state.cursor.selection(&self.value)
@@ -726,7 +728,8 @@ where
return event::Status::Captured;
}
keyboard::Key::Character("x")
- if state.keyboard_modifiers.command() =>
+ if state.keyboard_modifiers.command()
+ && !self.is_secure =>
{
if let Some((start, end)) =
state.cursor.selection(&self.value)
@@ -1058,8 +1061,8 @@ where
impl<'a, Message, Theme, Renderer> From<TextInput<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
- Message: 'a + Clone,
- Theme: 'a,
+ Message: Clone + 'a,
+ Theme: Catalog + 'a,
Renderer: text::Renderer + 'a,
{
fn from(
@@ -1400,7 +1403,7 @@ pub enum Status {
/// The appearance of a text input.
#[derive(Debug, Clone, Copy)]
-pub struct Appearance {
+pub struct Style {
/// The [`Background`] of the text input.
pub background: Background,
/// The [`Border`] of the text input.
@@ -1415,32 +1418,40 @@ pub struct Appearance {
pub selection: Color,
}
-/// The style of a [`TextInput`].
-pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>;
+/// The theme catalog of a [`TextInput`].
+pub trait Catalog: Sized {
+ /// The item class of the [`Catalog`].
+ type Class<'a>;
-/// The default style of a [`TextInput`].
-pub trait DefaultStyle {
- /// Returns the default style of a [`TextInput`].
- fn default_style(&self, status: Status) -> Appearance;
+ /// 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 DefaultStyle for Theme {
- fn default_style(&self, status: Status) -> Appearance {
- default(self, status)
+/// 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)
}
-}
-impl DefaultStyle for Appearance {
- fn default_style(&self, _status: Status) -> Appearance {
- *self
+ 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) -> Appearance {
+pub fn default(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
- let active = Appearance {
+ let active = Style {
background: Background::Color(palette.background.base.color),
border: Border {
radius: 2.0.into(),
@@ -1455,21 +1466,21 @@ pub fn default(theme: &Theme, status: Status) -> Appearance {
match status {
Status::Active => active,
- Status::Hovered => Appearance {
+ Status::Hovered => Style {
border: Border {
color: palette.background.base.text,
..active.border
},
..active
},
- Status::Focused => Appearance {
+ Status::Focused => Style {
border: Border {
color: palette.primary.strong.color,
..active.border
},
..active
},
- Status::Disabled => Appearance {
+ 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 a7eabd2c..f4597458 100644
--- a/widget/src/themer.rs
+++ b/widget/src/themer.rs
@@ -155,9 +155,9 @@ where
if let Some(background) = self.background {
container::draw_background(
renderer,
- &container::Appearance {
+ &container::Style {
background: Some(background(&theme)),
- ..container::Appearance::default()
+ ..container::Style::default()
},
layout.bounds(),
);
diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs
index fc9e06e1..ca6e37b0 100644
--- a/widget/src/toggler.rs
+++ b/widget/src/toggler.rs
@@ -35,6 +35,7 @@ pub struct Toggler<
Theme = crate::Theme,
Renderer = crate::Renderer,
> where
+ Theme: Catalog,
Renderer: text::Renderer,
{
is_toggled: bool,
@@ -48,11 +49,12 @@ pub struct Toggler<
text_shaping: text::Shaping,
spacing: f32,
font: Option<Renderer::Font>,
- style: Style<'a, Theme>,
+ class: Theme::Class<'a>,
}
impl<'a, Message, Theme, Renderer> Toggler<'a, Message, Theme, Renderer>
where
+ Theme: Catalog,
Renderer: text::Renderer,
{
/// The default size of a [`Toggler`].
@@ -72,7 +74,6 @@ where
f: F,
) -> Self
where
- Theme: 'a + DefaultStyle,
F: 'a + Fn(bool) -> Message,
{
Toggler {
@@ -87,7 +88,7 @@ where
text_shaping: text::Shaping::Basic,
spacing: Self::DEFAULT_SIZE / 2.0,
font: None,
- style: Box::new(Theme::default_style),
+ class: Theme::default(),
}
}
@@ -145,11 +146,20 @@ where
}
/// Sets the style of the [`Toggler`].
- pub fn style(
- mut self,
- style: impl Fn(&Theme, Status) -> Appearance + 'a,
- ) -> Self {
- self.style = Box::new(style);
+ #[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
}
}
@@ -157,6 +167,7 @@ where
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Toggler<'a, Message, Theme, Renderer>
where
+ Theme: Catalog,
Renderer: text::Renderer,
{
fn tag(&self) -> tree::Tag {
@@ -284,7 +295,7 @@ where
style,
label_layout,
tree.state.downcast_ref(),
- crate::text::Appearance::default(),
+ crate::text::Style::default(),
viewport,
);
}
@@ -302,7 +313,7 @@ where
}
};
- let appearance = (self.style)(theme, status);
+ let style = theme.style(&self.class, status);
let border_radius = bounds.height / BORDER_RADIUS_RATIO;
let space = SPACE_RATIO * bounds.height;
@@ -319,12 +330,12 @@ where
bounds: toggler_background_bounds,
border: Border {
radius: border_radius.into(),
- width: appearance.background_border_width,
- color: appearance.background_border_color,
+ width: style.background_border_width,
+ color: style.background_border_color,
},
..renderer::Quad::default()
},
- appearance.background,
+ style.background,
);
let toggler_foreground_bounds = Rectangle {
@@ -344,12 +355,12 @@ where
bounds: toggler_foreground_bounds,
border: Border {
radius: border_radius.into(),
- width: appearance.foreground_border_width,
- color: appearance.foreground_border_color,
+ width: style.foreground_border_width,
+ color: style.foreground_border_color,
},
..renderer::Quad::default()
},
- appearance.foreground,
+ style.foreground,
);
}
}
@@ -358,7 +369,7 @@ impl<'a, Message, Theme, Renderer> From<Toggler<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
- Theme: 'a,
+ Theme: Catalog + 'a,
Renderer: text::Renderer + 'a,
{
fn from(
@@ -385,7 +396,7 @@ pub enum Status {
/// The appearance of a toggler.
#[derive(Debug, Clone, Copy)]
-pub struct Appearance {
+pub struct Style {
/// The background [`Color`] of the toggler.
pub background: Color,
/// The width of the background border of the toggler.
@@ -400,29 +411,37 @@ pub struct Appearance {
pub foreground_border_color: Color,
}
-/// The style of a [`Toggler`].
-pub type Style<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Appearance + 'a>;
+/// The theme catalog of a [`Toggler`].
+pub trait Catalog: Sized {
+ /// The item class of the [`Catalog`].
+ type Class<'a>;
-/// The default style of a [`Toggler`].
-pub trait DefaultStyle {
- /// Returns the default style of a [`Toggler`].
- fn default_style(&self, status: Status) -> Appearance;
+ /// 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 DefaultStyle for Theme {
- fn default_style(&self, status: Status) -> Appearance {
- default(self, status)
+/// 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)
}
-}
-impl DefaultStyle for Appearance {
- fn default_style(&self, _status: Status) -> Appearance {
- *self
+ 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) -> Appearance {
+pub fn default(theme: &Theme, status: Status) -> Style {
let palette = theme.extended_palette();
let background = match status {
@@ -455,7 +474,7 @@ pub fn default(theme: &Theme, status: Status) -> Appearance {
}
};
- Appearance {
+ Style {
background,
foreground,
foreground_border_width: 0.0,
diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs
index 32c962fc..39f2e07d 100644
--- a/widget/src/tooltip.rs
+++ b/widget/src/tooltip.rs
@@ -20,6 +20,7 @@ pub struct Tooltip<
Theme = crate::Theme,
Renderer = crate::Renderer,
> where
+ Theme: container::Catalog,
Renderer: text::Renderer,
{
content: Element<'a, Message, Theme, Renderer>,
@@ -28,11 +29,12 @@ pub struct Tooltip<
gap: f32,
padding: f32,
snap_within_viewport: bool,
- style: container::Style<'a, Theme>,
+ class: Theme::Class<'a>,
}
impl<'a, Message, Theme, Renderer> Tooltip<'a, Message, Theme, Renderer>
where
+ Theme: container::Catalog,
Renderer: text::Renderer,
{
/// The default padding of a [`Tooltip`] drawn by this renderer.
@@ -45,10 +47,7 @@ where
content: impl Into<Element<'a, Message, Theme, Renderer>>,
tooltip: impl Into<Element<'a, Message, Theme, Renderer>>,
position: Position,
- ) -> Self
- where
- Theme: container::DefaultStyle + 'a,
- {
+ ) -> Self {
Tooltip {
content: content.into(),
tooltip: tooltip.into(),
@@ -56,7 +55,7 @@ where
gap: 0.0,
padding: Self::DEFAULT_PADDING,
snap_within_viewport: true,
- style: Box::new(Theme::default_style),
+ class: Theme::default(),
}
}
@@ -79,11 +78,23 @@ where
}
/// Sets the style of the [`Tooltip`].
+ #[must_use]
pub fn style(
mut self,
- style: impl Fn(&Theme, container::Status) -> container::Appearance + 'a,
- ) -> Self {
- self.style = Box::new(style);
+ 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
}
}
@@ -91,6 +102,7 @@ where
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Tooltip<'a, Message, Theme, Renderer>
where
+ 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: 'a,
+ Theme: container::Catalog + 'a,
Renderer: text::Renderer + 'a,
{
fn from(
@@ -299,6 +311,7 @@ enum State {
struct Overlay<'a, 'b, Message, Theme, Renderer>
where
+ Theme: container::Catalog,
Renderer: text::Renderer,
{
position: Point,
@@ -310,14 +323,14 @@ where
positioning: Position,
gap: f32,
padding: f32,
- style:
- &'b (dyn Fn(&Theme, container::Status) -> container::Appearance + 'a),
+ 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::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 = (self.style)(theme, container::Status::Idle);
+ 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 2aa8f4d1..defb442f 100644
--- a/widget/src/vertical_slider.rs
+++ b/widget/src/vertical_slider.rs
@@ -2,10 +2,9 @@
use std::ops::RangeInclusive;
pub use crate::slider::{
- default, Appearance, DefaultStyle, Handle, HandleShape, Status, Style,
+ 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
@@ -41,7 +40,10 @@ use crate::core::{
/// VerticalSlider::new(0.0..=100.0, value, Message::SliderChanged);
/// ```
#[allow(missing_debug_implementations)]
-pub struct VerticalSlider<'a, T, Message, Theme = crate::Theme> {
+pub struct VerticalSlider<'a, T, Message, Theme = crate::Theme>
+where
+ Theme: Catalog,
+{
range: RangeInclusive<T>,
step: T,
shift_step: Option<T>,
@@ -51,13 +53,14 @@ pub struct VerticalSlider<'a, T, Message, Theme = crate::Theme> {
on_release: Option<Message>,
width: f32,
height: Length,
- style: Style<'a, Theme>,
+ 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: Catalog,
{
/// The default width of a [`VerticalSlider`].
pub const DEFAULT_WIDTH: f32 = 16.0;
@@ -72,7 +75,6 @@ where
/// `Message`.
pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self
where
- Theme: DefaultStyle + 'a,
F: 'a + Fn(T) -> Message,
{
let value = if value >= *range.start() {
@@ -97,7 +99,7 @@ where
on_release: None,
width: Self::DEFAULT_WIDTH,
height: Length::Fill,
- style: Box::new(Theme::default_style),
+ class: Theme::default(),
}
}
@@ -132,15 +134,6 @@ where
self
}
- /// Sets the style of the [`VerticalSlider`].
- pub fn style(
- mut self,
- style: impl Fn(&Theme, Status) -> Appearance + 'a,
- ) -> Self {
- self.style = Box::new(style);
- self
- }
-
/// Sets the step size of the [`VerticalSlider`].
pub fn step(mut self, step: T) -> Self {
self.step = step;
@@ -154,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>
@@ -161,6 +172,7 @@ impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
where
T: Copy + Into<f64> + num_traits::FromPrimitive,
Message: Clone,
+ Theme: Catalog,
Renderer: core::Renderer,
{
fn tag(&self) -> tree::Tag {
@@ -354,8 +366,8 @@ where
let bounds = layout.bounds();
let is_mouse_over = cursor.is_over(bounds);
- let style = (self.style)(
- theme,
+ let style = theme.style(
+ &self.class,
if state.is_dragging {
Status::Dragged
} else if is_mouse_over {
@@ -467,7 +479,7 @@ impl<'a, T, Message, Theme, Renderer>
where
T: Copy + Into<f64> + num_traits::FromPrimitive + 'a,
Message: Clone + 'a,
- Theme: 'a,
+ Theme: Catalog + 'a,
Renderer: core::Renderer + 'a,
{
fn from(
diff --git a/winit/Cargo.toml b/winit/Cargo.toml
index 9d65cc1b..6d3dddde 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"]
debug = ["iced_runtime/debug"]
@@ -22,12 +25,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
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 +46,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 13d9282d..f7508b4c 100644
--- a/winit/src/application.rs
+++ b/winit/src/application.rs
@@ -13,6 +13,7 @@ use crate::core::window;
use crate::core::{Color, Event, Point, Size, Theme};
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;
@@ -21,7 +22,9 @@ use crate::runtime::{Command, Debug};
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;
@@ -128,9 +131,9 @@ pub fn default(theme: &Theme) -> Appearance {
/// Runs an [`Application`] with an executor, compositor, and the provided
/// settings.
-pub async fn run<A, E, C>(
+pub fn run<A, E, C>(
settings: Settings<A::Flags>,
- compositor_settings: C::Settings,
+ graphics_settings: graphics::Settings,
) -> Result<(), Error>
where
A: Application + 'static,
@@ -140,21 +143,22 @@ where
{
use futures::task;
use futures::Future;
- use winit::event_loop::EventLoopBuilder;
+ use winit::event_loop::EventLoop;
let mut debug = Debug::new();
debug.startup_started();
- 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) = {
@@ -163,104 +167,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);
+ let instance = Box::pin(run_instance::<A, E, C>(
+ application,
+ runtime,
+ proxy,
+ debug,
+ boot_receiver,
+ event_receiver,
+ control_sender,
+ init_command,
+ settings.fonts,
+ ));
- log::debug!("Window builder: {builder:#?}");
+ 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>>,
+ }
- let window = Arc::new(
- builder
- .build(&event_loop)
- .map_err(Error::WindowCreationFailed)?,
- );
+ struct BootConfig<C> {
+ sender: oneshot::Sender<Boot<C>>,
+ id: Option<String>,
+ title: String,
+ window_settings: window::Settings,
+ graphics_settings: graphics::Settings,
+ }
- #[cfg(target_arch = "wasm32")]
+ 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,
{
- use winit::platform::web::WindowExtWebSys;
+ 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 canvas = window.canvas().expect("Get window canvas");
- let _ = canvas.set_attribute(
- "style",
- "display: block; width: 100%; height: 100%",
- );
+ 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?;
- 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()));
+ 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();
}
- None => {
- let _ = body
- .append_child(&canvas)
- .expect("Append canvas to HTML body");
+
+ #[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;
+ });
}
- };
- }
+ }
- let compositor = C::new(compositor_settings, window.clone()).await?;
- let mut renderer = compositor.create_renderer();
+ fn new_events(
+ &mut self,
+ event_loop: &winit::event_loop::ActiveEventLoop,
+ cause: winit::event::StartCause,
+ ) {
+ if self.boot.is_some() {
+ return;
+ }
- for font in settings.fonts {
- use crate::core::text::Renderer;
+ self.process_event(
+ event_loop,
+ winit::event::Event::NewEvents(cause),
+ );
+ }
- renderer.load_font(font);
- }
+ 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,
+ );
+ }
+ }
+ }
- let (mut event_sender, event_receiver) = mpsc::unbounded();
- let (control_sender, mut control_receiver) = mpsc::unbounded();
+ fn user_event(
+ &mut self,
+ event_loop: &winit::event_loop::ActiveEventLoop,
+ message: Message,
+ ) {
+ self.process_event(
+ event_loop,
+ winit::event::Event::UserEvent(message),
+ );
+ }
- let mut instance = Box::pin(run_instance::<A, E, C>(
- application,
- compositor,
- renderer,
- runtime,
- proxy,
- debug,
- event_receiver,
- control_sender,
- init_command,
- window,
- should_be_visible,
- exit_on_close_request,
- ));
+ fn about_to_wait(
+ &mut self,
+ event_loop: &winit::event_loop::ActiveEventLoop,
+ ) {
+ self.process_event(event_loop, winit::event::Event::AboutToWait);
+ }
+ }
- let mut context = task::Context::from_waker(task::noop_waker_ref());
+ 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);
}
}
@@ -268,54 +450,45 @@ where
event_loop.exit();
}
}
- };
+ }
+ }
- #[cfg(not(target_os = "windows"))]
- let _ = event_loop.run(process_event);
+ #[cfg(not(target_arch = "wasm32"))]
+ {
+ let mut runner = runner;
+ let _ = event_loop.run_app(&mut runner);
- // 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")]
+ runner.error.map(Err).unwrap_or(Ok(()))
+ }
+
+ #[cfg(target_arch = "wasm32")]
{
- let mut process_event = process_event;
+ use winit::platform::web::EventLoopExtWebSys;
+ let _ = event_loop.spawn_app(runner);
- 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(())
}
+}
- 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 debug: Debug,
+ 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]>>,
) where
A: Application + 'static,
E: Executor + 'static,
@@ -326,6 +499,19 @@ async fn run_instance<A, E, C>(
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();
@@ -371,6 +557,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;
debug.startup_finished();
@@ -397,6 +584,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 { .. },
@@ -478,7 +666,7 @@ async fn run_instance<A, E, C>(
debug.draw_finished();
if new_mouse_interaction != mouse_interaction {
- window.set_cursor_icon(conversion::mouse_interaction(
+ window.set_cursor(conversion::mouse_interaction(
new_mouse_interaction,
));
@@ -594,6 +782,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 {
@@ -668,7 +861,7 @@ 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>,
debug: &mut Debug,
messages: &mut Vec<A::Message>,
window: &winit::window::Window,
@@ -718,7 +911,7 @@ 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>,
debug: &mut Debug,
window: &winit::window::Window,
) where
@@ -743,9 +936,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);
@@ -775,25 +966,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);
@@ -809,9 +991,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 {
@@ -836,9 +1016,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());
@@ -866,17 +1044,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));
}
}
@@ -889,12 +1063,10 @@ pub fn run_command<A, C, E>(
&debug.overlay(),
);
- 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 {
@@ -902,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 =
@@ -910,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);
});
}
}
@@ -935,9 +1105,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);
@@ -949,14 +1117,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/conversion.rs b/winit/src/conversion.rs
index fc3d1c08..ea33e610 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.
@@ -327,6 +323,35 @@ pub fn position(
y: f64::from(position.y),
}))
}
+ window::Position::SpecificWith(to_position) => {
+ if let Some(monitor) = monitor {
+ let start = monitor.position();
+
+ let resolution: winit::dpi::LogicalSize<f32> =
+ monitor.size().to_logical(monitor.scale_factor());
+
+ let position = to_position(
+ size,
+ Size::new(resolution.width, resolution.height),
+ );
+
+ let centered: winit::dpi::PhysicalPosition<i32> =
+ winit::dpi::LogicalPosition {
+ x: position.x,
+ y: position.y,
+ }
+ .to_physical(monitor.scale_factor());
+
+ Some(winit::dpi::Position::Physical(
+ winit::dpi::PhysicalPosition {
+ x: start.x + centered.x,
+ y: start.y + centered.y,
+ },
+ ))
+ } else {
+ None
+ }
+ }
window::Position::Centered => {
if let Some(monitor) = monitor {
let start = monitor.position();
@@ -396,7 +421,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 64912b3f..3619cde8 100644
--- a/winit/src/lib.rs
+++ b/winit/src/lib.rs
@@ -17,14 +17,6 @@
#![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;
diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs
index 18db1fb5..95d78b83 100644
--- a/winit/src/multi_window.rs
+++ b/winit/src/multi_window.rs
@@ -12,10 +12,12 @@ use crate::core::widget::operation;
use crate::core::window;
use crate::core::{Point, Size};
use crate::futures::futures::channel::mpsc;
+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};
@@ -26,7 +28,7 @@ use crate::{Clipboard, Error, Proxy, Settings};
pub use crate::application::{default, Appearance, DefaultStyle};
-use std::collections::HashMap;
+use rustc_hash::FxHashMap;
use std::mem::ManuallyDrop;
use std::sync::Arc;
use std::time::Instant;
@@ -105,7 +107,7 @@ 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,
@@ -113,22 +115,22 @@ where
C: Compositor<Renderer = A::Renderer> + 'static,
A::Theme: DefaultStyle,
{
- use winit::event_loop::EventLoopBuilder;
+ use winit::event_loop::EventLoop;
let mut debug = Debug::new();
debug.startup_started();
- 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) = {
@@ -137,187 +139,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 instance = Box::pin(run_instance::<A, E, C>(
+ application,
+ runtime,
+ proxy,
+ debug,
+ boot_receiver,
+ event_receiver,
+ control_sender,
+ init_command,
+ ));
- let main_window = Arc::new(
- builder
- .build(&event_loop)
- .map_err(Error::WindowCreationFailed)?,
- );
+ 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<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,
+ }
- #[cfg(target_arch = "wasm32")]
+ 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,
{
- use winit::platform::web::WindowExtWebSys;
+ 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 canvas = main_window.canvas();
+ let should_be_visible = window_settings.visible;
+ let exit_on_close_request = window_settings.exit_on_close_request;
- let window = web_sys::window().unwrap();
- let document = window.document().unwrap();
- let body = document.body().unwrap();
+ let window_attributes = conversion::window_attributes(
+ window_settings,
+ &title,
+ event_loop.primary_monitor(),
+ id,
+ )
+ .with_visible(false);
- let target = target.and_then(|target| {
- body.query_selector(&format!("#{}", target))
- .ok()
- .unwrap_or(None)
- });
+ log::debug!("Window attributes: {window_attributes:#?}");
- 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 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 mut compositor =
- executor::block_on(C::new(compositor_settings, main_window.clone()))?;
+ 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>(())
+ };
- let mut window_manager = WindowManager::new();
- let _ = window_manager.insert(
- window::Id::MAIN,
- main_window,
- &application,
- &mut compositor,
- exit_on_close_request,
- );
+ if let Err(error) = executor::block_on(finish_boot) {
+ self.error = Some(Error::GraphicsCreationFailed(error));
+ event_loop.exit();
+ }
+ }
- let (mut event_sender, event_receiver) = mpsc::unbounded();
- let (control_sender, mut control_receiver) = mpsc::unbounded();
+ fn new_events(
+ &mut self,
+ event_loop: &winit::event_loop::ActiveEventLoop,
+ cause: winit::event::StartCause,
+ ) {
+ if self.boot.is_some() {
+ return;
+ }
- let mut instance = Box::pin(run_instance::<A, E, C>(
- application,
- compositor,
- runtime,
- proxy,
- debug,
- event_receiver,
- control_sender,
- init_command,
- window_manager,
- should_main_be_visible,
- ));
+ 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,
+ ),
+ );
+ }
+ }
+ }
- let mut context = task::Context::from_waker(task::noop_waker_ref());
+ 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,
+ )),
+ );
+ }
- let process_event = move |event, event_loop: &winit::event_loop::EventLoopWindowTarget<_>| {
- if event_loop.exiting() {
- return;
+ fn about_to_wait(
+ &mut self,
+ event_loop: &winit::event_loop::ActiveEventLoop,
+ ) {
+ self.process_event(
+ event_loop,
+ Event::EventLoopAwakened(winit::event::Event::AboutToWait),
+ );
}
+ }
- 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);
+ 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 = 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();
+ 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;
}
- },
- 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);
+ };
}
- });
+ }
}
+ 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,
@@ -340,15 +447,13 @@ 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 debug: Debug,
+ 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,
) where
A: Application + 'static,
E: Executor + 'static,
@@ -358,11 +463,28 @@ async fn run_instance<A, E, C>(
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,
+ &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);
}
@@ -380,12 +502,12 @@ 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 debug,
&mut window_manager,
- HashMap::from_iter([(
+ FxHashMap::from_iter([(
window::Id::MAIN,
user_interface::Cache::default(),
)]),
@@ -407,6 +529,7 @@ async fn run_instance<A, E, C>(
runtime.track(application.subscription().into_recipes());
let mut messages = Vec::new();
+ let mut user_events = 0;
debug.startup_finished();
@@ -481,6 +604,7 @@ async fn run_instance<A, E, C>(
}
event::Event::UserEvent(message) => {
messages.push(message);
+ user_events += 1;
}
event::Event::WindowEvent {
window_id: id,
@@ -529,7 +653,7 @@ async fn run_instance<A, E, C>(
debug.draw_finished();
if new_mouse_interaction != window.mouse_interaction {
- window.raw.set_cursor_icon(
+ window.raw.set_cursor(
conversion::mouse_interaction(
new_mouse_interaction,
),
@@ -600,7 +724,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,
),
@@ -758,7 +882,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)
@@ -802,6 +926,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;
+ }
}
}
_ => {}
@@ -844,11 +973,11 @@ 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>,
debug: &mut Debug,
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: DefaultStyle,
@@ -886,10 +1015,10 @@ 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>,
debug: &mut Debug,
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,
@@ -912,9 +1041,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);
@@ -966,18 +1093,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) => {
@@ -987,9 +1108,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) => {
@@ -1011,9 +1130,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) => {
@@ -1048,9 +1165,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) => {
@@ -1098,9 +1213,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) => {
@@ -1110,9 +1223,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) => {
@@ -1125,12 +1236,10 @@ fn run_command<A, C, E>(
&debug.overlay(),
);
- 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(),
+ )));
}
}
},
@@ -1139,7 +1248,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 =
@@ -1147,9 +1256,7 @@ fn run_command<A, C, E>(
let message = _tag(information);
- proxy
- .send_event(message)
- .expect("Event loop doesn't exist.");
+ proxy.send(message);
});
}
}
@@ -1174,9 +1281,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;
@@ -1193,17 +1298,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");
@@ -1213,12 +1311,12 @@ 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,
debug: &mut Debug,
window_manager: &mut WindowManager<A, C>,
- mut cached_user_interfaces: HashMap<window::Id, user_interface::Cache>,
-) -> HashMap<window::Id, UserInterface<'a, A::Message, A::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
C: Compositor<Renderer = A::Renderer>,
A::Theme: DefaultStyle,
diff --git a/winit/src/multi_window/window_manager.rs b/winit/src/multi_window/window_manager.rs
index 71c1688b..57a7dc7e 100644
--- a/winit/src/multi_window/window_manager.rs
+++ b/winit/src/multi_window/window_manager.rs
@@ -9,8 +9,9 @@ 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: Application,
C: Compositor<Renderer = A::Renderer>,
A::Theme: DefaultStyle,
{
@@ -60,7 +61,7 @@ where
exit_on_close_request,
surface,
renderer,
- mouse_interaction: mouse::Interaction::Idle,
+ mouse_interaction: mouse::Interaction::None,
},
);
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(()))
}
}