summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Billy Messenger <BillyDM@tutamail.com>2021-07-22 12:37:39 -0500
committerLibravatar Billy Messenger <BillyDM@tutamail.com>2021-07-22 12:37:39 -0500
commite822f654e44d2d7375b7fda966bb772055f377d4 (patch)
tree8707561f1bb09c9e58cc9d9884bfb16d956f9f65
parent1c06920158e1a47977b2762bf8b34e56fd1a935a (diff)
parentdc0b96ce407283f2ffd9add5ad339f89097555d3 (diff)
downloadiced-e822f654e44d2d7375b7fda966bb772055f377d4.tar.gz
iced-e822f654e44d2d7375b7fda966bb772055f377d4.tar.bz2
iced-e822f654e44d2d7375b7fda966bb772055f377d4.zip
Merge branch 'master' of https://github.com/hecrj/iced into wgpu_outdatedframe
-rw-r--r--.github/FUNDING.yml1
-rw-r--r--.github/workflows/build.yml7
-rw-r--r--.github/workflows/test.yml10
-rw-r--r--CHANGELOG.md115
-rw-r--r--Cargo.toml23
-rw-r--r--README.md4
-rw-r--r--core/Cargo.toml3
-rw-r--r--core/README.md2
-rw-r--r--core/src/keyboard.rs2
-rw-r--r--core/src/keyboard/hotkey.rs18
-rw-r--r--core/src/keyboard/modifiers.rs76
-rw-r--r--core/src/lib.rs4
-rw-r--r--core/src/menu.rs145
-rw-r--r--core/src/mouse/event.rs9
-rw-r--r--core/src/padding.rs107
-rw-r--r--core/src/rectangle.rs4
-rw-r--r--core/src/size.rs8
-rw-r--r--examples/README.md4
-rw-r--r--examples/clock/src/main.rs10
-rw-r--r--examples/download_progress/Cargo.toml6
-rw-r--r--examples/download_progress/README.md2
-rw-r--r--examples/download_progress/src/download.rs46
-rw-r--r--examples/download_progress/src/main.rs205
-rw-r--r--examples/events/src/main.rs53
-rw-r--r--examples/game_of_life/Cargo.toml2
-rw-r--r--examples/game_of_life/src/main.rs23
-rw-r--r--examples/game_of_life/src/preset.rs13
-rw-r--r--examples/game_of_life/src/style.rs1
-rw-r--r--examples/integration/src/controls.rs11
-rw-r--r--examples/integration/src/main.rs13
-rw-r--r--examples/integration/src/scene.rs47
-rw-r--r--examples/menu/Cargo.toml10
-rw-r--r--examples/menu/src/main.rs117
-rw-r--r--examples/pane_grid/src/main.rs151
-rw-r--r--examples/pick_list/src/main.rs9
-rw-r--r--examples/pokedex/src/main.rs24
-rw-r--r--examples/scrollable/Cargo.toml2
-rw-r--r--examples/scrollable/src/main.rs107
-rw-r--r--examples/solar_system/Cargo.toml2
-rw-r--r--examples/solar_system/src/main.rs18
-rw-r--r--examples/stopwatch/Cargo.toml2
-rw-r--r--examples/stopwatch/src/main.rs10
-rw-r--r--examples/styling/src/main.rs75
-rw-r--r--examples/todos/src/main.rs18
-rw-r--r--examples/tooltip/Cargo.toml9
-rw-r--r--examples/tooltip/README.md14
-rw-r--r--examples/tooltip/src/main.rs138
-rw-r--r--examples/tour/src/main.rs32
-rw-r--r--examples/url_handler/Cargo.toml10
-rw-r--r--examples/url_handler/src/main.rs73
-rw-r--r--futures/Cargo.toml11
-rw-r--r--futures/src/executor.rs6
-rw-r--r--futures/src/executor/smol.rs18
-rw-r--r--futures/src/lib.rs16
-rw-r--r--futures/src/subscription.rs6
-rw-r--r--futures/src/subscription/tracker.rs2
-rw-r--r--futures/src/time.rs47
-rw-r--r--glow/Cargo.toml6
-rw-r--r--glow/src/backend.rs7
-rw-r--r--glow/src/program.rs4
-rw-r--r--glow/src/settings.rs18
-rw-r--r--glow/src/text.rs8
-rw-r--r--glow/src/widget.rs6
-rw-r--r--glow/src/widget/pane_grid.rs8
-rw-r--r--glow/src/widget/toggler.rs9
-rw-r--r--glow/src/widget/tooltip.rs6
-rw-r--r--glutin/Cargo.toml14
-rw-r--r--glutin/README.md2
-rw-r--r--glutin/src/application.rs68
-rw-r--r--glutin/src/lib.rs2
-rw-r--r--graphics/Cargo.toml9
-rw-r--r--graphics/src/layer.rs40
-rw-r--r--graphics/src/overlay/menu.rs12
-rw-r--r--graphics/src/viewport.rs2
-rw-r--r--graphics/src/widget.rs6
-rw-r--r--graphics/src/widget/button.rs4
-rw-r--r--graphics/src/widget/canvas.rs4
-rw-r--r--graphics/src/widget/canvas/frame.rs2
-rw-r--r--graphics/src/widget/canvas/program.rs2
-rw-r--r--graphics/src/widget/image.rs5
-rw-r--r--graphics/src/widget/image/viewer.rs55
-rw-r--r--graphics/src/widget/pane_grid.rs134
-rw-r--r--graphics/src/widget/pick_list.rs21
-rw-r--r--graphics/src/widget/scrollable.rs10
-rw-r--r--graphics/src/widget/toggler.rs99
-rw-r--r--graphics/src/widget/tooltip.rs168
-rw-r--r--graphics/src/window/compositor.rs5
-rw-r--r--native/Cargo.toml6
-rw-r--r--native/README.md2
-rw-r--r--native/src/clipboard.rs21
-rw-r--r--native/src/element.rs18
-rw-r--r--native/src/event.rs29
-rw-r--r--native/src/layout.rs7
-rw-r--r--native/src/layout/flex.rs12
-rw-r--r--native/src/layout/limits.rs9
-rw-r--r--native/src/lib.rs7
-rw-r--r--native/src/overlay.rs4
-rw-r--r--native/src/overlay/element.rs12
-rw-r--r--native/src/overlay/menu.rs58
-rw-r--r--native/src/program.rs13
-rw-r--r--native/src/program/state.rs9
-rw-r--r--native/src/renderer/null.rs39
-rw-r--r--native/src/touch.rs23
-rw-r--r--native/src/user_interface.rs18
-rw-r--r--native/src/widget.rs20
-rw-r--r--native/src/widget/button.rs80
-rw-r--r--native/src/widget/checkbox.rs22
-rw-r--r--native/src/widget/column.rs21
-rw-r--r--native/src/widget/container.rs30
-rw-r--r--native/src/widget/image.rs3
-rw-r--r--native/src/widget/image/viewer.rs413
-rw-r--r--native/src/widget/pane_grid.rs215
-rw-r--r--native/src/widget/pane_grid/content.rs39
-rw-r--r--native/src/widget/pane_grid/node.rs14
-rw-r--r--native/src/widget/pane_grid/pane.rs2
-rw-r--r--native/src/widget/pane_grid/split.rs2
-rw-r--r--native/src/widget/pane_grid/state.rs6
-rw-r--r--native/src/widget/pane_grid/title_bar.rs210
-rw-r--r--native/src/widget/pick_list.rs116
-rw-r--r--native/src/widget/radio.rs34
-rw-r--r--native/src/widget/row.rs21
-rw-r--r--native/src/widget/scrollable.rs180
-rw-r--r--native/src/widget/slider.rs50
-rw-r--r--native/src/widget/text_input.rs155
-rw-r--r--native/src/widget/text_input/cursor.rs25
-rw-r--r--native/src/widget/text_input/editor.rs4
-rw-r--r--native/src/widget/text_input/value.rs9
-rw-r--r--native/src/widget/toggler.rs277
-rw-r--r--native/src/widget/tooltip.rs210
-rw-r--r--native/src/window/event.rs14
-rw-r--r--rustfmt.toml1
-rw-r--r--src/application.rs79
-rw-r--r--src/error.rs13
-rw-r--r--src/executor.rs17
-rw-r--r--src/lib.rs15
-rw-r--r--src/sandbox.rs39
-rw-r--r--src/settings.rs25
-rw-r--r--src/widget.rs8
-rw-r--r--src/window.rs2
-rw-r--r--src/window/icon.rs18
-rw-r--r--src/window/mode.rs3
-rw-r--r--src/window/position.rs33
-rw-r--r--src/window/settings.rs7
-rw-r--r--style/Cargo.toml7
-rw-r--r--style/src/button.rs2
-rw-r--r--style/src/checkbox.rs2
-rw-r--r--style/src/lib.rs2
-rw-r--r--style/src/pane_grid.rs51
-rw-r--r--style/src/pick_list.rs2
-rw-r--r--style/src/progress_bar.rs2
-rw-r--r--style/src/radio.rs2
-rw-r--r--style/src/rule.rs18
-rw-r--r--style/src/toggler.rs57
-rw-r--r--web/Cargo.toml8
-rw-r--r--web/README.md2
-rw-r--r--web/src/clipboard.rs21
-rw-r--r--web/src/css.rs67
-rw-r--r--web/src/lib.rs19
-rw-r--r--web/src/widget.rs3
-rw-r--r--web/src/widget/button.rs52
-rw-r--r--web/src/widget/checkbox.rs1
-rw-r--r--web/src/widget/column.rs20
-rw-r--r--web/src/widget/container.rs21
-rw-r--r--web/src/widget/radio.rs1
-rw-r--r--web/src/widget/row.rs20
-rw-r--r--web/src/widget/scrollable.rs11
-rw-r--r--web/src/widget/slider.rs2
-rw-r--r--web/src/widget/text_input.rs32
-rw-r--r--web/src/widget/toggler.rs171
-rw-r--r--wgpu/Cargo.toml35
-rw-r--r--wgpu/README.md2
-rw-r--r--wgpu/src/backend.rs23
-rw-r--r--wgpu/src/image.rs195
-rw-r--r--wgpu/src/image/atlas.rs24
-rw-r--r--wgpu/src/image/atlas/entry.rs2
-rw-r--r--wgpu/src/image/raster.rs8
-rw-r--r--wgpu/src/image/vector.rs42
-rw-r--r--wgpu/src/lib.rs2
-rw-r--r--wgpu/src/quad.rs178
-rw-r--r--wgpu/src/settings.rs52
-rw-r--r--wgpu/src/shader/blit.frag12
-rw-r--r--wgpu/src/shader/blit.frag.spvbin684 -> 0 bytes
-rw-r--r--wgpu/src/shader/blit.vert26
-rw-r--r--wgpu/src/shader/blit.vert.spvbin1384 -> 0 bytes
-rw-r--r--wgpu/src/shader/blit.wgsl43
-rw-r--r--wgpu/src/shader/image.frag12
-rw-r--r--wgpu/src/shader/image.frag.spvbin684 -> 0 bytes
-rw-r--r--wgpu/src/shader/image.vert27
-rw-r--r--wgpu/src/shader/image.vert.spvbin2504 -> 0 bytes
-rw-r--r--wgpu/src/shader/image.wgsl47
-rw-r--r--wgpu/src/shader/quad.frag66
-rw-r--r--wgpu/src/shader/quad.frag.spvbin4212 -> 0 bytes
-rw-r--r--wgpu/src/shader/quad.vert47
-rw-r--r--wgpu/src/shader/quad.vert.spvbin3604 -> 0 bytes
-rw-r--r--wgpu/src/shader/quad.wgsl122
-rw-r--r--wgpu/src/shader/triangle.frag8
-rw-r--r--wgpu/src/shader/triangle.frag.spvbin372 -> 0 bytes
-rw-r--r--wgpu/src/shader/triangle.vert15
-rw-r--r--wgpu/src/shader/triangle.vert.spvbin1256 -> 0 bytes
-rw-r--r--wgpu/src/shader/triangle.wgsl31
-rw-r--r--wgpu/src/text.rs4
-rw-r--r--wgpu/src/triangle.rs146
-rw-r--r--wgpu/src/triangle/msaa.rs119
-rw-r--r--wgpu/src/widget.rs6
-rw-r--r--wgpu/src/widget/pane_grid.rs8
-rw-r--r--wgpu/src/widget/toggler.rs9
-rw-r--r--wgpu/src/widget/tooltip.rs6
-rw-r--r--wgpu/src/window/compositor.rs94
-rw-r--r--winit/Cargo.toml16
-rw-r--r--winit/README.md2
-rw-r--r--winit/src/application.rs90
-rw-r--r--winit/src/application/state.rs28
-rw-r--r--winit/src/clipboard.rs47
-rw-r--r--winit/src/conversion.rs413
-rw-r--r--winit/src/lib.rs2
-rw-r--r--winit/src/mode.rs3
-rw-r--r--winit/src/position.rs22
-rw-r--r--winit/src/settings.rs25
-rw-r--r--winit/src/settings/windows.rs16
219 files changed, 6266 insertions, 1761 deletions
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 4a3aad7c..8a891884 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1 +1,2 @@
+github: hecrj
ko_fi: hecrj_
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 3b0bf033..a00e2a22 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -11,6 +11,11 @@ jobs:
- name: Install cargo-deb
run: cargo install cargo-deb
- uses: actions/checkout@master
+ - name: Install dependencies
+ run: |
+ export DEBIAN_FRONTED=noninteractive
+ sudo apt-get -qq update
+ sudo apt-get install -y libxkbcommon-dev
- name: Enable Link Time Optimizations
run: |
echo "[profile.release]" >> Cargo.toml
@@ -67,6 +72,8 @@ jobs:
env:
MACOSX_DEPLOYMENT_TARGET: 10.14
run: cargo build --verbose --release --package todos
+ - name: Open binary via double-click
+ run: chmod +x target/release/todos
- name: Archive todos binary
uses: actions/upload-artifact@v1
with:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 9e73d3d3..0450f13d 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -12,6 +12,12 @@ jobs:
with:
rust-version: ${{ matrix.rust }}
- uses: actions/checkout@master
+ - name: Install dependencies
+ if: matrix.os == 'ubuntu-latest'
+ run: |
+ export DEBIAN_FRONTED=noninteractive
+ sudo apt-get -qq update
+ sudo apt-get install -y libxkbcommon-dev
- name: Run tests
run: |
cargo test --verbose --all
@@ -29,5 +35,5 @@ jobs:
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 `pokedex` example
- run: cargo build --package pokedex --target wasm32-unknown-unknown
+ - name: Check compilation of `todos` example
+ run: cargo build --package todos --target wasm32-unknown-unknown
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ee650337..3feaeba3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,10 +5,115 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+
+## [0.3.0] - 2021-03-31
### Added
-- `"system_font"` feature gates reading system fonts. [#370]
+- Touch support. [#57] [#650] (thanks to @simlay and @discordance!)
+- Clipboard write access for
+ - `TextInput` widget. [#770]
+ - `Application::update`. [#773]
+- `image::Viewer` widget. It allows panning and scaling of an image. [#319] (thanks to @tarkah!)
+- `Tooltip` widget. It annotates content with some text on mouse hover. [#465] (thanks to @yusdacra!)
+- Support for the [`smol`] async runtime. [#699] (thanks to @JayceFayne!)
+- Support for graceful exiting when using the `Application` trait. [#804]
+- Image format features in [`iced_wgpu`] to reduce code bloat. [#392] (thanks to @unrelentingtech!)
+- `Focused` and `Unfocused` variant to `window::Event`. [#701] (thanks to @cossonleo!)
+- `WGPU_BACKEND` environment variable to configure the internal graphics backend of `iced_wgpu`. [#789] (thanks to @Cupnfish!)
+
+### Changed
+- The `TitleBar` of a `PaneGrid` now supports generic elements. [#657] (thanks to @clarkmoody!)
+- The `Error` type now implements `Send` and `Sync`. [#719] (thanks to @taiki-e!)
+- The `Style` types in `iced_style` now implement `Clone` and `Copy`. [#720] (thanks to @taiki-e!)
+- The following dependencies have been updated:
+ - [`font-kit`] → `0.10` [#669]
+ - [`glutin`] → `0.26` [#658]
+ - [`resvg`] → `0.12` [#669]
+ - [`tokio`] → `1.0` [#672] (thanks to @yusdacra!)
+ - [`winit`] → `0.24` [#658]
+ - [`wgpu`] → `0.7` [#725] (thanks to @PolyMeilex)
+- The following examples were improved:
+ - `download_progress` now showcases multiple file downloads at once. [#283] (thanks to @Folyd!)
+ - `solar_system` uses the new `rand` API. [#760] (thanks to @TriedAngle!)
+
+### Fixed
+- Button events not being propagated to contents. [#668]
+- Incorrect overlay implementation for the `Button` widget. [#764]
+- `Viewport::physical_width` returning the wrong value. [#700]
+- Outdated documentation for the `Sandbox` trait. [#710]
+
+[#57]: https://github.com/hecrj/iced/pull/57
+[#283]: https://github.com/hecrj/iced/pull/283
+[#319]: https://github.com/hecrj/iced/pull/319
+[#392]: https://github.com/hecrj/iced/pull/392
+[#465]: https://github.com/hecrj/iced/pull/465
+[#650]: https://github.com/hecrj/iced/pull/650
+[#657]: https://github.com/hecrj/iced/pull/657
+[#658]: https://github.com/hecrj/iced/pull/658
+[#668]: https://github.com/hecrj/iced/pull/668
+[#669]: https://github.com/hecrj/iced/pull/669
+[#672]: https://github.com/hecrj/iced/pull/672
+[#699]: https://github.com/hecrj/iced/pull/699
+[#700]: https://github.com/hecrj/iced/pull/700
+[#701]: https://github.com/hecrj/iced/pull/701
+[#710]: https://github.com/hecrj/iced/pull/710
+[#719]: https://github.com/hecrj/iced/pull/719
+[#720]: https://github.com/hecrj/iced/pull/720
+[#725]: https://github.com/hecrj/iced/pull/725
+[#760]: https://github.com/hecrj/iced/pull/760
+[#764]: https://github.com/hecrj/iced/pull/764
+[#770]: https://github.com/hecrj/iced/pull/770
+[#773]: https://github.com/hecrj/iced/pull/773
+[#789]: https://github.com/hecrj/iced/pull/789
+[#804]: https://github.com/hecrj/iced/pull/804
+[`smol`]: https://github.com/smol-rs/smol
+[`winit`]: https://github.com/rust-windowing/winit
+[`glutin`]: https://github.com/rust-windowing/glutin
+[`font-kit`]: https://github.com/servo/font-kit
+
+## [0.2.0] - 2020-11-26
+### Added
+- __[`Canvas` interactivity][canvas]__ (#325)
+ A trait-based approach to react to mouse and keyboard interactions in [the `Canvas` widget][#193].
+
+- __[`iced_graphics` subcrate][opengl]__ (#354)
+ A backend-agnostic graphics subcrate that can be leveraged to build new renderers.
+
+- __[OpenGL renderer][opengl]__ (#354)
+ An OpenGL renderer powered by [`iced_graphics`], [`glow`], and [`glutin`]. It is an alternative to the default [`wgpu`] renderer.
+
+- __[Overlay support][pick_list]__ (#444)
+ Basic support for superpositioning interactive widgets on top of other widgets.
+
+- __[Faster event loop][view]__ (#597)
+ The event loop now takes advantage of the data dependencies in [The Elm Architecture] and leverages the borrow checker to keep the widget tree alive between iterations, avoiding unnecessary rebuilds.
+
+- __[Event capturing][event]__ (#614)
+ The runtime now can tell whether a widget has handled an event or not, easing [integration with existing applications].
+
+- __[`PickList` widget][pick_list]__ (#444)
+ A drop-down selector widget built on top of the new overlay support.
+
+- __[`QRCode` widget][qr_code]__ (#622)
+ A widget that displays a QR code, powered by [the `qrcode` crate].
+
+[canvas]: https://github.com/hecrj/iced/pull/325
+[opengl]: https://github.com/hecrj/iced/pull/354
+[`iced_graphics`]: https://github.com/hecrj/iced/pull/354
+[pane_grid]: https://github.com/hecrj/iced/pull/397
+[pick_list]: https://github.com/hecrj/iced/pull/444
+[error]: https://github.com/hecrj/iced/pull/514
+[view]: https://github.com/hecrj/iced/pull/597
+[event]: https://github.com/hecrj/iced/pull/614
+[color]: https://github.com/hecrj/iced/pull/200
+[qr_code]: https://github.com/hecrj/iced/pull/622
+[#193]: https://github.com/hecrj/iced/pull/193
+[`glutin`]: https://github.com/rust-windowing/glutin
+[`wgpu`]: https://github.com/gfx-rs/wgpu-rs
+[`glow`]: https://github.com/grovesNL/glow
+[the `qrcode` crate]: https://docs.rs/qrcode/0.12.0/qrcode/
+[integration with existing applications]: https://github.com/hecrj/iced/pull/183
+[The Elm Architecture]: https://guide.elm-lang.org/architecture/
-[#370]: https://github.com/hecrj/iced/pull/370
## [0.1.1] - 2020-04-15
### Added
@@ -102,7 +207,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[`wasm-bindgen-futures`]: https://github.com/rustwasm/wasm-bindgen/tree/master/crates/futures
[`resvg`]: https://github.com/RazrFalcon/resvg
[`raqote`]: https://github.com/jrmuizel/raqote
-[`iced_wgpu`]: https://github.com/hecrj/iced/tree/0.1/wgpu
+[`iced_wgpu`]: https://github.com/hecrj/iced/tree/master/wgpu
## [0.1.0-beta] - 2019-11-25
@@ -114,7 +219,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- First release! :tada:
-[Unreleased]: https://github.com/hecrj/iced/compare/0.1.1...HEAD
+[Unreleased]: https://github.com/hecrj/iced/compare/0.3.0...HEAD
+[0.3.0]: https://github.com/hecrj/iced/compare/0.2.0...0.3.0
+[0.2.0]: https://github.com/hecrj/iced/compare/0.1.1...0.2.0
[0.1.1]: https://github.com/hecrj/iced/compare/0.1.0...0.1.1
[0.1.0]: https://github.com/hecrj/iced/compare/0.1.0-beta...0.1.0
[0.1.0-beta]: https://github.com/hecrj/iced/compare/0.1.0-alpha...0.1.0-beta
diff --git a/Cargo.toml b/Cargo.toml
index 6221ae4b..5b1cfb0e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "iced"
-version = "0.2.0"
+version = "0.3.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
description = "A cross-platform GUI library inspired by Elm"
@@ -41,6 +41,8 @@ tokio = ["iced_futures/tokio"]
tokio_old = ["iced_futures/tokio_old"]
# Enables `async-std` as the `executor::Default` on native platforms
async-std = ["iced_futures/async-std"]
+# Enables `smol` as the `executor::Default` on native platforms
+smol = ["iced_futures/smol"]
# Enables advanced color conversion via `palette`
palette = ["iced_core/palette"]
@@ -81,22 +83,25 @@ members = [
"examples/svg",
"examples/todos",
"examples/tour",
+ "examples/tooltip",
+ "examples/url_handler",
+ "examples/menu",
]
[dependencies]
-iced_core = { version = "0.3", path = "core" }
-iced_futures = { version = "0.2", path = "futures" }
+iced_core = { version = "0.4", path = "core" }
+iced_futures = { version = "0.3", path = "futures" }
thiserror = "1.0"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
-iced_winit = { version = "0.2", path = "winit" }
-iced_glutin = { version = "0.1", path = "glutin", optional = true }
-iced_wgpu = { version = "0.3", path = "wgpu", optional = true }
-iced_glow = { version = "0.1", path = "glow", optional = true}
+iced_winit = { version = "0.3", path = "winit" }
+iced_glutin = { version = "0.2", path = "glutin", optional = true }
+iced_wgpu = { version = "0.4", path = "wgpu", optional = true }
+iced_glow = { version = "0.2", path = "glow", optional = true}
[target.'cfg(target_arch = "wasm32")'.dependencies]
-iced_web = { version = "0.3", path = "web" }
+iced_web = { version = "0.4", path = "web" }
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
-features = ["image", "svg", "canvas"]
+features = ["image", "svg", "canvas", "qr_code"]
diff --git a/README.md b/README.md
index 09647a1e..4ad2fb70 100644
--- a/README.md
+++ b/README.md
@@ -55,7 +55,7 @@ __Iced is currently experimental software.__ [Take a look at the roadmap],
Add `iced` as a dependency in your `Cargo.toml`:
```toml
-iced = "0.2"
+iced = "0.3"
```
__Iced moves fast and the `master` branch can contain breaking changes!__ If
@@ -168,7 +168,7 @@ Browse the [documentation] and the [examples] to learn more!
Iced was originally born as an attempt at bringing the simplicity of [Elm] and
[The Elm Architecture] into [Coffee], a 2D game engine I am working on.
-The core of the library was implemented during May in [this pull request].
+The core of the library was implemented during May 2019 in [this pull request].
[The first alpha version] was eventually released as
[a renderer-agnostic GUI library]. The library did not provide a renderer and
implemented the current [tour example] on top of [`ggez`], a game library.
diff --git a/core/Cargo.toml b/core/Cargo.toml
index a859c868..54d927af 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "iced_core"
-version = "0.3.0"
+version = "0.4.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
description = "The essential concepts of Iced"
@@ -8,6 +8,7 @@ license = "MIT"
repository = "https://github.com/hecrj/iced"
[dependencies]
+bitflags = "1.2"
[dependencies.palette]
version = "0.5.0"
diff --git a/core/README.md b/core/README.md
index 3ec053ac..86d631d2 100644
--- a/core/README.md
+++ b/core/README.md
@@ -18,7 +18,7 @@ This crate is meant to be a starting point for an Iced runtime.
Add `iced_core` as a dependency in your `Cargo.toml`:
```toml
-iced_core = "0.3"
+iced_core = "0.4"
```
__Iced moves fast and the `master` branch can contain breaking changes!__ If
diff --git a/core/src/keyboard.rs b/core/src/keyboard.rs
index 61e017ad..cb64701a 100644
--- a/core/src/keyboard.rs
+++ b/core/src/keyboard.rs
@@ -1,8 +1,10 @@
//! Reuse basic keyboard types.
mod event;
+mod hotkey;
mod key_code;
mod modifiers;
pub use event::Event;
+pub use hotkey::Hotkey;
pub use key_code::KeyCode;
pub use modifiers::Modifiers;
diff --git a/core/src/keyboard/hotkey.rs b/core/src/keyboard/hotkey.rs
new file mode 100644
index 00000000..310ef286
--- /dev/null
+++ b/core/src/keyboard/hotkey.rs
@@ -0,0 +1,18 @@
+use crate::keyboard::{KeyCode, Modifiers};
+
+/// Representation of a hotkey, consists on the combination of a [`KeyCode`] and [`Modifiers`].
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct Hotkey {
+ /// The key that represents this hotkey.
+ pub key: KeyCode,
+
+ /// The list of modifiers that represents this hotkey.
+ pub modifiers: Modifiers,
+}
+
+impl Hotkey {
+ /// Creates a new [`Hotkey`] with the given [`Modifiers`] and [`KeyCode`].
+ pub fn new(modifiers: Modifiers, key: KeyCode) -> Self {
+ Self { modifiers, key }
+ }
+}
diff --git a/core/src/keyboard/modifiers.rs b/core/src/keyboard/modifiers.rs
index d2a0500e..383b9370 100644
--- a/core/src/keyboard/modifiers.rs
+++ b/core/src/keyboard/modifiers.rs
@@ -1,20 +1,53 @@
-/// The current state of the keyboard modifiers.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
-pub struct Modifiers {
- /// Whether a shift key is pressed
- pub shift: bool,
+use bitflags::bitflags;
- /// Whether a control key is pressed
- pub control: bool,
-
- /// Whether an alt key is pressed
- pub alt: bool,
-
- /// Whether a logo key is pressed (e.g. windows key, command key...)
- pub logo: bool,
+bitflags! {
+ /// The current state of the keyboard modifiers.
+ #[derive(Default)]
+ pub struct Modifiers: u32{
+ /// The "shift" key.
+ const SHIFT = 0b100 << 0;
+ // const LSHIFT = 0b010 << 0;
+ // const RSHIFT = 0b001 << 0;
+ //
+ /// The "control" key.
+ const CTRL = 0b100 << 3;
+ // const LCTRL = 0b010 << 3;
+ // const RCTRL = 0b001 << 3;
+ //
+ /// The "alt" key.
+ const ALT = 0b100 << 6;
+ // const LALT = 0b010 << 6;
+ // const RALT = 0b001 << 6;
+ //
+ /// The "windows" key on Windows, "command" key on Mac, and
+ /// "super" key on Linux.
+ const LOGO = 0b100 << 9;
+ // const LLOGO = 0b010 << 9;
+ // const RLOGO = 0b001 << 9;
+ }
}
impl Modifiers {
+ /// Returns true if the [`SHIFT`] key is pressed in the [`Modifiers`].
+ pub fn shift(self) -> bool {
+ self.contains(Self::SHIFT)
+ }
+
+ /// Returns true if the [`CTRL`] key is pressed in the [`Modifiers`].
+ pub fn control(self) -> bool {
+ self.contains(Self::CTRL)
+ }
+
+ /// Returns true if the [`ALT`] key is pressed in the [`Modifiers`].
+ pub fn alt(self) -> bool {
+ self.contains(Self::ALT)
+ }
+
+ /// Returns true if the [`LOGO`] key is pressed in the [`Modifiers`].
+ pub fn logo(self) -> bool {
+ self.contains(Self::LOGO)
+ }
+
/// Returns true if a "command key" is pressed in the [`Modifiers`].
///
/// The "command key" is the main modifier key used to issue commands in the
@@ -22,24 +55,13 @@ impl Modifiers {
///
/// - It is the `logo` or command key (⌘) on macOS
/// - It is the `control` key on other platforms
- pub fn is_command_pressed(self) -> bool {
+ pub fn command(self) -> bool {
#[cfg(target_os = "macos")]
- let is_pressed = self.logo;
+ let is_pressed = self.logo();
#[cfg(not(target_os = "macos"))]
- let is_pressed = self.control;
+ let is_pressed = self.control();
is_pressed
}
-
- /// Returns true if the current [`Modifiers`] have at least the same
- /// keys pressed as the provided ones, and false otherwise.
- pub fn matches(&self, modifiers: Self) -> bool {
- let shift = !modifiers.shift || self.shift;
- let control = !modifiers.control || self.control;
- let alt = !modifiers.alt || self.alt;
- let logo = !modifiers.logo || self.logo;
-
- shift && control && alt && logo
- }
}
diff --git a/core/src/lib.rs b/core/src/lib.rs
index f2d21a5f..c4288158 100644
--- a/core/src/lib.rs
+++ b/core/src/lib.rs
@@ -15,6 +15,7 @@
#![forbid(unsafe_code)]
#![forbid(rust_2018_idioms)]
pub mod keyboard;
+pub mod menu;
pub mod mouse;
mod align;
@@ -22,6 +23,7 @@ mod background;
mod color;
mod font;
mod length;
+mod padding;
mod point;
mod rectangle;
mod size;
@@ -32,6 +34,8 @@ pub use background::Background;
pub use color::Color;
pub use font::Font;
pub use length::Length;
+pub use menu::Menu;
+pub use padding::Padding;
pub use point::Point;
pub use rectangle::Rectangle;
pub use size::Size;
diff --git a/core/src/menu.rs b/core/src/menu.rs
new file mode 100644
index 00000000..8a679085
--- /dev/null
+++ b/core/src/menu.rs
@@ -0,0 +1,145 @@
+//! Build menus for your application.
+use crate::keyboard::Hotkey;
+
+/// Menu representation.
+///
+/// This can be used by `shell` implementations to create a menu.
+#[derive(Debug, Clone)]
+pub struct Menu<Message> {
+ entries: Vec<Entry<Message>>,
+}
+
+impl<Message> PartialEq for Menu<Message> {
+ fn eq(&self, other: &Self) -> bool {
+ self.entries == other.entries
+ }
+}
+
+impl<Message> Menu<Message> {
+ /// Creates an empty [`Menu`].
+ pub fn new() -> Self {
+ Self::with_entries(Vec::new())
+ }
+
+ /// Creates a new [`Menu`] with the given entries.
+ pub fn with_entries(entries: Vec<Entry<Message>>) -> Self {
+ Self { entries }
+ }
+
+ /// Returns a [`MenuEntry`] iterator.
+ pub fn iter(&self) -> impl Iterator<Item = &Entry<Message>> {
+ self.entries.iter()
+ }
+
+ /// Adds an [`Entry`] to the [`Menu`].
+ pub fn push(mut self, entry: Entry<Message>) -> Self {
+ self.entries.push(entry);
+ self
+ }
+
+ /// Maps the `Message` of the [`Menu`] using the provided function.
+ ///
+ /// This is useful to compose menus and split them into different
+ /// abstraction levels.
+ pub fn map<B>(self, f: impl Fn(Message) -> B + Copy) -> Menu<B> {
+ // TODO: Use a boxed trait to avoid reallocation of entries
+ Menu {
+ entries: self
+ .entries
+ .into_iter()
+ .map(|entry| entry.map(f))
+ .collect(),
+ }
+ }
+}
+
+/// Represents one of the possible entries used to build a [`Menu`].
+#[derive(Debug, Clone)]
+pub enum Entry<Message> {
+ /// Item for a [`Menu`]
+ Item {
+ /// The title of the item
+ title: String,
+ /// The [`Hotkey`] to activate the item, if any
+ hotkey: Option<Hotkey>,
+ /// The message generated when the item is activated
+ on_activation: Message,
+ },
+ /// Dropdown for a [`Menu`]
+ Dropdown {
+ /// Title of the dropdown
+ title: String,
+ /// The submenu of the dropdown
+ submenu: Menu<Message>,
+ },
+ /// Separator for a [`Menu`]
+ Separator,
+}
+
+impl<Message> Entry<Message> {
+ /// Creates an [`Entry::Item`].
+ pub fn item<S: Into<String>>(
+ title: S,
+ hotkey: impl Into<Option<Hotkey>>,
+ on_activation: Message,
+ ) -> Self {
+ let title = title.into();
+ let hotkey = hotkey.into();
+
+ Self::Item {
+ title,
+ hotkey,
+ on_activation,
+ }
+ }
+
+ /// Creates an [`Entry::Dropdown`].
+ pub fn dropdown<S: Into<String>>(title: S, submenu: Menu<Message>) -> Self {
+ let title = title.into();
+
+ Self::Dropdown { title, submenu }
+ }
+
+ fn map<B>(self, f: impl Fn(Message) -> B + Copy) -> Entry<B> {
+ match self {
+ Self::Item {
+ title,
+ hotkey,
+ on_activation,
+ } => Entry::Item {
+ title,
+ hotkey,
+ on_activation: f(on_activation),
+ },
+ Self::Dropdown { title, submenu } => Entry::Dropdown {
+ title,
+ submenu: submenu.map(f),
+ },
+ Self::Separator => Entry::Separator,
+ }
+ }
+}
+
+impl<Message> PartialEq for Entry<Message> {
+ fn eq(&self, other: &Self) -> bool {
+ match (self, other) {
+ (
+ Entry::Item { title, hotkey, .. },
+ Entry::Item {
+ title: other_title,
+ hotkey: other_hotkey,
+ ..
+ },
+ ) => title == other_title && hotkey == other_hotkey,
+ (
+ Entry::Dropdown { title, submenu },
+ Entry::Dropdown {
+ title: other_title,
+ submenu: other_submenu,
+ },
+ ) => title == other_title && submenu == other_submenu,
+ (Entry::Separator, Entry::Separator) => true,
+ _ => false,
+ }
+ }
+}
diff --git a/core/src/mouse/event.rs b/core/src/mouse/event.rs
index 2f07b207..321b8399 100644
--- a/core/src/mouse/event.rs
+++ b/core/src/mouse/event.rs
@@ -1,3 +1,5 @@
+use crate::Point;
+
use super::Button;
/// A mouse event.
@@ -16,11 +18,8 @@ pub enum Event {
/// The mouse cursor was moved
CursorMoved {
- /// The X coordinate of the mouse position
- x: f32,
-
- /// The Y coordinate of the mouse position
- y: f32,
+ /// The new position of the mouse cursor
+ position: Point,
},
/// A mouse button was pressed.
diff --git a/core/src/padding.rs b/core/src/padding.rs
new file mode 100644
index 00000000..22467d6b
--- /dev/null
+++ b/core/src/padding.rs
@@ -0,0 +1,107 @@
+/// An amount of space to pad for each side of a box
+///
+/// You can leverage the `From` trait to build [`Padding`] conveniently:
+///
+/// ```
+/// # use iced_core::Padding;
+/// #
+/// let padding = Padding::from(20); // 20px on all sides
+/// let padding = Padding::from([10, 20]); // top/bottom, left/right
+/// let padding = Padding::from([5, 10, 15, 20]); // top, right, bottom, left
+/// ```
+///
+/// Normally, the `padding` method of a widget will ask for an `Into<Padding>`,
+/// so you can easily write:
+///
+/// ```
+/// # use iced_core::Padding;
+/// #
+/// # struct Widget;
+/// #
+/// impl Widget {
+/// # pub fn new() -> Self { Self }
+/// #
+/// pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
+/// // ...
+/// self
+/// }
+/// }
+///
+/// let widget = Widget::new().padding(20); // 20px on all sides
+/// let widget = Widget::new().padding([10, 20]); // top/bottom, left/right
+/// let widget = Widget::new().padding([5, 10, 15, 20]); // top, right, bottom, left
+/// ```
+#[derive(Debug, Hash, Copy, Clone)]
+pub struct Padding {
+ /// Top padding
+ pub top: u16,
+ /// Right padding
+ pub right: u16,
+ /// Bottom padding
+ pub bottom: u16,
+ /// Left padding
+ pub left: u16,
+}
+
+impl Padding {
+ /// Padding of zero
+ pub const ZERO: Padding = Padding {
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0,
+ };
+
+ /// Create a Padding that is equal on all sides
+ pub const fn new(padding: u16) -> Padding {
+ Padding {
+ top: padding,
+ right: padding,
+ bottom: padding,
+ left: padding,
+ }
+ }
+
+ /// Returns the total amount of vertical [`Padding`].
+ pub fn vertical(self) -> u16 {
+ self.top + self.bottom
+ }
+
+ /// Returns the total amount of horizontal [`Padding`].
+ pub fn horizontal(self) -> u16 {
+ self.left + self.right
+ }
+}
+
+impl std::convert::From<u16> for Padding {
+ fn from(p: u16) -> Self {
+ Padding {
+ top: p,
+ right: p,
+ bottom: p,
+ left: p,
+ }
+ }
+}
+
+impl std::convert::From<[u16; 2]> for Padding {
+ fn from(p: [u16; 2]) -> Self {
+ Padding {
+ top: p[0],
+ right: p[1],
+ bottom: p[0],
+ left: p[1],
+ }
+ }
+}
+
+impl std::convert::From<[u16; 4]> for Padding {
+ fn from(p: [u16; 4]) -> Self {
+ Padding {
+ top: p[0],
+ right: p[1],
+ bottom: p[2],
+ left: p[3],
+ }
+ }
+}
diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs
index 0a7f5fe2..4e082051 100644
--- a/core/src/rectangle.rs
+++ b/core/src/rectangle.rs
@@ -105,8 +105,8 @@ impl Rectangle<f32> {
Rectangle {
x: self.x as u32,
y: self.y as u32,
- width: self.width.ceil() as u32,
- height: self.height.ceil() as u32,
+ width: self.width as u32,
+ height: self.height as u32,
}
}
}
diff --git a/core/src/size.rs b/core/src/size.rs
index 9ea9e686..6745c6c8 100644
--- a/core/src/size.rs
+++ b/core/src/size.rs
@@ -1,4 +1,4 @@
-use crate::Vector;
+use crate::{Padding, Vector};
use std::f32;
/// An amount of space in 2 dimensions.
@@ -28,10 +28,10 @@ impl Size {
pub const INFINITY: Size = Size::new(f32::INFINITY, f32::INFINITY);
/// Increments the [`Size`] to account for the given padding.
- pub fn pad(&self, padding: f32) -> Self {
+ pub fn pad(&self, padding: Padding) -> Self {
Size {
- width: self.width + padding * 2.0,
- height: self.height + padding * 2.0,
+ width: self.width + padding.horizontal() as f32,
+ height: self.height + padding.vertical() as f32,
}
}
}
diff --git a/examples/README.md b/examples/README.md
index 32ccf724..10c28cf5 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -118,7 +118,7 @@ cargo run --package <example>
[Ghostscript Tiger]: https://commons.wikimedia.org/wiki/File:Ghostscript_Tiger.svg
## [Coffee]
-Since [Iced was born in May], it has been powering the user interfaces in
+Since [Iced was born in May 2019], it has been powering the user interfaces in
[Coffee], an experimental 2D game engine.
@@ -128,6 +128,6 @@ Since [Iced was born in May], it has been powering the user interfaces in
</a>
</div>
-[Iced was born in May]: https://github.com/hecrj/coffee/pull/35
+[Iced was born in May 2019]: https://github.com/hecrj/coffee/pull/35
[`ui` module]: https://docs.rs/coffee/0.3.2/coffee/ui/index.html
[Coffee]: https://github.com/hecrj/coffee
diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs
index b317ac00..9bcc827b 100644
--- a/examples/clock/src/main.rs
+++ b/examples/clock/src/main.rs
@@ -1,7 +1,7 @@
use iced::{
canvas::{self, Cache, Canvas, Cursor, Geometry, LineCap, Path, Stroke},
- executor, time, Application, Color, Command, Container, Element, Length,
- Point, Rectangle, Settings, Subscription, Vector,
+ executor, time, Application, Clipboard, Color, Command, Container, Element,
+ Length, Point, Rectangle, Settings, Subscription, Vector,
};
pub fn main() -> iced::Result {
@@ -40,7 +40,11 @@ impl Application for Clock {
String::from("Clock - Iced")
}
- fn update(&mut self, message: Message) -> Command<Message> {
+ fn update(
+ &mut self,
+ message: Message,
+ _clipboard: &mut Clipboard,
+ ) -> Command<Message> {
match message {
Message::Tick(local_time) => {
let now = local_time;
diff --git a/examples/download_progress/Cargo.toml b/examples/download_progress/Cargo.toml
index 4b05e7dc..d3c578b1 100644
--- a/examples/download_progress/Cargo.toml
+++ b/examples/download_progress/Cargo.toml
@@ -1,12 +1,12 @@
[package]
name = "download_progress"
version = "0.1.0"
-authors = ["Songtronix <contact@songtronix.com>"]
+authors = ["Songtronix <contact@songtronix.com>", "Folyd <lyshuhow@gmail.com>"]
edition = "2018"
publish = false
[dependencies]
-iced = { path = "../..", features = ["tokio_old"] }
+iced = { path = "../..", features = ["tokio"] }
iced_native = { path = "../../native" }
iced_futures = { path = "../../futures" }
-reqwest = "0.10"
+reqwest = "0.11"
diff --git a/examples/download_progress/README.md b/examples/download_progress/README.md
index c606c5f9..7999ce94 100644
--- a/examples/download_progress/README.md
+++ b/examples/download_progress/README.md
@@ -1,6 +1,6 @@
## Download progress
-A basic application that asynchronously downloads a dummy file of 100 MB and tracks the download progress.
+A basic application that asynchronously downloads multiple dummy files of 100 MB and tracks the download progress.
The example implements a custom `Subscription` in the __[`download`](src/download.rs)__ module. This subscription downloads and produces messages that can be used to keep track of its progress.
diff --git a/examples/download_progress/src/download.rs b/examples/download_progress/src/download.rs
index f46a01f7..08805f13 100644
--- a/examples/download_progress/src/download.rs
+++ b/examples/download_progress/src/download.rs
@@ -1,37 +1,46 @@
use iced_futures::futures;
+use std::hash::{Hash, Hasher};
// Just a little utility function
-pub fn file<T: ToString>(url: T) -> iced::Subscription<Progress> {
+pub fn file<I: 'static + Hash + Copy + Send, T: ToString>(
+ id: I,
+ url: T,
+) -> iced::Subscription<(I, Progress)> {
iced::Subscription::from_recipe(Download {
+ id,
url: url.to_string(),
})
}
-pub struct Download {
+pub struct Download<I> {
+ id: I,
url: String,
}
// Make sure iced can use our download stream
-impl<H, I> iced_native::subscription::Recipe<H, I> for Download
+impl<H, I, T> iced_native::subscription::Recipe<H, I> for Download<T>
where
- H: std::hash::Hasher,
+ T: 'static + Hash + Copy + Send,
+ H: Hasher,
{
- type Output = Progress;
+ type Output = (T, Progress);
fn hash(&self, state: &mut H) {
- use std::hash::Hash;
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
- std::any::TypeId::of::<Self>().hash(state);
- self.url.hash(state);
+ self.id.hash(state);
}
fn stream(
self: Box<Self>,
_input: futures::stream::BoxStream<'static, I>,
) -> futures::stream::BoxStream<'static, Self::Output> {
+ let id = self.id;
+
Box::pin(futures::stream::unfold(
State::Ready(self.url),
- |state| async move {
+ move |state| async move {
match state {
State::Ready(url) => {
let response = reqwest::get(&url).await;
@@ -40,7 +49,7 @@ where
Ok(response) => {
if let Some(total) = response.content_length() {
Some((
- Progress::Started,
+ (id, Progress::Started),
State::Downloading {
response,
total,
@@ -48,11 +57,14 @@ where
},
))
} else {
- Some((Progress::Errored, State::Finished))
+ Some((
+ (id, Progress::Errored),
+ State::Finished,
+ ))
}
}
Err(_) => {
- Some((Progress::Errored, State::Finished))
+ Some(((id, Progress::Errored), State::Finished))
}
}
}
@@ -68,7 +80,7 @@ where
(downloaded as f32 / total as f32) * 100.0;
Some((
- Progress::Advanced(percentage),
+ (id, Progress::Advanced(percentage)),
State::Downloading {
response,
total,
@@ -76,8 +88,12 @@ where
},
))
}
- Ok(None) => Some((Progress::Finished, State::Finished)),
- Err(_) => Some((Progress::Errored, State::Finished)),
+ Ok(None) => {
+ Some(((id, Progress::Finished), State::Finished))
+ }
+ Err(_) => {
+ Some(((id, Progress::Errored), State::Finished))
+ }
},
State::Finished => {
// We do not let the stream die, as it would start a
diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs
index 77b01354..6f844e66 100644
--- a/examples/download_progress/src/main.rs
+++ b/examples/download_progress/src/main.rs
@@ -1,6 +1,6 @@
use iced::{
- button, executor, Align, Application, Button, Column, Command, Container,
- Element, Length, ProgressBar, Settings, Subscription, Text,
+ button, executor, Align, Application, Button, Clipboard, Column, Command,
+ Container, Element, Length, ProgressBar, Settings, Subscription, Text,
};
mod download;
@@ -10,17 +10,17 @@ pub fn main() -> iced::Result {
}
#[derive(Debug)]
-enum Example {
- Idle { button: button::State },
- Downloading { progress: f32 },
- Finished { button: button::State },
- Errored { button: button::State },
+struct Example {
+ downloads: Vec<Download>,
+ last_id: usize,
+ add: button::State,
}
#[derive(Debug, Clone)]
pub enum Message {
- Download,
- DownloadProgressed(download::Progress),
+ Add,
+ Download(usize),
+ DownloadProgressed((usize, download::Progress)),
}
impl Application for Example {
@@ -30,8 +30,10 @@ impl Application for Example {
fn new(_flags: ()) -> (Example, Command<Message>) {
(
- Example::Idle {
- button: button::State::new(),
+ Example {
+ downloads: vec![Download::new(0)],
+ last_id: 0,
+ add: button::State::new(),
},
Command::none(),
)
@@ -41,104 +43,177 @@ impl Application for Example {
String::from("Download progress - Iced")
}
- fn update(&mut self, message: Message) -> Command<Message> {
+ fn update(
+ &mut self,
+ message: Message,
+ _clipboard: &mut Clipboard,
+ ) -> Command<Message> {
match message {
- Message::Download => match self {
- Example::Idle { .. }
- | Example::Finished { .. }
- | Example::Errored { .. } => {
- *self = Example::Downloading { progress: 0.0 };
+ Message::Add => {
+ self.last_id = self.last_id + 1;
+
+ self.downloads.push(Download::new(self.last_id));
+ }
+ Message::Download(index) => {
+ if let Some(download) = self.downloads.get_mut(index) {
+ download.start();
}
- _ => {}
- },
- Message::DownloadProgressed(message) => match self {
- Example::Downloading { progress } => match message {
- download::Progress::Started => {
- *progress = 0.0;
- }
- download::Progress::Advanced(percentage) => {
- *progress = percentage;
- }
- download::Progress::Finished => {
- *self = Example::Finished {
- button: button::State::new(),
- }
- }
- download::Progress::Errored => {
- *self = Example::Errored {
- button: button::State::new(),
- };
- }
- },
- _ => {}
- },
+ }
+ Message::DownloadProgressed((id, progress)) => {
+ if let Some(download) =
+ self.downloads.iter_mut().find(|download| download.id == id)
+ {
+ download.progress(progress);
+ }
+ }
};
Command::none()
}
fn subscription(&self) -> Subscription<Message> {
- match self {
- Example::Downloading { .. } => {
- download::file("https://speed.hetzner.de/100MB.bin")
+ Subscription::batch(self.downloads.iter().map(Download::subscription))
+ }
+
+ fn view(&mut self) -> Element<Message> {
+ let downloads = self
+ .downloads
+ .iter_mut()
+ .fold(Column::new().spacing(20), |column, download| {
+ column.push(download.view())
+ })
+ .push(
+ Button::new(&mut self.add, Text::new("Add another download"))
+ .on_press(Message::Add)
+ .padding(10),
+ )
+ .align_items(Align::End);
+
+ Container::new(downloads)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .center_x()
+ .center_y()
+ .padding(20)
+ .into()
+ }
+}
+
+#[derive(Debug)]
+struct Download {
+ id: usize,
+ state: State,
+}
+
+#[derive(Debug)]
+enum State {
+ Idle { button: button::State },
+ Downloading { progress: f32 },
+ Finished { button: button::State },
+ Errored { button: button::State },
+}
+
+impl Download {
+ pub fn new(id: usize) -> Self {
+ Download {
+ id,
+ state: State::Idle {
+ button: button::State::new(),
+ },
+ }
+ }
+
+ pub fn start(&mut self) {
+ match self.state {
+ State::Idle { .. }
+ | State::Finished { .. }
+ | State::Errored { .. } => {
+ self.state = State::Downloading { progress: 0.0 };
+ }
+ _ => {}
+ }
+ }
+
+ pub fn progress(&mut self, new_progress: download::Progress) {
+ match &mut self.state {
+ State::Downloading { progress } => match new_progress {
+ download::Progress::Started => {
+ *progress = 0.0;
+ }
+ download::Progress::Advanced(percentage) => {
+ *progress = percentage;
+ }
+ download::Progress::Finished => {
+ self.state = State::Finished {
+ button: button::State::new(),
+ }
+ }
+ download::Progress::Errored => {
+ self.state = State::Errored {
+ button: button::State::new(),
+ };
+ }
+ },
+ _ => {}
+ }
+ }
+
+ pub fn subscription(&self) -> Subscription<Message> {
+ match self.state {
+ State::Downloading { .. } => {
+ download::file(self.id, "https://speed.hetzner.de/100MB.bin?")
.map(Message::DownloadProgressed)
}
_ => Subscription::none(),
}
}
- fn view(&mut self) -> Element<Message> {
- let current_progress = match self {
- Example::Idle { .. } => 0.0,
- Example::Downloading { progress } => *progress,
- Example::Finished { .. } => 100.0,
- Example::Errored { .. } => 0.0,
+ pub fn view(&mut self) -> Element<Message> {
+ let current_progress = match &self.state {
+ State::Idle { .. } => 0.0,
+ State::Downloading { progress } => *progress,
+ State::Finished { .. } => 100.0,
+ State::Errored { .. } => 0.0,
};
let progress_bar = ProgressBar::new(0.0..=100.0, current_progress);
- let control: Element<_> = match self {
- Example::Idle { button } => {
+ let control: Element<_> = match &mut self.state {
+ State::Idle { button } => {
Button::new(button, Text::new("Start the download!"))
- .on_press(Message::Download)
+ .on_press(Message::Download(self.id))
.into()
}
- Example::Finished { button } => Column::new()
+ State::Finished { button } => Column::new()
.spacing(10)
.align_items(Align::Center)
.push(Text::new("Download finished!"))
.push(
Button::new(button, Text::new("Start again"))
- .on_press(Message::Download),
+ .on_press(Message::Download(self.id)),
)
.into(),
- Example::Downloading { .. } => {
+ State::Downloading { .. } => {
Text::new(format!("Downloading... {:.2}%", current_progress))
.into()
}
- Example::Errored { button } => Column::new()
+ State::Errored { button } => Column::new()
.spacing(10)
.align_items(Align::Center)
.push(Text::new("Something went wrong :("))
.push(
Button::new(button, Text::new("Try again"))
- .on_press(Message::Download),
+ .on_press(Message::Download(self.id)),
)
.into(),
};
- let content = Column::new()
+ Column::new()
.spacing(10)
.padding(10)
.align_items(Align::Center)
.push(progress_bar)
- .push(control);
-
- Container::new(content)
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y()
+ .push(control)
.into()
}
}
diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs
index 6eba6aad..446c190b 100644
--- a/examples/events/src/main.rs
+++ b/examples/events/src/main.rs
@@ -1,22 +1,30 @@
use iced::{
- executor, Align, Application, Checkbox, Column, Command, Container,
- Element, Length, Settings, Subscription, Text,
+ button, executor, Align, Application, Button, Checkbox, Clipboard, Column,
+ Command, Container, Element, HorizontalAlignment, Length, Settings,
+ Subscription, Text,
};
+use iced_native::{window, Event};
pub fn main() -> iced::Result {
- Events::run(Settings::default())
+ Events::run(Settings {
+ exit_on_close_request: false,
+ ..Settings::default()
+ })
}
#[derive(Debug, Default)]
struct Events {
last: Vec<iced_native::Event>,
enabled: bool,
+ exit: button::State,
+ should_exit: bool,
}
#[derive(Debug, Clone)]
enum Message {
EventOccurred(iced_native::Event),
Toggled(bool),
+ Exit,
}
impl Application for Events {
@@ -32,29 +40,41 @@ impl Application for Events {
String::from("Events - Iced")
}
- fn update(&mut self, message: Message) -> Command<Message> {
+ fn update(
+ &mut self,
+ message: Message,
+ _clipboard: &mut Clipboard,
+ ) -> Command<Message> {
match message {
- Message::EventOccurred(event) => {
+ Message::EventOccurred(event) if self.enabled => {
self.last.push(event);
if self.last.len() > 5 {
let _ = self.last.remove(0);
}
}
+ Message::EventOccurred(event) => {
+ if let Event::Window(window::Event::CloseRequested) = event {
+ self.should_exit = true;
+ }
+ }
Message::Toggled(enabled) => {
self.enabled = enabled;
}
+ Message::Exit => {
+ self.should_exit = true;
+ }
};
Command::none()
}
fn subscription(&self) -> Subscription<Message> {
- if self.enabled {
- iced_native::subscription::events().map(Message::EventOccurred)
- } else {
- Subscription::none()
- }
+ iced_native::subscription::events().map(Message::EventOccurred)
+ }
+
+ fn should_exit(&self) -> bool {
+ self.should_exit
}
fn view(&mut self) -> Element<Message> {
@@ -71,11 +91,22 @@ impl Application for Events {
Message::Toggled,
);
+ let exit = Button::new(
+ &mut self.exit,
+ Text::new("Exit")
+ .width(Length::Fill)
+ .horizontal_alignment(HorizontalAlignment::Center),
+ )
+ .width(Length::Units(100))
+ .padding(10)
+ .on_press(Message::Exit);
+
let content = Column::new()
.align_items(Align::Center)
.spacing(20)
.push(events)
- .push(toggle);
+ .push(toggle)
+ .push(exit);
Container::new(content)
.width(Length::Fill)
diff --git a/examples/game_of_life/Cargo.toml b/examples/game_of_life/Cargo.toml
index 9c4172c4..ffd2f19e 100644
--- a/examples/game_of_life/Cargo.toml
+++ b/examples/game_of_life/Cargo.toml
@@ -7,6 +7,6 @@ publish = false
[dependencies]
iced = { path = "../..", features = ["canvas", "tokio", "debug"] }
-tokio = { version = "0.3", features = ["sync"] }
+tokio = { version = "1.0", features = ["sync"] }
itertools = "0.9"
rustc-hash = "1.1"
diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs
index e18bd6e0..c3e16e8b 100644
--- a/examples/game_of_life/src/main.rs
+++ b/examples/game_of_life/src/main.rs
@@ -6,12 +6,14 @@ mod style;
use grid::Grid;
use iced::button::{self, Button};
use iced::executor;
+use iced::menu::{self, Menu};
use iced::pick_list::{self, PickList};
use iced::slider::{self, Slider};
use iced::time;
+use iced::window;
use iced::{
- Align, Application, Checkbox, Column, Command, Container, Element, Length,
- Row, Settings, Subscription, Text,
+ Align, Application, Checkbox, Clipboard, Column, Command, Container,
+ Element, Length, Row, Settings, Subscription, Text,
};
use preset::Preset;
use std::time::{Duration, Instant};
@@ -19,6 +21,10 @@ use std::time::{Duration, Instant};
pub fn main() -> iced::Result {
GameOfLife::run(Settings {
antialiasing: true,
+ window: window::Settings {
+ position: window::Position::Centered,
+ ..window::Settings::default()
+ },
..Settings::default()
})
}
@@ -65,7 +71,11 @@ impl Application for GameOfLife {
String::from("Game of Life - Iced")
}
- fn update(&mut self, message: Message) -> Command<Message> {
+ fn update(
+ &mut self,
+ message: Message,
+ _clipboard: &mut Clipboard,
+ ) -> Command<Message> {
match message {
Message::Grid(message, version) => {
if version == self.version {
@@ -124,6 +134,13 @@ impl Application for GameOfLife {
}
}
+ fn menu(&self) -> Menu<Message> {
+ Menu::with_entries(vec![menu::Entry::dropdown(
+ "Presets",
+ Preset::menu().map(Message::PresetPicked),
+ )])
+ }
+
fn view(&mut self) -> Element<Message> {
let version = self.version;
let selected_speed = self.next_speed.unwrap_or(self.speed);
diff --git a/examples/game_of_life/src/preset.rs b/examples/game_of_life/src/preset.rs
index 05157b6a..1c199a72 100644
--- a/examples/game_of_life/src/preset.rs
+++ b/examples/game_of_life/src/preset.rs
@@ -1,3 +1,5 @@
+use iced::menu::{self, Menu};
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Preset {
Custom,
@@ -26,6 +28,17 @@ pub static ALL: &[Preset] = &[
];
impl Preset {
+ pub fn menu() -> Menu<Self> {
+ Menu::with_entries(
+ ALL.iter()
+ .copied()
+ .map(|preset| {
+ menu::Entry::item(preset.to_string(), None, preset)
+ })
+ .collect(),
+ )
+ }
+
pub fn life(self) -> Vec<(isize, isize)> {
#[rustfmt::skip]
let cells = match self {
diff --git a/examples/game_of_life/src/style.rs b/examples/game_of_life/src/style.rs
index 6605826f..be9a0e96 100644
--- a/examples/game_of_life/src/style.rs
+++ b/examples/game_of_life/src/style.rs
@@ -171,6 +171,7 @@ impl pick_list::StyleSheet for PickList {
},
border_radius: 2.0,
icon_size: 0.5,
+ ..pick_list::Style::default()
}
}
diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs
index 824f9f53..36ee9b7e 100644
--- a/examples/integration/src/controls.rs
+++ b/examples/integration/src/controls.rs
@@ -1,7 +1,7 @@
use iced_wgpu::Renderer;
use iced_winit::{
- slider, Align, Color, Column, Command, Element, Length, Program, Row,
- Slider, Text,
+ slider, Align, Clipboard, Color, Column, Command, Element, Length, Program,
+ Row, Slider, Text,
};
pub struct Controls {
@@ -30,8 +30,13 @@ impl Controls {
impl Program for Controls {
type Renderer = Renderer;
type Message = Message;
+ type Clipboard = Clipboard;
- fn update(&mut self, message: Message) -> Command<Message> {
+ fn update(
+ &mut self,
+ message: Message,
+ _clipboard: &mut Clipboard,
+ ) -> Command<Message> {
match message {
Message::BackgroundColorChanged(color) => {
self.background_color = color;
diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs
index 88d8d023..ab0e2299 100644
--- a/examples/integration/src/main.rs
+++ b/examples/integration/src/main.rs
@@ -5,7 +5,7 @@ use controls::Controls;
use scene::Scene;
use iced_wgpu::{wgpu, Backend, Renderer, Settings, Viewport};
-use iced_winit::{conversion, futures, program, winit, Debug, Size};
+use iced_winit::{conversion, futures, program, winit, Clipboard, Debug, Size};
use futures::task::SpawnExt;
use winit::{
@@ -28,6 +28,7 @@ pub fn main() {
);
let mut cursor_position = PhysicalPosition::new(-1.0, -1.0);
let mut modifiers = ModifiersState::default();
+ let mut clipboard = Clipboard::connect(&window);
// Initialize wgpu
let instance = wgpu::Instance::new(wgpu::BackendBit::PRIMARY);
@@ -36,7 +37,7 @@ pub fn main() {
let (mut device, queue) = futures::executor::block_on(async {
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
- power_preference: wgpu::PowerPreference::Default,
+ power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: Some(&surface),
})
.await
@@ -45,9 +46,9 @@ pub fn main() {
adapter
.request_device(
&wgpu::DeviceDescriptor {
+ label: None,
features: wgpu::Features::empty(),
limits: wgpu::Limits::default(),
- shader_validation: false,
},
None,
)
@@ -63,7 +64,7 @@ pub fn main() {
device.create_swap_chain(
&surface,
&wgpu::SwapChainDescriptor {
- usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT,
+ usage: wgpu::TextureUsage::RENDER_ATTACHMENT,
format: format,
width: size.width,
height: size.height,
@@ -141,8 +142,8 @@ pub fn main() {
cursor_position,
viewport.scale_factor(),
),
- None,
&mut renderer,
+ &mut clipboard,
&mut debug,
);
@@ -157,7 +158,7 @@ pub fn main() {
swap_chain = device.create_swap_chain(
&surface,
&wgpu::SwapChainDescriptor {
- usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT,
+ usage: wgpu::TextureUsage::RENDER_ATTACHMENT,
format: format,
width: size.width,
height: size.height,
diff --git a/examples/integration/src/scene.rs b/examples/integration/src/scene.rs
index 03a338c6..3e8277c8 100644
--- a/examples/integration/src/scene.rs
+++ b/examples/integration/src/scene.rs
@@ -19,8 +19,9 @@ impl Scene {
background_color: Color,
) -> wgpu::RenderPass<'a> {
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
- color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor {
- attachment: target,
+ label: None,
+ color_attachments: &[wgpu::RenderPassColorAttachment {
+ view: target,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear({
@@ -48,10 +49,10 @@ impl Scene {
fn build_pipeline(device: &wgpu::Device) -> wgpu::RenderPipeline {
let vs_module =
- device.create_shader_module(wgpu::include_spirv!("shader/vert.spv"));
+ device.create_shader_module(&wgpu::include_spirv!("shader/vert.spv"));
let fs_module =
- device.create_shader_module(wgpu::include_spirv!("shader/frag.spv"));
+ device.create_shader_module(&wgpu::include_spirv!("shader/frag.spv"));
let pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
@@ -64,34 +65,34 @@ fn build_pipeline(device: &wgpu::Device) -> wgpu::RenderPipeline {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(&pipeline_layout),
- vertex_stage: wgpu::ProgrammableStageDescriptor {
+ vertex: wgpu::VertexState {
module: &vs_module,
entry_point: "main",
+ buffers: &[],
},
- fragment_stage: Some(wgpu::ProgrammableStageDescriptor {
+ fragment: Some(wgpu::FragmentState {
module: &fs_module,
entry_point: "main",
+ targets: &[wgpu::ColorTargetState {
+ format: wgpu::TextureFormat::Bgra8UnormSrgb,
+ blend: Some(wgpu::BlendState {
+ color: wgpu::BlendComponent::REPLACE,
+ alpha: wgpu::BlendComponent::REPLACE,
+ }),
+ write_mask: wgpu::ColorWrite::ALL,
+ }],
}),
- rasterization_state: Some(wgpu::RasterizationStateDescriptor {
+ primitive: wgpu::PrimitiveState {
+ topology: wgpu::PrimitiveTopology::TriangleList,
front_face: wgpu::FrontFace::Ccw,
- cull_mode: wgpu::CullMode::None,
..Default::default()
- }),
- primitive_topology: wgpu::PrimitiveTopology::TriangleList,
- color_states: &[wgpu::ColorStateDescriptor {
- format: wgpu::TextureFormat::Bgra8UnormSrgb,
- color_blend: wgpu::BlendDescriptor::REPLACE,
- alpha_blend: wgpu::BlendDescriptor::REPLACE,
- write_mask: wgpu::ColorWrite::ALL,
- }],
- depth_stencil_state: None,
- vertex_state: wgpu::VertexStateDescriptor {
- index_format: wgpu::IndexFormat::Uint16,
- vertex_buffers: &[],
},
- sample_count: 1,
- sample_mask: !0,
- alpha_to_coverage_enabled: false,
+ depth_stencil: None,
+ multisample: wgpu::MultisampleState {
+ count: 1,
+ mask: !0,
+ alpha_to_coverage_enabled: false,
+ },
});
pipeline
diff --git a/examples/menu/Cargo.toml b/examples/menu/Cargo.toml
new file mode 100644
index 00000000..44597734
--- /dev/null
+++ b/examples/menu/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "menu"
+version = "0.1.0"
+authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
+edition = "2018"
+publish = false
+
+[dependencies]
+iced = { path = "../.." }
+iced_native = { path = "../../native" } \ No newline at end of file
diff --git a/examples/menu/src/main.rs b/examples/menu/src/main.rs
new file mode 100644
index 00000000..7403713c
--- /dev/null
+++ b/examples/menu/src/main.rs
@@ -0,0 +1,117 @@
+use iced::menu::{self, Menu};
+use iced::{
+ executor, Application, Clipboard, Command, Container, Element, Length,
+ Settings, Text,
+};
+use iced_native::keyboard::{Hotkey, KeyCode, Modifiers};
+
+pub fn main() -> iced::Result {
+ App::run(Settings::default())
+}
+
+#[derive(Debug, Default)]
+struct App {
+ selected: Option<Entry>,
+}
+
+#[derive(Debug, Clone)]
+enum Entry {
+ One,
+ Two,
+ Three,
+ A,
+ B,
+ C,
+}
+
+#[derive(Debug, Clone)]
+enum Message {
+ MenuActivated(Entry),
+}
+
+impl Application for App {
+ type Executor = executor::Default;
+ type Message = Message;
+ type Flags = ();
+
+ fn new(_flags: ()) -> (App, Command<Message>) {
+ (App::default(), Command::none())
+ }
+
+ fn title(&self) -> String {
+ String::from("Menu - Iced")
+ }
+
+ fn menu(&self) -> Menu<Message> {
+ let alt = Modifiers::ALT;
+ let ctrl_shift = Modifiers::CTRL | Modifiers::SHIFT;
+
+ Menu::with_entries(vec![
+ menu::Entry::dropdown(
+ "First",
+ Menu::with_entries(vec![
+ menu::Entry::item(
+ "One",
+ Hotkey::new(alt, KeyCode::F1),
+ Message::MenuActivated(Entry::One),
+ ),
+ menu::Entry::item(
+ "Two",
+ Hotkey::new(alt, KeyCode::F2),
+ Message::MenuActivated(Entry::Two),
+ ),
+ menu::Entry::Separator,
+ menu::Entry::item(
+ "Three",
+ Hotkey::new(alt, KeyCode::F3),
+ Message::MenuActivated(Entry::Three),
+ ),
+ ]),
+ ),
+ menu::Entry::dropdown(
+ "Second",
+ Menu::with_entries(vec![
+ menu::Entry::item(
+ "A",
+ Hotkey::new(ctrl_shift, KeyCode::A),
+ Message::MenuActivated(Entry::A),
+ ),
+ menu::Entry::item(
+ "B",
+ Hotkey::new(ctrl_shift, KeyCode::B),
+ Message::MenuActivated(Entry::B),
+ ),
+ menu::Entry::Separator,
+ menu::Entry::item(
+ "C",
+ Hotkey::new(ctrl_shift, KeyCode::C),
+ Message::MenuActivated(Entry::C),
+ ),
+ ]),
+ ),
+ ])
+ }
+
+ fn update(
+ &mut self,
+ message: Message,
+ _clipboard: &mut Clipboard,
+ ) -> Command<Message> {
+ match message {
+ Message::MenuActivated(entry) => self.selected = Some(entry),
+ }
+
+ Command::none()
+ }
+
+ fn view(&mut self) -> Element<Message> {
+ Container::new(
+ Text::new(format!("Selected {:?}", self.selected)).size(48),
+ )
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .center_x()
+ .center_y()
+ .into()
+ }
+}
diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs
index 3c3256cf..3bd8aa25 100644
--- a/examples/pane_grid/src/main.rs
+++ b/examples/pane_grid/src/main.rs
@@ -1,7 +1,8 @@
use iced::{
button, executor, keyboard, pane_grid, scrollable, Align, Application,
- Button, Column, Command, Container, Element, HorizontalAlignment, Length,
- PaneGrid, Scrollable, Settings, Subscription, Text,
+ Button, Clipboard, Color, Column, Command, Container, Element,
+ HorizontalAlignment, Length, PaneGrid, Row, Scrollable, Settings,
+ Subscription, Text,
};
use iced_native::{event, subscription, Event};
@@ -10,7 +11,7 @@ pub fn main() -> iced::Result {
}
struct Example {
- panes: pane_grid::State<Content>,
+ panes: pane_grid::State<Pane>,
panes_created: usize,
focus: Option<pane_grid::Pane>,
}
@@ -23,6 +24,7 @@ enum Message {
Clicked(pane_grid::Pane),
Dragged(pane_grid::DragEvent),
Resized(pane_grid::ResizeEvent),
+ TogglePin(pane_grid::Pane),
Close(pane_grid::Pane),
CloseFocused,
}
@@ -33,7 +35,7 @@ impl Application for Example {
type Flags = ();
fn new(_flags: ()) -> (Self, Command<Message>) {
- let (panes, _) = pane_grid::State::new(Content::new(0));
+ let (panes, _) = pane_grid::State::new(Pane::new(0));
(
Example {
@@ -49,13 +51,17 @@ impl Application for Example {
String::from("Pane grid - Iced")
}
- fn update(&mut self, message: Message) -> Command<Message> {
+ fn update(
+ &mut self,
+ message: Message,
+ _clipboard: &mut Clipboard,
+ ) -> Command<Message> {
match message {
Message::Split(axis, pane) => {
let result = self.panes.split(
axis,
&pane,
- Content::new(self.panes_created),
+ Pane::new(self.panes_created),
);
if let Some((pane, _)) = result {
@@ -69,7 +75,7 @@ impl Application for Example {
let result = self.panes.split(
axis,
&pane,
- Content::new(self.panes_created),
+ Pane::new(self.panes_created),
);
if let Some((pane, _)) = result {
@@ -101,6 +107,12 @@ impl Application for Example {
self.panes.swap(&pane, &target);
}
Message::Dragged(_) => {}
+ Message::TogglePin(pane) => {
+ if let Some(Pane { is_pinned, .. }) = self.panes.get_mut(&pane)
+ {
+ *is_pinned = !*is_pinned;
+ }
+ }
Message::Close(pane) => {
if let Some((_, sibling)) = self.panes.close(&pane) {
self.focus = Some(sibling);
@@ -108,8 +120,14 @@ impl Application for Example {
}
Message::CloseFocused => {
if let Some(pane) = self.focus {
- if let Some((_, sibling)) = self.panes.close(&pane) {
- self.focus = Some(sibling);
+ if let Some(Pane { is_pinned, .. }) = self.panes.get(&pane)
+ {
+ if !is_pinned {
+ if let Some((_, sibling)) = self.panes.close(&pane)
+ {
+ self.focus = Some(sibling);
+ }
+ }
}
}
}
@@ -128,7 +146,7 @@ impl Application for Example {
Event::Keyboard(keyboard::Event::KeyPressed {
modifiers,
key_code,
- }) if modifiers.is_command_pressed() => handle_hotkey(key_code),
+ }) if modifiers.command() => handle_hotkey(key_code),
_ => None,
}
})
@@ -138,17 +156,41 @@ impl Application for Example {
let focus = self.focus;
let total_panes = self.panes.len();
- let pane_grid = PaneGrid::new(&mut self.panes, |pane, content| {
- let is_focused = focus == Some(pane);
-
- let title_bar =
- pane_grid::TitleBar::new(format!("Pane {}", content.id))
- .padding(10)
- .style(style::TitleBar { is_focused });
-
- pane_grid::Content::new(content.view(pane, total_panes))
- .title_bar(title_bar)
- .style(style::Pane { is_focused })
+ let pane_grid = PaneGrid::new(&mut self.panes, |id, pane| {
+ let is_focused = focus == Some(id);
+
+ let text = if pane.is_pinned { "Unpin" } else { "Pin" };
+ let pin_button =
+ Button::new(&mut pane.pin_button, Text::new(text).size(14))
+ .on_press(Message::TogglePin(id))
+ .style(style::Button::Pin)
+ .padding(3);
+
+ let title = Row::with_children(vec![
+ pin_button.into(),
+ Text::new("Pane").into(),
+ Text::new(pane.content.id.to_string())
+ .color(if is_focused {
+ PANE_ID_COLOR_FOCUSED
+ } else {
+ PANE_ID_COLOR_UNFOCUSED
+ })
+ .into(),
+ ])
+ .spacing(5);
+
+ let title_bar = pane_grid::TitleBar::new(title)
+ .controls(pane.controls.view(id, total_panes, pane.is_pinned))
+ .padding(10)
+ .style(style::TitleBar { is_focused });
+
+ pane_grid::Content::new(pane.content.view(
+ id,
+ total_panes,
+ pane.is_pinned,
+ ))
+ .title_bar(title_bar)
+ .style(style::Pane { is_focused })
})
.width(Length::Fill)
.height(Length::Fill)
@@ -165,6 +207,17 @@ impl Application for Example {
}
}
+const PANE_ID_COLOR_UNFOCUSED: Color = Color::from_rgb(
+ 0xFF as f32 / 255.0,
+ 0xC7 as f32 / 255.0,
+ 0xC7 as f32 / 255.0,
+);
+const PANE_ID_COLOR_FOCUSED: Color = Color::from_rgb(
+ 0xFF as f32 / 255.0,
+ 0x47 as f32 / 255.0,
+ 0x47 as f32 / 255.0,
+);
+
fn handle_hotkey(key_code: keyboard::KeyCode) -> Option<Message> {
use keyboard::KeyCode;
use pane_grid::{Axis, Direction};
@@ -185,6 +238,13 @@ fn handle_hotkey(key_code: keyboard::KeyCode) -> Option<Message> {
}
}
+struct Pane {
+ pub is_pinned: bool,
+ pub pin_button: button::State,
+ pub content: Content,
+ pub controls: Controls,
+}
+
struct Content {
id: usize,
scroll: scrollable::State,
@@ -193,6 +253,21 @@ struct Content {
close: button::State,
}
+struct Controls {
+ close: button::State,
+}
+
+impl Pane {
+ fn new(id: usize) -> Self {
+ Self {
+ is_pinned: false,
+ pin_button: button::State::new(),
+ content: Content::new(id),
+ controls: Controls::new(),
+ }
+ }
+}
+
impl Content {
fn new(id: usize) -> Self {
Content {
@@ -207,6 +282,7 @@ impl Content {
&mut self,
pane: pane_grid::Pane,
total_panes: usize,
+ is_pinned: bool,
) -> Element<Message> {
let Content {
scroll,
@@ -246,7 +322,7 @@ impl Content {
style::Button::Primary,
));
- if total_panes > 1 {
+ if total_panes > 1 && !is_pinned {
controls = controls.push(button(
close,
"Close",
@@ -270,7 +346,32 @@ impl Content {
}
}
+impl Controls {
+ fn new() -> Self {
+ Self {
+ close: button::State::new(),
+ }
+ }
+
+ pub fn view(
+ &mut self,
+ pane: pane_grid::Pane,
+ total_panes: usize,
+ is_pinned: bool,
+ ) -> Element<Message> {
+ let mut button =
+ Button::new(&mut self.close, Text::new("Close").size(14))
+ .style(style::Button::Control)
+ .padding(3);
+ if total_panes > 1 && !is_pinned {
+ button = button.on_press(Message::Close(pane));
+ }
+ button.into()
+ }
+}
+
mod style {
+ use crate::PANE_ID_COLOR_FOCUSED;
use iced::{button, container, Background, Color, Vector};
const SURFACE: Color = Color::from_rgb(
@@ -332,6 +433,8 @@ mod style {
pub enum Button {
Primary,
Destructive,
+ Control,
+ Pin,
}
impl button::StyleSheet for Button {
@@ -341,6 +444,8 @@ mod style {
Button::Destructive => {
(None, Color::from_rgb8(0xFF, 0x47, 0x47))
}
+ Button::Control => (Some(PANE_ID_COLOR_FOCUSED), Color::WHITE),
+ Button::Pin => (Some(ACTIVE), Color::WHITE),
};
button::Style {
@@ -361,6 +466,8 @@ mod style {
a: 0.2,
..active.text_color
}),
+ Button::Control => Some(PANE_ID_COLOR_FOCUSED),
+ Button::Pin => Some(HOVERED),
};
button::Style {
diff --git a/examples/pick_list/src/main.rs b/examples/pick_list/src/main.rs
index 68662602..1eec9791 100644
--- a/examples/pick_list/src/main.rs
+++ b/examples/pick_list/src/main.rs
@@ -11,7 +11,7 @@ pub fn main() -> iced::Result {
struct Example {
scroll: scrollable::State,
pick_list: pick_list::State<Language>,
- selected_language: Language,
+ selected_language: Option<Language>,
}
#[derive(Debug, Clone, Copy)]
@@ -33,7 +33,7 @@ impl Sandbox for Example {
fn update(&mut self, message: Message) {
match message {
Message::LanguageSelected(language) => {
- self.selected_language = language;
+ self.selected_language = Some(language);
}
}
}
@@ -42,9 +42,10 @@ impl Sandbox for Example {
let pick_list = PickList::new(
&mut self.pick_list,
&Language::ALL[..],
- Some(self.selected_language),
+ self.selected_language,
Message::LanguageSelected,
- );
+ )
+ .placeholder("Choose a language...");
let mut content = Scrollable::new(&mut self.scroll)
.width(Length::Fill)
diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs
index 187e5dee..da1d5d5d 100644
--- a/examples/pokedex/src/main.rs
+++ b/examples/pokedex/src/main.rs
@@ -1,6 +1,6 @@
use iced::{
- button, futures, image, Align, Application, Button, Column, Command,
- Container, Element, Image, Length, Row, Settings, Text,
+ button, futures, image, Align, Application, Button, Clipboard, Column,
+ Command, Container, Element, Length, Row, Settings, Text,
};
pub fn main() -> iced::Result {
@@ -48,7 +48,11 @@ impl Application for Pokedex {
format!("{} - Pokédex", subtitle)
}
- fn update(&mut self, message: Message) -> Command<Message> {
+ fn update(
+ &mut self,
+ message: Message,
+ _clipboard: &mut Clipboard,
+ ) -> Command<Message> {
match message {
Message::PokemonFound(Ok(pokemon)) => {
*self = Pokedex::Loaded {
@@ -112,16 +116,20 @@ struct Pokemon {
name: String,
description: String,
image: image::Handle,
+ image_viewer: image::viewer::State,
}
impl Pokemon {
const TOTAL: u16 = 807;
- fn view(&self) -> Element<Message> {
+ fn view(&mut self) -> Element<Message> {
Row::new()
.spacing(20)
.align_items(Align::Center)
- .push(Image::new(self.image.clone()))
+ .push(image::Viewer::new(
+ &mut self.image_viewer,
+ self.image.clone(),
+ ))
.push(
Column::new()
.spacing(20)
@@ -200,11 +208,15 @@ impl Pokemon {
.map(|c| if c.is_control() { ' ' } else { c })
.collect(),
image,
+ image_viewer: image::viewer::State::new(),
})
}
async fn fetch_image(id: u16) -> Result<image::Handle, reqwest::Error> {
- let url = format!("https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/{}.png", id);
+ let url = format!(
+ "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/{}.png",
+ id
+ );
#[cfg(not(target_arch = "wasm32"))]
{
diff --git a/examples/scrollable/Cargo.toml b/examples/scrollable/Cargo.toml
index 12753fb6..08502458 100644
--- a/examples/scrollable/Cargo.toml
+++ b/examples/scrollable/Cargo.toml
@@ -6,4 +6,4 @@ edition = "2018"
publish = false
[dependencies]
-iced = { path = "../.." }
+iced = { path = "../..", features = ["debug"] }
diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs
index 8dd2e20c..3416b83d 100644
--- a/examples/scrollable/src/main.rs
+++ b/examples/scrollable/src/main.rs
@@ -1,8 +1,8 @@
mod style;
use iced::{
- scrollable, Column, Container, Element, Length, Radio, Row, Rule, Sandbox,
- Scrollable, Settings, Space, Text,
+ button, scrollable, Button, Column, Container, Element, Length,
+ ProgressBar, Radio, Row, Rule, Sandbox, Scrollable, Settings, Space, Text,
};
pub fn main() -> iced::Result {
@@ -17,6 +17,9 @@ struct ScrollableDemo {
#[derive(Debug, Clone)]
enum Message {
ThemeChanged(style::Theme),
+ ScrollToTop(usize),
+ ScrollToBottom(usize),
+ Scrolled(usize, f32),
}
impl Sandbox for ScrollableDemo {
@@ -36,6 +39,25 @@ impl Sandbox for ScrollableDemo {
fn update(&mut self, message: Message) {
match message {
Message::ThemeChanged(theme) => self.theme = theme,
+ Message::ScrollToTop(i) => {
+ if let Some(variant) = self.variants.get_mut(i) {
+ variant.scrollable.snap_to(0.0);
+
+ variant.latest_offset = 0.0;
+ }
+ }
+ Message::ScrollToBottom(i) => {
+ if let Some(variant) = self.variants.get_mut(i) {
+ variant.scrollable.snap_to(1.0);
+
+ variant.latest_offset = 1.0;
+ }
+ }
+ Message::Scrolled(i, offset) => {
+ if let Some(variant) = self.variants.get_mut(i) {
+ variant.latest_offset = offset;
+ }
+ }
}
}
@@ -62,13 +84,28 @@ impl Sandbox for ScrollableDemo {
let scrollable_row = Row::with_children(
variants
.iter_mut()
- .map(|variant| {
- let mut scrollable = Scrollable::new(&mut variant.state)
- .padding(10)
- .width(Length::Fill)
- .height(Length::Fill)
- .style(*theme)
- .push(Text::new(variant.title));
+ .enumerate()
+ .map(|(i, variant)| {
+ let mut scrollable =
+ Scrollable::new(&mut variant.scrollable)
+ .padding(10)
+ .spacing(10)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .on_scroll(move |offset| {
+ Message::Scrolled(i, offset)
+ })
+ .style(*theme)
+ .push(Text::new(variant.title))
+ .push(
+ Button::new(
+ &mut variant.scroll_to_bottom,
+ Text::new("Scroll to bottom"),
+ )
+ .width(Length::Fill)
+ .padding(10)
+ .on_press(Message::ScrollToBottom(i)),
+ );
if let Some(scrollbar_width) = variant.scrollbar_width {
scrollable = scrollable
@@ -108,12 +145,31 @@ impl Sandbox for ScrollableDemo {
.push(Space::with_height(Length::Units(1200)))
.push(Text::new("Middle"))
.push(Space::with_height(Length::Units(1200)))
- .push(Text::new("The End."));
-
- Container::new(scrollable)
+ .push(Text::new("The End."))
+ .push(
+ Button::new(
+ &mut variant.scroll_to_top,
+ Text::new("Scroll to top"),
+ )
+ .width(Length::Fill)
+ .padding(10)
+ .on_press(Message::ScrollToTop(i)),
+ );
+
+ Column::new()
.width(Length::Fill)
.height(Length::Fill)
- .style(*theme)
+ .spacing(10)
+ .push(
+ Container::new(scrollable)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .style(*theme),
+ )
+ .push(ProgressBar::new(
+ 0.0..=1.0,
+ variant.latest_offset,
+ ))
.into()
})
.collect(),
@@ -142,10 +198,13 @@ impl Sandbox for ScrollableDemo {
/// A version of a scrollable
struct Variant {
title: &'static str,
- state: scrollable::State,
+ scrollable: scrollable::State,
+ scroll_to_top: button::State,
+ scroll_to_bottom: button::State,
scrollbar_width: Option<u16>,
scrollbar_margin: Option<u16>,
scroller_width: Option<u16>,
+ latest_offset: f32,
}
impl Variant {
@@ -153,31 +212,43 @@ impl Variant {
vec![
Self {
title: "Default Scrollbar",
- state: scrollable::State::new(),
+ scrollable: scrollable::State::new(),
+ scroll_to_top: button::State::new(),
+ scroll_to_bottom: button::State::new(),
scrollbar_width: None,
scrollbar_margin: None,
scroller_width: None,
+ latest_offset: 0.0,
},
Self {
title: "Slimmed & Margin",
- state: scrollable::State::new(),
+ scrollable: scrollable::State::new(),
+ scroll_to_top: button::State::new(),
+ scroll_to_bottom: button::State::new(),
scrollbar_width: Some(4),
scrollbar_margin: Some(3),
scroller_width: Some(4),
+ latest_offset: 0.0,
},
Self {
title: "Wide Scroller",
- state: scrollable::State::new(),
+ scrollable: scrollable::State::new(),
+ scroll_to_top: button::State::new(),
+ scroll_to_bottom: button::State::new(),
scrollbar_width: Some(4),
scrollbar_margin: None,
scroller_width: Some(10),
+ latest_offset: 0.0,
},
Self {
title: "Narrow Scroller",
- state: scrollable::State::new(),
+ scrollable: scrollable::State::new(),
+ scroll_to_top: button::State::new(),
+ scroll_to_bottom: button::State::new(),
scrollbar_width: Some(10),
scrollbar_margin: None,
scroller_width: Some(4),
+ latest_offset: 0.0,
},
]
}
diff --git a/examples/solar_system/Cargo.toml b/examples/solar_system/Cargo.toml
index 44ced729..327fe0aa 100644
--- a/examples/solar_system/Cargo.toml
+++ b/examples/solar_system/Cargo.toml
@@ -7,4 +7,4 @@ publish = false
[dependencies]
iced = { path = "../..", features = ["canvas", "tokio", "debug"] }
-rand = "0.7"
+rand = "0.8.3"
diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs
index 6a2de736..8f844828 100644
--- a/examples/solar_system/src/main.rs
+++ b/examples/solar_system/src/main.rs
@@ -8,8 +8,8 @@
//! [1]: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Basic_animations#An_animated_solar_system
use iced::{
canvas::{self, Cursor, Path, Stroke},
- executor, time, window, Application, Canvas, Color, Command, Element,
- Length, Point, Rectangle, Settings, Size, Subscription, Vector,
+ executor, time, window, Application, Canvas, Clipboard, Color, Command,
+ Element, Length, Point, Rectangle, Settings, Size, Subscription, Vector,
};
use std::time::Instant;
@@ -48,7 +48,11 @@ impl Application for SolarSystem {
String::from("Solar system - Iced")
}
- fn update(&mut self, message: Message) -> Command<Message> {
+ fn update(
+ &mut self,
+ message: Message,
+ _clipboard: &mut Clipboard,
+ ) -> Command<Message> {
match message {
Message::Tick(instant) => {
self.state.update(instant);
@@ -117,15 +121,13 @@ impl State {
(
Point::new(
rng.gen_range(
- -(width as f32) / 2.0,
- width as f32 / 2.0,
+ (-(width as f32) / 2.0)..(width as f32 / 2.0),
),
rng.gen_range(
- -(height as f32) / 2.0,
- height as f32 / 2.0,
+ (-(height as f32) / 2.0)..(height as f32 / 2.0),
),
),
- rng.gen_range(0.5, 1.0),
+ rng.gen_range(0.5..1.0),
)
})
.collect()
diff --git a/examples/stopwatch/Cargo.toml b/examples/stopwatch/Cargo.toml
index 075aa111..9f935951 100644
--- a/examples/stopwatch/Cargo.toml
+++ b/examples/stopwatch/Cargo.toml
@@ -6,4 +6,4 @@ edition = "2018"
publish = false
[dependencies]
-iced = { path = "../..", features = ["tokio"] }
+iced = { path = "../..", features = ["smol"] }
diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs
index 983cf3e6..51972e01 100644
--- a/examples/stopwatch/src/main.rs
+++ b/examples/stopwatch/src/main.rs
@@ -1,6 +1,6 @@
use iced::{
- button, executor, time, Align, Application, Button, Column, Command,
- Container, Element, HorizontalAlignment, Length, Row, Settings,
+ button, executor, time, Align, Application, Button, Clipboard, Column,
+ Command, Container, Element, HorizontalAlignment, Length, Row, Settings,
Subscription, Text,
};
use std::time::{Duration, Instant};
@@ -49,7 +49,11 @@ impl Application for Stopwatch {
String::from("Stopwatch - Iced")
}
- fn update(&mut self, message: Message) -> Command<Message> {
+ fn update(
+ &mut self,
+ message: Message,
+ _clipboard: &mut Clipboard,
+ ) -> Command<Message> {
match message {
Message::Toggle => match self.state {
State::Idle => {
diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs
index 8975fd9a..7bc49281 100644
--- a/examples/styling/src/main.rs
+++ b/examples/styling/src/main.rs
@@ -1,7 +1,7 @@
use iced::{
button, scrollable, slider, text_input, Align, Button, Checkbox, Column,
Container, Element, Length, ProgressBar, Radio, Row, Rule, Sandbox,
- Scrollable, Settings, Slider, Space, Text, TextInput,
+ Scrollable, Settings, Slider, Space, Text, TextInput, Toggler,
};
pub fn main() -> iced::Result {
@@ -17,7 +17,8 @@ struct Styling {
button: button::State,
slider: slider::State,
slider_value: f32,
- toggle_value: bool,
+ checkbox_value: bool,
+ toggler_value: bool,
}
#[derive(Debug, Clone)]
@@ -27,6 +28,7 @@ enum Message {
ButtonPressed,
SliderChanged(f32),
CheckboxToggled(bool),
+ TogglerToggled(bool),
}
impl Sandbox for Styling {
@@ -44,9 +46,10 @@ impl Sandbox for Styling {
match message {
Message::ThemeChanged(theme) => self.theme = theme,
Message::InputChanged(value) => self.input_value = value,
- Message::ButtonPressed => (),
+ Message::ButtonPressed => {}
Message::SliderChanged(value) => self.slider_value = value,
- Message::CheckboxToggled(value) => self.toggle_value = value,
+ Message::CheckboxToggled(value) => self.checkbox_value = value,
+ Message::TogglerToggled(value) => self.toggler_value = value,
}
}
@@ -101,11 +104,19 @@ impl Sandbox for Styling {
.push(Text::new("You did it!"));
let checkbox = Checkbox::new(
- self.toggle_value,
- "Toggle me!",
+ self.checkbox_value,
+ "Check me!",
Message::CheckboxToggled,
)
- .width(Length::Fill)
+ .style(self.theme);
+
+ let toggler = Toggler::new(
+ self.toggler_value,
+ String::from("Toggle me!"),
+ Message::TogglerToggled,
+ )
+ .width(Length::Shrink)
+ .spacing(10)
.style(self.theme);
let content = Column::new()
@@ -124,7 +135,13 @@ impl Sandbox for Styling {
.align_items(Align::Center)
.push(scrollable)
.push(Rule::vertical(38).style(self.theme))
- .push(checkbox),
+ .push(
+ Column::new()
+ .width(Length::Shrink)
+ .spacing(20)
+ .push(checkbox)
+ .push(toggler),
+ ),
);
Container::new(content)
@@ -140,7 +157,7 @@ impl Sandbox for Styling {
mod style {
use iced::{
button, checkbox, container, progress_bar, radio, rule, scrollable,
- slider, text_input,
+ slider, text_input, toggler,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -231,6 +248,15 @@ mod style {
}
}
+ impl From<Theme> for Box<dyn toggler::StyleSheet> {
+ fn from(theme: Theme) -> Self {
+ match theme {
+ Theme::Light => Default::default(),
+ Theme::Dark => dark::Toggler.into(),
+ }
+ }
+ }
+
impl From<Theme> for Box<dyn rule::StyleSheet> {
fn from(theme: Theme) -> Self {
match theme {
@@ -269,7 +295,7 @@ mod style {
mod dark {
use iced::{
button, checkbox, container, progress_bar, radio, rule, scrollable,
- slider, text_input, Color,
+ slider, text_input, toggler, Color,
};
const SURFACE: Color = Color::from_rgb(
@@ -520,6 +546,35 @@ mod style {
}
}
+ pub struct Toggler;
+
+ impl toggler::StyleSheet for Toggler {
+ fn active(&self, is_active: bool) -> toggler::Style {
+ toggler::Style {
+ background: if is_active { ACTIVE } else { SURFACE },
+ background_border: None,
+ foreground: if is_active { Color::WHITE } else { ACTIVE },
+ foreground_border: None,
+ }
+ }
+
+ fn hovered(&self, is_active: bool) -> toggler::Style {
+ toggler::Style {
+ background: if is_active { ACTIVE } else { SURFACE },
+ background_border: None,
+ foreground: if is_active {
+ Color {
+ a: 0.5,
+ ..Color::WHITE
+ }
+ } else {
+ Color { a: 0.5, ..ACTIVE }
+ },
+ foreground_border: None,
+ }
+ }
+ }
+
pub struct Rule;
impl rule::StyleSheet for Rule {
diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs
index ccee2703..97415475 100644
--- a/examples/todos/src/main.rs
+++ b/examples/todos/src/main.rs
@@ -1,7 +1,7 @@
use iced::{
button, scrollable, text_input, Align, Application, Button, Checkbox,
- Column, Command, Container, Element, Font, HorizontalAlignment, Length,
- Row, Scrollable, Settings, Text, TextInput,
+ Clipboard, Column, Command, Container, Element, Font, HorizontalAlignment,
+ Length, Row, Scrollable, Settings, Text, TextInput,
};
use serde::{Deserialize, Serialize};
@@ -58,7 +58,11 @@ impl Application for Todos {
format!("Todos{} - Iced", if dirty { "*" } else { "" })
}
- fn update(&mut self, message: Message) -> Command<Message> {
+ fn update(
+ &mut self,
+ message: Message,
+ _clipboard: &mut Clipboard,
+ ) -> Command<Message> {
match self {
Todos::Loading => {
match message {
@@ -261,8 +265,11 @@ impl Task {
self.completed = completed;
}
TaskMessage::Edit => {
+ let mut text_input = text_input::State::focused();
+ text_input.select_all();
+
self.state = TaskState::Editing {
- text_input: text_input::State::focused(),
+ text_input,
delete_button: button::State::new(),
};
}
@@ -489,7 +496,6 @@ enum LoadError {
#[derive(Debug, Clone)]
enum SaveError {
- DirectoryError,
FileError,
WriteError,
FormatError,
@@ -538,7 +544,7 @@ impl SavedState {
if let Some(dir) = path.parent() {
async_std::fs::create_dir_all(dir)
.await
- .map_err(|_| SaveError::DirectoryError)?;
+ .map_err(|_| SaveError::FileError)?;
}
{
diff --git a/examples/tooltip/Cargo.toml b/examples/tooltip/Cargo.toml
new file mode 100644
index 00000000..1171de00
--- /dev/null
+++ b/examples/tooltip/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "tooltip"
+version = "0.1.0"
+authors = ["Yusuf Bera Ertan <y.bera003.06@protonmail.com>"]
+edition = "2018"
+publish = false
+
+[dependencies]
+iced = { path = "../..", features = ["debug"] }
diff --git a/examples/tooltip/README.md b/examples/tooltip/README.md
new file mode 100644
index 00000000..4ccf6578
--- /dev/null
+++ b/examples/tooltip/README.md
@@ -0,0 +1,14 @@
+## Tooltip
+
+A tooltip.
+
+It displays and positions a widget on another based on cursor position.
+
+The __[`main`]__ file contains all the code of the example.
+
+You can run it with `cargo run`:
+```
+cargo run --package tooltip
+```
+
+[`main`]: src/main.rs
diff --git a/examples/tooltip/src/main.rs b/examples/tooltip/src/main.rs
new file mode 100644
index 00000000..d6c8b8e1
--- /dev/null
+++ b/examples/tooltip/src/main.rs
@@ -0,0 +1,138 @@
+use iced::tooltip::{self, Tooltip};
+use iced::{
+ button, Button, Column, Container, Element, HorizontalAlignment, Length,
+ Row, Sandbox, Settings, Text, VerticalAlignment,
+};
+
+pub fn main() {
+ Example::run(Settings::default()).unwrap()
+}
+
+#[derive(Default)]
+struct Example {
+ top: button::State,
+ bottom: button::State,
+ right: button::State,
+ left: button::State,
+ follow_cursor: button::State,
+}
+
+#[derive(Debug, Clone, Copy)]
+struct Message;
+
+impl Sandbox for Example {
+ type Message = Message;
+
+ fn new() -> Self {
+ Self::default()
+ }
+
+ fn title(&self) -> String {
+ String::from("Tooltip - Iced")
+ }
+
+ fn update(&mut self, _message: Message) {}
+
+ fn view(&mut self) -> Element<Message> {
+ let top =
+ tooltip("Tooltip at top", &mut self.top, tooltip::Position::Top);
+
+ let bottom = tooltip(
+ "Tooltip at bottom",
+ &mut self.bottom,
+ tooltip::Position::Bottom,
+ );
+
+ let left =
+ tooltip("Tooltip at left", &mut self.left, tooltip::Position::Left);
+
+ let right = tooltip(
+ "Tooltip at right",
+ &mut self.right,
+ tooltip::Position::Right,
+ );
+
+ let fixed_tooltips = Row::with_children(vec![
+ top.into(),
+ bottom.into(),
+ left.into(),
+ right.into(),
+ ])
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .align_items(iced::Align::Center)
+ .spacing(50);
+
+ let follow_cursor = tooltip(
+ "Tooltip follows cursor",
+ &mut self.follow_cursor,
+ tooltip::Position::FollowCursor,
+ );
+
+ let content = Column::with_children(vec![
+ Container::new(fixed_tooltips)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .center_x()
+ .center_y()
+ .into(),
+ follow_cursor.into(),
+ ])
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .spacing(50);
+
+ Container::new(content)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .center_x()
+ .center_y()
+ .padding(50)
+ .into()
+ }
+}
+
+fn tooltip<'a>(
+ label: &str,
+ button_state: &'a mut button::State,
+ position: tooltip::Position,
+) -> Element<'a, Message> {
+ Tooltip::new(
+ Button::new(
+ button_state,
+ Text::new(label)
+ .size(40)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .horizontal_alignment(HorizontalAlignment::Center)
+ .vertical_alignment(VerticalAlignment::Center),
+ )
+ .on_press(Message)
+ .width(Length::Fill)
+ .height(Length::Fill),
+ "Tooltip",
+ position,
+ )
+ .gap(5)
+ .padding(10)
+ .style(style::Tooltip)
+ .into()
+}
+
+mod style {
+ use iced::container;
+ use iced::Color;
+
+ pub struct Tooltip;
+
+ impl container::StyleSheet for Tooltip {
+ fn style(&self) -> container::Style {
+ container::Style {
+ text_color: Some(Color::from_rgb8(0xEE, 0xEE, 0xEE)),
+ background: Some(Color::from_rgb(0.11, 0.42, 0.87).into()),
+ border_radius: 12.0,
+ ..container::Style::default()
+ }
+ }
+ }
+}
diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs
index e8755d39..1215f83d 100644
--- a/examples/tour/src/main.rs
+++ b/examples/tour/src/main.rs
@@ -1,7 +1,7 @@
use iced::{
button, scrollable, slider, text_input, Button, Checkbox, Color, Column,
Container, Element, HorizontalAlignment, Image, Length, Radio, Row,
- Sandbox, Scrollable, Settings, Slider, Space, Text, TextInput,
+ Sandbox, Scrollable, Settings, Slider, Space, Text, TextInput, Toggler,
};
pub fn main() -> iced::Result {
@@ -135,6 +135,9 @@ impl Steps {
color: Color::BLACK,
},
Step::Radio { selection: None },
+ Step::Toggler {
+ can_continue: false,
+ },
Step::Image {
width: 300,
slider: slider::State::new(),
@@ -206,6 +209,9 @@ enum Step {
Radio {
selection: Option<Language>,
},
+ Toggler {
+ can_continue: bool,
+ },
Image {
width: u16,
slider: slider::State,
@@ -232,6 +238,7 @@ pub enum StepMessage {
InputChanged(String),
ToggleSecureInput(bool),
DebugToggled(bool),
+ TogglerChanged(bool),
}
impl<'a> Step {
@@ -287,6 +294,11 @@ impl<'a> Step {
*is_secure = toggle;
}
}
+ StepMessage::TogglerChanged(value) => {
+ if let Step::Toggler { can_continue, .. } = self {
+ *can_continue = value;
+ }
+ }
};
}
@@ -294,6 +306,7 @@ impl<'a> Step {
match self {
Step::Welcome => "Welcome",
Step::Radio { .. } => "Radio button",
+ Step::Toggler { .. } => "Toggler",
Step::Slider { .. } => "Slider",
Step::Text { .. } => "Text",
Step::Image { .. } => "Image",
@@ -309,6 +322,7 @@ impl<'a> Step {
match self {
Step::Welcome => true,
Step::Radio { selection } => *selection == Some(Language::Rust),
+ Step::Toggler { can_continue } => *can_continue,
Step::Slider { .. } => true,
Step::Text { .. } => true,
Step::Image { .. } => true,
@@ -324,6 +338,7 @@ impl<'a> Step {
match self {
Step::Welcome => Self::welcome(),
Step::Radio { selection } => Self::radio(*selection),
+ Step::Toggler { can_continue } => Self::toggler(*can_continue),
Step::Slider { state, value } => Self::slider(state, *value),
Step::Text {
size_slider,
@@ -545,6 +560,21 @@ impl<'a> Step {
))
}
+ fn toggler(can_continue: bool) -> Column<'a, StepMessage> {
+ Self::container("Toggler")
+ .push(Text::new(
+ "A toggler is mostly used to enable or disable something.",
+ ))
+ .push(
+ Container::new(Toggler::new(
+ can_continue,
+ String::from("Toggle me to continue..."),
+ StepMessage::TogglerChanged,
+ ))
+ .padding([0, 40]),
+ )
+ }
+
fn image(
width: u16,
slider: &'a mut slider::State,
diff --git a/examples/url_handler/Cargo.toml b/examples/url_handler/Cargo.toml
new file mode 100644
index 00000000..911b2f25
--- /dev/null
+++ b/examples/url_handler/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "url_handler"
+version = "0.1.0"
+authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
+edition = "2018"
+publish = false
+
+[dependencies]
+iced = { path = "../.." }
+iced_native = { path = "../../native" } \ No newline at end of file
diff --git a/examples/url_handler/src/main.rs b/examples/url_handler/src/main.rs
new file mode 100644
index 00000000..f14e5227
--- /dev/null
+++ b/examples/url_handler/src/main.rs
@@ -0,0 +1,73 @@
+use iced::{
+ executor, Application, Clipboard, Command, Container, Element, Length,
+ Settings, Subscription, Text,
+};
+use iced_native::{
+ event::{MacOS, PlatformSpecific},
+ Event,
+};
+
+pub fn main() -> iced::Result {
+ App::run(Settings::default())
+}
+
+#[derive(Debug, Default)]
+struct App {
+ url: Option<String>,
+}
+
+#[derive(Debug, Clone)]
+enum Message {
+ EventOccurred(iced_native::Event),
+}
+
+impl Application for App {
+ type Executor = executor::Default;
+ type Message = Message;
+ type Flags = ();
+
+ fn new(_flags: ()) -> (App, Command<Message>) {
+ (App::default(), Command::none())
+ }
+
+ fn title(&self) -> String {
+ String::from("Url - Iced")
+ }
+
+ fn update(
+ &mut self,
+ message: Message,
+ _clipboard: &mut Clipboard,
+ ) -> Command<Message> {
+ match message {
+ Message::EventOccurred(event) => {
+ if let Event::PlatformSpecific(PlatformSpecific::MacOS(
+ MacOS::ReceivedUrl(url),
+ )) = event
+ {
+ self.url = Some(url);
+ }
+ }
+ };
+
+ Command::none()
+ }
+
+ fn subscription(&self) -> Subscription<Message> {
+ iced_native::subscription::events().map(Message::EventOccurred)
+ }
+
+ fn view(&mut self) -> Element<Message> {
+ let content = match &self.url {
+ Some(url) => Text::new(format!("{}", url)),
+ None => Text::new("No URL received yet!"),
+ };
+
+ Container::new(content.size(48))
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .center_x()
+ .center_y()
+ .into()
+ }
+}
diff --git a/futures/Cargo.toml b/futures/Cargo.toml
index e8e47c08..3cea6e1a 100644
--- a/futures/Cargo.toml
+++ b/futures/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "iced_futures"
-version = "0.2.0"
+version = "0.3.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
description = "Commands, subscriptions, and runtimes for Iced"
@@ -26,15 +26,20 @@ optional = true
features = ["rt-core", "rt-threaded", "time", "stream"]
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio]
-version = "0.3"
+package = "tokio"
+version = "1.0"
optional = true
-features = ["rt-multi-thread", "time", "stream"]
+features = ["rt", "rt-multi-thread", "time"]
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.async-std]
version = "1.0"
optional = true
features = ["unstable"]
+[target.'cfg(not(target_arch = "wasm32"))'.dependencies.smol]
+version = "1.2"
+optional = true
+
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen-futures = "0.4"
diff --git a/futures/src/executor.rs b/futures/src/executor.rs
index fa87216a..b35b5bc1 100644
--- a/futures/src/executor.rs
+++ b/futures/src/executor.rs
@@ -13,6 +13,9 @@ mod tokio_old;
#[cfg(all(not(target_arch = "wasm32"), feature = "async-std"))]
mod async_std;
+#[cfg(all(not(target_arch = "wasm32"), feature = "smol"))]
+mod smol;
+
#[cfg(target_arch = "wasm32")]
mod wasm_bindgen;
@@ -30,6 +33,9 @@ pub use self::tokio_old::TokioOld;
#[cfg(all(not(target_arch = "wasm32"), feature = "async-std"))]
pub use self::async_std::AsyncStd;
+#[cfg(all(not(target_arch = "wasm32"), feature = "smol"))]
+pub use self::smol::Smol;
+
#[cfg(target_arch = "wasm32")]
pub use wasm_bindgen::WasmBindgen;
diff --git a/futures/src/executor/smol.rs b/futures/src/executor/smol.rs
new file mode 100644
index 00000000..deafd43a
--- /dev/null
+++ b/futures/src/executor/smol.rs
@@ -0,0 +1,18 @@
+use crate::Executor;
+
+use futures::Future;
+
+/// A `smol` runtime.
+#[cfg_attr(docsrs, doc(cfg(feature = "smol")))]
+#[derive(Debug)]
+pub struct Smol;
+
+impl Executor for Smol {
+ fn new() -> Result<Self, futures::io::Error> {
+ Ok(Self)
+ }
+
+ fn spawn(&self, future: impl Future<Output = ()> + Send + 'static) {
+ smol::spawn(future).detach();
+ }
+}
diff --git a/futures/src/lib.rs b/futures/src/lib.rs
index c7c6fd3a..01cf5c89 100644
--- a/futures/src/lib.rs
+++ b/futures/src/lib.rs
@@ -17,10 +17,22 @@ pub mod executor;
pub mod subscription;
#[cfg(all(
- any(feature = "tokio", feature = "tokio_old", feature = "async-std"),
+ any(
+ feature = "tokio",
+ feature = "tokio_old",
+ feature = "async-std",
+ feature = "smol"
+ ),
not(target_arch = "wasm32")
))]
-#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio", feature = "async-std"))))]
+#[cfg_attr(
+ docsrs,
+ doc(cfg(any(
+ feature = "tokio",
+ feature = "async-std",
+ feature = "smol"
+ )))
+)]
pub mod time;
pub use command::Command;
diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs
index 27d2d295..e60ad79a 100644
--- a/futures/src/subscription.rs
+++ b/futures/src/subscription.rs
@@ -125,9 +125,9 @@ impl<I, O, H> std::fmt::Debug for Subscription<I, O, H> {
/// - [`stopwatch`], a watch with start/stop and reset buttons showcasing how
/// to listen to time.
///
-/// [examples]: https://github.com/hecrj/iced/tree/0.2/examples
-/// [`download_progress`]: https://github.com/hecrj/iced/tree/0.2/examples/download_progress
-/// [`stopwatch`]: https://github.com/hecrj/iced/tree/0.2/examples/stopwatch
+/// [examples]: https://github.com/hecrj/iced/tree/0.3/examples
+/// [`download_progress`]: https://github.com/hecrj/iced/tree/0.3/examples/download_progress
+/// [`stopwatch`]: https://github.com/hecrj/iced/tree/0.3/examples/stopwatch
pub trait Recipe<Hasher: std::hash::Hasher, Event> {
/// The events that will be produced by a [`Subscription`] with this
/// [`Recipe`].
diff --git a/futures/src/subscription/tracker.rs b/futures/src/subscription/tracker.rs
index 43222b5b..3a8d4a87 100644
--- a/futures/src/subscription/tracker.rs
+++ b/futures/src/subscription/tracker.rs
@@ -135,7 +135,7 @@ where
.filter_map(|connection| connection.listener.as_mut())
.for_each(|listener| {
if let Err(error) = listener.try_send(event.clone()) {
- log::error!(
+ log::warn!(
"Error sending event to subscription: {:?}",
error
);
diff --git a/futures/src/time.rs b/futures/src/time.rs
index 5e9ea436..86b4a4e7 100644
--- a/futures/src/time.rs
+++ b/futures/src/time.rs
@@ -13,6 +13,33 @@ pub fn every<H: std::hash::Hasher, E>(
struct Every(std::time::Duration);
+#[cfg(all(
+ not(any(feature = "tokio_old", feature = "tokio", feature = "async-std")),
+ feature = "smol"
+))]
+impl<H, E> subscription::Recipe<H, E> for Every
+where
+ H: std::hash::Hasher,
+{
+ type Output = std::time::Instant;
+
+ fn hash(&self, state: &mut H) {
+ use std::hash::Hash;
+
+ std::any::TypeId::of::<Self>().hash(state);
+ self.0.hash(state);
+ }
+
+ fn stream(
+ self: Box<Self>,
+ _input: futures::stream::BoxStream<'static, E>,
+ ) -> futures::stream::BoxStream<'static, Self::Output> {
+ use futures::stream::StreamExt;
+
+ smol::Timer::interval(self.0).boxed()
+ }
+}
+
#[cfg(feature = "async-std")]
impl<H, E> subscription::Recipe<H, E> for Every
where
@@ -41,7 +68,7 @@ where
#[cfg(all(
any(feature = "tokio", feature = "tokio_old"),
- not(feature = "async-std")
+ not(any(feature = "async-std", feature = "smol"))
))]
impl<H, E> subscription::Recipe<H, E> for Every
where
@@ -67,8 +94,20 @@ where
let start = tokio::time::Instant::now() + self.0;
- tokio::time::interval_at(start, self.0)
- .map(|_| std::time::Instant::now())
- .boxed()
+ let stream = {
+ #[cfg(feature = "tokio")]
+ {
+ futures::stream::unfold(
+ tokio::time::interval_at(start, self.0),
+ |mut interval| async move {
+ Some((interval.tick().await, interval))
+ },
+ )
+ }
+ #[cfg(feature = "tokio_old")]
+ tokio::time::interval_at(start, self.0)
+ };
+
+ stream.map(tokio::time::Instant::into_std).boxed()
}
}
diff --git a/glow/Cargo.toml b/glow/Cargo.toml
index 3f85e52d..e40b8ba8 100644
--- a/glow/Cargo.toml
+++ b/glow/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "iced_glow"
-version = "0.1.0"
+version = "0.2.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
description = "A glow renderer for iced"
@@ -24,11 +24,11 @@ bytemuck = "1.4"
log = "0.4"
[dependencies.iced_native]
-version = "0.3"
+version = "0.4"
path = "../native"
[dependencies.iced_graphics]
-version = "0.1"
+version = "0.2"
path = "../graphics"
features = ["font-fallback", "font-icons", "opengl"]
diff --git a/glow/src/backend.rs b/glow/src/backend.rs
index 92bb993e..1680fc00 100644
--- a/glow/src/backend.rs
+++ b/glow/src/backend.rs
@@ -24,7 +24,12 @@ pub struct Backend {
impl Backend {
/// Creates a new [`Backend`].
pub fn new(gl: &glow::Context, settings: Settings) -> Self {
- let text_pipeline = text::Pipeline::new(gl, settings.default_font);
+ let text_pipeline = text::Pipeline::new(
+ gl,
+ settings.default_font,
+ settings.text_multithreading,
+ );
+
let quad_pipeline = quad::Pipeline::new(gl);
let triangle_pipeline = triangle::Pipeline::new(gl);
diff --git a/glow/src/program.rs b/glow/src/program.rs
index 489a194f..601f9ce6 100644
--- a/glow/src/program.rs
+++ b/glow/src/program.rs
@@ -17,7 +17,7 @@ pub unsafe fn create(
gl.compile_shader(shader);
if !gl.get_shader_compile_status(shader) {
- panic!(gl.get_shader_info_log(shader));
+ panic!("{}", gl.get_shader_info_log(shader));
}
gl.attach_shader(program, shader);
@@ -27,7 +27,7 @@ pub unsafe fn create(
gl.link_program(program);
if !gl.get_program_link_status(program) {
- panic!(gl.get_program_info_log(program));
+ panic!("{}", gl.get_program_info_log(program));
}
for shader in shaders {
diff --git a/glow/src/settings.rs b/glow/src/settings.rs
index 524d91a9..f3dddfaf 100644
--- a/glow/src/settings.rs
+++ b/glow/src/settings.rs
@@ -16,7 +16,15 @@ pub struct Settings {
/// By default, it will be set to 20.
pub default_text_size: u16,
+ /// If enabled, spread text workload in multiple threads when multiple cores
+ /// are available.
+ ///
+ /// By default, it is disabled.
+ pub text_multithreading: bool,
+
/// The antialiasing strategy that will be used for triangle primitives.
+ ///
+ /// By default, it is `None`.
pub antialiasing: Option<Antialiasing>,
}
@@ -25,7 +33,17 @@ impl Default for Settings {
Settings {
default_font: None,
default_text_size: 20,
+ text_multithreading: false,
antialiasing: None,
}
}
}
+
+impl Settings {
+ /// Creates new [`Settings`] using environment configuration.
+ ///
+ /// Currently, this is equivalent to calling [`Settings::default`].
+ pub fn from_env() -> Self {
+ Self::default()
+ }
+}
diff --git a/glow/src/text.rs b/glow/src/text.rs
index 925c7287..a4c39dfe 100644
--- a/glow/src/text.rs
+++ b/glow/src/text.rs
@@ -11,7 +11,11 @@ pub struct Pipeline {
}
impl Pipeline {
- pub fn new(gl: &glow::Context, default_font: Option<&[u8]>) -> Self {
+ pub fn new(
+ gl: &glow::Context,
+ default_font: Option<&[u8]>,
+ multithreading: bool,
+ ) -> Self {
let default_font = default_font.map(|slice| slice.to_vec());
// TODO: Font customization
@@ -41,7 +45,7 @@ impl Pipeline {
let draw_brush =
glow_glyph::GlyphBrushBuilder::using_font(font.clone())
.initial_cache_size((2048, 2048))
- .draw_cache_multithread(false) // TODO: Expose as a configuration flag
+ .draw_cache_multithread(multithreading)
.build(&gl);
let measure_brush =
diff --git a/glow/src/widget.rs b/glow/src/widget.rs
index b5c84c56..a77511e8 100644
--- a/glow/src/widget.rs
+++ b/glow/src/widget.rs
@@ -20,6 +20,8 @@ pub mod rule;
pub mod scrollable;
pub mod slider;
pub mod text_input;
+pub mod toggler;
+pub mod tooltip;
#[doc(no_inline)]
pub use button::Button;
@@ -43,6 +45,10 @@ pub use scrollable::Scrollable;
pub use slider::Slider;
#[doc(no_inline)]
pub use text_input::TextInput;
+#[doc(no_inline)]
+pub use toggler::Toggler;
+#[doc(no_inline)]
+pub use tooltip::Tooltip;
#[cfg(feature = "canvas")]
#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))]
diff --git a/glow/src/widget/pane_grid.rs b/glow/src/widget/pane_grid.rs
index c26dde48..fc36862c 100644
--- a/glow/src/widget/pane_grid.rs
+++ b/glow/src/widget/pane_grid.rs
@@ -6,12 +6,12 @@
//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing,
//! drag and drop, and hotkey support.
//!
-//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.2/examples/pane_grid
+//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.3/examples/pane_grid
use crate::Renderer;
-pub use iced_native::pane_grid::{
- Axis, Configuration, Direction, DragEvent, Node, Pane, ResizeEvent, Split,
- State,
+pub use iced_graphics::pane_grid::{
+ Axis, Configuration, Direction, DragEvent, Line, Node, Pane, ResizeEvent,
+ Split, State, StyleSheet,
};
/// A collection of panes distributed using either vertical or horizontal splits
diff --git a/glow/src/widget/toggler.rs b/glow/src/widget/toggler.rs
new file mode 100644
index 00000000..1cd8711b
--- /dev/null
+++ b/glow/src/widget/toggler.rs
@@ -0,0 +1,9 @@
+//! Show toggle controls using togglers.
+use crate::Renderer;
+
+pub use iced_graphics::toggler::{Style, StyleSheet};
+
+/// A toggler that can be toggled.
+///
+/// This is an alias of an `iced_native` checkbox with an `iced_wgpu::Renderer`.
+pub type Toggler<Message> = iced_native::Toggler<Message, Renderer>;
diff --git a/glow/src/widget/tooltip.rs b/glow/src/widget/tooltip.rs
new file mode 100644
index 00000000..89ab3a15
--- /dev/null
+++ b/glow/src/widget/tooltip.rs
@@ -0,0 +1,6 @@
+//! Display a widget over another.
+/// A widget allowing the selection of a single value from a list of options.
+pub type Tooltip<'a, Message> =
+ iced_native::Tooltip<'a, Message, crate::Renderer>;
+
+pub use iced_native::tooltip::Position;
diff --git a/glutin/Cargo.toml b/glutin/Cargo.toml
index 505ee7e5..b2a7f307 100644
--- a/glutin/Cargo.toml
+++ b/glutin/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "iced_glutin"
-version = "0.1.0"
+version = "0.2.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
description = "A glutin runtime for Iced"
@@ -13,18 +13,20 @@ categories = ["gui"]
[features]
debug = ["iced_winit/debug"]
-[dependencies]
-glutin = "0.26"
+[dependencies.glutin]
+version = "0.27"
+git = "https://github.com/iced-rs/glutin"
+rev = "03437d8a1826d83c62017b2bb7bf18bfc9e352cc"
[dependencies.iced_native]
-version = "0.3"
+version = "0.4"
path = "../native"
[dependencies.iced_winit]
-version = "0.2"
+version = "0.3"
path = "../winit"
[dependencies.iced_graphics]
-version = "0.1"
+version = "0.2"
path = "../graphics"
features = ["opengl"]
diff --git a/glutin/README.md b/glutin/README.md
index addb9228..fcae157e 100644
--- a/glutin/README.md
+++ b/glutin/README.md
@@ -20,7 +20,7 @@ It exposes a renderer-agnostic `Application` trait that can be implemented and t
Add `iced_glutin` as a dependency in your `Cargo.toml`:
```toml
-iced_glutin = "0.1"
+iced_glutin = "0.2"
```
__Iced moves fast and the `master` branch can contain breaking changes!__ If
diff --git a/glutin/src/application.rs b/glutin/src/application.rs
index 42513feb..991c8705 100644
--- a/glutin/src/application.rs
+++ b/glutin/src/application.rs
@@ -52,11 +52,14 @@ where
runtime.track(subscription);
let context = {
- let builder = settings.window.into_builder(
- &application.title(),
- application.mode(),
- event_loop.primary_monitor(),
- );
+ let builder = settings
+ .window
+ .into_builder(
+ &application.title(),
+ application.mode(),
+ event_loop.primary_monitor(),
+ )
+ .with_menu(Some(conversion::menu(&application.menu())));
let context = ContextBuilder::new()
.with_vsync(true)
@@ -92,10 +95,11 @@ where
application,
compositor,
renderer,
- context,
runtime,
debug,
receiver,
+ context,
+ settings.exit_on_close_request,
));
let mut context = task::Context::from_waker(task::noop_waker_ref());
@@ -107,7 +111,22 @@ where
return;
}
- if let Some(event) = event.to_static() {
+ let event = match event {
+ glutin::event::Event::WindowEvent {
+ event:
+ glutin::event::WindowEvent::ScaleFactorChanged {
+ new_inner_size,
+ ..
+ },
+ window_id,
+ } => Some(glutin::event::Event::WindowEvent {
+ event: glutin::event::WindowEvent::Resized(*new_inner_size),
+ window_id,
+ }),
+ _ => event.to_static(),
+ };
+
+ if let Some(event) = event {
sender.start_send(event).expect("Send event");
let poll = instance.as_mut().poll(&mut context);
@@ -124,10 +143,11 @@ async fn run_instance<A, E, C>(
mut application: A,
mut compositor: C,
mut renderer: A::Renderer,
- context: glutin::ContextWrapper<glutin::PossiblyCurrent, Window>,
mut runtime: Runtime<E, Proxy<A::Message>, A::Message>,
mut debug: Debug,
mut receiver: mpsc::UnboundedReceiver<glutin::event::Event<'_, A::Message>>,
+ context: glutin::ContextWrapper<glutin::PossiblyCurrent, Window>,
+ exit_on_close_request: bool,
) where
A: Application + 'static,
E: Executor + 'static,
@@ -136,7 +156,7 @@ async fn run_instance<A, E, C>(
use glutin::event;
use iced_winit::futures::stream::StreamExt;
- let clipboard = Clipboard::new(context.window());
+ let mut clipboard = Clipboard::connect(context.window());
let mut state = application::State::new(&application, context.window());
let mut viewport_version = state.viewport_version();
@@ -170,8 +190,8 @@ async fn run_instance<A, E, C>(
let statuses = user_interface.update(
&events,
state.cursor_position(),
- clipboard.as_ref().map(|c| c as _),
&mut renderer,
+ &mut clipboard,
&mut messages,
);
@@ -190,12 +210,15 @@ async fn run_instance<A, E, C>(
&mut application,
&mut runtime,
&mut debug,
+ &mut clipboard,
&mut messages,
);
// Update window
state.synchronize(&application, context.window());
+ let should_exit = application.should_exit();
+
user_interface =
ManuallyDrop::new(application::build_user_interface(
&mut application,
@@ -204,6 +227,10 @@ async fn run_instance<A, E, C>(
state.logical_size(),
&mut debug,
));
+
+ if should_exit {
+ break;
+ }
}
debug.draw_started();
@@ -213,6 +240,16 @@ async fn run_instance<A, E, C>(
context.window().request_redraw();
}
+ event::Event::PlatformSpecific(event::PlatformSpecific::MacOS(
+ event::MacOS::ReceivedUrl(url),
+ )) => {
+ use iced_native::event;
+ events.push(iced_native::Event::PlatformSpecific(
+ event::PlatformSpecific::MacOS(event::MacOS::ReceivedUrl(
+ url,
+ )),
+ ));
+ }
event::Event::UserEvent(message) => {
messages.push(message);
}
@@ -270,10 +307,21 @@ async fn run_instance<A, E, C>(
// Maybe we can use `ControlFlow::WaitUntil` for this.
}
event::Event::WindowEvent {
+ event: event::WindowEvent::MenuEntryActivated(entry_id),
+ ..
+ } => {
+ if let Some(message) =
+ conversion::menu_message(state.menu(), entry_id)
+ {
+ messages.push(message);
+ }
+ }
+ event::Event::WindowEvent {
event: window_event,
..
} => {
if application::requests_exit(&window_event, state.modifiers())
+ && exit_on_close_request
{
break;
}
diff --git a/glutin/src/lib.rs b/glutin/src/lib.rs
index f2c0102a..107d9556 100644
--- a/glutin/src/lib.rs
+++ b/glutin/src/lib.rs
@@ -17,7 +17,7 @@ pub use iced_native::*;
pub mod application;
pub use iced_winit::settings;
-pub use iced_winit::{Error, Mode};
+pub use iced_winit::{Clipboard, Error, Mode};
#[doc(no_inline)]
pub use application::Application;
diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml
index 73dc47bf..ea9471c6 100644
--- a/graphics/Cargo.toml
+++ b/graphics/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "iced_graphics"
-version = "0.1.0"
+version = "0.2.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
description = "A bunch of backend-agnostic types that can be leveraged to build a renderer for Iced"
@@ -28,11 +28,11 @@ version = "1.4"
features = ["derive"]
[dependencies.iced_native]
-version = "0.3"
+version = "0.4"
path = "../native"
[dependencies.iced_style]
-version = "0.2"
+version = "0.3"
path = "../style"
[dependencies.lyon]
@@ -42,9 +42,10 @@ optional = true
[dependencies.qrcode]
version = "0.12"
optional = true
+default-features = false
[dependencies.font-kit]
-version = "0.8"
+version = "0.10"
optional = true
[package.metadata.docs.rs]
diff --git a/graphics/src/layer.rs b/graphics/src/layer.rs
index ab40b114..7dce1d4c 100644
--- a/graphics/src/layer.rs
+++ b/graphics/src/layer.rs
@@ -82,7 +82,12 @@ impl<'a> Layer<'a> {
let mut layers = vec![first_layer];
- Self::process_primitive(&mut layers, Vector::new(0.0, 0.0), primitive);
+ Self::process_primitive(
+ &mut layers,
+ Vector::new(0.0, 0.0),
+ primitive,
+ 0,
+ );
layers
}
@@ -91,13 +96,19 @@ impl<'a> Layer<'a> {
layers: &mut Vec<Self>,
translation: Vector,
primitive: &'a Primitive,
+ current_layer: usize,
) {
match primitive {
Primitive::None => {}
Primitive::Group { primitives } => {
// TODO: Inspect a bit and regroup (?)
for primitive in primitives {
- Self::process_primitive(layers, translation, primitive)
+ Self::process_primitive(
+ layers,
+ translation,
+ primitive,
+ current_layer,
+ )
}
}
Primitive::Text {
@@ -109,7 +120,7 @@ impl<'a> Layer<'a> {
horizontal_alignment,
vertical_alignment,
} => {
- let layer = layers.last_mut().unwrap();
+ let layer = &mut layers[current_layer];
layer.text.push(Text {
content,
@@ -128,7 +139,7 @@ impl<'a> Layer<'a> {
border_width,
border_color,
} => {
- let layer = layers.last_mut().unwrap();
+ let layer = &mut layers[current_layer];
// TODO: Move some of these computations to the GPU (?)
layer.quads.push(Quad {
@@ -146,7 +157,7 @@ impl<'a> Layer<'a> {
});
}
Primitive::Mesh2D { buffers, size } => {
- let layer = layers.last_mut().unwrap();
+ let layer = &mut layers[current_layer];
let bounds = Rectangle::new(
Point::new(translation.x, translation.y),
@@ -167,7 +178,7 @@ impl<'a> Layer<'a> {
offset,
content,
} => {
- let layer = layers.last_mut().unwrap();
+ let layer = &mut layers[current_layer];
let translated_bounds = *bounds + translation;
// Only draw visible content
@@ -175,16 +186,15 @@ impl<'a> Layer<'a> {
layer.bounds.intersection(&translated_bounds)
{
let clip_layer = Layer::new(clip_bounds);
- let new_layer = Layer::new(layer.bounds);
-
layers.push(clip_layer);
+
Self::process_primitive(
layers,
translation
- Vector::new(offset.x as f32, offset.y as f32),
content,
+ layers.len() - 1,
);
- layers.push(new_layer);
}
}
Primitive::Translate {
@@ -195,13 +205,19 @@ impl<'a> Layer<'a> {
layers,
translation + *new_translation,
&content,
+ current_layer,
);
}
Primitive::Cached { cache } => {
- Self::process_primitive(layers, translation, &cache);
+ Self::process_primitive(
+ layers,
+ translation,
+ &cache,
+ current_layer,
+ );
}
Primitive::Image { handle, bounds } => {
- let layer = layers.last_mut().unwrap();
+ let layer = &mut layers[current_layer];
layer.images.push(Image::Raster {
handle: handle.clone(),
@@ -209,7 +225,7 @@ impl<'a> Layer<'a> {
});
}
Primitive::Svg { handle, bounds } => {
- let layer = layers.last_mut().unwrap();
+ let layer = &mut layers[current_layer];
layer.images.push(Image::Vector {
handle: handle.clone(),
diff --git a/graphics/src/overlay/menu.rs b/graphics/src/overlay/menu.rs
index ffe998c5..9e91a0ef 100644
--- a/graphics/src/overlay/menu.rs
+++ b/graphics/src/overlay/menu.rs
@@ -2,8 +2,8 @@
use crate::backend::{self, Backend};
use crate::{Primitive, Renderer};
use iced_native::{
- mouse, overlay, Color, Font, HorizontalAlignment, Point, Rectangle,
- VerticalAlignment,
+ mouse, overlay, Color, Font, HorizontalAlignment, Padding, Point,
+ Rectangle, VerticalAlignment,
};
pub use iced_style::menu::Style;
@@ -45,7 +45,7 @@ where
viewport: &Rectangle,
options: &[T],
hovered_option: Option<usize>,
- padding: u16,
+ padding: Padding,
text_size: u16,
font: Font,
style: &Style,
@@ -53,7 +53,7 @@ where
use std::f32;
let is_mouse_over = bounds.contains(cursor_position);
- let option_height = text_size as usize + padding as usize * 2;
+ let option_height = (text_size + padding.vertical()) as usize;
let mut primitives = Vec::new();
@@ -72,7 +72,7 @@ where
x: bounds.x,
y: bounds.y + (option_height * i) as f32,
width: bounds.width,
- height: f32::from(text_size + padding * 2),
+ height: f32::from(text_size + padding.vertical()),
};
if is_selected {
@@ -88,7 +88,7 @@ where
primitives.push(Primitive::Text {
content: option.to_string(),
bounds: Rectangle {
- x: bounds.x + f32::from(padding),
+ x: bounds.x + padding.left as f32,
y: bounds.center_y(),
width: f32::INFINITY,
..bounds
diff --git a/graphics/src/viewport.rs b/graphics/src/viewport.rs
index 78d539af..2c0b541a 100644
--- a/graphics/src/viewport.rs
+++ b/graphics/src/viewport.rs
@@ -31,7 +31,7 @@ impl Viewport {
/// Returns the physical width of the [`Viewport`].
pub fn physical_width(&self) -> u32 {
- self.physical_size.height
+ self.physical_size.width
}
/// Returns the physical height of the [`Viewport`].
diff --git a/graphics/src/widget.rs b/graphics/src/widget.rs
index 159ca91b..e34d267f 100644
--- a/graphics/src/widget.rs
+++ b/graphics/src/widget.rs
@@ -20,6 +20,8 @@ pub mod scrollable;
pub mod slider;
pub mod svg;
pub mod text_input;
+pub mod toggler;
+pub mod tooltip;
mod column;
mod row;
@@ -48,6 +50,10 @@ pub use scrollable::Scrollable;
pub use slider::Slider;
#[doc(no_inline)]
pub use text_input::TextInput;
+#[doc(no_inline)]
+pub use toggler::Toggler;
+#[doc(no_inline)]
+pub use tooltip::Tooltip;
pub use column::Column;
pub use image::Image;
diff --git a/graphics/src/widget/button.rs b/graphics/src/widget/button.rs
index 2e3f78ca..60400ed8 100644
--- a/graphics/src/widget/button.rs
+++ b/graphics/src/widget/button.rs
@@ -5,7 +5,7 @@ use crate::defaults::{self, Defaults};
use crate::{Backend, Primitive, Renderer};
use iced_native::mouse;
use iced_native::{
- Background, Color, Element, Layout, Point, Rectangle, Vector,
+ Background, Color, Element, Layout, Padding, Point, Rectangle, Vector,
};
pub use iced_native::button::State;
@@ -21,7 +21,7 @@ impl<B> iced_native::button::Renderer for Renderer<B>
where
B: Backend,
{
- const DEFAULT_PADDING: u16 = 5;
+ const DEFAULT_PADDING: Padding = Padding::new(5);
type Style = Box<dyn StyleSheet>;
diff --git a/graphics/src/widget/canvas.rs b/graphics/src/widget/canvas.rs
index 95ede50f..7897c8ec 100644
--- a/graphics/src/widget/canvas.rs
+++ b/graphics/src/widget/canvas.rs
@@ -154,9 +154,9 @@ where
event: iced_native::Event,
layout: Layout<'_>,
cursor_position: Point,
- messages: &mut Vec<Message>,
_renderer: &Renderer<B>,
- _clipboard: Option<&dyn Clipboard>,
+ _clipboard: &mut dyn Clipboard,
+ messages: &mut Vec<Message>,
) -> event::Status {
let bounds = layout.bounds();
diff --git a/graphics/src/widget/canvas/frame.rs b/graphics/src/widget/canvas/frame.rs
index b86f9e04..5af9d11f 100644
--- a/graphics/src/widget/canvas/frame.rs
+++ b/graphics/src/widget/canvas/frame.rs
@@ -54,7 +54,7 @@ impl Frame {
self.size.width
}
- /// Returns the width of the [`Frame`].
+ /// Returns the height of the [`Frame`].
#[inline]
pub fn height(&self) -> f32 {
self.size.height
diff --git a/graphics/src/widget/canvas/program.rs b/graphics/src/widget/canvas/program.rs
index d703caad..85a2f67b 100644
--- a/graphics/src/widget/canvas/program.rs
+++ b/graphics/src/widget/canvas/program.rs
@@ -34,7 +34,7 @@ pub trait Program<Message> {
/// [`Geometry`] can be easily generated with a [`Frame`] or stored in a
/// [`Cache`].
///
- /// [`Frame`]: crate::widget::canvas::Cache
+ /// [`Frame`]: crate::widget::canvas::Frame
/// [`Cache`]: crate::widget::canvas::Cache
fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec<Geometry>;
diff --git a/graphics/src/widget/image.rs b/graphics/src/widget/image.rs
index 30f446e8..bdf03de3 100644
--- a/graphics/src/widget/image.rs
+++ b/graphics/src/widget/image.rs
@@ -1,11 +1,14 @@
//! Display images in your user interface.
+pub mod viewer;
+
use crate::backend::{self, Backend};
+
use crate::{Primitive, Renderer};
use iced_native::image;
use iced_native::mouse;
use iced_native::Layout;
-pub use iced_native::image::{Handle, Image};
+pub use iced_native::image::{Handle, Image, Viewer};
impl<B> image::Renderer for Renderer<B>
where
diff --git a/graphics/src/widget/image/viewer.rs b/graphics/src/widget/image/viewer.rs
new file mode 100644
index 00000000..28dffc4f
--- /dev/null
+++ b/graphics/src/widget/image/viewer.rs
@@ -0,0 +1,55 @@
+//! Zoom and pan on an image.
+use crate::backend::{self, Backend};
+use crate::{Primitive, Renderer};
+
+use iced_native::image;
+use iced_native::image::viewer;
+use iced_native::mouse;
+use iced_native::{Rectangle, Size, Vector};
+
+impl<B> viewer::Renderer for Renderer<B>
+where
+ B: Backend + backend::Image,
+{
+ fn draw(
+ &mut self,
+ state: &viewer::State,
+ bounds: Rectangle,
+ image_size: Size,
+ translation: Vector,
+ handle: image::Handle,
+ is_mouse_over: bool,
+ ) -> Self::Output {
+ (
+ {
+ Primitive::Clip {
+ bounds,
+ content: Box::new(Primitive::Translate {
+ translation,
+ content: Box::new(Primitive::Image {
+ handle,
+ bounds: Rectangle {
+ x: bounds.x,
+ y: bounds.y,
+ ..Rectangle::with_size(image_size)
+ },
+ }),
+ }),
+ offset: Vector::new(0, 0),
+ }
+ },
+ {
+ if state.is_cursor_grabbed() {
+ mouse::Interaction::Grabbing
+ } else if is_mouse_over
+ && (image_size.width > bounds.width
+ || image_size.height > bounds.height)
+ {
+ mouse::Interaction::Grab
+ } else {
+ mouse::Interaction::Idle
+ }
+ },
+ )
+ }
+}
diff --git a/graphics/src/widget/pane_grid.rs b/graphics/src/widget/pane_grid.rs
index f09984fc..92cdbb77 100644
--- a/graphics/src/widget/pane_grid.rs
+++ b/graphics/src/widget/pane_grid.rs
@@ -6,23 +6,21 @@
//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing,
//! drag and drop, and hotkey support.
//!
-//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.2/examples/pane_grid
-use crate::backend::{self, Backend};
+//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.3/examples/pane_grid
use crate::defaults;
-use crate::{Primitive, Renderer};
+use crate::{Backend, Color, Primitive, Renderer};
+use iced_native::container;
use iced_native::mouse;
use iced_native::pane_grid;
-use iced_native::text;
-use iced_native::{
- Element, HorizontalAlignment, Layout, Point, Rectangle, Vector,
- VerticalAlignment,
-};
+use iced_native::{Element, Layout, Point, Rectangle, Vector};
pub use iced_native::pane_grid::{
- Axis, Configuration, Content, Direction, DragEvent, Pane, ResizeEvent,
- Split, State, TitleBar,
+ Axis, Configuration, Content, Direction, DragEvent, Node, Pane,
+ ResizeEvent, Split, State, TitleBar,
};
+pub use iced_style::pane_grid::{Line, StyleSheet};
+
/// A collection of panes distributed using either vertical or horizontal splits
/// to completely fill the space available.
///
@@ -34,16 +32,20 @@ pub type PaneGrid<'a, Message, Backend> =
impl<B> pane_grid::Renderer for Renderer<B>
where
- B: Backend + backend::Text,
+ B: Backend,
{
+ type Style = Box<dyn StyleSheet>;
+
fn draw<Message>(
&mut self,
defaults: &Self::Defaults,
content: &[(Pane, Content<'_, Message, Self>)],
dragging: Option<(Pane, Point)>,
- resizing: Option<Axis>,
+ resizing: Option<(Axis, Rectangle, bool)>,
layout: Layout<'_>,
+ style_sheet: &<Self as pane_grid::Renderer>::Style,
cursor_position: Point,
+ viewport: &Rectangle,
) -> Self::Output {
let pane_cursor_position = if dragging.is_some() {
// TODO: Remove once cursor availability is encoded in the type
@@ -61,8 +63,13 @@ where
.zip(layout.children())
.enumerate()
.map(|(i, ((id, pane), layout))| {
- let (primitive, new_mouse_interaction) =
- pane.draw(self, defaults, layout, pane_cursor_position);
+ let (primitive, new_mouse_interaction) = pane.draw(
+ self,
+ defaults,
+ layout,
+ pane_cursor_position,
+ viewport,
+ );
if new_mouse_interaction > mouse_interaction {
mouse_interaction = new_mouse_interaction;
@@ -78,7 +85,8 @@ where
})
.collect();
- let primitives = if let Some((index, layout, origin)) = dragged_pane {
+ let mut primitives = if let Some((index, layout, origin)) = dragged_pane
+ {
let pane = panes.remove(index);
let bounds = layout.bounds();
@@ -108,15 +116,62 @@ where
panes
};
+ let (primitives, mouse_interaction) =
+ if let Some((axis, split_region, is_picked)) = resizing {
+ let highlight = if is_picked {
+ style_sheet.picked_split()
+ } else {
+ style_sheet.hovered_split()
+ };
+
+ if let Some(highlight) = highlight {
+ primitives.push(Primitive::Quad {
+ bounds: match axis {
+ Axis::Horizontal => Rectangle {
+ x: split_region.x,
+ y: (split_region.y
+ + (split_region.height - highlight.width)
+ / 2.0)
+ .round(),
+ width: split_region.width,
+ height: highlight.width,
+ },
+ Axis::Vertical => Rectangle {
+ x: (split_region.x
+ + (split_region.width - highlight.width)
+ / 2.0)
+ .round(),
+ y: split_region.y,
+ width: highlight.width,
+ height: split_region.height,
+ },
+ },
+ background: highlight.color.into(),
+ border_radius: 0.0,
+ border_width: 0.0,
+ border_color: Color::TRANSPARENT,
+ });
+ }
+
+ (
+ primitives,
+ match axis {
+ Axis::Horizontal => {
+ mouse::Interaction::ResizingVertically
+ }
+ Axis::Vertical => {
+ mouse::Interaction::ResizingHorizontally
+ }
+ },
+ )
+ } else {
+ (primitives, mouse_interaction)
+ };
+
(
Primitive::Group { primitives },
if dragging.is_some() {
mouse::Interaction::Grabbing
- } else if let Some(axis) = resizing {
- match axis {
- Axis::Horizontal => mouse::Interaction::ResizingVertically,
- Axis::Vertical => mouse::Interaction::ResizingHorizontally,
- }
} else {
mouse_interaction
},
@@ -127,16 +182,17 @@ where
&mut self,
defaults: &Self::Defaults,
bounds: Rectangle,
- style_sheet: &Self::Style,
+ style_sheet: &<Self as container::Renderer>::Style,
title_bar: Option<(&TitleBar<'_, Message, Self>, Layout<'_>)>,
body: (&Element<'_, Message, Self>, Layout<'_>),
cursor_position: Point,
+ viewport: &Rectangle,
) -> Self::Output {
let style = style_sheet.style();
let (body, body_layout) = body;
let (body_primitive, body_interaction) =
- body.draw(self, defaults, body_layout, cursor_position, &bounds);
+ body.draw(self, defaults, body_layout, cursor_position, viewport);
let background = crate::widget::container::background(bounds, &style);
@@ -150,6 +206,7 @@ where
defaults,
title_bar_layout,
cursor_position,
+ viewport,
show_controls,
);
@@ -161,10 +218,10 @@ where
body_primitive,
],
},
- if is_over_pick_area {
- mouse::Interaction::Grab
- } else if title_bar_interaction > body_interaction {
+ if title_bar_interaction > body_interaction {
title_bar_interaction
+ } else if is_over_pick_area {
+ mouse::Interaction::Grab
} else {
body_interaction
},
@@ -187,15 +244,14 @@ where
&mut self,
defaults: &Self::Defaults,
bounds: Rectangle,
- style_sheet: &Self::Style,
- title: &str,
- title_size: u16,
- title_font: Self::Font,
- title_bounds: Rectangle,
+ style_sheet: &<Self as container::Renderer>::Style,
+ content: (&Element<'_, Message, Self>, Layout<'_>),
controls: Option<(&Element<'_, Message, Self>, Layout<'_>)>,
cursor_position: Point,
+ viewport: &Rectangle,
) -> Self::Output {
let style = style_sheet.style();
+ let (title_content, title_layout) = content;
let defaults = Self::Defaults {
text: defaults::Text {
@@ -205,16 +261,12 @@ where
let background = crate::widget::container::background(bounds, &style);
- let (title_primitive, _) = text::Renderer::draw(
+ let (title_primitive, title_interaction) = title_content.draw(
self,
&defaults,
- title_bounds,
- title,
- title_size,
- title_font,
- None,
- HorizontalAlignment::Left,
- VerticalAlignment::Top,
+ title_layout,
+ cursor_position,
+ viewport,
);
if let Some((controls, controls_layout)) = controls {
@@ -223,7 +275,7 @@ where
&defaults,
controls_layout,
cursor_position,
- &bounds,
+ viewport,
);
(
@@ -234,7 +286,7 @@ where
controls_primitive,
],
},
- controls_interaction,
+ controls_interaction.max(title_interaction),
)
} else {
(
@@ -245,7 +297,7 @@ where
} else {
title_primitive
},
- mouse::Interaction::default(),
+ title_interaction,
)
}
}
diff --git a/graphics/src/widget/pick_list.rs b/graphics/src/widget/pick_list.rs
index f42a8707..88a590b5 100644
--- a/graphics/src/widget/pick_list.rs
+++ b/graphics/src/widget/pick_list.rs
@@ -2,7 +2,8 @@
use crate::backend::{self, Backend};
use crate::{Primitive, Renderer};
use iced_native::{
- mouse, Font, HorizontalAlignment, Point, Rectangle, VerticalAlignment,
+ mouse, Font, HorizontalAlignment, Padding, Point, Rectangle,
+ VerticalAlignment,
};
use iced_style::menu;
@@ -19,7 +20,7 @@ where
{
type Style = Box<dyn StyleSheet>;
- const DEFAULT_PADDING: u16 = 5;
+ const DEFAULT_PADDING: Padding = Padding::new(5);
fn menu_style(style: &Box<dyn StyleSheet>) -> menu::Style {
style.menu()
@@ -30,12 +31,14 @@ where
bounds: Rectangle,
cursor_position: Point,
selected: Option<String>,
- padding: u16,
+ placeholder: Option<&str>,
+ padding: Padding,
text_size: u16,
font: Font,
style: &Box<dyn StyleSheet>,
) -> Self::Output {
let is_mouse_over = bounds.contains(cursor_position);
+ let is_selected = selected.is_some();
let style = if is_mouse_over {
style.hovered()
@@ -56,7 +59,7 @@ where
font: B::ICON_FONT,
size: bounds.height * style.icon_size,
bounds: Rectangle {
- x: bounds.x + bounds.width - f32::from(padding) * 2.0,
+ x: bounds.x + bounds.width - f32::from(padding.horizontal()),
y: bounds.center_y(),
..bounds
},
@@ -67,14 +70,18 @@ where
(
Primitive::Group {
- primitives: if let Some(label) = selected {
+ primitives: if let Some(label) =
+ selected.or_else(|| placeholder.map(str::to_string))
+ {
let label = Primitive::Text {
content: label,
size: f32::from(text_size),
font,
- color: style.text_color,
+ color: is_selected
+ .then(|| style.text_color)
+ .unwrap_or(style.placeholder_color),
bounds: Rectangle {
- x: bounds.x + f32::from(padding),
+ x: bounds.x + f32::from(padding.left),
y: bounds.center_y(),
..bounds
},
diff --git a/graphics/src/widget/scrollable.rs b/graphics/src/widget/scrollable.rs
index 57065ba2..2220e4b8 100644
--- a/graphics/src/widget/scrollable.rs
+++ b/graphics/src/widget/scrollable.rs
@@ -134,8 +134,16 @@ where
Primitive::None
};
+ let scroll = Primitive::Clip {
+ bounds,
+ offset: Vector::new(0, 0),
+ content: Box::new(Primitive::Group {
+ primitives: vec![scrollbar, scroller],
+ }),
+ };
+
Primitive::Group {
- primitives: vec![clip, scrollbar, scroller],
+ primitives: vec![clip, scroll],
}
} else {
content
diff --git a/graphics/src/widget/toggler.rs b/graphics/src/widget/toggler.rs
new file mode 100644
index 00000000..852d18ee
--- /dev/null
+++ b/graphics/src/widget/toggler.rs
@@ -0,0 +1,99 @@
+//! Show toggle controls using togglers.
+use crate::backend::{self, Backend};
+use crate::{Primitive, Renderer};
+use iced_native::mouse;
+use iced_native::toggler;
+use iced_native::Rectangle;
+
+pub use iced_style::toggler::{Style, StyleSheet};
+
+/// Makes sure that the border radius of the toggler looks good at every size.
+const BORDER_RADIUS_RATIO: f32 = 32.0 / 13.0;
+
+/// The space ratio between the background Quad and the Toggler bounds, and
+/// between the background Quad and foreground Quad.
+const SPACE_RATIO: f32 = 0.05;
+
+/// A toggler that can be toggled.
+///
+/// This is an alias of an `iced_native` toggler with an `iced_wgpu::Renderer`.
+pub type Toggler<Message, Backend> =
+ iced_native::Toggler<Message, Renderer<Backend>>;
+
+impl<B> toggler::Renderer for Renderer<B>
+where
+ B: Backend + backend::Text,
+{
+ type Style = Box<dyn StyleSheet>;
+
+ const DEFAULT_SIZE: u16 = 20;
+
+ fn draw(
+ &mut self,
+ bounds: Rectangle,
+ is_active: bool,
+ is_mouse_over: bool,
+ label: Option<Self::Output>,
+ style_sheet: &Self::Style,
+ ) -> Self::Output {
+ let style = if is_mouse_over {
+ style_sheet.hovered(is_active)
+ } else {
+ style_sheet.active(is_active)
+ };
+
+ let border_radius = bounds.height as f32 / BORDER_RADIUS_RATIO;
+ let space = SPACE_RATIO * bounds.height as f32;
+
+ let toggler_background_bounds = Rectangle {
+ x: bounds.x + space,
+ y: bounds.y + space,
+ width: bounds.width - (2.0 * space),
+ height: bounds.height - (2.0 * space),
+ };
+
+ let toggler_background = Primitive::Quad {
+ bounds: toggler_background_bounds,
+ background: style.background.into(),
+ border_radius,
+ border_width: 1.0,
+ border_color: style.background_border.unwrap_or(style.background),
+ };
+
+ let toggler_foreground_bounds = Rectangle {
+ x: bounds.x
+ + if is_active {
+ bounds.width - 2.0 * space - (bounds.height - (4.0 * space))
+ } else {
+ 2.0 * space
+ },
+ y: bounds.y + (2.0 * space),
+ width: bounds.height - (4.0 * space),
+ height: bounds.height - (4.0 * space),
+ };
+
+ let toggler_foreground = Primitive::Quad {
+ bounds: toggler_foreground_bounds,
+ background: style.foreground.into(),
+ border_radius,
+ border_width: 1.0,
+ border_color: style.foreground_border.unwrap_or(style.foreground),
+ };
+
+ (
+ Primitive::Group {
+ primitives: match label {
+ Some((l, _)) => {
+ vec![l, toggler_background, toggler_foreground]
+ }
+ None => vec![toggler_background, toggler_foreground],
+ },
+ },
+ if is_mouse_over {
+ mouse::Interaction::Pointer
+ } else {
+ mouse::Interaction::default()
+ },
+ )
+ }
+}
diff --git a/graphics/src/widget/tooltip.rs b/graphics/src/widget/tooltip.rs
new file mode 100644
index 00000000..493a6389
--- /dev/null
+++ b/graphics/src/widget/tooltip.rs
@@ -0,0 +1,168 @@
+//! Decorate content and apply alignment.
+use crate::backend::{self, Backend};
+use crate::defaults::{self, Defaults};
+use crate::{Primitive, Renderer, Vector};
+
+use iced_native::container;
+use iced_native::layout::{self, Layout};
+use iced_native::{Element, Padding, Point, Rectangle, Size, Text};
+
+/// An element decorating some content.
+///
+/// This is an alias of an `iced_native` tooltip with a default
+/// `Renderer`.
+pub type Tooltip<'a, Message, Backend> =
+ iced_native::Tooltip<'a, Message, Renderer<Backend>>;
+
+pub use iced_native::tooltip::Position;
+
+impl<B> iced_native::tooltip::Renderer for Renderer<B>
+where
+ B: Backend + backend::Text,
+{
+ const DEFAULT_PADDING: u16 = 5;
+
+ fn draw<Message>(
+ &mut self,
+ defaults: &Defaults,
+ cursor_position: Point,
+ content_layout: Layout<'_>,
+ viewport: &Rectangle,
+ content: &Element<'_, Message, Self>,
+ tooltip: &Text<Self>,
+ position: Position,
+ style_sheet: &<Self as container::Renderer>::Style,
+ gap: u16,
+ padding: u16,
+ ) -> Self::Output {
+ let (content, mouse_interaction) = content.draw(
+ self,
+ &defaults,
+ content_layout,
+ cursor_position,
+ viewport,
+ );
+
+ let bounds = content_layout.bounds();
+
+ if bounds.contains(cursor_position) {
+ use iced_native::Widget;
+
+ let gap = f32::from(gap);
+ let style = style_sheet.style();
+
+ let defaults = Defaults {
+ text: defaults::Text {
+ color: style.text_color.unwrap_or(defaults.text.color),
+ },
+ };
+
+ let text_layout = Widget::<(), Self>::layout(
+ tooltip,
+ self,
+ &layout::Limits::new(Size::ZERO, viewport.size())
+ .pad(Padding::new(padding)),
+ );
+
+ let padding = f32::from(padding);
+ let text_bounds = text_layout.bounds();
+ let x_center = bounds.x + (bounds.width - text_bounds.width) / 2.0;
+ let y_center =
+ bounds.y + (bounds.height - text_bounds.height) / 2.0;
+
+ let mut tooltip_bounds = {
+ let offset = match position {
+ Position::Top => Vector::new(
+ x_center,
+ bounds.y - text_bounds.height - gap - padding,
+ ),
+ Position::Bottom => Vector::new(
+ x_center,
+ bounds.y + bounds.height + gap + padding,
+ ),
+ Position::Left => Vector::new(
+ bounds.x - text_bounds.width - gap - padding,
+ y_center,
+ ),
+ Position::Right => Vector::new(
+ bounds.x + bounds.width + gap + padding,
+ y_center,
+ ),
+ Position::FollowCursor => Vector::new(
+ cursor_position.x,
+ cursor_position.y - text_bounds.height,
+ ),
+ };
+
+ Rectangle {
+ x: offset.x - padding,
+ y: offset.y - padding,
+ width: text_bounds.width + padding * 2.0,
+ height: text_bounds.height + padding * 2.0,
+ }
+ };
+
+ if tooltip_bounds.x < viewport.x {
+ tooltip_bounds.x = viewport.x;
+ } else if viewport.x + viewport.width
+ < tooltip_bounds.x + tooltip_bounds.width
+ {
+ tooltip_bounds.x =
+ viewport.x + viewport.width - tooltip_bounds.width;
+ }
+
+ if tooltip_bounds.y < viewport.y {
+ tooltip_bounds.y = viewport.y;
+ } else if viewport.y + viewport.height
+ < tooltip_bounds.y + tooltip_bounds.height
+ {
+ tooltip_bounds.y =
+ viewport.y + viewport.height - tooltip_bounds.height;
+ }
+
+ let (tooltip, _) = Widget::<(), Self>::draw(
+ tooltip,
+ self,
+ &defaults,
+ Layout::with_offset(
+ Vector::new(
+ tooltip_bounds.x + padding,
+ tooltip_bounds.y + padding,
+ ),
+ &text_layout,
+ ),
+ cursor_position,
+ viewport,
+ );
+
+ (
+ Primitive::Group {
+ primitives: vec![
+ content,
+ Primitive::Clip {
+ bounds: *viewport,
+ offset: Vector::new(0, 0),
+ content: Box::new(
+ if let Some(background) =
+ crate::container::background(
+ tooltip_bounds,
+ &style,
+ )
+ {
+ Primitive::Group {
+ primitives: vec![background, tooltip],
+ }
+ } else {
+ tooltip
+ },
+ ),
+ },
+ ],
+ },
+ mouse_interaction,
+ )
+ } else {
+ (content, mouse_interaction)
+ }
+ }
+}
diff --git a/graphics/src/window/compositor.rs b/graphics/src/window/compositor.rs
index 39485153..7342245c 100644
--- a/graphics/src/window/compositor.rs
+++ b/graphics/src/window/compositor.rs
@@ -17,7 +17,10 @@ pub trait Compositor: Sized {
type SwapChain;
/// Creates a new [`Compositor`].
- fn new(settings: Self::Settings) -> Result<(Self, Self::Renderer), Error>;
+ fn new<W: HasRawWindowHandle>(
+ settings: Self::Settings,
+ compatible_window: Option<&W>,
+ ) -> Result<(Self, Self::Renderer), Error>;
/// Crates a new [`Surface`] for the given window.
///
diff --git a/native/Cargo.toml b/native/Cargo.toml
index 2c99638a..a3134ef4 100644
--- a/native/Cargo.toml
+++ b/native/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "iced_native"
-version = "0.3.0"
+version = "0.4.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
description = "A renderer-agnostic library for native GUIs"
@@ -16,10 +16,10 @@ unicode-segmentation = "1.6"
num-traits = "0.2"
[dependencies.iced_core]
-version = "0.3"
+version = "0.4"
path = "../core"
[dependencies.iced_futures]
-version = "0.2"
+version = "0.3"
path = "../futures"
features = ["thread-pool"]
diff --git a/native/README.md b/native/README.md
index 6323dd4f..0d79690a 100644
--- a/native/README.md
+++ b/native/README.md
@@ -28,7 +28,7 @@ To achieve this, it introduces a bunch of reusable interfaces:
Add `iced_native` as a dependency in your `Cargo.toml`:
```toml
-iced_native = "0.3"
+iced_native = "0.4"
```
__Iced moves fast and the `master` branch can contain breaking changes!__ If
diff --git a/native/src/clipboard.rs b/native/src/clipboard.rs
index ecdccabf..081b4004 100644
--- a/native/src/clipboard.rs
+++ b/native/src/clipboard.rs
@@ -1,6 +1,23 @@
+//! Access the clipboard.
+
/// A buffer for short-term storage and transfer within and between
/// applications.
pub trait Clipboard {
- /// Returns the current content of the [`Clipboard`] as text.
- fn content(&self) -> Option<String>;
+ /// Reads the current content of the [`Clipboard`] as text.
+ fn read(&self) -> Option<String>;
+
+ /// Writes the given text contents to the [`Clipboard`].
+ fn write(&mut self, contents: String);
+}
+
+/// A null implementation of the [`Clipboard`] trait.
+#[derive(Debug, Clone, Copy)]
+pub struct Null;
+
+impl Clipboard for Null {
+ fn read(&self) -> Option<String> {
+ None
+ }
+
+ fn write(&mut self, _contents: String) {}
}
diff --git a/native/src/element.rs b/native/src/element.rs
index d6e9639a..5c84a388 100644
--- a/native/src/element.rs
+++ b/native/src/element.rs
@@ -223,17 +223,17 @@ where
event: Event,
layout: Layout<'_>,
cursor_position: Point,
- messages: &mut Vec<Message>,
renderer: &Renderer,
- clipboard: Option<&dyn Clipboard>,
+ clipboard: &mut dyn Clipboard,
+ messages: &mut Vec<Message>,
) -> event::Status {
self.widget.on_event(
event,
layout,
cursor_position,
- messages,
renderer,
clipboard,
+ messages,
)
}
@@ -311,9 +311,9 @@ where
event: Event,
layout: Layout<'_>,
cursor_position: Point,
- messages: &mut Vec<B>,
renderer: &Renderer,
- clipboard: Option<&dyn Clipboard>,
+ clipboard: &mut dyn Clipboard,
+ messages: &mut Vec<B>,
) -> event::Status {
let mut original_messages = Vec::new();
@@ -321,9 +321,9 @@ where
event,
layout,
cursor_position,
- &mut original_messages,
renderer,
clipboard,
+ &mut original_messages,
);
original_messages
@@ -401,17 +401,17 @@ where
event: Event,
layout: Layout<'_>,
cursor_position: Point,
- messages: &mut Vec<Message>,
renderer: &Renderer,
- clipboard: Option<&dyn Clipboard>,
+ clipboard: &mut dyn Clipboard,
+ messages: &mut Vec<Message>,
) -> event::Status {
self.element.widget.on_event(
event,
layout,
cursor_position,
- messages,
renderer,
clipboard,
+ messages,
)
}
diff --git a/native/src/event.rs b/native/src/event.rs
index 0e86171e..1c26b5f2 100644
--- a/native/src/event.rs
+++ b/native/src/event.rs
@@ -1,5 +1,8 @@
//! Handle events of a user interface.
-use crate::{keyboard, mouse, window};
+use crate::keyboard;
+use crate::mouse;
+use crate::touch;
+use crate::window;
/// A user interface event.
///
@@ -17,6 +20,30 @@ pub enum Event {
/// A window event
Window(window::Event),
+
+ /// A touch event
+ Touch(touch::Event),
+
+ /// A platform specific event
+ PlatformSpecific(PlatformSpecific),
+}
+
+/// A platform specific event
+#[derive(Debug, Clone, PartialEq)]
+pub enum PlatformSpecific {
+ /// A MacOS specific event
+ MacOS(MacOS),
+}
+
+/// Describes an event specific to MacOS
+#[derive(Debug, Clone, PartialEq)]
+pub enum MacOS {
+ /// Triggered when the app receives an URL from the system
+ ///
+ /// _**Note:** For this event to be triggered, the executable needs to be properly [bundled]!_
+ ///
+ /// [bundled]: https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW19
+ ReceivedUrl(String),
}
/// The status of an [`Event`] after being processed.
diff --git a/native/src/layout.rs b/native/src/layout.rs
index 6d144902..b4b4a021 100644
--- a/native/src/layout.rs
+++ b/native/src/layout.rs
@@ -19,11 +19,14 @@ pub struct Layout<'a> {
}
impl<'a> Layout<'a> {
- pub(crate) fn new(node: &'a Node) -> Self {
+ /// Creates a new [`Layout`] for the given [`Node`] at the origin.
+ pub fn new(node: &'a Node) -> Self {
Self::with_offset(Vector::new(0.0, 0.0), node)
}
- pub(crate) fn with_offset(offset: Vector, node: &'a Node) -> Self {
+ /// Creates a new [`Layout`] for the given [`Node`] with the provided offset
+ /// from the origin.
+ pub fn with_offset(offset: Vector, node: &'a Node) -> Self {
let bounds = node.bounds();
Self {
diff --git a/native/src/layout/flex.rs b/native/src/layout/flex.rs
index 4f6523fb..3d3ff82c 100644
--- a/native/src/layout/flex.rs
+++ b/native/src/layout/flex.rs
@@ -16,9 +16,10 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
+
use crate::{
layout::{Limits, Node},
- Align, Element, Point, Size,
+ Align, Element, Padding, Point, Size,
};
/// The main axis of a flex layout.
@@ -62,7 +63,7 @@ pub fn resolve<Message, Renderer>(
axis: Axis,
renderer: &Renderer,
limits: &Limits,
- padding: f32,
+ padding: Padding,
spacing: f32,
align_items: Align,
items: &[Element<'_, Message, Renderer>],
@@ -141,14 +142,15 @@ where
}
}
- let mut main = padding;
+ let pad = axis.pack(padding.left as f32, padding.top as f32);
+ let mut main = pad.0;
for (i, node) in nodes.iter_mut().enumerate() {
if i > 0 {
main += spacing;
}
- let (x, y) = axis.pack(main, padding);
+ let (x, y) = axis.pack(main, pad.1);
node.move_to(Point::new(x, y));
@@ -166,7 +168,7 @@ where
main += axis.main(size);
}
- let (width, height) = axis.pack(main - padding, cross);
+ let (width, height) = axis.pack(main - pad.0, cross);
let size = limits.resolve(Size::new(width, height));
Node::with_children(size.pad(padding), nodes)
diff --git a/native/src/layout/limits.rs b/native/src/layout/limits.rs
index a7bb5c9c..6d5f6563 100644
--- a/native/src/layout/limits.rs
+++ b/native/src/layout/limits.rs
@@ -1,4 +1,4 @@
-use crate::{Length, Size};
+use crate::{Length, Padding, Size};
/// A set of size constraints for layouting.
#[derive(Debug, Clone, Copy)]
@@ -117,8 +117,11 @@ impl Limits {
}
/// Shrinks the current [`Limits`] to account for the given padding.
- pub fn pad(&self, padding: f32) -> Limits {
- self.shrink(Size::new(padding * 2.0, padding * 2.0))
+ pub fn pad(&self, padding: Padding) -> Limits {
+ self.shrink(Size::new(
+ padding.horizontal() as f32,
+ padding.vertical() as f32,
+ ))
}
/// Shrinks the current [`Limits`] by the given [`Size`].
diff --git a/native/src/lib.rs b/native/src/lib.rs
index f9a99c48..cbb02506 100644
--- a/native/src/lib.rs
+++ b/native/src/lib.rs
@@ -33,6 +33,7 @@
#![deny(unused_results)]
#![forbid(unsafe_code)]
#![forbid(rust_2018_idioms)]
+pub mod clipboard;
pub mod event;
pub mod keyboard;
pub mod layout;
@@ -41,10 +42,10 @@ pub mod overlay;
pub mod program;
pub mod renderer;
pub mod subscription;
+pub mod touch;
pub mod widget;
pub mod window;
-mod clipboard;
mod element;
mod hasher;
mod runtime;
@@ -60,8 +61,8 @@ mod debug;
mod debug;
pub use iced_core::{
- Align, Background, Color, Font, HorizontalAlignment, Length, Point,
- Rectangle, Size, Vector, VerticalAlignment,
+ menu, Align, Background, Color, Font, HorizontalAlignment, Length, Menu,
+ Padding, Point, Rectangle, Size, Vector, VerticalAlignment,
};
pub use iced_futures::{executor, futures, Command};
diff --git a/native/src/overlay.rs b/native/src/overlay.rs
index ea8bb384..84145e7f 100644
--- a/native/src/overlay.rs
+++ b/native/src/overlay.rs
@@ -67,9 +67,9 @@ where
_event: Event,
_layout: Layout<'_>,
_cursor_position: Point,
- _messages: &mut Vec<Message>,
_renderer: &Renderer,
- _clipboard: Option<&dyn Clipboard>,
+ _clipboard: &mut dyn Clipboard,
+ _messages: &mut Vec<Message>,
) -> event::Status {
event::Status::Ignored
}
diff --git a/native/src/overlay/element.rs b/native/src/overlay/element.rs
index 0f44a781..e4819037 100644
--- a/native/src/overlay/element.rs
+++ b/native/src/overlay/element.rs
@@ -53,17 +53,17 @@ where
event: Event,
layout: Layout<'_>,
cursor_position: Point,
- messages: &mut Vec<Message>,
renderer: &Renderer,
- clipboard: Option<&dyn Clipboard>,
+ clipboard: &mut dyn Clipboard,
+ messages: &mut Vec<Message>,
) -> event::Status {
self.overlay.on_event(
event,
layout,
cursor_position,
- messages,
renderer,
clipboard,
+ messages,
)
}
@@ -117,9 +117,9 @@ where
event: Event,
layout: Layout<'_>,
cursor_position: Point,
- messages: &mut Vec<B>,
renderer: &Renderer,
- clipboard: Option<&dyn Clipboard>,
+ clipboard: &mut dyn Clipboard,
+ messages: &mut Vec<B>,
) -> event::Status {
let mut original_messages = Vec::new();
@@ -127,9 +127,9 @@ where
event,
layout,
cursor_position,
- &mut original_messages,
renderer,
clipboard,
+ &mut original_messages,
);
original_messages
diff --git a/native/src/overlay/menu.rs b/native/src/overlay/menu.rs
index abac849f..f62dcb46 100644
--- a/native/src/overlay/menu.rs
+++ b/native/src/overlay/menu.rs
@@ -6,9 +6,10 @@ use crate::mouse;
use crate::overlay;
use crate::scrollable;
use crate::text;
+use crate::touch;
use crate::{
- Clipboard, Container, Element, Hasher, Layout, Length, Point, Rectangle,
- Scrollable, Size, Vector, Widget,
+ Clipboard, Container, Element, Hasher, Layout, Length, Padding, Point,
+ Rectangle, Scrollable, Size, Vector, Widget,
};
/// A list of selectable options.
@@ -19,7 +20,7 @@ pub struct Menu<'a, T, Renderer: self::Renderer> {
hovered_option: &'a mut Option<usize>,
last_selection: &'a mut Option<T>,
width: u16,
- padding: u16,
+ padding: Padding,
text_size: Option<u16>,
font: Renderer::Font,
style: <Renderer as self::Renderer>::Style,
@@ -44,7 +45,7 @@ where
hovered_option,
last_selection,
width: 0,
- padding: 0,
+ padding: Padding::ZERO,
text_size: None,
font: Default::default(),
style: Default::default(),
@@ -57,9 +58,9 @@ where
self
}
- /// Sets the padding of the [`Menu`].
- pub fn padding(mut self, padding: u16) -> Self {
- self.padding = padding;
+ /// Sets the [`Padding`] of the [`Menu`].
+ pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
+ self.padding = padding.into();
self
}
@@ -218,17 +219,17 @@ where
event: Event,
layout: Layout<'_>,
cursor_position: Point,
- messages: &mut Vec<Message>,
renderer: &Renderer,
- clipboard: Option<&dyn Clipboard>,
+ clipboard: &mut dyn Clipboard,
+ messages: &mut Vec<Message>,
) -> event::Status {
self.container.on_event(
event.clone(),
layout,
cursor_position,
- messages,
renderer,
clipboard,
+ messages,
)
}
@@ -260,7 +261,7 @@ struct List<'a, T, Renderer: self::Renderer> {
options: &'a [T],
hovered_option: &'a mut Option<usize>,
last_selection: &'a mut Option<T>,
- padding: u16,
+ padding: Padding,
text_size: Option<u16>,
font: Renderer::Font,
style: <Renderer as self::Renderer>::Style,
@@ -293,7 +294,7 @@ where
let size = {
let intrinsic = Size::new(
0.0,
- f32::from(text_size + self.padding * 2)
+ f32::from(text_size + self.padding.vertical())
* self.options.len() as f32,
);
@@ -319,9 +320,9 @@ where
event: Event,
layout: Layout<'_>,
cursor_position: Point,
- _messages: &mut Vec<Message>,
renderer: &Renderer,
- _clipboard: Option<&dyn Clipboard>,
+ _clipboard: &mut dyn Clipboard,
+ _messages: &mut Vec<Message>,
) -> event::Status {
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
@@ -337,17 +338,38 @@ where
}
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
let bounds = layout.bounds();
- let text_size =
- self.text_size.unwrap_or(renderer.default_size());
if bounds.contains(cursor_position) {
+ let text_size =
+ self.text_size.unwrap_or(renderer.default_size());
+
*self.hovered_option = Some(
((cursor_position.y - bounds.y)
- / f32::from(text_size + self.padding * 2))
+ / f32::from(text_size + self.padding.vertical()))
as usize,
);
}
}
+ Event::Touch(touch::Event::FingerPressed { .. }) => {
+ let bounds = layout.bounds();
+
+ if bounds.contains(cursor_position) {
+ let text_size =
+ self.text_size.unwrap_or(renderer.default_size());
+
+ *self.hovered_option = Some(
+ ((cursor_position.y - bounds.y)
+ / f32::from(text_size + self.padding.vertical()))
+ as usize,
+ );
+
+ if let Some(index) = *self.hovered_option {
+ if let Some(option) = self.options.get(index) {
+ *self.last_selection = Some(option.clone());
+ }
+ }
+ }
+ }
_ => {}
}
@@ -408,7 +430,7 @@ pub trait Renderer:
viewport: &Rectangle,
options: &[T],
hovered_option: Option<usize>,
- padding: u16,
+ padding: Padding,
text_size: u16,
font: Self::Font,
style: &<Self as Renderer>::Style,
diff --git a/native/src/program.rs b/native/src/program.rs
index 9ee72703..75fab094 100644
--- a/native/src/program.rs
+++ b/native/src/program.rs
@@ -1,5 +1,5 @@
//! Build interactive programs using The Elm Architecture.
-use crate::{Command, Element, Renderer};
+use crate::{Clipboard, Command, Element, Renderer};
mod state;
@@ -11,7 +11,10 @@ pub trait Program: Sized {
type Renderer: Renderer;
/// The type of __messages__ your [`Program`] will produce.
- type Message: std::fmt::Debug + Send;
+ type Message: std::fmt::Debug + Clone + Send;
+
+ /// The type of [`Clipboard`] your [`Program`] will use.
+ type Clipboard: Clipboard;
/// Handles a __message__ and updates the state of the [`Program`].
///
@@ -21,7 +24,11 @@ pub trait Program: Sized {
///
/// Any [`Command`] returned will be executed immediately in the
/// background by shells.
- fn update(&mut self, message: Self::Message) -> Command<Self::Message>;
+ fn update(
+ &mut self,
+ message: Self::Message,
+ clipboard: &mut Self::Clipboard,
+ ) -> Command<Self::Message>;
/// Returns the widgets to display in the [`Program`].
///
diff --git a/native/src/program/state.rs b/native/src/program/state.rs
index e630890a..fd1f2b52 100644
--- a/native/src/program/state.rs
+++ b/native/src/program/state.rs
@@ -1,6 +1,5 @@
use crate::{
- Cache, Clipboard, Command, Debug, Event, Point, Program, Renderer, Size,
- UserInterface,
+ Cache, Command, Debug, Event, Point, Program, Renderer, Size, UserInterface,
};
/// The execution state of a [`Program`]. It leverages caching, event
@@ -91,8 +90,8 @@ where
&mut self,
bounds: Size,
cursor_position: Point,
- clipboard: Option<&dyn Clipboard>,
renderer: &mut P::Renderer,
+ clipboard: &mut P::Clipboard,
debug: &mut Debug,
) -> Option<Command<P::Message>> {
let mut user_interface = build_user_interface(
@@ -109,8 +108,8 @@ where
let _ = user_interface.update(
&self.queued_events,
cursor_position,
- clipboard,
renderer,
+ clipboard,
&mut messages,
);
@@ -136,7 +135,7 @@ where
debug.log_message(&message);
debug.update_started();
- let command = self.program.update(message);
+ let command = self.program.update(message, clipboard);
debug.update_finished();
command
diff --git a/native/src/renderer/null.rs b/native/src/renderer/null.rs
index 91ee9a28..bb57c163 100644
--- a/native/src/renderer/null.rs
+++ b/native/src/renderer/null.rs
@@ -1,7 +1,7 @@
use crate::{
button, checkbox, column, container, pane_grid, progress_bar, radio, row,
- scrollable, slider, text, text_input, Color, Element, Font,
- HorizontalAlignment, Layout, Point, Rectangle, Renderer, Size,
+ scrollable, slider, text, text_input, toggler, Color, Element, Font,
+ HorizontalAlignment, Layout, Padding, Point, Rectangle, Renderer, Size,
VerticalAlignment,
};
@@ -145,7 +145,7 @@ impl text_input::Renderer for Null {
}
impl button::Renderer for Null {
- const DEFAULT_PADDING: u16 = 0;
+ const DEFAULT_PADDING: Padding = Padding::ZERO;
type Style = ();
@@ -246,14 +246,18 @@ impl container::Renderer for Null {
}
impl pane_grid::Renderer for Null {
+ type Style = ();
+
fn draw<Message>(
&mut self,
_defaults: &Self::Defaults,
_content: &[(pane_grid::Pane, pane_grid::Content<'_, Message, Self>)],
_dragging: Option<(pane_grid::Pane, Point)>,
- _resizing: Option<pane_grid::Axis>,
+ _resizing: Option<(pane_grid::Axis, Rectangle, bool)>,
_layout: Layout<'_>,
+ _style: &<Self as pane_grid::Renderer>::Style,
_cursor_position: Point,
+ _viewport: &Rectangle,
) {
}
@@ -261,13 +265,14 @@ impl pane_grid::Renderer for Null {
&mut self,
_defaults: &Self::Defaults,
_bounds: Rectangle,
- _style: &Self::Style,
+ _style: &<Self as container::Renderer>::Style,
_title_bar: Option<(
&pane_grid::TitleBar<'_, Message, Self>,
Layout<'_>,
)>,
_body: (&Element<'_, Message, Self>, Layout<'_>),
_cursor_position: Point,
+ _viewport: &Rectangle,
) {
}
@@ -275,13 +280,27 @@ impl pane_grid::Renderer for Null {
&mut self,
_defaults: &Self::Defaults,
_bounds: Rectangle,
- _style: &Self::Style,
- _title: &str,
- _title_size: u16,
- _title_font: Self::Font,
- _title_bounds: Rectangle,
+ _style: &<Self as container::Renderer>::Style,
+ _content: (&Element<'_, Message, Self>, Layout<'_>),
_controls: Option<(&Element<'_, Message, Self>, Layout<'_>)>,
_cursor_position: Point,
+ _viewport: &Rectangle,
+ ) {
+ }
+}
+
+impl toggler::Renderer for Null {
+ type Style = ();
+
+ const DEFAULT_SIZE: u16 = 20;
+
+ fn draw(
+ &mut self,
+ _bounds: Rectangle,
+ _is_checked: bool,
+ _is_mouse_over: bool,
+ _label: Option<Self::Output>,
+ _style: &Self::Style,
) {
}
}
diff --git a/native/src/touch.rs b/native/src/touch.rs
new file mode 100644
index 00000000..18120644
--- /dev/null
+++ b/native/src/touch.rs
@@ -0,0 +1,23 @@
+//! Build touch events.
+use crate::Point;
+
+/// A touch interaction.
+#[derive(Debug, Clone, Copy, PartialEq)]
+#[allow(missing_docs)]
+pub enum Event {
+ /// A touch interaction was started.
+ FingerPressed { id: Finger, position: Point },
+
+ /// An on-going touch interaction was moved.
+ FingerMoved { id: Finger, position: Point },
+
+ /// A touch interaction was ended.
+ FingerLifted { id: Finger, position: Point },
+
+ /// A touch interaction was canceled.
+ FingerLost { id: Finger, position: Point },
+}
+
+/// A unique identifier representing a finger on a touch interaction.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct Finger(pub u64);
diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs
index 7a64ac59..8e0d7d1c 100644
--- a/native/src/user_interface.rs
+++ b/native/src/user_interface.rs
@@ -16,7 +16,7 @@ use std::hash::Hasher;
/// The [`integration` example] uses a [`UserInterface`] to integrate Iced in
/// an existing graphical application.
///
-/// [`integration` example]: https://github.com/hecrj/iced/tree/0.2/examples/integration
+/// [`integration` example]: https://github.com/hecrj/iced/tree/0.3/examples/integration
#[allow(missing_debug_implementations)]
pub struct UserInterface<'a, Message, Renderer> {
root: Element<'a, Message, Renderer>,
@@ -134,7 +134,7 @@ where
/// completing [the previous example](#example):
///
/// ```no_run
- /// use iced_native::{UserInterface, Cache, Size, Point};
+ /// use iced_native::{clipboard, UserInterface, Cache, Size, Point};
/// use iced_wgpu::Renderer;
///
/// # mod iced_wgpu {
@@ -157,6 +157,7 @@ where
/// let mut renderer = Renderer::new();
/// let mut window_size = Size::new(1024.0, 768.0);
/// let mut cursor_position = Point::default();
+ /// let mut clipboard = clipboard::Null;
///
/// // Initialize our event storage
/// let mut events = Vec::new();
@@ -176,8 +177,8 @@ where
/// let event_statuses = user_interface.update(
/// &events,
/// cursor_position,
- /// None,
/// &renderer,
+ /// &mut clipboard,
/// &mut messages
/// );
///
@@ -193,8 +194,8 @@ where
&mut self,
events: &[Event],
cursor_position: Point,
- clipboard: Option<&dyn Clipboard>,
renderer: &Renderer,
+ clipboard: &mut dyn Clipboard,
messages: &mut Vec<Message>,
) -> Vec<event::Status> {
let (base_cursor, overlay_statuses) = if let Some(mut overlay) =
@@ -215,9 +216,9 @@ where
event,
Layout::new(&layer.layout),
cursor_position,
- messages,
renderer,
clipboard,
+ messages,
)
})
.collect();
@@ -246,9 +247,9 @@ where
event,
Layout::new(&self.base.layout),
base_cursor,
- messages,
renderer,
clipboard,
+ messages,
);
event_status.merge(overlay_status)
@@ -269,7 +270,7 @@ where
/// [completing the last example](#example-1):
///
/// ```no_run
- /// use iced_native::{UserInterface, Cache, Size, Point};
+ /// use iced_native::{clipboard, UserInterface, Cache, Size, Point};
/// use iced_wgpu::Renderer;
///
/// # mod iced_wgpu {
@@ -292,6 +293,7 @@ where
/// let mut renderer = Renderer::new();
/// let mut window_size = Size::new(1024.0, 768.0);
/// let mut cursor_position = Point::default();
+ /// let mut clipboard = clipboard::Null;
/// let mut events = Vec::new();
/// let mut messages = Vec::new();
///
@@ -309,8 +311,8 @@ where
/// let event_statuses = user_interface.update(
/// &events,
/// cursor_position,
- /// None,
/// &renderer,
+ /// &mut clipboard,
/// &mut messages
/// );
///
diff --git a/native/src/widget.rs b/native/src/widget.rs
index 3677713a..43c1b023 100644
--- a/native/src/widget.rs
+++ b/native/src/widget.rs
@@ -36,6 +36,8 @@ pub mod space;
pub mod svg;
pub mod text;
pub mod text_input;
+pub mod toggler;
+pub mod tooltip;
#[doc(no_inline)]
pub use button::Button;
@@ -71,6 +73,10 @@ pub use svg::Svg;
pub use text::Text;
#[doc(no_inline)]
pub use text_input::TextInput;
+#[doc(no_inline)]
+pub use toggler::Toggler;
+#[doc(no_inline)]
+pub use tooltip::Tooltip;
use crate::event::{self, Event};
use crate::layout;
@@ -93,12 +99,12 @@ use crate::{Clipboard, Hasher, Layout, Length, Point, Rectangle};
/// - [`geometry`], a custom widget showcasing how to draw geometry with the
/// `Mesh2D` primitive in [`iced_wgpu`].
///
-/// [examples]: https://github.com/hecrj/iced/tree/0.2/examples
-/// [`bezier_tool`]: https://github.com/hecrj/iced/tree/0.2/examples/bezier_tool
-/// [`custom_widget`]: https://github.com/hecrj/iced/tree/0.2/examples/custom_widget
-/// [`geometry`]: https://github.com/hecrj/iced/tree/0.2/examples/geometry
+/// [examples]: https://github.com/hecrj/iced/tree/0.3/examples
+/// [`bezier_tool`]: https://github.com/hecrj/iced/tree/0.3/examples/bezier_tool
+/// [`custom_widget`]: https://github.com/hecrj/iced/tree/0.3/examples/custom_widget
+/// [`geometry`]: https://github.com/hecrj/iced/tree/0.3/examples/geometry
/// [`lyon`]: https://github.com/nical/lyon
-/// [`iced_wgpu`]: https://github.com/hecrj/iced/tree/0.2/wgpu
+/// [`iced_wgpu`]: https://github.com/hecrj/iced/tree/0.3/wgpu
pub trait Widget<Message, Renderer>
where
Renderer: crate::Renderer,
@@ -161,9 +167,9 @@ where
_event: Event,
_layout: Layout<'_>,
_cursor_position: Point,
- _messages: &mut Vec<Message>,
_renderer: &Renderer,
- _clipboard: Option<&dyn Clipboard>,
+ _clipboard: &mut dyn Clipboard,
+ _messages: &mut Vec<Message>,
) -> event::Status {
event::Status::Ignored
}
diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs
index dca20e13..c469a0e5 100644
--- a/native/src/widget/button.rs
+++ b/native/src/widget/button.rs
@@ -4,8 +4,11 @@
use crate::event::{self, Event};
use crate::layout;
use crate::mouse;
+use crate::overlay;
+use crate::touch;
use crate::{
- Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget,
+ Clipboard, Element, Hasher, Layout, Length, Padding, Point, Rectangle,
+ Widget,
};
use std::hash::Hash;
@@ -26,6 +29,29 @@ use std::hash::Hash;
/// let button = Button::new(&mut state, Text::new("Press me!"))
/// .on_press(Message::ButtonPressed);
/// ```
+///
+/// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will
+/// be disabled:
+///
+/// ```
+/// # use iced_native::{button, Text};
+/// #
+/// # type Button<'a, Message> =
+/// # iced_native::Button<'a, Message, iced_native::renderer::Null>;
+/// #
+/// #[derive(Clone)]
+/// enum Message {
+/// ButtonPressed,
+/// }
+///
+/// fn disabled_button(state: &mut button::State) -> Button<'_, Message> {
+/// Button::new(state, Text::new("I'm disabled!"))
+/// }
+///
+/// fn enabled_button(state: &mut button::State) -> Button<'_, Message> {
+/// disabled_button(state).on_press(Message::ButtonPressed)
+/// }
+/// ```
#[allow(missing_debug_implementations)]
pub struct Button<'a, Message, Renderer: self::Renderer> {
state: &'a mut State,
@@ -35,7 +61,7 @@ pub struct Button<'a, Message, Renderer: self::Renderer> {
height: Length,
min_width: u32,
min_height: u32,
- padding: u16,
+ padding: Padding,
style: Renderer::Style,
}
@@ -87,13 +113,14 @@ where
self
}
- /// Sets the padding of the [`Button`].
- pub fn padding(mut self, padding: u16) -> Self {
- self.padding = padding;
+ /// Sets the [`Padding`] of the [`Button`].
+ pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
+ self.padding = padding.into();
self
}
/// Sets the message that will be produced when the [`Button`] is pressed.
+ /// If on_press isn't set, button will be disabled.
pub fn on_press(mut self, msg: Message) -> Self {
self.on_press = Some(msg);
self
@@ -138,18 +165,20 @@ where
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let padding = f32::from(self.padding);
let limits = limits
.min_width(self.min_width)
.min_height(self.min_height)
.width(self.width)
.height(self.height)
- .pad(padding);
+ .pad(self.padding);
let mut content = self.content.layout(renderer, &limits);
- content.move_to(Point::new(padding, padding));
+ content.move_to(Point::new(
+ self.padding.left.into(),
+ self.padding.top.into(),
+ ));
- let size = limits.resolve(content.size()).pad(padding);
+ let size = limits.resolve(content.size()).pad(self.padding);
layout::Node::with_children(size, vec![content])
}
@@ -159,12 +188,24 @@ where
event: Event,
layout: Layout<'_>,
cursor_position: Point,
+ renderer: &Renderer,
+ clipboard: &mut dyn Clipboard,
messages: &mut Vec<Message>,
- _renderer: &Renderer,
- _clipboard: Option<&dyn Clipboard>,
) -> event::Status {
+ if let event::Status::Captured = self.content.on_event(
+ event.clone(),
+ layout.children().next().unwrap(),
+ cursor_position,
+ renderer,
+ clipboard,
+ messages,
+ ) {
+ return event::Status::Captured;
+ }
+
match event {
- Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
+ | Event::Touch(touch::Event::FingerPressed { .. }) => {
if self.on_press.is_some() {
let bounds = layout.bounds();
@@ -175,7 +216,8 @@ where
}
}
}
- Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
+ Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
+ | Event::Touch(touch::Event::FingerLifted { .. }) => {
if let Some(on_press) = self.on_press.clone() {
let bounds = layout.bounds();
@@ -190,6 +232,9 @@ where
}
}
}
+ Event::Touch(touch::Event::FingerLost { .. }) => {
+ self.state.is_pressed = false;
+ }
_ => {}
}
@@ -223,6 +268,13 @@ where
self.width.hash(state);
self.content.hash_layout(state);
}
+
+ fn overlay(
+ &mut self,
+ layout: Layout<'_>,
+ ) -> Option<overlay::Element<'_, Message, Renderer>> {
+ self.content.overlay(layout.children().next().unwrap())
+ }
}
/// The renderer of a [`Button`].
@@ -233,7 +285,7 @@ where
/// [renderer]: crate::renderer
pub trait Renderer: crate::Renderer + Sized {
/// The default padding of a [`Button`].
- const DEFAULT_PADDING: u16;
+ const DEFAULT_PADDING: Padding;
/// The style supported by this renderer.
type Style: Default;
diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs
index 81420458..0f21c873 100644
--- a/native/src/widget/checkbox.rs
+++ b/native/src/widget/checkbox.rs
@@ -6,9 +6,10 @@ use crate::layout;
use crate::mouse;
use crate::row;
use crate::text;
+use crate::touch;
use crate::{
- Align, Clipboard, Element, Hasher, HorizontalAlignment, Layout, Length,
- Point, Rectangle, Row, Text, VerticalAlignment, Widget,
+ Align, Clipboard, Color, Element, Hasher, HorizontalAlignment, Layout,
+ Length, Point, Rectangle, Row, Text, VerticalAlignment, Widget,
};
/// A box that can be checked.
@@ -38,6 +39,7 @@ pub struct Checkbox<Message, Renderer: self::Renderer + text::Renderer> {
spacing: u16,
text_size: Option<u16>,
font: Renderer::Font,
+ text_color: Option<Color>,
style: Renderer::Style,
}
@@ -65,6 +67,7 @@ impl<Message, Renderer: self::Renderer + text::Renderer>
spacing: Renderer::DEFAULT_SPACING,
text_size: None,
font: Renderer::Font::default(),
+ text_color: None,
style: Renderer::Style::default(),
}
}
@@ -101,6 +104,12 @@ impl<Message, Renderer: self::Renderer + text::Renderer>
self
}
+ /// Sets the text color of the [`Checkbox`] button.
+ pub fn text_color(mut self, color: Color) -> Self {
+ self.text_color = Some(color);
+ self
+ }
+
/// Sets the style of the [`Checkbox`].
pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {
self.style = style.into();
@@ -149,12 +158,13 @@ where
event: Event,
layout: Layout<'_>,
cursor_position: Point,
- messages: &mut Vec<Message>,
_renderer: &Renderer,
- _clipboard: Option<&dyn Clipboard>,
+ _clipboard: &mut dyn Clipboard,
+ messages: &mut Vec<Message>,
) -> event::Status {
match event {
- Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
+ | Event::Touch(touch::Event::FingerPressed { .. }) => {
let mouse_over = layout.bounds().contains(cursor_position);
if mouse_over {
@@ -191,7 +201,7 @@ where
&self.label,
self.text_size.unwrap_or(renderer.default_size()),
self.font,
- None,
+ self.text_color,
HorizontalAlignment::Left,
VerticalAlignment::Center,
);
diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs
index e0e88d31..52a2e80c 100644
--- a/native/src/widget/column.rs
+++ b/native/src/widget/column.rs
@@ -5,7 +5,8 @@ use crate::event::{self, Event};
use crate::layout;
use crate::overlay;
use crate::{
- Align, Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget,
+ Align, Clipboard, Element, Hasher, Layout, Length, Padding, Point,
+ Rectangle, Widget,
};
use std::u32;
@@ -14,7 +15,7 @@ use std::u32;
#[allow(missing_debug_implementations)]
pub struct Column<'a, Message, Renderer> {
spacing: u16,
- padding: u16,
+ padding: Padding,
width: Length,
height: Length,
max_width: u32,
@@ -35,7 +36,7 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> {
) -> Self {
Column {
spacing: 0,
- padding: 0,
+ padding: Padding::ZERO,
width: Length::Shrink,
height: Length::Shrink,
max_width: u32::MAX,
@@ -55,9 +56,9 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> {
self
}
- /// Sets the padding of the [`Column`].
- pub fn padding(mut self, units: u16) -> Self {
- self.padding = units;
+ /// Sets the [`Padding`] of the [`Column`].
+ pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
+ self.padding = padding.into();
self
}
@@ -129,7 +130,7 @@ where
layout::flex::Axis::Vertical,
renderer,
&limits,
- self.padding as f32,
+ self.padding,
self.spacing as f32,
self.align_items,
&self.children,
@@ -141,9 +142,9 @@ where
event: Event,
layout: Layout<'_>,
cursor_position: Point,
- messages: &mut Vec<Message>,
renderer: &Renderer,
- clipboard: Option<&dyn Clipboard>,
+ clipboard: &mut dyn Clipboard,
+ messages: &mut Vec<Message>,
) -> event::Status {
self.children
.iter_mut()
@@ -153,9 +154,9 @@ where
event.clone(),
layout,
cursor_position,
- messages,
renderer,
clipboard,
+ messages,
)
})
.fold(event::Status::Ignored, event::Status::merge)
diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs
index 65764148..69aee64d 100644
--- a/native/src/widget/container.rs
+++ b/native/src/widget/container.rs
@@ -5,7 +5,8 @@ use crate::event::{self, Event};
use crate::layout;
use crate::overlay;
use crate::{
- Align, Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget,
+ Align, Clipboard, Element, Hasher, Layout, Length, Padding, Point,
+ Rectangle, Widget,
};
use std::u32;
@@ -15,7 +16,7 @@ use std::u32;
/// It is normally used for alignment purposes.
#[allow(missing_debug_implementations)]
pub struct Container<'a, Message, Renderer: self::Renderer> {
- padding: u16,
+ padding: Padding,
width: Length,
height: Length,
max_width: u32,
@@ -36,7 +37,7 @@ where
T: Into<Element<'a, Message, Renderer>>,
{
Container {
- padding: 0,
+ padding: Padding::ZERO,
width: Length::Shrink,
height: Length::Shrink,
max_width: u32::MAX,
@@ -48,9 +49,9 @@ where
}
}
- /// Sets the padding of the [`Container`].
- pub fn padding(mut self, units: u16) -> Self {
- self.padding = units;
+ /// Sets the [`Padding`] of the [`Container`].
+ pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
+ self.padding = padding.into();
self
}
@@ -127,23 +128,24 @@ where
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let padding = f32::from(self.padding);
-
let limits = limits
.loose()
.max_width(self.max_width)
.max_height(self.max_height)
.width(self.width)
.height(self.height)
- .pad(padding);
+ .pad(self.padding);
let mut content = self.content.layout(renderer, &limits.loose());
let size = limits.resolve(content.size());
- content.move_to(Point::new(padding, padding));
+ content.move_to(Point::new(
+ self.padding.left.into(),
+ self.padding.top.into(),
+ ));
content.align(self.horizontal_alignment, self.vertical_alignment, size);
- layout::Node::with_children(size.pad(padding), vec![content])
+ layout::Node::with_children(size.pad(self.padding), vec![content])
}
fn on_event(
@@ -151,17 +153,17 @@ where
event: Event,
layout: Layout<'_>,
cursor_position: Point,
- messages: &mut Vec<Message>,
renderer: &Renderer,
- clipboard: Option<&dyn Clipboard>,
+ clipboard: &mut dyn Clipboard,
+ messages: &mut Vec<Message>,
) -> event::Status {
self.content.widget.on_event(
event,
layout.children().next().unwrap(),
cursor_position,
- messages,
renderer,
clipboard,
+ messages,
)
}
diff --git a/native/src/widget/image.rs b/native/src/widget/image.rs
index 51d7ba26..4d8e0a3f 100644
--- a/native/src/widget/image.rs
+++ b/native/src/widget/image.rs
@@ -1,4 +1,7 @@
//! Display images in your user interface.
+pub mod viewer;
+pub use viewer::Viewer;
+
use crate::layout;
use crate::{Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget};
diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs
new file mode 100644
index 00000000..405daf00
--- /dev/null
+++ b/native/src/widget/image/viewer.rs
@@ -0,0 +1,413 @@
+//! Zoom and pan on an image.
+use crate::event::{self, Event};
+use crate::image;
+use crate::layout;
+use crate::mouse;
+use crate::{
+ Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Vector,
+ Widget,
+};
+
+use std::hash::Hash;
+
+/// A frame that displays an image with the ability to zoom in/out and pan.
+#[allow(missing_debug_implementations)]
+pub struct Viewer<'a> {
+ state: &'a mut State,
+ padding: u16,
+ width: Length,
+ height: Length,
+ min_scale: f32,
+ max_scale: f32,
+ scale_step: f32,
+ handle: image::Handle,
+}
+
+impl<'a> Viewer<'a> {
+ /// Creates a new [`Viewer`] with the given [`State`] and [`Handle`].
+ ///
+ /// [`Handle`]: image::Handle
+ pub fn new(state: &'a mut State, handle: image::Handle) -> Self {
+ Viewer {
+ state,
+ padding: 0,
+ width: Length::Shrink,
+ height: Length::Shrink,
+ min_scale: 0.25,
+ max_scale: 10.0,
+ scale_step: 0.10,
+ handle,
+ }
+ }
+
+ /// Sets the padding of the [`Viewer`].
+ pub fn padding(mut self, units: u16) -> Self {
+ self.padding = units;
+ self
+ }
+
+ /// Sets the width of the [`Viewer`].
+ pub fn width(mut self, width: Length) -> Self {
+ self.width = width;
+ self
+ }
+
+ /// Sets the height of the [`Viewer`].
+ pub fn height(mut self, height: Length) -> Self {
+ self.height = height;
+ self
+ }
+
+ /// Sets the max scale applied to the image of the [`Viewer`].
+ ///
+ /// Default is `10.0`
+ pub fn max_scale(mut self, max_scale: f32) -> Self {
+ self.max_scale = max_scale;
+ self
+ }
+
+ /// Sets the min scale applied to the image of the [`Viewer`].
+ ///
+ /// Default is `0.25`
+ pub fn min_scale(mut self, min_scale: f32) -> Self {
+ self.min_scale = min_scale;
+ self
+ }
+
+ /// Sets the percentage the image of the [`Viewer`] will be scaled by
+ /// when zoomed in / out.
+ ///
+ /// Default is `0.10`
+ pub fn scale_step(mut self, scale_step: f32) -> Self {
+ self.scale_step = scale_step;
+ self
+ }
+
+ /// Returns the bounds of the underlying image, given the bounds of
+ /// the [`Viewer`]. Scaling will be applied and original aspect ratio
+ /// will be respected.
+ fn image_size<Renderer>(&self, renderer: &Renderer, bounds: Size) -> Size
+ where
+ Renderer: self::Renderer + image::Renderer,
+ {
+ let (width, height) = renderer.dimensions(&self.handle);
+
+ let (width, height) = {
+ let dimensions = (width as f32, height as f32);
+
+ let width_ratio = bounds.width / dimensions.0;
+ let height_ratio = bounds.height / dimensions.1;
+
+ let ratio = width_ratio.min(height_ratio);
+
+ let scale = self.state.scale;
+
+ if ratio < 1.0 {
+ (dimensions.0 * ratio * scale, dimensions.1 * ratio * scale)
+ } else {
+ (dimensions.0 * scale, dimensions.1 * scale)
+ }
+ };
+
+ Size::new(width, height)
+ }
+}
+
+impl<'a, Message, Renderer> Widget<Message, Renderer> for Viewer<'a>
+where
+ Renderer: self::Renderer + image::Renderer,
+{
+ fn width(&self) -> Length {
+ self.width
+ }
+
+ fn height(&self) -> Length {
+ self.height
+ }
+
+ fn layout(
+ &self,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ let (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 expansion_size = if height > width {
+ self.width
+ } else {
+ self.height
+ };
+
+ // Only calculate viewport sizes if the images are constrained to a limited space.
+ // If they are Fill|Portion let them expand within their alotted space.
+ match expansion_size {
+ Length::Shrink | Length::Units(_) => {
+ let aspect_ratio = width as f32 / height as f32;
+ let viewport_aspect_ratio = size.width / size.height;
+ if viewport_aspect_ratio > aspect_ratio {
+ size.width = width as f32 * size.height / height as f32;
+ } else {
+ size.height = height as f32 * size.width / width as f32;
+ }
+ }
+ Length::Fill | Length::FillPortion(_) => {}
+ }
+
+ layout::Node::new(size)
+ }
+
+ fn on_event(
+ &mut self,
+ event: Event,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ renderer: &Renderer,
+ _clipboard: &mut dyn Clipboard,
+ _messages: &mut Vec<Message>,
+ ) -> event::Status {
+ let bounds = layout.bounds();
+ let is_mouse_over = bounds.contains(cursor_position);
+
+ match event {
+ Event::Mouse(mouse::Event::WheelScrolled { delta })
+ if is_mouse_over =>
+ {
+ match delta {
+ mouse::ScrollDelta::Lines { y, .. }
+ | mouse::ScrollDelta::Pixels { y, .. } => {
+ let previous_scale = self.state.scale;
+
+ if y < 0.0 && previous_scale > self.min_scale
+ || y > 0.0 && previous_scale < self.max_scale
+ {
+ self.state.scale = (if y > 0.0 {
+ self.state.scale * (1.0 + self.scale_step)
+ } else {
+ self.state.scale / (1.0 + self.scale_step)
+ })
+ .max(self.min_scale)
+ .min(self.max_scale);
+
+ let image_size =
+ self.image_size(renderer, bounds.size());
+
+ let factor =
+ self.state.scale / previous_scale - 1.0;
+
+ let cursor_to_center =
+ cursor_position - bounds.center();
+
+ let adjustment = cursor_to_center * factor
+ + self.state.current_offset * factor;
+
+ self.state.current_offset = Vector::new(
+ if image_size.width > bounds.width {
+ self.state.current_offset.x + adjustment.x
+ } else {
+ 0.0
+ },
+ if image_size.height > bounds.height {
+ self.state.current_offset.y + adjustment.y
+ } else {
+ 0.0
+ },
+ );
+ }
+ }
+ }
+
+ event::Status::Captured
+ }
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
+ if is_mouse_over =>
+ {
+ self.state.cursor_grabbed_at = Some(cursor_position);
+ self.state.starting_offset = self.state.current_offset;
+
+ event::Status::Captured
+ }
+ Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
+ if self.state.cursor_grabbed_at.is_some() =>
+ {
+ self.state.cursor_grabbed_at = None;
+
+ event::Status::Captured
+ }
+ Event::Mouse(mouse::Event::CursorMoved { position }) => {
+ if let Some(origin) = self.state.cursor_grabbed_at {
+ let image_size = self.image_size(renderer, bounds.size());
+
+ let hidden_width = (image_size.width - bounds.width / 2.0)
+ .max(0.0)
+ .round();
+
+ let hidden_height = (image_size.height
+ - bounds.height / 2.0)
+ .max(0.0)
+ .round();
+
+ let delta = position - origin;
+
+ let x = if bounds.width < image_size.width {
+ (self.state.starting_offset.x - delta.x)
+ .min(hidden_width)
+ .max(-hidden_width)
+ } else {
+ 0.0
+ };
+
+ let y = if bounds.height < image_size.height {
+ (self.state.starting_offset.y - delta.y)
+ .min(hidden_height)
+ .max(-hidden_height)
+ } else {
+ 0.0
+ };
+
+ self.state.current_offset = Vector::new(x, y);
+
+ event::Status::Captured
+ } else {
+ event::Status::Ignored
+ }
+ }
+ _ => event::Status::Ignored,
+ }
+ }
+
+ fn draw(
+ &self,
+ renderer: &mut Renderer,
+ _defaults: &Renderer::Defaults,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ _viewport: &Rectangle,
+ ) -> Renderer::Output {
+ let bounds = layout.bounds();
+
+ let image_size = self.image_size(renderer, bounds.size());
+
+ let translation = {
+ let image_top_left = Vector::new(
+ bounds.width / 2.0 - image_size.width / 2.0,
+ bounds.height / 2.0 - image_size.height / 2.0,
+ );
+
+ image_top_left - self.state.offset(bounds, image_size)
+ };
+
+ let is_mouse_over = bounds.contains(cursor_position);
+
+ self::Renderer::draw(
+ renderer,
+ &self.state,
+ bounds,
+ image_size,
+ translation,
+ self.handle.clone(),
+ is_mouse_over,
+ )
+ }
+
+ fn hash_layout(&self, state: &mut Hasher) {
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
+ self.width.hash(state);
+ self.height.hash(state);
+ self.padding.hash(state);
+
+ self.handle.hash(state);
+ }
+}
+
+/// The local state of a [`Viewer`].
+#[derive(Debug, Clone, Copy)]
+pub struct State {
+ scale: f32,
+ starting_offset: Vector,
+ current_offset: Vector,
+ cursor_grabbed_at: Option<Point>,
+}
+
+impl Default for State {
+ fn default() -> Self {
+ Self {
+ scale: 1.0,
+ starting_offset: Vector::default(),
+ current_offset: Vector::default(),
+ cursor_grabbed_at: None,
+ }
+ }
+}
+
+impl State {
+ /// Creates a new [`State`].
+ pub fn new() -> Self {
+ State::default()
+ }
+
+ /// Returns the current offset of the [`State`], given the bounds
+ /// of the [`Viewer`] and its image.
+ fn offset(&self, bounds: Rectangle, image_size: Size) -> Vector {
+ let hidden_width =
+ (image_size.width - bounds.width / 2.0).max(0.0).round();
+
+ let hidden_height =
+ (image_size.height - bounds.height / 2.0).max(0.0).round();
+
+ Vector::new(
+ self.current_offset.x.min(hidden_width).max(-hidden_width),
+ self.current_offset.y.min(hidden_height).max(-hidden_height),
+ )
+ }
+
+ /// Returns if the cursor is currently grabbed by the [`Viewer`].
+ pub fn is_cursor_grabbed(&self) -> bool {
+ self.cursor_grabbed_at.is_some()
+ }
+}
+
+/// The renderer of an [`Viewer`].
+///
+/// Your [renderer] will need to implement this trait before being
+/// able to use a [`Viewer`] in your user interface.
+///
+/// [renderer]: crate::renderer
+pub trait Renderer: crate::Renderer + Sized {
+ /// Draws the [`Viewer`].
+ ///
+ /// It receives:
+ /// - the [`State`] of the [`Viewer`]
+ /// - the bounds of the [`Viewer`] widget
+ /// - the [`Size`] of the scaled [`Viewer`] image
+ /// - the translation of the clipped image
+ /// - the [`Handle`] to the underlying image
+ /// - whether the mouse is over the [`Viewer`] or not
+ ///
+ /// [`Handle`]: image::Handle
+ fn draw(
+ &mut self,
+ state: &State,
+ bounds: Rectangle,
+ image_size: Size,
+ translation: Vector,
+ handle: image::Handle,
+ is_mouse_over: bool,
+ ) -> Self::Output;
+}
+
+impl<'a, Message, Renderer> From<Viewer<'a>> for Element<'a, Message, Renderer>
+where
+ Renderer: 'a + self::Renderer + image::Renderer,
+ Message: 'a,
+{
+ fn from(viewer: Viewer<'a>) -> Element<'a, Message, Renderer> {
+ Element::new(viewer)
+ }
+}
diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs
index ff19cbc2..b72172cc 100644
--- a/native/src/widget/pane_grid.rs
+++ b/native/src/widget/pane_grid.rs
@@ -6,7 +6,7 @@
//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing,
//! drag and drop, and hotkey support.
//!
-//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.2/examples/pane_grid
+//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.3/examples/pane_grid
mod axis;
mod configuration;
mod content;
@@ -33,7 +33,7 @@ use crate::layout;
use crate::mouse;
use crate::overlay;
use crate::row;
-use crate::text;
+use crate::touch;
use crate::{
Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Vector,
Widget,
@@ -98,6 +98,7 @@ pub struct PaneGrid<'a, Message, Renderer: self::Renderer> {
on_click: Option<Box<dyn Fn(Pane) -> Message + 'a>>,
on_drag: Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
on_resize: Option<(u16, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>,
+ style: <Renderer as self::Renderer>::Style,
}
impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer>
@@ -129,6 +130,7 @@ where
on_click: None,
on_drag: None,
on_resize: None,
+ style: Default::default(),
}
}
@@ -186,6 +188,15 @@ where
self.on_resize = Some((leeway, Box::new(f)));
self
}
+
+ /// Sets the style of the [`PaneGrid`].
+ pub fn style(
+ mut self,
+ style: impl Into<<Renderer as self::Renderer>::Style>,
+ ) -> Self {
+ self.style = style.into();
+ self
+ }
}
impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer>
@@ -351,49 +362,41 @@ where
event: Event,
layout: Layout<'_>,
cursor_position: Point,
- messages: &mut Vec<Message>,
renderer: &Renderer,
- clipboard: Option<&dyn Clipboard>,
+ clipboard: &mut dyn Clipboard,
+ messages: &mut Vec<Message>,
) -> event::Status {
let mut event_status = event::Status::Ignored;
match event {
- Event::Mouse(mouse_event) => match mouse_event {
- mouse::Event::ButtonPressed(mouse::Button::Left) => {
- let bounds = layout.bounds();
-
- if bounds.contains(cursor_position) {
- event_status = event::Status::Captured;
-
- match self.on_resize {
- Some((leeway, _)) => {
- let relative_cursor = Point::new(
- cursor_position.x - bounds.x,
- cursor_position.y - bounds.y,
- );
-
- let splits = self.state.split_regions(
- f32::from(self.spacing),
- Size::new(bounds.width, bounds.height),
- );
-
- let clicked_split = hovered_split(
- splits.iter(),
- f32::from(self.spacing + leeway),
- relative_cursor,
- );
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
+ | Event::Touch(touch::Event::FingerPressed { .. }) => {
+ let bounds = layout.bounds();
- if let Some((split, axis)) = clicked_split {
- self.state.pick_split(&split, axis);
- } else {
- self.click_pane(
- layout,
- cursor_position,
- messages,
- );
- }
- }
- None => {
+ if bounds.contains(cursor_position) {
+ event_status = event::Status::Captured;
+
+ match self.on_resize {
+ Some((leeway, _)) => {
+ let relative_cursor = Point::new(
+ cursor_position.x - bounds.x,
+ cursor_position.y - bounds.y,
+ );
+
+ let splits = self.state.split_regions(
+ f32::from(self.spacing),
+ Size::new(bounds.width, bounds.height),
+ );
+
+ let clicked_split = hovered_split(
+ splits.iter(),
+ f32::from(self.spacing + leeway),
+ relative_cursor,
+ );
+
+ if let Some((split, axis, _)) = clicked_split {
+ self.state.pick_split(&split, axis);
+ } else {
self.click_pane(
layout,
cursor_position,
@@ -401,47 +404,51 @@ where
);
}
}
+ None => {
+ self.click_pane(layout, cursor_position, messages);
+ }
}
}
- mouse::Event::ButtonReleased(mouse::Button::Left) => {
- if let Some((pane, _)) = self.state.picked_pane() {
- if let Some(on_drag) = &self.on_drag {
- let mut dropped_region = self
- .elements
- .iter()
- .zip(layout.children())
- .filter(|(_, layout)| {
+ }
+ Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
+ | Event::Touch(touch::Event::FingerLifted { .. })
+ | Event::Touch(touch::Event::FingerLost { .. }) => {
+ if let Some((pane, _)) = self.state.picked_pane() {
+ if let Some(on_drag) = &self.on_drag {
+ let mut dropped_region =
+ self.elements.iter().zip(layout.children()).filter(
+ |(_, layout)| {
layout.bounds().contains(cursor_position)
- });
-
- let event = match dropped_region.next() {
- Some(((target, _), _)) if pane != *target => {
- DragEvent::Dropped {
- pane,
- target: *target,
- }
+ },
+ );
+
+ let event = match dropped_region.next() {
+ Some(((target, _), _)) if pane != *target => {
+ DragEvent::Dropped {
+ pane,
+ target: *target,
}
- _ => DragEvent::Canceled { pane },
- };
+ }
+ _ => DragEvent::Canceled { pane },
+ };
- messages.push(on_drag(event));
- }
+ messages.push(on_drag(event));
+ }
- self.state.idle();
+ self.state.idle();
- event_status = event::Status::Captured;
- } else if self.state.picked_split().is_some() {
- self.state.idle();
+ event_status = event::Status::Captured;
+ } else if self.state.picked_split().is_some() {
+ self.state.idle();
- event_status = event::Status::Captured;
- }
- }
- mouse::Event::CursorMoved { .. } => {
- event_status =
- self.trigger_resize(layout, cursor_position, messages);
+ event_status = event::Status::Captured;
}
- _ => {}
- },
+ }
+ Event::Mouse(mouse::Event::CursorMoved { .. })
+ | Event::Touch(touch::Event::FingerMoved { .. }) => {
+ event_status =
+ self.trigger_resize(layout, cursor_position, messages);
+ }
_ => {}
}
@@ -454,9 +461,9 @@ where
event.clone(),
layout,
cursor_position,
- messages,
renderer,
clipboard,
+ messages,
)
})
.fold(event_status, event::Status::merge)
@@ -471,11 +478,28 @@ where
defaults: &Renderer::Defaults,
layout: Layout<'_>,
cursor_position: Point,
- _viewport: &Rectangle,
+ viewport: &Rectangle,
) -> Renderer::Output {
let picked_split = self
.state
.picked_split()
+ .and_then(|(split, axis)| {
+ let bounds = layout.bounds();
+
+ let splits = self
+ .state
+ .split_regions(f32::from(self.spacing), bounds.size());
+
+ let (_axis, region, ratio) = splits.get(&split)?;
+
+ let region = axis.split_line_bounds(
+ *region,
+ *ratio,
+ f32::from(self.spacing),
+ );
+
+ Some((axis, region + Vector::new(bounds.x, bounds.y), true))
+ })
.or_else(|| match self.on_resize {
Some((leeway, _)) => {
let bounds = layout.bounds();
@@ -489,15 +513,20 @@ where
.state
.split_regions(f32::from(self.spacing), bounds.size());
- hovered_split(
+ let (_split, axis, region) = hovered_split(
splits.iter(),
f32::from(self.spacing + leeway),
relative_cursor,
- )
+ )?;
+
+ Some((
+ axis,
+ region + Vector::new(bounds.x, bounds.y),
+ false,
+ ))
}
None => None,
- })
- .map(|(_, axis)| axis);
+ });
self::Renderer::draw(
renderer,
@@ -506,7 +535,9 @@ where
self.state.picked_pane(),
picked_split,
layout,
+ &self.style,
cursor_position,
+ viewport,
)
}
@@ -543,9 +574,10 @@ where
/// able to use a [`PaneGrid`] in your user interface.
///
/// [renderer]: crate::renderer
-pub trait Renderer:
- crate::Renderer + container::Renderer + text::Renderer + Sized
-{
+pub trait Renderer: crate::Renderer + container::Renderer + Sized {
+ /// The style supported by this renderer.
+ type Style: Default;
+
/// Draws a [`PaneGrid`].
///
/// It receives:
@@ -559,9 +591,11 @@ pub trait Renderer:
defaults: &Self::Defaults,
content: &[(Pane, Content<'_, Message, Self>)],
dragging: Option<(Pane, Point)>,
- resizing: Option<Axis>,
+ resizing: Option<(Axis, Rectangle, bool)>,
layout: Layout<'_>,
+ style: &<Self as self::Renderer>::Style,
cursor_position: Point,
+ viewport: &Rectangle,
) -> Self::Output;
/// Draws a [`Pane`].
@@ -575,10 +609,11 @@ pub trait Renderer:
&mut self,
defaults: &Self::Defaults,
bounds: Rectangle,
- style: &Self::Style,
+ style: &<Self as container::Renderer>::Style,
title_bar: Option<(&TitleBar<'_, Message, Self>, Layout<'_>)>,
body: (&Element<'_, Message, Self>, Layout<'_>),
cursor_position: Point,
+ viewport: &Rectangle,
) -> Self::Output;
/// Draws a [`TitleBar`].
@@ -586,20 +621,18 @@ pub trait Renderer:
/// It receives:
/// - the bounds, style of the [`TitleBar`]
/// - the style of the [`TitleBar`]
- /// - the title of the [`TitleBar`] with its size, font, and bounds
- /// - the controls of the [`TitleBar`] with their [`Layout`+, if any
+ /// - the content of the [`TitleBar`] with its layout
+ /// - the controls of the [`TitleBar`] with their [`Layout`], if any
/// - the cursor position
fn draw_title_bar<Message>(
&mut self,
defaults: &Self::Defaults,
bounds: Rectangle,
- style: &Self::Style,
- title: &str,
- title_size: u16,
- title_font: Self::Font,
- title_bounds: Rectangle,
+ style: &<Self as container::Renderer>::Style,
+ content: (&Element<'_, Message, Self>, Layout<'_>),
controls: Option<(&Element<'_, Message, Self>, Layout<'_>)>,
cursor_position: Point,
+ viewport: &Rectangle,
) -> Self::Output;
}
@@ -623,14 +656,14 @@ fn hovered_split<'a>(
splits: impl Iterator<Item = (&'a Split, &'a (Axis, Rectangle, f32))>,
spacing: f32,
cursor_position: Point,
-) -> Option<(Split, Axis)> {
+) -> Option<(Split, Axis, Rectangle)> {
splits
.filter_map(|(split, (axis, region, ratio))| {
let bounds =
axis.split_line_bounds(*region, *ratio, f32::from(spacing));
if bounds.contains(cursor_position) {
- Some((*split, *axis))
+ Some((*split, *axis, bounds))
} else {
None
}
diff --git a/native/src/widget/pane_grid/content.rs b/native/src/widget/pane_grid/content.rs
index c9981903..b0110393 100644
--- a/native/src/widget/pane_grid/content.rs
+++ b/native/src/widget/pane_grid/content.rs
@@ -3,7 +3,7 @@ use crate::event::{self, Event};
use crate::layout;
use crate::overlay;
use crate::pane_grid::{self, TitleBar};
-use crate::{Clipboard, Element, Hasher, Layout, Point, Size};
+use crate::{Clipboard, Element, Hasher, Layout, Point, Rectangle, Size};
/// The content of a [`Pane`].
///
@@ -12,7 +12,7 @@ use crate::{Clipboard, Element, Hasher, Layout, Point, Size};
pub struct Content<'a, Message, Renderer: pane_grid::Renderer> {
title_bar: Option<TitleBar<'a, Message, Renderer>>,
body: Element<'a, Message, Renderer>,
- style: Renderer::Style,
+ style: <Renderer as container::Renderer>::Style,
}
impl<'a, Message, Renderer> Content<'a, Message, Renderer>
@@ -24,7 +24,7 @@ where
Self {
title_bar: None,
body: body.into(),
- style: Renderer::Style::default(),
+ style: Default::default(),
}
}
@@ -38,7 +38,10 @@ where
}
/// Sets the style of the [`Content`].
- pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {
+ pub fn style(
+ mut self,
+ style: impl Into<<Renderer as container::Renderer>::Style>,
+ ) -> Self {
self.style = style.into();
self
}
@@ -57,6 +60,7 @@ where
defaults: &Renderer::Defaults,
layout: Layout<'_>,
cursor_position: Point,
+ viewport: &Rectangle,
) -> Renderer::Output {
if let Some(title_bar) = &self.title_bar {
let mut children = layout.children();
@@ -70,6 +74,7 @@ where
Some((title_bar, title_bar_layout)),
(&self.body, body_layout),
cursor_position,
+ viewport,
)
} else {
renderer.draw_pane(
@@ -79,6 +84,7 @@ where
None,
(&self.body, layout),
cursor_position,
+ viewport,
)
}
}
@@ -140,9 +146,9 @@ where
event: Event,
layout: Layout<'_>,
cursor_position: Point,
- messages: &mut Vec<Message>,
renderer: &Renderer,
- clipboard: Option<&dyn Clipboard>,
+ clipboard: &mut dyn Clipboard,
+ messages: &mut Vec<Message>,
) -> event::Status {
let mut event_status = event::Status::Ignored;
@@ -153,9 +159,9 @@ where
event.clone(),
children.next().unwrap(),
cursor_position,
- messages,
renderer,
clipboard,
+ messages,
);
children.next().unwrap()
@@ -167,9 +173,9 @@ where
event,
body_layout,
cursor_position,
- messages,
renderer,
clipboard,
+ messages,
);
event_status.merge(body_status)
@@ -187,18 +193,17 @@ where
&mut self,
layout: Layout<'_>,
) -> Option<overlay::Element<'_, Message, Renderer>> {
- let body_layout = if self.title_bar.is_some() {
+ if let Some(title_bar) = self.title_bar.as_mut() {
let mut children = layout.children();
+ let title_bar_layout = children.next()?;
- // Overlays only allowed in the pane body, for now at least.
- let _title_bar_layout = children.next();
-
- children.next()?
+ match title_bar.overlay(title_bar_layout) {
+ Some(overlay) => Some(overlay),
+ None => self.body.overlay(children.next()?),
+ }
} else {
- layout
- };
-
- self.body.overlay(body_layout)
+ self.body.overlay(layout)
+ }
}
}
diff --git a/native/src/widget/pane_grid/node.rs b/native/src/widget/pane_grid/node.rs
index 319936fc..84714e00 100644
--- a/native/src/widget/pane_grid/node.rs
+++ b/native/src/widget/pane_grid/node.rs
@@ -3,7 +3,7 @@ use crate::{
Rectangle, Size,
};
-use std::collections::HashMap;
+use std::collections::BTreeMap;
/// A layout node of a [`PaneGrid`].
///
@@ -59,8 +59,8 @@ impl Node {
&self,
spacing: f32,
size: Size,
- ) -> HashMap<Pane, Rectangle> {
- let mut regions = HashMap::new();
+ ) -> BTreeMap<Pane, Rectangle> {
+ let mut regions = BTreeMap::new();
self.compute_regions(
spacing,
@@ -83,8 +83,8 @@ impl Node {
&self,
spacing: f32,
size: Size,
- ) -> HashMap<Split, (Axis, Rectangle, f32)> {
- let mut splits = HashMap::new();
+ ) -> BTreeMap<Split, (Axis, Rectangle, f32)> {
+ let mut splits = BTreeMap::new();
self.compute_splits(
spacing,
@@ -191,7 +191,7 @@ impl Node {
&self,
spacing: f32,
current: &Rectangle,
- regions: &mut HashMap<Pane, Rectangle>,
+ regions: &mut BTreeMap<Pane, Rectangle>,
) {
match self {
Node::Split {
@@ -212,7 +212,7 @@ impl Node {
&self,
spacing: f32,
current: &Rectangle,
- splits: &mut HashMap<Split, (Axis, Rectangle, f32)>,
+ splits: &mut BTreeMap<Split, (Axis, Rectangle, f32)>,
) {
match self {
Node::Split {
diff --git a/native/src/widget/pane_grid/pane.rs b/native/src/widget/pane_grid/pane.rs
index 39d9f3ef..d6fbab83 100644
--- a/native/src/widget/pane_grid/pane.rs
+++ b/native/src/widget/pane_grid/pane.rs
@@ -1,5 +1,5 @@
/// A rectangular region in a [`PaneGrid`] used to display widgets.
///
/// [`PaneGrid`]: crate::widget::PaneGrid
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Pane(pub(super) usize);
diff --git a/native/src/widget/pane_grid/split.rs b/native/src/widget/pane_grid/split.rs
index 16975abc..8132272a 100644
--- a/native/src/widget/pane_grid/split.rs
+++ b/native/src/widget/pane_grid/split.rs
@@ -1,5 +1,5 @@
/// A divider that splits a region in a [`PaneGrid`] into two different panes.
///
/// [`PaneGrid`]: crate::widget::PaneGrid
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Split(pub(super) usize);
diff --git a/native/src/widget/pane_grid/state.rs b/native/src/widget/pane_grid/state.rs
index 666e1ca0..fb96f89f 100644
--- a/native/src/widget/pane_grid/state.rs
+++ b/native/src/widget/pane_grid/state.rs
@@ -3,7 +3,7 @@ use crate::{
Hasher, Point, Rectangle, Size,
};
-use std::collections::HashMap;
+use std::collections::{BTreeMap, HashMap};
/// The state of a [`PaneGrid`].
///
@@ -257,7 +257,7 @@ impl Internal {
&self,
spacing: f32,
size: Size,
- ) -> HashMap<Pane, Rectangle> {
+ ) -> BTreeMap<Pane, Rectangle> {
self.layout.pane_regions(spacing, size)
}
@@ -265,7 +265,7 @@ impl Internal {
&self,
spacing: f32,
size: Size,
- ) -> HashMap<Split, (Axis, Rectangle, f32)> {
+ ) -> BTreeMap<Split, (Axis, Rectangle, f32)> {
self.layout.split_regions(spacing, size)
}
diff --git a/native/src/widget/pane_grid/title_bar.rs b/native/src/widget/pane_grid/title_bar.rs
index 475cb9ae..070010f8 100644
--- a/native/src/widget/pane_grid/title_bar.rs
+++ b/native/src/widget/pane_grid/title_bar.rs
@@ -1,43 +1,42 @@
+use crate::container;
use crate::event::{self, Event};
use crate::layout;
+use crate::overlay;
use crate::pane_grid;
-use crate::{Clipboard, Element, Hasher, Layout, Point, Rectangle, Size};
+use crate::{
+ Clipboard, Element, Hasher, Layout, Padding, Point, Rectangle, Size,
+};
/// The title bar of a [`Pane`].
///
/// [`Pane`]: crate::widget::pane_grid::Pane
#[allow(missing_debug_implementations)]
pub struct TitleBar<'a, Message, Renderer: pane_grid::Renderer> {
- title: String,
- title_size: Option<u16>,
+ content: Element<'a, Message, Renderer>,
controls: Option<Element<'a, Message, Renderer>>,
- padding: u16,
+ padding: Padding,
always_show_controls: bool,
- style: Renderer::Style,
+ style: <Renderer as container::Renderer>::Style,
}
impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer>
where
Renderer: pane_grid::Renderer,
{
- /// Creates a new [`TitleBar`] with the given title.
- pub fn new(title: impl Into<String>) -> Self {
+ /// Creates a new [`TitleBar`] with the given content.
+ pub fn new<E>(content: E) -> Self
+ where
+ E: Into<Element<'a, Message, Renderer>>,
+ {
Self {
- title: title.into(),
- title_size: None,
+ content: content.into(),
controls: None,
- padding: 0,
+ padding: Padding::ZERO,
always_show_controls: false,
- style: Renderer::Style::default(),
+ style: Default::default(),
}
}
- /// Sets the size of the title of the [`TitleBar`].
- pub fn title_size(mut self, size: u16) -> Self {
- self.title_size = Some(size);
- self
- }
-
/// Sets the controls of the [`TitleBar`].
pub fn controls(
mut self,
@@ -47,14 +46,17 @@ where
self
}
- /// Sets the padding of the [`TitleBar`].
- pub fn padding(mut self, units: u16) -> Self {
- self.padding = units;
+ /// Sets the [`Padding`] of the [`TitleBar`].
+ pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
+ self.padding = padding.into();
self
}
/// Sets the style of the [`TitleBar`].
- pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {
+ pub fn style(
+ mut self,
+ style: impl Into<<Renderer as container::Renderer>::Style>,
+ ) -> Self {
self.style = style.into();
self
}
@@ -86,53 +88,36 @@ where
defaults: &Renderer::Defaults,
layout: Layout<'_>,
cursor_position: Point,
+ viewport: &Rectangle,
show_controls: bool,
) -> Renderer::Output {
let mut children = layout.children();
let padded = children.next().unwrap();
- if let Some(controls) = &self.controls {
- let mut children = padded.children();
- let title_layout = children.next().unwrap();
+ let mut children = padded.children();
+ let title_layout = children.next().unwrap();
+
+ let controls = if let Some(controls) = &self.controls {
let controls_layout = children.next().unwrap();
- let (title_bounds, controls) =
- if show_controls || self.always_show_controls {
- (title_layout.bounds(), Some((controls, controls_layout)))
- } else {
- (
- Rectangle {
- width: padded.bounds().width,
- ..title_layout.bounds()
- },
- None,
- )
- };
-
- renderer.draw_title_bar(
- defaults,
- layout.bounds(),
- &self.style,
- &self.title,
- self.title_size.unwrap_or(renderer.default_size()),
- Renderer::Font::default(),
- title_bounds,
- controls,
- cursor_position,
- )
+ if show_controls || self.always_show_controls {
+ Some((controls, controls_layout))
+ } else {
+ None
+ }
} else {
- renderer.draw_title_bar::<()>(
- defaults,
- layout.bounds(),
- &self.style,
- &self.title,
- self.title_size.unwrap_or(renderer.default_size()),
- Renderer::Font::default(),
- padded.bounds(),
- None,
- cursor_position,
- )
- }
+ None
+ };
+
+ renderer.draw_title_bar(
+ defaults,
+ layout.bounds(),
+ &self.style,
+ (&self.content, title_layout),
+ controls,
+ cursor_position,
+ viewport,
+ )
}
/// Returns whether the mouse cursor is over the pick area of the
@@ -147,15 +132,16 @@ where
if layout.bounds().contains(cursor_position) {
let mut children = layout.children();
let padded = children.next().unwrap();
+ let mut children = padded.children();
+ let title_layout = children.next().unwrap();
if self.controls.is_some() {
- let mut children = padded.children();
- let _ = children.next().unwrap();
let controls_layout = children.next().unwrap();
!controls_layout.bounds().contains(cursor_position)
+ && !title_layout.bounds().contains(cursor_position)
} else {
- true
+ !title_layout.bounds().contains(cursor_position)
}
} else {
false
@@ -165,9 +151,12 @@ where
pub(crate) fn hash_layout(&self, hasher: &mut Hasher) {
use std::hash::Hash;
- self.title.hash(hasher);
- self.title_size.hash(hasher);
+ self.content.hash_layout(hasher);
self.padding.hash(hasher);
+
+ if let Some(controls) = &self.controls {
+ controls.hash_layout(hasher);
+ }
}
pub(crate) fn layout(
@@ -175,19 +164,13 @@ where
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let padding = f32::from(self.padding);
- let limits = limits.pad(padding);
+ let limits = limits.pad(self.padding);
let max_size = limits.max();
- let title_size = self.title_size.unwrap_or(renderer.default_size());
- let title_font = Renderer::Font::default();
-
- let (title_width, title_height) = renderer.measure(
- &self.title,
- title_size,
- title_font,
- Size::new(f32::INFINITY, max_size.height),
- );
+ let title_layout = self
+ .content
+ .layout(renderer, &layout::Limits::new(Size::ZERO, max_size));
+ let title_size = title_layout.size();
let mut node = if let Some(controls) = &self.controls {
let mut controls_layout = controls
@@ -196,16 +179,8 @@ where
let controls_size = controls_layout.size();
let space_before_controls = max_size.width - controls_size.width;
- let mut title_layout = layout::Node::new(Size::new(
- title_width.min(space_before_controls),
- title_height,
- ));
-
- let title_size = title_layout.size();
let height = title_size.height.max(controls_size.height);
- title_layout
- .move_to(Point::new(0.0, (height - title_size.height) / 2.0));
controls_layout.move_to(Point::new(space_before_controls, 0.0));
layout::Node::with_children(
@@ -213,12 +188,18 @@ where
vec![title_layout, controls_layout],
)
} else {
- layout::Node::new(Size::new(max_size.width, title_height))
+ layout::Node::with_children(
+ Size::new(max_size.width, title_size.height),
+ vec![title_layout],
+ )
};
- node.move_to(Point::new(padding, padding));
+ node.move_to(Point::new(
+ self.padding.left.into(),
+ self.padding.top.into(),
+ ));
- layout::Node::with_children(node.size().pad(padding), vec![node])
+ layout::Node::with_children(node.size().pad(self.padding), vec![node])
}
pub(crate) fn on_event(
@@ -226,28 +207,63 @@ where
event: Event,
layout: Layout<'_>,
cursor_position: Point,
- messages: &mut Vec<Message>,
renderer: &Renderer,
- clipboard: Option<&dyn Clipboard>,
+ clipboard: &mut dyn Clipboard,
+ messages: &mut Vec<Message>,
) -> event::Status {
- if let Some(controls) = &mut self.controls {
- let mut children = layout.children();
- let padded = children.next().unwrap();
+ let mut children = layout.children();
+ let padded = children.next().unwrap();
- let mut children = padded.children();
- let _ = children.next();
+ let mut children = padded.children();
+ let title_layout = children.next().unwrap();
+
+ let control_status = if let Some(controls) = &mut self.controls {
let controls_layout = children.next().unwrap();
controls.on_event(
- event,
+ event.clone(),
controls_layout,
cursor_position,
- messages,
renderer,
clipboard,
+ messages,
)
} else {
event::Status::Ignored
- }
+ };
+
+ let title_status = self.content.on_event(
+ event,
+ title_layout,
+ cursor_position,
+ renderer,
+ clipboard,
+ messages,
+ );
+
+ control_status.merge(title_status)
+ }
+
+ pub(crate) fn overlay(
+ &mut self,
+ layout: Layout<'_>,
+ ) -> Option<overlay::Element<'_, Message, Renderer>> {
+ let mut children = layout.children();
+ let padded = children.next()?;
+
+ let mut children = padded.children();
+ let title_layout = children.next()?;
+
+ let Self {
+ content, controls, ..
+ } = self;
+
+ content.overlay(title_layout).or_else(move || {
+ controls.as_mut().and_then(|controls| {
+ let controls_layout = children.next()?;
+
+ controls.overlay(controls_layout)
+ })
+ })
}
}
diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs
index 58c0dfe1..f4b60fc4 100644
--- a/native/src/widget/pick_list.rs
+++ b/native/src/widget/pick_list.rs
@@ -6,8 +6,10 @@ use crate::overlay;
use crate::overlay::menu::{self, Menu};
use crate::scrollable;
use crate::text;
+use crate::touch;
use crate::{
- Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget,
+ Clipboard, Element, Hasher, Layout, Length, Padding, Point, Rectangle,
+ Size, Widget,
};
use std::borrow::Cow;
@@ -23,9 +25,10 @@ where
last_selection: &'a mut Option<T>,
on_selected: Box<dyn Fn(T) -> Message>,
options: Cow<'a, [T]>,
+ placeholder: Option<String>,
selected: Option<T>,
width: Length,
- padding: u16,
+ padding: Padding,
text_size: Option<u16>,
font: Renderer::Font,
style: <Renderer as self::Renderer>::Style,
@@ -80,6 +83,7 @@ where
last_selection,
on_selected: Box::new(on_selected),
options: options.into(),
+ placeholder: None,
selected,
width: Length::Shrink,
text_size: None,
@@ -89,15 +93,21 @@ where
}
}
+ /// Sets the placeholder of the [`PickList`].
+ pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
+ self.placeholder = Some(placeholder.into());
+ self
+ }
+
/// Sets the width of the [`PickList`].
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
- /// Sets the padding of the [`PickList`].
- pub fn padding(mut self, padding: u16) -> Self {
- self.padding = padding;
+ /// Sets the [`Padding`] of the [`PickList`].
+ pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
+ self.padding = padding.into();
self
}
@@ -132,7 +142,7 @@ where
Renderer: self::Renderer + scrollable::Renderer + 'a,
{
fn width(&self) -> Length {
- Length::Shrink
+ self.width
}
fn height(&self) -> Length {
@@ -149,27 +159,37 @@ where
let limits = limits
.width(self.width)
.height(Length::Shrink)
- .pad(f32::from(self.padding));
+ .pad(self.padding);
let text_size = self.text_size.unwrap_or(renderer.default_size());
+ let font = self.font;
let max_width = match self.width {
Length::Shrink => {
+ let measure = |label: &str| -> u32 {
+ let (width, _) = renderer.measure(
+ label,
+ text_size,
+ font,
+ Size::new(f32::INFINITY, f32::INFINITY),
+ );
+
+ width.round() as u32
+ };
+
let labels = self.options.iter().map(ToString::to_string);
- labels
- .map(|label| {
- let (width, _) = renderer.measure(
- &label,
- text_size,
- Renderer::Font::default(),
- Size::new(f32::INFINITY, f32::INFINITY),
- );
-
- width.round() as u32
- })
- .max()
- .unwrap_or(100)
+ let labels_width =
+ labels.map(|label| measure(&label)).max().unwrap_or(100);
+
+ let placeholder_width = self
+ .placeholder
+ .as_ref()
+ .map(String::as_str)
+ .map(measure)
+ .unwrap_or(100);
+
+ labels_width.max(placeholder_width)
}
_ => 0,
};
@@ -178,11 +198,11 @@ where
let intrinsic = Size::new(
max_width as f32
+ f32::from(text_size)
- + f32::from(self.padding),
+ + f32::from(self.padding.left),
f32::from(text_size),
);
- limits.resolve(intrinsic).pad(f32::from(self.padding))
+ limits.resolve(intrinsic).pad(self.padding)
};
layout::Node::new(size)
@@ -193,6 +213,8 @@ where
match self.width {
Length::Shrink => {
+ self.placeholder.hash(state);
+
self.options
.iter()
.map(ToString::to_string)
@@ -209,12 +231,13 @@ where
event: Event,
layout: Layout<'_>,
cursor_position: Point,
- messages: &mut Vec<Message>,
_renderer: &Renderer,
- _clipboard: Option<&dyn Clipboard>,
+ _clipboard: &mut dyn Clipboard,
+ messages: &mut Vec<Message>,
) -> event::Status {
match event {
- Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
+ | Event::Touch(touch::Event::FingerPressed { .. }) => {
let event_status = if *self.is_open {
// TODO: Encode cursor availability in the type system
*self.is_open =
@@ -245,6 +268,43 @@ where
event_status
}
}
+ Event::Mouse(mouse::Event::WheelScrolled {
+ delta: mouse::ScrollDelta::Lines { y, .. },
+ }) if layout.bounds().contains(cursor_position)
+ && !*self.is_open =>
+ {
+ fn find_next<'a, T: PartialEq>(
+ selected: &'a T,
+ mut options: impl Iterator<Item = &'a T>,
+ ) -> Option<&'a T> {
+ let _ = options.find(|&option| option == selected);
+
+ options.next()
+ }
+
+ let next_option = if y < 0.0 {
+ if let Some(selected) = self.selected.as_ref() {
+ find_next(selected, self.options.iter())
+ } else {
+ self.options.first()
+ }
+ } else if y > 0.0 {
+ if let Some(selected) = self.selected.as_ref() {
+ find_next(selected, self.options.iter().rev())
+ } else {
+ self.options.last()
+ }
+ } else {
+ None
+ };
+
+ if let Some(next_option) = next_option {
+ messages.push((self.on_selected)(next_option.clone()));
+ }
+
+ return event::Status::Captured;
+ }
+
_ => event::Status::Ignored,
}
}
@@ -262,6 +322,7 @@ where
layout.bounds(),
cursor_position,
self.selected.as_ref().map(ToString::to_string),
+ self.placeholder.as_ref().map(String::as_str),
self.padding,
self.text_size.unwrap_or(renderer.default_size()),
self.font,
@@ -306,7 +367,7 @@ where
/// [renderer]: crate::renderer
pub trait Renderer: text::Renderer + menu::Renderer {
/// The default padding of a [`PickList`].
- const DEFAULT_PADDING: u16;
+ const DEFAULT_PADDING: Padding;
/// The [`PickList`] style supported by this renderer.
type Style: Default;
@@ -322,7 +383,8 @@ pub trait Renderer: text::Renderer + menu::Renderer {
bounds: Rectangle,
cursor_position: Point,
selected: Option<String>,
- padding: u16,
+ placeholder: Option<&str>,
+ padding: Padding,
text_size: u16,
font: Self::Font,
style: &<Self as Renderer>::Style,
diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs
index 4935569f..dee82d1f 100644
--- a/native/src/widget/radio.rs
+++ b/native/src/widget/radio.rs
@@ -1,16 +1,17 @@
//! Create choices using radio buttons.
+use std::hash::Hash;
+
use crate::event::{self, Event};
-use crate::layout;
use crate::mouse;
use crate::row;
use crate::text;
+use crate::touch;
+use crate::{layout, Color};
use crate::{
Align, Clipboard, Element, Hasher, HorizontalAlignment, Layout, Length,
Point, Rectangle, Row, Text, VerticalAlignment, Widget,
};
-use std::hash::Hash;
-
/// A circular button representing a choice.
///
/// # Example
@@ -46,6 +47,8 @@ pub struct Radio<Message, Renderer: self::Renderer + text::Renderer> {
size: u16,
spacing: u16,
text_size: Option<u16>,
+ text_color: Option<Color>,
+ font: Renderer::Font,
style: Renderer::Style,
}
@@ -80,6 +83,8 @@ where
size: <Renderer as self::Renderer>::DEFAULT_SIZE,
spacing: Renderer::DEFAULT_SPACING, //15
text_size: None,
+ text_color: None,
+ font: Default::default(),
style: Renderer::Style::default(),
}
}
@@ -108,6 +113,18 @@ where
self
}
+ /// Sets the text color of the [`Radio`] button.
+ pub fn text_color(mut self, color: Color) -> Self {
+ self.text_color = Some(color);
+ self
+ }
+
+ /// Sets the text font of the [`Radio`] button.
+ pub fn font(mut self, font: Renderer::Font) -> Self {
+ self.font = font;
+ self
+ }
+
/// Sets the style of the [`Radio`] button.
pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {
self.style = style.into();
@@ -155,12 +172,13 @@ where
event: Event,
layout: Layout<'_>,
cursor_position: Point,
- messages: &mut Vec<Message>,
_renderer: &Renderer,
- _clipboard: Option<&dyn Clipboard>,
+ _clipboard: &mut dyn Clipboard,
+ messages: &mut Vec<Message>,
) -> event::Status {
match event {
- Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
+ | Event::Touch(touch::Event::FingerPressed { .. }) => {
if layout.bounds().contains(cursor_position) {
messages.push(self.on_click.clone());
@@ -194,8 +212,8 @@ where
label_layout.bounds(),
&self.label,
self.text_size.unwrap_or(renderer.default_size()),
- Default::default(),
- None,
+ self.font,
+ self.text_color,
HorizontalAlignment::Left,
VerticalAlignment::Center,
);
diff --git a/native/src/widget/row.rs b/native/src/widget/row.rs
index b71663bd..9ebc9145 100644
--- a/native/src/widget/row.rs
+++ b/native/src/widget/row.rs
@@ -3,7 +3,8 @@ use crate::event::{self, Event};
use crate::layout;
use crate::overlay;
use crate::{
- Align, Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget,
+ Align, Clipboard, Element, Hasher, Layout, Length, Padding, Point,
+ Rectangle, Widget,
};
use std::hash::Hash;
@@ -13,7 +14,7 @@ use std::u32;
#[allow(missing_debug_implementations)]
pub struct Row<'a, Message, Renderer> {
spacing: u16,
- padding: u16,
+ padding: Padding,
width: Length,
height: Length,
max_width: u32,
@@ -34,7 +35,7 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> {
) -> Self {
Row {
spacing: 0,
- padding: 0,
+ padding: Padding::ZERO,
width: Length::Shrink,
height: Length::Shrink,
max_width: u32::MAX,
@@ -54,9 +55,9 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> {
self
}
- /// Sets the padding of the [`Row`].
- pub fn padding(mut self, units: u16) -> Self {
- self.padding = units;
+ /// Sets the [`Padding`] of the [`Row`].
+ pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
+ self.padding = padding.into();
self
}
@@ -128,7 +129,7 @@ where
layout::flex::Axis::Horizontal,
renderer,
&limits,
- self.padding as f32,
+ self.padding,
self.spacing as f32,
self.align_items,
&self.children,
@@ -140,9 +141,9 @@ where
event: Event,
layout: Layout<'_>,
cursor_position: Point,
- messages: &mut Vec<Message>,
renderer: &Renderer,
- clipboard: Option<&dyn Clipboard>,
+ clipboard: &mut dyn Clipboard,
+ messages: &mut Vec<Message>,
) -> event::Status {
self.children
.iter_mut()
@@ -152,9 +153,9 @@ where
event.clone(),
layout,
cursor_position,
- messages,
renderer,
clipboard,
+ messages,
)
})
.fold(event::Status::Ignored, event::Status::merge)
diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs
index e23ab06a..68da2e67 100644
--- a/native/src/widget/scrollable.rs
+++ b/native/src/widget/scrollable.rs
@@ -4,8 +4,9 @@ use crate::event::{self, Event};
use crate::layout;
use crate::mouse;
use crate::overlay;
+use crate::touch;
use crate::{
- Align, Clipboard, Column, Element, Hasher, Layout, Length, Point,
+ Align, Clipboard, Column, Element, Hasher, Layout, Length, Padding, Point,
Rectangle, Size, Vector, Widget,
};
@@ -22,6 +23,7 @@ pub struct Scrollable<'a, Message, Renderer: self::Renderer> {
scrollbar_margin: u16,
scroller_width: u16,
content: Column<'a, Message, Renderer>,
+ on_scroll: Option<Box<dyn Fn(f32) -> Message>>,
style: Renderer::Style,
}
@@ -36,6 +38,7 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> {
scrollbar_margin: 0,
scroller_width: 10,
content: Column::new(),
+ on_scroll: None,
style: Renderer::Style::default(),
}
}
@@ -50,9 +53,9 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> {
self
}
- /// Sets the padding of the [`Scrollable`].
- pub fn padding(mut self, units: u16) -> Self {
- self.content = self.content.padding(units);
+ /// Sets the [`Padding`] of the [`Scrollable`].
+ pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
+ self.content = self.content.padding(padding);
self
}
@@ -100,12 +103,22 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> {
}
/// Sets the scroller width of the [`Scrollable`] .
- /// Silently enforces a minimum value of 1.
+ ///
+ /// It silently enforces a minimum value of 1.
pub fn scroller_width(mut self, scroller_width: u16) -> Self {
self.scroller_width = scroller_width.max(1);
self
}
+ /// Sets a function to call when the [`Scrollable`] is scrolled.
+ ///
+ /// The function takes the new relative offset of the [`Scrollable`]
+ /// (e.g. `0` means top, while `1` means bottom).
+ pub fn on_scroll(mut self, f: impl Fn(f32) -> Message + 'static) -> Self {
+ self.on_scroll = Some(Box::new(f));
+ self
+ }
+
/// Sets the style of the [`Scrollable`] .
pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {
self.style = style.into();
@@ -120,6 +133,24 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> {
self.content = self.content.push(child);
self
}
+
+ fn notify_on_scroll(
+ &self,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+ messages: &mut Vec<Message>,
+ ) {
+ if content_bounds.height <= bounds.height {
+ return;
+ }
+
+ if let Some(on_scroll) = &self.on_scroll {
+ messages.push(on_scroll(
+ self.state.offset.absolute(bounds, content_bounds)
+ / (content_bounds.height - bounds.height),
+ ));
+ }
+ }
}
impl<'a, Message, Renderer> Widget<Message, Renderer>
@@ -161,9 +192,9 @@ where
event: Event,
layout: Layout<'_>,
cursor_position: Point,
- messages: &mut Vec<Message>,
renderer: &Renderer,
- clipboard: Option<&dyn Clipboard>,
+ clipboard: &mut dyn Clipboard,
+ messages: &mut Vec<Message>,
) -> event::Status {
let bounds = layout.bounds();
let is_mouse_over = bounds.contains(cursor_position);
@@ -204,9 +235,9 @@ where
event.clone(),
content,
cursor_position,
- messages,
renderer,
clipboard,
+ messages,
)
};
@@ -227,6 +258,45 @@ where
}
}
+ self.notify_on_scroll(bounds, content_bounds, messages);
+
+ return event::Status::Captured;
+ }
+ Event::Touch(event) => {
+ match event {
+ touch::Event::FingerPressed { .. } => {
+ self.state.scroll_box_touched_at =
+ Some(cursor_position);
+ }
+ touch::Event::FingerMoved { .. } => {
+ if let Some(scroll_box_touched_at) =
+ self.state.scroll_box_touched_at
+ {
+ let delta =
+ cursor_position.y - scroll_box_touched_at.y;
+
+ self.state.scroll(
+ delta,
+ bounds,
+ content_bounds,
+ );
+
+ self.state.scroll_box_touched_at =
+ Some(cursor_position);
+
+ self.notify_on_scroll(
+ bounds,
+ content_bounds,
+ messages,
+ );
+ }
+ }
+ touch::Event::FingerLifted { .. }
+ | touch::Event::FingerLost { .. } => {
+ self.state.scroll_box_touched_at = None;
+ }
+ }
+
return event::Status::Captured;
}
_ => {}
@@ -237,12 +307,15 @@ where
match event {
Event::Mouse(mouse::Event::ButtonReleased(
mouse::Button::Left,
- )) => {
+ ))
+ | Event::Touch(touch::Event::FingerLifted { .. })
+ | Event::Touch(touch::Event::FingerLost { .. }) => {
self.state.scroller_grabbed_at = None;
return event::Status::Captured;
}
- Event::Mouse(mouse::Event::CursorMoved { .. }) => {
+ Event::Mouse(mouse::Event::CursorMoved { .. })
+ | Event::Touch(touch::Event::FingerMoved { .. }) => {
if let (Some(scrollbar), Some(scroller_grabbed_at)) =
(scrollbar, self.state.scroller_grabbed_at)
{
@@ -255,6 +328,8 @@ where
content_bounds,
);
+ self.notify_on_scroll(bounds, content_bounds, messages);
+
return event::Status::Captured;
}
}
@@ -264,7 +339,8 @@ where
match event {
Event::Mouse(mouse::Event::ButtonPressed(
mouse::Button::Left,
- )) => {
+ ))
+ | Event::Touch(touch::Event::FingerPressed { .. }) => {
if let Some(scrollbar) = scrollbar {
if let Some(scroller_grabbed_at) =
scrollbar.grab_scroller(cursor_position)
@@ -281,6 +357,12 @@ where
self.state.scroller_grabbed_at =
Some(scroller_grabbed_at);
+ self.notify_on_scroll(
+ bounds,
+ content_bounds,
+ messages,
+ );
+
return event::Status::Captured;
}
}
@@ -382,10 +464,44 @@ where
}
/// The local state of a [`Scrollable`].
-#[derive(Debug, Clone, Copy, Default)]
+#[derive(Debug, Clone, Copy)]
pub struct State {
scroller_grabbed_at: Option<f32>,
- offset: f32,
+ scroll_box_touched_at: Option<Point>,
+ offset: Offset,
+}
+
+impl Default for State {
+ fn default() -> Self {
+ Self {
+ scroller_grabbed_at: None,
+ scroll_box_touched_at: None,
+ offset: Offset::Absolute(0.0),
+ }
+ }
+}
+
+/// The local state of a [`Scrollable`].
+#[derive(Debug, Clone, Copy)]
+enum Offset {
+ Absolute(f32),
+ Relative(f32),
+}
+
+impl Offset {
+ fn absolute(self, bounds: Rectangle, content_bounds: Rectangle) -> f32 {
+ match self {
+ Self::Absolute(absolute) => {
+ let hidden_content =
+ (content_bounds.height - bounds.height).max(0.0);
+
+ absolute.min(hidden_content)
+ }
+ Self::Relative(percentage) => {
+ ((content_bounds.height - bounds.height) * percentage).max(0.0)
+ }
+ }
+ }
}
impl State {
@@ -406,13 +522,14 @@ impl State {
return;
}
- self.offset = (self.offset - delta_y)
- .max(0.0)
- .min((content_bounds.height - bounds.height) as f32);
+ self.offset = Offset::Absolute(
+ (self.offset.absolute(bounds, content_bounds) - delta_y)
+ .max(0.0)
+ .min((content_bounds.height - bounds.height) as f32),
+ );
}
- /// Moves the scroll position to a relative amount, given the bounds of
- /// the [`Scrollable`] and its contents.
+ /// Scrolls the [`Scrollable`] to a relative amount.
///
/// `0` represents scrollbar at the top, while `1` represents scrollbar at
/// the bottom.
@@ -422,23 +539,40 @@ impl State {
bounds: Rectangle,
content_bounds: Rectangle,
) {
+ self.snap_to(percentage);
+ self.unsnap(bounds, content_bounds);
+ }
+
+ /// Snaps the scroll position to a relative amount.
+ ///
+ /// `0` represents scrollbar at the top, while `1` represents scrollbar at
+ /// the bottom.
+ pub fn snap_to(&mut self, percentage: f32) {
+ self.offset = Offset::Relative(percentage.max(0.0).min(1.0));
+ }
+
+ /// Unsnaps the current scroll position, if snapped, given the bounds of the
+ /// [`Scrollable`] and its contents.
+ pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) {
self.offset =
- ((content_bounds.height - bounds.height) * percentage).max(0.0);
+ Offset::Absolute(self.offset.absolute(bounds, content_bounds));
}
/// Returns the current scrolling offset of the [`State`], given the bounds
/// of the [`Scrollable`] and its contents.
pub fn offset(&self, bounds: Rectangle, content_bounds: Rectangle) -> u32 {
- let hidden_content =
- (content_bounds.height - bounds.height).max(0.0).round() as u32;
-
- self.offset.min(hidden_content as f32) as u32
+ self.offset.absolute(bounds, content_bounds) as u32
}
/// Returns whether the scroller is currently grabbed or not.
pub fn is_scroller_grabbed(&self) -> bool {
self.scroller_grabbed_at.is_some()
}
+
+ /// Returns whether the scroll box is currently touched or not.
+ pub fn is_scroll_box_touched(&self) -> bool {
+ self.scroll_box_touched_at.is_some()
+ }
}
/// The scrollbar of a [`Scrollable`].
diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs
index ff39b816..2a74d5a3 100644
--- a/native/src/widget/slider.rs
+++ b/native/src/widget/slider.rs
@@ -4,6 +4,7 @@
use crate::event::{self, Event};
use crate::layout;
use crate::mouse;
+use crate::touch;
use crate::{
Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget,
};
@@ -179,9 +180,9 @@ where
event: Event,
layout: Layout<'_>,
cursor_position: Point,
- messages: &mut Vec<Message>,
_renderer: &Renderer,
- _clipboard: Option<&dyn Clipboard>,
+ _clipboard: &mut dyn Clipboard,
+ messages: &mut Vec<Message>,
) -> event::Status {
let mut change = || {
let bounds = layout.bounds();
@@ -207,34 +208,35 @@ where
};
match event {
- Event::Mouse(mouse_event) => match mouse_event {
- mouse::Event::ButtonPressed(mouse::Button::Left) => {
- if layout.bounds().contains(cursor_position) {
- change();
- self.state.is_dragging = true;
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
+ | Event::Touch(touch::Event::FingerPressed { .. }) => {
+ if layout.bounds().contains(cursor_position) {
+ change();
+ self.state.is_dragging = true;
- return event::Status::Captured;
- }
+ return event::Status::Captured;
}
- mouse::Event::ButtonReleased(mouse::Button::Left) => {
- if self.state.is_dragging {
- if let Some(on_release) = self.on_release.clone() {
- messages.push(on_release);
- }
- self.state.is_dragging = false;
-
- return event::Status::Captured;
+ }
+ Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
+ | Event::Touch(touch::Event::FingerLifted { .. })
+ | Event::Touch(touch::Event::FingerLost { .. }) => {
+ if self.state.is_dragging {
+ if let Some(on_release) = self.on_release.clone() {
+ messages.push(on_release);
}
+ self.state.is_dragging = false;
+
+ return event::Status::Captured;
}
- mouse::Event::CursorMoved { .. } => {
- if self.state.is_dragging {
- change();
+ }
+ Event::Mouse(mouse::Event::CursorMoved { .. })
+ | Event::Touch(touch::Event::FingerMoved { .. }) => {
+ if self.state.is_dragging {
+ change();
- return event::Status::Captured;
- }
+ return event::Status::Captured;
}
- _ => {}
- },
+ }
_ => {}
}
diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs
index 3e637e97..cec1e485 100644
--- a/native/src/widget/text_input.rs
+++ b/native/src/widget/text_input.rs
@@ -16,8 +16,10 @@ use crate::keyboard;
use crate::layout;
use crate::mouse::{self, click};
use crate::text;
+use crate::touch;
use crate::{
- Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget,
+ Clipboard, Element, Hasher, Layout, Length, Padding, Point, Rectangle,
+ Size, Widget,
};
use std::u32;
@@ -55,7 +57,7 @@ pub struct TextInput<'a, Message, Renderer: self::Renderer> {
font: Renderer::Font,
width: Length,
max_width: u32,
- padding: u16,
+ padding: Padding,
size: Option<u16>,
on_change: Box<dyn Fn(String) -> Message>,
on_submit: Option<Message>,
@@ -91,7 +93,7 @@ where
font: Default::default(),
width: Length::Fill,
max_width: u32::MAX,
- padding: 0,
+ padding: Padding::ZERO,
size: None,
on_change: Box::new(on_change),
on_submit: None,
@@ -125,9 +127,9 @@ where
self
}
- /// Sets the padding of the [`TextInput`].
- pub fn padding(mut self, units: u16) -> Self {
- self.padding = units;
+ /// Sets the [`Padding`] of the [`TextInput`].
+ pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
+ self.padding = padding.into();
self
}
@@ -222,19 +224,21 @@ where
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let padding = self.padding as f32;
let text_size = self.size.unwrap_or(renderer.default_size());
let limits = limits
- .pad(padding)
+ .pad(self.padding)
.width(self.width)
.max_width(self.max_width)
.height(Length::Units(text_size));
let mut text = layout::Node::new(limits.resolve(Size::ZERO));
- text.move_to(Point::new(padding, padding));
+ text.move_to(Point::new(
+ self.padding.left.into(),
+ self.padding.top.into(),
+ ));
- layout::Node::with_children(text.size().pad(padding), vec![text])
+ layout::Node::with_children(text.size().pad(self.padding), vec![text])
}
fn on_event(
@@ -242,12 +246,13 @@ where
event: Event,
layout: Layout<'_>,
cursor_position: Point,
- messages: &mut Vec<Message>,
renderer: &Renderer,
- clipboard: Option<&dyn Clipboard>,
+ clipboard: &mut dyn Clipboard,
+ messages: &mut Vec<Message>,
) -> event::Status {
match event {
- Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
+ | Event::Touch(touch::Event::FingerPressed { .. }) => {
let is_clicked = layout.bounds().contains(cursor_position);
self.state.is_focused = is_clicked;
@@ -318,13 +323,16 @@ where
return event::Status::Captured;
}
}
- Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
+ Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
+ | Event::Touch(touch::Event::FingerLifted { .. })
+ | Event::Touch(touch::Event::FingerLost { .. }) => {
self.state.is_dragging = false;
}
- Event::Mouse(mouse::Event::CursorMoved { x, .. }) => {
+ Event::Mouse(mouse::Event::CursorMoved { position })
+ | Event::Touch(touch::Event::FingerMoved { position, .. }) => {
if self.state.is_dragging {
let text_layout = layout.children().next().unwrap();
- let target = x - text_layout.bounds().x;
+ let target = position.x - text_layout.bounds().x;
if target > 0.0 {
let value = if self.is_secure {
@@ -354,7 +362,7 @@ where
Event::Keyboard(keyboard::Event::CharacterReceived(c))
if self.state.is_focused
&& self.state.is_pasting.is_none()
- && !self.state.keyboard_modifiers.is_command_pressed()
+ && !self.state.keyboard_modifiers.command()
&& !c.is_control() =>
{
let mut editor =
@@ -442,7 +450,7 @@ where
if platform::is_jump_modifier_pressed(modifiers)
&& !self.is_secure
{
- if modifiers.shift {
+ if modifiers.shift() {
self.state
.cursor
.select_left_by_words(&self.value);
@@ -451,7 +459,7 @@ where
.cursor
.move_left_by_words(&self.value);
}
- } else if modifiers.shift {
+ } else if modifiers.shift() {
self.state.cursor.select_left(&self.value)
} else {
self.state.cursor.move_left(&self.value);
@@ -461,7 +469,7 @@ where
if platform::is_jump_modifier_pressed(modifiers)
&& !self.is_secure
{
- if modifiers.shift {
+ if modifiers.shift() {
self.state
.cursor
.select_right_by_words(&self.value);
@@ -470,14 +478,14 @@ where
.cursor
.move_right_by_words(&self.value);
}
- } else if modifiers.shift {
+ } else if modifiers.shift() {
self.state.cursor.select_right(&self.value)
} else {
self.state.cursor.move_right(&self.value);
}
}
keyboard::KeyCode::Home => {
- if modifiers.shift {
+ if modifiers.shift() {
self.state.cursor.select_range(
self.state.cursor.start(&self.value),
0,
@@ -487,7 +495,7 @@ where
}
}
keyboard::KeyCode::End => {
- if modifiers.shift {
+ if modifiers.shift() {
self.state.cursor.select_range(
self.state.cursor.start(&self.value),
self.value.len(),
@@ -496,45 +504,75 @@ where
self.state.cursor.move_to(self.value.len());
}
}
- keyboard::KeyCode::V => {
- if self.state.keyboard_modifiers.is_command_pressed() {
- if let Some(clipboard) = clipboard {
- let content = match self.state.is_pasting.take()
- {
- Some(content) => content,
- None => {
- let content: String = clipboard
- .content()
- .unwrap_or(String::new())
- .chars()
- .filter(|c| !c.is_control())
- .collect();
-
- Value::new(&content)
- }
- };
-
- let mut editor = Editor::new(
- &mut self.value,
- &mut self.state.cursor,
+ keyboard::KeyCode::C
+ if self.state.keyboard_modifiers.command() =>
+ {
+ match self.state.cursor.selection(&self.value) {
+ Some((start, end)) => {
+ clipboard.write(
+ self.value.select(start, end).to_string(),
);
+ }
+ None => {}
+ }
+ }
+ keyboard::KeyCode::X
+ if self.state.keyboard_modifiers.command() =>
+ {
+ match self.state.cursor.selection(&self.value) {
+ Some((start, end)) => {
+ clipboard.write(
+ self.value.select(start, end).to_string(),
+ );
+ }
+ None => {}
+ }
- editor.paste(content.clone());
+ let mut editor = Editor::new(
+ &mut self.value,
+ &mut self.state.cursor,
+ );
- let message =
- (self.on_change)(editor.contents());
- messages.push(message);
+ editor.delete();
- self.state.is_pasting = Some(content);
- }
+ let message = (self.on_change)(editor.contents());
+ messages.push(message);
+ }
+ keyboard::KeyCode::V => {
+ if self.state.keyboard_modifiers.command() {
+ let content = match self.state.is_pasting.take() {
+ Some(content) => content,
+ None => {
+ let content: String = clipboard
+ .read()
+ .unwrap_or(String::new())
+ .chars()
+ .filter(|c| !c.is_control())
+ .collect();
+
+ Value::new(&content)
+ }
+ };
+
+ let mut editor = Editor::new(
+ &mut self.value,
+ &mut self.state.cursor,
+ );
+
+ editor.paste(content.clone());
+
+ let message = (self.on_change)(editor.contents());
+ messages.push(message);
+
+ self.state.is_pasting = Some(content);
} else {
self.state.is_pasting = None;
}
}
- keyboard::KeyCode::A => {
- if self.state.keyboard_modifiers.is_command_pressed() {
- self.state.cursor.select_all(&self.value);
- }
+ keyboard::KeyCode::A
+ if self.state.keyboard_modifiers.command() =>
+ {
+ self.state.cursor.select_all(&self.value);
}
keyboard::KeyCode::Escape => {
self.state.is_focused = false;
@@ -748,6 +786,11 @@ impl State {
pub fn move_cursor_to(&mut self, position: usize) {
self.cursor.move_to(position);
}
+
+ /// Selects all the content of the [`TextInput`].
+ pub fn select_all(&mut self) {
+ self.cursor.select_range(0, usize::MAX);
+ }
}
// TODO: Reduce allocations
@@ -811,9 +854,9 @@ mod platform {
pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool {
if cfg!(target_os = "macos") {
- modifiers.alt
+ modifiers.alt()
} else {
- modifiers.control
+ modifiers.control()
}
}
}
diff --git a/native/src/widget/text_input/cursor.rs b/native/src/widget/text_input/cursor.rs
index e630e293..4f3b159b 100644
--- a/native/src/widget/text_input/cursor.rs
+++ b/native/src/widget/text_input/cursor.rs
@@ -48,6 +48,18 @@ impl Cursor {
}
}
+ /// Returns the current selection of the [`Cursor`] for the given [`Value`].
+ ///
+ /// `start` is guaranteed to be <= than `end`.
+ pub fn selection(&self, value: &Value) -> Option<(usize, usize)> {
+ match self.state(value) {
+ State::Selection { start, end } => {
+ Some((start.min(end), start.max(end)))
+ }
+ _ => None,
+ }
+ }
+
pub(crate) fn move_to(&mut self, position: usize) {
self.state = State::Index(position);
}
@@ -101,7 +113,7 @@ impl Cursor {
State::Selection { start, end } if end > 0 => {
self.select_range(start, end - 1)
}
- _ => (),
+ _ => {}
}
}
@@ -113,7 +125,7 @@ impl Cursor {
State::Selection { start, end } if end < value.len() => {
self.select_range(start, end + 1)
}
- _ => (),
+ _ => {}
}
}
@@ -161,15 +173,6 @@ impl Cursor {
end.min(value.len())
}
- pub(crate) fn selection(&self, value: &Value) -> Option<(usize, usize)> {
- match self.state(value) {
- State::Selection { start, end } => {
- Some((start.min(end), start.max(end)))
- }
- _ => None,
- }
- }
-
fn left(&self, value: &Value) -> usize {
match self.state(value) {
State::Index(index) => index,
diff --git a/native/src/widget/text_input/editor.rs b/native/src/widget/text_input/editor.rs
index 20e42567..0b50a382 100644
--- a/native/src/widget/text_input/editor.rs
+++ b/native/src/widget/text_input/editor.rs
@@ -20,7 +20,7 @@ impl<'a> Editor<'a> {
self.cursor.move_left(self.value);
self.value.remove_many(left, right);
}
- _ => (),
+ _ => {}
}
self.value.insert(self.cursor.end(self.value), character);
@@ -35,7 +35,7 @@ impl<'a> Editor<'a> {
self.cursor.move_left(self.value);
self.value.remove_many(left, right);
}
- _ => (),
+ _ => {}
}
self.value.insert_many(self.cursor.end(self.value), content);
diff --git a/native/src/widget/text_input/value.rs b/native/src/widget/text_input/value.rs
index 86be2790..2034cca4 100644
--- a/native/src/widget/text_input/value.rs
+++ b/native/src/widget/text_input/value.rs
@@ -73,6 +73,15 @@ impl Value {
.unwrap_or(self.len())
}
+ /// Returns a new [`Value`] containing the graphemes from `start` until the
+ /// given `end`.
+ pub fn select(&self, start: usize, end: usize) -> Self {
+ let graphemes =
+ self.graphemes[start.min(self.len())..end.min(self.len())].to_vec();
+
+ Self { graphemes }
+ }
+
/// Returns a new [`Value`] containing the graphemes until the given
/// `index`.
pub fn until(&self, index: usize) -> Self {
diff --git a/native/src/widget/toggler.rs b/native/src/widget/toggler.rs
new file mode 100644
index 00000000..4035276c
--- /dev/null
+++ b/native/src/widget/toggler.rs
@@ -0,0 +1,277 @@
+//! Show toggle controls using togglers.
+use std::hash::Hash;
+
+use crate::{
+ event, layout, mouse, row, text, Align, Clipboard, Element, Event, Hasher,
+ HorizontalAlignment, Layout, Length, Point, Rectangle, Row, Text,
+ VerticalAlignment, Widget,
+};
+
+/// A toggler widget
+///
+/// # Example
+///
+/// ```
+/// # type Toggler<Message> = iced_native::Toggler<Message, iced_native::renderer::Null>;
+/// #
+/// pub enum Message {
+/// TogglerToggled(bool),
+/// }
+///
+/// let is_active = true;
+///
+/// Toggler::new(is_active, String::from("Toggle me!"), |b| Message::TogglerToggled(b));
+/// ```
+#[allow(missing_debug_implementations)]
+pub struct Toggler<Message, Renderer: self::Renderer + text::Renderer> {
+ is_active: bool,
+ on_toggle: Box<dyn Fn(bool) -> Message>,
+ label: Option<String>,
+ width: Length,
+ size: u16,
+ text_size: Option<u16>,
+ text_alignment: HorizontalAlignment,
+ spacing: u16,
+ font: Renderer::Font,
+ style: Renderer::Style,
+}
+
+impl<Message, Renderer: self::Renderer + text::Renderer>
+ Toggler<Message, Renderer>
+{
+ /// Creates a new [`Toggler`].
+ ///
+ /// It expects:
+ /// * a boolean describing whether the [`Toggler`] is checked or not
+ /// * An optional label for the [`Toggler`]
+ /// * a function that will be called when the [`Toggler`] is toggled. It
+ /// will receive the new state of the [`Toggler`] and must produce a
+ /// `Message`.
+ pub fn new<F>(
+ is_active: bool,
+ label: impl Into<Option<String>>,
+ f: F,
+ ) -> Self
+ where
+ F: 'static + Fn(bool) -> Message,
+ {
+ Toggler {
+ is_active,
+ on_toggle: Box::new(f),
+ label: label.into(),
+ width: Length::Fill,
+ size: <Renderer as self::Renderer>::DEFAULT_SIZE,
+ text_size: None,
+ text_alignment: HorizontalAlignment::Left,
+ spacing: 0,
+ font: Renderer::Font::default(),
+ style: Renderer::Style::default(),
+ }
+ }
+
+ /// Sets the size of the [`Toggler`].
+ pub fn size(mut self, size: u16) -> Self {
+ self.size = size;
+ self
+ }
+
+ /// Sets the width of the [`Toggler`].
+ pub fn width(mut self, width: Length) -> Self {
+ self.width = width;
+ self
+ }
+
+ /// Sets the text size o the [`Toggler`].
+ pub fn text_size(mut self, text_size: u16) -> Self {
+ self.text_size = Some(text_size);
+ self
+ }
+
+ /// Sets the horizontal alignment of the text of the [`Toggler`]
+ pub fn text_alignment(mut self, alignment: HorizontalAlignment) -> Self {
+ self.text_alignment = alignment;
+ self
+ }
+
+ /// Sets the spacing between the [`Toggler`] and the text.
+ pub fn spacing(mut self, spacing: u16) -> Self {
+ self.spacing = spacing;
+ self
+ }
+
+ /// Sets the [`Font`] of the text of the [`Toggler`]
+ pub fn font(mut self, font: Renderer::Font) -> Self {
+ self.font = font;
+ self
+ }
+
+ /// Sets the style of the [`Toggler`].
+ pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {
+ self.style = style.into();
+ self
+ }
+}
+
+impl<Message, Renderer> Widget<Message, Renderer> for Toggler<Message, Renderer>
+where
+ Renderer: self::Renderer + text::Renderer + row::Renderer,
+{
+ fn width(&self) -> Length {
+ self.width
+ }
+
+ fn height(&self) -> Length {
+ Length::Shrink
+ }
+
+ fn layout(
+ &self,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ let mut row = Row::<(), Renderer>::new()
+ .width(self.width)
+ .spacing(self.spacing)
+ .align_items(Align::Center);
+
+ if let Some(label) = &self.label {
+ row = row.push(
+ Text::new(label)
+ .horizontal_alignment(self.text_alignment)
+ .font(self.font)
+ .width(self.width)
+ .size(self.text_size.unwrap_or(renderer.default_size())),
+ );
+ }
+
+ row = row.push(
+ Row::new()
+ .width(Length::Units(2 * self.size))
+ .height(Length::Units(self.size)),
+ );
+
+ row.layout(renderer, limits)
+ }
+
+ fn on_event(
+ &mut self,
+ event: Event,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ _renderer: &Renderer,
+ _clipboard: &mut dyn Clipboard,
+ messages: &mut Vec<Message>,
+ ) -> event::Status {
+ match event {
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
+ let mouse_over = layout.bounds().contains(cursor_position);
+
+ if mouse_over {
+ messages.push((self.on_toggle)(!self.is_active));
+
+ event::Status::Captured
+ } else {
+ event::Status::Ignored
+ }
+ }
+ _ => event::Status::Ignored,
+ }
+ }
+
+ fn draw(
+ &self,
+ renderer: &mut Renderer,
+ defaults: &Renderer::Defaults,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ _viewport: &Rectangle,
+ ) -> Renderer::Output {
+ let bounds = layout.bounds();
+ let mut children = layout.children();
+
+ let label = match &self.label {
+ Some(label) => {
+ let label_layout = children.next().unwrap();
+
+ Some(text::Renderer::draw(
+ renderer,
+ defaults,
+ label_layout.bounds(),
+ &label,
+ self.text_size.unwrap_or(renderer.default_size()),
+ self.font,
+ None,
+ self.text_alignment,
+ VerticalAlignment::Center,
+ ))
+ }
+
+ None => None,
+ };
+
+ let toggler_layout = children.next().unwrap();
+ let toggler_bounds = toggler_layout.bounds();
+
+ let is_mouse_over = bounds.contains(cursor_position);
+
+ self::Renderer::draw(
+ renderer,
+ toggler_bounds,
+ self.is_active,
+ is_mouse_over,
+ label,
+ &self.style,
+ )
+ }
+
+ fn hash_layout(&self, state: &mut Hasher) {
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
+ self.label.hash(state)
+ }
+}
+
+/// The renderer of a [`Toggler`].
+///
+/// Your [renderer] will need to implement this trait before being
+/// able to use a [`Toggler`] in your user interface.
+///
+/// [renderer]: ../../renderer/index.html
+pub trait Renderer: crate::Renderer {
+ /// The style supported by this renderer.
+ type Style: Default;
+
+ /// The default size of a [`Toggler`].
+ const DEFAULT_SIZE: u16;
+
+ /// Draws a [`Toggler`].
+ ///
+ /// It receives:
+ /// * the bounds of the [`Toggler`]
+ /// * whether the [`Toggler`] is activated or not
+ /// * whether the mouse is over the [`Toggler`] or not
+ /// * the drawn label of the [`Toggler`]
+ /// * the style of the [`Toggler`]
+ fn draw(
+ &mut self,
+ bounds: Rectangle,
+ is_active: bool,
+ is_mouse_over: bool,
+ label: Option<Self::Output>,
+ style: &Self::Style,
+ ) -> Self::Output;
+}
+
+impl<'a, Message, Renderer> From<Toggler<Message, Renderer>>
+ for Element<'a, Message, Renderer>
+where
+ Renderer: 'a + self::Renderer + text::Renderer + row::Renderer,
+ Message: 'a,
+{
+ fn from(
+ toggler: Toggler<Message, Renderer>,
+ ) -> Element<'a, Message, Renderer> {
+ Element::new(toggler)
+ }
+}
diff --git a/native/src/widget/tooltip.rs b/native/src/widget/tooltip.rs
new file mode 100644
index 00000000..276afd41
--- /dev/null
+++ b/native/src/widget/tooltip.rs
@@ -0,0 +1,210 @@
+//! Display a widget over another.
+use std::hash::Hash;
+
+use iced_core::Rectangle;
+
+use crate::widget::container;
+use crate::widget::text::{self, Text};
+use crate::{
+ event, layout, Clipboard, Element, Event, Hasher, Layout, Length, Point,
+ Widget,
+};
+
+/// An element to display a widget over another.
+#[allow(missing_debug_implementations)]
+pub struct Tooltip<'a, Message, Renderer: self::Renderer> {
+ content: Element<'a, Message, Renderer>,
+ tooltip: Text<Renderer>,
+ position: Position,
+ style: <Renderer as container::Renderer>::Style,
+ gap: u16,
+ padding: u16,
+}
+
+impl<'a, Message, Renderer> Tooltip<'a, Message, Renderer>
+where
+ Renderer: self::Renderer,
+{
+ /// Creates an empty [`Tooltip`].
+ ///
+ /// [`Tooltip`]: struct.Tooltip.html
+ pub fn new(
+ content: impl Into<Element<'a, Message, Renderer>>,
+ tooltip: impl ToString,
+ position: Position,
+ ) -> Self {
+ Tooltip {
+ content: content.into(),
+ tooltip: Text::new(tooltip.to_string()),
+ position,
+ style: Default::default(),
+ gap: 0,
+ padding: Renderer::DEFAULT_PADDING,
+ }
+ }
+
+ /// Sets the size of the text of the [`Tooltip`].
+ pub fn size(mut self, size: u16) -> Self {
+ self.tooltip = self.tooltip.size(size);
+ self
+ }
+
+ /// Sets the font of the [`Tooltip`].
+ ///
+ /// [`Font`]: Renderer::Font
+ pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
+ self.tooltip = self.tooltip.font(font);
+ self
+ }
+
+ /// Sets the gap between the content and its [`Tooltip`].
+ pub fn gap(mut self, gap: u16) -> Self {
+ self.gap = gap;
+ self
+ }
+
+ /// Sets the padding of the [`Tooltip`].
+ pub fn padding(mut self, padding: u16) -> Self {
+ self.padding = padding;
+ self
+ }
+
+ /// Sets the style of the [`Tooltip`].
+ pub fn style(
+ mut self,
+ style: impl Into<<Renderer as container::Renderer>::Style>,
+ ) -> Self {
+ self.style = style.into();
+ self
+ }
+}
+
+/// The position of the tooltip. Defaults to following the cursor.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Position {
+ /// The tooltip will follow the cursor.
+ FollowCursor,
+ /// The tooltip will appear on the top of the widget.
+ Top,
+ /// The tooltip will appear on the bottom of the widget.
+ Bottom,
+ /// The tooltip will appear on the left of the widget.
+ Left,
+ /// The tooltip will appear on the right of the widget.
+ Right,
+}
+
+impl<'a, Message, Renderer> Widget<Message, Renderer>
+ for Tooltip<'a, Message, Renderer>
+where
+ Renderer: self::Renderer,
+{
+ fn width(&self) -> Length {
+ self.content.width()
+ }
+
+ fn height(&self) -> Length {
+ self.content.height()
+ }
+
+ fn layout(
+ &self,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ self.content.layout(renderer, limits)
+ }
+
+ fn on_event(
+ &mut self,
+ event: Event,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ renderer: &Renderer,
+ clipboard: &mut dyn Clipboard,
+ messages: &mut Vec<Message>,
+ ) -> event::Status {
+ self.content.widget.on_event(
+ event,
+ layout,
+ cursor_position,
+ renderer,
+ clipboard,
+ messages,
+ )
+ }
+
+ fn draw(
+ &self,
+ renderer: &mut Renderer,
+ defaults: &Renderer::Defaults,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ viewport: &Rectangle,
+ ) -> Renderer::Output {
+ self::Renderer::draw(
+ renderer,
+ defaults,
+ cursor_position,
+ layout,
+ viewport,
+ &self.content,
+ &self.tooltip,
+ self.position,
+ &self.style,
+ self.gap,
+ self.padding,
+ )
+ }
+
+ fn hash_layout(&self, state: &mut Hasher) {
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
+ self.content.hash_layout(state);
+ }
+}
+
+/// The renderer of a [`Tooltip`].
+///
+/// Your [renderer] will need to implement this trait before being
+/// able to use a [`Tooltip`] in your user interface.
+///
+/// [`Tooltip`]: struct.Tooltip.html
+/// [renderer]: ../../renderer/index.html
+pub trait Renderer:
+ crate::Renderer + text::Renderer + container::Renderer
+{
+ /// The default padding of a [`Tooltip`] drawn by this renderer.
+ const DEFAULT_PADDING: u16;
+
+ /// Draws a [`Tooltip`].
+ ///
+ /// [`Tooltip`]: struct.Tooltip.html
+ fn draw<Message>(
+ &mut self,
+ defaults: &Self::Defaults,
+ cursor_position: Point,
+ content_layout: Layout<'_>,
+ viewport: &Rectangle,
+ content: &Element<'_, Message, Self>,
+ tooltip: &Text<Self>,
+ position: Position,
+ style: &<Self as container::Renderer>::Style,
+ gap: u16,
+ padding: u16,
+ ) -> Self::Output;
+}
+
+impl<'a, Message, Renderer> From<Tooltip<'a, Message, Renderer>>
+ for Element<'a, Message, Renderer>
+where
+ Renderer: 'a + self::Renderer,
+ Message: 'a,
+{
+ fn from(
+ column: Tooltip<'a, Message, Renderer>,
+ ) -> Element<'a, Message, Renderer> {
+ Element::new(column)
+ }
+}
diff --git a/native/src/window/event.rs b/native/src/window/event.rs
index b177141a..3aa1ab0b 100644
--- a/native/src/window/event.rs
+++ b/native/src/window/event.rs
@@ -3,7 +3,7 @@ use std::path::PathBuf;
/// A window-related event.
#[derive(PartialEq, Clone, Debug)]
pub enum Event {
- /// A window was resized
+ /// A window was resized.
Resized {
/// The new width of the window (in units)
width: u32,
@@ -12,6 +12,18 @@ pub enum Event {
height: u32,
},
+ /// 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.
+ Focused,
+
+ /// A window was unfocused.
+ Unfocused,
+
/// A file is being hovered over the window.
///
/// When the user hovers multiple files at once, this event will be emitted
diff --git a/rustfmt.toml b/rustfmt.toml
index d979d317..fa50ab92 100644
--- a/rustfmt.toml
+++ b/rustfmt.toml
@@ -1 +1,2 @@
max_width=80
+edition="2018"
diff --git a/src/application.rs b/src/application.rs
index 3b690a7c..78280e98 100644
--- a/src/application.rs
+++ b/src/application.rs
@@ -1,5 +1,7 @@
use crate::window;
-use crate::{Color, Command, Element, Executor, Settings, Subscription};
+use crate::{
+ Clipboard, Color, Command, Element, Executor, Menu, Settings, Subscription,
+};
/// An interactive cross-platform application.
///
@@ -37,15 +39,15 @@ use crate::{Color, Command, Element, Executor, Settings, Subscription};
/// to listen to time.
/// - [`todos`], a todos tracker inspired by [TodoMVC].
///
-/// [The repository has a bunch of examples]: https://github.com/hecrj/iced/tree/0.2/examples
-/// [`clock`]: https://github.com/hecrj/iced/tree/0.2/examples/clock
-/// [`download_progress`]: https://github.com/hecrj/iced/tree/0.2/examples/download_progress
-/// [`events`]: https://github.com/hecrj/iced/tree/0.2/examples/events
-/// [`game_of_life`]: https://github.com/hecrj/iced/tree/0.2/examples/game_of_life
-/// [`pokedex`]: https://github.com/hecrj/iced/tree/0.2/examples/pokedex
-/// [`solar_system`]: https://github.com/hecrj/iced/tree/0.2/examples/solar_system
-/// [`stopwatch`]: https://github.com/hecrj/iced/tree/0.2/examples/stopwatch
-/// [`todos`]: https://github.com/hecrj/iced/tree/0.2/examples/todos
+/// [The repository has a bunch of examples]: https://github.com/hecrj/iced/tree/0.3/examples
+/// [`clock`]: https://github.com/hecrj/iced/tree/0.3/examples/clock
+/// [`download_progress`]: https://github.com/hecrj/iced/tree/0.3/examples/download_progress
+/// [`events`]: https://github.com/hecrj/iced/tree/0.3/examples/events
+/// [`game_of_life`]: https://github.com/hecrj/iced/tree/0.3/examples/game_of_life
+/// [`pokedex`]: https://github.com/hecrj/iced/tree/0.3/examples/pokedex
+/// [`solar_system`]: https://github.com/hecrj/iced/tree/0.3/examples/solar_system
+/// [`stopwatch`]: https://github.com/hecrj/iced/tree/0.3/examples/stopwatch
+/// [`todos`]: https://github.com/hecrj/iced/tree/0.3/examples/todos
/// [`Sandbox`]: crate::Sandbox
/// [`Canvas`]: crate::widget::Canvas
/// [PokéAPI]: https://pokeapi.co/
@@ -57,7 +59,7 @@ use crate::{Color, Command, Element, Executor, Settings, Subscription};
/// says "Hello, world!":
///
/// ```no_run
-/// use iced::{executor, Application, Command, Element, Settings, Text};
+/// use iced::{executor, Application, Clipboard, Command, Element, Settings, Text};
///
/// pub fn main() -> iced::Result {
/// Hello::run(Settings::default())
@@ -78,7 +80,7 @@ use crate::{Color, Command, Element, Executor, Settings, Subscription};
/// String::from("A cool application")
/// }
///
-/// fn update(&mut self, _message: Self::Message) -> Command<Self::Message> {
+/// fn update(&mut self, _message: Self::Message, _clipboard: &mut Clipboard) -> Command<Self::Message> {
/// Command::none()
/// }
///
@@ -97,7 +99,7 @@ pub trait Application: Sized {
type Executor: Executor;
/// The type of __messages__ your [`Application`] will produce.
- type Message: std::fmt::Debug + Send;
+ type Message: std::fmt::Debug + Clone + Send;
/// The data needed to initialize your [`Application`].
type Flags;
@@ -127,7 +129,11 @@ pub trait Application: Sized {
/// this method.
///
/// Any [`Command`] returned will be executed immediately in the background.
- fn update(&mut self, message: Self::Message) -> Command<Self::Message>;
+ fn update(
+ &mut self,
+ message: Self::Message,
+ clipboard: &mut Clipboard,
+ ) -> Command<Self::Message>;
/// Returns the event [`Subscription`] for the current state of the
/// application.
@@ -178,6 +184,20 @@ pub trait Application: Sized {
1.0
}
+ /// Returns whether the [`Application`] should be terminated.
+ ///
+ /// By default, it returns `false`.
+ fn should_exit(&self) -> bool {
+ false
+ }
+
+ /// Returns the current system [`Menu`] of the [`Application`].
+ ///
+ /// By default, it returns an empty [`Menu`].
+ fn menu(&self) -> Menu<Self::Message> {
+ Menu::new()
+ }
+
/// Runs the [`Application`].
///
/// On native platforms, this method will take control of the current thread
@@ -195,12 +215,13 @@ pub trait Application: Sized {
let renderer_settings = crate::renderer::Settings {
default_font: settings.default_font,
default_text_size: settings.default_text_size,
+ text_multithreading: settings.text_multithreading,
antialiasing: if settings.antialiasing {
Some(crate::renderer::settings::Antialiasing::MSAAx4)
} else {
None
},
- ..crate::renderer::Settings::default()
+ ..crate::renderer::Settings::from_env()
};
Ok(crate::runtime::application::run::<
@@ -228,9 +249,14 @@ where
{
type Renderer = crate::renderer::Renderer;
type Message = A::Message;
-
- fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
- self.0.update(message)
+ type Clipboard = iced_winit::Clipboard;
+
+ fn update(
+ &mut self,
+ message: Self::Message,
+ clipboard: &mut iced_winit::Clipboard,
+ ) -> Command<Self::Message> {
+ self.0.update(message, clipboard)
}
fn view(&mut self) -> Element<'_, Self::Message> {
@@ -259,6 +285,7 @@ where
match self.0.mode() {
window::Mode::Windowed => iced_winit::Mode::Windowed,
window::Mode::Fullscreen => iced_winit::Mode::Fullscreen,
+ window::Mode::Hidden => iced_winit::Mode::Hidden,
}
}
@@ -273,6 +300,14 @@ where
fn scale_factor(&self) -> f64 {
self.0.scale_factor()
}
+
+ fn should_exit(&self) -> bool {
+ self.0.should_exit()
+ }
+
+ fn menu(&self) -> Menu<Self::Message> {
+ self.0.menu()
+ }
}
#[cfg(target_arch = "wasm32")]
@@ -294,8 +329,12 @@ where
self.0.title()
}
- fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
- self.0.update(message)
+ fn update(
+ &mut self,
+ message: Self::Message,
+ clipboard: &mut Clipboard,
+ ) -> Command<Self::Message> {
+ self.0.update(message, clipboard)
}
fn subscription(&self) -> Subscription<Self::Message> {
diff --git a/src/error.rs b/src/error.rs
index 31b87d17..c8fa6636 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -9,7 +9,7 @@ pub enum Error {
/// The application window could not be created.
#[error("the application window could not be created")]
- WindowCreationFailed(Box<dyn std::error::Error>),
+ WindowCreationFailed(Box<dyn std::error::Error + Send + Sync>),
/// A suitable graphics adapter or device could not be found.
#[error("a suitable graphics adapter or device could not be found")]
@@ -32,3 +32,14 @@ impl From<iced_winit::Error> for Error {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn assert_send_sync() {
+ fn _assert<T: Send + Sync>() {}
+ _assert::<Error>();
+ }
+}
diff --git a/src/executor.rs b/src/executor.rs
index 0333bc1d..9f3656b1 100644
--- a/src/executor.rs
+++ b/src/executor.rs
@@ -10,19 +10,30 @@ mod platform {
#[cfg(feature = "tokio_old")]
type Executor = executor::TokioOld;
- #[cfg(all(not(feature = "tokio_old"), feature = "tokio"))]
+ #[cfg(all(feature = "tokio", not(feature = "tokio_old")))]
type Executor = executor::Tokio;
#[cfg(all(
+ feature = "async-std",
not(any(feature = "tokio_old", feature = "tokio")),
- feature = "async-std"
))]
type Executor = executor::AsyncStd;
+ #[cfg(all(
+ feature = "smol",
+ not(any(
+ feature = "tokio_old",
+ feature = "tokio",
+ feature = "async-std"
+ )),
+ ))]
+ type Executor = executor::Smol;
+
#[cfg(not(any(
feature = "tokio_old",
feature = "tokio",
- feature = "async-std"
+ feature = "async-std",
+ feature = "smol",
)))]
type Executor = executor::ThreadPool;
diff --git a/src/lib.rs b/src/lib.rs
index 3578ea82..8cd6aa70 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -30,7 +30,7 @@
//! [windowing shell]: https://github.com/hecrj/iced/tree/master/winit
//! [`dodrio`]: https://github.com/fitzgen/dodrio
//! [web runtime]: https://github.com/hecrj/iced/tree/master/web
-//! [examples]: https://github.com/hecrj/iced/tree/0.2/examples
+//! [examples]: https://github.com/hecrj/iced/tree/0.3/examples
//! [repository]: https://github.com/hecrj/iced
//!
//! # Overview
@@ -191,7 +191,12 @@ pub mod widget;
pub mod window;
#[cfg(all(
- any(feature = "tokio", feature = "tokio_old", feature = "async-std"),
+ any(
+ feature = "tokio",
+ feature = "tokio_old",
+ feature = "async-std",
+ feature = "smol"
+ ),
not(target_arch = "wasm32")
))]
#[cfg_attr(
@@ -200,6 +205,7 @@ pub mod window;
feature = "tokio",
feature = "tokio_old",
feature = "async-std"
+ feature = "smol"
)))
)]
pub mod time;
@@ -239,6 +245,7 @@ pub use sandbox::Sandbox;
pub use settings::Settings;
pub use runtime::{
- futures, Align, Background, Color, Command, Font, HorizontalAlignment,
- Length, Point, Rectangle, Size, Subscription, Vector, VerticalAlignment,
+ futures, menu, Align, Background, Clipboard, Color, Command, Font,
+ HorizontalAlignment, Length, Menu, Point, Rectangle, Size, Subscription,
+ Vector, VerticalAlignment,
};
diff --git a/src/sandbox.rs b/src/sandbox.rs
index dbaa02f1..cb3cf624 100644
--- a/src/sandbox.rs
+++ b/src/sandbox.rs
@@ -1,5 +1,6 @@
use crate::{
- Application, Color, Command, Element, Error, Settings, Subscription,
+ Application, Clipboard, Color, Command, Element, Error, Settings,
+ Subscription,
};
/// A sandboxed [`Application`].
@@ -17,8 +18,8 @@ use crate::{
/// # Examples
/// [The repository has a bunch of examples] that use the [`Sandbox`] trait:
///
-/// - [`bezier_tool`], a Paint-like tool for drawing Bézier curves using
-/// [`lyon`].
+/// - [`bezier_tool`], a Paint-like tool for drawing Bézier curves using the
+/// [`Canvas widget`].
/// - [`counter`], the classic counter example explained in [the overview].
/// - [`custom_widget`], a demonstration of how to build a custom widget that
/// draws a circle.
@@ -35,19 +36,19 @@ use crate::{
/// - [`tour`], a simple UI tour that can run both on native platforms and the
/// web!
///
-/// [The repository has a bunch of examples]: https://github.com/hecrj/iced/tree/0.2/examples
-/// [`bezier_tool`]: https://github.com/hecrj/iced/tree/0.2/examples/bezier_tool
-/// [`counter`]: https://github.com/hecrj/iced/tree/0.2/examples/counter
-/// [`custom_widget`]: https://github.com/hecrj/iced/tree/0.2/examples/custom_widget
-/// [`geometry`]: https://github.com/hecrj/iced/tree/0.2/examples/geometry
-/// [`pane_grid`]: https://github.com/hecrj/iced/tree/0.2/examples/pane_grid
-/// [`progress_bar`]: https://github.com/hecrj/iced/tree/0.2/examples/progress_bar
-/// [`styling`]: https://github.com/hecrj/iced/tree/0.2/examples/styling
-/// [`svg`]: https://github.com/hecrj/iced/tree/0.2/examples/svg
-/// [`tour`]: https://github.com/hecrj/iced/tree/0.2/examples/tour
-/// [`lyon`]: https://github.com/nical/lyon
+/// [The repository has a bunch of examples]: https://github.com/hecrj/iced/tree/0.3/examples
+/// [`bezier_tool`]: https://github.com/hecrj/iced/tree/0.3/examples/bezier_tool
+/// [`counter`]: https://github.com/hecrj/iced/tree/0.3/examples/counter
+/// [`custom_widget`]: https://github.com/hecrj/iced/tree/0.3/examples/custom_widget
+/// [`geometry`]: https://github.com/hecrj/iced/tree/0.3/examples/geometry
+/// [`pane_grid`]: https://github.com/hecrj/iced/tree/0.3/examples/pane_grid
+/// [`progress_bar`]: https://github.com/hecrj/iced/tree/0.3/examples/progress_bar
+/// [`styling`]: https://github.com/hecrj/iced/tree/0.3/examples/styling
+/// [`svg`]: https://github.com/hecrj/iced/tree/0.3/examples/svg
+/// [`tour`]: https://github.com/hecrj/iced/tree/0.3/examples/tour
+/// [`Canvas widget`]: crate::widget::Canvas
/// [the overview]: index.html#overview
-/// [`iced_wgpu`]: https://github.com/hecrj/iced/tree/0.2/wgpu
+/// [`iced_wgpu`]: https://github.com/hecrj/iced/tree/0.3/wgpu
/// [`Svg` widget]: crate::widget::Svg
/// [Ghostscript Tiger]: https://commons.wikimedia.org/wiki/File:Ghostscript_Tiger.svg
///
@@ -87,7 +88,7 @@ use crate::{
/// ```
pub trait Sandbox {
/// The type of __messages__ your [`Sandbox`] will produce.
- type Message: std::fmt::Debug + Send;
+ type Message: std::fmt::Debug + Clone + Send;
/// Initializes the [`Sandbox`].
///
@@ -161,7 +162,11 @@ where
T::title(self)
}
- fn update(&mut self, message: T::Message) -> Command<T::Message> {
+ fn update(
+ &mut self,
+ message: T::Message,
+ _clipboard: &mut Clipboard,
+ ) -> Command<T::Message> {
T::update(self, message);
Command::none()
diff --git a/src/settings.rs b/src/settings.rs
index c82a1354..480bf813 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -25,6 +25,12 @@ pub struct Settings<Flags> {
/// The default value is 20.
pub default_text_size: u16,
+ /// If enabled, spread text workload in multiple threads when multiple cores
+ /// are available.
+ ///
+ /// By default, it is disabled.
+ pub text_multithreading: bool,
+
/// If set to true, the renderer will try to perform antialiasing for some
/// primitives.
///
@@ -35,6 +41,12 @@ 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.
+ pub exit_on_close_request: bool,
}
impl<Flags> Settings<Flags> {
@@ -46,10 +58,12 @@ impl<Flags> Settings<Flags> {
Self {
flags,
- antialiasing: default_settings.antialiasing,
+ window: default_settings.window,
default_font: default_settings.default_font,
default_text_size: default_settings.default_text_size,
- window: default_settings.window,
+ text_multithreading: default_settings.text_multithreading,
+ antialiasing: default_settings.antialiasing,
+ exit_on_close_request: default_settings.exit_on_close_request,
}
}
}
@@ -61,10 +75,12 @@ where
fn default() -> Self {
Self {
flags: Default::default(),
- antialiasing: Default::default(),
+ window: Default::default(),
default_font: Default::default(),
default_text_size: 20,
- window: Default::default(),
+ text_multithreading: false,
+ antialiasing: false,
+ exit_on_close_request: true,
}
}
}
@@ -75,6 +91,7 @@ impl<Flags> From<Settings<Flags>> for iced_winit::Settings<Flags> {
iced_winit::Settings {
window: settings.window.into(),
flags: settings.flags,
+ exit_on_close_request: settings.exit_on_close_request,
}
}
}
diff --git a/src/widget.rs b/src/widget.rs
index b9b65499..db052106 100644
--- a/src/widget.rs
+++ b/src/widget.rs
@@ -17,7 +17,8 @@
mod platform {
pub use crate::renderer::widget::{
button, checkbox, container, pane_grid, pick_list, progress_bar, radio,
- rule, scrollable, slider, text_input, Column, Row, Space, Text,
+ rule, scrollable, slider, text_input, toggler, tooltip, Column, Row,
+ Space, Text,
};
#[cfg(any(feature = "canvas", feature = "glow_canvas"))]
@@ -37,7 +38,8 @@ mod platform {
#[cfg_attr(docsrs, doc(cfg(feature = "image")))]
pub mod image {
//! Display images in your user interface.
- pub use crate::runtime::image::{Handle, Image};
+ pub use crate::runtime::image::viewer;
+ pub use crate::runtime::image::{Handle, Image, Viewer};
}
#[cfg_attr(docsrs, doc(cfg(feature = "svg")))]
@@ -51,7 +53,7 @@ mod platform {
button::Button, checkbox::Checkbox, container::Container, image::Image,
pane_grid::PaneGrid, pick_list::PickList, progress_bar::ProgressBar,
radio::Radio, rule::Rule, scrollable::Scrollable, slider::Slider,
- svg::Svg, text_input::TextInput,
+ svg::Svg, text_input::TextInput, toggler::Toggler, tooltip::Tooltip,
};
#[cfg(any(feature = "canvas", feature = "glow_canvas"))]
diff --git a/src/window.rs b/src/window.rs
index a2883b62..7d441062 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -1,9 +1,11 @@
//! Configure the window of your application in native platforms.
mod mode;
+mod position;
mod settings;
pub mod icon;
pub use icon::Icon;
pub use mode::Mode;
+pub use position::Position;
pub use settings::Settings;
diff --git a/src/window/icon.rs b/src/window/icon.rs
index 0d27b00e..287538b1 100644
--- a/src/window/icon.rs
+++ b/src/window/icon.rs
@@ -97,23 +97,25 @@ impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::InvalidData { byte_count } => {
- write!(f,
- "The provided RGBA data (with length {:?}) isn't divisble by \
+ write!(
+ f,
+ "The provided RGBA data (with length {:?}) isn't divisble by \
4. Therefore, it cannot be safely interpreted as 32bpp RGBA \
pixels.",
- byte_count,
- )
+ byte_count,
+ )
}
Error::DimensionsMismatch {
width,
height,
pixel_count,
} => {
- write!(f,
- "The number of RGBA pixels ({:?}) does not match the provided \
+ write!(
+ f,
+ "The number of RGBA pixels ({:?}) does not match the provided \
dimensions ({:?}x{:?}).",
- pixel_count, width, height,
- )
+ pixel_count, width, height,
+ )
}
Error::OsError(e) => write!(
f,
diff --git a/src/window/mode.rs b/src/window/mode.rs
index 37464711..fdce8e23 100644
--- a/src/window/mode.rs
+++ b/src/window/mode.rs
@@ -6,4 +6,7 @@ pub enum Mode {
/// The application takes the whole screen of its current monitor.
Fullscreen,
+
+ /// The application is hidden
+ Hidden,
}
diff --git a/src/window/position.rs b/src/window/position.rs
new file mode 100644
index 00000000..8535ef6a
--- /dev/null
+++ b/src/window/position.rs
@@ -0,0 +1,33 @@
+/// 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
+ }
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+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/src/window/settings.rs b/src/window/settings.rs
index 6b5d2985..ec6c3071 100644
--- a/src/window/settings.rs
+++ b/src/window/settings.rs
@@ -1,4 +1,4 @@
-use crate::window::Icon;
+use crate::window::{Icon, Position};
/// The window settings of an application.
#[derive(Debug, Clone)]
@@ -6,6 +6,9 @@ pub struct Settings {
/// The initial size of the window.
pub size: (u32, u32),
+ /// The initial position of the window.
+ pub position: Position,
+
/// The minimum size of the window.
pub min_size: Option<(u32, u32)>,
@@ -32,6 +35,7 @@ impl Default for Settings {
fn default() -> Settings {
Settings {
size: (1024, 768),
+ position: Position::default(),
min_size: None,
max_size: None,
resizable: true,
@@ -48,6 +52,7 @@ 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,
resizable: settings.resizable,
diff --git a/style/Cargo.toml b/style/Cargo.toml
index ac16f8ee..a3086477 100644
--- a/style/Cargo.toml
+++ b/style/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "iced_style"
-version = "0.2.0"
+version = "0.3.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
description = "The default set of styles of Iced"
@@ -10,5 +10,6 @@ documentation = "https://docs.rs/iced_style"
keywords = ["gui", "ui", "graphics", "interface", "widgets"]
categories = ["gui"]
-[dependencies]
-iced_core = { version = "0.3", path = "../core" }
+[dependencies.iced_core]
+version = "0.4"
+path = "../core"
diff --git a/style/src/button.rs b/style/src/button.rs
index 43d27216..2281e32f 100644
--- a/style/src/button.rs
+++ b/style/src/button.rs
@@ -2,7 +2,7 @@
use iced_core::{Background, Color, Vector};
/// The appearance of a button.
-#[derive(Debug)]
+#[derive(Debug, Clone, Copy)]
pub struct Style {
pub shadow_offset: Vector,
pub background: Option<Background>,
diff --git a/style/src/checkbox.rs b/style/src/checkbox.rs
index 1c5f2460..566136bb 100644
--- a/style/src/checkbox.rs
+++ b/style/src/checkbox.rs
@@ -2,7 +2,7 @@
use iced_core::{Background, Color};
/// The appearance of a checkbox.
-#[derive(Debug)]
+#[derive(Debug, Clone, Copy)]
pub struct Style {
pub background: Background,
pub checkmark_color: Color,
diff --git a/style/src/lib.rs b/style/src/lib.rs
index 7e0a9f49..08d9f044 100644
--- a/style/src/lib.rs
+++ b/style/src/lib.rs
@@ -10,6 +10,7 @@ pub mod button;
pub mod checkbox;
pub mod container;
pub mod menu;
+pub mod pane_grid;
pub mod pick_list;
pub mod progress_bar;
pub mod radio;
@@ -17,3 +18,4 @@ pub mod rule;
pub mod scrollable;
pub mod slider;
pub mod text_input;
+pub mod toggler;
diff --git a/style/src/pane_grid.rs b/style/src/pane_grid.rs
new file mode 100644
index 00000000..e39ee797
--- /dev/null
+++ b/style/src/pane_grid.rs
@@ -0,0 +1,51 @@
+//! Let your users split regions of your application and organize layout
+//! dynamically.
+use iced_core::Color;
+
+/// A set of rules that dictate the style of a container.
+pub trait StyleSheet {
+ /// The [`Line`] to draw when a split is picked.
+ fn picked_split(&self) -> Option<Line>;
+
+ /// The [`Line`] to draw when a split is hovered.
+ fn hovered_split(&self) -> Option<Line>;
+}
+
+/// A line.
+///
+/// It is normally used to define the highlight of something, like a split.
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Line {
+ /// The [`Color`] of the [`Line`].
+ pub color: Color,
+
+ /// The width of the [`Line`].
+ pub width: f32,
+}
+
+struct Default;
+
+impl StyleSheet for Default {
+ fn picked_split(&self) -> Option<Line> {
+ None
+ }
+
+ fn hovered_split(&self) -> Option<Line> {
+ None
+ }
+}
+
+impl std::default::Default for Box<dyn StyleSheet> {
+ fn default() -> Self {
+ Box::new(Default)
+ }
+}
+
+impl<T> From<T> for Box<dyn StyleSheet>
+where
+ T: 'static + StyleSheet,
+{
+ fn from(style: T) -> Self {
+ Box::new(style)
+ }
+}
diff --git a/style/src/pick_list.rs b/style/src/pick_list.rs
index a757ba98..d1801e5f 100644
--- a/style/src/pick_list.rs
+++ b/style/src/pick_list.rs
@@ -5,6 +5,7 @@ use iced_core::{Background, Color};
#[derive(Debug, Clone, Copy)]
pub struct Style {
pub text_color: Color,
+ pub placeholder_color: Color,
pub background: Background,
pub border_radius: f32,
pub border_width: f32,
@@ -16,6 +17,7 @@ impl std::default::Default for Style {
fn default() -> Self {
Self {
text_color: Color::BLACK,
+ placeholder_color: [0.4, 0.4, 0.4].into(),
background: Background::Color([0.87, 0.87, 0.87].into()),
border_radius: 0.0,
border_width: 1.0,
diff --git a/style/src/progress_bar.rs b/style/src/progress_bar.rs
index 36be63f9..d0878c84 100644
--- a/style/src/progress_bar.rs
+++ b/style/src/progress_bar.rs
@@ -2,7 +2,7 @@
use iced_core::{Background, Color};
/// The appearance of a progress bar.
-#[derive(Debug)]
+#[derive(Debug, Clone, Copy)]
pub struct Style {
pub background: Background,
pub bar: Background,
diff --git a/style/src/radio.rs b/style/src/radio.rs
index 83310e05..c41b70c0 100644
--- a/style/src/radio.rs
+++ b/style/src/radio.rs
@@ -2,7 +2,7 @@
use iced_core::{Background, Color};
/// The appearance of a radio button.
-#[derive(Debug)]
+#[derive(Debug, Clone, Copy)]
pub struct Style {
pub background: Background,
pub dot_color: Color,
diff --git a/style/src/rule.rs b/style/src/rule.rs
index 5021340b..be4c86d1 100644
--- a/style/src/rule.rs
+++ b/style/src/rule.rs
@@ -79,6 +79,17 @@ pub struct Style {
pub fill_mode: FillMode,
}
+impl std::default::Default for Style {
+ fn default() -> Self {
+ Style {
+ color: [0.6, 0.6, 0.6, 0.51].into(),
+ width: 1,
+ radius: 0.0,
+ fill_mode: FillMode::Percent(90.0),
+ }
+ }
+}
+
/// A set of rules that dictate the style of a rule.
pub trait StyleSheet {
/// Produces the style of a rule.
@@ -89,12 +100,7 @@ struct Default;
impl StyleSheet for Default {
fn style(&self) -> Style {
- Style {
- color: [0.6, 0.6, 0.6, 0.51].into(),
- width: 1,
- radius: 0.0,
- fill_mode: FillMode::Percent(90.0),
- }
+ Style::default()
}
}
diff --git a/style/src/toggler.rs b/style/src/toggler.rs
new file mode 100644
index 00000000..5a155123
--- /dev/null
+++ b/style/src/toggler.rs
@@ -0,0 +1,57 @@
+//! Show toggle controls using togglers.
+use iced_core::Color;
+
+/// The appearance of a toggler.
+#[derive(Debug)]
+pub struct Style {
+ pub background: Color,
+ pub background_border: Option<Color>,
+ pub foreground: Color,
+ pub foreground_border: Option<Color>,
+}
+
+/// A set of rules that dictate the style of a toggler.
+pub trait StyleSheet {
+ fn active(&self, is_active: bool) -> Style;
+
+ fn hovered(&self, is_active: bool) -> Style;
+}
+
+struct Default;
+
+impl StyleSheet for Default {
+ fn active(&self, is_active: bool) -> Style {
+ Style {
+ background: if is_active {
+ Color::from_rgb(0.0, 1.0, 0.0)
+ } else {
+ Color::from_rgb(0.7, 0.7, 0.7)
+ },
+ background_border: None,
+ foreground: Color::WHITE,
+ foreground_border: None,
+ }
+ }
+
+ fn hovered(&self, is_active: bool) -> Style {
+ Style {
+ foreground: Color::from_rgb(0.95, 0.95, 0.95),
+ ..self.active(is_active)
+ }
+ }
+}
+
+impl std::default::Default for Box<dyn StyleSheet> {
+ fn default() -> Self {
+ Box::new(Default)
+ }
+}
+
+impl<T> From<T> for Box<dyn StyleSheet>
+where
+ T: 'static + StyleSheet,
+{
+ fn from(style: T) -> Self {
+ Box::new(style)
+ }
+}
diff --git a/web/Cargo.toml b/web/Cargo.toml
index e063a021..06573b95 100644
--- a/web/Cargo.toml
+++ b/web/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "iced_web"
-version = "0.3.0"
+version = "0.4.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
description = "A web backend for Iced"
@@ -22,15 +22,15 @@ url = "2.0"
num-traits = "0.2"
[dependencies.iced_core]
-version = "0.3"
+version = "0.4"
path = "../core"
[dependencies.iced_futures]
-version = "0.2"
+version = "0.3"
path = "../futures"
[dependencies.iced_style]
-version = "0.2"
+version = "0.3"
path = "../style"
[dependencies.web-sys]
diff --git a/web/README.md b/web/README.md
index 0e770589..58ad8235 100644
--- a/web/README.md
+++ b/web/README.md
@@ -16,7 +16,7 @@ The crate is currently a __very experimental__, simple abstraction layer over [`
Add `iced_web` as a dependency in your `Cargo.toml`:
```toml
-iced_web = "0.3"
+iced_web = "0.4"
```
__Iced moves fast and the `master` branch can contain breaking changes!__ If
diff --git a/web/src/clipboard.rs b/web/src/clipboard.rs
new file mode 100644
index 00000000..167a1e53
--- /dev/null
+++ b/web/src/clipboard.rs
@@ -0,0 +1,21 @@
+/// A buffer for short-term storage and transfer within and between
+/// applications.
+#[derive(Debug, Clone, Copy)]
+pub struct Clipboard;
+
+impl Clipboard {
+ /// Creates a new [`Clipboard`].
+ pub fn new() -> Self {
+ Self
+ }
+
+ /// Reads the current content of the [`Clipboard`] as text.
+ pub fn read(&self) -> Option<String> {
+ unimplemented! {}
+ }
+
+ /// Writes the given text contents to the [`Clipboard`].
+ pub fn write(&mut self, _contents: String) {
+ unimplemented! {}
+ }
+}
diff --git a/web/src/css.rs b/web/src/css.rs
index bdde23f3..21f51f85 100644
--- a/web/src/css.rs
+++ b/web/src/css.rs
@@ -1,5 +1,5 @@
//! Style your widgets.
-use crate::{bumpalo, Align, Background, Color, Length};
+use crate::{bumpalo, Align, Background, Color, Length, Padding};
use std::collections::BTreeMap;
@@ -12,11 +12,11 @@ pub enum Rule {
/// Container with horizonal distribution
Row,
- /// Padding of the container
- Padding(u16),
-
/// Spacing between elements
Spacing(u16),
+
+ /// Toggler input for a specific size
+ Toggler(u16),
}
impl Rule {
@@ -25,8 +25,8 @@ impl Rule {
match self {
Rule::Column => String::from("c"),
Rule::Row => String::from("r"),
- Rule::Padding(padding) => format!("p-{}", padding),
Rule::Spacing(spacing) => format!("s-{}", spacing),
+ Rule::Toggler(size) => format!("toggler-{}", size),
}
}
@@ -45,13 +45,6 @@ impl Rule {
bumpalo::format!(in bump, ".{} {}", class, body).into_bump_str()
}
- Rule::Padding(padding) => bumpalo::format!(
- in bump,
- ".{} {{ box-sizing: border-box; padding: {}px }}",
- class,
- padding
- )
- .into_bump_str(),
Rule::Spacing(spacing) => bumpalo::format!(
in bump,
".c.{} > * {{ margin-bottom: {}px }} \
@@ -66,6 +59,46 @@ impl Rule {
class
)
.into_bump_str(),
+ Rule::Toggler(size) => bumpalo::format!(
+ in bump,
+ ".toggler-{} {{ display: flex; cursor: pointer; justify-content: space-between; }} \
+ .toggler-{} input {{ display:none; }} \
+ .toggler-{} span {{ background-color: #b1b1b1; position: relative; display: inline-flex; width:{}px; height: {}px; border-radius: {}px;}} \
+ .toggler-{} span > span {{ background-color: #FFFFFF; width: {}px; height: {}px; border-radius: 50%; top: 1px; left: 1px;}} \
+ .toggler-{}:hover span > span {{ background-color: #f1f1f1 !important; }} \
+ .toggler-{} input:checked + span {{ background-color: #00FF00; }} \
+ .toggler-{} input:checked + span > span {{ -webkit-transform: translateX({}px); -ms-transform:translateX({}px); transform: translateX({}px); }}
+ ",
+ // toggler
+ size,
+
+ // toggler input
+ size,
+
+ // toggler span
+ size,
+ size*2,
+ size,
+ size,
+
+ // toggler span > span
+ size,
+ size-2,
+ size-2,
+
+ // toggler: hover + span > span
+ size,
+
+ // toggler input:checked + span
+ size,
+
+ // toggler input:checked + span > span
+ size,
+ size,
+ size,
+ size
+ )
+ .into_bump_str(),
}
}
}
@@ -170,3 +203,13 @@ pub fn align(align: Align) -> &'static str {
Align::End => "flex-end",
}
}
+
+/// Returns the style value for the given [`Padding`].
+///
+/// [`Padding`]: struct.Padding.html
+pub fn padding(padding: Padding) -> String {
+ format!(
+ "{}px {}px {}px {}px",
+ padding.top, padding.right, padding.bottom, padding.left
+ )
+}
diff --git a/web/src/lib.rs b/web/src/lib.rs
index 58f6591d..6b7d0115 100644
--- a/web/src/lib.rs
+++ b/web/src/lib.rs
@@ -49,7 +49,7 @@
//!
//! [`wasm-pack`]: https://github.com/rustwasm/wasm-pack
//! [`wasm-bindgen`]: https://github.com/rustwasm/wasm-bindgen
-//! [`tour` example]: https://github.com/hecrj/iced/tree/0.2/examples/tour
+//! [`tour` example]: https://github.com/hecrj/iced/tree/0.3/examples/tour
#![deny(missing_docs)]
#![deny(missing_debug_implementations)]
#![deny(unused_results)]
@@ -59,6 +59,7 @@ use dodrio::bumpalo;
use std::{cell::RefCell, rc::Rc};
mod bus;
+mod clipboard;
mod element;
mod hasher;
@@ -67,13 +68,14 @@ pub mod subscription;
pub mod widget;
pub use bus::Bus;
+pub use clipboard::Clipboard;
pub use css::Css;
pub use dodrio;
pub use element::Element;
pub use hasher::Hasher;
pub use iced_core::{
- keyboard, mouse, Align, Background, Color, Font, HorizontalAlignment,
- Length, Point, Rectangle, Size, Vector, VerticalAlignment,
+ keyboard, menu, mouse, Align, Background, Color, Font, HorizontalAlignment,
+ Length, Menu, Padding, Point, Rectangle, Size, Vector, VerticalAlignment,
};
pub use iced_futures::{executor, futures, Command};
pub use subscription::Subscription;
@@ -126,7 +128,11 @@ pub trait Application {
/// this method.
///
/// Any [`Command`] returned will be executed immediately in the background.
- fn update(&mut self, message: Self::Message) -> Command<Self::Message>;
+ fn update(
+ &mut self,
+ message: Self::Message,
+ clipboard: &mut Clipboard,
+ ) -> Command<Self::Message>;
/// Returns the widgets to display in the [`Application`].
///
@@ -156,6 +162,8 @@ pub trait Application {
let document = window.document().unwrap();
let body = document.body().unwrap();
+ let mut clipboard = Clipboard::new();
+
let (sender, receiver) =
iced_futures::futures::channel::mpsc::unbounded();
@@ -182,7 +190,8 @@ pub trait Application {
let event_loop = receiver.for_each(move |message| {
let (command, subscription) = runtime.enter(|| {
- let command = application.borrow_mut().update(message);
+ let command =
+ application.borrow_mut().update(message, &mut clipboard);
let subscription = application.borrow().subscription();
(command, subscription)
diff --git a/web/src/widget.rs b/web/src/widget.rs
index 023f5f13..4cb0a9cc 100644
--- a/web/src/widget.rs
+++ b/web/src/widget.rs
@@ -24,6 +24,7 @@ pub mod radio;
pub mod scrollable;
pub mod slider;
pub mod text_input;
+pub mod toggler;
mod column;
mod row;
@@ -40,6 +41,8 @@ pub use slider::Slider;
pub use text::Text;
#[doc(no_inline)]
pub use text_input::TextInput;
+#[doc(no_inline)]
+pub use toggler::Toggler;
pub use checkbox::Checkbox;
pub use column::Column;
diff --git a/web/src/widget/button.rs b/web/src/widget/button.rs
index e7cff6a0..cd450b55 100644
--- a/web/src/widget/button.rs
+++ b/web/src/widget/button.rs
@@ -1,7 +1,7 @@
//! Allow your users to perform actions by pressing a button.
//!
//! A [`Button`] has some local [`State`].
-use crate::{css, Background, Bus, Css, Element, Length, Widget};
+use crate::{css, Background, Bus, Css, Element, Length, Padding, Widget};
pub use iced_style::button::{Style, StyleSheet};
@@ -20,15 +20,37 @@ use dodrio::bumpalo;
/// let button = Button::new(&mut state, Text::new("Press me!"))
/// .on_press(Message::ButtonPressed);
/// ```
+///
+/// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will
+/// be disabled:
+///
+/// ```
+/// # use iced_web::{button, Button, Text};
+/// #
+/// #[derive(Clone)]
+/// enum Message {
+/// ButtonPressed,
+/// }
+///
+/// fn disabled_button(state: &mut button::State) -> Button<'_, Message> {
+/// Button::new(state, Text::new("I'm disabled!"))
+/// }
+///
+/// fn enabled_button(state: &mut button::State) -> Button<'_, Message> {
+/// disabled_button(state).on_press(Message::ButtonPressed)
+/// }
+/// ```
#[allow(missing_debug_implementations)]
pub struct Button<'a, Message> {
content: Element<'a, Message>,
on_press: Option<Message>,
width: Length,
+ #[allow(dead_code)]
height: Length,
min_width: u32,
+ #[allow(dead_code)]
min_height: u32,
- padding: u16,
+ padding: Padding,
style: Box<dyn StyleSheet>,
}
@@ -46,7 +68,7 @@ impl<'a, Message> Button<'a, Message> {
height: Length::Shrink,
min_width: 0,
min_height: 0,
- padding: 5,
+ padding: Padding::new(5),
style: Default::default(),
}
}
@@ -75,9 +97,9 @@ impl<'a, Message> Button<'a, Message> {
self
}
- /// Sets the padding of the [`Button`].
- pub fn padding(mut self, padding: u16) -> Self {
- self.padding = padding;
+ /// Sets the [`Padding`] of the [`Button`].
+ pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
+ self.padding = padding.into();
self
}
@@ -88,6 +110,7 @@ impl<'a, Message> Button<'a, Message> {
}
/// Sets the message that will be produced when the [`Button`] is pressed.
+ /// If on_press isn't set, button will be disabled.
pub fn on_press(mut self, msg: Message) -> Self {
self.on_press = Some(msg);
self
@@ -120,9 +143,6 @@ where
// TODO: State-based styling
let style = self.style.active();
- let padding_class =
- style_sheet.insert(bump, css::Rule::Padding(self.padding));
-
let background = match style.background {
None => String::from("none"),
Some(background) => match background {
@@ -130,25 +150,19 @@ where
},
};
- let class = {
- use dodrio::bumpalo::collections::String;
-
- String::from_str_in(&padding_class, bump).into_bump_str()
- };
-
let mut node = button(bump)
- .attr("class", class)
.attr(
"style",
bumpalo::format!(
in bump,
"background: {}; border-radius: {}px; width:{}; \
- min-width: {}; color: {}",
+ min-width: {}; color: {}; padding: {}",
background,
style.border_radius,
css::length(self.width),
css::min_length(self.min_width),
- css::color(style.text_color)
+ css::color(style.text_color),
+ css::padding(self.padding)
)
.into_bump_str(),
)
@@ -160,6 +174,8 @@ where
node = node.on("click", move |_root, _vdom, _event| {
event_bus.publish(on_press.clone());
});
+ } else {
+ node = node.attr("disabled", "");
}
node.finish()
diff --git a/web/src/widget/checkbox.rs b/web/src/widget/checkbox.rs
index 543af99a..43110aa7 100644
--- a/web/src/widget/checkbox.rs
+++ b/web/src/widget/checkbox.rs
@@ -30,6 +30,7 @@ pub struct Checkbox<Message> {
label: String,
id: Option<String>,
width: Length,
+ #[allow(dead_code)]
style: Box<dyn StyleSheet>,
}
diff --git a/web/src/widget/column.rs b/web/src/widget/column.rs
index d832fdcb..8738c2af 100644
--- a/web/src/widget/column.rs
+++ b/web/src/widget/column.rs
@@ -1,4 +1,4 @@
-use crate::{css, Align, Bus, Css, Element, Length, Widget};
+use crate::{css, Align, Bus, Css, Element, Length, Padding, Widget};
use dodrio::bumpalo;
use std::u32;
@@ -9,7 +9,7 @@ use std::u32;
#[allow(missing_debug_implementations)]
pub struct Column<'a, Message> {
spacing: u16,
- padding: u16,
+ padding: Padding,
width: Length,
height: Length,
max_width: u32,
@@ -28,7 +28,7 @@ impl<'a, Message> Column<'a, Message> {
pub fn with_children(children: Vec<Element<'a, Message>>) -> Self {
Column {
spacing: 0,
- padding: 0,
+ padding: Padding::ZERO,
width: Length::Fill,
height: Length::Shrink,
max_width: u32::MAX,
@@ -48,9 +48,9 @@ impl<'a, Message> Column<'a, Message> {
self
}
- /// Sets the padding of the [`Column`].
- pub fn padding(mut self, units: u16) -> Self {
- self.padding = units;
+ /// Sets the [`Padding`] of the [`Column`].
+ pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
+ self.padding = padding.into();
self
}
@@ -114,23 +114,21 @@ impl<'a, Message> Widget<Message> for Column<'a, Message> {
let spacing_class =
style_sheet.insert(bump, css::Rule::Spacing(self.spacing));
- let padding_class =
- style_sheet.insert(bump, css::Rule::Padding(self.padding));
-
// TODO: Complete styling
div(bump)
.attr(
"class",
- bumpalo::format!(in bump, "{} {} {}", column_class, spacing_class, padding_class)
+ bumpalo::format!(in bump, "{} {}", column_class, spacing_class)
.into_bump_str(),
)
.attr("style", bumpalo::format!(
in bump,
- "width: {}; height: {}; max-width: {}; max-height: {}; align-items: {}",
+ "width: {}; height: {}; max-width: {}; max-height: {}; padding: {}; align-items: {}",
css::length(self.width),
css::length(self.height),
css::max_length(self.max_width),
css::max_length(self.max_height),
+ css::padding(self.padding),
css::align(self.align_items)
).into_bump_str()
)
diff --git a/web/src/widget/container.rs b/web/src/widget/container.rs
index 7187a4f0..c006e011 100644
--- a/web/src/widget/container.rs
+++ b/web/src/widget/container.rs
@@ -1,5 +1,5 @@
//! Decorate content and apply alignment.
-use crate::{bumpalo, css, Align, Bus, Css, Element, Length, Widget};
+use crate::{bumpalo, css, Align, Bus, Css, Element, Length, Padding, Widget};
pub use iced_style::container::{Style, StyleSheet};
@@ -8,10 +8,11 @@ pub use iced_style::container::{Style, StyleSheet};
/// It is normally used for alignment purposes.
#[allow(missing_debug_implementations)]
pub struct Container<'a, Message> {
- padding: u16,
+ padding: Padding,
width: Length,
height: Length,
max_width: u32,
+ #[allow(dead_code)]
max_height: u32,
horizontal_alignment: Align,
vertical_alignment: Align,
@@ -28,7 +29,7 @@ impl<'a, Message> Container<'a, Message> {
use std::u32;
Container {
- padding: 0,
+ padding: Padding::ZERO,
width: Length::Shrink,
height: Length::Shrink,
max_width: u32::MAX,
@@ -40,9 +41,9 @@ impl<'a, Message> Container<'a, Message> {
}
}
- /// Sets the padding of the [`Container`].
- pub fn padding(mut self, units: u16) -> Self {
- self.padding = units;
+ /// Sets the [`Padding`] of the [`Container`].
+ pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
+ self.padding = padding.into();
self
}
@@ -105,24 +106,22 @@ where
let column_class = style_sheet.insert(bump, css::Rule::Column);
- let padding_class =
- style_sheet.insert(bump, css::Rule::Padding(self.padding));
-
let style = self.style_sheet.style();
let node = div(bump)
.attr(
"class",
- bumpalo::format!(in bump, "{} {}", column_class, padding_class).into_bump_str(),
+ bumpalo::format!(in bump, "{}", column_class).into_bump_str(),
)
.attr(
"style",
bumpalo::format!(
in bump,
- "width: {}; height: {}; max-width: {}; align-items: {}; justify-content: {}; background: {}; color: {}; border-width: {}px; border-color: {}; border-radius: {}px",
+ "width: {}; height: {}; max-width: {}; padding: {}; align-items: {}; justify-content: {}; background: {}; color: {}; border-width: {}px; border-color: {}; border-radius: {}px",
css::length(self.width),
css::length(self.height),
css::max_length(self.max_width),
+ css::padding(self.padding),
css::align(self.horizontal_alignment),
css::align(self.vertical_alignment),
style.background.map(css::background).unwrap_or(String::from("initial")),
diff --git a/web/src/widget/radio.rs b/web/src/widget/radio.rs
index 5a9bc379..fbc88d29 100644
--- a/web/src/widget/radio.rs
+++ b/web/src/widget/radio.rs
@@ -37,6 +37,7 @@ pub struct Radio<Message> {
label: String,
id: Option<String>,
name: Option<String>,
+ #[allow(dead_code)]
style: Box<dyn StyleSheet>,
}
diff --git a/web/src/widget/row.rs b/web/src/widget/row.rs
index f00a544a..ffb515cf 100644
--- a/web/src/widget/row.rs
+++ b/web/src/widget/row.rs
@@ -1,4 +1,4 @@
-use crate::{css, Align, Bus, Css, Element, Length, Widget};
+use crate::{css, Align, Bus, Css, Element, Length, Padding, Widget};
use dodrio::bumpalo;
use std::u32;
@@ -9,7 +9,7 @@ use std::u32;
#[allow(missing_debug_implementations)]
pub struct Row<'a, Message> {
spacing: u16,
- padding: u16,
+ padding: Padding,
width: Length,
height: Length,
max_width: u32,
@@ -28,7 +28,7 @@ impl<'a, Message> Row<'a, Message> {
pub fn with_children(children: Vec<Element<'a, Message>>) -> Self {
Row {
spacing: 0,
- padding: 0,
+ padding: Padding::ZERO,
width: Length::Fill,
height: Length::Shrink,
max_width: u32::MAX,
@@ -48,9 +48,9 @@ impl<'a, Message> Row<'a, Message> {
self
}
- /// Sets the padding of the [`Row`].
- pub fn padding(mut self, units: u16) -> Self {
- self.padding = units;
+ /// Sets the [`Padding`] of the [`Row`].
+ pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
+ self.padding = padding.into();
self
}
@@ -114,23 +114,21 @@ impl<'a, Message> Widget<Message> for Row<'a, Message> {
let spacing_class =
style_sheet.insert(bump, css::Rule::Spacing(self.spacing));
- let padding_class =
- style_sheet.insert(bump, css::Rule::Padding(self.padding));
-
// TODO: Complete styling
div(bump)
.attr(
"class",
- bumpalo::format!(in bump, "{} {} {}", row_class, spacing_class, padding_class)
+ bumpalo::format!(in bump, "{} {}", row_class, spacing_class)
.into_bump_str(),
)
.attr("style", bumpalo::format!(
in bump,
- "width: {}; height: {}; max-width: {}; max-height: {}; align-items: {}",
+ "width: {}; height: {}; max-width: {}; max-height: {}; padding: {}; align-items: {}",
css::length(self.width),
css::length(self.height),
css::max_length(self.max_width),
css::max_length(self.max_height),
+ css::padding(self.padding),
css::align(self.align_items)
).into_bump_str()
)
diff --git a/web/src/widget/scrollable.rs b/web/src/widget/scrollable.rs
index f9135dd6..ce0a10d4 100644
--- a/web/src/widget/scrollable.rs
+++ b/web/src/widget/scrollable.rs
@@ -1,5 +1,7 @@
//! Navigate an endless amount of content with a scrollbar.
-use crate::{bumpalo, css, Align, Bus, Column, Css, Element, Length, Widget};
+use crate::{
+ bumpalo, css, Align, Bus, Column, Css, Element, Length, Padding, Widget,
+};
pub use iced_style::scrollable::{Scrollbar, Scroller, StyleSheet};
@@ -11,6 +13,7 @@ pub struct Scrollable<'a, Message> {
height: Length,
max_height: u32,
content: Column<'a, Message>,
+ #[allow(dead_code)]
style: Box<dyn StyleSheet>,
}
@@ -38,9 +41,9 @@ impl<'a, Message> Scrollable<'a, Message> {
self
}
- /// Sets the padding of the [`Scrollable`].
- pub fn padding(mut self, units: u16) -> Self {
- self.content = self.content.padding(units);
+ /// Sets the [`Padding`] of the [`Scrollable`].
+ pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
+ self.content = self.content.padding(padding);
self
}
diff --git a/web/src/widget/slider.rs b/web/src/widget/slider.rs
index 91a4d2ec..f457aa4c 100644
--- a/web/src/widget/slider.rs
+++ b/web/src/widget/slider.rs
@@ -38,7 +38,9 @@ pub struct Slider<'a, T, Message> {
step: T,
value: T,
on_change: Rc<Box<dyn Fn(T) -> Message>>,
+ #[allow(dead_code)]
width: Length,
+ #[allow(dead_code)]
style: Box<dyn StyleSheet>,
}
diff --git a/web/src/widget/text_input.rs b/web/src/widget/text_input.rs
index bc2048a8..e4877f2a 100644
--- a/web/src/widget/text_input.rs
+++ b/web/src/widget/text_input.rs
@@ -1,7 +1,7 @@
//! Display fields that can be filled with text.
//!
//! A [`TextInput`] has some local [`State`].
-use crate::{bumpalo, css, Bus, Css, Element, Length, Widget};
+use crate::{bumpalo, css, Bus, Css, Element, Length, Padding, Widget};
pub use iced_style::text_input::{Style, StyleSheet};
@@ -35,7 +35,7 @@ pub struct TextInput<'a, Message> {
is_secure: bool,
width: Length,
max_width: u32,
- padding: u16,
+ padding: Padding,
size: Option<u16>,
on_change: Rc<Box<dyn Fn(String) -> Message>>,
on_submit: Option<Message>,
@@ -66,7 +66,7 @@ impl<'a, Message> TextInput<'a, Message> {
is_secure: false,
width: Length::Fill,
max_width: u32::MAX,
- padding: 0,
+ padding: Padding::ZERO,
size: None,
on_change: Rc::new(Box::new(on_change)),
on_submit: None,
@@ -92,9 +92,9 @@ impl<'a, Message> TextInput<'a, Message> {
self
}
- /// Sets the padding of the [`TextInput`].
- pub fn padding(mut self, units: u16) -> Self {
- self.padding = units;
+ /// Sets the [`Padding`] of the [`TextInput`].
+ pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
+ self.padding = padding.into();
self
}
@@ -126,20 +126,11 @@ where
&self,
bump: &'b bumpalo::Bump,
bus: &Bus<Message>,
- style_sheet: &mut Css<'b>,
+ _style_sheet: &mut Css<'b>,
) -> dodrio::Node<'b> {
use dodrio::builder::*;
use wasm_bindgen::JsCast;
- let class = {
- use dodrio::bumpalo::collections::String;
-
- let padding_class =
- style_sheet.insert(bump, css::Rule::Padding(self.padding));
-
- String::from_str_in(&padding_class, bump).into_bump_str()
- };
-
let placeholder = {
use dodrio::bumpalo::collections::String;
@@ -159,16 +150,16 @@ where
let style = self.style_sheet.active();
input(bump)
- .attr("class", class)
.attr(
"style",
bumpalo::format!(
in bump,
- "width: {}; max-width: {}; font-size: {}px; \
+ "width: {}; max-width: {}; padding: {}; font-size: {}px; \
background: {}; border-width: {}px; border-color: {}; \
border-radius: {}px; color: {}",
css::length(self.width),
css::max_length(self.max_width),
+ css::padding(self.padding),
self.size.unwrap_or(20),
css::background(style.background),
style.border_width,
@@ -232,4 +223,9 @@ impl State {
// TODO
Self::default()
}
+
+ /// Selects all the content of the [`TextInput`].
+ pub fn select_all(&mut self) {
+ // TODO
+ }
}
diff --git a/web/src/widget/toggler.rs b/web/src/widget/toggler.rs
new file mode 100644
index 00000000..0a198079
--- /dev/null
+++ b/web/src/widget/toggler.rs
@@ -0,0 +1,171 @@
+//! Show toggle controls using togglers.
+use crate::{css, Bus, Css, Element, Length, Widget};
+
+pub use iced_style::toggler::{Style, StyleSheet};
+
+use dodrio::bumpalo;
+use std::rc::Rc;
+
+/// A toggler that can be toggled.
+///
+/// # Example
+///
+/// ```
+/// # use iced_web::Toggler;
+///
+/// pub enum Message {
+/// TogglerToggled(bool),
+/// }
+///
+/// let is_active = true;
+///
+/// Toggler::new(is_active, String::from("Toggle me!"), Message::TogglerToggled);
+/// ```
+///
+#[allow(missing_debug_implementations)]
+pub struct Toggler<Message> {
+ is_active: bool,
+ on_toggle: Rc<dyn Fn(bool) -> Message>,
+ label: Option<String>,
+ id: Option<String>,
+ width: Length,
+ style: Box<dyn StyleSheet>,
+}
+
+impl<Message> Toggler<Message> {
+ /// Creates a new [`Toggler`].
+ ///
+ /// It expects:
+ /// * a boolean describing whether the [`Toggler`] is active or not
+ /// * An optional label for the [`Toggler`]
+ /// * a function that will be called when the [`Toggler`] is toggled. It
+ /// will receive the new state of the [`Toggler`] and must produce a
+ /// `Message`.
+ ///
+ /// [`Toggler`]: struct.Toggler.html
+ pub fn new<F>(
+ is_active: bool,
+ label: impl Into<Option<String>>,
+ f: F,
+ ) -> Self
+ where
+ F: 'static + Fn(bool) -> Message,
+ {
+ Toggler {
+ is_active,
+ on_toggle: Rc::new(f),
+ label: label.into(),
+ id: None,
+ width: Length::Shrink,
+ style: Default::default(),
+ }
+ }
+
+ /// Sets the width of the [`Toggler`].
+ ///
+ /// [`Toggler`]: struct.Toggler.html
+ pub fn width(mut self, width: Length) -> Self {
+ self.width = width;
+ self
+ }
+
+ /// Sets the style of the [`Toggler`].
+ ///
+ /// [`Toggler`]: struct.Toggler.html
+ pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self {
+ self.style = style.into();
+ self
+ }
+
+ /// Sets the id of the [`Toggler`].
+ ///
+ /// [`Toggler`]: struct.Toggler.html
+ pub fn id(mut self, id: impl Into<String>) -> Self {
+ self.id = Some(id.into());
+ self
+ }
+}
+
+impl<Message> Widget<Message> for Toggler<Message>
+where
+ Message: 'static,
+{
+ fn node<'b>(
+ &self,
+ bump: &'b bumpalo::Bump,
+ bus: &Bus<Message>,
+ style_sheet: &mut Css<'b>,
+ ) -> dodrio::Node<'b> {
+ use dodrio::builder::*;
+ use dodrio::bumpalo::collections::String;
+
+ let toggler_label = &self
+ .label
+ .as_ref()
+ .map(|label| String::from_str_in(&label, bump).into_bump_str());
+
+ let event_bus = bus.clone();
+ let on_toggle = self.on_toggle.clone();
+ let is_active = self.is_active;
+
+ let row_class = style_sheet.insert(bump, css::Rule::Row);
+ let toggler_class = style_sheet.insert(bump, css::Rule::Toggler(16));
+
+ let (label, input) = if let Some(id) = &self.id {
+ let id = String::from_str_in(id, bump).into_bump_str();
+
+ (label(bump).attr("for", id), input(bump).attr("id", id))
+ } else {
+ (label(bump), input(bump))
+ };
+
+ let checkbox = input
+ .attr("type", "checkbox")
+ .bool_attr("checked", self.is_active)
+ .on("click", move |_root, vdom, _event| {
+ let msg = on_toggle(!is_active);
+ event_bus.publish(msg);
+
+ vdom.schedule_render();
+ })
+ .finish();
+
+ let toggler = span(bump).children(vec![span(bump).finish()]).finish();
+
+ label
+ .attr(
+ "class",
+ bumpalo::format!(in bump, "{} {}", row_class, toggler_class)
+ .into_bump_str(),
+ )
+ .attr(
+ "style",
+ bumpalo::format!(in bump, "width: {}; align-items: center", css::length(self.width))
+ .into_bump_str()
+ )
+ .children(
+ if let Some(label) = toggler_label {
+ vec![
+ text(label),
+ checkbox,
+ toggler,
+ ]
+ } else {
+ vec![
+ checkbox,
+ toggler,
+ ]
+ }
+ )
+ .finish()
+ }
+}
+
+impl<'a, Message> From<Toggler<Message>> for Element<'a, Message>
+where
+ Message: 'static,
+{
+ fn from(toggler: Toggler<Message>) -> Element<'a, Message> {
+ Element::new(toggler)
+ }
+}
diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml
index 5f4699a8..9675797d 100644
--- a/wgpu/Cargo.toml
+++ b/wgpu/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "iced_wgpu"
-version = "0.3.0"
+version = "0.4.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
description = "A wgpu renderer for Iced"
@@ -8,14 +8,26 @@ license = "MIT AND OFL-1.1"
repository = "https://github.com/hecrj/iced"
[features]
-svg = ["resvg"]
+svg = ["resvg", "usvg"]
+image = ["png", "jpeg", "jpeg_rayon", "gif", "webp", "bmp"]
+png = ["image_rs/png"]
+jpeg = ["image_rs/jpeg"]
+jpeg_rayon = ["image_rs/jpeg_rayon"]
+gif = ["image_rs/gif"]
+webp = ["image_rs/webp"]
+pnm = ["image_rs/pnm"]
+ico = ["image_rs/ico"]
+bmp = ["image_rs/bmp"]
+hdr = ["image_rs/hdr"]
+dds = ["image_rs/dds"]
+farbfeld = ["image_rs/farbfeld"]
canvas = ["iced_graphics/canvas"]
qr_code = ["iced_graphics/qr_code"]
default_system_font = ["iced_graphics/font-source"]
[dependencies]
-wgpu = "0.6"
-wgpu_glyph = "0.10"
+wgpu = "0.9"
+wgpu_glyph = "0.13"
glyph_brush = "0.7"
raw-window-handle = "0.3"
log = "0.4"
@@ -27,21 +39,26 @@ version = "1.4"
features = ["derive"]
[dependencies.iced_native]
-version = "0.3"
+version = "0.4"
path = "../native"
[dependencies.iced_graphics]
-version = "0.1"
+version = "0.2"
path = "../graphics"
features = ["font-fallback", "font-icons"]
-[dependencies.image]
+[dependencies.image_rs]
version = "0.23"
+package = "image"
+default-features = false
optional = true
[dependencies.resvg]
-version = "0.9"
-features = ["raqote-backend"]
+version = "0.12"
+optional = true
+
+[dependencies.usvg]
+version = "0.12"
optional = true
[package.metadata.docs.rs]
diff --git a/wgpu/README.md b/wgpu/README.md
index e8cb0a43..a9d95ea7 100644
--- a/wgpu/README.md
+++ b/wgpu/README.md
@@ -29,7 +29,7 @@ Currently, `iced_wgpu` supports the following primitives:
Add `iced_wgpu` as a dependency in your `Cargo.toml`:
```toml
-iced_wgpu = "0.3"
+iced_wgpu = "0.4"
```
__Iced moves fast and the `master` branch can contain breaking changes!__ If
diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs
index fccb5ac7..783079f3 100644
--- a/wgpu/src/backend.rs
+++ b/wgpu/src/backend.rs
@@ -9,7 +9,7 @@ use iced_graphics::{Primitive, Viewport};
use iced_native::mouse;
use iced_native::{Font, HorizontalAlignment, Size, VerticalAlignment};
-#[cfg(any(feature = "image", feature = "svg"))]
+#[cfg(any(feature = "image_rs", feature = "svg"))]
use crate::image;
/// A [`wgpu`] graphics backend for [`iced`].
@@ -22,7 +22,7 @@ pub struct Backend {
text_pipeline: text::Pipeline,
triangle_pipeline: triangle::Pipeline,
- #[cfg(any(feature = "image", feature = "svg"))]
+ #[cfg(any(feature = "image_rs", feature = "svg"))]
image_pipeline: image::Pipeline,
default_text_size: u16,
@@ -31,8 +31,13 @@ pub struct Backend {
impl Backend {
/// Creates a new [`Backend`].
pub fn new(device: &wgpu::Device, settings: Settings) -> Self {
- let text_pipeline =
- text::Pipeline::new(device, settings.format, settings.default_font);
+ let text_pipeline = text::Pipeline::new(
+ device,
+ settings.format,
+ settings.default_font,
+ settings.text_multithreading,
+ );
+
let quad_pipeline = quad::Pipeline::new(device, settings.format);
let triangle_pipeline = triangle::Pipeline::new(
device,
@@ -40,7 +45,7 @@ impl Backend {
settings.antialiasing,
);
- #[cfg(any(feature = "image", feature = "svg"))]
+ #[cfg(any(feature = "image_rs", feature = "svg"))]
let image_pipeline = image::Pipeline::new(device, settings.format);
Self {
@@ -48,7 +53,7 @@ impl Backend {
text_pipeline,
triangle_pipeline,
- #[cfg(any(feature = "image", feature = "svg"))]
+ #[cfg(any(feature = "image_rs", feature = "svg"))]
image_pipeline,
default_text_size: settings.default_text_size,
@@ -92,7 +97,7 @@ impl Backend {
);
}
- #[cfg(any(feature = "image", feature = "svg"))]
+ #[cfg(any(feature = "image_rs", feature = "svg"))]
self.image_pipeline.trim_cache();
*mouse_interaction
@@ -142,7 +147,7 @@ impl Backend {
);
}
- #[cfg(any(feature = "image", feature = "svg"))]
+ #[cfg(any(feature = "image_rs", feature = "svg"))]
{
if !layer.images.is_empty() {
let scaled = transformation
@@ -270,7 +275,7 @@ impl backend::Text for Backend {
}
}
-#[cfg(feature = "image")]
+#[cfg(feature = "image_rs")]
impl backend::Image for Backend {
fn dimensions(&self, handle: &iced_native::image::Handle) -> (u32, u32) {
self.image_pipeline.dimensions(handle)
diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs
index c256ca7e..85663bf5 100644
--- a/wgpu/src/image.rs
+++ b/wgpu/src/image.rs
@@ -1,6 +1,6 @@
mod atlas;
-#[cfg(feature = "image")]
+#[cfg(feature = "image_rs")]
mod raster;
#[cfg(feature = "svg")]
@@ -16,7 +16,7 @@ use std::mem;
use bytemuck::{Pod, Zeroable};
-#[cfg(feature = "image")]
+#[cfg(feature = "image_rs")]
use iced_native::image;
#[cfg(feature = "svg")]
@@ -24,7 +24,7 @@ use iced_native::svg;
#[derive(Debug)]
pub struct Pipeline {
- #[cfg(feature = "image")]
+ #[cfg(feature = "image_rs")]
raster_cache: RefCell<raster::Cache>,
#[cfg(feature = "svg")]
vector_cache: RefCell<vector::Cache>,
@@ -62,8 +62,9 @@ impl Pipeline {
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStage::VERTEX,
- ty: wgpu::BindingType::UniformBuffer {
- dynamic: false,
+ ty: wgpu::BindingType::Buffer {
+ ty: wgpu::BufferBindingType::Uniform,
+ has_dynamic_offset: false,
min_binding_size: wgpu::BufferSize::new(
mem::size_of::<Uniforms>() as u64,
),
@@ -73,7 +74,10 @@ impl Pipeline {
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStage::FRAGMENT,
- ty: wgpu::BindingType::Sampler { comparison: false },
+ ty: wgpu::BindingType::Sampler {
+ comparison: false,
+ filtering: true,
+ },
count: None,
},
],
@@ -94,7 +98,11 @@ impl Pipeline {
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(
- uniforms_buffer.slice(..),
+ wgpu::BufferBinding {
+ buffer: &uniforms_buffer,
+ offset: 0,
+ size: None,
+ },
),
},
wgpu::BindGroupEntry {
@@ -110,9 +118,11 @@ impl Pipeline {
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStage::FRAGMENT,
- ty: wgpu::BindingType::SampledTexture {
- dimension: wgpu::TextureViewDimension::D2,
- component_type: wgpu::TextureComponentType::Float,
+ ty: wgpu::BindingType::Texture {
+ sample_type: wgpu::TextureSampleType::Float {
+ filterable: true,
+ },
+ view_dimension: wgpu::TextureViewDimension::D2Array,
multisampled: false,
},
count: None,
@@ -126,95 +136,76 @@ impl Pipeline {
bind_group_layouts: &[&constant_layout, &texture_layout],
});
- let vs_module = device.create_shader_module(wgpu::include_spirv!(
- "shader/image.vert.spv"
- ));
-
- let fs_module = device.create_shader_module(wgpu::include_spirv!(
- "shader/image.frag.spv"
- ));
+ let shader =
+ device.create_shader_module(&wgpu::ShaderModuleDescriptor {
+ label: Some("iced_wgpu::image::shader"),
+ source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
+ include_str!("shader/image.wgsl"),
+ )),
+ flags: wgpu::ShaderFlags::all(),
+ });
let pipeline =
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("iced_wgpu::image pipeline"),
layout: Some(&layout),
- vertex_stage: wgpu::ProgrammableStageDescriptor {
- module: &vs_module,
- entry_point: "main",
- },
- fragment_stage: Some(wgpu::ProgrammableStageDescriptor {
- module: &fs_module,
- entry_point: "main",
- }),
- rasterization_state: Some(wgpu::RasterizationStateDescriptor {
- front_face: wgpu::FrontFace::Cw,
- cull_mode: wgpu::CullMode::None,
- ..Default::default()
- }),
- primitive_topology: wgpu::PrimitiveTopology::TriangleList,
- color_states: &[wgpu::ColorStateDescriptor {
- format,
- color_blend: wgpu::BlendDescriptor {
- src_factor: wgpu::BlendFactor::SrcAlpha,
- dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
- operation: wgpu::BlendOperation::Add,
- },
- alpha_blend: wgpu::BlendDescriptor {
- src_factor: wgpu::BlendFactor::One,
- dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
- operation: wgpu::BlendOperation::Add,
- },
- write_mask: wgpu::ColorWrite::ALL,
- }],
- depth_stencil_state: None,
- vertex_state: wgpu::VertexStateDescriptor {
- index_format: wgpu::IndexFormat::Uint16,
- vertex_buffers: &[
- wgpu::VertexBufferDescriptor {
- stride: mem::size_of::<Vertex>() as u64,
+ vertex: wgpu::VertexState {
+ module: &shader,
+ entry_point: "vs_main",
+ buffers: &[
+ wgpu::VertexBufferLayout {
+ array_stride: mem::size_of::<Vertex>() as u64,
step_mode: wgpu::InputStepMode::Vertex,
- attributes: &[wgpu::VertexAttributeDescriptor {
+ attributes: &[wgpu::VertexAttribute {
shader_location: 0,
- format: wgpu::VertexFormat::Float2,
+ format: wgpu::VertexFormat::Float32x2,
offset: 0,
}],
},
- wgpu::VertexBufferDescriptor {
- stride: mem::size_of::<Instance>() as u64,
+ wgpu::VertexBufferLayout {
+ array_stride: mem::size_of::<Instance>() as u64,
step_mode: wgpu::InputStepMode::Instance,
- attributes: &[
- wgpu::VertexAttributeDescriptor {
- shader_location: 1,
- format: wgpu::VertexFormat::Float2,
- offset: 0,
- },
- wgpu::VertexAttributeDescriptor {
- shader_location: 2,
- format: wgpu::VertexFormat::Float2,
- offset: 4 * 2,
- },
- wgpu::VertexAttributeDescriptor {
- shader_location: 3,
- format: wgpu::VertexFormat::Float2,
- offset: 4 * 4,
- },
- wgpu::VertexAttributeDescriptor {
- shader_location: 4,
- format: wgpu::VertexFormat::Float2,
- offset: 4 * 6,
- },
- wgpu::VertexAttributeDescriptor {
- shader_location: 5,
- format: wgpu::VertexFormat::Uint,
- offset: 4 * 8,
- },
- ],
+ attributes: &wgpu::vertex_attr_array!(
+ 1 => Float32x2,
+ 2 => Float32x2,
+ 3 => Float32x2,
+ 4 => Float32x2,
+ 5 => Sint32,
+ ),
},
],
},
- sample_count: 1,
- sample_mask: !0,
- alpha_to_coverage_enabled: false,
+ fragment: Some(wgpu::FragmentState {
+ module: &shader,
+ entry_point: "fs_main",
+ targets: &[wgpu::ColorTargetState {
+ format,
+ blend: Some(wgpu::BlendState {
+ color: wgpu::BlendComponent {
+ src_factor: wgpu::BlendFactor::SrcAlpha,
+ dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
+ operation: wgpu::BlendOperation::Add,
+ },
+ alpha: wgpu::BlendComponent {
+ src_factor: wgpu::BlendFactor::One,
+ dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
+ operation: wgpu::BlendOperation::Add,
+ },
+ }),
+ write_mask: wgpu::ColorWrite::ALL,
+ }],
+ }),
+ primitive: wgpu::PrimitiveState {
+ topology: wgpu::PrimitiveTopology::TriangleList,
+ front_face: wgpu::FrontFace::Cw,
+ ..Default::default()
+ },
+ depth_stencil: None,
+ multisample: wgpu::MultisampleState {
+ count: 1,
+ mask: !0,
+ alpha_to_coverage_enabled: false,
+ },
});
let vertices =
@@ -252,7 +243,7 @@ impl Pipeline {
});
Pipeline {
- #[cfg(feature = "image")]
+ #[cfg(feature = "image_rs")]
raster_cache: RefCell::new(raster::Cache::new()),
#[cfg(feature = "svg")]
@@ -271,7 +262,7 @@ impl Pipeline {
}
}
- #[cfg(feature = "image")]
+ #[cfg(feature = "image_rs")]
pub fn dimensions(&self, handle: &image::Handle) -> (u32, u32) {
let mut cache = self.raster_cache.borrow_mut();
let memory = cache.load(&handle);
@@ -300,7 +291,7 @@ impl Pipeline {
) {
let instances: &mut Vec<Instance> = &mut Vec::new();
- #[cfg(feature = "image")]
+ #[cfg(feature = "image_rs")]
let mut raster_cache = self.raster_cache.borrow_mut();
#[cfg(feature = "svg")]
@@ -308,7 +299,7 @@ impl Pipeline {
for image in images {
match &image {
- #[cfg(feature = "image")]
+ #[cfg(feature = "image_rs")]
layer::Image::Raster { handle, bounds } => {
if let Some(atlas_entry) = raster_cache.upload(
handle,
@@ -324,7 +315,7 @@ impl Pipeline {
);
}
}
- #[cfg(not(feature = "image"))]
+ #[cfg(not(feature = "image_rs"))]
layer::Image::Raster { .. } => {}
#[cfg(feature = "svg")]
@@ -415,23 +406,25 @@ impl Pipeline {
let mut render_pass =
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
- color_attachments: &[
- wgpu::RenderPassColorAttachmentDescriptor {
- attachment: target,
- resolve_target: None,
- ops: wgpu::Operations {
- load: wgpu::LoadOp::Load,
- store: true,
- },
+ label: Some("iced_wgpu::image render pass"),
+ color_attachments: &[wgpu::RenderPassColorAttachment {
+ view: target,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Load,
+ store: true,
},
- ],
+ }],
depth_stencil_attachment: None,
});
render_pass.set_pipeline(&self.pipeline);
render_pass.set_bind_group(0, &self.constants, &[]);
render_pass.set_bind_group(1, &self.texture, &[]);
- render_pass.set_index_buffer(self.indices.slice(..));
+ render_pass.set_index_buffer(
+ self.indices.slice(..),
+ wgpu::IndexFormat::Uint16,
+ );
render_pass.set_vertex_buffer(0, self.vertices.slice(..));
render_pass.set_vertex_buffer(1, self.instances.slice(..));
@@ -453,7 +446,7 @@ impl Pipeline {
}
pub fn trim_cache(&mut self) {
- #[cfg(feature = "image")]
+ #[cfg(feature = "image_rs")]
self.raster_cache.borrow_mut().trim(&mut self.texture_atlas);
#[cfg(feature = "svg")]
diff --git a/wgpu/src/image/atlas.rs b/wgpu/src/image/atlas.rs
index 660ebe44..4855fa4a 100644
--- a/wgpu/src/image/atlas.rs
+++ b/wgpu/src/image/atlas.rs
@@ -4,6 +4,8 @@ mod allocation;
mod allocator;
mod layer;
+use std::num::NonZeroU32;
+
pub use allocation::Allocation;
pub use entry::Entry;
pub use layer::Layer;
@@ -24,7 +26,7 @@ impl Atlas {
let extent = wgpu::Extent3d {
width: SIZE,
height: SIZE,
- depth: 1,
+ depth_or_array_layers: 1,
};
let texture = device.create_texture(&wgpu::TextureDescriptor {
@@ -294,19 +296,19 @@ impl Atlas {
let extent = wgpu::Extent3d {
width,
height,
- depth: 1,
+ depth_or_array_layers: 1,
};
encoder.copy_buffer_to_texture(
- wgpu::BufferCopyView {
+ wgpu::ImageCopyBuffer {
buffer,
- layout: wgpu::TextureDataLayout {
+ layout: wgpu::ImageDataLayout {
offset: offset as u64,
- bytes_per_row: 4 * image_width + padding,
- rows_per_image: image_height,
+ bytes_per_row: NonZeroU32::new(4 * image_width + padding),
+ rows_per_image: NonZeroU32::new(image_height),
},
},
- wgpu::TextureCopyView {
+ wgpu::ImageCopyTexture {
texture: &self.texture,
mip_level: 0,
origin: wgpu::Origin3d {
@@ -334,7 +336,7 @@ impl Atlas {
size: wgpu::Extent3d {
width: SIZE,
height: SIZE,
- depth: self.layers.len() as u32,
+ depth_or_array_layers: self.layers.len() as u32,
},
mip_level_count: 1,
sample_count: 1,
@@ -355,7 +357,7 @@ impl Atlas {
}
encoder.copy_texture_to_texture(
- wgpu::TextureCopyView {
+ wgpu::ImageCopyTexture {
texture: &self.texture,
mip_level: 0,
origin: wgpu::Origin3d {
@@ -364,7 +366,7 @@ impl Atlas {
z: i as u32,
},
},
- wgpu::TextureCopyView {
+ wgpu::ImageCopyTexture {
texture: &new_texture,
mip_level: 0,
origin: wgpu::Origin3d {
@@ -376,7 +378,7 @@ impl Atlas {
wgpu::Extent3d {
width: SIZE,
height: SIZE,
- depth: 1,
+ depth_or_array_layers: 1,
},
);
}
diff --git a/wgpu/src/image/atlas/entry.rs b/wgpu/src/image/atlas/entry.rs
index 0310fc54..9b3f16df 100644
--- a/wgpu/src/image/atlas/entry.rs
+++ b/wgpu/src/image/atlas/entry.rs
@@ -10,7 +10,7 @@ pub enum Entry {
}
impl Entry {
- #[cfg(feature = "image")]
+ #[cfg(feature = "image_rs")]
pub fn size(&self) -> (u32, u32) {
match self {
Entry::Contiguous(allocation) => allocation.size(),
diff --git a/wgpu/src/image/raster.rs b/wgpu/src/image/raster.rs
index 25607dab..d5c62545 100644
--- a/wgpu/src/image/raster.rs
+++ b/wgpu/src/image/raster.rs
@@ -4,7 +4,7 @@ use std::collections::{HashMap, HashSet};
#[derive(Debug)]
pub enum Memory {
- Host(::image::ImageBuffer<::image::Bgra<u8>, Vec<u8>>),
+ Host(::image_rs::ImageBuffer<::image_rs::Bgra<u8>, Vec<u8>>),
Device(atlas::Entry),
NotFound,
Invalid,
@@ -42,14 +42,14 @@ impl Cache {
let memory = match handle.data() {
image::Data::Path(path) => {
- if let Ok(image) = ::image::open(path) {
+ if let Ok(image) = ::image_rs::open(path) {
Memory::Host(image.to_bgra8())
} else {
Memory::NotFound
}
}
image::Data::Bytes(bytes) => {
- if let Ok(image) = ::image::load_from_memory(&bytes) {
+ if let Ok(image) = ::image_rs::load_from_memory(&bytes) {
Memory::Host(image.to_bgra8())
} else {
Memory::Invalid
@@ -60,7 +60,7 @@ impl Cache {
height,
pixels,
} => {
- if let Some(image) = ::image::ImageBuffer::from_vec(
+ if let Some(image) = ::image_rs::ImageBuffer::from_vec(
*width,
*height,
pixels.to_vec(),
diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs
index 95df2e99..cd511a45 100644
--- a/wgpu/src/image/vector.rs
+++ b/wgpu/src/image/vector.rs
@@ -1,9 +1,10 @@
-use crate::image::atlas::{self, Atlas};
use iced_native::svg;
use std::collections::{HashMap, HashSet};
+use crate::image::atlas::{self, Atlas};
+
pub enum Svg {
- Loaded(resvg::usvg::Tree),
+ Loaded(usvg::Tree),
NotFound,
}
@@ -43,17 +44,15 @@ impl Cache {
return self.svgs.get(&handle.id()).unwrap();
}
- let opt = resvg::Options::default();
-
let svg = match handle.data() {
svg::Data::Path(path) => {
- match resvg::usvg::Tree::from_file(path, &opt.usvg) {
+ match usvg::Tree::from_file(path, &Default::default()) {
Ok(tree) => Svg::Loaded(tree),
Err(_) => Svg::NotFound,
}
}
svg::Data::Bytes(bytes) => {
- match resvg::usvg::Tree::from_data(&bytes, &opt.usvg) {
+ match usvg::Tree::from_data(&bytes, &Default::default()) {
Ok(tree) => Svg::Loaded(tree),
Err(_) => Svg::NotFound,
}
@@ -76,8 +75,8 @@ impl Cache {
let id = handle.id();
let (width, height) = (
- (scale * width).round() as u32,
- (scale * height).round() as u32,
+ (scale * width).ceil() as u32,
+ (scale * height).ceil() as u32,
);
// TODO: Optimize!
@@ -101,26 +100,29 @@ impl Cache {
// We currently rerasterize the SVG when its size changes. This is slow
// as heck. A GPU rasterizer like `pathfinder` may perform better.
// It would be cool to be able to smooth resize the `svg` example.
- let screen_size =
- resvg::ScreenSize::new(width, height).unwrap();
-
- let mut canvas =
- resvg::raqote::DrawTarget::new(width as i32, height as i32);
-
- resvg::backend_raqote::render_to_canvas(
+ let img = resvg::render(
tree,
- &resvg::Options::default(),
- screen_size,
- &mut canvas,
- );
+ if width > height {
+ usvg::FitTo::Width(width)
+ } else {
+ usvg::FitTo::Height(height)
+ },
+ None,
+ )?;
+ let width = img.width();
+ let height = img.height();
+
+ let mut rgba = img.take();
+ rgba.chunks_exact_mut(4).for_each(|rgba| rgba.swap(0, 2));
let allocation = texture_atlas.upload(
width,
height,
- bytemuck::cast_slice(canvas.get_data()),
+ bytemuck::cast_slice(rgba.as_slice()),
device,
encoder,
)?;
+ log::debug!("allocating {} {}x{}", id, width, height);
let _ = self.svg_hits.insert(id);
let _ = self.rasterized_hits.insert((id, width, height));
diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs
index a4c2ac0e..e868a655 100644
--- a/wgpu/src/lib.rs
+++ b/wgpu/src/lib.rs
@@ -49,7 +49,7 @@ pub use widget::*;
pub(crate) use iced_graphics::Transformation;
-#[cfg(any(feature = "image", feature = "svg"))]
+#[cfg(any(feature = "image_rs", feature = "svg"))]
mod image;
/// A [`wgpu`] graphics renderer for [`iced`].
diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs
index 24d20cfa..93942fba 100644
--- a/wgpu/src/quad.rs
+++ b/wgpu/src/quad.rs
@@ -24,10 +24,11 @@ impl Pipeline {
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStage::VERTEX,
- ty: wgpu::BindingType::UniformBuffer {
- dynamic: false,
+ ty: wgpu::BindingType::Buffer {
+ ty: wgpu::BufferBindingType::Uniform,
+ has_dynamic_offset: false,
min_binding_size: wgpu::BufferSize::new(
- mem::size_of::<Uniforms>() as u64,
+ mem::size_of::<Uniforms>() as wgpu::BufferAddress,
),
},
count: None,
@@ -36,7 +37,7 @@ impl Pipeline {
let constants_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("iced_wgpu::quad uniforms buffer"),
- size: mem::size_of::<Uniforms>() as u64,
+ size: mem::size_of::<Uniforms>() as wgpu::BufferAddress,
usage: wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST,
mapped_at_creation: false,
});
@@ -46,9 +47,7 @@ impl Pipeline {
layout: &constant_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
- resource: wgpu::BindingResource::Buffer(
- constants_buffer.slice(..),
- ),
+ resource: constants_buffer.as_entire_binding(),
}],
});
@@ -59,98 +58,77 @@ impl Pipeline {
bind_group_layouts: &[&constant_layout],
});
- let vs_module = device
- .create_shader_module(wgpu::include_spirv!("shader/quad.vert.spv"));
-
- let fs_module = device
- .create_shader_module(wgpu::include_spirv!("shader/quad.frag.spv"));
+ let shader =
+ device.create_shader_module(&wgpu::ShaderModuleDescriptor {
+ label: Some("iced_wgpu::quad::shader"),
+ source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
+ include_str!("shader/quad.wgsl"),
+ )),
+ flags: wgpu::ShaderFlags::all(),
+ });
let pipeline =
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("iced_wgpu::quad pipeline"),
layout: Some(&layout),
- vertex_stage: wgpu::ProgrammableStageDescriptor {
- module: &vs_module,
- entry_point: "main",
- },
- fragment_stage: Some(wgpu::ProgrammableStageDescriptor {
- module: &fs_module,
- entry_point: "main",
- }),
- rasterization_state: Some(wgpu::RasterizationStateDescriptor {
- front_face: wgpu::FrontFace::Cw,
- cull_mode: wgpu::CullMode::None,
- ..Default::default()
- }),
- primitive_topology: wgpu::PrimitiveTopology::TriangleList,
- color_states: &[wgpu::ColorStateDescriptor {
- format,
- color_blend: wgpu::BlendDescriptor {
- src_factor: wgpu::BlendFactor::SrcAlpha,
- dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
- operation: wgpu::BlendOperation::Add,
- },
- alpha_blend: wgpu::BlendDescriptor {
- src_factor: wgpu::BlendFactor::One,
- dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
- operation: wgpu::BlendOperation::Add,
- },
- write_mask: wgpu::ColorWrite::ALL,
- }],
- depth_stencil_state: None,
- vertex_state: wgpu::VertexStateDescriptor {
- index_format: wgpu::IndexFormat::Uint16,
- vertex_buffers: &[
- wgpu::VertexBufferDescriptor {
- stride: mem::size_of::<Vertex>() as u64,
+ vertex: wgpu::VertexState {
+ module: &shader,
+ entry_point: "vs_main",
+ buffers: &[
+ wgpu::VertexBufferLayout {
+ array_stride: mem::size_of::<Vertex>() as u64,
step_mode: wgpu::InputStepMode::Vertex,
- attributes: &[wgpu::VertexAttributeDescriptor {
+ attributes: &[wgpu::VertexAttribute {
shader_location: 0,
- format: wgpu::VertexFormat::Float2,
+ format: wgpu::VertexFormat::Float32x2,
offset: 0,
}],
},
- wgpu::VertexBufferDescriptor {
- stride: mem::size_of::<layer::Quad>() as u64,
+ wgpu::VertexBufferLayout {
+ array_stride: mem::size_of::<layer::Quad>() as u64,
step_mode: wgpu::InputStepMode::Instance,
- attributes: &[
- wgpu::VertexAttributeDescriptor {
- shader_location: 1,
- format: wgpu::VertexFormat::Float2,
- offset: 0,
- },
- wgpu::VertexAttributeDescriptor {
- shader_location: 2,
- format: wgpu::VertexFormat::Float2,
- offset: 4 * 2,
- },
- wgpu::VertexAttributeDescriptor {
- shader_location: 3,
- format: wgpu::VertexFormat::Float4,
- offset: 4 * (2 + 2),
- },
- wgpu::VertexAttributeDescriptor {
- shader_location: 4,
- format: wgpu::VertexFormat::Float4,
- offset: 4 * (2 + 2 + 4),
- },
- wgpu::VertexAttributeDescriptor {
- shader_location: 5,
- format: wgpu::VertexFormat::Float,
- offset: 4 * (2 + 2 + 4 + 4),
- },
- wgpu::VertexAttributeDescriptor {
- shader_location: 6,
- format: wgpu::VertexFormat::Float,
- offset: 4 * (2 + 2 + 4 + 4 + 1),
- },
- ],
+ attributes: &wgpu::vertex_attr_array!(
+ 1 => Float32x2,
+ 2 => Float32x2,
+ 3 => Float32x4,
+ 4 => Float32x4,
+ 5 => Float32,
+ 6 => Float32,
+ ),
},
],
},
- sample_count: 1,
- sample_mask: !0,
- alpha_to_coverage_enabled: false,
+ fragment: Some(wgpu::FragmentState {
+ module: &shader,
+ entry_point: "fs_main",
+ targets: &[wgpu::ColorTargetState {
+ format,
+ blend: Some(wgpu::BlendState {
+ color: wgpu::BlendComponent {
+ src_factor: wgpu::BlendFactor::SrcAlpha,
+ dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
+ operation: wgpu::BlendOperation::Add,
+ },
+ alpha: wgpu::BlendComponent {
+ src_factor: wgpu::BlendFactor::One,
+ dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
+ operation: wgpu::BlendOperation::Add,
+ },
+ }),
+ write_mask: wgpu::ColorWrite::ALL,
+ }],
+ }),
+ primitive: wgpu::PrimitiveState {
+ topology: wgpu::PrimitiveTopology::TriangleList,
+ front_face: wgpu::FrontFace::Cw,
+ ..Default::default()
+ },
+ depth_stencil: None,
+ multisample: wgpu::MultisampleState {
+ count: 1,
+ mask: !0,
+ alpha_to_coverage_enabled: false,
+ },
});
let vertices =
@@ -232,30 +210,33 @@ impl Pipeline {
{
let mut render_pass =
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
- color_attachments: &[
- wgpu::RenderPassColorAttachmentDescriptor {
- attachment: target,
- resolve_target: None,
- ops: wgpu::Operations {
- load: wgpu::LoadOp::Load,
- store: true,
- },
+ label: Some("iced_wgpu::quad render pass"),
+ color_attachments: &[wgpu::RenderPassColorAttachment {
+ view: target,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Load,
+ store: true,
},
- ],
+ }],
depth_stencil_attachment: None,
});
render_pass.set_pipeline(&self.pipeline);
render_pass.set_bind_group(0, &self.constants, &[]);
- render_pass.set_index_buffer(self.indices.slice(..));
+ render_pass.set_index_buffer(
+ self.indices.slice(..),
+ wgpu::IndexFormat::Uint16,
+ );
render_pass.set_vertex_buffer(0, self.vertices.slice(..));
render_pass.set_vertex_buffer(1, self.instances.slice(..));
+
render_pass.set_scissor_rect(
bounds.x,
bounds.y,
bounds.width,
// TODO: Address anti-aliasing adjustments properly
- bounds.height + 1,
+ bounds.height,
);
render_pass.draw_indexed(
@@ -300,6 +281,9 @@ const MAX_INSTANCES: usize = 100_000;
struct Uniforms {
transform: [f32; 16],
scale: f32,
+ // Uniforms must be aligned to their largest member,
+ // this uses a mat4x4<f32> which aligns to 16, so align to that
+ _padding: [f32; 3],
}
impl Uniforms {
@@ -307,6 +291,7 @@ impl Uniforms {
Self {
transform: *transformation.as_ref(),
scale,
+ _padding: [0.0; 3],
}
}
}
@@ -316,6 +301,7 @@ impl Default for Uniforms {
Self {
transform: *Transformation::identity().as_ref(),
scale: 1.0,
+ _padding: [0.0; 3],
}
}
}
diff --git a/wgpu/src/settings.rs b/wgpu/src/settings.rs
index 26763e22..9a7eed34 100644
--- a/wgpu/src/settings.rs
+++ b/wgpu/src/settings.rs
@@ -16,6 +16,9 @@ pub struct Settings {
/// [`Backend`]: crate::Backend
pub present_mode: wgpu::PresentMode,
+ /// The internal graphics backend to use.
+ pub internal_backend: wgpu::BackendBit,
+
/// The bytes of the font that will be used by default.
///
/// If `None` is provided, a default system font will be chosen.
@@ -26,18 +29,67 @@ pub struct Settings {
/// By default, it will be set to 20.
pub default_text_size: u16,
+ /// If enabled, spread text workload in multiple threads when multiple cores
+ /// are available.
+ ///
+ /// By default, it is disabled.
+ pub text_multithreading: bool,
+
/// The antialiasing strategy that will be used for triangle primitives.
+ ///
+ /// By default, it is `None`.
pub antialiasing: Option<Antialiasing>,
}
+impl Settings {
+ /// Creates new [`Settings`] using environment configuration.
+ ///
+ /// Specifically:
+ ///
+ /// - The `internal_backend` can be configured using the `WGPU_BACKEND`
+ /// environment variable. If the variable is not set, the primary backend
+ /// will be used. The following values are allowed:
+ /// - `vulkan`
+ /// - `metal`
+ /// - `dx12`
+ /// - `dx11`
+ /// - `gl`
+ /// - `webgpu`
+ /// - `primary`
+ pub fn from_env() -> Self {
+ Settings {
+ internal_backend: backend_from_env()
+ .unwrap_or(wgpu::BackendBit::PRIMARY),
+ ..Self::default()
+ }
+ }
+}
+
impl Default for Settings {
fn default() -> Settings {
Settings {
format: wgpu::TextureFormat::Bgra8UnormSrgb,
present_mode: wgpu::PresentMode::Mailbox,
+ internal_backend: wgpu::BackendBit::PRIMARY,
default_font: None,
default_text_size: 20,
+ text_multithreading: false,
antialiasing: None,
}
}
}
+
+fn backend_from_env() -> Option<wgpu::BackendBit> {
+ std::env::var("WGPU_BACKEND").ok().map(|backend| {
+ match backend.to_lowercase().as_str() {
+ "vulkan" => wgpu::BackendBit::VULKAN,
+ "metal" => wgpu::BackendBit::METAL,
+ "dx12" => wgpu::BackendBit::DX12,
+ "dx11" => wgpu::BackendBit::DX11,
+ "gl" => wgpu::BackendBit::GL,
+ "webgpu" => wgpu::BackendBit::BROWSER_WEBGPU,
+ "primary" => wgpu::BackendBit::PRIMARY,
+ other => panic!("Unknown backend: {}", other),
+ }
+ })
+}
diff --git a/wgpu/src/shader/blit.frag b/wgpu/src/shader/blit.frag
deleted file mode 100644
index dfed960f..00000000
--- a/wgpu/src/shader/blit.frag
+++ /dev/null
@@ -1,12 +0,0 @@
-#version 450
-
-layout(location = 0) in vec2 v_Uv;
-
-layout(set = 0, binding = 0) uniform sampler u_Sampler;
-layout(set = 1, binding = 0) uniform texture2D u_Texture;
-
-layout(location = 0) out vec4 o_Color;
-
-void main() {
- o_Color = texture(sampler2D(u_Texture, u_Sampler), v_Uv);
-}
diff --git a/wgpu/src/shader/blit.frag.spv b/wgpu/src/shader/blit.frag.spv
deleted file mode 100644
index 2c5638b5..00000000
--- a/wgpu/src/shader/blit.frag.spv
+++ /dev/null
Binary files differ
diff --git a/wgpu/src/shader/blit.vert b/wgpu/src/shader/blit.vert
deleted file mode 100644
index 899cd39d..00000000
--- a/wgpu/src/shader/blit.vert
+++ /dev/null
@@ -1,26 +0,0 @@
-#version 450
-
-layout(location = 0) out vec2 o_Uv;
-
-const vec2 positions[6] = vec2[6](
- vec2(-1.0, 1.0),
- vec2(-1.0, -1.0),
- vec2(1.0, -1.0),
- vec2(-1.0, 1.0),
- vec2(1.0, 1.0),
- vec2(1.0, -1.0)
-);
-
-const vec2 uvs[6] = vec2[6](
- vec2(0.0, 0.0),
- vec2(0.0, 1.0),
- vec2(1.0, 1.0),
- vec2(0.0, 0.0),
- vec2(1.0, 0.0),
- vec2(1.0, 1.0)
-);
-
-void main() {
- o_Uv = uvs[gl_VertexIndex];
- gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
-}
diff --git a/wgpu/src/shader/blit.vert.spv b/wgpu/src/shader/blit.vert.spv
deleted file mode 100644
index e0b436ce..00000000
--- a/wgpu/src/shader/blit.vert.spv
+++ /dev/null
Binary files differ
diff --git a/wgpu/src/shader/blit.wgsl b/wgpu/src/shader/blit.wgsl
new file mode 100644
index 00000000..694f192e
--- /dev/null
+++ b/wgpu/src/shader/blit.wgsl
@@ -0,0 +1,43 @@
+var 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 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 u_sampler: sampler;
+[[group(1), binding(0)]] var u_texture: texture_2d<f32>;
+
+struct VertexInput {
+ [[builtin(vertex_index)]] vertex_index: u32;
+};
+
+struct VertexOutput {
+ [[builtin(position)]] position: vec4<f32>;
+ [[location(0)]] uv: vec2<f32>;
+};
+
+[[stage(vertex)]]
+fn vs_main(input: VertexInput) -> VertexOutput {
+ var out: VertexOutput;
+ out.uv = uvs[input.vertex_index];
+ out.position = vec4<f32>(positions[input.vertex_index], 0.0, 1.0);
+
+ return out;
+}
+
+[[stage(fragment)]]
+fn fs_main(input: VertexOutput) -> [[location(0)]] vec4<f32> {
+ return textureSample(u_texture, u_sampler, input.uv);
+}
diff --git a/wgpu/src/shader/image.frag b/wgpu/src/shader/image.frag
deleted file mode 100644
index 2809e9e6..00000000
--- a/wgpu/src/shader/image.frag
+++ /dev/null
@@ -1,12 +0,0 @@
-#version 450
-
-layout(location = 0) in vec3 v_Uv;
-
-layout(set = 0, binding = 1) uniform sampler u_Sampler;
-layout(set = 1, binding = 0) uniform texture2DArray u_Texture;
-
-layout(location = 0) out vec4 o_Color;
-
-void main() {
- o_Color = texture(sampler2DArray(u_Texture, u_Sampler), v_Uv);
-}
diff --git a/wgpu/src/shader/image.frag.spv b/wgpu/src/shader/image.frag.spv
deleted file mode 100644
index 65b08aa3..00000000
--- a/wgpu/src/shader/image.frag.spv
+++ /dev/null
Binary files differ
diff --git a/wgpu/src/shader/image.vert b/wgpu/src/shader/image.vert
deleted file mode 100644
index dab53cfe..00000000
--- a/wgpu/src/shader/image.vert
+++ /dev/null
@@ -1,27 +0,0 @@
-#version 450
-
-layout(location = 0) in vec2 v_Pos;
-layout(location = 1) in vec2 i_Pos;
-layout(location = 2) in vec2 i_Scale;
-layout(location = 3) in vec2 i_Atlas_Pos;
-layout(location = 4) in vec2 i_Atlas_Scale;
-layout(location = 5) in uint i_Layer;
-
-layout (set = 0, binding = 0) uniform Globals {
- mat4 u_Transform;
-};
-
-layout(location = 0) out vec3 o_Uv;
-
-void main() {
- o_Uv = vec3(v_Pos * i_Atlas_Scale + i_Atlas_Pos, i_Layer);
-
- mat4 i_Transform = mat4(
- vec4(i_Scale.x, 0.0, 0.0, 0.0),
- vec4(0.0, i_Scale.y, 0.0, 0.0),
- vec4(0.0, 0.0, 1.0, 0.0),
- vec4(i_Pos, 0.0, 1.0)
- );
-
- gl_Position = u_Transform * i_Transform * vec4(v_Pos, 0.0, 1.0);
-}
diff --git a/wgpu/src/shader/image.vert.spv b/wgpu/src/shader/image.vert.spv
deleted file mode 100644
index 21f5db2d..00000000
--- a/wgpu/src/shader/image.vert.spv
+++ /dev/null
Binary files differ
diff --git a/wgpu/src/shader/image.wgsl b/wgpu/src/shader/image.wgsl
new file mode 100644
index 00000000..a63ee8f6
--- /dev/null
+++ b/wgpu/src/shader/image.wgsl
@@ -0,0 +1,47 @@
+[[block]]
+struct Globals {
+ transform: mat4x4<f32>;
+};
+
+[[group(0), binding(0)]] var<uniform> globals: Globals;
+[[group(0), binding(1)]] var u_sampler: sampler;
+[[group(1), binding(0)]] var u_texture: texture_2d_array<f32>;
+
+struct VertexInput {
+ [[location(0)]] v_pos: vec2<f32>;
+ [[location(1)]] pos: vec2<f32>;
+ [[location(2)]] scale: vec2<f32>;
+ [[location(3)]] atlas_pos: vec2<f32>;
+ [[location(4)]] atlas_scale: vec2<f32>;
+ [[location(5)]] layer: i32;
+};
+
+struct VertexOutput {
+ [[builtin(position)]] position: vec4<f32>;
+ [[location(0)]] uv: vec2<f32>;
+ [[location(1)]] layer: f32; // this should be an i32, but naga currently reads that as requiring interpolation.
+};
+
+[[stage(vertex)]]
+fn vs_main(input: VertexInput) -> VertexOutput {
+ var out: VertexOutput;
+
+ out.uv = vec2<f32>(input.v_pos * input.atlas_scale + input.atlas_pos);
+ out.layer = f32(input.layer);
+
+ var transform: mat4x4<f32> = mat4x4<f32>(
+ vec4<f32>(input.scale.x, 0.0, 0.0, 0.0),
+ vec4<f32>(0.0, input.scale.y, 0.0, 0.0),
+ vec4<f32>(0.0, 0.0, 1.0, 0.0),
+ vec4<f32>(input.pos, 0.0, 1.0)
+ );
+
+ out.position = globals.transform * transform * vec4<f32>(input.v_pos, 0.0, 1.0);
+
+ return out;
+}
+
+[[stage(fragment)]]
+fn fs_main(input: VertexOutput) -> [[location(0)]] vec4<f32> {
+ return textureSample(u_texture, u_sampler, input.uv, i32(input.layer));
+}
diff --git a/wgpu/src/shader/quad.frag b/wgpu/src/shader/quad.frag
deleted file mode 100644
index ad1af1ad..00000000
--- a/wgpu/src/shader/quad.frag
+++ /dev/null
@@ -1,66 +0,0 @@
-#version 450
-
-layout(location = 0) in vec4 v_Color;
-layout(location = 1) in vec4 v_BorderColor;
-layout(location = 2) in vec2 v_Pos;
-layout(location = 3) in vec2 v_Scale;
-layout(location = 4) in float v_BorderRadius;
-layout(location = 5) in float v_BorderWidth;
-
-layout(location = 0) out vec4 o_Color;
-
-float distance(in vec2 frag_coord, in vec2 position, in vec2 size, float radius)
-{
- // TODO: Try SDF approach: https://www.shadertoy.com/view/wd3XRN
- vec2 inner_size = size - vec2(radius, radius) * 2.0;
- vec2 top_left = position + vec2(radius, radius);
- vec2 bottom_right = top_left + inner_size;
-
- vec2 top_left_distance = top_left - frag_coord;
- vec2 bottom_right_distance = frag_coord - bottom_right;
-
- vec2 distance = vec2(
- max(max(top_left_distance.x, bottom_right_distance.x), 0),
- max(max(top_left_distance.y, bottom_right_distance.y), 0)
- );
-
- return sqrt(distance.x * distance.x + distance.y * distance.y);
-}
-
-void main() {
- vec4 mixed_color;
-
- // TODO: Remove branching (?)
- if(v_BorderWidth > 0) {
- float internal_border = max(v_BorderRadius - v_BorderWidth, 0);
-
- float internal_distance = distance(
- gl_FragCoord.xy,
- v_Pos + vec2(v_BorderWidth),
- v_Scale - vec2(v_BorderWidth * 2.0),
- internal_border
- );
-
- float border_mix = smoothstep(
- max(internal_border - 0.5, 0.0),
- internal_border + 0.5,
- internal_distance
- );
-
- mixed_color = mix(v_Color, v_BorderColor, border_mix);
- } else {
- mixed_color = v_Color;
- }
-
- float d = distance(
- gl_FragCoord.xy,
- v_Pos,
- v_Scale,
- v_BorderRadius
- );
-
- float radius_alpha =
- 1.0 - smoothstep(max(v_BorderRadius - 0.5, 0), v_BorderRadius + 0.5, d);
-
- o_Color = vec4(mixed_color.xyz, mixed_color.w * radius_alpha);
-}
diff --git a/wgpu/src/shader/quad.frag.spv b/wgpu/src/shader/quad.frag.spv
deleted file mode 100644
index 519f5f01..00000000
--- a/wgpu/src/shader/quad.frag.spv
+++ /dev/null
Binary files differ
diff --git a/wgpu/src/shader/quad.vert b/wgpu/src/shader/quad.vert
deleted file mode 100644
index 09a278b1..00000000
--- a/wgpu/src/shader/quad.vert
+++ /dev/null
@@ -1,47 +0,0 @@
-#version 450
-
-layout(location = 0) in vec2 v_Pos;
-layout(location = 1) in vec2 i_Pos;
-layout(location = 2) in vec2 i_Scale;
-layout(location = 3) in vec4 i_Color;
-layout(location = 4) in vec4 i_BorderColor;
-layout(location = 5) in float i_BorderRadius;
-layout(location = 6) in float i_BorderWidth;
-
-layout (set = 0, binding = 0) uniform Globals {
- mat4 u_Transform;
- float u_Scale;
-};
-
-layout(location = 0) out vec4 o_Color;
-layout(location = 1) out vec4 o_BorderColor;
-layout(location = 2) out vec2 o_Pos;
-layout(location = 3) out vec2 o_Scale;
-layout(location = 4) out float o_BorderRadius;
-layout(location = 5) out float o_BorderWidth;
-
-void main() {
- vec2 p_Pos = i_Pos * u_Scale;
- vec2 p_Scale = i_Scale * u_Scale;
-
- float i_BorderRadius = min(
- i_BorderRadius,
- min(i_Scale.x, i_Scale.y) / 2.0
- );
-
- mat4 i_Transform = mat4(
- vec4(p_Scale.x + 1.0, 0.0, 0.0, 0.0),
- vec4(0.0, p_Scale.y + 1.0, 0.0, 0.0),
- vec4(0.0, 0.0, 1.0, 0.0),
- vec4(p_Pos - vec2(0.5, 0.5), 0.0, 1.0)
- );
-
- o_Color = i_Color;
- o_BorderColor = i_BorderColor;
- o_Pos = p_Pos;
- o_Scale = p_Scale;
- o_BorderRadius = i_BorderRadius * u_Scale;
- o_BorderWidth = i_BorderWidth * u_Scale;
-
- gl_Position = u_Transform * i_Transform * vec4(v_Pos, 0.0, 1.0);
-}
diff --git a/wgpu/src/shader/quad.vert.spv b/wgpu/src/shader/quad.vert.spv
deleted file mode 100644
index fa71ba1e..00000000
--- a/wgpu/src/shader/quad.vert.spv
+++ /dev/null
Binary files differ
diff --git a/wgpu/src/shader/quad.wgsl b/wgpu/src/shader/quad.wgsl
new file mode 100644
index 00000000..80d733ab
--- /dev/null
+++ b/wgpu/src/shader/quad.wgsl
@@ -0,0 +1,122 @@
+[[block]]
+struct Globals {
+ transform: mat4x4<f32>;
+ scale: f32;
+};
+
+[[group(0), binding(0)]] var<uniform> globals: Globals;
+
+struct VertexInput {
+ [[location(0)]] v_pos: vec2<f32>;
+ [[location(1)]] pos: vec2<f32>;
+ [[location(2)]] scale: vec2<f32>;
+ [[location(3)]] color: vec4<f32>;
+ [[location(4)]] border_color: vec4<f32>;
+ [[location(5)]] border_radius: f32;
+ [[location(6)]] border_width: f32;
+};
+
+struct VertexOutput {
+ [[builtin(position)]] position: vec4<f32>;
+ [[location(0)]] color: vec4<f32>;
+ [[location(1)]] border_color: vec4<f32>;
+ [[location(2)]] pos: vec2<f32>;
+ [[location(3)]] scale: vec2<f32>;
+ [[location(4)]] border_radius: f32;
+ [[location(5)]] border_width: f32;
+};
+
+[[stage(vertex)]]
+fn vs_main(input: VertexInput) -> VertexOutput {
+ var out: VertexOutput;
+
+ var pos: vec2<f32> = input.pos * globals.scale;
+ var scale: vec2<f32> = input.scale * globals.scale;
+
+ var border_radius: f32 = min(
+ input.border_radius,
+ min(input.scale.x, input.scale.y) / 2.0
+ );
+
+ var transform: mat4x4<f32> = mat4x4<f32>(
+ vec4<f32>(scale.x + 1.0, 0.0, 0.0, 0.0),
+ vec4<f32>(0.0, scale.y + 1.0, 0.0, 0.0),
+ vec4<f32>(0.0, 0.0, 1.0, 0.0),
+ vec4<f32>(pos - vec2<f32>(0.5, 0.5), 0.0, 1.0)
+ );
+
+ out.color = input.color;
+ out.border_color = input.border_color;
+ out.pos = pos;
+ out.scale = scale;
+ out.border_radius = border_radius * globals.scale;
+ out.border_width = input.border_width * globals.scale;
+ out.position = globals.transform * transform * vec4<f32>(input.v_pos, 0.0, 1.0);
+
+ return out;
+}
+
+fn distance_alg(
+ frag_coord: vec2<f32>,
+ position: vec2<f32>,
+ size: vec2<f32>,
+ radius: f32
+) -> f32 {
+ var inner_size: vec2<f32> = size - vec2<f32>(radius, radius) * 2.0;
+ var top_left: vec2<f32> = position + vec2<f32>(radius, radius);
+ var bottom_right: vec2<f32> = top_left + inner_size;
+
+ var top_left_distance: vec2<f32> = top_left - frag_coord;
+ var bottom_right_distance: vec2<f32> = frag_coord - bottom_right;
+
+ var dist: vec2<f32> = vec2<f32>(
+ max(max(top_left_distance.x, bottom_right_distance.x), 0.0),
+ max(max(top_left_distance.y, bottom_right_distance.y), 0.0)
+ );
+
+ return sqrt(dist.x * dist.x + dist.y * dist.y);
+}
+
+
+[[stage(fragment)]]
+fn fs_main(
+ input: VertexOutput
+) -> [[location(0)]] vec4<f32> {
+ var mixed_color: vec4<f32> = input.color;
+
+ if (input.border_width > 0.0) {
+ var internal_border: f32 = max(
+ input.border_radius - input.border_width,
+ 0.0
+ );
+
+ var internal_distance: f32 = distance_alg(
+ vec2<f32>(input.position.x, input.position.y),
+ input.pos + vec2<f32>(input.border_width, input.border_width),
+ input.scale - vec2<f32>(input.border_width * 2.0, input.border_width * 2.0),
+ internal_border
+ );
+
+ var border_mix: f32 = smoothStep(
+ max(internal_border - 0.5, 0.0),
+ internal_border + 0.5,
+ internal_distance
+ );
+
+ mixed_color = mix(input.color, input.border_color, vec4<f32>(border_mix, border_mix, border_mix, border_mix));
+ }
+
+ var dist: f32 = distance_alg(
+ vec2<f32>(input.position.x, input.position.y),
+ input.pos,
+ input.scale,
+ input.border_radius
+ );
+
+ var radius_alpha: f32 = 1.0 - smoothStep(
+ max(input.border_radius - 0.5, 0.0),
+ input.border_radius + 0.5,
+ dist);
+
+ return vec4<f32>(mixed_color.x, mixed_color.y, mixed_color.z, mixed_color.w * radius_alpha);
+}
diff --git a/wgpu/src/shader/triangle.frag b/wgpu/src/shader/triangle.frag
deleted file mode 100644
index e39c45e7..00000000
--- a/wgpu/src/shader/triangle.frag
+++ /dev/null
@@ -1,8 +0,0 @@
-#version 450
-
-layout(location = 0) in vec4 i_Color;
-layout(location = 0) out vec4 o_Color;
-
-void main() {
- o_Color = i_Color;
-}
diff --git a/wgpu/src/shader/triangle.frag.spv b/wgpu/src/shader/triangle.frag.spv
deleted file mode 100644
index 11201872..00000000
--- a/wgpu/src/shader/triangle.frag.spv
+++ /dev/null
Binary files differ
diff --git a/wgpu/src/shader/triangle.vert b/wgpu/src/shader/triangle.vert
deleted file mode 100644
index 1f2c009b..00000000
--- a/wgpu/src/shader/triangle.vert
+++ /dev/null
@@ -1,15 +0,0 @@
-#version 450
-
-layout(location = 0) in vec2 i_Position;
-layout(location = 1) in vec4 i_Color;
-
-layout(location = 0) out vec4 o_Color;
-
-layout (set = 0, binding = 0) uniform Globals {
- mat4 u_Transform;
-};
-
-void main() {
- gl_Position = u_Transform * vec4(i_Position, 0.0, 1.0);
- o_Color = i_Color;
-}
diff --git a/wgpu/src/shader/triangle.vert.spv b/wgpu/src/shader/triangle.vert.spv
deleted file mode 100644
index 871f4f55..00000000
--- a/wgpu/src/shader/triangle.vert.spv
+++ /dev/null
Binary files differ
diff --git a/wgpu/src/shader/triangle.wgsl b/wgpu/src/shader/triangle.wgsl
new file mode 100644
index 00000000..96eaabcc
--- /dev/null
+++ b/wgpu/src/shader/triangle.wgsl
@@ -0,0 +1,31 @@
+[[block]]
+struct Globals {
+ transform: mat4x4<f32>;
+};
+
+[[group(0), binding(0)]] var<uniform> globals: Globals;
+
+struct VertexInput {
+ [[location(0)]] position: vec2<f32>;
+ [[location(1)]] color: vec4<f32>;
+};
+
+struct VertexOutput {
+ [[builtin(position)]] position: vec4<f32>;
+ [[location(0)]] color: vec4<f32>;
+};
+
+[[stage(vertex)]]
+fn vs_main(input: VertexInput) -> VertexOutput {
+ var out: VertexOutput;
+
+ out.color = input.color;
+ out.position = globals.transform * vec4<f32>(input.position, 0.0, 1.0);
+
+ return out;
+}
+
+[[stage(fragment)]]
+fn fs_main(input: VertexOutput) -> [[location(0)]] vec4<f32> {
+ return input.color;
+}
diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs
index 78999cf8..2b5b94c9 100644
--- a/wgpu/src/text.rs
+++ b/wgpu/src/text.rs
@@ -15,10 +15,12 @@ impl Pipeline {
device: &wgpu::Device,
format: wgpu::TextureFormat,
default_font: Option<&[u8]>,
+ multithreading: bool,
) -> Self {
let default_font = default_font.map(|slice| slice.to_vec());
// TODO: Font customization
+ #[cfg(not(target_os = "ios"))]
#[cfg(feature = "default_system_font")]
let default_font = {
default_font.or_else(|| {
@@ -45,7 +47,7 @@ impl Pipeline {
let draw_brush =
wgpu_glyph::GlyphBrushBuilder::using_font(font.clone())
.initial_cache_size((2048, 2048))
- .draw_cache_multithread(false) // TODO: Expose as a configuration flag
+ .draw_cache_multithread(multithreading)
.build(device, format);
let measure_brush =
diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs
index 61a771d8..65b2b7b0 100644
--- a/wgpu/src/triangle.rs
+++ b/wgpu/src/triangle.rs
@@ -86,8 +86,9 @@ impl Pipeline {
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStage::VERTEX,
- ty: wgpu::BindingType::UniformBuffer {
- dynamic: true,
+ ty: wgpu::BindingType::Buffer {
+ ty: wgpu::BufferBindingType::Uniform,
+ has_dynamic_offset: true,
min_binding_size: wgpu::BufferSize::new(
mem::size_of::<Uniforms>() as u64,
),
@@ -110,9 +111,15 @@ impl Pipeline {
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(
- constants_buffer
- .raw
- .slice(0..std::mem::size_of::<Uniforms>() as u64),
+ wgpu::BufferBinding {
+ buffer: &constants_buffer.raw,
+ offset: 0,
+ size: wgpu::BufferSize::new(std::mem::size_of::<
+ Uniforms,
+ >(
+ )
+ as u64),
+ },
),
}],
});
@@ -124,73 +131,66 @@ impl Pipeline {
bind_group_layouts: &[&constants_layout],
});
- let vs_module = device.create_shader_module(wgpu::include_spirv!(
- "shader/triangle.vert.spv"
- ));
-
- let fs_module = device.create_shader_module(wgpu::include_spirv!(
- "shader/triangle.frag.spv"
- ));
+ let shader =
+ device.create_shader_module(&wgpu::ShaderModuleDescriptor {
+ label: Some("iced_wgpu::triangle::shader"),
+ source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
+ include_str!("shader/triangle.wgsl"),
+ )),
+ flags: wgpu::ShaderFlags::all(),
+ });
let pipeline =
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("iced_wgpu::triangle pipeline"),
layout: Some(&layout),
- vertex_stage: wgpu::ProgrammableStageDescriptor {
- module: &vs_module,
- entry_point: "main",
- },
- fragment_stage: Some(wgpu::ProgrammableStageDescriptor {
- module: &fs_module,
- entry_point: "main",
- }),
- rasterization_state: Some(wgpu::RasterizationStateDescriptor {
- front_face: wgpu::FrontFace::Cw,
- cull_mode: wgpu::CullMode::None,
- ..Default::default()
- }),
- primitive_topology: wgpu::PrimitiveTopology::TriangleList,
- color_states: &[wgpu::ColorStateDescriptor {
- format,
- color_blend: wgpu::BlendDescriptor {
- src_factor: wgpu::BlendFactor::SrcAlpha,
- dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
- operation: wgpu::BlendOperation::Add,
- },
- alpha_blend: wgpu::BlendDescriptor {
- src_factor: wgpu::BlendFactor::One,
- dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
- operation: wgpu::BlendOperation::Add,
- },
- write_mask: wgpu::ColorWrite::ALL,
- }],
- depth_stencil_state: None,
- vertex_state: wgpu::VertexStateDescriptor {
- index_format: wgpu::IndexFormat::Uint32,
- vertex_buffers: &[wgpu::VertexBufferDescriptor {
- stride: mem::size_of::<Vertex2D>() as u64,
+ vertex: wgpu::VertexState {
+ module: &shader,
+ entry_point: "vs_main",
+ buffers: &[wgpu::VertexBufferLayout {
+ array_stride: mem::size_of::<Vertex2D>() as u64,
step_mode: wgpu::InputStepMode::Vertex,
- attributes: &[
+ attributes: &wgpu::vertex_attr_array!(
// Position
- wgpu::VertexAttributeDescriptor {
- shader_location: 0,
- format: wgpu::VertexFormat::Float2,
- offset: 0,
- },
+ 0 => Float32x2,
// Color
- wgpu::VertexAttributeDescriptor {
- shader_location: 1,
- format: wgpu::VertexFormat::Float4,
- offset: 4 * 2,
+ 1 => Float32x4,
+ ),
+ }],
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: &shader,
+ entry_point: "fs_main",
+ targets: &[wgpu::ColorTargetState {
+ format,
+ blend: Some(wgpu::BlendState {
+ color: wgpu::BlendComponent {
+ src_factor: wgpu::BlendFactor::SrcAlpha,
+ dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
+ operation: wgpu::BlendOperation::Add,
+ },
+ alpha: wgpu::BlendComponent {
+ src_factor: wgpu::BlendFactor::One,
+ dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
+ operation: wgpu::BlendOperation::Add,
},
- ],
+ }),
+ write_mask: wgpu::ColorWrite::ALL,
}],
+ }),
+ primitive: wgpu::PrimitiveState {
+ topology: wgpu::PrimitiveTopology::TriangleList,
+ front_face: wgpu::FrontFace::Cw,
+ ..Default::default()
+ },
+ depth_stencil: None,
+ multisample: wgpu::MultisampleState {
+ count: u32::from(
+ antialiasing.map(|a| a.sample_count()).unwrap_or(1),
+ ),
+ mask: !0,
+ alpha_to_coverage_enabled: false,
},
- sample_count: u32::from(
- antialiasing.map(|a| a.sample_count()).unwrap_or(1),
- ),
- sample_mask: !0,
- alpha_to_coverage_enabled: false,
});
Pipeline {
@@ -253,9 +253,13 @@ impl Pipeline {
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(
- self.uniforms_buffer.raw.slice(
- 0..std::mem::size_of::<Uniforms>() as u64,
- ),
+ wgpu::BufferBinding {
+ buffer: &self.uniforms_buffer.raw,
+ offset: 0,
+ size: wgpu::BufferSize::new(
+ std::mem::size_of::<Uniforms>() as u64,
+ ),
+ },
),
}],
});
@@ -356,13 +360,12 @@ impl Pipeline {
let mut render_pass =
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
- color_attachments: &[
- wgpu::RenderPassColorAttachmentDescriptor {
- attachment,
- resolve_target,
- ops: wgpu::Operations { load, store: true },
- },
- ],
+ label: Some("iced_wgpu::triangle render pass"),
+ color_attachments: &[wgpu::RenderPassColorAttachment {
+ view: attachment,
+ resolve_target,
+ ops: wgpu::Operations { load, store: true },
+ }],
depth_stencil_attachment: None,
});
@@ -390,6 +393,7 @@ impl Pipeline {
self.index_buffer
.raw
.slice(index_offset * mem::size_of::<u32>() as u64..),
+ wgpu::IndexFormat::Uint32,
);
render_pass.set_vertex_buffer(
diff --git a/wgpu/src/triangle/msaa.rs b/wgpu/src/triangle/msaa.rs
index db86f748..c099d518 100644
--- a/wgpu/src/triangle/msaa.rs
+++ b/wgpu/src/triangle/msaa.rs
@@ -20,9 +20,9 @@ impl Blit {
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,
+ mag_filter: wgpu::FilterMode::Nearest,
+ min_filter: wgpu::FilterMode::Nearest,
+ mipmap_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
@@ -32,7 +32,10 @@ impl Blit {
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStage::FRAGMENT,
- ty: wgpu::BindingType::Sampler { comparison: false },
+ ty: wgpu::BindingType::Sampler {
+ comparison: false,
+ filtering: false,
+ },
count: None,
}],
});
@@ -53,9 +56,11 @@ impl Blit {
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStage::FRAGMENT,
- ty: wgpu::BindingType::SampledTexture {
- dimension: wgpu::TextureViewDimension::D2,
- component_type: wgpu::TextureComponentType::Float,
+ ty: wgpu::BindingType::Texture {
+ sample_type: wgpu::TextureSampleType::Float {
+ filterable: false,
+ },
+ view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
@@ -69,54 +74,55 @@ impl Blit {
bind_group_layouts: &[&constant_layout, &texture_layout],
});
- let vs_module = device.create_shader_module(wgpu::include_spirv!(
- "../shader/blit.vert.spv"
- ));
-
- let fs_module = device.create_shader_module(wgpu::include_spirv!(
- "../shader/blit.frag.spv"
- ));
+ let shader =
+ device.create_shader_module(&wgpu::ShaderModuleDescriptor {
+ label: Some("iced_wgpu::triangle::blit_shader"),
+ source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
+ include_str!("../shader/blit.wgsl"),
+ )),
+ flags: wgpu::ShaderFlags::all(),
+ });
let pipeline =
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("iced_wgpu::triangle::msaa pipeline"),
layout: Some(&layout),
- vertex_stage: wgpu::ProgrammableStageDescriptor {
- module: &vs_module,
- entry_point: "main",
+ vertex: wgpu::VertexState {
+ module: &shader,
+ entry_point: "vs_main",
+ buffers: &[],
},
- fragment_stage: Some(wgpu::ProgrammableStageDescriptor {
- module: &fs_module,
- entry_point: "main",
+ fragment: Some(wgpu::FragmentState {
+ module: &shader,
+ entry_point: "fs_main",
+ targets: &[wgpu::ColorTargetState {
+ format,
+ blend: Some(wgpu::BlendState {
+ color: wgpu::BlendComponent {
+ src_factor: wgpu::BlendFactor::SrcAlpha,
+ dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
+ operation: wgpu::BlendOperation::Add,
+ },
+ alpha: wgpu::BlendComponent {
+ src_factor: wgpu::BlendFactor::One,
+ dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
+ operation: wgpu::BlendOperation::Add,
+ },
+ }),
+ write_mask: wgpu::ColorWrite::ALL,
+ }],
}),
- rasterization_state: Some(wgpu::RasterizationStateDescriptor {
+ primitive: wgpu::PrimitiveState {
+ topology: wgpu::PrimitiveTopology::TriangleList,
front_face: wgpu::FrontFace::Cw,
- cull_mode: wgpu::CullMode::None,
..Default::default()
- }),
- primitive_topology: wgpu::PrimitiveTopology::TriangleList,
- color_states: &[wgpu::ColorStateDescriptor {
- format,
- color_blend: wgpu::BlendDescriptor {
- src_factor: wgpu::BlendFactor::SrcAlpha,
- dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
- operation: wgpu::BlendOperation::Add,
- },
- alpha_blend: wgpu::BlendDescriptor {
- src_factor: wgpu::BlendFactor::One,
- dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
- operation: wgpu::BlendOperation::Add,
- },
- write_mask: wgpu::ColorWrite::ALL,
- }],
- depth_stencil_state: None,
- vertex_state: wgpu::VertexStateDescriptor {
- index_format: wgpu::IndexFormat::Uint16,
- vertex_buffers: &[],
},
- sample_count: 1,
- sample_mask: !0,
- alpha_to_coverage_enabled: false,
+ depth_stencil: None,
+ multisample: wgpu::MultisampleState {
+ count: 1,
+ mask: !0,
+ alpha_to_coverage_enabled: false,
+ },
});
Blit {
@@ -172,16 +178,15 @@ impl Blit {
) {
let mut render_pass =
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
- color_attachments: &[
- wgpu::RenderPassColorAttachmentDescriptor {
- attachment: target,
- resolve_target: None,
- ops: wgpu::Operations {
- load: wgpu::LoadOp::Load,
- store: true,
- },
+ label: Some("iced_wgpu::triangle::msaa render pass"),
+ color_attachments: &[wgpu::RenderPassColorAttachment {
+ view: target,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Load,
+ store: true,
},
- ],
+ }],
depth_stencil_attachment: None,
});
@@ -217,7 +222,7 @@ impl Targets {
let extent = wgpu::Extent3d {
width,
height,
- depth: 1,
+ depth_or_array_layers: 1,
};
let attachment = device.create_texture(&wgpu::TextureDescriptor {
@@ -227,7 +232,7 @@ impl Targets {
sample_count,
dimension: wgpu::TextureDimension::D2,
format,
- usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT,
+ usage: wgpu::TextureUsage::RENDER_ATTACHMENT,
});
let resolve = device.create_texture(&wgpu::TextureDescriptor {
@@ -237,7 +242,7 @@ impl Targets {
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
- usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT
+ usage: wgpu::TextureUsage::RENDER_ATTACHMENT
| wgpu::TextureUsage::SAMPLED,
});
diff --git a/wgpu/src/widget.rs b/wgpu/src/widget.rs
index 177ae1b6..a575d036 100644
--- a/wgpu/src/widget.rs
+++ b/wgpu/src/widget.rs
@@ -20,6 +20,8 @@ pub mod rule;
pub mod scrollable;
pub mod slider;
pub mod text_input;
+pub mod toggler;
+pub mod tooltip;
#[doc(no_inline)]
pub use button::Button;
@@ -43,6 +45,10 @@ pub use scrollable::Scrollable;
pub use slider::Slider;
#[doc(no_inline)]
pub use text_input::TextInput;
+#[doc(no_inline)]
+pub use toggler::Toggler;
+#[doc(no_inline)]
+pub use tooltip::Tooltip;
#[cfg(feature = "canvas")]
#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))]
diff --git a/wgpu/src/widget/pane_grid.rs b/wgpu/src/widget/pane_grid.rs
index c26dde48..fc36862c 100644
--- a/wgpu/src/widget/pane_grid.rs
+++ b/wgpu/src/widget/pane_grid.rs
@@ -6,12 +6,12 @@
//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing,
//! drag and drop, and hotkey support.
//!
-//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.2/examples/pane_grid
+//! [`pane_grid` example]: https://github.com/hecrj/iced/tree/0.3/examples/pane_grid
use crate::Renderer;
-pub use iced_native::pane_grid::{
- Axis, Configuration, Direction, DragEvent, Node, Pane, ResizeEvent, Split,
- State,
+pub use iced_graphics::pane_grid::{
+ Axis, Configuration, Direction, DragEvent, Line, Node, Pane, ResizeEvent,
+ Split, State, StyleSheet,
};
/// A collection of panes distributed using either vertical or horizontal splits
diff --git a/wgpu/src/widget/toggler.rs b/wgpu/src/widget/toggler.rs
new file mode 100644
index 00000000..dfcf759b
--- /dev/null
+++ b/wgpu/src/widget/toggler.rs
@@ -0,0 +1,9 @@
+//! Show toggle controls using togglers.
+use crate::Renderer;
+
+pub use iced_graphics::toggler::{Style, StyleSheet};
+
+/// A toggler that can be toggled
+///
+/// This is an alias of an `iced_native` toggler with an `iced_wgpu::Renderer`.
+pub type Toggler<Message> = iced_native::Toggler<Message, Renderer>;
diff --git a/wgpu/src/widget/tooltip.rs b/wgpu/src/widget/tooltip.rs
new file mode 100644
index 00000000..89ab3a15
--- /dev/null
+++ b/wgpu/src/widget/tooltip.rs
@@ -0,0 +1,6 @@
+//! Display a widget over another.
+/// A widget allowing the selection of a single value from a list of options.
+pub type Tooltip<'a, Message> =
+ iced_native::Tooltip<'a, Message, crate::Renderer>;
+
+pub use iced_native::tooltip::Position;
diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs
index 072521ef..c21b0868 100644
--- a/wgpu/src/window/compositor.rs
+++ b/wgpu/src/window/compositor.rs
@@ -21,29 +21,38 @@ impl Compositor {
/// Requests a new [`Compositor`] with the given [`Settings`].
///
/// Returns `None` if no compatible graphics adapter could be found.
- pub async fn request(settings: Settings) -> Option<Self> {
- let instance = wgpu::Instance::new(wgpu::BackendBit::PRIMARY);
+ pub async fn request<W: HasRawWindowHandle>(
+ settings: Settings,
+ compatible_window: Option<&W>,
+ ) -> Option<Self> {
+ let instance = wgpu::Instance::new(settings.internal_backend);
+
+ #[allow(unsafe_code)]
+ let compatible_surface = compatible_window
+ .map(|window| unsafe { instance.create_surface(window) });
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: if settings.antialiasing.is_none() {
- wgpu::PowerPreference::Default
+ wgpu::PowerPreference::LowPower
} else {
wgpu::PowerPreference::HighPerformance
},
- compatible_surface: None,
+ compatible_surface: compatible_surface.as_ref(),
})
.await?;
let (device, queue) = adapter
.request_device(
&wgpu::DeviceDescriptor {
+ label: Some(
+ "iced_wgpu::window::compositor device descriptor",
+ ),
features: wgpu::Features::empty(),
limits: wgpu::Limits {
max_bind_groups: 2,
..wgpu::Limits::default()
},
- shader_validation: false,
},
None,
)
@@ -75,9 +84,15 @@ impl iced_graphics::window::Compositor for Compositor {
type Surface = wgpu::Surface;
type SwapChain = wgpu::SwapChain;
- fn new(settings: Self::Settings) -> Result<(Self, Renderer), Error> {
- let compositor = futures::executor::block_on(Self::request(settings))
- .ok_or(Error::AdapterNotFound)?;
+ fn new<W: HasRawWindowHandle>(
+ settings: Self::Settings,
+ compatible_window: Option<&W>,
+ ) -> Result<(Self, Renderer), Error> {
+ let compositor = futures::executor::block_on(Self::request(
+ settings,
+ compatible_window,
+ ))
+ .ok_or(Error::AdapterNotFound)?;
let backend = compositor.create_backend();
@@ -103,7 +118,7 @@ impl iced_graphics::window::Compositor for Compositor {
self.device.create_swap_chain(
surface,
&wgpu::SwapChainDescriptor {
- usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT,
+ usage: wgpu::TextureUsage::RENDER_ATTACHMENT,
format: self.settings.format,
present_mode: self.settings.present_mode,
width,
@@ -129,31 +144,28 @@ impl iced_graphics::window::Compositor for Compositor {
},
);
- let _ =
- encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
- color_attachments: &[
- wgpu::RenderPassColorAttachmentDescriptor {
- attachment: &frame.output.view,
- resolve_target: None,
- ops: wgpu::Operations {
- load: wgpu::LoadOp::Clear({
- let [r, g, b, a] =
- background_color.into_linear();
-
- wgpu::Color {
- r: f64::from(r),
- g: f64::from(g),
- b: f64::from(b),
- a: f64::from(a),
- }
- }),
- store: true,
- },
- },
- ],
- depth_stencil_attachment: None,
- });
-
+ let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some("iced_wgpu::window::Compositor render pass"),
+ color_attachments: &[wgpu::RenderPassColorAttachment {
+ view: &frame.output.view,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Clear({
+ let [r, g, b, a] = background_color.into_linear();
+
+ wgpu::Color {
+ r: f64::from(r),
+ g: f64::from(g),
+ b: f64::from(b),
+ a: f64::from(a),
+ }
+ }),
+ store: true,
+ },
+ }],
+ depth_stencil_attachment: None,
+ });
+
let mouse_interaction = renderer.backend_mut().draw(
&mut self.device,
&mut self.staging_belt,
@@ -163,27 +175,29 @@ impl iced_graphics::window::Compositor for Compositor {
output,
overlay,
);
-
+
// Submit work
self.staging_belt.finish();
self.queue.submit(Some(encoder.finish()));
-
+
// Recall staging buffers
self.local_pool
.spawner()
.spawn(self.staging_belt.recall())
.expect("Recall staging belt");
-
+
self.local_pool.run_until_stalled();
-
+
Ok(mouse_interaction)
}
Err(error) => match error {
- wgpu::SwapChainError::Outdated => {
+ wgpu::SwapChainError::OutOfMemory => {
+ panic!("Swapchain error: {:?}", error);
+ }
+ _ => {
// Try again next frame.
Err(())
}
- _ => panic!("Swapchain error: {:?}", error),
},
}
}
diff --git a/winit/Cargo.toml b/winit/Cargo.toml
index 39a6a5fa..b1192135 100644
--- a/winit/Cargo.toml
+++ b/winit/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "iced_winit"
-version = "0.2.0"
+version = "0.3.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
description = "A winit runtime for Iced"
@@ -14,21 +14,25 @@ categories = ["gui"]
debug = ["iced_native/debug"]
[dependencies]
-winit = "0.24"
-window_clipboard = "0.1"
+window_clipboard = "0.2"
log = "0.4"
thiserror = "1.0"
+[dependencies.winit]
+version = "0.25"
+git = "https://github.com/iced-rs/winit"
+rev = "844485272a7412cb35cdbfac3524decdf59475ca"
+
[dependencies.iced_native]
-version = "0.3"
+version = "0.4"
path = "../native"
[dependencies.iced_graphics]
-version = "0.1"
+version = "0.2"
path = "../graphics"
[dependencies.iced_futures]
-version = "0.2"
+version = "0.3"
path = "../futures"
[target.'cfg(target_os = "windows")'.dependencies.winapi]
diff --git a/winit/README.md b/winit/README.md
index 721baa14..58c782c6 100644
--- a/winit/README.md
+++ b/winit/README.md
@@ -20,7 +20,7 @@ It exposes a renderer-agnostic `Application` trait that can be implemented and t
Add `iced_winit` as a dependency in your `Cargo.toml`:
```toml
-iced_winit = "0.2"
+iced_winit = "0.3"
```
__Iced moves fast and the `master` branch can contain breaking changes!__ If
diff --git a/winit/src/application.rs b/winit/src/application.rs
index f19526cb..903d03e2 100644
--- a/winit/src/application.rs
+++ b/winit/src/application.rs
@@ -14,6 +14,7 @@ use iced_futures::futures;
use iced_futures::futures::channel::mpsc;
use iced_graphics::window;
use iced_native::program::Program;
+use iced_native::Menu;
use iced_native::{Cache, UserInterface};
use std::mem::ManuallyDrop;
@@ -29,7 +30,7 @@ use std::mem::ManuallyDrop;
///
/// When using an [`Application`] with the `debug` feature enabled, a debug view
/// can be toggled by pressing `F12`.
-pub trait Application: Program {
+pub trait Application: Program<Clipboard = Clipboard> {
/// The data needed to initialize your [`Application`].
type Flags;
@@ -91,6 +92,20 @@ pub trait Application: Program {
fn scale_factor(&self) -> f64 {
1.0
}
+
+ /// Returns whether the [`Application`] should be terminated.
+ ///
+ /// By default, it returns `false`.
+ fn should_exit(&self) -> bool {
+ false
+ }
+
+ /// Returns the current system [`Menu`] of the [`Application`].
+ ///
+ /// By default, it returns an empty [`Menu`].
+ fn menu(&self) -> Menu<Self::Message> {
+ Menu::new()
+ }
}
/// Runs an [`Application`] with an executor, compositor, and the provided
@@ -111,8 +126,6 @@ where
let mut debug = Debug::new();
debug.startup_started();
- let (compositor, renderer) = C::new(compositor_settings)?;
-
let event_loop = EventLoop::with_user_event();
let mut runtime = {
@@ -140,19 +153,23 @@ where
application.mode(),
event_loop.primary_monitor(),
)
+ .with_menu(Some(conversion::menu(&application.menu())))
.build(&event_loop)
.map_err(Error::WindowCreationFailed)?;
+ let (compositor, renderer) = C::new(compositor_settings, Some(&window))?;
+
let (mut sender, receiver) = mpsc::unbounded();
let mut instance = Box::pin(run_instance::<A, E, C>(
application,
compositor,
renderer,
- window,
runtime,
debug,
receiver,
+ window,
+ settings.exit_on_close_request,
));
let mut context = task::Context::from_waker(task::noop_waker_ref());
@@ -164,7 +181,22 @@ where
return;
}
- if let Some(event) = event.to_static() {
+ 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(),
+ };
+
+ if let Some(event) = event {
sender.start_send(event).expect("Send event");
let poll = instance.as_mut().poll(&mut context);
@@ -181,10 +213,11 @@ async fn run_instance<A, E, C>(
mut application: A,
mut compositor: C,
mut renderer: A::Renderer,
- window: winit::window::Window,
mut runtime: Runtime<E, Proxy<A::Message>, A::Message>,
mut debug: Debug,
mut receiver: mpsc::UnboundedReceiver<winit::event::Event<'_, A::Message>>,
+ window: winit::window::Window,
+ exit_on_close_request: bool,
) where
A: Application + 'static,
E: Executor + 'static,
@@ -194,7 +227,7 @@ async fn run_instance<A, E, C>(
use winit::event;
let surface = compositor.create_surface(&window);
- let clipboard = Clipboard::new(&window);
+ let mut clipboard = Clipboard::connect(&window);
let mut state = State::new(&application, &window);
let mut viewport_version = state.viewport_version();
@@ -237,8 +270,8 @@ async fn run_instance<A, E, C>(
let statuses = user_interface.update(
&events,
state.cursor_position(),
- clipboard.as_ref().map(|c| c as _),
&mut renderer,
+ &mut clipboard,
&mut messages,
);
@@ -257,12 +290,15 @@ async fn run_instance<A, E, C>(
&mut application,
&mut runtime,
&mut debug,
+ &mut clipboard,
&mut messages,
);
// Update window
state.synchronize(&application, &window);
+ let should_exit = application.should_exit();
+
user_interface = ManuallyDrop::new(build_user_interface(
&mut application,
cache,
@@ -270,6 +306,10 @@ async fn run_instance<A, E, C>(
state.logical_size(),
&mut debug,
));
+
+ if should_exit {
+ break;
+ }
}
debug.draw_started();
@@ -279,15 +319,30 @@ async fn run_instance<A, E, C>(
window.request_redraw();
}
+ event::Event::PlatformSpecific(event::PlatformSpecific::MacOS(
+ event::MacOS::ReceivedUrl(url),
+ )) => {
+ use iced_native::event;
+ events.push(iced_native::Event::PlatformSpecific(
+ event::PlatformSpecific::MacOS(event::MacOS::ReceivedUrl(
+ url,
+ )),
+ ));
+ }
event::Event::UserEvent(message) => {
messages.push(message);
}
event::Event::RedrawRequested(_) => {
+ 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 physical_size = state.physical_size();
let logical_size = state.logical_size();
debug.layout_started();
@@ -339,10 +394,22 @@ async fn run_instance<A, E, C>(
}
}
event::Event::WindowEvent {
+ event: event::WindowEvent::MenuEntryActivated(entry_id),
+ ..
+ } => {
+ if let Some(message) =
+ conversion::menu_message(state.menu(), entry_id)
+ {
+ messages.push(message);
+ }
+ }
+ event::Event::WindowEvent {
event: window_event,
..
} => {
- if requests_exit(&window_event, state.modifiers()) {
+ if requests_exit(&window_event, state.modifiers())
+ && exit_on_close_request
+ {
break;
}
@@ -414,13 +481,14 @@ pub fn update<A: Application, E: Executor>(
application: &mut A,
runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>,
debug: &mut Debug,
+ clipboard: &mut A::Clipboard,
messages: &mut Vec<A::Message>,
) {
for message in messages.drain(..) {
debug.log_message(&message);
debug.update_started();
- let command = runtime.enter(|| application.update(message));
+ let command = runtime.enter(|| application.update(message, clipboard));
debug.update_finished();
runtime.spawn(command);
diff --git a/winit/src/application/state.rs b/winit/src/application/state.rs
index 58bc7ed6..f60f09be 100644
--- a/winit/src/application/state.rs
+++ b/winit/src/application/state.rs
@@ -1,14 +1,15 @@
use crate::conversion;
-use crate::{Application, Color, Debug, Mode, Point, Size, Viewport};
+use crate::{Application, Color, Debug, Menu, Mode, Point, Size, Viewport};
use std::marker::PhantomData;
-use winit::event::WindowEvent;
+use winit::event::{Touch, WindowEvent};
use winit::window::Window;
/// The state of a windowed [`Application`].
#[derive(Debug, Clone)]
pub struct State<A: Application> {
title: String,
+ menu: Menu<A::Message>,
mode: Mode,
background_color: Color,
scale_factor: f64,
@@ -23,6 +24,7 @@ impl<A: Application> State<A> {
/// Creates a new [`State`] for the provided [`Application`] and window.
pub fn new(application: &A, window: &Window) -> Self {
let title = application.title();
+ let menu = application.menu();
let mode = application.mode();
let background_color = application.background_color();
let scale_factor = application.scale_factor();
@@ -38,6 +40,7 @@ impl<A: Application> State<A> {
Self {
title,
+ menu,
mode,
background_color,
scale_factor,
@@ -50,6 +53,11 @@ impl<A: Application> State<A> {
}
}
+ /// Returns the current [`Menu`] of the [`State`].
+ pub fn menu(&self) -> &Menu<A::Message> {
+ &self.menu
+ }
+
/// Returns the current background [`Color`] of the [`State`].
pub fn background_color(&self) -> Color {
self.background_color
@@ -128,7 +136,10 @@ impl<A: Application> State<A> {
self.viewport_version = self.viewport_version.wrapping_add(1);
}
- WindowEvent::CursorMoved { position, .. } => {
+ WindowEvent::CursorMoved { position, .. }
+ | WindowEvent::Touch(Touch {
+ location: position, ..
+ }) => {
self.cursor_position = *position;
}
WindowEvent::CursorLeft { .. } => {
@@ -179,6 +190,8 @@ impl<A: Application> State<A> {
new_mode,
));
+ window.set_visible(conversion::visible(new_mode));
+
self.mode = new_mode;
}
@@ -198,5 +211,14 @@ impl<A: Application> State<A> {
self.scale_factor = new_scale_factor;
}
+
+ // Update menu
+ let new_menu = application.menu();
+
+ if self.menu != new_menu {
+ window.set_menu(Some(conversion::menu(&new_menu)));
+
+ self.menu = new_menu;
+ }
}
}
diff --git a/winit/src/clipboard.rs b/winit/src/clipboard.rs
index 93d53b11..ca25c065 100644
--- a/winit/src/clipboard.rs
+++ b/winit/src/clipboard.rs
@@ -1,17 +1,54 @@
/// A buffer for short-term storage and transfer within and between
/// applications.
#[allow(missing_debug_implementations)]
-pub struct Clipboard(window_clipboard::Clipboard);
+pub struct Clipboard {
+ state: State,
+}
+
+enum State {
+ Connected(window_clipboard::Clipboard),
+ Unavailable,
+}
impl Clipboard {
/// Creates a new [`Clipboard`] for the given window.
- pub fn new(window: &winit::window::Window) -> Option<Clipboard> {
- window_clipboard::Clipboard::new(window).map(Clipboard).ok()
+ pub fn connect(window: &winit::window::Window) -> Clipboard {
+ let state = window_clipboard::Clipboard::connect(window)
+ .ok()
+ .map(State::Connected)
+ .unwrap_or(State::Unavailable);
+
+ Clipboard { state }
+ }
+
+ /// Reads the current content of the [`Clipboard`] as text.
+ pub fn read(&self) -> Option<String> {
+ match &self.state {
+ State::Connected(clipboard) => clipboard.read().ok(),
+ State::Unavailable => None,
+ }
+ }
+
+ /// Writes the given text contents to the [`Clipboard`].
+ pub fn write(&mut self, contents: String) {
+ match &mut self.state {
+ State::Connected(clipboard) => match clipboard.write(contents) {
+ Ok(()) => {}
+ Err(error) => {
+ log::warn!("error writing to clipboard: {}", error)
+ }
+ },
+ State::Unavailable => {}
+ }
}
}
impl iced_native::Clipboard for Clipboard {
- fn content(&self) -> Option<String> {
- self.0.read().ok()
+ fn read(&self) -> Option<String> {
+ self.read()
+ }
+
+ fn write(&mut self, contents: String) {
+ self.write(contents)
}
}
diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs
index 138bc64d..b3d05857 100644
--- a/winit/src/conversion.rs
+++ b/winit/src/conversion.rs
@@ -2,10 +2,12 @@
//!
//! [`winit`]: https://github.com/rust-windowing/winit
//! [`iced_native`]: https://github.com/hecrj/iced/tree/master/native
-use crate::{
- keyboard::{self, KeyCode, Modifiers},
- mouse, window, Event, Mode, Point,
-};
+use crate::keyboard;
+use crate::menu::{self, Menu};
+use crate::mouse;
+use crate::touch;
+use crate::window;
+use crate::{Event, Mode, Point, Position};
/// Converts a winit window event into an iced event.
pub fn window_event(
@@ -32,12 +34,14 @@ pub fn window_event(
height: logical_size.height,
}))
}
+ WindowEvent::CloseRequested => {
+ Some(Event::Window(window::Event::CloseRequested))
+ }
WindowEvent::CursorMoved { position, .. } => {
let position = position.to_logical::<f64>(scale_factor);
Some(Event::Mouse(mouse::Event::CursorMoved {
- x: position.x as f32,
- y: position.y as f32,
+ position: Point::new(position.x as f32, position.y as f32),
}))
}
WindowEvent::CursorEntered { .. } => {
@@ -109,6 +113,11 @@ pub fn window_event(
WindowEvent::ModifiersChanged(new_modifiers) => Some(Event::Keyboard(
keyboard::Event::ModifiersChanged(self::modifiers(*new_modifiers)),
)),
+ 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())))
}
@@ -118,10 +127,56 @@ pub fn window_event(
WindowEvent::HoveredFileCancelled => {
Some(Event::Window(window::Event::FilesHoveredLeft))
}
+ WindowEvent::Touch(touch) => {
+ Some(Event::Touch(touch_event(*touch, scale_factor)))
+ }
_ => None,
}
}
+/// Converts a [`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,
+) -> Option<winit::dpi::Position> {
+ match position {
+ Position::Default => None,
+ Position::Specific(x, y) => {
+ Some(winit::dpi::Position::Logical(winit::dpi::LogicalPosition {
+ x: f64::from(x),
+ y: f64::from(y),
+ }))
+ }
+ Position::Centered => {
+ if let Some(monitor) = monitor {
+ let start = monitor.position();
+
+ let resolution: winit::dpi::LogicalSize<f64> =
+ monitor.size().to_logical(monitor.scale_factor());
+
+ 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,
+ }
+ .to_physical(monitor.scale_factor());
+
+ Some(winit::dpi::Position::Physical(
+ winit::dpi::PhysicalPosition {
+ x: start.x + centered.x,
+ y: start.y + centered.y,
+ },
+ ))
+ } else {
+ None
+ }
+ }
+ }
+}
+
/// Converts a [`Mode`] to a [`winit`] fullscreen mode.
///
/// [`winit`]: https://github.com/rust-windowing/winit
@@ -130,13 +185,125 @@ pub fn fullscreen(
mode: Mode,
) -> Option<winit::window::Fullscreen> {
match mode {
- Mode::Windowed => None,
+ Mode::Windowed | Mode::Hidden => None,
Mode::Fullscreen => {
Some(winit::window::Fullscreen::Borderless(monitor))
}
}
}
+/// Converts a [`Mode`] to a visibility flag.
+pub fn visible(mode: Mode) -> bool {
+ match mode {
+ Mode::Windowed | Mode::Fullscreen => true,
+ Mode::Hidden => false,
+ }
+}
+
+/// Converts a `Hotkey` from [`iced_native`] to a [`winit`] Hotkey.
+///
+/// [`winit`]: https://github.com/rust-windowing/winit
+/// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native
+fn hotkey(hotkey: keyboard::Hotkey) -> winit::window::Hotkey {
+ use winit::event::ModifiersState;
+
+ let mut modifiers = ModifiersState::empty();
+ modifiers.set(ModifiersState::CTRL, hotkey.modifiers.control());
+ modifiers.set(ModifiersState::SHIFT, hotkey.modifiers.shift());
+ modifiers.set(ModifiersState::ALT, hotkey.modifiers.alt());
+ modifiers.set(ModifiersState::LOGO, hotkey.modifiers.logo());
+
+ winit::window::Hotkey::new(modifiers, to_virtual_keycode(hotkey.key))
+}
+
+/// Converts a `Menu` from [`iced_native`] to a [`winit`] menu.
+///
+/// [`winit`]: https://github.com/rust-windowing/winit
+/// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native
+pub fn menu<Message>(menu: &Menu<Message>) -> winit::window::Menu {
+ fn menu_i<Message>(
+ converted: &mut winit::window::Menu,
+ starting_id: usize,
+ menu: &Menu<Message>,
+ ) -> usize {
+ let mut id = starting_id;
+
+ for item in menu.iter() {
+ match item {
+ menu::Entry::Item { title, hotkey, .. } => {
+ converted.add_item(id, title, hotkey.map(self::hotkey));
+
+ id += 1;
+ }
+ menu::Entry::Dropdown { title, submenu } => {
+ let mut converted_submenu = winit::window::Menu::new();
+ let n_children =
+ menu_i(&mut converted_submenu, id, submenu);
+
+ converted.add_dropdown(title, converted_submenu);
+
+ id += n_children;
+ }
+ menu::Entry::Separator => {
+ converted.add_separator();
+ }
+ }
+ }
+
+ id - starting_id
+ }
+
+ let mut converted = winit::window::Menu::default();
+ let _ = menu_i(&mut converted, 0, menu);
+
+ converted
+}
+
+/// Given a [`Menu`] and an identifier of a [`menu::Entry`], it returns the
+/// `Message` that should be produced when that entry is activated.
+pub fn menu_message<Message>(menu: &Menu<Message>, id: u32) -> Option<Message>
+where
+ Message: Clone,
+{
+ fn find_message<Message>(
+ target: u32,
+ starting_id: u32,
+ menu: &Menu<Message>,
+ ) -> Result<Message, u32>
+ where
+ Message: Clone,
+ {
+ let mut id = starting_id;
+
+ for entry in menu.iter() {
+ match entry {
+ menu::Entry::Item { on_activation, .. } => {
+ if id == target {
+ return Ok(on_activation.clone());
+ }
+
+ id += 1;
+ }
+ menu::Entry::Dropdown { submenu, .. } => {
+ match find_message(target, id, submenu) {
+ Ok(message) => {
+ return Ok(message);
+ }
+ Err(n_children) => {
+ id += n_children;
+ }
+ }
+ }
+ menu::Entry::Separator => {}
+ }
+ }
+
+ Err(id - starting_id)
+ }
+
+ find_message(id, 0, menu).ok()
+}
+
/// Converts a `MouseCursor` from [`iced_native`] to a [`winit`] cursor icon.
///
/// [`winit`]: https://github.com/rust-windowing/winit
@@ -181,13 +348,17 @@ pub fn mouse_button(mouse_button: winit::event::MouseButton) -> mouse::Button {
///
/// [`winit`]: https://github.com/rust-windowing/winit
/// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native
-pub fn modifiers(modifiers: winit::event::ModifiersState) -> Modifiers {
- Modifiers {
- shift: modifiers.shift(),
- control: modifiers.ctrl(),
- alt: modifiers.alt(),
- logo: modifiers.logo(),
- }
+pub fn modifiers(
+ modifiers: winit::event::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
}
/// Converts a physical cursor position to a logical `Point`.
@@ -200,11 +371,223 @@ pub fn cursor_position(
Point::new(logical_position.x, logical_position.y)
}
+/// Converts a `Touch` from [`winit`] to an [`iced_native`] touch event.
+///
+/// [`winit`]: https://github.com/rust-windowing/winit
+/// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native
+pub fn touch_event(
+ touch: winit::event::Touch,
+ scale_factor: f64,
+) -> touch::Event {
+ let id = touch::Finger(touch.id);
+ let position = {
+ let location = touch.location.to_logical::<f64>(scale_factor);
+
+ Point::new(location.x as f32, location.y as f32)
+ };
+
+ match touch.phase {
+ winit::event::TouchPhase::Started => {
+ touch::Event::FingerPressed { id, position }
+ }
+ winit::event::TouchPhase::Moved => {
+ touch::Event::FingerMoved { id, position }
+ }
+ winit::event::TouchPhase::Ended => {
+ touch::Event::FingerLifted { id, position }
+ }
+ winit::event::TouchPhase::Cancelled => {
+ touch::Event::FingerLost { id, position }
+ }
+ }
+}
+
+/// Converts a `KeyCode` from [`iced_native`] to an [`winit`] key code.
+///
+/// [`winit`]: https://github.com/rust-windowing/winit
+/// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native
+fn to_virtual_keycode(
+ keycode: keyboard::KeyCode,
+) -> winit::event::VirtualKeyCode {
+ use keyboard::KeyCode;
+ use winit::event::VirtualKeyCode;
+
+ match keycode {
+ KeyCode::Key1 => VirtualKeyCode::Key1,
+ KeyCode::Key2 => VirtualKeyCode::Key2,
+ KeyCode::Key3 => VirtualKeyCode::Key3,
+ KeyCode::Key4 => VirtualKeyCode::Key4,
+ KeyCode::Key5 => VirtualKeyCode::Key5,
+ KeyCode::Key6 => VirtualKeyCode::Key6,
+ KeyCode::Key7 => VirtualKeyCode::Key7,
+ KeyCode::Key8 => VirtualKeyCode::Key8,
+ KeyCode::Key9 => VirtualKeyCode::Key9,
+ KeyCode::Key0 => VirtualKeyCode::Key0,
+ KeyCode::A => VirtualKeyCode::A,
+ KeyCode::B => VirtualKeyCode::B,
+ KeyCode::C => VirtualKeyCode::C,
+ KeyCode::D => VirtualKeyCode::D,
+ KeyCode::E => VirtualKeyCode::E,
+ KeyCode::F => VirtualKeyCode::F,
+ KeyCode::G => VirtualKeyCode::G,
+ KeyCode::H => VirtualKeyCode::H,
+ KeyCode::I => VirtualKeyCode::I,
+ KeyCode::J => VirtualKeyCode::J,
+ KeyCode::K => VirtualKeyCode::K,
+ KeyCode::L => VirtualKeyCode::L,
+ KeyCode::M => VirtualKeyCode::M,
+ KeyCode::N => VirtualKeyCode::N,
+ KeyCode::O => VirtualKeyCode::O,
+ KeyCode::P => VirtualKeyCode::P,
+ KeyCode::Q => VirtualKeyCode::Q,
+ KeyCode::R => VirtualKeyCode::R,
+ KeyCode::S => VirtualKeyCode::S,
+ KeyCode::T => VirtualKeyCode::T,
+ KeyCode::U => VirtualKeyCode::U,
+ KeyCode::V => VirtualKeyCode::V,
+ KeyCode::W => VirtualKeyCode::W,
+ KeyCode::X => VirtualKeyCode::X,
+ KeyCode::Y => VirtualKeyCode::Y,
+ KeyCode::Z => VirtualKeyCode::Z,
+ KeyCode::Escape => VirtualKeyCode::Escape,
+ KeyCode::F1 => VirtualKeyCode::F1,
+ KeyCode::F2 => VirtualKeyCode::F2,
+ KeyCode::F3 => VirtualKeyCode::F3,
+ KeyCode::F4 => VirtualKeyCode::F4,
+ KeyCode::F5 => VirtualKeyCode::F5,
+ KeyCode::F6 => VirtualKeyCode::F6,
+ KeyCode::F7 => VirtualKeyCode::F7,
+ KeyCode::F8 => VirtualKeyCode::F8,
+ KeyCode::F9 => VirtualKeyCode::F9,
+ KeyCode::F10 => VirtualKeyCode::F10,
+ KeyCode::F11 => VirtualKeyCode::F11,
+ KeyCode::F12 => VirtualKeyCode::F12,
+ KeyCode::F13 => VirtualKeyCode::F13,
+ KeyCode::F14 => VirtualKeyCode::F14,
+ KeyCode::F15 => VirtualKeyCode::F15,
+ KeyCode::F16 => VirtualKeyCode::F16,
+ KeyCode::F17 => VirtualKeyCode::F17,
+ KeyCode::F18 => VirtualKeyCode::F18,
+ KeyCode::F19 => VirtualKeyCode::F19,
+ KeyCode::F20 => VirtualKeyCode::F20,
+ KeyCode::F21 => VirtualKeyCode::F21,
+ KeyCode::F22 => VirtualKeyCode::F22,
+ KeyCode::F23 => VirtualKeyCode::F23,
+ KeyCode::F24 => VirtualKeyCode::F24,
+ KeyCode::Snapshot => VirtualKeyCode::Snapshot,
+ KeyCode::Scroll => VirtualKeyCode::Scroll,
+ KeyCode::Pause => VirtualKeyCode::Pause,
+ KeyCode::Insert => VirtualKeyCode::Insert,
+ KeyCode::Home => VirtualKeyCode::Home,
+ KeyCode::Delete => VirtualKeyCode::Delete,
+ KeyCode::End => VirtualKeyCode::End,
+ KeyCode::PageDown => VirtualKeyCode::PageDown,
+ KeyCode::PageUp => VirtualKeyCode::PageUp,
+ KeyCode::Left => VirtualKeyCode::Left,
+ KeyCode::Up => VirtualKeyCode::Up,
+ KeyCode::Right => VirtualKeyCode::Right,
+ KeyCode::Down => VirtualKeyCode::Down,
+ KeyCode::Backspace => VirtualKeyCode::Back,
+ KeyCode::Enter => VirtualKeyCode::Return,
+ KeyCode::Space => VirtualKeyCode::Space,
+ KeyCode::Compose => VirtualKeyCode::Compose,
+ KeyCode::Caret => VirtualKeyCode::Caret,
+ KeyCode::Numlock => VirtualKeyCode::Numlock,
+ KeyCode::Numpad0 => VirtualKeyCode::Numpad0,
+ KeyCode::Numpad1 => VirtualKeyCode::Numpad1,
+ KeyCode::Numpad2 => VirtualKeyCode::Numpad2,
+ KeyCode::Numpad3 => VirtualKeyCode::Numpad3,
+ KeyCode::Numpad4 => VirtualKeyCode::Numpad4,
+ KeyCode::Numpad5 => VirtualKeyCode::Numpad5,
+ KeyCode::Numpad6 => VirtualKeyCode::Numpad6,
+ KeyCode::Numpad7 => VirtualKeyCode::Numpad7,
+ KeyCode::Numpad8 => VirtualKeyCode::Numpad8,
+ KeyCode::Numpad9 => VirtualKeyCode::Numpad9,
+ KeyCode::AbntC1 => VirtualKeyCode::AbntC1,
+ KeyCode::AbntC2 => VirtualKeyCode::AbntC2,
+ KeyCode::NumpadAdd => VirtualKeyCode::NumpadAdd,
+ KeyCode::Plus => VirtualKeyCode::Plus,
+ KeyCode::Apostrophe => VirtualKeyCode::Apostrophe,
+ KeyCode::Apps => VirtualKeyCode::Apps,
+ KeyCode::At => VirtualKeyCode::At,
+ KeyCode::Ax => VirtualKeyCode::Ax,
+ KeyCode::Backslash => VirtualKeyCode::Backslash,
+ KeyCode::Calculator => VirtualKeyCode::Calculator,
+ KeyCode::Capital => VirtualKeyCode::Capital,
+ KeyCode::Colon => VirtualKeyCode::Colon,
+ KeyCode::Comma => VirtualKeyCode::Comma,
+ KeyCode::Convert => VirtualKeyCode::Convert,
+ KeyCode::NumpadDecimal => VirtualKeyCode::NumpadDecimal,
+ KeyCode::NumpadDivide => VirtualKeyCode::NumpadDivide,
+ KeyCode::Equals => VirtualKeyCode::Equals,
+ KeyCode::Grave => VirtualKeyCode::Grave,
+ KeyCode::Kana => VirtualKeyCode::Kana,
+ KeyCode::Kanji => VirtualKeyCode::Kanji,
+ KeyCode::LAlt => VirtualKeyCode::LAlt,
+ KeyCode::LBracket => VirtualKeyCode::LBracket,
+ KeyCode::LControl => VirtualKeyCode::LControl,
+ KeyCode::LShift => VirtualKeyCode::LShift,
+ KeyCode::LWin => VirtualKeyCode::LWin,
+ KeyCode::Mail => VirtualKeyCode::Mail,
+ KeyCode::MediaSelect => VirtualKeyCode::MediaSelect,
+ KeyCode::MediaStop => VirtualKeyCode::MediaStop,
+ KeyCode::Minus => VirtualKeyCode::Minus,
+ KeyCode::NumpadMultiply => VirtualKeyCode::NumpadMultiply,
+ KeyCode::Mute => VirtualKeyCode::Mute,
+ KeyCode::MyComputer => VirtualKeyCode::MyComputer,
+ KeyCode::NavigateForward => VirtualKeyCode::NavigateForward,
+ KeyCode::NavigateBackward => VirtualKeyCode::NavigateBackward,
+ KeyCode::NextTrack => VirtualKeyCode::NextTrack,
+ KeyCode::NoConvert => VirtualKeyCode::NoConvert,
+ KeyCode::NumpadComma => VirtualKeyCode::NumpadComma,
+ KeyCode::NumpadEnter => VirtualKeyCode::NumpadEnter,
+ KeyCode::NumpadEquals => VirtualKeyCode::NumpadEquals,
+ KeyCode::OEM102 => VirtualKeyCode::OEM102,
+ KeyCode::Period => VirtualKeyCode::Period,
+ KeyCode::PlayPause => VirtualKeyCode::PlayPause,
+ KeyCode::Power => VirtualKeyCode::Power,
+ KeyCode::PrevTrack => VirtualKeyCode::PrevTrack,
+ KeyCode::RAlt => VirtualKeyCode::RAlt,
+ KeyCode::RBracket => VirtualKeyCode::RBracket,
+ KeyCode::RControl => VirtualKeyCode::RControl,
+ KeyCode::RShift => VirtualKeyCode::RShift,
+ KeyCode::RWin => VirtualKeyCode::RWin,
+ KeyCode::Semicolon => VirtualKeyCode::Semicolon,
+ KeyCode::Slash => VirtualKeyCode::Slash,
+ KeyCode::Sleep => VirtualKeyCode::Sleep,
+ KeyCode::Stop => VirtualKeyCode::Stop,
+ KeyCode::NumpadSubtract => VirtualKeyCode::NumpadSubtract,
+ KeyCode::Sysrq => VirtualKeyCode::Sysrq,
+ KeyCode::Tab => VirtualKeyCode::Tab,
+ KeyCode::Underline => VirtualKeyCode::Underline,
+ KeyCode::Unlabeled => VirtualKeyCode::Unlabeled,
+ KeyCode::VolumeDown => VirtualKeyCode::VolumeDown,
+ KeyCode::VolumeUp => VirtualKeyCode::VolumeUp,
+ KeyCode::Wake => VirtualKeyCode::Wake,
+ KeyCode::WebBack => VirtualKeyCode::WebBack,
+ KeyCode::WebFavorites => VirtualKeyCode::WebFavorites,
+ KeyCode::WebForward => VirtualKeyCode::WebForward,
+ KeyCode::WebHome => VirtualKeyCode::WebHome,
+ KeyCode::WebRefresh => VirtualKeyCode::WebRefresh,
+ KeyCode::WebSearch => VirtualKeyCode::WebSearch,
+ KeyCode::WebStop => VirtualKeyCode::WebStop,
+ KeyCode::Yen => VirtualKeyCode::Yen,
+ KeyCode::Copy => VirtualKeyCode::Copy,
+ KeyCode::Paste => VirtualKeyCode::Paste,
+ KeyCode::Cut => VirtualKeyCode::Cut,
+ KeyCode::Asterisk => VirtualKeyCode::Asterisk,
+ }
+}
+
/// Converts a `VirtualKeyCode` from [`winit`] to an [`iced_native`] key code.
///
/// [`winit`]: https://github.com/rust-windowing/winit
/// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native
-pub fn key_code(virtual_keycode: winit::event::VirtualKeyCode) -> KeyCode {
+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,
diff --git a/winit/src/lib.rs b/winit/src/lib.rs
index c9f324dd..1707846a 100644
--- a/winit/src/lib.rs
+++ b/winit/src/lib.rs
@@ -31,12 +31,14 @@ pub mod settings;
mod clipboard;
mod error;
mod mode;
+mod position;
mod proxy;
pub use application::Application;
pub use clipboard::Clipboard;
pub use error::Error;
pub use mode::Mode;
+pub use position::Position;
pub use proxy::Proxy;
pub use settings::Settings;
diff --git a/winit/src/mode.rs b/winit/src/mode.rs
index 37464711..fdce8e23 100644
--- a/winit/src/mode.rs
+++ b/winit/src/mode.rs
@@ -6,4 +6,7 @@ pub enum Mode {
/// The application takes the whole screen of its current monitor.
Fullscreen,
+
+ /// The application is hidden
+ Hidden,
}
diff --git a/winit/src/position.rs b/winit/src/position.rs
new file mode 100644
index 00000000..c260c29e
--- /dev/null
+++ b/winit/src/position.rs
@@ -0,0 +1,22 @@
+/// 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
+ }
+}
diff --git a/winit/src/settings.rs b/winit/src/settings.rs
index 2e8715cd..743f79bc 100644
--- a/winit/src/settings.rs
+++ b/winit/src/settings.rs
@@ -9,7 +9,7 @@ mod platform;
pub use platform::PlatformSpecific;
use crate::conversion;
-use crate::Mode;
+use crate::{Mode, Position};
use winit::monitor::MonitorHandle;
use winit::window::WindowBuilder;
@@ -23,6 +23,10 @@ pub struct Settings<Flags> {
///
/// [`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).
+ pub exit_on_close_request: bool,
}
/// The window settings of an application.
@@ -31,6 +35,9 @@ 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)>,
@@ -76,7 +83,15 @@ impl Window {
.with_transparent(self.transparent)
.with_window_icon(self.icon)
.with_always_on_top(self.always_on_top)
- .with_fullscreen(conversion::fullscreen(primary_monitor, mode));
+ .with_visible(conversion::visible(mode));
+
+ 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
@@ -95,8 +110,13 @@ impl Window {
if let Some(parent) = self.platform_specific.parent {
window_builder = window_builder.with_parent_window(parent);
}
+ window_builder = window_builder
+ .with_drag_and_drop(self.platform_specific.drag_and_drop);
}
+ window_builder = window_builder
+ .with_fullscreen(conversion::fullscreen(primary_monitor, mode));
+
window_builder
}
}
@@ -105,6 +125,7 @@ impl Default for Window {
fn default() -> Window {
Window {
size: (1024, 768),
+ position: Position::default(),
min_size: None,
max_size: None,
resizable: true,
diff --git a/winit/src/settings/windows.rs b/winit/src/settings/windows.rs
index 76b8d067..fc26acd7 100644
--- a/winit/src/settings/windows.rs
+++ b/winit/src/settings/windows.rs
@@ -2,8 +2,20 @@
//! Platform specific settings for Windows.
/// The platform specific window settings of an application.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PlatformSpecific {
- /// Parent Window
+ /// Parent window
pub parent: Option<winapi::shared::windef::HWND>,
+
+ /// Drag and drop support
+ pub drag_and_drop: bool,
+}
+
+impl Default for PlatformSpecific {
+ fn default() -> Self {
+ Self {
+ parent: None,
+ drag_and_drop: true,
+ }
+ }
}