summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Héctor Ramón Jiménez <hector@hecrj.dev>2024-01-19 20:41:52 +0100
committerLibravatar Héctor Ramón Jiménez <hector@hecrj.dev>2024-01-19 20:41:52 +0100
commit1781068e1c3a65551db1e832fdbaddba99124051 (patch)
tree60e0b3854cc0541712572fbb0e56f14435951ea9
parent41dec5bd203ff5b1574a33a17d5f7358ae1beea2 (diff)
parent7ae7fcb89855002519bab752fd3686106ce448db (diff)
downloadiced-1781068e1c3a65551db1e832fdbaddba99124051.tar.gz
iced-1781068e1c3a65551db1e832fdbaddba99124051.tar.bz2
iced-1781068e1c3a65551db1e832fdbaddba99124051.zip
Merge branch 'master' into remove-vertex-indexing
-rw-r--r--.cargo/config.toml2
-rw-r--r--.github/ISSUE_TEMPLATE/BUG-REPORT.yml1
-rw-r--r--.github/workflows/audit.yml6
-rw-r--r--.github/workflows/check.yml29
-rw-r--r--.github/workflows/document.yml3
-rw-r--r--.github/workflows/lint.yml2
-rw-r--r--.github/workflows/test.yml25
-rw-r--r--Cargo.toml37
-rw-r--r--ECOSYSTEM.md8
-rw-r--r--README.md2
-rw-r--r--core/Cargo.toml10
-rw-r--r--core/src/color.rs20
-rw-r--r--core/src/element.rs18
-rw-r--r--core/src/event.rs2
-rw-r--r--core/src/font.rs4
-rw-r--r--core/src/hasher.rs5
-rw-r--r--core/src/image.rs17
-rw-r--r--core/src/keyboard.rs7
-rw-r--r--core/src/keyboard/event.rs29
-rw-r--r--core/src/keyboard/key.rs744
-rw-r--r--core/src/keyboard/key_code.rs203
-rw-r--r--core/src/keyboard/location.rs12
-rw-r--r--core/src/layout.rs106
-rw-r--r--core/src/layout/flex.rs117
-rw-r--r--core/src/layout/limits.rs90
-rw-r--r--core/src/layout/node.rs35
-rw-r--r--core/src/length.rs18
-rw-r--r--core/src/lib.rs2
-rw-r--r--core/src/mouse/button.rs6
-rw-r--r--core/src/mouse/click.rs5
-rw-r--r--core/src/overlay.rs3
-rw-r--r--core/src/overlay/element.rs20
-rw-r--r--core/src/overlay/group.rs9
-rw-r--r--core/src/padding.rs6
-rw-r--r--core/src/point.rs74
-rw-r--r--core/src/renderer/null.rs94
-rw-r--r--core/src/size.rs24
-rw-r--r--core/src/text.rs178
-rw-r--r--core/src/text/editor.rs181
-rw-r--r--core/src/text/highlighter.rs88
-rw-r--r--core/src/text/paragraph.rs59
-rw-r--r--core/src/time.rs13
-rw-r--r--core/src/widget.rs15
-rw-r--r--core/src/widget/text.rs42
-rw-r--r--core/src/widget/tree.rs2
-rw-r--r--core/src/window.rs6
-rw-r--r--core/src/window/event.rs24
-rw-r--r--core/src/window/id.rs21
-rw-r--r--core/src/window/position.rs (renamed from winit/src/position.rs)6
-rw-r--r--core/src/window/settings.rs (renamed from src/window/settings.rs)73
-rw-r--r--core/src/window/settings/linux.rs (renamed from winit/src/settings/linux.rs)0
-rw-r--r--core/src/window/settings/macos.rs (renamed from winit/src/settings/macos.rs)0
-rw-r--r--core/src/window/settings/other.rs (renamed from winit/src/settings/other.rs)0
-rw-r--r--core/src/window/settings/wasm.rs (renamed from winit/src/settings/wasm.rs)0
-rw-r--r--core/src/window/settings/windows.rs (renamed from winit/src/settings/windows.rs)0
-rw-r--r--examples/custom_quad/src/main.rs11
-rw-r--r--examples/custom_shader/Cargo.toml17
-rw-r--r--examples/custom_shader/src/main.rs163
-rw-r--r--examples/custom_shader/src/scene.rs186
-rw-r--r--examples/custom_shader/src/scene/camera.rs53
-rw-r--r--examples/custom_shader/src/scene/pipeline.rs621
-rw-r--r--examples/custom_shader/src/scene/pipeline/buffer.rs41
-rw-r--r--examples/custom_shader/src/scene/pipeline/cube.rs326
-rw-r--r--examples/custom_shader/src/scene/pipeline/uniforms.rs23
-rw-r--r--examples/custom_shader/src/scene/pipeline/vertex.rs31
-rw-r--r--examples/custom_shader/src/shaders/cubes.wgsl123
-rw-r--r--examples/custom_shader/src/shaders/depth.wgsl48
-rw-r--r--examples/custom_shader/textures/ice_cube_normal_map.pngbin0 -> 1773656 bytes
-rw-r--r--examples/custom_shader/textures/skybox/neg_x.jpgbin0 -> 7549 bytes
-rw-r--r--examples/custom_shader/textures/skybox/neg_y.jpgbin0 -> 2722 bytes
-rw-r--r--examples/custom_shader/textures/skybox/neg_z.jpgbin0 -> 3986 bytes
-rw-r--r--examples/custom_shader/textures/skybox/pos_x.jpgbin0 -> 5522 bytes
-rw-r--r--examples/custom_shader/textures/skybox/pos_y.jpgbin0 -> 3382 bytes
-rw-r--r--examples/custom_shader/textures/skybox/pos_z.jpgbin0 -> 5205 bytes
-rw-r--r--examples/custom_widget/src/main.rs11
-rw-r--r--examples/download_progress/src/main.rs19
-rw-r--r--examples/editor/Cargo.toml15
-rw-r--r--examples/editor/fonts/icons.ttfbin0 -> 6352 bytes
-rw-r--r--examples/editor/src/main.rs312
-rw-r--r--examples/events/src/main.rs15
-rw-r--r--examples/exit/src/main.rs2
-rw-r--r--examples/game_of_life/Cargo.toml2
-rw-r--r--examples/game_of_life/src/main.rs4
-rw-r--r--examples/geometry/src/main.rs15
-rw-r--r--examples/integration/src/controls.rs39
-rw-r--r--examples/integration/src/main.rs142
-rw-r--r--examples/integration/src/scene.rs4
-rw-r--r--examples/layout/Cargo.toml9
-rw-r--r--examples/layout/src/main.rs371
-rw-r--r--examples/lazy/src/main.rs48
-rw-r--r--examples/loading_spinners/src/circular.rs24
-rw-r--r--examples/loading_spinners/src/linear.rs24
-rw-r--r--examples/loading_spinners/src/main.rs11
-rw-r--r--examples/modal/src/main.rs32
-rw-r--r--examples/multi_window/Cargo.toml9
-rw-r--r--examples/multi_window/src/main.rs215
-rw-r--r--examples/pane_grid/src/main.rs32
-rw-r--r--examples/pick_list/src/main.rs9
-rw-r--r--examples/progress_bar/README.md2
-rw-r--r--examples/screenshot/src/main.rs41
-rw-r--r--examples/scrollable/src/main.rs52
-rw-r--r--examples/sierpinski_triangle/src/main.rs2
-rw-r--r--examples/solar_system/src/main.rs14
-rw-r--r--examples/stopwatch/src/main.rs12
-rw-r--r--examples/styling/src/main.rs26
-rw-r--r--examples/svg/src/main.rs1
-rw-r--r--examples/toast/src/main.rs36
-rw-r--r--examples/todos/src/main.rs39
-rw-r--r--examples/tour/src/main.rs51
-rw-r--r--examples/vectorial_text/Cargo.toml9
-rw-r--r--examples/vectorial_text/src/main.rs175
-rw-r--r--examples/visible_bounds/src/main.rs2
-rw-r--r--examples/websocket/Cargo.toml2
-rw-r--r--examples/websocket/src/main.rs15
-rw-r--r--futures/src/event.rs2
-rw-r--r--futures/src/keyboard.rs16
-rw-r--r--futures/src/lib.rs4
-rw-r--r--futures/src/maybe.rs35
-rw-r--r--futures/src/maybe_send.rs21
-rw-r--r--futures/src/runtime.rs25
-rw-r--r--graphics/Cargo.toml19
-rw-r--r--graphics/src/backend.rs4
-rw-r--r--graphics/src/compositor.rs32
-rw-r--r--graphics/src/damage.rs12
-rw-r--r--graphics/src/geometry/text.rs135
-rw-r--r--graphics/src/lib.rs3
-rw-r--r--graphics/src/primitive.rs36
-rw-r--r--graphics/src/renderer.rs68
-rw-r--r--graphics/src/text.rs97
-rw-r--r--graphics/src/text/cache.rs25
-rw-r--r--graphics/src/text/editor.rs779
-rw-r--r--graphics/src/text/paragraph.rs186
-rw-r--r--highlighter/Cargo.toml17
-rw-r--r--highlighter/src/lib.rs245
-rw-r--r--renderer/Cargo.toml1
-rw-r--r--renderer/src/backend.rs100
-rw-r--r--renderer/src/compositor.rs68
-rw-r--r--renderer/src/lib.rs80
-rw-r--r--renderer/src/widget.rs11
-rw-r--r--runtime/Cargo.toml1
-rw-r--r--runtime/src/command.rs41
-rw-r--r--runtime/src/command/action.rs13
-rw-r--r--runtime/src/lib.rs3
-rw-r--r--runtime/src/multi_window.rs6
-rw-r--r--runtime/src/multi_window/program.rs32
-rw-r--r--runtime/src/multi_window/state.rs280
-rw-r--r--runtime/src/overlay/nested.rs15
-rw-r--r--runtime/src/user_interface.rs30
-rw-r--r--runtime/src/window.rs124
-rw-r--r--runtime/src/window/action.rs197
-rw-r--r--src/lib.rs16
-rw-r--r--src/multi_window.rs4
-rw-r--r--src/multi_window/application.rs245
-rw-r--r--src/settings.rs21
-rw-r--r--src/time.rs1
-rw-r--r--src/window.rs4
-rw-r--r--src/window/position.rs32
-rw-r--r--style/src/container.rs26
-rw-r--r--style/src/lib.rs1
-rw-r--r--style/src/svg.rs3
-rw-r--r--style/src/text_editor.rs47
-rw-r--r--style/src/theme.rs163
-rw-r--r--style/src/theme/palette.rs3
-rw-r--r--tiny_skia/Cargo.toml7
-rw-r--r--tiny_skia/src/backend.rs102
-rw-r--r--tiny_skia/src/geometry.rs122
-rw-r--r--tiny_skia/src/primitive.rs4
-rw-r--r--tiny_skia/src/raster.rs12
-rw-r--r--tiny_skia/src/text.rs90
-rw-r--r--tiny_skia/src/vector.rs14
-rw-r--r--tiny_skia/src/window/compositor.rs141
-rw-r--r--wgpu/Cargo.toml1
-rw-r--r--wgpu/src/backend.rs76
-rw-r--r--wgpu/src/color.rs4
-rw-r--r--wgpu/src/geometry.rs154
-rw-r--r--wgpu/src/image.rs129
-rw-r--r--wgpu/src/image/vector.rs26
-rw-r--r--wgpu/src/layer.rs64
-rw-r--r--wgpu/src/layer/image.rs3
-rw-r--r--wgpu/src/layer/pipeline.rs17
-rw-r--r--wgpu/src/layer/text.rs23
-rw-r--r--wgpu/src/lib.rs2
-rw-r--r--wgpu/src/primitive.rs9
-rw-r--r--wgpu/src/primitive/pipeline.rs116
-rw-r--r--wgpu/src/text.rs97
-rw-r--r--wgpu/src/triangle.rs7
-rw-r--r--wgpu/src/triangle/msaa.rs4
-rw-r--r--wgpu/src/window/compositor.rs70
-rw-r--r--widget/Cargo.toml1
-rw-r--r--widget/src/button.rs34
-rw-r--r--widget/src/canvas.rs21
-rw-r--r--widget/src/canvas/event.rs2
-rw-r--r--widget/src/checkbox.rs15
-rw-r--r--widget/src/column.rs79
-rw-r--r--widget/src/combo_box.rs29
-rw-r--r--widget/src/container.rs85
-rw-r--r--widget/src/helpers.rs51
-rw-r--r--widget/src/image.rs42
-rw-r--r--widget/src/image/viewer.rs25
-rw-r--r--widget/src/keyed/column.rs60
-rw-r--r--widget/src/lazy.rs16
-rw-r--r--widget/src/lazy/component.rs14
-rw-r--r--widget/src/lazy/helpers.rs3
-rw-r--r--widget/src/lazy/responsive.rs17
-rw-r--r--widget/src/lib.rs10
-rw-r--r--widget/src/mouse_area.rs10
-rw-r--r--widget/src/overlay/menu.rs25
-rw-r--r--widget/src/pane_grid.rs89
-rw-r--r--widget/src/pane_grid/content.rs9
-rw-r--r--widget/src/pane_grid/state.rs17
-rw-r--r--widget/src/pane_grid/title_bar.rs18
-rw-r--r--widget/src/pick_list.rs52
-rw-r--r--widget/src/progress_bar.rs23
-rw-r--r--widget/src/qr_code.rs11
-rw-r--r--widget/src/radio.rs14
-rw-r--r--widget/src/row.rs78
-rw-r--r--widget/src/rule.rs15
-rw-r--r--widget/src/scrollable.rs50
-rw-r--r--widget/src/shader.rs216
-rw-r--r--widget/src/shader/event.rs25
-rw-r--r--widget/src/shader/program.rs62
-rw-r--r--widget/src/slider.rs16
-rw-r--r--widget/src/space.rs15
-rw-r--r--widget/src/svg.rs25
-rw-r--r--widget/src/text_editor.rs736
-rw-r--r--widget/src/text_input.rs222
-rw-r--r--widget/src/toggler.rs14
-rw-r--r--widget/src/tooltip.rs27
-rw-r--r--widget/src/vertical_slider.rs16
-rw-r--r--winit/Cargo.toml2
-rw-r--r--winit/src/application.rs560
-rw-r--r--winit/src/application/profiler.rs101
-rw-r--r--winit/src/application/state.rs24
-rw-r--r--winit/src/clipboard.rs3
-rw-r--r--winit/src/conversion.rs751
-rw-r--r--winit/src/lib.rs9
-rw-r--r--winit/src/multi_window.rs1189
-rw-r--r--winit/src/multi_window/state.rs242
-rw-r--r--winit/src/multi_window/window_manager.rs157
-rw-r--r--winit/src/settings.rs228
240 files changed, 12774 insertions, 3393 deletions
diff --git a/.cargo/config.toml b/.cargo/config.toml
index 3e02dda8..85a46cda 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -17,8 +17,6 @@ clippy --workspace --no-deps -- \
-D clippy::useless_conversion
"""
-#![allow(clippy::inherent_to_string, clippy::type_complexity)]
-
nitpick = """
clippy --workspace --no-deps -- \
-D warnings \
diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml
index d4c94fcd..09b31697 100644
--- a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml
+++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml
@@ -25,7 +25,6 @@ body:
Before filing an issue...
- If you are using `wgpu`, you need an environment that supports Vulkan, Metal, or DirectX 12. Please, make sure you can run [the `wgpu` examples].
- - If you are using `glow`, you need support for OpenGL 2.1+. Please, make sure you can run [the `glow` examples].
If you have any issues running any of the examples, make sure your graphics drivers are up-to-date. If the issues persist, please report them to the authors of the libraries directly!
diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml
index e9f4b0c5..57169796 100644
--- a/.github/workflows/audit.yml
+++ b/.github/workflows/audit.yml
@@ -12,6 +12,8 @@ jobs:
- name: Install cargo-audit
run: cargo install cargo-audit
- uses: actions/checkout@master
+ - name: Resolve dependencies
+ run: cargo update
- name: Audit vulnerabilities
run: cargo audit
@@ -22,5 +24,7 @@ jobs:
- name: Install cargo-outdated
run: cargo install cargo-outdated
- uses: actions/checkout@master
+ - name: Delete `web-sys` dependency from `integration` example
+ run: sed -i '$d' examples/integration/Cargo.toml
- name: Find outdated dependencies
- run: cargo outdated --workspace --exit-code 1
+ run: cargo outdated --workspace --exit-code 1 --ignore raw-window-handle
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
new file mode 100644
index 00000000..df9c480f
--- /dev/null
+++ b/.github/workflows/check.yml
@@ -0,0 +1,29 @@
+name: Check
+on: [push, pull_request]
+jobs:
+ widget:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: hecrj/setup-rust-action@v1
+ - uses: actions/checkout@master
+ - name: Check standalone `iced_widget` crate
+ run: cargo check --package iced_widget --features image,svg,canvas
+
+ wasm:
+ runs-on: ubuntu-latest
+ env:
+ RUSTFLAGS: --cfg=web_sys_unstable_apis
+ steps:
+ - uses: hecrj/setup-rust-action@v1
+ with:
+ rust-version: stable
+ targets: wasm32-unknown-unknown
+ - uses: actions/checkout@master
+ - name: Run checks
+ run: cargo check --package iced --target wasm32-unknown-unknown
+ - name: Check compilation of `tour` example
+ run: cargo build --package tour --target wasm32-unknown-unknown
+ - name: Check compilation of `todos` example
+ run: cargo build --package todos --target wasm32-unknown-unknown
+ - name: Check compilation of `integration` example
+ run: cargo build --package integration --target wasm32-unknown-unknown
diff --git a/.github/workflows/document.yml b/.github/workflows/document.yml
index 230c5cb0..35bf10f4 100644
--- a/.github/workflows/document.yml
+++ b/.github/workflows/document.yml
@@ -8,13 +8,14 @@ jobs:
steps:
- uses: hecrj/setup-rust-action@v1
with:
- rust-version: nightly
+ rust-version: nightly-2023-12-11
- uses: actions/checkout@v2
- name: Generate documentation
run: |
RUSTDOCFLAGS="--cfg docsrs" \
cargo doc --no-deps --all-features \
-p iced_core \
+ -p iced_highlighter \
-p iced_style \
-p iced_futures \
-p iced_runtime \
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 7fdc8867..2ff86614 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -2,7 +2,7 @@ name: Lint
on: [push, pull_request]
jobs:
all:
- runs-on: ubuntu-latest
+ runs-on: macOS-latest
steps:
- uses: hecrj/setup-rust-action@v1
with:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index ac8d27f9..9c5ee0d9 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,8 +1,10 @@
name: Test
on: [push, pull_request]
jobs:
- native:
+ all:
runs-on: ${{ matrix.os }}
+ env:
+ RUSTFLAGS: --deny warnings
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
@@ -17,27 +19,8 @@ jobs:
run: |
export DEBIAN_FRONTED=noninteractive
sudo apt-get -qq update
- sudo apt-get install -y libxkbcommon-dev
+ sudo apt-get install -y libxkbcommon-dev libgtk-3-dev
- name: Run tests
run: |
cargo test --verbose --workspace
cargo test --verbose --workspace --all-features
-
- web:
- runs-on: ubuntu-latest
- env:
- RUSTFLAGS: --cfg=web_sys_unstable_apis
- steps:
- - uses: hecrj/setup-rust-action@v1
- with:
- rust-version: stable
- targets: wasm32-unknown-unknown
- - uses: actions/checkout@master
- - name: Run checks
- run: cargo check --package iced --target wasm32-unknown-unknown
- - name: Check compilation of `tour` example
- run: cargo build --package tour --target wasm32-unknown-unknown
- - name: Check compilation of `todos` example
- run: cargo build --package todos --target wasm32-unknown-unknown
- - name: Check compilation of `integration` example
- run: cargo build --package integration --target wasm32-unknown-unknown
diff --git a/Cargo.toml b/Cargo.toml
index af74a3cf..c9dee6b7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,7 +20,7 @@ maintenance = { status = "actively-developed" }
[features]
default = ["wgpu"]
# Enable the `wgpu` GPU-accelerated renderer backend
-wgpu = ["iced_renderer/wgpu"]
+wgpu = ["iced_renderer/wgpu", "iced_widget/wgpu"]
# Enables the `Image` widget
image = ["iced_widget/image", "dep:image"]
# Enables the `Svg` widget
@@ -47,6 +47,10 @@ system = ["iced_winit/system"]
web-colors = ["iced_renderer/web-colors"]
# Enables the WebGL backend, replacing WebGPU
webgl = ["iced_renderer/webgl"]
+# Enables the syntax `highlighter` module
+highlighter = ["iced_highlighter"]
+# Enables experimental multi-window support.
+multi-window = ["iced_winit/multi-window"]
# Enables the advanced module
advanced = []
@@ -58,6 +62,9 @@ iced_widget.workspace = true
iced_winit.features = ["application"]
iced_winit.workspace = true
+iced_highlighter.workspace = true
+iced_highlighter.optional = true
+
thiserror.workspace = true
image.workspace = true
@@ -78,8 +85,9 @@ members = [
"core",
"futures",
"graphics",
- "runtime",
+ "highlighter",
"renderer",
+ "runtime",
"style",
"tiny_skia",
"wgpu",
@@ -103,6 +111,7 @@ iced = { version = "0.12", path = "." }
iced_core = { version = "0.12", path = "core" }
iced_futures = { version = "0.12", path = "futures" }
iced_graphics = { version = "0.12", path = "graphics" }
+iced_highlighter = { version = "0.12", path = "highlighter" }
iced_renderer = { version = "0.12", path = "renderer" }
iced_runtime = { version = "0.12", path = "runtime" }
iced_style = { version = "0.12", path = "style" }
@@ -114,14 +123,13 @@ iced_winit = { version = "0.12", path = "winit" }
async-std = "1.0"
bitflags = "1.0"
bytemuck = { version = "1.0", features = ["derive"] }
-cosmic-text = "0.9"
+cosmic-text = "0.10"
futures = "0.3"
glam = "0.24"
-glyphon = { git = "https://github.com/grovesNL/glyphon.git", rev = "20f0f8fa80e0d0df4c63634ce9176fa489546ca9" }
+glyphon = "0.5"
guillotiere = "0.6"
half = "2.2"
image = "0.24"
-instant = "0.1"
kamadak-exif = "0.5"
kurbo = "0.9"
log = "0.4"
@@ -132,22 +140,25 @@ once_cell = "1.0"
ouroboros = "0.17"
palette = "0.7"
qrcode = { version = "0.12", default-features = false }
-raw-window-handle = "0.5"
-resvg = "0.35"
+raw-window-handle = "0.6"
+resvg = "0.36"
rustc-hash = "1.0"
smol = "1.0"
-softbuffer = "0.2"
+smol_str = "0.2"
+softbuffer = "0.4"
+syntect = "5.1"
sysinfo = "0.28"
thiserror = "1.0"
-tiny-skia = "0.10"
+tiny-skia = "0.11"
tokio = "1.0"
tracing = "0.1"
-twox-hash = { version = "1.0", default-features = false }
+xxhash-rust = { version = "0.8", features = ["xxh3"] }
unicode-segmentation = "1.0"
wasm-bindgen-futures = "0.4"
wasm-timer = "0.2"
web-sys = "0.3"
-wgpu = "0.17"
+web-time = "0.2"
+wgpu = "0.19"
winapi = "0.3"
-window_clipboard = "0.3"
-winit = { git = "https://github.com/iced-rs/winit.git", rev = "c52db2045d0a2f1b8d9923870de1d4ab1994146e", default-features = false }
+window_clipboard = "0.4"
+winit = { git = "https://github.com/iced-rs/winit.git", rev = "b91e39ece2c0d378c3b80da7f3ab50e17bb798a5" }
diff --git a/ECOSYSTEM.md b/ECOSYSTEM.md
index 86581e4a..da3066d8 100644
--- a/ECOSYSTEM.md
+++ b/ECOSYSTEM.md
@@ -45,7 +45,7 @@ The widgets of a _graphical_ user interface produce some primitives that eventua
Currently, there are two different official renderers:
- [`iced_wgpu`] is powered by [`wgpu`] and supports Vulkan, DirectX 12, and Metal.
-- [`iced_glow`] is powered by [`glow`] and supports OpenGL 2.1+ and OpenGL ES 2.0+.
+- [`tiny-skia`] is used as a fallback software renderer when `wgpu` is not supported.
Additionally, the [`iced_graphics`] subcrate contains a bunch of backend-agnostic types that can be leveraged to build renderers. Both of the renderers rely on the graphical foundations provided by this crate.
@@ -54,10 +54,7 @@ The widgets of a graphical user _interface_ are interactive. __Shells__ gather a
Normally, a shell will be responsible of creating a window and managing the lifecycle of a user interface, implementing a runtime of [The Elm Architecture].
-As of now, there are two official shells:
-
-- [`iced_winit`] implements a shell runtime on top of [`winit`].
-- [`iced_glutin`] is similar to [`iced_winit`], but it also deals with [OpenGL context creation].
+As of now, there is one official shell: [`iced_winit`] implements a shell runtime on top of [`winit`].
## The web target
The Web platform provides all the abstractions necessary to draw widgets and gather users interactions.
@@ -91,5 +88,4 @@ Finally, [`iced`] unifies everything into a simple abstraction to create cross-p
[`winit`]: https://github.com/rust-windowing/winit
[`glutin`]: https://github.com/rust-windowing/glutin
[`dodrio`]: https://github.com/fitzgen/dodrio
-[OpenGL context creation]: https://www.khronos.org/opengl/wiki/Creating_an_OpenGL_Context
[The Elm Architecture]: https://guide.elm-lang.org/architecture/
diff --git a/README.md b/README.md
index 825219aa..eb2befbc 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
[![License](https://img.shields.io/crates/l/iced.svg)](https://github.com/iced-rs/iced/blob/master/LICENSE)
[![Downloads](https://img.shields.io/crates/d/iced.svg)](https://crates.io/crates/iced)
[![Test Status](https://img.shields.io/github/actions/workflow/status/iced-rs/iced/test.yml?branch=master&event=push&label=test)](https://github.com/iced-rs/iced/actions)
-[![Discourse](https://img.shields.io/discourse/users?server=https%3A%2F%2Fdiscourse.iced.rs&color=5e7ce2)](https://discourse.iced.rs/)
+[![Discourse](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdiscourse.iced.rs%2Fsite%2Fstatistics.json&query=%24.users_count&suffix=%20users&label=discourse&color=5e7ce2)](https://discourse.iced.rs/)
[![Discord Server](https://img.shields.io/discord/628993209984614400?label=&labelColor=6A7EC2&logo=discord&logoColor=ffffff&color=7389D8)](https://discord.gg/3xZJ65GAhd)
A cross-platform GUI library for Rust focused on simplicity and type-safety.
diff --git a/core/Cargo.toml b/core/Cargo.toml
index 7acb7511..32dd3df2 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -13,15 +13,17 @@ keywords.workspace = true
[dependencies]
bitflags.workspace = true
log.workspace = true
-thiserror.workspace = true
-twox-hash.workspace = true
num-traits.workspace = true
+smol_str.workspace = true
+thiserror.workspace = true
+web-time.workspace = true
+xxhash-rust.workspace = true
palette.workspace = true
palette.optional = true
-[target.'cfg(target_arch = "wasm32")'.dependencies]
-instant.workspace = true
+[target.'cfg(windows)'.dependencies]
+raw-window-handle.workspace = true
[dev-dependencies]
approx = "0.5"
diff --git a/core/src/color.rs b/core/src/color.rs
index 0e8b7475..13077628 100644
--- a/core/src/color.rs
+++ b/core/src/color.rs
@@ -89,6 +89,26 @@ impl Color {
}
}
+ /// Creates a [`Color`] from its linear RGBA components.
+ pub fn from_linear_rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
+ // As described in:
+ // https://en.wikipedia.org/wiki/SRGB
+ fn gamma_component(u: f32) -> f32 {
+ if u < 0.0031308 {
+ 12.92 * u
+ } else {
+ 1.055 * u.powf(1.0 / 2.4) - 0.055
+ }
+ }
+
+ Self {
+ r: gamma_component(r),
+ g: gamma_component(g),
+ b: gamma_component(b),
+ a,
+ }
+ }
+
/// Converts the [`Color`] into its RGBA8 equivalent.
#[must_use]
pub fn into_rgba8(self) -> [u8; 4] {
diff --git a/core/src/element.rs b/core/src/element.rs
index dea111af..8b510218 100644
--- a/core/src/element.rs
+++ b/core/src/element.rs
@@ -6,7 +6,7 @@ use crate::renderer;
use crate::widget;
use crate::widget::tree::{self, Tree};
use crate::{
- Clipboard, Color, Layout, Length, Rectangle, Shell, Vector, Widget,
+ Clipboard, Color, Layout, Length, Rectangle, Shell, Size, Vector, Widget,
};
use std::any::Any;
@@ -296,12 +296,8 @@ where
self.widget.diff(tree);
}
- fn width(&self) -> Length {
- self.widget.width()
- }
-
- fn height(&self) -> Length {
- self.widget.height()
+ fn size(&self) -> Size<Length> {
+ self.widget.size()
}
fn layout(
@@ -466,12 +462,8 @@ impl<'a, Message, Renderer> Widget<Message, Renderer>
where
Renderer: crate::Renderer,
{
- fn width(&self) -> Length {
- self.element.widget.width()
- }
-
- fn height(&self) -> Length {
- self.element.widget.height()
+ fn size(&self) -> Size<Length> {
+ self.element.widget.size()
}
fn tag(&self) -> tree::Tag {
diff --git a/core/src/event.rs b/core/src/event.rs
index 953cd73f..870b3074 100644
--- a/core/src/event.rs
+++ b/core/src/event.rs
@@ -19,7 +19,7 @@ pub enum Event {
Mouse(mouse::Event),
/// A window event
- Window(window::Event),
+ Window(window::Id, window::Event),
/// A touch event
Touch(touch::Event),
diff --git a/core/src/font.rs b/core/src/font.rs
index 7f647847..2b68decf 100644
--- a/core/src/font.rs
+++ b/core/src/font.rs
@@ -12,8 +12,6 @@ pub struct Font {
pub stretch: Stretch,
/// The [`Style`] of the [`Font`].
pub style: Style,
- /// Whether if the [`Font`] is monospaced or not.
- pub monospaced: bool,
}
impl Font {
@@ -23,13 +21,11 @@ impl Font {
weight: Weight::Normal,
stretch: Stretch::Normal,
style: Style::Normal,
- monospaced: false,
};
/// A monospaced font with normal [`Weight`].
pub const MONOSPACE: Font = Font {
family: Family::Monospace,
- monospaced: true,
..Self::DEFAULT
};
diff --git a/core/src/hasher.rs b/core/src/hasher.rs
index 9d8f75b3..a13d78af 100644
--- a/core/src/hasher.rs
+++ b/core/src/hasher.rs
@@ -1,6 +1,7 @@
/// The hasher used to compare layouts.
-#[derive(Debug, Default)]
-pub struct Hasher(twox_hash::XxHash64);
+#[allow(missing_debug_implementations)] // Doesn't really make sense to have debug on the hasher state anyways.
+#[derive(Default)]
+pub struct Hasher(xxhash_rust::xxh3::Xxh3);
impl core::hash::Hasher for Hasher {
fn write(&mut self, bytes: &[u8]) {
diff --git a/core/src/image.rs b/core/src/image.rs
index 85d9d475..e9675316 100644
--- a/core/src/image.rs
+++ b/core/src/image.rs
@@ -164,6 +164,16 @@ impl std::fmt::Debug for Data {
}
}
+/// Image filtering strategy.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
+pub enum FilterMethod {
+ /// Bilinear interpolation.
+ #[default]
+ Linear,
+ /// Nearest neighbor.
+ Nearest,
+}
+
/// A [`Renderer`] that can render raster graphics.
///
/// [renderer]: crate::renderer
@@ -178,5 +188,10 @@ pub trait Renderer: crate::Renderer {
/// Draws an image with the given [`Handle`] and inside the provided
/// `bounds`.
- fn draw(&mut self, handle: Self::Handle, bounds: Rectangle);
+ fn draw(
+ &mut self,
+ handle: Self::Handle,
+ filter_method: FilterMethod,
+ bounds: Rectangle,
+ );
}
diff --git a/core/src/keyboard.rs b/core/src/keyboard.rs
index 4c6ca08d..b810ccb0 100644
--- a/core/src/keyboard.rs
+++ b/core/src/keyboard.rs
@@ -1,8 +1,11 @@
//! Listen to keyboard events.
+pub mod key;
+
mod event;
-mod key_code;
+mod location;
mod modifiers;
pub use event::Event;
-pub use key_code::KeyCode;
+pub use key::Key;
+pub use location::Location;
pub use modifiers::Modifiers;
diff --git a/core/src/keyboard/event.rs b/core/src/keyboard/event.rs
index 016761af..1eb42334 100644
--- a/core/src/keyboard/event.rs
+++ b/core/src/keyboard/event.rs
@@ -1,4 +1,5 @@
-use super::{KeyCode, Modifiers};
+use crate::keyboard::{Key, Location, Modifiers};
+use crate::SmolStr;
/// A keyboard event.
///
@@ -6,29 +7,35 @@ use super::{KeyCode, Modifiers};
/// additional events, feel free to [open an issue] and share your use case!_
///
/// [open an issue]: https://github.com/iced-rs/iced/issues
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Event {
/// A keyboard key was pressed.
KeyPressed {
- /// The key identifier
- key_code: KeyCode,
+ /// The key pressed.
+ key: Key,
- /// The state of the modifier keys
+ /// The location of the key.
+ location: Location,
+
+ /// The state of the modifier keys.
modifiers: Modifiers,
+
+ /// The text produced by the key press, if any.
+ text: Option<SmolStr>,
},
/// A keyboard key was released.
KeyReleased {
- /// The key identifier
- key_code: KeyCode,
+ /// The key released.
+ key: Key,
- /// The state of the modifier keys
+ /// The location of the key.
+ location: Location,
+
+ /// The state of the modifier keys.
modifiers: Modifiers,
},
- /// A unicode character was received.
- CharacterReceived(char),
-
/// The keyboard modifiers have changed.
ModifiersChanged(Modifiers),
}
diff --git a/core/src/keyboard/key.rs b/core/src/keyboard/key.rs
new file mode 100644
index 00000000..dbde5196
--- /dev/null
+++ b/core/src/keyboard/key.rs
@@ -0,0 +1,744 @@
+//! Identify keyboard keys.
+use crate::SmolStr;
+
+/// A key on the keyboard.
+///
+/// This is mostly the `Key` type found in [`winit`].
+///
+/// [`winit`]: https://docs.rs/winit/0.29.10/winit/keyboard/enum.Key.html
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum Key<C = SmolStr> {
+ /// A key with an established name.
+ Named(Named),
+
+ /// A key string that corresponds to the character typed by the user, taking into account the
+ /// user’s current locale setting, and any system-level keyboard mapping overrides that are in
+ /// effect.
+ Character(C),
+
+ /// An unidentified key.
+ Unidentified,
+}
+
+impl Key {
+ /// Convert `Key::Character(SmolStr)` to `Key::Character(&str)` so you can more easily match on
+ /// `Key`. All other variants remain unchanged.
+ pub fn as_ref(&self) -> Key<&str> {
+ match self {
+ Self::Named(named) => Key::Named(*named),
+ Self::Character(c) => Key::Character(c.as_ref()),
+ Self::Unidentified => Key::Unidentified,
+ }
+ }
+}
+
+/// A named key.
+///
+/// This is mostly the `NamedKey` type found in [`winit`].
+///
+/// [`winit`]: https://docs.rs/winit/0.29.10/winit/keyboard/enum.Key.html
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[allow(missing_docs)]
+pub enum Named {
+ /// The `Alt` (Alternative) key.
+ ///
+ /// This key enables the alternate modifier function for interpreting concurrent or subsequent
+ /// keyboard input. This key value is also used for the Apple <kbd>Option</kbd> key.
+ Alt,
+ /// The Alternate Graphics (<kbd>AltGr</kbd> or <kbd>AltGraph</kbd>) key.
+ ///
+ /// This key is used enable the ISO Level 3 shift modifier (the standard `Shift` key is the
+ /// level 2 modifier).
+ AltGraph,
+ /// The `Caps Lock` (Capital) key.
+ ///
+ /// Toggle capital character lock function for interpreting subsequent keyboard input event.
+ CapsLock,
+ /// The `Control` or `Ctrl` key.
+ ///
+ /// Used to enable control modifier function for interpreting concurrent or subsequent keyboard
+ /// input.
+ Control,
+ /// The Function switch `Fn` key. Activating this key simultaneously with another key changes
+ /// that key’s value to an alternate character or function. This key is often handled directly
+ /// in the keyboard hardware and does not usually generate key events.
+ Fn,
+ /// The Function-Lock (`FnLock` or `F-Lock`) key. Activating this key switches the mode of the
+ /// keyboard to changes some keys' values to an alternate character or function. This key is
+ /// often handled directly in the keyboard hardware and does not usually generate key events.
+ FnLock,
+ /// The `NumLock` or Number Lock key. Used to toggle numpad mode function for interpreting
+ /// subsequent keyboard input.
+ NumLock,
+ /// Toggle between scrolling and cursor movement modes.
+ ScrollLock,
+ /// Used to enable shift modifier function for interpreting concurrent or subsequent keyboard
+ /// input.
+ Shift,
+ /// The Symbol modifier key (used on some virtual keyboards).
+ Symbol,
+ SymbolLock,
+ // Legacy modifier key. Also called "Super" in certain places.
+ Meta,
+ // Legacy modifier key.
+ Hyper,
+ /// Used to enable "super" modifier function for interpreting concurrent or subsequent keyboard
+ /// input. This key value is used for the "Windows Logo" key and the Apple `Command` or `⌘` key.
+ ///
+ /// Note: In some contexts (e.g. the Web) this is referred to as the "Meta" key.
+ Super,
+ /// The `Enter` or `↵` key. Used to activate current selection or accept current input. This key
+ /// value is also used for the `Return` (Macintosh numpad) key. This key value is also used for
+ /// the Android `KEYCODE_DPAD_CENTER`.
+ Enter,
+ /// The Horizontal Tabulation `Tab` key.
+ Tab,
+ /// Used in text to insert a space between words. Usually located below the character keys.
+ Space,
+ /// Navigate or traverse downward. (`KEYCODE_DPAD_DOWN`)
+ ArrowDown,
+ /// Navigate or traverse leftward. (`KEYCODE_DPAD_LEFT`)
+ ArrowLeft,
+ /// Navigate or traverse rightward. (`KEYCODE_DPAD_RIGHT`)
+ ArrowRight,
+ /// Navigate or traverse upward. (`KEYCODE_DPAD_UP`)
+ ArrowUp,
+ /// The End key, used with keyboard entry to go to the end of content (`KEYCODE_MOVE_END`).
+ End,
+ /// The Home key, used with keyboard entry, to go to start of content (`KEYCODE_MOVE_HOME`).
+ /// For the mobile phone `Home` key (which goes to the phone’s main screen), use [`GoHome`].
+ ///
+ /// [`GoHome`]: Self::GoHome
+ Home,
+ /// Scroll down or display next page of content.
+ PageDown,
+ /// Scroll up or display previous page of content.
+ PageUp,
+ /// Used to remove the character to the left of the cursor. This key value is also used for
+ /// the key labeled `Delete` on MacOS keyboards.
+ Backspace,
+ /// Remove the currently selected input.
+ Clear,
+ /// Copy the current selection. (`APPCOMMAND_COPY`)
+ Copy,
+ /// The Cursor Select key.
+ CrSel,
+ /// Cut the current selection. (`APPCOMMAND_CUT`)
+ Cut,
+ /// Used to delete the character to the right of the cursor. This key value is also used for the
+ /// key labeled `Delete` on MacOS keyboards when `Fn` is active.
+ Delete,
+ /// The Erase to End of Field key. This key deletes all characters from the current cursor
+ /// position to the end of the current field.
+ EraseEof,
+ /// The Extend Selection (Exsel) key.
+ ExSel,
+ /// Toggle between text modes for insertion or overtyping.
+ /// (`KEYCODE_INSERT`)
+ Insert,
+ /// The Paste key. (`APPCOMMAND_PASTE`)
+ Paste,
+ /// Redo the last action. (`APPCOMMAND_REDO`)
+ Redo,
+ /// Undo the last action. (`APPCOMMAND_UNDO`)
+ Undo,
+ /// The Accept (Commit, OK) key. Accept current option or input method sequence conversion.
+ Accept,
+ /// Redo or repeat an action.
+ Again,
+ /// The Attention (Attn) key.
+ Attn,
+ Cancel,
+ /// Show the application’s context menu.
+ /// This key is commonly found between the right `Super` key and the right `Control` key.
+ ContextMenu,
+ /// The `Esc` key. This key was originally used to initiate an escape sequence, but is
+ /// now more generally used to exit or "escape" the current context, such as closing a dialog
+ /// or exiting full screen mode.
+ Escape,
+ Execute,
+ /// Open the Find dialog. (`APPCOMMAND_FIND`)
+ Find,
+ /// Open a help dialog or toggle display of help information. (`APPCOMMAND_HELP`,
+ /// `KEYCODE_HELP`)
+ Help,
+ /// Pause the current state or application (as appropriate).
+ ///
+ /// Note: Do not use this value for the `Pause` button on media controllers. Use `"MediaPause"`
+ /// instead.
+ Pause,
+ /// Play or resume the current state or application (as appropriate).
+ ///
+ /// Note: Do not use this value for the `Play` button on media controllers. Use `"MediaPlay"`
+ /// instead.
+ Play,
+ /// The properties (Props) key.
+ Props,
+ Select,
+ /// The ZoomIn key. (`KEYCODE_ZOOM_IN`)
+ ZoomIn,
+ /// The ZoomOut key. (`KEYCODE_ZOOM_OUT`)
+ ZoomOut,
+ /// The Brightness Down key. Typically controls the display brightness.
+ /// (`KEYCODE_BRIGHTNESS_DOWN`)
+ BrightnessDown,
+ /// The Brightness Up key. Typically controls the display brightness. (`KEYCODE_BRIGHTNESS_UP`)
+ BrightnessUp,
+ /// Toggle removable media to eject (open) and insert (close) state. (`KEYCODE_MEDIA_EJECT`)
+ Eject,
+ LogOff,
+ /// Toggle power state. (`KEYCODE_POWER`)
+ /// Note: Note: Some devices might not expose this key to the operating environment.
+ Power,
+ /// The `PowerOff` key. Sometime called `PowerDown`.
+ PowerOff,
+ /// Initiate print-screen function.
+ PrintScreen,
+ /// The Hibernate key. This key saves the current state of the computer to disk so that it can
+ /// be restored. The computer will then shutdown.
+ Hibernate,
+ /// The Standby key. This key turns off the display and places the computer into a low-power
+ /// mode without completely shutting down. It is sometimes labelled `Suspend` or `Sleep` key.
+ /// (`KEYCODE_SLEEP`)
+ Standby,
+ /// The WakeUp key. (`KEYCODE_WAKEUP`)
+ WakeUp,
+ /// Initate the multi-candidate mode.
+ AllCandidates,
+ Alphanumeric,
+ /// Initiate the Code Input mode to allow characters to be entered by
+ /// their code points.
+ CodeInput,
+ /// The Compose key, also known as "Multi_key" on the X Window System. This key acts in a
+ /// manner similar to a dead key, triggering a mode where subsequent key presses are combined to
+ /// produce a different character.
+ Compose,
+ /// Convert the current input method sequence.
+ Convert,
+ /// The Final Mode `Final` key used on some Asian keyboards, to enable the final mode for IMEs.
+ FinalMode,
+ /// Switch to the first character group. (ISO/IEC 9995)
+ GroupFirst,
+ /// Switch to the last character group. (ISO/IEC 9995)
+ GroupLast,
+ /// Switch to the next character group. (ISO/IEC 9995)
+ GroupNext,
+ /// Switch to the previous character group. (ISO/IEC 9995)
+ GroupPrevious,
+ /// Toggle between or cycle through input modes of IMEs.
+ ModeChange,
+ NextCandidate,
+ /// Accept current input method sequence without
+ /// conversion in IMEs.
+ NonConvert,
+ PreviousCandidate,
+ Process,
+ SingleCandidate,
+ /// Toggle between Hangul and English modes.
+ HangulMode,
+ HanjaMode,
+ JunjaMode,
+ /// The Eisu key. This key may close the IME, but its purpose is defined by the current IME.
+ /// (`KEYCODE_EISU`)
+ Eisu,
+ /// The (Half-Width) Characters key.
+ Hankaku,
+ /// The Hiragana (Japanese Kana characters) key.
+ Hiragana,
+ /// The Hiragana/Katakana toggle key. (`KEYCODE_KATAKANA_HIRAGANA`)
+ HiraganaKatakana,
+ /// The Kana Mode (Kana Lock) key. This key is used to enter hiragana mode (typically from
+ /// romaji mode).
+ KanaMode,
+ /// The Kanji (Japanese name for ideographic characters of Chinese origin) Mode key. This key is
+ /// typically used to switch to a hiragana keyboard for the purpose of converting input into
+ /// kanji. (`KEYCODE_KANA`)
+ KanjiMode,
+ /// The Katakana (Japanese Kana characters) key.
+ Katakana,
+ /// The Roman characters function key.
+ Romaji,
+ /// The Zenkaku (Full-Width) Characters key.
+ Zenkaku,
+ /// The Zenkaku/Hankaku (full-width/half-width) toggle key. (`KEYCODE_ZENKAKU_HANKAKU`)
+ ZenkakuHankaku,
+ /// General purpose virtual function key, as index 1.
+ Soft1,
+ /// General purpose virtual function key, as index 2.
+ Soft2,
+ /// General purpose virtual function key, as index 3.
+ Soft3,
+ /// General purpose virtual function key, as index 4.
+ Soft4,
+ /// Select next (numerically or logically) lower channel. (`APPCOMMAND_MEDIA_CHANNEL_DOWN`,
+ /// `KEYCODE_CHANNEL_DOWN`)
+ ChannelDown,
+ /// Select next (numerically or logically) higher channel. (`APPCOMMAND_MEDIA_CHANNEL_UP`,
+ /// `KEYCODE_CHANNEL_UP`)
+ ChannelUp,
+ /// Close the current document or message (Note: This doesn’t close the application).
+ /// (`APPCOMMAND_CLOSE`)
+ Close,
+ /// Open an editor to forward the current message. (`APPCOMMAND_FORWARD_MAIL`)
+ MailForward,
+ /// Open an editor to reply to the current message. (`APPCOMMAND_REPLY_TO_MAIL`)
+ MailReply,
+ /// Send the current message. (`APPCOMMAND_SEND_MAIL`)
+ MailSend,
+ /// Close the current media, for example to close a CD or DVD tray. (`KEYCODE_MEDIA_CLOSE`)
+ MediaClose,
+ /// Initiate or continue forward playback at faster than normal speed, or increase speed if
+ /// already fast forwarding. (`APPCOMMAND_MEDIA_FAST_FORWARD`, `KEYCODE_MEDIA_FAST_FORWARD`)
+ MediaFastForward,
+ /// Pause the currently playing media. (`APPCOMMAND_MEDIA_PAUSE`, `KEYCODE_MEDIA_PAUSE`)
+ ///
+ /// Note: Media controller devices should use this value rather than `"Pause"` for their pause
+ /// keys.
+ MediaPause,
+ /// Initiate or continue media playback at normal speed, if not currently playing at normal
+ /// speed. (`APPCOMMAND_MEDIA_PLAY`, `KEYCODE_MEDIA_PLAY`)
+ MediaPlay,
+ /// Toggle media between play and pause states. (`APPCOMMAND_MEDIA_PLAY_PAUSE`,
+ /// `KEYCODE_MEDIA_PLAY_PAUSE`)
+ MediaPlayPause,
+ /// Initiate or resume recording of currently selected media. (`APPCOMMAND_MEDIA_RECORD`,
+ /// `KEYCODE_MEDIA_RECORD`)
+ MediaRecord,
+ /// Initiate or continue reverse playback at faster than normal speed, or increase speed if
+ /// already rewinding. (`APPCOMMAND_MEDIA_REWIND`, `KEYCODE_MEDIA_REWIND`)
+ MediaRewind,
+ /// Stop media playing, pausing, forwarding, rewinding, or recording, if not already stopped.
+ /// (`APPCOMMAND_MEDIA_STOP`, `KEYCODE_MEDIA_STOP`)
+ MediaStop,
+ /// Seek to next media or program track. (`APPCOMMAND_MEDIA_NEXTTRACK`, `KEYCODE_MEDIA_NEXT`)
+ MediaTrackNext,
+ /// Seek to previous media or program track. (`APPCOMMAND_MEDIA_PREVIOUSTRACK`,
+ /// `KEYCODE_MEDIA_PREVIOUS`)
+ MediaTrackPrevious,
+ /// Open a new document or message. (`APPCOMMAND_NEW`)
+ New,
+ /// Open an existing document or message. (`APPCOMMAND_OPEN`)
+ Open,
+ /// Print the current document or message. (`APPCOMMAND_PRINT`)
+ Print,
+ /// Save the current document or message. (`APPCOMMAND_SAVE`)
+ Save,
+ /// Spellcheck the current document or selection. (`APPCOMMAND_SPELL_CHECK`)
+ SpellCheck,
+ /// The `11` key found on media numpads that
+ /// have buttons from `1` ... `12`.
+ Key11,
+ /// The `12` key found on media numpads that
+ /// have buttons from `1` ... `12`.
+ Key12,
+ /// Adjust audio balance leftward. (`VK_AUDIO_BALANCE_LEFT`)
+ AudioBalanceLeft,
+ /// Adjust audio balance rightward. (`VK_AUDIO_BALANCE_RIGHT`)
+ AudioBalanceRight,
+ /// Decrease audio bass boost or cycle down through bass boost states. (`APPCOMMAND_BASS_DOWN`,
+ /// `VK_BASS_BOOST_DOWN`)
+ AudioBassBoostDown,
+ /// Toggle bass boost on/off. (`APPCOMMAND_BASS_BOOST`)
+ AudioBassBoostToggle,
+ /// Increase audio bass boost or cycle up through bass boost states. (`APPCOMMAND_BASS_UP`,
+ /// `VK_BASS_BOOST_UP`)
+ AudioBassBoostUp,
+ /// Adjust audio fader towards front. (`VK_FADER_FRONT`)
+ AudioFaderFront,
+ /// Adjust audio fader towards rear. (`VK_FADER_REAR`)
+ AudioFaderRear,
+ /// Advance surround audio mode to next available mode. (`VK_SURROUND_MODE_NEXT`)
+ AudioSurroundModeNext,
+ /// Decrease treble. (`APPCOMMAND_TREBLE_DOWN`)
+ AudioTrebleDown,
+ /// Increase treble. (`APPCOMMAND_TREBLE_UP`)
+ AudioTrebleUp,
+ /// Decrease audio volume. (`APPCOMMAND_VOLUME_DOWN`, `KEYCODE_VOLUME_DOWN`)
+ AudioVolumeDown,
+ /// Increase audio volume. (`APPCOMMAND_VOLUME_UP`, `KEYCODE_VOLUME_UP`)
+ AudioVolumeUp,
+ /// Toggle between muted state and prior volume level. (`APPCOMMAND_VOLUME_MUTE`,
+ /// `KEYCODE_VOLUME_MUTE`)
+ AudioVolumeMute,
+ /// Toggle the microphone on/off. (`APPCOMMAND_MIC_ON_OFF_TOGGLE`)
+ MicrophoneToggle,
+ /// Decrease microphone volume. (`APPCOMMAND_MICROPHONE_VOLUME_DOWN`)
+ MicrophoneVolumeDown,
+ /// Increase microphone volume. (`APPCOMMAND_MICROPHONE_VOLUME_UP`)
+ MicrophoneVolumeUp,
+ /// Mute the microphone. (`APPCOMMAND_MICROPHONE_VOLUME_MUTE`, `KEYCODE_MUTE`)
+ MicrophoneVolumeMute,
+ /// Show correction list when a word is incorrectly identified. (`APPCOMMAND_CORRECTION_LIST`)
+ SpeechCorrectionList,
+ /// Toggle between dictation mode and command/control mode.
+ /// (`APPCOMMAND_DICTATE_OR_COMMAND_CONTROL_TOGGLE`)
+ SpeechInputToggle,
+ /// The first generic "LaunchApplication" key. This is commonly associated with launching "My
+ /// Computer", and may have a computer symbol on the key. (`APPCOMMAND_LAUNCH_APP1`)
+ LaunchApplication1,
+ /// The second generic "LaunchApplication" key. This is commonly associated with launching
+ /// "Calculator", and may have a calculator symbol on the key. (`APPCOMMAND_LAUNCH_APP2`,
+ /// `KEYCODE_CALCULATOR`)
+ LaunchApplication2,
+ /// The "Calendar" key. (`KEYCODE_CALENDAR`)
+ LaunchCalendar,
+ /// The "Contacts" key. (`KEYCODE_CONTACTS`)
+ LaunchContacts,
+ /// The "Mail" key. (`APPCOMMAND_LAUNCH_MAIL`)
+ LaunchMail,
+ /// The "Media Player" key. (`APPCOMMAND_LAUNCH_MEDIA_SELECT`)
+ LaunchMediaPlayer,
+ LaunchMusicPlayer,
+ LaunchPhone,
+ LaunchScreenSaver,
+ LaunchSpreadsheet,
+ LaunchWebBrowser,
+ LaunchWebCam,
+ LaunchWordProcessor,
+ /// Navigate to previous content or page in current history. (`APPCOMMAND_BROWSER_BACKWARD`)
+ BrowserBack,
+ /// Open the list of browser favorites. (`APPCOMMAND_BROWSER_FAVORITES`)
+ BrowserFavorites,
+ /// Navigate to next content or page in current history. (`APPCOMMAND_BROWSER_FORWARD`)
+ BrowserForward,
+ /// Go to the user’s preferred home page. (`APPCOMMAND_BROWSER_HOME`)
+ BrowserHome,
+ /// Refresh the current page or content. (`APPCOMMAND_BROWSER_REFRESH`)
+ BrowserRefresh,
+ /// Call up the user’s preferred search page. (`APPCOMMAND_BROWSER_SEARCH`)
+ BrowserSearch,
+ /// Stop loading the current page or content. (`APPCOMMAND_BROWSER_STOP`)
+ BrowserStop,
+ /// The Application switch key, which provides a list of recent apps to switch between.
+ /// (`KEYCODE_APP_SWITCH`)
+ AppSwitch,
+ /// The Call key. (`KEYCODE_CALL`)
+ Call,
+ /// The Camera key. (`KEYCODE_CAMERA`)
+ Camera,
+ /// The Camera focus key. (`KEYCODE_FOCUS`)
+ CameraFocus,
+ /// The End Call key. (`KEYCODE_ENDCALL`)
+ EndCall,
+ /// The Back key. (`KEYCODE_BACK`)
+ GoBack,
+ /// The Home key, which goes to the phone’s main screen. (`KEYCODE_HOME`)
+ GoHome,
+ /// The Headset Hook key. (`KEYCODE_HEADSETHOOK`)
+ HeadsetHook,
+ LastNumberRedial,
+ /// The Notification key. (`KEYCODE_NOTIFICATION`)
+ Notification,
+ /// Toggle between manner mode state: silent, vibrate, ring, ... (`KEYCODE_MANNER_MODE`)
+ MannerMode,
+ VoiceDial,
+ /// Switch to viewing TV. (`KEYCODE_TV`)
+ TV,
+ /// TV 3D Mode. (`KEYCODE_3D_MODE`)
+ TV3DMode,
+ /// Toggle between antenna and cable input. (`KEYCODE_TV_ANTENNA_CABLE`)
+ TVAntennaCable,
+ /// Audio description. (`KEYCODE_TV_AUDIO_DESCRIPTION`)
+ TVAudioDescription,
+ /// Audio description mixing volume down. (`KEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN`)
+ TVAudioDescriptionMixDown,
+ /// Audio description mixing volume up. (`KEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP`)
+ TVAudioDescriptionMixUp,
+ /// Contents menu. (`KEYCODE_TV_CONTENTS_MENU`)
+ TVContentsMenu,
+ /// Contents menu. (`KEYCODE_TV_DATA_SERVICE`)
+ TVDataService,
+ /// Switch the input mode on an external TV. (`KEYCODE_TV_INPUT`)
+ TVInput,
+ /// Switch to component input #1. (`KEYCODE_TV_INPUT_COMPONENT_1`)
+ TVInputComponent1,
+ /// Switch to component input #2. (`KEYCODE_TV_INPUT_COMPONENT_2`)
+ TVInputComponent2,
+ /// Switch to composite input #1. (`KEYCODE_TV_INPUT_COMPOSITE_1`)
+ TVInputComposite1,
+ /// Switch to composite input #2. (`KEYCODE_TV_INPUT_COMPOSITE_2`)
+ TVInputComposite2,
+ /// Switch to HDMI input #1. (`KEYCODE_TV_INPUT_HDMI_1`)
+ TVInputHDMI1,
+ /// Switch to HDMI input #2. (`KEYCODE_TV_INPUT_HDMI_2`)
+ TVInputHDMI2,
+ /// Switch to HDMI input #3. (`KEYCODE_TV_INPUT_HDMI_3`)
+ TVInputHDMI3,
+ /// Switch to HDMI input #4. (`KEYCODE_TV_INPUT_HDMI_4`)
+ TVInputHDMI4,
+ /// Switch to VGA input #1. (`KEYCODE_TV_INPUT_VGA_1`)
+ TVInputVGA1,
+ /// Media context menu. (`KEYCODE_TV_MEDIA_CONTEXT_MENU`)
+ TVMediaContext,
+ /// Toggle network. (`KEYCODE_TV_NETWORK`)
+ TVNetwork,
+ /// Number entry. (`KEYCODE_TV_NUMBER_ENTRY`)
+ TVNumberEntry,
+ /// Toggle the power on an external TV. (`KEYCODE_TV_POWER`)
+ TVPower,
+ /// Radio. (`KEYCODE_TV_RADIO_SERVICE`)
+ TVRadioService,
+ /// Satellite. (`KEYCODE_TV_SATELLITE`)
+ TVSatellite,
+ /// Broadcast Satellite. (`KEYCODE_TV_SATELLITE_BS`)
+ TVSatelliteBS,
+ /// Communication Satellite. (`KEYCODE_TV_SATELLITE_CS`)
+ TVSatelliteCS,
+ /// Toggle between available satellites. (`KEYCODE_TV_SATELLITE_SERVICE`)
+ TVSatelliteToggle,
+ /// Analog Terrestrial. (`KEYCODE_TV_TERRESTRIAL_ANALOG`)
+ TVTerrestrialAnalog,
+ /// Digital Terrestrial. (`KEYCODE_TV_TERRESTRIAL_DIGITAL`)
+ TVTerrestrialDigital,
+ /// Timer programming. (`KEYCODE_TV_TIMER_PROGRAMMING`)
+ TVTimer,
+ /// Switch the input mode on an external AVR (audio/video receiver). (`KEYCODE_AVR_INPUT`)
+ AVRInput,
+ /// Toggle the power on an external AVR (audio/video receiver). (`KEYCODE_AVR_POWER`)
+ AVRPower,
+ /// General purpose color-coded media function key, as index 0 (red). (`VK_COLORED_KEY_0`,
+ /// `KEYCODE_PROG_RED`)
+ ColorF0Red,
+ /// General purpose color-coded media function key, as index 1 (green). (`VK_COLORED_KEY_1`,
+ /// `KEYCODE_PROG_GREEN`)
+ ColorF1Green,
+ /// General purpose color-coded media function key, as index 2 (yellow). (`VK_COLORED_KEY_2`,
+ /// `KEYCODE_PROG_YELLOW`)
+ ColorF2Yellow,
+ /// General purpose color-coded media function key, as index 3 (blue). (`VK_COLORED_KEY_3`,
+ /// `KEYCODE_PROG_BLUE`)
+ ColorF3Blue,
+ /// General purpose color-coded media function key, as index 4 (grey). (`VK_COLORED_KEY_4`)
+ ColorF4Grey,
+ /// General purpose color-coded media function key, as index 5 (brown). (`VK_COLORED_KEY_5`)
+ ColorF5Brown,
+ /// Toggle the display of Closed Captions. (`VK_CC`, `KEYCODE_CAPTIONS`)
+ ClosedCaptionToggle,
+ /// Adjust brightness of device, by toggling between or cycling through states. (`VK_DIMMER`)
+ Dimmer,
+ /// Swap video sources. (`VK_DISPLAY_SWAP`)
+ DisplaySwap,
+ /// Select Digital Video Rrecorder. (`KEYCODE_DVR`)
+ DVR,
+ /// Exit the current application. (`VK_EXIT`)
+ Exit,
+ /// Clear program or content stored as favorite 0. (`VK_CLEAR_FAVORITE_0`)
+ FavoriteClear0,
+ /// Clear program or content stored as favorite 1. (`VK_CLEAR_FAVORITE_1`)
+ FavoriteClear1,
+ /// Clear program or content stored as favorite 2. (`VK_CLEAR_FAVORITE_2`)
+ FavoriteClear2,
+ /// Clear program or content stored as favorite 3. (`VK_CLEAR_FAVORITE_3`)
+ FavoriteClear3,
+ /// Select (recall) program or content stored as favorite 0. (`VK_RECALL_FAVORITE_0`)
+ FavoriteRecall0,
+ /// Select (recall) program or content stored as favorite 1. (`VK_RECALL_FAVORITE_1`)
+ FavoriteRecall1,
+ /// Select (recall) program or content stored as favorite 2. (`VK_RECALL_FAVORITE_2`)
+ FavoriteRecall2,
+ /// Select (recall) program or content stored as favorite 3. (`VK_RECALL_FAVORITE_3`)
+ FavoriteRecall3,
+ /// Store current program or content as favorite 0. (`VK_STORE_FAVORITE_0`)
+ FavoriteStore0,
+ /// Store current program or content as favorite 1. (`VK_STORE_FAVORITE_1`)
+ FavoriteStore1,
+ /// Store current program or content as favorite 2. (`VK_STORE_FAVORITE_2`)
+ FavoriteStore2,
+ /// Store current program or content as favorite 3. (`VK_STORE_FAVORITE_3`)
+ FavoriteStore3,
+ /// Toggle display of program or content guide. (`VK_GUIDE`, `KEYCODE_GUIDE`)
+ Guide,
+ /// If guide is active and displayed, then display next day’s content. (`VK_NEXT_DAY`)
+ GuideNextDay,
+ /// If guide is active and displayed, then display previous day’s content. (`VK_PREV_DAY`)
+ GuidePreviousDay,
+ /// Toggle display of information about currently selected context or media. (`VK_INFO`,
+ /// `KEYCODE_INFO`)
+ Info,
+ /// Toggle instant replay. (`VK_INSTANT_REPLAY`)
+ InstantReplay,
+ /// Launch linked content, if available and appropriate. (`VK_LINK`)
+ Link,
+ /// List the current program. (`VK_LIST`)
+ ListProgram,
+ /// Toggle display listing of currently available live content or programs. (`VK_LIVE`)
+ LiveContent,
+ /// Lock or unlock current content or program. (`VK_LOCK`)
+ Lock,
+ /// Show a list of media applications: audio/video players and image viewers. (`VK_APPS`)
+ ///
+ /// Note: Do not confuse this key value with the Windows' `VK_APPS` / `VK_CONTEXT_MENU` key,
+ /// which is encoded as `"ContextMenu"`.
+ MediaApps,
+ /// Audio track key. (`KEYCODE_MEDIA_AUDIO_TRACK`)
+ MediaAudioTrack,
+ /// Select previously selected channel or media. (`VK_LAST`, `KEYCODE_LAST_CHANNEL`)
+ MediaLast,
+ /// Skip backward to next content or program. (`KEYCODE_MEDIA_SKIP_BACKWARD`)
+ MediaSkipBackward,
+ /// Skip forward to next content or program. (`VK_SKIP`, `KEYCODE_MEDIA_SKIP_FORWARD`)
+ MediaSkipForward,
+ /// Step backward to next content or program. (`KEYCODE_MEDIA_STEP_BACKWARD`)
+ MediaStepBackward,
+ /// Step forward to next content or program. (`KEYCODE_MEDIA_STEP_FORWARD`)
+ MediaStepForward,
+ /// Media top menu. (`KEYCODE_MEDIA_TOP_MENU`)
+ MediaTopMenu,
+ /// Navigate in. (`KEYCODE_NAVIGATE_IN`)
+ NavigateIn,
+ /// Navigate to next key. (`KEYCODE_NAVIGATE_NEXT`)
+ NavigateNext,
+ /// Navigate out. (`KEYCODE_NAVIGATE_OUT`)
+ NavigateOut,
+ /// Navigate to previous key. (`KEYCODE_NAVIGATE_PREVIOUS`)
+ NavigatePrevious,
+ /// Cycle to next favorite channel (in favorites list). (`VK_NEXT_FAVORITE_CHANNEL`)
+ NextFavoriteChannel,
+ /// Cycle to next user profile (if there are multiple user profiles). (`VK_USER`)
+ NextUserProfile,
+ /// Access on-demand content or programs. (`VK_ON_DEMAND`)
+ OnDemand,
+ /// Pairing key to pair devices. (`KEYCODE_PAIRING`)
+ Pairing,
+ /// Move picture-in-picture window down. (`VK_PINP_DOWN`)
+ PinPDown,
+ /// Move picture-in-picture window. (`VK_PINP_MOVE`)
+ PinPMove,
+ /// Toggle display of picture-in-picture window. (`VK_PINP_TOGGLE`)
+ PinPToggle,
+ /// Move picture-in-picture window up. (`VK_PINP_UP`)
+ PinPUp,
+ /// Decrease media playback speed. (`VK_PLAY_SPEED_DOWN`)
+ PlaySpeedDown,
+ /// Reset playback to normal speed. (`VK_PLAY_SPEED_RESET`)
+ PlaySpeedReset,
+ /// Increase media playback speed. (`VK_PLAY_SPEED_UP`)
+ PlaySpeedUp,
+ /// Toggle random media or content shuffle mode. (`VK_RANDOM_TOGGLE`)
+ RandomToggle,
+ /// Not a physical key, but this key code is sent when the remote control battery is low.
+ /// (`VK_RC_LOW_BATTERY`)
+ RcLowBattery,
+ /// Toggle or cycle between media recording speeds. (`VK_RECORD_SPEED_NEXT`)
+ RecordSpeedNext,
+ /// Toggle RF (radio frequency) input bypass mode (pass RF input directly to the RF output).
+ /// (`VK_RF_BYPASS`)
+ RfBypass,
+ /// Toggle scan channels mode. (`VK_SCAN_CHANNELS_TOGGLE`)
+ ScanChannelsToggle,
+ /// Advance display screen mode to next available mode. (`VK_SCREEN_MODE_NEXT`)
+ ScreenModeNext,
+ /// Toggle display of device settings screen. (`VK_SETTINGS`, `KEYCODE_SETTINGS`)
+ Settings,
+ /// Toggle split screen mode. (`VK_SPLIT_SCREEN_TOGGLE`)
+ SplitScreenToggle,
+ /// Switch the input mode on an external STB (set top box). (`KEYCODE_STB_INPUT`)
+ STBInput,
+ /// Toggle the power on an external STB (set top box). (`KEYCODE_STB_POWER`)
+ STBPower,
+ /// Toggle display of subtitles, if available. (`VK_SUBTITLE`)
+ Subtitle,
+ /// Toggle display of teletext, if available (`VK_TELETEXT`, `KEYCODE_TV_TELETEXT`).
+ Teletext,
+ /// Advance video mode to next available mode. (`VK_VIDEO_MODE_NEXT`)
+ VideoModeNext,
+ /// Cause device to identify itself in some manner, e.g., audibly or visibly. (`VK_WINK`)
+ Wink,
+ /// Toggle between full-screen and scaled content, or alter magnification level. (`VK_ZOOM`,
+ /// `KEYCODE_TV_ZOOM_MODE`)
+ ZoomToggle,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F1,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F2,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F3,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F4,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F5,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F6,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F7,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F8,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F9,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F10,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F11,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F12,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F13,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F14,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F15,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F16,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F17,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F18,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F19,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F20,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F21,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F22,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F23,
+ /// General-purpose function key.
+ /// Usually found at the top of the keyboard.
+ F24,
+ /// General-purpose function key.
+ F25,
+ /// General-purpose function key.
+ F26,
+ /// General-purpose function key.
+ F27,
+ /// General-purpose function key.
+ F28,
+ /// General-purpose function key.
+ F29,
+ /// General-purpose function key.
+ F30,
+ /// General-purpose function key.
+ F31,
+ /// General-purpose function key.
+ F32,
+ /// General-purpose function key.
+ F33,
+ /// General-purpose function key.
+ F34,
+ /// General-purpose function key.
+ F35,
+}
diff --git a/core/src/keyboard/key_code.rs b/core/src/keyboard/key_code.rs
deleted file mode 100644
index 74ead170..00000000
--- a/core/src/keyboard/key_code.rs
+++ /dev/null
@@ -1,203 +0,0 @@
-/// The symbolic name of a keyboard key.
-///
-/// This is mostly the `KeyCode` type found in [`winit`].
-///
-/// [`winit`]: https://docs.rs/winit/0.20.0-alpha3/winit/
-#[derive(Debug, Hash, Ord, PartialOrd, PartialEq, Eq, Clone, Copy)]
-#[repr(u32)]
-#[allow(missing_docs)]
-pub enum KeyCode {
- /// The '1' key over the letters.
- Key1,
- /// The '2' key over the letters.
- Key2,
- /// The '3' key over the letters.
- Key3,
- /// The '4' key over the letters.
- Key4,
- /// The '5' key over the letters.
- Key5,
- /// The '6' key over the letters.
- Key6,
- /// The '7' key over the letters.
- Key7,
- /// The '8' key over the letters.
- Key8,
- /// The '9' key over the letters.
- Key9,
- /// The '0' key over the 'O' and 'P' keys.
- Key0,
-
- A,
- B,
- C,
- D,
- E,
- F,
- G,
- H,
- I,
- J,
- K,
- L,
- M,
- N,
- O,
- P,
- Q,
- R,
- S,
- T,
- U,
- V,
- W,
- X,
- Y,
- Z,
-
- /// The Escape key, next to F1.
- Escape,
-
- F1,
- F2,
- F3,
- F4,
- F5,
- F6,
- F7,
- F8,
- F9,
- F10,
- F11,
- F12,
- F13,
- F14,
- F15,
- F16,
- F17,
- F18,
- F19,
- F20,
- F21,
- F22,
- F23,
- F24,
-
- /// Print Screen/SysRq.
- Snapshot,
- /// Scroll Lock.
- Scroll,
- /// Pause/Break key, next to Scroll lock.
- Pause,
-
- /// `Insert`, next to Backspace.
- Insert,
- Home,
- Delete,
- End,
- PageDown,
- PageUp,
-
- Left,
- Up,
- Right,
- Down,
-
- /// The Backspace key, right over Enter.
- Backspace,
- /// The Enter key.
- Enter,
- /// The space bar.
- Space,
-
- /// The "Compose" key on Linux.
- Compose,
-
- Caret,
-
- Numlock,
- Numpad0,
- Numpad1,
- Numpad2,
- Numpad3,
- Numpad4,
- Numpad5,
- Numpad6,
- Numpad7,
- Numpad8,
- Numpad9,
- NumpadAdd,
- NumpadDivide,
- NumpadDecimal,
- NumpadComma,
- NumpadEnter,
- NumpadEquals,
- NumpadMultiply,
- NumpadSubtract,
-
- AbntC1,
- AbntC2,
- Apostrophe,
- Apps,
- Asterisk,
- At,
- Ax,
- Backslash,
- Calculator,
- Capital,
- Colon,
- Comma,
- Convert,
- Equals,
- Grave,
- Kana,
- Kanji,
- LAlt,
- LBracket,
- LControl,
- LShift,
- LWin,
- Mail,
- MediaSelect,
- MediaStop,
- Minus,
- Mute,
- MyComputer,
- NavigateForward, // also called "Next"
- NavigateBackward, // also called "Prior"
- NextTrack,
- NoConvert,
- OEM102,
- Period,
- PlayPause,
- Plus,
- Power,
- PrevTrack,
- RAlt,
- RBracket,
- RControl,
- RShift,
- RWin,
- Semicolon,
- Slash,
- Sleep,
- Stop,
- Sysrq,
- Tab,
- Underline,
- Unlabeled,
- VolumeDown,
- VolumeUp,
- Wake,
- WebBack,
- WebFavorites,
- WebForward,
- WebHome,
- WebRefresh,
- WebSearch,
- WebStop,
- Yen,
- Copy,
- Paste,
- Cut,
-}
diff --git a/core/src/keyboard/location.rs b/core/src/keyboard/location.rs
new file mode 100644
index 00000000..feff0820
--- /dev/null
+++ b/core/src/keyboard/location.rs
@@ -0,0 +1,12 @@
+/// The location of a key on the keyboard.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Location {
+ /// The standard group of keys on the keyboard.
+ Standard,
+ /// The left side of the keyboard.
+ Left,
+ /// The right side of the keyboard.
+ Right,
+ /// The numpad of the keyboard.
+ Numpad,
+}
diff --git a/core/src/layout.rs b/core/src/layout.rs
index caf315b6..95720aba 100644
--- a/core/src/layout.rs
+++ b/core/src/layout.rs
@@ -7,7 +7,7 @@ pub mod flex;
pub use limits::Limits;
pub use node::Node;
-use crate::{Point, Rectangle, Size, Vector};
+use crate::{Length, Padding, Point, Rectangle, Size, Vector};
/// The bounds of a [`Node`] and its children, using absolute coordinates.
#[derive(Debug, Clone, Copy)]
@@ -71,12 +71,12 @@ pub fn next_to_each_other(
left: impl FnOnce(&Limits) -> Node,
right: impl FnOnce(&Limits) -> Node,
) -> Node {
- let mut left_node = left(limits);
+ let left_node = left(limits);
let left_size = left_node.size();
let right_limits = limits.shrink(Size::new(left_size.width + spacing, 0.0));
- let mut right_node = right(&right_limits);
+ let right_node = right(&right_limits);
let right_size = right_node.size();
let (left_y, right_y) = if left_size.height > right_size.height {
@@ -85,14 +85,106 @@ pub fn next_to_each_other(
((right_size.height - left_size.height) / 2.0, 0.0)
};
- left_node.move_to(Point::new(0.0, left_y));
- right_node.move_to(Point::new(left_size.width + spacing, right_y));
-
Node::with_children(
Size::new(
left_size.width + spacing + right_size.width,
left_size.height.max(right_size.height),
),
- vec![left_node, right_node],
+ vec![
+ left_node.move_to(Point::new(0.0, left_y)),
+ right_node.move_to(Point::new(left_size.width + spacing, right_y)),
+ ],
+ )
+}
+
+/// Computes the resulting [`Node`] that fits the [`Limits`] given
+/// some width and height requirements and no intrinsic size.
+pub fn atomic(
+ limits: &Limits,
+ width: impl Into<Length>,
+ height: impl Into<Length>,
+) -> Node {
+ let width = width.into();
+ let height = height.into();
+
+ Node::new(limits.resolve(width, height, Size::ZERO))
+}
+
+/// Computes the resulting [`Node`] that fits the [`Limits`] given
+/// some width and height requirements and a closure that produces
+/// the intrinsic [`Size`] inside the given [`Limits`].
+pub fn sized(
+ limits: &Limits,
+ width: impl Into<Length>,
+ height: impl Into<Length>,
+ f: impl FnOnce(&Limits) -> Size,
+) -> Node {
+ let width = width.into();
+ let height = height.into();
+
+ let limits = limits.width(width).height(height);
+ let intrinsic_size = f(&limits);
+
+ Node::new(limits.resolve(width, height, intrinsic_size))
+}
+
+/// Computes the resulting [`Node`] that fits the [`Limits`] given
+/// some width and height requirements and a closure that produces
+/// the content [`Node`] inside the given [`Limits`].
+pub fn contained(
+ limits: &Limits,
+ width: impl Into<Length>,
+ height: impl Into<Length>,
+ f: impl FnOnce(&Limits) -> Node,
+) -> Node {
+ let width = width.into();
+ let height = height.into();
+
+ let limits = limits.width(width).height(height);
+ let content = f(&limits);
+
+ Node::with_children(
+ limits.resolve(width, height, content.size()),
+ vec![content],
+ )
+}
+
+/// Computes the [`Node`] that fits the [`Limits`] given some width, height, and
+/// [`Padding`] requirements and a closure that produces the content [`Node`]
+/// inside the given [`Limits`].
+pub fn padded(
+ limits: &Limits,
+ width: impl Into<Length>,
+ height: impl Into<Length>,
+ padding: impl Into<Padding>,
+ layout: impl FnOnce(&Limits) -> Node,
+) -> Node {
+ positioned(limits, width, height, padding, layout, |content, _| content)
+}
+
+/// Computes a [`padded`] [`Node`] with a positioning step.
+pub fn positioned(
+ limits: &Limits,
+ width: impl Into<Length>,
+ height: impl Into<Length>,
+ padding: impl Into<Padding>,
+ layout: impl FnOnce(&Limits) -> Node,
+ position: impl FnOnce(Node, Size) -> Node,
+) -> Node {
+ let width = width.into();
+ let height = height.into();
+ let padding = padding.into();
+
+ let limits = limits.width(width).height(height);
+ let content = layout(&limits.shrink(padding));
+ let padding = padding.fit(content.size(), limits.max());
+
+ let size = limits
+ .shrink(padding)
+ .resolve(width, height, content.size());
+
+ Node::with_children(
+ size.expand(padding),
+ vec![position(content.move_to((padding.left, padding.top)), size)],
)
}
diff --git a/core/src/layout/flex.rs b/core/src/layout/flex.rs
index c02b63d8..3358ef3d 100644
--- a/core/src/layout/flex.rs
+++ b/core/src/layout/flex.rs
@@ -20,7 +20,7 @@ use crate::Element;
use crate::layout::{Limits, Node};
use crate::widget;
-use crate::{Alignment, Padding, Point, Size};
+use crate::{Alignment, Length, Padding, Point, Size};
/// The main axis of a flex layout.
#[derive(Debug)]
@@ -47,7 +47,7 @@ impl Axis {
}
}
- fn pack(&self, main: f32, cross: f32) -> (f32, f32) {
+ fn pack<T>(&self, main: T, cross: T) -> (T, T) {
match self {
Axis::Horizontal => (main, cross),
Axis::Vertical => (cross, main),
@@ -63,6 +63,8 @@ pub fn resolve<Message, Renderer>(
axis: Axis,
renderer: &Renderer,
limits: &Limits,
+ width: Length,
+ height: Length,
padding: Padding,
spacing: f32,
align_items: Alignment,
@@ -72,26 +74,64 @@ pub fn resolve<Message, Renderer>(
where
Renderer: crate::Renderer,
{
- let limits = limits.pad(padding);
+ let limits = limits.width(width).height(height).shrink(padding);
let total_spacing = spacing * items.len().saturating_sub(1) as f32;
let max_cross = axis.cross(limits.max());
- let mut fill_sum = 0;
- let mut cross = axis.cross(limits.min()).max(axis.cross(limits.fill()));
+ let mut fill_main_sum = 0;
+ let mut cross = match axis {
+ Axis::Horizontal => match height {
+ Length::Shrink => 0.0,
+ _ => max_cross,
+ },
+ Axis::Vertical => match width {
+ Length::Shrink => 0.0,
+ _ => max_cross,
+ },
+ };
+
let mut available = axis.main(limits.max()) - total_spacing;
let mut nodes: Vec<Node> = Vec::with_capacity(items.len());
nodes.resize(items.len(), Node::default());
for (i, (child, tree)) in items.iter().zip(trees.iter_mut()).enumerate() {
- let fill_factor = match axis {
- Axis::Horizontal => child.as_widget().width(),
- Axis::Vertical => child.as_widget().height(),
+ let (fill_main_factor, fill_cross_factor) = {
+ let size = child.as_widget().size();
+
+ axis.pack(size.width.fill_factor(), size.height.fill_factor())
+ };
+
+ if fill_main_factor == 0 {
+ if fill_cross_factor == 0 {
+ let (max_width, max_height) = axis.pack(available, max_cross);
+
+ let child_limits =
+ Limits::new(Size::ZERO, Size::new(max_width, max_height));
+
+ let layout =
+ child.as_widget().layout(tree, renderer, &child_limits);
+ let size = layout.size();
+
+ available -= axis.main(size);
+ cross = cross.max(axis.cross(size));
+
+ nodes[i] = layout;
+ }
+ } else {
+ fill_main_sum += fill_main_factor;
}
- .fill_factor();
+ }
- if fill_factor == 0 {
- let (max_width, max_height) = axis.pack(available, max_cross);
+ for (i, (child, tree)) in items.iter().zip(trees.iter_mut()).enumerate() {
+ let (fill_main_factor, fill_cross_factor) = {
+ let size = child.as_widget().size();
+
+ axis.pack(size.width.fill_factor(), size.height.fill_factor())
+ };
+
+ if fill_main_factor == 0 && fill_cross_factor != 0 {
+ let (max_width, max_height) = axis.pack(available, cross);
let child_limits =
Limits::new(Size::ZERO, Size::new(max_width, max_height));
@@ -101,34 +141,47 @@ where
let size = layout.size();
available -= axis.main(size);
- cross = cross.max(axis.cross(size));
+ cross = cross.max(axis.cross(layout.size()));
nodes[i] = layout;
- } else {
- fill_sum += fill_factor;
}
}
- let remaining = available.max(0.0);
+ let remaining = match axis {
+ Axis::Horizontal => match width {
+ Length::Shrink => 0.0,
+ _ => available.max(0.0),
+ },
+ Axis::Vertical => match height {
+ Length::Shrink => 0.0,
+ _ => available.max(0.0),
+ },
+ };
for (i, (child, tree)) in items.iter().zip(trees).enumerate() {
- let fill_factor = match axis {
- Axis::Horizontal => child.as_widget().width(),
- Axis::Vertical => child.as_widget().height(),
- }
- .fill_factor();
+ let (fill_main_factor, fill_cross_factor) = {
+ let size = child.as_widget().size();
+
+ axis.pack(size.width.fill_factor(), size.height.fill_factor())
+ };
+
+ if fill_main_factor != 0 {
+ let max_main =
+ remaining * fill_main_factor as f32 / fill_main_sum as f32;
- if fill_factor != 0 {
- let max_main = remaining * fill_factor as f32 / fill_sum as f32;
let min_main = if max_main.is_infinite() {
0.0
} else {
max_main
};
- let (min_width, min_height) =
- axis.pack(min_main, axis.cross(limits.min()));
+ let max_cross = if fill_cross_factor == 0 {
+ max_cross
+ } else {
+ cross
+ };
+ let (min_width, min_height) = axis.pack(min_main, 0.0);
let (max_width, max_height) = axis.pack(max_main, max_cross);
let child_limits = Limits::new(
@@ -154,18 +207,18 @@ where
let (x, y) = axis.pack(main, pad.1);
- node.move_to(Point::new(x, y));
+ node.move_to_mut(Point::new(x, y));
match axis {
Axis::Horizontal => {
- node.align(
+ node.align_mut(
Alignment::Start,
align_items,
Size::new(0.0, cross),
);
}
Axis::Vertical => {
- node.align(
+ node.align_mut(
align_items,
Alignment::Start,
Size::new(cross, 0.0),
@@ -178,8 +231,12 @@ where
main += axis.main(size);
}
- let (width, height) = axis.pack(main - pad.0, cross);
- let size = limits.resolve(Size::new(width, height));
+ let (intrinsic_width, intrinsic_height) = axis.pack(main - pad.0, cross);
+ let size = limits.resolve(
+ width,
+ height,
+ Size::new(intrinsic_width, intrinsic_height),
+ );
- Node::with_children(size.pad(padding), nodes)
+ Node::with_children(size.expand(padding), nodes)
}
diff --git a/core/src/layout/limits.rs b/core/src/layout/limits.rs
index 5d3c1556..7fbc7b9d 100644
--- a/core/src/layout/limits.rs
+++ b/core/src/layout/limits.rs
@@ -1,12 +1,11 @@
#![allow(clippy::manual_clamp)]
-use crate::{Length, Padding, Size};
+use crate::{Length, Size};
/// A set of size constraints for layouting.
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Limits {
min: Size,
max: Size,
- fill: Size,
}
impl Limits {
@@ -14,16 +13,11 @@ impl Limits {
pub const NONE: Limits = Limits {
min: Size::ZERO,
max: Size::INFINITY,
- fill: Size::INFINITY,
};
/// Creates new [`Limits`] with the given minimum and maximum [`Size`].
pub const fn new(min: Size, max: Size) -> Limits {
- Limits {
- min,
- max,
- fill: Size::INFINITY,
- }
+ Limits { min, max }
}
/// Returns the minimum [`Size`] of the [`Limits`].
@@ -36,26 +30,15 @@ impl Limits {
self.max
}
- /// Returns the fill [`Size`] of the [`Limits`].
- pub fn fill(&self) -> Size {
- self.fill
- }
-
/// Applies a width constraint to the current [`Limits`].
pub fn width(mut self, width: impl Into<Length>) -> Limits {
match width.into() {
- Length::Shrink => {
- self.fill.width = self.min.width;
- }
- Length::Fill | Length::FillPortion(_) => {
- self.fill.width = self.fill.width.min(self.max.width);
- }
+ Length::Shrink | Length::Fill | Length::FillPortion(_) => {}
Length::Fixed(amount) => {
let new_width = amount.min(self.max.width).max(self.min.width);
self.min.width = new_width;
self.max.width = new_width;
- self.fill.width = new_width;
}
}
@@ -65,19 +48,13 @@ impl Limits {
/// Applies a height constraint to the current [`Limits`].
pub fn height(mut self, height: impl Into<Length>) -> Limits {
match height.into() {
- Length::Shrink => {
- self.fill.height = self.min.height;
- }
- Length::Fill | Length::FillPortion(_) => {
- self.fill.height = self.fill.height.min(self.max.height);
- }
+ Length::Shrink | Length::Fill | Length::FillPortion(_) => {}
Length::Fixed(amount) => {
let new_height =
amount.min(self.max.height).max(self.min.height);
self.min.height = new_height;
self.max.height = new_height;
- self.fill.height = new_height;
}
}
@@ -112,13 +89,10 @@ impl Limits {
self
}
- /// Shrinks the current [`Limits`] to account for the given padding.
- pub fn pad(&self, padding: Padding) -> Limits {
- self.shrink(Size::new(padding.horizontal(), padding.vertical()))
- }
-
/// Shrinks the current [`Limits`] by the given [`Size`].
- pub fn shrink(&self, size: Size) -> Limits {
+ pub fn shrink(&self, size: impl Into<Size>) -> Limits {
+ let size = size.into();
+
let min = Size::new(
(self.min().width - size.width).max(0.0),
(self.min().height - size.height).max(0.0),
@@ -129,12 +103,7 @@ impl Limits {
(self.max().height - size.height).max(0.0),
);
- let fill = Size::new(
- (self.fill.width - size.width).max(0.0),
- (self.fill.height - size.height).max(0.0),
- );
-
- Limits { min, max, fill }
+ Limits { min, max }
}
/// Removes the minimum width constraint for the current [`Limits`].
@@ -142,22 +111,39 @@ impl Limits {
Limits {
min: Size::ZERO,
max: self.max,
- fill: self.fill,
}
}
- /// Computes the resulting [`Size`] that fits the [`Limits`] given the
- /// intrinsic size of some content.
- pub fn resolve(&self, intrinsic_size: Size) -> Size {
- Size::new(
- intrinsic_size
- .width
- .min(self.max.width)
- .max(self.fill.width),
- intrinsic_size
+ /// Computes the resulting [`Size`] that fits the [`Limits`] given
+ /// some width and height requirements and the intrinsic size of
+ /// some content.
+ pub fn resolve(
+ &self,
+ width: impl Into<Length>,
+ height: impl Into<Length>,
+ intrinsic_size: Size,
+ ) -> Size {
+ let width = match width.into() {
+ Length::Fill | Length::FillPortion(_) => self.max.width,
+ Length::Fixed(amount) => {
+ amount.min(self.max.width).max(self.min.width)
+ }
+ Length::Shrink => {
+ intrinsic_size.width.min(self.max.width).max(self.min.width)
+ }
+ };
+
+ let height = match height.into() {
+ Length::Fill | Length::FillPortion(_) => self.max.height,
+ Length::Fixed(amount) => {
+ amount.min(self.max.height).max(self.min.height)
+ }
+ Length::Shrink => intrinsic_size
.height
.min(self.max.height)
- .max(self.fill.height),
- )
+ .max(self.min.height),
+ };
+
+ Size::new(width, height)
}
}
diff --git a/core/src/layout/node.rs b/core/src/layout/node.rs
index 2b44a7d5..5743a9bd 100644
--- a/core/src/layout/node.rs
+++ b/core/src/layout/node.rs
@@ -1,4 +1,4 @@
-use crate::{Alignment, Point, Rectangle, Size, Vector};
+use crate::{Alignment, Padding, Point, Rectangle, Size, Vector};
/// The bounds of an element and its children.
#[derive(Debug, Clone, Default)]
@@ -26,6 +26,14 @@ impl Node {
}
}
+ /// Creates a new [`Node`] that wraps a single child with some [`Padding`].
+ pub fn container(child: Self, padding: Padding) -> Self {
+ Self::with_children(
+ child.bounds.size().expand(padding),
+ vec![child.move_to(Point::new(padding.left, padding.top))],
+ )
+ }
+
/// Returns the [`Size`] of the [`Node`].
pub fn size(&self) -> Size {
Size::new(self.bounds.width, self.bounds.height)
@@ -43,6 +51,17 @@ impl Node {
/// Aligns the [`Node`] in the given space.
pub fn align(
+ mut self,
+ horizontal_alignment: Alignment,
+ vertical_alignment: Alignment,
+ space: Size,
+ ) -> Self {
+ self.align_mut(horizontal_alignment, vertical_alignment, space);
+ self
+ }
+
+ /// Mutable reference version of [`Self::align`].
+ pub fn align_mut(
&mut self,
horizontal_alignment: Alignment,
vertical_alignment: Alignment,
@@ -70,13 +89,23 @@ impl Node {
}
/// Moves the [`Node`] to the given position.
- pub fn move_to(&mut self, position: Point) {
+ pub fn move_to(mut self, position: impl Into<Point>) -> Self {
+ self.move_to_mut(position);
+ self
+ }
+
+ /// Mutable reference version of [`Self::move_to`].
+ pub fn move_to_mut(&mut self, position: impl Into<Point>) {
+ let position = position.into();
+
self.bounds.x = position.x;
self.bounds.y = position.y;
}
/// Translates the [`Node`] by the given translation.
- pub fn translate(self, translation: Vector) -> Self {
+ pub fn translate(self, translation: impl Into<Vector>) -> Self {
+ let translation = translation.into();
+
Self {
bounds: self.bounds + translation,
..self
diff --git a/core/src/length.rs b/core/src/length.rs
index 3adb996e..4c139895 100644
--- a/core/src/length.rs
+++ b/core/src/length.rs
@@ -36,6 +36,24 @@ impl Length {
Length::Fixed(_) => 0,
}
}
+
+ /// Returns `true` iff the [`Length`] is either [`Length::Fill`] or
+ // [`Length::FillPortion`].
+ pub fn is_fill(&self) -> bool {
+ self.fill_factor() != 0
+ }
+
+ /// Returns the "fluid" variant of the [`Length`].
+ ///
+ /// Specifically:
+ /// - [`Length::Shrink`] if [`Length::Shrink`] or [`Length::Fixed`].
+ /// - [`Length::Fill`] otherwise.
+ pub fn fluid(&self) -> Length {
+ match self {
+ Length::Fill | Length::FillPortion(_) => Length::Fill,
+ Length::Shrink | Length::Fixed(_) => Length::Shrink,
+ }
+ }
}
impl From<Pixels> for Length {
diff --git a/core/src/lib.rs b/core/src/lib.rs
index 54ea5839..864df6e6 100644
--- a/core/src/lib.rs
+++ b/core/src/lib.rs
@@ -75,3 +75,5 @@ pub use size::Size;
pub use text::Text;
pub use vector::Vector;
pub use widget::Widget;
+
+pub use smol_str::SmolStr;
diff --git a/core/src/mouse/button.rs b/core/src/mouse/button.rs
index 3eec7f42..a8f90329 100644
--- a/core/src/mouse/button.rs
+++ b/core/src/mouse/button.rs
@@ -10,6 +10,12 @@ pub enum Button {
/// The middle (wheel) button.
Middle,
+ /// The back mouse button.
+ Back,
+
+ /// The forward mouse button.
+ Forward,
+
/// Some other button.
Other(u16),
}
diff --git a/core/src/mouse/click.rs b/core/src/mouse/click.rs
index 9cc44a71..6f3844be 100644
--- a/core/src/mouse/click.rs
+++ b/core/src/mouse/click.rs
@@ -61,6 +61,11 @@ impl Click {
self.kind
}
+ /// Returns the position of the [`Click`].
+ pub fn position(&self) -> Point {
+ self.position
+ }
+
fn is_consecutive(&self, new_position: Point, time: Instant) -> bool {
let duration = if time > self.time {
Some(time - self.time)
diff --git a/core/src/overlay.rs b/core/src/overlay.rs
index f71f25f7..af10afee 100644
--- a/core/src/overlay.rs
+++ b/core/src/overlay.rs
@@ -11,7 +11,7 @@ use crate::mouse;
use crate::renderer;
use crate::widget;
use crate::widget::Tree;
-use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size};
+use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size, Vector};
/// An interactive component that can be displayed on top of other widgets.
pub trait Overlay<Message, Renderer>
@@ -29,6 +29,7 @@ where
renderer: &Renderer,
bounds: Size,
position: Point,
+ translation: Vector,
) -> layout::Node;
/// Draws the [`Overlay`] using the associated `Renderer`.
diff --git a/core/src/overlay/element.rs b/core/src/overlay/element.rs
index 3dd58f9b..a279fe28 100644
--- a/core/src/overlay/element.rs
+++ b/core/src/overlay/element.rs
@@ -13,6 +13,7 @@ use std::any::Any;
#[allow(missing_debug_implementations)]
pub struct Element<'a, Message, Renderer> {
position: Point,
+ translation: Vector,
overlay: Box<dyn Overlay<Message, Renderer> + 'a>,
}
@@ -25,7 +26,11 @@ where
position: Point,
overlay: Box<dyn Overlay<Message, Renderer> + 'a>,
) -> Self {
- Self { position, overlay }
+ Self {
+ position,
+ overlay,
+ translation: Vector::ZERO,
+ }
}
/// Returns the position of the [`Element`].
@@ -36,6 +41,7 @@ where
/// Translates the [`Element`].
pub fn translate(mut self, translation: Vector) -> Self {
self.position = self.position + translation;
+ self.translation = self.translation + translation;
self
}
@@ -48,6 +54,7 @@ where
{
Element {
position: self.position,
+ translation: self.translation,
overlay: Box::new(Map::new(self.overlay, f)),
}
}
@@ -59,8 +66,12 @@ where
bounds: Size,
translation: Vector,
) -> layout::Node {
- self.overlay
- .layout(renderer, bounds, self.position + translation)
+ self.overlay.layout(
+ renderer,
+ bounds,
+ self.position + translation,
+ self.translation + translation,
+ )
}
/// Processes a runtime [`Event`].
@@ -154,8 +165,9 @@ where
renderer: &Renderer,
bounds: Size,
position: Point,
+ translation: Vector,
) -> layout::Node {
- self.content.layout(renderer, bounds, position)
+ self.content.layout(renderer, bounds, position, translation)
}
fn operate(
diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs
index dccf6dba..e1e9727a 100644
--- a/core/src/overlay/group.rs
+++ b/core/src/overlay/group.rs
@@ -4,7 +4,9 @@ use crate::mouse;
use crate::overlay;
use crate::renderer;
use crate::widget;
-use crate::{Clipboard, Event, Layout, Overlay, Point, Rectangle, Shell, Size};
+use crate::{
+ Clipboard, Event, Layout, Overlay, Point, Rectangle, Shell, Size, Vector,
+};
/// An [`Overlay`] container that displays multiple overlay [`overlay::Element`]
/// children.
@@ -64,10 +66,9 @@ where
&mut self,
renderer: &Renderer,
bounds: Size,
- position: Point,
+ _position: Point,
+ translation: Vector,
) -> layout::Node {
- let translation = position - Point::ORIGIN;
-
layout::Node::with_children(
bounds,
self.children
diff --git a/core/src/padding.rs b/core/src/padding.rs
index 0b1bba13..a63f6e29 100644
--- a/core/src/padding.rs
+++ b/core/src/padding.rs
@@ -154,3 +154,9 @@ impl From<[f32; 4]> for Padding {
}
}
}
+
+impl From<Padding> for Size {
+ fn from(padding: Padding) -> Self {
+ Self::new(padding.horizontal(), padding.vertical())
+ }
+}
diff --git a/core/src/point.rs b/core/src/point.rs
index 9bf7726b..cea57518 100644
--- a/core/src/point.rs
+++ b/core/src/point.rs
@@ -1,26 +1,34 @@
use crate::Vector;
+use num_traits::{Float, Num};
+use std::fmt;
+
/// A 2D point.
-#[derive(Debug, Clone, Copy, PartialEq, Default)]
-pub struct Point {
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub struct Point<T = f32> {
/// The X coordinate.
- pub x: f32,
+ pub x: T,
/// The Y coordinate.
- pub y: f32,
+ pub y: T,
}
impl Point {
/// The origin (i.e. a [`Point`] at (0, 0)).
- pub const ORIGIN: Point = Point::new(0.0, 0.0);
+ pub const ORIGIN: Self = Self::new(0.0, 0.0);
+}
+impl<T: Num> Point<T> {
/// Creates a new [`Point`] with the given coordinates.
- pub const fn new(x: f32, y: f32) -> Self {
+ pub const fn new(x: T, y: T) -> Self {
Self { x, y }
}
/// Computes the distance to another [`Point`].
- pub fn distance(&self, to: Point) -> f32 {
+ pub fn distance(&self, to: Self) -> T
+ where
+ T: Float,
+ {
let a = self.x - to.x;
let b = self.y - to.y;
@@ -28,28 +36,37 @@ impl Point {
}
}
-impl From<[f32; 2]> for Point {
- fn from([x, y]: [f32; 2]) -> Self {
+impl<T> From<[T; 2]> for Point<T>
+where
+ T: Num,
+{
+ fn from([x, y]: [T; 2]) -> Self {
Point { x, y }
}
}
-impl From<[u16; 2]> for Point {
- fn from([x, y]: [u16; 2]) -> Self {
- Point::new(x.into(), y.into())
+impl<T> From<(T, T)> for Point<T>
+where
+ T: Num,
+{
+ fn from((x, y): (T, T)) -> Self {
+ Self { x, y }
}
}
-impl From<Point> for [f32; 2] {
- fn from(point: Point) -> [f32; 2] {
+impl<T> From<Point<T>> for [T; 2] {
+ fn from(point: Point<T>) -> [T; 2] {
[point.x, point.y]
}
}
-impl std::ops::Add<Vector> for Point {
+impl<T> std::ops::Add<Vector<T>> for Point<T>
+where
+ T: std::ops::Add<Output = T>,
+{
type Output = Self;
- fn add(self, vector: Vector) -> Self {
+ fn add(self, vector: Vector<T>) -> Self {
Self {
x: self.x + vector.x,
y: self.y + vector.y,
@@ -57,10 +74,13 @@ impl std::ops::Add<Vector> for Point {
}
}
-impl std::ops::Sub<Vector> for Point {
+impl<T> std::ops::Sub<Vector<T>> for Point<T>
+where
+ T: std::ops::Sub<Output = T>,
+{
type Output = Self;
- fn sub(self, vector: Vector) -> Self {
+ fn sub(self, vector: Vector<T>) -> Self {
Self {
x: self.x - vector.x,
y: self.y - vector.y,
@@ -68,10 +88,22 @@ impl std::ops::Sub<Vector> for Point {
}
}
-impl std::ops::Sub<Point> for Point {
- type Output = Vector;
+impl<T> std::ops::Sub<Point<T>> for Point<T>
+where
+ T: std::ops::Sub<Output = T>,
+{
+ type Output = Vector<T>;
- fn sub(self, point: Point) -> Vector {
+ fn sub(self, point: Self) -> Vector<T> {
Vector::new(self.x - point.x, self.y - point.y)
}
}
+
+impl<T> fmt::Display for Point<T>
+where
+ T: fmt::Display,
+{
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "Point {{ x: {}, y: {} }}", self.x, self.y)
+ }
+}
diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs
index 55d58a59..7accd34e 100644
--- a/core/src/renderer/null.rs
+++ b/core/src/renderer/null.rs
@@ -43,6 +43,7 @@ impl Renderer for Null {
impl text::Renderer for Null {
type Font = Font;
type Paragraph = ();
+ type Editor = ();
const ICON_FONT: Font = Font::DEFAULT;
const CHECKMARK_ICON: char = '0';
@@ -58,21 +59,21 @@ impl text::Renderer for Null {
fn load_font(&mut self, _font: Cow<'static, [u8]>) {}
- fn create_paragraph(&self, _text: Text<'_, Self::Font>) -> Self::Paragraph {
- }
-
- fn resize_paragraph(
- &self,
- _paragraph: &mut Self::Paragraph,
- _new_bounds: Size,
+ fn fill_paragraph(
+ &mut self,
+ _paragraph: &Self::Paragraph,
+ _position: Point,
+ _color: Color,
+ _clip_bounds: Rectangle,
) {
}
- fn fill_paragraph(
+ fn fill_editor(
&mut self,
- _paragraph: &Self::Paragraph,
+ _editor: &Self::Editor,
_position: Point,
_color: Color,
+ _clip_bounds: Rectangle,
) {
}
@@ -81,6 +82,7 @@ impl text::Renderer for Null {
_paragraph: Text<'_, Self::Font>,
_position: Point,
_color: Color,
+ _clip_bounds: Rectangle,
) {
}
}
@@ -88,47 +90,83 @@ impl text::Renderer for Null {
impl text::Paragraph for () {
type Font = Font;
- fn content(&self) -> &str {
- ""
+ fn with_text(_text: Text<'_, Self::Font>) -> Self {}
+
+ fn resize(&mut self, _new_bounds: Size) {}
+
+ fn compare(&self, _text: Text<'_, Self::Font>) -> text::Difference {
+ text::Difference::None
}
- fn text_size(&self) -> Pixels {
- Pixels(16.0)
+ fn horizontal_alignment(&self) -> alignment::Horizontal {
+ alignment::Horizontal::Left
}
- fn font(&self) -> Self::Font {
- Font::default()
+ fn vertical_alignment(&self) -> alignment::Vertical {
+ alignment::Vertical::Top
}
- fn line_height(&self) -> text::LineHeight {
- text::LineHeight::default()
+ fn grapheme_position(&self, _line: usize, _index: usize) -> Option<Point> {
+ None
}
- fn shaping(&self) -> text::Shaping {
- text::Shaping::default()
+ fn min_bounds(&self) -> Size {
+ Size::ZERO
}
- fn horizontal_alignment(&self) -> alignment::Horizontal {
- alignment::Horizontal::Left
+ fn hit_test(&self, _point: Point) -> Option<text::Hit> {
+ None
}
+}
- fn vertical_alignment(&self) -> alignment::Vertical {
- alignment::Vertical::Top
+impl text::Editor for () {
+ type Font = Font;
+
+ fn with_text(_text: &str) -> Self {}
+
+ fn cursor(&self) -> text::editor::Cursor {
+ text::editor::Cursor::Caret(Point::ORIGIN)
}
- fn grapheme_position(&self, _line: usize, _index: usize) -> Option<Point> {
+ fn cursor_position(&self) -> (usize, usize) {
+ (0, 0)
+ }
+
+ fn selection(&self) -> Option<String> {
+ None
+ }
+
+ fn line(&self, _index: usize) -> Option<&str> {
None
}
+ fn line_count(&self) -> usize {
+ 0
+ }
+
+ fn perform(&mut self, _action: text::editor::Action) {}
+
fn bounds(&self) -> Size {
Size::ZERO
}
- fn min_bounds(&self) -> Size {
- Size::ZERO
+ fn update(
+ &mut self,
+ _new_bounds: Size,
+ _new_font: Self::Font,
+ _new_size: Pixels,
+ _new_line_height: text::LineHeight,
+ _new_highlighter: &mut impl text::Highlighter,
+ ) {
}
- fn hit_test(&self, _point: Point) -> Option<text::Hit> {
- None
+ fn highlight<H: text::Highlighter>(
+ &mut self,
+ _font: Self::Font,
+ _highlighter: &mut H,
+ _format_highlight: impl Fn(
+ &H::Highlight,
+ ) -> text::highlighter::Format<Self::Font>,
+ ) {
}
}
diff --git a/core/src/size.rs b/core/src/size.rs
index 7ef2f602..90e50d13 100644
--- a/core/src/size.rs
+++ b/core/src/size.rs
@@ -1,4 +1,4 @@
-use crate::{Padding, Vector};
+use crate::Vector;
/// An amount of space in 2 dimensions.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -26,15 +26,7 @@ impl Size {
/// A [`Size`] with infinite width and height.
pub const INFINITY: Size = Size::new(f32::INFINITY, f32::INFINITY);
- /// Increments the [`Size`] to account for the given padding.
- pub fn pad(&self, padding: Padding) -> Self {
- Size {
- width: self.width + padding.horizontal(),
- height: self.height + padding.vertical(),
- }
- }
-
- /// Returns the minimum of each component of this size and another
+ /// Returns the minimum of each component of this size and another.
pub fn min(self, other: Self) -> Self {
Size {
width: self.width.min(other.width),
@@ -42,13 +34,23 @@ impl Size {
}
}
- /// Returns the maximum of each component of this size and another
+ /// Returns the maximum of each component of this size and another.
pub fn max(self, other: Self) -> Self {
Size {
width: self.width.max(other.width),
height: self.height.max(other.height),
}
}
+
+ /// Expands this [`Size`] by the given amount.
+ pub fn expand(self, other: impl Into<Size>) -> Self {
+ let other = other.into();
+
+ Size {
+ width: self.width + other.width,
+ height: self.height + other.height,
+ }
+ }
}
impl From<[f32; 2]> for Size {
diff --git a/core/src/text.rs b/core/src/text.rs
index 0e3617b1..edef79c2 100644
--- a/core/src/text.rs
+++ b/core/src/text.rs
@@ -1,6 +1,15 @@
//! Draw and interact with text.
+mod paragraph;
+
+pub mod editor;
+pub mod highlighter;
+
+pub use editor::Editor;
+pub use highlighter::Highlighter;
+pub use paragraph::Paragraph;
+
use crate::alignment;
-use crate::{Color, Pixels, Point, Size};
+use crate::{Color, Pixels, Point, Rectangle, Size};
use std::borrow::Cow;
use std::hash::{Hash, Hasher};
@@ -126,6 +135,33 @@ impl Hit {
}
}
+/// The difference detected in some text.
+///
+/// You will obtain a [`Difference`] when you [`compare`] a [`Paragraph`] with some
+/// [`Text`].
+///
+/// [`compare`]: Paragraph::compare
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Difference {
+ /// No difference.
+ ///
+ /// The text can be reused as it is!
+ None,
+
+ /// A bounds difference.
+ ///
+ /// This normally means a relayout is necessary, but the shape of the text can
+ /// be reused.
+ Bounds,
+
+ /// A shape difference.
+ ///
+ /// The contents, alignment, sizes, fonts, or any other essential attributes
+ /// of the shape of the text have changed. A complete reshape and relayout of
+ /// the text is necessary.
+ Shape,
+}
+
/// A renderer capable of measuring and drawing [`Text`].
pub trait Renderer: crate::Renderer {
/// The font type used.
@@ -134,6 +170,9 @@ pub trait Renderer: crate::Renderer {
/// The [`Paragraph`] of this [`Renderer`].
type Paragraph: Paragraph<Font = Self::Font> + 'static;
+ /// The [`Editor`] of this [`Renderer`].
+ type Editor: Editor<Font = Self::Font> + 'static;
+
/// The icon font of the backend.
const ICON_FONT: Self::Font;
@@ -156,33 +195,6 @@ pub trait Renderer: crate::Renderer {
/// Loads a [`Self::Font`] from its bytes.
fn load_font(&mut self, font: Cow<'static, [u8]>);
- /// Creates a new [`Paragraph`] laid out with the given [`Text`].
- fn create_paragraph(&self, text: Text<'_, Self::Font>) -> Self::Paragraph;
-
- /// Lays out the given [`Paragraph`] with some new boundaries.
- fn resize_paragraph(
- &self,
- paragraph: &mut Self::Paragraph,
- new_bounds: Size,
- );
-
- /// Updates a [`Paragraph`] to match the given [`Text`], if needed.
- fn update_paragraph(
- &self,
- paragraph: &mut Self::Paragraph,
- text: Text<'_, Self::Font>,
- ) {
- match compare(paragraph, text) {
- Difference::None => {}
- Difference::Bounds => {
- self.resize_paragraph(paragraph, text.bounds);
- }
- Difference::Shape => {
- *paragraph = self.create_paragraph(text);
- }
- }
- }
-
/// Draws the given [`Paragraph`] at the given position and with the given
/// [`Color`].
fn fill_paragraph(
@@ -190,6 +202,17 @@ pub trait Renderer: crate::Renderer {
text: &Self::Paragraph,
position: Point,
color: Color,
+ clip_bounds: Rectangle,
+ );
+
+ /// Draws the given [`Editor`] at the given position and with the given
+ /// [`Color`].
+ fn fill_editor(
+ &mut self,
+ editor: &Self::Editor,
+ position: Point,
+ color: Color,
+ clip_bounds: Rectangle,
);
/// Draws the given [`Text`] at the given position and with the given
@@ -199,103 +222,6 @@ pub trait Renderer: crate::Renderer {
text: Text<'_, Self::Font>,
position: Point,
color: Color,
+ clip_bounds: Rectangle,
);
}
-/// A text paragraph.
-pub trait Paragraph: Default {
- /// The font of this [`Paragraph`].
- type Font;
-
- /// Returns the content of the [`Paragraph`].
- fn content(&self) -> &str;
-
- /// Returns the text size of the [`Paragraph`].
- fn text_size(&self) -> Pixels;
-
- /// Returns the [`LineHeight`] of the [`Paragraph`].
- fn line_height(&self) -> LineHeight;
-
- /// Returns the [`Self::Font`] of the [`Paragraph`].
- fn font(&self) -> Self::Font;
-
- /// Returns the [`Shaping`] strategy of the [`Paragraph`].
- fn shaping(&self) -> Shaping;
-
- /// Returns the horizontal alignment of the [`Paragraph`].
- fn horizontal_alignment(&self) -> alignment::Horizontal;
-
- /// Returns the vertical alignment of the [`Paragraph`].
- fn vertical_alignment(&self) -> alignment::Vertical;
-
- /// Returns the boundaries of the [`Paragraph`].
- fn bounds(&self) -> Size;
-
- /// Returns the minimum boundaries that can fit the contents of the
- /// [`Paragraph`].
- fn min_bounds(&self) -> Size;
-
- /// Tests whether the provided point is within the boundaries of the
- /// [`Paragraph`], returning information about the nearest character.
- fn hit_test(&self, point: Point) -> Option<Hit>;
-
- /// Returns the distance to the given grapheme index in the [`Paragraph`].
- fn grapheme_position(&self, line: usize, index: usize) -> Option<Point>;
-
- /// Returns the minimum width that can fit the contents of the [`Paragraph`].
- fn min_width(&self) -> f32 {
- self.min_bounds().width
- }
-
- /// Returns the minimum height that can fit the contents of the [`Paragraph`].
- fn min_height(&self) -> f32 {
- self.min_bounds().height
- }
-}
-
-/// The difference detected in some text.
-///
-/// You will obtain a [`Difference`] when you [`compare`] a [`Paragraph`] with some
-/// [`Text`].
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum Difference {
- /// No difference.
- ///
- /// The text can be reused as it is!
- None,
-
- /// A bounds difference.
- ///
- /// This normally means a relayout is necessary, but the shape of the text can
- /// be reused.
- Bounds,
-
- /// A shape difference.
- ///
- /// The contents, alignment, sizes, fonts, or any other essential attributes
- /// of the shape of the text have changed. A complete reshape and relayout of
- /// the text is necessary.
- Shape,
-}
-
-/// Compares a [`Paragraph`] with some desired [`Text`] and returns the
-/// [`Difference`].
-pub fn compare<Font: PartialEq>(
- paragraph: &impl Paragraph<Font = Font>,
- text: Text<'_, Font>,
-) -> Difference {
- if paragraph.content() != text.content
- || paragraph.text_size() != text.size
- || paragraph.line_height().to_absolute(text.size)
- != text.line_height.to_absolute(text.size)
- || paragraph.font() != text.font
- || paragraph.shaping() != text.shaping
- || paragraph.horizontal_alignment() != text.horizontal_alignment
- || paragraph.vertical_alignment() != text.vertical_alignment
- {
- Difference::Shape
- } else if paragraph.bounds() != text.bounds {
- Difference::Bounds
- } else {
- Difference::None
- }
-}
diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs
new file mode 100644
index 00000000..f3c6e342
--- /dev/null
+++ b/core/src/text/editor.rs
@@ -0,0 +1,181 @@
+//! Edit text.
+use crate::text::highlighter::{self, Highlighter};
+use crate::text::LineHeight;
+use crate::{Pixels, Point, Rectangle, Size};
+
+use std::sync::Arc;
+
+/// A component that can be used by widgets to edit multi-line text.
+pub trait Editor: Sized + Default {
+ /// The font of the [`Editor`].
+ type Font: Copy + PartialEq + Default;
+
+ /// Creates a new [`Editor`] laid out with the given text.
+ fn with_text(text: &str) -> Self;
+
+ /// Returns the current [`Cursor`] of the [`Editor`].
+ fn cursor(&self) -> Cursor;
+
+ /// Returns the current cursor position of the [`Editor`].
+ ///
+ /// Line and column, respectively.
+ fn cursor_position(&self) -> (usize, usize);
+
+ /// Returns the current selected text of the [`Editor`].
+ fn selection(&self) -> Option<String>;
+
+ /// Returns the text of the given line in the [`Editor`], if it exists.
+ fn line(&self, index: usize) -> Option<&str>;
+
+ /// Returns the amount of lines in the [`Editor`].
+ fn line_count(&self) -> usize;
+
+ /// Performs an [`Action`] on the [`Editor`].
+ fn perform(&mut self, action: Action);
+
+ /// Returns the current boundaries of the [`Editor`].
+ fn bounds(&self) -> Size;
+
+ /// Updates the [`Editor`] with some new attributes.
+ fn update(
+ &mut self,
+ new_bounds: Size,
+ new_font: Self::Font,
+ new_size: Pixels,
+ new_line_height: LineHeight,
+ new_highlighter: &mut impl Highlighter,
+ );
+
+ /// Runs a text [`Highlighter`] in the [`Editor`].
+ fn highlight<H: Highlighter>(
+ &mut self,
+ font: Self::Font,
+ highlighter: &mut H,
+ format_highlight: impl Fn(&H::Highlight) -> highlighter::Format<Self::Font>,
+ );
+}
+
+/// An interaction with an [`Editor`].
+#[derive(Debug, Clone, PartialEq)]
+pub enum Action {
+ /// Apply a [`Motion`].
+ Move(Motion),
+ /// Select text with a given [`Motion`].
+ Select(Motion),
+ /// Select the word at the current cursor.
+ SelectWord,
+ /// Select the line at the current cursor.
+ SelectLine,
+ /// Perform an [`Edit`].
+ Edit(Edit),
+ /// Click the [`Editor`] at the given [`Point`].
+ Click(Point),
+ /// Drag the mouse on the [`Editor`] to the given [`Point`].
+ Drag(Point),
+ /// Scroll the [`Editor`] a certain amount of lines.
+ Scroll {
+ /// The amount of lines to scroll.
+ lines: i32,
+ },
+}
+
+impl Action {
+ /// Returns whether the [`Action`] is an editing action.
+ pub fn is_edit(&self) -> bool {
+ matches!(self, Self::Edit(_))
+ }
+}
+
+/// An action that edits text.
+#[derive(Debug, Clone, PartialEq)]
+pub enum Edit {
+ /// Insert the given character.
+ Insert(char),
+ /// Paste the given text.
+ Paste(Arc<String>),
+ /// Break the current line.
+ Enter,
+ /// Delete the previous character.
+ Backspace,
+ /// Delete the next character.
+ Delete,
+}
+
+/// A cursor movement.
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum Motion {
+ /// Move left.
+ Left,
+ /// Move right.
+ Right,
+ /// Move up.
+ Up,
+ /// Move down.
+ Down,
+ /// Move to the left boundary of a word.
+ WordLeft,
+ /// Move to the right boundary of a word.
+ WordRight,
+ /// Move to the start of the line.
+ Home,
+ /// Move to the end of the line.
+ End,
+ /// Move to the start of the previous window.
+ PageUp,
+ /// Move to the start of the next window.
+ PageDown,
+ /// Move to the start of the text.
+ DocumentStart,
+ /// Move to the end of the text.
+ DocumentEnd,
+}
+
+impl Motion {
+ /// Widens the [`Motion`], if possible.
+ pub fn widen(self) -> Self {
+ match self {
+ Self::Left => Self::WordLeft,
+ Self::Right => Self::WordRight,
+ Self::Home => Self::DocumentStart,
+ Self::End => Self::DocumentEnd,
+ _ => self,
+ }
+ }
+
+ /// Returns the [`Direction`] of the [`Motion`].
+ pub fn direction(&self) -> Direction {
+ match self {
+ Self::Left
+ | Self::Up
+ | Self::WordLeft
+ | Self::Home
+ | Self::PageUp
+ | Self::DocumentStart => Direction::Left,
+ Self::Right
+ | Self::Down
+ | Self::WordRight
+ | Self::End
+ | Self::PageDown
+ | Self::DocumentEnd => Direction::Right,
+ }
+ }
+}
+
+/// A direction in some text.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Direction {
+ /// <-
+ Left,
+ /// ->
+ Right,
+}
+
+/// The cursor of an [`Editor`].
+#[derive(Debug, Clone)]
+pub enum Cursor {
+ /// Cursor without a selection
+ Caret(Point),
+
+ /// Cursor selecting a range of text
+ Selection(Vec<Rectangle>),
+}
diff --git a/core/src/text/highlighter.rs b/core/src/text/highlighter.rs
new file mode 100644
index 00000000..a0535228
--- /dev/null
+++ b/core/src/text/highlighter.rs
@@ -0,0 +1,88 @@
+//! Highlight text.
+use crate::Color;
+
+use std::ops::Range;
+
+/// A type capable of highlighting text.
+///
+/// A [`Highlighter`] highlights lines in sequence. When a line changes,
+/// it must be notified and the lines after the changed one must be fed
+/// again to the [`Highlighter`].
+pub trait Highlighter: 'static {
+ /// The settings to configure the [`Highlighter`].
+ type Settings: PartialEq + Clone;
+
+ /// The output of the [`Highlighter`].
+ type Highlight;
+
+ /// The highlight iterator type.
+ type Iterator<'a>: Iterator<Item = (Range<usize>, Self::Highlight)>
+ where
+ Self: 'a;
+
+ /// Creates a new [`Highlighter`] from its [`Self::Settings`].
+ fn new(settings: &Self::Settings) -> Self;
+
+ /// Updates the [`Highlighter`] with some new [`Self::Settings`].
+ fn update(&mut self, new_settings: &Self::Settings);
+
+ /// Notifies the [`Highlighter`] that the line at the given index has changed.
+ fn change_line(&mut self, line: usize);
+
+ /// Highlights the given line.
+ ///
+ /// If a line changed prior to this, the first line provided here will be the
+ /// line that changed.
+ fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_>;
+
+ /// Returns the current line of the [`Highlighter`].
+ ///
+ /// If `change_line` has been called, this will normally be the least index
+ /// that changed.
+ fn current_line(&self) -> usize;
+}
+
+/// A highlighter that highlights nothing.
+#[derive(Debug, Clone, Copy)]
+pub struct PlainText;
+
+impl Highlighter for PlainText {
+ type Settings = ();
+ type Highlight = ();
+
+ type Iterator<'a> = std::iter::Empty<(Range<usize>, Self::Highlight)>;
+
+ fn new(_settings: &Self::Settings) -> Self {
+ Self
+ }
+
+ fn update(&mut self, _new_settings: &Self::Settings) {}
+
+ fn change_line(&mut self, _line: usize) {}
+
+ fn highlight_line(&mut self, _line: &str) -> Self::Iterator<'_> {
+ std::iter::empty()
+ }
+
+ fn current_line(&self) -> usize {
+ usize::MAX
+ }
+}
+
+/// The format of some text.
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Format<Font> {
+ /// The [`Color`] of the text.
+ pub color: Option<Color>,
+ /// The `Font` of the text.
+ pub font: Option<Font>,
+}
+
+impl<Font> Default for Format<Font> {
+ fn default() -> Self {
+ Self {
+ color: None,
+ font: None,
+ }
+ }
+}
diff --git a/core/src/text/paragraph.rs b/core/src/text/paragraph.rs
new file mode 100644
index 00000000..de1fb74d
--- /dev/null
+++ b/core/src/text/paragraph.rs
@@ -0,0 +1,59 @@
+use crate::alignment;
+use crate::text::{Difference, Hit, Text};
+use crate::{Point, Size};
+
+/// A text paragraph.
+pub trait Paragraph: Sized + Default {
+ /// The font of this [`Paragraph`].
+ type Font: Copy + PartialEq;
+
+ /// Creates a new [`Paragraph`] laid out with the given [`Text`].
+ fn with_text(text: Text<'_, Self::Font>) -> Self;
+
+ /// Lays out the [`Paragraph`] with some new boundaries.
+ fn resize(&mut self, new_bounds: Size);
+
+ /// Compares the [`Paragraph`] with some desired [`Text`] and returns the
+ /// [`Difference`].
+ fn compare(&self, text: Text<'_, Self::Font>) -> Difference;
+
+ /// Returns the horizontal alignment of the [`Paragraph`].
+ fn horizontal_alignment(&self) -> alignment::Horizontal;
+
+ /// Returns the vertical alignment of the [`Paragraph`].
+ fn vertical_alignment(&self) -> alignment::Vertical;
+
+ /// Returns the minimum boundaries that can fit the contents of the
+ /// [`Paragraph`].
+ fn min_bounds(&self) -> Size;
+
+ /// Tests whether the provided point is within the boundaries of the
+ /// [`Paragraph`], returning information about the nearest character.
+ fn hit_test(&self, point: Point) -> Option<Hit>;
+
+ /// Returns the distance to the given grapheme index in the [`Paragraph`].
+ fn grapheme_position(&self, line: usize, index: usize) -> Option<Point>;
+
+ /// Updates the [`Paragraph`] to match the given [`Text`], if needed.
+ fn update(&mut self, text: Text<'_, Self::Font>) {
+ match self.compare(text) {
+ Difference::None => {}
+ Difference::Bounds => {
+ self.resize(text.bounds);
+ }
+ Difference::Shape => {
+ *self = Self::with_text(text);
+ }
+ }
+ }
+
+ /// Returns the minimum width that can fit the contents of the [`Paragraph`].
+ fn min_width(&self) -> f32 {
+ self.min_bounds().width
+ }
+
+ /// Returns the minimum height that can fit the contents of the [`Paragraph`].
+ fn min_height(&self) -> f32 {
+ self.min_bounds().height
+ }
+}
diff --git a/core/src/time.rs b/core/src/time.rs
index 9355ae6d..dcfe4e41 100644
--- a/core/src/time.rs
+++ b/core/src/time.rs
@@ -1,13 +1,4 @@
//! Keep track of time, both in native and web platforms!
-#[cfg(target_arch = "wasm32")]
-pub use instant::Instant;
-
-#[cfg(target_arch = "wasm32")]
-pub use instant::Duration;
-
-#[cfg(not(target_arch = "wasm32"))]
-pub use std::time::Instant;
-
-#[cfg(not(target_arch = "wasm32"))]
-pub use std::time::Duration;
+pub use web_time::Duration;
+pub use web_time::Instant;
diff --git a/core/src/widget.rs b/core/src/widget.rs
index 294d5984..7f5632ae 100644
--- a/core/src/widget.rs
+++ b/core/src/widget.rs
@@ -15,7 +15,7 @@ use crate::layout::{self, Layout};
use crate::mouse;
use crate::overlay;
use crate::renderer;
-use crate::{Clipboard, Length, Rectangle, Shell};
+use crate::{Clipboard, Length, Rectangle, Shell, Size};
/// A component that displays information and allows interaction.
///
@@ -43,11 +43,16 @@ pub trait Widget<Message, Renderer>
where
Renderer: crate::Renderer,
{
- /// Returns the width of the [`Widget`].
- fn width(&self) -> Length;
+ /// Returns the [`Size`] of the [`Widget`] in lengths.
+ fn size(&self) -> Size<Length>;
- /// Returns the height of the [`Widget`].
- fn height(&self) -> Length;
+ /// Returns a [`Size`] hint for laying out the [`Widget`].
+ ///
+ /// This hint may be used by some widget containers to adjust their sizing strategy
+ /// during construction.
+ fn size_hint(&self) -> Size<Length> {
+ self.size()
+ }
/// Returns the [`layout::Node`] of the [`Widget`].
///
diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs
index ba98f2d8..4cabc7ce 100644
--- a/core/src/widget/text.rs
+++ b/core/src/widget/text.rs
@@ -5,7 +5,9 @@ use crate::mouse;
use crate::renderer;
use crate::text::{self, Paragraph};
use crate::widget::tree::{self, Tree};
-use crate::{Color, Element, Layout, Length, Pixels, Point, Rectangle, Widget};
+use crate::{
+ Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Widget,
+};
use std::borrow::Cow;
@@ -134,12 +136,11 @@ where
tree::State::new(State(Renderer::Paragraph::default()))
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -172,7 +173,7 @@ where
style: &renderer::Style,
layout: Layout<'_>,
_cursor_position: mouse::Cursor,
- _viewport: &Rectangle,
+ viewport: &Rectangle,
) {
let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
@@ -182,6 +183,7 @@ where
layout,
state,
theme.appearance(self.style.clone()),
+ viewport,
);
}
}
@@ -204,17 +206,15 @@ pub fn layout<Renderer>(
where
Renderer: text::Renderer,
{
- let limits = limits.width(width).height(height);
- let bounds = limits.max();
+ layout::sized(limits, width, height, |limits| {
+ let bounds = limits.max();
- let size = size.unwrap_or_else(|| renderer.default_size());
- let font = font.unwrap_or_else(|| renderer.default_font());
+ let size = size.unwrap_or_else(|| renderer.default_size());
+ let font = font.unwrap_or_else(|| renderer.default_font());
- let State(ref mut paragraph) = state;
+ let State(ref mut paragraph) = state;
- renderer.update_paragraph(
- paragraph,
- text::Text {
+ paragraph.update(text::Text {
content,
bounds,
size,
@@ -223,12 +223,10 @@ where
horizontal_alignment,
vertical_alignment,
shaping,
- },
- );
-
- let size = limits.resolve(paragraph.min_bounds());
+ });
- layout::Node::new(size)
+ paragraph.min_bounds()
+ })
}
/// Draws text using the same logic as the [`Text`] widget.
@@ -247,6 +245,7 @@ pub fn draw<Renderer>(
layout: Layout<'_>,
state: &State<Renderer::Paragraph>,
appearance: Appearance,
+ viewport: &Rectangle,
) where
Renderer: text::Renderer,
{
@@ -269,6 +268,7 @@ pub fn draw<Renderer>(
paragraph,
Point::new(x, y),
appearance.color.unwrap_or(style.text_color),
+ *viewport,
);
}
diff --git a/core/src/widget/tree.rs b/core/src/widget/tree.rs
index d4b8828a..ff52b1ce 100644
--- a/core/src/widget/tree.rs
+++ b/core/src/widget/tree.rs
@@ -67,7 +67,7 @@ impl Tree {
}
}
- /// Reconciliates the children of the tree with the provided list of widgets.
+ /// Reconciles the children of the tree with the provided list of widgets.
pub fn diff_children<'a, Message, Renderer>(
&mut self,
new_children: &[impl Borrow<dyn Widget<Message, Renderer> + 'a>],
diff --git a/core/src/window.rs b/core/src/window.rs
index a6dbdfb4..448ffc45 100644
--- a/core/src/window.rs
+++ b/core/src/window.rs
@@ -1,15 +1,21 @@
//! Build window-based GUI applications.
pub mod icon;
+pub mod settings;
mod event;
+mod id;
mod level;
mod mode;
+mod position;
mod redraw_request;
mod user_attention;
pub use event::Event;
pub use icon::Icon;
+pub use id::Id;
pub use level::Level;
pub use mode::Mode;
+pub use position::Position;
pub use redraw_request::RedrawRequest;
+pub use settings::Settings;
pub use user_attention::UserAttention;
diff --git a/core/src/window/event.rs b/core/src/window/event.rs
index e2fb5e66..a14d127f 100644
--- a/core/src/window/event.rs
+++ b/core/src/window/event.rs
@@ -1,10 +1,27 @@
use crate::time::Instant;
+use crate::{Point, Size};
use std::path::PathBuf;
/// A window-related event.
-#[derive(PartialEq, Eq, Clone, Debug)]
+#[derive(PartialEq, Clone, Debug)]
pub enum Event {
+ /// A window was opened.
+ Opened {
+ /// The position of the opened window. This is relative to the top-left corner of the desktop
+ /// the window is on, including virtual desktops. Refers to window's "inner" position,
+ /// or the client area, in logical pixels.
+ ///
+ /// **Note**: Not available in Wayland.
+ position: Option<Point>,
+ /// The size of the created window. This is its "inner" size, or the size of the
+ /// client area, in logical pixels.
+ size: Size,
+ },
+
+ /// A window was closed.
+ Closed,
+
/// A window was moved.
Moved {
/// The new logical x location of the window
@@ -27,9 +44,6 @@ pub enum Event {
RedrawRequested(Instant),
/// The user has requested for the window to close.
- ///
- /// Usually, you will want to terminate the execution whenever this event
- /// occurs.
CloseRequested,
/// A window was focused.
@@ -44,7 +58,7 @@ pub enum Event {
/// for each file separately.
FileHovered(PathBuf),
- /// A file has beend dropped into the window.
+ /// A file has been dropped into the window.
///
/// When the user drops multiple files at once, this event will be emitted
/// for each file separately.
diff --git a/core/src/window/id.rs b/core/src/window/id.rs
new file mode 100644
index 00000000..20474c8f
--- /dev/null
+++ b/core/src/window/id.rs
@@ -0,0 +1,21 @@
+use std::hash::Hash;
+
+use std::sync::atomic::{self, AtomicU64};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
+/// The id of the window.
+///
+/// Internally Iced reserves `window::Id::MAIN` for the first window spawned.
+pub struct Id(u64);
+
+static COUNT: AtomicU64 = AtomicU64::new(1);
+
+impl Id {
+ /// The reserved window [`Id`] for the first window in an Iced application.
+ pub const MAIN: Self = Id(0);
+
+ /// Creates a new unique window [`Id`].
+ pub fn unique() -> Id {
+ Id(COUNT.fetch_add(1, atomic::Ordering::Relaxed))
+ }
+}
diff --git a/winit/src/position.rs b/core/src/window/position.rs
index c260c29e..73391e75 100644
--- a/winit/src/position.rs
+++ b/core/src/window/position.rs
@@ -1,5 +1,7 @@
+use crate::Point;
+
/// The position of a window in a given screen.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Position {
/// The platform-specific default position for a new window.
Default,
@@ -12,7 +14,7 @@ pub enum Position {
/// position. So if you have decorations enabled and want the window to be
/// at (0, 0) you would have to set the position to
/// `(PADDING_X, PADDING_Y)`.
- Specific(i32, i32),
+ Specific(Point),
}
impl Default for Position {
diff --git a/src/window/settings.rs b/core/src/window/settings.rs
index 0ee573e5..fbbf86ab 100644
--- a/src/window/settings.rs
+++ b/core/src/window/settings.rs
@@ -1,21 +1,47 @@
-use crate::window::{Icon, Level, Position};
+//! Configure your windows.
+#[cfg(target_os = "windows")]
+#[path = "settings/windows.rs"]
+mod platform;
+
+#[cfg(target_os = "macos")]
+#[path = "settings/macos.rs"]
+mod platform;
+
+#[cfg(target_os = "linux")]
+#[path = "settings/linux.rs"]
+mod platform;
+
+#[cfg(target_arch = "wasm32")]
+#[path = "settings/wasm.rs"]
+mod platform;
-pub use iced_winit::settings::PlatformSpecific;
+#[cfg(not(any(
+ target_os = "windows",
+ target_os = "macos",
+ target_os = "linux",
+ target_arch = "wasm32"
+)))]
+#[path = "settings/other.rs"]
+mod platform;
+use crate::window::{Icon, Level, Position};
+use crate::Size;
+
+pub use platform::PlatformSpecific;
/// The window settings of an application.
#[derive(Debug, Clone)]
pub struct Settings {
- /// The initial size of the window.
- pub size: (u32, u32),
+ /// The initial logical dimensions of the window.
+ pub size: Size,
/// The initial position of the window.
pub position: Position,
/// The minimum size of the window.
- pub min_size: Option<(u32, u32)>,
+ pub min_size: Option<Size>,
/// The maximum size of the window.
- pub max_size: Option<(u32, u32)>,
+ pub max_size: Option<Size>,
/// Whether the window should be visible or not.
pub visible: bool,
@@ -37,12 +63,22 @@ pub struct Settings {
/// Platform specific settings.
pub platform_specific: PlatformSpecific,
+
+ /// Whether the window will close when the user requests it, e.g. when a user presses the
+ /// close button.
+ ///
+ /// This can be useful if you want to have some behavior that executes before the window is
+ /// actually destroyed. If you disable this, you must manually close the window with the
+ /// `window::close` command.
+ ///
+ /// By default this is enabled.
+ pub exit_on_close_request: bool,
}
impl Default for Settings {
- fn default() -> Settings {
- Settings {
- size: (1024, 768),
+ fn default() -> Self {
+ Self {
+ size: Size::new(1024.0, 768.0),
position: Position::default(),
min_size: None,
max_size: None,
@@ -52,25 +88,8 @@ impl Default for Settings {
transparent: false,
level: Level::default(),
icon: None,
+ exit_on_close_request: true,
platform_specific: PlatformSpecific::default(),
}
}
}
-
-impl From<Settings> for iced_winit::settings::Window {
- fn from(settings: Settings) -> Self {
- Self {
- size: settings.size,
- position: iced_winit::Position::from(settings.position),
- min_size: settings.min_size,
- max_size: settings.max_size,
- visible: settings.visible,
- resizable: settings.resizable,
- decorations: settings.decorations,
- transparent: settings.transparent,
- level: settings.level,
- icon: settings.icon.map(Icon::into),
- platform_specific: settings.platform_specific,
- }
- }
-}
diff --git a/winit/src/settings/linux.rs b/core/src/window/settings/linux.rs
index 009b9d9e..009b9d9e 100644
--- a/winit/src/settings/linux.rs
+++ b/core/src/window/settings/linux.rs
diff --git a/winit/src/settings/macos.rs b/core/src/window/settings/macos.rs
index f86e63ad..f86e63ad 100644
--- a/winit/src/settings/macos.rs
+++ b/core/src/window/settings/macos.rs
diff --git a/winit/src/settings/other.rs b/core/src/window/settings/other.rs
index b1103f62..b1103f62 100644
--- a/winit/src/settings/other.rs
+++ b/core/src/window/settings/other.rs
diff --git a/winit/src/settings/wasm.rs b/core/src/window/settings/wasm.rs
index 8e0f1bbc..8e0f1bbc 100644
--- a/winit/src/settings/wasm.rs
+++ b/core/src/window/settings/wasm.rs
diff --git a/winit/src/settings/windows.rs b/core/src/window/settings/windows.rs
index 45d753bd..45d753bd 100644
--- a/winit/src/settings/windows.rs
+++ b/core/src/window/settings/windows.rs
diff --git a/examples/custom_quad/src/main.rs b/examples/custom_quad/src/main.rs
index 13b08250..cc9ad528 100644
--- a/examples/custom_quad/src/main.rs
+++ b/examples/custom_quad/src/main.rs
@@ -26,12 +26,11 @@ mod quad {
where
Renderer: renderer::Renderer,
{
- fn width(&self) -> Length {
- Length::Shrink
- }
-
- fn height(&self) -> Length {
- Length::Shrink
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: Length::Shrink,
+ height: Length::Shrink,
+ }
}
fn layout(
diff --git a/examples/custom_shader/Cargo.toml b/examples/custom_shader/Cargo.toml
new file mode 100644
index 00000000..b602f98d
--- /dev/null
+++ b/examples/custom_shader/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "custom_shader"
+version = "0.1.0"
+authors = ["Bingus <shankern@protonmail.com>"]
+edition = "2021"
+
+[dependencies]
+iced.workspace = true
+iced.features = ["debug", "advanced"]
+
+image.workspace = true
+bytemuck.workspace = true
+
+glam.workspace = true
+glam.features = ["bytemuck"]
+
+rand = "0.8.5"
diff --git a/examples/custom_shader/src/main.rs b/examples/custom_shader/src/main.rs
new file mode 100644
index 00000000..3bfa3a43
--- /dev/null
+++ b/examples/custom_shader/src/main.rs
@@ -0,0 +1,163 @@
+mod scene;
+
+use scene::Scene;
+
+use iced::executor;
+use iced::time::Instant;
+use iced::widget::shader::wgpu;
+use iced::widget::{checkbox, column, container, row, shader, slider, text};
+use iced::window;
+use iced::{
+ Alignment, Application, Color, Command, Element, Length, Renderer,
+ Subscription, Theme,
+};
+
+fn main() -> iced::Result {
+ IcedCubes::run(iced::Settings::default())
+}
+
+struct IcedCubes {
+ start: Instant,
+ scene: Scene,
+}
+
+#[derive(Debug, Clone)]
+enum Message {
+ CubeAmountChanged(u32),
+ CubeSizeChanged(f32),
+ Tick(Instant),
+ ShowDepthBuffer(bool),
+ LightColorChanged(Color),
+}
+
+impl Application for IcedCubes {
+ type Executor = executor::Default;
+ type Message = Message;
+ type Theme = Theme;
+ type Flags = ();
+
+ fn new(_flags: Self::Flags) -> (Self, Command<Self::Message>) {
+ (
+ Self {
+ start: Instant::now(),
+ scene: Scene::new(),
+ },
+ Command::none(),
+ )
+ }
+
+ fn title(&self) -> String {
+ "Iced Cubes".to_string()
+ }
+
+ fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
+ match message {
+ Message::CubeAmountChanged(amount) => {
+ self.scene.change_amount(amount);
+ }
+ Message::CubeSizeChanged(size) => {
+ self.scene.size = size;
+ }
+ Message::Tick(time) => {
+ self.scene.update(time - self.start);
+ }
+ Message::ShowDepthBuffer(show) => {
+ self.scene.show_depth_buffer = show;
+ }
+ Message::LightColorChanged(color) => {
+ self.scene.light_color = color;
+ }
+ }
+
+ Command::none()
+ }
+
+ fn view(&self) -> Element<'_, Self::Message, Renderer<Self::Theme>> {
+ let top_controls = row![
+ control(
+ "Amount",
+ slider(
+ 1..=scene::MAX,
+ self.scene.cubes.len() as u32,
+ Message::CubeAmountChanged
+ )
+ .width(100)
+ ),
+ control(
+ "Size",
+ slider(0.1..=0.25, self.scene.size, Message::CubeSizeChanged)
+ .step(0.01)
+ .width(100),
+ ),
+ checkbox(
+ "Show Depth Buffer",
+ self.scene.show_depth_buffer,
+ Message::ShowDepthBuffer
+ ),
+ ]
+ .spacing(40);
+
+ let bottom_controls = row![
+ control(
+ "R",
+ slider(0.0..=1.0, self.scene.light_color.r, move |r| {
+ Message::LightColorChanged(Color {
+ r,
+ ..self.scene.light_color
+ })
+ })
+ .step(0.01)
+ .width(100)
+ ),
+ control(
+ "G",
+ slider(0.0..=1.0, self.scene.light_color.g, move |g| {
+ Message::LightColorChanged(Color {
+ g,
+ ..self.scene.light_color
+ })
+ })
+ .step(0.01)
+ .width(100)
+ ),
+ control(
+ "B",
+ slider(0.0..=1.0, self.scene.light_color.b, move |b| {
+ Message::LightColorChanged(Color {
+ b,
+ ..self.scene.light_color
+ })
+ })
+ .step(0.01)
+ .width(100)
+ )
+ ]
+ .spacing(40);
+
+ let controls = column![top_controls, bottom_controls,]
+ .spacing(10)
+ .padding(20)
+ .align_items(Alignment::Center);
+
+ let shader =
+ shader(&self.scene).width(Length::Fill).height(Length::Fill);
+
+ container(column![shader, controls].align_items(Alignment::Center))
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .center_x()
+ .center_y()
+ .into()
+ }
+
+ fn subscription(&self) -> Subscription<Self::Message> {
+ window::frames().map(Message::Tick)
+ }
+}
+
+fn control<'a>(
+ label: &'static str,
+ control: impl Into<Element<'a, Message>>,
+) -> Element<'a, Message> {
+ row![text(label), control.into()].spacing(10).into()
+}
diff --git a/examples/custom_shader/src/scene.rs b/examples/custom_shader/src/scene.rs
new file mode 100644
index 00000000..a35efdd9
--- /dev/null
+++ b/examples/custom_shader/src/scene.rs
@@ -0,0 +1,186 @@
+mod camera;
+mod pipeline;
+
+use camera::Camera;
+use pipeline::Pipeline;
+
+use crate::wgpu;
+use pipeline::cube::{self, Cube};
+
+use iced::mouse;
+use iced::time::Duration;
+use iced::widget::shader;
+use iced::{Color, Rectangle, Size};
+
+use glam::Vec3;
+use rand::Rng;
+use std::cmp::Ordering;
+use std::iter;
+
+pub const MAX: u32 = 500;
+
+#[derive(Clone)]
+pub struct Scene {
+ pub size: f32,
+ pub cubes: Vec<Cube>,
+ pub camera: Camera,
+ pub show_depth_buffer: bool,
+ pub light_color: Color,
+}
+
+impl Scene {
+ pub fn new() -> Self {
+ let mut scene = Self {
+ size: 0.2,
+ cubes: vec![],
+ camera: Camera::default(),
+ show_depth_buffer: false,
+ light_color: Color::WHITE,
+ };
+
+ scene.change_amount(MAX);
+
+ scene
+ }
+
+ pub fn update(&mut self, time: Duration) {
+ for cube in self.cubes.iter_mut() {
+ cube.update(self.size, time.as_secs_f32());
+ }
+ }
+
+ pub fn change_amount(&mut self, amount: u32) {
+ let curr_cubes = self.cubes.len() as u32;
+
+ match amount.cmp(&curr_cubes) {
+ Ordering::Greater => {
+ // spawn
+ let cubes_2_spawn = (amount - curr_cubes) as usize;
+
+ let mut cubes = 0;
+ self.cubes.extend(iter::from_fn(|| {
+ if cubes < cubes_2_spawn {
+ cubes += 1;
+ Some(Cube::new(self.size, rnd_origin()))
+ } else {
+ None
+ }
+ }));
+ }
+ Ordering::Less => {
+ // chop
+ let cubes_2_cut = curr_cubes - amount;
+ let new_len = self.cubes.len() - cubes_2_cut as usize;
+ self.cubes.truncate(new_len);
+ }
+ Ordering::Equal => {}
+ }
+ }
+}
+
+impl<Message> shader::Program<Message> for Scene {
+ type State = ();
+ type Primitive = Primitive;
+
+ fn draw(
+ &self,
+ _state: &Self::State,
+ _cursor: mouse::Cursor,
+ bounds: Rectangle,
+ ) -> Self::Primitive {
+ Primitive::new(
+ &self.cubes,
+ &self.camera,
+ bounds,
+ self.show_depth_buffer,
+ self.light_color,
+ )
+ }
+}
+
+/// A collection of `Cube`s that can be rendered.
+#[derive(Debug)]
+pub struct Primitive {
+ cubes: Vec<cube::Raw>,
+ uniforms: pipeline::Uniforms,
+ show_depth_buffer: bool,
+}
+
+impl Primitive {
+ pub fn new(
+ cubes: &[Cube],
+ camera: &Camera,
+ bounds: Rectangle,
+ show_depth_buffer: bool,
+ light_color: Color,
+ ) -> Self {
+ let uniforms = pipeline::Uniforms::new(camera, bounds, light_color);
+
+ Self {
+ cubes: cubes
+ .iter()
+ .map(cube::Raw::from_cube)
+ .collect::<Vec<cube::Raw>>(),
+ uniforms,
+ show_depth_buffer,
+ }
+ }
+}
+
+impl shader::Primitive for Primitive {
+ fn prepare(
+ &self,
+ format: wgpu::TextureFormat,
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ _bounds: Rectangle,
+ target_size: Size<u32>,
+ _scale_factor: f32,
+ storage: &mut shader::Storage,
+ ) {
+ if !storage.has::<Pipeline>() {
+ storage.store(Pipeline::new(device, queue, format, target_size));
+ }
+
+ let pipeline = storage.get_mut::<Pipeline>().unwrap();
+
+ //upload data to GPU
+ pipeline.update(
+ device,
+ queue,
+ target_size,
+ &self.uniforms,
+ self.cubes.len(),
+ &self.cubes,
+ );
+ }
+
+ fn render(
+ &self,
+ storage: &shader::Storage,
+ target: &wgpu::TextureView,
+ _target_size: Size<u32>,
+ viewport: Rectangle<u32>,
+ encoder: &mut wgpu::CommandEncoder,
+ ) {
+ //at this point our pipeline should always be initialized
+ let pipeline = storage.get::<Pipeline>().unwrap();
+
+ //render primitive
+ pipeline.render(
+ target,
+ encoder,
+ viewport,
+ self.cubes.len() as u32,
+ self.show_depth_buffer,
+ );
+ }
+}
+
+fn rnd_origin() -> Vec3 {
+ Vec3::new(
+ rand::thread_rng().gen_range(-4.0..4.0),
+ rand::thread_rng().gen_range(-4.0..4.0),
+ rand::thread_rng().gen_range(-4.0..2.0),
+ )
+}
diff --git a/examples/custom_shader/src/scene/camera.rs b/examples/custom_shader/src/scene/camera.rs
new file mode 100644
index 00000000..2a49c102
--- /dev/null
+++ b/examples/custom_shader/src/scene/camera.rs
@@ -0,0 +1,53 @@
+use glam::{mat4, vec3, vec4};
+use iced::Rectangle;
+
+#[derive(Copy, Clone)]
+pub struct Camera {
+ eye: glam::Vec3,
+ target: glam::Vec3,
+ up: glam::Vec3,
+ fov_y: f32,
+ near: f32,
+ far: f32,
+}
+
+impl Default for Camera {
+ fn default() -> Self {
+ Self {
+ eye: vec3(0.0, 2.0, 3.0),
+ target: glam::Vec3::ZERO,
+ up: glam::Vec3::Y,
+ fov_y: 45.0,
+ near: 0.1,
+ far: 100.0,
+ }
+ }
+}
+
+pub const OPENGL_TO_WGPU_MATRIX: glam::Mat4 = mat4(
+ vec4(1.0, 0.0, 0.0, 0.0),
+ vec4(0.0, 1.0, 0.0, 0.0),
+ vec4(0.0, 0.0, 0.5, 0.0),
+ vec4(0.0, 0.0, 0.5, 1.0),
+);
+
+impl Camera {
+ pub fn build_view_proj_matrix(&self, bounds: Rectangle) -> glam::Mat4 {
+ //TODO looks distorted without padding; base on surface texture size instead?
+ let aspect_ratio = bounds.width / (bounds.height + 150.0);
+
+ let view = glam::Mat4::look_at_rh(self.eye, self.target, self.up);
+ let proj = glam::Mat4::perspective_rh(
+ self.fov_y,
+ aspect_ratio,
+ self.near,
+ self.far,
+ );
+
+ OPENGL_TO_WGPU_MATRIX * proj * view
+ }
+
+ pub fn position(&self) -> glam::Vec4 {
+ glam::Vec4::from((self.eye, 0.0))
+ }
+}
diff --git a/examples/custom_shader/src/scene/pipeline.rs b/examples/custom_shader/src/scene/pipeline.rs
new file mode 100644
index 00000000..50b70a98
--- /dev/null
+++ b/examples/custom_shader/src/scene/pipeline.rs
@@ -0,0 +1,621 @@
+pub mod cube;
+
+mod buffer;
+mod uniforms;
+mod vertex;
+
+pub use uniforms::Uniforms;
+
+use buffer::Buffer;
+use vertex::Vertex;
+
+use crate::wgpu;
+use crate::wgpu::util::DeviceExt;
+
+use iced::{Rectangle, Size};
+
+const SKY_TEXTURE_SIZE: u32 = 128;
+
+pub struct Pipeline {
+ pipeline: wgpu::RenderPipeline,
+ vertices: wgpu::Buffer,
+ cubes: Buffer,
+ uniforms: wgpu::Buffer,
+ uniform_bind_group: wgpu::BindGroup,
+ depth_texture_size: Size<u32>,
+ depth_view: wgpu::TextureView,
+ depth_pipeline: DepthPipeline,
+}
+
+impl Pipeline {
+ pub fn new(
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ format: wgpu::TextureFormat,
+ target_size: Size<u32>,
+ ) -> Self {
+ //vertices of one cube
+ let vertices =
+ device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+ label: Some("cubes vertex buffer"),
+ contents: bytemuck::cast_slice(&cube::Raw::vertices()),
+ usage: wgpu::BufferUsages::VERTEX,
+ });
+
+ //cube instance data
+ let cubes_buffer = Buffer::new(
+ device,
+ "cubes instance buffer",
+ std::mem::size_of::<cube::Raw>() as u64,
+ wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+ );
+
+ //uniforms for all cubes
+ let uniforms = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("cubes uniform buffer"),
+ size: std::mem::size_of::<Uniforms>() as u64,
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+
+ //depth buffer
+ let depth_texture = device.create_texture(&wgpu::TextureDescriptor {
+ label: Some("cubes depth texture"),
+ size: wgpu::Extent3d {
+ width: target_size.width,
+ height: target_size.height,
+ depth_or_array_layers: 1,
+ },
+ mip_level_count: 1,
+ sample_count: 1,
+ dimension: wgpu::TextureDimension::D2,
+ format: wgpu::TextureFormat::Depth32Float,
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT
+ | wgpu::TextureUsages::TEXTURE_BINDING,
+ view_formats: &[],
+ });
+
+ let depth_view =
+ depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
+
+ let normal_map_data = load_normal_map_data();
+
+ //normal map
+ let normal_texture = device.create_texture_with_data(
+ queue,
+ &wgpu::TextureDescriptor {
+ label: Some("cubes normal map texture"),
+ size: wgpu::Extent3d {
+ width: 1024,
+ height: 1024,
+ depth_or_array_layers: 1,
+ },
+ mip_level_count: 1,
+ sample_count: 1,
+ dimension: wgpu::TextureDimension::D2,
+ format: wgpu::TextureFormat::Rgba8Unorm,
+ usage: wgpu::TextureUsages::TEXTURE_BINDING,
+ view_formats: &[],
+ },
+ wgpu::util::TextureDataOrder::LayerMajor,
+ &normal_map_data,
+ );
+
+ let normal_view =
+ normal_texture.create_view(&wgpu::TextureViewDescriptor::default());
+
+ //skybox texture for reflection/refraction
+ let skybox_data = load_skybox_data();
+
+ let skybox_texture = device.create_texture_with_data(
+ queue,
+ &wgpu::TextureDescriptor {
+ label: Some("cubes skybox texture"),
+ size: wgpu::Extent3d {
+ width: SKY_TEXTURE_SIZE,
+ height: SKY_TEXTURE_SIZE,
+ depth_or_array_layers: 6, //one for each face of the cube
+ },
+ mip_level_count: 1,
+ sample_count: 1,
+ dimension: wgpu::TextureDimension::D2,
+ format: wgpu::TextureFormat::Rgba8Unorm,
+ usage: wgpu::TextureUsages::TEXTURE_BINDING,
+ view_formats: &[],
+ },
+ wgpu::util::TextureDataOrder::LayerMajor,
+ &skybox_data,
+ );
+
+ let sky_view =
+ skybox_texture.create_view(&wgpu::TextureViewDescriptor {
+ label: Some("cubes skybox texture view"),
+ dimension: Some(wgpu::TextureViewDimension::Cube),
+ ..Default::default()
+ });
+
+ let sky_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
+ label: Some("cubes skybox sampler"),
+ address_mode_u: wgpu::AddressMode::ClampToEdge,
+ address_mode_v: wgpu::AddressMode::ClampToEdge,
+ address_mode_w: wgpu::AddressMode::ClampToEdge,
+ mag_filter: wgpu::FilterMode::Linear,
+ min_filter: wgpu::FilterMode::Linear,
+ mipmap_filter: wgpu::FilterMode::Linear,
+ ..Default::default()
+ });
+
+ let uniform_bind_group_layout =
+ device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("cubes uniform bind group layout"),
+ entries: &[
+ wgpu::BindGroupLayoutEntry {
+ binding: 0,
+ visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
+ ty: wgpu::BindingType::Buffer {
+ ty: wgpu::BufferBindingType::Uniform,
+ has_dynamic_offset: false,
+ min_binding_size: None,
+ },
+ count: None,
+ },
+ wgpu::BindGroupLayoutEntry {
+ binding: 1,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Texture {
+ sample_type: wgpu::TextureSampleType::Float {
+ filterable: true,
+ },
+ view_dimension: wgpu::TextureViewDimension::Cube,
+ multisampled: false,
+ },
+ count: None,
+ },
+ wgpu::BindGroupLayoutEntry {
+ binding: 2,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Sampler(
+ wgpu::SamplerBindingType::Filtering,
+ ),
+ count: None,
+ },
+ wgpu::BindGroupLayoutEntry {
+ binding: 3,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Texture {
+ sample_type: wgpu::TextureSampleType::Float {
+ filterable: true,
+ },
+ view_dimension: wgpu::TextureViewDimension::D2,
+ multisampled: false,
+ },
+ count: None,
+ },
+ ],
+ });
+
+ let uniform_bind_group =
+ device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("cubes uniform bind group"),
+ layout: &uniform_bind_group_layout,
+ entries: &[
+ wgpu::BindGroupEntry {
+ binding: 0,
+ resource: uniforms.as_entire_binding(),
+ },
+ wgpu::BindGroupEntry {
+ binding: 1,
+ resource: wgpu::BindingResource::TextureView(&sky_view),
+ },
+ wgpu::BindGroupEntry {
+ binding: 2,
+ resource: wgpu::BindingResource::Sampler(&sky_sampler),
+ },
+ wgpu::BindGroupEntry {
+ binding: 3,
+ resource: wgpu::BindingResource::TextureView(
+ &normal_view,
+ ),
+ },
+ ],
+ });
+
+ let layout =
+ device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("cubes pipeline layout"),
+ bind_group_layouts: &[&uniform_bind_group_layout],
+ push_constant_ranges: &[],
+ });
+
+ let shader =
+ device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("cubes shader"),
+ source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
+ include_str!("../shaders/cubes.wgsl"),
+ )),
+ });
+
+ let pipeline =
+ device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: Some("cubes pipeline"),
+ layout: Some(&layout),
+ vertex: wgpu::VertexState {
+ module: &shader,
+ entry_point: "vs_main",
+ buffers: &[Vertex::desc(), cube::Raw::desc()],
+ },
+ primitive: wgpu::PrimitiveState::default(),
+ depth_stencil: Some(wgpu::DepthStencilState {
+ format: wgpu::TextureFormat::Depth32Float,
+ depth_write_enabled: true,
+ depth_compare: wgpu::CompareFunction::Less,
+ stencil: wgpu::StencilState::default(),
+ bias: wgpu::DepthBiasState::default(),
+ }),
+ multisample: wgpu::MultisampleState {
+ count: 1,
+ mask: !0,
+ alpha_to_coverage_enabled: false,
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: &shader,
+ entry_point: "fs_main",
+ targets: &[Some(wgpu::ColorTargetState {
+ format,
+ blend: Some(wgpu::BlendState {
+ color: wgpu::BlendComponent {
+ src_factor: wgpu::BlendFactor::SrcAlpha,
+ dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
+ operation: wgpu::BlendOperation::Add,
+ },
+ alpha: wgpu::BlendComponent {
+ src_factor: wgpu::BlendFactor::One,
+ dst_factor: wgpu::BlendFactor::One,
+ operation: wgpu::BlendOperation::Max,
+ },
+ }),
+ write_mask: wgpu::ColorWrites::ALL,
+ })],
+ }),
+ multiview: None,
+ });
+
+ let depth_pipeline = DepthPipeline::new(
+ device,
+ format,
+ depth_texture.create_view(&wgpu::TextureViewDescriptor::default()),
+ );
+
+ Self {
+ pipeline,
+ cubes: cubes_buffer,
+ uniforms,
+ uniform_bind_group,
+ vertices,
+ depth_texture_size: target_size,
+ depth_view,
+ depth_pipeline,
+ }
+ }
+
+ fn update_depth_texture(&mut self, device: &wgpu::Device, size: Size<u32>) {
+ if self.depth_texture_size.height != size.height
+ || self.depth_texture_size.width != size.width
+ {
+ let text = device.create_texture(&wgpu::TextureDescriptor {
+ label: Some("cubes depth texture"),
+ size: wgpu::Extent3d {
+ width: size.width,
+ height: size.height,
+ depth_or_array_layers: 1,
+ },
+ mip_level_count: 1,
+ sample_count: 1,
+ dimension: wgpu::TextureDimension::D2,
+ format: wgpu::TextureFormat::Depth32Float,
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT
+ | wgpu::TextureUsages::TEXTURE_BINDING,
+ view_formats: &[],
+ });
+
+ self.depth_view =
+ text.create_view(&wgpu::TextureViewDescriptor::default());
+ self.depth_texture_size = size;
+
+ self.depth_pipeline.update(device, &text);
+ }
+ }
+
+ pub fn update(
+ &mut self,
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ target_size: Size<u32>,
+ uniforms: &Uniforms,
+ num_cubes: usize,
+ cubes: &[cube::Raw],
+ ) {
+ //recreate depth texture if surface texture size has changed
+ self.update_depth_texture(device, target_size);
+
+ // update uniforms
+ queue.write_buffer(&self.uniforms, 0, bytemuck::bytes_of(uniforms));
+
+ //resize cubes vertex buffer if cubes amount changed
+ let new_size = num_cubes * std::mem::size_of::<cube::Raw>();
+ self.cubes.resize(device, new_size as u64);
+
+ //always write new cube data since they are constantly rotating
+ queue.write_buffer(&self.cubes.raw, 0, bytemuck::cast_slice(cubes));
+ }
+
+ pub fn render(
+ &self,
+ target: &wgpu::TextureView,
+ encoder: &mut wgpu::CommandEncoder,
+ viewport: Rectangle<u32>,
+ num_cubes: u32,
+ show_depth: bool,
+ ) {
+ {
+ let mut pass =
+ encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some("cubes.pipeline.pass"),
+ color_attachments: &[Some(
+ wgpu::RenderPassColorAttachment {
+ view: target,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Load,
+ store: wgpu::StoreOp::Store,
+ },
+ },
+ )],
+ depth_stencil_attachment: Some(
+ wgpu::RenderPassDepthStencilAttachment {
+ view: &self.depth_view,
+ depth_ops: Some(wgpu::Operations {
+ load: wgpu::LoadOp::Clear(1.0),
+ store: wgpu::StoreOp::Store,
+ }),
+ stencil_ops: None,
+ },
+ ),
+ timestamp_writes: None,
+ occlusion_query_set: None,
+ });
+
+ pass.set_scissor_rect(
+ viewport.x,
+ viewport.y,
+ viewport.width,
+ viewport.height,
+ );
+ pass.set_pipeline(&self.pipeline);
+ pass.set_bind_group(0, &self.uniform_bind_group, &[]);
+ pass.set_vertex_buffer(0, self.vertices.slice(..));
+ pass.set_vertex_buffer(1, self.cubes.raw.slice(..));
+ pass.draw(0..36, 0..num_cubes);
+ }
+
+ if show_depth {
+ self.depth_pipeline.render(encoder, target, viewport);
+ }
+ }
+}
+
+struct DepthPipeline {
+ pipeline: wgpu::RenderPipeline,
+ bind_group_layout: wgpu::BindGroupLayout,
+ bind_group: wgpu::BindGroup,
+ sampler: wgpu::Sampler,
+ depth_view: wgpu::TextureView,
+}
+
+impl DepthPipeline {
+ pub fn new(
+ device: &wgpu::Device,
+ format: wgpu::TextureFormat,
+ depth_texture: wgpu::TextureView,
+ ) -> Self {
+ let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
+ label: Some("cubes.depth_pipeline.sampler"),
+ ..Default::default()
+ });
+
+ let bind_group_layout =
+ device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("cubes.depth_pipeline.bind_group_layout"),
+ entries: &[
+ wgpu::BindGroupLayoutEntry {
+ binding: 0,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Sampler(
+ wgpu::SamplerBindingType::NonFiltering,
+ ),
+ count: None,
+ },
+ wgpu::BindGroupLayoutEntry {
+ binding: 1,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Texture {
+ sample_type: wgpu::TextureSampleType::Float {
+ filterable: false,
+ },
+ view_dimension: wgpu::TextureViewDimension::D2,
+ multisampled: false,
+ },
+ count: None,
+ },
+ ],
+ });
+
+ let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("cubes.depth_pipeline.bind_group"),
+ layout: &bind_group_layout,
+ entries: &[
+ wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::Sampler(&sampler),
+ },
+ wgpu::BindGroupEntry {
+ binding: 1,
+ resource: wgpu::BindingResource::TextureView(
+ &depth_texture,
+ ),
+ },
+ ],
+ });
+
+ let layout =
+ device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("cubes.depth_pipeline.layout"),
+ bind_group_layouts: &[&bind_group_layout],
+ push_constant_ranges: &[],
+ });
+
+ let shader =
+ device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("cubes.depth_pipeline.shader"),
+ source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
+ include_str!("../shaders/depth.wgsl"),
+ )),
+ });
+
+ let pipeline =
+ device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: Some("cubes.depth_pipeline.pipeline"),
+ layout: Some(&layout),
+ vertex: wgpu::VertexState {
+ module: &shader,
+ entry_point: "vs_main",
+ buffers: &[],
+ },
+ primitive: wgpu::PrimitiveState::default(),
+ depth_stencil: Some(wgpu::DepthStencilState {
+ format: wgpu::TextureFormat::Depth32Float,
+ depth_write_enabled: false,
+ depth_compare: wgpu::CompareFunction::Less,
+ stencil: wgpu::StencilState::default(),
+ bias: wgpu::DepthBiasState::default(),
+ }),
+ multisample: wgpu::MultisampleState::default(),
+ fragment: Some(wgpu::FragmentState {
+ module: &shader,
+ entry_point: "fs_main",
+ targets: &[Some(wgpu::ColorTargetState {
+ format,
+ blend: Some(wgpu::BlendState::REPLACE),
+ write_mask: wgpu::ColorWrites::ALL,
+ })],
+ }),
+ multiview: None,
+ });
+
+ Self {
+ pipeline,
+ bind_group_layout,
+ bind_group,
+ sampler,
+ depth_view: depth_texture,
+ }
+ }
+
+ pub fn update(
+ &mut self,
+ device: &wgpu::Device,
+ depth_texture: &wgpu::Texture,
+ ) {
+ self.depth_view =
+ depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
+
+ self.bind_group =
+ device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("cubes.depth_pipeline.bind_group"),
+ layout: &self.bind_group_layout,
+ entries: &[
+ wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::Sampler(&self.sampler),
+ },
+ wgpu::BindGroupEntry {
+ binding: 1,
+ resource: wgpu::BindingResource::TextureView(
+ &self.depth_view,
+ ),
+ },
+ ],
+ });
+ }
+
+ pub fn render(
+ &self,
+ encoder: &mut wgpu::CommandEncoder,
+ target: &wgpu::TextureView,
+ viewport: Rectangle<u32>,
+ ) {
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some("cubes.pipeline.depth_pass"),
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+ view: target,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Load,
+ store: wgpu::StoreOp::Store,
+ },
+ })],
+ depth_stencil_attachment: Some(
+ wgpu::RenderPassDepthStencilAttachment {
+ view: &self.depth_view,
+ depth_ops: None,
+ stencil_ops: None,
+ },
+ ),
+ timestamp_writes: None,
+ occlusion_query_set: None,
+ });
+
+ pass.set_scissor_rect(
+ viewport.x,
+ viewport.y,
+ viewport.width,
+ viewport.height,
+ );
+ pass.set_pipeline(&self.pipeline);
+ pass.set_bind_group(0, &self.bind_group, &[]);
+ pass.draw(0..6, 0..1);
+ }
+}
+
+fn load_skybox_data() -> Vec<u8> {
+ let pos_x: &[u8] = include_bytes!("../../textures/skybox/pos_x.jpg");
+ let neg_x: &[u8] = include_bytes!("../../textures/skybox/neg_x.jpg");
+ let pos_y: &[u8] = include_bytes!("../../textures/skybox/pos_y.jpg");
+ let neg_y: &[u8] = include_bytes!("../../textures/skybox/neg_y.jpg");
+ let pos_z: &[u8] = include_bytes!("../../textures/skybox/pos_z.jpg");
+ let neg_z: &[u8] = include_bytes!("../../textures/skybox/neg_z.jpg");
+
+ let data: [&[u8]; 6] = [pos_x, neg_x, pos_y, neg_y, pos_z, neg_z];
+
+ data.iter().fold(vec![], |mut acc, bytes| {
+ let i = image::load_from_memory_with_format(
+ bytes,
+ image::ImageFormat::Jpeg,
+ )
+ .unwrap()
+ .to_rgba8()
+ .into_raw();
+
+ acc.extend(i);
+ acc
+ })
+}
+
+fn load_normal_map_data() -> Vec<u8> {
+ let bytes: &[u8] = include_bytes!("../../textures/ice_cube_normal_map.png");
+
+ image::load_from_memory_with_format(bytes, image::ImageFormat::Png)
+ .unwrap()
+ .to_rgba8()
+ .into_raw()
+}
diff --git a/examples/custom_shader/src/scene/pipeline/buffer.rs b/examples/custom_shader/src/scene/pipeline/buffer.rs
new file mode 100644
index 00000000..ef4c41c9
--- /dev/null
+++ b/examples/custom_shader/src/scene/pipeline/buffer.rs
@@ -0,0 +1,41 @@
+use crate::wgpu;
+
+// A custom buffer container for dynamic resizing.
+pub struct Buffer {
+ pub raw: wgpu::Buffer,
+ label: &'static str,
+ size: u64,
+ usage: wgpu::BufferUsages,
+}
+
+impl Buffer {
+ pub fn new(
+ device: &wgpu::Device,
+ label: &'static str,
+ size: u64,
+ usage: wgpu::BufferUsages,
+ ) -> Self {
+ Self {
+ raw: device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some(label),
+ size,
+ usage,
+ mapped_at_creation: false,
+ }),
+ label,
+ size,
+ usage,
+ }
+ }
+
+ pub fn resize(&mut self, device: &wgpu::Device, new_size: u64) {
+ if new_size > self.size {
+ self.raw = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some(self.label),
+ size: new_size,
+ usage: self.usage,
+ mapped_at_creation: false,
+ });
+ }
+ }
+}
diff --git a/examples/custom_shader/src/scene/pipeline/cube.rs b/examples/custom_shader/src/scene/pipeline/cube.rs
new file mode 100644
index 00000000..de8bad6c
--- /dev/null
+++ b/examples/custom_shader/src/scene/pipeline/cube.rs
@@ -0,0 +1,326 @@
+use crate::scene::pipeline::Vertex;
+use crate::wgpu;
+
+use glam::{vec2, vec3, Vec3};
+use rand::{thread_rng, Rng};
+
+/// A single instance of a cube.
+#[derive(Debug, Clone)]
+pub struct Cube {
+ pub rotation: glam::Quat,
+ pub position: Vec3,
+ pub size: f32,
+ rotation_dir: f32,
+ rotation_axis: glam::Vec3,
+}
+
+impl Default for Cube {
+ fn default() -> Self {
+ Self {
+ rotation: glam::Quat::IDENTITY,
+ position: glam::Vec3::ZERO,
+ size: 0.1,
+ rotation_dir: 1.0,
+ rotation_axis: glam::Vec3::Y,
+ }
+ }
+}
+
+impl Cube {
+ pub fn new(size: f32, origin: Vec3) -> Self {
+ let rnd = thread_rng().gen_range(0.0..=1.0f32);
+
+ Self {
+ rotation: glam::Quat::IDENTITY,
+ position: origin + Vec3::new(0.1, 0.1, 0.1),
+ size,
+ rotation_dir: if rnd <= 0.5 { -1.0 } else { 1.0 },
+ rotation_axis: if rnd <= 0.33 {
+ glam::Vec3::Y
+ } else if rnd <= 0.66 {
+ glam::Vec3::X
+ } else {
+ glam::Vec3::Z
+ },
+ }
+ }
+
+ pub fn update(&mut self, size: f32, time: f32) {
+ self.rotation = glam::Quat::from_axis_angle(
+ self.rotation_axis,
+ time / 2.0 * self.rotation_dir,
+ );
+ self.size = size;
+ }
+}
+
+#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable, Debug)]
+#[repr(C)]
+pub struct Raw {
+ transformation: glam::Mat4,
+ normal: glam::Mat3,
+ _padding: [f32; 3],
+}
+
+impl Raw {
+ const ATTRIBS: [wgpu::VertexAttribute; 7] = wgpu::vertex_attr_array![
+ //cube transformation matrix
+ 4 => Float32x4,
+ 5 => Float32x4,
+ 6 => Float32x4,
+ 7 => Float32x4,
+ //normal rotation matrix
+ 8 => Float32x3,
+ 9 => Float32x3,
+ 10 => Float32x3,
+ ];
+
+ pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
+ wgpu::VertexBufferLayout {
+ array_stride: std::mem::size_of::<Self>() as wgpu::BufferAddress,
+ step_mode: wgpu::VertexStepMode::Instance,
+ attributes: &Self::ATTRIBS,
+ }
+ }
+}
+
+impl Raw {
+ pub fn from_cube(cube: &Cube) -> Raw {
+ Raw {
+ transformation: glam::Mat4::from_scale_rotation_translation(
+ glam::vec3(cube.size, cube.size, cube.size),
+ cube.rotation,
+ cube.position,
+ ),
+ normal: glam::Mat3::from_quat(cube.rotation),
+ _padding: [0.0; 3],
+ }
+ }
+
+ pub fn vertices() -> [Vertex; 36] {
+ [
+ //face 1
+ Vertex {
+ pos: vec3(-0.5, -0.5, -0.5),
+ normal: vec3(0.0, 0.0, -1.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(0.0, 1.0),
+ },
+ Vertex {
+ pos: vec3(0.5, -0.5, -0.5),
+ normal: vec3(0.0, 0.0, -1.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(1.0, 1.0),
+ },
+ Vertex {
+ pos: vec3(0.5, 0.5, -0.5),
+ normal: vec3(0.0, 0.0, -1.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(1.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(0.5, 0.5, -0.5),
+ normal: vec3(0.0, 0.0, -1.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(1.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(-0.5, 0.5, -0.5),
+ normal: vec3(0.0, 0.0, -1.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(0.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(-0.5, -0.5, -0.5),
+ normal: vec3(0.0, 0.0, -1.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(0.0, 1.0),
+ },
+ //face 2
+ Vertex {
+ pos: vec3(-0.5, -0.5, 0.5),
+ normal: vec3(0.0, 0.0, 1.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(0.0, 1.0),
+ },
+ Vertex {
+ pos: vec3(0.5, -0.5, 0.5),
+ normal: vec3(0.0, 0.0, 1.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(1.0, 1.0),
+ },
+ Vertex {
+ pos: vec3(0.5, 0.5, 0.5),
+ normal: vec3(0.0, 0.0, 1.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(1.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(0.5, 0.5, 0.5),
+ normal: vec3(0.0, 0.0, 1.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(1.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(-0.5, 0.5, 0.5),
+ normal: vec3(0.0, 0.0, 1.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(0.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(-0.5, -0.5, 0.5),
+ normal: vec3(0.0, 0.0, 1.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(0.0, 1.0),
+ },
+ //face 3
+ Vertex {
+ pos: vec3(-0.5, 0.5, 0.5),
+ normal: vec3(-1.0, 0.0, 0.0),
+ tangent: vec3(0.0, 0.0, -1.0),
+ uv: vec2(0.0, 1.0),
+ },
+ Vertex {
+ pos: vec3(-0.5, 0.5, -0.5),
+ normal: vec3(-1.0, 0.0, 0.0),
+ tangent: vec3(0.0, 0.0, -1.0),
+ uv: vec2(1.0, 1.0),
+ },
+ Vertex {
+ pos: vec3(-0.5, -0.5, -0.5),
+ normal: vec3(-1.0, 0.0, 0.0),
+ tangent: vec3(0.0, 0.0, -1.0),
+ uv: vec2(1.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(-0.5, -0.5, -0.5),
+ normal: vec3(-1.0, 0.0, 0.0),
+ tangent: vec3(0.0, 0.0, -1.0),
+ uv: vec2(1.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(-0.5, -0.5, 0.5),
+ normal: vec3(-1.0, 0.0, 0.0),
+ tangent: vec3(0.0, 0.0, -1.0),
+ uv: vec2(0.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(-0.5, 0.5, 0.5),
+ normal: vec3(-1.0, 0.0, 0.0),
+ tangent: vec3(0.0, 0.0, -1.0),
+ uv: vec2(0.0, 1.0),
+ },
+ //face 4
+ Vertex {
+ pos: vec3(0.5, 0.5, 0.5),
+ normal: vec3(1.0, 0.0, 0.0),
+ tangent: vec3(0.0, 0.0, -1.0),
+ uv: vec2(0.0, 1.0),
+ },
+ Vertex {
+ pos: vec3(0.5, 0.5, -0.5),
+ normal: vec3(1.0, 0.0, 0.0),
+ tangent: vec3(0.0, 0.0, -1.0),
+ uv: vec2(1.0, 1.0),
+ },
+ Vertex {
+ pos: vec3(0.5, -0.5, -0.5),
+ normal: vec3(1.0, 0.0, 0.0),
+ tangent: vec3(0.0, 0.0, -1.0),
+ uv: vec2(1.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(0.5, -0.5, -0.5),
+ normal: vec3(1.0, 0.0, 0.0),
+ tangent: vec3(0.0, 0.0, -1.0),
+ uv: vec2(1.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(0.5, -0.5, 0.5),
+ normal: vec3(1.0, 0.0, 0.0),
+ tangent: vec3(0.0, 0.0, -1.0),
+ uv: vec2(0.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(0.5, 0.5, 0.5),
+ normal: vec3(1.0, 0.0, 0.0),
+ tangent: vec3(0.0, 0.0, -1.0),
+ uv: vec2(0.0, 1.0),
+ },
+ //face 5
+ Vertex {
+ pos: vec3(-0.5, -0.5, -0.5),
+ normal: vec3(0.0, -1.0, 0.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(0.0, 1.0),
+ },
+ Vertex {
+ pos: vec3(0.5, -0.5, -0.5),
+ normal: vec3(0.0, -1.0, 0.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(1.0, 1.0),
+ },
+ Vertex {
+ pos: vec3(0.5, -0.5, 0.5),
+ normal: vec3(0.0, -1.0, 0.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(1.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(0.5, -0.5, 0.5),
+ normal: vec3(0.0, -1.0, 0.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(1.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(-0.5, -0.5, 0.5),
+ normal: vec3(0.0, -1.0, 0.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(0.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(-0.5, -0.5, -0.5),
+ normal: vec3(0.0, -1.0, 0.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(0.0, 1.0),
+ },
+ //face 6
+ Vertex {
+ pos: vec3(-0.5, 0.5, -0.5),
+ normal: vec3(0.0, 1.0, 0.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(0.0, 1.0),
+ },
+ Vertex {
+ pos: vec3(0.5, 0.5, -0.5),
+ normal: vec3(0.0, 1.0, 0.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(1.0, 1.0),
+ },
+ Vertex {
+ pos: vec3(0.5, 0.5, 0.5),
+ normal: vec3(0.0, 1.0, 0.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(1.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(0.5, 0.5, 0.5),
+ normal: vec3(0.0, 1.0, 0.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(1.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(-0.5, 0.5, 0.5),
+ normal: vec3(0.0, 1.0, 0.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(0.0, 0.0),
+ },
+ Vertex {
+ pos: vec3(-0.5, 0.5, -0.5),
+ normal: vec3(0.0, 1.0, 0.0),
+ tangent: vec3(1.0, 0.0, 0.0),
+ uv: vec2(0.0, 1.0),
+ },
+ ]
+ }
+}
diff --git a/examples/custom_shader/src/scene/pipeline/uniforms.rs b/examples/custom_shader/src/scene/pipeline/uniforms.rs
new file mode 100644
index 00000000..1eac8292
--- /dev/null
+++ b/examples/custom_shader/src/scene/pipeline/uniforms.rs
@@ -0,0 +1,23 @@
+use crate::scene::Camera;
+
+use iced::{Color, Rectangle};
+
+#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
+#[repr(C)]
+pub struct Uniforms {
+ camera_proj: glam::Mat4,
+ camera_pos: glam::Vec4,
+ light_color: glam::Vec4,
+}
+
+impl Uniforms {
+ pub fn new(camera: &Camera, bounds: Rectangle, light_color: Color) -> Self {
+ let camera_proj = camera.build_view_proj_matrix(bounds);
+
+ Self {
+ camera_proj,
+ camera_pos: camera.position(),
+ light_color: glam::Vec4::from(light_color.into_linear()),
+ }
+ }
+}
diff --git a/examples/custom_shader/src/scene/pipeline/vertex.rs b/examples/custom_shader/src/scene/pipeline/vertex.rs
new file mode 100644
index 00000000..e64cd926
--- /dev/null
+++ b/examples/custom_shader/src/scene/pipeline/vertex.rs
@@ -0,0 +1,31 @@
+use crate::wgpu;
+
+#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
+#[repr(C)]
+pub struct Vertex {
+ pub pos: glam::Vec3,
+ pub normal: glam::Vec3,
+ pub tangent: glam::Vec3,
+ pub uv: glam::Vec2,
+}
+
+impl Vertex {
+ const ATTRIBS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
+ //position
+ 0 => Float32x3,
+ //normal
+ 1 => Float32x3,
+ //tangent
+ 2 => Float32x3,
+ //uv
+ 3 => Float32x2,
+ ];
+
+ pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
+ wgpu::VertexBufferLayout {
+ array_stride: std::mem::size_of::<Self>() as wgpu::BufferAddress,
+ step_mode: wgpu::VertexStepMode::Vertex,
+ attributes: &Self::ATTRIBS,
+ }
+ }
+}
diff --git a/examples/custom_shader/src/shaders/cubes.wgsl b/examples/custom_shader/src/shaders/cubes.wgsl
new file mode 100644
index 00000000..cd7f94d8
--- /dev/null
+++ b/examples/custom_shader/src/shaders/cubes.wgsl
@@ -0,0 +1,123 @@
+struct Uniforms {
+ projection: mat4x4<f32>,
+ camera_pos: vec4<f32>,
+ light_color: vec4<f32>,
+}
+
+const LIGHT_POS: vec3<f32> = vec3<f32>(0.0, 3.0, 3.0);
+
+@group(0) @binding(0) var<uniform> uniforms: Uniforms;
+@group(0) @binding(1) var sky_texture: texture_cube<f32>;
+@group(0) @binding(2) var tex_sampler: sampler;
+@group(0) @binding(3) var normal_texture: texture_2d<f32>;
+
+struct Vertex {
+ @location(0) position: vec3<f32>,
+ @location(1) normal: vec3<f32>,
+ @location(2) tangent: vec3<f32>,
+ @location(3) uv: vec2<f32>,
+}
+
+struct Cube {
+ @location(4) matrix_0: vec4<f32>,
+ @location(5) matrix_1: vec4<f32>,
+ @location(6) matrix_2: vec4<f32>,
+ @location(7) matrix_3: vec4<f32>,
+ @location(8) normal_matrix_0: vec3<f32>,
+ @location(9) normal_matrix_1: vec3<f32>,
+ @location(10) normal_matrix_2: vec3<f32>,
+}
+
+struct Output {
+ @builtin(position) clip_pos: vec4<f32>,
+ @location(0) uv: vec2<f32>,
+ @location(1) tangent_pos: vec3<f32>,
+ @location(2) tangent_camera_pos: vec3<f32>,
+ @location(3) tangent_light_pos: vec3<f32>,
+}
+
+@vertex
+fn vs_main(vertex: Vertex, cube: Cube) -> Output {
+ let cube_matrix = mat4x4<f32>(
+ cube.matrix_0,
+ cube.matrix_1,
+ cube.matrix_2,
+ cube.matrix_3,
+ );
+
+ let normal_matrix = mat3x3<f32>(
+ cube.normal_matrix_0,
+ cube.normal_matrix_1,
+ cube.normal_matrix_2,
+ );
+
+ //convert to tangent space to calculate lighting in same coordinate space as normal map sample
+ let tangent = normalize(normal_matrix * vertex.tangent);
+ let normal = normalize(normal_matrix * vertex.normal);
+ let bitangent = cross(tangent, normal);
+
+ //shift everything into tangent space
+ let tbn = transpose(mat3x3<f32>(tangent, bitangent, normal));
+
+ let world_pos = cube_matrix * vec4<f32>(vertex.position, 1.0);
+
+ var out: Output;
+ out.clip_pos = uniforms.projection * world_pos;
+ out.uv = vertex.uv;
+ out.tangent_pos = tbn * world_pos.xyz;
+ out.tangent_camera_pos = tbn * uniforms.camera_pos.xyz;
+ out.tangent_light_pos = tbn * LIGHT_POS;
+
+ return out;
+}
+
+//cube properties
+const CUBE_BASE_COLOR: vec4<f32> = vec4<f32>(0.294118, 0.462745, 0.611765, 0.6);
+const SHINE_DAMPER: f32 = 1.0;
+const REFLECTIVITY: f32 = 0.8;
+const REFRACTION_INDEX: f32 = 1.31;
+
+//fog, for the ~* cinematic effect *~
+const FOG_DENSITY: f32 = 0.15;
+const FOG_GRADIENT: f32 = 8.0;
+const FOG_COLOR: vec4<f32> = vec4<f32>(1.0, 1.0, 1.0, 1.0);
+
+@fragment
+fn fs_main(in: Output) -> @location(0) vec4<f32> {
+ let to_camera = in.tangent_camera_pos - in.tangent_pos;
+
+ //normal sample from texture
+ var normal = textureSample(normal_texture, tex_sampler, in.uv).xyz;
+ normal = normal * 2.0 - 1.0;
+
+ //diffuse
+ let dir_to_light: vec3<f32> = normalize(in.tangent_light_pos - in.tangent_pos);
+ let brightness = max(dot(normal, dir_to_light), 0.0);
+ let diffuse: vec3<f32> = brightness * uniforms.light_color.xyz;
+
+ //specular
+ let dir_to_camera = normalize(to_camera);
+ let light_dir = -dir_to_light;
+ let reflected_light_dir = reflect(light_dir, normal);
+ let specular_factor = max(dot(reflected_light_dir, dir_to_camera), 0.0);
+ let damped_factor = pow(specular_factor, SHINE_DAMPER);
+ let specular: vec3<f32> = damped_factor * uniforms.light_color.xyz * REFLECTIVITY;
+
+ //fog
+ let distance = length(to_camera);
+ let visibility = clamp(exp(-pow((distance * FOG_DENSITY), FOG_GRADIENT)), 0.0, 1.0);
+
+ //reflection
+ let reflection_dir = reflect(dir_to_camera, normal);
+ let reflection_color = textureSample(sky_texture, tex_sampler, reflection_dir);
+ let refraction_dir = refract(dir_to_camera, normal, REFRACTION_INDEX);
+ let refraction_color = textureSample(sky_texture, tex_sampler, refraction_dir);
+ let final_reflect_color = mix(reflection_color, refraction_color, 0.5);
+
+ //mix it all together!
+ var color = vec4<f32>(CUBE_BASE_COLOR.xyz * diffuse + specular, CUBE_BASE_COLOR.w);
+ color = mix(color, final_reflect_color, 0.8);
+ color = mix(FOG_COLOR, color, visibility);
+
+ return color;
+}
diff --git a/examples/custom_shader/src/shaders/depth.wgsl b/examples/custom_shader/src/shaders/depth.wgsl
new file mode 100644
index 00000000..a3f7e5ec
--- /dev/null
+++ b/examples/custom_shader/src/shaders/depth.wgsl
@@ -0,0 +1,48 @@
+var<private> positions: array<vec2<f32>, 6> = array<vec2<f32>, 6>(
+ vec2<f32>(-1.0, 1.0),
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>(1.0, -1.0),
+ vec2<f32>(-1.0, 1.0),
+ vec2<f32>(1.0, 1.0),
+ vec2<f32>(1.0, -1.0)
+);
+
+var<private> uvs: array<vec2<f32>, 6> = array<vec2<f32>, 6>(
+ vec2<f32>(0.0, 0.0),
+ vec2<f32>(0.0, 1.0),
+ vec2<f32>(1.0, 1.0),
+ vec2<f32>(0.0, 0.0),
+ vec2<f32>(1.0, 0.0),
+ vec2<f32>(1.0, 1.0)
+);
+
+@group(0) @binding(0) var depth_sampler: sampler;
+@group(0) @binding(1) var depth_texture: texture_2d<f32>;
+
+struct Output {
+ @builtin(position) position: vec4<f32>,
+ @location(0) uv: vec2<f32>,
+}
+
+@vertex
+fn vs_main(@builtin(vertex_index) v_index: u32) -> Output {
+ var out: Output;
+
+ out.position = vec4<f32>(positions[v_index], 0.0, 1.0);
+ out.uv = uvs[v_index];
+
+ return out;
+}
+
+@fragment
+fn fs_main(input: Output) -> @location(0) vec4<f32> {
+ let depth = textureSample(depth_texture, depth_sampler, input.uv).r;
+
+ if (depth > .9999) {
+ discard;
+ }
+
+ let c = 1.0 - depth;
+
+ return vec4<f32>(c, c, c, 1.0);
+}
diff --git a/examples/custom_shader/textures/ice_cube_normal_map.png b/examples/custom_shader/textures/ice_cube_normal_map.png
new file mode 100644
index 00000000..7b4b7228
--- /dev/null
+++ b/examples/custom_shader/textures/ice_cube_normal_map.png
Binary files differ
diff --git a/examples/custom_shader/textures/skybox/neg_x.jpg b/examples/custom_shader/textures/skybox/neg_x.jpg
new file mode 100644
index 00000000..00cc783d
--- /dev/null
+++ b/examples/custom_shader/textures/skybox/neg_x.jpg
Binary files differ
diff --git a/examples/custom_shader/textures/skybox/neg_y.jpg b/examples/custom_shader/textures/skybox/neg_y.jpg
new file mode 100644
index 00000000..548f6445
--- /dev/null
+++ b/examples/custom_shader/textures/skybox/neg_y.jpg
Binary files differ
diff --git a/examples/custom_shader/textures/skybox/neg_z.jpg b/examples/custom_shader/textures/skybox/neg_z.jpg
new file mode 100644
index 00000000..5698512e
--- /dev/null
+++ b/examples/custom_shader/textures/skybox/neg_z.jpg
Binary files differ
diff --git a/examples/custom_shader/textures/skybox/pos_x.jpg b/examples/custom_shader/textures/skybox/pos_x.jpg
new file mode 100644
index 00000000..dddecba7
--- /dev/null
+++ b/examples/custom_shader/textures/skybox/pos_x.jpg
Binary files differ
diff --git a/examples/custom_shader/textures/skybox/pos_y.jpg b/examples/custom_shader/textures/skybox/pos_y.jpg
new file mode 100644
index 00000000..361427fd
--- /dev/null
+++ b/examples/custom_shader/textures/skybox/pos_y.jpg
Binary files differ
diff --git a/examples/custom_shader/textures/skybox/pos_z.jpg b/examples/custom_shader/textures/skybox/pos_z.jpg
new file mode 100644
index 00000000..0085a49e
--- /dev/null
+++ b/examples/custom_shader/textures/skybox/pos_z.jpg
Binary files differ
diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs
index 32a14cbe..7ffb4cd0 100644
--- a/examples/custom_widget/src/main.rs
+++ b/examples/custom_widget/src/main.rs
@@ -33,12 +33,11 @@ mod circle {
where
Renderer: renderer::Renderer,
{
- fn width(&self) -> Length {
- Length::Shrink
- }
-
- fn height(&self) -> Length {
- Length::Shrink
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: Length::Shrink,
+ height: Length::Shrink,
+ }
}
fn layout(
diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs
index a2fcb275..675e9e26 100644
--- a/examples/download_progress/src/main.rs
+++ b/examples/download_progress/src/main.rs
@@ -73,16 +73,15 @@ impl Application for Example {
}
fn view(&self) -> Element<Message> {
- let downloads = Column::with_children(
- self.downloads.iter().map(Download::view).collect(),
- )
- .push(
- button("Add another download")
- .on_press(Message::Add)
- .padding(10),
- )
- .spacing(20)
- .align_items(Alignment::End);
+ let downloads =
+ Column::with_children(self.downloads.iter().map(Download::view))
+ .push(
+ button("Add another download")
+ .on_press(Message::Add)
+ .padding(10),
+ )
+ .spacing(20)
+ .align_items(Alignment::End);
container(downloads)
.width(Length::Fill)
diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml
new file mode 100644
index 00000000..dc885728
--- /dev/null
+++ b/examples/editor/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "editor"
+version = "0.1.0"
+authors = ["Héctor Ramón Jiménez <hector@hecrj.dev>"]
+edition = "2021"
+publish = false
+
+[dependencies]
+iced.workspace = true
+iced.features = ["highlighter", "tokio", "debug"]
+
+tokio.workspace = true
+tokio.features = ["fs"]
+
+rfd = "0.13"
diff --git a/examples/editor/fonts/icons.ttf b/examples/editor/fonts/icons.ttf
new file mode 100644
index 00000000..393c6922
--- /dev/null
+++ b/examples/editor/fonts/icons.ttf
Binary files differ
diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs
new file mode 100644
index 00000000..bf2aaaa3
--- /dev/null
+++ b/examples/editor/src/main.rs
@@ -0,0 +1,312 @@
+use iced::executor;
+use iced::highlighter::{self, Highlighter};
+use iced::keyboard;
+use iced::theme::{self, Theme};
+use iced::widget::{
+ button, column, container, horizontal_space, pick_list, row, text,
+ text_editor, tooltip,
+};
+use iced::{
+ Alignment, Application, Command, Element, Font, Length, Settings,
+ Subscription,
+};
+
+use std::ffi;
+use std::io;
+use std::path::{Path, PathBuf};
+use std::sync::Arc;
+
+pub fn main() -> iced::Result {
+ Editor::run(Settings {
+ fonts: vec![include_bytes!("../fonts/icons.ttf").as_slice().into()],
+ default_font: Font::MONOSPACE,
+ ..Settings::default()
+ })
+}
+
+struct Editor {
+ file: Option<PathBuf>,
+ content: text_editor::Content,
+ theme: highlighter::Theme,
+ is_loading: bool,
+ is_dirty: bool,
+}
+
+#[derive(Debug, Clone)]
+enum Message {
+ ActionPerformed(text_editor::Action),
+ ThemeSelected(highlighter::Theme),
+ NewFile,
+ OpenFile,
+ FileOpened(Result<(PathBuf, Arc<String>), Error>),
+ SaveFile,
+ FileSaved(Result<PathBuf, Error>),
+}
+
+impl Application for Editor {
+ type Message = Message;
+ type Theme = Theme;
+ type Executor = executor::Default;
+ type Flags = ();
+
+ fn new(_flags: Self::Flags) -> (Self, Command<Message>) {
+ (
+ Self {
+ file: None,
+ content: text_editor::Content::new(),
+ theme: highlighter::Theme::SolarizedDark,
+ is_loading: true,
+ is_dirty: false,
+ },
+ Command::perform(load_file(default_file()), Message::FileOpened),
+ )
+ }
+
+ fn title(&self) -> String {
+ String::from("Editor - Iced")
+ }
+
+ fn update(&mut self, message: Message) -> Command<Message> {
+ match message {
+ Message::ActionPerformed(action) => {
+ self.is_dirty = self.is_dirty || action.is_edit();
+
+ self.content.perform(action);
+
+ Command::none()
+ }
+ Message::ThemeSelected(theme) => {
+ self.theme = theme;
+
+ Command::none()
+ }
+ Message::NewFile => {
+ if !self.is_loading {
+ self.file = None;
+ self.content = text_editor::Content::new();
+ }
+
+ Command::none()
+ }
+ Message::OpenFile => {
+ if self.is_loading {
+ Command::none()
+ } else {
+ self.is_loading = true;
+
+ Command::perform(open_file(), Message::FileOpened)
+ }
+ }
+ Message::FileOpened(result) => {
+ self.is_loading = false;
+ self.is_dirty = false;
+
+ if let Ok((path, contents)) = result {
+ self.file = Some(path);
+ self.content = text_editor::Content::with_text(&contents);
+ }
+
+ Command::none()
+ }
+ Message::SaveFile => {
+ if self.is_loading {
+ Command::none()
+ } else {
+ self.is_loading = true;
+
+ Command::perform(
+ save_file(self.file.clone(), self.content.text()),
+ Message::FileSaved,
+ )
+ }
+ }
+ Message::FileSaved(result) => {
+ self.is_loading = false;
+
+ if let Ok(path) = result {
+ self.file = Some(path);
+ self.is_dirty = false;
+ }
+
+ Command::none()
+ }
+ }
+ }
+
+ fn subscription(&self) -> Subscription<Message> {
+ keyboard::on_key_press(|key, modifiers| match key.as_ref() {
+ keyboard::Key::Character("s") if modifiers.command() => {
+ Some(Message::SaveFile)
+ }
+ _ => None,
+ })
+ }
+
+ fn view(&self) -> Element<Message> {
+ let controls = row![
+ action(new_icon(), "New file", Some(Message::NewFile)),
+ action(
+ open_icon(),
+ "Open file",
+ (!self.is_loading).then_some(Message::OpenFile)
+ ),
+ action(
+ save_icon(),
+ "Save file",
+ self.is_dirty.then_some(Message::SaveFile)
+ ),
+ horizontal_space(Length::Fill),
+ pick_list(
+ highlighter::Theme::ALL,
+ Some(self.theme),
+ Message::ThemeSelected
+ )
+ .text_size(14)
+ .padding([5, 10])
+ ]
+ .spacing(10)
+ .align_items(Alignment::Center);
+
+ let status = row![
+ text(if let Some(path) = &self.file {
+ let path = path.display().to_string();
+
+ if path.len() > 60 {
+ format!("...{}", &path[path.len() - 40..])
+ } else {
+ path
+ }
+ } else {
+ String::from("New file")
+ }),
+ horizontal_space(Length::Fill),
+ text({
+ let (line, column) = self.content.cursor_position();
+
+ format!("{}:{}", line + 1, column + 1)
+ })
+ ]
+ .spacing(10);
+
+ column![
+ controls,
+ text_editor(&self.content)
+ .on_action(Message::ActionPerformed)
+ .highlight::<Highlighter>(
+ highlighter::Settings {
+ theme: self.theme,
+ extension: self
+ .file
+ .as_deref()
+ .and_then(Path::extension)
+ .and_then(ffi::OsStr::to_str)
+ .map(str::to_string)
+ .unwrap_or(String::from("rs")),
+ },
+ |highlight, _theme| highlight.to_format()
+ ),
+ status,
+ ]
+ .spacing(10)
+ .padding(10)
+ .into()
+ }
+
+ fn theme(&self) -> Theme {
+ if self.theme.is_dark() {
+ Theme::Dark
+ } else {
+ Theme::Light
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub enum Error {
+ DialogClosed,
+ IoError(io::ErrorKind),
+}
+
+fn default_file() -> PathBuf {
+ PathBuf::from(format!("{}/src/main.rs", env!("CARGO_MANIFEST_DIR")))
+}
+
+async fn open_file() -> Result<(PathBuf, Arc<String>), Error> {
+ let picked_file = rfd::AsyncFileDialog::new()
+ .set_title("Open a text file...")
+ .pick_file()
+ .await
+ .ok_or(Error::DialogClosed)?;
+
+ load_file(picked_file.path().to_owned()).await
+}
+
+async fn load_file(path: PathBuf) -> Result<(PathBuf, Arc<String>), Error> {
+ let contents = tokio::fs::read_to_string(&path)
+ .await
+ .map(Arc::new)
+ .map_err(|error| Error::IoError(error.kind()))?;
+
+ Ok((path, contents))
+}
+
+async fn save_file(
+ path: Option<PathBuf>,
+ contents: String,
+) -> Result<PathBuf, Error> {
+ let path = if let Some(path) = path {
+ path
+ } else {
+ rfd::AsyncFileDialog::new()
+ .save_file()
+ .await
+ .as_ref()
+ .map(rfd::FileHandle::path)
+ .map(Path::to_owned)
+ .ok_or(Error::DialogClosed)?
+ };
+
+ tokio::fs::write(&path, contents)
+ .await
+ .map_err(|error| Error::IoError(error.kind()))?;
+
+ Ok(path)
+}
+
+fn action<'a, Message: Clone + 'a>(
+ content: impl Into<Element<'a, Message>>,
+ label: &'a str,
+ on_press: Option<Message>,
+) -> Element<'a, Message> {
+ let action = button(container(content).width(30).center_x());
+
+ if let Some(on_press) = on_press {
+ tooltip(
+ action.on_press(on_press),
+ label,
+ tooltip::Position::FollowCursor,
+ )
+ .style(theme::Container::Box)
+ .into()
+ } else {
+ action.style(theme::Button::Secondary).into()
+ }
+}
+
+fn new_icon<'a, Message>() -> Element<'a, Message> {
+ icon('\u{0e800}')
+}
+
+fn save_icon<'a, Message>() -> Element<'a, Message> {
+ icon('\u{0e801}')
+}
+
+fn open_icon<'a, Message>() -> Element<'a, Message> {
+ icon('\u{0f115}')
+}
+
+fn icon<'a, Message>(codepoint: char) -> Element<'a, Message> {
+ const ICON_FONT: Font = Font::with_name("editor-icons");
+
+ text(codepoint).font(ICON_FONT).into()
+}
diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs
index 32d0da2c..fc51ac4a 100644
--- a/examples/events/src/main.rs
+++ b/examples/events/src/main.rs
@@ -10,7 +10,10 @@ use iced::{
pub fn main() -> iced::Result {
Events::run(Settings {
- exit_on_close_request: false,
+ window: window::Settings {
+ exit_on_close_request: false,
+ ..window::Settings::default()
+ },
..Settings::default()
})
}
@@ -54,8 +57,9 @@ impl Application for Events {
Command::none()
}
Message::EventOccurred(event) => {
- if let Event::Window(window::Event::CloseRequested) = event {
- window::close()
+ if let Event::Window(id, window::Event::CloseRequested) = event
+ {
+ window::close(id)
} else {
Command::none()
}
@@ -65,7 +69,7 @@ impl Application for Events {
Command::none()
}
- Message::Exit => window::close(),
+ Message::Exit => window::close(window::Id::MAIN),
}
}
@@ -78,8 +82,7 @@ impl Application for Events {
self.last
.iter()
.map(|event| text(format!("{event:?}")).size(40))
- .map(Element::from)
- .collect(),
+ .map(Element::from),
);
let toggle = checkbox(
diff --git a/examples/exit/src/main.rs b/examples/exit/src/main.rs
index 6152f627..ec618dc1 100644
--- a/examples/exit/src/main.rs
+++ b/examples/exit/src/main.rs
@@ -34,7 +34,7 @@ impl Application for Exit {
fn update(&mut self, message: Message) -> Command<Message> {
match message {
- Message::Confirm => window::close(),
+ Message::Confirm => window::close(window::Id::MAIN),
Message::Exit => {
self.show_confirm = true;
diff --git a/examples/game_of_life/Cargo.toml b/examples/game_of_life/Cargo.toml
index 9b291de8..7596844c 100644
--- a/examples/game_of_life/Cargo.toml
+++ b/examples/game_of_life/Cargo.toml
@@ -9,7 +9,7 @@ publish = false
iced.workspace = true
iced.features = ["debug", "canvas", "tokio"]
-itertools = "0.11"
+itertools = "0.12"
rustc-hash.workspace = true
tokio = { workspace = true, features = ["sync"] }
tracing-subscriber = "0.3"
diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs
index 96840143..56f7afd5 100644
--- a/examples/game_of_life/src/main.rs
+++ b/examples/game_of_life/src/main.rs
@@ -146,7 +146,8 @@ impl Application for GameOfLife {
.view()
.map(move |message| Message::Grid(message, version)),
controls,
- ];
+ ]
+ .height(Length::Fill);
container(content)
.width(Length::Fill)
@@ -178,7 +179,6 @@ fn view_controls<'a>(
slider(1.0..=1000.0, speed as f32, Message::SpeedChanged),
text(format!("x{speed}")).size(16),
]
- .width(Length::Fill)
.align_items(Alignment::Center)
.spacing(10);
diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs
index 8ab3b493..5cf9963d 100644
--- a/examples/geometry/src/main.rs
+++ b/examples/geometry/src/main.rs
@@ -16,12 +16,11 @@ mod rainbow {
}
impl<Message> Widget<Message, Renderer> for Rainbow {
- fn width(&self) -> Length {
- Length::Fill
- }
-
- fn height(&self) -> Length {
- Length::Shrink
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: Length::Fill,
+ height: Length::Shrink,
+ }
}
fn layout(
@@ -30,9 +29,9 @@ mod rainbow {
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let size = limits.width(Length::Fill).resolve(Size::ZERO);
+ let width = limits.max().width;
- layout::Node::new(Size::new(size.width, size.width))
+ layout::Node::new(Size::new(width, width))
}
fn draw(
diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs
index 4714c397..89a595c1 100644
--- a/examples/integration/src/controls.rs
+++ b/examples/integration/src/controls.rs
@@ -81,32 +81,25 @@ impl Program for Controls {
);
Row::new()
- .width(Length::Fill)
.height(Length::Fill)
.align_items(Alignment::End)
.push(
- Column::new()
- .width(Length::Fill)
- .align_items(Alignment::End)
- .push(
- Column::new()
- .padding(10)
- .spacing(10)
- .push(
- Text::new("Background color")
- .style(Color::WHITE),
- )
- .push(sliders)
- .push(
- Text::new(format!("{background_color:?}"))
- .size(14)
- .style(Color::WHITE),
- )
- .push(
- text_input("Placeholder", text)
- .on_input(Message::TextChanged),
- ),
- ),
+ Column::new().align_items(Alignment::End).push(
+ Column::new()
+ .padding(10)
+ .spacing(10)
+ .push(Text::new("Background color").style(Color::WHITE))
+ .push(sliders)
+ .push(
+ Text::new(format!("{background_color:?}"))
+ .size(14)
+ .style(Color::WHITE),
+ )
+ .push(
+ text_input("Placeholder", text)
+ .on_input(Message::TextChanged),
+ ),
+ ),
)
.into()
}
diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs
index c26d52fe..ed61459f 100644
--- a/examples/integration/src/main.rs
+++ b/examples/integration/src/main.rs
@@ -6,19 +6,26 @@ use scene::Scene;
use iced_wgpu::graphics::Viewport;
use iced_wgpu::{wgpu, Backend, Renderer, Settings};
+use iced_winit::conversion;
use iced_winit::core::mouse;
use iced_winit::core::renderer;
+use iced_winit::core::window;
use iced_winit::core::{Color, Font, Pixels, Size};
+use iced_winit::futures;
use iced_winit::runtime::program;
use iced_winit::runtime::Debug;
use iced_winit::style::Theme;
-use iced_winit::{conversion, futures, winit, Clipboard};
+use iced_winit::winit;
+use iced_winit::Clipboard;
use winit::{
- event::{Event, ModifiersState, WindowEvent},
+ event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop},
+ keyboard::ModifiersState,
};
+use std::sync::Arc;
+
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::JsCast;
#[cfg(target_arch = "wasm32")]
@@ -44,7 +51,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
// Initialize winit
- let event_loop = EventLoop::new();
+ let event_loop = EventLoop::new()?;
#[cfg(target_arch = "wasm32")]
let window = winit::window::WindowBuilder::new()
@@ -54,6 +61,8 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(not(target_arch = "wasm32"))]
let window = winit::window::Window::new(&event_loop)?;
+ let window = Arc::new(window);
+
let physical_size = window.inner_size();
let mut viewport = Viewport::with_physical_size(
Size::new(physical_size.width, physical_size.height),
@@ -76,7 +85,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
backends: backend,
..Default::default()
});
- let surface = unsafe { instance.create_surface(&window) }?;
+ let surface = instance.create_surface(window.clone())?;
let (format, (device, queue)) =
futures::futures::executor::block_on(async {
@@ -110,9 +119,9 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
.request_device(
&wgpu::DeviceDescriptor {
label: None,
- features: adapter_features
+ required_features: adapter_features
& wgpu::Features::default(),
- limits: needed_limits,
+ required_limits: needed_limits,
},
None,
)
@@ -131,6 +140,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
present_mode: wgpu::PresentMode::AutoVsync,
alpha_mode: wgpu::CompositeAlphaMode::Auto,
view_formats: vec![],
+ desired_maximum_frame_latency: 2,
},
);
@@ -156,66 +166,15 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
);
// Run event loop
- event_loop.run(move |event, _, control_flow| {
+ event_loop.run(move |event, window_target| {
// You should change this if you want to render continuosly
- *control_flow = ControlFlow::Wait;
+ window_target.set_control_flow(ControlFlow::Wait);
match event {
- Event::WindowEvent { event, .. } => {
- match event {
- WindowEvent::CursorMoved { position, .. } => {
- cursor_position = Some(position);
- }
- WindowEvent::ModifiersChanged(new_modifiers) => {
- modifiers = new_modifiers;
- }
- WindowEvent::Resized(_) => {
- resized = true;
- }
- WindowEvent::CloseRequested => {
- *control_flow = ControlFlow::Exit;
- }
- _ => {}
- }
-
- // Map window event to iced event
- if let Some(event) = iced_winit::conversion::window_event(
- &event,
- window.scale_factor(),
- modifiers,
- ) {
- state.queue_event(event);
- }
- }
- Event::MainEventsCleared => {
- // If there are events pending
- if !state.is_queue_empty() {
- // We update iced
- let _ = state.update(
- viewport.logical_size(),
- cursor_position
- .map(|p| {
- conversion::cursor_position(
- p,
- viewport.scale_factor(),
- )
- })
- .map(mouse::Cursor::Available)
- .unwrap_or(mouse::Cursor::Unavailable),
- &mut renderer,
- &Theme::Dark,
- &renderer::Style {
- text_color: Color::WHITE,
- },
- &mut clipboard,
- &mut debug,
- );
-
- // and request a redraw
- window.request_redraw();
- }
- }
- Event::RedrawRequested(_) => {
+ Event::WindowEvent {
+ event: WindowEvent::RedrawRequested,
+ ..
+ } => {
if resized {
let size = window.inner_size();
@@ -234,6 +193,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
present_mode: wgpu::PresentMode::AutoVsync,
alpha_mode: wgpu::CompositeAlphaMode::Auto,
view_formats: vec![],
+ desired_maximum_frame_latency: 2,
},
);
@@ -271,6 +231,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
&queue,
&mut encoder,
None,
+ frame.texture.format(),
&view,
primitive,
&viewport,
@@ -303,7 +264,60 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
},
}
}
+ Event::WindowEvent { event, .. } => {
+ match event {
+ WindowEvent::CursorMoved { position, .. } => {
+ cursor_position = Some(position);
+ }
+ WindowEvent::ModifiersChanged(new_modifiers) => {
+ modifiers = new_modifiers.state();
+ }
+ WindowEvent::Resized(_) => {
+ resized = true;
+ }
+ WindowEvent::CloseRequested => {
+ window_target.exit();
+ }
+ _ => {}
+ }
+
+ // Map window event to iced event
+ if let Some(event) = iced_winit::conversion::window_event(
+ window::Id::MAIN,
+ event,
+ window.scale_factor(),
+ modifiers,
+ ) {
+ state.queue_event(event);
+ }
+ }
_ => {}
}
- })
+
+ // If there are events pending
+ if !state.is_queue_empty() {
+ // We update iced
+ let _ = state.update(
+ viewport.logical_size(),
+ cursor_position
+ .map(|p| {
+ conversion::cursor_position(p, viewport.scale_factor())
+ })
+ .map(mouse::Cursor::Available)
+ .unwrap_or(mouse::Cursor::Unavailable),
+ &mut renderer,
+ &Theme::Dark,
+ &renderer::Style {
+ text_color: Color::WHITE,
+ },
+ &mut clipboard,
+ &mut debug,
+ );
+
+ // and request a redraw
+ window.request_redraw();
+ }
+ })?;
+
+ Ok(())
}
diff --git a/examples/integration/src/scene.rs b/examples/integration/src/scene.rs
index 01808f40..e29558bf 100644
--- a/examples/integration/src/scene.rs
+++ b/examples/integration/src/scene.rs
@@ -36,10 +36,12 @@ impl Scene {
a: a as f64,
}
}),
- store: true,
+ store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
+ timestamp_writes: None,
+ occlusion_query_set: None,
})
}
diff --git a/examples/layout/Cargo.toml b/examples/layout/Cargo.toml
new file mode 100644
index 00000000..855f98d0
--- /dev/null
+++ b/examples/layout/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "layout"
+version = "0.1.0"
+authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
+edition = "2021"
+publish = false
+
+[dependencies]
+iced = { path = "../..", features = ["canvas"] }
diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs
new file mode 100644
index 00000000..6cf0e570
--- /dev/null
+++ b/examples/layout/src/main.rs
@@ -0,0 +1,371 @@
+use iced::executor;
+use iced::keyboard;
+use iced::mouse;
+use iced::theme;
+use iced::widget::{
+ button, canvas, checkbox, column, container, horizontal_space, pick_list,
+ row, scrollable, text, vertical_rule,
+};
+use iced::{
+ color, Alignment, Application, Color, Command, Element, Font, Length,
+ Point, Rectangle, Renderer, Settings, Subscription, Theme,
+};
+
+pub fn main() -> iced::Result {
+ Layout::run(Settings::default())
+}
+
+#[derive(Debug)]
+struct Layout {
+ example: Example,
+ explain: bool,
+ theme: Theme,
+}
+
+#[derive(Debug, Clone)]
+enum Message {
+ Next,
+ Previous,
+ ExplainToggled(bool),
+ ThemeSelected(Theme),
+}
+
+impl Application for Layout {
+ type Message = Message;
+ type Theme = Theme;
+ type Executor = executor::Default;
+ type Flags = ();
+
+ fn new(_flags: Self::Flags) -> (Self, Command<Message>) {
+ (
+ Self {
+ example: Example::default(),
+ explain: false,
+ theme: Theme::Light,
+ },
+ Command::none(),
+ )
+ }
+
+ fn title(&self) -> String {
+ format!("{} - Layout - Iced", self.example.title)
+ }
+
+ fn update(&mut self, message: Self::Message) -> Command<Message> {
+ match message {
+ Message::Next => {
+ self.example = self.example.next();
+ }
+ Message::Previous => {
+ self.example = self.example.previous();
+ }
+ Message::ExplainToggled(explain) => {
+ self.explain = explain;
+ }
+ Message::ThemeSelected(theme) => {
+ self.theme = theme;
+ }
+ }
+
+ Command::none()
+ }
+
+ fn subscription(&self) -> Subscription<Message> {
+ use keyboard::key;
+
+ keyboard::on_key_release(|key, _modifiers| match key {
+ keyboard::Key::Named(key::Named::ArrowLeft) => {
+ Some(Message::Previous)
+ }
+ keyboard::Key::Named(key::Named::ArrowRight) => Some(Message::Next),
+ _ => None,
+ })
+ }
+
+ fn view(&self) -> Element<Message> {
+ let header = row![
+ text(self.example.title).size(20).font(Font::MONOSPACE),
+ horizontal_space(Length::Fill),
+ checkbox("Explain", self.explain, Message::ExplainToggled),
+ pick_list(
+ Theme::ALL,
+ Some(self.theme.clone()),
+ Message::ThemeSelected
+ ),
+ ]
+ .spacing(20)
+ .align_items(Alignment::Center);
+
+ let example = container(if self.explain {
+ self.example.view().explain(color!(0x0000ff))
+ } else {
+ self.example.view()
+ })
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+
+ container::Appearance::default()
+ .with_border(palette.background.strong.color, 4.0)
+ })
+ .padding(4)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .center_x()
+ .center_y();
+
+ let controls = row([
+ (!self.example.is_first()).then_some(
+ button("← Previous")
+ .padding([5, 10])
+ .on_press(Message::Previous)
+ .into(),
+ ),
+ Some(horizontal_space(Length::Fill).into()),
+ (!self.example.is_last()).then_some(
+ button("Next →")
+ .padding([5, 10])
+ .on_press(Message::Next)
+ .into(),
+ ),
+ ]
+ .into_iter()
+ .flatten());
+
+ column![header, example, controls]
+ .spacing(10)
+ .padding(20)
+ .into()
+ }
+
+ fn theme(&self) -> Theme {
+ self.theme.clone()
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+struct Example {
+ title: &'static str,
+ view: fn() -> Element<'static, Message>,
+}
+
+impl Example {
+ const LIST: &'static [Self] = &[
+ Self {
+ title: "Centered",
+ view: centered,
+ },
+ Self {
+ title: "Column",
+ view: column_,
+ },
+ Self {
+ title: "Row",
+ view: row_,
+ },
+ Self {
+ title: "Space",
+ view: space,
+ },
+ Self {
+ title: "Application",
+ view: application,
+ },
+ Self {
+ title: "Nested Quotes",
+ view: nested_quotes,
+ },
+ ];
+
+ fn is_first(self) -> bool {
+ Self::LIST.first() == Some(&self)
+ }
+
+ fn is_last(self) -> bool {
+ Self::LIST.last() == Some(&self)
+ }
+
+ fn previous(self) -> Self {
+ let Some(index) =
+ Self::LIST.iter().position(|&example| example == self)
+ else {
+ return self;
+ };
+
+ Self::LIST
+ .get(index.saturating_sub(1))
+ .copied()
+ .unwrap_or(self)
+ }
+
+ fn next(self) -> Self {
+ let Some(index) =
+ Self::LIST.iter().position(|&example| example == self)
+ else {
+ return self;
+ };
+
+ Self::LIST.get(index + 1).copied().unwrap_or(self)
+ }
+
+ fn view(&self) -> Element<Message> {
+ (self.view)()
+ }
+}
+
+impl Default for Example {
+ fn default() -> Self {
+ Self::LIST[0]
+ }
+}
+
+fn centered<'a>() -> Element<'a, Message> {
+ container(text("I am centered!").size(50))
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .center_x()
+ .center_y()
+ .into()
+}
+
+fn column_<'a>() -> Element<'a, Message> {
+ column![
+ "A column can be used to",
+ "lay out widgets vertically.",
+ square(50),
+ square(50),
+ square(50),
+ "The amount of space between",
+ "elements can be configured!",
+ ]
+ .spacing(40)
+ .into()
+}
+
+fn row_<'a>() -> Element<'a, Message> {
+ row![
+ "A row works like a column...",
+ square(50),
+ square(50),
+ square(50),
+ "but lays out widgets horizontally!",
+ ]
+ .spacing(40)
+ .into()
+}
+
+fn space<'a>() -> Element<'a, Message> {
+ row!["Left!", horizontal_space(Length::Fill), "Right!"].into()
+}
+
+fn application<'a>() -> Element<'a, Message> {
+ let header = container(
+ row![
+ square(40),
+ horizontal_space(Length::Fill),
+ "Header!",
+ horizontal_space(Length::Fill),
+ square(40),
+ ]
+ .padding(10)
+ .align_items(Alignment::Center),
+ )
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+
+ container::Appearance::default()
+ .with_border(palette.background.strong.color, 1)
+ });
+
+ let sidebar = container(
+ column!["Sidebar!", square(50), square(50)]
+ .spacing(40)
+ .padding(10)
+ .width(200)
+ .align_items(Alignment::Center),
+ )
+ .style(theme::Container::Box)
+ .height(Length::Fill)
+ .center_y();
+
+ let content = container(
+ scrollable(
+ column![
+ "Content!",
+ square(400),
+ square(200),
+ square(400),
+ "The end"
+ ]
+ .spacing(40)
+ .align_items(Alignment::Center)
+ .width(Length::Fill),
+ )
+ .height(Length::Fill),
+ )
+ .padding(10);
+
+ column![header, row![sidebar, content]].into()
+}
+
+fn nested_quotes<'a>() -> Element<'a, Message> {
+ (1..5)
+ .fold(column![text("Original text")].padding(10), |quotes, i| {
+ column![
+ container(
+ row![vertical_rule(2), quotes].height(Length::Shrink)
+ )
+ .style(|theme: &Theme| {
+ let palette = theme.extended_palette();
+
+ container::Appearance::default().with_background(
+ if palette.is_dark {
+ Color {
+ a: 0.01,
+ ..Color::WHITE
+ }
+ } else {
+ Color {
+ a: 0.08,
+ ..Color::BLACK
+ }
+ },
+ )
+ }),
+ text(format!("Reply {i}"))
+ ]
+ .spacing(10)
+ .padding(10)
+ })
+ .into()
+}
+
+fn square<'a>(size: impl Into<Length> + Copy) -> Element<'a, Message> {
+ struct Square;
+
+ impl canvas::Program<Message> for Square {
+ type State = ();
+
+ fn draw(
+ &self,
+ _state: &Self::State,
+ renderer: &Renderer,
+ theme: &Theme,
+ bounds: Rectangle,
+ _cursor: mouse::Cursor,
+ ) -> Vec<canvas::Geometry> {
+ let mut frame = canvas::Frame::new(renderer, bounds.size());
+
+ let palette = theme.extended_palette();
+
+ frame.fill_rectangle(
+ Point::ORIGIN,
+ bounds.size(),
+ palette.background.strong.color,
+ );
+
+ vec![frame.into_geometry()]
+ }
+ }
+
+ canvas(Square).width(size).height(size).into()
+}
diff --git a/examples/lazy/src/main.rs b/examples/lazy/src/main.rs
index 9bf17c56..04df0744 100644
--- a/examples/lazy/src/main.rs
+++ b/examples/lazy/src/main.rs
@@ -46,7 +46,7 @@ enum Color {
}
impl Color {
- const ALL: &[Color] = &[
+ const ALL: &'static [Color] = &[
Color::Black,
Color::Red,
Color::Orange,
@@ -178,35 +178,23 @@ impl Sandbox for App {
}
});
- column(
- items
- .into_iter()
- .map(|item| {
- let button = button("Delete")
- .on_press(Message::DeleteItem(item.clone()))
- .style(theme::Button::Destructive);
-
- row![
- text(&item.name)
- .style(theme::Text::Color(item.color.into())),
- horizontal_space(Length::Fill),
- pick_list(
- Color::ALL,
- Some(item.color),
- move |color| {
- Message::ItemColorChanged(
- item.clone(),
- color,
- )
- }
- ),
- button
- ]
- .spacing(20)
- .into()
- })
- .collect(),
- )
+ column(items.into_iter().map(|item| {
+ let button = button("Delete")
+ .on_press(Message::DeleteItem(item.clone()))
+ .style(theme::Button::Destructive);
+
+ row![
+ text(&item.name)
+ .style(theme::Text::Color(item.color.into())),
+ horizontal_space(Length::Fill),
+ pick_list(Color::ALL, Some(item.color), move |color| {
+ Message::ItemColorChanged(item.clone(), color)
+ }),
+ button
+ ]
+ .spacing(20)
+ .into()
+ }))
.spacing(10)
});
diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs
index bf01c3b4..2e119979 100644
--- a/examples/loading_spinners/src/circular.rs
+++ b/examples/loading_spinners/src/circular.rs
@@ -244,12 +244,11 @@ where
tree::State::new(State::default())
}
- fn width(&self) -> Length {
- Length::Fixed(self.size)
- }
-
- fn height(&self) -> Length {
- Length::Fixed(self.size)
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: Length::Fixed(self.size),
+ height: Length::Fixed(self.size),
+ }
}
fn layout(
@@ -258,10 +257,7 @@ where
_renderer: &iced::Renderer<Theme>,
limits: &layout::Limits,
) -> layout::Node {
- let limits = limits.width(self.size).height(self.size);
- let size = limits.resolve(Size::ZERO);
-
- layout::Node::new(size)
+ layout::atomic(limits, self.size, self.size)
}
fn on_event(
@@ -275,11 +271,9 @@ where
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
- const FRAME_RATE: u64 = 60;
-
let state = tree.state.downcast_mut::<State>();
- if let Event::Window(window::Event::RedrawRequested(now)) = event {
+ if let Event::Window(_, window::Event::RedrawRequested(now)) = event {
state.animation = state.animation.timed_transition(
self.cycle_duration,
self.rotation_duration,
@@ -287,9 +281,7 @@ where
);
state.cache.clear();
- shell.request_redraw(RedrawRequest::At(
- now + Duration::from_millis(1000 / FRAME_RATE),
- ));
+ shell.request_redraw(RedrawRequest::NextFrame);
}
event::Status::Ignored
diff --git a/examples/loading_spinners/src/linear.rs b/examples/loading_spinners/src/linear.rs
index c5bb4791..497e0834 100644
--- a/examples/loading_spinners/src/linear.rs
+++ b/examples/loading_spinners/src/linear.rs
@@ -165,12 +165,11 @@ where
tree::State::new(State::default())
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -179,10 +178,7 @@ where
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let limits = limits.width(self.width).height(self.height);
- let size = limits.resolve(Size::ZERO);
-
- layout::Node::new(size)
+ layout::atomic(limits, self.width, self.height)
}
fn on_event(
@@ -196,16 +192,12 @@ where
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
- const FRAME_RATE: u64 = 60;
-
let state = tree.state.downcast_mut::<State>();
- if let Event::Window(window::Event::RedrawRequested(now)) = event {
+ if let Event::Window(_, window::Event::RedrawRequested(now)) = event {
*state = state.timed_transition(self.cycle_duration, now);
- shell.request_redraw(RedrawRequest::At(
- now + Duration::from_millis(1000 / FRAME_RATE),
- ));
+ shell.request_redraw(RedrawRequest::NextFrame);
}
event::Status::Ignored
diff --git a/examples/loading_spinners/src/main.rs b/examples/loading_spinners/src/main.rs
index a78e9590..93a4605e 100644
--- a/examples/loading_spinners/src/main.rs
+++ b/examples/loading_spinners/src/main.rs
@@ -96,15 +96,14 @@ impl Application for LoadingSpinners {
container(
column.push(
- row(vec![
- text("Cycle duration:").into(),
+ row![
+ text("Cycle duration:"),
slider(1.0..=1000.0, self.cycle_duration * 100.0, |x| {
Message::CycleDurationChanged(x / 100.0)
})
- .width(200.0)
- .into(),
- text(format!("{:.2}s", self.cycle_duration)).into(),
- ])
+ .width(200.0),
+ text(format!("{:.2}s", self.cycle_duration)),
+ ]
.align_items(iced::Alignment::Center)
.spacing(20.0),
),
diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs
index b0e2c81b..963c839e 100644
--- a/examples/modal/src/main.rs
+++ b/examples/modal/src/main.rs
@@ -1,6 +1,7 @@
use iced::event::{self, Event};
use iced::executor;
use iced::keyboard;
+use iced::keyboard::key;
use iced::theme;
use iced::widget::{
self, button, column, container, horizontal_space, pick_list, row, text,
@@ -85,8 +86,9 @@ impl Application for App {
}
Message::Event(event) => match event {
Event::Keyboard(keyboard::Event::KeyPressed {
- key_code: keyboard::KeyCode::Tab,
+ key: keyboard::Key::Named(key::Named::Tab),
modifiers,
+ ..
}) => {
if modifiers.shift() {
widget::focus_previous()
@@ -95,7 +97,7 @@ impl Application for App {
}
}
Event::Keyboard(keyboard::Event::KeyPressed {
- key_code: keyboard::KeyCode::Escape,
+ key: keyboard::Key::Named(key::Named::Escape),
..
}) => {
self.hide_modal();
@@ -205,7 +207,8 @@ enum Plan {
}
impl Plan {
- pub const ALL: &[Self] = &[Self::Basic, Self::Pro, Self::Enterprise];
+ pub const ALL: &'static [Self] =
+ &[Self::Basic, Self::Pro, Self::Enterprise];
}
impl fmt::Display for Plan {
@@ -230,6 +233,7 @@ mod modal {
use iced::mouse;
use iced::{
BorderRadius, Color, Element, Event, Length, Point, Rectangle, Size,
+ Vector,
};
/// A widget that centers a modal element over some base element
@@ -279,12 +283,8 @@ mod modal {
tree.diff_children(&[&self.base, &self.modal]);
}
- fn width(&self) -> Length {
- self.base.as_widget().width()
- }
-
- fn height(&self) -> Length {
- self.base.as_widget().height()
+ fn size(&self) -> Size<Length> {
+ self.base.as_widget().size()
}
fn layout(
@@ -412,22 +412,20 @@ mod modal {
renderer: &Renderer,
_bounds: Size,
position: Point,
+ _translation: Vector,
) -> layout::Node {
let limits = layout::Limits::new(Size::ZERO, self.size)
.width(Length::Fill)
.height(Length::Fill);
- let mut child = self
+ let child = self
.content
.as_widget()
- .layout(self.tree, renderer, &limits);
-
- child.align(Alignment::Center, Alignment::Center, limits.max());
-
- let mut node = layout::Node::with_children(self.size, vec![child]);
- node.move_to(position);
+ .layout(self.tree, renderer, &limits)
+ .align(Alignment::Center, Alignment::Center, limits.max());
- node
+ layout::Node::with_children(self.size, vec![child])
+ .move_to(position)
}
fn on_event(
diff --git a/examples/multi_window/Cargo.toml b/examples/multi_window/Cargo.toml
new file mode 100644
index 00000000..2e222dfb
--- /dev/null
+++ b/examples/multi_window/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "multi_window"
+version = "0.1.0"
+authors = ["Bingus <shankern@protonmail.com>"]
+edition = "2021"
+publish = false
+
+[dependencies]
+iced = { path = "../..", features = ["debug", "multi-window"] }
diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs
new file mode 100644
index 00000000..5a5e70c1
--- /dev/null
+++ b/examples/multi_window/src/main.rs
@@ -0,0 +1,215 @@
+use iced::event;
+use iced::executor;
+use iced::multi_window::{self, Application};
+use iced::widget::{button, column, container, scrollable, text, text_input};
+use iced::window;
+use iced::{
+ Alignment, Command, Element, Length, Point, Settings, Subscription, Theme,
+ Vector,
+};
+
+use std::collections::HashMap;
+
+fn main() -> iced::Result {
+ Example::run(Settings::default())
+}
+
+#[derive(Default)]
+struct Example {
+ windows: HashMap<window::Id, Window>,
+ next_window_pos: window::Position,
+}
+
+#[derive(Debug)]
+struct Window {
+ title: String,
+ scale_input: String,
+ current_scale: f64,
+ theme: Theme,
+ input_id: iced::widget::text_input::Id,
+}
+
+#[derive(Debug, Clone)]
+enum Message {
+ ScaleInputChanged(window::Id, String),
+ ScaleChanged(window::Id, String),
+ TitleChanged(window::Id, String),
+ CloseWindow(window::Id),
+ WindowOpened(window::Id, Option<Point>),
+ WindowClosed(window::Id),
+ NewWindow,
+}
+
+impl multi_window::Application for Example {
+ type Executor = executor::Default;
+ type Message = Message;
+ type Theme = Theme;
+ type Flags = ();
+
+ fn new(_flags: ()) -> (Self, Command<Message>) {
+ (
+ Example {
+ windows: HashMap::from([(window::Id::MAIN, Window::new(1))]),
+ next_window_pos: window::Position::Default,
+ },
+ Command::none(),
+ )
+ }
+
+ fn title(&self, window: window::Id) -> String {
+ self.windows
+ .get(&window)
+ .map(|window| window.title.clone())
+ .unwrap_or("Example".to_string())
+ }
+
+ fn update(&mut self, message: Message) -> Command<Message> {
+ match message {
+ Message::ScaleInputChanged(id, scale) => {
+ let window =
+ self.windows.get_mut(&id).expect("Window not found!");
+ window.scale_input = scale;
+
+ Command::none()
+ }
+ Message::ScaleChanged(id, scale) => {
+ let window =
+ self.windows.get_mut(&id).expect("Window not found!");
+
+ window.current_scale = scale
+ .parse::<f64>()
+ .unwrap_or(window.current_scale)
+ .clamp(0.5, 5.0);
+
+ Command::none()
+ }
+ Message::TitleChanged(id, title) => {
+ let window =
+ self.windows.get_mut(&id).expect("Window not found.");
+
+ window.title = title;
+
+ Command::none()
+ }
+ Message::CloseWindow(id) => window::close(id),
+ Message::WindowClosed(id) => {
+ self.windows.remove(&id);
+ Command::none()
+ }
+ Message::WindowOpened(id, position) => {
+ if let Some(position) = position {
+ self.next_window_pos = window::Position::Specific(
+ position + Vector::new(20.0, 20.0),
+ );
+ }
+
+ if let Some(window) = self.windows.get(&id) {
+ text_input::focus(window.input_id.clone())
+ } else {
+ Command::none()
+ }
+ }
+ Message::NewWindow => {
+ let count = self.windows.len() + 1;
+
+ let (id, spawn_window) = window::spawn(window::Settings {
+ position: self.next_window_pos,
+ exit_on_close_request: count % 2 == 0,
+ ..Default::default()
+ });
+
+ self.windows.insert(id, Window::new(count));
+
+ spawn_window
+ }
+ }
+ }
+
+ fn view(&self, window: window::Id) -> Element<Message> {
+ let content = self.windows.get(&window).unwrap().view(window);
+
+ container(content)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .center_x()
+ .center_y()
+ .into()
+ }
+
+ fn theme(&self, window: window::Id) -> Self::Theme {
+ self.windows.get(&window).unwrap().theme.clone()
+ }
+
+ fn scale_factor(&self, window: window::Id) -> f64 {
+ self.windows
+ .get(&window)
+ .map(|window| window.current_scale)
+ .unwrap_or(1.0)
+ }
+
+ fn subscription(&self) -> Subscription<Self::Message> {
+ event::listen_with(|event, _| {
+ if let iced::Event::Window(id, window_event) = event {
+ match window_event {
+ window::Event::CloseRequested => {
+ Some(Message::CloseWindow(id))
+ }
+ window::Event::Opened { position, .. } => {
+ Some(Message::WindowOpened(id, position))
+ }
+ window::Event::Closed => Some(Message::WindowClosed(id)),
+ _ => None,
+ }
+ } else {
+ None
+ }
+ })
+ }
+}
+
+impl Window {
+ fn new(count: usize) -> Self {
+ Self {
+ title: format!("Window_{}", count),
+ scale_input: "1.0".to_string(),
+ current_scale: 1.0,
+ theme: if count % 2 == 0 {
+ Theme::Light
+ } else {
+ Theme::Dark
+ },
+ input_id: text_input::Id::unique(),
+ }
+ }
+
+ fn view(&self, id: window::Id) -> Element<Message> {
+ let scale_input = column![
+ text("Window scale factor:"),
+ text_input("Window Scale", &self.scale_input)
+ .on_input(move |msg| { Message::ScaleInputChanged(id, msg) })
+ .on_submit(Message::ScaleChanged(
+ id,
+ self.scale_input.to_string()
+ ))
+ ];
+
+ let title_input = column![
+ text("Window title:"),
+ text_input("Window Title", &self.title)
+ .on_input(move |msg| { Message::TitleChanged(id, msg) })
+ .id(self.input_id.clone())
+ ];
+
+ let new_window_button =
+ button(text("New Window")).on_press(Message::NewWindow);
+
+ let content = scrollable(
+ column![scale_input, title_input, new_window_button]
+ .spacing(50)
+ .width(Length::Fill)
+ .align_items(Alignment::Center),
+ );
+
+ container(content).width(200).center_x().into()
+ }
+}
diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs
index aa3149bb..d5e5bcbe 100644
--- a/examples/pane_grid/src/main.rs
+++ b/examples/pane_grid/src/main.rs
@@ -220,23 +220,26 @@ const PANE_ID_COLOR_FOCUSED: Color = Color::from_rgb(
0x47 as f32 / 255.0,
);
-fn handle_hotkey(key_code: keyboard::KeyCode) -> Option<Message> {
- use keyboard::KeyCode;
+fn handle_hotkey(key: keyboard::Key) -> Option<Message> {
+ use keyboard::key::{self, Key};
use pane_grid::{Axis, Direction};
- let direction = match key_code {
- KeyCode::Up => Some(Direction::Up),
- KeyCode::Down => Some(Direction::Down),
- KeyCode::Left => Some(Direction::Left),
- KeyCode::Right => Some(Direction::Right),
- _ => None,
- };
+ match key.as_ref() {
+ Key::Character("v") => Some(Message::SplitFocused(Axis::Vertical)),
+ Key::Character("h") => Some(Message::SplitFocused(Axis::Horizontal)),
+ Key::Character("w") => Some(Message::CloseFocused),
+ Key::Named(key) => {
+ let direction = match key {
+ key::Named::ArrowUp => Some(Direction::Up),
+ key::Named::ArrowDown => Some(Direction::Down),
+ key::Named::ArrowLeft => Some(Direction::Left),
+ key::Named::ArrowRight => Some(Direction::Right),
+ _ => None,
+ };
- match key_code {
- KeyCode::V => Some(Message::SplitFocused(Axis::Vertical)),
- KeyCode::H => Some(Message::SplitFocused(Axis::Horizontal)),
- KeyCode::W => Some(Message::CloseFocused),
- _ => direction.map(Message::FocusAdjacent),
+ direction.map(Message::FocusAdjacent)
+ }
+ _ => None,
}
}
@@ -297,7 +300,6 @@ fn view_content<'a>(
text(format!("{}x{}", size.width, size.height)).size(24),
controls,
]
- .width(Length::Fill)
.spacing(10)
.align_items(Alignment::Center);
diff --git a/examples/pick_list/src/main.rs b/examples/pick_list/src/main.rs
index 21200621..e4d96dc8 100644
--- a/examples/pick_list/src/main.rs
+++ b/examples/pick_list/src/main.rs
@@ -1,4 +1,4 @@
-use iced::widget::{column, container, pick_list, scrollable, vertical_space};
+use iced::widget::{column, pick_list, scrollable, vertical_space};
use iced::{Alignment, Element, Length, Sandbox, Settings};
pub fn main() -> iced::Result {
@@ -52,12 +52,7 @@ impl Sandbox for Example {
.align_items(Alignment::Center)
.spacing(10);
- container(scrollable(content))
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y()
- .into()
+ scrollable(content).into()
}
}
diff --git a/examples/progress_bar/README.md b/examples/progress_bar/README.md
index 1268ac6b..a87829c6 100644
--- a/examples/progress_bar/README.md
+++ b/examples/progress_bar/README.md
@@ -5,7 +5,7 @@ A simple progress bar that can be filled by using a slider.
The __[`main`]__ file contains all the code of the example.
<div align="center">
- <img src="https://iced.rs/examples/pokedex.gif">
+ <img src="https://iced.rs/examples/progress_bar.gif">
</div>
You can run it with `cargo run`:
diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs
index ab0a2ae3..6955551e 100644
--- a/examples/screenshot/src/main.rs
+++ b/examples/screenshot/src/main.rs
@@ -1,11 +1,13 @@
use iced::alignment;
-use iced::keyboard::KeyCode;
-use iced::theme::{Button, Container};
+use iced::executor;
+use iced::keyboard;
+use iced::theme;
use iced::widget::{button, column, container, image, row, text, text_input};
+use iced::window;
use iced::window::screenshot::{self, Screenshot};
use iced::{
- event, executor, keyboard, Alignment, Application, Command, ContentFit,
- Element, Event, Length, Rectangle, Renderer, Subscription, Theme,
+ Alignment, Application, Command, ContentFit, Element, Length, Rectangle,
+ Renderer, Subscription, Theme,
};
use ::image as img;
@@ -70,7 +72,10 @@ impl Application for Example {
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
match message {
Message::Screenshot => {
- return iced::window::screenshot(Message::ScreenshotData);
+ return iced::window::screenshot(
+ window::Id::MAIN,
+ Message::ScreenshotData,
+ );
}
Message::ScreenshotData(screenshot) => {
self.screenshot = Some(screenshot);
@@ -144,7 +149,7 @@ impl Application for Example {
let image = container(image)
.padding(10)
- .style(Container::Box)
+ .style(theme::Container::Box)
.width(Length::FillPortion(2))
.height(Length::Fill)
.center_x()
@@ -199,9 +204,10 @@ impl Application for Example {
self.screenshot.is_some().then(|| Message::Png),
)
} else {
- button(centered_text("Saving...")).style(Button::Secondary)
+ button(centered_text("Saving..."))
+ .style(theme::Button::Secondary)
}
- .style(Button::Secondary)
+ .style(theme::Button::Secondary)
.padding([10, 20, 10, 20])
.width(Length::Fill)
]
@@ -210,7 +216,7 @@ impl Application for Example {
crop_controls,
button(centered_text("Crop"))
.on_press(Message::Crop)
- .style(Button::Destructive)
+ .style(theme::Button::Destructive)
.padding([10, 20, 10, 20])
.width(Length::Fill),
]
@@ -253,16 +259,10 @@ impl Application for Example {
}
fn subscription(&self) -> Subscription<Self::Message> {
- event::listen_with(|event, status| {
- if let event::Status::Captured = status {
- return None;
- }
+ use keyboard::key;
- if let Event::Keyboard(keyboard::Event::KeyPressed {
- key_code: KeyCode::F5,
- ..
- }) = event
- {
+ keyboard::on_key_press(|key, _modifiers| {
+ if let keyboard::Key::Named(key::Named::F5) = key {
Some(Message::Screenshot)
} else {
None
@@ -298,10 +298,7 @@ fn numeric_input(
) -> Element<'_, Option<u32>> {
text_input(
placeholder,
- &value
- .as_ref()
- .map(ToString::to_string)
- .unwrap_or_else(String::new),
+ &value.as_ref().map(ToString::to_string).unwrap_or_default(),
)
.on_input(move |text| {
if text.is_empty() {
diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs
index d82ea841..4b57a5a4 100644
--- a/examples/scrollable/src/main.rs
+++ b/examples/scrollable/src/main.rs
@@ -147,63 +147,54 @@ impl Application for ScrollableDemo {
text("Scroller width:"),
scroller_width_slider,
]
- .spacing(10)
- .width(Length::Fill);
+ .spacing(10);
- let scroll_orientation_controls = column(vec![
- text("Scrollbar direction:").into(),
+ let scroll_orientation_controls = column![
+ text("Scrollbar direction:"),
radio(
"Vertical",
Direction::Vertical,
Some(self.scrollable_direction),
Message::SwitchDirection,
- )
- .into(),
+ ),
radio(
"Horizontal",
Direction::Horizontal,
Some(self.scrollable_direction),
Message::SwitchDirection,
- )
- .into(),
+ ),
radio(
"Both!",
Direction::Multi,
Some(self.scrollable_direction),
Message::SwitchDirection,
- )
- .into(),
- ])
- .spacing(10)
- .width(Length::Fill);
+ ),
+ ]
+ .spacing(10);
- let scroll_alignment_controls = column(vec![
- text("Scrollable alignment:").into(),
+ let scroll_alignment_controls = column![
+ text("Scrollable alignment:"),
radio(
"Start",
scrollable::Alignment::Start,
Some(self.alignment),
Message::AlignmentChanged,
- )
- .into(),
+ ),
radio(
"End",
scrollable::Alignment::End,
Some(self.alignment),
Message::AlignmentChanged,
)
- .into(),
- ])
- .spacing(10)
- .width(Length::Fill);
+ ]
+ .spacing(10);
let scroll_controls = row![
scroll_slider_controls,
scroll_orientation_controls,
scroll_alignment_controls
]
- .spacing(20)
- .width(Length::Fill);
+ .spacing(20);
let scroll_to_end_button = || {
button("Scroll to end")
@@ -229,11 +220,11 @@ impl Application for ScrollableDemo {
text("End!"),
scroll_to_beginning_button(),
]
- .width(Length::Fill)
.align_items(Alignment::Center)
.padding([40, 0, 40, 0])
.spacing(40),
)
+ .width(Length::Fill)
.height(Length::Fill)
.direction(scrollable::Direction::Vertical(
Properties::new()
@@ -259,6 +250,7 @@ impl Application for ScrollableDemo {
.padding([0, 40, 0, 40])
.spacing(40),
)
+ .width(Length::Fill)
.height(Length::Fill)
.direction(scrollable::Direction::Horizontal(
Properties::new()
@@ -301,6 +293,7 @@ impl Application for ScrollableDemo {
.padding([0, 40, 0, 40])
.spacing(40),
)
+ .width(Length::Fill)
.height(Length::Fill)
.direction({
let properties = Properties::new()
@@ -341,20 +334,11 @@ impl Application for ScrollableDemo {
let content: Element<Message> =
column![scroll_controls, scrollable_content, progress_bars]
- .width(Length::Fill)
- .height(Length::Fill)
.align_items(Alignment::Center)
.spacing(10)
.into();
- Element::from(
- container(content)
- .width(Length::Fill)
- .height(Length::Fill)
- .padding(40)
- .center_x()
- .center_y(),
- )
+ container(content).padding(20).center_x().center_y().into()
}
fn theme(&self) -> Self::Theme {
diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs
index ef935c33..01a114bb 100644
--- a/examples/sierpinski_triangle/src/main.rs
+++ b/examples/sierpinski_triangle/src/main.rs
@@ -79,12 +79,10 @@ impl Application for SierpinskiEmulator {
row![
text(format!("Iteration: {:?}", self.graph.iteration)),
slider(0..=10000, self.graph.iteration, Message::IterationSet)
- .width(Length::Fill)
]
.padding(10)
.spacing(20),
]
- .width(Length::Fill)
.align_items(iced::Alignment::Center)
.into()
}
diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs
index 8295dded..82421a86 100644
--- a/examples/solar_system/src/main.rs
+++ b/examples/solar_system/src/main.rs
@@ -114,14 +114,14 @@ impl State {
pub fn new() -> State {
let now = Instant::now();
- let (width, height) = window::Settings::default().size;
+ let size = window::Settings::default().size;
State {
space_cache: canvas::Cache::default(),
system_cache: canvas::Cache::default(),
start: now,
now,
- stars: Self::generate_stars(width, height),
+ stars: Self::generate_stars(size.width, size.height),
}
}
@@ -130,7 +130,7 @@ impl State {
self.system_cache.clear();
}
- fn generate_stars(width: u32, height: u32) -> Vec<(Point, f32)> {
+ fn generate_stars(width: f32, height: f32) -> Vec<(Point, f32)> {
use rand::Rng;
let mut rng = rand::thread_rng();
@@ -139,12 +139,8 @@ impl State {
.map(|_| {
(
Point::new(
- rng.gen_range(
- (-(width as f32) / 2.0)..(width as f32 / 2.0),
- ),
- rng.gen_range(
- (-(height as f32) / 2.0)..(height as f32 / 2.0),
- ),
+ rng.gen_range((-width / 2.0)..(width / 2.0)),
+ rng.gen_range((-height / 2.0)..(height / 2.0)),
),
rng.gen_range(0.5..1.0),
)
diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs
index 0b0f0607..8a0674c1 100644
--- a/examples/stopwatch/src/main.rs
+++ b/examples/stopwatch/src/main.rs
@@ -86,12 +86,16 @@ impl Application for Stopwatch {
};
fn handle_hotkey(
- key_code: keyboard::KeyCode,
+ key: keyboard::Key,
_modifiers: keyboard::Modifiers,
) -> Option<Message> {
- match key_code {
- keyboard::KeyCode::Space => Some(Message::Toggle),
- keyboard::KeyCode::R => Some(Message::Reset),
+ use keyboard::key;
+
+ match key.as_ref() {
+ keyboard::Key::Named(key::Named::Space) => {
+ Some(Message::Toggle)
+ }
+ keyboard::Key::Character("r") => Some(Message::Reset),
_ => None,
}
}
diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs
index 51538ec2..10f3c79d 100644
--- a/examples/styling/src/main.rs
+++ b/examples/styling/src/main.rs
@@ -53,13 +53,16 @@ impl Sandbox for Styling {
self.theme = match theme {
ThemeType::Light => Theme::Light,
ThemeType::Dark => Theme::Dark,
- ThemeType::Custom => Theme::custom(theme::Palette {
- background: Color::from_rgb(1.0, 0.9, 1.0),
- text: Color::BLACK,
- primary: Color::from_rgb(0.5, 0.5, 0.0),
- success: Color::from_rgb(0.0, 1.0, 0.0),
- danger: Color::from_rgb(1.0, 0.0, 0.0),
- }),
+ ThemeType::Custom => Theme::custom(
+ String::from("Custom"),
+ theme::Palette {
+ background: Color::from_rgb(1.0, 0.9, 1.0),
+ text: Color::BLACK,
+ primary: Color::from_rgb(0.5, 0.5, 0.0),
+ success: Color::from_rgb(0.0, 1.0, 0.0),
+ danger: Color::from_rgb(1.0, 0.0, 0.0),
+ },
+ ),
}
}
Message::InputChanged(value) => self.input_value = value,
@@ -104,10 +107,11 @@ impl Sandbox for Styling {
let progress_bar = progress_bar(0.0..=100.0, self.slider_value);
- let scrollable = scrollable(
- column!["Scroll me!", vertical_space(800), "You did it!"]
- .width(Length::Fill),
- )
+ let scrollable = scrollable(column![
+ "Scroll me!",
+ vertical_space(800),
+ "You did it!"
+ ])
.width(Length::Fill)
.height(100);
diff --git a/examples/svg/src/main.rs b/examples/svg/src/main.rs
index 4dc92416..3bf4960f 100644
--- a/examples/svg/src/main.rs
+++ b/examples/svg/src/main.rs
@@ -63,7 +63,6 @@ impl Sandbox for Tiger {
container(apply_color_filter).width(Length::Fill).center_x()
]
.spacing(20)
- .width(Length::Fill)
.height(Length::Fill),
)
.width(Length::Fill)
diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs
index 20c3dd42..2e837fa3 100644
--- a/examples/toast/src/main.rs
+++ b/examples/toast/src/main.rs
@@ -1,6 +1,7 @@
use iced::event::{self, Event};
use iced::executor;
use iced::keyboard;
+use iced::keyboard::key;
use iced::widget::{
self, button, column, container, pick_list, row, slider, text, text_input,
};
@@ -93,11 +94,12 @@ impl Application for App {
Command::none()
}
Message::Event(Event::Keyboard(keyboard::Event::KeyPressed {
- key_code: keyboard::KeyCode::Tab,
+ key: keyboard::Key::Named(key::Named::Tab),
modifiers,
+ ..
})) if modifiers.shift() => widget::focus_previous(),
Message::Event(Event::Keyboard(keyboard::Event::KeyPressed {
- key_code: keyboard::KeyCode::Tab,
+ key: keyboard::Key::Named(key::Named::Tab),
..
})) => widget::focus_next(),
Message::Event(_) => Command::none(),
@@ -106,9 +108,7 @@ impl Application for App {
fn view<'a>(&'a self) -> Element<'a, Message> {
let subtitle = |title, content: Element<'a, Message>| {
- column![text(title).size(14), content]
- .width(Length::Fill)
- .spacing(5)
+ column![text(title).size(14), content].spacing(5)
};
let mut add_toast = button("Add Toast");
@@ -153,14 +153,11 @@ impl Application for App {
Message::Timeout
)
.step(1.0)
- .width(Length::Fill)
]
.spacing(5)
.into()
),
- column![add_toast]
- .width(Length::Fill)
- .align_items(Alignment::End)
+ column![add_toast].align_items(Alignment::End)
]
.spacing(10)
.max_width(200),
@@ -210,7 +207,7 @@ mod toast {
}
impl Status {
- pub const ALL: &[Self] =
+ pub const ALL: &'static [Self] =
&[Self::Primary, Self::Secondary, Self::Success, Self::Danger];
}
@@ -318,12 +315,8 @@ mod toast {
}
impl<'a, Message> Widget<Message, Renderer> for Manager<'a, Message> {
- fn width(&self) -> Length {
- self.content.as_widget().width()
- }
-
- fn height(&self) -> Length {
- self.content.as_widget().height()
+ fn size(&self) -> Size<Length> {
+ self.content.as_widget().size()
}
fn layout(
@@ -511,15 +504,16 @@ mod toast {
renderer: &Renderer,
bounds: Size,
position: Point,
+ _translation: Vector,
) -> layout::Node {
- let limits = layout::Limits::new(Size::ZERO, bounds)
- .width(Length::Fill)
- .height(Length::Fill);
+ let limits = layout::Limits::new(Size::ZERO, bounds);
layout::flex::resolve(
layout::flex::Axis::Vertical,
renderer,
&limits,
+ Length::Fill,
+ Length::Fill,
10.into(),
10.0,
Alignment::End,
@@ -538,7 +532,9 @@ mod toast {
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
- if let Event::Window(window::Event::RedrawRequested(now)) = &event {
+ if let Event::Window(_, window::Event::RedrawRequested(now)) =
+ &event
+ {
let mut next_redraw: Option<window::RedrawRequest> = None;
self.instants.iter_mut().enumerate().for_each(
diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs
index 1ad3aba7..3d79f087 100644
--- a/examples/todos/src/main.rs
+++ b/examples/todos/src/main.rs
@@ -8,7 +8,7 @@ use iced::widget::{
};
use iced::window;
use iced::{Application, Element};
-use iced::{Color, Command, Length, Settings, Subscription};
+use iced::{Color, Command, Length, Settings, Size, Subscription};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
@@ -22,7 +22,7 @@ pub fn main() -> iced::Result {
Todos::run(Settings {
window: window::Settings {
- size: (500, 800),
+ size: Size::new(500.0, 800.0),
..window::Settings::default()
},
..Settings::default()
@@ -54,7 +54,7 @@ enum Message {
FilterChanged(Filter),
TaskMessage(usize, TaskMessage),
TabPressed { shift: bool },
- ChangeWindowMode(window::Mode),
+ ToggleFullscreen(window::Mode),
}
impl Application for Todos {
@@ -165,8 +165,8 @@ impl Application for Todos {
widget::focus_next()
}
}
- Message::ChangeWindowMode(mode) => {
- window::change_mode(mode)
+ Message::ToggleFullscreen(mode) => {
+ window::change_mode(window::Id::MAIN, mode)
}
_ => Command::none(),
};
@@ -254,28 +254,28 @@ impl Application for Todos {
.spacing(20)
.max_width(800);
- scrollable(
- container(content)
- .width(Length::Fill)
- .padding(40)
- .center_x(),
- )
- .into()
+ scrollable(container(content).padding(40).center_x()).into()
}
}
}
fn subscription(&self) -> Subscription<Message> {
- keyboard::on_key_press(|key_code, modifiers| {
- match (key_code, modifiers) {
- (keyboard::KeyCode::Tab, _) => Some(Message::TabPressed {
+ use keyboard::key;
+
+ keyboard::on_key_press(|key, modifiers| {
+ let keyboard::Key::Named(key) = key else {
+ return None;
+ };
+
+ match (key, modifiers) {
+ (key::Named::Tab, _) => Some(Message::TabPressed {
shift: modifiers.shift(),
}),
- (keyboard::KeyCode::Up, keyboard::Modifiers::SHIFT) => {
- Some(Message::ChangeWindowMode(window::Mode::Fullscreen))
+ (key::Named::ArrowUp, keyboard::Modifiers::SHIFT) => {
+ Some(Message::ToggleFullscreen(window::Mode::Fullscreen))
}
- (keyboard::KeyCode::Down, keyboard::Modifiers::SHIFT) => {
- Some(Message::ChangeWindowMode(window::Mode::Windowed))
+ (key::Named::ArrowDown, keyboard::Modifiers::SHIFT) => {
+ Some(Message::ToggleFullscreen(window::Mode::Windowed))
}
_ => None,
}
@@ -472,7 +472,6 @@ fn empty_message(message: &str) -> Element<'_, Message> {
.horizontal_alignment(alignment::Horizontal::Center)
.style(Color::from([0.7, 0.7, 0.7])),
)
- .width(Length::Fill)
.height(200)
.center_y()
.into()
diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs
index d46e40d1..8633bc0a 100644
--- a/examples/tour/src/main.rs
+++ b/examples/tour/src/main.rs
@@ -1,4 +1,4 @@
-use iced::alignment;
+use iced::alignment::{self, Alignment};
use iced::theme;
use iced::widget::{
checkbox, column, container, horizontal_space, image, radio, row,
@@ -126,7 +126,10 @@ impl Steps {
Step::Toggler {
can_continue: false,
},
- Step::Image { width: 300 },
+ Step::Image {
+ width: 300,
+ filter_method: image::FilterMethod::Linear,
+ },
Step::Scrollable,
Step::TextInput {
value: String::new(),
@@ -195,6 +198,7 @@ enum Step {
},
Image {
width: u16,
+ filter_method: image::FilterMethod,
},
Scrollable,
TextInput {
@@ -215,6 +219,7 @@ pub enum StepMessage {
TextColorChanged(Color),
LanguageSelected(Language),
ImageWidthChanged(u16),
+ ImageUseNearestToggled(bool),
InputChanged(String),
ToggleSecureInput(bool),
ToggleTextInputIcon(bool),
@@ -265,6 +270,15 @@ impl<'a> Step {
*width = new_width;
}
}
+ StepMessage::ImageUseNearestToggled(use_nearest) => {
+ if let Step::Image { filter_method, .. } = self {
+ *filter_method = if use_nearest {
+ image::FilterMethod::Nearest
+ } else {
+ image::FilterMethod::Linear
+ };
+ }
+ }
StepMessage::InputChanged(new_value) => {
if let Step::TextInput { value, .. } = self {
*value = new_value;
@@ -330,7 +344,10 @@ impl<'a> Step {
Step::Toggler { can_continue } => Self::toggler(*can_continue),
Step::Slider { value } => Self::slider(*value),
Step::Text { size, color } => Self::text(*size, *color),
- Step::Image { width } => Self::image(*width),
+ Step::Image {
+ width,
+ filter_method,
+ } => Self::image(*width, *filter_method),
Step::RowsAndColumns { layout, spacing } => {
Self::rows_and_columns(*layout, *spacing)
}
@@ -492,7 +509,6 @@ impl<'a> Step {
)
})
.map(Element::from)
- .collect()
)
.spacing(10)
]
@@ -525,16 +541,25 @@ impl<'a> Step {
)
}
- fn image(width: u16) -> Column<'a, StepMessage> {
+ fn image(
+ width: u16,
+ filter_method: image::FilterMethod,
+ ) -> Column<'a, StepMessage> {
Self::container("Image")
.push("An image that tries to keep its aspect ratio.")
- .push(ferris(width))
+ .push(ferris(width, filter_method))
.push(slider(100..=500, width, StepMessage::ImageWidthChanged))
.push(
text(format!("Width: {width} px"))
.width(Length::Fill)
.horizontal_alignment(alignment::Horizontal::Center),
)
+ .push(checkbox(
+ "Use nearest interpolation",
+ filter_method == image::FilterMethod::Nearest,
+ StepMessage::ImageUseNearestToggled,
+ ))
+ .align_items(Alignment::Center)
}
fn scrollable() -> Column<'a, StepMessage> {
@@ -555,7 +580,7 @@ impl<'a> Step {
.horizontal_alignment(alignment::Horizontal::Center),
)
.push(vertical_space(4096))
- .push(ferris(300))
+ .push(ferris(300, image::FilterMethod::Linear))
.push(
text("You made it!")
.width(Length::Fill)
@@ -646,7 +671,10 @@ impl<'a> Step {
}
}
-fn ferris<'a>(width: u16) -> Container<'a, StepMessage> {
+fn ferris<'a>(
+ width: u16,
+ filter_method: image::FilterMethod,
+) -> Container<'a, StepMessage> {
container(
// This should go away once we unify resource loading on native
// platforms
@@ -655,6 +683,7 @@ fn ferris<'a>(width: u16) -> Container<'a, StepMessage> {
} else {
image(format!("{}/images/ferris.png", env!("CARGO_MANIFEST_DIR")))
}
+ .filter_method(filter_method)
.width(width),
)
.width(Length::Fill)
@@ -662,11 +691,7 @@ fn ferris<'a>(width: u16) -> Container<'a, StepMessage> {
}
fn button<'a, Message: Clone>(label: &str) -> Button<'a, Message> {
- iced::widget::button(
- text(label).horizontal_alignment(alignment::Horizontal::Center),
- )
- .padding(12)
- .width(100)
+ iced::widget::button(text(label)).padding([12, 24])
}
fn color_slider<'a>(
diff --git a/examples/vectorial_text/Cargo.toml b/examples/vectorial_text/Cargo.toml
new file mode 100644
index 00000000..76c1af7c
--- /dev/null
+++ b/examples/vectorial_text/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "vectorial_text"
+version = "0.1.0"
+authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
+edition = "2021"
+publish = false
+
+[dependencies]
+iced = { path = "../..", features = ["canvas", "debug"] }
diff --git a/examples/vectorial_text/src/main.rs b/examples/vectorial_text/src/main.rs
new file mode 100644
index 00000000..d366b907
--- /dev/null
+++ b/examples/vectorial_text/src/main.rs
@@ -0,0 +1,175 @@
+use iced::alignment::{self, Alignment};
+use iced::mouse;
+use iced::widget::{
+ canvas, checkbox, column, horizontal_space, row, slider, text,
+};
+use iced::{
+ Element, Length, Point, Rectangle, Renderer, Sandbox, Settings, Theme,
+ Vector,
+};
+
+pub fn main() -> iced::Result {
+ VectorialText::run(Settings {
+ antialiasing: true,
+ ..Settings::default()
+ })
+}
+
+struct VectorialText {
+ state: State,
+}
+
+#[derive(Debug, Clone, Copy)]
+enum Message {
+ SizeChanged(f32),
+ AngleChanged(f32),
+ ScaleChanged(f32),
+ ToggleJapanese(bool),
+}
+
+impl Sandbox for VectorialText {
+ type Message = Message;
+
+ fn new() -> Self {
+ Self {
+ state: State::new(),
+ }
+ }
+
+ fn title(&self) -> String {
+ String::from("Vectorial Text - Iced")
+ }
+
+ fn update(&mut self, message: Message) {
+ match message {
+ Message::SizeChanged(size) => {
+ self.state.size = size;
+ }
+ Message::AngleChanged(angle) => {
+ self.state.angle = angle;
+ }
+ Message::ScaleChanged(scale) => {
+ self.state.scale = scale;
+ }
+ Message::ToggleJapanese(use_japanese) => {
+ self.state.use_japanese = use_japanese;
+ }
+ }
+
+ self.state.cache.clear();
+ }
+
+ fn view(&self) -> Element<Message> {
+ let slider_with_label = |label, range, value, message: fn(f32) -> _| {
+ column![
+ row![
+ text(label),
+ horizontal_space(Length::Fill),
+ text(format!("{:.2}", value))
+ ],
+ slider(range, value, message).step(0.01)
+ ]
+ .spacing(2)
+ };
+
+ column![
+ canvas(&self.state).width(Length::Fill).height(Length::Fill),
+ column![
+ checkbox(
+ "Use Japanese",
+ self.state.use_japanese,
+ Message::ToggleJapanese
+ ),
+ row![
+ slider_with_label(
+ "Size",
+ 2.0..=80.0,
+ self.state.size,
+ Message::SizeChanged,
+ ),
+ slider_with_label(
+ "Angle",
+ 0.0..=360.0,
+ self.state.angle,
+ Message::AngleChanged,
+ ),
+ slider_with_label(
+ "Scale",
+ 1.0..=20.0,
+ self.state.scale,
+ Message::ScaleChanged,
+ ),
+ ]
+ .spacing(20),
+ ]
+ .align_items(Alignment::Center)
+ .spacing(10)
+ ]
+ .spacing(10)
+ .padding(20)
+ .into()
+ }
+
+ fn theme(&self) -> Theme {
+ Theme::Dark
+ }
+}
+
+struct State {
+ size: f32,
+ angle: f32,
+ scale: f32,
+ use_japanese: bool,
+ cache: canvas::Cache,
+}
+
+impl State {
+ pub fn new() -> Self {
+ Self {
+ size: 40.0,
+ angle: 0.0,
+ scale: 1.0,
+ use_japanese: false,
+ cache: canvas::Cache::new(),
+ }
+ }
+}
+
+impl<Message> canvas::Program<Message> for State {
+ type State = ();
+
+ fn draw(
+ &self,
+ _state: &Self::State,
+ renderer: &Renderer,
+ theme: &Theme,
+ bounds: Rectangle,
+ _cursor: mouse::Cursor,
+ ) -> Vec<canvas::Geometry> {
+ let geometry = self.cache.draw(renderer, bounds.size(), |frame| {
+ let palette = theme.palette();
+ let center = bounds.center();
+
+ frame.translate(Vector::new(center.x, center.y));
+ frame.scale(self.scale);
+ frame.rotate(self.angle * std::f32::consts::PI / 180.0);
+
+ frame.fill_text(canvas::Text {
+ position: Point::new(0.0, 0.0),
+ color: palette.text,
+ size: self.size.into(),
+ content: String::from(if self.use_japanese {
+ "ベクトルテキスト🎉"
+ } else {
+ "Vectorial Text! 🎉"
+ }),
+ horizontal_alignment: alignment::Horizontal::Center,
+ vertical_alignment: alignment::Vertical::Center,
+ shaping: text::Shaping::Advanced,
+ ..canvas::Text::default()
+ });
+ });
+
+ vec![geometry]
+ }
+}
diff --git a/examples/visible_bounds/src/main.rs b/examples/visible_bounds/src/main.rs
index 697badb4..fdf1e0f9 100644
--- a/examples/visible_bounds/src/main.rs
+++ b/examples/visible_bounds/src/main.rs
@@ -167,7 +167,7 @@ impl Application for Example {
Event::Mouse(mouse::Event::CursorMoved { position }) => {
Some(Message::MouseMoved(position))
}
- Event::Window(window::Event::Resized { .. }) => {
+ Event::Window(_, window::Event::Resized { .. }) => {
Some(Message::WindowResized)
}
_ => None,
diff --git a/examples/websocket/Cargo.toml b/examples/websocket/Cargo.toml
index 2756e8e0..8f1b876a 100644
--- a/examples/websocket/Cargo.toml
+++ b/examples/websocket/Cargo.toml
@@ -13,7 +13,7 @@ once_cell.workspace = true
warp = "0.3"
[dependencies.async-tungstenite]
-version = "0.23"
+version = "0.24"
features = ["tokio-rustls-webpki-roots"]
[dependencies.tokio]
diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs
index 920189f5..38a6db1e 100644
--- a/examples/websocket/src/main.rs
+++ b/examples/websocket/src/main.rs
@@ -3,7 +3,7 @@ mod echo;
use iced::alignment::{self, Alignment};
use iced::executor;
use iced::widget::{
- button, column, container, row, scrollable, text, text_input, Column,
+ button, column, container, row, scrollable, text, text_input,
};
use iced::{
Application, Color, Command, Element, Length, Settings, Subscription, Theme,
@@ -108,15 +108,9 @@ impl Application for WebSocket {
.into()
} else {
scrollable(
- Column::with_children(
- self.messages
- .iter()
- .cloned()
- .map(text)
- .map(Element::from)
- .collect(),
+ column(
+ self.messages.iter().cloned().map(text).map(Element::from),
)
- .width(Length::Fill)
.spacing(10),
)
.id(MESSAGE_LOG.clone())
@@ -131,7 +125,7 @@ impl Application for WebSocket {
let mut button = button(
text("Send")
- .height(Length::Fill)
+ .height(40)
.vertical_alignment(alignment::Vertical::Center),
)
.padding([0, 20]);
@@ -149,7 +143,6 @@ impl Application for WebSocket {
};
column![message_log, new_message_input]
- .width(Length::Fill)
.height(Length::Fill)
.padding(20)
.spacing(10)
diff --git a/futures/src/event.rs b/futures/src/event.rs
index 214d2d40..97224506 100644
--- a/futures/src/event.rs
+++ b/futures/src/event.rs
@@ -35,7 +35,7 @@ where
subscription::filter_map(
(EventsWith, f),
move |event, status| match event {
- Event::Window(window::Event::RedrawRequested(_)) => None,
+ Event::Window(_, window::Event::RedrawRequested(_)) => None,
_ => f(event, status),
},
)
diff --git a/futures/src/keyboard.rs b/futures/src/keyboard.rs
index af68e1f2..8e7da38f 100644
--- a/futures/src/keyboard.rs
+++ b/futures/src/keyboard.rs
@@ -1,6 +1,6 @@
//! Listen to keyboard events.
use crate::core;
-use crate::core::keyboard::{Event, KeyCode, Modifiers};
+use crate::core::keyboard::{Event, Key, Modifiers};
use crate::subscription::{self, Subscription};
use crate::MaybeSend;
@@ -10,7 +10,7 @@ use crate::MaybeSend;
/// If the function returns `None`, the key press will be simply
/// ignored.
pub fn on_key_press<Message>(
- f: fn(KeyCode, Modifiers) -> Option<Message>,
+ f: fn(Key, Modifiers) -> Option<Message>,
) -> Subscription<Message>
where
Message: MaybeSend + 'static,
@@ -22,11 +22,10 @@ where
match (event, status) {
(
core::Event::Keyboard(Event::KeyPressed {
- key_code,
- modifiers,
+ key, modifiers, ..
}),
core::event::Status::Ignored,
- ) => f(key_code, modifiers),
+ ) => f(key, modifiers),
_ => None,
}
})
@@ -38,7 +37,7 @@ where
/// If the function returns `None`, the key release will be simply
/// ignored.
pub fn on_key_release<Message>(
- f: fn(KeyCode, Modifiers) -> Option<Message>,
+ f: fn(Key, Modifiers) -> Option<Message>,
) -> Subscription<Message>
where
Message: MaybeSend + 'static,
@@ -50,11 +49,12 @@ where
match (event, status) {
(
core::Event::Keyboard(Event::KeyReleased {
- key_code,
+ key,
modifiers,
+ ..
}),
core::event::Status::Ignored,
- ) => f(key_code, modifiers),
+ ) => f(key, modifiers),
_ => None,
}
})
diff --git a/futures/src/lib.rs b/futures/src/lib.rs
index d54ba18a..b0acb76f 100644
--- a/futures/src/lib.rs
+++ b/futures/src/lib.rs
@@ -15,7 +15,7 @@
pub use futures;
pub use iced_core as core;
-mod maybe_send;
+mod maybe;
mod runtime;
pub mod backend;
@@ -25,7 +25,7 @@ pub mod keyboard;
pub mod subscription;
pub use executor::Executor;
-pub use maybe_send::MaybeSend;
+pub use maybe::{MaybeSend, MaybeSync};
pub use platform::*;
pub use runtime::Runtime;
pub use subscription::Subscription;
diff --git a/futures/src/maybe.rs b/futures/src/maybe.rs
new file mode 100644
index 00000000..c6a507c1
--- /dev/null
+++ b/futures/src/maybe.rs
@@ -0,0 +1,35 @@
+#[cfg(not(target_arch = "wasm32"))]
+mod platform {
+ /// An extension trait that enforces `Send` only on native platforms.
+ ///
+ /// Useful for writing cross-platform async code!
+ pub trait MaybeSend: Send {}
+
+ impl<T> MaybeSend for T where T: Send {}
+
+ /// An extension trait that enforces `Sync` only on native platforms.
+ ///
+ /// Useful for writing cross-platform async code!
+ pub trait MaybeSync: Sync {}
+
+ impl<T> MaybeSync for T where T: Sync {}
+}
+
+#[cfg(target_arch = "wasm32")]
+mod platform {
+ /// An extension trait that enforces `Send` only on native platforms.
+ ///
+ /// Useful for writing cross-platform async code!
+ pub trait MaybeSend {}
+
+ impl<T> MaybeSend for T {}
+
+ /// An extension trait that enforces `Sync` only on native platforms.
+ ///
+ /// Useful for writing cross-platform async code!
+ pub trait MaybeSync {}
+
+ impl<T> MaybeSync for T {}
+}
+
+pub use platform::{MaybeSend, MaybeSync};
diff --git a/futures/src/maybe_send.rs b/futures/src/maybe_send.rs
deleted file mode 100644
index a6670f0e..00000000
--- a/futures/src/maybe_send.rs
+++ /dev/null
@@ -1,21 +0,0 @@
-#[cfg(not(target_arch = "wasm32"))]
-mod platform {
- /// An extension trait that enforces `Send` only on native platforms.
- ///
- /// Useful to write cross-platform async code!
- pub trait MaybeSend: Send {}
-
- impl<T> MaybeSend for T where T: Send {}
-}
-
-#[cfg(target_arch = "wasm32")]
-mod platform {
- /// An extension trait that enforces `Send` only on native platforms.
- ///
- /// Useful to write cross-platform async code!
- pub trait MaybeSend {}
-
- impl<T> MaybeSend for T {}
-}
-
-pub use platform::MaybeSend;
diff --git a/futures/src/runtime.rs b/futures/src/runtime.rs
index 16111b36..cac7b7e1 100644
--- a/futures/src/runtime.rs
+++ b/futures/src/runtime.rs
@@ -1,7 +1,7 @@
//! Run commands and keep track of subscriptions.
use crate::core::event::{self, Event};
use crate::subscription;
-use crate::{BoxFuture, Executor, MaybeSend};
+use crate::{BoxFuture, BoxStream, Executor, MaybeSend};
use futures::{channel::mpsc, Sink};
use std::marker::PhantomData;
@@ -69,6 +69,29 @@ where
self.executor.spawn(future);
}
+ /// Runs a [`Stream`] in the [`Runtime`] until completion.
+ ///
+ /// The resulting `Message`s will be forwarded to the `Sender` of the
+ /// [`Runtime`].
+ ///
+ /// [`Stream`]: BoxStream
+ pub fn run(&mut self, stream: BoxStream<Message>) {
+ use futures::{FutureExt, StreamExt};
+
+ let sender = self.sender.clone();
+ let future =
+ stream.map(Ok).forward(sender).map(|result| match result {
+ Ok(()) => (),
+ Err(error) => {
+ log::warn!(
+ "Stream could not run until completion: {error}"
+ );
+ }
+ });
+
+ self.executor.spawn(future);
+ }
+
/// Tracks a [`Subscription`] in the [`Runtime`].
///
/// It will spawn new streams or close old ones as necessary! See
diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml
index ff698649..4f323f9e 100644
--- a/graphics/Cargo.toml
+++ b/graphics/Cargo.toml
@@ -16,25 +16,25 @@ all-features = true
[features]
geometry = ["lyon_path"]
-opengl = []
image = ["dep:image", "kamadak-exif"]
web-colors = []
[dependencies]
iced_core.workspace = true
+iced_futures.workspace = true
bitflags.workspace = true
bytemuck.workspace = true
+cosmic-text.workspace = true
glam.workspace = true
half.workspace = true
log.workspace = true
+once_cell.workspace = true
raw-window-handle.workspace = true
-thiserror.workspace = true
-cosmic-text.workspace = true
rustc-hash.workspace = true
-
-lyon_path.workspace = true
-lyon_path.optional = true
+thiserror.workspace = true
+unicode-segmentation.workspace = true
+xxhash-rust.workspace = true
image.workspace = true
image.optional = true
@@ -42,8 +42,5 @@ image.optional = true
kamadak-exif.workspace = true
kamadak-exif.optional = true
-twox-hash.workspace = true
-
-[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
-twox-hash.workspace = true
-twox-hash.features = ["std"]
+lyon_path.workspace = true
+lyon_path.optional = true
diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs
index c2ac82ba..10eb337f 100644
--- a/graphics/src/backend.rs
+++ b/graphics/src/backend.rs
@@ -2,7 +2,6 @@
use crate::core::image;
use crate::core::svg;
use crate::core::Size;
-use crate::text;
use std::borrow::Cow;
@@ -18,9 +17,6 @@ pub trait Backend {
pub trait Text {
/// Loads a font from its bytes.
fn load_font(&mut self, font: Cow<'static, [u8]>);
-
- /// Returns the [`cosmic_text::FontSystem`] of the [`Backend`].
- fn font_system(&self) -> &text::FontSystem;
}
/// A graphics backend that supports image rendering.
diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs
index 0222a80f..0188f4d8 100644
--- a/graphics/src/compositor.rs
+++ b/graphics/src/compositor.rs
@@ -2,9 +2,10 @@
//! surfaces.
use crate::{Error, Viewport};
-use iced_core::Color;
+use crate::core::Color;
+use crate::futures::{MaybeSend, MaybeSync};
-use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle};
+use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
use thiserror::Error;
/// A graphics compositor that can draw to windows.
@@ -19,17 +20,20 @@ pub trait Compositor: Sized {
type Surface;
/// Creates a new [`Compositor`].
- fn new<W: HasRawWindowHandle + HasRawDisplayHandle>(
+ fn new<W: Window + Clone>(
settings: Self::Settings,
- compatible_window: Option<&W>,
- ) -> Result<(Self, Self::Renderer), Error>;
+ compatible_window: W,
+ ) -> Result<Self, Error>;
+
+ /// Creates a [`Self::Renderer`] for the [`Compositor`].
+ fn create_renderer(&self) -> Self::Renderer;
/// Crates a new [`Surface`] for the given window.
///
/// [`Surface`]: Self::Surface
- fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>(
+ fn create_surface<W: Window + Clone>(
&mut self,
- window: &W,
+ window: W,
width: u32,
height: u32,
) -> Self::Surface;
@@ -74,6 +78,20 @@ pub trait Compositor: Sized {
) -> Vec<u8>;
}
+/// A window that can be used in a [`Compositor`].
+///
+/// This is just a convenient super trait of the `raw-window-handle`
+/// traits.
+pub trait Window:
+ HasWindowHandle + HasDisplayHandle + MaybeSend + MaybeSync + 'static
+{
+}
+
+impl<T> Window for T where
+ T: HasWindowHandle + HasDisplayHandle + MaybeSend + MaybeSync + 'static
+{
+}
+
/// Result of an unsuccessful call to [`Compositor::present`].
#[derive(Clone, PartialEq, Eq, Debug, Error)]
pub enum SurfaceError {
diff --git a/graphics/src/damage.rs b/graphics/src/damage.rs
index 3276c2d4..59e9f5b4 100644
--- a/graphics/src/damage.rs
+++ b/graphics/src/damage.rs
@@ -66,6 +66,18 @@ impl<T: Damage> Damage for Primitive<T> {
bounds.expand(1.5)
}
+ Self::Editor {
+ editor, position, ..
+ } => {
+ let bounds = Rectangle::new(*position, editor.bounds);
+
+ bounds.expand(1.5)
+ }
+ Self::RawText(raw) => {
+ // TODO: Add `size` field to `raw` to compute more accurate
+ // damage bounds (?)
+ raw.clip_bounds.expand(1.5)
+ }
Self::Quad { bounds, .. }
| Self::Image { bounds, .. }
| Self::Svg { bounds, .. } => bounds.expand(1.0),
diff --git a/graphics/src/geometry/text.rs b/graphics/src/geometry/text.rs
index 0bf7ec97..d314e85e 100644
--- a/graphics/src/geometry/text.rs
+++ b/graphics/src/geometry/text.rs
@@ -1,6 +1,8 @@
use crate::core::alignment;
use crate::core::text::{LineHeight, Shaping};
-use crate::core::{Color, Font, Pixels, Point};
+use crate::core::{Color, Font, Pixels, Point, Size, Vector};
+use crate::geometry::Path;
+use crate::text;
/// A bunch of text that can be drawn to a canvas
#[derive(Debug, Clone)]
@@ -32,6 +34,137 @@ pub struct Text {
pub shaping: Shaping,
}
+impl Text {
+ /// Computes the [`Path`]s of the [`Text`] and draws them using
+ /// the given closure.
+ pub fn draw_with(&self, mut f: impl FnMut(Path, Color)) {
+ let mut font_system =
+ text::font_system().write().expect("Write font system");
+
+ let mut buffer = cosmic_text::BufferLine::new(
+ &self.content,
+ cosmic_text::AttrsList::new(text::to_attributes(self.font)),
+ text::to_shaping(self.shaping),
+ );
+
+ let layout = buffer.layout(
+ font_system.raw(),
+ self.size.0,
+ f32::MAX,
+ cosmic_text::Wrap::None,
+ );
+
+ let translation_x = match self.horizontal_alignment {
+ alignment::Horizontal::Left => self.position.x,
+ alignment::Horizontal::Center | alignment::Horizontal::Right => {
+ let mut line_width = 0.0f32;
+
+ for line in layout.iter() {
+ line_width = line_width.max(line.w);
+ }
+
+ if self.horizontal_alignment == alignment::Horizontal::Center {
+ self.position.x - line_width / 2.0
+ } else {
+ self.position.x - line_width
+ }
+ }
+ };
+
+ let translation_y = {
+ let line_height = self.line_height.to_absolute(self.size);
+
+ match self.vertical_alignment {
+ alignment::Vertical::Top => self.position.y,
+ alignment::Vertical::Center => {
+ self.position.y - line_height.0 / 2.0
+ }
+ alignment::Vertical::Bottom => self.position.y - line_height.0,
+ }
+ };
+
+ let mut swash_cache = cosmic_text::SwashCache::new();
+
+ for run in layout.iter() {
+ for glyph in run.glyphs.iter() {
+ let physical_glyph = glyph.physical((0.0, 0.0), 1.0);
+
+ let start_x = translation_x + glyph.x + glyph.x_offset;
+ let start_y = translation_y + glyph.y_offset + self.size.0;
+ let offset = Vector::new(start_x, start_y);
+
+ if let Some(commands) = swash_cache.get_outline_commands(
+ font_system.raw(),
+ physical_glyph.cache_key,
+ ) {
+ let glyph = Path::new(|path| {
+ use cosmic_text::Command;
+
+ for command in commands {
+ match command {
+ Command::MoveTo(p) => {
+ path.move_to(
+ Point::new(p.x, -p.y) + offset,
+ );
+ }
+ Command::LineTo(p) => {
+ path.line_to(
+ Point::new(p.x, -p.y) + offset,
+ );
+ }
+ Command::CurveTo(control_a, control_b, to) => {
+ path.bezier_curve_to(
+ Point::new(control_a.x, -control_a.y)
+ + offset,
+ Point::new(control_b.x, -control_b.y)
+ + offset,
+ Point::new(to.x, -to.y) + offset,
+ );
+ }
+ Command::QuadTo(control, to) => {
+ path.quadratic_curve_to(
+ Point::new(control.x, -control.y)
+ + offset,
+ Point::new(to.x, -to.y) + offset,
+ );
+ }
+ Command::Close => {
+ path.close();
+ }
+ }
+ }
+ });
+
+ f(glyph, self.color);
+ } else {
+ // TODO: Raster image support for `Canvas`
+ let [r, g, b, a] = self.color.into_rgba8();
+
+ swash_cache.with_pixels(
+ font_system.raw(),
+ physical_glyph.cache_key,
+ cosmic_text::Color::rgba(r, g, b, a),
+ |x, y, color| {
+ f(
+ Path::rectangle(
+ Point::new(x as f32, y as f32) + offset,
+ Size::new(1.0, 1.0),
+ ),
+ Color::from_rgba8(
+ color.r(),
+ color.g(),
+ color.b(),
+ color.a() as f32 / 255.0,
+ ),
+ );
+ },
+ );
+ }
+ }
+ }
+ }
+}
+
impl Default for Text {
fn default() -> Text {
Text {
diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs
index a0729058..76de56bf 100644
--- a/graphics/src/lib.rs
+++ b/graphics/src/lib.rs
@@ -10,7 +10,7 @@
#![forbid(rust_2018_idioms)]
#![deny(
missing_debug_implementations,
- //missing_docs,
+ missing_docs,
unsafe_code,
unused_results,
rustdoc::broken_intra_doc_links
@@ -50,3 +50,4 @@ pub use transformation::Transformation;
pub use viewport::Viewport;
pub use iced_core as core;
+pub use iced_futures as futures;
diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs
index 8a97e6e7..20affaaf 100644
--- a/graphics/src/primitive.rs
+++ b/graphics/src/primitive.rs
@@ -4,6 +4,7 @@ use crate::core::image;
use crate::core::svg;
use crate::core::text;
use crate::core::{Background, Color, Font, Pixels, Point, Rectangle, Vector};
+use crate::text::editor;
use crate::text::paragraph;
use std::sync::Arc;
@@ -13,24 +14,26 @@ use std::sync::Arc;
pub enum Primitive<T> {
/// A text primitive
Text {
- /// The contents of the text
+ /// The contents of the text.
content: String,
- /// The bounds of the text
+ /// The bounds of the text.
bounds: Rectangle,
- /// The color of the text
+ /// The color of the text.
color: Color,
- /// The size of the text in logical pixels
+ /// The size of the text in logical pixels.
size: Pixels,
- /// The line height of the text
+ /// The line height of the text.
line_height: text::LineHeight,
- /// The font of the text
+ /// The font of the text.
font: Font,
- /// The horizontal alignment of the text
+ /// The horizontal alignment of the text.
horizontal_alignment: alignment::Horizontal,
- /// The vertical alignment of the text
+ /// The vertical alignment of the text.
vertical_alignment: alignment::Vertical,
/// The shaping strategy of the text.
shaping: text::Shaping,
+ /// The clip bounds of the text.
+ clip_bounds: Rectangle,
},
/// A paragraph primitive
Paragraph {
@@ -40,7 +43,22 @@ pub enum Primitive<T> {
position: Point,
/// The color of the paragraph.
color: Color,
+ /// The clip bounds of the paragraph.
+ clip_bounds: Rectangle,
},
+ /// An editor primitive
+ Editor {
+ /// The [`editor::Weak`] reference.
+ editor: editor::Weak,
+ /// The position of the editor.
+ position: Point,
+ /// The color of the editor.
+ color: Color,
+ /// The clip bounds of the editor.
+ clip_bounds: Rectangle,
+ },
+ /// A raw `cosmic-text` primitive
+ RawText(crate::text::Raw),
/// A quad primitive
Quad {
/// The bounds of the quad
@@ -58,6 +76,8 @@ pub enum Primitive<T> {
Image {
/// The handle of the image
handle: image::Handle,
+ /// The filter method of the image
+ filter_method: image::FilterMethod,
/// The bounds of the image
bounds: Rectangle,
},
diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs
index a9d7895e..1b0f5c5b 100644
--- a/graphics/src/renderer.rs
+++ b/graphics/src/renderer.rs
@@ -141,6 +141,7 @@ where
{
type Font = Font;
type Paragraph = text::Paragraph;
+ type Editor = text::Editor;
const ICON_FONT: Font = Font::with_name("Iced-Icons");
const CHECKMARK_ICON: char = '\u{f00c}';
@@ -158,51 +159,33 @@ where
self.backend.load_font(bytes);
}
- fn create_paragraph(&self, text: Text<'_, Self::Font>) -> text::Paragraph {
- text::Paragraph::with_text(text, self.backend.font_system())
- }
-
- fn update_paragraph(
- &self,
- paragraph: &mut Self::Paragraph,
- text: Text<'_, Self::Font>,
- ) {
- let font_system = self.backend.font_system();
-
- if paragraph.version() != font_system.version() {
- // The font system has changed, paragraph fonts may be outdated
- *paragraph = self.create_paragraph(text);
- } else {
- match core::text::compare(paragraph, text) {
- core::text::Difference::None => {}
- core::text::Difference::Bounds => {
- self.resize_paragraph(paragraph, text.bounds);
- }
- core::text::Difference::Shape => {
- *paragraph = self.create_paragraph(text);
- }
- }
- }
- }
-
- fn resize_paragraph(
- &self,
- paragraph: &mut Self::Paragraph,
- new_bounds: Size,
- ) {
- paragraph.resize(new_bounds, self.backend.font_system());
- }
-
fn fill_paragraph(
&mut self,
paragraph: &Self::Paragraph,
position: Point,
color: Color,
+ clip_bounds: Rectangle,
) {
self.primitives.push(Primitive::Paragraph {
paragraph: paragraph.downgrade(),
position,
color,
+ clip_bounds,
+ });
+ }
+
+ fn fill_editor(
+ &mut self,
+ editor: &Self::Editor,
+ position: Point,
+ color: Color,
+ clip_bounds: Rectangle,
+ ) {
+ self.primitives.push(Primitive::Editor {
+ editor: editor.downgrade(),
+ position,
+ color,
+ clip_bounds,
});
}
@@ -211,6 +194,7 @@ where
text: Text<'_, Self::Font>,
position: Point,
color: Color,
+ clip_bounds: Rectangle,
) {
self.primitives.push(Primitive::Text {
content: text.content.to_string(),
@@ -222,6 +206,7 @@ where
horizontal_alignment: text.horizontal_alignment,
vertical_alignment: text.vertical_alignment,
shaping: text.shaping,
+ clip_bounds,
});
}
}
@@ -236,8 +221,17 @@ where
self.backend().dimensions(handle)
}
- fn draw(&mut self, handle: image::Handle, bounds: Rectangle) {
- self.primitives.push(Primitive::Image { handle, bounds });
+ fn draw(
+ &mut self,
+ handle: image::Handle,
+ filter_method: image::FilterMethod,
+ bounds: Rectangle,
+ ) {
+ self.primitives.push(Primitive::Image {
+ handle,
+ filter_method,
+ bounds,
+ });
}
}
diff --git a/graphics/src/text.rs b/graphics/src/text.rs
index bc06aa3c..7c4b5e31 100644
--- a/graphics/src/text.rs
+++ b/graphics/src/text.rs
@@ -1,69 +1,96 @@
+//! Draw text.
pub mod cache;
+pub mod editor;
pub mod paragraph;
pub use cache::Cache;
+pub use editor::Editor;
pub use paragraph::Paragraph;
pub use cosmic_text;
use crate::core::font::{self, Font};
use crate::core::text::Shaping;
-use crate::core::Size;
+use crate::core::{Color, Point, Rectangle, Size};
+use once_cell::sync::OnceCell;
use std::borrow::Cow;
-use std::sync::{self, Arc, RwLock};
+use std::sync::{Arc, RwLock, Weak};
+/// Returns the global [`FontSystem`].
+pub fn font_system() -> &'static RwLock<FontSystem> {
+ static FONT_SYSTEM: OnceCell<RwLock<FontSystem>> = OnceCell::new();
+
+ FONT_SYSTEM.get_or_init(|| {
+ RwLock::new(FontSystem {
+ raw: cosmic_text::FontSystem::new_with_fonts([
+ cosmic_text::fontdb::Source::Binary(Arc::new(
+ include_bytes!("../fonts/Iced-Icons.ttf").as_slice(),
+ )),
+ ]),
+ version: Version::default(),
+ })
+ })
+}
+
+/// A set of system fonts.
#[allow(missing_debug_implementations)]
pub struct FontSystem {
- raw: RwLock<cosmic_text::FontSystem>,
+ raw: cosmic_text::FontSystem,
version: Version,
}
impl FontSystem {
- pub fn new() -> Self {
- FontSystem {
- raw: RwLock::new(cosmic_text::FontSystem::new_with_fonts(
- [cosmic_text::fontdb::Source::Binary(Arc::new(
- include_bytes!("../fonts/Iced-Icons.ttf").as_slice(),
- ))]
- .into_iter(),
- )),
- version: Version::default(),
- }
- }
-
- pub fn get_mut(&mut self) -> &mut cosmic_text::FontSystem {
- self.raw.get_mut().expect("Lock font system")
- }
-
- pub fn write(
- &self,
- ) -> (sync::RwLockWriteGuard<'_, cosmic_text::FontSystem>, Version) {
- (self.raw.write().expect("Write font system"), self.version)
+ /// Returns the raw [`cosmic_text::FontSystem`].
+ pub fn raw(&mut self) -> &mut cosmic_text::FontSystem {
+ &mut self.raw
}
+ /// Loads a font from its bytes.
pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
- let _ = self.get_mut().db_mut().load_font_source(
+ let _ = self.raw.db_mut().load_font_source(
cosmic_text::fontdb::Source::Binary(Arc::new(bytes.into_owned())),
);
self.version = Version(self.version.0 + 1);
}
+ /// Returns the current [`Version`] of the [`FontSystem`].
+ ///
+ /// Loading a font will increase the version of a [`FontSystem`].
pub fn version(&self) -> Version {
self.version
}
}
+/// A version number.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct Version(u32);
-impl Default for FontSystem {
- fn default() -> Self {
- Self::new()
+/// A weak reference to a [`cosmic-text::Buffer`] that can be drawn.
+#[derive(Debug, Clone)]
+pub struct Raw {
+ /// A weak reference to a [`cosmic_text::Buffer`].
+ pub buffer: Weak<cosmic_text::Buffer>,
+ /// The position of the text.
+ pub position: Point,
+ /// The color of the text.
+ pub color: Color,
+ /// The clip bounds of the text.
+ pub clip_bounds: Rectangle,
+}
+
+impl PartialEq for Raw {
+ fn eq(&self, _other: &Self) -> bool {
+ // TODO: There is no proper way to compare raw buffers
+ // For now, no two instances of `Raw` text will be equal.
+ // This should be fine, but could trigger unnecessary redraws
+ // in the future.
+ false
}
}
+/// Measures the dimensions of the given [`cosmic_text::Buffer`].
pub fn measure(buffer: &cosmic_text::Buffer) -> Size {
let (width, total_lines) = buffer
.layout_runs()
@@ -71,9 +98,15 @@ pub fn measure(buffer: &cosmic_text::Buffer) -> Size {
(run.line_w.max(width), total_lines + 1)
});
- Size::new(width, total_lines as f32 * buffer.metrics().line_height)
+ let (max_width, max_height) = buffer.size();
+
+ Size::new(
+ width.min(max_width),
+ (total_lines as f32 * buffer.metrics().line_height).min(max_height),
+ )
}
+/// Returns the attributes of the given [`Font`].
pub fn to_attributes(font: Font) -> cosmic_text::Attrs<'static> {
cosmic_text::Attrs::new()
.family(to_family(font.family))
@@ -129,9 +162,17 @@ fn to_style(style: font::Style) -> cosmic_text::Style {
}
}
+/// Converts some [`Shaping`] strategy to a [`cosmic_text::Shaping`] strategy.
pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping {
match shaping {
Shaping::Basic => cosmic_text::Shaping::Basic,
Shaping::Advanced => cosmic_text::Shaping::Advanced,
}
}
+
+/// Converts some [`Color`] to a [`cosmic_text::Color`].
+pub fn to_color(color: Color) -> cosmic_text::Color {
+ let [r, g, b, a] = color.into_rgba8();
+
+ cosmic_text::Color::rgba(r, g, b, a)
+}
diff --git a/graphics/src/text/cache.rs b/graphics/src/text/cache.rs
index 577c4687..7fb33567 100644
--- a/graphics/src/text/cache.rs
+++ b/graphics/src/text/cache.rs
@@ -1,3 +1,4 @@
+//! Cache text.
use crate::core::{Font, Size};
use crate::text;
@@ -5,6 +6,7 @@ use rustc_hash::{FxHashMap, FxHashSet};
use std::collections::hash_map;
use std::hash::{BuildHasher, Hash, Hasher};
+/// A store of recently used sections of text.
#[allow(missing_debug_implementations)]
#[derive(Default)]
pub struct Cache {
@@ -14,21 +16,20 @@ pub struct Cache {
hasher: HashBuilder,
}
-#[cfg(not(target_arch = "wasm32"))]
-type HashBuilder = twox_hash::RandomXxHashBuilder64;
-
-#[cfg(target_arch = "wasm32")]
-type HashBuilder = std::hash::BuildHasherDefault<twox_hash::XxHash64>;
+type HashBuilder = xxhash_rust::xxh3::Xxh3Builder;
impl Cache {
+ /// Creates a new empty [`Cache`].
pub fn new() -> Self {
Self::default()
}
+ /// Gets the text [`Entry`] with the given [`KeyHash`].
pub fn get(&self, key: &KeyHash) -> Option<&Entry> {
self.entries.get(key)
}
+ /// Allocates a text [`Entry`] if it is not already present in the [`Cache`].
pub fn allocate(
&mut self,
font_system: &mut cosmic_text::FontSystem,
@@ -88,6 +89,9 @@ impl Cache {
(hash, self.entries.get_mut(&hash).unwrap())
}
+ /// Trims the [`Cache`].
+ ///
+ /// This will clear the sections of text that have not been used since the last `trim`.
pub fn trim(&mut self) {
self.entries
.retain(|key, _| self.recently_used.contains(key));
@@ -99,13 +103,20 @@ impl Cache {
}
}
+/// A cache key representing a section of text.
#[derive(Debug, Clone, Copy)]
pub struct Key<'a> {
+ /// The content of the text.
pub content: &'a str,
+ /// The size of the text.
pub size: f32,
+ /// The line height of the text.
pub line_height: f32,
+ /// The [`Font`] of the text.
pub font: Font,
+ /// The bounds of the text.
pub bounds: Size,
+ /// The shaping strategy of the text.
pub shaping: text::Shaping,
}
@@ -123,10 +134,14 @@ impl Key<'_> {
}
}
+/// The hash of a [`Key`].
pub type KeyHash = u64;
+/// A cache entry.
#[allow(missing_debug_implementations)]
pub struct Entry {
+ /// The buffer of text, ready for drawing.
pub buffer: cosmic_text::Buffer,
+ /// The minimum bounds of the text.
pub min_bounds: Size,
}
diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs
new file mode 100644
index 00000000..d5262ae8
--- /dev/null
+++ b/graphics/src/text/editor.rs
@@ -0,0 +1,779 @@
+//! Draw and edit text.
+use crate::core::text::editor::{
+ self, Action, Cursor, Direction, Edit, Motion,
+};
+use crate::core::text::highlighter::{self, Highlighter};
+use crate::core::text::LineHeight;
+use crate::core::{Font, Pixels, Point, Rectangle, Size};
+use crate::text;
+
+use cosmic_text::Edit as _;
+
+use std::fmt;
+use std::sync::{self, Arc};
+
+/// A multi-line text editor.
+#[derive(Debug, PartialEq)]
+pub struct Editor(Option<Arc<Internal>>);
+
+struct Internal {
+ editor: cosmic_text::Editor,
+ font: Font,
+ bounds: Size,
+ topmost_line_changed: Option<usize>,
+ version: text::Version,
+}
+
+impl Editor {
+ /// Creates a new empty [`Editor`].
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ /// Returns the buffer of the [`Editor`].
+ pub fn buffer(&self) -> &cosmic_text::Buffer {
+ self.internal().editor.buffer()
+ }
+
+ /// Creates a [`Weak`] reference to the [`Editor`].
+ ///
+ /// This is useful to avoid cloning the [`Editor`] when
+ /// referential guarantees are unnecessary. For instance,
+ /// when creating a rendering tree.
+ pub fn downgrade(&self) -> Weak {
+ let editor = self.internal();
+
+ Weak {
+ raw: Arc::downgrade(editor),
+ bounds: editor.bounds,
+ }
+ }
+
+ fn internal(&self) -> &Arc<Internal> {
+ self.0
+ .as_ref()
+ .expect("Editor should always be initialized")
+ }
+}
+
+impl editor::Editor for Editor {
+ type Font = Font;
+
+ fn with_text(text: &str) -> Self {
+ let mut buffer = cosmic_text::Buffer::new_empty(cosmic_text::Metrics {
+ font_size: 1.0,
+ line_height: 1.0,
+ });
+
+ let mut font_system =
+ text::font_system().write().expect("Write font system");
+
+ buffer.set_text(
+ font_system.raw(),
+ text,
+ cosmic_text::Attrs::new(),
+ cosmic_text::Shaping::Advanced,
+ );
+
+ Editor(Some(Arc::new(Internal {
+ editor: cosmic_text::Editor::new(buffer),
+ version: font_system.version(),
+ ..Default::default()
+ })))
+ }
+
+ fn line(&self, index: usize) -> Option<&str> {
+ self.buffer()
+ .lines
+ .get(index)
+ .map(cosmic_text::BufferLine::text)
+ }
+
+ fn line_count(&self) -> usize {
+ self.buffer().lines.len()
+ }
+
+ fn selection(&self) -> Option<String> {
+ self.internal().editor.copy_selection()
+ }
+
+ fn cursor(&self) -> editor::Cursor {
+ let internal = self.internal();
+
+ let cursor = internal.editor.cursor();
+ let buffer = internal.editor.buffer();
+
+ match internal.editor.select_opt() {
+ Some(selection) => {
+ let (start, end) = if cursor < selection {
+ (cursor, selection)
+ } else {
+ (selection, cursor)
+ };
+
+ let line_height = buffer.metrics().line_height;
+ let selected_lines = end.line - start.line + 1;
+
+ let visual_lines_offset =
+ visual_lines_offset(start.line, buffer);
+
+ let regions = buffer
+ .lines
+ .iter()
+ .skip(start.line)
+ .take(selected_lines)
+ .enumerate()
+ .flat_map(|(i, line)| {
+ highlight_line(
+ line,
+ if i == 0 { start.index } else { 0 },
+ if i == selected_lines - 1 {
+ end.index
+ } else {
+ line.text().len()
+ },
+ )
+ })
+ .enumerate()
+ .filter_map(|(visual_line, (x, width))| {
+ if width > 0.0 {
+ Some(Rectangle {
+ x,
+ width,
+ y: (visual_line as i32 + visual_lines_offset)
+ as f32
+ * line_height,
+ height: line_height,
+ })
+ } else {
+ None
+ }
+ })
+ .collect();
+
+ Cursor::Selection(regions)
+ }
+ _ => {
+ let line_height = buffer.metrics().line_height;
+
+ let visual_lines_offset =
+ visual_lines_offset(cursor.line, buffer);
+
+ let line = buffer
+ .lines
+ .get(cursor.line)
+ .expect("Cursor line should be present");
+
+ let layout = line
+ .layout_opt()
+ .as_ref()
+ .expect("Line layout should be cached");
+
+ let mut lines = layout.iter().enumerate();
+
+ let (visual_line, offset) = lines
+ .find_map(|(i, line)| {
+ let start = line
+ .glyphs
+ .first()
+ .map(|glyph| glyph.start)
+ .unwrap_or(0);
+ let end = line
+ .glyphs
+ .last()
+ .map(|glyph| glyph.end)
+ .unwrap_or(0);
+
+ let is_cursor_before_start = start > cursor.index;
+
+ let is_cursor_before_end = match cursor.affinity {
+ cosmic_text::Affinity::Before => {
+ cursor.index <= end
+ }
+ cosmic_text::Affinity::After => cursor.index < end,
+ };
+
+ if is_cursor_before_start {
+ // Sometimes, the glyph we are looking for is right
+ // between lines. This can happen when a line wraps
+ // on a space.
+ // In that case, we can assume the cursor is at the
+ // end of the previous line.
+ // i is guaranteed to be > 0 because `start` is always
+ // 0 for the first line, so there is no way for the
+ // cursor to be before it.
+ Some((i - 1, layout[i - 1].w))
+ } else if is_cursor_before_end {
+ let offset = line
+ .glyphs
+ .iter()
+ .take_while(|glyph| cursor.index > glyph.start)
+ .map(|glyph| glyph.w)
+ .sum();
+
+ Some((i, offset))
+ } else {
+ None
+ }
+ })
+ .unwrap_or((
+ layout.len().saturating_sub(1),
+ layout.last().map(|line| line.w).unwrap_or(0.0),
+ ));
+
+ Cursor::Caret(Point::new(
+ offset,
+ (visual_lines_offset + visual_line as i32) as f32
+ * line_height,
+ ))
+ }
+ }
+ }
+
+ fn cursor_position(&self) -> (usize, usize) {
+ let cursor = self.internal().editor.cursor();
+
+ (cursor.line, cursor.index)
+ }
+
+ fn perform(&mut self, action: Action) {
+ let mut font_system =
+ text::font_system().write().expect("Write font system");
+
+ let editor =
+ self.0.take().expect("Editor should always be initialized");
+
+ // TODO: Handle multiple strong references somehow
+ let mut internal = Arc::try_unwrap(editor)
+ .expect("Editor cannot have multiple strong references");
+
+ let editor = &mut internal.editor;
+
+ match action {
+ // Motion events
+ Action::Move(motion) => {
+ if let Some(selection) = editor.select_opt() {
+ let cursor = editor.cursor();
+
+ let (left, right) = if cursor < selection {
+ (cursor, selection)
+ } else {
+ (selection, cursor)
+ };
+
+ editor.set_select_opt(None);
+
+ match motion {
+ // These motions are performed as-is even when a selection
+ // is present
+ Motion::Home
+ | Motion::End
+ | Motion::DocumentStart
+ | Motion::DocumentEnd => {
+ editor.action(
+ font_system.raw(),
+ motion_to_action(motion),
+ );
+ }
+ // Other motions simply move the cursor to one end of the selection
+ _ => editor.set_cursor(match motion.direction() {
+ Direction::Left => left,
+ Direction::Right => right,
+ }),
+ }
+ } else {
+ editor.action(font_system.raw(), motion_to_action(motion));
+ }
+ }
+
+ // Selection events
+ Action::Select(motion) => {
+ let cursor = editor.cursor();
+
+ if editor.select_opt().is_none() {
+ editor.set_select_opt(Some(cursor));
+ }
+
+ editor.action(font_system.raw(), motion_to_action(motion));
+
+ // Deselect if selection matches cursor position
+ if let Some(selection) = editor.select_opt() {
+ let cursor = editor.cursor();
+
+ if cursor.line == selection.line
+ && cursor.index == selection.index
+ {
+ editor.set_select_opt(None);
+ }
+ }
+ }
+ Action::SelectWord => {
+ use unicode_segmentation::UnicodeSegmentation;
+
+ let cursor = editor.cursor();
+
+ if let Some(line) = editor.buffer().lines.get(cursor.line) {
+ let (start, end) =
+ UnicodeSegmentation::unicode_word_indices(line.text())
+ // Split words with dots
+ .flat_map(|(i, word)| {
+ word.split('.').scan(i, |current, word| {
+ let start = *current;
+ *current += word.len() + 1;
+
+ Some((start, word))
+ })
+ })
+ // Turn words into ranges
+ .map(|(i, word)| (i, i + word.len()))
+ // Find the word at cursor
+ .find(|&(start, end)| {
+ start <= cursor.index && cursor.index < end
+ })
+ // Cursor is not in a word. Let's select its punctuation cluster.
+ .unwrap_or_else(|| {
+ let start = line.text()[..cursor.index]
+ .char_indices()
+ .rev()
+ .take_while(|(_, c)| {
+ c.is_ascii_punctuation()
+ })
+ .map(|(i, _)| i)
+ .last()
+ .unwrap_or(cursor.index);
+
+ let end = line.text()[cursor.index..]
+ .char_indices()
+ .skip_while(|(_, c)| {
+ c.is_ascii_punctuation()
+ })
+ .map(|(i, _)| i + cursor.index)
+ .next()
+ .unwrap_or(cursor.index);
+
+ (start, end)
+ });
+
+ if start != end {
+ editor.set_cursor(cosmic_text::Cursor {
+ index: start,
+ ..cursor
+ });
+
+ editor.set_select_opt(Some(cosmic_text::Cursor {
+ index: end,
+ ..cursor
+ }));
+ }
+ }
+ }
+ Action::SelectLine => {
+ let cursor = editor.cursor();
+
+ if let Some(line_length) = editor
+ .buffer()
+ .lines
+ .get(cursor.line)
+ .map(|line| line.text().len())
+ {
+ editor
+ .set_cursor(cosmic_text::Cursor { index: 0, ..cursor });
+
+ editor.set_select_opt(Some(cosmic_text::Cursor {
+ index: line_length,
+ ..cursor
+ }));
+ }
+ }
+
+ // Editing events
+ Action::Edit(edit) => {
+ match edit {
+ Edit::Insert(c) => {
+ editor.action(
+ font_system.raw(),
+ cosmic_text::Action::Insert(c),
+ );
+ }
+ Edit::Paste(text) => {
+ editor.insert_string(&text, None);
+ }
+ Edit::Enter => {
+ editor.action(
+ font_system.raw(),
+ cosmic_text::Action::Enter,
+ );
+ }
+ Edit::Backspace => {
+ editor.action(
+ font_system.raw(),
+ cosmic_text::Action::Backspace,
+ );
+ }
+ Edit::Delete => {
+ editor.action(
+ font_system.raw(),
+ cosmic_text::Action::Delete,
+ );
+ }
+ }
+
+ let cursor = editor.cursor();
+ let selection = editor.select_opt().unwrap_or(cursor);
+
+ internal.topmost_line_changed =
+ Some(cursor.min(selection).line);
+ }
+
+ // Mouse events
+ Action::Click(position) => {
+ editor.action(
+ font_system.raw(),
+ cosmic_text::Action::Click {
+ x: position.x as i32,
+ y: position.y as i32,
+ },
+ );
+ }
+ Action::Drag(position) => {
+ editor.action(
+ font_system.raw(),
+ cosmic_text::Action::Drag {
+ x: position.x as i32,
+ y: position.y as i32,
+ },
+ );
+
+ // Deselect if selection matches cursor position
+ if let Some(selection) = editor.select_opt() {
+ let cursor = editor.cursor();
+
+ if cursor.line == selection.line
+ && cursor.index == selection.index
+ {
+ editor.set_select_opt(None);
+ }
+ }
+ }
+ Action::Scroll { lines } => {
+ editor.action(
+ font_system.raw(),
+ cosmic_text::Action::Scroll { lines },
+ );
+ }
+ }
+
+ self.0 = Some(Arc::new(internal));
+ }
+
+ fn bounds(&self) -> Size {
+ self.internal().bounds
+ }
+
+ fn update(
+ &mut self,
+ new_bounds: Size,
+ new_font: Font,
+ new_size: Pixels,
+ new_line_height: LineHeight,
+ new_highlighter: &mut impl Highlighter,
+ ) {
+ let editor =
+ self.0.take().expect("Editor should always be initialized");
+
+ let mut internal = Arc::try_unwrap(editor)
+ .expect("Editor cannot have multiple strong references");
+
+ let mut font_system =
+ text::font_system().write().expect("Write font system");
+
+ if font_system.version() != internal.version {
+ log::trace!("Updating `FontSystem` of `Editor`...");
+
+ for line in internal.editor.buffer_mut().lines.iter_mut() {
+ line.reset();
+ }
+
+ internal.version = font_system.version();
+ internal.topmost_line_changed = Some(0);
+ }
+
+ if new_font != internal.font {
+ log::trace!("Updating font of `Editor`...");
+
+ for line in internal.editor.buffer_mut().lines.iter_mut() {
+ let _ = line.set_attrs_list(cosmic_text::AttrsList::new(
+ text::to_attributes(new_font),
+ ));
+ }
+
+ internal.font = new_font;
+ internal.topmost_line_changed = Some(0);
+ }
+
+ let metrics = internal.editor.buffer().metrics();
+ let new_line_height = new_line_height.to_absolute(new_size);
+
+ if new_size.0 != metrics.font_size
+ || new_line_height.0 != metrics.line_height
+ {
+ log::trace!("Updating `Metrics` of `Editor`...");
+
+ internal.editor.buffer_mut().set_metrics(
+ font_system.raw(),
+ cosmic_text::Metrics::new(new_size.0, new_line_height.0),
+ );
+ }
+
+ if new_bounds != internal.bounds {
+ log::trace!("Updating size of `Editor`...");
+
+ internal.editor.buffer_mut().set_size(
+ font_system.raw(),
+ new_bounds.width,
+ new_bounds.height,
+ );
+
+ internal.bounds = new_bounds;
+ }
+
+ if let Some(topmost_line_changed) = internal.topmost_line_changed.take()
+ {
+ log::trace!(
+ "Notifying highlighter of line change: {topmost_line_changed}"
+ );
+
+ new_highlighter.change_line(topmost_line_changed);
+ }
+
+ internal.editor.shape_as_needed(font_system.raw());
+
+ self.0 = Some(Arc::new(internal));
+ }
+
+ fn highlight<H: Highlighter>(
+ &mut self,
+ font: Self::Font,
+ highlighter: &mut H,
+ format_highlight: impl Fn(&H::Highlight) -> highlighter::Format<Self::Font>,
+ ) {
+ let internal = self.internal();
+ let buffer = internal.editor.buffer();
+
+ let mut window = buffer.scroll() + buffer.visible_lines();
+
+ let last_visible_line = buffer
+ .lines
+ .iter()
+ .enumerate()
+ .find_map(|(i, line)| {
+ let visible_lines = line
+ .layout_opt()
+ .as_ref()
+ .expect("Line layout should be cached")
+ .len() as i32;
+
+ if window > visible_lines {
+ window -= visible_lines;
+ None
+ } else {
+ Some(i)
+ }
+ })
+ .unwrap_or(buffer.lines.len().saturating_sub(1));
+
+ let current_line = highlighter.current_line();
+
+ if current_line > last_visible_line {
+ return;
+ }
+
+ let editor =
+ self.0.take().expect("Editor should always be initialized");
+
+ let mut internal = Arc::try_unwrap(editor)
+ .expect("Editor cannot have multiple strong references");
+
+ let mut font_system =
+ text::font_system().write().expect("Write font system");
+
+ let attributes = text::to_attributes(font);
+
+ for line in &mut internal.editor.buffer_mut().lines
+ [current_line..=last_visible_line]
+ {
+ let mut list = cosmic_text::AttrsList::new(attributes);
+
+ for (range, highlight) in highlighter.highlight_line(line.text()) {
+ let format = format_highlight(&highlight);
+
+ if format.color.is_some() || format.font.is_some() {
+ list.add_span(
+ range,
+ cosmic_text::Attrs {
+ color_opt: format.color.map(text::to_color),
+ ..if let Some(font) = format.font {
+ text::to_attributes(font)
+ } else {
+ attributes
+ }
+ },
+ );
+ }
+ }
+
+ let _ = line.set_attrs_list(list);
+ }
+
+ internal.editor.shape_as_needed(font_system.raw());
+
+ self.0 = Some(Arc::new(internal));
+ }
+}
+
+impl Default for Editor {
+ fn default() -> Self {
+ Self(Some(Arc::new(Internal::default())))
+ }
+}
+
+impl PartialEq for Internal {
+ fn eq(&self, other: &Self) -> bool {
+ self.font == other.font
+ && self.bounds == other.bounds
+ && self.editor.buffer().metrics() == other.editor.buffer().metrics()
+ }
+}
+
+impl Default for Internal {
+ fn default() -> Self {
+ Self {
+ editor: cosmic_text::Editor::new(cosmic_text::Buffer::new_empty(
+ cosmic_text::Metrics {
+ font_size: 1.0,
+ line_height: 1.0,
+ },
+ )),
+ font: Font::default(),
+ bounds: Size::ZERO,
+ topmost_line_changed: None,
+ version: text::Version::default(),
+ }
+ }
+}
+
+impl fmt::Debug for Internal {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("Internal")
+ .field("font", &self.font)
+ .field("bounds", &self.bounds)
+ .finish()
+ }
+}
+
+/// A weak reference to an [`Editor`].
+#[derive(Debug, Clone)]
+pub struct Weak {
+ raw: sync::Weak<Internal>,
+ /// The bounds of the [`Editor`].
+ pub bounds: Size,
+}
+
+impl Weak {
+ /// Tries to update the reference into an [`Editor`].
+ pub fn upgrade(&self) -> Option<Editor> {
+ self.raw.upgrade().map(Some).map(Editor)
+ }
+}
+
+impl PartialEq for Weak {
+ fn eq(&self, other: &Self) -> bool {
+ match (self.raw.upgrade(), other.raw.upgrade()) {
+ (Some(p1), Some(p2)) => p1 == p2,
+ _ => false,
+ }
+ }
+}
+
+fn highlight_line(
+ line: &cosmic_text::BufferLine,
+ from: usize,
+ to: usize,
+) -> impl Iterator<Item = (f32, f32)> + '_ {
+ let layout = line
+ .layout_opt()
+ .as_ref()
+ .expect("Line layout should be cached");
+
+ layout.iter().map(move |visual_line| {
+ let start = visual_line
+ .glyphs
+ .first()
+ .map(|glyph| glyph.start)
+ .unwrap_or(0);
+ let end = visual_line
+ .glyphs
+ .last()
+ .map(|glyph| glyph.end)
+ .unwrap_or(0);
+
+ let range = start.max(from)..end.min(to);
+
+ if range.is_empty() {
+ (0.0, 0.0)
+ } else if range.start == start && range.end == end {
+ (0.0, visual_line.w)
+ } else {
+ let first_glyph = visual_line
+ .glyphs
+ .iter()
+ .position(|glyph| range.start <= glyph.start)
+ .unwrap_or(0);
+
+ let mut glyphs = visual_line.glyphs.iter();
+
+ let x =
+ glyphs.by_ref().take(first_glyph).map(|glyph| glyph.w).sum();
+
+ let width: f32 = glyphs
+ .take_while(|glyph| range.end > glyph.start)
+ .map(|glyph| glyph.w)
+ .sum();
+
+ (x, width)
+ }
+ })
+}
+
+fn visual_lines_offset(line: usize, buffer: &cosmic_text::Buffer) -> i32 {
+ let visual_lines_before_start: usize = buffer
+ .lines
+ .iter()
+ .take(line)
+ .map(|line| {
+ line.layout_opt()
+ .as_ref()
+ .expect("Line layout should be cached")
+ .len()
+ })
+ .sum();
+
+ visual_lines_before_start as i32 - buffer.scroll()
+}
+
+fn motion_to_action(motion: Motion) -> cosmic_text::Action {
+ match motion {
+ Motion::Left => cosmic_text::Action::Left,
+ Motion::Right => cosmic_text::Action::Right,
+ Motion::Up => cosmic_text::Action::Up,
+ Motion::Down => cosmic_text::Action::Down,
+ Motion::WordLeft => cosmic_text::Action::LeftWord,
+ Motion::WordRight => cosmic_text::Action::RightWord,
+ Motion::Home => cosmic_text::Action::Home,
+ Motion::End => cosmic_text::Action::End,
+ Motion::PageUp => cosmic_text::Action::PageUp,
+ Motion::PageDown => cosmic_text::Action::PageDown,
+ Motion::DocumentStart => cosmic_text::Action::BufferStart,
+ Motion::DocumentEnd => cosmic_text::Action::BufferEnd,
+ }
+}
diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs
index e4350cff..5d027542 100644
--- a/graphics/src/text/paragraph.rs
+++ b/graphics/src/text/paragraph.rs
@@ -1,12 +1,14 @@
+//! Draw paragraphs.
use crate::core;
use crate::core::alignment;
use crate::core::text::{Hit, LineHeight, Shaping, Text};
use crate::core::{Font, Pixels, Point, Size};
-use crate::text::{self, FontSystem};
+use crate::text;
use std::fmt;
use std::sync::{self, Arc};
+/// A bunch of text.
#[derive(Clone, PartialEq)]
pub struct Paragraph(Option<Arc<Internal>>);
@@ -23,17 +25,50 @@ struct Internal {
}
impl Paragraph {
+ /// Creates a new empty [`Paragraph`].
pub fn new() -> Self {
Self::default()
}
- pub fn with_text(text: Text<'_, Font>, font_system: &FontSystem) -> Self {
+ /// Returns the buffer of the [`Paragraph`].
+ pub fn buffer(&self) -> &cosmic_text::Buffer {
+ &self.internal().buffer
+ }
+
+ /// Creates a [`Weak`] reference to the [`Paragraph`].
+ ///
+ /// This is useful to avoid cloning the [`Paragraph`] when
+ /// referential guarantees are unnecessary. For instance,
+ /// when creating a rendering tree.
+ pub fn downgrade(&self) -> Weak {
+ let paragraph = self.internal();
+
+ Weak {
+ raw: Arc::downgrade(paragraph),
+ min_bounds: paragraph.min_bounds,
+ horizontal_alignment: paragraph.horizontal_alignment,
+ vertical_alignment: paragraph.vertical_alignment,
+ }
+ }
+
+ fn internal(&self) -> &Arc<Internal> {
+ self.0
+ .as_ref()
+ .expect("paragraph should always be initialized")
+ }
+}
+
+impl core::text::Paragraph for Paragraph {
+ type Font = Font;
+
+ fn with_text(text: Text<'_, Font>) -> Self {
log::trace!("Allocating paragraph: {}", text.content);
- let (mut font_system, version) = font_system.write();
+ let mut font_system =
+ text::font_system().write().expect("Write font system");
let mut buffer = cosmic_text::Buffer::new(
- &mut font_system,
+ font_system.raw(),
cosmic_text::Metrics::new(
text.size.into(),
text.line_height.to_absolute(text.size).into(),
@@ -41,13 +76,13 @@ impl Paragraph {
);
buffer.set_size(
- &mut font_system,
+ font_system.raw(),
text.bounds.width,
text.bounds.height,
);
buffer.set_text(
- &mut font_system,
+ font_system.raw(),
text.content,
text::to_attributes(text.font),
text::to_shaping(text.shaping),
@@ -64,30 +99,11 @@ impl Paragraph {
shaping: text.shaping,
bounds: text.bounds,
min_bounds,
- version,
+ version: font_system.version(),
})))
}
- pub fn buffer(&self) -> &cosmic_text::Buffer {
- &self.internal().buffer
- }
-
- pub fn version(&self) -> text::Version {
- self.internal().version
- }
-
- pub fn downgrade(&self) -> Weak {
- let paragraph = self.internal();
-
- Weak {
- raw: Arc::downgrade(paragraph),
- min_bounds: paragraph.min_bounds,
- horizontal_alignment: paragraph.horizontal_alignment,
- vertical_alignment: paragraph.vertical_alignment,
- }
- }
-
- pub fn resize(&mut self, new_bounds: Size, font_system: &FontSystem) {
+ fn resize(&mut self, new_bounds: Size) {
let paragraph = self
.0
.take()
@@ -95,10 +111,11 @@ impl Paragraph {
match Arc::try_unwrap(paragraph) {
Ok(mut internal) => {
- let (mut font_system, _) = font_system.write();
+ let mut font_system =
+ text::font_system().write().expect("Write font system");
internal.buffer.set_size(
- &mut font_system,
+ font_system.raw(),
new_bounds.width,
new_bounds.height,
);
@@ -113,55 +130,42 @@ impl Paragraph {
// If there is a strong reference somewhere, we recompute the
// buffer from scratch
- *self = Self::with_text(
- Text {
- content: &internal.content,
- bounds: internal.bounds,
- size: Pixels(metrics.font_size),
- line_height: LineHeight::Absolute(Pixels(
- metrics.line_height,
- )),
- font: internal.font,
- horizontal_alignment: internal.horizontal_alignment,
- vertical_alignment: internal.vertical_alignment,
- shaping: internal.shaping,
- },
- font_system,
- );
+ *self = Self::with_text(Text {
+ content: &internal.content,
+ bounds: internal.bounds,
+ size: Pixels(metrics.font_size),
+ line_height: LineHeight::Absolute(Pixels(
+ metrics.line_height,
+ )),
+ font: internal.font,
+ horizontal_alignment: internal.horizontal_alignment,
+ vertical_alignment: internal.vertical_alignment,
+ shaping: internal.shaping,
+ });
}
}
}
- fn internal(&self) -> &Arc<Internal> {
- self.0
- .as_ref()
- .expect("paragraph should always be initialized")
- }
-}
-
-impl core::text::Paragraph for Paragraph {
- type Font = Font;
-
- fn content(&self) -> &str {
- &self.internal().content
- }
-
- fn text_size(&self) -> Pixels {
- Pixels(self.internal().buffer.metrics().font_size)
- }
-
- fn line_height(&self) -> LineHeight {
- LineHeight::Absolute(Pixels(
- self.internal().buffer.metrics().line_height,
- ))
- }
-
- fn font(&self) -> Font {
- self.internal().font
- }
-
- fn shaping(&self) -> Shaping {
- self.internal().shaping
+ fn compare(&self, text: Text<'_, Font>) -> core::text::Difference {
+ let font_system = text::font_system().read().expect("Read font system");
+ let paragraph = self.internal();
+ let metrics = paragraph.buffer.metrics();
+
+ if paragraph.version != font_system.version
+ || paragraph.content != text.content
+ || metrics.font_size != text.size.0
+ || metrics.line_height != text.line_height.to_absolute(text.size).0
+ || paragraph.font != text.font
+ || paragraph.shaping != text.shaping
+ || paragraph.horizontal_alignment != text.horizontal_alignment
+ || paragraph.vertical_alignment != text.vertical_alignment
+ {
+ core::text::Difference::Shape
+ } else if paragraph.bounds != text.bounds {
+ core::text::Difference::Bounds
+ } else {
+ core::text::Difference::None
+ }
}
fn horizontal_alignment(&self) -> alignment::Horizontal {
@@ -172,10 +176,6 @@ impl core::text::Paragraph for Paragraph {
self.internal().vertical_alignment
}
- fn bounds(&self) -> Size {
- self.internal().bounds
- }
-
fn min_bounds(&self) -> Size {
self.internal().min_bounds
}
@@ -187,38 +187,43 @@ impl core::text::Paragraph for Paragraph {
}
fn grapheme_position(&self, line: usize, index: usize) -> Option<Point> {
+ use unicode_segmentation::UnicodeSegmentation;
+
let run = self.internal().buffer.layout_runs().nth(line)?;
// index represents a grapheme, not a glyph
// Let's find the first glyph for the given grapheme cluster
let mut last_start = None;
+ let mut last_grapheme_count = 0;
let mut graphemes_seen = 0;
let glyph = run
.glyphs
.iter()
.find(|glyph| {
- if graphemes_seen == index {
- return true;
- }
-
if Some(glyph.start) != last_start {
+ last_grapheme_count = run.text[glyph.start..glyph.end]
+ .graphemes(false)
+ .count();
last_start = Some(glyph.start);
- graphemes_seen += 1;
+ graphemes_seen += last_grapheme_count;
}
- false
+ graphemes_seen >= index
})
.or_else(|| run.glyphs.last())?;
- let advance_last = if index == run.glyphs.len() {
- glyph.w
- } else {
+ let advance = if index == 0 {
0.0
+ } else {
+ glyph.w
+ * (1.0
+ - graphemes_seen.saturating_sub(index) as f32
+ / last_grapheme_count.max(1) as f32)
};
Some(Point::new(
- glyph.x + glyph.x_offset * glyph.font_size + advance_last,
+ glyph.x + glyph.x_offset * glyph.font_size + advance,
glyph.y - glyph.y_offset * glyph.font_size,
))
}
@@ -278,15 +283,20 @@ impl Default for Internal {
}
}
+/// A weak reference to a [`Paragraph`].
#[derive(Debug, Clone)]
pub struct Weak {
raw: sync::Weak<Internal>,
+ /// The minimum bounds of the [`Paragraph`].
pub min_bounds: Size,
+ /// The horizontal alignment of the [`Paragraph`].
pub horizontal_alignment: alignment::Horizontal,
+ /// The vertical alignment of the [`Paragraph`].
pub vertical_alignment: alignment::Vertical,
}
impl Weak {
+ /// Tries to update the reference into a [`Paragraph`].
pub fn upgrade(&self) -> Option<Paragraph> {
self.raw.upgrade().map(Some).map(Paragraph)
}
diff --git a/highlighter/Cargo.toml b/highlighter/Cargo.toml
new file mode 100644
index 00000000..2d108d6f
--- /dev/null
+++ b/highlighter/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "iced_highlighter"
+description = "A syntax highlighter for iced"
+version.workspace = true
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+homepage.workspace = true
+categories.workspace = true
+keywords.workspace = true
+
+[dependencies]
+iced_core.workspace = true
+
+once_cell.workspace = true
+syntect.workspace = true
diff --git a/highlighter/src/lib.rs b/highlighter/src/lib.rs
new file mode 100644
index 00000000..63f21fc0
--- /dev/null
+++ b/highlighter/src/lib.rs
@@ -0,0 +1,245 @@
+use iced_core as core;
+
+use crate::core::text::highlighter::{self, Format};
+use crate::core::{Color, Font};
+
+use once_cell::sync::Lazy;
+use std::ops::Range;
+use syntect::highlighting;
+use syntect::parsing;
+
+static SYNTAXES: Lazy<parsing::SyntaxSet> =
+ Lazy::new(parsing::SyntaxSet::load_defaults_nonewlines);
+
+static THEMES: Lazy<highlighting::ThemeSet> =
+ Lazy::new(highlighting::ThemeSet::load_defaults);
+
+const LINES_PER_SNAPSHOT: usize = 50;
+
+pub struct Highlighter {
+ syntax: &'static parsing::SyntaxReference,
+ highlighter: highlighting::Highlighter<'static>,
+ caches: Vec<(parsing::ParseState, parsing::ScopeStack)>,
+ current_line: usize,
+}
+
+impl highlighter::Highlighter for Highlighter {
+ type Settings = Settings;
+ type Highlight = Highlight;
+
+ type Iterator<'a> =
+ Box<dyn Iterator<Item = (Range<usize>, Self::Highlight)> + 'a>;
+
+ fn new(settings: &Self::Settings) -> Self {
+ let syntax = SYNTAXES
+ .find_syntax_by_token(&settings.extension)
+ .unwrap_or_else(|| SYNTAXES.find_syntax_plain_text());
+
+ let highlighter = highlighting::Highlighter::new(
+ &THEMES.themes[settings.theme.key()],
+ );
+
+ let parser = parsing::ParseState::new(syntax);
+ let stack = parsing::ScopeStack::new();
+
+ Highlighter {
+ syntax,
+ highlighter,
+ caches: vec![(parser, stack)],
+ current_line: 0,
+ }
+ }
+
+ fn update(&mut self, new_settings: &Self::Settings) {
+ self.syntax = SYNTAXES
+ .find_syntax_by_token(&new_settings.extension)
+ .unwrap_or_else(|| SYNTAXES.find_syntax_plain_text());
+
+ self.highlighter = highlighting::Highlighter::new(
+ &THEMES.themes[new_settings.theme.key()],
+ );
+
+ // Restart the highlighter
+ self.change_line(0);
+ }
+
+ fn change_line(&mut self, line: usize) {
+ let snapshot = line / LINES_PER_SNAPSHOT;
+
+ if snapshot <= self.caches.len() {
+ self.caches.truncate(snapshot);
+ self.current_line = snapshot * LINES_PER_SNAPSHOT;
+ } else {
+ self.caches.truncate(1);
+ self.current_line = 0;
+ }
+
+ let (parser, stack) =
+ self.caches.last().cloned().unwrap_or_else(|| {
+ (
+ parsing::ParseState::new(self.syntax),
+ parsing::ScopeStack::new(),
+ )
+ });
+
+ self.caches.push((parser, stack));
+ }
+
+ fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_> {
+ if self.current_line / LINES_PER_SNAPSHOT >= self.caches.len() {
+ let (parser, stack) =
+ self.caches.last().expect("Caches must not be empty");
+
+ self.caches.push((parser.clone(), stack.clone()));
+ }
+
+ self.current_line += 1;
+
+ let (parser, stack) =
+ self.caches.last_mut().expect("Caches must not be empty");
+
+ let ops = parser.parse_line(line, &SYNTAXES).unwrap_or_default();
+
+ let highlighter = &self.highlighter;
+
+ Box::new(
+ ScopeRangeIterator {
+ ops,
+ line_length: line.len(),
+ index: 0,
+ last_str_index: 0,
+ }
+ .filter_map(move |(range, scope)| {
+ let _ = stack.apply(&scope);
+
+ if range.is_empty() {
+ None
+ } else {
+ Some((
+ range,
+ Highlight(
+ highlighter.style_mod_for_stack(&stack.scopes),
+ ),
+ ))
+ }
+ }),
+ )
+ }
+
+ fn current_line(&self) -> usize {
+ self.current_line
+ }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct Settings {
+ pub theme: Theme,
+ pub extension: String,
+}
+
+pub struct Highlight(highlighting::StyleModifier);
+
+impl Highlight {
+ pub fn color(&self) -> Option<Color> {
+ self.0.foreground.map(|color| {
+ Color::from_rgba8(color.r, color.g, color.b, color.a as f32 / 255.0)
+ })
+ }
+
+ pub fn font(&self) -> Option<Font> {
+ None
+ }
+
+ pub fn to_format(&self) -> Format<Font> {
+ Format {
+ color: self.color(),
+ font: self.font(),
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Theme {
+ SolarizedDark,
+ Base16Mocha,
+ Base16Ocean,
+ Base16Eighties,
+ InspiredGitHub,
+}
+
+impl Theme {
+ pub const ALL: &'static [Self] = &[
+ Self::SolarizedDark,
+ Self::Base16Mocha,
+ Self::Base16Ocean,
+ Self::Base16Eighties,
+ Self::InspiredGitHub,
+ ];
+
+ pub fn is_dark(self) -> bool {
+ match self {
+ Self::SolarizedDark
+ | Self::Base16Mocha
+ | Self::Base16Ocean
+ | Self::Base16Eighties => true,
+ Self::InspiredGitHub => false,
+ }
+ }
+
+ fn key(self) -> &'static str {
+ match self {
+ Theme::SolarizedDark => "Solarized (dark)",
+ Theme::Base16Mocha => "base16-mocha.dark",
+ Theme::Base16Ocean => "base16-ocean.dark",
+ Theme::Base16Eighties => "base16-eighties.dark",
+ Theme::InspiredGitHub => "InspiredGitHub",
+ }
+ }
+}
+
+impl std::fmt::Display for Theme {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Theme::SolarizedDark => write!(f, "Solarized Dark"),
+ Theme::Base16Mocha => write!(f, "Mocha"),
+ Theme::Base16Ocean => write!(f, "Ocean"),
+ Theme::Base16Eighties => write!(f, "Eighties"),
+ Theme::InspiredGitHub => write!(f, "Inspired GitHub"),
+ }
+ }
+}
+
+pub struct ScopeRangeIterator {
+ ops: Vec<(usize, parsing::ScopeStackOp)>,
+ line_length: usize,
+ index: usize,
+ last_str_index: usize,
+}
+
+impl Iterator for ScopeRangeIterator {
+ type Item = (std::ops::Range<usize>, parsing::ScopeStackOp);
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.index > self.ops.len() {
+ return None;
+ }
+
+ let next_str_i = if self.index == self.ops.len() {
+ self.line_length
+ } else {
+ self.ops[self.index].0
+ };
+
+ let range = self.last_str_index..next_str_i;
+ self.last_str_index = next_str_i;
+
+ let op = if self.index == 0 {
+ parsing::ScopeStackOp::Noop
+ } else {
+ self.ops[self.index - 1].1.clone()
+ };
+
+ self.index += 1;
+ Some((range, op))
+ }
+}
diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml
index 56e17209..a159978c 100644
--- a/renderer/Cargo.toml
+++ b/renderer/Cargo.toml
@@ -27,5 +27,4 @@ iced_wgpu.workspace = true
iced_wgpu.optional = true
log.workspace = true
-raw-window-handle.workspace = true
thiserror.workspace = true
diff --git a/renderer/src/backend.rs b/renderer/src/backend.rs
deleted file mode 100644
index 3f229b52..00000000
--- a/renderer/src/backend.rs
+++ /dev/null
@@ -1,100 +0,0 @@
-use crate::core::text;
-use crate::core::{Font, Point, Size};
-use crate::graphics::backend;
-
-use std::borrow::Cow;
-
-#[allow(clippy::large_enum_variant)]
-pub enum Backend {
- TinySkia(iced_tiny_skia::Backend),
- #[cfg(feature = "wgpu")]
- Wgpu(iced_wgpu::Backend),
-}
-
-macro_rules! delegate {
- ($backend:expr, $name:ident, $body:expr) => {
- match $backend {
- Self::TinySkia($name) => $body,
- #[cfg(feature = "wgpu")]
- Self::Wgpu($name) => $body,
- }
- };
-}
-
-impl backend::Text for Backend {
- const ICON_FONT: Font = Font::with_name("Iced-Icons");
- const CHECKMARK_ICON: char = '\u{f00c}';
- const ARROW_DOWN_ICON: char = '\u{e800}';
-
- fn default_font(&self) -> Font {
- delegate!(self, backend, backend.default_font())
- }
-
- fn default_size(&self) -> f32 {
- delegate!(self, backend, backend.default_size())
- }
-
- fn measure(
- &self,
- contents: &str,
- size: f32,
- line_height: text::LineHeight,
- font: Font,
- bounds: Size,
- shaping: text::Shaping,
- ) -> Size {
- delegate!(
- self,
- backend,
- backend.measure(contents, size, line_height, font, bounds, shaping)
- )
- }
-
- fn hit_test(
- &self,
- contents: &str,
- size: f32,
- line_height: text::LineHeight,
- font: Font,
- bounds: Size,
- shaping: text::Shaping,
- position: Point,
- nearest_only: bool,
- ) -> Option<text::Hit> {
- delegate!(
- self,
- backend,
- backend.hit_test(
- contents,
- size,
- line_height,
- font,
- bounds,
- shaping,
- position,
- nearest_only,
- )
- )
- }
-
- fn load_font(&mut self, font: Cow<'static, [u8]>) {
- delegate!(self, backend, backend.load_font(font));
- }
-}
-
-#[cfg(feature = "image")]
-impl backend::Image for Backend {
- fn dimensions(&self, handle: &crate::core::image::Handle) -> Size<u32> {
- delegate!(self, backend, backend.dimensions(handle))
- }
-}
-
-#[cfg(feature = "svg")]
-impl backend::Svg for Backend {
- fn viewport_dimensions(
- &self,
- handle: &crate::core::svg::Handle,
- ) -> Size<u32> {
- delegate!(self, backend, backend.viewport_dimensions(handle))
- }
-}
diff --git a/renderer/src/compositor.rs b/renderer/src/compositor.rs
index d1500089..f10ed048 100644
--- a/renderer/src/compositor.rs
+++ b/renderer/src/compositor.rs
@@ -1,9 +1,8 @@
use crate::core::Color;
-use crate::graphics::compositor::{Information, SurfaceError};
+use crate::graphics::compositor::{Information, SurfaceError, Window};
use crate::graphics::{Error, Viewport};
use crate::{Renderer, Settings};
-use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle};
use std::env;
pub enum Compositor<Theme> {
@@ -15,7 +14,7 @@ pub enum Compositor<Theme> {
pub enum Surface {
TinySkia(iced_tiny_skia::window::Surface),
#[cfg(feature = "wgpu")]
- Wgpu(iced_wgpu::window::Surface),
+ Wgpu(iced_wgpu::window::Surface<'static>),
}
impl<Theme> crate::graphics::Compositor for Compositor<Theme> {
@@ -23,20 +22,18 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> {
type Renderer = Renderer<Theme>;
type Surface = Surface;
- fn new<W: HasRawWindowHandle + HasRawDisplayHandle>(
+ fn new<W: Window + Clone>(
settings: Self::Settings,
- compatible_window: Option<&W>,
- ) -> Result<(Self, Self::Renderer), Error> {
+ compatible_window: W,
+ ) -> Result<Self, Error> {
let candidates =
Candidate::list_from_env().unwrap_or(Candidate::default_list());
let mut error = Error::GraphicsAdapterNotFound;
for candidate in candidates {
- match candidate.build(settings, compatible_window) {
- Ok((compositor, renderer)) => {
- return Ok((compositor, renderer))
- }
+ match candidate.build(settings, compatible_window.clone()) {
+ Ok(compositor) => return Ok(compositor),
Err(new_error) => {
error = new_error;
}
@@ -46,9 +43,21 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> {
Err(error)
}
- fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>(
+ fn create_renderer(&self) -> Self::Renderer {
+ match self {
+ Compositor::TinySkia(compositor) => {
+ Renderer::TinySkia(compositor.create_renderer())
+ }
+ #[cfg(feature = "wgpu")]
+ Compositor::Wgpu(compositor) => {
+ Renderer::Wgpu(compositor.create_renderer())
+ }
+ }
+ }
+
+ fn create_surface<W: Window + Clone>(
&mut self,
- window: &W,
+ window: W,
width: u32,
height: u32,
) -> Surface {
@@ -216,28 +225,26 @@ impl Candidate {
)
}
- fn build<Theme, W: HasRawWindowHandle + HasRawDisplayHandle>(
+ fn build<Theme, W: Window>(
self,
settings: Settings,
- _compatible_window: Option<&W>,
- ) -> Result<(Compositor<Theme>, Renderer<Theme>), Error> {
+ _compatible_window: W,
+ ) -> Result<Compositor<Theme>, Error> {
match self {
Self::TinySkia => {
- let (compositor, backend) =
- iced_tiny_skia::window::compositor::new();
+ let compositor = iced_tiny_skia::window::compositor::new(
+ iced_tiny_skia::Settings {
+ default_font: settings.default_font,
+ default_text_size: settings.default_text_size,
+ },
+ _compatible_window,
+ );
- Ok((
- Compositor::TinySkia(compositor),
- Renderer::TinySkia(iced_tiny_skia::Renderer::new(
- backend,
- settings.default_font,
- settings.default_text_size,
- )),
- ))
+ Ok(Compositor::TinySkia(compositor))
}
#[cfg(feature = "wgpu")]
Self::Wgpu => {
- let (compositor, backend) = iced_wgpu::window::compositor::new(
+ let compositor = iced_wgpu::window::compositor::new(
iced_wgpu::Settings {
default_font: settings.default_font,
default_text_size: settings.default_text_size,
@@ -247,14 +254,7 @@ impl Candidate {
_compatible_window,
)?;
- Ok((
- Compositor::Wgpu(compositor),
- Renderer::Wgpu(iced_wgpu::Renderer::new(
- backend,
- settings.default_font,
- settings.default_text_size,
- )),
- ))
+ Ok(Compositor::Wgpu(compositor))
}
#[cfg(not(feature = "wgpu"))]
Self::Wgpu => {
diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs
index 7594d532..f2acfa00 100644
--- a/renderer/src/lib.rs
+++ b/renderer/src/lib.rs
@@ -1,6 +1,9 @@
#![forbid(rust_2018_idioms)]
#![deny(unsafe_code, unused_results, rustdoc::broken_intra_doc_links)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
+#[cfg(feature = "wgpu")]
+pub use iced_wgpu as wgpu;
+
pub mod compositor;
#[cfg(feature = "geometry")]
@@ -19,9 +22,8 @@ pub use geometry::Geometry;
use crate::core::renderer;
use crate::core::text::{self, Text};
-use crate::core::{
- Background, Color, Font, Pixels, Point, Rectangle, Size, Vector,
-};
+use crate::core::{Background, Color, Font, Pixels, Point, Rectangle, Vector};
+use crate::graphics::text::Editor;
use crate::graphics::text::Paragraph;
use crate::graphics::Mesh;
@@ -149,6 +151,7 @@ impl<T> core::Renderer for Renderer<T> {
impl<T> text::Renderer for Renderer<T> {
type Font = Font;
type Paragraph = Paragraph;
+ type Editor = Editor;
const ICON_FONT: Font = iced_tiny_skia::Renderer::<T>::ICON_FONT;
const CHECKMARK_ICON: char = iced_tiny_skia::Renderer::<T>::CHECKMARK_ICON;
@@ -163,36 +166,35 @@ impl<T> text::Renderer for Renderer<T> {
delegate!(self, renderer, renderer.default_size())
}
- fn create_paragraph(&self, text: Text<'_, Self::Font>) -> Self::Paragraph {
- delegate!(self, renderer, renderer.create_paragraph(text))
+ fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
+ delegate!(self, renderer, renderer.load_font(bytes));
}
- fn resize_paragraph(
- &self,
- paragraph: &mut Self::Paragraph,
- new_bounds: Size,
+ fn fill_paragraph(
+ &mut self,
+ paragraph: &Self::Paragraph,
+ position: Point,
+ color: Color,
+ clip_bounds: Rectangle,
) {
delegate!(
self,
renderer,
- renderer.resize_paragraph(paragraph, new_bounds)
+ renderer.fill_paragraph(paragraph, position, color, clip_bounds)
);
}
- fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
- delegate!(self, renderer, renderer.load_font(bytes));
- }
-
- fn fill_paragraph(
+ fn fill_editor(
&mut self,
- text: &Self::Paragraph,
+ editor: &Self::Editor,
position: Point,
color: Color,
+ clip_bounds: Rectangle,
) {
delegate!(
self,
renderer,
- renderer.fill_paragraph(text, position, color)
+ renderer.fill_editor(editor, position, color, clip_bounds)
);
}
@@ -201,8 +203,13 @@ impl<T> text::Renderer for Renderer<T> {
text: Text<'_, Self::Font>,
position: Point,
color: Color,
+ clip_bounds: Rectangle,
) {
- delegate!(self, renderer, renderer.fill_text(text, position, color));
+ delegate!(
+ self,
+ renderer,
+ renderer.fill_text(text, position, color, clip_bounds)
+ );
}
}
@@ -210,18 +217,26 @@ impl<T> text::Renderer for Renderer<T> {
impl<T> crate::core::image::Renderer for Renderer<T> {
type Handle = crate::core::image::Handle;
- fn dimensions(&self, handle: &crate::core::image::Handle) -> Size<u32> {
+ fn dimensions(
+ &self,
+ handle: &crate::core::image::Handle,
+ ) -> core::Size<u32> {
delegate!(self, renderer, renderer.dimensions(handle))
}
- fn draw(&mut self, handle: crate::core::image::Handle, bounds: Rectangle) {
- delegate!(self, renderer, renderer.draw(handle, bounds));
+ fn draw(
+ &mut self,
+ handle: crate::core::image::Handle,
+ filter_method: crate::core::image::FilterMethod,
+ bounds: Rectangle,
+ ) {
+ delegate!(self, renderer, renderer.draw(handle, filter_method, bounds));
}
}
#[cfg(feature = "svg")]
impl<T> crate::core::svg::Renderer for Renderer<T> {
- fn dimensions(&self, handle: &crate::core::svg::Handle) -> Size<u32> {
+ fn dimensions(&self, handle: &crate::core::svg::Handle) -> core::Size<u32> {
delegate!(self, renderer, renderer.dimensions(handle))
}
@@ -247,6 +262,7 @@ impl<T> crate::graphics::geometry::Renderer for Renderer<T> {
crate::Geometry::TinySkia(primitive) => {
renderer.draw_primitive(primitive);
}
+ #[cfg(feature = "wgpu")]
crate::Geometry::Wgpu(_) => unreachable!(),
}
}
@@ -265,3 +281,23 @@ impl<T> crate::graphics::geometry::Renderer for Renderer<T> {
}
}
}
+
+#[cfg(feature = "wgpu")]
+impl<T> iced_wgpu::primitive::pipeline::Renderer for Renderer<T> {
+ fn draw_pipeline_primitive(
+ &mut self,
+ bounds: Rectangle,
+ primitive: impl wgpu::primitive::pipeline::Primitive,
+ ) {
+ match self {
+ Self::TinySkia(_renderer) => {
+ log::warn!(
+ "Custom shader primitive is unavailable with tiny-skia."
+ );
+ }
+ Self::Wgpu(renderer) => {
+ renderer.draw_pipeline_primitive(bounds, primitive);
+ }
+ }
+ }
+}
diff --git a/renderer/src/widget.rs b/renderer/src/widget.rs
deleted file mode 100644
index 6c0c2a83..00000000
--- a/renderer/src/widget.rs
+++ /dev/null
@@ -1,11 +0,0 @@
-#[cfg(feature = "canvas")]
-pub mod canvas;
-
-#[cfg(feature = "canvas")]
-pub use canvas::Canvas;
-
-#[cfg(feature = "qr_code")]
-pub mod qr_code;
-
-#[cfg(feature = "qr_code")]
-pub use qr_code::QRCode;
diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml
index d19aedd3..8089d545 100644
--- a/runtime/Cargo.toml
+++ b/runtime/Cargo.toml
@@ -12,6 +12,7 @@ keywords.workspace = true
[features]
debug = []
+multi-window = []
[dependencies]
iced_core.workspace = true
diff --git a/runtime/src/command.rs b/runtime/src/command.rs
index cd4c51ff..f70da915 100644
--- a/runtime/src/command.rs
+++ b/runtime/src/command.rs
@@ -4,8 +4,11 @@ mod action;
pub use action::Action;
use crate::core::widget;
+use crate::futures::futures;
use crate::futures::MaybeSend;
+use futures::channel::mpsc;
+use futures::Stream;
use std::fmt;
use std::future::Future;
@@ -40,14 +43,24 @@ impl<T> Command<T> {
/// Creates a [`Command`] that performs the action of the given future.
pub fn perform<A>(
- future: impl Future<Output = T> + 'static + MaybeSend,
- f: impl FnOnce(T) -> A + 'static + MaybeSend,
- ) -> Command<A> {
- use iced_futures::futures::FutureExt;
+ future: impl Future<Output = A> + 'static + MaybeSend,
+ f: impl FnOnce(A) -> T + 'static + MaybeSend,
+ ) -> Command<T> {
+ use futures::FutureExt;
Command::single(Action::Future(Box::pin(future.map(f))))
}
+ /// Creates a [`Command`] that runs the given stream to completion.
+ pub fn run<A>(
+ stream: impl Stream<Item = A> + 'static + MaybeSend,
+ f: impl Fn(A) -> T + 'static + MaybeSend,
+ ) -> Command<T> {
+ use futures::StreamExt;
+
+ Command::single(Action::Stream(Box::pin(stream.map(f))))
+ }
+
/// Creates a [`Command`] that performs the actions of all the given
/// commands.
///
@@ -106,3 +119,23 @@ impl<T> fmt::Debug for Command<T> {
command.fmt(f)
}
}
+
+/// Creates a [`Command`] that produces the `Message`s published from a [`Future`]
+/// to an [`mpsc::Sender`] with the given bounds.
+pub fn channel<Fut, Message>(
+ size: usize,
+ f: impl FnOnce(mpsc::Sender<Message>) -> Fut + MaybeSend + 'static,
+) -> Command<Message>
+where
+ Fut: Future<Output = ()> + MaybeSend + 'static,
+ Message: 'static + MaybeSend,
+{
+ use futures::future;
+ use futures::stream::{self, StreamExt};
+
+ let (sender, receiver) = mpsc::channel(size);
+
+ let runner = stream::once(f(sender)).filter_map(|_| future::ready(None));
+
+ Command::single(Action::Stream(Box::pin(stream::select(receiver, runner))))
+}
diff --git a/runtime/src/command/action.rs b/runtime/src/command/action.rs
index 6c74f0ef..cb0936df 100644
--- a/runtime/src/command/action.rs
+++ b/runtime/src/command/action.rs
@@ -18,6 +18,11 @@ pub enum Action<T> {
/// [`Future`]: iced_futures::BoxFuture
Future(iced_futures::BoxFuture<T>),
+ /// Run a [`Stream`] to completion.
+ ///
+ /// [`Stream`]: iced_futures::BoxStream
+ Stream(iced_futures::BoxStream<T>),
+
/// Run a clipboard action.
Clipboard(clipboard::Action<T>),
@@ -52,10 +57,11 @@ impl<T> Action<T> {
A: 'static,
T: 'static,
{
- use iced_futures::futures::FutureExt;
+ use iced_futures::futures::{FutureExt, StreamExt};
match self {
Self::Future(future) => Action::Future(Box::pin(future.map(f))),
+ Self::Stream(stream) => Action::Stream(Box::pin(stream.map(f))),
Self::Clipboard(action) => Action::Clipboard(action.map(f)),
Self::Window(window) => Action::Window(window.map(f)),
Self::System(system) => Action::System(system.map(f)),
@@ -74,10 +80,13 @@ impl<T> fmt::Debug for Action<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Future(_) => write!(f, "Action::Future"),
+ Self::Stream(_) => write!(f, "Action::Stream"),
Self::Clipboard(action) => {
write!(f, "Action::Clipboard({action:?})")
}
- Self::Window(action) => write!(f, "Action::Window({action:?})"),
+ Self::Window(action) => {
+ write!(f, "Action::Window({action:?})")
+ }
Self::System(action) => write!(f, "Action::System({action:?})"),
Self::Widget(_action) => write!(f, "Action::Widget"),
Self::LoadFont { .. } => write!(f, "Action::LoadFont"),
diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs
index 29e94d65..03906f45 100644
--- a/runtime/src/lib.rs
+++ b/runtime/src/lib.rs
@@ -26,6 +26,9 @@ pub mod system;
pub mod user_interface;
pub mod window;
+#[cfg(feature = "multi-window")]
+pub mod multi_window;
+
// We disable debug capabilities on release builds unless the `debug` feature
// is explicitly enabled.
#[cfg(feature = "debug")]
diff --git a/runtime/src/multi_window.rs b/runtime/src/multi_window.rs
new file mode 100644
index 00000000..cf778a20
--- /dev/null
+++ b/runtime/src/multi_window.rs
@@ -0,0 +1,6 @@
+//! A multi-window application.
+pub mod program;
+pub mod state;
+
+pub use program::Program;
+pub use state::State;
diff --git a/runtime/src/multi_window/program.rs b/runtime/src/multi_window/program.rs
new file mode 100644
index 00000000..591b3e9a
--- /dev/null
+++ b/runtime/src/multi_window/program.rs
@@ -0,0 +1,32 @@
+//! Build interactive programs using The Elm Architecture.
+use crate::core::text;
+use crate::core::window;
+use crate::core::{Element, Renderer};
+use crate::Command;
+
+/// The core of a user interface for a multi-window application following The Elm Architecture.
+pub trait Program: Sized {
+ /// The graphics backend to use to draw the [`Program`].
+ type Renderer: Renderer + text::Renderer;
+
+ /// The type of __messages__ your [`Program`] will produce.
+ type Message: std::fmt::Debug + Send;
+
+ /// Handles a __message__ and updates the state of the [`Program`].
+ ///
+ /// This is where you define your __update logic__. All the __messages__,
+ /// produced by either user interactions or commands, will be handled by
+ /// this method.
+ ///
+ /// Any [`Command`] returned will be executed immediately in the
+ /// background by shells.
+ fn update(&mut self, message: Self::Message) -> Command<Self::Message>;
+
+ /// Returns the widgets to display in the [`Program`] for the `window`.
+ ///
+ /// These widgets can produce __messages__ based on user interaction.
+ fn view(
+ &self,
+ window: window::Id,
+ ) -> Element<'_, Self::Message, Self::Renderer>;
+}
diff --git a/runtime/src/multi_window/state.rs b/runtime/src/multi_window/state.rs
new file mode 100644
index 00000000..49f72c39
--- /dev/null
+++ b/runtime/src/multi_window/state.rs
@@ -0,0 +1,280 @@
+//! The internal state of a multi-window [`Program`].
+use crate::core::event::{self, Event};
+use crate::core::mouse;
+use crate::core::renderer;
+use crate::core::widget::operation::{self, Operation};
+use crate::core::{Clipboard, Size};
+use crate::user_interface::{self, UserInterface};
+use crate::{Command, Debug, Program};
+
+/// The execution state of a multi-window [`Program`]. It leverages caching, event
+/// processing, and rendering primitive storage.
+#[allow(missing_debug_implementations)]
+pub struct State<P>
+where
+ P: Program + 'static,
+{
+ program: P,
+ caches: Option<Vec<user_interface::Cache>>,
+ queued_events: Vec<Event>,
+ queued_messages: Vec<P::Message>,
+ mouse_interaction: mouse::Interaction,
+}
+
+impl<P> State<P>
+where
+ P: Program + 'static,
+{
+ /// Creates a new [`State`] with the provided [`Program`], initializing its
+ /// primitive with the given logical bounds and renderer.
+ pub fn new(
+ program: P,
+ bounds: Size,
+ renderer: &mut P::Renderer,
+ debug: &mut Debug,
+ ) -> Self {
+ let user_interface = build_user_interface(
+ &program,
+ user_interface::Cache::default(),
+ renderer,
+ bounds,
+ debug,
+ );
+
+ let caches = Some(vec![user_interface.into_cache()]);
+
+ State {
+ program,
+ caches,
+ queued_events: Vec::new(),
+ queued_messages: Vec::new(),
+ mouse_interaction: mouse::Interaction::Idle,
+ }
+ }
+
+ /// Returns a reference to the [`Program`] of the [`State`].
+ pub fn program(&self) -> &P {
+ &self.program
+ }
+
+ /// Queues an event in the [`State`] for processing during an [`update`].
+ ///
+ /// [`update`]: Self::update
+ pub fn queue_event(&mut self, event: Event) {
+ self.queued_events.push(event);
+ }
+
+ /// Queues a message in the [`State`] for processing during an [`update`].
+ ///
+ /// [`update`]: Self::update
+ pub fn queue_message(&mut self, message: P::Message) {
+ self.queued_messages.push(message);
+ }
+
+ /// Returns whether the event queue of the [`State`] is empty or not.
+ pub fn is_queue_empty(&self) -> bool {
+ self.queued_events.is_empty() && self.queued_messages.is_empty()
+ }
+
+ /// Returns the current [`mouse::Interaction`] of the [`State`].
+ pub fn mouse_interaction(&self) -> mouse::Interaction {
+ self.mouse_interaction
+ }
+
+ /// Processes all the queued events and messages, rebuilding and redrawing
+ /// the widgets of the linked [`Program`] if necessary.
+ ///
+ /// Returns a list containing the instances of [`Event`] that were not
+ /// captured by any widget, and the [`Command`] obtained from [`Program`]
+ /// after updating it, only if an update was necessary.
+ pub fn update(
+ &mut self,
+ bounds: Size,
+ cursor: mouse::Cursor,
+ renderer: &mut P::Renderer,
+ theme: &<P::Renderer as iced_core::Renderer>::Theme,
+ style: &renderer::Style,
+ clipboard: &mut dyn Clipboard,
+ debug: &mut Debug,
+ ) -> (Vec<Event>, Option<Command<P::Message>>) {
+ let mut user_interfaces = build_user_interfaces(
+ &self.program,
+ self.caches.take().unwrap(),
+ renderer,
+ bounds,
+ debug,
+ );
+
+ debug.event_processing_started();
+ let mut messages = Vec::new();
+
+ let uncaptured_events = user_interfaces.iter_mut().fold(
+ vec![],
+ |mut uncaptured_events, ui| {
+ let (_, event_statuses) = ui.update(
+ &self.queued_events,
+ cursor,
+ renderer,
+ clipboard,
+ &mut messages,
+ );
+
+ uncaptured_events.extend(
+ self.queued_events
+ .iter()
+ .zip(event_statuses)
+ .filter_map(|(event, status)| {
+ matches!(status, event::Status::Ignored)
+ .then_some(event)
+ })
+ .cloned(),
+ );
+ uncaptured_events
+ },
+ );
+
+ self.queued_events.clear();
+ messages.append(&mut self.queued_messages);
+ debug.event_processing_finished();
+
+ let commands = if messages.is_empty() {
+ debug.draw_started();
+
+ for ui in &mut user_interfaces {
+ self.mouse_interaction =
+ ui.draw(renderer, theme, style, cursor);
+ }
+
+ debug.draw_finished();
+
+ self.caches = Some(
+ user_interfaces
+ .drain(..)
+ .map(UserInterface::into_cache)
+ .collect(),
+ );
+
+ None
+ } else {
+ let temp_caches = user_interfaces
+ .drain(..)
+ .map(UserInterface::into_cache)
+ .collect();
+
+ drop(user_interfaces);
+
+ let commands = Command::batch(messages.into_iter().map(|msg| {
+ debug.log_message(&msg);
+
+ debug.update_started();
+ let command = self.program.update(msg);
+ debug.update_finished();
+
+ command
+ }));
+
+ let mut user_interfaces = build_user_interfaces(
+ &self.program,
+ temp_caches,
+ renderer,
+ bounds,
+ debug,
+ );
+
+ debug.draw_started();
+ for ui in &mut user_interfaces {
+ self.mouse_interaction =
+ ui.draw(renderer, theme, style, cursor);
+ }
+ debug.draw_finished();
+
+ self.caches = Some(
+ user_interfaces
+ .drain(..)
+ .map(UserInterface::into_cache)
+ .collect(),
+ );
+
+ Some(commands)
+ };
+
+ (uncaptured_events, commands)
+ }
+
+ /// Applies widget [`Operation`]s to the [`State`].
+ pub fn operate(
+ &mut self,
+ renderer: &mut P::Renderer,
+ operations: impl Iterator<Item = Box<dyn Operation<P::Message>>>,
+ bounds: Size,
+ debug: &mut Debug,
+ ) {
+ let mut user_interfaces = build_user_interfaces(
+ &self.program,
+ self.caches.take().unwrap(),
+ renderer,
+ bounds,
+ debug,
+ );
+
+ for operation in operations {
+ let mut current_operation = Some(operation);
+
+ while let Some(mut operation) = current_operation.take() {
+ for ui in &mut user_interfaces {
+ ui.operate(renderer, operation.as_mut());
+ }
+
+ match operation.finish() {
+ operation::Outcome::None => {}
+ operation::Outcome::Some(message) => {
+ self.queued_messages.push(message);
+ }
+ operation::Outcome::Chain(next) => {
+ current_operation = Some(next);
+ }
+ };
+ }
+ }
+
+ self.caches = Some(
+ user_interfaces
+ .drain(..)
+ .map(UserInterface::into_cache)
+ .collect(),
+ );
+ }
+}
+
+fn build_user_interfaces<'a, P: Program>(
+ program: &'a P,
+ mut caches: Vec<user_interface::Cache>,
+ renderer: &mut P::Renderer,
+ size: Size,
+ debug: &mut Debug,
+) -> Vec<UserInterface<'a, P::Message, P::Renderer>> {
+ caches
+ .drain(..)
+ .map(|cache| {
+ build_user_interface(program, cache, renderer, size, debug)
+ })
+ .collect()
+}
+
+fn build_user_interface<'a, P: Program>(
+ program: &'a P,
+ cache: user_interface::Cache,
+ renderer: &mut P::Renderer,
+ size: Size,
+ debug: &mut Debug,
+) -> UserInterface<'a, P::Message, P::Renderer> {
+ debug.view_started();
+ let view = program.view();
+ debug.view_finished();
+
+ debug.layout_started();
+ let user_interface = UserInterface::build(view, size, cache, renderer);
+ debug.layout_finished();
+
+ user_interface
+}
diff --git a/runtime/src/overlay/nested.rs b/runtime/src/overlay/nested.rs
index 062ccc72..4256efb7 100644
--- a/runtime/src/overlay/nested.rs
+++ b/runtime/src/overlay/nested.rs
@@ -4,7 +4,9 @@ use crate::core::mouse;
use crate::core::overlay;
use crate::core::renderer;
use crate::core::widget;
-use crate::core::{Clipboard, Event, Layout, Point, Rectangle, Shell, Size};
+use crate::core::{
+ Clipboard, Event, Layout, Point, Rectangle, Shell, Size, Vector,
+};
/// An overlay container that displays nested overlays
#[allow(missing_debug_implementations)]
@@ -33,19 +35,18 @@ where
&mut self,
renderer: &Renderer,
bounds: Size,
- position: Point,
+ _position: Point,
+ translation: Vector,
) -> layout::Node {
fn recurse<Message, Renderer>(
element: &mut overlay::Element<'_, Message, Renderer>,
renderer: &Renderer,
bounds: Size,
- position: Point,
+ translation: Vector,
) -> layout::Node
where
Renderer: renderer::Renderer,
{
- let translation = position - Point::ORIGIN;
-
let node = element.layout(renderer, bounds, translation);
if let Some(mut nested) =
@@ -55,7 +56,7 @@ where
node.size(),
vec![
node,
- recurse(&mut nested, renderer, bounds, position),
+ recurse(&mut nested, renderer, bounds, translation),
],
)
} else {
@@ -63,7 +64,7 @@ where
}
}
- recurse(&mut self.overlay, renderer, bounds, position)
+ recurse(&mut self.overlay, renderer, bounds, translation)
}
/// Draws the [`Nested`] overlay using the associated `Renderer`.
diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs
index dae9e0ac..3594ac18 100644
--- a/runtime/src/user_interface.rs
+++ b/runtime/src/user_interface.rs
@@ -5,7 +5,9 @@ use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget;
use crate::core::window;
-use crate::core::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size};
+use crate::core::{
+ Clipboard, Element, Layout, Point, Rectangle, Shell, Size, Vector,
+};
use crate::overlay;
/// A set of interactive graphical elements with a specific [`Layout`].
@@ -199,7 +201,8 @@ where
let bounds = self.bounds;
let mut overlay = manual_overlay.as_mut().unwrap();
- let mut layout = overlay.layout(renderer, bounds, Point::ORIGIN);
+ let mut layout =
+ overlay.layout(renderer, bounds, Point::ORIGIN, Vector::ZERO);
let mut event_statuses = Vec::new();
for event in events.iter().cloned() {
@@ -253,8 +256,12 @@ where
overlay = manual_overlay.as_mut().unwrap();
shell.revalidate_layout(|| {
- layout =
- overlay.layout(renderer, bounds, Point::ORIGIN);
+ layout = overlay.layout(
+ renderer,
+ bounds,
+ Point::ORIGIN,
+ Vector::ZERO,
+ );
});
}
@@ -448,7 +455,12 @@ where
.map(overlay::Nested::new)
{
let overlay_layout = self.overlay.take().unwrap_or_else(|| {
- overlay.layout(renderer, self.bounds, Point::ORIGIN)
+ overlay.layout(
+ renderer,
+ self.bounds,
+ Point::ORIGIN,
+ Vector::ZERO,
+ )
});
let cursor = if cursor
@@ -566,8 +578,12 @@ where
.map(overlay::Nested::new)
{
if self.overlay.is_none() {
- self.overlay =
- Some(overlay.layout(renderer, self.bounds, Point::ORIGIN));
+ self.overlay = Some(overlay.layout(
+ renderer,
+ self.bounds,
+ Point::ORIGIN,
+ Vector::ZERO,
+ ));
}
overlay.operate(
diff --git a/runtime/src/window.rs b/runtime/src/window.rs
index 41816967..2136d64d 100644
--- a/runtime/src/window.rs
+++ b/runtime/src/window.rs
@@ -8,95 +8,135 @@ pub use screenshot::Screenshot;
use crate::command::{self, Command};
use crate::core::time::Instant;
-use crate::core::window::{Event, Icon, Level, Mode, UserAttention};
-use crate::core::Size;
+use crate::core::window::{
+ Event, Icon, Id, Level, Mode, Settings, UserAttention,
+};
+use crate::core::{Point, Size};
use crate::futures::event;
use crate::futures::Subscription;
/// Subscribes to the frames of the window of the running application.
///
/// The resulting [`Subscription`] will produce items at a rate equal to the
-/// refresh rate of the window. Note that this rate may be variable, as it is
+/// refresh rate of the first application window. Note that this rate may be variable, as it is
/// normally managed by the graphics driver and/or the OS.
///
/// In any case, this [`Subscription`] is useful to smoothly draw application-driven
/// animations without missing any frames.
pub fn frames() -> Subscription<Instant> {
event::listen_raw(|event, _status| match event {
- iced_core::Event::Window(Event::RedrawRequested(at)) => Some(at),
+ crate::core::Event::Window(_, Event::RedrawRequested(at)) => Some(at),
_ => None,
})
}
-/// Closes the current window and exits the application.
-pub fn close<Message>() -> Command<Message> {
- Command::single(command::Action::Window(Action::Close))
+/// Spawns a new window with the given `settings`.
+///
+/// Returns the new window [`Id`] alongside the [`Command`].
+pub fn spawn<Message>(settings: Settings) -> (Id, Command<Message>) {
+ let id = Id::unique();
+
+ (
+ id,
+ Command::single(command::Action::Window(Action::Spawn(id, settings))),
+ )
+}
+
+/// Closes the window with `id`.
+pub fn close<Message>(id: Id) -> Command<Message> {
+ Command::single(command::Action::Window(Action::Close(id)))
}
/// Begins dragging the window while the left mouse button is held.
-pub fn drag<Message>() -> Command<Message> {
- Command::single(command::Action::Window(Action::Drag))
+pub fn drag<Message>(id: Id) -> Command<Message> {
+ Command::single(command::Action::Window(Action::Drag(id)))
}
/// Resizes the window to the given logical dimensions.
-pub fn resize<Message>(new_size: Size<u32>) -> Command<Message> {
- Command::single(command::Action::Window(Action::Resize(new_size)))
+pub fn resize<Message>(id: Id, new_size: Size) -> Command<Message> {
+ Command::single(command::Action::Window(Action::Resize(id, new_size)))
}
-/// Fetches the current window size in logical dimensions.
+/// Fetches the window's size in logical dimensions.
pub fn fetch_size<Message>(
- f: impl FnOnce(Size<u32>) -> Message + 'static,
+ id: Id,
+ f: impl FnOnce(Size) -> Message + 'static,
) -> Command<Message> {
- Command::single(command::Action::Window(Action::FetchSize(Box::new(f))))
+ Command::single(command::Action::Window(Action::FetchSize(id, Box::new(f))))
+}
+
+/// Fetches if the window is maximized.
+pub fn fetch_maximized<Message>(
+ id: Id,
+ f: impl FnOnce(bool) -> Message + 'static,
+) -> Command<Message> {
+ Command::single(command::Action::Window(Action::FetchMaximized(
+ id,
+ Box::new(f),
+ )))
}
/// Maximizes the window.
-pub fn maximize<Message>(maximized: bool) -> Command<Message> {
- Command::single(command::Action::Window(Action::Maximize(maximized)))
+pub fn maximize<Message>(id: Id, maximized: bool) -> Command<Message> {
+ Command::single(command::Action::Window(Action::Maximize(id, maximized)))
+}
+
+/// Fetches if the window is minimized.
+pub fn fetch_minimized<Message>(
+ id: Id,
+ f: impl FnOnce(Option<bool>) -> Message + 'static,
+) -> Command<Message> {
+ Command::single(command::Action::Window(Action::FetchMinimized(
+ id,
+ Box::new(f),
+ )))
}
-/// Minimes the window.
-pub fn minimize<Message>(minimized: bool) -> Command<Message> {
- Command::single(command::Action::Window(Action::Minimize(minimized)))
+/// Minimizes the window.
+pub fn minimize<Message>(id: Id, minimized: bool) -> Command<Message> {
+ Command::single(command::Action::Window(Action::Minimize(id, minimized)))
}
-/// Moves a window to the given logical coordinates.
-pub fn move_to<Message>(x: i32, y: i32) -> Command<Message> {
- Command::single(command::Action::Window(Action::Move { x, y }))
+/// Moves the window to the given logical coordinates.
+pub fn move_to<Message>(id: Id, position: Point) -> Command<Message> {
+ Command::single(command::Action::Window(Action::Move(id, position)))
}
/// Changes the [`Mode`] of the window.
-pub fn change_mode<Message>(mode: Mode) -> Command<Message> {
- Command::single(command::Action::Window(Action::ChangeMode(mode)))
+pub fn change_mode<Message>(id: Id, mode: Mode) -> Command<Message> {
+ Command::single(command::Action::Window(Action::ChangeMode(id, mode)))
}
/// Fetches the current [`Mode`] of the window.
pub fn fetch_mode<Message>(
+ id: Id,
f: impl FnOnce(Mode) -> Message + 'static,
) -> Command<Message> {
- Command::single(command::Action::Window(Action::FetchMode(Box::new(f))))
+ Command::single(command::Action::Window(Action::FetchMode(id, Box::new(f))))
}
/// Toggles the window to maximized or back.
-pub fn toggle_maximize<Message>() -> Command<Message> {
- Command::single(command::Action::Window(Action::ToggleMaximize))
+pub fn toggle_maximize<Message>(id: Id) -> Command<Message> {
+ Command::single(command::Action::Window(Action::ToggleMaximize(id)))
}
/// Toggles the window decorations.
-pub fn toggle_decorations<Message>() -> Command<Message> {
- Command::single(command::Action::Window(Action::ToggleDecorations))
+pub fn toggle_decorations<Message>(id: Id) -> Command<Message> {
+ Command::single(command::Action::Window(Action::ToggleDecorations(id)))
}
-/// Request user attention to the window, this has no effect if the application
+/// Request user attention to the window. This has no effect if the application
/// is already focused. How requesting for user attention manifests is platform dependent,
/// see [`UserAttention`] for details.
///
/// Providing `None` will unset the request for user attention. Unsetting the request for
/// user attention might not be done automatically by the WM when the window receives input.
pub fn request_user_attention<Message>(
+ id: Id,
user_attention: Option<UserAttention>,
) -> Command<Message> {
Command::single(command::Action::Window(Action::RequestUserAttention(
+ id,
user_attention,
)))
}
@@ -107,30 +147,36 @@ pub fn request_user_attention<Message>(
/// This [`Command`] steals input focus from other applications. Do not use this method unless
/// you are certain that's what the user wants. Focus stealing can cause an extremely disruptive
/// user experience.
-pub fn gain_focus<Message>() -> Command<Message> {
- Command::single(command::Action::Window(Action::GainFocus))
+pub fn gain_focus<Message>(id: Id) -> Command<Message> {
+ Command::single(command::Action::Window(Action::GainFocus(id)))
}
/// Changes the window [`Level`].
-pub fn change_level<Message>(level: Level) -> Command<Message> {
- Command::single(command::Action::Window(Action::ChangeLevel(level)))
+pub fn change_level<Message>(id: Id, level: Level) -> Command<Message> {
+ Command::single(command::Action::Window(Action::ChangeLevel(id, level)))
}
-/// Fetches an identifier unique to the window.
+/// Fetches an identifier unique to the window, provided by the underlying windowing system. This is
+/// not to be confused with [`Id`].
pub fn fetch_id<Message>(
+ id: Id,
f: impl FnOnce(u64) -> Message + 'static,
) -> Command<Message> {
- Command::single(command::Action::Window(Action::FetchId(Box::new(f))))
+ Command::single(command::Action::Window(Action::FetchId(id, Box::new(f))))
}
/// Changes the [`Icon`] of the window.
-pub fn change_icon<Message>(icon: Icon) -> Command<Message> {
- Command::single(command::Action::Window(Action::ChangeIcon(icon)))
+pub fn change_icon<Message>(id: Id, icon: Icon) -> Command<Message> {
+ Command::single(command::Action::Window(Action::ChangeIcon(id, icon)))
}
/// Captures a [`Screenshot`] from the window.
pub fn screenshot<Message>(
+ id: Id,
f: impl FnOnce(Screenshot) -> Message + Send + 'static,
) -> Command<Message> {
- Command::single(command::Action::Window(Action::Screenshot(Box::new(f))))
+ Command::single(command::Action::Window(Action::Screenshot(
+ id,
+ Box::new(f),
+ )))
}
diff --git a/runtime/src/window/action.rs b/runtime/src/window/action.rs
index b6964e36..8b532569 100644
--- a/runtime/src/window/action.rs
+++ b/runtime/src/window/action.rs
@@ -1,5 +1,5 @@
-use crate::core::window::{Icon, Level, Mode, UserAttention};
-use crate::core::Size;
+use crate::core::window::{Icon, Id, Level, Mode, Settings, UserAttention};
+use crate::core::{Point, Size};
use crate::futures::MaybeSend;
use crate::window::Screenshot;
@@ -7,43 +7,51 @@ use std::fmt;
/// An operation to be performed on some window.
pub enum Action<T> {
- /// Close the current window and exits the application.
- Close,
+ /// Spawns a new window with some [`Settings`].
+ Spawn(Id, Settings),
+ /// Close the window and exits the application.
+ Close(Id),
/// Move the window with the left mouse button until the button is
/// released.
///
/// There’s no guarantee that this will work unless the left mouse
/// button was pressed immediately before this function is called.
- Drag,
- /// Resize the window.
- Resize(Size<u32>),
- /// Fetch the current size of the window.
- FetchSize(Box<dyn FnOnce(Size<u32>) -> T + 'static>),
+ Drag(Id),
+ /// Resize the window to the given logical dimensions.
+ Resize(Id, Size),
+ /// Fetch the current logical dimensions of the window.
+ FetchSize(Id, Box<dyn FnOnce(Size) -> T + 'static>),
+ /// Fetch if the current window is maximized or not.
+ ///
+ /// ## Platform-specific
+ /// - **iOS / Android / Web:** Unsupported.
+ FetchMaximized(Id, Box<dyn FnOnce(bool) -> T + 'static>),
/// Set the window to maximized or back
- Maximize(bool),
+ Maximize(Id, bool),
+ /// Fetch if the current window is minimized or not.
+ ///
+ /// ## Platform-specific
+ /// - **Wayland:** Always `None`.
+ /// - **iOS / Android / Web:** Unsupported.
+ FetchMinimized(Id, Box<dyn FnOnce(Option<bool>) -> T + 'static>),
/// Set the window to minimized or back
- Minimize(bool),
- /// Move the window.
+ Minimize(Id, bool),
+ /// Move the window to the given logical coordinates.
///
/// Unsupported on Wayland.
- Move {
- /// The new logical x location of the window
- x: i32,
- /// The new logical y location of the window
- y: i32,
- },
+ Move(Id, Point),
/// Change the [`Mode`] of the window.
- ChangeMode(Mode),
+ ChangeMode(Id, Mode),
/// Fetch the current [`Mode`] of the window.
- FetchMode(Box<dyn FnOnce(Mode) -> T + 'static>),
+ FetchMode(Id, Box<dyn FnOnce(Mode) -> T + 'static>),
/// Toggle the window to maximized or back
- ToggleMaximize,
+ ToggleMaximize(Id),
/// Toggle whether window has decorations.
///
/// ## Platform-specific
/// - **X11:** Not implemented.
/// - **Web:** Unsupported.
- ToggleDecorations,
+ ToggleDecorations(Id),
/// Request user attention to the window, this has no effect if the application
/// is already focused. How requesting for user attention manifests is platform dependent,
/// see [`UserAttention`] for details.
@@ -57,7 +65,7 @@ pub enum Action<T> {
/// - **macOS:** `None` has no effect.
/// - **X11:** Requests for user attention must be manually cleared.
/// - **Wayland:** Requires `xdg_activation_v1` protocol, `None` has no effect.
- RequestUserAttention(Option<UserAttention>),
+ RequestUserAttention(Id, Option<UserAttention>),
/// Bring the window to the front and sets input focus. Has no effect if the window is
/// already in focus, minimized, or not visible.
///
@@ -68,11 +76,11 @@ pub enum Action<T> {
/// ## Platform-specific
///
/// - **Web / Wayland:** Unsupported.
- GainFocus,
+ GainFocus(Id),
/// Change the window [`Level`].
- ChangeLevel(Level),
- /// Fetch an identifier unique to the window.
- FetchId(Box<dyn FnOnce(u64) -> T + 'static>),
+ ChangeLevel(Id, Level),
+ /// Fetch the raw identifier unique to the window.
+ FetchId(Id, Box<dyn FnOnce(u64) -> T + 'static>),
/// Change the window [`Icon`].
///
/// On Windows and X11, this is typically the small icon in the top-left
@@ -87,9 +95,9 @@ pub enum Action<T> {
///
/// - **X11:** Has no universal guidelines for icon sizes, so you're at the whims of the WM. That
/// said, it's usually in the same ballpark as on Windows.
- ChangeIcon(Icon),
+ ChangeIcon(Id, Icon),
/// Screenshot the viewport of the window.
- Screenshot(Box<dyn FnOnce(Screenshot) -> T + 'static>),
+ Screenshot(Id, Box<dyn FnOnce(Screenshot) -> T + 'static>),
}
impl<T> Action<T> {
@@ -102,29 +110,41 @@ impl<T> Action<T> {
T: 'static,
{
match self {
- Self::Close => Action::Close,
- Self::Drag => Action::Drag,
- Self::Resize(size) => Action::Resize(size),
- Self::FetchSize(o) => Action::FetchSize(Box::new(move |s| f(o(s)))),
- Self::Maximize(maximized) => Action::Maximize(maximized),
- Self::Minimize(minimized) => Action::Minimize(minimized),
- Self::Move { x, y } => Action::Move { x, y },
- Self::ChangeMode(mode) => Action::ChangeMode(mode),
- Self::FetchMode(o) => Action::FetchMode(Box::new(move |s| f(o(s)))),
- Self::ToggleMaximize => Action::ToggleMaximize,
- Self::ToggleDecorations => Action::ToggleDecorations,
- Self::RequestUserAttention(attention_type) => {
- Action::RequestUserAttention(attention_type)
- }
- Self::GainFocus => Action::GainFocus,
- Self::ChangeLevel(level) => Action::ChangeLevel(level),
- Self::FetchId(o) => Action::FetchId(Box::new(move |s| f(o(s)))),
- Self::ChangeIcon(icon) => Action::ChangeIcon(icon),
- Self::Screenshot(tag) => {
- Action::Screenshot(Box::new(move |screenshot| {
- f(tag(screenshot))
- }))
+ Self::Spawn(id, settings) => Action::Spawn(id, settings),
+ Self::Close(id) => Action::Close(id),
+ Self::Drag(id) => Action::Drag(id),
+ Self::Resize(id, size) => Action::Resize(id, size),
+ Self::FetchSize(id, o) => {
+ Action::FetchSize(id, Box::new(move |s| f(o(s))))
}
+ Self::FetchMaximized(id, o) => {
+ Action::FetchMaximized(id, Box::new(move |s| f(o(s))))
+ }
+ Self::Maximize(id, maximized) => Action::Maximize(id, maximized),
+ Self::FetchMinimized(id, o) => {
+ Action::FetchMinimized(id, Box::new(move |s| f(o(s))))
+ }
+ Self::Minimize(id, minimized) => Action::Minimize(id, minimized),
+ Self::Move(id, position) => Action::Move(id, position),
+ Self::ChangeMode(id, mode) => Action::ChangeMode(id, mode),
+ Self::FetchMode(id, o) => {
+ Action::FetchMode(id, Box::new(move |s| f(o(s))))
+ }
+ Self::ToggleMaximize(id) => Action::ToggleMaximize(id),
+ Self::ToggleDecorations(id) => Action::ToggleDecorations(id),
+ Self::RequestUserAttention(id, attention_type) => {
+ Action::RequestUserAttention(id, attention_type)
+ }
+ Self::GainFocus(id) => Action::GainFocus(id),
+ Self::ChangeLevel(id, level) => Action::ChangeLevel(id, level),
+ Self::FetchId(id, o) => {
+ Action::FetchId(id, Box::new(move |s| f(o(s))))
+ }
+ Self::ChangeIcon(id, icon) => Action::ChangeIcon(id, icon),
+ Self::Screenshot(id, tag) => Action::Screenshot(
+ id,
+ Box::new(move |screenshot| f(tag(screenshot))),
+ ),
}
}
}
@@ -132,35 +152,52 @@ impl<T> Action<T> {
impl<T> fmt::Debug for Action<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
- Self::Close => write!(f, "Action::Close"),
- Self::Drag => write!(f, "Action::Drag"),
- Self::Resize(size) => write!(f, "Action::Resize({size:?})"),
- Self::FetchSize(_) => write!(f, "Action::FetchSize"),
- Self::Maximize(maximized) => {
- write!(f, "Action::Maximize({maximized})")
- }
- Self::Minimize(minimized) => {
- write!(f, "Action::Minimize({minimized}")
- }
- Self::Move { x, y } => {
- write!(f, "Action::Move {{ x: {x}, y: {y} }}")
- }
- Self::ChangeMode(mode) => write!(f, "Action::SetMode({mode:?})"),
- Self::FetchMode(_) => write!(f, "Action::FetchMode"),
- Self::ToggleMaximize => write!(f, "Action::ToggleMaximize"),
- Self::ToggleDecorations => write!(f, "Action::ToggleDecorations"),
- Self::RequestUserAttention(_) => {
- write!(f, "Action::RequestUserAttention")
- }
- Self::GainFocus => write!(f, "Action::GainFocus"),
- Self::ChangeLevel(level) => {
- write!(f, "Action::ChangeLevel({level:?})")
- }
- Self::FetchId(_) => write!(f, "Action::FetchId"),
- Self::ChangeIcon(_icon) => {
- write!(f, "Action::ChangeIcon(icon)")
- }
- Self::Screenshot(_) => write!(f, "Action::Screenshot"),
+ Self::Spawn(id, settings) => {
+ write!(f, "Action::Spawn({id:?}, {settings:?})")
+ }
+ Self::Close(id) => write!(f, "Action::Close({id:?})"),
+ Self::Drag(id) => write!(f, "Action::Drag({id:?})"),
+ Self::Resize(id, size) => {
+ write!(f, "Action::Resize({id:?}, {size:?})")
+ }
+ Self::FetchSize(id, _) => write!(f, "Action::FetchSize({id:?})"),
+ Self::FetchMaximized(id, _) => {
+ write!(f, "Action::FetchMaximized({id:?})")
+ }
+ Self::Maximize(id, maximized) => {
+ write!(f, "Action::Maximize({id:?}, {maximized})")
+ }
+ Self::FetchMinimized(id, _) => {
+ write!(f, "Action::FetchMinimized({id:?})")
+ }
+ Self::Minimize(id, minimized) => {
+ write!(f, "Action::Minimize({id:?}, {minimized}")
+ }
+ Self::Move(id, position) => {
+ write!(f, "Action::Move({id:?}, {position})")
+ }
+ Self::ChangeMode(id, mode) => {
+ write!(f, "Action::SetMode({id:?}, {mode:?})")
+ }
+ Self::FetchMode(id, _) => write!(f, "Action::FetchMode({id:?})"),
+ Self::ToggleMaximize(id) => {
+ write!(f, "Action::ToggleMaximize({id:?})")
+ }
+ Self::ToggleDecorations(id) => {
+ write!(f, "Action::ToggleDecorations({id:?})")
+ }
+ Self::RequestUserAttention(id, _) => {
+ write!(f, "Action::RequestUserAttention({id:?})")
+ }
+ Self::GainFocus(id) => write!(f, "Action::GainFocus({id:?})"),
+ Self::ChangeLevel(id, level) => {
+ write!(f, "Action::ChangeLevel({id:?}, {level:?})")
+ }
+ Self::FetchId(id, _) => write!(f, "Action::FetchId({id:?})"),
+ Self::ChangeIcon(id, _icon) => {
+ write!(f, "Action::ChangeIcon({id:?})")
+ }
+ Self::Screenshot(id, _) => write!(f, "Action::Screenshot({id:?})"),
}
}
}
diff --git a/src/lib.rs b/src/lib.rs
index cb6e86d4..446590ec 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -168,6 +168,9 @@ use iced_winit::runtime;
pub use iced_futures::futures;
+#[cfg(feature = "highlighter")]
+pub use iced_highlighter as highlighter;
+
mod error;
mod sandbox;
@@ -179,6 +182,9 @@ pub mod window;
#[cfg(feature = "advanced")]
pub mod advanced;
+#[cfg(feature = "multi-window")]
+pub mod multi_window;
+
pub use style::theme;
pub use crate::core::alignment;
@@ -187,7 +193,6 @@ pub use crate::core::{
color, Alignment, Background, BorderRadius, Color, ContentFit, Degrees,
Gradient, Length, Padding, Pixels, Point, Radians, Rectangle, Size, Vector,
};
-pub use crate::runtime::Command;
pub mod clipboard {
//! Access the clipboard.
@@ -225,7 +230,8 @@ pub mod event {
pub mod keyboard {
//! Listen and react to keyboard events.
- pub use crate::core::keyboard::{Event, KeyCode, Modifiers};
+ pub use crate::core::keyboard::key;
+ pub use crate::core::keyboard::{Event, Key, Location, Modifiers};
pub use iced_futures::keyboard::{on_key_press, on_key_release};
}
@@ -236,6 +242,11 @@ pub mod mouse {
};
}
+pub mod command {
+ //! Run asynchronous actions.
+ pub use crate::runtime::command::{channel, Command};
+}
+
pub mod subscription {
//! Listen to external events in your application.
pub use iced_futures::subscription::{
@@ -284,6 +295,7 @@ pub mod widget {
}
pub use application::Application;
+pub use command::Command;
pub use error::Error;
pub use event::Event;
pub use executor::Executor;
diff --git a/src/multi_window.rs b/src/multi_window.rs
new file mode 100644
index 00000000..5b7a00b4
--- /dev/null
+++ b/src/multi_window.rs
@@ -0,0 +1,4 @@
+//! Leverage multi-window support in your application.
+mod application;
+
+pub use application::Application;
diff --git a/src/multi_window/application.rs b/src/multi_window/application.rs
new file mode 100644
index 00000000..4a91bdf4
--- /dev/null
+++ b/src/multi_window/application.rs
@@ -0,0 +1,245 @@
+use crate::style::application::StyleSheet;
+use crate::window;
+use crate::{Command, Element, Executor, Settings, Subscription};
+
+/// An interactive cross-platform multi-window application.
+///
+/// This trait is the main entrypoint of Iced. Once implemented, you can run
+/// your GUI application by simply calling [`run`](#method.run).
+///
+/// - On native platforms, it will run in its own windows.
+/// - On the web, it will take control of the `<title>` and the `<body>` of the
+/// document and display only the contents of the `window::Id::MAIN` window.
+///
+/// An [`Application`] can execute asynchronous actions by returning a
+/// [`Command`] in some of its methods. If you do not intend to perform any
+/// background work in your program, the [`Sandbox`] trait offers a simplified
+/// interface.
+///
+/// When using an [`Application`] with the `debug` feature enabled, a debug view
+/// can be toggled by pressing `F12`.
+///
+/// # Examples
+/// See the `examples/multi-window` example to see this multi-window `Application` trait in action.
+///
+/// ## A simple "Hello, world!"
+///
+/// If you just want to get started, here is a simple [`Application`] that
+/// says "Hello, world!":
+///
+/// ```no_run
+/// use iced::{executor, window};
+/// use iced::{Command, Element, Settings, Theme};
+/// use iced::multi_window::{self, Application};
+///
+/// pub fn main() -> iced::Result {
+/// Hello::run(Settings::default())
+/// }
+///
+/// struct Hello;
+///
+/// impl multi_window::Application for Hello {
+/// type Executor = executor::Default;
+/// type Flags = ();
+/// type Message = ();
+/// type Theme = Theme;
+///
+/// fn new(_flags: ()) -> (Hello, Command<Self::Message>) {
+/// (Hello, Command::none())
+/// }
+///
+/// fn title(&self, _window: window::Id) -> String {
+/// String::from("A cool application")
+/// }
+///
+/// fn update(&mut self, _message: Self::Message) -> Command<Self::Message> {
+/// Command::none()
+/// }
+///
+/// fn view(&self, _window: window::Id) -> Element<Self::Message> {
+/// "Hello, world!".into()
+/// }
+/// }
+/// ```
+///
+/// [`Sandbox`]: crate::Sandbox
+pub trait Application: Sized {
+ /// The [`Executor`] that will run commands and subscriptions.
+ ///
+ /// The [default executor] can be a good starting point!
+ ///
+ /// [`Executor`]: Self::Executor
+ /// [default executor]: crate::executor::Default
+ type Executor: Executor;
+
+ /// The type of __messages__ your [`Application`] will produce.
+ type Message: std::fmt::Debug + Send;
+
+ /// The theme of your [`Application`].
+ type Theme: Default + StyleSheet;
+
+ /// The data needed to initialize your [`Application`].
+ type Flags;
+
+ /// Initializes the [`Application`] with the flags provided to
+ /// [`run`] as part of the [`Settings`].
+ ///
+ /// Here is where you should return the initial state of your app.
+ ///
+ /// Additionally, you can return a [`Command`] if you need to perform some
+ /// async action in the background on startup. This is useful if you want to
+ /// load state from a file, perform an initial HTTP request, etc.
+ ///
+ /// [`run`]: Self::run
+ fn new(flags: Self::Flags) -> (Self, Command<Self::Message>);
+
+ /// Returns the current title of the `window` of the [`Application`].
+ ///
+ /// This title can be dynamic! The runtime will automatically update the
+ /// title of your window when necessary.
+ fn title(&self, window: window::Id) -> String;
+
+ /// Handles a __message__ and updates the state of the [`Application`].
+ ///
+ /// This is where you define your __update logic__. All the __messages__,
+ /// produced by either user interactions or commands, will be handled by
+ /// this method.
+ ///
+ /// Any [`Command`] returned will be executed immediately in the background.
+ fn update(&mut self, message: Self::Message) -> Command<Self::Message>;
+
+ /// Returns the widgets to display in the `window` of the [`Application`].
+ ///
+ /// These widgets can produce __messages__ based on user interaction.
+ fn view(
+ &self,
+ window: window::Id,
+ ) -> Element<'_, Self::Message, crate::Renderer<Self::Theme>>;
+
+ /// Returns the current [`Theme`] of the `window` of the [`Application`].
+ ///
+ /// [`Theme`]: Self::Theme
+ #[allow(unused_variables)]
+ fn theme(&self, window: window::Id) -> Self::Theme {
+ Self::Theme::default()
+ }
+
+ /// Returns the current `Style` of the [`Theme`].
+ ///
+ /// [`Theme`]: Self::Theme
+ fn style(&self) -> <Self::Theme as StyleSheet>::Style {
+ <Self::Theme as StyleSheet>::Style::default()
+ }
+
+ /// Returns the event [`Subscription`] for the current state of the
+ /// application.
+ ///
+ /// A [`Subscription`] will be kept alive as long as you keep returning it,
+ /// and the __messages__ produced will be handled by
+ /// [`update`](#tymethod.update).
+ ///
+ /// By default, this method returns an empty [`Subscription`].
+ fn subscription(&self) -> Subscription<Self::Message> {
+ Subscription::none()
+ }
+
+ /// Returns the scale factor of the `window` of the [`Application`].
+ ///
+ /// It can be used to dynamically control the size of the UI at runtime
+ /// (i.e. zooming).
+ ///
+ /// For instance, a scale factor of `2.0` will make widgets twice as big,
+ /// while a scale factor of `0.5` will shrink them to half their size.
+ ///
+ /// By default, it returns `1.0`.
+ #[allow(unused_variables)]
+ fn scale_factor(&self, window: window::Id) -> f64 {
+ 1.0
+ }
+
+ /// Runs the multi-window [`Application`].
+ ///
+ /// On native platforms, this method will take control of the current thread
+ /// until the [`Application`] exits.
+ ///
+ /// On the web platform, this method __will NOT return__ unless there is an
+ /// [`Error`] during startup.
+ ///
+ /// [`Error`]: crate::Error
+ fn run(settings: Settings<Self::Flags>) -> crate::Result
+ where
+ Self: 'static,
+ {
+ #[allow(clippy::needless_update)]
+ let renderer_settings = crate::renderer::Settings {
+ default_font: settings.default_font,
+ default_text_size: settings.default_text_size,
+ antialiasing: if settings.antialiasing {
+ Some(crate::graphics::Antialiasing::MSAAx4)
+ } else {
+ None
+ },
+ ..crate::renderer::Settings::default()
+ };
+
+ Ok(crate::shell::multi_window::run::<
+ Instance<Self>,
+ Self::Executor,
+ crate::renderer::Compositor<Self::Theme>,
+ >(settings.into(), renderer_settings)?)
+ }
+}
+
+struct Instance<A: Application>(A);
+
+impl<A> crate::runtime::multi_window::Program for Instance<A>
+where
+ A: Application,
+{
+ type Renderer = crate::Renderer<A::Theme>;
+ type Message = A::Message;
+
+ fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
+ self.0.update(message)
+ }
+
+ fn view(
+ &self,
+ window: window::Id,
+ ) -> Element<'_, Self::Message, Self::Renderer> {
+ self.0.view(window)
+ }
+}
+
+impl<A> crate::shell::multi_window::Application for Instance<A>
+where
+ A: Application,
+{
+ type Flags = A::Flags;
+
+ fn new(flags: Self::Flags) -> (Self, Command<A::Message>) {
+ let (app, command) = A::new(flags);
+
+ (Instance(app), command)
+ }
+
+ fn title(&self, window: window::Id) -> String {
+ self.0.title(window)
+ }
+
+ fn theme(&self, window: window::Id) -> A::Theme {
+ self.0.theme(window)
+ }
+
+ fn style(&self) -> <A::Theme as StyleSheet>::Style {
+ self.0.style()
+ }
+
+ fn subscription(&self) -> Subscription<Self::Message> {
+ self.0.subscription()
+ }
+
+ fn scale_factor(&self, window: window::Id) -> f64 {
+ self.0.scale_factor(window)
+ }
+}
diff --git a/src/settings.rs b/src/settings.rs
index c5e28e86..d9476b61 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -2,6 +2,8 @@
use crate::window;
use crate::{Font, Pixels};
+use std::borrow::Cow;
+
/// The settings of an application.
#[derive(Debug, Clone)]
pub struct Settings<Flags> {
@@ -21,6 +23,9 @@ pub struct Settings<Flags> {
/// [`Application`]: crate::Application
pub flags: Flags,
+ /// The fonts to load on boot.
+ pub fonts: Vec<Cow<'static, [u8]>>,
+
/// The default [`Font`] to be used.
///
/// By default, it uses [`Family::SansSerif`](crate::font::Family::SansSerif).
@@ -41,14 +46,6 @@ pub struct Settings<Flags> {
///
/// [`Canvas`]: crate::widget::Canvas
pub antialiasing: bool,
-
- /// Whether the [`Application`] should exit when the user requests the
- /// window to close (e.g. the user presses the close button).
- ///
- /// By default, it is enabled.
- ///
- /// [`Application`]: crate::Application
- pub exit_on_close_request: bool,
}
impl<Flags> Settings<Flags> {
@@ -62,10 +59,10 @@ impl<Flags> Settings<Flags> {
flags,
id: default_settings.id,
window: default_settings.window,
+ fonts: default_settings.fonts,
default_font: default_settings.default_font,
default_text_size: default_settings.default_text_size,
antialiasing: default_settings.antialiasing,
- exit_on_close_request: default_settings.exit_on_close_request,
}
}
}
@@ -79,10 +76,10 @@ where
id: None,
window: window::Settings::default(),
flags: Default::default(),
+ fonts: Vec::new(),
default_font: Font::default(),
default_text_size: Pixels(16.0),
antialiasing: false,
- exit_on_close_request: true,
}
}
}
@@ -91,9 +88,9 @@ impl<Flags> From<Settings<Flags>> for iced_winit::Settings<Flags> {
fn from(settings: Settings<Flags>) -> iced_winit::Settings<Flags> {
iced_winit::Settings {
id: settings.id,
- window: settings.window.into(),
+ window: settings.window,
flags: settings.flags,
- exit_on_close_request: settings.exit_on_close_request,
+ fonts: settings.fonts,
}
}
}
diff --git a/src/time.rs b/src/time.rs
index 37d454ed..f10f7a5e 100644
--- a/src/time.rs
+++ b/src/time.rs
@@ -1,4 +1,5 @@
//! Listen and react to time.
pub use iced_core::time::{Duration, Instant};
+#[allow(unused_imports)]
pub use iced_futures::backend::default::time::*;
diff --git a/src/window.rs b/src/window.rs
index e4601575..9f96da52 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -1,12 +1,8 @@
//! Configure the window of your application in native platforms.
-mod position;
-mod settings;
pub mod icon;
pub use icon::Icon;
-pub use position::Position;
-pub use settings::{PlatformSpecific, Settings};
pub use crate::core::window::*;
pub use crate::runtime::window::*;
diff --git a/src/window/position.rs b/src/window/position.rs
deleted file mode 100644
index 6b9fac41..00000000
--- a/src/window/position.rs
+++ /dev/null
@@ -1,32 +0,0 @@
-/// The position of a window in a given screen.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum Position {
- /// The platform-specific default position for a new window.
- Default,
- /// The window is completely centered on the screen.
- Centered,
- /// The window is positioned with specific coordinates: `(X, Y)`.
- ///
- /// When the decorations of the window are enabled, Windows 10 will add some
- /// invisible padding to the window. This padding gets included in the
- /// position. So if you have decorations enabled and want the window to be
- /// at (0, 0) you would have to set the position to
- /// `(PADDING_X, PADDING_Y)`.
- Specific(i32, i32),
-}
-
-impl Default for Position {
- fn default() -> Self {
- Self::Default
- }
-}
-
-impl From<Position> for iced_winit::Position {
- fn from(position: Position) -> Self {
- match position {
- Position::Default => Self::Default,
- Position::Centered => Self::Centered,
- Position::Specific(x, y) => Self::Specific(x, y),
- }
- }
-}
diff --git a/style/src/container.rs b/style/src/container.rs
index ec543ae4..490a9dab 100644
--- a/style/src/container.rs
+++ b/style/src/container.rs
@@ -1,5 +1,5 @@
//! Change the appearance of a container.
-use iced_core::{Background, BorderRadius, Color};
+use crate::core::{Background, BorderRadius, Color, Pixels};
/// The appearance of a container.
#[derive(Debug, Clone, Copy)]
@@ -16,6 +16,30 @@ pub struct Appearance {
pub border_color: Color,
}
+impl Appearance {
+ /// Derives a new [`Appearance`] with a border of the given [`Color`] and
+ /// `width`.
+ pub fn with_border(
+ self,
+ color: impl Into<Color>,
+ width: impl Into<Pixels>,
+ ) -> Self {
+ Self {
+ border_color: color.into(),
+ border_width: width.into().0,
+ ..self
+ }
+ }
+
+ /// Derives a new [`Appearance`] with the given [`Background`].
+ pub fn with_background(self, background: impl Into<Background>) -> Self {
+ Self {
+ background: Some(background.into()),
+ ..self
+ }
+ }
+}
+
impl std::default::Default for Appearance {
fn default() -> Self {
Self {
diff --git a/style/src/lib.rs b/style/src/lib.rs
index 30f17a44..e4097434 100644
--- a/style/src/lib.rs
+++ b/style/src/lib.rs
@@ -29,6 +29,7 @@ pub mod rule;
pub mod scrollable;
pub mod slider;
pub mod svg;
+pub mod text_editor;
pub mod text_input;
pub mod theme;
pub mod toggler;
diff --git a/style/src/svg.rs b/style/src/svg.rs
index 9378c1a7..5053f9f8 100644
--- a/style/src/svg.rs
+++ b/style/src/svg.rs
@@ -20,4 +20,7 @@ pub trait StyleSheet {
/// Produces the [`Appearance`] of the svg.
fn appearance(&self, style: &Self::Style) -> Appearance;
+
+ /// Produces the hovered [`Appearance`] of a svg content.
+ fn hovered(&self, style: &Self::Style) -> Appearance;
}
diff --git a/style/src/text_editor.rs b/style/src/text_editor.rs
new file mode 100644
index 00000000..f6bae7e6
--- /dev/null
+++ b/style/src/text_editor.rs
@@ -0,0 +1,47 @@
+//! Change the appearance of a text editor.
+use crate::core::{Background, BorderRadius, Color};
+
+/// The appearance of a text input.
+#[derive(Debug, Clone, Copy)]
+pub struct Appearance {
+ /// The [`Background`] of the text input.
+ pub background: Background,
+ /// The border radius of the text input.
+ pub border_radius: BorderRadius,
+ /// The border width of the text input.
+ pub border_width: f32,
+ /// The border [`Color`] of the text input.
+ pub border_color: Color,
+}
+
+/// A set of rules that dictate the style of a text input.
+pub trait StyleSheet {
+ /// The supported style of the [`StyleSheet`].
+ type Style: Default;
+
+ /// Produces the style of an active text input.
+ fn active(&self, style: &Self::Style) -> Appearance;
+
+ /// Produces the style of a focused text input.
+ fn focused(&self, style: &Self::Style) -> Appearance;
+
+ /// Produces the [`Color`] of the placeholder of a text input.
+ fn placeholder_color(&self, style: &Self::Style) -> Color;
+
+ /// Produces the [`Color`] of the value of a text input.
+ fn value_color(&self, style: &Self::Style) -> Color;
+
+ /// Produces the [`Color`] of the value of a disabled text input.
+ fn disabled_color(&self, style: &Self::Style) -> Color;
+
+ /// Produces the [`Color`] of the selection of a text input.
+ fn selection_color(&self, style: &Self::Style) -> Color;
+
+ /// Produces the style of an hovered text input.
+ fn hovered(&self, style: &Self::Style) -> Appearance {
+ self.focused(style)
+ }
+
+ /// Produces the style of a disabled text input.
+ fn disabled(&self, style: &Self::Style) -> Appearance;
+}
diff --git a/style/src/theme.rs b/style/src/theme.rs
index 3c1f2de6..f78587e5 100644
--- a/style/src/theme.rs
+++ b/style/src/theme.rs
@@ -17,11 +17,13 @@ use crate::rule;
use crate::scrollable;
use crate::slider;
use crate::svg;
+use crate::text_editor;
use crate::text_input;
use crate::toggler;
use iced_core::{Background, Color, Vector};
+use std::fmt;
use std::rc::Rc;
/// A built-in theme.
@@ -37,18 +39,22 @@ pub enum Theme {
}
impl Theme {
+ /// A list with all the defined themes.
+ pub const ALL: &'static [Self] = &[Self::Light, Self::Dark];
+
/// Creates a new custom [`Theme`] from the given [`Palette`].
- pub fn custom(palette: Palette) -> Self {
- Self::custom_with_fn(palette, palette::Extended::generate)
+ pub fn custom(name: String, palette: Palette) -> Self {
+ Self::custom_with_fn(name, palette, palette::Extended::generate)
}
/// Creates a new custom [`Theme`] from the given [`Palette`], with
/// a custom generator of a [`palette::Extended`].
pub fn custom_with_fn(
+ name: String,
palette: Palette,
generate: impl FnOnce(Palette) -> palette::Extended,
) -> Self {
- Self::Custom(Box::new(Custom::with_fn(palette, generate)))
+ Self::Custom(Box::new(Custom::with_fn(name, palette, generate)))
}
/// Returns the [`Palette`] of the [`Theme`].
@@ -70,32 +76,51 @@ impl Theme {
}
}
+impl fmt::Display for Theme {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::Light => write!(f, "Light"),
+ Self::Dark => write!(f, "Dark"),
+ Self::Custom(custom) => custom.fmt(f),
+ }
+ }
+}
+
/// A [`Theme`] with a customized [`Palette`].
-#[derive(Debug, Clone, Copy, PartialEq)]
+#[derive(Debug, Clone, PartialEq)]
pub struct Custom {
+ name: String,
palette: Palette,
extended: palette::Extended,
}
impl Custom {
/// Creates a [`Custom`] theme from the given [`Palette`].
- pub fn new(palette: Palette) -> Self {
- Self::with_fn(palette, palette::Extended::generate)
+ pub fn new(name: String, palette: Palette) -> Self {
+ Self::with_fn(name, palette, palette::Extended::generate)
}
/// Creates a [`Custom`] theme from the given [`Palette`] with
/// a custom generator of a [`palette::Extended`].
pub fn with_fn(
+ name: String,
palette: Palette,
generate: impl FnOnce(Palette) -> palette::Extended,
) -> Self {
Self {
+ name,
palette,
extended: generate(palette),
}
}
}
+impl fmt::Display for Custom {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", self.name)
+ }
+}
+
/// The style of an application.
#[derive(Default)]
pub enum Application {
@@ -382,6 +407,12 @@ pub enum Container {
Custom(Box<dyn container::StyleSheet<Style = Theme>>),
}
+impl From<container::Appearance> for Container {
+ fn from(appearance: container::Appearance) -> Self {
+ Self::Custom(Box::new(move |_: &_| appearance))
+ }
+}
+
impl<T: Fn(&Theme) -> container::Appearance + 'static> From<T> for Container {
fn from(f: T) -> Self {
Self::Custom(Box::new(f))
@@ -908,6 +939,10 @@ impl svg::StyleSheet for Theme {
Svg::Custom(custom) => custom.appearance(self),
}
}
+
+ fn hovered(&self, style: &Self::Style) -> svg::Appearance {
+ self.appearance(style)
+ }
}
impl svg::StyleSheet for fn(&Theme) -> svg::Appearance {
@@ -916,6 +951,10 @@ impl svg::StyleSheet for fn(&Theme) -> svg::Appearance {
fn appearance(&self, style: &Self::Style) -> svg::Appearance {
(self)(style)
}
+
+ fn hovered(&self, style: &Self::Style) -> svg::Appearance {
+ self.appearance(style)
+ }
}
/// The style of a scrollable.
@@ -1174,3 +1213,115 @@ impl text_input::StyleSheet for Theme {
self.placeholder_color(style)
}
}
+
+/// The style of a text input.
+#[derive(Default)]
+pub enum TextEditor {
+ /// The default style.
+ #[default]
+ Default,
+ /// A custom style.
+ Custom(Box<dyn text_editor::StyleSheet<Style = Theme>>),
+}
+
+impl text_editor::StyleSheet for Theme {
+ type Style = TextEditor;
+
+ fn active(&self, style: &Self::Style) -> text_editor::Appearance {
+ if let TextEditor::Custom(custom) = style {
+ return custom.active(self);
+ }
+
+ let palette = self.extended_palette();
+
+ text_editor::Appearance {
+ background: palette.background.base.color.into(),
+ border_radius: 2.0.into(),
+ border_width: 1.0,
+ border_color: palette.background.strong.color,
+ }
+ }
+
+ fn hovered(&self, style: &Self::Style) -> text_editor::Appearance {
+ if let TextEditor::Custom(custom) = style {
+ return custom.hovered(self);
+ }
+
+ let palette = self.extended_palette();
+
+ text_editor::Appearance {
+ background: palette.background.base.color.into(),
+ border_radius: 2.0.into(),
+ border_width: 1.0,
+ border_color: palette.background.base.text,
+ }
+ }
+
+ fn focused(&self, style: &Self::Style) -> text_editor::Appearance {
+ if let TextEditor::Custom(custom) = style {
+ return custom.focused(self);
+ }
+
+ let palette = self.extended_palette();
+
+ text_editor::Appearance {
+ background: palette.background.base.color.into(),
+ border_radius: 2.0.into(),
+ border_width: 1.0,
+ border_color: palette.primary.strong.color,
+ }
+ }
+
+ fn placeholder_color(&self, style: &Self::Style) -> Color {
+ if let TextEditor::Custom(custom) = style {
+ return custom.placeholder_color(self);
+ }
+
+ let palette = self.extended_palette();
+
+ palette.background.strong.color
+ }
+
+ fn value_color(&self, style: &Self::Style) -> Color {
+ if let TextEditor::Custom(custom) = style {
+ return custom.value_color(self);
+ }
+
+ let palette = self.extended_palette();
+
+ palette.background.base.text
+ }
+
+ fn selection_color(&self, style: &Self::Style) -> Color {
+ if let TextEditor::Custom(custom) = style {
+ return custom.selection_color(self);
+ }
+
+ let palette = self.extended_palette();
+
+ palette.primary.weak.color
+ }
+
+ fn disabled(&self, style: &Self::Style) -> text_editor::Appearance {
+ if let TextEditor::Custom(custom) = style {
+ return custom.disabled(self);
+ }
+
+ let palette = self.extended_palette();
+
+ text_editor::Appearance {
+ background: palette.background.weak.color.into(),
+ border_radius: 2.0.into(),
+ border_width: 1.0,
+ border_color: palette.background.strong.color,
+ }
+ }
+
+ fn disabled_color(&self, style: &Self::Style) -> Color {
+ if let TextEditor::Custom(custom) = style {
+ return custom.disabled_color(self);
+ }
+
+ self.placeholder_color(style)
+ }
+}
diff --git a/style/src/theme/palette.rs b/style/src/theme/palette.rs
index aaeb799d..76977a29 100644
--- a/style/src/theme/palette.rs
+++ b/style/src/theme/palette.rs
@@ -82,6 +82,8 @@ pub struct Extended {
pub success: Success,
/// The set of danger colors.
pub danger: Danger,
+ /// Whether the palette is dark or not.
+ pub is_dark: bool,
}
/// The built-in light variant of an [`Extended`] palette.
@@ -113,6 +115,7 @@ impl Extended {
palette.background,
palette.text,
),
+ is_dark: is_dark(palette.background),
}
}
}
diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml
index 15a6928a..68b2a03a 100644
--- a/tiny_skia/Cargo.toml
+++ b/tiny_skia/Cargo.toml
@@ -22,15 +22,10 @@ bytemuck.workspace = true
cosmic-text.workspace = true
kurbo.workspace = true
log.workspace = true
-raw-window-handle.workspace = true
rustc-hash.workspace = true
softbuffer.workspace = true
tiny-skia.workspace = true
-twox-hash.workspace = true
+xxhash-rust.workspace = true
resvg.workspace = true
resvg.optional = true
-
-[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
-twox-hash.workspace = true
-twox-hash.features = ["std"]
diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs
index 65aca4b0..d1393b4d 100644
--- a/tiny_skia/src/backend.rs
+++ b/tiny_skia/src/backend.rs
@@ -1,7 +1,7 @@
use crate::core::{Background, Color, Gradient, Rectangle, Vector};
use crate::graphics::backend;
use crate::graphics::text;
-use crate::graphics::{Damage, Viewport};
+use crate::graphics::Viewport;
use crate::primitive::{self, Primitive};
use std::borrow::Cow;
@@ -362,11 +362,10 @@ impl Backend {
paragraph,
position,
color,
+ clip_bounds: text_clip_bounds,
} => {
let physical_bounds =
- (Rectangle::new(*position, paragraph.min_bounds)
- + translation)
- * scale_factor;
+ (*text_clip_bounds + translation) * scale_factor;
if !clip_bounds.intersects(&physical_bounds) {
return;
@@ -384,6 +383,31 @@ impl Backend {
clip_mask,
);
}
+ Primitive::Editor {
+ editor,
+ position,
+ color,
+ clip_bounds: text_clip_bounds,
+ } => {
+ let physical_bounds =
+ (*text_clip_bounds + translation) * scale_factor;
+
+ if !clip_bounds.intersects(&physical_bounds) {
+ return;
+ }
+
+ let clip_mask = (!physical_bounds.is_within(&clip_bounds))
+ .then_some(clip_mask as &_);
+
+ self.text_pipeline.draw_editor(
+ editor,
+ *position + translation,
+ *color,
+ scale_factor,
+ pixels,
+ clip_mask,
+ );
+ }
Primitive::Text {
content,
bounds,
@@ -394,9 +418,10 @@ impl Backend {
horizontal_alignment,
vertical_alignment,
shaping,
+ clip_bounds: text_clip_bounds,
} => {
let physical_bounds =
- (primitive.bounds() + translation) * scale_factor;
+ (*text_clip_bounds + translation) * scale_factor;
if !clip_bounds.intersects(&physical_bounds) {
return;
@@ -420,8 +445,41 @@ impl Backend {
clip_mask,
);
}
+ Primitive::RawText(text::Raw {
+ buffer,
+ position,
+ color,
+ clip_bounds: text_clip_bounds,
+ }) => {
+ let Some(buffer) = buffer.upgrade() else {
+ return;
+ };
+
+ let physical_bounds =
+ (*text_clip_bounds + translation) * scale_factor;
+
+ if !clip_bounds.intersects(&physical_bounds) {
+ return;
+ }
+
+ let clip_mask = (!physical_bounds.is_within(&clip_bounds))
+ .then_some(clip_mask as &_);
+
+ self.text_pipeline.draw_raw(
+ &buffer,
+ *position + translation,
+ *color,
+ scale_factor,
+ pixels,
+ clip_mask,
+ );
+ }
#[cfg(feature = "image")]
- Primitive::Image { handle, bounds } => {
+ Primitive::Image {
+ handle,
+ filter_method,
+ bounds,
+ } => {
let physical_bounds = (*bounds + translation) * scale_factor;
if !clip_bounds.intersects(&physical_bounds) {
@@ -437,8 +495,14 @@ impl Backend {
)
.post_scale(scale_factor, scale_factor);
- self.raster_pipeline
- .draw(handle, *bounds, pixels, transform, clip_mask);
+ self.raster_pipeline.draw(
+ handle,
+ *filter_method,
+ *bounds,
+ pixels,
+ transform,
+ clip_mask,
+ );
}
#[cfg(not(feature = "image"))]
Primitive::Image { .. } => {
@@ -479,7 +543,6 @@ impl Backend {
path,
paint,
rule,
- transform,
}) => {
let bounds = path.bounds();
@@ -502,9 +565,11 @@ impl Backend {
path,
paint,
*rule,
- transform
- .post_translate(translation.x, translation.y)
- .post_scale(scale_factor, scale_factor),
+ tiny_skia::Transform::from_translate(
+ translation.x,
+ translation.y,
+ )
+ .post_scale(scale_factor, scale_factor),
clip_mask,
);
}
@@ -512,7 +577,6 @@ impl Backend {
path,
paint,
stroke,
- transform,
}) => {
let bounds = path.bounds();
@@ -535,9 +599,11 @@ impl Backend {
path,
paint,
stroke,
- transform
- .post_translate(translation.x, translation.y)
- .post_scale(scale_factor, scale_factor),
+ tiny_skia::Transform::from_translate(
+ translation.x,
+ translation.y,
+ )
+ .post_scale(scale_factor, scale_factor),
clip_mask,
);
}
@@ -803,10 +869,6 @@ impl iced_graphics::Backend for Backend {
}
impl backend::Text for Backend {
- fn font_system(&self) -> &text::FontSystem {
- self.text_pipeline.font_system()
- }
-
fn load_font(&mut self, font: Cow<'static, [u8]>) {
self.text_pipeline.load_font(font);
}
diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs
index 1d14aa03..74a08d38 100644
--- a/tiny_skia/src/geometry.rs
+++ b/tiny_skia/src/geometry.rs
@@ -1,4 +1,5 @@
-use crate::core::{Point, Rectangle, Size, Vector};
+use crate::core::text::LineHeight;
+use crate::core::{Pixels, Point, Rectangle, Size, Vector};
use crate::graphics::geometry::fill::{self, Fill};
use crate::graphics::geometry::stroke::{self, Stroke};
use crate::graphics::geometry::{Path, Style, Text};
@@ -39,17 +40,22 @@ impl Frame {
}
pub fn fill(&mut self, path: &Path, fill: impl Into<Fill>) {
- let Some(path) = convert_path(path) else {
+ let Some(path) =
+ convert_path(path).and_then(|path| path.transform(self.transform))
+ else {
return;
};
+
let fill = fill.into();
+ let mut paint = into_paint(fill.style);
+ paint.shader.transform(self.transform);
+
self.primitives
.push(Primitive::Custom(primitive::Custom::Fill {
path,
- paint: into_paint(fill.style),
+ paint,
rule: into_fill_rule(fill.rule),
- transform: self.transform,
}));
}
@@ -59,73 +65,111 @@ impl Frame {
size: Size,
fill: impl Into<Fill>,
) {
- let Some(path) = convert_path(&Path::rectangle(top_left, size)) else {
+ let Some(path) = convert_path(&Path::rectangle(top_left, size))
+ .and_then(|path| path.transform(self.transform))
+ else {
return;
};
+
let fill = fill.into();
+ let mut paint = tiny_skia::Paint {
+ anti_alias: false,
+ ..into_paint(fill.style)
+ };
+ paint.shader.transform(self.transform);
+
self.primitives
.push(Primitive::Custom(primitive::Custom::Fill {
path,
- paint: tiny_skia::Paint {
- anti_alias: false,
- ..into_paint(fill.style)
- },
+ paint,
rule: into_fill_rule(fill.rule),
- transform: self.transform,
}));
}
pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) {
- let Some(path) = convert_path(path) else {
+ let Some(path) =
+ convert_path(path).and_then(|path| path.transform(self.transform))
+ else {
return;
};
let stroke = stroke.into();
let skia_stroke = into_stroke(&stroke);
+ let mut paint = into_paint(stroke.style);
+ paint.shader.transform(self.transform);
+
self.primitives
.push(Primitive::Custom(primitive::Custom::Stroke {
path,
- paint: into_paint(stroke.style),
+ paint,
stroke: skia_stroke,
- transform: self.transform,
}));
}
pub fn fill_text(&mut self, text: impl Into<Text>) {
let text = text.into();
- let position = if self.transform.is_identity() {
- text.position
- } else {
- let mut transformed = [tiny_skia::Point {
- x: text.position.x,
- y: text.position.y,
- }];
-
- self.transform.map_points(&mut transformed);
-
- Point::new(transformed[0].x, transformed[0].y)
- };
-
- // TODO: Use vectorial text instead of primitive
- self.primitives.push(Primitive::Text {
- content: text.content,
- bounds: Rectangle {
+ let (scale_x, scale_y) = self.transform.get_scale();
+
+ if self.transform.is_scale_translate()
+ && scale_x == scale_y
+ && scale_x > 0.0
+ && scale_y > 0.0
+ {
+ let (position, size, line_height) = if self.transform.is_identity()
+ {
+ (text.position, text.size, text.line_height)
+ } else {
+ let mut position = [tiny_skia::Point {
+ x: text.position.x,
+ y: text.position.y,
+ }];
+
+ self.transform.map_points(&mut position);
+
+ let size = text.size.0 * scale_y;
+
+ let line_height = match text.line_height {
+ LineHeight::Absolute(size) => {
+ LineHeight::Absolute(Pixels(size.0 * scale_y))
+ }
+ LineHeight::Relative(factor) => {
+ LineHeight::Relative(factor)
+ }
+ };
+
+ (
+ Point::new(position[0].x, position[0].y),
+ size.into(),
+ line_height,
+ )
+ };
+
+ let bounds = Rectangle {
x: position.x,
y: position.y,
width: f32::INFINITY,
height: f32::INFINITY,
- },
- color: text.color,
- size: text.size,
- line_height: text.line_height,
- font: text.font,
- horizontal_alignment: text.horizontal_alignment,
- vertical_alignment: text.vertical_alignment,
- shaping: text.shaping,
- });
+ };
+
+ // TODO: Honor layering!
+ self.primitives.push(Primitive::Text {
+ content: text.content,
+ bounds,
+ color: text.color,
+ size,
+ line_height,
+ font: text.font,
+ horizontal_alignment: text.horizontal_alignment,
+ vertical_alignment: text.vertical_alignment,
+ shaping: text.shaping,
+ clip_bounds: Rectangle::with_size(Size::INFINITY),
+ });
+ } else {
+ text.draw_with(|path, color| self.fill(&path, color));
+ }
}
pub fn push_transform(&mut self) {
diff --git a/tiny_skia/src/primitive.rs b/tiny_skia/src/primitive.rs
index 0ed24969..7718d542 100644
--- a/tiny_skia/src/primitive.rs
+++ b/tiny_skia/src/primitive.rs
@@ -13,8 +13,6 @@ pub enum Custom {
paint: tiny_skia::Paint<'static>,
/// The fill rule to follow.
rule: tiny_skia::FillRule,
- /// The transform to apply to the path.
- transform: tiny_skia::Transform,
},
/// A path stroked with some paint.
Stroke {
@@ -24,8 +22,6 @@ pub enum Custom {
paint: tiny_skia::Paint<'static>,
/// The stroke settings.
stroke: tiny_skia::Stroke,
- /// The transform to apply to the path.
- transform: tiny_skia::Transform,
},
}
diff --git a/tiny_skia/src/raster.rs b/tiny_skia/src/raster.rs
index d13b1167..5f17ae60 100644
--- a/tiny_skia/src/raster.rs
+++ b/tiny_skia/src/raster.rs
@@ -28,6 +28,7 @@ impl Pipeline {
pub fn draw(
&mut self,
handle: &raster::Handle,
+ filter_method: raster::FilterMethod,
bounds: Rectangle,
pixels: &mut tiny_skia::PixmapMut<'_>,
transform: tiny_skia::Transform,
@@ -39,12 +40,21 @@ impl Pipeline {
let transform = transform.pre_scale(width_scale, height_scale);
+ let quality = match filter_method {
+ raster::FilterMethod::Linear => {
+ tiny_skia::FilterQuality::Bilinear
+ }
+ raster::FilterMethod::Nearest => {
+ tiny_skia::FilterQuality::Nearest
+ }
+ };
+
pixels.draw_pixmap(
(bounds.x / width_scale) as i32,
(bounds.y / height_scale) as i32,
image,
&tiny_skia::PixmapPaint {
- quality: tiny_skia::FilterQuality::Bilinear,
+ quality,
..Default::default()
},
transform,
diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs
index cb3ef54c..9413e311 100644
--- a/tiny_skia/src/text.rs
+++ b/tiny_skia/src/text.rs
@@ -1,9 +1,10 @@
use crate::core::alignment;
use crate::core::text::{LineHeight, Shaping};
-use crate::core::{Color, Font, Pixels, Point, Rectangle};
+use crate::core::{Color, Font, Pixels, Point, Rectangle, Size};
use crate::graphics::text::cache::{self, Cache};
+use crate::graphics::text::editor;
+use crate::graphics::text::font_system;
use crate::graphics::text::paragraph;
-use crate::graphics::text::FontSystem;
use rustc_hash::{FxHashMap, FxHashSet};
use std::borrow::Cow;
@@ -12,7 +13,6 @@ use std::collections::hash_map;
#[allow(missing_debug_implementations)]
pub struct Pipeline {
- font_system: FontSystem,
glyph_cache: GlyphCache,
cache: RefCell<Cache>,
}
@@ -20,18 +20,16 @@ pub struct Pipeline {
impl Pipeline {
pub fn new() -> Self {
Pipeline {
- font_system: FontSystem::new(),
glyph_cache: GlyphCache::new(),
cache: RefCell::new(Cache::new()),
}
}
- pub fn font_system(&self) -> &FontSystem {
- &self.font_system
- }
-
pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
- self.font_system.load_font(bytes);
+ font_system()
+ .write()
+ .expect("Write font system")
+ .load_font(bytes);
self.cache = RefCell::new(Cache::new());
}
@@ -51,8 +49,10 @@ impl Pipeline {
return;
};
+ let mut font_system = font_system().write().expect("Write font system");
+
draw(
- self.font_system.get_mut(),
+ font_system.raw(),
&mut self.glyph_cache,
paragraph.buffer(),
Rectangle::new(position, paragraph.min_bounds()),
@@ -65,6 +65,37 @@ impl Pipeline {
);
}
+ pub fn draw_editor(
+ &mut self,
+ editor: &editor::Weak,
+ position: Point,
+ color: Color,
+ scale_factor: f32,
+ pixels: &mut tiny_skia::PixmapMut<'_>,
+ clip_mask: Option<&tiny_skia::Mask>,
+ ) {
+ use crate::core::text::Editor as _;
+
+ let Some(editor) = editor.upgrade() else {
+ return;
+ };
+
+ let mut font_system = font_system().write().expect("Write font system");
+
+ draw(
+ font_system.raw(),
+ &mut self.glyph_cache,
+ editor.buffer(),
+ Rectangle::new(position, editor.bounds()),
+ color,
+ alignment::Horizontal::Left,
+ alignment::Vertical::Top,
+ scale_factor,
+ pixels,
+ clip_mask,
+ );
+ }
+
pub fn draw_cached(
&mut self,
content: &str,
@@ -82,7 +113,9 @@ impl Pipeline {
) {
let line_height = f32::from(line_height.to_absolute(size));
- let font_system = self.font_system.get_mut();
+ let mut font_system = font_system().write().expect("Write font system");
+ let font_system = font_system.raw();
+
let key = cache::Key {
bounds: bounds.size(),
content,
@@ -115,6 +148,33 @@ impl Pipeline {
);
}
+ pub fn draw_raw(
+ &mut self,
+ buffer: &cosmic_text::Buffer,
+ position: Point,
+ color: Color,
+ scale_factor: f32,
+ pixels: &mut tiny_skia::PixmapMut<'_>,
+ clip_mask: Option<&tiny_skia::Mask>,
+ ) {
+ let mut font_system = font_system().write().expect("Write font system");
+
+ let (width, height) = buffer.size();
+
+ draw(
+ font_system.raw(),
+ &mut self.glyph_cache,
+ buffer,
+ Rectangle::new(position, Size::new(width, height)),
+ color,
+ alignment::Horizontal::Left,
+ alignment::Vertical::Top,
+ scale_factor,
+ pixels,
+ clip_mask,
+ );
+ }
+
pub fn trim_cache(&mut self) {
self.cache.get_mut().trim();
self.glyph_cache.trim();
@@ -155,7 +215,7 @@ fn draw(
if let Some((buffer, placement)) = glyph_cache.allocate(
physical_glyph.cache_key,
- color,
+ glyph.color_opt.map(from_color).unwrap_or(color),
font_system,
&mut swash,
) {
@@ -180,6 +240,12 @@ fn draw(
}
}
+fn from_color(color: cosmic_text::Color) -> Color {
+ let [r, g, b, a] = color.as_rgba();
+
+ Color::from_rgba8(r, g, b, a as f32 / 255.0)
+}
+
#[derive(Debug, Clone, Default)]
struct GlyphCache {
entries: FxHashMap<
diff --git a/tiny_skia/src/vector.rs b/tiny_skia/src/vector.rs
index a1cd269d..fd1ab3de 100644
--- a/tiny_skia/src/vector.rs
+++ b/tiny_skia/src/vector.rs
@@ -1,7 +1,8 @@
use crate::core::svg::{Data, Handle};
use crate::core::{Color, Rectangle, Size};
+use crate::graphics::text;
-use resvg::usvg;
+use resvg::usvg::{self, TreeTextToPath};
use rustc_hash::{FxHashMap, FxHashSet};
use std::cell::RefCell;
@@ -77,7 +78,7 @@ impl Cache {
let id = handle.id();
if let hash_map::Entry::Vacant(entry) = self.trees.entry(id) {
- let svg = match handle.data() {
+ let mut svg = match handle.data() {
Data::Path(path) => {
fs::read_to_string(path).ok().and_then(|contents| {
usvg::Tree::from_str(
@@ -92,6 +93,15 @@ impl Cache {
}
};
+ if let Some(svg) = &mut svg {
+ if svg.has_text_nodes() {
+ let mut font_system =
+ text::font_system().write().expect("Write font system");
+
+ svg.convert_text(font_system.raw().db_mut());
+ }
+ }
+
let _ = entry.insert(svg);
}
diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs
index 828e522f..781ed8a5 100644
--- a/tiny_skia/src/window/compositor.rs
+++ b/tiny_skia/src/window/compositor.rs
@@ -4,19 +4,25 @@ use crate::graphics::damage;
use crate::graphics::{Error, Viewport};
use crate::{Backend, Primitive, Renderer, Settings};
-use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle};
+use std::collections::VecDeque;
use std::marker::PhantomData;
+use std::num::NonZeroU32;
pub struct Compositor<Theme> {
+ context: softbuffer::Context<Box<dyn compositor::Window>>,
+ settings: Settings,
_theme: PhantomData<Theme>,
}
pub struct Surface {
- window: softbuffer::GraphicsContext,
- buffer: Vec<u32>,
+ window: softbuffer::Surface<
+ Box<dyn compositor::Window>,
+ Box<dyn compositor::Window>,
+ >,
clip_mask: tiny_skia::Mask,
- primitives: Option<Vec<Primitive>>,
+ primitive_stack: VecDeque<Vec<Primitive>>,
background_color: Color,
+ max_age: u8,
}
impl<Theme> crate::graphics::Compositor for Compositor<Theme> {
@@ -24,53 +30,64 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> {
type Renderer = Renderer<Theme>;
type Surface = Surface;
- fn new<W: HasRawWindowHandle + HasRawDisplayHandle>(
+ fn new<W: compositor::Window>(
settings: Self::Settings,
- _compatible_window: Option<&W>,
- ) -> Result<(Self, Self::Renderer), Error> {
- let (compositor, backend) = new();
+ compatible_window: W,
+ ) -> Result<Self, Error> {
+ Ok(new(settings, compatible_window))
+ }
- Ok((
- compositor,
- Renderer::new(
- backend,
- settings.default_font,
- settings.default_text_size,
- ),
- ))
+ fn create_renderer(&self) -> Self::Renderer {
+ Renderer::new(
+ Backend::new(),
+ self.settings.default_font,
+ self.settings.default_text_size,
+ )
}
- fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>(
+ fn create_surface<W: compositor::Window + Clone>(
&mut self,
- window: &W,
+ window: W,
width: u32,
height: u32,
- ) -> Surface {
- #[allow(unsafe_code)]
- let window =
- unsafe { softbuffer::GraphicsContext::new(window, window) }
- .expect("Create softbuffer for window");
+ ) -> Self::Surface {
+ let window = softbuffer::Surface::new(
+ &self.context,
+ Box::new(window.clone()) as _,
+ )
+ .expect("Create softbuffer surface for window");
- Surface {
+ let mut surface = Surface {
window,
- buffer: vec![0; width as usize * height as usize],
clip_mask: tiny_skia::Mask::new(width, height)
.expect("Create clip mask"),
- primitives: None,
+ primitive_stack: VecDeque::new(),
background_color: Color::BLACK,
- }
+ max_age: 0,
+ };
+
+ self.configure_surface(&mut surface, width, height);
+
+ surface
}
fn configure_surface(
&mut self,
- surface: &mut Surface,
+ surface: &mut Self::Surface,
width: u32,
height: u32,
) {
- surface.buffer.resize((width * height) as usize, 0);
+ surface
+ .window
+ .resize(
+ NonZeroU32::new(width).expect("Non-zero width"),
+ NonZeroU32::new(height).expect("Non-zero height"),
+ )
+ .expect("Resize surface");
+
surface.clip_mask =
tiny_skia::Mask::new(width, height).expect("Create clip mask");
- surface.primitives = None;
+ surface.primitive_stack.clear();
}
fn fetch_information(&self) -> Information {
@@ -121,13 +138,19 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> {
}
}
-pub fn new<Theme>() -> (Compositor<Theme>, Backend) {
- (
- Compositor {
- _theme: PhantomData,
- },
- Backend::new(),
- )
+pub fn new<W: compositor::Window, Theme>(
+ settings: Settings,
+ compatible_window: W,
+) -> Compositor<Theme> {
+ #[allow(unsafe_code)]
+ let context = softbuffer::Context::new(Box::new(compatible_window) as _)
+ .expect("Create softbuffer context");
+
+ Compositor {
+ context,
+ settings,
+ _theme: PhantomData,
+ }
}
pub fn present<T: AsRef<str>>(
@@ -141,16 +164,25 @@ pub fn present<T: AsRef<str>>(
let physical_size = viewport.physical_size();
let scale_factor = viewport.scale_factor() as f32;
- let mut pixels = tiny_skia::PixmapMut::from_bytes(
- bytemuck::cast_slice_mut(&mut surface.buffer),
- physical_size.width,
- physical_size.height,
- )
- .expect("Create pixel map");
+ let mut buffer = surface
+ .window
+ .buffer_mut()
+ .map_err(|_| compositor::SurfaceError::Lost)?;
+
+ let last_primitives = {
+ let age = buffer.age();
- let damage = surface
- .primitives
- .as_deref()
+ surface.max_age = surface.max_age.max(age);
+ surface.primitive_stack.truncate(surface.max_age as usize);
+
+ if age > 0 {
+ surface.primitive_stack.get(age as usize - 1)
+ } else {
+ None
+ }
+ };
+
+ let damage = last_primitives
.and_then(|last_primitives| {
(surface.background_color == background_color)
.then(|| damage::list(last_primitives, primitives))
@@ -161,11 +193,18 @@ pub fn present<T: AsRef<str>>(
return Ok(());
}
- surface.primitives = Some(primitives.to_vec());
+ surface.primitive_stack.push_front(primitives.to_vec());
surface.background_color = background_color;
let damage = damage::group(damage, scale_factor, physical_size);
+ let mut pixels = tiny_skia::PixmapMut::from_bytes(
+ bytemuck::cast_slice_mut(&mut buffer),
+ physical_size.width,
+ physical_size.height,
+ )
+ .expect("Create pixel map");
+
backend.draw(
&mut pixels,
&mut surface.clip_mask,
@@ -176,13 +215,7 @@ pub fn present<T: AsRef<str>>(
overlay,
);
- surface.window.set_buffer(
- &surface.buffer,
- physical_size.width as u16,
- physical_size.height as u16,
- );
-
- Ok(())
+ buffer.present().map_err(|_| compositor::SurfaceError::Lost)
}
pub fn screenshot<T: AsRef<str>>(
diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml
index a460c127..1d3b57a7 100644
--- a/wgpu/Cargo.toml
+++ b/wgpu/Cargo.toml
@@ -32,7 +32,6 @@ glyphon.workspace = true
guillotiere.workspace = true
log.workspace = true
once_cell.workspace = true
-raw-window-handle.workspace = true
wgpu.workspace = true
lyon.workspace = true
diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs
index 65c63f19..25134d68 100644
--- a/wgpu/src/backend.rs
+++ b/wgpu/src/backend.rs
@@ -1,8 +1,8 @@
use crate::core::{Color, Size};
-use crate::graphics;
use crate::graphics::backend;
use crate::graphics::color;
use crate::graphics::{Transformation, Viewport};
+use crate::primitive::pipeline;
use crate::primitive::{self, Primitive};
use crate::quad;
use crate::text;
@@ -26,6 +26,7 @@ pub struct Backend {
quad_pipeline: quad::Pipeline,
text_pipeline: text::Pipeline,
triangle_pipeline: triangle::Pipeline,
+ pipeline_storage: pipeline::Storage,
#[cfg(any(feature = "image", feature = "svg"))]
image_pipeline: image::Pipeline,
@@ -51,6 +52,7 @@ impl Backend {
quad_pipeline,
text_pipeline,
triangle_pipeline,
+ pipeline_storage: pipeline::Storage::default(),
#[cfg(any(feature = "image", feature = "svg"))]
image_pipeline,
@@ -67,6 +69,7 @@ impl Backend {
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
clear_color: Option<Color>,
+ format: wgpu::TextureFormat,
frame: &wgpu::TextureView,
primitives: &[Primitive],
viewport: &Viewport,
@@ -89,6 +92,7 @@ impl Backend {
self.prepare(
device,
queue,
+ format,
encoder,
scale_factor,
target_size,
@@ -118,6 +122,7 @@ impl Backend {
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
+ format: wgpu::TextureFormat,
_encoder: &mut wgpu::CommandEncoder,
scale_factor: f32,
target_size: Size<u32>,
@@ -180,6 +185,20 @@ impl Backend {
target_size,
);
}
+
+ if !layer.pipelines.is_empty() {
+ for pipeline in &layer.pipelines {
+ pipeline.primitive.prepare(
+ format,
+ device,
+ queue,
+ pipeline.bounds,
+ target_size,
+ scale_factor,
+ &mut self.pipeline_storage,
+ );
+ }
+ }
}
}
@@ -203,7 +222,7 @@ impl Backend {
let mut render_pass = ManuallyDrop::new(encoder.begin_render_pass(
&wgpu::RenderPassDescriptor {
- label: Some("iced_wgpu::quad render pass"),
+ label: Some("iced_wgpu render pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target,
resolve_target: None,
@@ -222,10 +241,12 @@ impl Backend {
}),
None => wgpu::LoadOp::Load,
},
- store: true,
+ store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
+ timestamp_writes: None,
+ occlusion_query_set: None,
},
));
@@ -264,18 +285,20 @@ impl Backend {
render_pass = ManuallyDrop::new(encoder.begin_render_pass(
&wgpu::RenderPassDescriptor {
- label: Some("iced_wgpu::quad render pass"),
+ label: Some("iced_wgpu render pass"),
color_attachments: &[Some(
wgpu::RenderPassColorAttachment {
view: target,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
- store: true,
+ store: wgpu::StoreOp::Store,
},
},
)],
depth_stencil_attachment: None,
+ timestamp_writes: None,
+ occlusion_query_set: None,
},
));
}
@@ -299,6 +322,45 @@ impl Backend {
text_layer += 1;
}
+
+ if !layer.pipelines.is_empty() {
+ let _ = ManuallyDrop::into_inner(render_pass);
+
+ for pipeline in &layer.pipelines {
+ let viewport = (pipeline.viewport * scale_factor).snap();
+
+ if viewport.width < 1 || viewport.height < 1 {
+ continue;
+ }
+
+ pipeline.primitive.render(
+ &self.pipeline_storage,
+ target,
+ target_size,
+ viewport,
+ encoder,
+ );
+ }
+
+ render_pass = ManuallyDrop::new(encoder.begin_render_pass(
+ &wgpu::RenderPassDescriptor {
+ label: Some("iced_wgpu render pass"),
+ color_attachments: &[Some(
+ wgpu::RenderPassColorAttachment {
+ view: target,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Load,
+ store: wgpu::StoreOp::Store,
+ },
+ },
+ )],
+ depth_stencil_attachment: None,
+ timestamp_writes: None,
+ occlusion_query_set: None,
+ },
+ ));
+ }
}
let _ = ManuallyDrop::into_inner(render_pass);
@@ -310,10 +372,6 @@ impl crate::graphics::Backend for Backend {
}
impl backend::Text for Backend {
- fn font_system(&self) -> &graphics::text::FontSystem {
- self.text_pipeline.font_system()
- }
-
fn load_font(&mut self, font: Cow<'static, [u8]>) {
self.text_pipeline.load_font(font);
}
diff --git a/wgpu/src/color.rs b/wgpu/src/color.rs
index 20827e3c..4598b0a6 100644
--- a/wgpu/src/color.rs
+++ b/wgpu/src/color.rs
@@ -143,10 +143,12 @@ pub fn convert(
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
- store: true,
+ store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
+ timestamp_writes: None,
+ occlusion_query_set: None,
});
pass.set_pipeline(&pipeline);
diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs
index 655362b7..4d7f443e 100644
--- a/wgpu/src/geometry.rs
+++ b/wgpu/src/geometry.rs
@@ -1,5 +1,6 @@
//! Build and draw geometry.
-use crate::core::{Point, Rectangle, Size, Vector};
+use crate::core::text::LineHeight;
+use crate::core::{Pixels, Point, Rectangle, Size, Vector};
use crate::graphics::color;
use crate::graphics::geometry::fill::{self, Fill};
use crate::graphics::geometry::{
@@ -115,19 +116,31 @@ struct Transforms {
}
#[derive(Debug, Clone, Copy)]
-struct Transform {
- raw: lyon::math::Transform,
- is_identity: bool,
-}
+struct Transform(lyon::math::Transform);
impl Transform {
- /// Transforms the given [Point] by the transformation matrix.
- fn transform_point(&self, point: &mut Point) {
+ fn is_identity(&self) -> bool {
+ self.0 == lyon::math::Transform::identity()
+ }
+
+ fn is_scale_translation(&self) -> bool {
+ self.0.m12.abs() < 2.0 * f32::EPSILON
+ && self.0.m21.abs() < 2.0 * f32::EPSILON
+ }
+
+ fn scale(&self) -> (f32, f32) {
+ (self.0.m11, self.0.m22)
+ }
+
+ fn transform_point(&self, point: Point) -> Point {
let transformed = self
- .raw
+ .0
.transform_point(euclid::Point2D::new(point.x, point.y));
- point.x = transformed.x;
- point.y = transformed.y;
+
+ Point {
+ x: transformed.x,
+ y: transformed.y,
+ }
}
fn transform_style(&self, style: Style) -> Style {
@@ -142,8 +155,8 @@ impl Transform {
fn transform_gradient(&self, mut gradient: Gradient) -> Gradient {
match &mut gradient {
Gradient::Linear(linear) => {
- self.transform_point(&mut linear.start);
- self.transform_point(&mut linear.end);
+ linear.start = self.transform_point(linear.start);
+ linear.end = self.transform_point(linear.end);
}
}
@@ -163,10 +176,7 @@ impl Frame {
primitives: Vec::new(),
transforms: Transforms {
previous: Vec::new(),
- current: Transform {
- raw: lyon::math::Transform::identity(),
- is_identity: true,
- },
+ current: Transform(lyon::math::Transform::identity()),
},
fill_tessellator: tessellation::FillTessellator::new(),
stroke_tessellator: tessellation::StrokeTessellator::new(),
@@ -209,14 +219,14 @@ impl Frame {
let options = tessellation::FillOptions::default()
.with_fill_rule(into_fill_rule(rule));
- if self.transforms.current.is_identity {
+ if self.transforms.current.is_identity() {
self.fill_tessellator.tessellate_path(
path.raw(),
&options,
buffer.as_mut(),
)
} else {
- let path = path.transform(&self.transforms.current.raw);
+ let path = path.transform(&self.transforms.current.0);
self.fill_tessellator.tessellate_path(
path.raw(),
@@ -241,13 +251,14 @@ impl Frame {
.buffers
.get_fill(&self.transforms.current.transform_style(style));
- let top_left =
- self.transforms.current.raw.transform_point(
- lyon::math::Point::new(top_left.x, top_left.y),
- );
+ let top_left = self
+ .transforms
+ .current
+ .0
+ .transform_point(lyon::math::Point::new(top_left.x, top_left.y));
let size =
- self.transforms.current.raw.transform_vector(
+ self.transforms.current.0.transform_vector(
lyon::math::Vector::new(size.width, size.height),
);
@@ -284,14 +295,14 @@ impl Frame {
Cow::Owned(dashed(path, stroke.line_dash))
};
- if self.transforms.current.is_identity {
+ if self.transforms.current.is_identity() {
self.stroke_tessellator.tessellate_path(
path.raw(),
&options,
buffer.as_mut(),
)
} else {
- let path = path.transform(&self.transforms.current.raw);
+ let path = path.transform(&self.transforms.current.0);
self.stroke_tessellator.tessellate_path(
path.raw(),
@@ -318,33 +329,57 @@ impl Frame {
pub fn fill_text(&mut self, text: impl Into<Text>) {
let text = text.into();
- let position = if self.transforms.current.is_identity {
- text.position
- } else {
- let transformed = self.transforms.current.raw.transform_point(
- lyon::math::Point::new(text.position.x, text.position.y),
- );
+ let (scale_x, scale_y) = self.transforms.current.scale();
+
+ if self.transforms.current.is_scale_translation()
+ && scale_x == scale_y
+ && scale_x > 0.0
+ && scale_y > 0.0
+ {
+ let (position, size, line_height) =
+ if self.transforms.current.is_identity() {
+ (text.position, text.size, text.line_height)
+ } else {
+ let position =
+ self.transforms.current.transform_point(text.position);
+
+ let size = Pixels(text.size.0 * scale_y);
+
+ let line_height = match text.line_height {
+ LineHeight::Absolute(size) => {
+ LineHeight::Absolute(Pixels(size.0 * scale_y))
+ }
+ LineHeight::Relative(factor) => {
+ LineHeight::Relative(factor)
+ }
+ };
- Point::new(transformed.x, transformed.y)
- };
+ (position, size, line_height)
+ };
- // TODO: Use vectorial text instead of primitive
- self.primitives.push(Primitive::Text {
- content: text.content,
- bounds: Rectangle {
+ let bounds = Rectangle {
x: position.x,
y: position.y,
width: f32::INFINITY,
height: f32::INFINITY,
- },
- color: text.color,
- size: text.size,
- line_height: text.line_height,
- font: text.font,
- horizontal_alignment: text.horizontal_alignment,
- vertical_alignment: text.vertical_alignment,
- shaping: text.shaping,
- });
+ };
+
+ // TODO: Honor layering!
+ self.primitives.push(Primitive::Text {
+ content: text.content,
+ bounds,
+ color: text.color,
+ size,
+ line_height,
+ font: text.font,
+ horizontal_alignment: text.horizontal_alignment,
+ vertical_alignment: text.vertical_alignment,
+ shaping: text.shaping,
+ clip_bounds: Rectangle::with_size(Size::INFINITY),
+ });
+ } else {
+ text.draw_with(|path, color| self.fill(&path, color));
+ }
}
/// Stores the current transform of the [`Frame`] and executes the given
@@ -420,26 +455,24 @@ impl Frame {
/// Applies a translation to the current transform of the [`Frame`].
#[inline]
pub fn translate(&mut self, translation: Vector) {
- self.transforms.current.raw = self
- .transforms
- .current
- .raw
- .pre_translate(lyon::math::Vector::new(
- translation.x,
- translation.y,
- ));
- self.transforms.current.is_identity = false;
+ self.transforms.current.0 =
+ self.transforms
+ .current
+ .0
+ .pre_translate(lyon::math::Vector::new(
+ translation.x,
+ translation.y,
+ ));
}
/// Applies a rotation in radians to the current transform of the [`Frame`].
#[inline]
pub fn rotate(&mut self, angle: f32) {
- self.transforms.current.raw = self
+ self.transforms.current.0 = self
.transforms
.current
- .raw
+ .0
.pre_rotate(lyon::math::Angle::radians(angle));
- self.transforms.current.is_identity = false;
}
/// Applies a uniform scaling to the current transform of the [`Frame`].
@@ -455,9 +488,8 @@ impl Frame {
pub fn scale_nonuniform(&mut self, scale: impl Into<Vector>) {
let scale = scale.into();
- self.transforms.current.raw =
- self.transforms.current.raw.pre_scale(scale.x, scale.y);
- self.transforms.current.is_identity = false;
+ self.transforms.current.0 =
+ self.transforms.current.0.pre_scale(scale.x, scale.y);
}
/// Produces the [`Primitive`] representing everything drawn on the [`Frame`].
diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs
index 36c1e228..1e5d3ee0 100644
--- a/wgpu/src/image.rs
+++ b/wgpu/src/image.rs
@@ -35,7 +35,8 @@ pub struct Pipeline {
vector_cache: RefCell<vector::Cache>,
pipeline: wgpu::RenderPipeline,
- sampler: wgpu::Sampler,
+ nearest_sampler: wgpu::Sampler,
+ linear_sampler: wgpu::Sampler,
texture: wgpu::BindGroup,
texture_version: usize,
texture_atlas: Atlas,
@@ -49,16 +50,16 @@ pub struct Pipeline {
#[derive(Debug)]
struct Layer {
uniforms: wgpu::Buffer,
- constants: wgpu::BindGroup,
- instances: Buffer<Instance>,
- instance_count: usize,
+ nearest: Data,
+ linear: Data,
}
impl Layer {
fn new(
device: &wgpu::Device,
constant_layout: &wgpu::BindGroupLayout,
- sampler: &wgpu::Sampler,
+ nearest_sampler: &wgpu::Sampler,
+ linear_sampler: &wgpu::Sampler,
) -> Self {
let uniforms = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("iced_wgpu::image uniforms buffer"),
@@ -67,6 +68,59 @@ impl Layer {
mapped_at_creation: false,
});
+ let nearest =
+ Data::new(device, constant_layout, nearest_sampler, &uniforms);
+
+ let linear =
+ Data::new(device, constant_layout, linear_sampler, &uniforms);
+
+ Self {
+ uniforms,
+ nearest,
+ linear,
+ }
+ }
+
+ fn prepare(
+ &mut self,
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ nearest_instances: &[Instance],
+ linear_instances: &[Instance],
+ transformation: Transformation,
+ ) {
+ queue.write_buffer(
+ &self.uniforms,
+ 0,
+ bytemuck::bytes_of(&Uniforms {
+ transform: transformation.into(),
+ }),
+ );
+
+ self.nearest.upload(device, queue, nearest_instances);
+ self.linear.upload(device, queue, linear_instances);
+ }
+
+ fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {
+ self.nearest.render(render_pass);
+ self.linear.render(render_pass);
+ }
+}
+
+#[derive(Debug)]
+struct Data {
+ constants: wgpu::BindGroup,
+ instances: Buffer<Instance>,
+ instance_count: usize,
+}
+
+impl Data {
+ pub fn new(
+ device: &wgpu::Device,
+ constant_layout: &wgpu::BindGroupLayout,
+ sampler: &wgpu::Sampler,
+ uniforms: &wgpu::Buffer,
+ ) -> Self {
let constants = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iced_wgpu::image constants bind group"),
layout: constant_layout,
@@ -75,7 +129,7 @@ impl Layer {
binding: 0,
resource: wgpu::BindingResource::Buffer(
wgpu::BufferBinding {
- buffer: &uniforms,
+ buffer: uniforms,
offset: 0,
size: None,
},
@@ -96,28 +150,18 @@ impl Layer {
);
Self {
- uniforms,
constants,
instances,
instance_count: 0,
}
}
- fn prepare(
+ fn upload(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
instances: &[Instance],
- transformation: Transformation,
) {
- queue.write_buffer(
- &self.uniforms,
- 0,
- bytemuck::bytes_of(&Uniforms {
- transform: transformation.into(),
- }),
- );
-
let _ = self.instances.resize(device, instances.len());
let _ = self.instances.write(queue, 0, instances);
@@ -134,12 +178,22 @@ impl Layer {
impl Pipeline {
pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
- let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
+ let nearest_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
+ address_mode_u: wgpu::AddressMode::ClampToEdge,
+ address_mode_v: wgpu::AddressMode::ClampToEdge,
+ address_mode_w: wgpu::AddressMode::ClampToEdge,
+ min_filter: wgpu::FilterMode::Nearest,
+ mag_filter: wgpu::FilterMode::Nearest,
+ mipmap_filter: wgpu::FilterMode::Nearest,
+ ..Default::default()
+ });
+
+ let linear_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
- mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
+ mag_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::FilterMode::Linear,
..Default::default()
});
@@ -286,7 +340,8 @@ impl Pipeline {
vector_cache: RefCell::new(vector::Cache::default()),
pipeline,
- sampler,
+ nearest_sampler,
+ linear_sampler,
texture,
texture_version: texture_atlas.layer_count(),
texture_atlas,
@@ -329,7 +384,8 @@ impl Pipeline {
#[cfg(feature = "tracing")]
let _ = info_span!("Wgpu::Image", "DRAW").entered();
- let instances: &mut Vec<Instance> = &mut Vec::new();
+ let nearest_instances: &mut Vec<Instance> = &mut Vec::new();
+ let linear_instances: &mut Vec<Instance> = &mut Vec::new();
#[cfg(feature = "image")]
let mut raster_cache = self.raster_cache.borrow_mut();
@@ -340,7 +396,11 @@ impl Pipeline {
for image in images {
match &image {
#[cfg(feature = "image")]
- layer::Image::Raster { handle, bounds } => {
+ layer::Image::Raster {
+ handle,
+ filter_method,
+ bounds,
+ } => {
if let Some(atlas_entry) = raster_cache.upload(
device,
encoder,
@@ -351,7 +411,12 @@ impl Pipeline {
[bounds.x, bounds.y],
[bounds.width, bounds.height],
atlas_entry,
- instances,
+ match filter_method {
+ image::FilterMethod::Nearest => {
+ nearest_instances
+ }
+ image::FilterMethod::Linear => linear_instances,
+ },
);
}
}
@@ -379,7 +444,7 @@ impl Pipeline {
[bounds.x, bounds.y],
size,
atlas_entry,
- instances,
+ nearest_instances,
);
}
}
@@ -388,7 +453,7 @@ impl Pipeline {
}
}
- if instances.is_empty() {
+ if nearest_instances.is_empty() && linear_instances.is_empty() {
return;
}
@@ -416,12 +481,20 @@ impl Pipeline {
self.layers.push(Layer::new(
device,
&self.constant_layout,
- &self.sampler,
+ &self.nearest_sampler,
+ &self.linear_sampler,
));
}
let layer = &mut self.layers[self.prepare_layer];
- layer.prepare(device, queue, instances, transformation);
+
+ layer.prepare(
+ device,
+ queue,
+ nearest_instances,
+ linear_instances,
+ transformation,
+ );
self.prepare_layer += 1;
}
@@ -470,7 +543,7 @@ struct Instance {
}
impl Instance {
- pub const INITIAL: usize = 1_000;
+ pub const INITIAL: usize = 20;
}
#[repr(C)]
diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs
index 6582bb82..d9be50d7 100644
--- a/wgpu/src/image/vector.rs
+++ b/wgpu/src/image/vector.rs
@@ -1,9 +1,10 @@
use crate::core::svg;
use crate::core::{Color, Size};
+use crate::graphics::text;
use crate::image::atlas::{self, Atlas};
use resvg::tiny_skia;
-use resvg::usvg;
+use resvg::usvg::{self, TreeTextToPath};
use std::collections::{HashMap, HashSet};
use std::fs;
@@ -49,15 +50,15 @@ impl Cache {
return self.svgs.get(&handle.id()).unwrap();
}
- let svg = match handle.data() {
- svg::Data::Path(path) => {
- let tree = fs::read_to_string(path).ok().and_then(|contents| {
+ let mut svg = match handle.data() {
+ svg::Data::Path(path) => fs::read_to_string(path)
+ .ok()
+ .and_then(|contents| {
usvg::Tree::from_str(&contents, &usvg::Options::default())
.ok()
- });
-
- tree.map(Svg::Loaded).unwrap_or(Svg::NotFound)
- }
+ })
+ .map(Svg::Loaded)
+ .unwrap_or(Svg::NotFound),
svg::Data::Bytes(bytes) => {
match usvg::Tree::from_data(bytes, &usvg::Options::default()) {
Ok(tree) => Svg::Loaded(tree),
@@ -66,6 +67,15 @@ impl Cache {
}
};
+ if let Svg::Loaded(svg) = &mut svg {
+ if svg.has_text_nodes() {
+ let mut font_system =
+ text::font_system().write().expect("Write font system");
+
+ svg.convert_text(font_system.raw().db_mut());
+ }
+ }
+
let _ = self.svgs.insert(handle.id(), svg);
self.svgs.get(&handle.id()).unwrap()
}
diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs
index d20dbe66..4ad12a88 100644
--- a/wgpu/src/layer.rs
+++ b/wgpu/src/layer.rs
@@ -1,11 +1,13 @@
//! Organize rendering primitives into a flattened list of layers.
mod image;
+mod pipeline;
mod text;
pub mod mesh;
pub use image::Image;
pub use mesh::Mesh;
+pub use pipeline::Pipeline;
pub use text::Text;
use crate::core;
@@ -34,6 +36,9 @@ pub struct Layer<'a> {
/// The images of the [`Layer`].
pub images: Vec<Image>,
+
+ /// The custom pipelines of this [`Layer`].
+ pub pipelines: Vec<Pipeline>,
}
impl<'a> Layer<'a> {
@@ -45,6 +50,7 @@ impl<'a> Layer<'a> {
meshes: Vec::new(),
text: Vec::new(),
images: Vec::new(),
+ pipelines: Vec::new(),
}
}
@@ -69,6 +75,7 @@ impl<'a> Layer<'a> {
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
shaping: core::text::Shaping::Basic,
+ clip_bounds: Rectangle::with_size(Size::INFINITY),
};
overlay.text.push(Text::Cached(text.clone()));
@@ -117,13 +124,30 @@ impl<'a> Layer<'a> {
paragraph,
position,
color,
+ clip_bounds,
} => {
let layer = &mut layers[current_layer];
- layer.text.push(Text::Managed {
+ layer.text.push(Text::Paragraph {
paragraph: paragraph.clone(),
position: *position + translation,
color: *color,
+ clip_bounds: *clip_bounds + translation,
+ });
+ }
+ Primitive::Editor {
+ editor,
+ position,
+ color,
+ clip_bounds,
+ } => {
+ let layer = &mut layers[current_layer];
+
+ layer.text.push(Text::Editor {
+ editor: editor.clone(),
+ position: *position + translation,
+ color: *color,
+ clip_bounds: *clip_bounds + translation,
});
}
Primitive::Text {
@@ -136,6 +160,7 @@ impl<'a> Layer<'a> {
horizontal_alignment,
vertical_alignment,
shaping,
+ clip_bounds,
} => {
let layer = &mut layers[current_layer];
@@ -149,6 +174,22 @@ impl<'a> Layer<'a> {
horizontal_alignment: *horizontal_alignment,
vertical_alignment: *vertical_alignment,
shaping: *shaping,
+ clip_bounds: *clip_bounds + translation,
+ }));
+ }
+ graphics::Primitive::RawText(graphics::text::Raw {
+ buffer,
+ position,
+ color,
+ clip_bounds,
+ }) => {
+ let layer = &mut layers[current_layer];
+
+ layer.text.push(Text::Raw(graphics::text::Raw {
+ buffer: buffer.clone(),
+ position: *position + translation,
+ color: *color,
+ clip_bounds: *clip_bounds + translation,
}));
}
Primitive::Quad {
@@ -173,11 +214,16 @@ impl<'a> Layer<'a> {
layer.quads.add(quad, background);
}
- Primitive::Image { handle, bounds } => {
+ Primitive::Image {
+ handle,
+ filter_method,
+ bounds,
+ } => {
let layer = &mut layers[current_layer];
layer.images.push(Image::Raster {
handle: handle.clone(),
+ filter_method: *filter_method,
bounds: *bounds + translation,
});
}
@@ -290,6 +336,20 @@ impl<'a> Layer<'a> {
}
}
},
+ primitive::Custom::Pipeline(pipeline) => {
+ let layer = &mut layers[current_layer];
+ let bounds = pipeline.bounds + translation;
+
+ if let Some(clip_bounds) =
+ layer.bounds.intersection(&bounds)
+ {
+ layer.pipelines.push(Pipeline {
+ bounds,
+ viewport: clip_bounds,
+ primitive: pipeline.primitive.clone(),
+ });
+ }
+ }
},
}
}
diff --git a/wgpu/src/layer/image.rs b/wgpu/src/layer/image.rs
index 0de589f8..facbe192 100644
--- a/wgpu/src/layer/image.rs
+++ b/wgpu/src/layer/image.rs
@@ -10,6 +10,9 @@ pub enum Image {
/// The handle of a raster image.
handle: image::Handle,
+ /// The filter method of a raster image.
+ filter_method: image::FilterMethod,
+
/// The bounds of the image.
bounds: Rectangle,
},
diff --git a/wgpu/src/layer/pipeline.rs b/wgpu/src/layer/pipeline.rs
new file mode 100644
index 00000000..6dfe6750
--- /dev/null
+++ b/wgpu/src/layer/pipeline.rs
@@ -0,0 +1,17 @@
+use crate::core::Rectangle;
+use crate::primitive::pipeline::Primitive;
+
+use std::sync::Arc;
+
+#[derive(Clone, Debug)]
+/// A custom primitive which can be used to render primitives associated with a custom pipeline.
+pub struct Pipeline {
+ /// The bounds of the [`Pipeline`].
+ pub bounds: Rectangle,
+
+ /// The viewport of the [`Pipeline`].
+ pub viewport: Rectangle,
+
+ /// The [`Primitive`] to render.
+ pub primitive: Arc<dyn Primitive>,
+}
diff --git a/wgpu/src/layer/text.rs b/wgpu/src/layer/text.rs
index b61615d6..37ee5247 100644
--- a/wgpu/src/layer/text.rs
+++ b/wgpu/src/layer/text.rs
@@ -1,17 +1,33 @@
use crate::core::alignment;
use crate::core::text;
use crate::core::{Color, Font, Pixels, Point, Rectangle};
+use crate::graphics;
+use crate::graphics::text::editor;
use crate::graphics::text::paragraph;
-/// A paragraph of text.
+/// A text primitive.
#[derive(Debug, Clone)]
pub enum Text<'a> {
- Managed {
+ /// A paragraph.
+ #[allow(missing_docs)]
+ Paragraph {
paragraph: paragraph::Weak,
position: Point,
color: Color,
+ clip_bounds: Rectangle,
},
+ /// An editor.
+ #[allow(missing_docs)]
+ Editor {
+ editor: editor::Weak,
+ position: Point,
+ color: Color,
+ clip_bounds: Rectangle,
+ },
+ /// Some cached text.
Cached(Cached<'a>),
+ /// Some raw text.
+ Raw(graphics::text::Raw),
}
#[derive(Debug, Clone)]
@@ -42,4 +58,7 @@ pub struct Cached<'a> {
/// The shaping strategy of the text.
pub shaping: text::Shaping,
+
+ /// The clip bounds of the text.
+ pub clip_bounds: Rectangle,
}
diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs
index 6d26723e..424dfeb3 100644
--- a/wgpu/src/lib.rs
+++ b/wgpu/src/lib.rs
@@ -23,7 +23,7 @@
#![forbid(rust_2018_idioms)]
#![deny(
missing_debug_implementations,
- //missing_docs,
+ missing_docs,
unsafe_code,
unused_results,
rustdoc::broken_intra_doc_links
diff --git a/wgpu/src/primitive.rs b/wgpu/src/primitive.rs
index 8dbf3008..fff927ea 100644
--- a/wgpu/src/primitive.rs
+++ b/wgpu/src/primitive.rs
@@ -1,7 +1,13 @@
//! Draw using different graphical primitives.
+pub mod pipeline;
+
+pub use pipeline::Pipeline;
+
use crate::core::Rectangle;
use crate::graphics::{Damage, Mesh};
+use std::fmt::Debug;
+
/// The graphical primitives supported by `iced_wgpu`.
pub type Primitive = crate::graphics::Primitive<Custom>;
@@ -10,12 +16,15 @@ pub type Primitive = crate::graphics::Primitive<Custom>;
pub enum Custom {
/// A mesh primitive.
Mesh(Mesh),
+ /// A custom pipeline primitive.
+ Pipeline(Pipeline),
}
impl Damage for Custom {
fn bounds(&self) -> Rectangle {
match self {
Self::Mesh(mesh) => mesh.bounds(),
+ Self::Pipeline(pipeline) => pipeline.bounds,
}
}
}
diff --git a/wgpu/src/primitive/pipeline.rs b/wgpu/src/primitive/pipeline.rs
new file mode 100644
index 00000000..c8e45458
--- /dev/null
+++ b/wgpu/src/primitive/pipeline.rs
@@ -0,0 +1,116 @@
+//! Draw primitives using custom pipelines.
+use crate::core::{Rectangle, Size};
+
+use std::any::{Any, TypeId};
+use std::collections::HashMap;
+use std::fmt::Debug;
+use std::sync::Arc;
+
+#[derive(Clone, Debug)]
+/// A custom primitive which can be used to render primitives associated with a custom pipeline.
+pub struct Pipeline {
+ /// The bounds of the [`Pipeline`].
+ pub bounds: Rectangle,
+
+ /// The [`Primitive`] to render.
+ pub primitive: Arc<dyn Primitive>,
+}
+
+impl Pipeline {
+ /// Creates a new [`Pipeline`] with the given [`Primitive`].
+ pub fn new(bounds: Rectangle, primitive: impl Primitive) -> Self {
+ Pipeline {
+ bounds,
+ primitive: Arc::new(primitive),
+ }
+ }
+}
+
+impl PartialEq for Pipeline {
+ fn eq(&self, other: &Self) -> bool {
+ self.primitive.type_id() == other.primitive.type_id()
+ }
+}
+
+/// A set of methods which allows a [`Primitive`] to be rendered.
+pub trait Primitive: Debug + Send + Sync + 'static {
+ /// Processes the [`Primitive`], allowing for GPU buffer allocation.
+ fn prepare(
+ &self,
+ format: wgpu::TextureFormat,
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ bounds: Rectangle,
+ target_size: Size<u32>,
+ scale_factor: f32,
+ storage: &mut Storage,
+ );
+
+ /// Renders the [`Primitive`].
+ fn render(
+ &self,
+ storage: &Storage,
+ target: &wgpu::TextureView,
+ target_size: Size<u32>,
+ viewport: Rectangle<u32>,
+ encoder: &mut wgpu::CommandEncoder,
+ );
+}
+
+/// A renderer than can draw custom pipeline primitives.
+pub trait Renderer: crate::core::Renderer {
+ /// Draws a custom pipeline primitive.
+ fn draw_pipeline_primitive(
+ &mut self,
+ bounds: Rectangle,
+ primitive: impl Primitive,
+ );
+}
+
+impl<Theme> Renderer for crate::Renderer<Theme> {
+ fn draw_pipeline_primitive(
+ &mut self,
+ bounds: Rectangle,
+ primitive: impl Primitive,
+ ) {
+ self.draw_primitive(super::Primitive::Custom(super::Custom::Pipeline(
+ Pipeline::new(bounds, primitive),
+ )));
+ }
+}
+
+/// Stores custom, user-provided pipelines.
+#[derive(Default, Debug)]
+pub struct Storage {
+ pipelines: HashMap<TypeId, Box<dyn Any + Send>>,
+}
+
+impl Storage {
+ /// Returns `true` if `Storage` contains a pipeline with type `T`.
+ pub fn has<T: 'static>(&self) -> bool {
+ self.pipelines.get(&TypeId::of::<T>()).is_some()
+ }
+
+ /// Inserts the pipeline `T` in to [`Storage`].
+ pub fn store<T: 'static + Send>(&mut self, pipeline: T) {
+ let _ = self.pipelines.insert(TypeId::of::<T>(), Box::new(pipeline));
+ }
+
+ /// Returns a reference to pipeline with type `T` if it exists in [`Storage`].
+ pub fn get<T: 'static>(&self) -> Option<&T> {
+ self.pipelines.get(&TypeId::of::<T>()).map(|pipeline| {
+ pipeline
+ .downcast_ref::<T>()
+ .expect("Pipeline with this type does not exist in Storage.")
+ })
+ }
+
+ /// Returns a mutable reference to pipeline `T` if it exists in [`Storage`].
+ pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> {
+ self.pipelines.get_mut(&TypeId::of::<T>()).map(|pipeline| {
+ pipeline
+ .downcast_mut::<T>()
+ .expect("Pipeline with this type does not exist in Storage.")
+ })
+ }
+}
diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs
index 2a530cad..dca09cb8 100644
--- a/wgpu/src/text.rs
+++ b/wgpu/src/text.rs
@@ -2,15 +2,15 @@ use crate::core::alignment;
use crate::core::{Rectangle, Size};
use crate::graphics::color;
use crate::graphics::text::cache::{self, Cache};
-use crate::graphics::text::{FontSystem, Paragraph};
+use crate::graphics::text::{font_system, to_color, Editor, Paragraph};
use crate::layer::Text;
use std::borrow::Cow;
use std::cell::RefCell;
+use std::sync::Arc;
#[allow(missing_debug_implementations)]
pub struct Pipeline {
- font_system: FontSystem,
renderers: Vec<glyphon::TextRenderer>,
atlas: glyphon::TextAtlas,
prepare_layer: usize,
@@ -24,7 +24,6 @@ impl Pipeline {
format: wgpu::TextureFormat,
) -> Self {
Pipeline {
- font_system: FontSystem::new(),
renderers: Vec::new(),
atlas: glyphon::TextAtlas::with_color_mode(
device,
@@ -41,12 +40,11 @@ impl Pipeline {
}
}
- pub fn font_system(&self) -> &FontSystem {
- &self.font_system
- }
-
pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
- self.font_system.load_font(bytes);
+ font_system()
+ .write()
+ .expect("Write font system")
+ .load_font(bytes);
self.cache = RefCell::new(Cache::new());
}
@@ -69,21 +67,28 @@ impl Pipeline {
));
}
- let font_system = self.font_system.get_mut();
+ let mut font_system = font_system().write().expect("Write font system");
+ let font_system = font_system.raw();
+
let renderer = &mut self.renderers[self.prepare_layer];
let cache = self.cache.get_mut();
enum Allocation {
Paragraph(Paragraph),
+ Editor(Editor),
Cache(cache::KeyHash),
+ Raw(Arc<glyphon::Buffer>),
}
let allocations: Vec<_> = sections
.iter()
.map(|section| match section {
- Text::Managed { paragraph, .. } => {
+ Text::Paragraph { paragraph, .. } => {
paragraph.upgrade().map(Allocation::Paragraph)
}
+ Text::Editor { editor, .. } => {
+ editor.upgrade().map(Allocation::Editor)
+ }
Text::Cached(text) => {
let (key, _) = cache.allocate(
font_system,
@@ -104,6 +109,7 @@ impl Pipeline {
Some(Allocation::Cache(key))
}
+ Text::Raw(text) => text.buffer.upgrade().map(Allocation::Raw),
})
.collect();
@@ -117,9 +123,13 @@ impl Pipeline {
horizontal_alignment,
vertical_alignment,
color,
+ clip_bounds,
) = match section {
- Text::Managed {
- position, color, ..
+ Text::Paragraph {
+ position,
+ color,
+ clip_bounds,
+ ..
} => {
use crate::core::text::Paragraph as _;
@@ -134,6 +144,29 @@ impl Pipeline {
paragraph.horizontal_alignment(),
paragraph.vertical_alignment(),
*color,
+ *clip_bounds,
+ )
+ }
+ Text::Editor {
+ position,
+ color,
+ clip_bounds,
+ ..
+ } => {
+ use crate::core::text::Editor as _;
+
+ let Some(Allocation::Editor(editor)) = allocation
+ else {
+ return None;
+ };
+
+ (
+ editor.buffer(),
+ Rectangle::new(*position, editor.bounds()),
+ alignment::Horizontal::Left,
+ alignment::Vertical::Top,
+ *color,
+ *clip_bounds,
)
}
Text::Cached(text) => {
@@ -152,6 +185,26 @@ impl Pipeline {
text.horizontal_alignment,
text.vertical_alignment,
text.color,
+ text.clip_bounds,
+ )
+ }
+ Text::Raw(text) => {
+ let Some(Allocation::Raw(buffer)) = allocation else {
+ return None;
+ };
+
+ let (width, height) = buffer.size();
+
+ (
+ buffer.as_ref(),
+ Rectangle::new(
+ text.position,
+ Size::new(width, height),
+ ),
+ alignment::Horizontal::Left,
+ alignment::Vertical::Top,
+ text.color,
+ text.clip_bounds,
)
}
};
@@ -174,13 +227,8 @@ impl Pipeline {
alignment::Vertical::Bottom => bounds.y - bounds.height,
};
- let section_bounds = Rectangle {
- x: left,
- y: top,
- ..bounds
- };
-
- let clip_bounds = layer_bounds.intersection(&section_bounds)?;
+ let clip_bounds =
+ layer_bounds.intersection(&(clip_bounds * scale_factor))?;
Some(glyphon::TextArea {
buffer,
@@ -193,16 +241,7 @@ impl Pipeline {
right: (clip_bounds.x + clip_bounds.width) as i32,
bottom: (clip_bounds.y + clip_bounds.height) as i32,
},
- default_color: {
- let [r, g, b, a] = color::pack(color).components();
-
- glyphon::Color::rgba(
- (r * 255.0) as u8,
- (g * 255.0) as u8,
- (b * 255.0) as u8,
- (a * 255.0) as u8,
- )
- },
+ default_color: to_color(color),
})
},
);
diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs
index 644c9f84..69270a73 100644
--- a/wgpu/src/triangle.rs
+++ b/wgpu/src/triangle.rs
@@ -300,10 +300,15 @@ impl Pipeline {
wgpu::RenderPassColorAttachment {
view: attachment,
resolve_target,
- ops: wgpu::Operations { load, store: true },
+ ops: wgpu::Operations {
+ load,
+ store: wgpu::StoreOp::Store,
+ },
},
)],
depth_stencil_attachment: None,
+ timestamp_writes: None,
+ occlusion_query_set: None,
});
let layer = &mut self.layers[layer];
diff --git a/wgpu/src/triangle/msaa.rs b/wgpu/src/triangle/msaa.rs
index 320b5b12..14abd20b 100644
--- a/wgpu/src/triangle/msaa.rs
+++ b/wgpu/src/triangle/msaa.rs
@@ -167,10 +167,12 @@ impl Blit {
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
- store: true,
+ store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
+ timestamp_writes: None,
+ occlusion_query_set: None,
});
render_pass.set_pipeline(&self.pipeline);
diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs
index 1ddbe5fe..31cf3819 100644
--- a/wgpu/src/window/compositor.rs
+++ b/wgpu/src/window/compositor.rs
@@ -6,8 +6,6 @@ use crate::graphics::compositor;
use crate::graphics::{Error, Viewport};
use crate::{Backend, Primitive, Renderer, Settings};
-use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle};
-
use std::marker::PhantomData;
/// A window graphics backend for iced powered by `wgpu`.
@@ -26,9 +24,9 @@ impl<Theme> Compositor<Theme> {
/// Requests a new [`Compositor`] with the given [`Settings`].
///
/// Returns `None` if no compatible graphics adapter could be found.
- pub async fn request<W: HasRawWindowHandle + HasRawDisplayHandle>(
+ pub async fn request<W: compositor::Window>(
settings: Settings,
- compatible_window: Option<&W>,
+ compatible_window: Option<W>,
) -> Option<Self> {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: settings.internal_backend,
@@ -41,14 +39,15 @@ impl<Theme> Compositor<Theme> {
if log::max_level() >= log::LevelFilter::Info {
let available_adapters: Vec<_> = instance
.enumerate_adapters(settings.internal_backend)
- .map(|adapter| adapter.get_info())
+ .iter()
+ .map(wgpu::Adapter::get_info)
.collect();
log::info!("Available adapters: {available_adapters:#?}");
}
#[allow(unsafe_code)]
let compatible_surface = compatible_window
- .and_then(|window| unsafe { instance.create_surface(window).ok() });
+ .and_then(|window| instance.create_surface(window).ok());
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
@@ -100,14 +99,14 @@ impl<Theme> Compositor<Theme> {
let (device, queue) =
loop {
- let limits = limits.next()?;
+ let required_limits = limits.next()?;
let device = adapter.request_device(
&wgpu::DeviceDescriptor {
label: Some(
"iced_wgpu::window::compositor device descriptor",
),
- features: wgpu::Features::empty(),
- limits,
+ required_features: wgpu::Features::empty(),
+ required_limits,
},
None,
).await.ok();
@@ -136,26 +135,24 @@ impl<Theme> Compositor<Theme> {
/// Creates a [`Compositor`] and its [`Backend`] for the given [`Settings`] and
/// window.
-pub fn new<Theme, W: HasRawWindowHandle + HasRawDisplayHandle>(
+pub fn new<W: compositor::Window, Theme>(
settings: Settings,
- compatible_window: Option<&W>,
-) -> Result<(Compositor<Theme>, Backend), Error> {
+ compatible_window: W,
+) -> Result<Compositor<Theme>, Error> {
let compositor = futures::executor::block_on(Compositor::request(
settings,
- compatible_window,
+ Some(compatible_window),
))
.ok_or(Error::GraphicsAdapterNotFound)?;
- let backend = compositor.create_backend();
-
- Ok((compositor, backend))
+ Ok(compositor)
}
/// Presents the given primitives with the given [`Compositor`] and [`Backend`].
pub fn present<Theme, T: AsRef<str>>(
compositor: &mut Compositor<Theme>,
backend: &mut Backend,
- surface: &mut wgpu::Surface,
+ surface: &mut wgpu::Surface<'static>,
primitives: &[Primitive],
viewport: &Viewport,
background_color: Color,
@@ -178,6 +175,7 @@ pub fn present<Theme, T: AsRef<str>>(
&compositor.queue,
&mut encoder,
Some(background_color),
+ frame.texture.format(),
view,
primitives,
viewport,
@@ -208,32 +206,32 @@ pub fn present<Theme, T: AsRef<str>>(
impl<Theme> graphics::Compositor for Compositor<Theme> {
type Settings = Settings;
type Renderer = Renderer<Theme>;
- type Surface = wgpu::Surface;
+ type Surface = wgpu::Surface<'static>;
- fn new<W: HasRawWindowHandle + HasRawDisplayHandle>(
+ fn new<W: compositor::Window>(
settings: Self::Settings,
- compatible_window: Option<&W>,
- ) -> Result<(Self, Self::Renderer), Error> {
- let (compositor, backend) = new(settings, compatible_window)?;
+ compatible_window: W,
+ ) -> Result<Self, Error> {
+ new(settings, compatible_window)
+ }
- Ok((
- compositor,
- Renderer::new(
- backend,
- settings.default_font,
- settings.default_text_size,
- ),
- ))
+ fn create_renderer(&self) -> Self::Renderer {
+ Renderer::new(
+ self.create_backend(),
+ self.settings.default_font,
+ self.settings.default_text_size,
+ )
}
- fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>(
+ fn create_surface<W: compositor::Window>(
&mut self,
- window: &W,
+ window: W,
width: u32,
height: u32,
- ) -> wgpu::Surface {
- #[allow(unsafe_code)]
- let mut surface = unsafe { self.instance.create_surface(window) }
+ ) -> Self::Surface {
+ let mut surface = self
+ .instance
+ .create_surface(window)
.expect("Create surface");
self.configure_surface(&mut surface, width, height);
@@ -257,6 +255,7 @@ impl<Theme> graphics::Compositor for Compositor<Theme> {
height,
alpha_mode: wgpu::CompositeAlphaMode::Auto,
view_formats: vec![],
+ desired_maximum_frame_latency: 2,
},
);
}
@@ -357,6 +356,7 @@ pub fn screenshot<Theme, T: AsRef<str>>(
&compositor.queue,
&mut encoder,
Some(background_color),
+ texture.format(),
&view,
primitives,
viewport,
diff --git a/widget/Cargo.toml b/widget/Cargo.toml
index 6d62c181..e8e363c4 100644
--- a/widget/Cargo.toml
+++ b/widget/Cargo.toml
@@ -20,6 +20,7 @@ image = ["iced_renderer/image"]
svg = ["iced_renderer/svg"]
canvas = ["iced_renderer/geometry"]
qr_code = ["canvas", "qrcode"]
+wgpu = ["iced_renderer/wgpu"]
[dependencies]
iced_renderer.workspace = true
diff --git a/widget/src/button.rs b/widget/src/button.rs
index 384a3156..0ebb8dcc 100644
--- a/widget/src/button.rs
+++ b/widget/src/button.rs
@@ -10,8 +10,8 @@ use crate::core::touch;
use crate::core::widget::tree::{self, Tree};
use crate::core::widget::Operation;
use crate::core::{
- Background, Clipboard, Color, Element, Layout, Length, Padding, Point,
- Rectangle, Shell, Vector, Widget,
+ Background, Clipboard, Color, Element, Layout, Length, Padding, Rectangle,
+ Shell, Size, Vector, Widget,
};
pub use iced_style::button::{Appearance, StyleSheet};
@@ -71,11 +71,14 @@ where
{
/// Creates a new [`Button`] with the given content.
pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self {
+ let content = content.into();
+ let size = content.as_widget().size_hint();
+
Button {
- content: content.into(),
+ content,
on_press: None,
- width: Length::Shrink,
- height: Length::Shrink,
+ width: size.width.fluid(),
+ height: size.height.fluid(),
padding: Padding::new(5.0),
style: <Renderer::Theme as StyleSheet>::Style::default(),
}
@@ -149,12 +152,11 @@ where
tree.diff_children(std::slice::from_ref(&self.content));
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -431,15 +433,7 @@ pub fn layout(
padding: Padding,
layout_content: impl FnOnce(&layout::Limits) -> layout::Node,
) -> layout::Node {
- let limits = limits.width(width).height(height);
-
- let mut content = layout_content(&limits.pad(padding));
- let padding = padding.fit(content.size(), limits.max());
- let size = limits.pad(padding).resolve(content.size()).pad(padding);
-
- content.move_to(Point::new(padding.left, padding.top));
-
- layout::Node::with_children(size, vec![content])
+ layout::padded(limits, width, height, padding, layout_content)
}
/// Returns the [`mouse::Interaction`] of a [`Button`].
diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs
index 390f4d92..4e42a671 100644
--- a/widget/src/canvas.rs
+++ b/widget/src/canvas.rs
@@ -14,8 +14,9 @@ use crate::core::layout::{self, Layout};
use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget::tree::{self, Tree};
-use crate::core::{Clipboard, Element, Shell, Widget};
-use crate::core::{Length, Rectangle, Size, Vector};
+use crate::core::{
+ Clipboard, Element, Length, Rectangle, Shell, Size, Vector, Widget,
+};
use crate::graphics::geometry;
use std::marker::PhantomData;
@@ -119,12 +120,11 @@ where
tree::State::new(P::State::default())
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -133,10 +133,7 @@ where
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let limits = limits.width(self.width).height(self.height);
- let size = limits.resolve(Size::ZERO);
-
- layout::Node::new(size)
+ layout::atomic(limits, self.width, self.height)
}
fn on_event(
diff --git a/widget/src/canvas/event.rs b/widget/src/canvas/event.rs
index 1288365f..a8eb47f7 100644
--- a/widget/src/canvas/event.rs
+++ b/widget/src/canvas/event.rs
@@ -8,7 +8,7 @@ pub use crate::core::event::Status;
/// A [`Canvas`] event.
///
/// [`Canvas`]: crate::Canvas
-#[derive(Debug, Clone, Copy, PartialEq)]
+#[derive(Debug, Clone, PartialEq)]
pub enum Event {
/// A mouse event.
Mouse(mouse::Event),
diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs
index d7fdf339..0353b3ad 100644
--- a/widget/src/checkbox.rs
+++ b/widget/src/checkbox.rs
@@ -174,12 +174,11 @@ where
tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- Length::Shrink
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: Length::Shrink,
+ }
}
fn layout(
@@ -266,7 +265,7 @@ where
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
- _viewport: &Rectangle,
+ viewport: &Rectangle,
) {
let is_mouse_over = cursor.is_over(layout.bounds());
@@ -315,6 +314,7 @@ where
},
bounds.center(),
custom_style.icon_color,
+ *viewport,
);
}
}
@@ -330,6 +330,7 @@ where
crate::text::Appearance {
color: custom_style.text_color,
},
+ viewport,
);
}
}
diff --git a/widget/src/column.rs b/widget/src/column.rs
index 42e90ac1..d6eea84b 100644
--- a/widget/src/column.rs
+++ b/widget/src/column.rs
@@ -7,7 +7,7 @@ use crate::core::renderer;
use crate::core::widget::{Operation, Tree};
use crate::core::{
Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle,
- Shell, Widget,
+ Shell, Size, Widget,
};
/// A container that distributes its contents vertically.
@@ -22,16 +22,12 @@ pub struct Column<'a, Message, Renderer = crate::Renderer> {
children: Vec<Element<'a, Message, Renderer>>,
}
-impl<'a, Message, Renderer> Column<'a, Message, Renderer> {
+impl<'a, Message, Renderer> Column<'a, Message, Renderer>
+where
+ Renderer: crate::core::Renderer,
+{
/// Creates an empty [`Column`].
pub fn new() -> Self {
- Self::with_children(Vec::new())
- }
-
- /// Creates a [`Column`] with the given elements.
- pub fn with_children(
- children: Vec<Element<'a, Message, Renderer>>,
- ) -> Self {
Column {
spacing: 0.0,
padding: Padding::ZERO,
@@ -39,10 +35,17 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> {
height: Length::Shrink,
max_width: f32::INFINITY,
align_items: Alignment::Start,
- children,
+ children: Vec::new(),
}
}
+ /// Creates a [`Column`] with the given elements.
+ pub fn with_children(
+ children: impl IntoIterator<Item = Element<'a, Message, Renderer>>,
+ ) -> Self {
+ children.into_iter().fold(Self::new(), Self::push)
+ }
+
/// Sets the vertical spacing _between_ elements.
///
/// Custom margins per element do not exist in iced. You should use this
@@ -88,12 +91,26 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> {
mut self,
child: impl Into<Element<'a, Message, Renderer>>,
) -> Self {
- self.children.push(child.into());
+ let child = child.into();
+ let size = child.as_widget().size_hint();
+
+ if size.width.is_fill() {
+ self.width = Length::Fill;
+ }
+
+ if size.height.is_fill() {
+ self.height = Length::Fill;
+ }
+
+ self.children.push(child);
self
}
}
-impl<'a, Message, Renderer> Default for Column<'a, Message, Renderer> {
+impl<'a, Message, Renderer> Default for Column<'a, Message, Renderer>
+where
+ Renderer: crate::core::Renderer,
+{
fn default() -> Self {
Self::new()
}
@@ -112,12 +129,11 @@ where
tree.diff_children(&self.children);
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -126,15 +142,14 @@ where
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let limits = limits
- .max_width(self.max_width)
- .width(self.width)
- .height(self.height);
+ let limits = limits.max_width(self.max_width);
layout::flex::resolve(
layout::flex::Axis::Vertical,
renderer,
&limits,
+ self.width,
+ self.height,
self.padding,
self.spacing,
self.align_items,
@@ -224,15 +239,17 @@ where
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
- for ((child, state), layout) in self
- .children
- .iter()
- .zip(&tree.children)
- .zip(layout.children())
- {
- child
- .as_widget()
- .draw(state, renderer, theme, style, layout, cursor, viewport);
+ if let Some(viewport) = layout.bounds().intersection(viewport) {
+ for ((child, state), layout) in self
+ .children
+ .iter()
+ .zip(&tree.children)
+ .zip(layout.children())
+ {
+ child.as_widget().draw(
+ state, renderer, theme, style, layout, cursor, &viewport,
+ );
+ }
}
}
diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs
index 768c2402..73beeac3 100644
--- a/widget/src/combo_box.rs
+++ b/widget/src/combo_box.rs
@@ -1,6 +1,7 @@
//! Display a dropdown list of searchable and selectable options.
use crate::core::event::{self, Event};
use crate::core::keyboard;
+use crate::core::keyboard::key;
use crate::core::layout::{self, Layout};
use crate::core::mouse;
use crate::core::overlay;
@@ -8,7 +9,9 @@ use crate::core::renderer;
use crate::core::text;
use crate::core::time::Instant;
use crate::core::widget::{self, Widget};
-use crate::core::{Clipboard, Element, Length, Padding, Rectangle, Shell};
+use crate::core::{
+ Clipboard, Element, Length, Padding, Rectangle, Shell, Size,
+};
use crate::overlay::menu;
use crate::text::LineHeight;
use crate::{container, scrollable, text_input, TextInput};
@@ -297,12 +300,8 @@ where
+ scrollable::StyleSheet
+ menu::StyleSheet,
{
- fn width(&self) -> Length {
- Widget::<TextInputEvent, Renderer>::width(&self.text_input)
- }
-
- fn height(&self) -> Length {
- Widget::<TextInputEvent, Renderer>::height(&self.text_input)
+ fn size(&self) -> Size<Length> {
+ Widget::<TextInputEvent, Renderer>::size(&self.text_input)
}
fn layout(
@@ -438,14 +437,14 @@ where
}
if let Event::Keyboard(keyboard::Event::KeyPressed {
- key_code,
+ key: keyboard::Key::Named(named_key),
modifiers,
..
}) = event
{
let shift_modifer = modifiers.shift();
- match (key_code, shift_modifer) {
- (keyboard::KeyCode::Enter, _) => {
+ match (named_key, shift_modifer) {
+ (key::Named::Enter, _) => {
if let Some(index) = &menu.hovered_option {
if let Some(option) =
state.filtered_options.options.get(*index)
@@ -457,8 +456,7 @@ where
event_status = event::Status::Captured;
}
- (keyboard::KeyCode::Up, _)
- | (keyboard::KeyCode::Tab, true) => {
+ (key::Named::ArrowUp, _) | (key::Named::Tab, true) => {
if let Some(index) = &mut menu.hovered_option {
if *index == 0 {
*index = state
@@ -494,8 +492,8 @@ where
event_status = event::Status::Captured;
}
- (keyboard::KeyCode::Down, _)
- | (keyboard::KeyCode::Tab, false)
+ (key::Named::ArrowDown, _)
+ | (key::Named::Tab, false)
if !modifiers.shift() =>
{
if let Some(index) = &mut menu.hovered_option {
@@ -622,7 +620,7 @@ where
_style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
- _viewport: &Rectangle,
+ viewport: &Rectangle,
) {
let is_focused = {
let text_input_state = tree.children[0]
@@ -645,6 +643,7 @@ where
layout,
cursor,
selection,
+ viewport,
);
}
diff --git a/widget/src/container.rs b/widget/src/container.rs
index ee7a4965..cffb0458 100644
--- a/widget/src/container.rs
+++ b/widget/src/container.rs
@@ -46,17 +46,20 @@ where
where
T: Into<Element<'a, Message, Renderer>>,
{
+ let content = content.into();
+ let size = content.as_widget().size_hint();
+
Container {
id: None,
padding: Padding::ZERO,
- width: Length::Shrink,
- height: Length::Shrink,
+ width: size.width.fluid(),
+ height: size.height.fluid(),
max_width: f32::INFINITY,
max_height: f32::INFINITY,
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
style: Default::default(),
- content: content.into(),
+ content,
}
}
@@ -152,12 +155,11 @@ where
self.content.as_widget().diff(tree);
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -252,21 +254,23 @@ where
) {
let style = theme.appearance(&self.style);
- draw_background(renderer, &style, layout.bounds());
-
- self.content.as_widget().draw(
- tree,
- renderer,
- theme,
- &renderer::Style {
- text_color: style
- .text_color
- .unwrap_or(renderer_style.text_color),
- },
- layout.children().next().unwrap(),
- cursor,
- viewport,
- );
+ if let Some(viewport) = layout.bounds().intersection(viewport) {
+ draw_background(renderer, &style, layout.bounds());
+
+ self.content.as_widget().draw(
+ tree,
+ renderer,
+ theme,
+ &renderer::Style {
+ text_color: style
+ .text_color
+ .unwrap_or(renderer_style.text_color),
+ },
+ layout.children().next().unwrap(),
+ cursor,
+ &viewport,
+ );
+ }
}
fn overlay<'b>(
@@ -309,25 +313,20 @@ pub fn layout(
vertical_alignment: alignment::Vertical,
layout_content: impl FnOnce(&layout::Limits) -> layout::Node,
) -> layout::Node {
- let limits = limits
- .loose()
- .max_width(max_width)
- .max_height(max_height)
- .width(width)
- .height(height);
-
- let mut content = layout_content(&limits.pad(padding).loose());
- let padding = padding.fit(content.size(), limits.max());
- let size = limits.pad(padding).resolve(content.size());
-
- content.move_to(Point::new(padding.left, padding.top));
- content.align(
- Alignment::from(horizontal_alignment),
- Alignment::from(vertical_alignment),
- size,
- );
-
- layout::Node::with_children(size.pad(padding), vec![content])
+ layout::positioned(
+ &limits.max_width(max_width).max_height(max_height),
+ width,
+ height,
+ padding,
+ |limits| layout_content(&limits.loose()),
+ |content, size| {
+ content.align(
+ Alignment::from(horizontal_alignment),
+ Alignment::from(vertical_alignment),
+ size,
+ )
+ },
+ )
}
/// Draws the background of a [`Container`] given its [`Appearance`] and its `bounds`.
diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs
index 3c9c2b29..498dd76c 100644
--- a/widget/src/helpers.rs
+++ b/widget/src/helpers.rs
@@ -16,6 +16,7 @@ use crate::runtime::Command;
use crate::scrollable::{self, Scrollable};
use crate::slider::{self, Slider};
use crate::text::{self, Text};
+use crate::text_editor::{self, TextEditor};
use crate::text_input::{self, TextInput};
use crate::toggler::{self, Toggler};
use crate::tooltip::{self, Tooltip};
@@ -33,7 +34,7 @@ macro_rules! column {
$crate::Column::new()
);
($($x:expr),+ $(,)?) => (
- $crate::Column::with_children(vec![$($crate::core::Element::from($x)),+])
+ $crate::Column::with_children([$($crate::core::Element::from($x)),+])
);
}
@@ -46,7 +47,7 @@ macro_rules! row {
$crate::Row::new()
);
($($x:expr),+ $(,)?) => (
- $crate::Row::with_children(vec![$($crate::core::Element::from($x)),+])
+ $crate::Row::with_children([$($crate::core::Element::from($x)),+])
);
}
@@ -64,9 +65,12 @@ where
}
/// Creates a new [`Column`] with the given children.
-pub fn column<Message, Renderer>(
- children: Vec<Element<'_, Message, Renderer>>,
-) -> Column<'_, Message, Renderer> {
+pub fn column<'a, Message, Renderer>(
+ children: impl IntoIterator<Item = Element<'a, Message, Renderer>>,
+) -> Column<'a, Message, Renderer>
+where
+ Renderer: core::Renderer,
+{
Column::with_children(children)
}
@@ -76,6 +80,7 @@ pub fn keyed_column<'a, Key, Message, Renderer>(
) -> keyed::Column<'a, Key, Message, Renderer>
where
Key: Copy + PartialEq,
+ Renderer: core::Renderer,
{
keyed::Column::with_children(children)
}
@@ -83,9 +88,12 @@ where
/// Creates a new [`Row`] with the given children.
///
/// [`Row`]: crate::Row
-pub fn row<Message, Renderer>(
- children: Vec<Element<'_, Message, Renderer>>,
-) -> Row<'_, Message, Renderer> {
+pub fn row<'a, Message, Renderer>(
+ children: impl IntoIterator<Item = Element<'a, Message, Renderer>>,
+) -> Row<'a, Message, Renderer>
+where
+ Renderer: core::Renderer,
+{
Row::with_children(children)
}
@@ -206,6 +214,20 @@ where
TextInput::new(placeholder, value)
}
+/// Creates a new [`TextEditor`].
+///
+/// [`TextEditor`]: crate::TextEditor
+pub fn text_editor<Message, Renderer>(
+ content: &text_editor::Content<Renderer>,
+) -> TextEditor<'_, core::text::highlighter::PlainText, Message, Renderer>
+where
+ Message: Clone,
+ Renderer: core::text::Renderer,
+ Renderer::Theme: text_editor::StyleSheet,
+{
+ TextEditor::new(content)
+}
+
/// Creates a new [`Slider`].
///
/// [`Slider`]: crate::Slider
@@ -249,7 +271,7 @@ pub fn pick_list<'a, Message, Renderer, T>(
on_selected: impl Fn(T) -> Message + 'a,
) -> PickList<'a, T, Message, Renderer>
where
- T: ToString + Eq + 'static,
+ T: ToString + PartialEq + 'static,
[T]: ToOwned<Owned = Vec<T>>,
Renderer: core::text::Renderer,
Renderer::Theme: pick_list::StyleSheet
@@ -370,6 +392,17 @@ where
crate::Canvas::new(program)
}
+/// Creates a new [`Shader`].
+///
+/// [`Shader`]: crate::Shader
+#[cfg(feature = "wgpu")]
+pub fn shader<Message, P>(program: P) -> crate::Shader<Message, P>
+where
+ P: crate::shader::Program<Message>,
+{
+ crate::Shader::new(program)
+}
+
/// Focuses the previous focusable widget.
pub fn focus_previous<Message>() -> Command<Message>
where
diff --git a/widget/src/image.rs b/widget/src/image.rs
index a0e89920..e906ac13 100644
--- a/widget/src/image.rs
+++ b/widget/src/image.rs
@@ -13,7 +13,7 @@ use crate::core::{
use std::hash::Hash;
-pub use image::Handle;
+pub use image::{FilterMethod, Handle};
/// Creates a new [`Viewer`] with the given image `Handle`.
pub fn viewer<Handle>(handle: Handle) -> Viewer<Handle> {
@@ -37,6 +37,7 @@ pub struct Image<Handle> {
width: Length,
height: Length,
content_fit: ContentFit,
+ filter_method: FilterMethod,
}
impl<Handle> Image<Handle> {
@@ -47,6 +48,7 @@ impl<Handle> Image<Handle> {
width: Length::Shrink,
height: Length::Shrink,
content_fit: ContentFit::Contain,
+ filter_method: FilterMethod::default(),
}
}
@@ -65,11 +67,15 @@ impl<Handle> Image<Handle> {
/// Sets the [`ContentFit`] of the [`Image`].
///
/// Defaults to [`ContentFit::Contain`]
- pub fn content_fit(self, content_fit: ContentFit) -> Self {
- Self {
- content_fit,
- ..self
- }
+ pub fn content_fit(mut self, content_fit: ContentFit) -> Self {
+ self.content_fit = content_fit;
+ self
+ }
+
+ /// Sets the [`FilterMethod`] of the [`Image`].
+ pub fn filter_method(mut self, filter_method: FilterMethod) -> Self {
+ self.filter_method = filter_method;
+ self
}
}
@@ -93,7 +99,7 @@ where
};
// The size to be available to the widget prior to `Shrink`ing
- let raw_size = limits.width(width).height(height).resolve(image_size);
+ let raw_size = limits.resolve(width, height, image_size);
// The uncropped size of the image when fit to the bounds above
let full_size = content_fit.fit(image_size, raw_size);
@@ -119,6 +125,7 @@ pub fn draw<Renderer, Handle>(
layout: Layout<'_>,
handle: &Handle,
content_fit: ContentFit,
+ filter_method: FilterMethod,
) where
Renderer: image::Renderer<Handle = Handle>,
Handle: Clone + Hash,
@@ -141,7 +148,7 @@ pub fn draw<Renderer, Handle>(
..bounds
};
- renderer.draw(handle.clone(), drawing_bounds + offset);
+ renderer.draw(handle.clone(), filter_method, drawing_bounds + offset);
};
if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height
@@ -157,12 +164,11 @@ where
Renderer: image::Renderer<Handle = Handle>,
Handle: Clone + Hash,
{
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -191,7 +197,13 @@ where
_cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
- draw(renderer, layout, &self.handle, self.content_fit);
+ draw(
+ renderer,
+ layout,
+ &self.handle,
+ self.content_fit,
+ self.filter_method,
+ );
}
}
diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs
index 44624fc8..98080577 100644
--- a/widget/src/image/viewer.rs
+++ b/widget/src/image/viewer.rs
@@ -22,19 +22,21 @@ pub struct Viewer<Handle> {
max_scale: f32,
scale_step: f32,
handle: Handle,
+ filter_method: image::FilterMethod,
}
impl<Handle> Viewer<Handle> {
/// Creates a new [`Viewer`] with the given [`State`].
pub fn new(handle: Handle) -> Self {
Viewer {
+ handle,
padding: 0.0,
width: Length::Shrink,
height: Length::Shrink,
min_scale: 0.25,
max_scale: 10.0,
scale_step: 0.10,
- handle,
+ filter_method: image::FilterMethod::default(),
}
}
@@ -95,12 +97,11 @@ where
tree::State::new(State::new())
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -111,10 +112,11 @@ where
) -> layout::Node {
let Size { width, height } = renderer.dimensions(&self.handle);
- let mut size = limits
- .width(self.width)
- .height(self.height)
- .resolve(Size::new(width as f32, height as f32));
+ let mut size = limits.resolve(
+ self.width,
+ self.height,
+ Size::new(width as f32, height as f32),
+ );
let expansion_size = if height > width {
self.width
@@ -329,6 +331,7 @@ where
image::Renderer::draw(
renderer,
self.handle.clone(),
+ self.filter_method,
Rectangle {
x: bounds.x,
y: bounds.y,
diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs
index 0ef82407..7f05a81e 100644
--- a/widget/src/keyed/column.rs
+++ b/widget/src/keyed/column.rs
@@ -8,7 +8,7 @@ use crate::core::widget::tree::{self, Tree};
use crate::core::widget::Operation;
use crate::core::{
Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle,
- Shell, Widget,
+ Shell, Size, Widget,
};
/// A container that distributes its contents vertically.
@@ -30,26 +30,10 @@ where
impl<'a, Key, Message, Renderer> Column<'a, Key, Message, Renderer>
where
Key: Copy + PartialEq,
+ Renderer: crate::core::Renderer,
{
/// Creates an empty [`Column`].
pub fn new() -> Self {
- Self::with_children(Vec::new())
- }
-
- /// Creates a [`Column`] with the given elements.
- pub fn with_children(
- children: impl IntoIterator<Item = (Key, Element<'a, Message, Renderer>)>,
- ) -> Self {
- let (keys, children) = children.into_iter().fold(
- (Vec::new(), Vec::new()),
- |(mut keys, mut children), (key, child)| {
- keys.push(key);
- children.push(child);
-
- (keys, children)
- },
- );
-
Column {
spacing: 0.0,
padding: Padding::ZERO,
@@ -57,11 +41,20 @@ where
height: Length::Shrink,
max_width: f32::INFINITY,
align_items: Alignment::Start,
- keys,
- children,
+ keys: Vec::new(),
+ children: Vec::new(),
}
}
+ /// Creates a [`Column`] with the given elements.
+ pub fn with_children(
+ children: impl IntoIterator<Item = (Key, Element<'a, Message, Renderer>)>,
+ ) -> Self {
+ children
+ .into_iter()
+ .fold(Self::new(), |column, (key, child)| column.push(key, child))
+ }
+
/// Sets the vertical spacing _between_ elements.
///
/// Custom margins per element do not exist in iced. You should use this
@@ -108,8 +101,19 @@ where
key: Key,
child: impl Into<Element<'a, Message, Renderer>>,
) -> Self {
+ let child = child.into();
+ let size = child.as_widget().size_hint();
+
+ if size.width.is_fill() {
+ self.width = Length::Fill;
+ }
+
+ if size.height.is_fill() {
+ self.height = Length::Fill;
+ }
+
self.keys.push(key);
- self.children.push(child.into());
+ self.children.push(child);
self
}
}
@@ -117,6 +121,7 @@ where
impl<'a, Key, Message, Renderer> Default for Column<'a, Key, Message, Renderer>
where
Key: Copy + PartialEq,
+ Renderer: crate::core::Renderer,
{
fn default() -> Self {
Self::new()
@@ -173,12 +178,11 @@ where
}
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -196,6 +200,8 @@ where
layout::flex::Axis::Vertical,
renderer,
&limits,
+ self.width,
+ self.height,
self.padding,
self.spacing,
self.align_items,
diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs
index 589dd938..e9edbb4c 100644
--- a/widget/src/lazy.rs
+++ b/widget/src/lazy.rs
@@ -18,7 +18,7 @@ use crate::core::widget::tree::{self, Tree};
use crate::core::widget::{self, Widget};
use crate::core::Element;
use crate::core::{
- self, Clipboard, Hasher, Length, Point, Rectangle, Shell, Size,
+ self, Clipboard, Hasher, Length, Point, Rectangle, Shell, Size, Vector,
};
use crate::runtime::overlay::Nested;
@@ -142,12 +142,15 @@ where
}
}
- fn width(&self) -> Length {
- self.with_element(|element| element.as_widget().width())
+ fn size(&self) -> Size<Length> {
+ self.with_element(|element| element.as_widget().size())
}
- fn height(&self) -> Length {
- self.with_element(|element| element.as_widget().height())
+ fn size_hint(&self) -> Size<Length> {
+ Size {
+ width: Length::Shrink,
+ height: Length::Shrink,
+ }
}
fn layout(
@@ -333,9 +336,10 @@ where
renderer: &Renderer,
bounds: Size,
position: Point,
+ translation: Vector,
) -> layout::Node {
self.with_overlay_maybe(|overlay| {
- overlay.layout(renderer, bounds, position)
+ overlay.layout(renderer, bounds, position, translation)
})
.unwrap_or_default()
}
diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs
index d454b72b..3684e0c9 100644
--- a/widget/src/lazy/component.rs
+++ b/widget/src/lazy/component.rs
@@ -244,12 +244,15 @@ where
self.rebuild_element_if_necessary();
}
- fn width(&self) -> Length {
- self.with_element(|element| element.as_widget().width())
+ fn size(&self) -> Size<Length> {
+ self.with_element(|element| element.as_widget().size())
}
- fn height(&self) -> Length {
- self.with_element(|element| element.as_widget().height())
+ fn size_hint(&self) -> Size<Length> {
+ Size {
+ width: Length::Shrink,
+ height: Length::Shrink,
+ }
}
fn layout(
@@ -577,9 +580,10 @@ where
renderer: &Renderer,
bounds: Size,
position: Point,
+ translation: Vector,
) -> layout::Node {
self.with_overlay_maybe(|overlay| {
- overlay.layout(renderer, bounds, position)
+ overlay.layout(renderer, bounds, position, translation)
})
.unwrap_or_default()
}
diff --git a/widget/src/lazy/helpers.rs b/widget/src/lazy/helpers.rs
index 8ca9cb86..5dc60d52 100644
--- a/widget/src/lazy/helpers.rs
+++ b/widget/src/lazy/helpers.rs
@@ -6,6 +6,7 @@ use std::hash::Hash;
/// Creates a new [`Lazy`] widget with the given data `Dependency` and a
/// closure that can turn this data into a widget tree.
+#[cfg(feature = "lazy")]
pub fn lazy<'a, Message, Renderer, Dependency, View>(
dependency: Dependency,
view: impl Fn(&Dependency) -> View + 'a,
@@ -19,6 +20,7 @@ where
/// Turns an implementor of [`Component`] into an [`Element`] that can be
/// embedded in any application.
+#[cfg(feature = "lazy")]
pub fn component<'a, C, Message, Renderer>(
component: C,
) -> Element<'a, Message, Renderer>
@@ -37,6 +39,7 @@ where
/// The `view` closure will be provided with the current [`Size`] of
/// the [`Responsive`] widget and, therefore, can be used to build the
/// contents of the widget in a responsive way.
+#[cfg(feature = "lazy")]
pub fn responsive<'a, Message, Renderer>(
f: impl Fn(Size) -> Element<'a, Message, Renderer> + 'a,
) -> Responsive<'a, Message, Renderer>
diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs
index ed471988..1df0866f 100644
--- a/widget/src/lazy/responsive.rs
+++ b/widget/src/lazy/responsive.rs
@@ -6,7 +6,8 @@ use crate::core::renderer;
use crate::core::widget;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
- self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Widget,
+ self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Vector,
+ Widget,
};
use crate::horizontal_space;
use crate::runtime::overlay::Nested;
@@ -134,12 +135,11 @@ where
})
}
- fn width(&self) -> Length {
- Length::Fill
- }
-
- fn height(&self) -> Length {
- Length::Fill
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: Length::Fill,
+ height: Length::Fill,
+ }
}
fn layout(
@@ -367,9 +367,10 @@ where
renderer: &Renderer,
bounds: Size,
position: Point,
+ translation: Vector,
) -> layout::Node {
self.with_overlay_maybe(|overlay| {
- overlay.layout(renderer, bounds, position)
+ overlay.layout(renderer, bounds, position, translation)
})
.unwrap_or_default()
}
diff --git a/widget/src/lib.rs b/widget/src/lib.rs
index 6feb948c..07378d83 100644
--- a/widget/src/lib.rs
+++ b/widget/src/lib.rs
@@ -35,6 +35,7 @@ pub mod scrollable;
pub mod slider;
pub mod space;
pub mod text;
+pub mod text_editor;
pub mod text_input;
pub mod toggler;
pub mod tooltip;
@@ -86,6 +87,8 @@ pub use space::Space;
#[doc(no_inline)]
pub use text::Text;
#[doc(no_inline)]
+pub use text_editor::TextEditor;
+#[doc(no_inline)]
pub use text_input::TextInput;
#[doc(no_inline)]
pub use toggler::Toggler;
@@ -94,6 +97,13 @@ pub use tooltip::Tooltip;
#[doc(no_inline)]
pub use vertical_slider::VerticalSlider;
+#[cfg(feature = "wgpu")]
+pub mod shader;
+
+#[cfg(feature = "wgpu")]
+#[doc(no_inline)]
+pub use shader::Shader;
+
#[cfg(feature = "svg")]
pub mod svg;
diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs
index 3a5b01a3..87cac3a7 100644
--- a/widget/src/mouse_area.rs
+++ b/widget/src/mouse_area.rs
@@ -8,7 +8,7 @@ use crate::core::renderer;
use crate::core::touch;
use crate::core::widget::{tree, Operation, Tree};
use crate::core::{
- Clipboard, Element, Layout, Length, Rectangle, Shell, Widget,
+ Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Widget,
};
/// Emit messages on mouse events.
@@ -110,12 +110,8 @@ where
tree.diff_children(std::slice::from_ref(&self.content));
}
- fn width(&self) -> Length {
- self.content.as_widget().width()
- }
-
- fn height(&self) -> Length {
- self.content.as_widget().height()
+ fn size(&self) -> Size<Length> {
+ self.content.as_widget().size()
}
fn layout(
diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs
index b293f9fa..f83eebea 100644
--- a/widget/src/overlay/menu.rs
+++ b/widget/src/overlay/menu.rs
@@ -236,6 +236,7 @@ where
renderer: &Renderer,
bounds: Size,
position: Point,
+ _translation: Vector,
) -> layout::Node {
let space_below = bounds.height - (position.y + self.target_height);
let space_above = position.y;
@@ -253,15 +254,14 @@ where
)
.width(self.width);
- let mut node = self.container.layout(self.state, renderer, &limits);
+ let node = self.container.layout(self.state, renderer, &limits);
+ let size = node.size();
node.move_to(if space_below > space_above {
position + Vector::new(0.0, self.target_height)
} else {
- position - Vector::new(0.0, node.size().height)
- });
-
- node
+ position - Vector::new(0.0, size.height)
+ })
}
fn on_event(
@@ -342,12 +342,11 @@ where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
{
- fn width(&self) -> Length {
- Length::Fill
- }
-
- fn height(&self) -> Length {
- Length::Shrink
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: Length::Fill,
+ height: Length::Shrink,
+ }
}
fn layout(
@@ -358,7 +357,6 @@ where
) -> layout::Node {
use std::f32;
- let limits = limits.width(Length::Fill).height(Length::Shrink);
let text_size =
self.text_size.unwrap_or_else(|| renderer.default_size());
@@ -371,7 +369,7 @@ where
* self.options.len() as f32,
);
- limits.resolve(intrinsic)
+ limits.resolve(Length::Fill, Length::Shrink, intrinsic)
};
layout::Node::new(size)
@@ -543,6 +541,7 @@ where
} else {
appearance.text_color
},
+ *viewport,
);
}
}
diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs
index 2d25a543..cf1f0455 100644
--- a/widget/src/pane_grid.rs
+++ b/widget/src/pane_grid.rs
@@ -265,12 +265,11 @@ where
}
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -490,8 +489,7 @@ pub fn layout<Renderer, T>(
&layout::Limits,
) -> layout::Node,
) -> layout::Node {
- let limits = limits.width(width).height(height);
- let size = limits.resolve(Size::ZERO);
+ let size = limits.resolve(width, height, Size::ZERO);
let regions = node.pane_regions(spacing, size);
let children = contents
@@ -500,16 +498,14 @@ pub fn layout<Renderer, T>(
let region = regions.get(&pane)?;
let size = Size::new(region.width, region.height);
- let mut node = layout_content(
+ let node = layout_content(
content,
tree,
renderer,
&layout::Limits::new(size, size),
);
- node.move_to(Point::new(region.x, region.y));
-
- Some(node)
+ Some(node.move_to(Point::new(region.x, region.y)))
})
.collect();
@@ -531,6 +527,8 @@ pub fn update<'a, Message, T: Draggable>(
on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
on_resize: &Option<(f32, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>,
) -> event::Status {
+ const DRAG_DEADBAND_DISTANCE: f32 = 10.0;
+
let mut event_status = event::Status::Ignored;
match event {
@@ -572,7 +570,6 @@ pub fn update<'a, Message, T: Draggable>(
shell,
contents,
on_click,
- on_drag,
);
}
}
@@ -584,7 +581,6 @@ pub fn update<'a, Message, T: Draggable>(
shell,
contents,
on_click,
- on_drag,
);
}
}
@@ -637,7 +633,49 @@ pub fn update<'a, Message, T: Draggable>(
}
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => {
- if let Some((_, on_resize)) = on_resize {
+ if let Some((_, origin)) = action.clicked_pane() {
+ if let Some(on_drag) = &on_drag {
+ let bounds = layout.bounds();
+
+ if let Some(cursor_position) = cursor.position_over(bounds)
+ {
+ let mut clicked_region = contents
+ .zip(layout.children())
+ .filter(|(_, layout)| {
+ layout.bounds().contains(cursor_position)
+ });
+
+ if let Some(((pane, content), layout)) =
+ clicked_region.next()
+ {
+ if content
+ .can_be_dragged_at(layout, cursor_position)
+ {
+ let pane_position = layout.position();
+
+ let new_origin = cursor_position
+ - Vector::new(
+ pane_position.x,
+ pane_position.y,
+ );
+
+ if new_origin.distance(origin)
+ > DRAG_DEADBAND_DISTANCE
+ {
+ *action = state::Action::Dragging {
+ pane,
+ origin,
+ };
+
+ shell.publish(on_drag(DragEvent::Picked {
+ pane,
+ }));
+ }
+ }
+ }
+ }
+ }
+ } else if let Some((_, on_resize)) = on_resize {
if let Some((split, _)) = action.picked_split() {
let bounds = layout.bounds();
@@ -712,7 +750,6 @@ fn click_pane<'a, Message, T>(
shell: &mut Shell<'_, Message>,
contents: impl Iterator<Item = (Pane, T)>,
on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>,
- on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
) where
T: Draggable,
{
@@ -720,23 +757,15 @@ fn click_pane<'a, Message, T>(
.zip(layout.children())
.filter(|(_, layout)| layout.bounds().contains(cursor_position));
- if let Some(((pane, content), layout)) = clicked_region.next() {
+ if let Some(((pane, _), layout)) = clicked_region.next() {
if let Some(on_click) = &on_click {
shell.publish(on_click(pane));
}
- if let Some(on_drag) = &on_drag {
- if content.can_be_dragged_at(layout, cursor_position) {
- let pane_position = layout.position();
-
- let origin = cursor_position
- - Vector::new(pane_position.x, pane_position.y);
-
- *action = state::Action::Dragging { pane, origin };
-
- shell.publish(on_drag(DragEvent::Picked { pane }));
- }
- }
+ let pane_position = layout.position();
+ let origin =
+ cursor_position - Vector::new(pane_position.x, pane_position.y);
+ *action = state::Action::Clicking { pane, origin };
}
}
@@ -749,7 +778,7 @@ pub fn mouse_interaction(
spacing: f32,
resize_leeway: Option<f32>,
) -> Option<mouse::Interaction> {
- if action.picked_pane().is_some() {
+ if action.clicked_pane().is_some() || action.picked_pane().is_some() {
return Some(mouse::Interaction::Grabbing);
}
diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs
index 826ea663..ee00f186 100644
--- a/widget/src/pane_grid/content.rs
+++ b/widget/src/pane_grid/content.rs
@@ -165,7 +165,7 @@ where
let title_bar_size = title_bar_layout.size();
- let mut body_layout = self.body.as_widget().layout(
+ let body_layout = self.body.as_widget().layout(
&mut tree.children[0],
renderer,
&layout::Limits::new(
@@ -177,11 +177,12 @@ where
),
);
- body_layout.move_to(Point::new(0.0, title_bar_size.height));
-
layout::Node::with_children(
max_size,
- vec![title_bar_layout, body_layout],
+ vec![
+ title_bar_layout,
+ body_layout.move_to(Point::new(0.0, title_bar_size.height)),
+ ],
)
} else {
self.body.as_widget().layout(
diff --git a/widget/src/pane_grid/state.rs b/widget/src/pane_grid/state.rs
index 481cd770..5d1fe254 100644
--- a/widget/src/pane_grid/state.rs
+++ b/widget/src/pane_grid/state.rs
@@ -403,6 +403,15 @@ pub enum Action {
///
/// [`PaneGrid`]: super::PaneGrid
Idle,
+ /// A [`Pane`] in the [`PaneGrid`] is being clicked.
+ ///
+ /// [`PaneGrid`]: super::PaneGrid
+ Clicking {
+ /// The [`Pane`] being clicked.
+ pane: Pane,
+ /// The starting [`Point`] of the click interaction.
+ origin: Point,
+ },
/// A [`Pane`] in the [`PaneGrid`] is being dragged.
///
/// [`PaneGrid`]: super::PaneGrid
@@ -432,6 +441,14 @@ impl Action {
}
}
+ /// Returns the current [`Pane`] that is being clicked, if any.
+ pub fn clicked_pane(&self) -> Option<(Pane, Point)> {
+ match *self {
+ Action::Clicking { pane, origin, .. } => Some((pane, origin)),
+ _ => None,
+ }
+ }
+
/// Returns the current [`Split`] that is being dragged, if any.
pub fn picked_split(&self) -> Option<(Split, Axis)> {
match *self {
diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs
index f4dbb6b1..eb21b743 100644
--- a/widget/src/pane_grid/title_bar.rs
+++ b/widget/src/pane_grid/title_bar.rs
@@ -217,7 +217,7 @@ where
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let limits = limits.pad(self.padding);
+ let limits = limits.shrink(self.padding);
let max_size = limits.max();
let title_layout = self.content.as_widget().layout(
@@ -228,8 +228,8 @@ where
let title_size = title_layout.size();
- let mut node = if let Some(controls) = &self.controls {
- let mut controls_layout = controls.as_widget().layout(
+ let node = if let Some(controls) = &self.controls {
+ let controls_layout = controls.as_widget().layout(
&mut tree.children[1],
renderer,
&layout::Limits::new(Size::ZERO, max_size),
@@ -240,11 +240,13 @@ where
let height = title_size.height.max(controls_size.height);
- controls_layout.move_to(Point::new(space_before_controls, 0.0));
-
layout::Node::with_children(
Size::new(max_size.width, height),
- vec![title_layout, controls_layout],
+ vec![
+ title_layout,
+ controls_layout
+ .move_to(Point::new(space_before_controls, 0.0)),
+ ],
)
} else {
layout::Node::with_children(
@@ -253,9 +255,7 @@ where
)
};
- node.move_to(Point::new(self.padding.left, self.padding.top));
-
- layout::Node::with_children(node.size().pad(self.padding), vec![node])
+ layout::Node::container(node, self.padding)
}
pub(crate) fn operate(
diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs
index 27f32907..2e3aab6f 100644
--- a/widget/src/pick_list.rs
+++ b/widget/src/pick_list.rs
@@ -45,7 +45,7 @@ where
impl<'a, T: 'a, Message, Renderer> PickList<'a, T, Message, Renderer>
where
- T: ToString + Eq,
+ T: ToString + PartialEq,
[T]: ToOwned<Owned = Vec<T>>,
Renderer: text::Renderer,
Renderer::Theme: StyleSheet
@@ -145,7 +145,7 @@ where
impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer>
for PickList<'a, T, Message, Renderer>
where
- T: Clone + ToString + Eq + 'static,
+ T: Clone + ToString + PartialEq + 'static,
[T]: ToOwned<Owned = Vec<T>>,
Message: 'a,
Renderer: text::Renderer + 'a,
@@ -164,12 +164,11 @@ where
tree::State::new(State::<Renderer::Paragraph>::new())
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- Length::Shrink
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: Length::Shrink,
+ }
}
fn layout(
@@ -235,7 +234,7 @@ where
_style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
- _viewport: &Rectangle,
+ viewport: &Rectangle,
) {
let font = self.font.unwrap_or_else(|| renderer.default_font());
draw(
@@ -253,6 +252,7 @@ where
&self.handle,
&self.style,
|| tree.state.downcast_ref::<State<Renderer::Paragraph>>(),
+ viewport,
);
}
@@ -281,7 +281,7 @@ where
impl<'a, T: 'a, Message, Renderer> From<PickList<'a, T, Message, Renderer>>
for Element<'a, Message, Renderer>
where
- T: Clone + ToString + Eq + 'static,
+ T: Clone + ToString + PartialEq + 'static,
[T]: ToOwned<Owned = Vec<T>>,
Message: 'a,
Renderer: text::Renderer + 'a,
@@ -392,7 +392,6 @@ where
{
use std::f32;
- let limits = limits.width(width).height(Length::Shrink).pad(padding);
let font = font.unwrap_or_else(|| renderer.default_font());
let text_size = text_size.unwrap_or_else(|| renderer.default_size());
@@ -415,23 +414,17 @@ where
for (option, paragraph) in options.iter().zip(state.options.iter_mut()) {
let label = option.to_string();
- renderer.update_paragraph(
- paragraph,
- Text {
- content: &label,
- ..option_text
- },
- );
+ paragraph.update(Text {
+ content: &label,
+ ..option_text
+ });
}
if let Some(placeholder) = placeholder {
- renderer.update_paragraph(
- &mut state.placeholder,
- Text {
- content: placeholder,
- ..option_text
- },
- );
+ state.placeholder.update(Text {
+ content: placeholder,
+ ..option_text
+ });
}
let max_width = match width {
@@ -456,7 +449,11 @@ where
f32::from(text_line_height.to_absolute(text_size)),
);
- limits.resolve(intrinsic).pad(padding)
+ limits
+ .width(width)
+ .shrink(padding)
+ .resolve(width, Length::Shrink, intrinsic)
+ .expand(padding)
};
layout::Node::new(size)
@@ -637,6 +634,7 @@ pub fn draw<'a, T, Renderer>(
handle: &Handle<Renderer::Font>,
style: &<Renderer::Theme as StyleSheet>::Style,
state: impl FnOnce() -> &'a State<Renderer::Paragraph>,
+ viewport: &Rectangle,
) where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
@@ -721,6 +719,7 @@ pub fn draw<'a, T, Renderer>(
bounds.center_y(),
),
style.handle_color,
+ *viewport,
);
}
@@ -749,6 +748,7 @@ pub fn draw<'a, T, Renderer>(
} else {
style.placeholder_color
},
+ *viewport,
);
}
}
diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs
index 07de72d5..15f1277b 100644
--- a/widget/src/progress_bar.rs
+++ b/widget/src/progress_bar.rs
@@ -85,12 +85,11 @@ where
Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT))
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT)),
+ }
}
fn layout(
@@ -99,13 +98,11 @@ where
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let limits = limits
- .width(self.width)
- .height(self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT)));
-
- let size = limits.resolve(Size::ZERO);
-
- layout::Node::new(size)
+ layout::atomic(
+ limits,
+ self.width,
+ self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT)),
+ )
}
fn draw(
diff --git a/widget/src/qr_code.rs b/widget/src/qr_code.rs
index 1dc4da7f..a229eb59 100644
--- a/widget/src/qr_code.rs
+++ b/widget/src/qr_code.rs
@@ -50,12 +50,11 @@ impl<'a> QRCode<'a> {
}
impl<'a, Message, Theme> Widget<Message, Renderer<Theme>> for QRCode<'a> {
- fn width(&self) -> Length {
- Length::Shrink
- }
-
- fn height(&self) -> Length {
- Length::Shrink
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: Length::Shrink,
+ height: Length::Shrink,
+ }
}
fn layout(
diff --git a/widget/src/radio.rs b/widget/src/radio.rs
index 57acc033..f91b20b1 100644
--- a/widget/src/radio.rs
+++ b/widget/src/radio.rs
@@ -201,12 +201,11 @@ where
tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- Length::Shrink
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: Length::Shrink,
+ }
}
fn layout(
@@ -291,7 +290,7 @@ where
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
- _viewport: &Rectangle,
+ viewport: &Rectangle,
) {
let is_mouse_over = cursor.is_over(layout.bounds());
@@ -349,6 +348,7 @@ where
crate::text::Appearance {
color: custom_style.text_color,
},
+ viewport,
);
}
}
diff --git a/widget/src/row.rs b/widget/src/row.rs
index 7ca90fbb..90fd2926 100644
--- a/widget/src/row.rs
+++ b/widget/src/row.rs
@@ -7,7 +7,7 @@ use crate::core::renderer;
use crate::core::widget::{Operation, Tree};
use crate::core::{
Alignment, Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell,
- Widget,
+ Size, Widget,
};
/// A container that distributes its contents horizontally.
@@ -21,26 +21,29 @@ pub struct Row<'a, Message, Renderer = crate::Renderer> {
children: Vec<Element<'a, Message, Renderer>>,
}
-impl<'a, Message, Renderer> Row<'a, Message, Renderer> {
+impl<'a, Message, Renderer> Row<'a, Message, Renderer>
+where
+ Renderer: crate::core::Renderer,
+{
/// Creates an empty [`Row`].
pub fn new() -> Self {
- Self::with_children(Vec::new())
- }
-
- /// Creates a [`Row`] with the given elements.
- pub fn with_children(
- children: Vec<Element<'a, Message, Renderer>>,
- ) -> Self {
Row {
spacing: 0.0,
padding: Padding::ZERO,
width: Length::Shrink,
height: Length::Shrink,
align_items: Alignment::Start,
- children,
+ children: Vec::new(),
}
}
+ /// Creates a [`Row`] with the given elements.
+ pub fn with_children(
+ children: impl IntoIterator<Item = Element<'a, Message, Renderer>>,
+ ) -> Self {
+ children.into_iter().fold(Self::new(), Self::push)
+ }
+
/// Sets the horizontal spacing _between_ elements.
///
/// Custom margins per element do not exist in iced. You should use this
@@ -80,12 +83,26 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> {
mut self,
child: impl Into<Element<'a, Message, Renderer>>,
) -> Self {
- self.children.push(child.into());
+ let child = child.into();
+ let size = child.as_widget().size_hint();
+
+ if size.width.is_fill() {
+ self.width = Length::Fill;
+ }
+
+ if size.height.is_fill() {
+ self.height = Length::Fill;
+ }
+
+ self.children.push(child);
self
}
}
-impl<'a, Message, Renderer> Default for Row<'a, Message, Renderer> {
+impl<'a, Message, Renderer> Default for Row<'a, Message, Renderer>
+where
+ Renderer: crate::core::Renderer,
+{
fn default() -> Self {
Self::new()
}
@@ -104,12 +121,11 @@ where
tree.diff_children(&self.children);
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -118,12 +134,12 @@ where
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let limits = limits.width(self.width).height(self.height);
-
layout::flex::resolve(
layout::flex::Axis::Horizontal,
renderer,
- &limits,
+ limits,
+ self.width,
+ self.height,
self.padding,
self.spacing,
self.align_items,
@@ -213,15 +229,17 @@ where
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
- for ((child, state), layout) in self
- .children
- .iter()
- .zip(&tree.children)
- .zip(layout.children())
- {
- child
- .as_widget()
- .draw(state, renderer, theme, style, layout, cursor, viewport);
+ if let Some(viewport) = layout.bounds().intersection(viewport) {
+ for ((child, state), layout) in self
+ .children
+ .iter()
+ .zip(&tree.children)
+ .zip(layout.children())
+ {
+ child.as_widget().draw(
+ state, renderer, theme, style, layout, cursor, &viewport,
+ );
+ }
}
}
diff --git a/widget/src/rule.rs b/widget/src/rule.rs
index b5c5fa55..cded9cb1 100644
--- a/widget/src/rule.rs
+++ b/widget/src/rule.rs
@@ -62,12 +62,11 @@ where
Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -76,9 +75,7 @@ where
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let limits = limits.width(self.width).height(self.height);
-
- layout::Node::new(limits.resolve(Size::ZERO))
+ layout::atomic(limits, self.width, self.height)
}
fn draw(
diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs
index 49aed2f0..70db490a 100644
--- a/widget/src/scrollable.rs
+++ b/widget/src/scrollable.rs
@@ -220,12 +220,11 @@ where
tree.diff_children(std::slice::from_ref(&self.content));
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -470,28 +469,25 @@ pub fn layout<Renderer>(
direction: &Direction,
layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
) -> layout::Node {
- let limits = limits.width(width).height(height);
-
- let child_limits = layout::Limits::new(
- Size::new(limits.min().width, limits.min().height),
- Size::new(
- if direction.horizontal().is_some() {
- f32::INFINITY
- } else {
- limits.max().width
- },
- if direction.vertical().is_some() {
- f32::MAX
- } else {
- limits.max().height
- },
- ),
- );
-
- let content = layout_content(renderer, &child_limits);
- let size = limits.resolve(content.size());
+ layout::contained(limits, width, height, |limits| {
+ let child_limits = layout::Limits::new(
+ Size::new(limits.min().width, limits.min().height),
+ Size::new(
+ if direction.horizontal().is_some() {
+ f32::INFINITY
+ } else {
+ limits.max().width
+ },
+ if direction.vertical().is_some() {
+ f32::MAX
+ } else {
+ limits.max().height
+ },
+ ),
+ );
- layout::Node::with_children(size, vec![content])
+ layout_content(renderer, &child_limits)
+ })
}
/// Processes an [`Event`] and updates the [`State`] of a [`Scrollable`]
diff --git a/widget/src/shader.rs b/widget/src/shader.rs
new file mode 100644
index 00000000..16b68c55
--- /dev/null
+++ b/widget/src/shader.rs
@@ -0,0 +1,216 @@
+//! A custom shader widget for wgpu applications.
+mod event;
+mod program;
+
+pub use event::Event;
+pub use program::Program;
+
+use crate::core;
+use crate::core::layout::{self, Layout};
+use crate::core::mouse;
+use crate::core::renderer;
+use crate::core::widget::tree::{self, Tree};
+use crate::core::widget::{self, Widget};
+use crate::core::window;
+use crate::core::{Clipboard, Element, Length, Rectangle, Shell, Size};
+use crate::renderer::wgpu::primitive::pipeline;
+
+use std::marker::PhantomData;
+
+pub use crate::renderer::wgpu::wgpu;
+pub use pipeline::{Primitive, Storage};
+
+/// A widget which can render custom shaders with Iced's `wgpu` backend.
+///
+/// Must be initialized with a [`Program`], which describes the internal widget state & how
+/// its [`Program::Primitive`]s are drawn.
+#[allow(missing_debug_implementations)]
+pub struct Shader<Message, P: Program<Message>> {
+ width: Length,
+ height: Length,
+ program: P,
+ _message: PhantomData<Message>,
+}
+
+impl<Message, P: Program<Message>> Shader<Message, P> {
+ /// Create a new custom [`Shader`].
+ pub fn new(program: P) -> Self {
+ Self {
+ width: Length::Fixed(100.0),
+ height: Length::Fixed(100.0),
+ program,
+ _message: PhantomData,
+ }
+ }
+
+ /// Set the `width` of the custom [`Shader`].
+ pub fn width(mut self, width: impl Into<Length>) -> Self {
+ self.width = width.into();
+ self
+ }
+
+ /// Set the `height` of the custom [`Shader`].
+ pub fn height(mut self, height: impl Into<Length>) -> Self {
+ self.height = height.into();
+ self
+ }
+}
+
+impl<P, Message, Renderer> Widget<Message, Renderer> for Shader<Message, P>
+where
+ P: Program<Message>,
+ Renderer: pipeline::Renderer,
+{
+ fn tag(&self) -> tree::Tag {
+ struct Tag<T>(T);
+ tree::Tag::of::<Tag<P::State>>()
+ }
+
+ fn state(&self) -> tree::State {
+ tree::State::new(P::State::default())
+ }
+
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
+ }
+
+ fn layout(
+ &self,
+ _tree: &mut Tree,
+ _renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ layout::atomic(limits, self.width, self.height)
+ }
+
+ fn on_event(
+ &mut self,
+ tree: &mut Tree,
+ event: crate::core::Event,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ _renderer: &Renderer,
+ _clipboard: &mut dyn Clipboard,
+ shell: &mut Shell<'_, Message>,
+ _viewport: &Rectangle,
+ ) -> event::Status {
+ let bounds = layout.bounds();
+
+ let custom_shader_event = match event {
+ core::Event::Mouse(mouse_event) => Some(Event::Mouse(mouse_event)),
+ core::Event::Keyboard(keyboard_event) => {
+ Some(Event::Keyboard(keyboard_event))
+ }
+ core::Event::Touch(touch_event) => Some(Event::Touch(touch_event)),
+ core::Event::Window(_, window::Event::RedrawRequested(instant)) => {
+ Some(Event::RedrawRequested(instant))
+ }
+ _ => None,
+ };
+
+ if let Some(custom_shader_event) = custom_shader_event {
+ let state = tree.state.downcast_mut::<P::State>();
+
+ let (event_status, message) = self.program.update(
+ state,
+ custom_shader_event,
+ bounds,
+ cursor,
+ shell,
+ );
+
+ if let Some(message) = message {
+ shell.publish(message);
+ }
+
+ return event_status;
+ }
+
+ event::Status::Ignored
+ }
+
+ fn mouse_interaction(
+ &self,
+ tree: &Tree,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ _viewport: &Rectangle,
+ _renderer: &Renderer,
+ ) -> mouse::Interaction {
+ let bounds = layout.bounds();
+ let state = tree.state.downcast_ref::<P::State>();
+
+ self.program.mouse_interaction(state, bounds, cursor)
+ }
+
+ fn draw(
+ &self,
+ tree: &widget::Tree,
+ renderer: &mut Renderer,
+ _theme: &Renderer::Theme,
+ _style: &renderer::Style,
+ layout: Layout<'_>,
+ cursor_position: mouse::Cursor,
+ _viewport: &Rectangle,
+ ) {
+ let bounds = layout.bounds();
+ let state = tree.state.downcast_ref::<P::State>();
+
+ renderer.draw_pipeline_primitive(
+ bounds,
+ self.program.draw(state, cursor_position, bounds),
+ );
+ }
+}
+
+impl<'a, Message, Renderer, P> From<Shader<Message, P>>
+ for Element<'a, Message, Renderer>
+where
+ Message: 'a,
+ Renderer: pipeline::Renderer,
+ P: Program<Message> + 'a,
+{
+ fn from(custom: Shader<Message, P>) -> Element<'a, Message, Renderer> {
+ Element::new(custom)
+ }
+}
+
+impl<Message, T> Program<Message> for &T
+where
+ T: Program<Message>,
+{
+ type State = T::State;
+ type Primitive = T::Primitive;
+
+ fn update(
+ &self,
+ state: &mut Self::State,
+ event: Event,
+ bounds: Rectangle,
+ cursor: mouse::Cursor,
+ shell: &mut Shell<'_, Message>,
+ ) -> (event::Status, Option<Message>) {
+ T::update(self, state, event, bounds, cursor, shell)
+ }
+
+ fn draw(
+ &self,
+ state: &Self::State,
+ cursor: mouse::Cursor,
+ bounds: Rectangle,
+ ) -> Self::Primitive {
+ T::draw(self, state, cursor, bounds)
+ }
+
+ fn mouse_interaction(
+ &self,
+ state: &Self::State,
+ bounds: Rectangle,
+ cursor: mouse::Cursor,
+ ) -> mouse::Interaction {
+ T::mouse_interaction(self, state, bounds, cursor)
+ }
+}
diff --git a/widget/src/shader/event.rs b/widget/src/shader/event.rs
new file mode 100644
index 00000000..005c8725
--- /dev/null
+++ b/widget/src/shader/event.rs
@@ -0,0 +1,25 @@
+//! Handle events of a custom shader widget.
+use crate::core::keyboard;
+use crate::core::mouse;
+use crate::core::time::Instant;
+use crate::core::touch;
+
+pub use crate::core::event::Status;
+
+/// A [`Shader`] event.
+///
+/// [`Shader`]: crate::Shader
+#[derive(Debug, Clone, PartialEq)]
+pub enum Event {
+ /// A mouse event.
+ Mouse(mouse::Event),
+
+ /// A touch event.
+ Touch(touch::Event),
+
+ /// A keyboard event.
+ Keyboard(keyboard::Event),
+
+ /// A window requested a redraw.
+ RedrawRequested(Instant),
+}
diff --git a/widget/src/shader/program.rs b/widget/src/shader/program.rs
new file mode 100644
index 00000000..6dd50404
--- /dev/null
+++ b/widget/src/shader/program.rs
@@ -0,0 +1,62 @@
+use crate::core::event;
+use crate::core::mouse;
+use crate::core::{Rectangle, Shell};
+use crate::renderer::wgpu::primitive::pipeline;
+use crate::shader;
+
+/// The state and logic of a [`Shader`] widget.
+///
+/// A [`Program`] can mutate the internal state of a [`Shader`] widget
+/// and produce messages for an application.
+///
+/// [`Shader`]: crate::Shader
+pub trait Program<Message> {
+ /// The internal state of the [`Program`].
+ type State: Default + 'static;
+
+ /// The type of primitive this [`Program`] can draw.
+ type Primitive: pipeline::Primitive + 'static;
+
+ /// Update the internal [`State`] of the [`Program`]. This can be used to reflect state changes
+ /// based on mouse & other events. You can use the [`Shell`] to publish messages, request a
+ /// redraw for the window, etc.
+ ///
+ /// By default, this method does and returns nothing.
+ ///
+ /// [`State`]: Self::State
+ fn update(
+ &self,
+ _state: &mut Self::State,
+ _event: shader::Event,
+ _bounds: Rectangle,
+ _cursor: mouse::Cursor,
+ _shell: &mut Shell<'_, Message>,
+ ) -> (event::Status, Option<Message>) {
+ (event::Status::Ignored, None)
+ }
+
+ /// Draws the [`Primitive`].
+ ///
+ /// [`Primitive`]: Self::Primitive
+ fn draw(
+ &self,
+ state: &Self::State,
+ cursor: mouse::Cursor,
+ bounds: Rectangle,
+ ) -> Self::Primitive;
+
+ /// Returns the current mouse interaction of the [`Program`].
+ ///
+ /// The interaction returned will be in effect even if the cursor position is out of
+ /// bounds of the [`Shader`]'s program.
+ ///
+ /// [`Shader`]: crate::Shader
+ fn mouse_interaction(
+ &self,
+ _state: &Self::State,
+ _bounds: Rectangle,
+ _cursor: mouse::Cursor,
+ ) -> mouse::Interaction {
+ mouse::Interaction::default()
+ }
+}
diff --git a/widget/src/slider.rs b/widget/src/slider.rs
index ac0982c8..1bc94661 100644
--- a/widget/src/slider.rs
+++ b/widget/src/slider.rs
@@ -159,12 +159,11 @@ where
tree::State::new(State::new())
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- Length::Shrink
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: Length::Shrink,
+ }
}
fn layout(
@@ -173,10 +172,7 @@ where
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let limits = limits.width(self.width).height(self.height);
- let size = limits.resolve(Size::ZERO);
-
- layout::Node::new(size)
+ layout::atomic(limits, self.width, self.height)
}
fn on_event(
diff --git a/widget/src/space.rs b/widget/src/space.rs
index e5a8f169..eef990d1 100644
--- a/widget/src/space.rs
+++ b/widget/src/space.rs
@@ -45,12 +45,11 @@ impl<Message, Renderer> Widget<Message, Renderer> for Space
where
Renderer: core::Renderer,
{
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -59,9 +58,7 @@ where
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let limits = limits.width(self.width).height(self.height);
-
- layout::Node::new(limits.resolve(Size::ZERO))
+ layout::atomic(limits, self.width, self.height)
}
fn draw(
diff --git a/widget/src/svg.rs b/widget/src/svg.rs
index 2d01d1ab..2357cf65 100644
--- a/widget/src/svg.rs
+++ b/widget/src/svg.rs
@@ -96,12 +96,11 @@ where
Renderer: svg::Renderer,
Renderer::Theme: iced_style::svg::StyleSheet,
{
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
}
fn layout(
@@ -115,10 +114,7 @@ where
let image_size = Size::new(width as f32, height as f32);
// The size to be available to the widget prior to `Shrink`ing
- let raw_size = limits
- .width(self.width)
- .height(self.height)
- .resolve(image_size);
+ let raw_size = limits.resolve(self.width, self.height, image_size);
// The uncropped size of the image when fit to the bounds above
let full_size = self.content_fit.fit(image_size, raw_size);
@@ -145,7 +141,7 @@ where
theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
- _cursor: mouse::Cursor,
+ cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
let Size { width, height } = renderer.dimensions(&self.handle);
@@ -153,6 +149,7 @@ where
let bounds = layout.bounds();
let adjusted_fit = self.content_fit.fit(image_size, bounds.size());
+ let is_mouse_over = cursor.is_over(bounds);
let render = |renderer: &mut Renderer| {
let offset = Vector::new(
@@ -166,7 +163,11 @@ where
..bounds
};
- let appearance = theme.appearance(&self.style);
+ let appearance = if is_mouse_over {
+ theme.hovered(&self.style)
+ } else {
+ theme.appearance(&self.style)
+ };
renderer.draw(
self.handle.clone(),
diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs
new file mode 100644
index 00000000..09a0cac0
--- /dev/null
+++ b/widget/src/text_editor.rs
@@ -0,0 +1,736 @@
+//! Display a multi-line text input for text editing.
+use crate::core::event::{self, Event};
+use crate::core::keyboard;
+use crate::core::keyboard::key;
+use crate::core::layout::{self, Layout};
+use crate::core::mouse;
+use crate::core::renderer;
+use crate::core::text::editor::{Cursor, Editor as _};
+use crate::core::text::highlighter::{self, Highlighter};
+use crate::core::text::{self, LineHeight};
+use crate::core::widget::{self, Widget};
+use crate::core::{
+ Clipboard, Color, Element, Length, Padding, Pixels, Rectangle, Shell, Size,
+ Vector,
+};
+
+use std::cell::RefCell;
+use std::fmt;
+use std::ops::DerefMut;
+use std::sync::Arc;
+
+pub use crate::style::text_editor::{Appearance, StyleSheet};
+pub use text::editor::{Action, Edit, Motion};
+
+/// A multi-line text input.
+#[allow(missing_debug_implementations)]
+pub struct TextEditor<'a, Highlighter, Message, Renderer = crate::Renderer>
+where
+ Highlighter: text::Highlighter,
+ Renderer: text::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ content: &'a Content<Renderer>,
+ font: Option<Renderer::Font>,
+ text_size: Option<Pixels>,
+ line_height: LineHeight,
+ width: Length,
+ height: Length,
+ padding: Padding,
+ style: <Renderer::Theme as StyleSheet>::Style,
+ on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>,
+ highlighter_settings: Highlighter::Settings,
+ highlighter_format: fn(
+ &Highlighter::Highlight,
+ &Renderer::Theme,
+ ) -> highlighter::Format<Renderer::Font>,
+}
+
+impl<'a, Message, Renderer>
+ TextEditor<'a, highlighter::PlainText, Message, Renderer>
+where
+ Renderer: text::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ /// Creates new [`TextEditor`] with the given [`Content`].
+ pub fn new(content: &'a Content<Renderer>) -> Self {
+ Self {
+ content,
+ font: None,
+ text_size: None,
+ line_height: LineHeight::default(),
+ width: Length::Fill,
+ height: Length::Fill,
+ padding: Padding::new(5.0),
+ style: Default::default(),
+ on_edit: None,
+ highlighter_settings: (),
+ highlighter_format: |_highlight, _theme| {
+ highlighter::Format::default()
+ },
+ }
+ }
+}
+
+impl<'a, Highlighter, Message, Renderer>
+ TextEditor<'a, Highlighter, Message, Renderer>
+where
+ Highlighter: text::Highlighter,
+ Renderer: text::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ /// Sets the message that should be produced when some action is performed in
+ /// the [`TextEditor`].
+ ///
+ /// If this method is not called, the [`TextEditor`] will be disabled.
+ pub fn on_action(
+ mut self,
+ on_edit: impl Fn(Action) -> Message + 'a,
+ ) -> Self {
+ self.on_edit = Some(Box::new(on_edit));
+ self
+ }
+
+ /// Sets the [`Font`] of the [`TextEditor`].
+ ///
+ /// [`Font`]: text::Renderer::Font
+ pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
+ self.font = Some(font.into());
+ self
+ }
+
+ /// Sets the [`Padding`] of the [`TextEditor`].
+ pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
+ self.padding = padding.into();
+ self
+ }
+
+ /// Highlights the [`TextEditor`] with the given [`Highlighter`] and
+ /// a strategy to turn its highlights into some text format.
+ pub fn highlight<H: text::Highlighter>(
+ self,
+ settings: H::Settings,
+ to_format: fn(
+ &H::Highlight,
+ &Renderer::Theme,
+ ) -> highlighter::Format<Renderer::Font>,
+ ) -> TextEditor<'a, H, Message, Renderer> {
+ TextEditor {
+ content: self.content,
+ font: self.font,
+ text_size: self.text_size,
+ line_height: self.line_height,
+ width: self.width,
+ height: self.height,
+ padding: self.padding,
+ style: self.style,
+ on_edit: self.on_edit,
+ highlighter_settings: settings,
+ highlighter_format: to_format,
+ }
+ }
+
+ /// Sets the style of the [`TextEditor`].
+ pub fn style(
+ mut self,
+ style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
+ ) -> Self {
+ self.style = style.into();
+ self
+ }
+}
+
+/// The content of a [`TextEditor`].
+pub struct Content<R = crate::Renderer>(RefCell<Internal<R>>)
+where
+ R: text::Renderer;
+
+struct Internal<R>
+where
+ R: text::Renderer,
+{
+ editor: R::Editor,
+ is_dirty: bool,
+}
+
+impl<R> Content<R>
+where
+ R: text::Renderer,
+{
+ /// Creates an empty [`Content`].
+ pub fn new() -> Self {
+ Self::with_text("")
+ }
+
+ /// Creates a [`Content`] with the given text.
+ pub fn with_text(text: &str) -> Self {
+ Self(RefCell::new(Internal {
+ editor: R::Editor::with_text(text),
+ is_dirty: true,
+ }))
+ }
+
+ /// Performs an [`Action`] on the [`Content`].
+ pub fn perform(&mut self, action: Action) {
+ let internal = self.0.get_mut();
+
+ internal.editor.perform(action);
+ internal.is_dirty = true;
+ }
+
+ /// Returns the amount of lines of the [`Content`].
+ pub fn line_count(&self) -> usize {
+ self.0.borrow().editor.line_count()
+ }
+
+ /// Returns the text of the line at the given index, if it exists.
+ pub fn line(
+ &self,
+ index: usize,
+ ) -> Option<impl std::ops::Deref<Target = str> + '_> {
+ std::cell::Ref::filter_map(self.0.borrow(), |internal| {
+ internal.editor.line(index)
+ })
+ .ok()
+ }
+
+ /// Returns an iterator of the text of the lines in the [`Content`].
+ pub fn lines(
+ &self,
+ ) -> impl Iterator<Item = impl std::ops::Deref<Target = str> + '_> {
+ struct Lines<'a, Renderer: text::Renderer> {
+ internal: std::cell::Ref<'a, Internal<Renderer>>,
+ current: usize,
+ }
+
+ impl<'a, Renderer: text::Renderer> Iterator for Lines<'a, Renderer> {
+ type Item = std::cell::Ref<'a, str>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let line = std::cell::Ref::filter_map(
+ std::cell::Ref::clone(&self.internal),
+ |internal| internal.editor.line(self.current),
+ )
+ .ok()?;
+
+ self.current += 1;
+
+ Some(line)
+ }
+ }
+
+ Lines {
+ internal: self.0.borrow(),
+ current: 0,
+ }
+ }
+
+ /// Returns the text of the [`Content`].
+ ///
+ /// Lines are joined with `'\n'`.
+ pub fn text(&self) -> String {
+ let mut text = self.lines().enumerate().fold(
+ String::new(),
+ |mut contents, (i, line)| {
+ if i > 0 {
+ contents.push('\n');
+ }
+
+ contents.push_str(&line);
+
+ contents
+ },
+ );
+
+ if !text.ends_with('\n') {
+ text.push('\n');
+ }
+
+ text
+ }
+
+ /// Returns the selected text of the [`Content`].
+ pub fn selection(&self) -> Option<String> {
+ self.0.borrow().editor.selection()
+ }
+
+ /// Returns the current cursor position of the [`Content`].
+ pub fn cursor_position(&self) -> (usize, usize) {
+ self.0.borrow().editor.cursor_position()
+ }
+}
+
+impl<Renderer> Default for Content<Renderer>
+where
+ Renderer: text::Renderer,
+{
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl<Renderer> fmt::Debug for Content<Renderer>
+where
+ Renderer: text::Renderer,
+ Renderer::Editor: fmt::Debug,
+{
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let internal = self.0.borrow();
+
+ f.debug_struct("Content")
+ .field("editor", &internal.editor)
+ .field("is_dirty", &internal.is_dirty)
+ .finish()
+ }
+}
+
+struct State<Highlighter: text::Highlighter> {
+ is_focused: bool,
+ last_click: Option<mouse::Click>,
+ drag_click: Option<mouse::click::Kind>,
+ highlighter: RefCell<Highlighter>,
+ highlighter_settings: Highlighter::Settings,
+ highlighter_format_address: usize,
+}
+
+impl<'a, Highlighter, Message, Renderer> Widget<Message, Renderer>
+ for TextEditor<'a, Highlighter, Message, Renderer>
+where
+ Highlighter: text::Highlighter,
+ Renderer: text::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ fn tag(&self) -> widget::tree::Tag {
+ widget::tree::Tag::of::<State<Highlighter>>()
+ }
+
+ fn state(&self) -> widget::tree::State {
+ widget::tree::State::new(State {
+ is_focused: false,
+ last_click: None,
+ drag_click: None,
+ highlighter: RefCell::new(Highlighter::new(
+ &self.highlighter_settings,
+ )),
+ highlighter_settings: self.highlighter_settings.clone(),
+ highlighter_format_address: self.highlighter_format as usize,
+ })
+ }
+
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
+ }
+
+ fn layout(
+ &self,
+ tree: &mut widget::Tree,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> iced_renderer::core::layout::Node {
+ let mut internal = self.content.0.borrow_mut();
+ let state = tree.state.downcast_mut::<State<Highlighter>>();
+
+ if state.highlighter_format_address != self.highlighter_format as usize
+ {
+ state.highlighter.borrow_mut().change_line(0);
+
+ state.highlighter_format_address = self.highlighter_format as usize;
+ }
+
+ if state.highlighter_settings != self.highlighter_settings {
+ state
+ .highlighter
+ .borrow_mut()
+ .update(&self.highlighter_settings);
+
+ state.highlighter_settings = self.highlighter_settings.clone();
+ }
+
+ internal.editor.update(
+ limits.shrink(self.padding).max(),
+ self.font.unwrap_or_else(|| renderer.default_font()),
+ self.text_size.unwrap_or_else(|| renderer.default_size()),
+ self.line_height,
+ state.highlighter.borrow_mut().deref_mut(),
+ );
+
+ layout::Node::new(limits.max())
+ }
+
+ fn on_event(
+ &mut self,
+ tree: &mut widget::Tree,
+ event: Event,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ _renderer: &Renderer,
+ clipboard: &mut dyn Clipboard,
+ shell: &mut Shell<'_, Message>,
+ _viewport: &Rectangle,
+ ) -> event::Status {
+ let Some(on_edit) = self.on_edit.as_ref() else {
+ return event::Status::Ignored;
+ };
+
+ let state = tree.state.downcast_mut::<State<Highlighter>>();
+
+ let Some(update) = Update::from_event(
+ event,
+ state,
+ layout.bounds(),
+ self.padding,
+ cursor,
+ ) else {
+ return event::Status::Ignored;
+ };
+
+ match update {
+ Update::Click(click) => {
+ let action = match click.kind() {
+ mouse::click::Kind::Single => {
+ Action::Click(click.position())
+ }
+ mouse::click::Kind::Double => Action::SelectWord,
+ mouse::click::Kind::Triple => Action::SelectLine,
+ };
+
+ state.is_focused = true;
+ state.last_click = Some(click);
+ state.drag_click = Some(click.kind());
+
+ shell.publish(on_edit(action));
+ }
+ Update::Unfocus => {
+ state.is_focused = false;
+ state.drag_click = None;
+ }
+ Update::Release => {
+ state.drag_click = None;
+ }
+ Update::Action(action) => {
+ shell.publish(on_edit(action));
+ }
+ Update::Copy => {
+ if let Some(selection) = self.content.selection() {
+ clipboard.write(selection);
+ }
+ }
+ Update::Paste => {
+ if let Some(contents) = clipboard.read() {
+ shell.publish(on_edit(Action::Edit(Edit::Paste(
+ Arc::new(contents),
+ ))));
+ }
+ }
+ }
+
+ event::Status::Captured
+ }
+
+ fn draw(
+ &self,
+ tree: &widget::Tree,
+ renderer: &mut Renderer,
+ theme: &<Renderer as renderer::Renderer>::Theme,
+ style: &renderer::Style,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ viewport: &Rectangle,
+ ) {
+ let bounds = layout.bounds();
+
+ let mut internal = self.content.0.borrow_mut();
+ let state = tree.state.downcast_ref::<State<Highlighter>>();
+
+ internal.editor.highlight(
+ self.font.unwrap_or_else(|| renderer.default_font()),
+ state.highlighter.borrow_mut().deref_mut(),
+ |highlight| (self.highlighter_format)(highlight, theme),
+ );
+
+ let is_disabled = self.on_edit.is_none();
+ let is_mouse_over = cursor.is_over(bounds);
+
+ let appearance = if is_disabled {
+ theme.disabled(&self.style)
+ } else if state.is_focused {
+ theme.focused(&self.style)
+ } else if is_mouse_over {
+ theme.hovered(&self.style)
+ } else {
+ theme.active(&self.style)
+ };
+
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds,
+ border_radius: appearance.border_radius,
+ border_width: appearance.border_width,
+ border_color: appearance.border_color,
+ },
+ appearance.background,
+ );
+
+ renderer.fill_editor(
+ &internal.editor,
+ bounds.position()
+ + Vector::new(self.padding.left, self.padding.top),
+ style.text_color,
+ *viewport,
+ );
+
+ let translation = Vector::new(
+ bounds.x + self.padding.left,
+ bounds.y + self.padding.top,
+ );
+
+ if state.is_focused {
+ match internal.editor.cursor() {
+ Cursor::Caret(position) => {
+ let position = position + translation;
+
+ if bounds.contains(position) {
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: Rectangle {
+ x: position.x,
+ y: position.y,
+ width: 1.0,
+ height: self
+ .line_height
+ .to_absolute(
+ self.text_size.unwrap_or_else(
+ || renderer.default_size(),
+ ),
+ )
+ .into(),
+ },
+ border_radius: 0.0.into(),
+ border_width: 0.0,
+ border_color: Color::TRANSPARENT,
+ },
+ theme.value_color(&self.style),
+ );
+ }
+ }
+ Cursor::Selection(ranges) => {
+ for range in ranges.into_iter().filter_map(|range| {
+ bounds.intersection(&(range + translation))
+ }) {
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: range,
+ border_radius: 0.0.into(),
+ border_width: 0.0,
+ border_color: Color::TRANSPARENT,
+ },
+ theme.selection_color(&self.style),
+ );
+ }
+ }
+ }
+ }
+ }
+
+ fn mouse_interaction(
+ &self,
+ _state: &widget::Tree,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ _viewport: &Rectangle,
+ _renderer: &Renderer,
+ ) -> mouse::Interaction {
+ let is_disabled = self.on_edit.is_none();
+
+ if cursor.is_over(layout.bounds()) {
+ if is_disabled {
+ mouse::Interaction::NotAllowed
+ } else {
+ mouse::Interaction::Text
+ }
+ } else {
+ mouse::Interaction::default()
+ }
+ }
+}
+
+impl<'a, Highlighter, Message, Renderer>
+ From<TextEditor<'a, Highlighter, Message, Renderer>>
+ for Element<'a, Message, Renderer>
+where
+ Highlighter: text::Highlighter,
+ Message: 'a,
+ Renderer: text::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ fn from(
+ text_editor: TextEditor<'a, Highlighter, Message, Renderer>,
+ ) -> Self {
+ Self::new(text_editor)
+ }
+}
+
+enum Update {
+ Click(mouse::Click),
+ Unfocus,
+ Release,
+ Action(Action),
+ Copy,
+ Paste,
+}
+
+impl Update {
+ fn from_event<H: Highlighter>(
+ event: Event,
+ state: &State<H>,
+ bounds: Rectangle,
+ padding: Padding,
+ cursor: mouse::Cursor,
+ ) -> Option<Self> {
+ let action = |action| Some(Update::Action(action));
+ let edit = |edit| action(Action::Edit(edit));
+
+ match event {
+ Event::Mouse(event) => match event {
+ mouse::Event::ButtonPressed(mouse::Button::Left) => {
+ if let Some(cursor_position) = cursor.position_in(bounds) {
+ let cursor_position = cursor_position
+ - Vector::new(padding.top, padding.left);
+
+ let click = mouse::Click::new(
+ cursor_position,
+ state.last_click,
+ );
+
+ Some(Update::Click(click))
+ } else if state.is_focused {
+ Some(Update::Unfocus)
+ } else {
+ None
+ }
+ }
+ mouse::Event::ButtonReleased(mouse::Button::Left) => {
+ Some(Update::Release)
+ }
+ mouse::Event::CursorMoved { .. } => match state.drag_click {
+ Some(mouse::click::Kind::Single) => {
+ let cursor_position = cursor.position_in(bounds)?
+ - Vector::new(padding.top, padding.left);
+
+ action(Action::Drag(cursor_position))
+ }
+ _ => None,
+ },
+ mouse::Event::WheelScrolled { delta }
+ if cursor.is_over(bounds) =>
+ {
+ action(Action::Scroll {
+ lines: match delta {
+ mouse::ScrollDelta::Lines { y, .. } => {
+ if y.abs() > 0.0 {
+ (y.signum() * -(y.abs() * 4.0).max(1.0))
+ as i32
+ } else {
+ 0
+ }
+ }
+ mouse::ScrollDelta::Pixels { y, .. } => {
+ (-y / 4.0) as i32
+ }
+ },
+ })
+ }
+ _ => None,
+ },
+ Event::Keyboard(event) => match event {
+ keyboard::Event::KeyPressed {
+ key,
+ modifiers,
+ text,
+ ..
+ } if state.is_focused => {
+ if let keyboard::Key::Named(named_key) = key.as_ref() {
+ if let Some(motion) = motion(named_key) {
+ let motion = if platform::is_jump_modifier_pressed(
+ modifiers,
+ ) {
+ motion.widen()
+ } else {
+ motion
+ };
+
+ return action(if modifiers.shift() {
+ Action::Select(motion)
+ } else {
+ Action::Move(motion)
+ });
+ }
+ }
+
+ match key.as_ref() {
+ keyboard::Key::Named(key::Named::Enter) => {
+ edit(Edit::Enter)
+ }
+ keyboard::Key::Named(key::Named::Backspace) => {
+ edit(Edit::Backspace)
+ }
+ keyboard::Key::Named(key::Named::Delete) => {
+ edit(Edit::Delete)
+ }
+ keyboard::Key::Named(key::Named::Escape) => {
+ Some(Self::Unfocus)
+ }
+ keyboard::Key::Character("c")
+ if modifiers.command() =>
+ {
+ Some(Self::Copy)
+ }
+ keyboard::Key::Character("v")
+ if modifiers.command() && !modifiers.alt() =>
+ {
+ Some(Self::Paste)
+ }
+ _ => {
+ let text = text?;
+
+ edit(Edit::Insert(
+ text.chars().next().unwrap_or_default(),
+ ))
+ }
+ }
+ }
+ _ => None,
+ },
+ _ => None,
+ }
+ }
+}
+
+fn motion(key: key::Named) -> Option<Motion> {
+ match key {
+ key::Named::ArrowLeft => Some(Motion::Left),
+ key::Named::ArrowRight => Some(Motion::Right),
+ key::Named::ArrowUp => Some(Motion::Up),
+ key::Named::ArrowDown => Some(Motion::Down),
+ key::Named::Home => Some(Motion::Home),
+ key::Named::End => Some(Motion::End),
+ key::Named::PageUp => Some(Motion::PageUp),
+ key::Named::PageDown => Some(Motion::PageDown),
+ _ => None,
+ }
+}
+
+mod platform {
+ use crate::core::keyboard;
+
+ pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool {
+ if cfg!(target_os = "macos") {
+ modifiers.alt()
+ } else {
+ modifiers.control()
+ }
+ }
+}
diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs
index 9e1fb796..c3dce8be 100644
--- a/widget/src/text_input.rs
+++ b/widget/src/text_input.rs
@@ -14,6 +14,7 @@ use editor::Editor;
use crate::core::alignment;
use crate::core::event::{self, Event};
use crate::core::keyboard;
+use crate::core::keyboard::key;
use crate::core::layout;
use crate::core::mouse::{self, click};
use crate::core::renderer;
@@ -238,6 +239,7 @@ where
layout: Layout<'_>,
cursor: mouse::Cursor,
value: Option<&Value>,
+ viewport: &Rectangle,
) {
draw(
renderer,
@@ -250,6 +252,7 @@ where
self.is_secure,
self.icon.as_ref(),
&self.style,
+ viewport,
);
}
}
@@ -281,12 +284,11 @@ where
}
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- Length::Shrink
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: Length::Shrink,
+ }
}
fn layout(
@@ -362,7 +364,7 @@ where
_style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
- _viewport: &Rectangle,
+ viewport: &Rectangle,
) {
draw(
renderer,
@@ -375,6 +377,7 @@ where
self.is_secure,
self.icon.as_ref(),
&self.style,
+ viewport,
);
}
@@ -503,14 +506,11 @@ where
{
let font = font.unwrap_or_else(|| renderer.default_font());
let text_size = size.unwrap_or_else(|| renderer.default_size());
-
let padding = padding.fit(Size::ZERO, limits.max());
- let limits = limits
- .width(width)
- .pad(padding)
- .height(line_height.to_absolute(text_size));
+ let height = line_height.to_absolute(text_size);
- let text_bounds = limits.resolve(Size::ZERO);
+ let limits = limits.width(width).shrink(padding);
+ let text_bounds = limits.resolve(width, height, Size::ZERO);
let placeholder_text = Text {
font,
@@ -523,18 +523,15 @@ where
shaping: text::Shaping::Advanced,
};
- renderer.update_paragraph(&mut state.placeholder, placeholder_text);
+ state.placeholder.update(placeholder_text);
let secure_value = is_secure.then(|| value.secure());
let value = secure_value.as_ref().unwrap_or(value);
- renderer.update_paragraph(
- &mut state.value,
- Text {
- content: &value.to_string(),
- ..placeholder_text
- },
- );
+ state.value.update(Text {
+ content: &value.to_string(),
+ ..placeholder_text
+ });
if let Some(icon) = icon {
let icon_text = Text {
@@ -548,45 +545,45 @@ where
shaping: text::Shaping::Advanced,
};
- renderer.update_paragraph(&mut state.icon, icon_text);
+ state.icon.update(icon_text);
let icon_width = state.icon.min_width();
- let mut text_node = layout::Node::new(
- text_bounds - Size::new(icon_width + icon.spacing, 0.0),
- );
-
- let mut icon_node =
- layout::Node::new(Size::new(icon_width, text_bounds.height));
-
- match icon.side {
- Side::Left => {
- text_node.move_to(Point::new(
+ let (text_position, icon_position) = match icon.side {
+ Side::Left => (
+ Point::new(
padding.left + icon_width + icon.spacing,
padding.top,
- ));
-
- icon_node.move_to(Point::new(padding.left, padding.top));
- }
- Side::Right => {
- text_node.move_to(Point::new(padding.left, padding.top));
-
- icon_node.move_to(Point::new(
+ ),
+ Point::new(padding.left, padding.top),
+ ),
+ Side::Right => (
+ Point::new(padding.left, padding.top),
+ Point::new(
padding.left + text_bounds.width - icon_width,
padding.top,
- ));
- }
+ ),
+ ),
};
+ let text_node = layout::Node::new(
+ text_bounds - Size::new(icon_width + icon.spacing, 0.0),
+ )
+ .move_to(text_position);
+
+ let icon_node =
+ layout::Node::new(Size::new(icon_width, text_bounds.height))
+ .move_to(icon_position);
+
layout::Node::with_children(
- text_bounds.pad(padding),
+ text_bounds.expand(padding),
vec![text_node, icon_node],
)
} else {
- let mut text = layout::Node::new(text_bounds);
- text.move_to(Point::new(padding.left, padding.top));
+ let text = layout::Node::new(text_bounds)
+ .move_to(Point::new(padding.left, padding.top));
- layout::Node::with_children(text_bounds.pad(padding), vec![text])
+ layout::Node::with_children(text_bounds.expand(padding), vec![text])
}
}
@@ -752,34 +749,7 @@ where
return event::Status::Captured;
}
}
- Event::Keyboard(keyboard::Event::CharacterReceived(c)) => {
- let state = state();
-
- if let Some(focus) = &mut state.is_focused {
- let Some(on_input) = on_input else {
- return event::Status::Ignored;
- };
-
- if state.is_pasting.is_none()
- && !state.keyboard_modifiers.command()
- && !c.is_control()
- {
- let mut editor = Editor::new(value, &mut state.cursor);
-
- editor.insert(c);
-
- let message = (on_input)(editor.contents());
- shell.publish(message);
-
- focus.updated_at = Instant::now();
-
- update_cache(state, value);
-
- return event::Status::Captured;
- }
- }
- }
- Event::Keyboard(keyboard::Event::KeyPressed { key_code, .. }) => {
+ Event::Keyboard(keyboard::Event::KeyPressed { key, text, .. }) => {
let state = state();
if let Some(focus) = &mut state.is_focused {
@@ -790,14 +760,13 @@ where
let modifiers = state.keyboard_modifiers;
focus.updated_at = Instant::now();
- match key_code {
- keyboard::KeyCode::Enter
- | keyboard::KeyCode::NumpadEnter => {
+ match key.as_ref() {
+ keyboard::Key::Named(key::Named::Enter) => {
if let Some(on_submit) = on_submit.clone() {
shell.publish(on_submit);
}
}
- keyboard::KeyCode::Backspace => {
+ keyboard::Key::Named(key::Named::Backspace) => {
if platform::is_jump_modifier_pressed(modifiers)
&& state.cursor.selection(value).is_none()
{
@@ -817,7 +786,7 @@ where
update_cache(state, value);
}
- keyboard::KeyCode::Delete => {
+ keyboard::Key::Named(key::Named::Delete) => {
if platform::is_jump_modifier_pressed(modifiers)
&& state.cursor.selection(value).is_none()
{
@@ -839,7 +808,7 @@ where
update_cache(state, value);
}
- keyboard::KeyCode::Left => {
+ keyboard::Key::Named(key::Named::ArrowLeft) => {
if platform::is_jump_modifier_pressed(modifiers)
&& !is_secure
{
@@ -854,7 +823,7 @@ where
state.cursor.move_left(value);
}
}
- keyboard::KeyCode::Right => {
+ keyboard::Key::Named(key::Named::ArrowRight) => {
if platform::is_jump_modifier_pressed(modifiers)
&& !is_secure
{
@@ -869,7 +838,7 @@ where
state.cursor.move_right(value);
}
}
- keyboard::KeyCode::Home => {
+ keyboard::Key::Named(key::Named::Home) => {
if modifiers.shift() {
state
.cursor
@@ -878,7 +847,7 @@ where
state.cursor.move_to(0);
}
}
- keyboard::KeyCode::End => {
+ keyboard::Key::Named(key::Named::End) => {
if modifiers.shift() {
state.cursor.select_range(
state.cursor.start(value),
@@ -888,7 +857,7 @@ where
state.cursor.move_to(value.len());
}
}
- keyboard::KeyCode::C
+ keyboard::Key::Character("c")
if state.keyboard_modifiers.command() =>
{
if let Some((start, end)) =
@@ -898,7 +867,7 @@ where
.write(value.select(start, end).to_string());
}
}
- keyboard::KeyCode::X
+ keyboard::Key::Character("x")
if state.keyboard_modifiers.command() =>
{
if let Some((start, end)) =
@@ -916,7 +885,7 @@ where
update_cache(state, value);
}
- keyboard::KeyCode::V => {
+ keyboard::Key::Character("v") => {
if state.keyboard_modifiers.command()
&& !state.keyboard_modifiers.alt()
{
@@ -953,12 +922,12 @@ where
state.is_pasting = None;
}
}
- keyboard::KeyCode::A
+ keyboard::Key::Character("a")
if state.keyboard_modifiers.command() =>
{
state.cursor.select_all(value);
}
- keyboard::KeyCode::Escape => {
+ keyboard::Key::Named(key::Named::Escape) => {
state.is_focused = None;
state.is_dragging = false;
state.is_pasting = None;
@@ -966,28 +935,55 @@ where
state.keyboard_modifiers =
keyboard::Modifiers::default();
}
- keyboard::KeyCode::Tab
- | keyboard::KeyCode::Up
- | keyboard::KeyCode::Down => {
+ keyboard::Key::Named(
+ key::Named::Tab
+ | key::Named::ArrowUp
+ | key::Named::ArrowDown,
+ ) => {
return event::Status::Ignored;
}
- _ => {}
+ _ => {
+ if let Some(text) = text {
+ let c = text.chars().next().unwrap_or_default();
+
+ if state.is_pasting.is_none()
+ && !state.keyboard_modifiers.command()
+ && !c.is_control()
+ {
+ let mut editor =
+ Editor::new(value, &mut state.cursor);
+
+ editor.insert(c);
+
+ let message = (on_input)(editor.contents());
+ shell.publish(message);
+
+ focus.updated_at = Instant::now();
+
+ update_cache(state, value);
+
+ return event::Status::Captured;
+ }
+ }
+ }
}
return event::Status::Captured;
}
}
- Event::Keyboard(keyboard::Event::KeyReleased { key_code, .. }) => {
+ Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => {
let state = state();
if state.is_focused.is_some() {
- match key_code {
- keyboard::KeyCode::V => {
+ match key.as_ref() {
+ keyboard::Key::Character("v") => {
state.is_pasting = None;
}
- keyboard::KeyCode::Tab
- | keyboard::KeyCode::Up
- | keyboard::KeyCode::Down => {
+ keyboard::Key::Named(
+ key::Named::Tab
+ | key::Named::ArrowUp
+ | key::Named::ArrowDown,
+ ) => {
return event::Status::Ignored;
}
_ => {}
@@ -1003,14 +999,14 @@ where
state.keyboard_modifiers = modifiers;
}
- Event::Window(window::Event::Unfocused) => {
+ Event::Window(_, window::Event::Unfocused) => {
let state = state();
if let Some(focus) = &mut state.is_focused {
focus.is_window_focused = false;
}
}
- Event::Window(window::Event::Focused) => {
+ Event::Window(_, window::Event::Focused) => {
let state = state();
if let Some(focus) = &mut state.is_focused {
@@ -1020,7 +1016,7 @@ where
shell.request_redraw(window::RedrawRequest::NextFrame);
}
}
- Event::Window(window::Event::RedrawRequested(now)) => {
+ Event::Window(_, window::Event::RedrawRequested(now)) => {
let state = state();
if let Some(focus) = &mut state.is_focused {
@@ -1058,6 +1054,7 @@ pub fn draw<Renderer>(
is_secure: bool,
icon: Option<&Icon<Renderer::Font>>,
style: &<Renderer::Theme as StyleSheet>::Style,
+ viewport: &Rectangle,
) where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
@@ -1099,6 +1096,7 @@ pub fn draw<Renderer>(
&state.icon,
icon_layout.bounds().center(),
appearance.icon_color,
+ *viewport,
);
}
@@ -1192,11 +1190,11 @@ pub fn draw<Renderer>(
(None, 0.0)
};
- let text_width = state.value.min_width();
-
- let render = |renderer: &mut Renderer| {
+ let draw = |renderer: &mut Renderer, viewport| {
if let Some((cursor, color)) = cursor {
- renderer.fill_quad(cursor, color);
+ renderer.with_translation(Vector::new(-offset, 0.0), |renderer| {
+ renderer.fill_quad(cursor, color);
+ });
} else {
renderer.with_translation(Vector::ZERO, |_| {});
}
@@ -1207,7 +1205,8 @@ pub fn draw<Renderer>(
} else {
&state.value
},
- Point::new(text_bounds.x, text_bounds.center_y()),
+ Point::new(text_bounds.x, text_bounds.center_y())
+ - Vector::new(offset, 0.0),
if text.is_empty() {
theme.placeholder_color(style)
} else if is_disabled {
@@ -1215,15 +1214,14 @@ pub fn draw<Renderer>(
} else {
theme.value_color(style)
},
+ viewport,
);
};
- if text_width > text_bounds.width {
- renderer.with_layer(text_bounds, |renderer| {
- renderer.with_translation(Vector::new(-offset, 0.0), render);
- });
+ if cursor.is_some() {
+ renderer.with_layer(text_bounds, |renderer| draw(renderer, *viewport));
} else {
- render(renderer);
+ draw(renderer, text_bounds);
}
}
@@ -1461,7 +1459,7 @@ fn replace_paragraph<Renderer>(
let mut children_layout = layout.children();
let text_bounds = children_layout.next().unwrap().bounds();
- state.value = renderer.create_paragraph(Text {
+ state.value = Renderer::Paragraph::with_text(Text {
font,
line_height,
content: &value.to_string(),
diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs
index 476c8330..941159ea 100644
--- a/widget/src/toggler.rs
+++ b/widget/src/toggler.rs
@@ -168,12 +168,11 @@ where
tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
}
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- Length::Shrink
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: Length::Shrink,
+ }
}
fn layout(
@@ -266,7 +265,7 @@ where
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
- _viewport: &Rectangle,
+ viewport: &Rectangle,
) {
/// Makes sure that the border radius of the toggler looks good at every size.
const BORDER_RADIUS_RATIO: f32 = 32.0 / 13.0;
@@ -287,6 +286,7 @@ where
label_layout,
tree.state.downcast_ref(),
crate::text::Appearance::default(),
+ viewport,
);
}
diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs
index b041d2e9..d09a9255 100644
--- a/widget/src/tooltip.rs
+++ b/widget/src/tooltip.rs
@@ -64,6 +64,12 @@ where
self
}
+ /// Sets the [`text::Shaping`] strategy of the [`Tooltip`].
+ pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
+ self.tooltip = self.tooltip.shaping(shaping);
+ self
+ }
+
/// Sets the font of the [`Tooltip`].
///
/// [`Font`]: Renderer::Font
@@ -125,12 +131,8 @@ where
widget::tree::Tag::of::<State>()
}
- fn width(&self) -> Length {
- self.content.as_widget().width()
- }
-
- fn height(&self) -> Length {
- self.content.as_widget().height()
+ fn size(&self) -> Size<Length> {
+ self.content.as_widget().size()
}
fn layout(
@@ -157,11 +159,19 @@ where
) -> event::Status {
let state = tree.state.downcast_mut::<State>();
+ let was_idle = *state == State::Idle;
+
*state = cursor
.position_over(layout.bounds())
.map(|cursor_position| State::Hovered { cursor_position })
.unwrap_or_default();
+ let is_idle = *state == State::Idle;
+
+ if was_idle != is_idle {
+ shell.invalidate_layout();
+ }
+
self.content.as_widget_mut().on_event(
&mut tree.children[0],
event,
@@ -289,7 +299,7 @@ pub enum Position {
Right,
}
-#[derive(Debug, Clone, Copy, Default)]
+#[derive(Debug, Clone, Copy, PartialEq, Default)]
enum State {
#[default]
Idle,
@@ -325,6 +335,7 @@ where
renderer: &Renderer,
bounds: Size,
position: Point,
+ _translation: Vector,
) -> layout::Node {
let viewport = Rectangle::with_size(bounds);
@@ -338,7 +349,7 @@ where
.then(|| viewport.size())
.unwrap_or(Size::INFINITY),
)
- .pad(Padding::new(self.padding)),
+ .shrink(Padding::new(self.padding)),
);
let text_bounds = text_layout.bounds();
diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs
index 01d3359c..a3029d76 100644
--- a/widget/src/vertical_slider.rs
+++ b/widget/src/vertical_slider.rs
@@ -156,12 +156,11 @@ where
tree::State::new(State::new())
}
- fn width(&self) -> Length {
- Length::Shrink
- }
-
- fn height(&self) -> Length {
- self.height
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: Length::Shrink,
+ height: self.height,
+ }
}
fn layout(
@@ -170,10 +169,7 @@ where
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let limits = limits.width(self.width).height(self.height);
- let size = limits.resolve(Size::ZERO);
-
- layout::Node::new(size)
+ layout::atomic(limits, self.width, self.height)
}
fn on_event(
diff --git a/winit/Cargo.toml b/winit/Cargo.toml
index 674a66d3..87e600ae 100644
--- a/winit/Cargo.toml
+++ b/winit/Cargo.toml
@@ -19,6 +19,7 @@ x11 = ["winit/x11"]
wayland = ["winit/wayland"]
wayland-dlopen = ["winit/wayland-dlopen"]
wayland-csd-adwaita = ["winit/wayland-csd-adwaita"]
+multi-window = ["iced_runtime/multi-window"]
[dependencies]
iced_graphics.workspace = true
@@ -26,7 +27,6 @@ iced_runtime.workspace = true
iced_style.workspace = true
log.workspace = true
-raw-window-handle.workspace = true
thiserror.workspace = true
tracing.workspace = true
window_clipboard.workspace = true
diff --git a/winit/src/application.rs b/winit/src/application.rs
index 8105f8d9..09bf63cc 100644
--- a/winit/src/application.rs
+++ b/winit/src/application.rs
@@ -1,6 +1,4 @@
//! Create interactive, native cross-platform applications.
-#[cfg(feature = "trace")]
-mod profiler;
mod state;
pub use state::State;
@@ -26,11 +24,7 @@ use crate::{Clipboard, Error, Proxy, Settings};
use futures::channel::mpsc;
use std::mem::ManuallyDrop;
-
-#[cfg(feature = "trace")]
-pub use profiler::Profiler;
-#[cfg(feature = "trace")]
-use tracing::{info_span, instrument::Instrument};
+use std::sync::Arc;
/// An interactive, native cross-platform application.
///
@@ -119,16 +113,12 @@ where
use futures::Future;
use winit::event_loop::EventLoopBuilder;
- #[cfg(feature = "trace")]
- let _guard = Profiler::init();
-
let mut debug = Debug::new();
debug.startup_started();
- #[cfg(feature = "trace")]
- let _ = info_span!("Application", "RUN").entered();
-
- let event_loop = EventLoopBuilder::with_user_event().build();
+ let event_loop = EventLoopBuilder::with_user_event()
+ .build()
+ .expect("Create event loop");
let proxy = event_loop.create_proxy();
let runtime = {
@@ -148,26 +138,29 @@ where
let target = settings.window.platform_specific.target.clone();
let should_be_visible = settings.window.visible;
- let builder = settings
- .window
- .into_builder(
- &application.title(),
- event_loop.primary_monitor(),
- settings.id,
- )
- .with_visible(false);
+ let exit_on_close_request = settings.window.exit_on_close_request;
+
+ let builder = conversion::window_settings(
+ settings.window,
+ &application.title(),
+ event_loop.primary_monitor(),
+ settings.id,
+ )
+ .with_visible(false);
log::debug!("Window builder: {builder:#?}");
- let window = builder
- .build(&event_loop)
- .map_err(Error::WindowCreationFailed)?;
+ let window = Arc::new(
+ builder
+ .build(&event_loop)
+ .map_err(Error::WindowCreationFailed)?,
+ );
#[cfg(target_arch = "wasm32")]
{
use winit::platform::web::WindowExtWebSys;
- let canvas = window.canvas();
+ let canvas = window.canvas().expect("Get window canvas");
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
@@ -193,75 +186,57 @@ where
};
}
- let (compositor, renderer) = C::new(compositor_settings, Some(&window))?;
+ let compositor = C::new(compositor_settings, window.clone())?;
+ let mut renderer = compositor.create_renderer();
- let (mut event_sender, event_receiver) = mpsc::unbounded();
- let (control_sender, mut control_receiver) = mpsc::unbounded();
+ for font in settings.fonts {
+ use crate::core::text::Renderer;
- let mut instance = Box::pin({
- let run_instance = run_instance::<A, E, C>(
- application,
- compositor,
- renderer,
- runtime,
- proxy,
- debug,
- event_receiver,
- control_sender,
- init_command,
- window,
- should_be_visible,
- settings.exit_on_close_request,
- );
+ renderer.load_font(font);
+ }
- #[cfg(feature = "trace")]
- let run_instance =
- run_instance.instrument(info_span!("Application", "LOOP"));
+ let (mut event_sender, event_receiver) = mpsc::unbounded();
+ let (control_sender, mut control_receiver) = mpsc::unbounded();
- run_instance
- });
+ let mut instance = Box::pin(run_instance::<A, E, C>(
+ application,
+ compositor,
+ renderer,
+ runtime,
+ proxy,
+ debug,
+ event_receiver,
+ control_sender,
+ init_command,
+ window,
+ should_be_visible,
+ exit_on_close_request,
+ ));
let mut context = task::Context::from_waker(task::noop_waker_ref());
- platform::run(event_loop, move |event, _, control_flow| {
- use winit::event_loop::ControlFlow;
-
- if let ControlFlow::ExitWithCode(_) = control_flow {
+ let _ = event_loop.run(move |event, event_loop| {
+ if event_loop.exiting() {
return;
}
- let event = match event {
- winit::event::Event::WindowEvent {
- event:
- winit::event::WindowEvent::ScaleFactorChanged {
- new_inner_size,
- ..
- },
- window_id,
- } => Some(winit::event::Event::WindowEvent {
- event: winit::event::WindowEvent::Resized(*new_inner_size),
- window_id,
- }),
- _ => event.to_static(),
- };
+ event_sender.start_send(event).expect("Send event");
- if let Some(event) = event {
- event_sender.start_send(event).expect("Send event");
+ let poll = instance.as_mut().poll(&mut context);
- let poll = instance.as_mut().poll(&mut context);
-
- match poll {
- task::Poll::Pending => {
- if let Ok(Some(flow)) = control_receiver.try_next() {
- *control_flow = flow;
- }
+ match poll {
+ task::Poll::Pending => {
+ if let Ok(Some(flow)) = control_receiver.try_next() {
+ event_loop.set_control_flow(flow);
}
- task::Poll::Ready(_) => {
- *control_flow = ControlFlow::Exit;
- }
- };
- }
- })
+ }
+ task::Poll::Ready(_) => {
+ event_loop.exit();
+ }
+ };
+ });
+
+ Ok(())
}
async fn run_instance<A, E, C>(
@@ -272,11 +247,11 @@ async fn run_instance<A, E, C>(
mut proxy: winit::event_loop::EventLoopProxy<A::Message>,
mut debug: Debug,
mut event_receiver: mpsc::UnboundedReceiver<
- winit::event::Event<'_, A::Message>,
+ winit::event::Event<A::Message>,
>,
mut control_sender: mpsc::UnboundedSender<winit::event_loop::ControlFlow>,
init_command: Command<A::Message>,
- window: winit::window::Window,
+ window: Arc<winit::window::Window>,
should_be_visible: bool,
exit_on_close_request: bool,
) where
@@ -296,7 +271,7 @@ async fn run_instance<A, E, C>(
let mut clipboard = Clipboard::connect(&window);
let mut cache = user_interface::Cache::default();
let mut surface = compositor.create_surface(
- &window,
+ window.clone(),
physical_size.width,
physical_size.height,
);
@@ -340,77 +315,56 @@ async fn run_instance<A, E, C>(
while let Some(event) = event_receiver.next().await {
match event {
- event::Event::NewEvents(start_cause) => {
- redraw_pending = matches!(
- start_cause,
- event::StartCause::Init
- | event::StartCause::Poll
- | event::StartCause::ResumeTimeReached { .. }
- );
+ event::Event::NewEvents(
+ event::StartCause::Init
+ | event::StartCause::ResumeTimeReached { .. },
+ ) if !redraw_pending => {
+ window.request_redraw();
+ redraw_pending = true;
}
- event::Event::MainEventsCleared => {
- if !redraw_pending && events.is_empty() && messages.is_empty() {
- continue;
- }
+ event::Event::PlatformSpecific(event::PlatformSpecific::MacOS(
+ event::MacOS::ReceivedUrl(url),
+ )) => {
+ use crate::core::event;
- debug.event_processing_started();
+ events.push(Event::PlatformSpecific(
+ event::PlatformSpecific::MacOS(event::MacOS::ReceivedUrl(
+ url,
+ )),
+ ));
+ }
+ event::Event::UserEvent(message) => {
+ messages.push(message);
+ }
+ event::Event::WindowEvent {
+ event: event::WindowEvent::RedrawRequested { .. },
+ ..
+ } => {
+ let physical_size = state.physical_size();
- let (interface_state, statuses) = user_interface.update(
- &events,
- state.cursor(),
- &mut renderer,
- &mut clipboard,
- &mut messages,
- );
+ if physical_size.width == 0 || physical_size.height == 0 {
+ continue;
+ }
- debug.event_processing_finished();
+ let current_viewport_version = state.viewport_version();
- for (event, status) in
- events.drain(..).zip(statuses.into_iter())
- {
- runtime.broadcast(event, status);
- }
+ if viewport_version != current_viewport_version {
+ let logical_size = state.logical_size();
- if !messages.is_empty()
- || matches!(
- interface_state,
- user_interface::State::Outdated
- )
- {
- let mut cache =
- ManuallyDrop::into_inner(user_interface).into_cache();
+ debug.layout_started();
+ user_interface = ManuallyDrop::new(
+ ManuallyDrop::into_inner(user_interface)
+ .relayout(logical_size, &mut renderer),
+ );
+ debug.layout_finished();
- // Update application
- update(
- &mut application,
- &mut compositor,
+ compositor.configure_surface(
&mut surface,
- &mut cache,
- &state,
- &mut renderer,
- &mut runtime,
- &mut clipboard,
- &mut should_exit,
- &mut proxy,
- &mut debug,
- &mut messages,
- &window,
+ physical_size.width,
+ physical_size.height,
);
- // Update window
- state.synchronize(&application, &window);
-
- user_interface = ManuallyDrop::new(build_user_interface(
- &application,
- cache,
- &mut renderer,
- state.logical_size(),
- &mut debug,
- ));
-
- if should_exit {
- break;
- }
+ viewport_version = current_viewport_version;
}
// TODO: Avoid redrawing all the time by forcing widgets to
@@ -419,6 +373,7 @@ async fn run_instance<A, E, C>(
// Then, we can use the `interface_state` here to decide if a redraw
// is needed right away, or simply wait until a specific time.
let redraw_event = Event::Window(
+ window::Id::MAIN,
window::Event::RedrawRequested(Instant::now()),
);
@@ -430,6 +385,24 @@ async fn run_instance<A, E, C>(
&mut messages,
);
+ let _ = control_sender.start_send(match interface_state {
+ user_interface::State::Updated {
+ redraw_request: Some(redraw_request),
+ } => match redraw_request {
+ window::RedrawRequest::NextFrame => {
+ window.request_redraw();
+
+ ControlFlow::Wait
+ }
+ window::RedrawRequest::At(at) => {
+ ControlFlow::WaitUntil(at)
+ }
+ },
+ _ => ControlFlow::Wait,
+ });
+
+ runtime.broadcast(redraw_event, core::event::Status::Ignored);
+
debug.draw_started();
let new_mouse_interaction = user_interface.draw(
&mut renderer,
@@ -439,6 +412,7 @@ async fn run_instance<A, E, C>(
},
state.cursor(),
);
+ redraw_pending = false;
debug.draw_finished();
if new_mouse_interaction != mouse_interaction {
@@ -449,88 +423,7 @@ async fn run_instance<A, E, C>(
mouse_interaction = new_mouse_interaction;
}
- window.request_redraw();
- runtime.broadcast(redraw_event, core::event::Status::Ignored);
-
- let _ = control_sender.start_send(match interface_state {
- user_interface::State::Updated {
- redraw_request: Some(redraw_request),
- } => match redraw_request {
- window::RedrawRequest::NextFrame => ControlFlow::Poll,
- window::RedrawRequest::At(at) => {
- ControlFlow::WaitUntil(at)
- }
- },
- _ => ControlFlow::Wait,
- });
-
- redraw_pending = false;
- }
- event::Event::PlatformSpecific(event::PlatformSpecific::MacOS(
- event::MacOS::ReceivedUrl(url),
- )) => {
- use crate::core::event;
-
- events.push(Event::PlatformSpecific(
- event::PlatformSpecific::MacOS(event::MacOS::ReceivedUrl(
- url,
- )),
- ));
- }
- event::Event::UserEvent(message) => {
- messages.push(message);
- }
- event::Event::RedrawRequested(_) => {
- #[cfg(feature = "trace")]
- let _ = info_span!("Application", "FRAME").entered();
-
- let physical_size = state.physical_size();
-
- if physical_size.width == 0 || physical_size.height == 0 {
- continue;
- }
-
debug.render_started();
- let current_viewport_version = state.viewport_version();
-
- if viewport_version != current_viewport_version {
- let logical_size = state.logical_size();
-
- debug.layout_started();
- user_interface = ManuallyDrop::new(
- ManuallyDrop::into_inner(user_interface)
- .relayout(logical_size, &mut renderer),
- );
- debug.layout_finished();
-
- debug.draw_started();
- let new_mouse_interaction = user_interface.draw(
- &mut renderer,
- state.theme(),
- &renderer::Style {
- text_color: state.text_color(),
- },
- state.cursor(),
- );
-
- if new_mouse_interaction != mouse_interaction {
- window.set_cursor_icon(conversion::mouse_interaction(
- new_mouse_interaction,
- ));
-
- mouse_interaction = new_mouse_interaction;
- }
- debug.draw_finished();
-
- compositor.configure_surface(
- &mut surface,
- physical_size.width,
- physical_size.height,
- );
-
- viewport_version = current_viewport_version;
- }
-
match compositor.present(
&mut renderer,
&mut surface,
@@ -571,13 +464,81 @@ async fn run_instance<A, E, C>(
state.update(&window, &window_event, &mut debug);
if let Some(event) = conversion::window_event(
- &window_event,
+ window::Id::MAIN,
+ window_event,
state.scale_factor(),
state.modifiers(),
) {
events.push(event);
}
}
+ event::Event::AboutToWait => {
+ if events.is_empty() && messages.is_empty() {
+ continue;
+ }
+
+ debug.event_processing_started();
+
+ let (interface_state, statuses) = user_interface.update(
+ &events,
+ state.cursor(),
+ &mut renderer,
+ &mut clipboard,
+ &mut messages,
+ );
+
+ debug.event_processing_finished();
+
+ for (event, status) in
+ events.drain(..).zip(statuses.into_iter())
+ {
+ runtime.broadcast(event, status);
+ }
+
+ if !messages.is_empty()
+ || matches!(
+ interface_state,
+ user_interface::State::Outdated
+ )
+ {
+ let mut cache =
+ ManuallyDrop::into_inner(user_interface).into_cache();
+
+ // Update application
+ update(
+ &mut application,
+ &mut compositor,
+ &mut surface,
+ &mut cache,
+ &mut state,
+ &mut renderer,
+ &mut runtime,
+ &mut clipboard,
+ &mut should_exit,
+ &mut proxy,
+ &mut debug,
+ &mut messages,
+ &window,
+ );
+
+ user_interface = ManuallyDrop::new(build_user_interface(
+ &application,
+ cache,
+ &mut renderer,
+ state.logical_size(),
+ &mut debug,
+ ));
+
+ if should_exit {
+ break;
+ }
+ }
+
+ if !redraw_pending {
+ window.request_redraw();
+ redraw_pending = true;
+ }
+ }
_ => {}
}
}
@@ -589,8 +550,8 @@ async fn run_instance<A, E, C>(
/// Returns true if the provided event should cause an [`Application`] to
/// exit.
pub fn requests_exit(
- event: &winit::event::WindowEvent<'_>,
- _modifiers: winit::event::ModifiersState,
+ event: &winit::event::WindowEvent,
+ _modifiers: winit::keyboard::ModifiersState,
) -> bool {
use winit::event::WindowEvent;
@@ -598,14 +559,14 @@ pub fn requests_exit(
WindowEvent::CloseRequested => true,
#[cfg(target_os = "macos")]
WindowEvent::KeyboardInput {
- input:
- winit::event::KeyboardInput {
- virtual_keycode: Some(winit::event::VirtualKeyCode::Q),
+ event:
+ winit::event::KeyEvent {
+ logical_key: winit::keyboard::Key::Character(c),
state: winit::event::ElementState::Pressed,
..
},
..
- } if _modifiers.logo() => true,
+ } if c == "q" && _modifiers.super_key() => true,
_ => false,
}
}
@@ -622,24 +583,12 @@ pub fn build_user_interface<'a, A: Application>(
where
<A::Renderer as core::Renderer>::Theme: StyleSheet,
{
- #[cfg(feature = "trace")]
- let view_span = info_span!("Application", "VIEW").entered();
-
debug.view_started();
let view = application.view();
-
- #[cfg(feature = "trace")]
- let _ = view_span.exit();
debug.view_finished();
- #[cfg(feature = "trace")]
- let layout_span = info_span!("Application", "LAYOUT").entered();
-
debug.layout_started();
let user_interface = UserInterface::build(view, size, cache, renderer);
-
- #[cfg(feature = "trace")]
- let _ = layout_span.exit();
debug.layout_finished();
user_interface
@@ -652,7 +601,7 @@ pub fn update<A: Application, C, E: Executor>(
compositor: &mut C,
surface: &mut C::Surface,
cache: &mut user_interface::Cache,
- state: &State<A>,
+ state: &mut State<A>,
renderer: &mut A::Renderer,
runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>,
clipboard: &mut Clipboard,
@@ -666,16 +615,10 @@ pub fn update<A: Application, C, E: Executor>(
<A::Renderer as core::Renderer>::Theme: StyleSheet,
{
for message in messages.drain(..) {
- #[cfg(feature = "trace")]
- let update_span = info_span!("Application", "UPDATE").entered();
-
debug.log_message(&message);
debug.update_started();
let command = runtime.enter(|| application.update(message));
-
- #[cfg(feature = "trace")]
- let _ = update_span.exit();
debug.update_finished();
run_command(
@@ -695,6 +638,8 @@ pub fn update<A: Application, C, E: Executor>(
);
}
+ state.synchronize(application, window);
+
let subscription = application.subscription();
runtime.track(subscription.into_recipes());
}
@@ -729,6 +674,9 @@ pub fn run_command<A, C, E>(
command::Action::Future(future) => {
runtime.spawn(future);
}
+ command::Action::Stream(stream) => {
+ runtime.run(stream);
+ }
command::Action::Clipboard(action) => match action {
clipboard::Action::Read(tag) => {
let message = tag(clipboard.read());
@@ -742,20 +690,28 @@ pub fn run_command<A, C, E>(
}
},
command::Action::Window(action) => match action {
- window::Action::Close => {
+ window::Action::Close(_id) => {
*should_exit = true;
}
- window::Action::Drag => {
+ window::Action::Drag(_id) => {
let _res = window.drag_window();
}
- window::Action::Resize(size) => {
- window.set_inner_size(winit::dpi::LogicalSize {
- width: size.width,
- height: size.height,
- });
+ window::Action::Spawn { .. } => {
+ log::warn!(
+ "Spawning a window is only available with \
+ multi-window applications."
+ );
}
- window::Action::FetchSize(callback) => {
- let size = window.inner_size();
+ window::Action::Resize(_id, size) => {
+ let _ =
+ window.request_inner_size(winit::dpi::LogicalSize {
+ width: size.width,
+ height: size.height,
+ });
+ }
+ window::Action::FetchSize(_id, callback) => {
+ let size =
+ window.inner_size().to_logical(window.scale_factor());
proxy
.send_event(callback(Size::new(
@@ -764,29 +720,39 @@ pub fn run_command<A, C, E>(
)))
.expect("Send message to event loop");
}
- window::Action::Maximize(maximized) => {
+ window::Action::FetchMaximized(_id, callback) => {
+ proxy
+ .send_event(callback(window.is_maximized()))
+ .expect("Send message to event loop");
+ }
+ window::Action::Maximize(_id, maximized) => {
window.set_maximized(maximized);
}
- window::Action::Minimize(minimized) => {
+ window::Action::FetchMinimized(_id, callback) => {
+ proxy
+ .send_event(callback(window.is_minimized()))
+ .expect("Send message to event loop");
+ }
+ window::Action::Minimize(_id, minimized) => {
window.set_minimized(minimized);
}
- window::Action::Move { x, y } => {
+ window::Action::Move(_id, position) => {
window.set_outer_position(winit::dpi::LogicalPosition {
- x,
- y,
+ x: position.x,
+ y: position.y,
});
}
- window::Action::ChangeMode(mode) => {
+ window::Action::ChangeMode(_id, mode) => {
window.set_visible(conversion::visible(mode));
window.set_fullscreen(conversion::fullscreen(
window.current_monitor(),
mode,
));
}
- window::Action::ChangeIcon(icon) => {
+ window::Action::ChangeIcon(_id, icon) => {
window.set_window_icon(conversion::icon(icon));
}
- window::Action::FetchMode(tag) => {
+ window::Action::FetchMode(_id, tag) => {
let mode = if window.is_visible().unwrap_or(true) {
conversion::mode(window.fullscreen())
} else {
@@ -797,29 +763,29 @@ pub fn run_command<A, C, E>(
.send_event(tag(mode))
.expect("Send message to event loop");
}
- window::Action::ToggleMaximize => {
+ window::Action::ToggleMaximize(_id) => {
window.set_maximized(!window.is_maximized());
}
- window::Action::ToggleDecorations => {
+ window::Action::ToggleDecorations(_id) => {
window.set_decorations(!window.is_decorated());
}
- window::Action::RequestUserAttention(user_attention) => {
+ window::Action::RequestUserAttention(_id, user_attention) => {
window.request_user_attention(
user_attention.map(conversion::user_attention),
);
}
- window::Action::GainFocus => {
+ window::Action::GainFocus(_id) => {
window.focus_window();
}
- window::Action::ChangeLevel(level) => {
+ window::Action::ChangeLevel(_id, level) => {
window.set_window_level(conversion::window_level(level));
}
- window::Action::FetchId(tag) => {
+ window::Action::FetchId(_id, tag) => {
proxy
.send_event(tag(window.id().into()))
.expect("Send message to event loop");
}
- window::Action::Screenshot(tag) => {
+ window::Action::Screenshot(_id, tag) => {
let bytes = compositor.screenshot(
renderer,
surface,
@@ -900,43 +866,3 @@ pub fn run_command<A, C, E>(
}
}
}
-
-#[cfg(not(target_arch = "wasm32"))]
-mod platform {
- pub fn run<T, F>(
- mut event_loop: winit::event_loop::EventLoop<T>,
- event_handler: F,
- ) -> Result<(), super::Error>
- where
- F: 'static
- + FnMut(
- winit::event::Event<'_, T>,
- &winit::event_loop::EventLoopWindowTarget<T>,
- &mut winit::event_loop::ControlFlow,
- ),
- {
- use winit::platform::run_return::EventLoopExtRunReturn;
-
- let _ = event_loop.run_return(event_handler);
-
- Ok(())
- }
-}
-
-#[cfg(target_arch = "wasm32")]
-mod platform {
- pub fn run<T, F>(
- event_loop: winit::event_loop::EventLoop<T>,
- event_handler: F,
- ) -> !
- where
- F: 'static
- + FnMut(
- winit::event::Event<'_, T>,
- &winit::event_loop::EventLoopWindowTarget<T>,
- &mut winit::event_loop::ControlFlow,
- ),
- {
- event_loop.run(event_handler)
- }
-}
diff --git a/winit/src/application/profiler.rs b/winit/src/application/profiler.rs
deleted file mode 100644
index 7031507a..00000000
--- a/winit/src/application/profiler.rs
+++ /dev/null
@@ -1,101 +0,0 @@
-//! A simple profiler for Iced.
-use std::ffi::OsStr;
-use std::path::Path;
-use std::time::Duration;
-use tracing_subscriber::prelude::*;
-use tracing_subscriber::Registry;
-#[cfg(feature = "chrome-trace")]
-use {
- tracing_chrome::FlushGuard,
- tracing_subscriber::fmt::{format::DefaultFields, FormattedFields},
-};
-
-/// Profiler state. This will likely need to be updated or reworked when adding new tracing backends.
-#[allow(missing_debug_implementations)]
-pub struct Profiler {
- #[cfg(feature = "chrome-trace")]
- /// [`FlushGuard`] must not be dropped until the application scope is dropped for accurate tracing.
- _guard: FlushGuard,
-}
-
-impl Profiler {
- /// Initializes the [`Profiler`].
- pub fn init() -> Self {
- // Registry stores the spans & generates unique span IDs
- let subscriber = Registry::default();
-
- let default_path = Path::new(env!("CARGO_MANIFEST_DIR"));
- let curr_exe = std::env::current_exe()
- .unwrap_or_else(|_| default_path.to_path_buf());
- let out_dir = curr_exe.parent().unwrap_or(default_path).join("traces");
-
- #[cfg(feature = "chrome-trace")]
- let (chrome_layer, guard) = {
- let mut layer = tracing_chrome::ChromeLayerBuilder::new();
-
- // Optional configurable env var: CHROME_TRACE_FILE=/path/to/trace_file/file.json,
- // for uploading to chrome://tracing (old) or ui.perfetto.dev (new).
- if let Ok(path) = std::env::var("CHROME_TRACE_FILE") {
- layer = layer.file(path);
- } else if std::fs::create_dir_all(&out_dir).is_ok() {
- let time = std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .unwrap_or(Duration::from_millis(0))
- .as_millis();
-
- let curr_exe_name = curr_exe
- .file_name()
- .unwrap_or_else(|| OsStr::new("trace"))
- .to_str()
- .unwrap_or("trace");
-
- let path =
- out_dir.join(format!("{curr_exe_name}_trace_{time}.json"));
-
- layer = layer.file(path);
- } else {
- layer = layer.file(env!("CARGO_MANIFEST_DIR"))
- }
-
- let (chrome_layer, guard) = layer
- .name_fn(Box::new(|event_or_span| match event_or_span {
- tracing_chrome::EventOrSpan::Event(event) => {
- event.metadata().name().into()
- }
- tracing_chrome::EventOrSpan::Span(span) => {
- if let Some(fields) = span
- .extensions()
- .get::<FormattedFields<DefaultFields>>()
- {
- format!(
- "{}: {}",
- span.metadata().name(),
- fields.fields.as_str()
- )
- } else {
- span.metadata().name().into()
- }
- }
- }))
- .build();
-
- (chrome_layer, guard)
- };
-
- let fmt_layer = tracing_subscriber::fmt::Layer::default();
- let subscriber = subscriber.with(fmt_layer);
-
- #[cfg(feature = "chrome-trace")]
- let subscriber = subscriber.with(chrome_layer);
-
- // create dispatcher which will forward span events to the subscriber
- // this can only be set once or will panic
- tracing::subscriber::set_global_default(subscriber)
- .expect("Tracer could not set the global default subscriber.");
-
- Profiler {
- #[cfg(feature = "chrome-trace")]
- _guard: guard,
- }
- }
-}
diff --git a/winit/src/application/state.rs b/winit/src/application/state.rs
index e655529a..8c9b20e0 100644
--- a/winit/src/application/state.rs
+++ b/winit/src/application/state.rs
@@ -22,7 +22,7 @@ where
viewport: Viewport,
viewport_version: usize,
cursor_position: Option<winit::dpi::PhysicalPosition<f64>>,
- modifiers: winit::event::ModifiersState,
+ modifiers: winit::keyboard::ModifiersState,
theme: <A::Renderer as core::Renderer>::Theme,
appearance: application::Appearance,
application: PhantomData<A>,
@@ -54,7 +54,7 @@ where
viewport,
viewport_version: 0,
cursor_position: None,
- modifiers: winit::event::ModifiersState::default(),
+ modifiers: winit::keyboard::ModifiersState::default(),
theme,
appearance,
application: PhantomData,
@@ -102,7 +102,7 @@ where
}
/// Returns the current keyboard modifiers of the [`State`].
- pub fn modifiers(&self) -> winit::event::ModifiersState {
+ pub fn modifiers(&self) -> winit::keyboard::ModifiersState {
self.modifiers
}
@@ -126,7 +126,7 @@ where
pub fn update(
&mut self,
window: &Window,
- event: &WindowEvent<'_>,
+ event: &WindowEvent,
_debug: &mut Debug,
) {
match event {
@@ -142,10 +142,9 @@ where
}
WindowEvent::ScaleFactorChanged {
scale_factor: new_scale_factor,
- new_inner_size,
+ ..
} => {
- let size =
- Size::new(new_inner_size.width, new_inner_size.height);
+ let size = self.viewport.physical_size();
self.viewport = Viewport::with_physical_size(
size,
@@ -164,13 +163,16 @@ where
self.cursor_position = None;
}
WindowEvent::ModifiersChanged(new_modifiers) => {
- self.modifiers = *new_modifiers;
+ self.modifiers = new_modifiers.state();
}
#[cfg(feature = "debug")]
WindowEvent::KeyboardInput {
- input:
- winit::event::KeyboardInput {
- virtual_keycode: Some(winit::event::VirtualKeyCode::F12),
+ event:
+ winit::event::KeyEvent {
+ logical_key:
+ winit::keyboard::Key::Named(
+ winit::keyboard::NamedKey::F12,
+ ),
state: winit::event::ElementState::Pressed,
..
},
diff --git a/winit/src/clipboard.rs b/winit/src/clipboard.rs
index f7a32868..8f5c5e63 100644
--- a/winit/src/clipboard.rs
+++ b/winit/src/clipboard.rs
@@ -15,7 +15,8 @@ enum State {
impl Clipboard {
/// Creates a new [`Clipboard`] for the given window.
pub fn connect(window: &winit::window::Window) -> Clipboard {
- let state = window_clipboard::Clipboard::connect(window)
+ #[allow(unsafe_code)]
+ let state = unsafe { window_clipboard::Clipboard::connect(window) }
.ok()
.map(State::Connected)
.unwrap_or(State::Unavailable);
diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs
index 3ecd044c..90a5d27f 100644
--- a/winit/src/conversion.rs
+++ b/winit/src/conversion.rs
@@ -6,14 +6,131 @@ use crate::core::keyboard;
use crate::core::mouse;
use crate::core::touch;
use crate::core::window;
-use crate::core::{Event, Point};
-use crate::Position;
+use crate::core::{Event, Point, Size};
+
+/// Converts some [`window::Settings`] into a `WindowBuilder` from `winit`.
+pub fn window_settings(
+ settings: window::Settings,
+ title: &str,
+ primary_monitor: Option<winit::monitor::MonitorHandle>,
+ _id: Option<String>,
+) -> winit::window::WindowBuilder {
+ let mut window_builder = winit::window::WindowBuilder::new();
+
+ window_builder = window_builder
+ .with_title(title)
+ .with_inner_size(winit::dpi::LogicalSize {
+ width: settings.size.width,
+ height: settings.size.height,
+ })
+ .with_resizable(settings.resizable)
+ .with_enabled_buttons(if settings.resizable {
+ winit::window::WindowButtons::all()
+ } else {
+ winit::window::WindowButtons::CLOSE
+ | winit::window::WindowButtons::MINIMIZE
+ })
+ .with_decorations(settings.decorations)
+ .with_transparent(settings.transparent)
+ .with_window_icon(settings.icon.and_then(icon))
+ .with_window_level(window_level(settings.level))
+ .with_visible(settings.visible);
+
+ if let Some(position) =
+ position(primary_monitor.as_ref(), settings.size, settings.position)
+ {
+ window_builder = window_builder.with_position(position);
+ }
+
+ if let Some(min_size) = settings.min_size {
+ window_builder =
+ window_builder.with_min_inner_size(winit::dpi::LogicalSize {
+ width: min_size.width,
+ height: min_size.height,
+ });
+ }
+
+ if let Some(max_size) = settings.max_size {
+ window_builder =
+ window_builder.with_max_inner_size(winit::dpi::LogicalSize {
+ width: max_size.width,
+ height: max_size.height,
+ });
+ }
+
+ #[cfg(any(
+ target_os = "dragonfly",
+ target_os = "freebsd",
+ target_os = "netbsd",
+ target_os = "openbsd"
+ ))]
+ {
+ // `with_name` is available on both `WindowBuilderExtWayland` and `WindowBuilderExtX11` and they do
+ // exactly the same thing. We arbitrarily choose `WindowBuilderExtWayland` here.
+ use ::winit::platform::wayland::WindowBuilderExtWayland;
+
+ if let Some(id) = _id {
+ window_builder = window_builder.with_name(id.clone(), id);
+ }
+ }
+
+ #[cfg(target_os = "windows")]
+ {
+ use winit::platform::windows::WindowBuilderExtWindows;
+ #[allow(unsafe_code)]
+ unsafe {
+ window_builder = window_builder
+ .with_parent_window(settings.platform_specific.parent);
+ }
+ window_builder = window_builder
+ .with_drag_and_drop(settings.platform_specific.drag_and_drop);
+ }
+
+ #[cfg(target_os = "macos")]
+ {
+ use winit::platform::macos::WindowBuilderExtMacOS;
+
+ window_builder = window_builder
+ .with_title_hidden(settings.platform_specific.title_hidden)
+ .with_titlebar_transparent(
+ settings.platform_specific.titlebar_transparent,
+ )
+ .with_fullsize_content_view(
+ settings.platform_specific.fullsize_content_view,
+ );
+ }
+
+ #[cfg(target_os = "linux")]
+ {
+ #[cfg(feature = "x11")]
+ {
+ use winit::platform::x11::WindowBuilderExtX11;
+
+ window_builder = window_builder.with_name(
+ &settings.platform_specific.application_id,
+ &settings.platform_specific.application_id,
+ );
+ }
+ #[cfg(feature = "wayland")]
+ {
+ use winit::platform::wayland::WindowBuilderExtWayland;
+
+ window_builder = window_builder.with_name(
+ &settings.platform_specific.application_id,
+ &settings.platform_specific.application_id,
+ );
+ }
+ }
+
+ window_builder
+}
/// Converts a winit window event into an iced event.
pub fn window_event(
- event: &winit::event::WindowEvent<'_>,
+ id: window::Id,
+ event: winit::event::WindowEvent,
scale_factor: f64,
- modifiers: winit::event::ModifiersState,
+ modifiers: winit::keyboard::ModifiersState,
) -> Option<Event> {
use winit::event::WindowEvent;
@@ -21,21 +138,16 @@ pub fn window_event(
WindowEvent::Resized(new_size) => {
let logical_size = new_size.to_logical(scale_factor);
- Some(Event::Window(window::Event::Resized {
- width: logical_size.width,
- height: logical_size.height,
- }))
- }
- WindowEvent::ScaleFactorChanged { new_inner_size, .. } => {
- let logical_size = new_inner_size.to_logical(scale_factor);
-
- Some(Event::Window(window::Event::Resized {
- width: logical_size.width,
- height: logical_size.height,
- }))
+ Some(Event::Window(
+ id,
+ window::Event::Resized {
+ width: logical_size.width,
+ height: logical_size.height,
+ },
+ ))
}
WindowEvent::CloseRequested => {
- Some(Event::Window(window::Event::CloseRequested))
+ Some(Event::Window(id, window::Event::CloseRequested))
}
WindowEvent::CursorMoved { position, .. } => {
let position = position.to_logical::<f64>(scale_factor);
@@ -51,7 +163,7 @@ pub fn window_event(
Some(Event::Mouse(mouse::Event::CursorLeft))
}
WindowEvent::MouseInput { button, state, .. } => {
- let button = mouse_button(*button);
+ let button = mouse_button(button);
Some(Event::Mouse(match state {
winit::event::ElementState::Pressed => {
@@ -66,8 +178,8 @@ pub fn window_event(
winit::event::MouseScrollDelta::LineDelta(delta_x, delta_y) => {
Some(Event::Mouse(mouse::Event::WheelScrolled {
delta: mouse::ScrollDelta::Lines {
- x: *delta_x,
- y: *delta_y,
+ x: delta_x,
+ y: delta_y,
},
}))
}
@@ -80,61 +192,81 @@ pub fn window_event(
}))
}
},
- WindowEvent::ReceivedCharacter(c) if !is_private_use_character(*c) => {
- Some(Event::Keyboard(keyboard::Event::CharacterReceived(*c)))
- }
WindowEvent::KeyboardInput {
- input:
- winit::event::KeyboardInput {
- virtual_keycode: Some(virtual_keycode),
+ event:
+ winit::event::KeyEvent {
+ logical_key,
state,
+ text,
+ location,
..
},
..
} => Some(Event::Keyboard({
- let key_code = key_code(*virtual_keycode);
+ let key = key(logical_key);
let modifiers = self::modifiers(modifiers);
+ let location = match location {
+ winit::keyboard::KeyLocation::Standard => {
+ keyboard::Location::Standard
+ }
+ winit::keyboard::KeyLocation::Left => keyboard::Location::Left,
+ winit::keyboard::KeyLocation::Right => {
+ keyboard::Location::Right
+ }
+ winit::keyboard::KeyLocation::Numpad => {
+ keyboard::Location::Numpad
+ }
+ };
+
match state {
winit::event::ElementState::Pressed => {
keyboard::Event::KeyPressed {
- key_code,
+ key,
modifiers,
+ location,
+ text,
}
}
winit::event::ElementState::Released => {
keyboard::Event::KeyReleased {
- key_code,
+ key,
modifiers,
+ location,
}
}
}
})),
- WindowEvent::ModifiersChanged(new_modifiers) => Some(Event::Keyboard(
- keyboard::Event::ModifiersChanged(self::modifiers(*new_modifiers)),
+ WindowEvent::ModifiersChanged(new_modifiers) => {
+ Some(Event::Keyboard(keyboard::Event::ModifiersChanged(
+ self::modifiers(new_modifiers.state()),
+ )))
+ }
+ WindowEvent::Focused(focused) => Some(Event::Window(
+ id,
+ if focused {
+ window::Event::Focused
+ } else {
+ window::Event::Unfocused
+ },
)),
- WindowEvent::Focused(focused) => Some(Event::Window(if *focused {
- window::Event::Focused
- } else {
- window::Event::Unfocused
- })),
WindowEvent::HoveredFile(path) => {
- Some(Event::Window(window::Event::FileHovered(path.clone())))
+ Some(Event::Window(id, window::Event::FileHovered(path.clone())))
}
WindowEvent::DroppedFile(path) => {
- Some(Event::Window(window::Event::FileDropped(path.clone())))
+ Some(Event::Window(id, window::Event::FileDropped(path.clone())))
}
WindowEvent::HoveredFileCancelled => {
- Some(Event::Window(window::Event::FilesHoveredLeft))
+ Some(Event::Window(id, window::Event::FilesHoveredLeft))
}
WindowEvent::Touch(touch) => {
- Some(Event::Touch(touch_event(*touch, scale_factor)))
+ Some(Event::Touch(touch_event(touch, scale_factor)))
}
WindowEvent::Moved(position) => {
let winit::dpi::LogicalPosition { x, y } =
position.to_logical(scale_factor);
- Some(Event::Window(window::Event::Moved { x, y }))
+ Some(Event::Window(id, window::Event::Moved { x, y }))
}
_ => None,
}
@@ -153,23 +285,23 @@ pub fn window_level(level: window::Level) -> winit::window::WindowLevel {
}
}
-/// Converts a [`Position`] to a [`winit`] logical position for a given monitor.
+/// Converts a [`window::Position`] to a [`winit`] logical position for a given monitor.
///
/// [`winit`]: https://github.com/rust-windowing/winit
pub fn position(
monitor: Option<&winit::monitor::MonitorHandle>,
- (width, height): (u32, u32),
- position: Position,
+ size: Size,
+ position: window::Position,
) -> Option<winit::dpi::Position> {
match position {
- Position::Default => None,
- Position::Specific(x, y) => {
+ window::Position::Default => None,
+ window::Position::Specific(position) => {
Some(winit::dpi::Position::Logical(winit::dpi::LogicalPosition {
- x: f64::from(x),
- y: f64::from(y),
+ x: f64::from(position.x),
+ y: f64::from(position.y),
}))
}
- Position::Centered => {
+ window::Position::Centered => {
if let Some(monitor) = monitor {
let start = monitor.position();
@@ -178,8 +310,8 @@ pub fn position(
let centered: winit::dpi::PhysicalPosition<i32> =
winit::dpi::LogicalPosition {
- x: (resolution.width - f64::from(width)) / 2.0,
- y: (resolution.height - f64::from(height)) / 2.0,
+ x: (resolution.width - f64::from(size.width)) / 2.0,
+ y: (resolution.height - f64::from(size.height)) / 2.0,
}
.to_physical(monitor.scale_factor());
@@ -239,7 +371,7 @@ pub fn mouse_interaction(
match interaction {
Interaction::Idle => winit::window::CursorIcon::Default,
- Interaction::Pointer => winit::window::CursorIcon::Hand,
+ Interaction::Pointer => winit::window::CursorIcon::Pointer,
Interaction::Working => winit::window::CursorIcon::Progress,
Interaction::Grab => winit::window::CursorIcon::Grab,
Interaction::Grabbing => winit::window::CursorIcon::Grabbing,
@@ -262,6 +394,8 @@ pub fn mouse_button(mouse_button: winit::event::MouseButton) -> mouse::Button {
winit::event::MouseButton::Left => mouse::Button::Left,
winit::event::MouseButton::Right => mouse::Button::Right,
winit::event::MouseButton::Middle => mouse::Button::Middle,
+ winit::event::MouseButton::Back => mouse::Button::Back,
+ winit::event::MouseButton::Forward => mouse::Button::Forward,
winit::event::MouseButton::Other(other) => mouse::Button::Other(other),
}
}
@@ -272,14 +406,14 @@ pub fn mouse_button(mouse_button: winit::event::MouseButton) -> mouse::Button {
/// [`winit`]: https://github.com/rust-windowing/winit
/// [`iced`]: https://github.com/iced-rs/iced/tree/0.10
pub fn modifiers(
- modifiers: winit::event::ModifiersState,
+ modifiers: winit::keyboard::ModifiersState,
) -> keyboard::Modifiers {
let mut result = keyboard::Modifiers::empty();
- result.set(keyboard::Modifiers::SHIFT, modifiers.shift());
- result.set(keyboard::Modifiers::CTRL, modifiers.ctrl());
- result.set(keyboard::Modifiers::ALT, modifiers.alt());
- result.set(keyboard::Modifiers::LOGO, modifiers.logo());
+ result.set(keyboard::Modifiers::SHIFT, modifiers.shift_key());
+ result.set(keyboard::Modifiers::CTRL, modifiers.control_key());
+ result.set(keyboard::Modifiers::ALT, modifiers.alt_key());
+ result.set(keyboard::Modifiers::LOGO, modifiers.super_key());
result
}
@@ -329,179 +463,328 @@ pub fn touch_event(
///
/// [`winit`]: https://github.com/rust-windowing/winit
/// [`iced`]: https://github.com/iced-rs/iced/tree/0.10
-pub fn key_code(
- virtual_keycode: winit::event::VirtualKeyCode,
-) -> keyboard::KeyCode {
- use keyboard::KeyCode;
-
- match virtual_keycode {
- winit::event::VirtualKeyCode::Key1 => KeyCode::Key1,
- winit::event::VirtualKeyCode::Key2 => KeyCode::Key2,
- winit::event::VirtualKeyCode::Key3 => KeyCode::Key3,
- winit::event::VirtualKeyCode::Key4 => KeyCode::Key4,
- winit::event::VirtualKeyCode::Key5 => KeyCode::Key5,
- winit::event::VirtualKeyCode::Key6 => KeyCode::Key6,
- winit::event::VirtualKeyCode::Key7 => KeyCode::Key7,
- winit::event::VirtualKeyCode::Key8 => KeyCode::Key8,
- winit::event::VirtualKeyCode::Key9 => KeyCode::Key9,
- winit::event::VirtualKeyCode::Key0 => KeyCode::Key0,
- winit::event::VirtualKeyCode::A => KeyCode::A,
- winit::event::VirtualKeyCode::B => KeyCode::B,
- winit::event::VirtualKeyCode::C => KeyCode::C,
- winit::event::VirtualKeyCode::D => KeyCode::D,
- winit::event::VirtualKeyCode::E => KeyCode::E,
- winit::event::VirtualKeyCode::F => KeyCode::F,
- winit::event::VirtualKeyCode::G => KeyCode::G,
- winit::event::VirtualKeyCode::H => KeyCode::H,
- winit::event::VirtualKeyCode::I => KeyCode::I,
- winit::event::VirtualKeyCode::J => KeyCode::J,
- winit::event::VirtualKeyCode::K => KeyCode::K,
- winit::event::VirtualKeyCode::L => KeyCode::L,
- winit::event::VirtualKeyCode::M => KeyCode::M,
- winit::event::VirtualKeyCode::N => KeyCode::N,
- winit::event::VirtualKeyCode::O => KeyCode::O,
- winit::event::VirtualKeyCode::P => KeyCode::P,
- winit::event::VirtualKeyCode::Q => KeyCode::Q,
- winit::event::VirtualKeyCode::R => KeyCode::R,
- winit::event::VirtualKeyCode::S => KeyCode::S,
- winit::event::VirtualKeyCode::T => KeyCode::T,
- winit::event::VirtualKeyCode::U => KeyCode::U,
- winit::event::VirtualKeyCode::V => KeyCode::V,
- winit::event::VirtualKeyCode::W => KeyCode::W,
- winit::event::VirtualKeyCode::X => KeyCode::X,
- winit::event::VirtualKeyCode::Y => KeyCode::Y,
- winit::event::VirtualKeyCode::Z => KeyCode::Z,
- winit::event::VirtualKeyCode::Escape => KeyCode::Escape,
- winit::event::VirtualKeyCode::F1 => KeyCode::F1,
- winit::event::VirtualKeyCode::F2 => KeyCode::F2,
- winit::event::VirtualKeyCode::F3 => KeyCode::F3,
- winit::event::VirtualKeyCode::F4 => KeyCode::F4,
- winit::event::VirtualKeyCode::F5 => KeyCode::F5,
- winit::event::VirtualKeyCode::F6 => KeyCode::F6,
- winit::event::VirtualKeyCode::F7 => KeyCode::F7,
- winit::event::VirtualKeyCode::F8 => KeyCode::F8,
- winit::event::VirtualKeyCode::F9 => KeyCode::F9,
- winit::event::VirtualKeyCode::F10 => KeyCode::F10,
- winit::event::VirtualKeyCode::F11 => KeyCode::F11,
- winit::event::VirtualKeyCode::F12 => KeyCode::F12,
- winit::event::VirtualKeyCode::F13 => KeyCode::F13,
- winit::event::VirtualKeyCode::F14 => KeyCode::F14,
- winit::event::VirtualKeyCode::F15 => KeyCode::F15,
- winit::event::VirtualKeyCode::F16 => KeyCode::F16,
- winit::event::VirtualKeyCode::F17 => KeyCode::F17,
- winit::event::VirtualKeyCode::F18 => KeyCode::F18,
- winit::event::VirtualKeyCode::F19 => KeyCode::F19,
- winit::event::VirtualKeyCode::F20 => KeyCode::F20,
- winit::event::VirtualKeyCode::F21 => KeyCode::F21,
- winit::event::VirtualKeyCode::F22 => KeyCode::F22,
- winit::event::VirtualKeyCode::F23 => KeyCode::F23,
- winit::event::VirtualKeyCode::F24 => KeyCode::F24,
- winit::event::VirtualKeyCode::Snapshot => KeyCode::Snapshot,
- winit::event::VirtualKeyCode::Scroll => KeyCode::Scroll,
- winit::event::VirtualKeyCode::Pause => KeyCode::Pause,
- winit::event::VirtualKeyCode::Insert => KeyCode::Insert,
- winit::event::VirtualKeyCode::Home => KeyCode::Home,
- winit::event::VirtualKeyCode::Delete => KeyCode::Delete,
- winit::event::VirtualKeyCode::End => KeyCode::End,
- winit::event::VirtualKeyCode::PageDown => KeyCode::PageDown,
- winit::event::VirtualKeyCode::PageUp => KeyCode::PageUp,
- winit::event::VirtualKeyCode::Left => KeyCode::Left,
- winit::event::VirtualKeyCode::Up => KeyCode::Up,
- winit::event::VirtualKeyCode::Right => KeyCode::Right,
- winit::event::VirtualKeyCode::Down => KeyCode::Down,
- winit::event::VirtualKeyCode::Back => KeyCode::Backspace,
- winit::event::VirtualKeyCode::Return => KeyCode::Enter,
- winit::event::VirtualKeyCode::Space => KeyCode::Space,
- winit::event::VirtualKeyCode::Compose => KeyCode::Compose,
- winit::event::VirtualKeyCode::Caret => KeyCode::Caret,
- winit::event::VirtualKeyCode::Numlock => KeyCode::Numlock,
- winit::event::VirtualKeyCode::Numpad0 => KeyCode::Numpad0,
- winit::event::VirtualKeyCode::Numpad1 => KeyCode::Numpad1,
- winit::event::VirtualKeyCode::Numpad2 => KeyCode::Numpad2,
- winit::event::VirtualKeyCode::Numpad3 => KeyCode::Numpad3,
- winit::event::VirtualKeyCode::Numpad4 => KeyCode::Numpad4,
- winit::event::VirtualKeyCode::Numpad5 => KeyCode::Numpad5,
- winit::event::VirtualKeyCode::Numpad6 => KeyCode::Numpad6,
- winit::event::VirtualKeyCode::Numpad7 => KeyCode::Numpad7,
- winit::event::VirtualKeyCode::Numpad8 => KeyCode::Numpad8,
- winit::event::VirtualKeyCode::Numpad9 => KeyCode::Numpad9,
- winit::event::VirtualKeyCode::AbntC1 => KeyCode::AbntC1,
- winit::event::VirtualKeyCode::AbntC2 => KeyCode::AbntC2,
- winit::event::VirtualKeyCode::NumpadAdd => KeyCode::NumpadAdd,
- winit::event::VirtualKeyCode::Plus => KeyCode::Plus,
- winit::event::VirtualKeyCode::Apostrophe => KeyCode::Apostrophe,
- winit::event::VirtualKeyCode::Apps => KeyCode::Apps,
- winit::event::VirtualKeyCode::At => KeyCode::At,
- winit::event::VirtualKeyCode::Ax => KeyCode::Ax,
- winit::event::VirtualKeyCode::Backslash => KeyCode::Backslash,
- winit::event::VirtualKeyCode::Calculator => KeyCode::Calculator,
- winit::event::VirtualKeyCode::Capital => KeyCode::Capital,
- winit::event::VirtualKeyCode::Colon => KeyCode::Colon,
- winit::event::VirtualKeyCode::Comma => KeyCode::Comma,
- winit::event::VirtualKeyCode::Convert => KeyCode::Convert,
- winit::event::VirtualKeyCode::NumpadDecimal => KeyCode::NumpadDecimal,
- winit::event::VirtualKeyCode::NumpadDivide => KeyCode::NumpadDivide,
- winit::event::VirtualKeyCode::Equals => KeyCode::Equals,
- winit::event::VirtualKeyCode::Grave => KeyCode::Grave,
- winit::event::VirtualKeyCode::Kana => KeyCode::Kana,
- winit::event::VirtualKeyCode::Kanji => KeyCode::Kanji,
- winit::event::VirtualKeyCode::LAlt => KeyCode::LAlt,
- winit::event::VirtualKeyCode::LBracket => KeyCode::LBracket,
- winit::event::VirtualKeyCode::LControl => KeyCode::LControl,
- winit::event::VirtualKeyCode::LShift => KeyCode::LShift,
- winit::event::VirtualKeyCode::LWin => KeyCode::LWin,
- winit::event::VirtualKeyCode::Mail => KeyCode::Mail,
- winit::event::VirtualKeyCode::MediaSelect => KeyCode::MediaSelect,
- winit::event::VirtualKeyCode::MediaStop => KeyCode::MediaStop,
- winit::event::VirtualKeyCode::Minus => KeyCode::Minus,
- winit::event::VirtualKeyCode::NumpadMultiply => KeyCode::NumpadMultiply,
- winit::event::VirtualKeyCode::Mute => KeyCode::Mute,
- winit::event::VirtualKeyCode::MyComputer => KeyCode::MyComputer,
- winit::event::VirtualKeyCode::NavigateForward => {
- KeyCode::NavigateForward
- }
- winit::event::VirtualKeyCode::NavigateBackward => {
- KeyCode::NavigateBackward
+pub fn key(key: winit::keyboard::Key) -> keyboard::Key {
+ use keyboard::key::Named;
+ use winit::keyboard::NamedKey;
+
+ match key {
+ winit::keyboard::Key::Character(c) => keyboard::Key::Character(c),
+ winit::keyboard::Key::Named(named_key) => {
+ keyboard::Key::Named(match named_key {
+ NamedKey::Alt => Named::Alt,
+ NamedKey::AltGraph => Named::AltGraph,
+ NamedKey::CapsLock => Named::CapsLock,
+ NamedKey::Control => Named::Control,
+ NamedKey::Fn => Named::Fn,
+ NamedKey::FnLock => Named::FnLock,
+ NamedKey::NumLock => Named::NumLock,
+ NamedKey::ScrollLock => Named::ScrollLock,
+ NamedKey::Shift => Named::Shift,
+ NamedKey::Symbol => Named::Symbol,
+ NamedKey::SymbolLock => Named::SymbolLock,
+ NamedKey::Meta => Named::Meta,
+ NamedKey::Hyper => Named::Hyper,
+ NamedKey::Super => Named::Super,
+ NamedKey::Enter => Named::Enter,
+ NamedKey::Tab => Named::Tab,
+ NamedKey::Space => Named::Space,
+ NamedKey::ArrowDown => Named::ArrowDown,
+ NamedKey::ArrowLeft => Named::ArrowLeft,
+ NamedKey::ArrowRight => Named::ArrowRight,
+ NamedKey::ArrowUp => Named::ArrowUp,
+ NamedKey::End => Named::End,
+ NamedKey::Home => Named::Home,
+ NamedKey::PageDown => Named::PageDown,
+ NamedKey::PageUp => Named::PageUp,
+ NamedKey::Backspace => Named::Backspace,
+ NamedKey::Clear => Named::Clear,
+ NamedKey::Copy => Named::Copy,
+ NamedKey::CrSel => Named::CrSel,
+ NamedKey::Cut => Named::Cut,
+ NamedKey::Delete => Named::Delete,
+ NamedKey::EraseEof => Named::EraseEof,
+ NamedKey::ExSel => Named::ExSel,
+ NamedKey::Insert => Named::Insert,
+ NamedKey::Paste => Named::Paste,
+ NamedKey::Redo => Named::Redo,
+ NamedKey::Undo => Named::Undo,
+ NamedKey::Accept => Named::Accept,
+ NamedKey::Again => Named::Again,
+ NamedKey::Attn => Named::Attn,
+ NamedKey::Cancel => Named::Cancel,
+ NamedKey::ContextMenu => Named::ContextMenu,
+ NamedKey::Escape => Named::Escape,
+ NamedKey::Execute => Named::Execute,
+ NamedKey::Find => Named::Find,
+ NamedKey::Help => Named::Help,
+ NamedKey::Pause => Named::Pause,
+ NamedKey::Play => Named::Play,
+ NamedKey::Props => Named::Props,
+ NamedKey::Select => Named::Select,
+ NamedKey::ZoomIn => Named::ZoomIn,
+ NamedKey::ZoomOut => Named::ZoomOut,
+ NamedKey::BrightnessDown => Named::BrightnessDown,
+ NamedKey::BrightnessUp => Named::BrightnessUp,
+ NamedKey::Eject => Named::Eject,
+ NamedKey::LogOff => Named::LogOff,
+ NamedKey::Power => Named::Power,
+ NamedKey::PowerOff => Named::PowerOff,
+ NamedKey::PrintScreen => Named::PrintScreen,
+ NamedKey::Hibernate => Named::Hibernate,
+ NamedKey::Standby => Named::Standby,
+ NamedKey::WakeUp => Named::WakeUp,
+ NamedKey::AllCandidates => Named::AllCandidates,
+ NamedKey::Alphanumeric => Named::Alphanumeric,
+ NamedKey::CodeInput => Named::CodeInput,
+ NamedKey::Compose => Named::Compose,
+ NamedKey::Convert => Named::Convert,
+ NamedKey::FinalMode => Named::FinalMode,
+ NamedKey::GroupFirst => Named::GroupFirst,
+ NamedKey::GroupLast => Named::GroupLast,
+ NamedKey::GroupNext => Named::GroupNext,
+ NamedKey::GroupPrevious => Named::GroupPrevious,
+ NamedKey::ModeChange => Named::ModeChange,
+ NamedKey::NextCandidate => Named::NextCandidate,
+ NamedKey::NonConvert => Named::NonConvert,
+ NamedKey::PreviousCandidate => Named::PreviousCandidate,
+ NamedKey::Process => Named::Process,
+ NamedKey::SingleCandidate => Named::SingleCandidate,
+ NamedKey::HangulMode => Named::HangulMode,
+ NamedKey::HanjaMode => Named::HanjaMode,
+ NamedKey::JunjaMode => Named::JunjaMode,
+ NamedKey::Eisu => Named::Eisu,
+ NamedKey::Hankaku => Named::Hankaku,
+ NamedKey::Hiragana => Named::Hiragana,
+ NamedKey::HiraganaKatakana => Named::HiraganaKatakana,
+ NamedKey::KanaMode => Named::KanaMode,
+ NamedKey::KanjiMode => Named::KanjiMode,
+ NamedKey::Katakana => Named::Katakana,
+ NamedKey::Romaji => Named::Romaji,
+ NamedKey::Zenkaku => Named::Zenkaku,
+ NamedKey::ZenkakuHankaku => Named::ZenkakuHankaku,
+ NamedKey::Soft1 => Named::Soft1,
+ NamedKey::Soft2 => Named::Soft2,
+ NamedKey::Soft3 => Named::Soft3,
+ NamedKey::Soft4 => Named::Soft4,
+ NamedKey::ChannelDown => Named::ChannelDown,
+ NamedKey::ChannelUp => Named::ChannelUp,
+ NamedKey::Close => Named::Close,
+ NamedKey::MailForward => Named::MailForward,
+ NamedKey::MailReply => Named::MailReply,
+ NamedKey::MailSend => Named::MailSend,
+ NamedKey::MediaClose => Named::MediaClose,
+ NamedKey::MediaFastForward => Named::MediaFastForward,
+ NamedKey::MediaPause => Named::MediaPause,
+ NamedKey::MediaPlay => Named::MediaPlay,
+ NamedKey::MediaPlayPause => Named::MediaPlayPause,
+ NamedKey::MediaRecord => Named::MediaRecord,
+ NamedKey::MediaRewind => Named::MediaRewind,
+ NamedKey::MediaStop => Named::MediaStop,
+ NamedKey::MediaTrackNext => Named::MediaTrackNext,
+ NamedKey::MediaTrackPrevious => Named::MediaTrackPrevious,
+ NamedKey::New => Named::New,
+ NamedKey::Open => Named::Open,
+ NamedKey::Print => Named::Print,
+ NamedKey::Save => Named::Save,
+ NamedKey::SpellCheck => Named::SpellCheck,
+ NamedKey::Key11 => Named::Key11,
+ NamedKey::Key12 => Named::Key12,
+ NamedKey::AudioBalanceLeft => Named::AudioBalanceLeft,
+ NamedKey::AudioBalanceRight => Named::AudioBalanceRight,
+ NamedKey::AudioBassBoostDown => Named::AudioBassBoostDown,
+ NamedKey::AudioBassBoostToggle => Named::AudioBassBoostToggle,
+ NamedKey::AudioBassBoostUp => Named::AudioBassBoostUp,
+ NamedKey::AudioFaderFront => Named::AudioFaderFront,
+ NamedKey::AudioFaderRear => Named::AudioFaderRear,
+ NamedKey::AudioSurroundModeNext => Named::AudioSurroundModeNext,
+ NamedKey::AudioTrebleDown => Named::AudioTrebleDown,
+ NamedKey::AudioTrebleUp => Named::AudioTrebleUp,
+ NamedKey::AudioVolumeDown => Named::AudioVolumeDown,
+ NamedKey::AudioVolumeUp => Named::AudioVolumeUp,
+ NamedKey::AudioVolumeMute => Named::AudioVolumeMute,
+ NamedKey::MicrophoneToggle => Named::MicrophoneToggle,
+ NamedKey::MicrophoneVolumeDown => Named::MicrophoneVolumeDown,
+ NamedKey::MicrophoneVolumeUp => Named::MicrophoneVolumeUp,
+ NamedKey::MicrophoneVolumeMute => Named::MicrophoneVolumeMute,
+ NamedKey::SpeechCorrectionList => Named::SpeechCorrectionList,
+ NamedKey::SpeechInputToggle => Named::SpeechInputToggle,
+ NamedKey::LaunchApplication1 => Named::LaunchApplication1,
+ NamedKey::LaunchApplication2 => Named::LaunchApplication2,
+ NamedKey::LaunchCalendar => Named::LaunchCalendar,
+ NamedKey::LaunchContacts => Named::LaunchContacts,
+ NamedKey::LaunchMail => Named::LaunchMail,
+ NamedKey::LaunchMediaPlayer => Named::LaunchMediaPlayer,
+ NamedKey::LaunchMusicPlayer => Named::LaunchMusicPlayer,
+ NamedKey::LaunchPhone => Named::LaunchPhone,
+ NamedKey::LaunchScreenSaver => Named::LaunchScreenSaver,
+ NamedKey::LaunchSpreadsheet => Named::LaunchSpreadsheet,
+ NamedKey::LaunchWebBrowser => Named::LaunchWebBrowser,
+ NamedKey::LaunchWebCam => Named::LaunchWebCam,
+ NamedKey::LaunchWordProcessor => Named::LaunchWordProcessor,
+ NamedKey::BrowserBack => Named::BrowserBack,
+ NamedKey::BrowserFavorites => Named::BrowserFavorites,
+ NamedKey::BrowserForward => Named::BrowserForward,
+ NamedKey::BrowserHome => Named::BrowserHome,
+ NamedKey::BrowserRefresh => Named::BrowserRefresh,
+ NamedKey::BrowserSearch => Named::BrowserSearch,
+ NamedKey::BrowserStop => Named::BrowserStop,
+ NamedKey::AppSwitch => Named::AppSwitch,
+ NamedKey::Call => Named::Call,
+ NamedKey::Camera => Named::Camera,
+ NamedKey::CameraFocus => Named::CameraFocus,
+ NamedKey::EndCall => Named::EndCall,
+ NamedKey::GoBack => Named::GoBack,
+ NamedKey::GoHome => Named::GoHome,
+ NamedKey::HeadsetHook => Named::HeadsetHook,
+ NamedKey::LastNumberRedial => Named::LastNumberRedial,
+ NamedKey::Notification => Named::Notification,
+ NamedKey::MannerMode => Named::MannerMode,
+ NamedKey::VoiceDial => Named::VoiceDial,
+ NamedKey::TV => Named::TV,
+ NamedKey::TV3DMode => Named::TV3DMode,
+ NamedKey::TVAntennaCable => Named::TVAntennaCable,
+ NamedKey::TVAudioDescription => Named::TVAudioDescription,
+ NamedKey::TVAudioDescriptionMixDown => {
+ Named::TVAudioDescriptionMixDown
+ }
+ NamedKey::TVAudioDescriptionMixUp => {
+ Named::TVAudioDescriptionMixUp
+ }
+ NamedKey::TVContentsMenu => Named::TVContentsMenu,
+ NamedKey::TVDataService => Named::TVDataService,
+ NamedKey::TVInput => Named::TVInput,
+ NamedKey::TVInputComponent1 => Named::TVInputComponent1,
+ NamedKey::TVInputComponent2 => Named::TVInputComponent2,
+ NamedKey::TVInputComposite1 => Named::TVInputComposite1,
+ NamedKey::TVInputComposite2 => Named::TVInputComposite2,
+ NamedKey::TVInputHDMI1 => Named::TVInputHDMI1,
+ NamedKey::TVInputHDMI2 => Named::TVInputHDMI2,
+ NamedKey::TVInputHDMI3 => Named::TVInputHDMI3,
+ NamedKey::TVInputHDMI4 => Named::TVInputHDMI4,
+ NamedKey::TVInputVGA1 => Named::TVInputVGA1,
+ NamedKey::TVMediaContext => Named::TVMediaContext,
+ NamedKey::TVNetwork => Named::TVNetwork,
+ NamedKey::TVNumberEntry => Named::TVNumberEntry,
+ NamedKey::TVPower => Named::TVPower,
+ NamedKey::TVRadioService => Named::TVRadioService,
+ NamedKey::TVSatellite => Named::TVSatellite,
+ NamedKey::TVSatelliteBS => Named::TVSatelliteBS,
+ NamedKey::TVSatelliteCS => Named::TVSatelliteCS,
+ NamedKey::TVSatelliteToggle => Named::TVSatelliteToggle,
+ NamedKey::TVTerrestrialAnalog => Named::TVTerrestrialAnalog,
+ NamedKey::TVTerrestrialDigital => Named::TVTerrestrialDigital,
+ NamedKey::TVTimer => Named::TVTimer,
+ NamedKey::AVRInput => Named::AVRInput,
+ NamedKey::AVRPower => Named::AVRPower,
+ NamedKey::ColorF0Red => Named::ColorF0Red,
+ NamedKey::ColorF1Green => Named::ColorF1Green,
+ NamedKey::ColorF2Yellow => Named::ColorF2Yellow,
+ NamedKey::ColorF3Blue => Named::ColorF3Blue,
+ NamedKey::ColorF4Grey => Named::ColorF4Grey,
+ NamedKey::ColorF5Brown => Named::ColorF5Brown,
+ NamedKey::ClosedCaptionToggle => Named::ClosedCaptionToggle,
+ NamedKey::Dimmer => Named::Dimmer,
+ NamedKey::DisplaySwap => Named::DisplaySwap,
+ NamedKey::DVR => Named::DVR,
+ NamedKey::Exit => Named::Exit,
+ NamedKey::FavoriteClear0 => Named::FavoriteClear0,
+ NamedKey::FavoriteClear1 => Named::FavoriteClear1,
+ NamedKey::FavoriteClear2 => Named::FavoriteClear2,
+ NamedKey::FavoriteClear3 => Named::FavoriteClear3,
+ NamedKey::FavoriteRecall0 => Named::FavoriteRecall0,
+ NamedKey::FavoriteRecall1 => Named::FavoriteRecall1,
+ NamedKey::FavoriteRecall2 => Named::FavoriteRecall2,
+ NamedKey::FavoriteRecall3 => Named::FavoriteRecall3,
+ NamedKey::FavoriteStore0 => Named::FavoriteStore0,
+ NamedKey::FavoriteStore1 => Named::FavoriteStore1,
+ NamedKey::FavoriteStore2 => Named::FavoriteStore2,
+ NamedKey::FavoriteStore3 => Named::FavoriteStore3,
+ NamedKey::Guide => Named::Guide,
+ NamedKey::GuideNextDay => Named::GuideNextDay,
+ NamedKey::GuidePreviousDay => Named::GuidePreviousDay,
+ NamedKey::Info => Named::Info,
+ NamedKey::InstantReplay => Named::InstantReplay,
+ NamedKey::Link => Named::Link,
+ NamedKey::ListProgram => Named::ListProgram,
+ NamedKey::LiveContent => Named::LiveContent,
+ NamedKey::Lock => Named::Lock,
+ NamedKey::MediaApps => Named::MediaApps,
+ NamedKey::MediaAudioTrack => Named::MediaAudioTrack,
+ NamedKey::MediaLast => Named::MediaLast,
+ NamedKey::MediaSkipBackward => Named::MediaSkipBackward,
+ NamedKey::MediaSkipForward => Named::MediaSkipForward,
+ NamedKey::MediaStepBackward => Named::MediaStepBackward,
+ NamedKey::MediaStepForward => Named::MediaStepForward,
+ NamedKey::MediaTopMenu => Named::MediaTopMenu,
+ NamedKey::NavigateIn => Named::NavigateIn,
+ NamedKey::NavigateNext => Named::NavigateNext,
+ NamedKey::NavigateOut => Named::NavigateOut,
+ NamedKey::NavigatePrevious => Named::NavigatePrevious,
+ NamedKey::NextFavoriteChannel => Named::NextFavoriteChannel,
+ NamedKey::NextUserProfile => Named::NextUserProfile,
+ NamedKey::OnDemand => Named::OnDemand,
+ NamedKey::Pairing => Named::Pairing,
+ NamedKey::PinPDown => Named::PinPDown,
+ NamedKey::PinPMove => Named::PinPMove,
+ NamedKey::PinPToggle => Named::PinPToggle,
+ NamedKey::PinPUp => Named::PinPUp,
+ NamedKey::PlaySpeedDown => Named::PlaySpeedDown,
+ NamedKey::PlaySpeedReset => Named::PlaySpeedReset,
+ NamedKey::PlaySpeedUp => Named::PlaySpeedUp,
+ NamedKey::RandomToggle => Named::RandomToggle,
+ NamedKey::RcLowBattery => Named::RcLowBattery,
+ NamedKey::RecordSpeedNext => Named::RecordSpeedNext,
+ NamedKey::RfBypass => Named::RfBypass,
+ NamedKey::ScanChannelsToggle => Named::ScanChannelsToggle,
+ NamedKey::ScreenModeNext => Named::ScreenModeNext,
+ NamedKey::Settings => Named::Settings,
+ NamedKey::SplitScreenToggle => Named::SplitScreenToggle,
+ NamedKey::STBInput => Named::STBInput,
+ NamedKey::STBPower => Named::STBPower,
+ NamedKey::Subtitle => Named::Subtitle,
+ NamedKey::Teletext => Named::Teletext,
+ NamedKey::VideoModeNext => Named::VideoModeNext,
+ NamedKey::Wink => Named::Wink,
+ NamedKey::ZoomToggle => Named::ZoomToggle,
+ NamedKey::F1 => Named::F1,
+ NamedKey::F2 => Named::F2,
+ NamedKey::F3 => Named::F3,
+ NamedKey::F4 => Named::F4,
+ NamedKey::F5 => Named::F5,
+ NamedKey::F6 => Named::F6,
+ NamedKey::F7 => Named::F7,
+ NamedKey::F8 => Named::F8,
+ NamedKey::F9 => Named::F9,
+ NamedKey::F10 => Named::F10,
+ NamedKey::F11 => Named::F11,
+ NamedKey::F12 => Named::F12,
+ NamedKey::F13 => Named::F13,
+ NamedKey::F14 => Named::F14,
+ NamedKey::F15 => Named::F15,
+ NamedKey::F16 => Named::F16,
+ NamedKey::F17 => Named::F17,
+ NamedKey::F18 => Named::F18,
+ NamedKey::F19 => Named::F19,
+ NamedKey::F20 => Named::F20,
+ NamedKey::F21 => Named::F21,
+ NamedKey::F22 => Named::F22,
+ NamedKey::F23 => Named::F23,
+ NamedKey::F24 => Named::F24,
+ NamedKey::F25 => Named::F25,
+ NamedKey::F26 => Named::F26,
+ NamedKey::F27 => Named::F27,
+ NamedKey::F28 => Named::F28,
+ NamedKey::F29 => Named::F29,
+ NamedKey::F30 => Named::F30,
+ NamedKey::F31 => Named::F31,
+ NamedKey::F32 => Named::F32,
+ NamedKey::F33 => Named::F33,
+ NamedKey::F34 => Named::F34,
+ NamedKey::F35 => Named::F35,
+ _ => return keyboard::Key::Unidentified,
+ })
}
- winit::event::VirtualKeyCode::NextTrack => KeyCode::NextTrack,
- winit::event::VirtualKeyCode::NoConvert => KeyCode::NoConvert,
- winit::event::VirtualKeyCode::NumpadComma => KeyCode::NumpadComma,
- winit::event::VirtualKeyCode::NumpadEnter => KeyCode::NumpadEnter,
- winit::event::VirtualKeyCode::NumpadEquals => KeyCode::NumpadEquals,
- winit::event::VirtualKeyCode::OEM102 => KeyCode::OEM102,
- winit::event::VirtualKeyCode::Period => KeyCode::Period,
- winit::event::VirtualKeyCode::PlayPause => KeyCode::PlayPause,
- winit::event::VirtualKeyCode::Power => KeyCode::Power,
- winit::event::VirtualKeyCode::PrevTrack => KeyCode::PrevTrack,
- winit::event::VirtualKeyCode::RAlt => KeyCode::RAlt,
- winit::event::VirtualKeyCode::RBracket => KeyCode::RBracket,
- winit::event::VirtualKeyCode::RControl => KeyCode::RControl,
- winit::event::VirtualKeyCode::RShift => KeyCode::RShift,
- winit::event::VirtualKeyCode::RWin => KeyCode::RWin,
- winit::event::VirtualKeyCode::Semicolon => KeyCode::Semicolon,
- winit::event::VirtualKeyCode::Slash => KeyCode::Slash,
- winit::event::VirtualKeyCode::Sleep => KeyCode::Sleep,
- winit::event::VirtualKeyCode::Stop => KeyCode::Stop,
- winit::event::VirtualKeyCode::NumpadSubtract => KeyCode::NumpadSubtract,
- winit::event::VirtualKeyCode::Sysrq => KeyCode::Sysrq,
- winit::event::VirtualKeyCode::Tab => KeyCode::Tab,
- winit::event::VirtualKeyCode::Underline => KeyCode::Underline,
- winit::event::VirtualKeyCode::Unlabeled => KeyCode::Unlabeled,
- winit::event::VirtualKeyCode::VolumeDown => KeyCode::VolumeDown,
- winit::event::VirtualKeyCode::VolumeUp => KeyCode::VolumeUp,
- winit::event::VirtualKeyCode::Wake => KeyCode::Wake,
- winit::event::VirtualKeyCode::WebBack => KeyCode::WebBack,
- winit::event::VirtualKeyCode::WebFavorites => KeyCode::WebFavorites,
- winit::event::VirtualKeyCode::WebForward => KeyCode::WebForward,
- winit::event::VirtualKeyCode::WebHome => KeyCode::WebHome,
- winit::event::VirtualKeyCode::WebRefresh => KeyCode::WebRefresh,
- winit::event::VirtualKeyCode::WebSearch => KeyCode::WebSearch,
- winit::event::VirtualKeyCode::WebStop => KeyCode::WebStop,
- winit::event::VirtualKeyCode::Yen => KeyCode::Yen,
- winit::event::VirtualKeyCode::Copy => KeyCode::Copy,
- winit::event::VirtualKeyCode::Paste => KeyCode::Paste,
- winit::event::VirtualKeyCode::Cut => KeyCode::Cut,
- winit::event::VirtualKeyCode::Asterisk => KeyCode::Asterisk,
+ _ => keyboard::Key::Unidentified,
}
}
@@ -529,13 +812,3 @@ pub fn icon(icon: window::Icon) -> Option<winit::window::Icon> {
winit::window::Icon::from_rgba(pixels, size.width, size.height).ok()
}
-
-// As defined in: http://www.unicode.org/faq/private_use.html
-pub(crate) fn is_private_use_character(c: char) -> bool {
- matches!(
- c,
- '\u{E000}'..='\u{F8FF}'
- | '\u{F0000}'..='\u{FFFFD}'
- | '\u{100000}'..='\u{10FFFD}'
- )
-}
diff --git a/winit/src/lib.rs b/winit/src/lib.rs
index 95b55bb9..948576a2 100644
--- a/winit/src/lib.rs
+++ b/winit/src/lib.rs
@@ -33,6 +33,9 @@ pub use iced_runtime::futures;
pub use iced_style as style;
pub use winit;
+#[cfg(feature = "multi-window")]
+pub mod multi_window;
+
#[cfg(feature = "application")]
pub mod application;
pub mod clipboard;
@@ -43,17 +46,11 @@ pub mod settings;
pub mod system;
mod error;
-mod position;
mod proxy;
#[cfg(feature = "application")]
pub use application::Application;
-#[cfg(feature = "trace")]
-pub use application::Profiler;
pub use clipboard::Clipboard;
pub use error::Error;
-pub use position::Position;
pub use proxy::Proxy;
pub use settings::Settings;
-
-pub use iced_graphics::Viewport;
diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs
new file mode 100644
index 00000000..3f0ba056
--- /dev/null
+++ b/winit/src/multi_window.rs
@@ -0,0 +1,1189 @@
+//! Create interactive, native cross-platform applications for WGPU.
+mod state;
+mod window_manager;
+
+pub use state::State;
+
+use crate::conversion;
+use crate::core;
+use crate::core::renderer;
+use crate::core::widget::operation;
+use crate::core::window;
+use crate::core::Size;
+use crate::futures::futures::channel::mpsc;
+use crate::futures::futures::{task, Future, StreamExt};
+use crate::futures::{Executor, Runtime, Subscription};
+use crate::graphics::{compositor, Compositor};
+use crate::multi_window::window_manager::WindowManager;
+use crate::runtime::command::{self, Command};
+use crate::runtime::multi_window::Program;
+use crate::runtime::user_interface::{self, UserInterface};
+use crate::runtime::Debug;
+use crate::style::application::StyleSheet;
+use crate::{Clipboard, Error, Proxy, Settings};
+
+use std::collections::HashMap;
+use std::mem::ManuallyDrop;
+use std::sync::Arc;
+use std::time::Instant;
+
+/// An interactive, native, cross-platform, multi-windowed application.
+///
+/// This trait is the main entrypoint of multi-window Iced. Once implemented, you can run
+/// your GUI application by simply calling [`run`]. It will run in
+/// its own window.
+///
+/// An [`Application`] can execute asynchronous actions by returning a
+/// [`Command`] in some of its methods.
+///
+/// When using an [`Application`] with the `debug` feature enabled, a debug view
+/// can be toggled by pressing `F12`.
+pub trait Application: Program
+where
+ <Self::Renderer as core::Renderer>::Theme: StyleSheet,
+{
+ /// The data needed to initialize your [`Application`].
+ type Flags;
+
+ /// Initializes the [`Application`] with the flags provided to
+ /// [`run`] as part of the [`Settings`].
+ ///
+ /// Here is where you should return the initial state of your app.
+ ///
+ /// Additionally, you can return a [`Command`] if you need to perform some
+ /// async action in the background on startup. This is useful if you want to
+ /// load state from a file, perform an initial HTTP request, etc.
+ fn new(flags: Self::Flags) -> (Self, Command<Self::Message>);
+
+ /// Returns the current title of the [`Application`].
+ ///
+ /// This title can be dynamic! The runtime will automatically update the
+ /// title of your application when necessary.
+ fn title(&self, window: window::Id) -> String;
+
+ /// Returns the current `Theme` of the [`Application`].
+ fn theme(
+ &self,
+ window: window::Id,
+ ) -> <Self::Renderer as core::Renderer>::Theme;
+
+ /// Returns the `Style` variation of the `Theme`.
+ fn style(
+ &self,
+ ) -> <<Self::Renderer as core::Renderer>::Theme as StyleSheet>::Style {
+ Default::default()
+ }
+
+ /// Returns the event `Subscription` for the current state of the
+ /// application.
+ ///
+ /// The messages produced by the `Subscription` will be handled by
+ /// [`update`](#tymethod.update).
+ ///
+ /// A `Subscription` will be kept alive as long as you keep returning it!
+ ///
+ /// By default, it returns an empty subscription.
+ fn subscription(&self) -> Subscription<Self::Message> {
+ Subscription::none()
+ }
+
+ /// Returns the scale factor of the window of the [`Application`].
+ ///
+ /// It can be used to dynamically control the size of the UI at runtime
+ /// (i.e. zooming).
+ ///
+ /// For instance, a scale factor of `2.0` will make widgets twice as big,
+ /// while a scale factor of `0.5` will shrink them to half their size.
+ ///
+ /// By default, it returns `1.0`.
+ #[allow(unused_variables)]
+ fn scale_factor(&self, window: window::Id) -> f64 {
+ 1.0
+ }
+}
+
+/// Runs an [`Application`] with an executor, compositor, and the provided
+/// settings.
+pub fn run<A, E, C>(
+ settings: Settings<A::Flags>,
+ compositor_settings: C::Settings,
+) -> Result<(), Error>
+where
+ A: Application + 'static,
+ E: Executor + 'static,
+ C: Compositor<Renderer = A::Renderer> + 'static,
+ <A::Renderer as core::Renderer>::Theme: StyleSheet,
+{
+ use winit::event_loop::EventLoopBuilder;
+
+ let mut debug = Debug::new();
+ debug.startup_started();
+
+ let event_loop = EventLoopBuilder::with_user_event()
+ .build()
+ .expect("Create event loop");
+
+ let proxy = event_loop.create_proxy();
+
+ let runtime = {
+ let proxy = Proxy::new(event_loop.create_proxy());
+ let executor = E::new().map_err(Error::ExecutorCreationFailed)?;
+
+ Runtime::new(executor, proxy)
+ };
+
+ let (application, init_command) = {
+ let flags = settings.flags;
+
+ runtime.enter(|| A::new(flags))
+ };
+
+ let should_main_be_visible = settings.window.visible;
+ let exit_on_close_request = settings.window.exit_on_close_request;
+
+ let builder = conversion::window_settings(
+ settings.window,
+ &application.title(window::Id::MAIN),
+ event_loop.primary_monitor(),
+ settings.id,
+ )
+ .with_visible(false);
+
+ log::info!("Window builder: {:#?}", builder);
+
+ let main_window = Arc::new(
+ builder
+ .build(&event_loop)
+ .map_err(Error::WindowCreationFailed)?,
+ );
+
+ #[cfg(target_arch = "wasm32")]
+ {
+ use winit::platform::web::WindowExtWebSys;
+
+ let canvas = main_window.canvas();
+
+ let window = web_sys::window().unwrap();
+ let document = window.document().unwrap();
+ let body = document.body().unwrap();
+
+ let target = target.and_then(|target| {
+ body.query_selector(&format!("#{}", target))
+ .ok()
+ .unwrap_or(None)
+ });
+
+ match target {
+ Some(node) => {
+ let _ = node
+ .replace_with_with_node_1(&canvas)
+ .expect(&format!("Could not replace #{}", node.id()));
+ }
+ None => {
+ let _ = body
+ .append_child(&canvas)
+ .expect("Append canvas to HTML body");
+ }
+ };
+ }
+
+ let mut compositor = C::new(compositor_settings, main_window.clone())?;
+
+ let mut window_manager = WindowManager::new();
+ let _ = window_manager.insert(
+ window::Id::MAIN,
+ main_window,
+ &application,
+ &mut compositor,
+ exit_on_close_request,
+ );
+
+ let (mut event_sender, event_receiver) = mpsc::unbounded();
+ let (control_sender, mut control_receiver) = mpsc::unbounded();
+
+ let mut instance = Box::pin(run_instance::<A, E, C>(
+ application,
+ compositor,
+ runtime,
+ proxy,
+ debug,
+ event_receiver,
+ control_sender,
+ init_command,
+ window_manager,
+ should_main_be_visible,
+ ));
+
+ let mut context = task::Context::from_waker(task::noop_waker_ref());
+
+ let _ = event_loop.run(move |event, event_loop| {
+ if event_loop.exiting() {
+ return;
+ }
+
+ event_sender
+ .start_send(Event::EventLoopAwakened(event))
+ .expect("Send event");
+
+ loop {
+ let poll = instance.as_mut().poll(&mut context);
+
+ match poll {
+ task::Poll::Pending => match control_receiver.try_next() {
+ Ok(Some(control)) => match control {
+ Control::ChangeFlow(flow) => {
+ use winit::event_loop::ControlFlow;
+
+ match (event_loop.control_flow(), flow) {
+ (
+ ControlFlow::WaitUntil(current),
+ ControlFlow::WaitUntil(new),
+ ) if new < current => {}
+ (
+ ControlFlow::WaitUntil(target),
+ ControlFlow::Wait,
+ ) if target > Instant::now() => {}
+ _ => {
+ event_loop.set_control_flow(flow);
+ }
+ }
+ }
+ Control::CreateWindow {
+ id,
+ settings,
+ title,
+ monitor,
+ } => {
+ let exit_on_close_request =
+ settings.exit_on_close_request;
+
+ let window = conversion::window_settings(
+ settings, &title, monitor, None,
+ )
+ .build(event_loop)
+ .expect("Failed to build window");
+
+ event_sender
+ .start_send(Event::WindowCreated {
+ id,
+ window,
+ exit_on_close_request,
+ })
+ .expect("Send event");
+ }
+ Control::Exit => {
+ event_loop.exit();
+ }
+ },
+ _ => {
+ break;
+ }
+ },
+ task::Poll::Ready(_) => {
+ event_loop.exit();
+ break;
+ }
+ };
+ }
+ });
+
+ Ok(())
+}
+
+enum Event<Message: 'static> {
+ WindowCreated {
+ id: window::Id,
+ window: winit::window::Window,
+ exit_on_close_request: bool,
+ },
+ EventLoopAwakened(winit::event::Event<Message>),
+}
+
+enum Control {
+ ChangeFlow(winit::event_loop::ControlFlow),
+ Exit,
+ CreateWindow {
+ id: window::Id,
+ settings: window::Settings,
+ title: String,
+ monitor: Option<winit::monitor::MonitorHandle>,
+ },
+}
+
+async fn run_instance<A, E, C>(
+ mut application: A,
+ mut compositor: C,
+ mut runtime: Runtime<E, Proxy<A::Message>, A::Message>,
+ mut proxy: winit::event_loop::EventLoopProxy<A::Message>,
+ mut debug: Debug,
+ mut event_receiver: mpsc::UnboundedReceiver<Event<A::Message>>,
+ mut control_sender: mpsc::UnboundedSender<Control>,
+ init_command: Command<A::Message>,
+ mut window_manager: WindowManager<A, C>,
+ should_main_window_be_visible: bool,
+) where
+ A: Application + 'static,
+ E: Executor + 'static,
+ C: Compositor<Renderer = A::Renderer> + 'static,
+ <A::Renderer as core::Renderer>::Theme: StyleSheet,
+{
+ use winit::event;
+ use winit::event_loop::ControlFlow;
+
+ let main_window = window_manager
+ .get_mut(window::Id::MAIN)
+ .expect("Get main window");
+
+ if should_main_window_be_visible {
+ main_window.raw.set_visible(true);
+ }
+
+ let mut clipboard = Clipboard::connect(&main_window.raw);
+ let mut events = {
+ vec![(
+ Some(window::Id::MAIN),
+ core::Event::Window(
+ window::Id::MAIN,
+ window::Event::Opened {
+ position: main_window.position(),
+ size: main_window.size(),
+ },
+ ),
+ )]
+ };
+
+ let mut ui_caches = HashMap::new();
+ let mut user_interfaces = ManuallyDrop::new(build_user_interfaces(
+ &application,
+ &mut debug,
+ &mut window_manager,
+ HashMap::from_iter([(
+ window::Id::MAIN,
+ user_interface::Cache::default(),
+ )]),
+ ));
+
+ run_command(
+ &application,
+ &mut compositor,
+ init_command,
+ &mut runtime,
+ &mut clipboard,
+ &mut control_sender,
+ &mut proxy,
+ &mut debug,
+ &mut window_manager,
+ &mut ui_caches,
+ );
+
+ runtime.track(application.subscription().into_recipes());
+
+ let mut messages = Vec::new();
+
+ debug.startup_finished();
+
+ 'main: while let Some(event) = event_receiver.next().await {
+ match event {
+ Event::WindowCreated {
+ id,
+ window,
+ exit_on_close_request,
+ } => {
+ let window = window_manager.insert(
+ id,
+ Arc::new(window),
+ &application,
+ &mut compositor,
+ exit_on_close_request,
+ );
+
+ let logical_size = window.state.logical_size();
+
+ let _ = user_interfaces.insert(
+ id,
+ build_user_interface(
+ &application,
+ user_interface::Cache::default(),
+ &mut window.renderer,
+ logical_size,
+ &mut debug,
+ id,
+ ),
+ );
+ let _ = ui_caches.insert(id, user_interface::Cache::default());
+
+ events.push((
+ Some(id),
+ core::Event::Window(
+ id,
+ window::Event::Opened {
+ position: window.position(),
+ size: window.size(),
+ },
+ ),
+ ));
+ }
+ Event::EventLoopAwakened(event) => {
+ match event {
+ event::Event::NewEvents(
+ event::StartCause::Init
+ | event::StartCause::ResumeTimeReached { .. },
+ ) => {
+ for (_id, window) in window_manager.iter_mut() {
+ // TODO once widgets can request to be redrawn, we can avoid always requesting a
+ // redraw
+ window.raw.request_redraw();
+ }
+ }
+ event::Event::PlatformSpecific(
+ event::PlatformSpecific::MacOS(
+ event::MacOS::ReceivedUrl(url),
+ ),
+ ) => {
+ use crate::core::event;
+
+ events.push((
+ None,
+ event::Event::PlatformSpecific(
+ event::PlatformSpecific::MacOS(
+ event::MacOS::ReceivedUrl(url),
+ ),
+ ),
+ ));
+ }
+ event::Event::UserEvent(message) => {
+ messages.push(message);
+ }
+ event::Event::WindowEvent {
+ window_id: id,
+ event: event::WindowEvent::RedrawRequested,
+ ..
+ } => {
+ let Some((id, window)) =
+ window_manager.get_mut_alias(id)
+ else {
+ continue;
+ };
+
+ // TODO: Avoid redrawing all the time by forcing widgets to
+ // request redraws on state changes
+ //
+ // Then, we can use the `interface_state` here to decide if a redraw
+ // is needed right away, or simply wait until a specific time.
+ let redraw_event = core::Event::Window(
+ id,
+ window::Event::RedrawRequested(Instant::now()),
+ );
+
+ let cursor = window.state.cursor();
+
+ let ui = user_interfaces
+ .get_mut(&id)
+ .expect("Get user interface");
+
+ let (ui_state, _) = ui.update(
+ &[redraw_event.clone()],
+ cursor,
+ &mut window.renderer,
+ &mut clipboard,
+ &mut messages,
+ );
+
+ debug.draw_started();
+ let new_mouse_interaction = ui.draw(
+ &mut window.renderer,
+ window.state.theme(),
+ &renderer::Style {
+ text_color: window.state.text_color(),
+ },
+ cursor,
+ );
+ debug.draw_finished();
+
+ if new_mouse_interaction != window.mouse_interaction {
+ window.raw.set_cursor_icon(
+ conversion::mouse_interaction(
+ new_mouse_interaction,
+ ),
+ );
+
+ window.mouse_interaction = new_mouse_interaction;
+ }
+
+ runtime.broadcast(
+ redraw_event.clone(),
+ core::event::Status::Ignored,
+ );
+
+ let _ = control_sender.start_send(Control::ChangeFlow(
+ match ui_state {
+ user_interface::State::Updated {
+ redraw_request: Some(redraw_request),
+ } => match redraw_request {
+ window::RedrawRequest::NextFrame => {
+ window.raw.request_redraw();
+
+ ControlFlow::Wait
+ }
+ window::RedrawRequest::At(at) => {
+ ControlFlow::WaitUntil(at)
+ }
+ },
+ _ => ControlFlow::Wait,
+ },
+ ));
+
+ let physical_size = window.state.physical_size();
+
+ if physical_size.width == 0 || physical_size.height == 0
+ {
+ continue;
+ }
+
+ if window.viewport_version
+ != window.state.viewport_version()
+ {
+ let logical_size = window.state.logical_size();
+
+ debug.layout_started();
+ let ui = user_interfaces
+ .remove(&id)
+ .expect("Remove user interface");
+
+ let _ = user_interfaces.insert(
+ id,
+ ui.relayout(logical_size, &mut window.renderer),
+ );
+ debug.layout_finished();
+
+ debug.draw_started();
+ let new_mouse_interaction = user_interfaces
+ .get_mut(&id)
+ .expect("Get user interface")
+ .draw(
+ &mut window.renderer,
+ window.state.theme(),
+ &renderer::Style {
+ text_color: window.state.text_color(),
+ },
+ window.state.cursor(),
+ );
+ debug.draw_finished();
+
+ if new_mouse_interaction != window.mouse_interaction
+ {
+ window.raw.set_cursor_icon(
+ conversion::mouse_interaction(
+ new_mouse_interaction,
+ ),
+ );
+
+ window.mouse_interaction =
+ new_mouse_interaction;
+ }
+
+ compositor.configure_surface(
+ &mut window.surface,
+ physical_size.width,
+ physical_size.height,
+ );
+
+ window.viewport_version =
+ window.state.viewport_version();
+ }
+
+ debug.render_started();
+ match compositor.present(
+ &mut window.renderer,
+ &mut window.surface,
+ window.state.viewport(),
+ window.state.background_color(),
+ &debug.overlay(),
+ ) {
+ Ok(()) => {
+ debug.render_finished();
+
+ // TODO: Handle animations!
+ // Maybe we can use `ControlFlow::WaitUntil` for this.
+ }
+ Err(error) => match error {
+ // This is an unrecoverable error.
+ compositor::SurfaceError::OutOfMemory => {
+ panic!("{:?}", error);
+ }
+ _ => {
+ debug.render_finished();
+
+ log::error!(
+ "Error {error:?} when \
+ presenting surface."
+ );
+
+ // Try rendering all windows again next frame.
+ for (_id, window) in
+ window_manager.iter_mut()
+ {
+ window.raw.request_redraw();
+ }
+ }
+ },
+ }
+ }
+ event::Event::WindowEvent {
+ event: window_event,
+ window_id,
+ } => {
+ let Some((id, window)) =
+ window_manager.get_mut_alias(window_id)
+ else {
+ continue;
+ };
+
+ if matches!(
+ window_event,
+ winit::event::WindowEvent::CloseRequested
+ ) && window.exit_on_close_request
+ {
+ let _ = window_manager.remove(id);
+ let _ = user_interfaces.remove(&id);
+ let _ = ui_caches.remove(&id);
+
+ events.push((
+ None,
+ core::Event::Window(id, window::Event::Closed),
+ ));
+
+ if window_manager.is_empty() {
+ break 'main;
+ }
+ } else {
+ window.state.update(
+ &window.raw,
+ &window_event,
+ &mut debug,
+ );
+
+ if let Some(event) = conversion::window_event(
+ id,
+ window_event,
+ window.state.scale_factor(),
+ window.state.modifiers(),
+ ) {
+ events.push((Some(id), event));
+ }
+ }
+ }
+ event::Event::AboutToWait => {
+ if events.is_empty() && messages.is_empty() {
+ continue;
+ }
+
+ debug.event_processing_started();
+ let mut uis_stale = false;
+
+ for (id, window) in window_manager.iter_mut() {
+ let mut window_events = vec![];
+
+ events.retain(|(window_id, event)| {
+ if *window_id == Some(id) || window_id.is_none()
+ {
+ window_events.push(event.clone());
+ false
+ } else {
+ true
+ }
+ });
+
+ if window_events.is_empty() && messages.is_empty() {
+ continue;
+ }
+
+ let (ui_state, statuses) = user_interfaces
+ .get_mut(&id)
+ .expect("Get user interface")
+ .update(
+ &window_events,
+ window.state.cursor(),
+ &mut window.renderer,
+ &mut clipboard,
+ &mut messages,
+ );
+
+ window.raw.request_redraw();
+
+ if !uis_stale {
+ uis_stale = matches!(
+ ui_state,
+ user_interface::State::Outdated
+ );
+ }
+
+ for (event, status) in window_events
+ .into_iter()
+ .zip(statuses.into_iter())
+ {
+ runtime.broadcast(event, status);
+ }
+ }
+
+ debug.event_processing_finished();
+
+ // TODO mw application update returns which window IDs to update
+ if !messages.is_empty() || uis_stale {
+ let mut cached_interfaces: HashMap<
+ window::Id,
+ user_interface::Cache,
+ > = ManuallyDrop::into_inner(user_interfaces)
+ .drain()
+ .map(|(id, ui)| (id, ui.into_cache()))
+ .collect();
+
+ // Update application
+ update(
+ &mut application,
+ &mut compositor,
+ &mut runtime,
+ &mut clipboard,
+ &mut control_sender,
+ &mut proxy,
+ &mut debug,
+ &mut messages,
+ &mut window_manager,
+ &mut cached_interfaces,
+ );
+
+ // we must synchronize all window states with application state after an
+ // application update since we don't know what changed
+ for (id, window) in window_manager.iter_mut() {
+ window.state.synchronize(
+ &application,
+ id,
+ &window.raw,
+ );
+
+ // TODO once widgets can request to be redrawn, we can avoid always requesting a
+ // redraw
+ window.raw.request_redraw();
+ }
+
+ // rebuild UIs with the synchronized states
+ user_interfaces =
+ ManuallyDrop::new(build_user_interfaces(
+ &application,
+ &mut debug,
+ &mut window_manager,
+ cached_interfaces,
+ ));
+ }
+ }
+ _ => {}
+ }
+ }
+ }
+ }
+
+ let _ = ManuallyDrop::into_inner(user_interfaces);
+}
+
+/// Builds a window's [`UserInterface`] for the [`Application`].
+fn build_user_interface<'a, A: Application>(
+ application: &'a A,
+ cache: user_interface::Cache,
+ renderer: &mut A::Renderer,
+ size: Size,
+ debug: &mut Debug,
+ id: window::Id,
+) -> UserInterface<'a, A::Message, A::Renderer>
+where
+ <A::Renderer as core::Renderer>::Theme: StyleSheet,
+{
+ debug.view_started();
+ let view = application.view(id);
+ debug.view_finished();
+
+ debug.layout_started();
+ let user_interface = UserInterface::build(view, size, cache, renderer);
+ debug.layout_finished();
+
+ user_interface
+}
+
+/// Updates a multi-window [`Application`] by feeding it messages, spawning any
+/// resulting [`Command`], and tracking its [`Subscription`].
+fn update<A: Application, C, E: Executor>(
+ application: &mut A,
+ compositor: &mut C,
+ runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>,
+ clipboard: &mut Clipboard,
+ control_sender: &mut mpsc::UnboundedSender<Control>,
+ proxy: &mut winit::event_loop::EventLoopProxy<A::Message>,
+ debug: &mut Debug,
+ messages: &mut Vec<A::Message>,
+ window_manager: &mut WindowManager<A, C>,
+ ui_caches: &mut HashMap<window::Id, user_interface::Cache>,
+) where
+ C: Compositor<Renderer = A::Renderer> + 'static,
+ <A::Renderer as core::Renderer>::Theme: StyleSheet,
+{
+ for message in messages.drain(..) {
+ debug.log_message(&message);
+ debug.update_started();
+
+ let command = runtime.enter(|| application.update(message));
+ debug.update_finished();
+
+ run_command(
+ application,
+ compositor,
+ command,
+ runtime,
+ clipboard,
+ control_sender,
+ proxy,
+ debug,
+ window_manager,
+ ui_caches,
+ );
+ }
+
+ let subscription = application.subscription();
+ runtime.track(subscription.into_recipes());
+}
+
+/// Runs the actions of a [`Command`].
+fn run_command<A, C, E>(
+ application: &A,
+ compositor: &mut C,
+ command: Command<A::Message>,
+ runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>,
+ clipboard: &mut Clipboard,
+ control_sender: &mut mpsc::UnboundedSender<Control>,
+ proxy: &mut winit::event_loop::EventLoopProxy<A::Message>,
+ debug: &mut Debug,
+ window_manager: &mut WindowManager<A, C>,
+ ui_caches: &mut HashMap<window::Id, user_interface::Cache>,
+) where
+ A: Application,
+ E: Executor,
+ C: Compositor<Renderer = A::Renderer> + 'static,
+ <A::Renderer as core::Renderer>::Theme: StyleSheet,
+{
+ use crate::runtime::clipboard;
+ use crate::runtime::system;
+ use crate::runtime::window;
+
+ for action in command.actions() {
+ match action {
+ command::Action::Future(future) => {
+ runtime.spawn(Box::pin(future));
+ }
+ command::Action::Stream(stream) => {
+ runtime.run(Box::pin(stream));
+ }
+ command::Action::Clipboard(action) => match action {
+ clipboard::Action::Read(tag) => {
+ let message = tag(clipboard.read());
+
+ proxy
+ .send_event(message)
+ .expect("Send message to event loop");
+ }
+ clipboard::Action::Write(contents) => {
+ clipboard.write(contents);
+ }
+ },
+ command::Action::Window(action) => match action {
+ window::Action::Spawn(id, settings) => {
+ let monitor = window_manager.last_monitor();
+
+ control_sender
+ .start_send(Control::CreateWindow {
+ id,
+ settings,
+ title: application.title(id),
+ monitor,
+ })
+ .expect("Send control action");
+ }
+ window::Action::Close(id) => {
+ let _ = window_manager.remove(id);
+ let _ = ui_caches.remove(&id);
+
+ if window_manager.is_empty() {
+ control_sender
+ .start_send(Control::Exit)
+ .expect("Send control action");
+ }
+ }
+ window::Action::Drag(id) => {
+ if let Some(window) = window_manager.get_mut(id) {
+ let _ = window.raw.drag_window();
+ }
+ }
+ window::Action::Resize(id, size) => {
+ if let Some(window) = window_manager.get_mut(id) {
+ let _ = window.raw.request_inner_size(
+ winit::dpi::LogicalSize {
+ width: size.width,
+ height: size.height,
+ },
+ );
+ }
+ }
+ window::Action::FetchSize(id, callback) => {
+ if let Some(window) = window_manager.get_mut(id) {
+ let size = window
+ .raw
+ .inner_size()
+ .to_logical(window.raw.scale_factor());
+
+ proxy
+ .send_event(callback(Size::new(
+ size.width,
+ size.height,
+ )))
+ .expect("Send message to event loop");
+ }
+ }
+ window::Action::FetchMaximized(id, callback) => {
+ if let Some(window) = window_manager.get_mut(id) {
+ proxy
+ .send_event(callback(window.raw.is_maximized()))
+ .expect("Send message to event loop");
+ }
+ }
+ window::Action::Maximize(id, maximized) => {
+ if let Some(window) = window_manager.get_mut(id) {
+ window.raw.set_maximized(maximized);
+ }
+ }
+ window::Action::FetchMinimized(id, callback) => {
+ if let Some(window) = window_manager.get_mut(id) {
+ proxy
+ .send_event(callback(window.raw.is_minimized()))
+ .expect("Send message to event loop");
+ }
+ }
+ window::Action::Minimize(id, minimized) => {
+ if let Some(window) = window_manager.get_mut(id) {
+ window.raw.set_minimized(minimized);
+ }
+ }
+ window::Action::Move(id, position) => {
+ if let Some(window) = window_manager.get_mut(id) {
+ window.raw.set_outer_position(
+ winit::dpi::LogicalPosition {
+ x: position.x,
+ y: position.y,
+ },
+ );
+ }
+ }
+ window::Action::ChangeMode(id, mode) => {
+ if let Some(window) = window_manager.get_mut(id) {
+ window.raw.set_visible(conversion::visible(mode));
+ window.raw.set_fullscreen(conversion::fullscreen(
+ window.raw.current_monitor(),
+ mode,
+ ));
+ }
+ }
+ window::Action::ChangeIcon(id, icon) => {
+ if let Some(window) = window_manager.get_mut(id) {
+ window.raw.set_window_icon(conversion::icon(icon));
+ }
+ }
+ window::Action::FetchMode(id, tag) => {
+ if let Some(window) = window_manager.get_mut(id) {
+ let mode = if window.raw.is_visible().unwrap_or(true) {
+ conversion::mode(window.raw.fullscreen())
+ } else {
+ core::window::Mode::Hidden
+ };
+
+ proxy
+ .send_event(tag(mode))
+ .expect("Event loop doesn't exist.");
+ }
+ }
+ window::Action::ToggleMaximize(id) => {
+ if let Some(window) = window_manager.get_mut(id) {
+ window.raw.set_maximized(!window.raw.is_maximized());
+ }
+ }
+ window::Action::ToggleDecorations(id) => {
+ if let Some(window) = window_manager.get_mut(id) {
+ window.raw.set_decorations(!window.raw.is_decorated());
+ }
+ }
+ window::Action::RequestUserAttention(id, attention_type) => {
+ if let Some(window) = window_manager.get_mut(id) {
+ window.raw.request_user_attention(
+ attention_type.map(conversion::user_attention),
+ );
+ }
+ }
+ window::Action::GainFocus(id) => {
+ if let Some(window) = window_manager.get_mut(id) {
+ window.raw.focus_window();
+ }
+ }
+ window::Action::ChangeLevel(id, level) => {
+ if let Some(window) = window_manager.get_mut(id) {
+ window
+ .raw
+ .set_window_level(conversion::window_level(level));
+ }
+ }
+ window::Action::FetchId(id, tag) => {
+ if let Some(window) = window_manager.get_mut(id) {
+ proxy
+ .send_event(tag(window.raw.id().into()))
+ .expect("Event loop doesn't exist.");
+ }
+ }
+ window::Action::Screenshot(id, tag) => {
+ if let Some(window) = window_manager.get_mut(id) {
+ let bytes = compositor.screenshot(
+ &mut window.renderer,
+ &mut window.surface,
+ window.state.viewport(),
+ window.state.background_color(),
+ &debug.overlay(),
+ );
+
+ proxy
+ .send_event(tag(window::Screenshot::new(
+ bytes,
+ window.state.physical_size(),
+ )))
+ .expect("Event loop doesn't exist.");
+ }
+ }
+ },
+ command::Action::System(action) => match action {
+ system::Action::QueryInformation(_tag) => {
+ #[cfg(feature = "system")]
+ {
+ let graphics_info = compositor.fetch_information();
+ let proxy = proxy.clone();
+
+ let _ = std::thread::spawn(move || {
+ let information =
+ crate::system::information(graphics_info);
+
+ let message = _tag(information);
+
+ proxy
+ .send_event(message)
+ .expect("Event loop doesn't exist.");
+ });
+ }
+ }
+ },
+ command::Action::Widget(action) => {
+ let mut current_operation = Some(action);
+
+ let mut uis = build_user_interfaces(
+ application,
+ debug,
+ window_manager,
+ std::mem::take(ui_caches),
+ );
+
+ 'operate: while let Some(mut operation) =
+ current_operation.take()
+ {
+ for (id, ui) in uis.iter_mut() {
+ if let Some(window) = window_manager.get_mut(*id) {
+ ui.operate(&window.renderer, operation.as_mut());
+
+ match operation.finish() {
+ operation::Outcome::None => {}
+ operation::Outcome::Some(message) => {
+ proxy
+ .send_event(message)
+ .expect("Event loop doesn't exist.");
+
+ // operation completed, don't need to try to operate on rest of UIs
+ break 'operate;
+ }
+ operation::Outcome::Chain(next) => {
+ current_operation = Some(next);
+ }
+ }
+ }
+ }
+ }
+
+ *ui_caches =
+ uis.drain().map(|(id, ui)| (id, ui.into_cache())).collect();
+ }
+ command::Action::LoadFont { bytes, tagger } => {
+ use crate::core::text::Renderer;
+
+ // TODO change this once we change each renderer to having a single backend reference.. :pain:
+ // TODO: Error handling (?)
+ for (_, window) in window_manager.iter_mut() {
+ window.renderer.load_font(bytes.clone());
+ }
+
+ proxy
+ .send_event(tagger(Ok(())))
+ .expect("Send message to event loop");
+ }
+ }
+ }
+}
+
+/// Build the user interface for every window.
+pub fn build_user_interfaces<'a, A: Application, C: Compositor>(
+ application: &'a A,
+ debug: &mut Debug,
+ window_manager: &mut WindowManager<A, C>,
+ mut cached_user_interfaces: HashMap<window::Id, user_interface::Cache>,
+) -> HashMap<window::Id, UserInterface<'a, A::Message, A::Renderer>>
+where
+ <A::Renderer as core::Renderer>::Theme: StyleSheet,
+ C: Compositor<Renderer = A::Renderer>,
+{
+ cached_user_interfaces
+ .drain()
+ .filter_map(|(id, cache)| {
+ let window = window_manager.get_mut(id)?;
+
+ Some((
+ id,
+ build_user_interface(
+ application,
+ cache,
+ &mut window.renderer,
+ window.state.logical_size(),
+ debug,
+ id,
+ ),
+ ))
+ })
+ .collect()
+}
+
+/// Returns true if the provided event should cause an [`Application`] to
+/// exit.
+pub fn user_force_quit(
+ event: &winit::event::WindowEvent,
+ _modifiers: winit::keyboard::ModifiersState,
+) -> bool {
+ match event {
+ #[cfg(target_os = "macos")]
+ winit::event::WindowEvent::KeyboardInput {
+ event:
+ winit::event::KeyEvent {
+ logical_key: winit::keyboard::Key::Character(c),
+ state: winit::event::ElementState::Pressed,
+ ..
+ },
+ ..
+ } if c == "q" && _modifiers.super_key() => true,
+ _ => false,
+ }
+}
diff --git a/winit/src/multi_window/state.rs b/winit/src/multi_window/state.rs
new file mode 100644
index 00000000..235771f4
--- /dev/null
+++ b/winit/src/multi_window/state.rs
@@ -0,0 +1,242 @@
+use crate::conversion;
+use crate::core;
+use crate::core::{mouse, window};
+use crate::core::{Color, Size};
+use crate::graphics::Viewport;
+use crate::multi_window::Application;
+use crate::style::application;
+use std::fmt::{Debug, Formatter};
+
+use iced_style::application::StyleSheet;
+use winit::event::{Touch, WindowEvent};
+use winit::window::Window;
+
+/// The state of a multi-windowed [`Application`].
+pub struct State<A: Application>
+where
+ <A::Renderer as core::Renderer>::Theme: application::StyleSheet,
+{
+ title: String,
+ scale_factor: f64,
+ viewport: Viewport,
+ viewport_version: u64,
+ cursor_position: Option<winit::dpi::PhysicalPosition<f64>>,
+ modifiers: winit::keyboard::ModifiersState,
+ theme: <A::Renderer as core::Renderer>::Theme,
+ appearance: application::Appearance,
+}
+
+impl<A: Application> Debug for State<A>
+where
+ <A::Renderer as core::Renderer>::Theme: application::StyleSheet,
+{
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("multi_window::State")
+ .field("title", &self.title)
+ .field("scale_factor", &self.scale_factor)
+ .field("viewport", &self.viewport)
+ .field("viewport_version", &self.viewport_version)
+ .field("cursor_position", &self.cursor_position)
+ .field("appearance", &self.appearance)
+ .finish()
+ }
+}
+
+impl<A: Application> State<A>
+where
+ <A::Renderer as core::Renderer>::Theme: application::StyleSheet,
+{
+ /// Creates a new [`State`] for the provided [`Application`]'s `window`.
+ pub fn new(
+ application: &A,
+ window_id: window::Id,
+ window: &Window,
+ ) -> Self {
+ let title = application.title(window_id);
+ let scale_factor = application.scale_factor(window_id);
+ let theme = application.theme(window_id);
+ let appearance = theme.appearance(&application.style());
+
+ let viewport = {
+ let physical_size = window.inner_size();
+
+ Viewport::with_physical_size(
+ Size::new(physical_size.width, physical_size.height),
+ window.scale_factor() * scale_factor,
+ )
+ };
+
+ Self {
+ title,
+ scale_factor,
+ viewport,
+ viewport_version: 0,
+ cursor_position: None,
+ modifiers: winit::keyboard::ModifiersState::default(),
+ theme,
+ appearance,
+ }
+ }
+
+ /// Returns the current [`Viewport`] of the [`State`].
+ pub fn viewport(&self) -> &Viewport {
+ &self.viewport
+ }
+
+ /// Returns the version of the [`Viewport`] of the [`State`].
+ ///
+ /// The version is incremented every time the [`Viewport`] changes.
+ pub fn viewport_version(&self) -> u64 {
+ self.viewport_version
+ }
+
+ /// Returns the physical [`Size`] of the [`Viewport`] of the [`State`].
+ pub fn physical_size(&self) -> Size<u32> {
+ self.viewport.physical_size()
+ }
+
+ /// Returns the logical [`Size`] of the [`Viewport`] of the [`State`].
+ pub fn logical_size(&self) -> Size<f32> {
+ self.viewport.logical_size()
+ }
+
+ /// Returns the current scale factor of the [`Viewport`] of the [`State`].
+ pub fn scale_factor(&self) -> f64 {
+ self.viewport.scale_factor()
+ }
+
+ /// Returns the current cursor position of the [`State`].
+ pub fn cursor(&self) -> mouse::Cursor {
+ self.cursor_position
+ .map(|cursor_position| {
+ conversion::cursor_position(
+ cursor_position,
+ self.viewport.scale_factor(),
+ )
+ })
+ .map(mouse::Cursor::Available)
+ .unwrap_or(mouse::Cursor::Unavailable)
+ }
+
+ /// Returns the current keyboard modifiers of the [`State`].
+ pub fn modifiers(&self) -> winit::keyboard::ModifiersState {
+ self.modifiers
+ }
+
+ /// Returns the current theme of the [`State`].
+ pub fn theme(&self) -> &<A::Renderer as core::Renderer>::Theme {
+ &self.theme
+ }
+
+ /// Returns the current background [`Color`] of the [`State`].
+ pub fn background_color(&self) -> Color {
+ self.appearance.background_color
+ }
+
+ /// Returns the current text [`Color`] of the [`State`].
+ pub fn text_color(&self) -> Color {
+ self.appearance.text_color
+ }
+
+ /// Processes the provided window event and updates the [`State`] accordingly.
+ pub fn update(
+ &mut self,
+ window: &Window,
+ event: &WindowEvent,
+ _debug: &mut crate::runtime::Debug,
+ ) {
+ match event {
+ WindowEvent::Resized(new_size) => {
+ let size = Size::new(new_size.width, new_size.height);
+
+ self.viewport = Viewport::with_physical_size(
+ size,
+ window.scale_factor() * self.scale_factor,
+ );
+
+ self.viewport_version = self.viewport_version.wrapping_add(1);
+ }
+ WindowEvent::ScaleFactorChanged {
+ scale_factor: new_scale_factor,
+ ..
+ } => {
+ let size = self.viewport.physical_size();
+
+ self.viewport = Viewport::with_physical_size(
+ size,
+ new_scale_factor * self.scale_factor,
+ );
+
+ self.viewport_version = self.viewport_version.wrapping_add(1);
+ }
+ WindowEvent::CursorMoved { position, .. }
+ | WindowEvent::Touch(Touch {
+ location: position, ..
+ }) => {
+ self.cursor_position = Some(*position);
+ }
+ WindowEvent::CursorLeft { .. } => {
+ self.cursor_position = None;
+ }
+ WindowEvent::ModifiersChanged(new_modifiers) => {
+ self.modifiers = new_modifiers.state();
+ }
+ #[cfg(feature = "debug")]
+ WindowEvent::KeyboardInput {
+ event:
+ winit::event::KeyEvent {
+ logical_key:
+ winit::keyboard::Key::Named(
+ winit::keyboard::NamedKey::F12,
+ ),
+ state: winit::event::ElementState::Pressed,
+ ..
+ },
+ ..
+ } => _debug.toggle(),
+ _ => {}
+ }
+ }
+
+ /// Synchronizes the [`State`] with its [`Application`] and its respective
+ /// window.
+ ///
+ /// Normally, an [`Application`] should be synchronized with its [`State`]
+ /// and window after calling [`State::update`].
+ pub fn synchronize(
+ &mut self,
+ application: &A,
+ window_id: window::Id,
+ window: &Window,
+ ) {
+ // Update window title
+ let new_title = application.title(window_id);
+
+ if self.title != new_title {
+ window.set_title(&new_title);
+ self.title = new_title;
+ }
+
+ // Update scale factor and size
+ let new_scale_factor = application.scale_factor(window_id);
+ let new_size = window.inner_size();
+ let current_size = self.viewport.physical_size();
+
+ if self.scale_factor != new_scale_factor
+ || (current_size.width, current_size.height)
+ != (new_size.width, new_size.height)
+ {
+ self.viewport = Viewport::with_physical_size(
+ Size::new(new_size.width, new_size.height),
+ window.scale_factor() * new_scale_factor,
+ );
+ self.viewport_version = self.viewport_version.wrapping_add(1);
+
+ self.scale_factor = new_scale_factor;
+ }
+
+ // Update theme and appearance
+ self.theme = application.theme(window_id);
+ self.appearance = self.theme.appearance(&application.style());
+ }
+}
diff --git a/winit/src/multi_window/window_manager.rs b/winit/src/multi_window/window_manager.rs
new file mode 100644
index 00000000..9e15f9ea
--- /dev/null
+++ b/winit/src/multi_window/window_manager.rs
@@ -0,0 +1,157 @@
+use crate::core::mouse;
+use crate::core::window::Id;
+use crate::core::{Point, Size};
+use crate::graphics::Compositor;
+use crate::multi_window::{Application, State};
+use crate::style::application::StyleSheet;
+
+use std::collections::BTreeMap;
+use std::sync::Arc;
+use winit::monitor::MonitorHandle;
+
+#[allow(missing_debug_implementations)]
+pub struct WindowManager<A: Application, C: Compositor>
+where
+ <A::Renderer as crate::core::Renderer>::Theme: StyleSheet,
+ C: Compositor<Renderer = A::Renderer>,
+{
+ aliases: BTreeMap<winit::window::WindowId, Id>,
+ entries: BTreeMap<Id, Window<A, C>>,
+}
+
+impl<A, C> WindowManager<A, C>
+where
+ A: Application,
+ C: Compositor<Renderer = A::Renderer>,
+ <A::Renderer as crate::core::Renderer>::Theme: StyleSheet,
+{
+ pub fn new() -> Self {
+ Self {
+ aliases: BTreeMap::new(),
+ entries: BTreeMap::new(),
+ }
+ }
+
+ pub fn insert(
+ &mut self,
+ id: Id,
+ window: Arc<winit::window::Window>,
+ application: &A,
+ compositor: &mut C,
+ exit_on_close_request: bool,
+ ) -> &mut Window<A, C> {
+ let state = State::new(application, id, &window);
+ let viewport_version = state.viewport_version();
+ let physical_size = state.physical_size();
+ let surface = compositor.create_surface(
+ window.clone(),
+ physical_size.width,
+ physical_size.height,
+ );
+ let renderer = compositor.create_renderer();
+
+ let _ = self.aliases.insert(window.id(), id);
+
+ let _ = self.entries.insert(
+ id,
+ Window {
+ raw: window,
+ state,
+ viewport_version,
+ exit_on_close_request,
+ surface,
+ renderer,
+ mouse_interaction: mouse::Interaction::Idle,
+ },
+ );
+
+ self.entries
+ .get_mut(&id)
+ .expect("Get window that was just inserted")
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.entries.is_empty()
+ }
+
+ pub fn iter_mut(
+ &mut self,
+ ) -> impl Iterator<Item = (Id, &mut Window<A, C>)> {
+ self.entries.iter_mut().map(|(k, v)| (*k, v))
+ }
+
+ pub fn get_mut(&mut self, id: Id) -> Option<&mut Window<A, C>> {
+ self.entries.get_mut(&id)
+ }
+
+ pub fn get_mut_alias(
+ &mut self,
+ id: winit::window::WindowId,
+ ) -> Option<(Id, &mut Window<A, C>)> {
+ let id = self.aliases.get(&id).copied()?;
+
+ Some((id, self.get_mut(id)?))
+ }
+
+ pub fn last_monitor(&self) -> Option<MonitorHandle> {
+ self.entries.values().last()?.raw.current_monitor()
+ }
+
+ pub fn remove(&mut self, id: Id) -> Option<Window<A, C>> {
+ let window = self.entries.remove(&id)?;
+ let _ = self.aliases.remove(&window.raw.id());
+
+ Some(window)
+ }
+}
+
+impl<A, C> Default for WindowManager<A, C>
+where
+ A: Application,
+ C: Compositor<Renderer = A::Renderer>,
+ <A::Renderer as crate::core::Renderer>::Theme: StyleSheet,
+{
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+#[allow(missing_debug_implementations)]
+pub struct Window<A, C>
+where
+ A: Application,
+ C: Compositor<Renderer = A::Renderer>,
+ <A::Renderer as crate::core::Renderer>::Theme: StyleSheet,
+{
+ pub raw: Arc<winit::window::Window>,
+ pub state: State<A>,
+ pub viewport_version: u64,
+ pub exit_on_close_request: bool,
+ pub mouse_interaction: mouse::Interaction,
+ pub surface: C::Surface,
+ pub renderer: A::Renderer,
+}
+
+impl<A, C> Window<A, C>
+where
+ A: Application,
+ C: Compositor<Renderer = A::Renderer>,
+ <A::Renderer as crate::core::Renderer>::Theme: StyleSheet,
+{
+ pub fn position(&self) -> Option<Point> {
+ self.raw
+ .inner_position()
+ .ok()
+ .map(|position| position.to_logical(self.raw.scale_factor()))
+ .map(|position| Point {
+ x: position.x,
+ y: position.y,
+ })
+ }
+
+ pub fn size(&self) -> Size {
+ let size = self.raw.inner_size().to_logical(self.raw.scale_factor());
+
+ Size::new(size.width, size.height)
+ }
+}
diff --git a/winit/src/settings.rs b/winit/src/settings.rs
index 867dad0f..2e541128 100644
--- a/winit/src/settings.rs
+++ b/winit/src/settings.rs
@@ -1,39 +1,7 @@
//! Configure your application.
-#[cfg(target_os = "windows")]
-#[path = "settings/windows.rs"]
-mod platform;
+use crate::core::window;
-#[cfg(target_os = "macos")]
-#[path = "settings/macos.rs"]
-mod platform;
-
-#[cfg(target_os = "linux")]
-#[path = "settings/linux.rs"]
-mod platform;
-
-#[cfg(target_arch = "wasm32")]
-#[path = "settings/wasm.rs"]
-mod platform;
-
-#[cfg(not(any(
- target_os = "windows",
- target_os = "macos",
- target_os = "linux",
- target_arch = "wasm32"
-)))]
-#[path = "settings/other.rs"]
-mod platform;
-
-pub use platform::PlatformSpecific;
-
-use crate::conversion;
-use crate::core::window::{Icon, Level};
-use crate::Position;
-
-use winit::monitor::MonitorHandle;
-use winit::window::WindowBuilder;
-
-use std::fmt;
+use std::borrow::Cow;
/// The settings of an application.
#[derive(Debug, Clone, Default)]
@@ -44,198 +12,14 @@ pub struct Settings<Flags> {
/// communicate with it through the windowing system.
pub id: Option<String>,
- /// The [`Window`] settings.
- pub window: Window,
+ /// The [`window::Settings`].
+ pub window: window::Settings,
/// The data needed to initialize an [`Application`].
///
/// [`Application`]: crate::Application
pub flags: Flags,
- /// Whether the [`Application`] should exit when the user requests the
- /// window to close (e.g. the user presses the close button).
- ///
- /// [`Application`]: crate::Application
- pub exit_on_close_request: bool,
-}
-
-/// The window settings of an application.
-#[derive(Clone)]
-pub struct Window {
- /// The size of the window.
- pub size: (u32, u32),
-
- /// The position of the window.
- pub position: Position,
-
- /// The minimum size of the window.
- pub min_size: Option<(u32, u32)>,
-
- /// The maximum size of the window.
- pub max_size: Option<(u32, u32)>,
-
- /// Whether the window should be visible or not.
- pub visible: bool,
-
- /// Whether the window should be resizable or not.
- pub resizable: bool,
-
- /// Whether the window should have a border, a title bar, etc.
- pub decorations: bool,
-
- /// Whether the window should be transparent.
- pub transparent: bool,
-
- /// The window [`Level`].
- pub level: Level,
-
- /// The window icon, which is also usually used in the taskbar
- pub icon: Option<Icon>,
-
- /// Platform specific settings.
- pub platform_specific: platform::PlatformSpecific,
-}
-
-impl fmt::Debug for Window {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_struct("Window")
- .field("size", &self.size)
- .field("position", &self.position)
- .field("min_size", &self.min_size)
- .field("max_size", &self.max_size)
- .field("visible", &self.visible)
- .field("resizable", &self.resizable)
- .field("decorations", &self.decorations)
- .field("transparent", &self.transparent)
- .field("level", &self.level)
- .field("icon", &self.icon.is_some())
- .field("platform_specific", &self.platform_specific)
- .finish()
- }
-}
-
-impl Window {
- /// Converts the window settings into a `WindowBuilder` from `winit`.
- pub fn into_builder(
- self,
- title: &str,
- primary_monitor: Option<MonitorHandle>,
- _id: Option<String>,
- ) -> WindowBuilder {
- let mut window_builder = WindowBuilder::new();
-
- let (width, height) = self.size;
-
- window_builder = window_builder
- .with_title(title)
- .with_inner_size(winit::dpi::LogicalSize { width, height })
- .with_resizable(self.resizable)
- .with_decorations(self.decorations)
- .with_transparent(self.transparent)
- .with_window_icon(self.icon.and_then(conversion::icon))
- .with_window_level(conversion::window_level(self.level))
- .with_visible(self.visible);
-
- if let Some(position) = conversion::position(
- primary_monitor.as_ref(),
- self.size,
- self.position,
- ) {
- window_builder = window_builder.with_position(position);
- }
-
- if let Some((width, height)) = self.min_size {
- window_builder = window_builder
- .with_min_inner_size(winit::dpi::LogicalSize { width, height });
- }
-
- if let Some((width, height)) = self.max_size {
- window_builder = window_builder
- .with_max_inner_size(winit::dpi::LogicalSize { width, height });
- }
-
- #[cfg(any(
- target_os = "dragonfly",
- target_os = "freebsd",
- target_os = "netbsd",
- target_os = "openbsd"
- ))]
- {
- // `with_name` is available on both `WindowBuilderExtWayland` and `WindowBuilderExtX11` and they do
- // exactly the same thing. We arbitrarily choose `WindowBuilderExtWayland` here.
- use ::winit::platform::wayland::WindowBuilderExtWayland;
-
- if let Some(id) = _id {
- window_builder = window_builder.with_name(id.clone(), id);
- }
- }
-
- #[cfg(target_os = "windows")]
- {
- use winit::platform::windows::WindowBuilderExtWindows;
- #[allow(unsafe_code)]
- unsafe {
- window_builder = window_builder
- .with_parent_window(self.platform_specific.parent);
- }
- window_builder = window_builder
- .with_drag_and_drop(self.platform_specific.drag_and_drop);
- }
-
- #[cfg(target_os = "macos")]
- {
- use winit::platform::macos::WindowBuilderExtMacOS;
-
- window_builder = window_builder
- .with_title_hidden(self.platform_specific.title_hidden)
- .with_titlebar_transparent(
- self.platform_specific.titlebar_transparent,
- )
- .with_fullsize_content_view(
- self.platform_specific.fullsize_content_view,
- );
- }
-
- #[cfg(target_os = "linux")]
- {
- #[cfg(feature = "x11")]
- {
- use winit::platform::x11::WindowBuilderExtX11;
-
- window_builder = window_builder.with_name(
- &self.platform_specific.application_id,
- &self.platform_specific.application_id,
- );
- }
- #[cfg(feature = "wayland")]
- {
- use winit::platform::wayland::WindowBuilderExtWayland;
-
- window_builder = window_builder.with_name(
- &self.platform_specific.application_id,
- &self.platform_specific.application_id,
- );
- }
- }
-
- window_builder
- }
-}
-
-impl Default for Window {
- fn default() -> Window {
- Window {
- size: (1024, 768),
- position: Position::default(),
- min_size: None,
- max_size: None,
- visible: true,
- resizable: true,
- decorations: true,
- transparent: false,
- level: Level::default(),
- icon: None,
- platform_specific: PlatformSpecific::default(),
- }
- }
+ /// The fonts to load on boot.
+ pub fonts: Vec<Cow<'static, [u8]>>,
}