summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/format.yml12
-rw-r--r--.github/workflows/test.yml4
-rw-r--r--.gitignore2
-rw-r--r--CHANGELOG.md103
-rw-r--r--Cargo.toml47
-rw-r--r--ECOSYSTEM.md95
-rw-r--r--README.md34
-rw-r--r--ROADMAP.md23
-rw-r--r--core/CHANGELOG.md19
-rw-r--r--core/Cargo.toml6
-rw-r--r--core/README.md6
-rw-r--r--core/src/background.rs6
-rw-r--r--core/src/color.rs139
-rw-r--r--core/src/font.rs6
-rw-r--r--core/src/keyboard.rs8
-rw-r--r--core/src/keyboard/event.rs (renamed from native/src/input/keyboard/event.rs)22
-rw-r--r--core/src/keyboard/key_code.rs (renamed from native/src/input/keyboard/key_code.rs)37
-rw-r--r--core/src/keyboard/modifiers.rs45
-rw-r--r--core/src/length.rs2
-rw-r--r--core/src/lib.rs6
-rw-r--r--core/src/mouse.rs (renamed from native/src/input/mouse.rs)4
-rw-r--r--core/src/mouse/button.rs (renamed from native/src/input/mouse/button.rs)0
-rw-r--r--core/src/mouse/event.rs (renamed from native/src/input/mouse/event.rs)14
-rw-r--r--core/src/mouse/interaction.rs20
-rw-r--r--core/src/point.rs30
-rw-r--r--core/src/rectangle.rs92
-rw-r--r--core/src/size.rs51
-rw-r--r--core/src/vector.rs32
-rw-r--r--docs/graphs/core.dot13
-rw-r--r--docs/graphs/core.pngbin13172 -> 0 bytes
-rw-r--r--docs/graphs/ecosystem.dot56
-rw-r--r--docs/graphs/ecosystem.pngbin44705 -> 127113 bytes
-rw-r--r--docs/graphs/foundations.pngbin0 -> 18626 bytes
-rwxr-xr-xdocs/graphs/generate.sh6
-rw-r--r--docs/graphs/iced.dot46
-rw-r--r--docs/graphs/iced.pngbin26302 -> 100093 bytes
-rw-r--r--docs/graphs/native.dot41
-rw-r--r--docs/graphs/native.pngbin24980 -> 59288 bytes
-rw-r--r--docs/graphs/web.dot12
-rw-r--r--docs/graphs/web.pngbin11717 -> 0 bytes
-rw-r--r--docs/graphs/wgpu.dot31
-rw-r--r--docs/graphs/wgpu.pngbin16570 -> 0 bytes
-rw-r--r--docs/graphs/winit.dot31
-rw-r--r--docs/graphs/winit.pngbin15892 -> 0 bytes
-rw-r--r--examples/README.md28
-rw-r--r--examples/bezier_tool/Cargo.toml5
-rw-r--r--examples/bezier_tool/README.md3
-rw-r--r--examples/bezier_tool/src/main.rs477
-rw-r--r--examples/clock/Cargo.toml7
-rw-r--r--examples/clock/src/main.rs176
-rw-r--r--examples/color_palette/Cargo.toml10
-rw-r--r--examples/color_palette/README.md15
-rw-r--r--examples/color_palette/screenshot.pngbin0 -> 105201 bytes
-rw-r--r--examples/color_palette/src/main.rs462
-rw-r--r--examples/counter/src/main.rs2
-rw-r--r--examples/custom_widget/Cargo.toml2
-rw-r--r--examples/custom_widget/src/main.rs65
-rw-r--r--examples/download_progress/Cargo.toml12
-rw-r--r--examples/download_progress/README.md17
-rw-r--r--examples/download_progress/src/download.rs112
-rw-r--r--examples/download_progress/src/main.rs144
-rw-r--r--examples/events/src/main.rs5
-rw-r--r--examples/game_of_life/Cargo.toml12
-rw-r--r--examples/game_of_life/README.md22
-rw-r--r--examples/game_of_life/src/main.rs885
-rw-r--r--examples/game_of_life/src/preset.rs142
-rw-r--r--examples/game_of_life/src/style.rs188
-rw-r--r--examples/geometry/Cargo.toml2
-rw-r--r--examples/geometry/src/main.rs142
-rw-r--r--examples/integration/Cargo.toml2
-rw-r--r--examples/integration/src/controls.rs67
-rw-r--r--examples/integration/src/main.rs252
-rw-r--r--examples/integration/src/scene.rs113
-rw-r--r--examples/pane_grid/Cargo.toml10
-rw-r--r--examples/pane_grid/README.md28
-rw-r--r--examples/pane_grid/src/main.rs372
-rw-r--r--examples/pick_list/Cargo.toml9
-rw-r--r--examples/pick_list/README.md18
-rw-r--r--examples/pick_list/src/main.rs113
-rw-r--r--examples/pokedex/Cargo.toml2
-rw-r--r--examples/pokedex/src/main.rs9
-rw-r--r--examples/progress_bar/src/main.rs17
-rw-r--r--examples/qr_code/Cargo.toml9
-rw-r--r--examples/qr_code/README.md18
-rw-r--r--examples/qr_code/src/main.rs81
-rw-r--r--examples/scrollable/Cargo.toml9
-rw-r--r--examples/scrollable/README.md15
-rw-r--r--examples/scrollable/screenshot.pngbin0 -> 148253 bytes
-rw-r--r--examples/scrollable/src/main.rs184
-rw-r--r--examples/scrollable/src/style.rs190
-rw-r--r--examples/solar_system/Cargo.toml7
-rw-r--r--examples/solar_system/src/main.rs227
-rw-r--r--examples/stopwatch/Cargo.toml5
-rw-r--r--examples/stopwatch/src/main.rs51
-rw-r--r--examples/styling/src/main.rs122
-rw-r--r--examples/svg/Cargo.toml1
-rw-r--r--examples/svg/src/main.rs28
-rw-r--r--examples/todos/Cargo.toml4
-rw-r--r--examples/todos/src/main.rs15
-rw-r--r--examples/tour/Cargo.toml2
-rw-r--r--examples/tour/src/main.rs84
-rw-r--r--futures/Cargo.toml17
-rw-r--r--futures/src/command.rs139
-rw-r--r--futures/src/command/native.rs100
-rw-r--r--futures/src/command/web.rs101
-rw-r--r--futures/src/executor.rs12
-rw-r--r--futures/src/executor/async_std.rs1
-rw-r--r--futures/src/executor/thread_pool.rs1
-rw-r--r--futures/src/executor/tokio.rs4
-rw-r--r--futures/src/executor/tokio_old.rs21
-rw-r--r--futures/src/lib.rs39
-rw-r--r--futures/src/runtime.rs22
-rw-r--r--futures/src/subscription.rs133
-rw-r--r--futures/src/subscription/tracker.rs18
-rw-r--r--futures/src/time.rs74
-rw-r--r--glow/Cargo.toml37
-rw-r--r--glow/src/backend.rs226
-rw-r--r--glow/src/lib.rs41
-rw-r--r--glow/src/program.rs39
-rw-r--r--glow/src/quad.rs235
-rw-r--r--glow/src/settings.rs31
-rw-r--r--glow/src/shader/quad.frag70
-rw-r--r--glow/src/shader/quad.vert52
-rw-r--r--glow/src/shader/triangle.frag9
-rw-r--r--glow/src/shader/triangle.vert13
-rw-r--r--glow/src/text.rs156
-rw-r--r--glow/src/triangle.rs292
-rw-r--r--glow/src/widget.rs72
-rw-r--r--glow/src/widget/button.rs12
-rw-r--r--glow/src/widget/canvas.rs6
-rw-r--r--glow/src/widget/checkbox.rs9
-rw-r--r--glow/src/widget/container.rs10
-rw-r--r--glow/src/widget/pane_grid.rs31
-rw-r--r--glow/src/widget/pick_list.rs9
-rw-r--r--glow/src/widget/progress_bar.rs13
-rw-r--r--glow/src/widget/qr_code.rs2
-rw-r--r--glow/src/widget/radio.rs10
-rw-r--r--glow/src/widget/rule.rs10
-rw-r--r--glow/src/widget/scrollable.rs13
-rw-r--r--glow/src/widget/slider.rs13
-rw-r--r--glow/src/widget/text_input.rs12
-rw-r--r--glow/src/window.rs4
-rw-r--r--glow/src/window/compositor.rs76
-rw-r--r--glutin/Cargo.toml30
-rw-r--r--glutin/README.md29
-rw-r--r--glutin/src/application.rs297
-rw-r--r--glutin/src/lib.rs25
-rw-r--r--graphics/Cargo.toml52
-rw-r--r--graphics/fonts/Icons.ttf (renamed from wgpu/src/text/icons.ttf)bin4912 -> 5032 bytes
-rw-r--r--graphics/fonts/Lato-Regular.ttf (renamed from wgpu/fonts/Lato-Regular.ttf)bin75136 -> 75136 bytes
-rw-r--r--graphics/fonts/OFL.txt (renamed from wgpu/fonts/OFL.txt)0
-rw-r--r--graphics/src/antialiasing.rs24
-rw-r--r--graphics/src/backend.rs58
-rw-r--r--graphics/src/defaults.rs (renamed from wgpu/src/defaults.rs)0
-rw-r--r--graphics/src/error.rs7
-rw-r--r--graphics/src/font.rs35
-rw-r--r--graphics/src/font/source.rs (renamed from wgpu/src/text/font.rs)8
-rw-r--r--graphics/src/layer.rs311
-rw-r--r--graphics/src/lib.rs45
-rw-r--r--graphics/src/overlay.rs2
-rw-r--r--graphics/src/overlay/menu.rs117
-rw-r--r--graphics/src/primitive.rs (renamed from wgpu/src/primitive.rs)25
-rw-r--r--graphics/src/renderer.rs121
-rw-r--r--graphics/src/transformation.rs (renamed from wgpu/src/transformation.rs)13
-rw-r--r--graphics/src/triangle.rs25
-rw-r--r--graphics/src/viewport.rs56
-rw-r--r--graphics/src/widget.rs73
-rw-r--r--graphics/src/widget/button.rs (renamed from wgpu/src/renderer/widget/button.rs)37
-rw-r--r--graphics/src/widget/canvas.rs236
-rw-r--r--graphics/src/widget/canvas/cache.rs98
-rw-r--r--graphics/src/widget/canvas/cursor.rs64
-rw-r--r--graphics/src/widget/canvas/event.rs17
-rw-r--r--graphics/src/widget/canvas/fill.rs60
-rw-r--r--graphics/src/widget/canvas/frame.rs (renamed from wgpu/src/widget/canvas/frame.rs)153
-rw-r--r--graphics/src/widget/canvas/geometry.rs30
-rw-r--r--graphics/src/widget/canvas/path.rs (renamed from wgpu/src/widget/canvas/path.rs)29
-rw-r--r--graphics/src/widget/canvas/path/arc.rs (renamed from wgpu/src/widget/canvas/path/arc.rs)2
-rw-r--r--graphics/src/widget/canvas/path/builder.rs (renamed from wgpu/src/widget/canvas/path/builder.rs)44
-rw-r--r--graphics/src/widget/canvas/program.rs80
-rw-r--r--graphics/src/widget/canvas/stroke.rs (renamed from wgpu/src/widget/canvas/stroke.rs)22
-rw-r--r--graphics/src/widget/canvas/text.rs (renamed from wgpu/src/widget/canvas/text.rs)15
-rw-r--r--graphics/src/widget/checkbox.rs (renamed from wgpu/src/renderer/widget/checkbox.rs)37
-rw-r--r--graphics/src/widget/column.rs49
-rw-r--r--graphics/src/widget/container.rs78
-rw-r--r--graphics/src/widget/image.rs31
-rw-r--r--graphics/src/widget/pane_grid.rs252
-rw-r--r--graphics/src/widget/pick_list.rs97
-rw-r--r--graphics/src/widget/progress_bar.rs (renamed from wgpu/src/renderer/widget/progress_bar.rs)38
-rw-r--r--graphics/src/widget/qr_code.rs305
-rw-r--r--graphics/src/widget/radio.rs (renamed from wgpu/src/renderer/widget/radio.rs)48
-rw-r--r--graphics/src/widget/row.rs49
-rw-r--r--graphics/src/widget/rule.rs73
-rw-r--r--graphics/src/widget/scrollable.rs (renamed from wgpu/src/renderer/widget/scrollable.rs)83
-rw-r--r--graphics/src/widget/slider.rs (renamed from wgpu/src/renderer/widget/slider.rs)77
-rw-r--r--graphics/src/widget/space.rs15
-rw-r--r--graphics/src/widget/svg.rs (renamed from wgpu/src/renderer/widget/svg.rs)15
-rw-r--r--graphics/src/widget/text.rs74
-rw-r--r--graphics/src/widget/text_input.rs265
-rw-r--r--graphics/src/window.rs10
-rw-r--r--graphics/src/window/compositor.rs (renamed from native/src/window/backend.rs)32
-rw-r--r--graphics/src/window/gl_compositor.rs63
-rw-r--r--native/CHANGELOG.md38
-rw-r--r--native/Cargo.toml11
-rw-r--r--native/README.md6
-rw-r--r--native/src/clipboard.rs2
-rw-r--r--native/src/debug/basic.rs (renamed from winit/src/debug/basic.rs)13
-rw-r--r--native/src/debug/null.rs (renamed from winit/src/debug/null.rs)1
-rw-r--r--native/src/element.rs97
-rw-r--r--native/src/event.rs42
-rw-r--r--native/src/input.rs9
-rw-r--r--native/src/input/button_state.rs9
-rw-r--r--native/src/input/keyboard.rs8
-rw-r--r--native/src/input/keyboard/modifiers_state.rs15
-rw-r--r--native/src/keyboard.rs2
-rw-r--r--native/src/layout.rs18
-rw-r--r--native/src/layout/debugger.rs10
-rw-r--r--native/src/layout/flex.rs7
-rw-r--r--native/src/layout/limits.rs33
-rw-r--r--native/src/layout/node.rs17
-rw-r--r--native/src/lib.rs38
-rw-r--r--native/src/mouse.rs6
-rw-r--r--native/src/mouse/click.rs71
-rw-r--r--native/src/mouse_cursor.rs30
-rw-r--r--native/src/overlay.rs76
-rw-r--r--native/src/overlay/element.rs156
-rw-r--r--native/src/overlay/menu.rs428
-rw-r--r--native/src/program.rs30
-rw-r--r--native/src/program/state.rs180
-rw-r--r--native/src/renderer.rs27
-rw-r--r--native/src/renderer/null.rs97
-rw-r--r--native/src/runtime.rs16
-rw-r--r--native/src/subscription.rs49
-rw-r--r--native/src/subscription/events.rs32
-rw-r--r--native/src/touch.rs (renamed from native/src/input/touch.rs)1
-rw-r--r--native/src/user_interface.rs329
-rw-r--r--native/src/widget.rs67
-rw-r--r--native/src/widget/button.rs83
-rw-r--r--native/src/widget/checkbox.rs79
-rw-r--r--native/src/widget/column.rs74
-rw-r--r--native/src/widget/container.rs68
-rw-r--r--native/src/widget/image.rs53
-rw-r--r--native/src/widget/pane_grid.rs639
-rw-r--r--native/src/widget/pane_grid/axis.rs238
-rw-r--r--native/src/widget/pane_grid/configuration.rs26
-rw-r--r--native/src/widget/pane_grid/content.rs213
-rw-r--r--native/src/widget/pane_grid/direction.rs12
-rw-r--r--native/src/widget/pane_grid/node.rs258
-rw-r--r--native/src/widget/pane_grid/pane.rs5
-rw-r--r--native/src/widget/pane_grid/split.rs5
-rw-r--r--native/src/widget/pane_grid/state.rs301
-rw-r--r--native/src/widget/pane_grid/title_bar.rs253
-rw-r--r--native/src/widget/pick_list.rs343
-rw-r--r--native/src/widget/progress_bar.rs23
-rw-r--r--native/src/widget/radio.rs122
-rw-r--r--native/src/widget/row.rs79
-rw-r--r--native/src/widget/rule.rs116
-rw-r--r--native/src/widget/scrollable.rs245
-rw-r--r--native/src/widget/slider.rs182
-rw-r--r--native/src/widget/space.rs16
-rw-r--r--native/src/widget/svg.rs98
-rw-r--r--native/src/widget/text.rs97
-rw-r--r--native/src/widget/text_input.rs837
-rw-r--r--native/src/widget/text_input/cursor.rs186
-rw-r--r--native/src/widget/text_input/editor.rs77
-rw-r--r--native/src/widget/text_input/value.rs125
-rw-r--r--native/src/window.rs2
-rw-r--r--rustfmt.toml2
-rw-r--r--src/application.rs251
-rw-r--r--src/element.rs2
-rw-r--r--src/error.rs34
-rw-r--r--src/executor.rs30
-rw-r--r--src/keyboard.rs2
-rw-r--r--src/lib.rs62
-rw-r--r--src/mouse.rs2
-rw-r--r--src/result.rs6
-rw-r--r--src/sandbox.rs171
-rw-r--r--src/settings.rs64
-rw-r--r--src/time.rs12
-rw-r--r--src/widget.rs53
-rw-r--r--src/window.rs3
-rw-r--r--src/window/icon.rs132
-rw-r--r--src/window/settings.rs43
-rw-r--r--style/Cargo.toml4
-rw-r--r--style/src/button.rs12
-rw-r--r--style/src/checkbox.rs8
-rw-r--r--style/src/container.rs12
-rw-r--r--style/src/lib.rs7
-rw-r--r--style/src/menu.rs25
-rw-r--r--style/src/pick_list.rs70
-rw-r--r--style/src/progress_bar.rs4
-rw-r--r--style/src/radio.rs4
-rw-r--r--style/src/rule.rs114
-rw-r--r--style/src/scrollable.rs16
-rw-r--r--style/src/slider.rs10
-rw-r--r--style/src/text_input.rs18
-rw-r--r--web/CHANGELOG.md19
-rw-r--r--web/Cargo.toml16
-rw-r--r--web/README.md7
-rw-r--r--web/src/bus.rs6
-rw-r--r--web/src/css.rs29
-rw-r--r--web/src/element.rs9
-rw-r--r--web/src/lib.rs90
-rw-r--r--web/src/subscription.rs3
-rw-r--r--web/src/widget.rs6
-rw-r--r--web/src/widget/button.rs38
-rw-r--r--web/src/widget/checkbox.rs37
-rw-r--r--web/src/widget/column.rs20
-rw-r--r--web/src/widget/container.rs29
-rw-r--r--web/src/widget/image.rs41
-rw-r--r--web/src/widget/progress_bar.rs8
-rw-r--r--web/src/widget/radio.rs55
-rw-r--r--web/src/widget/row.rs21
-rw-r--r--web/src/widget/scrollable.rs23
-rw-r--r--web/src/widget/slider.rs82
-rw-r--r--web/src/widget/space.rs6
-rw-r--r--web/src/widget/text.rs30
-rw-r--r--web/src/widget/text_input.rs80
-rw-r--r--wgpu/CHANGELOG.md14
-rw-r--r--wgpu/Cargo.toml42
-rw-r--r--wgpu/README.md9
-rw-r--r--wgpu/src/backend.rs288
-rw-r--r--wgpu/src/image.rs326
-rw-r--r--wgpu/src/image/atlas.rs92
-rw-r--r--wgpu/src/image/raster.rs4
-rw-r--r--wgpu/src/image/vector.rs18
-rw-r--r--wgpu/src/lib.rs34
-rw-r--r--wgpu/src/quad.rs261
-rw-r--r--wgpu/src/renderer.rs509
-rw-r--r--wgpu/src/renderer/widget.rs18
-rw-r--r--wgpu/src/renderer/widget/column.rs34
-rw-r--r--wgpu/src/renderer/widget/container.rs49
-rw-r--r--wgpu/src/renderer/widget/image.rs22
-rw-r--r--wgpu/src/renderer/widget/row.rs34
-rw-r--r--wgpu/src/renderer/widget/space.rs8
-rw-r--r--wgpu/src/renderer/widget/text.rs47
-rw-r--r--wgpu/src/renderer/widget/text_input.rs182
-rw-r--r--wgpu/src/settings.rs49
-rw-r--r--wgpu/src/shader/blit.vert6
-rw-r--r--wgpu/src/shader/blit.vert.spvbin1384 -> 1384 bytes
-rw-r--r--wgpu/src/shader/quad.vert5
-rw-r--r--wgpu/src/shader/quad.vert.spvbin3372 -> 3604 bytes
-rw-r--r--wgpu/src/target.rs14
-rw-r--r--wgpu/src/text.rs116
-rw-r--r--wgpu/src/triangle.rs369
-rw-r--r--wgpu/src/triangle/msaa.rs77
-rw-r--r--wgpu/src/viewport.rs29
-rw-r--r--wgpu/src/widget.rs31
-rw-r--r--wgpu/src/widget/button.rs5
-rw-r--r--wgpu/src/widget/canvas.rs147
-rw-r--r--wgpu/src/widget/canvas/drawable.rs12
-rw-r--r--wgpu/src/widget/canvas/fill.rs14
-rw-r--r--wgpu/src/widget/canvas/layer.rs25
-rw-r--r--wgpu/src/widget/canvas/layer/cache.rs99
-rw-r--r--wgpu/src/widget/checkbox.rs2
-rw-r--r--wgpu/src/widget/container.rs2
-rw-r--r--wgpu/src/widget/pane_grid.rs31
-rw-r--r--wgpu/src/widget/pick_list.rs9
-rw-r--r--wgpu/src/widget/progress_bar.rs10
-rw-r--r--wgpu/src/widget/qr_code.rs2
-rw-r--r--wgpu/src/widget/radio.rs2
-rw-r--r--wgpu/src/widget/rule.rs10
-rw-r--r--wgpu/src/widget/scrollable.rs2
-rw-r--r--wgpu/src/widget/slider.rs7
-rw-r--r--wgpu/src/widget/text_input.rs5
-rw-r--r--wgpu/src/window.rs6
-rw-r--r--wgpu/src/window/backend.rs113
-rw-r--r--wgpu/src/window/compositor.rs177
-rw-r--r--wgpu/src/window/swap_chain.rs57
-rw-r--r--winit/CHANGELOG.md14
-rw-r--r--winit/Cargo.toml17
-rw-r--r--winit/README.md8
-rw-r--r--winit/src/application.rs602
-rw-r--r--winit/src/application/state.rs202
-rw-r--r--winit/src/clipboard.rs2
-rw-r--r--winit/src/conversion.rs151
-rw-r--r--winit/src/error.rs27
-rw-r--r--winit/src/lib.rs23
-rw-r--r--winit/src/proxy.rs3
-rw-r--r--winit/src/settings.rs118
-rw-r--r--winit/src/settings/mod.rs53
-rw-r--r--winit/src/size.rs30
381 files changed, 18855 insertions, 6689 deletions
diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml
new file mode 100644
index 00000000..92caff79
--- /dev/null
+++ b/.github/workflows/format.yml
@@ -0,0 +1,12 @@
+name: Format
+on: [push, pull_request]
+jobs:
+ all:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: hecrj/setup-rust-action@v1
+ with:
+ components: rustfmt
+ - uses: actions/checkout@master
+ - name: Check format
+ run: cargo fmt --all -- --check
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 8c5ded3a..9e73d3d3 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -27,3 +27,7 @@ jobs:
- uses: actions/checkout@master
- name: Run checks
run: cargo check --package iced --target wasm32-unknown-unknown
+ - name: Check compilation of `tour` example
+ run: cargo build --package tour --target wasm32-unknown-unknown
+ - name: Check compilation of `pokedex` example
+ run: cargo build --package pokedex --target wasm32-unknown-unknown
diff --git a/.gitignore b/.gitignore
index eee98b1e..3872ed2f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
-/target
+target/
pkg/
**/*.rs.bk
Cargo.lock
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 62fb335a..ee650337 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,105 @@ 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]
+### Added
+- `"system_font"` feature gates reading system fonts. [#370]
+
+[#370]: https://github.com/hecrj/iced/pull/370
+
+## [0.1.1] - 2020-04-15
+### Added
+- `Settings::with_flags` to easily initialize some default settings with flags. [#266]
+- `Default` implementation for `canvas::layer::Cache`. [#267]
+- `Ctrl + Del` support for `TextInput`. [#268]
+- Helper methods in `canvas::Path` to easily draw lines, rectangles, and circles. [#293]
+- `From<Color>` implementation for `canvas::Fill`. [#293]
+- `From<String>` implementation for `canvas::Text`. [#293]
+- `From<&str>` implementation for `canvas::Text`. [#293]
+
+### Changed
+- `new` method of `Radio` and `Checkbox` now take a generic `Into<String>` for the label. [#260]
+- `Frame::fill` now takes a generic `Into<canvas::Fill>`. [#293]
+- `Frame::stroke` now takes a generic `Into<canvas::Stroke>`. [#293]
+- `Frame::fill_text` now takes a generic `Into<canvas::Text>`. [#293]
+
+### Fixed
+- Feature flags not being referenced in documentation. [#259]
+- Crash in some graphics drivers when displaying an empty `Canvas`. [#278]
+- Text measuring when spaces where present at the beginning of a `TextInput` value. [#279]
+- `TextInput` producing a `Clip` primitive when unnecessary. [#279]
+- Alignment of `Text` primitive in `iced_wgpu`. [#281]
+- `CursorEntered` and `CursorLeft` not being generated. [#289]
+
+### Removed
+- Unnecessary `'static` lifetimes in `Renderer` bounds. [#290]
+
+[#259]: https://github.com/hecrj/iced/pull/259
+[#260]: https://github.com/hecrj/iced/pull/260
+[#266]: https://github.com/hecrj/iced/pull/266
+[#267]: https://github.com/hecrj/iced/pull/267
+[#268]: https://github.com/hecrj/iced/pull/268
+[#278]: https://github.com/hecrj/iced/pull/278
+[#279]: https://github.com/hecrj/iced/pull/279
+[#281]: https://github.com/hecrj/iced/pull/281
+[#289]: https://github.com/hecrj/iced/pull/289
+[#290]: https://github.com/hecrj/iced/pull/290
+[#293]: https://github.com/hecrj/iced/pull/293
+
+
+## [0.1.0] - 2020-04-02
+### Added
+- __[Event subscriptions]__ (#122)
+ A declarative way to listen to external events asynchronously by leveraging [streams].
+
+- __[Custom styling]__ (#146)
+ A simple, trait-based approach for customizing the appearance of different widgets.
+
+- __[`Canvas` widget]__ (#193)
+ A widget for drawing 2D graphics with an interface inspired by the [Web Canvas API] and powered by [`lyon`].
+
+- __[`PaneGrid` widget]__ (#224)
+ A widget that dynamically organizes layout by splitting panes that can be resized and drag and dropped.
+
+- __[`Svg` widget]__ (#111)
+ A widget that renders vector graphics on top of [`resvg`] and [`raqote`]. Thanks to @Maldela!
+
+- __[`ProgressBar` widget]__ (#141)
+ A widget to notify progress of asynchronous tasks to your users. Thanks to @Songtronix!
+
+- __[Configurable futures executor]__ (#164)
+ Support for plugging [`tokio`], [`async-std`], [`wasm-bindgen-futures`], or your own custom futures executor to an application.
+
+- __[Compatibility with existing `wgpu` projects]__ (#183)
+ A bunch of improvements to the flexibility of [`iced_wgpu`] to allow integration in existing codebases.
+
+- __[Text selection for `TextInput`]__ (#202)
+ Thanks to @FabianLars and @Finnerale!
+
+- __[Texture atlas for `iced_wgpu`]__ (#154)
+ An atlas on top of [`guillotiere`] for batching draw calls. Thanks to @Maldela!
+
+[Event subscriptions]: https://github.com/hecrj/iced/pull/122
+[Custom styling]: https://github.com/hecrj/iced/pull/146
+[`Canvas` widget]: https://github.com/hecrj/iced/pull/193
+[`PaneGrid` widget]: https://github.com/hecrj/iced/pull/224
+[`Svg` widget]: https://github.com/hecrj/iced/pull/111
+[`ProgressBar` widget]: https://github.com/hecrj/iced/pull/141
+[Configurable futures executor]: https://github.com/hecrj/iced/pull/164
+[Compatibility with existing `wgpu` projects]: https://github.com/hecrj/iced/pull/183
+[Clipboard access]: https://github.com/hecrj/iced/pull/132
+[Texture atlas for `iced_wgpu`]: https://github.com/hecrj/iced/pull/154
+[Text selection for `TextInput`]: https://github.com/hecrj/iced/pull/202
+[`lyon`]: https://github.com/nical/lyon
+[`guillotiere`]: https://github.com/nical/guillotiere
+[Web Canvas API]: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API
+[streams]: https://docs.rs/futures/0.3.4/futures/stream/index.html
+[`tokio`]: https://github.com/tokio-rs/tokio
+[`async-std`]: https://github.com/async-rs/async-std
+[`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
+
## [0.1.0-beta] - 2019-11-25
### Changed
@@ -15,6 +114,8 @@ 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.0-beta...HEAD
+[Unreleased]: https://github.com/hecrj/iced/compare/0.1.1...HEAD
+[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
[0.1.0-alpha]: https://github.com/hecrj/iced/releases/tag/0.1.0-alpha
diff --git a/Cargo.toml b/Cargo.toml
index 01231b70..6221ae4b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "iced"
-version = "0.1.0-beta"
+version = "0.2.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
description = "A cross-platform GUI library inspired by Elm"
@@ -12,18 +12,37 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"]
categories = ["gui"]
[features]
+default = ["wgpu", "default_system_font"]
+# Enables the `iced_wgpu` renderer
+wgpu = ["iced_wgpu"]
# Enables the `Image` widget
image = ["iced_wgpu/image"]
# Enables the `Svg` widget
svg = ["iced_wgpu/svg"]
# Enables the `Canvas` widget
canvas = ["iced_wgpu/canvas"]
+# Enables the `QRCode` widget
+qr_code = ["iced_wgpu/qr_code"]
+# Enables using system fonts
+default_system_font = ["iced_wgpu/default_system_font"]
+# Enables the `iced_glow` renderer. Overrides `iced_wgpu`
+glow = ["iced_glow", "iced_glutin"]
+# Enables the `Canvas` widget for `iced_glow`
+glow_canvas = ["iced_glow/canvas"]
+# Enables the `QRCode` widget for `iced_glow`
+glow_qr_code = ["iced_glow/qr_code"]
+# Enables using system fonts for `iced_glow`
+glow_default_system_font = ["iced_glow/default_system_font"]
# Enables a debug view in native platforms (press F12)
debug = ["iced_winit/debug"]
# Enables `tokio` as the `executor::Default` on native platforms
tokio = ["iced_futures/tokio"]
+# Enables old `tokio` (0.2) as the `executor::Default` on native platforms
+tokio_old = ["iced_futures/tokio_old"]
# Enables `async-std` as the `executor::Default` on native platforms
async-std = ["iced_futures/async-std"]
+# Enables advanced color conversion via `palette`
+palette = ["iced_core/palette"]
[badges]
maintenance = { status = "actively-developed" }
@@ -32,6 +51,9 @@ maintenance = { status = "actively-developed" }
members = [
"core",
"futures",
+ "graphics",
+ "glow",
+ "glutin",
"native",
"style",
"web",
@@ -39,13 +61,20 @@ members = [
"winit",
"examples/bezier_tool",
"examples/clock",
+ "examples/color_palette",
"examples/counter",
"examples/custom_widget",
+ "examples/download_progress",
"examples/events",
+ "examples/game_of_life",
"examples/geometry",
"examples/integration",
+ "examples/pane_grid",
+ "examples/pick_list",
"examples/pokedex",
"examples/progress_bar",
+ "examples/qr_code",
+ "examples/scrollable",
"examples/solar_system",
"examples/stopwatch",
"examples/styling",
@@ -55,11 +84,19 @@ members = [
]
[dependencies]
-iced_futures = { version = "0.1.0-alpha", path = "futures" }
+iced_core = { version = "0.3", path = "core" }
+iced_futures = { version = "0.2", path = "futures" }
+thiserror = "1.0"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
-iced_winit = { version = "0.1.0-alpha", path = "winit" }
-iced_wgpu = { version = "0.1.0", path = "wgpu" }
+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}
[target.'cfg(target_arch = "wasm32")'.dependencies]
-iced_web = { version = "0.1.0", path = "web" }
+iced_web = { version = "0.3", path = "web" }
+
+[package.metadata.docs.rs]
+rustdoc-args = ["--cfg", "docsrs"]
+features = ["image", "svg", "canvas"]
diff --git a/ECOSYSTEM.md b/ECOSYSTEM.md
index 177b8709..82303130 100644
--- a/ECOSYSTEM.md
+++ b/ECOSYSTEM.md
@@ -1,10 +1,7 @@
# Ecosystem
-This document describes the Iced ecosystem.
-
-It quickly lists the different audiences of the library and explains how the different crates relate to each other.
-
-## Users
+This document describes the Iced ecosystem and explains how the different crates relate to each other.
+## Overview
Iced is meant to be used by 2 different types of users:
- __End-users__. They should be able to:
@@ -18,71 +15,81 @@ Iced is meant to be used by 2 different types of users:
- integrate existing runtimes in their own system (like game engines),
- and create their own custom renderers.
-## Crates
Iced consists of different crates which offer different layers of abstractions for our users. This modular architecture helps us keep implementation details hidden and decoupled, which should allow us to rewrite or change strategies in the future.
-![Ecosystem graph](docs/graphs/ecosystem.png)
+<p align="center">
+ <img alt="The Iced Ecosystem" src="docs/graphs/ecosystem.png" width="60%">
+</p>
-### [`iced_core`]
+## The foundations
+There are a bunch of concepts that permeate the whole ecosystem. These concepts are considered __the foundations__, and they are provided by three different crates:
-[`iced_core`] holds basic reusable types of the public API. For instance, basic data types like `Point`, `Rectangle`, `Length`, etc.
+- [`iced_core`] contains many lightweight, reusable primitives (e.g. `Point`, `Rectangle`, `Color`).
+- [`iced_futures`] implements the concurrent concepts of [The Elm Architecture] on top of the [`futures`] ecosystem.
+- [`iced_style`] defines the default styling capabilities of built-in widgets.
-This crate is meant to be a starting point for an Iced runtime.
+<p align="center">
+ <img alt="The foundations" src="docs/graphs/foundations.png" width="50%">
+</p>
-### [`iced_native`]
-[`iced_native`] takes [`iced_core`] and builds a native runtime on top of it, featuring:
-- A custom layout engine, greatly inspired by [`druid`]
-- Event handling for all the built-in widgets
-- A renderer-agnostic API
+## The native target
+The native side of the ecosystem is split into two different groups: __renderers__ and __shells__.
-To achieve this, it introduces a bunch of reusable interfaces:
-- A `Widget` trait, which is used to implement new widgets: from layout requirements to event and drawing logic.
-- A bunch of `Renderer` traits, meant to keep the crate renderer-agnostic.
-- A `Windowed` trait, leveraging [`raw-window-handle`], which can be implemented by graphical renderers that target _windows_. Window-based shells (like [`iced_winit`]) can use this trait to stay renderer-agnostic.
+<p align="center">
+ <img alt="The native target" src="docs/graphs/native.png" width="80%">
+</p>
-[`druid`]: https://github.com/xi-editor/druid
-[`raw-window-handle`]: https://github.com/rust-windowing/raw-window-handle
+### Renderers
+The widgets of a _graphical_ user interface produce some primitives that eventually need to be drawn on screen. __Renderers__ take care of this task, potentially leveraging GPU acceleration.
-### [`iced_web`]
-[`iced_web`] takes [`iced_core`] and builds a WebAssembly runtime on top. It achieves this by introducing a `Widget` trait that can be used to produce VDOM nodes.
+Currently, there are two different official renderers:
-The crate is currently a simple abstraction layer over [`dodrio`].
+- [`iced_wgpu`] is powered by [`wgpu`] and supports Vulkan, DirectX 12, and Metal.
+- [`iced_glow`] is powered by [`glow`] and supports OpenGL 3.3+.
-[`dodrio`]: https://github.com/fitzgen/dodrio
+Additionally, the [`iced_graphics`] subcrate contains a bunch of backend-agnostic types that can be leveraged to build renderers. Both of the renderers rely on the graphical foundations provided by this crate.
-### [`iced_wgpu`]
-[`iced_wgpu`] is a [`wgpu`] renderer for [`iced_native`]. For now, it is the default renderer of Iced in native platforms.
+### Shells
+The widgets of a graphical user _interface_ are interactive. __Shells__ gather and process user interactions in an event loop.
-[`wgpu`] supports most modern graphics backends: Vulkan, Metal, DX11, and DX12 (OpenGL and WebGL are still WIP). Additionally, it will support the incoming [WebGPU API].
+Normally, a shell will be responsible of creating a window and managing the lifecycle of a user interface, implementing a runtime of [The Elm Architecture].
-Currently, [`iced_wgpu`] supports the following primitives:
-- Text, which is rendered using [`wgpu_glyph`]. No shaping at all.
-- Quads or rectangles, with rounded borders and a solid background color.
-- Images, lazily loaded from the filesystem.
-- Clip areas, useful to implement scrollables or hide overflowing content.
+As of now, there are two official shells:
-[`wgpu`]: https://github.com/gfx-rs/wgpu-rs
-[WebGPU API]: https://gpuweb.github.io/gpuweb/
-[`wgpu_glyph`]: https://github.com/hecrj/wgpu_glyph
+- [`iced_winit`] implements a shell runtime on top of [`winit`].
+- [`iced_glutin`] is similar to [`iced_winit`], but it also deals with [OpenGL context creation].
-### [`iced_winit`]
-[`iced_winit`] offers some convenient abstractions on top of [`iced_native`] to quickstart development when using [`winit`].
+## The web target
+The Web platform provides all the abstractions necessary to draw widgets and gather users interactions.
-It exposes a renderer-agnostic `Application` trait that can be implemented and then run with a simple call. The use of this trait is optional. A `conversion` module is provided for users that decide to implement a custom event loop.
+Therefore, unlike the native path, the web side of the ecosystem does not need to split renderers and shells. Instead, [`iced_web`] leverages [`dodrio`] to both render widgets and implement a proper runtime.
-[`winit`]: https://github.com/rust-windowing/winit
-
-### [`iced`]
+## Iced
Finally, [`iced`] unifies everything into a simple abstraction to create cross-platform applications:
-- On native, it uses [`iced_winit`] and [`iced_wgpu`].
+- On native, it uses __[shells](#shells)__ and __[renderers](#renderers)__.
- On the web, it uses [`iced_web`].
-This is the crate meant to be used by __end-users__.
+<p align="center">
+ <img alt="Iced" src="docs/graphs/iced.png" width="80%">
+</p>
[`iced_core`]: core
+[`iced_futures`]: futures
+[`iced_style`]: style
[`iced_native`]: native
[`iced_web`]: web
+[`iced_graphics`]: graphics
[`iced_wgpu`]: wgpu
+[`iced_glow`]: glow
[`iced_winit`]: winit
+[`iced_glutin`]: glutin
[`iced`]: ..
+[`futures`]: https://github.com/rust-lang/futures-rs
+[`glow`]: https://github.com/grovesNL/glow
+[`wgpu`]: https://github.com/gfx-rs/wgpu-rs
+[`winit`]: https://github.com/rust-windowing/winit
+[`glutin`]: https://github.com/rust-windowing/glutin
+[`dodrio`]: https://github.com/fitzgen/dodrio
+[OpenGL context creation]: https://www.khronos.org/opengl/wiki/Creating_an_OpenGL_Context
+[The Elm Architecture]: https://guide.elm-lang.org/architecture/
diff --git a/README.md b/README.md
index 10992ad4..09647a1e 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# Iced
[![Test Status](https://github.com/hecrj/iced/workflows/Test/badge.svg?event=push)](https://github.com/hecrj/iced/actions)
[![Documentation](https://docs.rs/iced/badge.svg)][documentation]
-[![Crates.io](https://img.shields.io/crates/v/iced.svg)](https://crates.io/crates/iced/0.1.0-beta)
+[![Crates.io](https://img.shields.io/crates/v/iced.svg)](https://crates.io/crates/iced)
[![License](https://img.shields.io/crates/l/iced.svg)](https://github.com/hecrj/iced/blob/master/LICENSE)
[![project chat](https://img.shields.io/badge/chat-on_zulip-brightgreen.svg)](https://iced.zulipchat.com)
@@ -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.1.0-beta"
+iced = "0.2"
```
__Iced moves fast and the `master` branch can contain breaking changes!__ If
@@ -174,20 +174,13 @@ The core of the library was implemented during May in [this pull request].
implemented the current [tour example] on top of [`ggez`], a game library.
Since then, the focus has shifted towards providing a batteries-included,
-end-user-oriented GUI library, while keeping [the ecosystem] modular.
+end-user-oriented GUI library, while keeping [the ecosystem] modular:
-Currently, Iced is a cross-platform GUI library built on top of smaller crates:
-
- - [`iced_core`], a bunch of basic types that can be reused in different runtimes.
- - [`iced_native`], a renderer-agnostic native runtime implementing widget
- logic and a layout engine inspired by [`druid`].
- - [`iced_web`], an experimental web runtime that targets the DOM thanks to
- [`dodrio`].
- - [`iced_wgpu`], a renderer leveraging [`wgpu`], [`wgpu_glyph`], and
- [`font-kit`].
- - [`iced_winit`], a windowing shell on top of [`winit`].
-
-[![Iced ecosystem](docs/graphs/ecosystem.png)](https://github.com/hecrj/iced/blob/master/ECOSYSTEM.md)
+<p align="center">
+ <a href="https://github.com/hecrj/iced/blob/master/ECOSYSTEM.md">
+ <img alt="Iced Ecosystem" src="docs/graphs/ecosystem.png" width="80%">
+ </a>
+</p>
[this pull request]: https://github.com/hecrj/coffee/pull/35
[The first alpha version]: https://github.com/hecrj/iced/tree/0.1.0-alpha
@@ -195,15 +188,6 @@ Currently, Iced is a cross-platform GUI library built on top of smaller crates:
[tour example]: https://github.com/hecrj/iced/blob/master/examples/README.md#tour
[`ggez`]: https://github.com/ggez/ggez
[the ecosystem]: https://github.com/hecrj/iced/blob/master/ECOSYSTEM.md
-[`iced_core`]: https://github.com/hecrj/iced/tree/master/core
-[`iced_native`]: https://github.com/hecrj/iced/tree/master/native
-[`iced_web`]: https://github.com/hecrj/iced/tree/master/web
-[`iced_wgpu`]: https://github.com/hecrj/iced/tree/master/wgpu
-[`iced_winit`]: https://github.com/hecrj/iced/tree/master/winit
-[`druid`]: https://github.com/xi-editor/druid
-[`wgpu_glyph`]: https://github.com/hecrj/wgpu_glyph
-[`font-kit`]: https://github.com/servo/font-kit
-[`winit`]: https://github.com/rust-windowing/winit
## Contributing / Feedback
Contributions are greatly appreciated! If you want to contribute, please
@@ -217,7 +201,7 @@ the [Rust Community Discord]. I go by `lone_scientist#9554` there.
## Sponsors
The development of Iced is sponsored by the [Cryptowatch] team at [Kraken.com]
-[documentation]: https://docs.rs/iced/0.1.0-beta/iced/
+[documentation]: https://docs.rs/iced/
[examples]: https://github.com/hecrj/iced/tree/master/examples
[Coffee]: https://github.com/hecrj/coffee
[Elm]: https://elm-lang.org/
diff --git a/ROADMAP.md b/ROADMAP.md
index db0c6625..05aa9bda 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -6,17 +6,19 @@ Before diving into the roadmap, check out [the ecosystem overview] to get an ide
[the ecosystem overview]: ECOSYSTEM.md
## Next steps
-Most of the work related to these features needs to happen in the `iced_native` path of the ecosystem, as the web already supports many of them.
+Most of the work related to these features needs to happen in the __native__ path of the ecosystem, as the web already supports many of them.
Once a step is completed, it is collapsed and added to this list:
* [x] Scrollables / Clippables ([#24])
* [x] Text input widget ([#25])
* [x] TodoMVC example ([#26])
- * [x] Async actions ([#27])
+ * [x] Async actions ([#28])
* [x] Custom layout engine ([#52])
* [x] Event subscriptions ([#122])
* [x] Custom styling ([#146])
+ * [x] Canvas for 2D graphics ([#193])
+ * [x] Basic overlay support ([#444])
[#24]: https://github.com/hecrj/iced/issues/24
[#25]: https://github.com/hecrj/iced/issues/25
@@ -25,6 +27,8 @@ Once a step is completed, it is collapsed and added to this list:
[#52]: https://github.com/hecrj/iced/pull/52
[#122]: https://github.com/hecrj/iced/pull/122
[#146]: https://github.com/hecrj/iced/pull/146
+[#193]: https://github.com/hecrj/iced/pull/193
+[#444]: https://github.com/hecrj/iced/pull/444
### Multi-window support ([#27])
Open and control multiple windows at runtime.
@@ -35,17 +39,6 @@ This approach should also allow us to perform custom optimizations for this part
[#27]: https://github.com/hecrj/iced/issues/27
-### Layers ([#30])
-Currently, Iced assumes widgets cannot be laid out on top of each other. We should implement support for multiple layers of widgets.
-
-This is a necessary feature to implement many kinds of interactables, like dropdown menus, select fields, etc.
-
-`iced_native` will need to group widgets to perform layouting and process some events first for widgets positioned on top.
-
-`iced_wgpu` will also need to process the scene graph and sort draw calls based on the different layers.
-
-[#30]: https://github.com/hecrj/iced/issues/30
-
### Animations ([#31])
Allow widgets to request a redraw at a specific time.
@@ -55,8 +48,8 @@ This is a necessary feature to render loading spinners, a blinking text cursor,
[#31]: https://github.com/hecrj/iced/issues/31
-### Canvas widget ([#32])
-A widget to draw freely in 2D or 3D. It could be used to draw charts, implement a Paint clone, a CAD application, etc.
+### Canvas widget for 3D graphics ([#32])
+A widget to draw freely in 3D. It could be used to draw charts, implement a Paint clone, a CAD application, etc.
As a first approach, we could expose the underlying renderer directly here, and couple this widget with it ([`wgpu`] for now). Once [`wgpu`] gets WebGL or WebGPU support, this widget will be able to run on the web too. The renderer primitive could be a simple texture that the widget draws to.
diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md
deleted file mode 100644
index c0796e66..00000000
--- a/core/CHANGELOG.md
+++ /dev/null
@@ -1,19 +0,0 @@
-# Changelog
-All notable changes to this project will be documented in this file.
-
-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]
-### Added
-- `Color::from_rgb8` to easily build a `Color` from its hexadecimal representation. [#90]
-
-[#90]: https://github.com/hecrj/iced/pull/90
-
-
-## [0.1.0] - 2019-11-25
-### Added
-- First release! :tada:
-
-[Unreleased]: https://github.com/hecrj/iced/compare/core-0.1.0...HEAD
-[0.1.0]: https://github.com/hecrj/iced/releases/tag/core-0.1.0
diff --git a/core/Cargo.toml b/core/Cargo.toml
index 22bc7ceb..a859c868 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "iced_core"
-version = "0.1.0"
+version = "0.3.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
description = "The essential concepts of Iced"
@@ -8,3 +8,7 @@ license = "MIT"
repository = "https://github.com/hecrj/iced"
[dependencies]
+
+[dependencies.palette]
+version = "0.5.0"
+optional = true
diff --git a/core/README.md b/core/README.md
index 31630ea8..3ec053ac 100644
--- a/core/README.md
+++ b/core/README.md
@@ -8,7 +8,9 @@
This crate is meant to be a starting point for an Iced runtime.
-![iced_core](../docs/graphs/core.png)
+<p align="center">
+ <img alt="The foundations" src="../docs/graphs/foundations.png" width="50%">
+</p>
[documentation]: https://docs.rs/iced_core
@@ -16,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.1.0"
+iced_core = "0.3"
```
__Iced moves fast and the `master` branch can contain breaking changes!__ If
diff --git a/core/src/background.rs b/core/src/background.rs
index e1a37ddc..cfb95867 100644
--- a/core/src/background.rs
+++ b/core/src/background.rs
@@ -13,3 +13,9 @@ impl From<Color> for Background {
Background::Color(color)
}
}
+
+impl From<Color> for Option<Background> {
+ fn from(color: Color) -> Self {
+ Some(Background::from(color))
+ }
+}
diff --git a/core/src/color.rs b/core/src/color.rs
index db509b88..c66ee97c 100644
--- a/core/src/color.rs
+++ b/core/src/color.rs
@@ -1,10 +1,16 @@
+#[cfg(feature = "palette")]
+use palette::rgb::{Srgb, Srgba};
+
/// A color in the sRGB color space.
-#[derive(Debug, Clone, Copy, PartialEq)]
-#[allow(missing_docs)]
+#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Color {
+ /// Red component, 0.0 - 1.0
pub r: f32,
+ /// Green component, 0.0 - 1.0
pub g: f32,
+ /// Blue component, 0.0 - 1.0
pub b: f32,
+ /// Transparency, 0.0 - 1.0
pub a: f32,
}
@@ -33,23 +39,47 @@ impl Color {
a: 0.0,
};
- /// Creates a [`Color`] from its RGB components.
+ /// Creates a new [`Color`].
///
- /// [`Color`]: struct.Color.html
+ /// In debug mode, it will panic if the values are not in the correct
+ /// range: 0.0 - 1.0
+ pub fn new(r: f32, g: f32, b: f32, a: f32) -> Color {
+ debug_assert!(
+ (0.0..=1.0).contains(&r),
+ "Red component must be on [0, 1]"
+ );
+ debug_assert!(
+ (0.0..=1.0).contains(&g),
+ "Green component must be on [0, 1]"
+ );
+ debug_assert!(
+ (0.0..=1.0).contains(&b),
+ "Blue component must be on [0, 1]"
+ );
+ debug_assert!(
+ (0.0..=1.0).contains(&a),
+ "Alpha component must be on [0, 1]"
+ );
+
+ Color { r, g, b, a }
+ }
+
+ /// Creates a [`Color`] from its RGB components.
pub const fn from_rgb(r: f32, g: f32, b: f32) -> Color {
- Color { r, g, b, a: 1.0 }
+ Color::from_rgba(r, g, b, 1.0f32)
+ }
+
+ /// Creates a [`Color`] from its RGBA components.
+ pub const fn from_rgba(r: f32, g: f32, b: f32, a: f32) -> Color {
+ Color { r, g, b, a }
}
/// Creates a [`Color`] from its RGB8 components.
- ///
- /// [`Color`]: struct.Color.html
pub fn from_rgb8(r: u8, g: u8, b: u8) -> Color {
Color::from_rgba8(r, g, b, 1.0)
}
/// Creates a [`Color`] from its RGB8 components and an alpha value.
- ///
- /// [`Color`]: struct.Color.html
pub fn from_rgba8(r: u8, g: u8, b: u8, a: f32) -> Color {
Color {
r: f32::from(r) / 255.0,
@@ -60,8 +90,6 @@ impl Color {
}
/// Converts the [`Color`] into its linear values.
- ///
- /// [`Color`]: struct.Color.html
pub fn into_linear(self) -> [f32; 4] {
// As described in:
// https://en.wikipedia.org/wiki/SRGB#The_reverse_transformation
@@ -80,16 +108,101 @@ impl Color {
self.a,
]
}
+
+ /// Inverts the [`Color`] in-place.
+ pub fn invert(&mut self) {
+ self.r = 1.0f32 - self.r;
+ self.b = 1.0f32 - self.g;
+ self.g = 1.0f32 - self.b;
+ }
+
+ /// Returns the inverted [`Color`].
+ pub fn inverse(self) -> Color {
+ Color::new(1.0f32 - self.r, 1.0f32 - self.g, 1.0f32 - self.b, self.a)
+ }
}
impl From<[f32; 3]> for Color {
fn from([r, g, b]: [f32; 3]) -> Self {
- Color { r, g, b, a: 1.0 }
+ Color::new(r, g, b, 1.0)
}
}
impl From<[f32; 4]> for Color {
fn from([r, g, b, a]: [f32; 4]) -> Self {
- Color { r, g, b, a }
+ Color::new(r, g, b, a)
+ }
+}
+
+#[cfg(feature = "palette")]
+/// Converts from palette's `Srgba` type to a [`Color`].
+impl From<Srgba> for Color {
+ fn from(srgba: Srgba) -> Self {
+ Color::new(srgba.red, srgba.green, srgba.blue, srgba.alpha)
+ }
+}
+
+#[cfg(feature = "palette")]
+/// Converts from [`Color`] to palette's `Srgba` type.
+impl From<Color> for Srgba {
+ fn from(c: Color) -> Self {
+ Srgba::new(c.r, c.g, c.b, c.a)
+ }
+}
+
+#[cfg(feature = "palette")]
+/// Converts from palette's `Srgb` type to a [`Color`].
+impl From<Srgb> for Color {
+ fn from(srgb: Srgb) -> Self {
+ Color::new(srgb.red, srgb.green, srgb.blue, 1.0)
+ }
+}
+
+#[cfg(feature = "palette")]
+/// Converts from [`Color`] to palette's `Srgb` type.
+impl From<Color> for Srgb {
+ fn from(c: Color) -> Self {
+ Srgb::new(c.r, c.g, c.b)
+ }
+}
+
+#[cfg(feature = "palette")]
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use palette::Blend;
+
+ #[test]
+ fn srgba_traits() {
+ let c = Color::from_rgb(0.5, 0.4, 0.3);
+ // Round-trip conversion to the palette:Srgba type
+ let s: Srgba = c.into();
+ let r: Color = s.into();
+ assert_eq!(c, r);
+ }
+
+ #[test]
+ fn color_manipulation() {
+ let c1 = Color::from_rgb(0.5, 0.4, 0.3);
+ let c2 = Color::from_rgb(0.2, 0.5, 0.3);
+
+ // Convert to linear color for manipulation
+ let l1 = Srgba::from(c1).into_linear();
+ let l2 = Srgba::from(c2).into_linear();
+
+ // Take the lighter of each of the RGB components
+ let lighter = l1.lighten(l2);
+
+ // Convert back to our Color
+ let r: Color = Srgba::from_linear(lighter).into();
+ assert_eq!(
+ r,
+ Color {
+ r: 0.5,
+ g: 0.5,
+ b: 0.3,
+ a: 1.0
+ }
+ );
}
}
diff --git a/core/src/font.rs b/core/src/font.rs
index be49c825..3f9ad2b5 100644
--- a/core/src/font.rs
+++ b/core/src/font.rs
@@ -16,3 +16,9 @@ pub enum Font {
bytes: &'static [u8],
},
}
+
+impl Default for Font {
+ fn default() -> Font {
+ Font::Default
+ }
+}
diff --git a/core/src/keyboard.rs b/core/src/keyboard.rs
new file mode 100644
index 00000000..61e017ad
--- /dev/null
+++ b/core/src/keyboard.rs
@@ -0,0 +1,8 @@
+//! Reuse basic keyboard types.
+mod event;
+mod key_code;
+mod modifiers;
+
+pub use event::Event;
+pub use key_code::KeyCode;
+pub use modifiers::Modifiers;
diff --git a/native/src/input/keyboard/event.rs b/core/src/keyboard/event.rs
index 862f30c4..0564c171 100644
--- a/native/src/input/keyboard/event.rs
+++ b/core/src/keyboard/event.rs
@@ -1,5 +1,4 @@
-use super::{KeyCode, ModifiersState};
-use crate::input::ButtonState;
+use super::{KeyCode, Modifiers};
/// A keyboard event.
///
@@ -9,18 +8,27 @@ use crate::input::ButtonState;
/// [open an issue]: https://github.com/hecrj/iced/issues
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Event {
- /// A keyboard key was pressed or released.
- Input {
- /// The state of the key
- state: ButtonState,
+ /// A keyboard key was pressed.
+ KeyPressed {
+ /// The key identifier
+ key_code: KeyCode,
+
+ /// The state of the modifier keys
+ modifiers: Modifiers,
+ },
+ /// A keyboard key was released.
+ KeyReleased {
/// The key identifier
key_code: KeyCode,
/// The state of the modifier keys
- modifiers: ModifiersState,
+ modifiers: Modifiers,
},
/// A unicode character was received.
CharacterReceived(char),
+
+ /// The keyboard modifiers have changed.
+ ModifiersChanged(Modifiers),
}
diff --git a/native/src/input/keyboard/key_code.rs b/core/src/keyboard/key_code.rs
index 26020a57..74ead170 100644
--- a/native/src/input/keyboard/key_code.rs
+++ b/core/src/keyboard/key_code.rs
@@ -55,7 +55,7 @@ pub enum KeyCode {
Y,
Z,
- /// The Escape key, next to F1
+ /// The Escape key, next to F1.
Escape,
F1,
@@ -83,14 +83,14 @@ pub enum KeyCode {
F23,
F24,
- /// Print Screen/SysRq
+ /// Print Screen/SysRq.
Snapshot,
- /// Scroll Lock
+ /// Scroll Lock.
Scroll,
- /// Pause/Break key, next to Scroll lock
+ /// Pause/Break key, next to Scroll lock.
Pause,
- /// `Insert`, next to Backspace
+ /// `Insert`, next to Backspace.
Insert,
Home,
Delete,
@@ -103,11 +103,14 @@ pub enum KeyCode {
Right,
Down,
+ /// The Backspace key, right over Enter.
Backspace,
+ /// The Enter key.
Enter,
+ /// The space bar.
Space,
- /// The "Compose" key on Linux
+ /// The "Compose" key on Linux.
Compose,
Caret,
@@ -123,12 +126,20 @@ pub enum KeyCode {
Numpad7,
Numpad8,
Numpad9,
+ NumpadAdd,
+ NumpadDivide,
+ NumpadDecimal,
+ NumpadComma,
+ NumpadEnter,
+ NumpadEquals,
+ NumpadMultiply,
+ NumpadSubtract,
AbntC1,
AbntC2,
- Add,
Apostrophe,
Apps,
+ Asterisk,
At,
Ax,
Backslash,
@@ -137,8 +148,6 @@ pub enum KeyCode {
Colon,
Comma,
Convert,
- Decimal,
- Divide,
Equals,
Grave,
Kana,
@@ -152,19 +161,16 @@ pub enum KeyCode {
MediaSelect,
MediaStop,
Minus,
- Multiply,
Mute,
MyComputer,
- NavigateForward, // also called "Prior"
- NavigateBackward, // also called "Next"
+ NavigateForward, // also called "Next"
+ NavigateBackward, // also called "Prior"
NextTrack,
NoConvert,
- NumpadComma,
- NumpadEnter,
- NumpadEquals,
OEM102,
Period,
PlayPause,
+ Plus,
Power,
PrevTrack,
RAlt,
@@ -176,7 +182,6 @@ pub enum KeyCode {
Slash,
Sleep,
Stop,
- Subtract,
Sysrq,
Tab,
Underline,
diff --git a/core/src/keyboard/modifiers.rs b/core/src/keyboard/modifiers.rs
new file mode 100644
index 00000000..d2a0500e
--- /dev/null
+++ b/core/src/keyboard/modifiers.rs
@@ -0,0 +1,45 @@
+/// 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,
+
+ /// 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,
+}
+
+impl Modifiers {
+ /// 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
+ /// current platform. Specifically:
+ ///
+ /// - It is the `logo` or command key (⌘) on macOS
+ /// - It is the `control` key on other platforms
+ pub fn is_command_pressed(self) -> bool {
+ #[cfg(target_os = "macos")]
+ let is_pressed = self.logo;
+
+ #[cfg(not(target_os = "macos"))]
+ 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/length.rs b/core/src/length.rs
index 06d8cf0a..186411a5 100644
--- a/core/src/length.rs
+++ b/core/src/length.rs
@@ -26,8 +26,6 @@ impl Length {
/// The _fill factor_ is a relative unit describing how much of the
/// remaining space should be filled when compared to other elements. It
/// is only meant to be used by layout engines.
- ///
- /// [`Length`]: enum.Length.html
pub fn fill_factor(&self) -> u16 {
match self {
Length::Fill => 1,
diff --git a/core/src/lib.rs b/core/src/lib.rs
index ea5e8b43..f2d21a5f 100644
--- a/core/src/lib.rs
+++ b/core/src/lib.rs
@@ -1,11 +1,11 @@
//! The core library of [Iced].
//!
-//! ![`iced_core` crate graph](https://github.com/hecrj/iced/blob/cae26cb7bc627f4a5b3bcf1cd023a0c552e8c65e/docs/graphs/core.png?raw=true)
-//!
//! This library holds basic types that can be reused and re-exported in
//! different runtime implementations. For instance, both [`iced_native`] and
//! [`iced_web`] are built on top of `iced_core`.
//!
+//! ![The foundations of the Iced ecosystem](https://github.com/hecrj/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/foundations.png?raw=true)
+//!
//! [Iced]: https://github.com/hecrj/iced
//! [`iced_native`]: https://github.com/hecrj/iced/tree/master/native
//! [`iced_web`]: https://github.com/hecrj/iced/tree/master/web
@@ -14,6 +14,8 @@
#![deny(unused_results)]
#![forbid(unsafe_code)]
#![forbid(rust_2018_idioms)]
+pub mod keyboard;
+pub mod mouse;
mod align;
mod background;
diff --git a/native/src/input/mouse.rs b/core/src/mouse.rs
index 69dc6b4c..25ce6ac3 100644
--- a/native/src/input/mouse.rs
+++ b/core/src/mouse.rs
@@ -1,6 +1,8 @@
-//! Build mouse events.
+//! Reuse basic mouse types.
mod button;
mod event;
+mod interaction;
pub use button::Button;
pub use event::{Event, ScrollDelta};
+pub use interaction::Interaction;
diff --git a/native/src/input/mouse/button.rs b/core/src/mouse/button.rs
index aeb8a55d..aeb8a55d 100644
--- a/native/src/input/mouse/button.rs
+++ b/core/src/mouse/button.rs
diff --git a/native/src/input/mouse/event.rs b/core/src/mouse/event.rs
index 5068d634..321b8399 100644
--- a/native/src/input/mouse/event.rs
+++ b/core/src/mouse/event.rs
@@ -1,5 +1,6 @@
+use crate::Point;
+
use super::Button;
-use crate::{input::ButtonState, Point};
/// A mouse event.
///
@@ -21,14 +22,11 @@ pub enum Event {
position: Point,
},
- /// A mouse button was pressed or released.
- Input {
- /// The state of the button
- state: ButtonState,
+ /// A mouse button was pressed.
+ ButtonPressed(Button),
- /// The button identifier
- button: Button,
- },
+ /// A mouse button was released.
+ ButtonReleased(Button),
/// The mouse wheel was scrolled.
WheelScrolled {
diff --git a/core/src/mouse/interaction.rs b/core/src/mouse/interaction.rs
new file mode 100644
index 00000000..664147a7
--- /dev/null
+++ b/core/src/mouse/interaction.rs
@@ -0,0 +1,20 @@
+/// The interaction of a mouse cursor.
+#[derive(Debug, Eq, PartialEq, Clone, Copy, PartialOrd, Ord)]
+#[allow(missing_docs)]
+pub enum Interaction {
+ Idle,
+ Pointer,
+ Grab,
+ Text,
+ Crosshair,
+ Working,
+ Grabbing,
+ ResizingHorizontally,
+ ResizingVertically,
+}
+
+impl Default for Interaction {
+ fn default() -> Interaction {
+ Interaction::Idle
+ }
+}
diff --git a/core/src/point.rs b/core/src/point.rs
index b55f5099..9bf7726b 100644
--- a/core/src/point.rs
+++ b/core/src/point.rs
@@ -1,7 +1,7 @@
use crate::Vector;
/// A 2D point.
-#[derive(Debug, Clone, Copy, PartialEq)]
+#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Point {
/// The X coordinate.
pub x: f32,
@@ -11,17 +11,21 @@ pub struct Point {
}
impl Point {
- /// The origin (i.e. a [`Point`] with both X=0 and Y=0).
- ///
- /// [`Point`]: struct.Point.html
+ /// The origin (i.e. a [`Point`] at (0, 0)).
pub const ORIGIN: Point = Point::new(0.0, 0.0);
/// Creates a new [`Point`] with the given coordinates.
- ///
- /// [`Point`]: struct.Point.html
pub const fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
+
+ /// Computes the distance to another [`Point`].
+ pub fn distance(&self, to: Point) -> f32 {
+ let a = self.x - to.x;
+ let b = self.y - to.y;
+
+ a.hypot(b)
+ }
}
impl From<[f32; 2]> for Point {
@@ -36,6 +40,12 @@ impl From<[u16; 2]> for Point {
}
}
+impl From<Point> for [f32; 2] {
+ fn from(point: Point) -> [f32; 2] {
+ [point.x, point.y]
+ }
+}
+
impl std::ops::Add<Vector> for Point {
type Output = Self;
@@ -57,3 +67,11 @@ impl std::ops::Sub<Vector> for Point {
}
}
}
+
+impl std::ops::Sub<Point> for Point {
+ type Output = Vector;
+
+ fn sub(self, point: Point) -> Vector {
+ Vector::new(self.x - point.x, self.y - point.y)
+ }
+}
diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs
index 7ed3d2df..0a7f5fe2 100644
--- a/core/src/rectangle.rs
+++ b/core/src/rectangle.rs
@@ -1,4 +1,4 @@
-use crate::Point;
+use crate::{Point, Size, Vector};
/// A rectangle.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
@@ -17,10 +17,56 @@ pub struct Rectangle<T = f32> {
}
impl Rectangle<f32> {
+ /// Creates a new [`Rectangle`] with its top-left corner in the given
+ /// [`Point`] and with the provided [`Size`].
+ pub fn new(top_left: Point, size: Size) -> Self {
+ Self {
+ x: top_left.x,
+ y: top_left.y,
+ width: size.width,
+ height: size.height,
+ }
+ }
+
+ /// Creates a new [`Rectangle`] with its top-left corner at the origin
+ /// and with the provided [`Size`].
+ pub fn with_size(size: Size) -> Self {
+ Self {
+ x: 0.0,
+ y: 0.0,
+ width: size.width,
+ height: size.height,
+ }
+ }
+
+ /// Returns the [`Point`] at the center of the [`Rectangle`].
+ pub fn center(&self) -> Point {
+ Point::new(self.center_x(), self.center_y())
+ }
+
+ /// Returns the X coordinate of the [`Point`] at the center of the
+ /// [`Rectangle`].
+ pub fn center_x(&self) -> f32 {
+ self.x + self.width / 2.0
+ }
+
+ /// Returns the Y coordinate of the [`Point`] at the center of the
+ /// [`Rectangle`].
+ pub fn center_y(&self) -> f32 {
+ self.y + self.height / 2.0
+ }
+
+ /// Returns the position of the top left corner of the [`Rectangle`].
+ pub fn position(&self) -> Point {
+ Point::new(self.x, self.y)
+ }
+
+ /// Returns the [`Size`] of the [`Rectangle`].
+ pub fn size(&self) -> Size {
+ Size::new(self.width, self.height)
+ }
+
/// Returns true if the given [`Point`] is contained in the [`Rectangle`].
- ///
- /// [`Point`]: struct.Point.html
- /// [`Rectangle`]: struct.Rectangle.html
pub fn contains(&self, point: Point) -> bool {
self.x <= point.x
&& point.x <= self.x + self.width
@@ -29,8 +75,6 @@ impl Rectangle<f32> {
}
/// Computes the intersection with the given [`Rectangle`].
- ///
- /// [`Rectangle`]: struct.Rectangle.html
pub fn intersection(
&self,
other: &Rectangle<f32>,
@@ -55,17 +99,27 @@ impl Rectangle<f32> {
None
}
}
+
+ /// Snaps the [`Rectangle`] to __unsigned__ integer coordinates.
+ pub fn snap(self) -> Rectangle<u32> {
+ Rectangle {
+ x: self.x as u32,
+ y: self.y as u32,
+ width: self.width.ceil() as u32,
+ height: self.height.ceil() as u32,
+ }
+ }
}
-impl std::ops::Mul<f32> for Rectangle<u32> {
+impl std::ops::Mul<f32> for Rectangle<f32> {
type Output = Self;
fn mul(self, scale: f32) -> Self {
Self {
- x: (self.x as f32 * scale).round() as u32,
- y: (self.y as f32 * scale).round() as u32,
- width: (self.width as f32 * scale).round() as u32,
- height: (self.height as f32 * scale).round() as u32,
+ x: self.x as f32 * scale,
+ y: self.y as f32 * scale,
+ width: self.width * scale,
+ height: self.height * scale,
}
}
}
@@ -81,13 +135,17 @@ impl From<Rectangle<u32>> for Rectangle<f32> {
}
}
-impl From<Rectangle<f32>> for Rectangle<u32> {
- fn from(rectangle: Rectangle<f32>) -> Rectangle<u32> {
+impl<T> std::ops::Add<Vector<T>> for Rectangle<T>
+where
+ T: std::ops::Add<Output = T>,
+{
+ type Output = Rectangle<T>;
+
+ fn add(self, translation: Vector<T>) -> Self {
Rectangle {
- x: rectangle.x as u32,
- y: rectangle.y as u32,
- width: rectangle.width.ceil() as u32,
- height: rectangle.height.ceil() as u32,
+ x: self.x + translation.x,
+ y: self.y + translation.y,
+ ..self
}
}
}
diff --git a/core/src/size.rs b/core/src/size.rs
index 389b3247..9ea9e686 100644
--- a/core/src/size.rs
+++ b/core/src/size.rs
@@ -1,35 +1,33 @@
+use crate::Vector;
use std::f32;
/// An amount of space in 2 dimensions.
#[derive(Debug, Clone, Copy, PartialEq)]
-pub struct Size {
+pub struct Size<T = f32> {
/// The width.
- pub width: f32,
+ pub width: T,
/// The height.
- pub height: f32,
+ pub height: T,
+}
+
+impl<T> Size<T> {
+ /// Creates a new [`Size`] with the given width and height.
+ pub const fn new(width: T, height: T) -> Self {
+ Size { width, height }
+ }
}
impl Size {
/// A [`Size`] with zero width and height.
- ///
- /// [`Size`]: struct.Size.html
pub const ZERO: Size = Size::new(0., 0.);
+ /// A [`Size`] with a width and height of 1 unit.
+ pub const UNIT: Size = Size::new(1., 1.);
+
/// A [`Size`] with infinite width and height.
- ///
- /// [`Size`]: struct.Size.html
pub const INFINITY: Size = Size::new(f32::INFINITY, f32::INFINITY);
- /// A [`Size`] of infinite width and height.
- ///
- /// [`Size`]: struct.Size.html
- pub const fn new(width: f32, height: f32) -> Self {
- Size { width, height }
- }
-
/// Increments the [`Size`] to account for the given padding.
- ///
- /// [`Size`]: struct.Size.html
pub fn pad(&self, padding: f32) -> Self {
Size {
width: self.width + padding * 2.0,
@@ -49,3 +47,24 @@ impl From<[u16; 2]> for Size {
Size::new(width.into(), height.into())
}
}
+
+impl From<Vector<f32>> for Size {
+ fn from(vector: Vector<f32>) -> Self {
+ Size {
+ width: vector.x,
+ height: vector.y,
+ }
+ }
+}
+
+impl From<Size> for [f32; 2] {
+ fn from(size: Size) -> [f32; 2] {
+ [size.width, size.height]
+ }
+}
+
+impl From<Size> for Vector<f32> {
+ fn from(size: Size) -> Self {
+ Vector::new(size.width, size.height)
+ }
+}
diff --git a/core/src/vector.rs b/core/src/vector.rs
index a75053a0..92bb7648 100644
--- a/core/src/vector.rs
+++ b/core/src/vector.rs
@@ -2,20 +2,14 @@
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Vector<T = f32> {
/// The X component of the [`Vector`]
- ///
- /// [`Vector`]: struct.Vector.html
pub x: T,
/// The Y component of the [`Vector`]
- ///
- /// [`Vector`]: struct.Vector.html
pub y: T,
}
impl<T> Vector<T> {
/// Creates a new [`Vector`] with the given components.
- ///
- /// [`Vector`]: struct.Vector.html
pub const fn new(x: T, y: T) -> Self {
Self { x, y }
}
@@ -43,6 +37,17 @@ where
}
}
+impl<T> std::ops::Mul<T> for Vector<T>
+where
+ T: std::ops::Mul<Output = T> + Copy,
+{
+ type Output = Self;
+
+ fn mul(self, scale: T) -> Self {
+ Self::new(self.x * scale, self.y * scale)
+ }
+}
+
impl<T> Default for Vector<T>
where
T: Default,
@@ -54,3 +59,18 @@ where
}
}
}
+
+impl<T> From<[T; 2]> for Vector<T> {
+ fn from([x, y]: [T; 2]) -> Self {
+ Self::new(x, y)
+ }
+}
+
+impl<T> From<Vector<T>> for [T; 2]
+where
+ T: Copy,
+{
+ fn from(other: Vector<T>) -> Self {
+ [other.x, other.y]
+ }
+}
diff --git a/docs/graphs/core.dot b/docs/graphs/core.dot
deleted file mode 100644
index 93724927..00000000
--- a/docs/graphs/core.dot
+++ /dev/null
@@ -1,13 +0,0 @@
-digraph G {
- fontname = "Roboto";
- newrank=true;
- node[fontname = "Roboto", style="filled", fontcolor="#333333", fillcolor=white, color="#333333"];
- edge[color="#333333"];
-
- { rank = same; iced_native iced_web }
-
- iced_core -> iced_native [style=dashed];
- iced_core -> iced_web [style=dashed];
-
- iced_core [style=dashed];
-}
diff --git a/docs/graphs/core.png b/docs/graphs/core.png
deleted file mode 100644
index 0b14ab6c..00000000
--- a/docs/graphs/core.png
+++ /dev/null
Binary files differ
diff --git a/docs/graphs/ecosystem.dot b/docs/graphs/ecosystem.dot
deleted file mode 100644
index 609cf726..00000000
--- a/docs/graphs/ecosystem.dot
+++ /dev/null
@@ -1,56 +0,0 @@
-digraph G {
- fontname = "Roboto";
- newrank=true;
- node[fontname = "Roboto", style="filled", fontcolor="#333333", fillcolor=white, color="#333333"];
- edge[color="#333333"];
-
- subgraph cluster_1 {
- label = "renderers ";
- labelloc = "b";
- labeljust = "r";
- fontcolor = "#0366d6";
- color="#f6f8fa";
- bgcolor="#f6f8fa";
- style=rounded;
-
- etc_1 [label="...", style=solid, shape=none];
- iced_wgpu;
- }
-
- subgraph cluster_2 {
- label = "shells ";
- labelloc = "b";
- labeljust = "r";
- fontcolor = "#0366d6";
- color="#f6f8fa";
- bgcolor="#f6f8fa";
- style=rounded;
-
- etc_2 [label="...", style=solid, shape=none];
- iced_winit;
- }
-
- subgraph cluster_3 {
- style=invis;
- margin=20;
- iced;
- }
-
- { rank = same; iced_native iced_web }
- { rank = same; iced_wgpu iced_winit etc_1 etc_2 }
-
- iced_core -> iced_native [style=dashed];
- iced_core -> iced_web [style=dashed];
- iced_native -> iced_wgpu;
- iced_native -> iced_winit;
-
- iced_winit -> iced;
- iced_wgpu -> iced;
- iced_web -> iced;
-
- iced -> "cross-platform application";
-
- iced_core [style=dashed];
-
- "cross-platform application" [shape=box, width=2.8, height=0.6];
-}
diff --git a/docs/graphs/ecosystem.png b/docs/graphs/ecosystem.png
index 03fe1130..8b418c52 100644
--- a/docs/graphs/ecosystem.png
+++ b/docs/graphs/ecosystem.png
Binary files differ
diff --git a/docs/graphs/foundations.png b/docs/graphs/foundations.png
new file mode 100644
index 00000000..cc043c99
--- /dev/null
+++ b/docs/graphs/foundations.png
Binary files differ
diff --git a/docs/graphs/generate.sh b/docs/graphs/generate.sh
deleted file mode 100755
index 45073820..00000000
--- a/docs/graphs/generate.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env bash
-
-for file in *.dot
-do
- dot -Tpng ${file} -o ${file%.*}.png
-done
diff --git a/docs/graphs/iced.dot b/docs/graphs/iced.dot
deleted file mode 100644
index 24dbb972..00000000
--- a/docs/graphs/iced.dot
+++ /dev/null
@@ -1,46 +0,0 @@
-digraph G {
- fontname = "Roboto";
- newrank=true;
- node[fontname = "Roboto", style="filled", fontcolor="#333333", fillcolor=white, color="#333333"];
- edge[color="#333333"];
-
- subgraph cluster_1 {
- label = "renderers ";
- labelloc = "b";
- labeljust = "r";
- fontcolor = "#0366d6";
- color="#f6f8fa";
- bgcolor="#f6f8fa";
- style=rounded;
-
- etc_1 [label="...", style=solid, shape=none];
- iced_wgpu;
- }
-
- subgraph cluster_2 {
- label = "shells ";
- labelloc = "b";
- labeljust = "r";
- fontcolor = "#0366d6";
- color="#f6f8fa";
- bgcolor="#f6f8fa";
- style=rounded;
-
- etc_2 [label="...", style=solid, shape=none];
- iced_winit;
- }
-
- subgraph cluster_3 {
- style=invis;
- margin=20;
- iced;
- }
-
- { rank = same; iced_wgpu iced_winit etc_1 etc_2 }
-
- iced_winit -> iced;
- iced_wgpu -> iced;
- iced_web -> iced;
-
- iced;
-}
diff --git a/docs/graphs/iced.png b/docs/graphs/iced.png
index 5d4a35bc..bf777e53 100644
--- a/docs/graphs/iced.png
+++ b/docs/graphs/iced.png
Binary files differ
diff --git a/docs/graphs/native.dot b/docs/graphs/native.dot
deleted file mode 100644
index b57736b5..00000000
--- a/docs/graphs/native.dot
+++ /dev/null
@@ -1,41 +0,0 @@
-digraph G {
- fontname = "Roboto";
- newrank=true;
- node[fontname = "Roboto", style="filled", fontcolor="#333333", fillcolor=white, color="#333333"];
- edge[color="#333333"];
-
- subgraph cluster_1 {
- label = "renderers ";
- labelloc = "b";
- labeljust = "r";
- fontcolor = "#0366d6";
- color="#f6f8fa";
- bgcolor="#f6f8fa";
- style=rounded;
-
- etc_1 [label="...", style=solid, shape=none];
- iced_wgpu;
- }
-
- subgraph cluster_2 {
- label = "shells ";
- labelloc = "b";
- labeljust = "r";
- fontcolor = "#0366d6";
- color="#f6f8fa";
- bgcolor="#f6f8fa";
- style=rounded;
-
- etc_2 [label="...", style=solid, shape=none];
- iced_winit;
- }
-
-
- { rank = same; iced_wgpu iced_winit etc_1 etc_2 }
-
- iced_core -> iced_native [style=dashed];
- iced_native -> iced_wgpu;
- iced_native -> iced_winit;
-
- iced_core [style=dashed];
-}
diff --git a/docs/graphs/native.png b/docs/graphs/native.png
index 892e4fee..6a8759e0 100644
--- a/docs/graphs/native.png
+++ b/docs/graphs/native.png
Binary files differ
diff --git a/docs/graphs/web.dot b/docs/graphs/web.dot
deleted file mode 100644
index 853ca398..00000000
--- a/docs/graphs/web.dot
+++ /dev/null
@@ -1,12 +0,0 @@
-digraph G {
- fontname = "Roboto";
- newrank=true;
- node[fontname = "Roboto", style="filled", fontcolor="#333333", fillcolor=white, color="#333333"];
- edge[color="#333333"];
-
- iced_core -> iced_web [style=dashed];
-
- iced_web -> iced;
-
- iced_core [style=dashed];
-}
diff --git a/docs/graphs/web.png b/docs/graphs/web.png
deleted file mode 100644
index e6a1a5f6..00000000
--- a/docs/graphs/web.png
+++ /dev/null
Binary files differ
diff --git a/docs/graphs/wgpu.dot b/docs/graphs/wgpu.dot
deleted file mode 100644
index 410c2eeb..00000000
--- a/docs/graphs/wgpu.dot
+++ /dev/null
@@ -1,31 +0,0 @@
-digraph G {
- fontname = "Roboto";
- newrank=true;
- node[fontname = "Roboto", style="filled", fontcolor="#333333", fillcolor=white, color="#333333"];
- edge[color="#333333"];
-
- subgraph cluster_1 {
- label = "renderers ";
- labelloc = "b";
- labeljust = "r";
- fontcolor = "#0366d6";
- color="#f6f8fa";
- bgcolor="#f6f8fa";
- style=rounded;
-
- etc_1 [label="...", style=solid, shape=none];
- iced_wgpu;
- }
-
- subgraph cluster_3 {
- style=invis;
- margin=20;
- iced;
- }
-
- { rank = same; iced_wgpu etc_1 }
-
- iced_native -> iced_wgpu;
-
- iced_wgpu -> iced;
-}
diff --git a/docs/graphs/wgpu.png b/docs/graphs/wgpu.png
deleted file mode 100644
index 4831caba..00000000
--- a/docs/graphs/wgpu.png
+++ /dev/null
Binary files differ
diff --git a/docs/graphs/winit.dot b/docs/graphs/winit.dot
deleted file mode 100644
index 4ea5149a..00000000
--- a/docs/graphs/winit.dot
+++ /dev/null
@@ -1,31 +0,0 @@
-digraph G {
- fontname = "Roboto";
- newrank=true;
- node[fontname = "Roboto", style="filled", fontcolor="#333333", fillcolor=white, color="#333333"];
- edge[color="#333333"];
-
- subgraph cluster_2 {
- label = "shells ";
- labelloc = "b";
- labeljust = "r";
- fontcolor = "#0366d6";
- color="#f6f8fa";
- bgcolor="#f6f8fa";
- style=rounded;
-
- etc_2 [label="...", style=solid, shape=none];
- iced_winit;
- }
-
- subgraph cluster_3 {
- style=invis;
- margin=20;
- iced;
- }
-
- { rank = same; iced_winit etc_2 }
-
- iced_native -> iced_winit;
-
- iced_winit -> iced;
-}
diff --git a/docs/graphs/winit.png b/docs/graphs/winit.png
deleted file mode 100644
index 1c028b29..00000000
--- a/docs/graphs/winit.png
+++ /dev/null
Binary files differ
diff --git a/examples/README.md b/examples/README.md
index 04399b93..32ccf724 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -50,6 +50,27 @@ We have not yet implemented a `LocalStorage` version of the auto-save feature. T
[TodoMVC]: http://todomvc.com/
+## [Game of Life](game_of_life)
+An interactive version of the [Game of Life], invented by [John Horton Conway].
+
+It runs a simulation in a background thread while allowing interaction with a `Canvas` that displays an infinite grid with zooming, panning, and drawing support.
+
+The relevant code is located in the __[`main`](game_of_life/src/main.rs)__ file.
+
+<div align="center">
+ <a href="https://gfycat.com/briefaccurateaardvark">
+ <img src="https://thumbs.gfycat.com/BriefAccurateAardvark-size_restricted.gif">
+ </a>
+</div>
+
+You can run it with `cargo run`:
+```
+cargo run --package game_of_life
+```
+
+[Game of Life]: https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life
+[John Horton Conway]: https://en.wikipedia.org/wiki/John_Horton_Conway
+
## [Styling](styling)
An example showcasing custom styling with a light and dark theme.
@@ -69,15 +90,20 @@ cargo run --package styling
## Extras
A bunch of simpler examples exist:
-- [`bezier_tool`](bezier_tool), a Paint-like tool for drawing Bézier curves using [`lyon`].
+- [`bezier_tool`](bezier_tool), a Paint-like tool for drawing Bézier curves using the `Canvas` widget.
- [`clock`](clock), an application that uses the `Canvas` widget to draw a clock and its hands to display the current time.
+- [`color_palette`](color_palette), a color palette generator based on a user-defined root color.
- [`counter`](counter), the classic counter example explained in the [`README`](../README.md).
- [`custom_widget`](custom_widget), a demonstration of how to build a custom widget that draws a circle.
+- [`download_progress`](download_progress), a basic application that asynchronously downloads a dummy file of 100 MB and tracks the download progress.
- [`events`](events), a log of native events displayed using a conditional `Subscription`.
- [`geometry`](geometry), a custom widget showcasing how to draw geometry with the `Mesh2D` primitive in [`iced_wgpu`](../wgpu).
- [`integration`](integration), a demonstration of how to integrate Iced in an existing graphical application.
+- [`pane_grid`](pane_grid), a grid of panes that can be split, resized, and reorganized.
+- [`pick_list`](pick_list), a dropdown list of selectable options.
- [`pokedex`](pokedex), an application that displays a random Pokédex entry (sprite included!) by using the [PokéAPI].
- [`progress_bar`](progress_bar), a simple progress bar that can be filled by using a slider.
+- [`scrollable`](scrollable), a showcase of the various scrollbar width options.
- [`solar_system`](solar_system), an animated solar system drawn using the `Canvas` widget and showcasing how to compose different transforms.
- [`stopwatch`](stopwatch), a watch with start/stop and reset buttons showcasing how to listen to time.
- [`svg`](svg), an application that renders the [Ghostscript Tiger] by leveraging the `Svg` widget.
diff --git a/examples/bezier_tool/Cargo.toml b/examples/bezier_tool/Cargo.toml
index b13a0aa5..a88975a7 100644
--- a/examples/bezier_tool/Cargo.toml
+++ b/examples/bezier_tool/Cargo.toml
@@ -6,7 +6,4 @@ edition = "2018"
publish = false
[dependencies]
-iced = { path = "../.." }
-iced_native = { path = "../../native" }
-iced_wgpu = { path = "../../wgpu" }
-lyon = "0.15"
+iced = { path = "../..", features = ["canvas"] }
diff --git a/examples/bezier_tool/README.md b/examples/bezier_tool/README.md
index 933f2120..ebbb12cc 100644
--- a/examples/bezier_tool/README.md
+++ b/examples/bezier_tool/README.md
@@ -1,6 +1,6 @@
## Bézier tool
-A Paint-like tool for drawing Bézier curves using [`lyon`].
+A Paint-like tool for drawing Bézier curves using the `Canvas` widget.
The __[`main`]__ file contains all the code of the example.
@@ -16,4 +16,3 @@ cargo run --package bezier_tool
```
[`main`]: src/main.rs
-[`lyon`]: https://github.com/nical/lyon
diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs
index c3fbf276..97832e01 100644
--- a/examples/bezier_tool/src/main.rs
+++ b/examples/bezier_tool/src/main.rs
@@ -1,294 +1,13 @@
-//! This example showcases a simple native custom widget that renders arbitrary
-//! path with `lyon`.
-mod bezier {
- // For now, to implement a custom native widget you will need to add
- // `iced_native` and `iced_wgpu` to your dependencies.
- //
- // Then, you simply need to define your widget type and implement the
- // `iced_native::Widget` trait with the `iced_wgpu::Renderer`.
- //
- // Of course, you can choose to make the implementation renderer-agnostic,
- // if you wish to, by creating your own `Renderer` trait, which could be
- // implemented by `iced_wgpu` and other renderers.
- use iced_native::{
- input, layout, Clipboard, Color, Element, Event, Font, Hasher,
- HorizontalAlignment, Layout, Length, MouseCursor, Point, Size, Vector,
- VerticalAlignment, Widget,
- };
- use iced_wgpu::{
- triangle::{Mesh2D, Vertex2D},
- Defaults, Primitive, Renderer,
- };
- use lyon::tessellation::{
- basic_shapes, BuffersBuilder, StrokeAttributes, StrokeOptions,
- StrokeTessellator, VertexBuffers,
- };
-
- pub struct Bezier<'a, Message> {
- state: &'a mut State,
- curves: &'a [Curve],
- // [from, to, ctrl]
- on_click: Box<dyn Fn(Curve) -> Message>,
- }
-
- #[derive(Debug, Clone, Copy)]
- pub struct Curve {
- from: Point,
- to: Point,
- control: Point,
- }
-
- #[derive(Default)]
- pub struct State {
- pending: Option<Pending>,
- }
-
- enum Pending {
- One { from: Point },
- Two { from: Point, to: Point },
- }
-
- impl<'a, Message> Bezier<'a, Message> {
- pub fn new<F>(
- state: &'a mut State,
- curves: &'a [Curve],
- on_click: F,
- ) -> Self
- where
- F: 'static + Fn(Curve) -> Message,
- {
- Self {
- state,
- curves,
- on_click: Box::new(on_click),
- }
- }
- }
-
- impl<'a, Message> Widget<Message, Renderer> for Bezier<'a, Message> {
- fn width(&self) -> Length {
- Length::Fill
- }
-
- fn height(&self) -> Length {
- Length::Fill
- }
-
- fn layout(
- &self,
- _renderer: &Renderer,
- limits: &layout::Limits,
- ) -> layout::Node {
- let size = limits
- .height(Length::Fill)
- .width(Length::Fill)
- .resolve(Size::ZERO);
- layout::Node::new(size)
- }
-
- fn draw(
- &self,
- _renderer: &mut Renderer,
- defaults: &Defaults,
- layout: Layout<'_>,
- cursor_position: Point,
- ) -> (Primitive, MouseCursor) {
- let mut buffer: VertexBuffers<Vertex2D, u32> = VertexBuffers::new();
- let mut path_builder = lyon::path::Path::builder();
-
- let bounds = layout.bounds();
-
- // Draw rectangle border with lyon.
- basic_shapes::stroke_rectangle(
- &lyon::math::Rect::new(
- lyon::math::Point::new(0.5, 0.5),
- lyon::math::Size::new(
- bounds.width - 1.0,
- bounds.height - 1.0,
- ),
- ),
- &StrokeOptions::default().with_line_width(1.0),
- &mut BuffersBuilder::new(
- &mut buffer,
- |pos: lyon::math::Point, _: StrokeAttributes| Vertex2D {
- position: pos.to_array(),
- color: [0.0, 0.0, 0.0, 1.0],
- },
- ),
- )
- .unwrap();
-
- for curve in self.curves {
- path_builder.move_to(lyon::math::Point::new(
- curve.from.x,
- curve.from.y,
- ));
-
- path_builder.quadratic_bezier_to(
- lyon::math::Point::new(curve.control.x, curve.control.y),
- lyon::math::Point::new(curve.to.x, curve.to.y),
- );
- }
-
- match self.state.pending {
- None => {}
- Some(Pending::One { from }) => {
- path_builder
- .move_to(lyon::math::Point::new(from.x, from.y));
- path_builder.line_to(lyon::math::Point::new(
- cursor_position.x - bounds.x,
- cursor_position.y - bounds.y,
- ));
- }
- Some(Pending::Two { from, to }) => {
- path_builder
- .move_to(lyon::math::Point::new(from.x, from.y));
- path_builder.quadratic_bezier_to(
- lyon::math::Point::new(
- cursor_position.x - bounds.x,
- cursor_position.y - bounds.y,
- ),
- lyon::math::Point::new(to.x, to.y),
- );
- }
- }
-
- let mut tessellator = StrokeTessellator::new();
-
- // Draw strokes with lyon.
- tessellator
- .tessellate(
- &path_builder.build(),
- &StrokeOptions::default().with_line_width(3.0),
- &mut BuffersBuilder::new(
- &mut buffer,
- |pos: lyon::math::Point, _: StrokeAttributes| {
- Vertex2D {
- position: pos.to_array(),
- color: [0.0, 0.0, 0.0, 1.0],
- }
- },
- ),
- )
- .unwrap();
-
- let mesh = Primitive::Mesh2D {
- origin: Point::new(bounds.x, bounds.y),
- buffers: Mesh2D {
- vertices: buffer.vertices,
- indices: buffer.indices,
- },
- };
-
- (
- Primitive::Clip {
- bounds,
- offset: Vector::new(0, 0),
- content: Box::new(
- if self.curves.is_empty()
- && self.state.pending.is_none()
- {
- let instructions = Primitive::Text {
- bounds,
- color: Color {
- a: defaults.text.color.a * 0.7,
- ..defaults.text.color
- },
- content: String::from(
- "Click to create bezier curves!",
- ),
- font: Font::Default,
- size: 30.0,
- horizontal_alignment:
- HorizontalAlignment::Center,
- vertical_alignment: VerticalAlignment::Center,
- };
-
- Primitive::Group {
- primitives: vec![mesh, instructions],
- }
- } else {
- mesh
- },
- ),
- },
- MouseCursor::OutOfBounds,
- )
- }
-
- fn hash_layout(&self, _state: &mut Hasher) {}
-
- fn on_event(
- &mut self,
- event: Event,
- layout: Layout<'_>,
- cursor_position: Point,
- messages: &mut Vec<Message>,
- _renderer: &Renderer,
- _clipboard: Option<&dyn Clipboard>,
- ) {
- let bounds = layout.bounds();
-
- if bounds.contains(cursor_position) {
- match event {
- Event::Mouse(input::mouse::Event::Input {
- state: input::ButtonState::Pressed,
- ..
- }) => {
- let new_point = Point::new(
- cursor_position.x - bounds.x,
- cursor_position.y - bounds.y,
- );
-
- match self.state.pending {
- None => {
- self.state.pending =
- Some(Pending::One { from: new_point });
- }
- Some(Pending::One { from }) => {
- self.state.pending = Some(Pending::Two {
- from,
- to: new_point,
- });
- }
- Some(Pending::Two { from, to }) => {
- self.state.pending = None;
-
- messages.push((self.on_click)(Curve {
- from,
- to,
- control: new_point,
- }));
- }
- }
- }
- _ => {}
- }
- }
- }
- }
-
- impl<'a, Message> Into<Element<'a, Message, Renderer>> for Bezier<'a, Message>
- where
- Message: 'static,
- {
- fn into(self) -> Element<'a, Message, Renderer> {
- Element::new(self)
- }
- }
-}
-
-use bezier::Bezier;
+//! This example showcases an interactive `Canvas` for drawing Bézier curves.
use iced::{
- button, Align, Button, Column, Container, Element, Length, Sandbox,
- Settings, Text,
+ button, Align, Button, Column, Element, Length, Sandbox, Settings, Text,
};
-pub fn main() {
+pub fn main() -> iced::Result {
Example::run(Settings {
antialiasing: true,
..Settings::default()
- });
+ })
}
#[derive(Default)]
@@ -319,6 +38,7 @@ impl Sandbox for Example {
match message {
Message::AddCurve(curve) => {
self.curves.push(curve);
+ self.bezier.request_redraw();
}
Message::Clear => {
self.bezier = bezier::State::default();
@@ -328,7 +48,7 @@ impl Sandbox for Example {
}
fn view(&mut self) -> Element<Message> {
- let content = Column::new()
+ Column::new()
.padding(20)
.spacing(20)
.align_items(Align::Center)
@@ -337,22 +57,189 @@ impl Sandbox for Example {
.width(Length::Shrink)
.size(50),
)
- .push(Bezier::new(
- &mut self.bezier,
- self.curves.as_slice(),
- Message::AddCurve,
- ))
+ .push(self.bezier.view(&self.curves).map(Message::AddCurve))
.push(
Button::new(&mut self.button_state, Text::new("Clear"))
.padding(8)
.on_press(Message::Clear),
- );
+ )
+ .into()
+ }
+}
+
+mod bezier {
+ use iced::{
+ canvas::event::{self, Event},
+ canvas::{self, Canvas, Cursor, Frame, Geometry, Path, Stroke},
+ mouse, Element, Length, Point, Rectangle,
+ };
+
+ #[derive(Default)]
+ pub struct State {
+ pending: Option<Pending>,
+ cache: canvas::Cache,
+ }
- Container::new(content)
+ impl State {
+ pub fn view<'a>(
+ &'a mut self,
+ curves: &'a [Curve],
+ ) -> Element<'a, Curve> {
+ Canvas::new(Bezier {
+ state: self,
+ curves,
+ })
.width(Length::Fill)
.height(Length::Fill)
- .center_x()
- .center_y()
.into()
+ }
+
+ pub fn request_redraw(&mut self) {
+ self.cache.clear()
+ }
+ }
+
+ struct Bezier<'a> {
+ state: &'a mut State,
+ curves: &'a [Curve],
+ }
+
+ impl<'a> canvas::Program<Curve> for Bezier<'a> {
+ fn update(
+ &mut self,
+ event: Event,
+ bounds: Rectangle,
+ cursor: Cursor,
+ ) -> (event::Status, Option<Curve>) {
+ let cursor_position =
+ if let Some(position) = cursor.position_in(&bounds) {
+ position
+ } else {
+ return (event::Status::Ignored, None);
+ };
+
+ match event {
+ Event::Mouse(mouse_event) => {
+ let message = match mouse_event {
+ mouse::Event::ButtonPressed(mouse::Button::Left) => {
+ match self.state.pending {
+ None => {
+ self.state.pending = Some(Pending::One {
+ from: cursor_position,
+ });
+
+ None
+ }
+ Some(Pending::One { from }) => {
+ self.state.pending = Some(Pending::Two {
+ from,
+ to: cursor_position,
+ });
+
+ None
+ }
+ Some(Pending::Two { from, to }) => {
+ self.state.pending = None;
+
+ Some(Curve {
+ from,
+ to,
+ control: cursor_position,
+ })
+ }
+ }
+ }
+ _ => None,
+ };
+
+ (event::Status::Captured, message)
+ }
+ _ => (event::Status::Ignored, None),
+ }
+ }
+
+ fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec<Geometry> {
+ let content =
+ self.state.cache.draw(bounds.size(), |frame: &mut Frame| {
+ Curve::draw_all(self.curves, frame);
+
+ frame.stroke(
+ &Path::rectangle(Point::ORIGIN, frame.size()),
+ Stroke::default(),
+ );
+ });
+
+ if let Some(pending) = &self.state.pending {
+ let pending_curve = pending.draw(bounds, cursor);
+
+ vec![content, pending_curve]
+ } else {
+ vec![content]
+ }
+ }
+
+ fn mouse_interaction(
+ &self,
+ bounds: Rectangle,
+ cursor: Cursor,
+ ) -> mouse::Interaction {
+ if cursor.is_over(&bounds) {
+ mouse::Interaction::Crosshair
+ } else {
+ mouse::Interaction::default()
+ }
+ }
+ }
+
+ #[derive(Debug, Clone, Copy)]
+ pub struct Curve {
+ from: Point,
+ to: Point,
+ control: Point,
+ }
+
+ impl Curve {
+ fn draw_all(curves: &[Curve], frame: &mut Frame) {
+ let curves = Path::new(|p| {
+ for curve in curves {
+ p.move_to(curve.from);
+ p.quadratic_curve_to(curve.control, curve.to);
+ }
+ });
+
+ frame.stroke(&curves, Stroke::default().with_width(2.0));
+ }
+ }
+
+ #[derive(Debug, Clone, Copy)]
+ enum Pending {
+ One { from: Point },
+ Two { from: Point, to: Point },
+ }
+
+ impl Pending {
+ fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Geometry {
+ let mut frame = Frame::new(bounds.size());
+
+ if let Some(cursor_position) = cursor.position_in(&bounds) {
+ match *self {
+ Pending::One { from } => {
+ let line = Path::line(from, cursor_position);
+ frame.stroke(&line, Stroke::default().with_width(2.0));
+ }
+ Pending::Two { from, to } => {
+ let curve = Curve {
+ from,
+ to,
+ control: cursor_position,
+ };
+
+ Curve::draw_all(&[curve], &mut frame);
+ }
+ };
+ }
+
+ frame.into_geometry()
+ }
}
}
diff --git a/examples/clock/Cargo.toml b/examples/clock/Cargo.toml
index 308cbfbb..c6e32379 100644
--- a/examples/clock/Cargo.toml
+++ b/examples/clock/Cargo.toml
@@ -5,11 +5,6 @@ authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
publish = false
-[features]
-canvas = []
-
[dependencies]
-iced = { path = "../..", features = ["canvas", "async-std", "debug"] }
-iced_native = { path = "../../native" }
+iced = { path = "../..", features = ["canvas", "tokio", "debug"] }
chrono = "0.4"
-async-std = { version = "1.0", features = ["unstable"] }
diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs
index d8266f06..b317ac00 100644
--- a/examples/clock/src/main.rs
+++ b/examples/clock/src/main.rs
@@ -1,9 +1,10 @@
use iced::{
- canvas, executor, Application, Canvas, Color, Command, Container, Element,
- Length, Point, Settings, Subscription, Vector,
+ canvas::{self, Cache, Canvas, Cursor, Geometry, LineCap, Path, Stroke},
+ executor, time, Application, Color, Command, Container, Element, Length,
+ Point, Rectangle, Settings, Subscription, Vector,
};
-pub fn main() {
+pub fn main() -> iced::Result {
Clock::run(Settings {
antialiasing: true,
..Settings::default()
@@ -11,8 +12,8 @@ pub fn main() {
}
struct Clock {
- now: LocalTime,
- clock: canvas::layer::Cache<LocalTime>,
+ now: chrono::DateTime<chrono::Local>,
+ clock: Cache,
}
#[derive(Debug, Clone, Copy)]
@@ -23,12 +24,13 @@ enum Message {
impl Application for Clock {
type Executor = executor::Default;
type Message = Message;
+ type Flags = ();
- fn new() -> (Self, Command<Message>) {
+ fn new(_flags: ()) -> (Self, Command<Message>) {
(
Clock {
- now: chrono::Local::now().into(),
- clock: canvas::layer::Cache::new(),
+ now: chrono::Local::now(),
+ clock: Default::default(),
},
Command::none(),
)
@@ -41,7 +43,7 @@ impl Application for Clock {
fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::Tick(local_time) => {
- let now = local_time.into();
+ let now = local_time;
if now != self.now {
self.now = now;
@@ -54,140 +56,78 @@ impl Application for Clock {
}
fn subscription(&self) -> Subscription<Message> {
- time::every(std::time::Duration::from_millis(500)).map(Message::Tick)
+ time::every(std::time::Duration::from_millis(500))
+ .map(|_| Message::Tick(chrono::Local::now()))
}
fn view(&mut self) -> Element<Message> {
- let canvas = Canvas::new()
+ let canvas = Canvas::new(self)
.width(Length::Units(400))
- .height(Length::Units(400))
- .push(self.clock.with(&self.now));
+ .height(Length::Units(400));
Container::new(canvas)
.width(Length::Fill)
.height(Length::Fill)
+ .padding(20)
.center_x()
.center_y()
.into()
}
}
-#[derive(Debug, PartialEq, Eq)]
-struct LocalTime {
- hour: u32,
- minute: u32,
- second: u32,
-}
-
-impl From<chrono::DateTime<chrono::Local>> for LocalTime {
- fn from(date_time: chrono::DateTime<chrono::Local>) -> LocalTime {
+impl canvas::Program<Message> for Clock {
+ fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry> {
use chrono::Timelike;
- LocalTime {
- hour: date_time.hour(),
- minute: date_time.minute(),
- second: date_time.second(),
- }
- }
-}
+ let clock = self.clock.draw(bounds.size(), |frame| {
+ let center = frame.center();
+ let radius = frame.width().min(frame.height()) / 2.0;
-impl canvas::Drawable for LocalTime {
- fn draw(&self, frame: &mut canvas::Frame) {
- let center = frame.center();
- let radius = frame.width().min(frame.height()) / 2.0;
- let offset = Vector::new(center.x, center.y);
-
- let clock = canvas::Path::new(|path| path.circle(center, radius));
-
- frame.fill(
- &clock,
- canvas::Fill::Color(Color::from_rgb8(0x12, 0x93, 0xD8)),
- );
-
- fn draw_hand(
- n: u32,
- total: u32,
- length: f32,
- offset: Vector,
- path: &mut canvas::path::Builder,
- ) {
- let turns = n as f32 / total as f32;
- let t = 2.0 * std::f32::consts::PI * (turns - 0.25);
-
- let x = length * t.cos();
- let y = length * t.sin();
-
- path.line_to(Point::new(x, y) + offset);
- }
+ let background = Path::circle(center, radius);
+ frame.fill(&background, Color::from_rgb8(0x12, 0x93, 0xD8));
- let hour_and_minute_hands = canvas::Path::new(|path| {
- path.move_to(center);
- draw_hand(self.hour, 12, 0.5 * radius, offset, path);
+ let short_hand =
+ Path::line(Point::ORIGIN, Point::new(0.0, -0.5 * radius));
- path.move_to(center);
- draw_hand(self.minute, 60, 0.8 * radius, offset, path)
- });
+ let long_hand =
+ Path::line(Point::ORIGIN, Point::new(0.0, -0.8 * radius));
- frame.stroke(
- &hour_and_minute_hands,
- canvas::Stroke {
- width: 6.0,
+ let thin_stroke = Stroke {
+ width: radius / 100.0,
color: Color::WHITE,
- line_cap: canvas::LineCap::Round,
- ..canvas::Stroke::default()
- },
- );
-
- let second_hand = canvas::Path::new(|path| {
- path.move_to(center);
- draw_hand(self.second, 60, 0.8 * radius, offset, path)
+ line_cap: LineCap::Round,
+ ..Stroke::default()
+ };
+
+ let wide_stroke = Stroke {
+ width: thin_stroke.width * 3.0,
+ ..thin_stroke
+ };
+
+ frame.translate(Vector::new(center.x, center.y));
+
+ frame.with_save(|frame| {
+ frame.rotate(hand_rotation(self.now.hour(), 12));
+ frame.stroke(&short_hand, wide_stroke);
+ });
+
+ frame.with_save(|frame| {
+ frame.rotate(hand_rotation(self.now.minute(), 60));
+ frame.stroke(&long_hand, wide_stroke);
+ });
+
+ frame.with_save(|frame| {
+ frame.rotate(hand_rotation(self.now.second(), 60));
+ frame.stroke(&long_hand, thin_stroke);
+ })
});
- frame.stroke(
- &second_hand,
- canvas::Stroke {
- width: 3.0,
- color: Color::WHITE,
- line_cap: canvas::LineCap::Round,
- ..canvas::Stroke::default()
- },
- );
+ vec![clock]
}
}
-mod time {
- use iced::futures;
+fn hand_rotation(n: u32, total: u32) -> f32 {
+ let turns = n as f32 / total as f32;
- pub fn every(
- duration: std::time::Duration,
- ) -> iced::Subscription<chrono::DateTime<chrono::Local>> {
- iced::Subscription::from_recipe(Every(duration))
- }
-
- struct Every(std::time::Duration);
-
- impl<H, I> iced_native::subscription::Recipe<H, I> for Every
- where
- H: std::hash::Hasher,
- {
- type Output = chrono::DateTime<chrono::Local>;
-
- 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, I>,
- ) -> futures::stream::BoxStream<'static, Self::Output> {
- use futures::stream::StreamExt;
-
- async_std::stream::interval(self.0)
- .map(|_| chrono::Local::now())
- .boxed()
- }
- }
+ 2.0 * std::f32::consts::PI * turns
}
diff --git a/examples/color_palette/Cargo.toml b/examples/color_palette/Cargo.toml
new file mode 100644
index 00000000..00f33e20
--- /dev/null
+++ b/examples/color_palette/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "color_palette"
+version = "0.1.0"
+authors = ["Clark Moody <clark@clarkmoody.com>"]
+edition = "2018"
+publish = false
+
+[dependencies]
+iced = { path = "../..", features = ["canvas", "palette"] }
+palette = "0.5.0"
diff --git a/examples/color_palette/README.md b/examples/color_palette/README.md
new file mode 100644
index 00000000..e70188f8
--- /dev/null
+++ b/examples/color_palette/README.md
@@ -0,0 +1,15 @@
+## Color palette
+
+A color palette generator, based on a user-defined root color.
+
+<div align="center">
+ <a href="https://gfycat.com/dirtylonebighornsheep">
+ <img src="https://github.com/hecrj/iced/raw/1a8d253611d3796b0a32b2f096bb54565a5292e0/examples/color_palette/screenshot.png">
+ </a>
+</div>
+
+You can run it with `cargo run`:
+
+```
+cargo run --package color_palette
+```
diff --git a/examples/color_palette/screenshot.png b/examples/color_palette/screenshot.png
new file mode 100644
index 00000000..aa4772e0
--- /dev/null
+++ b/examples/color_palette/screenshot.png
Binary files differ
diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs
new file mode 100644
index 00000000..bb2c61cb
--- /dev/null
+++ b/examples/color_palette/src/main.rs
@@ -0,0 +1,462 @@
+use iced::canvas::{self, Cursor, Frame, Geometry, Path};
+use iced::{
+ slider, Align, Canvas, Color, Column, Element, HorizontalAlignment, Length,
+ Point, Rectangle, Row, Sandbox, Settings, Size, Slider, Text, Vector,
+ VerticalAlignment,
+};
+use palette::{self, Hsl, Limited, Srgb};
+use std::marker::PhantomData;
+use std::ops::RangeInclusive;
+
+pub fn main() -> iced::Result {
+ ColorPalette::run(Settings {
+ antialiasing: true,
+ ..Settings::default()
+ })
+}
+
+#[derive(Default)]
+pub struct ColorPalette {
+ theme: Theme,
+ rgb: ColorPicker<Color>,
+ hsl: ColorPicker<palette::Hsl>,
+ hsv: ColorPicker<palette::Hsv>,
+ hwb: ColorPicker<palette::Hwb>,
+ lab: ColorPicker<palette::Lab>,
+ lch: ColorPicker<palette::Lch>,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub enum Message {
+ RgbColorChanged(Color),
+ HslColorChanged(palette::Hsl),
+ HsvColorChanged(palette::Hsv),
+ HwbColorChanged(palette::Hwb),
+ LabColorChanged(palette::Lab),
+ LchColorChanged(palette::Lch),
+}
+
+impl Sandbox for ColorPalette {
+ type Message = Message;
+
+ fn new() -> Self {
+ Self::default()
+ }
+
+ fn title(&self) -> String {
+ String::from("Color palette - Iced")
+ }
+
+ fn update(&mut self, message: Message) {
+ let srgb = match message {
+ Message::RgbColorChanged(rgb) => palette::Srgb::from(rgb),
+ Message::HslColorChanged(hsl) => palette::Srgb::from(hsl),
+ Message::HsvColorChanged(hsv) => palette::Srgb::from(hsv),
+ Message::HwbColorChanged(hwb) => palette::Srgb::from(hwb),
+ Message::LabColorChanged(lab) => palette::Srgb::from(lab),
+ Message::LchColorChanged(lch) => palette::Srgb::from(lch),
+ };
+
+ self.theme = Theme::new(srgb.clamp());
+ }
+
+ fn view(&mut self) -> Element<Message> {
+ let base = self.theme.base;
+
+ let srgb = palette::Srgb::from(base);
+ let hsl = palette::Hsl::from(srgb);
+ let hsv = palette::Hsv::from(srgb);
+ let hwb = palette::Hwb::from(srgb);
+ let lab = palette::Lab::from(srgb);
+ let lch = palette::Lch::from(srgb);
+
+ Column::new()
+ .padding(10)
+ .spacing(10)
+ .push(self.rgb.view(base).map(Message::RgbColorChanged))
+ .push(self.hsl.view(hsl).map(Message::HslColorChanged))
+ .push(self.hsv.view(hsv).map(Message::HsvColorChanged))
+ .push(self.hwb.view(hwb).map(Message::HwbColorChanged))
+ .push(self.lab.view(lab).map(Message::LabColorChanged))
+ .push(self.lch.view(lch).map(Message::LchColorChanged))
+ .push(self.theme.view())
+ .into()
+ }
+}
+
+#[derive(Debug)]
+pub struct Theme {
+ lower: Vec<Color>,
+ base: Color,
+ higher: Vec<Color>,
+ canvas_cache: canvas::Cache,
+}
+
+impl Theme {
+ pub fn new(base: impl Into<Color>) -> Theme {
+ use palette::{Hue, Shade};
+
+ let base = base.into();
+
+ // Convert to HSL color for manipulation
+ let hsl = Hsl::from(Srgb::from(base));
+
+ let lower = [
+ hsl.shift_hue(-135.0).lighten(0.075),
+ hsl.shift_hue(-120.0),
+ hsl.shift_hue(-105.0).darken(0.075),
+ hsl.darken(0.075),
+ ];
+
+ let higher = [
+ hsl.lighten(0.075),
+ hsl.shift_hue(105.0).darken(0.075),
+ hsl.shift_hue(120.0),
+ hsl.shift_hue(135.0).lighten(0.075),
+ ];
+
+ Theme {
+ lower: lower
+ .iter()
+ .map(|&color| Srgb::from(color).clamp().into())
+ .collect(),
+ base,
+ higher: higher
+ .iter()
+ .map(|&color| Srgb::from(color).clamp().into())
+ .collect(),
+ canvas_cache: canvas::Cache::default(),
+ }
+ }
+
+ pub fn len(&self) -> usize {
+ self.lower.len() + self.higher.len() + 1
+ }
+
+ pub fn colors(&self) -> impl Iterator<Item = &Color> {
+ self.lower
+ .iter()
+ .chain(std::iter::once(&self.base))
+ .chain(self.higher.iter())
+ }
+
+ pub fn view(&mut self) -> Element<Message> {
+ Canvas::new(self)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .into()
+ }
+
+ fn draw(&self, frame: &mut Frame) {
+ let pad = 20.0;
+
+ let box_size = Size {
+ width: frame.width() / self.len() as f32,
+ height: frame.height() / 2.0 - pad,
+ };
+
+ let triangle = Path::new(|path| {
+ path.move_to(Point { x: 0.0, y: -0.5 });
+ path.line_to(Point { x: -0.5, y: 0.0 });
+ path.line_to(Point { x: 0.5, y: 0.0 });
+ path.close();
+ });
+
+ let mut text = canvas::Text {
+ horizontal_alignment: HorizontalAlignment::Center,
+ vertical_alignment: VerticalAlignment::Top,
+ size: 15.0,
+ ..canvas::Text::default()
+ };
+
+ for (i, &color) in self.colors().enumerate() {
+ let anchor = Point {
+ x: (i as f32) * box_size.width,
+ y: 0.0,
+ };
+ frame.fill_rectangle(anchor, box_size, color);
+
+ // We show a little indicator for the base color
+ if color == self.base {
+ let triangle_x = anchor.x + box_size.width / 2.0;
+
+ frame.with_save(|frame| {
+ frame.translate(Vector::new(triangle_x, 0.0));
+ frame.scale(10.0);
+ frame.rotate(std::f32::consts::PI);
+
+ frame.fill(&triangle, Color::WHITE);
+ });
+
+ frame.with_save(|frame| {
+ frame.translate(Vector::new(triangle_x, box_size.height));
+ frame.scale(10.0);
+
+ frame.fill(&triangle, Color::WHITE);
+ });
+ }
+
+ frame.fill_text(canvas::Text {
+ content: color_hex_string(&color),
+ position: Point {
+ x: anchor.x + box_size.width / 2.0,
+ y: box_size.height,
+ },
+ ..text
+ });
+ }
+
+ text.vertical_alignment = VerticalAlignment::Bottom;
+
+ let hsl = Hsl::from(Srgb::from(self.base));
+ for i in 0..self.len() {
+ let pct = (i as f32 + 1.0) / (self.len() as f32 + 1.0);
+ let graded = Hsl {
+ lightness: 1.0 - pct,
+ ..hsl
+ };
+ let color: Color = Srgb::from(graded.clamp()).into();
+
+ let anchor = Point {
+ x: (i as f32) * box_size.width,
+ y: box_size.height + 2.0 * pad,
+ };
+
+ frame.fill_rectangle(anchor, box_size, color);
+
+ frame.fill_text(canvas::Text {
+ content: color_hex_string(&color),
+ position: Point {
+ x: anchor.x + box_size.width / 2.0,
+ y: box_size.height + 2.0 * pad,
+ },
+ ..text
+ });
+ }
+ }
+}
+
+impl canvas::Program<Message> for Theme {
+ fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry> {
+ let theme = self.canvas_cache.draw(bounds.size(), |frame| {
+ self.draw(frame);
+ });
+
+ vec![theme]
+ }
+}
+
+impl Default for Theme {
+ fn default() -> Self {
+ Theme::new(Color::from_rgb8(75, 128, 190))
+ }
+}
+
+fn color_hex_string(color: &Color) -> String {
+ format!(
+ "#{:x}{:x}{:x}",
+ (255.0 * color.r).round() as u8,
+ (255.0 * color.g).round() as u8,
+ (255.0 * color.b).round() as u8
+ )
+}
+
+#[derive(Default)]
+struct ColorPicker<C: ColorSpace> {
+ sliders: [slider::State; 3],
+ color_space: PhantomData<C>,
+}
+
+trait ColorSpace: Sized {
+ const LABEL: &'static str;
+ const COMPONENT_RANGES: [RangeInclusive<f64>; 3];
+
+ fn new(a: f32, b: f32, c: f32) -> Self;
+
+ fn components(&self) -> [f32; 3];
+
+ fn to_string(&self) -> String;
+}
+
+impl<C: 'static + ColorSpace + Copy> ColorPicker<C> {
+ fn view(&mut self, color: C) -> Element<C> {
+ let [c1, c2, c3] = color.components();
+ let [s1, s2, s3] = &mut self.sliders;
+ let [cr1, cr2, cr3] = C::COMPONENT_RANGES;
+
+ fn slider<C: Clone>(
+ state: &mut slider::State,
+ range: RangeInclusive<f64>,
+ component: f32,
+ update: impl Fn(f32) -> C + 'static,
+ ) -> Slider<f64, C> {
+ Slider::new(state, range, f64::from(component), move |v| {
+ update(v as f32)
+ })
+ .step(0.01)
+ }
+
+ Row::new()
+ .spacing(10)
+ .align_items(Align::Center)
+ .push(Text::new(C::LABEL).width(Length::Units(50)))
+ .push(slider(s1, cr1, c1, move |v| C::new(v, c2, c3)))
+ .push(slider(s2, cr2, c2, move |v| C::new(c1, v, c3)))
+ .push(slider(s3, cr3, c3, move |v| C::new(c1, c2, v)))
+ .push(
+ Text::new(color.to_string())
+ .width(Length::Units(185))
+ .size(14),
+ )
+ .into()
+ }
+}
+
+impl ColorSpace for Color {
+ const LABEL: &'static str = "RGB";
+ const COMPONENT_RANGES: [RangeInclusive<f64>; 3] =
+ [0.0..=1.0, 0.0..=1.0, 0.0..=1.0];
+
+ fn new(r: f32, g: f32, b: f32) -> Self {
+ Color::from_rgb(r, g, b)
+ }
+
+ fn components(&self) -> [f32; 3] {
+ [self.r, self.g, self.b]
+ }
+
+ fn to_string(&self) -> String {
+ format!(
+ "rgb({:.0}, {:.0}, {:.0})",
+ 255.0 * self.r,
+ 255.0 * self.g,
+ 255.0 * self.b
+ )
+ }
+}
+
+impl ColorSpace for palette::Hsl {
+ const LABEL: &'static str = "HSL";
+ const COMPONENT_RANGES: [RangeInclusive<f64>; 3] =
+ [0.0..=360.0, 0.0..=1.0, 0.0..=1.0];
+
+ fn new(hue: f32, saturation: f32, lightness: f32) -> Self {
+ palette::Hsl::new(
+ palette::RgbHue::from_degrees(hue),
+ saturation,
+ lightness,
+ )
+ }
+
+ fn components(&self) -> [f32; 3] {
+ [
+ self.hue.to_positive_degrees(),
+ self.saturation,
+ self.lightness,
+ ]
+ }
+
+ fn to_string(&self) -> String {
+ format!(
+ "hsl({:.1}, {:.1}%, {:.1}%)",
+ self.hue.to_positive_degrees(),
+ 100.0 * self.saturation,
+ 100.0 * self.lightness
+ )
+ }
+}
+
+impl ColorSpace for palette::Hsv {
+ const LABEL: &'static str = "HSV";
+ const COMPONENT_RANGES: [RangeInclusive<f64>; 3] =
+ [0.0..=360.0, 0.0..=1.0, 0.0..=1.0];
+
+ fn new(hue: f32, saturation: f32, value: f32) -> Self {
+ palette::Hsv::new(palette::RgbHue::from_degrees(hue), saturation, value)
+ }
+
+ fn components(&self) -> [f32; 3] {
+ [self.hue.to_positive_degrees(), self.saturation, self.value]
+ }
+
+ fn to_string(&self) -> String {
+ format!(
+ "hsv({:.1}, {:.1}%, {:.1}%)",
+ self.hue.to_positive_degrees(),
+ 100.0 * self.saturation,
+ 100.0 * self.value
+ )
+ }
+}
+
+impl ColorSpace for palette::Hwb {
+ const LABEL: &'static str = "HWB";
+ const COMPONENT_RANGES: [RangeInclusive<f64>; 3] =
+ [0.0..=360.0, 0.0..=1.0, 0.0..=1.0];
+
+ fn new(hue: f32, whiteness: f32, blackness: f32) -> Self {
+ palette::Hwb::new(
+ palette::RgbHue::from_degrees(hue),
+ whiteness,
+ blackness,
+ )
+ }
+
+ fn components(&self) -> [f32; 3] {
+ [
+ self.hue.to_positive_degrees(),
+ self.whiteness,
+ self.blackness,
+ ]
+ }
+
+ fn to_string(&self) -> String {
+ format!(
+ "hwb({:.1}, {:.1}%, {:.1}%)",
+ self.hue.to_positive_degrees(),
+ 100.0 * self.whiteness,
+ 100.0 * self.blackness
+ )
+ }
+}
+
+impl ColorSpace for palette::Lab {
+ const LABEL: &'static str = "Lab";
+ const COMPONENT_RANGES: [RangeInclusive<f64>; 3] =
+ [0.0..=100.0, -128.0..=127.0, -128.0..=127.0];
+
+ fn new(l: f32, a: f32, b: f32) -> Self {
+ palette::Lab::new(l, a, b)
+ }
+
+ fn components(&self) -> [f32; 3] {
+ [self.l, self.a, self.b]
+ }
+
+ fn to_string(&self) -> String {
+ format!("Lab({:.1}, {:.1}, {:.1})", self.l, self.a, self.b)
+ }
+}
+
+impl ColorSpace for palette::Lch {
+ const LABEL: &'static str = "Lch";
+ const COMPONENT_RANGES: [RangeInclusive<f64>; 3] =
+ [0.0..=100.0, 0.0..=128.0, 0.0..=360.0];
+
+ fn new(l: f32, chroma: f32, hue: f32) -> Self {
+ palette::Lch::new(l, chroma, palette::LabHue::from_degrees(hue))
+ }
+
+ fn components(&self) -> [f32; 3] {
+ [self.l, self.chroma, self.hue.to_positive_degrees()]
+ }
+
+ fn to_string(&self) -> String {
+ format!(
+ "Lch({:.1}, {:.1}, {:.1})",
+ self.l,
+ self.chroma,
+ self.hue.to_positive_degrees()
+ )
+ }
+}
diff --git a/examples/counter/src/main.rs b/examples/counter/src/main.rs
index bde0ea94..e0b2ebd6 100644
--- a/examples/counter/src/main.rs
+++ b/examples/counter/src/main.rs
@@ -1,6 +1,6 @@
use iced::{button, Align, Button, Column, Element, Sandbox, Settings, Text};
-pub fn main() {
+pub fn main() -> iced::Result {
Counter::run(Settings::default())
}
diff --git a/examples/custom_widget/Cargo.toml b/examples/custom_widget/Cargo.toml
index 30747dc0..3942538d 100644
--- a/examples/custom_widget/Cargo.toml
+++ b/examples/custom_widget/Cargo.toml
@@ -8,4 +8,4 @@ publish = false
[dependencies]
iced = { path = "../.." }
iced_native = { path = "../../native" }
-iced_wgpu = { path = "../../wgpu" }
+iced_graphics = { path = "../../graphics" }
diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs
index 0a570745..36f468c7 100644
--- a/examples/custom_widget/src/main.rs
+++ b/examples/custom_widget/src/main.rs
@@ -9,23 +9,26 @@ mod circle {
// Of course, you can choose to make the implementation renderer-agnostic,
// if you wish to, by creating your own `Renderer` trait, which could be
// implemented by `iced_wgpu` and other renderers.
+ use iced_graphics::{Backend, Defaults, Primitive, Renderer};
use iced_native::{
- layout, Background, Color, Element, Hasher, Layout, Length,
- MouseCursor, Point, Size, Widget,
+ layout, mouse, Background, Color, Element, Hasher, Layout, Length,
+ Point, Rectangle, Size, Widget,
};
- use iced_wgpu::{Defaults, Primitive, Renderer};
pub struct Circle {
- radius: u16,
+ radius: f32,
}
impl Circle {
- pub fn new(radius: u16) -> Self {
+ pub fn new(radius: f32) -> Self {
Self { radius }
}
}
- impl<Message> Widget<Message, Renderer> for Circle {
+ impl<Message, B> Widget<Message, Renderer<B>> for Circle
+ where
+ B: Backend,
+ {
fn width(&self) -> Length {
Length::Shrink
}
@@ -36,43 +39,44 @@ mod circle {
fn layout(
&self,
- _renderer: &Renderer,
+ _renderer: &Renderer<B>,
_limits: &layout::Limits,
) -> layout::Node {
- layout::Node::new(Size::new(
- f32::from(self.radius) * 2.0,
- f32::from(self.radius) * 2.0,
- ))
+ layout::Node::new(Size::new(self.radius * 2.0, self.radius * 2.0))
}
fn hash_layout(&self, state: &mut Hasher) {
use std::hash::Hash;
- self.radius.hash(state);
+ self.radius.to_bits().hash(state);
}
fn draw(
&self,
- _renderer: &mut Renderer,
+ _renderer: &mut Renderer<B>,
_defaults: &Defaults,
layout: Layout<'_>,
_cursor_position: Point,
- ) -> (Primitive, MouseCursor) {
+ _viewport: &Rectangle,
+ ) -> (Primitive, mouse::Interaction) {
(
Primitive::Quad {
bounds: layout.bounds(),
background: Background::Color(Color::BLACK),
border_radius: self.radius,
- border_width: 0,
+ border_width: 0.0,
border_color: Color::TRANSPARENT,
},
- MouseCursor::OutOfBounds,
+ mouse::Interaction::default(),
)
}
}
- impl<'a, Message> Into<Element<'a, Message, Renderer>> for Circle {
- fn into(self) -> Element<'a, Message, Renderer> {
+ impl<'a, Message, B> Into<Element<'a, Message, Renderer<B>>> for Circle
+ where
+ B: Backend,
+ {
+ fn into(self) -> Element<'a, Message, Renderer<B>> {
Element::new(self)
}
}
@@ -84,12 +88,12 @@ use iced::{
Slider, Text,
};
-pub fn main() {
+pub fn main() -> iced::Result {
Example::run(Settings::default())
}
struct Example {
- radius: u16,
+ radius: f32,
slider: slider::State,
}
@@ -103,7 +107,7 @@ impl Sandbox for Example {
fn new() -> Self {
Example {
- radius: 50,
+ radius: 50.0,
slider: slider::State::new(),
}
}
@@ -115,7 +119,7 @@ impl Sandbox for Example {
fn update(&mut self, message: Message) {
match message {
Message::RadiusChanged(radius) => {
- self.radius = radius.round() as u16;
+ self.radius = radius;
}
}
}
@@ -127,13 +131,16 @@ impl Sandbox for Example {
.max_width(500)
.align_items(Align::Center)
.push(Circle::new(self.radius))
- .push(Text::new(format!("Radius: {}", self.radius.to_string())))
- .push(Slider::new(
- &mut self.slider,
- 1.0..=100.0,
- f32::from(self.radius),
- Message::RadiusChanged,
- ));
+ .push(Text::new(format!("Radius: {:.2}", self.radius)))
+ .push(
+ Slider::new(
+ &mut self.slider,
+ 1.0..=100.0,
+ self.radius,
+ Message::RadiusChanged,
+ )
+ .step(0.01),
+ );
Container::new(content)
.width(Length::Fill)
diff --git a/examples/download_progress/Cargo.toml b/examples/download_progress/Cargo.toml
new file mode 100644
index 00000000..4b05e7dc
--- /dev/null
+++ b/examples/download_progress/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "download_progress"
+version = "0.1.0"
+authors = ["Songtronix <contact@songtronix.com>"]
+edition = "2018"
+publish = false
+
+[dependencies]
+iced = { path = "../..", features = ["tokio_old"] }
+iced_native = { path = "../../native" }
+iced_futures = { path = "../../futures" }
+reqwest = "0.10"
diff --git a/examples/download_progress/README.md b/examples/download_progress/README.md
new file mode 100644
index 00000000..c606c5f9
--- /dev/null
+++ b/examples/download_progress/README.md
@@ -0,0 +1,17 @@
+## Download progress
+
+A basic application that asynchronously downloads a dummy file 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.
+
+<div align="center">
+ <a href="https://gfycat.com/wildearlyafricanwilddog">
+ <img src="https://thumbs.gfycat.com/WildEarlyAfricanwilddog-small.gif">
+ </a>
+</div>
+
+You can run it with `cargo run`:
+
+```
+cargo run --package download_progress
+```
diff --git a/examples/download_progress/src/download.rs b/examples/download_progress/src/download.rs
new file mode 100644
index 00000000..f46a01f7
--- /dev/null
+++ b/examples/download_progress/src/download.rs
@@ -0,0 +1,112 @@
+use iced_futures::futures;
+
+// Just a little utility function
+pub fn file<T: ToString>(url: T) -> iced::Subscription<Progress> {
+ iced::Subscription::from_recipe(Download {
+ url: url.to_string(),
+ })
+}
+
+pub struct Download {
+ url: String,
+}
+
+// Make sure iced can use our download stream
+impl<H, I> iced_native::subscription::Recipe<H, I> for Download
+where
+ H: std::hash::Hasher,
+{
+ type Output = Progress;
+
+ fn hash(&self, state: &mut H) {
+ use std::hash::Hash;
+
+ std::any::TypeId::of::<Self>().hash(state);
+ self.url.hash(state);
+ }
+
+ fn stream(
+ self: Box<Self>,
+ _input: futures::stream::BoxStream<'static, I>,
+ ) -> futures::stream::BoxStream<'static, Self::Output> {
+ Box::pin(futures::stream::unfold(
+ State::Ready(self.url),
+ |state| async move {
+ match state {
+ State::Ready(url) => {
+ let response = reqwest::get(&url).await;
+
+ match response {
+ Ok(response) => {
+ if let Some(total) = response.content_length() {
+ Some((
+ Progress::Started,
+ State::Downloading {
+ response,
+ total,
+ downloaded: 0,
+ },
+ ))
+ } else {
+ Some((Progress::Errored, State::Finished))
+ }
+ }
+ Err(_) => {
+ Some((Progress::Errored, State::Finished))
+ }
+ }
+ }
+ State::Downloading {
+ mut response,
+ total,
+ downloaded,
+ } => match response.chunk().await {
+ Ok(Some(chunk)) => {
+ let downloaded = downloaded + chunk.len() as u64;
+
+ let percentage =
+ (downloaded as f32 / total as f32) * 100.0;
+
+ Some((
+ Progress::Advanced(percentage),
+ State::Downloading {
+ response,
+ total,
+ downloaded,
+ },
+ ))
+ }
+ Ok(None) => Some((Progress::Finished, State::Finished)),
+ Err(_) => Some((Progress::Errored, State::Finished)),
+ },
+ State::Finished => {
+ // We do not let the stream die, as it would start a
+ // new download repeatedly if the user is not careful
+ // in case of errors.
+ let _: () = iced::futures::future::pending().await;
+
+ None
+ }
+ }
+ },
+ ))
+ }
+}
+
+#[derive(Debug, Clone)]
+pub enum Progress {
+ Started,
+ Advanced(f32),
+ Finished,
+ Errored,
+}
+
+pub enum State {
+ Ready(String),
+ Downloading {
+ response: reqwest::Response,
+ total: u64,
+ downloaded: u64,
+ },
+ Finished,
+}
diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs
new file mode 100644
index 00000000..77b01354
--- /dev/null
+++ b/examples/download_progress/src/main.rs
@@ -0,0 +1,144 @@
+use iced::{
+ button, executor, Align, Application, Button, Column, Command, Container,
+ Element, Length, ProgressBar, Settings, Subscription, Text,
+};
+
+mod download;
+
+pub fn main() -> iced::Result {
+ Example::run(Settings::default())
+}
+
+#[derive(Debug)]
+enum Example {
+ Idle { button: button::State },
+ Downloading { progress: f32 },
+ Finished { button: button::State },
+ Errored { button: button::State },
+}
+
+#[derive(Debug, Clone)]
+pub enum Message {
+ Download,
+ DownloadProgressed(download::Progress),
+}
+
+impl Application for Example {
+ type Executor = executor::Default;
+ type Message = Message;
+ type Flags = ();
+
+ fn new(_flags: ()) -> (Example, Command<Message>) {
+ (
+ Example::Idle {
+ button: button::State::new(),
+ },
+ Command::none(),
+ )
+ }
+
+ fn title(&self) -> String {
+ String::from("Download progress - Iced")
+ }
+
+ fn update(&mut self, message: Message) -> Command<Message> {
+ match message {
+ Message::Download => match self {
+ Example::Idle { .. }
+ | Example::Finished { .. }
+ | Example::Errored { .. } => {
+ *self = Example::Downloading { progress: 0.0 };
+ }
+ _ => {}
+ },
+ 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(),
+ };
+ }
+ },
+ _ => {}
+ },
+ };
+
+ Command::none()
+ }
+
+ fn subscription(&self) -> Subscription<Message> {
+ match self {
+ Example::Downloading { .. } => {
+ download::file("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,
+ };
+
+ let progress_bar = ProgressBar::new(0.0..=100.0, current_progress);
+
+ let control: Element<_> = match self {
+ Example::Idle { button } => {
+ Button::new(button, Text::new("Start the download!"))
+ .on_press(Message::Download)
+ .into()
+ }
+ Example::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),
+ )
+ .into(),
+ Example::Downloading { .. } => {
+ Text::new(format!("Downloading... {:.2}%", current_progress))
+ .into()
+ }
+ Example::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),
+ )
+ .into(),
+ };
+
+ let content = 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()
+ .into()
+ }
+}
diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs
index 0c9dca05..6eba6aad 100644
--- a/examples/events/src/main.rs
+++ b/examples/events/src/main.rs
@@ -3,7 +3,7 @@ use iced::{
Element, Length, Settings, Subscription, Text,
};
-pub fn main() {
+pub fn main() -> iced::Result {
Events::run(Settings::default())
}
@@ -22,8 +22,9 @@ enum Message {
impl Application for Events {
type Executor = executor::Default;
type Message = Message;
+ type Flags = ();
- fn new() -> (Events, Command<Message>) {
+ fn new(_flags: ()) -> (Events, Command<Message>) {
(Events::default(), Command::none())
}
diff --git a/examples/game_of_life/Cargo.toml b/examples/game_of_life/Cargo.toml
new file mode 100644
index 00000000..9c4172c4
--- /dev/null
+++ b/examples/game_of_life/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "game_of_life"
+version = "0.1.0"
+authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
+edition = "2018"
+publish = false
+
+[dependencies]
+iced = { path = "../..", features = ["canvas", "tokio", "debug"] }
+tokio = { version = "0.3", features = ["sync"] }
+itertools = "0.9"
+rustc-hash = "1.1"
diff --git a/examples/game_of_life/README.md b/examples/game_of_life/README.md
new file mode 100644
index 00000000..aa39201c
--- /dev/null
+++ b/examples/game_of_life/README.md
@@ -0,0 +1,22 @@
+## Game of Life
+
+An interactive version of the [Game of Life], invented by [John Horton Conway].
+
+It runs a simulation in a background thread while allowing interaction with a `Canvas` that displays an infinite grid with zooming, panning, and drawing support.
+
+The __[`main`]__ file contains the relevant code of the example.
+
+<div align="center">
+ <a href="https://gfycat.com/WhichPaltryChick">
+ <img src="https://thumbs.gfycat.com/WhichPaltryChick-size_restricted.gif">
+ </a>
+</div>
+
+You can run it with `cargo run`:
+```
+cargo run --package game_of_life
+```
+
+[`main`]: src/main.rs
+[Game of Life]: https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life
+[John Horton Conway]: https://en.wikipedia.org/wiki/John_Horton_Conway
diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs
new file mode 100644
index 00000000..e18bd6e0
--- /dev/null
+++ b/examples/game_of_life/src/main.rs
@@ -0,0 +1,885 @@
+//! This example showcases an interactive version of the Game of Life, invented
+//! by John Conway. It leverages a `Canvas` together with other widgets.
+mod preset;
+mod style;
+
+use grid::Grid;
+use iced::button::{self, Button};
+use iced::executor;
+use iced::pick_list::{self, PickList};
+use iced::slider::{self, Slider};
+use iced::time;
+use iced::{
+ Align, Application, Checkbox, Column, Command, Container, Element, Length,
+ Row, Settings, Subscription, Text,
+};
+use preset::Preset;
+use std::time::{Duration, Instant};
+
+pub fn main() -> iced::Result {
+ GameOfLife::run(Settings {
+ antialiasing: true,
+ ..Settings::default()
+ })
+}
+
+#[derive(Default)]
+struct GameOfLife {
+ grid: Grid,
+ controls: Controls,
+ is_playing: bool,
+ queued_ticks: usize,
+ speed: usize,
+ next_speed: Option<usize>,
+ version: usize,
+}
+
+#[derive(Debug, Clone)]
+enum Message {
+ Grid(grid::Message, usize),
+ Tick(Instant),
+ TogglePlayback,
+ ToggleGrid(bool),
+ Next,
+ Clear,
+ SpeedChanged(f32),
+ PresetPicked(Preset),
+}
+
+impl Application for GameOfLife {
+ type Message = Message;
+ type Executor = executor::Default;
+ type Flags = ();
+
+ fn new(_flags: ()) -> (Self, Command<Message>) {
+ (
+ Self {
+ speed: 5,
+ ..Self::default()
+ },
+ Command::none(),
+ )
+ }
+
+ fn title(&self) -> String {
+ String::from("Game of Life - Iced")
+ }
+
+ fn update(&mut self, message: Message) -> Command<Message> {
+ match message {
+ Message::Grid(message, version) => {
+ if version == self.version {
+ self.grid.update(message);
+ }
+ }
+ Message::Tick(_) | Message::Next => {
+ self.queued_ticks = (self.queued_ticks + 1).min(self.speed);
+
+ if let Some(task) = self.grid.tick(self.queued_ticks) {
+ if let Some(speed) = self.next_speed.take() {
+ self.speed = speed;
+ }
+
+ self.queued_ticks = 0;
+
+ let version = self.version;
+
+ return Command::perform(task, move |message| {
+ Message::Grid(message, version)
+ });
+ }
+ }
+ Message::TogglePlayback => {
+ self.is_playing = !self.is_playing;
+ }
+ Message::ToggleGrid(show_grid_lines) => {
+ self.grid.toggle_lines(show_grid_lines);
+ }
+ Message::Clear => {
+ self.grid.clear();
+ self.version += 1;
+ }
+ Message::SpeedChanged(speed) => {
+ if self.is_playing {
+ self.next_speed = Some(speed.round() as usize);
+ } else {
+ self.speed = speed.round() as usize;
+ }
+ }
+ Message::PresetPicked(new_preset) => {
+ self.grid = Grid::from_preset(new_preset);
+ self.version += 1;
+ }
+ }
+
+ Command::none()
+ }
+
+ fn subscription(&self) -> Subscription<Message> {
+ if self.is_playing {
+ time::every(Duration::from_millis(1000 / self.speed as u64))
+ .map(Message::Tick)
+ } else {
+ Subscription::none()
+ }
+ }
+
+ fn view(&mut self) -> Element<Message> {
+ let version = self.version;
+ let selected_speed = self.next_speed.unwrap_or(self.speed);
+ let controls = self.controls.view(
+ self.is_playing,
+ self.grid.are_lines_visible(),
+ selected_speed,
+ self.grid.preset(),
+ );
+
+ let content = Column::new()
+ .push(
+ self.grid
+ .view()
+ .map(move |message| Message::Grid(message, version)),
+ )
+ .push(controls);
+
+ Container::new(content)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .style(style::Container)
+ .into()
+ }
+}
+
+mod grid {
+ use crate::Preset;
+ use iced::{
+ canvas::event::{self, Event},
+ canvas::{self, Cache, Canvas, Cursor, Frame, Geometry, Path, Text},
+ mouse, Color, Element, HorizontalAlignment, Length, Point, Rectangle,
+ Size, Vector, VerticalAlignment,
+ };
+ use rustc_hash::{FxHashMap, FxHashSet};
+ use std::future::Future;
+ use std::ops::RangeInclusive;
+ use std::time::{Duration, Instant};
+
+ pub struct Grid {
+ state: State,
+ preset: Preset,
+ interaction: Interaction,
+ life_cache: Cache,
+ grid_cache: Cache,
+ translation: Vector,
+ scaling: f32,
+ show_lines: bool,
+ last_tick_duration: Duration,
+ last_queued_ticks: usize,
+ }
+
+ #[derive(Debug, Clone)]
+ pub enum Message {
+ Populate(Cell),
+ Unpopulate(Cell),
+ Ticked {
+ result: Result<Life, TickError>,
+ tick_duration: Duration,
+ },
+ }
+
+ #[derive(Debug, Clone)]
+ pub enum TickError {
+ JoinFailed,
+ }
+
+ impl Default for Grid {
+ fn default() -> Self {
+ Self::from_preset(Preset::default())
+ }
+ }
+
+ impl Grid {
+ const MIN_SCALING: f32 = 0.1;
+ const MAX_SCALING: f32 = 2.0;
+
+ pub fn from_preset(preset: Preset) -> Self {
+ Self {
+ state: State::with_life(
+ preset
+ .life()
+ .into_iter()
+ .map(|(i, j)| Cell { i, j })
+ .collect(),
+ ),
+ preset,
+ interaction: Interaction::None,
+ life_cache: Cache::default(),
+ grid_cache: Cache::default(),
+ translation: Vector::default(),
+ scaling: 1.0,
+ show_lines: true,
+ last_tick_duration: Duration::default(),
+ last_queued_ticks: 0,
+ }
+ }
+
+ pub fn tick(
+ &mut self,
+ amount: usize,
+ ) -> Option<impl Future<Output = Message>> {
+ let tick = self.state.tick(amount)?;
+
+ self.last_queued_ticks = amount;
+
+ Some(async move {
+ let start = Instant::now();
+ let result = tick.await;
+ let tick_duration = start.elapsed() / amount as u32;
+
+ Message::Ticked {
+ result,
+ tick_duration,
+ }
+ })
+ }
+
+ pub fn update(&mut self, message: Message) {
+ match message {
+ Message::Populate(cell) => {
+ self.state.populate(cell);
+ self.life_cache.clear();
+
+ self.preset = Preset::Custom;
+ }
+ Message::Unpopulate(cell) => {
+ self.state.unpopulate(&cell);
+ self.life_cache.clear();
+
+ self.preset = Preset::Custom;
+ }
+ Message::Ticked {
+ result: Ok(life),
+ tick_duration,
+ } => {
+ self.state.update(life);
+ self.life_cache.clear();
+
+ self.last_tick_duration = tick_duration;
+ }
+ Message::Ticked {
+ result: Err(error), ..
+ } => {
+ dbg!(error);
+ }
+ }
+ }
+
+ pub fn view<'a>(&'a mut self) -> Element<'a, Message> {
+ Canvas::new(self)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .into()
+ }
+
+ pub fn clear(&mut self) {
+ self.state = State::default();
+ self.preset = Preset::Custom;
+
+ self.life_cache.clear();
+ }
+
+ pub fn preset(&self) -> Preset {
+ self.preset
+ }
+
+ pub fn toggle_lines(&mut self, enabled: bool) {
+ self.show_lines = enabled;
+ }
+
+ pub fn are_lines_visible(&self) -> bool {
+ self.show_lines
+ }
+
+ fn visible_region(&self, size: Size) -> Region {
+ let width = size.width / self.scaling;
+ let height = size.height / self.scaling;
+
+ Region {
+ x: -self.translation.x - width / 2.0,
+ y: -self.translation.y - height / 2.0,
+ width,
+ height,
+ }
+ }
+
+ fn project(&self, position: Point, size: Size) -> Point {
+ let region = self.visible_region(size);
+
+ Point::new(
+ position.x / self.scaling + region.x,
+ position.y / self.scaling + region.y,
+ )
+ }
+ }
+
+ impl<'a> canvas::Program<Message> for Grid {
+ fn update(
+ &mut self,
+ event: Event,
+ bounds: Rectangle,
+ cursor: Cursor,
+ ) -> (event::Status, Option<Message>) {
+ if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event {
+ self.interaction = Interaction::None;
+ }
+
+ let cursor_position =
+ if let Some(position) = cursor.position_in(&bounds) {
+ position
+ } else {
+ return (event::Status::Ignored, None);
+ };
+
+ let cell = Cell::at(self.project(cursor_position, bounds.size()));
+ let is_populated = self.state.contains(&cell);
+
+ let (populate, unpopulate) = if is_populated {
+ (None, Some(Message::Unpopulate(cell)))
+ } else {
+ (Some(Message::Populate(cell)), None)
+ };
+
+ match event {
+ Event::Mouse(mouse_event) => match mouse_event {
+ mouse::Event::ButtonPressed(button) => {
+ let message = match button {
+ mouse::Button::Left => {
+ self.interaction = if is_populated {
+ Interaction::Erasing
+ } else {
+ Interaction::Drawing
+ };
+
+ populate.or(unpopulate)
+ }
+ mouse::Button::Right => {
+ self.interaction = Interaction::Panning {
+ translation: self.translation,
+ start: cursor_position,
+ };
+
+ None
+ }
+ _ => None,
+ };
+
+ (event::Status::Captured, message)
+ }
+ mouse::Event::CursorMoved { .. } => {
+ let message = match self.interaction {
+ Interaction::Drawing => populate,
+ Interaction::Erasing => unpopulate,
+ Interaction::Panning { translation, start } => {
+ self.translation = translation
+ + (cursor_position - start)
+ * (1.0 / self.scaling);
+
+ self.life_cache.clear();
+ self.grid_cache.clear();
+
+ None
+ }
+ _ => None,
+ };
+
+ let event_status = match self.interaction {
+ Interaction::None => event::Status::Ignored,
+ _ => event::Status::Captured,
+ };
+
+ (event_status, message)
+ }
+ mouse::Event::WheelScrolled { delta } => match delta {
+ mouse::ScrollDelta::Lines { y, .. }
+ | mouse::ScrollDelta::Pixels { y, .. } => {
+ if y < 0.0 && self.scaling > Self::MIN_SCALING
+ || y > 0.0 && self.scaling < Self::MAX_SCALING
+ {
+ let old_scaling = self.scaling;
+
+ self.scaling = (self.scaling
+ * (1.0 + y / 30.0))
+ .max(Self::MIN_SCALING)
+ .min(Self::MAX_SCALING);
+
+ if let Some(cursor_to_center) =
+ cursor.position_from(bounds.center())
+ {
+ let factor = self.scaling - old_scaling;
+
+ self.translation = self.translation
+ - Vector::new(
+ cursor_to_center.x * factor
+ / (old_scaling * old_scaling),
+ cursor_to_center.y * factor
+ / (old_scaling * old_scaling),
+ );
+ }
+
+ self.life_cache.clear();
+ self.grid_cache.clear();
+ }
+
+ (event::Status::Captured, None)
+ }
+ },
+ _ => (event::Status::Ignored, None),
+ },
+ _ => (event::Status::Ignored, None),
+ }
+ }
+
+ fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec<Geometry> {
+ let center = Vector::new(bounds.width / 2.0, bounds.height / 2.0);
+
+ let life = self.life_cache.draw(bounds.size(), |frame| {
+ let background = Path::rectangle(Point::ORIGIN, frame.size());
+ frame.fill(&background, Color::from_rgb8(0x40, 0x44, 0x4B));
+
+ frame.with_save(|frame| {
+ frame.translate(center);
+ frame.scale(self.scaling);
+ frame.translate(self.translation);
+ frame.scale(Cell::SIZE as f32);
+
+ let region = self.visible_region(frame.size());
+
+ for cell in region.cull(self.state.cells()) {
+ frame.fill_rectangle(
+ Point::new(cell.j as f32, cell.i as f32),
+ Size::UNIT,
+ Color::WHITE,
+ );
+ }
+ });
+ });
+
+ let overlay = {
+ let mut frame = Frame::new(bounds.size());
+
+ let hovered_cell =
+ cursor.position_in(&bounds).map(|position| {
+ Cell::at(self.project(position, frame.size()))
+ });
+
+ if let Some(cell) = hovered_cell {
+ frame.with_save(|frame| {
+ frame.translate(center);
+ frame.scale(self.scaling);
+ frame.translate(self.translation);
+ frame.scale(Cell::SIZE as f32);
+
+ frame.fill_rectangle(
+ Point::new(cell.j as f32, cell.i as f32),
+ Size::UNIT,
+ Color {
+ a: 0.5,
+ ..Color::BLACK
+ },
+ );
+ });
+ }
+
+ let text = Text {
+ color: Color::WHITE,
+ size: 14.0,
+ position: Point::new(frame.width(), frame.height()),
+ horizontal_alignment: HorizontalAlignment::Right,
+ vertical_alignment: VerticalAlignment::Bottom,
+ ..Text::default()
+ };
+
+ if let Some(cell) = hovered_cell {
+ frame.fill_text(Text {
+ content: format!("({}, {})", cell.j, cell.i),
+ position: text.position - Vector::new(0.0, 16.0),
+ ..text
+ });
+ }
+
+ let cell_count = self.state.cell_count();
+
+ frame.fill_text(Text {
+ content: format!(
+ "{} cell{} @ {:?} ({})",
+ cell_count,
+ if cell_count == 1 { "" } else { "s" },
+ self.last_tick_duration,
+ self.last_queued_ticks
+ ),
+ ..text
+ });
+
+ frame.into_geometry()
+ };
+
+ if self.scaling < 0.2 || !self.show_lines {
+ vec![life, overlay]
+ } else {
+ let grid = self.grid_cache.draw(bounds.size(), |frame| {
+ frame.translate(center);
+ frame.scale(self.scaling);
+ frame.translate(self.translation);
+ frame.scale(Cell::SIZE as f32);
+
+ let region = self.visible_region(frame.size());
+ let rows = region.rows();
+ let columns = region.columns();
+ let (total_rows, total_columns) =
+ (rows.clone().count(), columns.clone().count());
+ let width = 2.0 / Cell::SIZE as f32;
+ let color = Color::from_rgb8(70, 74, 83);
+
+ frame.translate(Vector::new(-width / 2.0, -width / 2.0));
+
+ for row in region.rows() {
+ frame.fill_rectangle(
+ Point::new(*columns.start() as f32, row as f32),
+ Size::new(total_columns as f32, width),
+ color,
+ );
+ }
+
+ for column in region.columns() {
+ frame.fill_rectangle(
+ Point::new(column as f32, *rows.start() as f32),
+ Size::new(width, total_rows as f32),
+ color,
+ );
+ }
+ });
+
+ vec![life, grid, overlay]
+ }
+ }
+
+ fn mouse_interaction(
+ &self,
+ bounds: Rectangle,
+ cursor: Cursor,
+ ) -> mouse::Interaction {
+ match self.interaction {
+ Interaction::Drawing => mouse::Interaction::Crosshair,
+ Interaction::Erasing => mouse::Interaction::Crosshair,
+ Interaction::Panning { .. } => mouse::Interaction::Grabbing,
+ Interaction::None if cursor.is_over(&bounds) => {
+ mouse::Interaction::Crosshair
+ }
+ _ => mouse::Interaction::default(),
+ }
+ }
+ }
+
+ #[derive(Default)]
+ struct State {
+ life: Life,
+ births: FxHashSet<Cell>,
+ is_ticking: bool,
+ }
+
+ impl State {
+ pub fn with_life(life: Life) -> Self {
+ Self {
+ life,
+ ..Self::default()
+ }
+ }
+
+ fn cell_count(&self) -> usize {
+ self.life.len() + self.births.len()
+ }
+
+ fn contains(&self, cell: &Cell) -> bool {
+ self.life.contains(cell) || self.births.contains(cell)
+ }
+
+ fn cells(&self) -> impl Iterator<Item = &Cell> {
+ self.life.iter().chain(self.births.iter())
+ }
+
+ fn populate(&mut self, cell: Cell) {
+ if self.is_ticking {
+ self.births.insert(cell);
+ } else {
+ self.life.populate(cell);
+ }
+ }
+
+ fn unpopulate(&mut self, cell: &Cell) {
+ if self.is_ticking {
+ let _ = self.births.remove(cell);
+ } else {
+ self.life.unpopulate(cell);
+ }
+ }
+
+ fn update(&mut self, mut life: Life) {
+ self.births.drain().for_each(|cell| life.populate(cell));
+
+ self.life = life;
+ self.is_ticking = false;
+ }
+
+ fn tick(
+ &mut self,
+ amount: usize,
+ ) -> Option<impl Future<Output = Result<Life, TickError>>> {
+ if self.is_ticking {
+ return None;
+ }
+
+ self.is_ticking = true;
+
+ let mut life = self.life.clone();
+
+ Some(async move {
+ tokio::task::spawn_blocking(move || {
+ for _ in 0..amount {
+ life.tick();
+ }
+
+ life
+ })
+ .await
+ .map_err(|_| TickError::JoinFailed)
+ })
+ }
+ }
+
+ #[derive(Clone, Default)]
+ pub struct Life {
+ cells: FxHashSet<Cell>,
+ }
+
+ impl Life {
+ fn len(&self) -> usize {
+ self.cells.len()
+ }
+
+ fn contains(&self, cell: &Cell) -> bool {
+ self.cells.contains(cell)
+ }
+
+ fn populate(&mut self, cell: Cell) {
+ self.cells.insert(cell);
+ }
+
+ fn unpopulate(&mut self, cell: &Cell) {
+ let _ = self.cells.remove(cell);
+ }
+
+ fn tick(&mut self) {
+ let mut adjacent_life = FxHashMap::default();
+
+ for cell in &self.cells {
+ let _ = adjacent_life.entry(*cell).or_insert(0);
+
+ for neighbor in Cell::neighbors(*cell) {
+ let amount = adjacent_life.entry(neighbor).or_insert(0);
+
+ *amount += 1;
+ }
+ }
+
+ for (cell, amount) in adjacent_life.iter() {
+ match amount {
+ 2 => {}
+ 3 => {
+ let _ = self.cells.insert(*cell);
+ }
+ _ => {
+ let _ = self.cells.remove(cell);
+ }
+ }
+ }
+ }
+
+ pub fn iter(&self) -> impl Iterator<Item = &Cell> {
+ self.cells.iter()
+ }
+ }
+
+ impl std::iter::FromIterator<Cell> for Life {
+ fn from_iter<I: IntoIterator<Item = Cell>>(iter: I) -> Self {
+ Life {
+ cells: iter.into_iter().collect(),
+ }
+ }
+ }
+
+ impl std::fmt::Debug for Life {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("Life")
+ .field("cells", &self.cells.len())
+ .finish()
+ }
+ }
+
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+ pub struct Cell {
+ i: isize,
+ j: isize,
+ }
+
+ impl Cell {
+ const SIZE: usize = 20;
+
+ fn at(position: Point) -> Cell {
+ let i = (position.y / Cell::SIZE as f32).ceil() as isize;
+ let j = (position.x / Cell::SIZE as f32).ceil() as isize;
+
+ Cell {
+ i: i.saturating_sub(1),
+ j: j.saturating_sub(1),
+ }
+ }
+
+ fn cluster(cell: Cell) -> impl Iterator<Item = Cell> {
+ use itertools::Itertools;
+
+ let rows = cell.i.saturating_sub(1)..=cell.i.saturating_add(1);
+ let columns = cell.j.saturating_sub(1)..=cell.j.saturating_add(1);
+
+ rows.cartesian_product(columns).map(|(i, j)| Cell { i, j })
+ }
+
+ fn neighbors(cell: Cell) -> impl Iterator<Item = Cell> {
+ Cell::cluster(cell).filter(move |candidate| *candidate != cell)
+ }
+ }
+
+ pub struct Region {
+ x: f32,
+ y: f32,
+ width: f32,
+ height: f32,
+ }
+
+ impl Region {
+ fn rows(&self) -> RangeInclusive<isize> {
+ let first_row = (self.y / Cell::SIZE as f32).floor() as isize;
+
+ let visible_rows =
+ (self.height / Cell::SIZE as f32).ceil() as isize;
+
+ first_row..=first_row + visible_rows
+ }
+
+ fn columns(&self) -> RangeInclusive<isize> {
+ let first_column = (self.x / Cell::SIZE as f32).floor() as isize;
+
+ let visible_columns =
+ (self.width / Cell::SIZE as f32).ceil() as isize;
+
+ first_column..=first_column + visible_columns
+ }
+
+ fn cull<'a>(
+ &self,
+ cells: impl Iterator<Item = &'a Cell>,
+ ) -> impl Iterator<Item = &'a Cell> {
+ let rows = self.rows();
+ let columns = self.columns();
+
+ cells.filter(move |cell| {
+ rows.contains(&cell.i) && columns.contains(&cell.j)
+ })
+ }
+ }
+
+ enum Interaction {
+ None,
+ Drawing,
+ Erasing,
+ Panning { translation: Vector, start: Point },
+ }
+}
+
+#[derive(Default)]
+struct Controls {
+ toggle_button: button::State,
+ next_button: button::State,
+ clear_button: button::State,
+ speed_slider: slider::State,
+ preset_list: pick_list::State<Preset>,
+}
+
+impl Controls {
+ fn view<'a>(
+ &'a mut self,
+ is_playing: bool,
+ is_grid_enabled: bool,
+ speed: usize,
+ preset: Preset,
+ ) -> Element<'a, Message> {
+ let playback_controls = Row::new()
+ .spacing(10)
+ .push(
+ Button::new(
+ &mut self.toggle_button,
+ Text::new(if is_playing { "Pause" } else { "Play" }),
+ )
+ .on_press(Message::TogglePlayback)
+ .style(style::Button),
+ )
+ .push(
+ Button::new(&mut self.next_button, Text::new("Next"))
+ .on_press(Message::Next)
+ .style(style::Button),
+ );
+
+ let speed_controls = Row::new()
+ .width(Length::Fill)
+ .align_items(Align::Center)
+ .spacing(10)
+ .push(
+ Slider::new(
+ &mut self.speed_slider,
+ 1.0..=1000.0,
+ speed as f32,
+ Message::SpeedChanged,
+ )
+ .style(style::Slider),
+ )
+ .push(Text::new(format!("x{}", speed)).size(16));
+
+ Row::new()
+ .padding(10)
+ .spacing(20)
+ .align_items(Align::Center)
+ .push(playback_controls)
+ .push(speed_controls)
+ .push(
+ Checkbox::new(is_grid_enabled, "Grid", Message::ToggleGrid)
+ .size(16)
+ .spacing(5)
+ .text_size(16),
+ )
+ .push(
+ PickList::new(
+ &mut self.preset_list,
+ preset::ALL,
+ Some(preset),
+ Message::PresetPicked,
+ )
+ .padding(8)
+ .text_size(16)
+ .style(style::PickList),
+ )
+ .push(
+ Button::new(&mut self.clear_button, Text::new("Clear"))
+ .on_press(Message::Clear)
+ .style(style::Clear),
+ )
+ .into()
+ }
+}
diff --git a/examples/game_of_life/src/preset.rs b/examples/game_of_life/src/preset.rs
new file mode 100644
index 00000000..05157b6a
--- /dev/null
+++ b/examples/game_of_life/src/preset.rs
@@ -0,0 +1,142 @@
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Preset {
+ Custom,
+ XKCD,
+ Glider,
+ SmallExploder,
+ Exploder,
+ TenCellRow,
+ LightweightSpaceship,
+ Tumbler,
+ GliderGun,
+ Acorn,
+}
+
+pub static ALL: &[Preset] = &[
+ Preset::Custom,
+ Preset::XKCD,
+ Preset::Glider,
+ Preset::SmallExploder,
+ Preset::Exploder,
+ Preset::TenCellRow,
+ Preset::LightweightSpaceship,
+ Preset::Tumbler,
+ Preset::GliderGun,
+ Preset::Acorn,
+];
+
+impl Preset {
+ pub fn life(self) -> Vec<(isize, isize)> {
+ #[rustfmt::skip]
+ let cells = match self {
+ Preset::Custom => vec![],
+ Preset::XKCD => vec![
+ " xxx ",
+ " x x ",
+ " x x ",
+ " x ",
+ "x xxx ",
+ " x x x ",
+ " x x",
+ " x x ",
+ " x x ",
+ ],
+ Preset::Glider => vec![
+ " x ",
+ " x",
+ "xxx"
+ ],
+ Preset::SmallExploder => vec![
+ " x ",
+ "xxx",
+ "x x",
+ " x ",
+ ],
+ Preset::Exploder => vec![
+ "x x x",
+ "x x",
+ "x x",
+ "x x",
+ "x x x",
+ ],
+ Preset::TenCellRow => vec![
+ "xxxxxxxxxx",
+ ],
+ Preset::LightweightSpaceship => vec![
+ " xxxxx",
+ "x x",
+ " x",
+ "x x ",
+ ],
+ Preset::Tumbler => vec![
+ " xx xx ",
+ " xx xx ",
+ " x x ",
+ "x x x x",
+ "x x x x",
+ "xx xx",
+ ],
+ Preset::GliderGun => vec![
+ " x ",
+ " x x ",
+ " xx xx xx",
+ " x x xx xx",
+ "xx x x xx ",
+ "xx x x xx x x ",
+ " x x x ",
+ " x x ",
+ " xx ",
+ ],
+ Preset::Acorn => vec![
+ " x ",
+ " x ",
+ "xx xxx",
+ ],
+ };
+
+ let start_row = -(cells.len() as isize / 2);
+
+ cells
+ .into_iter()
+ .enumerate()
+ .flat_map(|(i, cells)| {
+ let start_column = -(cells.len() as isize / 2);
+
+ cells
+ .chars()
+ .enumerate()
+ .filter(|(_, c)| !c.is_whitespace())
+ .map(move |(j, _)| {
+ (start_row + i as isize, start_column + j as isize)
+ })
+ })
+ .collect()
+ }
+}
+
+impl Default for Preset {
+ fn default() -> Preset {
+ Preset::XKCD
+ }
+}
+
+impl std::fmt::Display for Preset {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "{}",
+ match self {
+ Preset::Custom => "Custom",
+ Preset::XKCD => "xkcd #2293",
+ Preset::Glider => "Glider",
+ Preset::SmallExploder => "Small Exploder",
+ Preset::Exploder => "Exploder",
+ Preset::TenCellRow => "10 Cell Row",
+ Preset::LightweightSpaceship => "Lightweight spaceship",
+ Preset::Tumbler => "Tumbler",
+ Preset::GliderGun => "Gosper Glider Gun",
+ Preset::Acorn => "Acorn",
+ }
+ )
+ }
+}
diff --git a/examples/game_of_life/src/style.rs b/examples/game_of_life/src/style.rs
new file mode 100644
index 00000000..6605826f
--- /dev/null
+++ b/examples/game_of_life/src/style.rs
@@ -0,0 +1,188 @@
+use iced::{button, container, pick_list, slider, Background, Color};
+
+const ACTIVE: Color = Color::from_rgb(
+ 0x72 as f32 / 255.0,
+ 0x89 as f32 / 255.0,
+ 0xDA as f32 / 255.0,
+);
+
+const DESTRUCTIVE: Color = Color::from_rgb(
+ 0xC0 as f32 / 255.0,
+ 0x47 as f32 / 255.0,
+ 0x47 as f32 / 255.0,
+);
+
+const HOVERED: Color = Color::from_rgb(
+ 0x67 as f32 / 255.0,
+ 0x7B as f32 / 255.0,
+ 0xC4 as f32 / 255.0,
+);
+
+const BACKGROUND: Color = Color::from_rgb(
+ 0x2F as f32 / 255.0,
+ 0x31 as f32 / 255.0,
+ 0x36 as f32 / 255.0,
+);
+
+pub struct Container;
+
+impl container::StyleSheet for Container {
+ fn style(&self) -> container::Style {
+ container::Style {
+ background: Some(Background::Color(Color::from_rgb8(
+ 0x36, 0x39, 0x3F,
+ ))),
+ text_color: Some(Color::WHITE),
+ ..container::Style::default()
+ }
+ }
+}
+
+pub struct Button;
+
+impl button::StyleSheet for Button {
+ fn active(&self) -> button::Style {
+ button::Style {
+ background: Some(Background::Color(ACTIVE)),
+ border_radius: 3.0,
+ text_color: Color::WHITE,
+ ..button::Style::default()
+ }
+ }
+
+ fn hovered(&self) -> button::Style {
+ button::Style {
+ background: Some(Background::Color(HOVERED)),
+ text_color: Color::WHITE,
+ ..self.active()
+ }
+ }
+
+ fn pressed(&self) -> button::Style {
+ button::Style {
+ border_width: 1.0,
+ border_color: Color::WHITE,
+ ..self.hovered()
+ }
+ }
+}
+
+pub struct Clear;
+
+impl button::StyleSheet for Clear {
+ fn active(&self) -> button::Style {
+ button::Style {
+ background: Some(Background::Color(DESTRUCTIVE)),
+ border_radius: 3.0,
+ text_color: Color::WHITE,
+ ..button::Style::default()
+ }
+ }
+
+ fn hovered(&self) -> button::Style {
+ button::Style {
+ background: Some(Background::Color(Color {
+ a: 0.5,
+ ..DESTRUCTIVE
+ })),
+ text_color: Color::WHITE,
+ ..self.active()
+ }
+ }
+
+ fn pressed(&self) -> button::Style {
+ button::Style {
+ border_width: 1.0,
+ border_color: Color::WHITE,
+ ..self.hovered()
+ }
+ }
+}
+
+pub struct Slider;
+
+impl slider::StyleSheet for Slider {
+ fn active(&self) -> slider::Style {
+ slider::Style {
+ rail_colors: (ACTIVE, Color { a: 0.1, ..ACTIVE }),
+ handle: slider::Handle {
+ shape: slider::HandleShape::Circle { radius: 9.0 },
+ color: ACTIVE,
+ border_width: 0.0,
+ border_color: Color::TRANSPARENT,
+ },
+ }
+ }
+
+ fn hovered(&self) -> slider::Style {
+ let active = self.active();
+
+ slider::Style {
+ handle: slider::Handle {
+ color: HOVERED,
+ ..active.handle
+ },
+ ..active
+ }
+ }
+
+ fn dragging(&self) -> slider::Style {
+ let active = self.active();
+
+ slider::Style {
+ handle: slider::Handle {
+ color: Color::from_rgb(0.85, 0.85, 0.85),
+ ..active.handle
+ },
+ ..active
+ }
+ }
+}
+
+pub struct PickList;
+
+impl pick_list::StyleSheet for PickList {
+ fn menu(&self) -> pick_list::Menu {
+ pick_list::Menu {
+ text_color: Color::WHITE,
+ background: BACKGROUND.into(),
+ border_width: 1.0,
+ border_color: Color {
+ a: 0.7,
+ ..Color::BLACK
+ },
+ selected_background: Color {
+ a: 0.5,
+ ..Color::BLACK
+ }
+ .into(),
+ selected_text_color: Color::WHITE,
+ }
+ }
+
+ fn active(&self) -> pick_list::Style {
+ pick_list::Style {
+ text_color: Color::WHITE,
+ background: BACKGROUND.into(),
+ border_width: 1.0,
+ border_color: Color {
+ a: 0.6,
+ ..Color::BLACK
+ },
+ border_radius: 2.0,
+ icon_size: 0.5,
+ }
+ }
+
+ fn hovered(&self) -> pick_list::Style {
+ let active = self.active();
+
+ pick_list::Style {
+ border_color: Color {
+ a: 0.9,
+ ..Color::BLACK
+ },
+ ..active
+ }
+ }
+}
diff --git a/examples/geometry/Cargo.toml b/examples/geometry/Cargo.toml
index 9df52454..34eec4fb 100644
--- a/examples/geometry/Cargo.toml
+++ b/examples/geometry/Cargo.toml
@@ -8,4 +8,4 @@ publish = false
[dependencies]
iced = { path = "../.." }
iced_native = { path = "../../native" }
-iced_wgpu = { path = "../../wgpu" }
+iced_graphics = { path = "../../graphics" }
diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs
index 13a687ab..f650b2c1 100644
--- a/examples/geometry/src/main.rs
+++ b/examples/geometry/src/main.rs
@@ -10,13 +10,13 @@ mod rainbow {
// Of course, you can choose to make the implementation renderer-agnostic,
// if you wish to, by creating your own `Renderer` trait, which could be
// implemented by `iced_wgpu` and other renderers.
- use iced_native::{
- layout, Element, Hasher, Layout, Length, MouseCursor, Point, Size,
- Widget,
- };
- use iced_wgpu::{
+ use iced_graphics::{
triangle::{Mesh2D, Vertex2D},
- Defaults, Primitive, Renderer,
+ Backend, Defaults, Primitive, Renderer,
+ };
+ use iced_native::{
+ layout, mouse, Element, Hasher, Layout, Length, Point, Rectangle, Size,
+ Vector, Widget,
};
pub struct Rainbow;
@@ -27,7 +27,10 @@ mod rainbow {
}
}
- impl<Message> Widget<Message, Renderer> for Rainbow {
+ impl<Message, B> Widget<Message, Renderer<B>> for Rainbow
+ where
+ B: Backend,
+ {
fn width(&self) -> Length {
Length::Fill
}
@@ -38,7 +41,7 @@ mod rainbow {
fn layout(
&self,
- _renderer: &Renderer,
+ _renderer: &Renderer<B>,
limits: &layout::Limits,
) -> layout::Node {
let size = limits.width(Length::Fill).resolve(Size::ZERO);
@@ -50,11 +53,12 @@ mod rainbow {
fn draw(
&self,
- _renderer: &mut Renderer,
+ _renderer: &mut Renderer<B>,
_defaults: &Defaults,
layout: Layout<'_>,
cursor_position: Point,
- ) -> (Primitive, MouseCursor) {
+ _viewport: &Rectangle,
+ ) -> (Primitive, mouse::Interaction) {
let b = layout.bounds();
// R O Y G B I V
@@ -85,66 +89,72 @@ mod rainbow {
let posn_l = [0.0, b.height / 2.0];
(
- Primitive::Mesh2D {
- origin: Point::new(b.x, b.y),
- buffers: Mesh2D {
- vertices: vec![
- Vertex2D {
- position: posn_center,
- color: [1.0, 1.0, 1.0, 1.0],
- },
- Vertex2D {
- position: posn_tl,
- color: color_r,
- },
- Vertex2D {
- position: posn_t,
- color: color_o,
- },
- Vertex2D {
- position: posn_tr,
- color: color_y,
- },
- Vertex2D {
- position: posn_r,
- color: color_g,
- },
- Vertex2D {
- position: posn_br,
- color: color_gb,
- },
- Vertex2D {
- position: posn_b,
- color: color_b,
- },
- Vertex2D {
- position: posn_bl,
- color: color_i,
- },
- Vertex2D {
- position: posn_l,
- color: color_v,
- },
- ],
- indices: vec![
- 0, 1, 2, // TL
- 0, 2, 3, // T
- 0, 3, 4, // TR
- 0, 4, 5, // R
- 0, 5, 6, // BR
- 0, 6, 7, // B
- 0, 7, 8, // BL
- 0, 8, 1, // L
- ],
- },
+ Primitive::Translate {
+ translation: Vector::new(b.x, b.y),
+ content: Box::new(Primitive::Mesh2D {
+ size: b.size(),
+ buffers: Mesh2D {
+ vertices: vec![
+ Vertex2D {
+ position: posn_center,
+ color: [1.0, 1.0, 1.0, 1.0],
+ },
+ Vertex2D {
+ position: posn_tl,
+ color: color_r,
+ },
+ Vertex2D {
+ position: posn_t,
+ color: color_o,
+ },
+ Vertex2D {
+ position: posn_tr,
+ color: color_y,
+ },
+ Vertex2D {
+ position: posn_r,
+ color: color_g,
+ },
+ Vertex2D {
+ position: posn_br,
+ color: color_gb,
+ },
+ Vertex2D {
+ position: posn_b,
+ color: color_b,
+ },
+ Vertex2D {
+ position: posn_bl,
+ color: color_i,
+ },
+ Vertex2D {
+ position: posn_l,
+ color: color_v,
+ },
+ ],
+ indices: vec![
+ 0, 1, 2, // TL
+ 0, 2, 3, // T
+ 0, 3, 4, // TR
+ 0, 4, 5, // R
+ 0, 5, 6, // BR
+ 0, 6, 7, // B
+ 0, 7, 8, // BL
+ 0, 8, 1, // L
+ ],
+ },
+ }),
},
- MouseCursor::OutOfBounds,
+ mouse::Interaction::default(),
)
}
}
- impl<'a, Message> Into<Element<'a, Message, Renderer>> for Rainbow {
- fn into(self) -> Element<'a, Message, Renderer> {
+ impl<'a, Message, B> Into<Element<'a, Message, Renderer<B>>> for Rainbow
+ where
+ B: Backend,
+ {
+ fn into(self) -> Element<'a, Message, Renderer<B>> {
Element::new(self)
}
}
@@ -156,7 +166,7 @@ use iced::{
};
use rainbow::Rainbow;
-pub fn main() {
+pub fn main() -> iced::Result {
Example::run(Settings::default())
}
diff --git a/examples/integration/Cargo.toml b/examples/integration/Cargo.toml
index afc2c791..4515502f 100644
--- a/examples/integration/Cargo.toml
+++ b/examples/integration/Cargo.toml
@@ -8,4 +8,4 @@ publish = false
[dependencies]
iced_winit = { path = "../../winit" }
iced_wgpu = { path = "../../wgpu" }
-env_logger = "0.7"
+env_logger = "0.8"
diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs
index 0457a058..824f9f53 100644
--- a/examples/integration/src/controls.rs
+++ b/examples/integration/src/controls.rs
@@ -1,15 +1,15 @@
-use crate::Scene;
-
use iced_wgpu::Renderer;
use iced_winit::{
- slider, Align, Color, Column, Element, Length, Row, Slider, Text,
+ slider, Align, Color, Column, Command, Element, Length, Program, Row,
+ Slider, Text,
};
pub struct Controls {
+ background_color: Color,
sliders: [slider::State; 3],
}
-#[derive(Debug)]
+#[derive(Debug, Clone)]
pub enum Message {
BackgroundColorChanged(Color),
}
@@ -17,61 +17,64 @@ pub enum Message {
impl Controls {
pub fn new() -> Controls {
Controls {
+ background_color: Color::BLACK,
sliders: Default::default(),
}
}
- pub fn update(&self, message: Message, scene: &mut Scene) {
+ pub fn background_color(&self) -> Color {
+ self.background_color
+ }
+}
+
+impl Program for Controls {
+ type Renderer = Renderer;
+ type Message = Message;
+
+ fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::BackgroundColorChanged(color) => {
- scene.background_color = color;
+ self.background_color = color;
}
}
+
+ Command::none()
}
- pub fn view<'a>(
- &'a mut self,
- scene: &Scene,
- ) -> Element<'a, Message, Renderer> {
+ fn view(&mut self) -> Element<Message, Renderer> {
let [r, g, b] = &mut self.sliders;
- let background_color = scene.background_color;
+ let background_color = self.background_color;
let sliders = Row::new()
.width(Length::Units(500))
.spacing(20)
- .push(Slider::new(
- r,
- 0.0..=1.0,
- scene.background_color.r,
- move |r| {
+ .push(
+ Slider::new(r, 0.0..=1.0, background_color.r, move |r| {
Message::BackgroundColorChanged(Color {
r,
..background_color
})
- },
- ))
- .push(Slider::new(
- g,
- 0.0..=1.0,
- scene.background_color.g,
- move |g| {
+ })
+ .step(0.01),
+ )
+ .push(
+ Slider::new(g, 0.0..=1.0, background_color.g, move |g| {
Message::BackgroundColorChanged(Color {
g,
..background_color
})
- },
- ))
- .push(Slider::new(
- b,
- 0.0..=1.0,
- scene.background_color.b,
- move |b| {
+ })
+ .step(0.01),
+ )
+ .push(
+ Slider::new(b, 0.0..=1.0, background_color.b, move |b| {
Message::BackgroundColorChanged(Color {
b,
..background_color
})
- },
- ));
+ })
+ .step(0.01),
+ );
Row::new()
.width(Length::Fill)
diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs
index 2cb89ffc..9b52f3a5 100644
--- a/examples/integration/src/main.rs
+++ b/examples/integration/src/main.rs
@@ -4,12 +4,12 @@ mod scene;
use controls::Controls;
use scene::Scene;
-use iced_wgpu::{
- wgpu, window::SwapChain, Primitive, Renderer, Settings, Target,
-};
-use iced_winit::{winit, Cache, Clipboard, MouseCursor, Size, UserInterface};
+use iced_wgpu::{wgpu, Backend, Renderer, Settings, Viewport};
+use iced_winit::{conversion, futures, program, winit, Debug, Size};
+use futures::task::SpawnExt;
use winit::{
+ dpi::PhysicalPosition,
event::{Event, ModifiersState, WindowEvent},
event_loop::{ControlFlow, EventLoop},
};
@@ -20,45 +20,79 @@ pub fn main() {
// Initialize winit
let event_loop = EventLoop::new();
let window = winit::window::Window::new(&event_loop).unwrap();
- let mut logical_size =
- window.inner_size().to_logical(window.scale_factor());
- let mut modifiers = ModifiersState::default();
- // Initialize WGPU
- let adapter = wgpu::Adapter::request(&wgpu::RequestAdapterOptions {
- power_preference: wgpu::PowerPreference::Default,
- backends: wgpu::BackendBit::PRIMARY,
- })
- .expect("Request adapter");
+ let physical_size = window.inner_size();
+ let mut viewport = Viewport::with_physical_size(
+ Size::new(physical_size.width, physical_size.height),
+ window.scale_factor(),
+ );
+ let mut cursor_position = PhysicalPosition::new(-1.0, -1.0);
+ let mut modifiers = ModifiersState::default();
- let (mut device, mut queue) =
- adapter.request_device(&wgpu::DeviceDescriptor {
- extensions: wgpu::Extensions {
- anisotropic_filtering: false,
- },
- limits: wgpu::Limits::default(),
- });
+ // Initialize wgpu
+ let instance = wgpu::Instance::new(wgpu::BackendBit::PRIMARY);
+ let surface = unsafe { instance.create_surface(&window) };
+
+ let (mut device, queue) = futures::executor::block_on(async {
+ let adapter = instance
+ .request_adapter(&wgpu::RequestAdapterOptions {
+ power_preference: wgpu::PowerPreference::Default,
+ compatible_surface: Some(&surface),
+ })
+ .await
+ .expect("Request adapter");
+
+ adapter
+ .request_device(
+ &wgpu::DeviceDescriptor {
+ features: wgpu::Features::empty(),
+ limits: wgpu::Limits::default(),
+ shader_validation: false,
+ },
+ None,
+ )
+ .await
+ .expect("Request device")
+ });
- let surface = wgpu::Surface::create(&window);
let format = wgpu::TextureFormat::Bgra8UnormSrgb;
let mut swap_chain = {
let size = window.inner_size();
- SwapChain::new(&device, &surface, format, size.width, size.height)
+ device.create_swap_chain(
+ &surface,
+ &wgpu::SwapChainDescriptor {
+ usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT,
+ format: format,
+ width: size.width,
+ height: size.height,
+ present_mode: wgpu::PresentMode::Mailbox,
+ },
+ )
};
let mut resized = false;
- // Initialize iced
- let mut events = Vec::new();
- let mut cache = Some(Cache::default());
- let mut renderer = Renderer::new(&mut device, Settings::default());
- let mut output = (Primitive::None, MouseCursor::OutOfBounds);
- let clipboard = Clipboard::new(&window);
+ // Initialize staging belt and local pool
+ let mut staging_belt = wgpu::util::StagingBelt::new(5 * 1024);
+ let mut local_pool = futures::executor::LocalPool::new();
// Initialize scene and GUI controls
- let mut scene = Scene::new(&device);
- let mut controls = Controls::new();
+ let scene = Scene::new(&mut device);
+ let controls = Controls::new();
+
+ // Initialize iced
+ let mut debug = Debug::new();
+ let mut renderer =
+ Renderer::new(Backend::new(&mut device, Settings::default()));
+
+ let mut state = program::State::new(
+ controls,
+ viewport.logical_size(),
+ conversion::cursor_position(cursor_position, viewport.scale_factor()),
+ &mut renderer,
+ &mut debug,
+ );
// Run event loop
event_loop.run(move |event, _, control_flow| {
@@ -68,135 +102,121 @@ pub fn main() {
match event {
Event::WindowEvent { event, .. } => {
match event {
+ WindowEvent::CursorMoved { position, .. } => {
+ cursor_position = position;
+ }
WindowEvent::ModifiersChanged(new_modifiers) => {
modifiers = new_modifiers;
}
WindowEvent::Resized(new_size) => {
- logical_size =
- new_size.to_logical(window.scale_factor());
+ viewport = Viewport::with_physical_size(
+ Size::new(new_size.width, new_size.height),
+ window.scale_factor(),
+ );
+
resized = true;
}
WindowEvent::CloseRequested => {
*control_flow = ControlFlow::Exit;
}
-
_ => {}
}
// Map window event to iced event
if let Some(event) = iced_winit::conversion::window_event(
- event,
+ &event,
window.scale_factor(),
modifiers,
) {
- events.push(event);
+ state.queue_event(event);
}
}
Event::MainEventsCleared => {
- // If no relevant events happened, we can simply skip this
- if events.is_empty() {
- return;
- }
-
- // We need to:
- // 1. Process events of our user interface.
- // 2. Update state as a result of any interaction.
- // 3. Generate a new output for our renderer.
-
- // First, we build our user interface.
- let mut user_interface = UserInterface::build(
- controls.view(&scene),
- Size::new(logical_size.width, logical_size.height),
- cache.take().unwrap(),
- &mut renderer,
- );
-
- // Then, we process the events, obtaining messages in return.
- let messages = user_interface.update(
- events.drain(..),
- clipboard.as_ref().map(|c| c as _),
- &renderer,
- );
-
- let user_interface = if messages.is_empty() {
- // If there are no messages, no interactions we care about have
- // happened. We can simply leave our user interface as it is.
- user_interface
- } else {
- // If there are messages, we need to update our state
- // accordingly and rebuild our user interface.
- // We can only do this if we drop our user interface first
- // by turning it into its cache.
- cache = Some(user_interface.into_cache());
-
- // In this example, `Controls` is the only part that cares
- // about messages, so updating our state is pretty
- // straightforward.
- for message in messages {
- controls.update(message, &mut scene);
- }
-
- // Once the state has been changed, we rebuild our updated
- // user interface.
- UserInterface::build(
- controls.view(&scene),
- Size::new(logical_size.width, logical_size.height),
- cache.take().unwrap(),
+ // If there are events pending
+ if !state.is_queue_empty() {
+ // We update iced
+ let _ = state.update(
+ viewport.logical_size(),
+ conversion::cursor_position(
+ cursor_position,
+ viewport.scale_factor(),
+ ),
+ None,
&mut renderer,
- )
- };
-
- // Finally, we just need to draw a new output for our renderer,
- output = user_interface.draw(&mut renderer);
-
- // update our cache,
- cache = Some(user_interface.into_cache());
+ &mut debug,
+ );
- // and request a redraw
- window.request_redraw();
+ // and request a redraw
+ window.request_redraw();
+ }
}
Event::RedrawRequested(_) => {
if resized {
let size = window.inner_size();
- swap_chain = SwapChain::new(
- &device,
+ swap_chain = device.create_swap_chain(
&surface,
- format,
- size.width,
- size.height,
+ &wgpu::SwapChainDescriptor {
+ usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT,
+ format: format,
+ width: size.width,
+ height: size.height,
+ present_mode: wgpu::PresentMode::Mailbox,
+ },
);
+
+ resized = false;
}
- let (frame, viewport) = swap_chain.next_frame();
+ let frame = swap_chain.get_current_frame().expect("Next frame");
let mut encoder = device.create_command_encoder(
- &wgpu::CommandEncoderDescriptor { todo: 0 },
+ &wgpu::CommandEncoderDescriptor { label: None },
);
- // We draw the scene first
- scene.draw(&mut encoder, &frame.view);
+ let program = state.program();
+
+ {
+ // We clear the frame
+ let mut render_pass = scene.clear(
+ &frame.output.view,
+ &mut encoder,
+ program.background_color(),
+ );
+
+ // Draw the scene
+ scene.draw(&mut render_pass);
+ }
// And then iced on top
- let mouse_cursor = renderer.draw(
+ let mouse_interaction = renderer.backend_mut().draw(
&mut device,
+ &mut staging_belt,
&mut encoder,
- Target {
- texture: &frame.view,
- viewport,
- },
- &output,
- window.scale_factor(),
- &["Some debug information!"],
+ &frame.output.view,
+ &viewport,
+ state.primitive(),
+ &debug.overlay(),
);
// Then we submit the work
- queue.submit(&[encoder.finish()]);
+ staging_belt.finish();
+ queue.submit(Some(encoder.finish()));
+
+ // Update the mouse cursor
+ window.set_cursor_icon(
+ iced_winit::conversion::mouse_interaction(
+ mouse_interaction,
+ ),
+ );
+
+ // And recall staging buffers
+ local_pool
+ .spawner()
+ .spawn(staging_belt.recall())
+ .expect("Recall staging buffers");
- // And update the mouse cursor
- window.set_cursor_icon(iced_winit::conversion::mouse_cursor(
- mouse_cursor,
- ));
+ local_pool.run_until_stalled();
}
_ => {}
}
diff --git a/examples/integration/src/scene.rs b/examples/integration/src/scene.rs
index efb1921b..03a338c6 100644
--- a/examples/integration/src/scene.rs
+++ b/examples/integration/src/scene.rs
@@ -2,89 +2,68 @@ use iced_wgpu::wgpu;
use iced_winit::Color;
pub struct Scene {
- pub background_color: Color,
pipeline: wgpu::RenderPipeline,
- bind_group: wgpu::BindGroup,
}
impl Scene {
pub fn new(device: &wgpu::Device) -> Scene {
- let (pipeline, bind_group) = build_pipeline(device);
+ let pipeline = build_pipeline(device);
- Scene {
- background_color: Color::BLACK,
- pipeline,
- bind_group,
- }
+ Scene { pipeline }
}
- pub fn draw(
+ pub fn clear<'a>(
&self,
- encoder: &mut wgpu::CommandEncoder,
- target: &wgpu::TextureView,
- ) {
- let mut rpass =
- encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
- color_attachments: &[
- wgpu::RenderPassColorAttachmentDescriptor {
- attachment: target,
- resolve_target: None,
- load_op: wgpu::LoadOp::Clear,
- store_op: wgpu::StoreOp::Store,
- clear_color: {
- let [r, g, b, a] =
- self.background_color.into_linear();
+ target: &'a wgpu::TextureView,
+ encoder: &'a mut wgpu::CommandEncoder,
+ background_color: Color,
+ ) -> wgpu::RenderPass<'a> {
+ encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor {
+ attachment: target,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Clear({
+ let [r, g, b, a] = background_color.into_linear();
- wgpu::Color {
- r: r as f64,
- g: g as f64,
- b: b as f64,
- a: a as f64,
- }
- },
- },
- ],
- depth_stencil_attachment: None,
- });
+ wgpu::Color {
+ r: r as f64,
+ g: g as f64,
+ b: b as f64,
+ a: a as f64,
+ }
+ }),
+ store: true,
+ },
+ }],
+ depth_stencil_attachment: None,
+ })
+ }
- rpass.set_pipeline(&self.pipeline);
- rpass.set_bind_group(0, &self.bind_group, &[]);
- rpass.draw(0..3, 0..1);
+ pub fn draw<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {
+ render_pass.set_pipeline(&self.pipeline);
+ render_pass.draw(0..3, 0..1);
}
}
-fn build_pipeline(
- device: &wgpu::Device,
-) -> (wgpu::RenderPipeline, wgpu::BindGroup) {
- let vs = include_bytes!("shader/vert.spv");
- let fs = include_bytes!("shader/frag.spv");
+fn build_pipeline(device: &wgpu::Device) -> wgpu::RenderPipeline {
+ let vs_module =
+ device.create_shader_module(wgpu::include_spirv!("shader/vert.spv"));
- let vs_module = device.create_shader_module(
- &wgpu::read_spirv(std::io::Cursor::new(&vs[..])).unwrap(),
- );
-
- let fs_module = device.create_shader_module(
- &wgpu::read_spirv(std::io::Cursor::new(&fs[..])).unwrap(),
- );
-
- let bind_group_layout =
- device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
- bindings: &[],
- });
-
- let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
- layout: &bind_group_layout,
- bindings: &[],
- });
+ let fs_module =
+ device.create_shader_module(wgpu::include_spirv!("shader/frag.spv"));
let pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
- bind_group_layouts: &[&bind_group_layout],
+ label: None,
+ push_constant_ranges: &[],
+ bind_group_layouts: &[],
});
let pipeline =
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
- layout: &pipeline_layout,
+ label: None,
+ layout: Some(&pipeline_layout),
vertex_stage: wgpu::ProgrammableStageDescriptor {
module: &vs_module,
entry_point: "main",
@@ -96,9 +75,7 @@ fn build_pipeline(
rasterization_state: Some(wgpu::RasterizationStateDescriptor {
front_face: wgpu::FrontFace::Ccw,
cull_mode: wgpu::CullMode::None,
- depth_bias: 0,
- depth_bias_slope_scale: 0.0,
- depth_bias_clamp: 0.0,
+ ..Default::default()
}),
primitive_topology: wgpu::PrimitiveTopology::TriangleList,
color_states: &[wgpu::ColorStateDescriptor {
@@ -108,12 +85,14 @@ fn build_pipeline(
write_mask: wgpu::ColorWrite::ALL,
}],
depth_stencil_state: None,
- index_format: wgpu::IndexFormat::Uint16,
- vertex_buffers: &[],
+ vertex_state: wgpu::VertexStateDescriptor {
+ index_format: wgpu::IndexFormat::Uint16,
+ vertex_buffers: &[],
+ },
sample_count: 1,
sample_mask: !0,
alpha_to_coverage_enabled: false,
});
- (pipeline, bind_group)
+ pipeline
}
diff --git a/examples/pane_grid/Cargo.toml b/examples/pane_grid/Cargo.toml
new file mode 100644
index 00000000..e489f210
--- /dev/null
+++ b/examples/pane_grid/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "pane_grid"
+version = "0.1.0"
+authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
+edition = "2018"
+publish = false
+
+[dependencies]
+iced = { path = "../..", features = ["debug"] }
+iced_native = { path = "../../native" }
diff --git a/examples/pane_grid/README.md b/examples/pane_grid/README.md
new file mode 100644
index 00000000..a4cfcb7d
--- /dev/null
+++ b/examples/pane_grid/README.md
@@ -0,0 +1,28 @@
+## Pane grid
+
+A grid of panes that can be split, resized, and reorganized.
+
+This example showcases the `PaneGrid` widget, which features:
+
+* Vertical and horizontal splits
+* Tracking of the last active pane
+* Mouse-based resizing
+* Drag and drop to reorganize panes
+* Hotkey support
+* Configurable modifier keys
+* API to perform actions programmatically (`split`, `swap`, `resize`, etc.)
+
+The __[`main`]__ file contains all the code of the example.
+
+<div align="center">
+ <a href="https://gfycat.com/frailfreshairedaleterrier">
+ <img src="https://thumbs.gfycat.com/FrailFreshAiredaleterrier-small.gif">
+ </a>
+</div>
+
+You can run it with `cargo run`:
+```
+cargo run --package pane_grid
+```
+
+[`main`]: src/main.rs
diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs
new file mode 100644
index 00000000..3c3256cf
--- /dev/null
+++ b/examples/pane_grid/src/main.rs
@@ -0,0 +1,372 @@
+use iced::{
+ button, executor, keyboard, pane_grid, scrollable, Align, Application,
+ Button, Column, Command, Container, Element, HorizontalAlignment, Length,
+ PaneGrid, Scrollable, Settings, Subscription, Text,
+};
+use iced_native::{event, subscription, Event};
+
+pub fn main() -> iced::Result {
+ Example::run(Settings::default())
+}
+
+struct Example {
+ panes: pane_grid::State<Content>,
+ panes_created: usize,
+ focus: Option<pane_grid::Pane>,
+}
+
+#[derive(Debug, Clone, Copy)]
+enum Message {
+ Split(pane_grid::Axis, pane_grid::Pane),
+ SplitFocused(pane_grid::Axis),
+ FocusAdjacent(pane_grid::Direction),
+ Clicked(pane_grid::Pane),
+ Dragged(pane_grid::DragEvent),
+ Resized(pane_grid::ResizeEvent),
+ Close(pane_grid::Pane),
+ CloseFocused,
+}
+
+impl Application for Example {
+ type Message = Message;
+ type Executor = executor::Default;
+ type Flags = ();
+
+ fn new(_flags: ()) -> (Self, Command<Message>) {
+ let (panes, _) = pane_grid::State::new(Content::new(0));
+
+ (
+ Example {
+ panes,
+ panes_created: 1,
+ focus: None,
+ },
+ Command::none(),
+ )
+ }
+
+ fn title(&self) -> String {
+ String::from("Pane grid - Iced")
+ }
+
+ fn update(&mut self, message: Message) -> Command<Message> {
+ match message {
+ Message::Split(axis, pane) => {
+ let result = self.panes.split(
+ axis,
+ &pane,
+ Content::new(self.panes_created),
+ );
+
+ if let Some((pane, _)) = result {
+ self.focus = Some(pane);
+ }
+
+ self.panes_created += 1;
+ }
+ Message::SplitFocused(axis) => {
+ if let Some(pane) = self.focus {
+ let result = self.panes.split(
+ axis,
+ &pane,
+ Content::new(self.panes_created),
+ );
+
+ if let Some((pane, _)) = result {
+ self.focus = Some(pane);
+ }
+
+ self.panes_created += 1;
+ }
+ }
+ Message::FocusAdjacent(direction) => {
+ if let Some(pane) = self.focus {
+ if let Some(adjacent) =
+ self.panes.adjacent(&pane, direction)
+ {
+ self.focus = Some(adjacent);
+ }
+ }
+ }
+ Message::Clicked(pane) => {
+ self.focus = Some(pane);
+ }
+ Message::Resized(pane_grid::ResizeEvent { split, ratio }) => {
+ self.panes.resize(&split, ratio);
+ }
+ Message::Dragged(pane_grid::DragEvent::Dropped {
+ pane,
+ target,
+ }) => {
+ self.panes.swap(&pane, &target);
+ }
+ Message::Dragged(_) => {}
+ Message::Close(pane) => {
+ if let Some((_, sibling)) = self.panes.close(&pane) {
+ self.focus = Some(sibling);
+ }
+ }
+ Message::CloseFocused => {
+ if let Some(pane) = self.focus {
+ if let Some((_, sibling)) = self.panes.close(&pane) {
+ self.focus = Some(sibling);
+ }
+ }
+ }
+ }
+
+ Command::none()
+ }
+
+ fn subscription(&self) -> Subscription<Message> {
+ subscription::events_with(|event, status| {
+ if let event::Status::Captured = status {
+ return None;
+ }
+
+ match event {
+ Event::Keyboard(keyboard::Event::KeyPressed {
+ modifiers,
+ key_code,
+ }) if modifiers.is_command_pressed() => handle_hotkey(key_code),
+ _ => None,
+ }
+ })
+ }
+
+ fn view(&mut self) -> Element<Message> {
+ 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 })
+ })
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .spacing(10)
+ .on_click(Message::Clicked)
+ .on_drag(Message::Dragged)
+ .on_resize(10, Message::Resized);
+
+ Container::new(pane_grid)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .padding(10)
+ .into()
+ }
+}
+
+fn handle_hotkey(key_code: keyboard::KeyCode) -> Option<Message> {
+ use keyboard::KeyCode;
+ use pane_grid::{Axis, Direction};
+
+ let direction = match key_code {
+ KeyCode::Up => Some(Direction::Up),
+ KeyCode::Down => Some(Direction::Down),
+ KeyCode::Left => Some(Direction::Left),
+ KeyCode::Right => Some(Direction::Right),
+ _ => None,
+ };
+
+ match key_code {
+ KeyCode::V => Some(Message::SplitFocused(Axis::Vertical)),
+ KeyCode::H => Some(Message::SplitFocused(Axis::Horizontal)),
+ KeyCode::W => Some(Message::CloseFocused),
+ _ => direction.map(Message::FocusAdjacent),
+ }
+}
+
+struct Content {
+ id: usize,
+ scroll: scrollable::State,
+ split_horizontally: button::State,
+ split_vertically: button::State,
+ close: button::State,
+}
+
+impl Content {
+ fn new(id: usize) -> Self {
+ Content {
+ id,
+ scroll: scrollable::State::new(),
+ split_horizontally: button::State::new(),
+ split_vertically: button::State::new(),
+ close: button::State::new(),
+ }
+ }
+ fn view(
+ &mut self,
+ pane: pane_grid::Pane,
+ total_panes: usize,
+ ) -> Element<Message> {
+ let Content {
+ scroll,
+ split_horizontally,
+ split_vertically,
+ close,
+ ..
+ } = self;
+
+ let button = |state, label, message, style| {
+ Button::new(
+ state,
+ Text::new(label)
+ .width(Length::Fill)
+ .horizontal_alignment(HorizontalAlignment::Center)
+ .size(16),
+ )
+ .width(Length::Fill)
+ .padding(8)
+ .on_press(message)
+ .style(style)
+ };
+
+ let mut controls = Column::new()
+ .spacing(5)
+ .max_width(150)
+ .push(button(
+ split_horizontally,
+ "Split horizontally",
+ Message::Split(pane_grid::Axis::Horizontal, pane),
+ style::Button::Primary,
+ ))
+ .push(button(
+ split_vertically,
+ "Split vertically",
+ Message::Split(pane_grid::Axis::Vertical, pane),
+ style::Button::Primary,
+ ));
+
+ if total_panes > 1 {
+ controls = controls.push(button(
+ close,
+ "Close",
+ Message::Close(pane),
+ style::Button::Destructive,
+ ));
+ }
+
+ let content = Scrollable::new(scroll)
+ .width(Length::Fill)
+ .spacing(10)
+ .align_items(Align::Center)
+ .push(controls);
+
+ Container::new(content)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .padding(5)
+ .center_y()
+ .into()
+ }
+}
+
+mod style {
+ use iced::{button, container, Background, Color, Vector};
+
+ const SURFACE: Color = Color::from_rgb(
+ 0xF2 as f32 / 255.0,
+ 0xF3 as f32 / 255.0,
+ 0xF5 as f32 / 255.0,
+ );
+
+ const ACTIVE: Color = Color::from_rgb(
+ 0x72 as f32 / 255.0,
+ 0x89 as f32 / 255.0,
+ 0xDA as f32 / 255.0,
+ );
+
+ const HOVERED: Color = Color::from_rgb(
+ 0x67 as f32 / 255.0,
+ 0x7B as f32 / 255.0,
+ 0xC4 as f32 / 255.0,
+ );
+
+ pub struct TitleBar {
+ pub is_focused: bool,
+ }
+
+ impl container::StyleSheet for TitleBar {
+ fn style(&self) -> container::Style {
+ let pane = Pane {
+ is_focused: self.is_focused,
+ }
+ .style();
+
+ container::Style {
+ text_color: Some(Color::WHITE),
+ background: Some(pane.border_color.into()),
+ ..Default::default()
+ }
+ }
+ }
+
+ pub struct Pane {
+ pub is_focused: bool,
+ }
+
+ impl container::StyleSheet for Pane {
+ fn style(&self) -> container::Style {
+ container::Style {
+ background: Some(Background::Color(SURFACE)),
+ border_width: 2.0,
+ border_color: if self.is_focused {
+ Color::BLACK
+ } else {
+ Color::from_rgb(0.7, 0.7, 0.7)
+ },
+ ..Default::default()
+ }
+ }
+ }
+
+ pub enum Button {
+ Primary,
+ Destructive,
+ }
+
+ impl button::StyleSheet for Button {
+ fn active(&self) -> button::Style {
+ let (background, text_color) = match self {
+ Button::Primary => (Some(ACTIVE), Color::WHITE),
+ Button::Destructive => {
+ (None, Color::from_rgb8(0xFF, 0x47, 0x47))
+ }
+ };
+
+ button::Style {
+ text_color,
+ background: background.map(Background::Color),
+ border_radius: 5.0,
+ shadow_offset: Vector::new(0.0, 0.0),
+ ..button::Style::default()
+ }
+ }
+
+ fn hovered(&self) -> button::Style {
+ let active = self.active();
+
+ let background = match self {
+ Button::Primary => Some(HOVERED),
+ Button::Destructive => Some(Color {
+ a: 0.2,
+ ..active.text_color
+ }),
+ };
+
+ button::Style {
+ background: background.map(Background::Color),
+ ..active
+ }
+ }
+ }
+}
diff --git a/examples/pick_list/Cargo.toml b/examples/pick_list/Cargo.toml
new file mode 100644
index 00000000..a87d7217
--- /dev/null
+++ b/examples/pick_list/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "pick_list"
+version = "0.1.0"
+authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
+edition = "2018"
+publish = false
+
+[dependencies]
+iced = { path = "../..", features = ["debug"] }
diff --git a/examples/pick_list/README.md b/examples/pick_list/README.md
new file mode 100644
index 00000000..6dc80bf4
--- /dev/null
+++ b/examples/pick_list/README.md
@@ -0,0 +1,18 @@
+## Pick-list
+
+A dropdown list of selectable options.
+
+It displays and positions an overlay based on the window position of the widget.
+
+The __[`main`]__ file contains all the code of the example.
+
+<div align="center">
+ <img src="https://user-images.githubusercontent.com/518289/87125075-2c232e80-c28a-11ea-95c2-769c610b8843.gif">
+</div>
+
+You can run it with `cargo run`:
+```
+cargo run --package pick_list
+```
+
+[`main`]: src/main.rs
diff --git a/examples/pick_list/src/main.rs b/examples/pick_list/src/main.rs
new file mode 100644
index 00000000..68662602
--- /dev/null
+++ b/examples/pick_list/src/main.rs
@@ -0,0 +1,113 @@
+use iced::{
+ pick_list, scrollable, Align, Container, Element, Length, PickList,
+ Sandbox, Scrollable, Settings, Space, Text,
+};
+
+pub fn main() -> iced::Result {
+ Example::run(Settings::default())
+}
+
+#[derive(Default)]
+struct Example {
+ scroll: scrollable::State,
+ pick_list: pick_list::State<Language>,
+ selected_language: Language,
+}
+
+#[derive(Debug, Clone, Copy)]
+enum Message {
+ LanguageSelected(Language),
+}
+
+impl Sandbox for Example {
+ type Message = Message;
+
+ fn new() -> Self {
+ Self::default()
+ }
+
+ fn title(&self) -> String {
+ String::from("Pick list - Iced")
+ }
+
+ fn update(&mut self, message: Message) {
+ match message {
+ Message::LanguageSelected(language) => {
+ self.selected_language = language;
+ }
+ }
+ }
+
+ fn view(&mut self) -> Element<Message> {
+ let pick_list = PickList::new(
+ &mut self.pick_list,
+ &Language::ALL[..],
+ Some(self.selected_language),
+ Message::LanguageSelected,
+ );
+
+ let mut content = Scrollable::new(&mut self.scroll)
+ .width(Length::Fill)
+ .align_items(Align::Center)
+ .spacing(10)
+ .push(Space::with_height(Length::Units(600)))
+ .push(Text::new("Which is your favorite language?"))
+ .push(pick_list);
+
+ content = content.push(Space::with_height(Length::Units(600)));
+
+ Container::new(content)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .center_x()
+ .center_y()
+ .into()
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Language {
+ Rust,
+ Elm,
+ Ruby,
+ Haskell,
+ C,
+ Javascript,
+ Other,
+}
+
+impl Language {
+ const ALL: [Language; 7] = [
+ Language::C,
+ Language::Elm,
+ Language::Ruby,
+ Language::Haskell,
+ Language::Rust,
+ Language::Javascript,
+ Language::Other,
+ ];
+}
+
+impl Default for Language {
+ fn default() -> Language {
+ Language::Rust
+ }
+}
+
+impl std::fmt::Display for Language {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "{}",
+ match self {
+ Language::Rust => "Rust",
+ Language::Elm => "Elm",
+ Language::Ruby => "Ruby",
+ Language::Haskell => "Haskell",
+ Language::C => "C",
+ Language::Javascript => "Javascript",
+ Language::Other => "Some other language",
+ }
+ )
+ }
+}
diff --git a/examples/pokedex/Cargo.toml b/examples/pokedex/Cargo.toml
index 94320086..05e73992 100644
--- a/examples/pokedex/Cargo.toml
+++ b/examples/pokedex/Cargo.toml
@@ -6,7 +6,7 @@ edition = "2018"
publish = false
[dependencies]
-iced = { path = "../..", features = ["image", "debug", "tokio"] }
+iced = { path = "../..", features = ["image", "debug", "tokio_old"] }
serde_json = "1.0"
[dependencies.serde]
diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs
index 4449b901..187e5dee 100644
--- a/examples/pokedex/src/main.rs
+++ b/examples/pokedex/src/main.rs
@@ -3,7 +3,7 @@ use iced::{
Container, Element, Image, Length, Row, Settings, Text,
};
-pub fn main() {
+pub fn main() -> iced::Result {
Pokedex::run(Settings::default())
}
@@ -29,8 +29,9 @@ enum Message {
impl Application for Pokedex {
type Executor = iced::executor::Default;
type Message = Message;
+ type Flags = ();
- fn new() -> (Pokedex, Command<Message>) {
+ fn new(_flags: ()) -> (Pokedex, Command<Message>) {
(
Pokedex::Loading,
Command::perform(Pokemon::search(), Message::PokemonFound),
@@ -225,7 +226,7 @@ enum Error {
impl From<reqwest::Error> for Error {
fn from(error: reqwest::Error) -> Error {
- dbg!(&error);
+ dbg!(error);
Error::APIError
}
@@ -250,7 +251,7 @@ mod style {
background: Some(Background::Color(match self {
Button::Primary => Color::from_rgb(0.11, 0.42, 0.87),
})),
- border_radius: 12,
+ border_radius: 12.0,
shadow_offset: Vector::new(1.0, 1.0),
text_color: Color::WHITE,
..button::Style::default()
diff --git a/examples/progress_bar/src/main.rs b/examples/progress_bar/src/main.rs
index 43b09928..c9a8e798 100644
--- a/examples/progress_bar/src/main.rs
+++ b/examples/progress_bar/src/main.rs
@@ -1,6 +1,6 @@
use iced::{slider, Column, Element, ProgressBar, Sandbox, Settings, Slider};
-pub fn main() {
+pub fn main() -> iced::Result {
Progress::run(Settings::default())
}
@@ -36,12 +36,15 @@ impl Sandbox for Progress {
Column::new()
.padding(20)
.push(ProgressBar::new(0.0..=100.0, self.value))
- .push(Slider::new(
- &mut self.progress_bar_slider,
- 0.0..=100.0,
- self.value,
- Message::SliderChanged,
- ))
+ .push(
+ Slider::new(
+ &mut self.progress_bar_slider,
+ 0.0..=100.0,
+ self.value,
+ Message::SliderChanged,
+ )
+ .step(0.01),
+ )
.into()
}
}
diff --git a/examples/qr_code/Cargo.toml b/examples/qr_code/Cargo.toml
new file mode 100644
index 00000000..7f2d4e42
--- /dev/null
+++ b/examples/qr_code/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "qr_code"
+version = "0.1.0"
+authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
+edition = "2018"
+publish = false
+
+[dependencies]
+iced = { path = "../..", features = ["qr_code"] }
diff --git a/examples/qr_code/README.md b/examples/qr_code/README.md
new file mode 100644
index 00000000..2dd89c26
--- /dev/null
+++ b/examples/qr_code/README.md
@@ -0,0 +1,18 @@
+## QR Code Generator
+
+A basic QR code generator that showcases the `QRCode` widget.
+
+The __[`main`]__ file contains all the code of the example.
+
+<div align="center">
+ <a href="https://gfycat.com/heavyexhaustedaracari">
+ <img src="https://thumbs.gfycat.com/HeavyExhaustedAracari-size_restricted.gif">
+ </a>
+</div>
+
+You can run it with `cargo run`:
+```
+cargo run --package qr_code
+```
+
+[`main`]: src/main.rs
diff --git a/examples/qr_code/src/main.rs b/examples/qr_code/src/main.rs
new file mode 100644
index 00000000..37b4855d
--- /dev/null
+++ b/examples/qr_code/src/main.rs
@@ -0,0 +1,81 @@
+use iced::qr_code::{self, QRCode};
+use iced::text_input::{self, TextInput};
+use iced::{
+ Align, Column, Container, Element, Length, Sandbox, Settings, Text,
+};
+
+pub fn main() -> iced::Result {
+ QRGenerator::run(Settings::default())
+}
+
+#[derive(Default)]
+struct QRGenerator {
+ data: String,
+ input: text_input::State,
+ qr_code: Option<qr_code::State>,
+}
+
+#[derive(Debug, Clone)]
+enum Message {
+ DataChanged(String),
+}
+
+impl Sandbox for QRGenerator {
+ type Message = Message;
+
+ fn new() -> Self {
+ QRGenerator {
+ qr_code: qr_code::State::new("").ok(),
+ ..Self::default()
+ }
+ }
+
+ fn title(&self) -> String {
+ String::from("QR Code Generator - Iced")
+ }
+
+ fn update(&mut self, message: Message) {
+ match message {
+ Message::DataChanged(mut data) => {
+ data.truncate(100);
+
+ self.qr_code = qr_code::State::new(&data).ok();
+ self.data = data;
+ }
+ }
+ }
+
+ fn view(&mut self) -> Element<Message> {
+ let title = Text::new("QR Code Generator")
+ .size(70)
+ .color([0.5, 0.5, 0.5]);
+
+ let input = TextInput::new(
+ &mut self.input,
+ "Type the data of your QR code here...",
+ &self.data,
+ Message::DataChanged,
+ )
+ .size(30)
+ .padding(15);
+
+ let mut content = Column::new()
+ .width(Length::Units(700))
+ .spacing(20)
+ .align_items(Align::Center)
+ .push(title)
+ .push(input);
+
+ if let Some(qr_code) = self.qr_code.as_mut() {
+ content = content.push(QRCode::new(qr_code).cell_size(10));
+ }
+
+ Container::new(content)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .padding(20)
+ .center_x()
+ .center_y()
+ .into()
+ }
+}
diff --git a/examples/scrollable/Cargo.toml b/examples/scrollable/Cargo.toml
new file mode 100644
index 00000000..12753fb6
--- /dev/null
+++ b/examples/scrollable/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "scrollable"
+version = "0.1.0"
+authors = ["Clark Moody <clark@clarkmoody.com>"]
+edition = "2018"
+publish = false
+
+[dependencies]
+iced = { path = "../.." }
diff --git a/examples/scrollable/README.md b/examples/scrollable/README.md
new file mode 100644
index 00000000..ed0e31b5
--- /dev/null
+++ b/examples/scrollable/README.md
@@ -0,0 +1,15 @@
+# Scrollable
+An example showcasing the various size and style options for the Scrollable.
+
+All the example code is located in the __[`main`](src/main.rs)__ file.
+
+<div align="center">
+ <a href="./screenshot.png">
+ <img src="./screenshot.png" height="640px">
+ </a>
+</div>
+
+You can run it with `cargo run`:
+```
+cargo run --package scrollable
+```
diff --git a/examples/scrollable/screenshot.png b/examples/scrollable/screenshot.png
new file mode 100644
index 00000000..2d800251
--- /dev/null
+++ b/examples/scrollable/screenshot.png
Binary files differ
diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs
new file mode 100644
index 00000000..8dd2e20c
--- /dev/null
+++ b/examples/scrollable/src/main.rs
@@ -0,0 +1,184 @@
+mod style;
+
+use iced::{
+ scrollable, Column, Container, Element, Length, Radio, Row, Rule, Sandbox,
+ Scrollable, Settings, Space, Text,
+};
+
+pub fn main() -> iced::Result {
+ ScrollableDemo::run(Settings::default())
+}
+
+struct ScrollableDemo {
+ theme: style::Theme,
+ variants: Vec<Variant>,
+}
+
+#[derive(Debug, Clone)]
+enum Message {
+ ThemeChanged(style::Theme),
+}
+
+impl Sandbox for ScrollableDemo {
+ type Message = Message;
+
+ fn new() -> Self {
+ ScrollableDemo {
+ theme: Default::default(),
+ variants: Variant::all(),
+ }
+ }
+
+ fn title(&self) -> String {
+ String::from("Scrollable - Iced")
+ }
+
+ fn update(&mut self, message: Message) {
+ match message {
+ Message::ThemeChanged(theme) => self.theme = theme,
+ }
+ }
+
+ fn view(&mut self) -> Element<Message> {
+ let ScrollableDemo {
+ theme, variants, ..
+ } = self;
+
+ let choose_theme = style::Theme::ALL.iter().fold(
+ Column::new().spacing(10).push(Text::new("Choose a theme:")),
+ |column, option| {
+ column.push(
+ Radio::new(
+ *option,
+ &format!("{:?}", option),
+ Some(*theme),
+ Message::ThemeChanged,
+ )
+ .style(*theme),
+ )
+ },
+ );
+
+ 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));
+
+ if let Some(scrollbar_width) = variant.scrollbar_width {
+ scrollable = scrollable
+ .scrollbar_width(scrollbar_width)
+ .push(Text::new(format!(
+ "scrollbar_width: {:?}",
+ scrollbar_width
+ )));
+ }
+
+ if let Some(scrollbar_margin) = variant.scrollbar_margin {
+ scrollable = scrollable
+ .scrollbar_margin(scrollbar_margin)
+ .push(Text::new(format!(
+ "scrollbar_margin: {:?}",
+ scrollbar_margin
+ )));
+ }
+
+ if let Some(scroller_width) = variant.scroller_width {
+ scrollable = scrollable
+ .scroller_width(scroller_width)
+ .push(Text::new(format!(
+ "scroller_width: {:?}",
+ scroller_width
+ )));
+ }
+
+ scrollable = scrollable
+ .push(Space::with_height(Length::Units(100)))
+ .push(Text::new(
+ "Some content that should wrap within the \
+ scrollable. Let's output a lot of short words, so \
+ that we'll make sure to see how wrapping works \
+ with these scrollbars.",
+ ))
+ .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)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .style(*theme)
+ .into()
+ })
+ .collect(),
+ )
+ .spacing(20)
+ .width(Length::Fill)
+ .height(Length::Fill);
+
+ let content = Column::new()
+ .spacing(20)
+ .padding(20)
+ .push(choose_theme)
+ .push(Rule::horizontal(20).style(self.theme))
+ .push(scrollable_row);
+
+ Container::new(content)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .center_x()
+ .center_y()
+ .style(self.theme)
+ .into()
+ }
+}
+
+/// A version of a scrollable
+struct Variant {
+ title: &'static str,
+ state: scrollable::State,
+ scrollbar_width: Option<u16>,
+ scrollbar_margin: Option<u16>,
+ scroller_width: Option<u16>,
+}
+
+impl Variant {
+ pub fn all() -> Vec<Self> {
+ vec![
+ Self {
+ title: "Default Scrollbar",
+ state: scrollable::State::new(),
+ scrollbar_width: None,
+ scrollbar_margin: None,
+ scroller_width: None,
+ },
+ Self {
+ title: "Slimmed & Margin",
+ state: scrollable::State::new(),
+ scrollbar_width: Some(4),
+ scrollbar_margin: Some(3),
+ scroller_width: Some(4),
+ },
+ Self {
+ title: "Wide Scroller",
+ state: scrollable::State::new(),
+ scrollbar_width: Some(4),
+ scrollbar_margin: None,
+ scroller_width: Some(10),
+ },
+ Self {
+ title: "Narrow Scroller",
+ state: scrollable::State::new(),
+ scrollbar_width: Some(10),
+ scrollbar_margin: None,
+ scroller_width: Some(4),
+ },
+ ]
+ }
+}
diff --git a/examples/scrollable/src/style.rs b/examples/scrollable/src/style.rs
new file mode 100644
index 00000000..ae449141
--- /dev/null
+++ b/examples/scrollable/src/style.rs
@@ -0,0 +1,190 @@
+use iced::{container, radio, rule, scrollable};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Theme {
+ Light,
+ Dark,
+}
+
+impl Theme {
+ pub const ALL: [Theme; 2] = [Theme::Light, Theme::Dark];
+}
+
+impl Default for Theme {
+ fn default() -> Theme {
+ Theme::Light
+ }
+}
+
+impl From<Theme> for Box<dyn container::StyleSheet> {
+ fn from(theme: Theme) -> Self {
+ match theme {
+ Theme::Light => Default::default(),
+ Theme::Dark => dark::Container.into(),
+ }
+ }
+}
+
+impl From<Theme> for Box<dyn radio::StyleSheet> {
+ fn from(theme: Theme) -> Self {
+ match theme {
+ Theme::Light => Default::default(),
+ Theme::Dark => dark::Radio.into(),
+ }
+ }
+}
+
+impl From<Theme> for Box<dyn scrollable::StyleSheet> {
+ fn from(theme: Theme) -> Self {
+ match theme {
+ Theme::Light => Default::default(),
+ Theme::Dark => dark::Scrollable.into(),
+ }
+ }
+}
+
+impl From<Theme> for Box<dyn rule::StyleSheet> {
+ fn from(theme: Theme) -> Self {
+ match theme {
+ Theme::Light => Default::default(),
+ Theme::Dark => dark::Rule.into(),
+ }
+ }
+}
+
+mod dark {
+ use iced::{container, radio, rule, scrollable, Color};
+
+ const BACKGROUND: Color = Color::from_rgb(
+ 0x36 as f32 / 255.0,
+ 0x39 as f32 / 255.0,
+ 0x3F as f32 / 255.0,
+ );
+
+ const SURFACE: Color = Color::from_rgb(
+ 0x40 as f32 / 255.0,
+ 0x44 as f32 / 255.0,
+ 0x4B as f32 / 255.0,
+ );
+
+ const ACCENT: Color = Color::from_rgb(
+ 0x6F as f32 / 255.0,
+ 0xFF as f32 / 255.0,
+ 0xE9 as f32 / 255.0,
+ );
+
+ const ACTIVE: Color = Color::from_rgb(
+ 0x72 as f32 / 255.0,
+ 0x89 as f32 / 255.0,
+ 0xDA as f32 / 255.0,
+ );
+
+ const SCROLLBAR: Color = Color::from_rgb(
+ 0x2E as f32 / 255.0,
+ 0x33 as f32 / 255.0,
+ 0x38 as f32 / 255.0,
+ );
+
+ const SCROLLER: Color = Color::from_rgb(
+ 0x20 as f32 / 255.0,
+ 0x22 as f32 / 255.0,
+ 0x25 as f32 / 255.0,
+ );
+
+ pub struct Container;
+
+ impl container::StyleSheet for Container {
+ fn style(&self) -> container::Style {
+ container::Style {
+ background: Color {
+ a: 0.99,
+ ..BACKGROUND
+ }
+ .into(),
+ text_color: Color::WHITE.into(),
+ ..container::Style::default()
+ }
+ }
+ }
+
+ pub struct Radio;
+
+ impl radio::StyleSheet for Radio {
+ fn active(&self) -> radio::Style {
+ radio::Style {
+ background: SURFACE.into(),
+ dot_color: ACTIVE,
+ border_width: 1.0,
+ border_color: ACTIVE,
+ }
+ }
+
+ fn hovered(&self) -> radio::Style {
+ radio::Style {
+ background: Color { a: 0.5, ..SURFACE }.into(),
+ ..self.active()
+ }
+ }
+ }
+
+ pub struct Scrollable;
+
+ impl scrollable::StyleSheet for Scrollable {
+ fn active(&self) -> scrollable::Scrollbar {
+ scrollable::Scrollbar {
+ background: Color {
+ a: 0.8,
+ ..SCROLLBAR
+ }
+ .into(),
+ border_radius: 2.0,
+ border_width: 0.0,
+ border_color: Color::TRANSPARENT,
+ scroller: scrollable::Scroller {
+ color: Color { a: 0.7, ..SCROLLER },
+ border_radius: 2.0,
+ border_width: 0.0,
+ border_color: Color::TRANSPARENT,
+ },
+ }
+ }
+
+ fn hovered(&self) -> scrollable::Scrollbar {
+ let active = self.active();
+
+ scrollable::Scrollbar {
+ background: SCROLLBAR.into(),
+ scroller: scrollable::Scroller {
+ color: SCROLLER,
+ ..active.scroller
+ },
+ ..active
+ }
+ }
+
+ fn dragging(&self) -> scrollable::Scrollbar {
+ let hovered = self.hovered();
+
+ scrollable::Scrollbar {
+ scroller: scrollable::Scroller {
+ color: ACCENT,
+ ..hovered.scroller
+ },
+ ..hovered
+ }
+ }
+ }
+
+ pub struct Rule;
+
+ impl rule::StyleSheet for Rule {
+ fn style(&self) -> rule::Style {
+ rule::Style {
+ color: SURFACE,
+ width: 2,
+ radius: 1.0,
+ fill_mode: rule::FillMode::Percent(30.0),
+ }
+ }
+ }
+}
diff --git a/examples/solar_system/Cargo.toml b/examples/solar_system/Cargo.toml
index c88cda50..44ced729 100644
--- a/examples/solar_system/Cargo.toml
+++ b/examples/solar_system/Cargo.toml
@@ -5,11 +5,6 @@ authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
publish = false
-[features]
-canvas = []
-
[dependencies]
-iced = { path = "../..", features = ["canvas", "async-std", "debug"] }
-iced_native = { path = "../../native" }
-async-std = { version = "1.0", features = ["unstable"] }
+iced = { path = "../..", features = ["canvas", "tokio", "debug"] }
rand = "0.7"
diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs
index 4c239806..6a2de736 100644
--- a/examples/solar_system/src/main.rs
+++ b/examples/solar_system/src/main.rs
@@ -7,13 +7,14 @@
//!
//! [1]: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Basic_animations#An_animated_solar_system
use iced::{
- canvas, executor, Application, Canvas, Color, Command, Container, Element,
- Length, Point, Settings, Size, Subscription, Vector,
+ canvas::{self, Cursor, Path, Stroke},
+ executor, time, window, Application, Canvas, Color, Command, Element,
+ Length, Point, Rectangle, Settings, Size, Subscription, Vector,
};
use std::time::Instant;
-pub fn main() {
+pub fn main() -> iced::Result {
SolarSystem::run(Settings {
antialiasing: true,
..Settings::default()
@@ -22,7 +23,6 @@ pub fn main() {
struct SolarSystem {
state: State,
- solar_system: canvas::layer::Cache<State>,
}
#[derive(Debug, Clone, Copy)]
@@ -33,12 +33,12 @@ enum Message {
impl Application for SolarSystem {
type Executor = executor::Default;
type Message = Message;
+ type Flags = ();
- fn new() -> (Self, Command<Message>) {
+ fn new(_flags: ()) -> (Self, Command<Message>) {
(
SolarSystem {
state: State::new(),
- solar_system: canvas::layer::Cache::new(),
},
Command::none(),
)
@@ -52,7 +52,6 @@ impl Application for SolarSystem {
match message {
Message::Tick(instant) => {
self.state.update(instant);
- self.solar_system.clear();
}
}
@@ -65,24 +64,20 @@ impl Application for SolarSystem {
}
fn view(&mut self) -> Element<Message> {
- let canvas = Canvas::new()
+ Canvas::new(&mut self.state)
.width(Length::Fill)
.height(Length::Fill)
- .push(self.solar_system.with(&self.state));
-
- Container::new(canvas)
- .width(Length::Fill)
- .height(Length::Fill)
- .center_x()
- .center_y()
.into()
}
}
#[derive(Debug)]
struct State {
+ space_cache: canvas::Cache,
+ system_cache: canvas::Cache,
+ cursor_position: Point,
start: Instant,
- current: Instant,
+ now: Instant,
stars: Vec<(Point, f32)>,
}
@@ -95,153 +90,125 @@ impl State {
pub fn new() -> State {
let now = Instant::now();
- let (width, height) = Settings::default().window.size;
+ let (width, height) = window::Settings::default().size;
State {
+ space_cache: Default::default(),
+ system_cache: Default::default(),
+ cursor_position: Point::ORIGIN,
start: now,
- current: now,
- stars: {
- use rand::Rng;
-
- let mut rng = rand::thread_rng();
-
- (0..100)
- .map(|_| {
- (
- Point::new(
- rng.gen_range(0.0, width as f32),
- rng.gen_range(0.0, height as f32),
- ),
- rng.gen_range(0.5, 1.0),
- )
- })
- .collect()
- },
+ now,
+ stars: Self::generate_stars(width, height),
}
}
pub fn update(&mut self, now: Instant) {
- self.current = now;
+ self.now = now;
+ self.system_cache.clear();
+ }
+
+ fn generate_stars(width: u32, height: u32) -> Vec<(Point, f32)> {
+ use rand::Rng;
+
+ let mut rng = rand::thread_rng();
+
+ (0..100)
+ .map(|_| {
+ (
+ Point::new(
+ rng.gen_range(
+ -(width as f32) / 2.0,
+ width as f32 / 2.0,
+ ),
+ rng.gen_range(
+ -(height as f32) / 2.0,
+ height as f32 / 2.0,
+ ),
+ ),
+ rng.gen_range(0.5, 1.0),
+ )
+ })
+ .collect()
}
}
-impl canvas::Drawable for State {
- fn draw(&self, frame: &mut canvas::Frame) {
- use canvas::{Fill, Path, Stroke};
+impl<Message> canvas::Program<Message> for State {
+ fn draw(
+ &self,
+ bounds: Rectangle,
+ _cursor: Cursor,
+ ) -> Vec<canvas::Geometry> {
use std::f32::consts::PI;
- let center = frame.center();
+ let background = self.space_cache.draw(bounds.size(), |frame| {
+ let space = Path::rectangle(Point::new(0.0, 0.0), frame.size());
- let space = Path::new(|path| {
- path.rectangle(Point::new(0.0, 0.0), frame.size())
- });
+ let stars = Path::new(|path| {
+ for (p, size) in &self.stars {
+ path.rectangle(*p, Size::new(*size, *size));
+ }
+ });
- let stars = Path::new(|path| {
- for (p, size) in &self.stars {
- path.rectangle(*p, Size::new(*size, *size));
- }
+ frame.fill(&space, Color::BLACK);
+
+ frame.translate(frame.center() - Point::ORIGIN);
+ frame.fill(&stars, Color::WHITE);
});
- let sun = Path::new(|path| path.circle(center, Self::SUN_RADIUS));
- let orbit = Path::new(|path| path.circle(center, Self::ORBIT_RADIUS));
-
- frame.fill(&space, Fill::Color(Color::BLACK));
- frame.fill(&stars, Fill::Color(Color::WHITE));
- frame.fill(&sun, Fill::Color(Color::from_rgb8(0xF9, 0xD7, 0x1C)));
- frame.stroke(
- &orbit,
- Stroke {
- width: 1.0,
- color: Color::from_rgba8(0, 153, 255, 0.1),
- ..Stroke::default()
- },
- );
+ let system = self.system_cache.draw(bounds.size(), |frame| {
+ let center = frame.center();
- let elapsed = self.current - self.start;
- let elapsed_seconds = elapsed.as_secs() as f32;
- let elapsed_millis = elapsed.subsec_millis() as f32;
+ let sun = Path::circle(center, Self::SUN_RADIUS);
+ let orbit = Path::circle(center, Self::ORBIT_RADIUS);
- frame.with_save(|frame| {
- frame.translate(Vector::new(center.x, center.y));
- frame.rotate(
- (2.0 * PI / 60.0) * elapsed_seconds
- + (2.0 * PI / 60_000.0) * elapsed_millis,
+ frame.fill(&sun, Color::from_rgb8(0xF9, 0xD7, 0x1C));
+ frame.stroke(
+ &orbit,
+ Stroke {
+ width: 1.0,
+ color: Color::from_rgba8(0, 153, 255, 0.1),
+ ..Stroke::default()
+ },
);
- frame.translate(Vector::new(Self::ORBIT_RADIUS, 0.0));
- let earth = Path::new(|path| {
- path.circle(Point::ORIGIN, Self::EARTH_RADIUS)
- });
+ let elapsed = self.now - self.start;
+ let rotation = (2.0 * PI / 60.0) * elapsed.as_secs() as f32
+ + (2.0 * PI / 60_000.0) * elapsed.subsec_millis() as f32;
- let shadow = Path::new(|path| {
- path.rectangle(
+ frame.with_save(|frame| {
+ frame.translate(Vector::new(center.x, center.y));
+ frame.rotate(rotation);
+ frame.translate(Vector::new(Self::ORBIT_RADIUS, 0.0));
+
+ let earth = Path::circle(Point::ORIGIN, Self::EARTH_RADIUS);
+ let shadow = Path::rectangle(
Point::new(0.0, -Self::EARTH_RADIUS),
Size::new(
Self::EARTH_RADIUS * 4.0,
Self::EARTH_RADIUS * 2.0,
),
- )
- });
+ );
- frame.fill(&earth, Fill::Color(Color::from_rgb8(0x6B, 0x93, 0xD6)));
+ frame.fill(&earth, Color::from_rgb8(0x6B, 0x93, 0xD6));
- frame.with_save(|frame| {
- frame.rotate(
- ((2.0 * PI) / 6.0) * elapsed_seconds
- + ((2.0 * PI) / 6_000.0) * elapsed_millis,
- );
- frame.translate(Vector::new(0.0, Self::MOON_DISTANCE));
+ frame.with_save(|frame| {
+ frame.rotate(rotation * 10.0);
+ frame.translate(Vector::new(0.0, Self::MOON_DISTANCE));
- let moon = Path::new(|path| {
- path.circle(Point::ORIGIN, Self::MOON_RADIUS)
+ let moon = Path::circle(Point::ORIGIN, Self::MOON_RADIUS);
+ frame.fill(&moon, Color::WHITE);
});
- frame.fill(&moon, Fill::Color(Color::WHITE));
+ frame.fill(
+ &shadow,
+ Color {
+ a: 0.7,
+ ..Color::BLACK
+ },
+ );
});
-
- frame.fill(
- &shadow,
- Fill::Color(Color {
- a: 0.7,
- ..Color::BLACK
- }),
- );
});
- }
-}
-
-mod time {
- use iced::futures;
- use std::time::Instant;
- pub fn every(duration: std::time::Duration) -> iced::Subscription<Instant> {
- iced::Subscription::from_recipe(Every(duration))
- }
-
- struct Every(std::time::Duration);
-
- impl<H, I> iced_native::subscription::Recipe<H, I> for Every
- where
- H: std::hash::Hasher,
- {
- type Output = 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, I>,
- ) -> futures::stream::BoxStream<'static, Self::Output> {
- use futures::stream::StreamExt;
-
- async_std::stream::interval(self.0)
- .map(|_| Instant::now())
- .boxed()
- }
+ vec![background, system]
}
}
diff --git a/examples/stopwatch/Cargo.toml b/examples/stopwatch/Cargo.toml
index 1dae3b83..075aa111 100644
--- a/examples/stopwatch/Cargo.toml
+++ b/examples/stopwatch/Cargo.toml
@@ -6,7 +6,4 @@ edition = "2018"
publish = false
[dependencies]
-iced = { path = "../.." }
-iced_native = { path = "../../native" }
-iced_futures = { path = "../../futures", features = ["async-std"] }
-async-std = { version = "1.0", features = ["unstable"] }
+iced = { path = "../..", features = ["tokio"] }
diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs
index d84c4817..983cf3e6 100644
--- a/examples/stopwatch/src/main.rs
+++ b/examples/stopwatch/src/main.rs
@@ -1,10 +1,11 @@
use iced::{
- button, Align, Application, Button, Column, Command, Container, Element,
- HorizontalAlignment, Length, Row, Settings, Subscription, Text,
+ button, executor, time, Align, Application, Button, Column, Command,
+ Container, Element, HorizontalAlignment, Length, Row, Settings,
+ Subscription, Text,
};
use std::time::{Duration, Instant};
-pub fn main() {
+pub fn main() -> iced::Result {
Stopwatch::run(Settings::default())
}
@@ -28,10 +29,11 @@ enum Message {
}
impl Application for Stopwatch {
- type Executor = iced_futures::executor::AsyncStd;
+ type Executor = executor::Default;
type Message = Message;
+ type Flags = ();
- fn new() -> (Stopwatch, Command<Message>) {
+ fn new(_flags: ()) -> (Stopwatch, Command<Message>) {
(
Stopwatch {
duration: Duration::default(),
@@ -142,43 +144,6 @@ impl Application for Stopwatch {
}
}
-mod time {
- use iced::futures;
-
- pub fn every(
- duration: std::time::Duration,
- ) -> iced::Subscription<std::time::Instant> {
- iced::Subscription::from_recipe(Every(duration))
- }
-
- struct Every(std::time::Duration);
-
- impl<H, I> iced_native::subscription::Recipe<H, I> 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, I>,
- ) -> futures::stream::BoxStream<'static, Self::Output> {
- use futures::stream::StreamExt;
-
- async_std::stream::interval(self.0)
- .map(|_| std::time::Instant::now())
- .boxed()
- }
- }
-}
-
mod style {
use iced::{button, Background, Color, Vector};
@@ -196,7 +161,7 @@ mod style {
Button::Secondary => Color::from_rgb(0.5, 0.5, 0.5),
Button::Destructive => Color::from_rgb(0.8, 0.2, 0.2),
})),
- border_radius: 12,
+ border_radius: 12.0,
shadow_offset: Vector::new(1.0, 1.0),
text_color: Color::WHITE,
..button::Style::default()
diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs
index d6f41b04..8975fd9a 100644
--- a/examples/styling/src/main.rs
+++ b/examples/styling/src/main.rs
@@ -1,10 +1,10 @@
use iced::{
button, scrollable, slider, text_input, Align, Button, Checkbox, Column,
- Container, Element, Length, ProgressBar, Radio, Row, Sandbox, Scrollable,
- Settings, Slider, Space, Text, TextInput,
+ Container, Element, Length, ProgressBar, Radio, Row, Rule, Sandbox,
+ Scrollable, Settings, Slider, Space, Text, TextInput,
};
-pub fn main() {
+pub fn main() -> iced::Result {
Styling::run(Settings::default())
}
@@ -113,14 +113,17 @@ impl Sandbox for Styling {
.padding(20)
.max_width(600)
.push(choose_theme)
+ .push(Rule::horizontal(38).style(self.theme))
.push(Row::new().spacing(10).push(text_input).push(button))
.push(slider)
.push(progress_bar)
.push(
Row::new()
.spacing(10)
+ .height(Length::Units(100))
.align_items(Align::Center)
.push(scrollable)
+ .push(Rule::vertical(38).style(self.theme))
.push(checkbox),
);
@@ -136,8 +139,8 @@ impl Sandbox for Styling {
mod style {
use iced::{
- button, checkbox, container, progress_bar, radio, scrollable, slider,
- text_input,
+ button, checkbox, container, progress_bar, radio, rule, scrollable,
+ slider, text_input,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -228,18 +231,25 @@ mod style {
}
}
+ impl From<Theme> for Box<dyn rule::StyleSheet> {
+ fn from(theme: Theme) -> Self {
+ match theme {
+ Theme::Light => Default::default(),
+ Theme::Dark => dark::Rule.into(),
+ }
+ }
+ }
+
mod light {
- use iced::{button, Background, Color, Vector};
+ use iced::{button, Color, Vector};
pub struct Button;
impl button::StyleSheet for Button {
fn active(&self) -> button::Style {
button::Style {
- background: Some(Background::Color(Color::from_rgb(
- 0.11, 0.42, 0.87,
- ))),
- border_radius: 12,
+ background: Color::from_rgb(0.11, 0.42, 0.87).into(),
+ border_radius: 12.0,
shadow_offset: Vector::new(1.0, 1.0),
text_color: Color::from_rgb8(0xEE, 0xEE, 0xEE),
..button::Style::default()
@@ -258,8 +268,8 @@ mod style {
mod dark {
use iced::{
- button, checkbox, container, progress_bar, radio, scrollable,
- slider, text_input, Background, Color,
+ button, checkbox, container, progress_bar, radio, rule, scrollable,
+ slider, text_input, Color,
};
const SURFACE: Color = Color::from_rgb(
@@ -291,10 +301,8 @@ mod style {
impl container::StyleSheet for Container {
fn style(&self) -> container::Style {
container::Style {
- background: Some(Background::Color(Color::from_rgb8(
- 0x36, 0x39, 0x3F,
- ))),
- text_color: Some(Color::WHITE),
+ background: Color::from_rgb8(0x36, 0x39, 0x3F).into(),
+ text_color: Color::WHITE.into(),
..container::Style::default()
}
}
@@ -305,16 +313,16 @@ mod style {
impl radio::StyleSheet for Radio {
fn active(&self) -> radio::Style {
radio::Style {
- background: Background::Color(SURFACE),
+ background: SURFACE.into(),
dot_color: ACTIVE,
- border_width: 1,
+ border_width: 1.0,
border_color: ACTIVE,
}
}
fn hovered(&self) -> radio::Style {
radio::Style {
- background: Background::Color(Color { a: 0.5, ..SURFACE }),
+ background: Color { a: 0.5, ..SURFACE }.into(),
..self.active()
}
}
@@ -325,16 +333,16 @@ mod style {
impl text_input::StyleSheet for TextInput {
fn active(&self) -> text_input::Style {
text_input::Style {
- background: Background::Color(SURFACE),
- border_radius: 2,
- border_width: 0,
+ background: SURFACE.into(),
+ border_radius: 2.0,
+ border_width: 0.0,
border_color: Color::TRANSPARENT,
}
}
fn focused(&self) -> text_input::Style {
text_input::Style {
- border_width: 1,
+ border_width: 1.0,
border_color: ACCENT,
..self.active()
}
@@ -342,7 +350,7 @@ mod style {
fn hovered(&self) -> text_input::Style {
text_input::Style {
- border_width: 1,
+ border_width: 1.0,
border_color: Color { a: 0.3, ..ACCENT },
..self.focused()
}
@@ -355,6 +363,10 @@ mod style {
fn value_color(&self) -> Color {
Color::WHITE
}
+
+ fn selection_color(&self) -> Color {
+ ACTIVE
+ }
}
pub struct Button;
@@ -362,8 +374,8 @@ mod style {
impl button::StyleSheet for Button {
fn active(&self) -> button::Style {
button::Style {
- background: Some(Background::Color(ACTIVE)),
- border_radius: 3,
+ background: ACTIVE.into(),
+ border_radius: 3.0,
text_color: Color::WHITE,
..button::Style::default()
}
@@ -371,7 +383,7 @@ mod style {
fn hovered(&self) -> button::Style {
button::Style {
- background: Some(Background::Color(HOVERED)),
+ background: HOVERED.into(),
text_color: Color::WHITE,
..self.active()
}
@@ -379,7 +391,7 @@ mod style {
fn pressed(&self) -> button::Style {
button::Style {
- border_width: 1,
+ border_width: 1.0,
border_color: Color::WHITE,
..self.hovered()
}
@@ -391,14 +403,14 @@ mod style {
impl scrollable::StyleSheet for Scrollable {
fn active(&self) -> scrollable::Scrollbar {
scrollable::Scrollbar {
- background: Some(Background::Color(SURFACE)),
- border_radius: 2,
- border_width: 0,
+ background: SURFACE.into(),
+ border_radius: 2.0,
+ border_width: 0.0,
border_color: Color::TRANSPARENT,
scroller: scrollable::Scroller {
color: ACTIVE,
- border_radius: 2,
- border_width: 0,
+ border_radius: 2.0,
+ border_width: 0.0,
border_color: Color::TRANSPARENT,
},
}
@@ -408,10 +420,7 @@ mod style {
let active = self.active();
scrollable::Scrollbar {
- background: Some(Background::Color(Color {
- a: 0.5,
- ..SURFACE
- })),
+ background: Color { a: 0.5, ..SURFACE }.into(),
scroller: scrollable::Scroller {
color: HOVERED,
..active.scroller
@@ -440,9 +449,9 @@ mod style {
slider::Style {
rail_colors: (ACTIVE, Color { a: 0.1, ..ACTIVE }),
handle: slider::Handle {
- shape: slider::HandleShape::Circle { radius: 9 },
+ shape: slider::HandleShape::Circle { radius: 9.0 },
color: ACTIVE,
- border_width: 0,
+ border_width: 0.0,
border_color: Color::TRANSPARENT,
},
}
@@ -478,9 +487,9 @@ mod style {
impl progress_bar::StyleSheet for ProgressBar {
fn style(&self) -> progress_bar::Style {
progress_bar::Style {
- background: Background::Color(SURFACE),
- bar: Background::Color(ACTIVE),
- border_radius: 10,
+ background: SURFACE.into(),
+ bar: ACTIVE.into(),
+ border_radius: 10.0,
}
}
}
@@ -490,27 +499,38 @@ mod style {
impl checkbox::StyleSheet for Checkbox {
fn active(&self, is_checked: bool) -> checkbox::Style {
checkbox::Style {
- background: Background::Color(if is_checked {
- ACTIVE
- } else {
- SURFACE
- }),
+ background: if is_checked { ACTIVE } else { SURFACE }
+ .into(),
checkmark_color: Color::WHITE,
- border_radius: 2,
- border_width: 1,
+ border_radius: 2.0,
+ border_width: 1.0,
border_color: ACTIVE,
}
}
fn hovered(&self, is_checked: bool) -> checkbox::Style {
checkbox::Style {
- background: Background::Color(Color {
+ background: Color {
a: 0.8,
..if is_checked { ACTIVE } else { SURFACE }
- }),
+ }
+ .into(),
..self.active(is_checked)
}
}
}
+
+ pub struct Rule;
+
+ impl rule::StyleSheet for Rule {
+ fn style(&self) -> rule::Style {
+ rule::Style {
+ color: SURFACE,
+ width: 2,
+ radius: 1.0,
+ fill_mode: rule::FillMode::Padded(15),
+ }
+ }
+ }
}
}
diff --git a/examples/svg/Cargo.toml b/examples/svg/Cargo.toml
index 161ee6a8..d8f83ac2 100644
--- a/examples/svg/Cargo.toml
+++ b/examples/svg/Cargo.toml
@@ -7,4 +7,3 @@ publish = false
[dependencies]
iced = { path = "../..", features = ["svg"] }
-env_logger = "0.7"
diff --git a/examples/svg/src/main.rs b/examples/svg/src/main.rs
index 811fdfb5..8707fa3b 100644
--- a/examples/svg/src/main.rs
+++ b/examples/svg/src/main.rs
@@ -1,19 +1,16 @@
-use iced::{Column, Container, Element, Length, Sandbox, Settings, Svg};
-
-pub fn main() {
- env_logger::init();
+use iced::{Container, Element, Length, Sandbox, Settings, Svg};
+pub fn main() -> iced::Result {
Tiger::run(Settings::default())
}
-#[derive(Default)]
struct Tiger;
impl Sandbox for Tiger {
type Message = ();
fn new() -> Self {
- Self::default()
+ Tiger
}
fn title(&self) -> String {
@@ -23,18 +20,17 @@ impl Sandbox for Tiger {
fn update(&mut self, _message: ()) {}
fn view(&mut self) -> Element<()> {
- let content = Column::new().padding(20).push(
- Svg::new(format!(
- "{}/resources/tiger.svg",
- env!("CARGO_MANIFEST_DIR")
- ))
- .width(Length::Fill)
- .height(Length::Fill),
- );
-
- Container::new(content)
+ let svg = Svg::from_path(format!(
+ "{}/resources/tiger.svg",
+ env!("CARGO_MANIFEST_DIR")
+ ))
+ .width(Length::Fill)
+ .height(Length::Fill);
+
+ Container::new(svg)
.width(Length::Fill)
.height(Length::Fill)
+ .padding(20)
.center_x()
.center_y()
.into()
diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml
index f945cde5..c8926c33 100644
--- a/examples/todos/Cargo.toml
+++ b/examples/todos/Cargo.toml
@@ -6,13 +6,13 @@ edition = "2018"
publish = false
[dependencies]
-iced = { path = "../..", features = ["async-std"] }
+iced = { path = "../..", features = ["async-std", "debug"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
async-std = "1.0"
-directories = "2.0"
+directories-next = "2.0"
[target.'cfg(target_arch = "wasm32")'.dependencies]
web-sys = { version = "0.3", features = ["Window", "Storage"] }
diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs
index 7e866b19..ccee2703 100644
--- a/examples/todos/src/main.rs
+++ b/examples/todos/src/main.rs
@@ -5,7 +5,7 @@ use iced::{
};
use serde::{Deserialize, Serialize};
-pub fn main() {
+pub fn main() -> iced::Result {
Todos::run(Settings::default())
}
@@ -40,8 +40,9 @@ enum Message {
impl Application for Todos {
type Executor = iced::executor::Default;
type Message = Message;
+ type Flags = ();
- fn new() -> (Todos, Command<Message>) {
+ fn new(_flags: ()) -> (Todos, Command<Message>) {
(
Todos::Loading,
Command::perform(SavedState::load(), Message::Loaded),
@@ -424,7 +425,7 @@ impl Filter {
}
}
-fn loading_message() -> Element<'static, Message> {
+fn loading_message<'a>() -> Element<'a, Message> {
Container::new(
Text::new("Loading...")
.horizontal_alignment(HorizontalAlignment::Center)
@@ -436,7 +437,7 @@ fn loading_message() -> Element<'static, Message> {
.into()
}
-fn empty_message(message: &str) -> Element<'static, Message> {
+fn empty_message<'a>(message: &str) -> Element<'a, Message> {
Container::new(
Text::new(message)
.width(Length::Fill)
@@ -498,7 +499,7 @@ enum SaveError {
impl SavedState {
fn path() -> std::path::PathBuf {
let mut path = if let Some(project_dirs) =
- directories::ProjectDirs::from("rs", "Iced", "Todos")
+ directories_next::ProjectDirs::from("rs", "Iced", "Todos")
{
project_dirs.data_dir().into()
} else {
@@ -610,7 +611,7 @@ mod style {
background: Some(Background::Color(
Color::from_rgb(0.2, 0.2, 0.7),
)),
- border_radius: 10,
+ border_radius: 10.0,
text_color: Color::WHITE,
..button::Style::default()
}
@@ -626,7 +627,7 @@ mod style {
background: Some(Background::Color(Color::from_rgb(
0.8, 0.2, 0.2,
))),
- border_radius: 5,
+ border_radius: 5.0,
text_color: Color::WHITE,
shadow_offset: Vector::new(1.0, 1.0),
..button::Style::default()
diff --git a/examples/tour/Cargo.toml b/examples/tour/Cargo.toml
index 96749e90..bc7fac11 100644
--- a/examples/tour/Cargo.toml
+++ b/examples/tour/Cargo.toml
@@ -7,4 +7,4 @@ publish = false
[dependencies]
iced = { path = "../..", features = ["image", "debug"] }
-env_logger = "0.7"
+env_logger = "0.8"
diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs
index 800254ed..e8755d39 100644
--- a/examples/tour/src/main.rs
+++ b/examples/tour/src/main.rs
@@ -4,7 +4,7 @@ use iced::{
Sandbox, Scrollable, Settings, Slider, Space, Text, TextInput,
};
-pub fn main() {
+pub fn main() -> iced::Result {
env_logger::init();
Tour::run(Settings::default())
@@ -190,7 +190,7 @@ enum Step {
Welcome,
Slider {
state: slider::State,
- value: u16,
+ value: u8,
},
RowsAndColumns {
layout: Layout,
@@ -222,13 +222,13 @@ enum Step {
#[derive(Debug, Clone)]
pub enum StepMessage {
- SliderChanged(f32),
+ SliderChanged(u8),
LayoutChanged(Layout),
- SpacingChanged(f32),
- TextSizeChanged(f32),
+ SpacingChanged(u16),
+ TextSizeChanged(u16),
TextColorChanged(Color),
LanguageSelected(Language),
- ImageWidthChanged(f32),
+ ImageWidthChanged(u16),
InputChanged(String),
ToggleSecureInput(bool),
DebugToggled(bool),
@@ -249,12 +249,12 @@ impl<'a> Step {
}
StepMessage::SliderChanged(new_value) => {
if let Step::Slider { value, .. } = self {
- *value = new_value.round() as u16;
+ *value = new_value;
}
}
StepMessage::TextSizeChanged(new_size) => {
if let Step::Text { size, .. } = self {
- *size = new_size.round() as u16;
+ *size = new_size;
}
}
StepMessage::TextColorChanged(new_color) => {
@@ -269,12 +269,12 @@ impl<'a> Step {
}
StepMessage::SpacingChanged(new_spacing) => {
if let Step::RowsAndColumns { spacing, .. } = self {
- *spacing = new_spacing.round() as u16;
+ *spacing = new_spacing;
}
}
StepMessage::ImageWidthChanged(new_width) => {
if let Step::Image { width, .. } = self {
- *width = new_width.round() as u16;
+ *width = new_width;
}
}
StepMessage::InputChanged(new_value) => {
@@ -384,7 +384,7 @@ impl<'a> Step {
fn slider(
state: &'a mut slider::State,
- value: u16,
+ value: u8,
) -> Column<'a, StepMessage> {
Self::container("Slider")
.push(Text::new(
@@ -397,8 +397,8 @@ impl<'a> Step {
))
.push(Slider::new(
state,
- 0.0..=100.0,
- value as f32,
+ 0..=100,
+ value,
StepMessage::SliderChanged,
))
.push(
@@ -444,8 +444,8 @@ impl<'a> Step {
.spacing(10)
.push(Slider::new(
spacing_slider,
- 0.0..=80.0,
- spacing as f32,
+ 0..=80,
+ spacing,
StepMessage::SpacingChanged,
))
.push(
@@ -486,30 +486,25 @@ impl<'a> Step {
)
.push(Slider::new(
size_slider,
- 10.0..=70.0,
- size as f32,
+ 10..=70,
+ size,
StepMessage::TextSizeChanged,
));
let [red, green, blue] = color_sliders;
+
+ let color_sliders = Row::new()
+ .spacing(10)
+ .push(color_slider(red, color.r, move |r| Color { r, ..color }))
+ .push(color_slider(green, color.g, move |g| Color { g, ..color }))
+ .push(color_slider(blue, color.b, move |b| Color { b, ..color }));
+
let color_section = Column::new()
.padding(20)
.spacing(20)
.push(Text::new("And its color:"))
.push(Text::new(&format!("{:?}", color)).color(color))
- .push(
- Row::new()
- .spacing(10)
- .push(Slider::new(red, 0.0..=1.0, color.r, move |r| {
- StepMessage::TextColorChanged(Color { r, ..color })
- }))
- .push(Slider::new(green, 0.0..=1.0, color.g, move |g| {
- StepMessage::TextColorChanged(Color { g, ..color })
- }))
- .push(Slider::new(blue, 0.0..=1.0, color.b, move |b| {
- StepMessage::TextColorChanged(Color { b, ..color })
- })),
- );
+ .push(color_sliders);
Self::container("Text")
.push(Text::new(
@@ -530,7 +525,7 @@ impl<'a> Step {
|choices, language| {
choices.push(Radio::new(
language,
- language.into(),
+ language,
selection,
StepMessage::LanguageSelected,
))
@@ -559,8 +554,8 @@ impl<'a> Step {
.push(ferris(width))
.push(Slider::new(
slider,
- 100.0..=500.0,
- width as f32,
+ 100..=500,
+ width,
StepMessage::ImageWidthChanged,
))
.push(
@@ -694,7 +689,7 @@ fn ferris<'a>(width: u16) -> Container<'a, StepMessage> {
.center_x()
}
-fn button<'a, Message>(
+fn button<'a, Message: Clone>(
state: &'a mut button::State,
label: &str,
) -> Button<'a, Message> {
@@ -706,6 +701,17 @@ fn button<'a, Message>(
.min_width(100)
}
+fn color_slider(
+ state: &mut slider::State,
+ component: f32,
+ update: impl Fn(f32) -> Color + 'static,
+) -> Slider<f64, StepMessage> {
+ Slider::new(state, 0.0..=1.0, f64::from(component), move |c| {
+ StepMessage::TextColorChanged(update(c as f32))
+ })
+ .step(0.01)
+}
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Language {
Rust,
@@ -729,16 +735,16 @@ impl Language {
}
}
-impl From<Language> for &str {
- fn from(language: Language) -> &'static str {
- match language {
+impl From<Language> for String {
+ fn from(language: Language) -> String {
+ String::from(match language {
Language::Rust => "Rust",
Language::Elm => "Elm",
Language::Ruby => "Ruby",
Language::Haskell => "Haskell",
Language::C => "C",
Language::Other => "Other",
- }
+ })
}
}
@@ -763,7 +769,7 @@ mod style {
Button::Primary => Color::from_rgb(0.11, 0.42, 0.87),
Button::Secondary => Color::from_rgb(0.5, 0.5, 0.5),
})),
- border_radius: 12,
+ border_radius: 12.0,
shadow_offset: Vector::new(1.0, 1.0),
text_color: Color::from_rgb8(0xEE, 0xEE, 0xEE),
..button::Style::default()
diff --git a/futures/Cargo.toml b/futures/Cargo.toml
index 483e60cb..e8e47c08 100644
--- a/futures/Cargo.toml
+++ b/futures/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "iced_futures"
-version = "0.1.0-alpha"
+version = "0.2.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
description = "Commands, subscriptions, and runtimes for Iced"
@@ -19,14 +19,25 @@ log = "0.4"
[dependencies.futures]
version = "0.3"
-[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio]
+[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio_old]
+package = "tokio"
version = "0.2"
optional = true
-features = ["rt-core", "rt-threaded"]
+features = ["rt-core", "rt-threaded", "time", "stream"]
+
+[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio]
+version = "0.3"
+optional = true
+features = ["rt-multi-thread", "time", "stream"]
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.async-std]
version = "1.0"
optional = true
+features = ["unstable"]
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen-futures = "0.4"
+
+[package.metadata.docs.rs]
+rustdoc-args = ["--cfg", "docsrs"]
+all-features = true
diff --git a/futures/src/command.rs b/futures/src/command.rs
index 26f58fde..b06ab3f8 100644
--- a/futures/src/command.rs
+++ b/futures/src/command.rs
@@ -1,11 +1,138 @@
-#[cfg(not(target_arch = "wasm32"))]
-mod native;
+use crate::BoxFuture;
+use futures::future::{Future, FutureExt};
+
+/// A collection of async operations.
+///
+/// You should be able to turn a future easily into a [`Command`], either by
+/// using the `From` trait or [`Command::perform`].
+pub struct Command<T> {
+ futures: Vec<BoxFuture<T>>,
+}
+
+impl<T> Command<T> {
+ /// Creates an empty [`Command`].
+ ///
+ /// In other words, a [`Command`] that does nothing.
+ pub fn none() -> Self {
+ Self {
+ futures: Vec::new(),
+ }
+ }
+
+ /// Creates a [`Command`] that performs the action of the given future.
+ #[cfg(not(target_arch = "wasm32"))]
+ pub fn perform<A>(
+ future: impl Future<Output = T> + 'static + Send,
+ f: impl Fn(T) -> A + 'static + Send,
+ ) -> Command<A> {
+ Command {
+ futures: vec![Box::pin(future.map(f))],
+ }
+ }
+
+ /// Creates a [`Command`] that performs the action of the given future.
+ #[cfg(target_arch = "wasm32")]
+ pub fn perform<A>(
+ future: impl Future<Output = T> + 'static,
+ f: impl Fn(T) -> A + 'static + Send,
+ ) -> Command<A> {
+ Command {
+ futures: vec![Box::pin(future.map(f))],
+ }
+ }
+
+ /// Applies a transformation to the result of a [`Command`].
+ #[cfg(not(target_arch = "wasm32"))]
+ pub fn map<A>(
+ mut self,
+ f: impl Fn(T) -> A + 'static + Send + Sync,
+ ) -> Command<A>
+ where
+ T: 'static,
+ {
+ let f = std::sync::Arc::new(f);
+
+ Command {
+ futures: self
+ .futures
+ .drain(..)
+ .map(|future| {
+ let f = f.clone();
+
+ Box::pin(future.map(move |result| f(result)))
+ as BoxFuture<A>
+ })
+ .collect(),
+ }
+ }
+
+ /// Applies a transformation to the result of a [`Command`].
+ #[cfg(target_arch = "wasm32")]
+ pub fn map<A>(mut self, f: impl Fn(T) -> A + 'static) -> Command<A>
+ where
+ T: 'static,
+ {
+ let f = std::rc::Rc::new(f);
+
+ Command {
+ futures: self
+ .futures
+ .drain(..)
+ .map(|future| {
+ let f = f.clone();
+
+ Box::pin(future.map(move |result| f(result)))
+ as BoxFuture<A>
+ })
+ .collect(),
+ }
+ }
+
+ /// Creates a [`Command`] that performs the actions of all the given
+ /// commands.
+ ///
+ /// Once this command is run, all the commands will be executed at once.
+ pub fn batch(commands: impl IntoIterator<Item = Command<T>>) -> Self {
+ Self {
+ futures: commands
+ .into_iter()
+ .flat_map(|command| command.futures)
+ .collect(),
+ }
+ }
+
+ /// Converts a [`Command`] into its underlying list of futures.
+ pub fn futures(self) -> Vec<BoxFuture<T>> {
+ self.futures
+ }
+}
#[cfg(not(target_arch = "wasm32"))]
-pub use native::Command;
+impl<T, A> From<A> for Command<T>
+where
+ A: Future<Output = T> + 'static + Send,
+{
+ fn from(future: A) -> Self {
+ Self {
+ futures: vec![future.boxed()],
+ }
+ }
+}
#[cfg(target_arch = "wasm32")]
-mod web;
+impl<T, A> From<A> for Command<T>
+where
+ A: Future<Output = T> + 'static,
+{
+ fn from(future: A) -> Self {
+ Self {
+ futures: vec![future.boxed_local()],
+ }
+ }
+}
-#[cfg(target_arch = "wasm32")]
-pub use web::Command;
+impl<T> std::fmt::Debug for Command<T> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("Command").finish()
+ }
+}
diff --git a/futures/src/command/native.rs b/futures/src/command/native.rs
deleted file mode 100644
index 38cb4e06..00000000
--- a/futures/src/command/native.rs
+++ /dev/null
@@ -1,100 +0,0 @@
-use futures::future::{BoxFuture, Future, FutureExt};
-
-/// A collection of async operations.
-///
-/// You should be able to turn a future easily into a [`Command`], either by
-/// using the `From` trait or [`Command::perform`].
-///
-/// [`Command`]: struct.Command.html
-pub struct Command<T> {
- futures: Vec<BoxFuture<'static, T>>,
-}
-
-impl<T> Command<T> {
- /// Creates an empty [`Command`].
- ///
- /// In other words, a [`Command`] that does nothing.
- ///
- /// [`Command`]: struct.Command.html
- pub fn none() -> Self {
- Self {
- futures: Vec::new(),
- }
- }
-
- /// Creates a [`Command`] that performs the action of the given future.
- ///
- /// [`Command`]: struct.Command.html
- pub fn perform<A>(
- future: impl Future<Output = T> + 'static + Send,
- f: impl Fn(T) -> A + 'static + Send,
- ) -> Command<A> {
- Command {
- futures: vec![future.map(f).boxed()],
- }
- }
-
- /// Applies a transformation to the result of a [`Command`].
- ///
- /// [`Command`]: struct.Command.html
- pub fn map<A>(
- mut self,
- f: impl Fn(T) -> A + 'static + Send + Sync,
- ) -> Command<A>
- where
- T: 'static,
- {
- let f = std::sync::Arc::new(f);
-
- Command {
- futures: self
- .futures
- .drain(..)
- .map(|future| {
- let f = f.clone();
-
- future.map(move |result| f(result)).boxed()
- })
- .collect(),
- }
- }
-
- /// Creates a [`Command`] that performs the actions of all the given
- /// commands.
- ///
- /// Once this command is run, all the commands will be executed at once.
- ///
- /// [`Command`]: struct.Command.html
- pub fn batch(commands: impl IntoIterator<Item = Command<T>>) -> Self {
- Self {
- futures: commands
- .into_iter()
- .flat_map(|command| command.futures)
- .collect(),
- }
- }
-
- /// Converts a [`Command`] into its underlying list of futures.
- ///
- /// [`Command`]: struct.Command.html
- pub fn futures(self) -> Vec<BoxFuture<'static, T>> {
- self.futures
- }
-}
-
-impl<T, A> From<A> for Command<T>
-where
- A: Future<Output = T> + 'static + Send,
-{
- fn from(future: A) -> Self {
- Self {
- futures: vec![future.boxed()],
- }
- }
-}
-
-impl<T> std::fmt::Debug for Command<T> {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.debug_struct("Command").finish()
- }
-}
diff --git a/futures/src/command/web.rs b/futures/src/command/web.rs
deleted file mode 100644
index 11b46b90..00000000
--- a/futures/src/command/web.rs
+++ /dev/null
@@ -1,101 +0,0 @@
-use futures::future::{Future, FutureExt};
-use std::pin::Pin;
-
-/// A collection of async operations.
-///
-/// You should be able to turn a future easily into a [`Command`], either by
-/// using the `From` trait or [`Command::perform`].
-///
-/// [`Command`]: struct.Command.html
-pub struct Command<T> {
- futures: Vec<Pin<Box<dyn Future<Output = T> + 'static>>>,
-}
-
-impl<T> Command<T> {
- /// Creates an empty [`Command`].
- ///
- /// In other words, a [`Command`] that does nothing.
- ///
- /// [`Command`]: struct.Command.html
- pub fn none() -> Self {
- Self {
- futures: Vec::new(),
- }
- }
-
- /// Creates a [`Command`] that performs the action of the given future.
- ///
- /// [`Command`]: struct.Command.html
- pub fn perform<A>(
- future: impl Future<Output = T> + 'static,
- f: impl Fn(T) -> A + 'static,
- ) -> Command<A> {
- Command {
- futures: vec![future.map(f).boxed_local()],
- }
- }
-
- /// Applies a transformation to the result of a [`Command`].
- ///
- /// [`Command`]: struct.Command.html
- pub fn map<A>(
- mut self,
- f: impl Fn(T) -> A + 'static + Send + Sync + Unpin,
- ) -> Command<A>
- where
- T: 'static,
- {
- let f = std::sync::Arc::new(f);
-
- Command {
- futures: self
- .futures
- .drain(..)
- .map(|future| {
- let f = f.clone();
-
- future.map(move |result| f(result)).boxed_local()
- })
- .collect(),
- }
- }
-
- /// Creates a [`Command`] that performs the actions of all the given
- /// commands.
- ///
- /// Once this command is run, all the commands will be executed at once.
- ///
- /// [`Command`]: struct.Command.html
- pub fn batch(commands: impl IntoIterator<Item = Command<T>>) -> Self {
- Self {
- futures: commands
- .into_iter()
- .flat_map(|command| command.futures)
- .collect(),
- }
- }
-
- /// Converts a [`Command`] into its underlying list of futures.
- ///
- /// [`Command`]: struct.Command.html
- pub fn futures(self) -> Vec<Pin<Box<dyn Future<Output = T> + 'static>>> {
- self.futures
- }
-}
-
-impl<T, A> From<A> for Command<T>
-where
- A: Future<Output = T> + 'static,
-{
- fn from(future: A) -> Self {
- Self {
- futures: vec![future.boxed_local()],
- }
- }
-}
-
-impl<T> std::fmt::Debug for Command<T> {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.debug_struct("Command").finish()
- }
-}
diff --git a/futures/src/executor.rs b/futures/src/executor.rs
index 5378c0b3..fa87216a 100644
--- a/futures/src/executor.rs
+++ b/futures/src/executor.rs
@@ -7,6 +7,9 @@ mod thread_pool;
#[cfg(all(not(target_arch = "wasm32"), feature = "tokio"))]
mod tokio;
+#[cfg(all(not(target_arch = "wasm32"), feature = "tokio_old"))]
+mod tokio_old;
+
#[cfg(all(not(target_arch = "wasm32"), feature = "async-std"))]
mod async_std;
@@ -21,6 +24,9 @@ pub use thread_pool::ThreadPool;
#[cfg(all(not(target_arch = "wasm32"), feature = "tokio"))]
pub use self::tokio::Tokio;
+#[cfg(all(not(target_arch = "wasm32"), feature = "tokio_old"))]
+pub use self::tokio_old::TokioOld;
+
#[cfg(all(not(target_arch = "wasm32"), feature = "async-std"))]
pub use self::async_std::AsyncStd;
@@ -32,21 +38,15 @@ use futures::Future;
/// A type that can run futures.
pub trait Executor: Sized {
/// Creates a new [`Executor`].
- ///
- /// [`Executor`]: trait.Executor.html
fn new() -> Result<Self, futures::io::Error>
where
Self: Sized;
/// Spawns a future in the [`Executor`].
- ///
- /// [`Executor`]: trait.Executor.html
#[cfg(not(target_arch = "wasm32"))]
fn spawn(&self, future: impl Future<Output = ()> + Send + 'static);
/// Spawns a local future in the [`Executor`].
- ///
- /// [`Executor`]: trait.Executor.html
#[cfg(target_arch = "wasm32")]
fn spawn(&self, future: impl Future<Output = ()> + 'static);
diff --git a/futures/src/executor/async_std.rs b/futures/src/executor/async_std.rs
index 27949e31..471be369 100644
--- a/futures/src/executor/async_std.rs
+++ b/futures/src/executor/async_std.rs
@@ -3,6 +3,7 @@ use crate::Executor;
use futures::Future;
/// An `async-std` runtime.
+#[cfg_attr(docsrs, doc(cfg(feature = "async-std")))]
#[derive(Debug)]
pub struct AsyncStd;
diff --git a/futures/src/executor/thread_pool.rs b/futures/src/executor/thread_pool.rs
index 1ec5bf69..a6c6168e 100644
--- a/futures/src/executor/thread_pool.rs
+++ b/futures/src/executor/thread_pool.rs
@@ -3,6 +3,7 @@ use crate::Executor;
use futures::Future;
/// A thread pool runtime for futures.
+#[cfg_attr(docsrs, doc(cfg(feature = "thread-pool")))]
pub type ThreadPool = futures::executor::ThreadPool;
impl Executor for futures::executor::ThreadPool {
diff --git a/futures/src/executor/tokio.rs b/futures/src/executor/tokio.rs
index 20802ceb..c6a21cec 100644
--- a/futures/src/executor/tokio.rs
+++ b/futures/src/executor/tokio.rs
@@ -3,6 +3,7 @@ use crate::Executor;
use futures::Future;
/// A `tokio` runtime.
+#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
pub type Tokio = tokio::runtime::Runtime;
impl Executor for Tokio {
@@ -15,6 +16,7 @@ impl Executor for Tokio {
}
fn enter<R>(&self, f: impl FnOnce() -> R) -> R {
- tokio::runtime::Runtime::enter(self, f)
+ let _guard = tokio::runtime::Runtime::enter(self);
+ f()
}
}
diff --git a/futures/src/executor/tokio_old.rs b/futures/src/executor/tokio_old.rs
new file mode 100644
index 00000000..d64729fa
--- /dev/null
+++ b/futures/src/executor/tokio_old.rs
@@ -0,0 +1,21 @@
+use crate::Executor;
+
+use futures::Future;
+
+/// An old `tokio` runtime.
+#[cfg_attr(docsrs, doc(cfg(feature = "tokio_old")))]
+pub type TokioOld = tokio_old::runtime::Runtime;
+
+impl Executor for TokioOld {
+ fn new() -> Result<Self, futures::io::Error> {
+ tokio_old::runtime::Runtime::new()
+ }
+
+ fn spawn(&self, future: impl Future<Output = ()> + Send + 'static) {
+ let _ = tokio_old::runtime::Runtime::spawn(self, future);
+ }
+
+ fn enter<R>(&self, f: impl FnOnce() -> R) -> R {
+ tokio_old::runtime::Runtime::enter(self, f)
+ }
+}
diff --git a/futures/src/lib.rs b/futures/src/lib.rs
index c25c0853..c7c6fd3a 100644
--- a/futures/src/lib.rs
+++ b/futures/src/lib.rs
@@ -1,9 +1,13 @@
//! Asynchronous tasks for GUI programming, inspired by Elm.
+//!
+//! ![The foundations of the Iced ecosystem](https://github.com/hecrj/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/foundations.png?raw=true)
#![deny(missing_docs)]
#![deny(missing_debug_implementations)]
#![deny(unused_results)]
#![forbid(unsafe_code)]
#![forbid(rust_2018_idioms)]
+#![cfg_attr(docsrs, feature(doc_cfg))]
+
pub use futures;
mod command;
@@ -12,7 +16,42 @@ mod runtime;
pub mod executor;
pub mod subscription;
+#[cfg(all(
+ any(feature = "tokio", feature = "tokio_old", feature = "async-std"),
+ not(target_arch = "wasm32")
+))]
+#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio", feature = "async-std"))))]
+pub mod time;
+
pub use command::Command;
pub use executor::Executor;
pub use runtime::Runtime;
pub use subscription::Subscription;
+
+/// A boxed static future.
+///
+/// - On native platforms, it needs a `Send` requirement.
+/// - On the Web platform, it does not need a `Send` requirement.
+#[cfg(not(target_arch = "wasm32"))]
+pub type BoxFuture<T> = futures::future::BoxFuture<'static, T>;
+
+/// A boxed static future.
+///
+/// - On native platforms, it needs a `Send` requirement.
+/// - On the Web platform, it does not need a `Send` requirement.
+#[cfg(target_arch = "wasm32")]
+pub type BoxFuture<T> = futures::future::LocalBoxFuture<'static, T>;
+
+/// A boxed static stream.
+///
+/// - On native platforms, it needs a `Send` requirement.
+/// - On the Web platform, it does not need a `Send` requirement.
+#[cfg(not(target_arch = "wasm32"))]
+pub type BoxStream<T> = futures::stream::BoxStream<'static, T>;
+
+/// A boxed static stream.
+///
+/// - On native platforms, it needs a `Send` requirement.
+/// - On the Web platform, it does not need a `Send` requirement.
+#[cfg(target_arch = "wasm32")]
+pub type BoxStream<T> = futures::stream::LocalBoxStream<'static, T>;
diff --git a/futures/src/runtime.rs b/futures/src/runtime.rs
index d204670b..e56a4eb0 100644
--- a/futures/src/runtime.rs
+++ b/futures/src/runtime.rs
@@ -8,11 +8,6 @@ use std::marker::PhantomData;
///
/// If you have an [`Executor`], a [`Runtime`] can be leveraged to run any
/// [`Command`] or [`Subscription`] and get notified of the results!
-///
-/// [`Runtime`]: struct.Runtime.html
-/// [`Executor`]: executor/trait.Executor.html
-/// [`Command`]: struct.Command.html
-/// [`Subscription`]: subscription/struct.Subscription.html
#[derive(Debug)]
pub struct Runtime<Hasher, Event, Executor, Sender, Message> {
executor: Executor,
@@ -36,8 +31,6 @@ where
/// You need to provide:
/// - an [`Executor`] to spawn futures
/// - a `Sender` implementing `Sink` to receive the results
- ///
- /// [`Runtime`]: struct.Runtime.html
pub fn new(executor: Executor, sender: Sender) -> Self {
Self {
executor,
@@ -50,10 +43,6 @@ where
/// Runs the given closure inside the [`Executor`] of the [`Runtime`].
///
/// See [`Executor::enter`] to learn more.
- ///
- /// [`Executor`]: executor/trait.Executor.html
- /// [`Runtime`]: struct.Runtime.html
- /// [`Executor::enter`]: executor/trait.Executor.html#method.enter
pub fn enter<R>(&self, f: impl FnOnce() -> R) -> R {
self.executor.enter(f)
}
@@ -62,9 +51,6 @@ where
///
/// The resulting `Message` will be forwarded to the `Sender` of the
/// [`Runtime`].
- ///
- /// [`Command`]: struct.Command.html
- /// [`Runtime`]: struct.Runtime.html
pub fn spawn(&mut self, command: Command<Message>) {
use futures::{FutureExt, SinkExt};
@@ -88,9 +74,7 @@ where
/// It will spawn new streams or close old ones as necessary! See
/// [`Tracker::update`] to learn more about this!
///
- /// [`Subscription`]: subscription/struct.Subscription.html
- /// [`Runtime`]: struct.Runtime.html
- /// [`Tracker::update`]: subscription/struct.Tracker.html#method.update
+ /// [`Tracker::update`]: subscription::Tracker::update
pub fn track(
&mut self,
subscription: Subscription<Hasher, Event, Message>,
@@ -115,9 +99,7 @@ where
///
/// See [`Tracker::broadcast`] to learn more.
///
- /// [`Runtime`]: struct.Runtime.html
- /// [`Tracker::broadcast`]:
- /// subscription/struct.Tracker.html#method.broadcast
+ /// [`Tracker::broadcast`]: subscription::Tracker::broadcast
pub fn broadcast(&mut self, event: Event) {
self.subscriptions.broadcast(event);
}
diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs
index b68444cd..27d2d295 100644
--- a/futures/src/subscription.rs
+++ b/futures/src/subscription.rs
@@ -3,7 +3,7 @@ mod tracker;
pub use tracker::Tracker;
-use futures::stream::BoxStream;
+use crate::BoxStream;
/// A request to listen to external events.
///
@@ -19,8 +19,7 @@ use futures::stream::BoxStream;
/// This type is normally aliased by runtimes with a specific `Event` and/or
/// `Hasher`.
///
-/// [`Command`]: ../struct.Command.html
-/// [`Subscription`]: struct.Subscription.html
+/// [`Command`]: crate::Command
pub struct Subscription<Hasher, Event, Output> {
recipes: Vec<Box<dyn Recipe<Hasher, Event, Output = Output>>>,
}
@@ -30,8 +29,6 @@ where
H: std::hash::Hasher,
{
/// Returns an empty [`Subscription`] that will not produce any output.
- ///
- /// [`Subscription`]: struct.Subscription.html
pub fn none() -> Self {
Self {
recipes: Vec::new(),
@@ -39,9 +36,6 @@ where
}
/// Creates a [`Subscription`] from a [`Recipe`] describing it.
- ///
- /// [`Subscription`]: struct.Subscription.html
- /// [`Recipe`]: trait.Recipe.html
pub fn from_recipe(
recipe: impl Recipe<H, E, Output = O> + 'static,
) -> Self {
@@ -52,8 +46,6 @@ where
/// Batches all the provided subscriptions and returns the resulting
/// [`Subscription`].
- ///
- /// [`Subscription`]: struct.Subscription.html
pub fn batch(
subscriptions: impl IntoIterator<Item = Subscription<H, E, O>>,
) -> Self {
@@ -66,33 +58,46 @@ where
}
/// Returns the different recipes of the [`Subscription`].
- ///
- /// [`Subscription`]: struct.Subscription.html
pub fn recipes(self) -> Vec<Box<dyn Recipe<H, E, Output = O>>> {
self.recipes
}
- /// Transforms the [`Subscription`] output with the given function.
+ /// Adds a value to the [`Subscription`] context.
///
- /// [`Subscription`]: struct.Subscription.html
- pub fn map<A>(
- mut self,
- f: impl Fn(O) -> A + Send + Sync + 'static,
- ) -> Subscription<H, E, A>
+ /// The value will be part of the identity of a [`Subscription`].
+ pub fn with<T>(mut self, value: T) -> Subscription<H, E, (T, O)>
where
H: 'static,
E: 'static,
O: 'static,
- A: 'static,
+ T: std::hash::Hash + Clone + Send + Sync + 'static,
{
- let function = std::sync::Arc::new(f);
+ Subscription {
+ recipes: self
+ .recipes
+ .drain(..)
+ .map(|recipe| {
+ Box::new(With::new(recipe, value.clone()))
+ as Box<dyn Recipe<H, E, Output = (T, O)>>
+ })
+ .collect(),
+ }
+ }
+ /// Transforms the [`Subscription`] output with the given function.
+ pub fn map<A>(mut self, f: fn(O) -> A) -> Subscription<H, E, A>
+ where
+ H: 'static,
+ E: 'static,
+ O: 'static,
+ A: 'static,
+ {
Subscription {
recipes: self
.recipes
.drain(..)
.map(|recipe| {
- Box::new(Map::new(recipe, function.clone()))
+ Box::new(Map::new(recipe, f))
as Box<dyn Recipe<H, E, Output = A>>
})
.collect(),
@@ -112,22 +117,25 @@ impl<I, O, H> std::fmt::Debug for Subscription<I, O, H> {
/// by runtimes to run and identify subscriptions. You can use it to create your
/// own!
///
-/// [`Subscription`]: struct.Subscription.html
-/// [`Recipe`]: trait.Recipe.html
+/// # Examples
+/// The repository has a couple of [examples] that use a custom [`Recipe`]:
+///
+/// - [`download_progress`], a basic application that asynchronously downloads
+/// a dummy file of 100 MB and tracks the download progress.
+/// - [`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
pub trait Recipe<Hasher: std::hash::Hasher, Event> {
/// The events that will be produced by a [`Subscription`] with this
/// [`Recipe`].
- ///
- /// [`Subscription`]: struct.Subscription.html
- /// [`Recipe`]: trait.Recipe.html
type Output;
/// Hashes the [`Recipe`].
///
/// This is used by runtimes to uniquely identify a [`Subscription`].
- ///
- /// [`Subscription`]: struct.Subscription.html
- /// [`Recipe`]: trait.Recipe.html
fn hash(&self, state: &mut Hasher);
/// Executes the [`Recipe`] and produces the stream of events of its
@@ -135,24 +143,21 @@ pub trait Recipe<Hasher: std::hash::Hasher, Event> {
///
/// It receives some stream of generic events, which is normally defined by
/// shells.
- ///
- /// [`Subscription`]: struct.Subscription.html
- /// [`Recipe`]: trait.Recipe.html
fn stream(
self: Box<Self>,
- input: BoxStream<'static, Event>,
- ) -> BoxStream<'static, Self::Output>;
+ input: BoxStream<Event>,
+ ) -> BoxStream<Self::Output>;
}
struct Map<Hasher, Event, A, B> {
recipe: Box<dyn Recipe<Hasher, Event, Output = A>>,
- mapper: std::sync::Arc<dyn Fn(A) -> B + Send + Sync>,
+ mapper: fn(A) -> B,
}
impl<H, E, A, B> Map<H, E, A, B> {
fn new(
recipe: Box<dyn Recipe<H, E, Output = A>>,
- mapper: std::sync::Arc<dyn Fn(A) -> B + Send + Sync + 'static>,
+ mapper: fn(A) -> B,
) -> Self {
Map { recipe, mapper }
}
@@ -169,21 +174,59 @@ where
fn hash(&self, state: &mut H) {
use std::hash::Hash;
- std::any::TypeId::of::<B>().hash(state);
self.recipe.hash(state);
+ self.mapper.hash(state);
}
- fn stream(
- self: Box<Self>,
- input: BoxStream<'static, E>,
- ) -> futures::stream::BoxStream<'static, Self::Output> {
+ fn stream(self: Box<Self>, input: BoxStream<E>) -> BoxStream<Self::Output> {
use futures::StreamExt;
let mapper = self.mapper;
- self.recipe
- .stream(input)
- .map(move |element| mapper(element))
- .boxed()
+ Box::pin(
+ self.recipe
+ .stream(input)
+ .map(move |element| mapper(element)),
+ )
+ }
+}
+
+struct With<Hasher, Event, A, B> {
+ recipe: Box<dyn Recipe<Hasher, Event, Output = A>>,
+ value: B,
+}
+
+impl<H, E, A, B> With<H, E, A, B> {
+ fn new(recipe: Box<dyn Recipe<H, E, Output = A>>, value: B) -> Self {
+ With { recipe, value }
+ }
+}
+
+impl<H, E, A, B> Recipe<H, E> for With<H, E, A, B>
+where
+ A: 'static,
+ B: 'static + std::hash::Hash + Clone + Send + Sync,
+ H: std::hash::Hasher,
+{
+ type Output = (B, A);
+
+ fn hash(&self, state: &mut H) {
+ use std::hash::Hash;
+
+ std::any::TypeId::of::<B>().hash(state);
+ self.value.hash(state);
+ self.recipe.hash(state);
+ }
+
+ fn stream(self: Box<Self>, input: BoxStream<E>) -> BoxStream<Self::Output> {
+ use futures::StreamExt;
+
+ let value = self.value;
+
+ Box::pin(
+ self.recipe
+ .stream(input)
+ .map(move |element| (value.clone(), element)),
+ )
}
}
diff --git a/futures/src/subscription/tracker.rs b/futures/src/subscription/tracker.rs
index cfa36170..43222b5b 100644
--- a/futures/src/subscription/tracker.rs
+++ b/futures/src/subscription/tracker.rs
@@ -1,6 +1,6 @@
-use crate::Subscription;
+use crate::{BoxFuture, Subscription};
-use futures::{channel::mpsc, future::BoxFuture, sink::Sink};
+use futures::{channel::mpsc, sink::Sink};
use std::{collections::HashMap, marker::PhantomData};
/// A registry of subscription streams.
@@ -26,8 +26,6 @@ where
Event: 'static + Send + Clone,
{
/// Creates a new empty [`Tracker`].
- ///
- /// [`Tracker`]: struct.Tracker.html
pub fn new() -> Self {
Self {
subscriptions: HashMap::new(),
@@ -52,14 +50,12 @@ where
/// It returns a list of futures that need to be spawned to materialize
/// the [`Tracker`] changes.
///
- /// [`Tracker`]: struct.Tracker.html
- /// [`Subscription`]: struct.Subscription.html
- /// [`Recipe`]: trait.Recipe.html
+ /// [`Recipe`]: crate::subscription::Recipe
pub fn update<Message, Receiver>(
&mut self,
subscription: Subscription<Hasher, Event, Message>,
receiver: Receiver,
- ) -> Vec<BoxFuture<'static, ()>>
+ ) -> Vec<BoxFuture<()>>
where
Message: 'static + Send,
Receiver: 'static
@@ -70,7 +66,7 @@ where
{
use futures::{future::FutureExt, stream::StreamExt};
- let mut futures = Vec::new();
+ let mut futures: Vec<BoxFuture<()>> = Vec::new();
let recipes = subscription.recipes();
let mut alive = std::collections::HashSet::new();
@@ -115,7 +111,7 @@ where
},
);
- futures.push(future.boxed());
+ futures.push(Box::pin(future));
}
self.subscriptions.retain(|id, _| alive.contains(&id));
@@ -131,6 +127,8 @@ where
///
/// This method publishes the given event to all the subscription streams
/// currently open.
+ ///
+ /// [`Recipe::stream`]: crate::subscription::Recipe::stream
pub fn broadcast(&mut self, event: Event) {
self.subscriptions
.values_mut()
diff --git a/futures/src/time.rs b/futures/src/time.rs
new file mode 100644
index 00000000..5e9ea436
--- /dev/null
+++ b/futures/src/time.rs
@@ -0,0 +1,74 @@
+//! Listen and react to time.
+use crate::subscription::{self, Subscription};
+
+/// Returns a [`Subscription`] that produces messages at a set interval.
+///
+/// The first message is produced after a `duration`, and then continues to
+/// produce more messages every `duration` after that.
+pub fn every<H: std::hash::Hasher, E>(
+ duration: std::time::Duration,
+) -> Subscription<H, E, std::time::Instant> {
+ Subscription::from_recipe(Every(duration))
+}
+
+struct Every(std::time::Duration);
+
+#[cfg(feature = "async-std")]
+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;
+
+ async_std::stream::interval(self.0)
+ .map(|_| std::time::Instant::now())
+ .boxed()
+ }
+}
+
+#[cfg(all(
+ any(feature = "tokio", feature = "tokio_old"),
+ not(feature = "async-std")
+))]
+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;
+
+ #[cfg(feature = "tokio_old")]
+ use tokio_old as tokio;
+
+ let start = tokio::time::Instant::now() + self.0;
+
+ tokio::time::interval_at(start, self.0)
+ .map(|_| std::time::Instant::now())
+ .boxed()
+ }
+}
diff --git a/glow/Cargo.toml b/glow/Cargo.toml
new file mode 100644
index 00000000..3f85e52d
--- /dev/null
+++ b/glow/Cargo.toml
@@ -0,0 +1,37 @@
+[package]
+name = "iced_glow"
+version = "0.1.0"
+authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
+edition = "2018"
+description = "A glow renderer for iced"
+license = "MIT AND OFL-1.1"
+repository = "https://github.com/hecrj/iced"
+
+[features]
+canvas = ["iced_graphics/canvas"]
+qr_code = ["iced_graphics/qr_code"]
+default_system_font = ["iced_graphics/font-source"]
+# Not supported yet!
+image = []
+svg = []
+
+[dependencies]
+glow = "0.6"
+glow_glyph = "0.4"
+glyph_brush = "0.7"
+euclid = "0.22"
+bytemuck = "1.4"
+log = "0.4"
+
+[dependencies.iced_native]
+version = "0.3"
+path = "../native"
+
+[dependencies.iced_graphics]
+version = "0.1"
+path = "../graphics"
+features = ["font-fallback", "font-icons", "opengl"]
+
+[package.metadata.docs.rs]
+rustdoc-args = ["--cfg", "docsrs"]
+all-features = true
diff --git a/glow/src/backend.rs b/glow/src/backend.rs
new file mode 100644
index 00000000..92bb993e
--- /dev/null
+++ b/glow/src/backend.rs
@@ -0,0 +1,226 @@
+use crate::quad;
+use crate::text;
+use crate::triangle;
+use crate::{Settings, Transformation, Viewport};
+use iced_graphics::backend;
+use iced_graphics::font;
+use iced_graphics::Layer;
+use iced_graphics::Primitive;
+use iced_native::mouse;
+use iced_native::{Font, HorizontalAlignment, Size, VerticalAlignment};
+
+/// A [`glow`] graphics backend for [`iced`].
+///
+/// [`glow`]: https://github.com/grovesNL/glow
+/// [`iced`]: https://github.com/hecrj/iced
+#[derive(Debug)]
+pub struct Backend {
+ quad_pipeline: quad::Pipeline,
+ text_pipeline: text::Pipeline,
+ triangle_pipeline: triangle::Pipeline,
+ default_text_size: u16,
+}
+
+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 quad_pipeline = quad::Pipeline::new(gl);
+ let triangle_pipeline = triangle::Pipeline::new(gl);
+
+ Self {
+ quad_pipeline,
+ text_pipeline,
+ triangle_pipeline,
+ default_text_size: settings.default_text_size,
+ }
+ }
+
+ /// Draws the provided primitives in the default framebuffer.
+ ///
+ /// The text provided as overlay will be rendered on top of the primitives.
+ /// This is useful for rendering debug information.
+ pub fn draw<T: AsRef<str>>(
+ &mut self,
+ gl: &glow::Context,
+ viewport: &Viewport,
+ (primitive, mouse_interaction): &(Primitive, mouse::Interaction),
+ overlay_text: &[T],
+ ) -> mouse::Interaction {
+ let viewport_size = viewport.physical_size();
+ let scale_factor = viewport.scale_factor() as f32;
+ let projection = viewport.projection();
+
+ let mut layers = Layer::generate(primitive, viewport);
+ layers.push(Layer::overlay(overlay_text, viewport));
+
+ for layer in layers {
+ self.flush(
+ gl,
+ scale_factor,
+ projection,
+ &layer,
+ viewport_size.height,
+ );
+ }
+
+ *mouse_interaction
+ }
+
+ fn flush(
+ &mut self,
+ gl: &glow::Context,
+ scale_factor: f32,
+ transformation: Transformation,
+ layer: &Layer<'_>,
+ target_height: u32,
+ ) {
+ let mut bounds = (layer.bounds * scale_factor).snap();
+ bounds.height = bounds.height.min(target_height);
+
+ if !layer.quads.is_empty() {
+ self.quad_pipeline.draw(
+ gl,
+ target_height,
+ &layer.quads,
+ transformation,
+ scale_factor,
+ bounds,
+ );
+ }
+
+ if !layer.meshes.is_empty() {
+ let scaled = transformation
+ * Transformation::scale(scale_factor, scale_factor);
+
+ self.triangle_pipeline.draw(
+ gl,
+ target_height,
+ scaled,
+ scale_factor,
+ &layer.meshes,
+ );
+ }
+
+ if !layer.text.is_empty() {
+ for text in layer.text.iter() {
+ // Target physical coordinates directly to avoid blurry text
+ let text = glow_glyph::Section {
+ // TODO: We `round` here to avoid rerasterizing text when
+ // its position changes slightly. This can make text feel a
+ // bit "jumpy". We may be able to do better once we improve
+ // our text rendering/caching pipeline.
+ screen_position: (
+ (text.bounds.x * scale_factor).round(),
+ (text.bounds.y * scale_factor).round(),
+ ),
+ // TODO: Fix precision issues with some scale factors.
+ //
+ // The `ceil` here can cause some words to render on the
+ // same line when they should not.
+ //
+ // Ideally, `wgpu_glyph` should be able to compute layout
+ // using logical positions, and then apply the proper
+ // scaling when rendering. This would ensure that both
+ // measuring and rendering follow the same layout rules.
+ bounds: (
+ (text.bounds.width * scale_factor).ceil(),
+ (text.bounds.height * scale_factor).ceil(),
+ ),
+ text: vec![glow_glyph::Text {
+ text: text.content,
+ scale: glow_glyph::ab_glyph::PxScale {
+ x: text.size * scale_factor,
+ y: text.size * scale_factor,
+ },
+ font_id: self.text_pipeline.find_font(text.font),
+ extra: glow_glyph::Extra {
+ color: text.color,
+ z: 0.0,
+ },
+ }],
+ layout: glow_glyph::Layout::default()
+ .h_align(match text.horizontal_alignment {
+ HorizontalAlignment::Left => {
+ glow_glyph::HorizontalAlign::Left
+ }
+ HorizontalAlignment::Center => {
+ glow_glyph::HorizontalAlign::Center
+ }
+ HorizontalAlignment::Right => {
+ glow_glyph::HorizontalAlign::Right
+ }
+ })
+ .v_align(match text.vertical_alignment {
+ VerticalAlignment::Top => {
+ glow_glyph::VerticalAlign::Top
+ }
+ VerticalAlignment::Center => {
+ glow_glyph::VerticalAlign::Center
+ }
+ VerticalAlignment::Bottom => {
+ glow_glyph::VerticalAlign::Bottom
+ }
+ }),
+ ..Default::default()
+ };
+
+ self.text_pipeline.queue(text);
+ }
+
+ self.text_pipeline.draw_queued(
+ gl,
+ transformation,
+ glow_glyph::Region {
+ x: bounds.x,
+ y: target_height - (bounds.y + bounds.height),
+ width: bounds.width,
+ height: bounds.height,
+ },
+ );
+ }
+ }
+}
+
+impl iced_graphics::Backend for Backend {
+ fn trim_measurements(&mut self) {
+ self.text_pipeline.trim_measurement_cache()
+ }
+}
+
+impl backend::Text for Backend {
+ const ICON_FONT: Font = font::ICONS;
+ const CHECKMARK_ICON: char = font::CHECKMARK_ICON;
+ const ARROW_DOWN_ICON: char = font::ARROW_DOWN_ICON;
+
+ fn default_size(&self) -> u16 {
+ self.default_text_size
+ }
+
+ fn measure(
+ &self,
+ contents: &str,
+ size: f32,
+ font: Font,
+ bounds: Size,
+ ) -> (f32, f32) {
+ self.text_pipeline.measure(contents, size, font, bounds)
+ }
+}
+
+#[cfg(feature = "image")]
+impl backend::Image for Backend {
+ fn dimensions(&self, _handle: &iced_native::image::Handle) -> (u32, u32) {
+ (50, 50)
+ }
+}
+
+#[cfg(feature = "svg")]
+impl backend::Svg for Backend {
+ fn viewport_dimensions(
+ &self,
+ _handle: &iced_native::svg::Handle,
+ ) -> (u32, u32) {
+ (50, 50)
+ }
+}
diff --git a/glow/src/lib.rs b/glow/src/lib.rs
new file mode 100644
index 00000000..98faf24c
--- /dev/null
+++ b/glow/src/lib.rs
@@ -0,0 +1,41 @@
+//! A [`glow`] renderer for [`iced_native`].
+//!
+//! ![The native path of the Iced ecosystem](https://github.com/hecrj/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/native.png?raw=true)
+//!
+//! [`glow`]: https://github.com/grovesNL/glow
+//! [`iced_native`]: https://github.com/hecrj/iced/tree/master/native
+#![deny(missing_docs)]
+#![deny(missing_debug_implementations)]
+#![deny(unused_results)]
+#![forbid(rust_2018_idioms)]
+#![cfg_attr(docsrs, feature(doc_cfg))]
+
+mod backend;
+mod program;
+mod quad;
+mod text;
+mod triangle;
+
+pub mod settings;
+pub mod widget;
+pub mod window;
+
+pub use backend::Backend;
+pub use settings::Settings;
+
+pub(crate) use iced_graphics::Transformation;
+
+#[doc(no_inline)]
+pub use widget::*;
+
+pub use iced_graphics::{Error, Viewport};
+pub use iced_native::{
+ Background, Color, Command, HorizontalAlignment, Length, Vector,
+ VerticalAlignment,
+};
+
+/// A [`glow`] graphics renderer for [`iced`].
+///
+/// [`glow`]: https://github.com/grovesNL/glow
+/// [`iced`]: https://github.com/hecrj/iced
+pub type Renderer = iced_graphics::Renderer<Backend>;
diff --git a/glow/src/program.rs b/glow/src/program.rs
new file mode 100644
index 00000000..489a194f
--- /dev/null
+++ b/glow/src/program.rs
@@ -0,0 +1,39 @@
+use glow::HasContext;
+
+pub unsafe fn create(
+ gl: &glow::Context,
+ shader_sources: &[(u32, &str)],
+) -> <glow::Context as HasContext>::Program {
+ let program = gl.create_program().expect("Cannot create program");
+
+ let mut shaders = Vec::with_capacity(shader_sources.len());
+
+ for (shader_type, shader_source) in shader_sources.iter() {
+ let shader = gl
+ .create_shader(*shader_type)
+ .expect("Cannot create shader");
+
+ gl.shader_source(shader, shader_source);
+ gl.compile_shader(shader);
+
+ if !gl.get_shader_compile_status(shader) {
+ panic!(gl.get_shader_info_log(shader));
+ }
+
+ gl.attach_shader(program, shader);
+
+ shaders.push(shader);
+ }
+
+ gl.link_program(program);
+ if !gl.get_program_link_status(program) {
+ panic!(gl.get_program_info_log(program));
+ }
+
+ for shader in shaders {
+ gl.detach_shader(program, shader);
+ gl.delete_shader(shader);
+ }
+
+ program
+}
diff --git a/glow/src/quad.rs b/glow/src/quad.rs
new file mode 100644
index 00000000..a8fbb9e5
--- /dev/null
+++ b/glow/src/quad.rs
@@ -0,0 +1,235 @@
+use crate::program;
+use crate::Transformation;
+use glow::HasContext;
+use iced_graphics::layer;
+use iced_native::Rectangle;
+
+const MAX_INSTANCES: usize = 100_000;
+
+#[derive(Debug)]
+pub struct Pipeline {
+ program: <glow::Context as HasContext>::Program,
+ vertex_array: <glow::Context as HasContext>::VertexArray,
+ instances: <glow::Context as HasContext>::Buffer,
+ transform_location: <glow::Context as HasContext>::UniformLocation,
+ scale_location: <glow::Context as HasContext>::UniformLocation,
+ screen_height_location: <glow::Context as HasContext>::UniformLocation,
+ current_transform: Transformation,
+ current_scale: f32,
+ current_target_height: u32,
+}
+
+impl Pipeline {
+ pub fn new(gl: &glow::Context) -> Pipeline {
+ let program = unsafe {
+ program::create(
+ gl,
+ &[
+ (glow::VERTEX_SHADER, include_str!("shader/quad.vert")),
+ (glow::FRAGMENT_SHADER, include_str!("shader/quad.frag")),
+ ],
+ )
+ };
+
+ let transform_location =
+ unsafe { gl.get_uniform_location(program, "u_Transform") }
+ .expect("Get transform location");
+
+ let scale_location =
+ unsafe { gl.get_uniform_location(program, "u_Scale") }
+ .expect("Get scale location");
+
+ let screen_height_location =
+ unsafe { gl.get_uniform_location(program, "u_ScreenHeight") }
+ .expect("Get target height location");
+
+ unsafe {
+ gl.use_program(Some(program));
+
+ let matrix: [f32; 16] = Transformation::identity().into();
+ gl.uniform_matrix_4_f32_slice(
+ Some(&transform_location),
+ false,
+ &matrix,
+ );
+
+ gl.uniform_1_f32(Some(&scale_location), 1.0);
+ gl.uniform_1_f32(Some(&screen_height_location), 0.0);
+
+ gl.use_program(None);
+ }
+
+ let (vertex_array, instances) =
+ unsafe { create_instance_buffer(gl, MAX_INSTANCES) };
+
+ Pipeline {
+ program,
+ vertex_array,
+ instances,
+ transform_location,
+ scale_location,
+ screen_height_location,
+ current_transform: Transformation::identity(),
+ current_scale: 1.0,
+ current_target_height: 0,
+ }
+ }
+
+ pub fn draw(
+ &mut self,
+ gl: &glow::Context,
+ target_height: u32,
+ instances: &[layer::Quad],
+ transformation: Transformation,
+ scale: f32,
+ bounds: Rectangle<u32>,
+ ) {
+ unsafe {
+ gl.enable(glow::SCISSOR_TEST);
+ gl.scissor(
+ bounds.x as i32,
+ (target_height - (bounds.y + bounds.height)) as i32,
+ bounds.width as i32,
+ bounds.height as i32,
+ );
+
+ gl.use_program(Some(self.program));
+ gl.bind_vertex_array(Some(self.vertex_array));
+ gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.instances));
+ }
+
+ if transformation != self.current_transform {
+ unsafe {
+ let matrix: [f32; 16] = transformation.into();
+ gl.uniform_matrix_4_f32_slice(
+ Some(&self.transform_location),
+ false,
+ &matrix,
+ );
+
+ self.current_transform = transformation;
+ }
+ }
+
+ if scale != self.current_scale {
+ unsafe {
+ gl.uniform_1_f32(Some(&self.scale_location), scale);
+ }
+
+ self.current_scale = scale;
+ }
+
+ if target_height != self.current_target_height {
+ unsafe {
+ gl.uniform_1_f32(
+ Some(&self.screen_height_location),
+ target_height as f32,
+ );
+ }
+
+ self.current_target_height = target_height;
+ }
+
+ let mut i = 0;
+ let total = instances.len();
+
+ while i < total {
+ let end = (i + MAX_INSTANCES).min(total);
+ let amount = end - i;
+
+ unsafe {
+ gl.buffer_sub_data_u8_slice(
+ glow::ARRAY_BUFFER,
+ 0,
+ bytemuck::cast_slice(&instances[i..end]),
+ );
+
+ gl.draw_arrays_instanced(
+ glow::TRIANGLE_STRIP,
+ 0,
+ 4,
+ amount as i32,
+ );
+ }
+
+ i += MAX_INSTANCES;
+ }
+
+ unsafe {
+ gl.bind_vertex_array(None);
+ gl.use_program(None);
+ gl.disable(glow::SCISSOR_TEST);
+ }
+ }
+}
+
+unsafe fn create_instance_buffer(
+ gl: &glow::Context,
+ size: usize,
+) -> (
+ <glow::Context as HasContext>::VertexArray,
+ <glow::Context as HasContext>::Buffer,
+) {
+ let vertex_array = gl.create_vertex_array().expect("Create vertex array");
+ let buffer = gl.create_buffer().expect("Create instance buffer");
+
+ gl.bind_vertex_array(Some(vertex_array));
+ gl.bind_buffer(glow::ARRAY_BUFFER, Some(buffer));
+ gl.buffer_data_size(
+ glow::ARRAY_BUFFER,
+ (size * std::mem::size_of::<layer::Quad>()) as i32,
+ glow::DYNAMIC_DRAW,
+ );
+
+ let stride = std::mem::size_of::<layer::Quad>() as i32;
+
+ gl.enable_vertex_attrib_array(0);
+ gl.vertex_attrib_pointer_f32(0, 2, glow::FLOAT, false, stride, 0);
+ gl.vertex_attrib_divisor(0, 1);
+
+ gl.enable_vertex_attrib_array(1);
+ gl.vertex_attrib_pointer_f32(1, 2, glow::FLOAT, false, stride, 4 * 2);
+ gl.vertex_attrib_divisor(1, 1);
+
+ gl.enable_vertex_attrib_array(2);
+ gl.vertex_attrib_pointer_f32(2, 4, glow::FLOAT, false, stride, 4 * (2 + 2));
+ gl.vertex_attrib_divisor(2, 1);
+
+ gl.enable_vertex_attrib_array(3);
+ gl.vertex_attrib_pointer_f32(
+ 3,
+ 4,
+ glow::FLOAT,
+ false,
+ stride,
+ 4 * (2 + 2 + 4),
+ );
+ gl.vertex_attrib_divisor(3, 1);
+
+ gl.enable_vertex_attrib_array(4);
+ gl.vertex_attrib_pointer_f32(
+ 4,
+ 1,
+ glow::FLOAT,
+ false,
+ stride,
+ 4 * (2 + 2 + 4 + 4),
+ );
+ gl.vertex_attrib_divisor(4, 1);
+
+ gl.enable_vertex_attrib_array(5);
+ gl.vertex_attrib_pointer_f32(
+ 5,
+ 1,
+ glow::FLOAT,
+ false,
+ stride,
+ 4 * (2 + 2 + 4 + 4 + 1),
+ );
+ gl.vertex_attrib_divisor(5, 1);
+
+ gl.bind_vertex_array(None);
+ gl.bind_buffer(glow::ARRAY_BUFFER, None);
+
+ (vertex_array, buffer)
+}
diff --git a/glow/src/settings.rs b/glow/src/settings.rs
new file mode 100644
index 00000000..524d91a9
--- /dev/null
+++ b/glow/src/settings.rs
@@ -0,0 +1,31 @@
+//! Configure a renderer.
+pub use iced_graphics::Antialiasing;
+
+/// The settings of a [`Backend`].
+///
+/// [`Backend`]: crate::Backend
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct Settings {
+ /// The bytes of the font that will be used by default.
+ ///
+ /// If `None` is provided, a default system font will be chosen.
+ pub default_font: Option<&'static [u8]>,
+
+ /// The default size of text.
+ ///
+ /// By default, it will be set to 20.
+ pub default_text_size: u16,
+
+ /// The antialiasing strategy that will be used for triangle primitives.
+ pub antialiasing: Option<Antialiasing>,
+}
+
+impl Default for Settings {
+ fn default() -> Settings {
+ Settings {
+ default_font: None,
+ default_text_size: 20,
+ antialiasing: None,
+ }
+ }
+}
diff --git a/glow/src/shader/quad.frag b/glow/src/shader/quad.frag
new file mode 100644
index 00000000..cea36bdc
--- /dev/null
+++ b/glow/src/shader/quad.frag
@@ -0,0 +1,70 @@
+#version 330
+
+uniform float u_ScreenHeight;
+
+in vec4 v_Color;
+in vec4 v_BorderColor;
+in vec2 v_Pos;
+in vec2 v_Scale;
+in float v_BorderRadius;
+in float v_BorderWidth;
+
+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.0),
+ max(max(top_left_distance.y, bottom_right_distance.y), 0.0)
+ );
+
+ return sqrt(distance.x * distance.x + distance.y * distance.y);
+}
+
+void main() {
+ vec4 mixed_color;
+
+ vec2 fragCoord = vec2(gl_FragCoord.x, u_ScreenHeight - gl_FragCoord.y);
+
+ // TODO: Remove branching (?)
+ if(v_BorderWidth > 0) {
+ float internal_border = max(v_BorderRadius - v_BorderWidth, 0.0);
+
+ float internal_distance = distance(
+ fragCoord,
+ 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(
+ fragCoord,
+ v_Pos,
+ v_Scale,
+ v_BorderRadius
+ );
+
+ float radius_alpha =
+ 1.0 - smoothstep(max(v_BorderRadius - 0.5, 0.0), v_BorderRadius + 0.5, d);
+
+ o_Color = vec4(mixed_color.xyz, mixed_color.w * radius_alpha);
+}
diff --git a/glow/src/shader/quad.vert b/glow/src/shader/quad.vert
new file mode 100644
index 00000000..82417856
--- /dev/null
+++ b/glow/src/shader/quad.vert
@@ -0,0 +1,52 @@
+#version 330
+
+uniform mat4 u_Transform;
+uniform float u_Scale;
+
+layout(location = 0) in vec2 i_Pos;
+layout(location = 1) in vec2 i_Scale;
+layout(location = 2) in vec4 i_Color;
+layout(location = 3) in vec4 i_BorderColor;
+layout(location = 4) in float i_BorderRadius;
+layout(location = 5) in float i_BorderWidth;
+
+out vec4 v_Color;
+out vec4 v_BorderColor;
+out vec2 v_Pos;
+out vec2 v_Scale;
+out float v_BorderRadius;
+out float v_BorderWidth;
+
+const vec2 positions[4] = vec2[](
+ vec2(0.0, 0.0),
+ vec2(0.0, 1.0),
+ vec2(1.0, 0.0),
+ vec2(1.0, 1.0)
+);
+
+void main() {
+ vec2 q_Pos = positions[gl_VertexID];
+ 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)
+ );
+
+ v_Color = i_Color;
+ v_BorderColor = i_BorderColor;
+ v_Pos = p_Pos;
+ v_Scale = p_Scale;
+ v_BorderRadius = i_BorderRadius * u_Scale;
+ v_BorderWidth = i_BorderWidth * u_Scale;
+
+ gl_Position = u_Transform * i_Transform * vec4(q_Pos, 0.0, 1.0);
+}
diff --git a/glow/src/shader/triangle.frag b/glow/src/shader/triangle.frag
new file mode 100644
index 00000000..d186784a
--- /dev/null
+++ b/glow/src/shader/triangle.frag
@@ -0,0 +1,9 @@
+#version 330
+
+in vec4 v_Color;
+
+out vec4 o_Color;
+
+void main() {
+ o_Color = v_Color;
+}
diff --git a/glow/src/shader/triangle.vert b/glow/src/shader/triangle.vert
new file mode 100644
index 00000000..5723436a
--- /dev/null
+++ b/glow/src/shader/triangle.vert
@@ -0,0 +1,13 @@
+#version 330
+
+uniform mat4 u_Transform;
+
+layout(location = 0) in vec2 i_Position;
+layout(location = 1) in vec4 i_Color;
+
+out vec4 v_Color;
+
+void main() {
+ gl_Position = u_Transform * vec4(i_Position, 0.0, 1.0);
+ v_Color = i_Color;
+}
diff --git a/glow/src/text.rs b/glow/src/text.rs
new file mode 100644
index 00000000..925c7287
--- /dev/null
+++ b/glow/src/text.rs
@@ -0,0 +1,156 @@
+use crate::Transformation;
+use glow_glyph::ab_glyph;
+use iced_graphics::font;
+use std::{cell::RefCell, collections::HashMap};
+
+#[derive(Debug)]
+pub struct Pipeline {
+ draw_brush: RefCell<glow_glyph::GlyphBrush>,
+ draw_font_map: RefCell<HashMap<String, glow_glyph::FontId>>,
+ measure_brush: RefCell<glyph_brush::GlyphBrush<()>>,
+}
+
+impl Pipeline {
+ pub fn new(gl: &glow::Context, default_font: Option<&[u8]>) -> Self {
+ let default_font = default_font.map(|slice| slice.to_vec());
+
+ // TODO: Font customization
+ #[cfg(feature = "default_system_font")]
+ let default_font = {
+ default_font.or_else(|| {
+ font::Source::new()
+ .load(&[font::Family::SansSerif, font::Family::Serif])
+ .ok()
+ })
+ };
+
+ let default_font =
+ default_font.unwrap_or_else(|| font::FALLBACK.to_vec());
+
+ let font = ab_glyph::FontArc::try_from_vec(default_font)
+ .unwrap_or_else(|_| {
+ log::warn!(
+ "System font failed to load. Falling back to \
+ embedded font..."
+ );
+
+ ab_glyph::FontArc::try_from_slice(font::FALLBACK)
+ .expect("Load fallback font")
+ });
+
+ 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
+ .build(&gl);
+
+ let measure_brush =
+ glyph_brush::GlyphBrushBuilder::using_font(font).build();
+
+ Pipeline {
+ draw_brush: RefCell::new(draw_brush),
+ draw_font_map: RefCell::new(HashMap::new()),
+ measure_brush: RefCell::new(measure_brush),
+ }
+ }
+
+ pub fn queue(&mut self, section: glow_glyph::Section<'_>) {
+ self.draw_brush.borrow_mut().queue(section);
+ }
+
+ pub fn draw_queued(
+ &mut self,
+ gl: &glow::Context,
+ transformation: Transformation,
+ region: glow_glyph::Region,
+ ) {
+ self.draw_brush
+ .borrow_mut()
+ .draw_queued_with_transform_and_scissoring(
+ gl,
+ transformation.into(),
+ region,
+ )
+ .expect("Draw text");
+ }
+
+ pub fn measure(
+ &self,
+ content: &str,
+ size: f32,
+ font: iced_native::Font,
+ bounds: iced_native::Size,
+ ) -> (f32, f32) {
+ use glow_glyph::GlyphCruncher;
+
+ let glow_glyph::FontId(font_id) = self.find_font(font);
+
+ let section = glow_glyph::Section {
+ bounds: (bounds.width, bounds.height),
+ text: vec![glow_glyph::Text {
+ text: content,
+ scale: size.into(),
+ font_id: glow_glyph::FontId(font_id),
+ extra: glow_glyph::Extra::default(),
+ }],
+ ..Default::default()
+ };
+
+ if let Some(bounds) =
+ self.measure_brush.borrow_mut().glyph_bounds(section)
+ {
+ (bounds.width().ceil(), bounds.height().ceil())
+ } else {
+ (0.0, 0.0)
+ }
+ }
+
+ pub fn trim_measurement_cache(&mut self) {
+ // TODO: We should probably use a `GlyphCalculator` for this. However,
+ // it uses a lifetimed `GlyphCalculatorGuard` with side-effects on drop.
+ // This makes stuff quite inconvenient. A manual method for trimming the
+ // cache would make our lives easier.
+ loop {
+ let action = self
+ .measure_brush
+ .borrow_mut()
+ .process_queued(|_, _| {}, |_| {});
+
+ match action {
+ Ok(_) => break,
+ Err(glyph_brush::BrushError::TextureTooSmall { suggested }) => {
+ let (width, height) = suggested;
+
+ self.measure_brush
+ .borrow_mut()
+ .resize_texture(width, height);
+ }
+ }
+ }
+ }
+
+ pub fn find_font(&self, font: iced_native::Font) -> glow_glyph::FontId {
+ match font {
+ iced_native::Font::Default => glow_glyph::FontId(0),
+ iced_native::Font::External { name, bytes } => {
+ if let Some(font_id) = self.draw_font_map.borrow().get(name) {
+ return *font_id;
+ }
+
+ let font = ab_glyph::FontArc::try_from_slice(bytes)
+ .expect("Load font");
+
+ let _ = self.measure_brush.borrow_mut().add_font(font.clone());
+
+ let font_id = self.draw_brush.borrow_mut().add_font(font);
+
+ let _ = self
+ .draw_font_map
+ .borrow_mut()
+ .insert(String::from(name), font_id);
+
+ font_id
+ }
+ }
+ }
+}
diff --git a/glow/src/triangle.rs b/glow/src/triangle.rs
new file mode 100644
index 00000000..9202bcb2
--- /dev/null
+++ b/glow/src/triangle.rs
@@ -0,0 +1,292 @@
+//! Draw meshes of triangles.
+use crate::program;
+use crate::Transformation;
+use glow::HasContext;
+use iced_graphics::layer;
+use std::marker::PhantomData;
+
+pub use iced_graphics::triangle::{Mesh2D, Vertex2D};
+
+const VERTEX_BUFFER_SIZE: usize = 10_000;
+const INDEX_BUFFER_SIZE: usize = 10_000;
+
+#[derive(Debug)]
+pub(crate) struct Pipeline {
+ program: <glow::Context as HasContext>::Program,
+ vertex_array: <glow::Context as HasContext>::VertexArray,
+ vertices: Buffer<Vertex2D>,
+ indices: Buffer<u32>,
+ transform_location: <glow::Context as HasContext>::UniformLocation,
+ current_transform: Transformation,
+}
+
+impl Pipeline {
+ pub fn new(gl: &glow::Context) -> Pipeline {
+ let program = unsafe {
+ program::create(
+ gl,
+ &[
+ (glow::VERTEX_SHADER, include_str!("shader/triangle.vert")),
+ (
+ glow::FRAGMENT_SHADER,
+ include_str!("shader/triangle.frag"),
+ ),
+ ],
+ )
+ };
+
+ let transform_location =
+ unsafe { gl.get_uniform_location(program, "u_Transform") }
+ .expect("Get transform location");
+
+ unsafe {
+ gl.use_program(Some(program));
+
+ let transform: [f32; 16] = Transformation::identity().into();
+ gl.uniform_matrix_4_f32_slice(
+ Some(&transform_location),
+ false,
+ &transform,
+ );
+
+ gl.use_program(None);
+ }
+
+ let vertex_array =
+ unsafe { gl.create_vertex_array().expect("Create vertex array") };
+
+ unsafe {
+ gl.bind_vertex_array(Some(vertex_array));
+ }
+
+ let vertices = unsafe {
+ Buffer::new(
+ gl,
+ glow::ARRAY_BUFFER,
+ glow::DYNAMIC_DRAW,
+ VERTEX_BUFFER_SIZE,
+ )
+ };
+
+ let indices = unsafe {
+ Buffer::new(
+ gl,
+ glow::ELEMENT_ARRAY_BUFFER,
+ glow::DYNAMIC_DRAW,
+ INDEX_BUFFER_SIZE,
+ )
+ };
+
+ unsafe {
+ let stride = std::mem::size_of::<Vertex2D>() as i32;
+
+ gl.enable_vertex_attrib_array(0);
+ gl.vertex_attrib_pointer_f32(0, 2, glow::FLOAT, false, stride, 0);
+
+ gl.enable_vertex_attrib_array(1);
+ gl.vertex_attrib_pointer_f32(
+ 1,
+ 4,
+ glow::FLOAT,
+ false,
+ stride,
+ 4 * 2,
+ );
+
+ gl.bind_vertex_array(None);
+ }
+
+ Pipeline {
+ program,
+ vertex_array,
+ vertices,
+ indices,
+ transform_location,
+ current_transform: Transformation::identity(),
+ }
+ }
+
+ pub fn draw(
+ &mut self,
+ gl: &glow::Context,
+ target_height: u32,
+ transformation: Transformation,
+ scale_factor: f32,
+ meshes: &[layer::Mesh<'_>],
+ ) {
+ unsafe {
+ gl.enable(glow::MULTISAMPLE);
+ gl.enable(glow::SCISSOR_TEST);
+ gl.use_program(Some(self.program));
+ gl.bind_vertex_array(Some(self.vertex_array));
+ }
+
+ // This looks a bit crazy, but we are just counting how many vertices
+ // and indices we will need to handle.
+ // TODO: Improve readability
+ let (total_vertices, total_indices) = meshes
+ .iter()
+ .map(|layer::Mesh { buffers, .. }| {
+ (buffers.vertices.len(), buffers.indices.len())
+ })
+ .fold((0, 0), |(total_v, total_i), (v, i)| {
+ (total_v + v, total_i + i)
+ });
+
+ // Then we ensure the current buffers are big enough, resizing if
+ // necessary
+ unsafe {
+ self.vertices.bind(gl, total_vertices);
+ self.indices.bind(gl, total_indices);
+ }
+
+ // We upload all the vertices and indices upfront
+ let mut last_vertex = 0;
+ let mut last_index = 0;
+
+ for layer::Mesh { buffers, .. } in meshes {
+ unsafe {
+ gl.buffer_sub_data_u8_slice(
+ glow::ARRAY_BUFFER,
+ (last_vertex * std::mem::size_of::<Vertex2D>()) as i32,
+ bytemuck::cast_slice(&buffers.vertices),
+ );
+
+ gl.buffer_sub_data_u8_slice(
+ glow::ELEMENT_ARRAY_BUFFER,
+ (last_index * std::mem::size_of::<u32>()) as i32,
+ bytemuck::cast_slice(&buffers.indices),
+ );
+
+ last_vertex += buffers.vertices.len();
+ last_index += buffers.indices.len();
+ }
+ }
+
+ // Then we draw each mesh using offsets
+ let mut last_vertex = 0;
+ let mut last_index = 0;
+
+ for layer::Mesh {
+ buffers,
+ origin,
+ clip_bounds,
+ } in meshes
+ {
+ let transform =
+ transformation * Transformation::translate(origin.x, origin.y);
+
+ let clip_bounds = (*clip_bounds * scale_factor).snap();
+
+ unsafe {
+ if self.current_transform != transform {
+ let matrix: [f32; 16] = transform.into();
+ gl.uniform_matrix_4_f32_slice(
+ Some(&self.transform_location),
+ false,
+ &matrix,
+ );
+
+ self.current_transform = transform;
+ }
+
+ gl.scissor(
+ clip_bounds.x as i32,
+ (target_height - (clip_bounds.y + clip_bounds.height))
+ as i32,
+ clip_bounds.width as i32,
+ clip_bounds.height as i32,
+ );
+
+ gl.draw_elements_base_vertex(
+ glow::TRIANGLES,
+ buffers.indices.len() as i32,
+ glow::UNSIGNED_INT,
+ (last_index * std::mem::size_of::<u32>()) as i32,
+ last_vertex as i32,
+ );
+
+ last_vertex += buffers.vertices.len();
+ last_index += buffers.indices.len();
+ }
+ }
+
+ unsafe {
+ gl.bind_vertex_array(None);
+ gl.use_program(None);
+ gl.disable(glow::SCISSOR_TEST);
+ gl.disable(glow::MULTISAMPLE);
+ }
+ }
+}
+
+#[repr(C)]
+#[derive(Debug, Clone, Copy)]
+struct Uniforms {
+ transform: [f32; 16],
+}
+
+unsafe impl bytemuck::Zeroable for Uniforms {}
+unsafe impl bytemuck::Pod for Uniforms {}
+
+impl Default for Uniforms {
+ fn default() -> Self {
+ Self {
+ transform: *Transformation::identity().as_ref(),
+ }
+ }
+}
+
+impl From<Transformation> for Uniforms {
+ fn from(transformation: Transformation) -> Uniforms {
+ Self {
+ transform: transformation.into(),
+ }
+ }
+}
+
+#[derive(Debug)]
+struct Buffer<T> {
+ raw: <glow::Context as HasContext>::Buffer,
+ target: u32,
+ usage: u32,
+ size: usize,
+ phantom: PhantomData<T>,
+}
+
+impl<T> Buffer<T> {
+ pub unsafe fn new(
+ gl: &glow::Context,
+ target: u32,
+ usage: u32,
+ size: usize,
+ ) -> Self {
+ let raw = gl.create_buffer().expect("Create buffer");
+
+ let mut buffer = Buffer {
+ raw,
+ target,
+ usage,
+ size: 0,
+ phantom: PhantomData,
+ };
+
+ buffer.bind(gl, size);
+
+ buffer
+ }
+
+ pub unsafe fn bind(&mut self, gl: &glow::Context, size: usize) {
+ gl.bind_buffer(self.target, Some(self.raw));
+
+ if self.size < size {
+ gl.buffer_data_size(
+ self.target,
+ (size * std::mem::size_of::<T>()) as i32,
+ self.usage,
+ );
+
+ self.size = size;
+ }
+ }
+}
diff --git a/glow/src/widget.rs b/glow/src/widget.rs
new file mode 100644
index 00000000..b5c84c56
--- /dev/null
+++ b/glow/src/widget.rs
@@ -0,0 +1,72 @@
+//! Use the widgets supported out-of-the-box.
+//!
+//! # Re-exports
+//! For convenience, the contents of this module are available at the root
+//! module. Therefore, you can directly type:
+//!
+//! ```
+//! use iced_glow::{button, Button};
+//! ```
+use crate::Renderer;
+
+pub mod button;
+pub mod checkbox;
+pub mod container;
+pub mod pane_grid;
+pub mod pick_list;
+pub mod progress_bar;
+pub mod radio;
+pub mod rule;
+pub mod scrollable;
+pub mod slider;
+pub mod text_input;
+
+#[doc(no_inline)]
+pub use button::Button;
+#[doc(no_inline)]
+pub use checkbox::Checkbox;
+#[doc(no_inline)]
+pub use container::Container;
+#[doc(no_inline)]
+pub use pane_grid::PaneGrid;
+#[doc(no_inline)]
+pub use pick_list::PickList;
+#[doc(no_inline)]
+pub use progress_bar::ProgressBar;
+#[doc(no_inline)]
+pub use radio::Radio;
+#[doc(no_inline)]
+pub use rule::Rule;
+#[doc(no_inline)]
+pub use scrollable::Scrollable;
+#[doc(no_inline)]
+pub use slider::Slider;
+#[doc(no_inline)]
+pub use text_input::TextInput;
+
+#[cfg(feature = "canvas")]
+#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))]
+pub mod canvas;
+
+#[cfg(feature = "canvas")]
+#[doc(no_inline)]
+pub use canvas::Canvas;
+
+#[cfg(feature = "qr_code")]
+#[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))]
+pub mod qr_code;
+
+#[cfg(feature = "qr_code")]
+#[doc(no_inline)]
+pub use qr_code::QRCode;
+
+pub use iced_native::{Image, Space};
+
+/// A container that distributes its contents vertically.
+pub type Column<'a, Message> = iced_native::Column<'a, Message, Renderer>;
+
+/// A container that distributes its contents horizontally.
+pub type Row<'a, Message> = iced_native::Row<'a, Message, Renderer>;
+
+/// A paragraph of text.
+pub type Text = iced_native::Text<Renderer>;
diff --git a/glow/src/widget/button.rs b/glow/src/widget/button.rs
new file mode 100644
index 00000000..fc729cd5
--- /dev/null
+++ b/glow/src/widget/button.rs
@@ -0,0 +1,12 @@
+//! Allow your users to perform actions by pressing a button.
+//!
+//! A [`Button`] has some local [`State`].
+use crate::Renderer;
+
+pub use iced_graphics::button::{Style, StyleSheet};
+pub use iced_native::button::State;
+
+/// A widget that produces a message when clicked.
+///
+/// This is an alias of an `iced_native` button with an `iced_wgpu::Renderer`.
+pub type Button<'a, Message> = iced_native::Button<'a, Message, Renderer>;
diff --git a/glow/src/widget/canvas.rs b/glow/src/widget/canvas.rs
new file mode 100644
index 00000000..399dd19c
--- /dev/null
+++ b/glow/src/widget/canvas.rs
@@ -0,0 +1,6 @@
+//! Draw 2D graphics for your users.
+//!
+//! A [`Canvas`] widget can be used to draw different kinds of 2D shapes in a
+//! [`Frame`]. It can be used for animation, data visualization, game graphics,
+//! and more!
+pub use iced_graphics::canvas::*;
diff --git a/glow/src/widget/checkbox.rs b/glow/src/widget/checkbox.rs
new file mode 100644
index 00000000..d27d77cc
--- /dev/null
+++ b/glow/src/widget/checkbox.rs
@@ -0,0 +1,9 @@
+//! Show toggle controls using checkboxes.
+use crate::Renderer;
+
+pub use iced_graphics::checkbox::{Style, StyleSheet};
+
+/// A box that can be checked.
+///
+/// This is an alias of an `iced_native` checkbox with an `iced_wgpu::Renderer`.
+pub type Checkbox<Message> = iced_native::Checkbox<Message, Renderer>;
diff --git a/glow/src/widget/container.rs b/glow/src/widget/container.rs
new file mode 100644
index 00000000..bc26cef2
--- /dev/null
+++ b/glow/src/widget/container.rs
@@ -0,0 +1,10 @@
+//! Decorate content and apply alignment.
+use crate::Renderer;
+
+pub use iced_graphics::container::{Style, StyleSheet};
+
+/// An element decorating some content.
+///
+/// This is an alias of an `iced_native` container with a default
+/// `Renderer`.
+pub type Container<'a, Message> = iced_native::Container<'a, Message, Renderer>;
diff --git a/glow/src/widget/pane_grid.rs b/glow/src/widget/pane_grid.rs
new file mode 100644
index 00000000..c26dde48
--- /dev/null
+++ b/glow/src/widget/pane_grid.rs
@@ -0,0 +1,31 @@
+//! Let your users split regions of your application and organize layout dynamically.
+//!
+//! [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](https://gfycat.com/mixedflatjellyfish)
+//!
+//! # Example
+//! 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::Renderer;
+
+pub use iced_native::pane_grid::{
+ Axis, Configuration, Direction, DragEvent, Node, Pane, ResizeEvent, Split,
+ State,
+};
+
+/// A collection of panes distributed using either vertical or horizontal splits
+/// to completely fill the space available.
+///
+/// [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](https://gfycat.com/mixedflatjellyfish)
+///
+/// This is an alias of an `iced_native` pane grid with an `iced_wgpu::Renderer`.
+pub type PaneGrid<'a, Message> = iced_native::PaneGrid<'a, Message, Renderer>;
+
+/// The content of a [`Pane`].
+pub type Content<'a, Message> =
+ iced_native::pane_grid::Content<'a, Message, Renderer>;
+
+/// The title bar of a [`Pane`].
+pub type TitleBar<'a, Message> =
+ iced_native::pane_grid::TitleBar<'a, Message, Renderer>;
diff --git a/glow/src/widget/pick_list.rs b/glow/src/widget/pick_list.rs
new file mode 100644
index 00000000..fccc68c9
--- /dev/null
+++ b/glow/src/widget/pick_list.rs
@@ -0,0 +1,9 @@
+//! Display a dropdown list of selectable values.
+pub use iced_native::pick_list::State;
+
+pub use iced_graphics::overlay::menu::Style as Menu;
+pub use iced_graphics::pick_list::{Style, StyleSheet};
+
+/// A widget allowing the selection of a single value from a list of options.
+pub type PickList<'a, T, Message> =
+ iced_native::PickList<'a, T, Message, crate::Renderer>;
diff --git a/glow/src/widget/progress_bar.rs b/glow/src/widget/progress_bar.rs
new file mode 100644
index 00000000..45a25d00
--- /dev/null
+++ b/glow/src/widget/progress_bar.rs
@@ -0,0 +1,13 @@
+//! Allow your users to visually track the progress of a computation.
+//!
+//! A [`ProgressBar`] has a range of possible values and a current value,
+//! as well as a length, height and style.
+use crate::Renderer;
+
+pub use iced_graphics::progress_bar::{Style, StyleSheet};
+
+/// A bar that displays progress.
+///
+/// This is an alias of an `iced_native` progress bar with an
+/// `iced_wgpu::Renderer`.
+pub type ProgressBar = iced_native::ProgressBar<Renderer>;
diff --git a/glow/src/widget/qr_code.rs b/glow/src/widget/qr_code.rs
new file mode 100644
index 00000000..7b1c2408
--- /dev/null
+++ b/glow/src/widget/qr_code.rs
@@ -0,0 +1,2 @@
+//! Encode and display information in a QR code.
+pub use iced_graphics::qr_code::*;
diff --git a/glow/src/widget/radio.rs b/glow/src/widget/radio.rs
new file mode 100644
index 00000000..0b843d1f
--- /dev/null
+++ b/glow/src/widget/radio.rs
@@ -0,0 +1,10 @@
+//! Create choices using radio buttons.
+use crate::Renderer;
+
+pub use iced_graphics::radio::{Style, StyleSheet};
+
+/// A circular button representing a choice.
+///
+/// This is an alias of an `iced_native` radio button with an
+/// `iced_wgpu::Renderer`.
+pub type Radio<Message> = iced_native::Radio<Message, Renderer>;
diff --git a/glow/src/widget/rule.rs b/glow/src/widget/rule.rs
new file mode 100644
index 00000000..faa2be86
--- /dev/null
+++ b/glow/src/widget/rule.rs
@@ -0,0 +1,10 @@
+//! Display a horizontal or vertical rule for dividing content.
+
+use crate::Renderer;
+
+pub use iced_graphics::rule::{FillMode, Style, StyleSheet};
+
+/// Display a horizontal or vertical rule for dividing content.
+///
+/// This is an alias of an `iced_native` rule with an `iced_glow::Renderer`.
+pub type Rule = iced_native::Rule<Renderer>;
diff --git a/glow/src/widget/scrollable.rs b/glow/src/widget/scrollable.rs
new file mode 100644
index 00000000..fabb4318
--- /dev/null
+++ b/glow/src/widget/scrollable.rs
@@ -0,0 +1,13 @@
+//! Navigate an endless amount of content with a scrollbar.
+use crate::Renderer;
+
+pub use iced_graphics::scrollable::{Scrollbar, Scroller, StyleSheet};
+pub use iced_native::scrollable::State;
+
+/// A widget that can vertically display an infinite amount of content
+/// with a scrollbar.
+///
+/// This is an alias of an `iced_native` scrollable with a default
+/// `Renderer`.
+pub type Scrollable<'a, Message> =
+ iced_native::Scrollable<'a, Message, Renderer>;
diff --git a/glow/src/widget/slider.rs b/glow/src/widget/slider.rs
new file mode 100644
index 00000000..9a269858
--- /dev/null
+++ b/glow/src/widget/slider.rs
@@ -0,0 +1,13 @@
+//! Display an interactive selector of a single value from a range of values.
+//!
+//! A [`Slider`] has some local [`State`].
+use crate::Renderer;
+
+pub use iced_graphics::slider::{Handle, HandleShape, Style, StyleSheet};
+pub use iced_native::slider::State;
+
+/// An horizontal bar and a handle that selects a single value from a range of
+/// values.
+///
+/// This is an alias of an `iced_native` slider with an `iced_wgpu::Renderer`.
+pub type Slider<'a, T, Message> = iced_native::Slider<'a, T, Message, Renderer>;
diff --git a/glow/src/widget/text_input.rs b/glow/src/widget/text_input.rs
new file mode 100644
index 00000000..db18b1cc
--- /dev/null
+++ b/glow/src/widget/text_input.rs
@@ -0,0 +1,12 @@
+//! Display fields that can be filled with text.
+//!
+//! A [`TextInput`] has some local [`State`].
+use crate::Renderer;
+
+pub use iced_graphics::text_input::{Style, StyleSheet};
+pub use iced_native::text_input::State;
+
+/// A field that can be filled with text.
+///
+/// This is an alias of an `iced_native` text input with an `iced_wgpu::Renderer`.
+pub type TextInput<'a, Message> = iced_native::TextInput<'a, Message, Renderer>;
diff --git a/glow/src/window.rs b/glow/src/window.rs
new file mode 100644
index 00000000..aac5fb9e
--- /dev/null
+++ b/glow/src/window.rs
@@ -0,0 +1,4 @@
+//! Display rendering results on windows.
+mod compositor;
+
+pub use compositor::Compositor;
diff --git a/glow/src/window/compositor.rs b/glow/src/window/compositor.rs
new file mode 100644
index 00000000..1fb671ad
--- /dev/null
+++ b/glow/src/window/compositor.rs
@@ -0,0 +1,76 @@
+use crate::{Backend, Color, Error, Renderer, Settings, Viewport};
+
+use core::ffi::c_void;
+use glow::HasContext;
+use iced_graphics::{Antialiasing, Size};
+use iced_native::mouse;
+
+/// A window graphics backend for iced powered by `glow`.
+#[allow(missing_debug_implementations)]
+pub struct Compositor {
+ gl: glow::Context,
+}
+
+impl iced_graphics::window::GLCompositor for Compositor {
+ type Settings = Settings;
+ type Renderer = Renderer;
+
+ unsafe fn new(
+ settings: Self::Settings,
+ loader_function: impl FnMut(&str) -> *const c_void,
+ ) -> Result<(Self, Self::Renderer), Error> {
+ let gl = glow::Context::from_loader_function(loader_function);
+
+ // Enable auto-conversion from/to sRGB
+ gl.enable(glow::FRAMEBUFFER_SRGB);
+
+ // Enable alpha blending
+ gl.enable(glow::BLEND);
+ gl.blend_func(glow::SRC_ALPHA, glow::ONE_MINUS_SRC_ALPHA);
+
+ // Disable multisampling by default
+ gl.disable(glow::MULTISAMPLE);
+
+ let renderer = Renderer::new(Backend::new(&gl, settings));
+
+ Ok((Self { gl }, renderer))
+ }
+
+ fn sample_count(settings: &Settings) -> u32 {
+ settings
+ .antialiasing
+ .map(Antialiasing::sample_count)
+ .unwrap_or(0)
+ }
+
+ fn resize_viewport(&mut self, physical_size: Size<u32>) {
+ unsafe {
+ self.gl.viewport(
+ 0,
+ 0,
+ physical_size.width as i32,
+ physical_size.height as i32,
+ );
+ }
+ }
+
+ fn draw<T: AsRef<str>>(
+ &mut self,
+ renderer: &mut Self::Renderer,
+ viewport: &Viewport,
+ color: Color,
+ output: &<Self::Renderer as iced_native::Renderer>::Output,
+ overlay: &[T],
+ ) -> mouse::Interaction {
+ let gl = &self.gl;
+
+ let [r, g, b, a] = color.into_linear();
+
+ unsafe {
+ gl.clear_color(r, g, b, a);
+ gl.clear(glow::COLOR_BUFFER_BIT);
+ }
+
+ renderer.backend_mut().draw(gl, viewport, output, overlay)
+ }
+}
diff --git a/glutin/Cargo.toml b/glutin/Cargo.toml
new file mode 100644
index 00000000..505ee7e5
--- /dev/null
+++ b/glutin/Cargo.toml
@@ -0,0 +1,30 @@
+[package]
+name = "iced_glutin"
+version = "0.1.0"
+authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
+edition = "2018"
+description = "A glutin runtime for Iced"
+license = "MIT"
+repository = "https://github.com/hecrj/iced"
+documentation = "https://docs.rs/iced_glutin"
+keywords = ["gui", "ui", "graphics", "interface", "widgets"]
+categories = ["gui"]
+
+[features]
+debug = ["iced_winit/debug"]
+
+[dependencies]
+glutin = "0.26"
+
+[dependencies.iced_native]
+version = "0.3"
+path = "../native"
+
+[dependencies.iced_winit]
+version = "0.2"
+path = "../winit"
+
+[dependencies.iced_graphics]
+version = "0.1"
+path = "../graphics"
+features = ["opengl"]
diff --git a/glutin/README.md b/glutin/README.md
new file mode 100644
index 00000000..addb9228
--- /dev/null
+++ b/glutin/README.md
@@ -0,0 +1,29 @@
+# `iced_glutin`
+[![Documentation](https://docs.rs/iced_glutin/badge.svg)][documentation]
+[![Crates.io](https://img.shields.io/crates/v/iced_glutin.svg)](https://crates.io/crates/iced_glutin)
+[![License](https://img.shields.io/crates/l/iced_glutin.svg)](https://github.com/hecrj/iced/blob/master/LICENSE)
+[![project chat](https://img.shields.io/badge/chat-on_zulip-brightgreen.svg)](https://iced.zulipchat.com)
+
+`iced_glutin` offers some convenient abstractions on top of [`iced_native`] to quickstart development when using [`glutin`].
+
+It exposes a renderer-agnostic `Application` trait that can be implemented and then run with a simple call. The use of this trait is optional. A `conversion` module is provided for users that decide to implement a custom event loop.
+
+<p align="center">
+ <img alt="The native target" src="../docs/graphs/native.png" width="80%">
+</p>
+
+[documentation]: https://docs.rs/iced_glutin
+[`iced_native`]: ../native
+[`glutin`]: https://github.com/rust-windowing/glutin
+
+## Installation
+Add `iced_glutin` as a dependency in your `Cargo.toml`:
+
+```toml
+iced_glutin = "0.1"
+```
+
+__Iced moves fast and the `master` branch can contain breaking changes!__ If
+you want to learn about a specific release, check out [the release list].
+
+[the release list]: https://github.com/hecrj/iced/releases
diff --git a/glutin/src/application.rs b/glutin/src/application.rs
new file mode 100644
index 00000000..42513feb
--- /dev/null
+++ b/glutin/src/application.rs
@@ -0,0 +1,297 @@
+//! Create interactive, native cross-platform applications.
+use crate::{mouse, Error, Executor, Runtime};
+
+pub use iced_winit::Application;
+
+use iced_graphics::window;
+use iced_winit::application;
+use iced_winit::conversion;
+use iced_winit::futures;
+use iced_winit::futures::channel::mpsc;
+use iced_winit::{Cache, Clipboard, Debug, Proxy, Settings};
+
+use glutin::window::Window;
+use std::mem::ManuallyDrop;
+
+/// Runs an [`Application`] with an executor, compositor, and the provided
+/// settings.
+pub fn run<A, E, C>(
+ settings: Settings<A::Flags>,
+ compositor_settings: C::Settings,
+) -> Result<(), Error>
+where
+ A: Application + 'static,
+ E: Executor + 'static,
+ C: window::GLCompositor<Renderer = A::Renderer> + 'static,
+{
+ use futures::task;
+ use futures::Future;
+ use glutin::event_loop::EventLoop;
+ use glutin::ContextBuilder;
+
+ let mut debug = Debug::new();
+ debug.startup_started();
+
+ let event_loop = EventLoop::with_user_event();
+ let mut runtime = {
+ let executor = E::new().map_err(Error::ExecutorCreationFailed)?;
+ let proxy = Proxy::new(event_loop.create_proxy());
+
+ Runtime::new(executor, proxy)
+ };
+
+ let (application, init_command) = {
+ let flags = settings.flags;
+
+ runtime.enter(|| A::new(flags))
+ };
+
+ let subscription = application.subscription();
+
+ runtime.spawn(init_command);
+ runtime.track(subscription);
+
+ let context = {
+ let builder = settings.window.into_builder(
+ &application.title(),
+ application.mode(),
+ event_loop.primary_monitor(),
+ );
+
+ let context = ContextBuilder::new()
+ .with_vsync(true)
+ .with_multisampling(C::sample_count(&compositor_settings) as u16)
+ .build_windowed(builder, &event_loop)
+ .map_err(|error| {
+ use glutin::CreationError;
+
+ match error {
+ CreationError::Window(error) => {
+ Error::WindowCreationFailed(error)
+ }
+ _ => Error::GraphicsAdapterNotFound,
+ }
+ })?;
+
+ #[allow(unsafe_code)]
+ unsafe {
+ context.make_current().expect("Make OpenGL context current")
+ }
+ };
+
+ #[allow(unsafe_code)]
+ let (compositor, renderer) = unsafe {
+ C::new(compositor_settings, |address| {
+ context.get_proc_address(address)
+ })?
+ };
+
+ let (mut sender, receiver) = mpsc::unbounded();
+
+ let mut instance = Box::pin(run_instance::<A, E, C>(
+ application,
+ compositor,
+ renderer,
+ context,
+ runtime,
+ debug,
+ receiver,
+ ));
+
+ let mut context = task::Context::from_waker(task::noop_waker_ref());
+
+ event_loop.run(move |event, _, control_flow| {
+ use glutin::event_loop::ControlFlow;
+
+ if let ControlFlow::Exit = control_flow {
+ return;
+ }
+
+ if let Some(event) = event.to_static() {
+ sender.start_send(event).expect("Send event");
+
+ let poll = instance.as_mut().poll(&mut context);
+
+ *control_flow = match poll {
+ task::Poll::Pending => ControlFlow::Wait,
+ task::Poll::Ready(_) => ControlFlow::Exit,
+ };
+ }
+ });
+}
+
+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>>,
+) where
+ A: Application + 'static,
+ E: Executor + 'static,
+ C: window::GLCompositor<Renderer = A::Renderer> + 'static,
+{
+ use glutin::event;
+ use iced_winit::futures::stream::StreamExt;
+
+ let clipboard = Clipboard::new(context.window());
+
+ let mut state = application::State::new(&application, context.window());
+ let mut viewport_version = state.viewport_version();
+ let mut user_interface =
+ ManuallyDrop::new(application::build_user_interface(
+ &mut application,
+ Cache::default(),
+ &mut renderer,
+ state.logical_size(),
+ &mut debug,
+ ));
+
+ let mut primitive =
+ user_interface.draw(&mut renderer, state.cursor_position());
+ let mut mouse_interaction = mouse::Interaction::default();
+
+ let mut events = Vec::new();
+ let mut messages = Vec::new();
+
+ debug.startup_finished();
+
+ while let Some(event) = receiver.next().await {
+ match event {
+ event::Event::MainEventsCleared => {
+ if events.is_empty() && messages.is_empty() {
+ continue;
+ }
+
+ debug.event_processing_started();
+
+ let statuses = user_interface.update(
+ &events,
+ state.cursor_position(),
+ clipboard.as_ref().map(|c| c as _),
+ &mut renderer,
+ &mut messages,
+ );
+
+ debug.event_processing_finished();
+
+ for event in events.drain(..).zip(statuses.into_iter()) {
+ runtime.broadcast(event);
+ }
+
+ if !messages.is_empty() {
+ let cache =
+ ManuallyDrop::into_inner(user_interface).into_cache();
+
+ // Update application
+ application::update(
+ &mut application,
+ &mut runtime,
+ &mut debug,
+ &mut messages,
+ );
+
+ // Update window
+ state.synchronize(&application, context.window());
+
+ user_interface =
+ ManuallyDrop::new(application::build_user_interface(
+ &mut application,
+ cache,
+ &mut renderer,
+ state.logical_size(),
+ &mut debug,
+ ));
+ }
+
+ debug.draw_started();
+ primitive =
+ user_interface.draw(&mut renderer, state.cursor_position());
+ debug.draw_finished();
+
+ context.window().request_redraw();
+ }
+ event::Event::UserEvent(message) => {
+ messages.push(message);
+ }
+ event::Event::RedrawRequested(_) => {
+ 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();
+ user_interface = ManuallyDrop::new(
+ ManuallyDrop::into_inner(user_interface)
+ .relayout(logical_size, &mut renderer),
+ );
+ debug.layout_finished();
+
+ debug.draw_started();
+ primitive = user_interface
+ .draw(&mut renderer, state.cursor_position());
+ debug.draw_finished();
+
+ context.resize(glutin::dpi::PhysicalSize::new(
+ physical_size.width,
+ physical_size.height,
+ ));
+
+ compositor.resize_viewport(physical_size);
+
+ viewport_version = current_viewport_version;
+ }
+
+ let new_mouse_interaction = compositor.draw(
+ &mut renderer,
+ state.viewport(),
+ state.background_color(),
+ &primitive,
+ &debug.overlay(),
+ );
+
+ context.swap_buffers().expect("Swap buffers");
+
+ debug.render_finished();
+
+ if new_mouse_interaction != mouse_interaction {
+ context.window().set_cursor_icon(
+ conversion::mouse_interaction(new_mouse_interaction),
+ );
+
+ mouse_interaction = new_mouse_interaction;
+ }
+
+ // TODO: Handle animations!
+ // Maybe we can use `ControlFlow::WaitUntil` for this.
+ }
+ event::Event::WindowEvent {
+ event: window_event,
+ ..
+ } => {
+ if application::requests_exit(&window_event, state.modifiers())
+ {
+ break;
+ }
+
+ state.update(context.window(), &window_event, &mut debug);
+
+ if let Some(event) = conversion::window_event(
+ &window_event,
+ state.scale_factor(),
+ state.modifiers(),
+ ) {
+ events.push(event);
+ }
+ }
+ _ => {}
+ }
+ }
+
+ // Manually drop the user interface
+ drop(ManuallyDrop::into_inner(user_interface));
+}
diff --git a/glutin/src/lib.rs b/glutin/src/lib.rs
new file mode 100644
index 00000000..f2c0102a
--- /dev/null
+++ b/glutin/src/lib.rs
@@ -0,0 +1,25 @@
+//! A windowing shell for [`iced`], on top of [`glutin`].
+//!
+//! ![The native path of the Iced ecosystem](https://github.com/hecrj/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/native.png?raw=true)
+//!
+//! [`iced`]: https://github.com/hecrj/iced
+//! [`glutin`]: https://github.com/rust-windowing/glutin
+#![deny(missing_docs)]
+#![deny(missing_debug_implementations)]
+#![deny(unused_results)]
+#![deny(unsafe_code)]
+#![forbid(rust_2018_idioms)]
+
+pub use glutin;
+#[doc(no_inline)]
+pub use iced_native::*;
+
+pub mod application;
+
+pub use iced_winit::settings;
+pub use iced_winit::{Error, Mode};
+
+#[doc(no_inline)]
+pub use application::Application;
+#[doc(no_inline)]
+pub use settings::Settings;
diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml
new file mode 100644
index 00000000..73dc47bf
--- /dev/null
+++ b/graphics/Cargo.toml
@@ -0,0 +1,52 @@
+[package]
+name = "iced_graphics"
+version = "0.1.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"
+license = "MIT"
+repository = "https://github.com/hecrj/iced"
+documentation = "https://docs.rs/iced_graphics"
+keywords = ["gui", "ui", "graphics", "interface", "widgets"]
+categories = ["gui"]
+
+[features]
+canvas = ["lyon"]
+qr_code = ["qrcode", "canvas"]
+font-source = ["font-kit"]
+font-fallback = []
+font-icons = []
+opengl = []
+
+[dependencies]
+glam = "0.10"
+raw-window-handle = "0.3"
+thiserror = "1.0"
+
+[dependencies.bytemuck]
+version = "1.4"
+features = ["derive"]
+
+[dependencies.iced_native]
+version = "0.3"
+path = "../native"
+
+[dependencies.iced_style]
+version = "0.2"
+path = "../style"
+
+[dependencies.lyon]
+version = "0.16"
+optional = true
+
+[dependencies.qrcode]
+version = "0.12"
+optional = true
+
+[dependencies.font-kit]
+version = "0.8"
+optional = true
+
+[package.metadata.docs.rs]
+rustdoc-args = ["--cfg", "docsrs"]
+all-features = true
diff --git a/wgpu/src/text/icons.ttf b/graphics/fonts/Icons.ttf
index 1c832f86..5e455b69 100644
--- a/wgpu/src/text/icons.ttf
+++ b/graphics/fonts/Icons.ttf
Binary files differ
diff --git a/wgpu/fonts/Lato-Regular.ttf b/graphics/fonts/Lato-Regular.ttf
index 33eba8b1..33eba8b1 100644
--- a/wgpu/fonts/Lato-Regular.ttf
+++ b/graphics/fonts/Lato-Regular.ttf
Binary files differ
diff --git a/wgpu/fonts/OFL.txt b/graphics/fonts/OFL.txt
index dfca0da4..dfca0da4 100644
--- a/wgpu/fonts/OFL.txt
+++ b/graphics/fonts/OFL.txt
diff --git a/graphics/src/antialiasing.rs b/graphics/src/antialiasing.rs
new file mode 100644
index 00000000..7631c97c
--- /dev/null
+++ b/graphics/src/antialiasing.rs
@@ -0,0 +1,24 @@
+/// An antialiasing strategy.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Antialiasing {
+ /// Multisample AA with 2 samples
+ MSAAx2,
+ /// Multisample AA with 4 samples
+ MSAAx4,
+ /// Multisample AA with 8 samples
+ MSAAx8,
+ /// Multisample AA with 16 samples
+ MSAAx16,
+}
+
+impl Antialiasing {
+ /// Returns the amount of samples of the [`Antialiasing`].
+ pub fn sample_count(self) -> u32 {
+ match self {
+ Antialiasing::MSAAx2 => 2,
+ Antialiasing::MSAAx4 => 4,
+ Antialiasing::MSAAx8 => 8,
+ Antialiasing::MSAAx16 => 16,
+ }
+ }
+}
diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs
new file mode 100644
index 00000000..ed1b9e08
--- /dev/null
+++ b/graphics/src/backend.rs
@@ -0,0 +1,58 @@
+//! Write a graphics backend.
+use iced_native::image;
+use iced_native::svg;
+use iced_native::{Font, Size};
+
+/// The graphics backend of a [`Renderer`].
+///
+/// [`Renderer`]: crate::Renderer
+pub trait Backend {
+ /// Trims the measurements cache.
+ ///
+ /// This method is currently necessary to properly trim the text cache in
+ /// `iced_wgpu` and `iced_glow` because of limitations in the text rendering
+ /// pipeline. It will be removed in the future.
+ fn trim_measurements(&mut self) {}
+}
+
+/// A graphics backend that supports text rendering.
+pub trait Text {
+ /// The icon font of the backend.
+ const ICON_FONT: Font;
+
+ /// The `char` representing a ✔ icon in the [`ICON_FONT`].
+ ///
+ /// [`ICON_FONT`]: Self::ICON_FONT
+ const CHECKMARK_ICON: char;
+
+ /// The `char` representing a ▼ icon in the built-in [`ICON_FONT`].
+ ///
+ /// [`ICON_FONT`]: Self::ICON_FONT
+ const ARROW_DOWN_ICON: char;
+
+ /// Returns the default size of text.
+ fn default_size(&self) -> u16;
+
+ /// Measures the text contents with the given size and font,
+ /// returning the size of a laid out paragraph that fits in the provided
+ /// bounds.
+ fn measure(
+ &self,
+ contents: &str,
+ size: f32,
+ font: Font,
+ bounds: Size,
+ ) -> (f32, f32);
+}
+
+/// A graphics backend that supports image rendering.
+pub trait Image {
+ /// Returns the dimensions of the provided image.
+ fn dimensions(&self, handle: &image::Handle) -> (u32, u32);
+}
+
+/// A graphics backend that supports SVG rendering.
+pub trait Svg {
+ /// Returns the viewport dimensions of the provided SVG.
+ fn viewport_dimensions(&self, handle: &svg::Handle) -> (u32, u32);
+}
diff --git a/wgpu/src/defaults.rs b/graphics/src/defaults.rs
index 11718a87..11718a87 100644
--- a/wgpu/src/defaults.rs
+++ b/graphics/src/defaults.rs
diff --git a/graphics/src/error.rs b/graphics/src/error.rs
new file mode 100644
index 00000000..c86e326a
--- /dev/null
+++ b/graphics/src/error.rs
@@ -0,0 +1,7 @@
+/// A graphical error that occurred while running an application.
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+ /// A suitable graphics adapter or device could not be found
+ #[error("a suitable graphics adapter or device could not be found")]
+ AdapterNotFound,
+}
diff --git a/graphics/src/font.rs b/graphics/src/font.rs
new file mode 100644
index 00000000..d55d0faf
--- /dev/null
+++ b/graphics/src/font.rs
@@ -0,0 +1,35 @@
+//! Find system fonts or use the built-in ones.
+#[cfg(feature = "font-source")]
+mod source;
+
+#[cfg(feature = "font-source")]
+#[cfg_attr(docsrs, doc(cfg(feature = "font-source")))]
+pub use source::Source;
+
+#[cfg(feature = "font-source")]
+#[cfg_attr(docsrs, doc(cfg(feature = "font-source")))]
+pub use font_kit::{
+ error::SelectionError as LoadError, family_name::FamilyName as Family,
+};
+
+/// A built-in fallback font, for convenience.
+#[cfg(feature = "font-fallback")]
+#[cfg_attr(docsrs, doc(cfg(feature = "font-fallback")))]
+pub const FALLBACK: &[u8] = include_bytes!("../fonts/Lato-Regular.ttf");
+
+/// A built-in icon font, for convenience.
+#[cfg(feature = "font-icons")]
+#[cfg_attr(docsrs, doc(cfg(feature = "font-icons")))]
+pub const ICONS: iced_native::Font = iced_native::Font::External {
+ name: "iced_wgpu icons",
+ bytes: include_bytes!("../fonts/Icons.ttf"),
+};
+
+/// The `char` representing a ✔ icon in the built-in [`ICONS`] font.
+#[cfg(feature = "font-icons")]
+#[cfg_attr(docsrs, doc(cfg(feature = "font-icons")))]
+pub const CHECKMARK_ICON: char = '\u{F00C}';
+
+/// The `char` representing a ▼ icon in the built-in [`ICONS`] font.
+#[cfg(feature = "font-icons")]
+pub const ARROW_DOWN_ICON: char = '\u{E800}';
diff --git a/wgpu/src/text/font.rs b/graphics/src/font/source.rs
index 7346ccdb..a2d3f51d 100644
--- a/wgpu/src/text/font.rs
+++ b/graphics/src/font/source.rs
@@ -1,18 +1,20 @@
-pub use font_kit::{
- error::SelectionError as LoadError, family_name::FamilyName as Family,
-};
+use crate::font::{Family, LoadError};
+/// A font source that can find and load system fonts.
+#[allow(missing_debug_implementations)]
pub struct Source {
raw: font_kit::source::SystemSource,
}
impl Source {
+ /// Creates a new [`Source`].
pub fn new() -> Self {
Source {
raw: font_kit::source::SystemSource::new(),
}
}
+ /// Finds and loads a font matching the set of provided family priorities.
pub fn load(&self, families: &[Family]) -> Result<Vec<u8>, LoadError> {
let font = self.raw.select_best_match(
families,
diff --git a/graphics/src/layer.rs b/graphics/src/layer.rs
new file mode 100644
index 00000000..ab40b114
--- /dev/null
+++ b/graphics/src/layer.rs
@@ -0,0 +1,311 @@
+//! Organize rendering primitives into a flattened list of layers.
+use crate::image;
+use crate::svg;
+use crate::triangle;
+use crate::{
+ Background, Font, HorizontalAlignment, Point, Primitive, Rectangle, Size,
+ Vector, VerticalAlignment, Viewport,
+};
+
+/// A group of primitives that should be clipped together.
+#[derive(Debug, Clone)]
+pub struct Layer<'a> {
+ /// The clipping bounds of the [`Layer`].
+ pub bounds: Rectangle,
+
+ /// The quads of the [`Layer`].
+ pub quads: Vec<Quad>,
+
+ /// The triangle meshes of the [`Layer`].
+ pub meshes: Vec<Mesh<'a>>,
+
+ /// The text of the [`Layer`].
+ pub text: Vec<Text<'a>>,
+
+ /// The images of the [`Layer`].
+ pub images: Vec<Image>,
+}
+
+impl<'a> Layer<'a> {
+ /// Creates a new [`Layer`] with the given clipping bounds.
+ pub fn new(bounds: Rectangle) -> Self {
+ Self {
+ bounds,
+ quads: Vec::new(),
+ meshes: Vec::new(),
+ text: Vec::new(),
+ images: Vec::new(),
+ }
+ }
+
+ /// Creates a new [`Layer`] for the provided overlay text.
+ ///
+ /// This can be useful for displaying debug information.
+ pub fn overlay(lines: &'a [impl AsRef<str>], viewport: &Viewport) -> Self {
+ let mut overlay =
+ Layer::new(Rectangle::with_size(viewport.logical_size()));
+
+ for (i, line) in lines.iter().enumerate() {
+ let text = Text {
+ content: line.as_ref(),
+ bounds: Rectangle::new(
+ Point::new(11.0, 11.0 + 25.0 * i as f32),
+ Size::INFINITY,
+ ),
+ color: [0.9, 0.9, 0.9, 1.0],
+ size: 20.0,
+ font: Font::Default,
+ horizontal_alignment: HorizontalAlignment::Left,
+ vertical_alignment: VerticalAlignment::Top,
+ };
+
+ overlay.text.push(text);
+
+ overlay.text.push(Text {
+ bounds: text.bounds + Vector::new(-1.0, -1.0),
+ color: [0.0, 0.0, 0.0, 1.0],
+ ..text
+ });
+ }
+
+ overlay
+ }
+
+ /// Distributes the given [`Primitive`] and generates a list of layers based
+ /// on its contents.
+ pub fn generate(
+ primitive: &'a Primitive,
+ viewport: &Viewport,
+ ) -> Vec<Self> {
+ let first_layer =
+ Layer::new(Rectangle::with_size(viewport.logical_size()));
+
+ let mut layers = vec![first_layer];
+
+ Self::process_primitive(&mut layers, Vector::new(0.0, 0.0), primitive);
+
+ layers
+ }
+
+ fn process_primitive(
+ layers: &mut Vec<Self>,
+ translation: Vector,
+ primitive: &'a Primitive,
+ ) {
+ match primitive {
+ Primitive::None => {}
+ Primitive::Group { primitives } => {
+ // TODO: Inspect a bit and regroup (?)
+ for primitive in primitives {
+ Self::process_primitive(layers, translation, primitive)
+ }
+ }
+ Primitive::Text {
+ content,
+ bounds,
+ size,
+ color,
+ font,
+ horizontal_alignment,
+ vertical_alignment,
+ } => {
+ let layer = layers.last_mut().unwrap();
+
+ layer.text.push(Text {
+ content,
+ bounds: *bounds + translation,
+ size: *size,
+ color: color.into_linear(),
+ font: *font,
+ horizontal_alignment: *horizontal_alignment,
+ vertical_alignment: *vertical_alignment,
+ });
+ }
+ Primitive::Quad {
+ bounds,
+ background,
+ border_radius,
+ border_width,
+ border_color,
+ } => {
+ let layer = layers.last_mut().unwrap();
+
+ // TODO: Move some of these computations to the GPU (?)
+ layer.quads.push(Quad {
+ position: [
+ bounds.x + translation.x,
+ bounds.y + translation.y,
+ ],
+ size: [bounds.width, bounds.height],
+ color: match background {
+ Background::Color(color) => color.into_linear(),
+ },
+ border_radius: *border_radius,
+ border_width: *border_width,
+ border_color: border_color.into_linear(),
+ });
+ }
+ Primitive::Mesh2D { buffers, size } => {
+ let layer = layers.last_mut().unwrap();
+
+ let bounds = Rectangle::new(
+ Point::new(translation.x, translation.y),
+ *size,
+ );
+
+ // Only draw visible content
+ if let Some(clip_bounds) = layer.bounds.intersection(&bounds) {
+ layer.meshes.push(Mesh {
+ origin: Point::new(translation.x, translation.y),
+ buffers,
+ clip_bounds,
+ });
+ }
+ }
+ Primitive::Clip {
+ bounds,
+ offset,
+ content,
+ } => {
+ let layer = layers.last_mut().unwrap();
+ let translated_bounds = *bounds + translation;
+
+ // Only draw visible content
+ if let Some(clip_bounds) =
+ 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.push(new_layer);
+ }
+ }
+ Primitive::Translate {
+ translation: new_translation,
+ content,
+ } => {
+ Self::process_primitive(
+ layers,
+ translation + *new_translation,
+ &content,
+ );
+ }
+ Primitive::Cached { cache } => {
+ Self::process_primitive(layers, translation, &cache);
+ }
+ Primitive::Image { handle, bounds } => {
+ let layer = layers.last_mut().unwrap();
+
+ layer.images.push(Image::Raster {
+ handle: handle.clone(),
+ bounds: *bounds + translation,
+ });
+ }
+ Primitive::Svg { handle, bounds } => {
+ let layer = layers.last_mut().unwrap();
+
+ layer.images.push(Image::Vector {
+ handle: handle.clone(),
+ bounds: *bounds + translation,
+ });
+ }
+ }
+ }
+}
+
+/// A colored rectangle with a border.
+///
+/// This type can be directly uploaded to GPU memory.
+#[derive(Debug, Clone, Copy)]
+#[repr(C)]
+pub struct Quad {
+ /// The position of the [`Quad`].
+ pub position: [f32; 2],
+
+ /// The size of the [`Quad`].
+ pub size: [f32; 2],
+
+ /// The color of the [`Quad`], in __linear RGB__.
+ pub color: [f32; 4],
+
+ /// The border color of the [`Quad`], in __linear RGB__.
+ pub border_color: [f32; 4],
+
+ /// The border radius of the [`Quad`].
+ pub border_radius: f32,
+
+ /// The border width of the [`Quad`].
+ pub border_width: f32,
+}
+
+/// A mesh of triangles.
+#[derive(Debug, Clone, Copy)]
+pub struct Mesh<'a> {
+ /// The origin of the vertices of the [`Mesh`].
+ pub origin: Point,
+
+ /// The vertex and index buffers of the [`Mesh`].
+ pub buffers: &'a triangle::Mesh2D,
+
+ /// The clipping bounds of the [`Mesh`].
+ pub clip_bounds: Rectangle<f32>,
+}
+
+/// A paragraph of text.
+#[derive(Debug, Clone, Copy)]
+pub struct Text<'a> {
+ /// The content of the [`Text`].
+ pub content: &'a str,
+
+ /// The layout bounds of the [`Text`].
+ pub bounds: Rectangle,
+
+ /// The color of the [`Text`], in __linear RGB_.
+ pub color: [f32; 4],
+
+ /// The size of the [`Text`].
+ pub size: f32,
+
+ /// The font of the [`Text`].
+ pub font: Font,
+
+ /// The horizontal alignment of the [`Text`].
+ pub horizontal_alignment: HorizontalAlignment,
+
+ /// The vertical alignment of the [`Text`].
+ pub vertical_alignment: VerticalAlignment,
+}
+
+/// A raster or vector image.
+#[derive(Debug, Clone)]
+pub enum Image {
+ /// A raster image.
+ Raster {
+ /// The handle of a raster image.
+ handle: image::Handle,
+
+ /// The bounds of the image.
+ bounds: Rectangle,
+ },
+ /// A vector image.
+ Vector {
+ /// The handle of a vector image.
+ handle: svg::Handle,
+
+ /// The bounds of the image.
+ bounds: Rectangle,
+ },
+}
+
+#[allow(unsafe_code)]
+unsafe impl bytemuck::Zeroable for Quad {}
+
+#[allow(unsafe_code)]
+unsafe impl bytemuck::Pod for Quad {}
diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs
new file mode 100644
index 00000000..14388653
--- /dev/null
+++ b/graphics/src/lib.rs
@@ -0,0 +1,45 @@
+//! A bunch of backend-agnostic types that can be leveraged to build a renderer
+//! for [`iced`].
+//!
+//! ![The native path of the Iced ecosystem](https://github.com/hecrj/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/native.png?raw=true)
+//!
+//! [`iced`]: https://github.com/hecrj/iced
+#![deny(missing_docs)]
+#![deny(missing_debug_implementations)]
+#![deny(unused_results)]
+#![deny(unsafe_code)]
+#![forbid(rust_2018_idioms)]
+#![cfg_attr(docsrs, feature(doc_cfg))]
+mod antialiasing;
+mod error;
+mod primitive;
+mod renderer;
+mod transformation;
+mod viewport;
+
+pub mod backend;
+pub mod defaults;
+pub mod font;
+pub mod layer;
+pub mod overlay;
+pub mod triangle;
+pub mod widget;
+pub mod window;
+
+#[doc(no_inline)]
+pub use widget::*;
+
+pub use antialiasing::Antialiasing;
+pub use backend::Backend;
+pub use defaults::Defaults;
+pub use error::Error;
+pub use layer::Layer;
+pub use primitive::Primitive;
+pub use renderer::Renderer;
+pub use transformation::Transformation;
+pub use viewport::Viewport;
+
+pub use iced_native::{
+ Background, Color, Font, HorizontalAlignment, Point, Rectangle, Size,
+ Vector, VerticalAlignment,
+};
diff --git a/graphics/src/overlay.rs b/graphics/src/overlay.rs
new file mode 100644
index 00000000..bc0ed744
--- /dev/null
+++ b/graphics/src/overlay.rs
@@ -0,0 +1,2 @@
+//! Display interactive elements on top of other widgets.
+pub mod menu;
diff --git a/graphics/src/overlay/menu.rs b/graphics/src/overlay/menu.rs
new file mode 100644
index 00000000..ffe998c5
--- /dev/null
+++ b/graphics/src/overlay/menu.rs
@@ -0,0 +1,117 @@
+//! Build and show dropdown menus.
+use crate::backend::{self, Backend};
+use crate::{Primitive, Renderer};
+use iced_native::{
+ mouse, overlay, Color, Font, HorizontalAlignment, Point, Rectangle,
+ VerticalAlignment,
+};
+
+pub use iced_style::menu::Style;
+
+impl<B> overlay::menu::Renderer for Renderer<B>
+where
+ B: Backend + backend::Text,
+{
+ type Style = Style;
+
+ fn decorate(
+ &mut self,
+ bounds: Rectangle,
+ _cursor_position: Point,
+ style: &Style,
+ (primitives, mouse_cursor): Self::Output,
+ ) -> Self::Output {
+ (
+ Primitive::Group {
+ primitives: vec![
+ Primitive::Quad {
+ bounds,
+ background: style.background,
+ border_color: style.border_color,
+ border_width: style.border_width,
+ border_radius: 0.0,
+ },
+ primitives,
+ ],
+ },
+ mouse_cursor,
+ )
+ }
+
+ fn draw<T: ToString>(
+ &mut self,
+ bounds: Rectangle,
+ cursor_position: Point,
+ viewport: &Rectangle,
+ options: &[T],
+ hovered_option: Option<usize>,
+ padding: u16,
+ text_size: u16,
+ font: Font,
+ style: &Style,
+ ) -> Self::Output {
+ use std::f32;
+
+ let is_mouse_over = bounds.contains(cursor_position);
+ let option_height = text_size as usize + padding as usize * 2;
+
+ let mut primitives = Vec::new();
+
+ let offset = viewport.y - bounds.y;
+ let start = (offset / option_height as f32) as usize;
+ let end =
+ ((offset + viewport.height) / option_height as f32).ceil() as usize;
+
+ let visible_options = &options[start..end.min(options.len())];
+
+ for (i, option) in visible_options.iter().enumerate() {
+ let i = start + i;
+ let is_selected = hovered_option == Some(i);
+
+ let bounds = Rectangle {
+ x: bounds.x,
+ y: bounds.y + (option_height * i) as f32,
+ width: bounds.width,
+ height: f32::from(text_size + padding * 2),
+ };
+
+ if is_selected {
+ primitives.push(Primitive::Quad {
+ bounds,
+ background: style.selected_background,
+ border_color: Color::TRANSPARENT,
+ border_width: 0.0,
+ border_radius: 0.0,
+ });
+ }
+
+ primitives.push(Primitive::Text {
+ content: option.to_string(),
+ bounds: Rectangle {
+ x: bounds.x + f32::from(padding),
+ y: bounds.center_y(),
+ width: f32::INFINITY,
+ ..bounds
+ },
+ size: f32::from(text_size),
+ font,
+ color: if is_selected {
+ style.selected_text_color
+ } else {
+ style.text_color
+ },
+ horizontal_alignment: HorizontalAlignment::Left,
+ vertical_alignment: VerticalAlignment::Center,
+ });
+ }
+
+ (
+ Primitive::Group { primitives },
+ if is_mouse_over {
+ mouse::Interaction::Pointer
+ } else {
+ mouse::Interaction::default()
+ },
+ )
+ }
+}
diff --git a/wgpu/src/primitive.rs b/graphics/src/primitive.rs
index 46d9e624..30263bd4 100644
--- a/wgpu/src/primitive.rs
+++ b/graphics/src/primitive.rs
@@ -1,5 +1,5 @@
use iced_native::{
- image, svg, Background, Color, Font, HorizontalAlignment, Point, Rectangle,
+ image, svg, Background, Color, Font, HorizontalAlignment, Rectangle, Size,
Vector, VerticalAlignment,
};
@@ -40,9 +40,9 @@ pub enum Primitive {
/// The background of the quad
background: Background,
/// The border radius of the quad
- border_radius: u16,
+ border_radius: f32,
/// The border width of the quad
- border_width: u16,
+ border_width: f32,
/// The border color of the quad
border_color: Color,
},
@@ -70,24 +70,31 @@ pub enum Primitive {
/// The content of the clip
content: Box<Primitive>,
},
+ /// A primitive that applies a translation
+ Translate {
+ /// The translation vector
+ translation: Vector,
+
+ /// The primitive to translate
+ content: Box<Primitive>,
+ },
/// A low-level primitive to render a mesh of triangles.
///
/// It can be used to render many kinds of geometry freely.
Mesh2D {
- /// The top-left coordinate of the mesh
- origin: Point,
-
/// The vertex and index buffers of the mesh
buffers: triangle::Mesh2D,
+
+ /// The size of the drawable region of the mesh.
+ ///
+ /// Any geometry that falls out of this region will be clipped.
+ size: Size,
},
/// A cached primitive.
///
/// This can be useful if you are implementing a widget where primitive
/// generation is expensive.
Cached {
- /// The origin of the coordinate system of the cached primitives
- origin: Point,
-
/// The cached primitive
cache: Arc<Primitive>,
},
diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs
new file mode 100644
index 00000000..fa63991b
--- /dev/null
+++ b/graphics/src/renderer.rs
@@ -0,0 +1,121 @@
+use crate::{Backend, Defaults, Primitive};
+use iced_native::layout::{self, Layout};
+use iced_native::mouse;
+use iced_native::{
+ Background, Color, Element, Point, Rectangle, Vector, Widget,
+};
+
+/// A backend-agnostic renderer that supports all the built-in widgets.
+#[derive(Debug)]
+pub struct Renderer<B: Backend> {
+ backend: B,
+}
+
+impl<B: Backend> Renderer<B> {
+ /// Creates a new [`Renderer`] from the given [`Backend`].
+ pub fn new(backend: B) -> Self {
+ Self { backend }
+ }
+
+ /// Returns a reference to the [`Backend`] of the [`Renderer`].
+ pub fn backend(&self) -> &B {
+ &self.backend
+ }
+
+ /// Returns a mutable reference to the [`Backend`] of the [`Renderer`].
+ pub fn backend_mut(&mut self) -> &mut B {
+ &mut self.backend
+ }
+}
+
+impl<B> iced_native::Renderer for Renderer<B>
+where
+ B: Backend,
+{
+ type Output = (Primitive, mouse::Interaction);
+ type Defaults = Defaults;
+
+ fn layout<'a, Message>(
+ &mut self,
+ element: &Element<'a, Message, Self>,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ let layout = element.layout(self, limits);
+
+ self.backend.trim_measurements();
+
+ layout
+ }
+
+ fn overlay(
+ &mut self,
+ (base_primitive, base_cursor): (Primitive, mouse::Interaction),
+ (overlay_primitives, overlay_cursor): (Primitive, mouse::Interaction),
+ overlay_bounds: Rectangle,
+ ) -> (Primitive, mouse::Interaction) {
+ (
+ Primitive::Group {
+ primitives: vec![
+ base_primitive,
+ Primitive::Clip {
+ bounds: Rectangle {
+ width: overlay_bounds.width + 0.5,
+ height: overlay_bounds.height + 0.5,
+ ..overlay_bounds
+ },
+ offset: Vector::new(0, 0),
+ content: Box::new(overlay_primitives),
+ },
+ ],
+ },
+ if base_cursor > overlay_cursor {
+ base_cursor
+ } else {
+ overlay_cursor
+ },
+ )
+ }
+}
+
+impl<B> layout::Debugger for Renderer<B>
+where
+ B: Backend,
+{
+ fn explain<Message>(
+ &mut self,
+ defaults: &Defaults,
+ widget: &dyn Widget<Message, Self>,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ viewport: &Rectangle,
+ color: Color,
+ ) -> Self::Output {
+ let (primitive, cursor) =
+ widget.draw(self, defaults, layout, cursor_position, viewport);
+
+ let mut primitives = Vec::new();
+
+ explain_layout(layout, color, &mut primitives);
+ primitives.push(primitive);
+
+ (Primitive::Group { primitives }, cursor)
+ }
+}
+
+fn explain_layout(
+ layout: Layout<'_>,
+ color: Color,
+ primitives: &mut Vec<Primitive>,
+) {
+ primitives.push(Primitive::Quad {
+ bounds: layout.bounds(),
+ background: Background::Color(Color::TRANSPARENT),
+ border_radius: 0.0,
+ border_width: 1.0,
+ border_color: [0.6, 0.6, 0.6, 0.5].into(),
+ });
+
+ for child in layout.children() {
+ explain_layout(child, color, primitives);
+ }
+}
diff --git a/wgpu/src/transformation.rs b/graphics/src/transformation.rs
index 666696f3..2a19caed 100644
--- a/wgpu/src/transformation.rs
+++ b/graphics/src/transformation.rs
@@ -1,4 +1,4 @@
-use glam::{Mat4, Vec3, Vec4};
+use glam::{Mat4, Vec3};
use std::ops::Mul;
/// A 2D transformation matrix.
@@ -14,11 +14,10 @@ impl Transformation {
/// Creates an orthographic projection.
#[rustfmt::skip]
pub fn orthographic(width: u32, height: u32) -> Transformation {
- Transformation(Mat4::from_cols(
- Vec4::new(2.0 / width as f32, 0.0, 0.0, 0.0),
- Vec4::new(0.0, 2.0 / height as f32, 0.0, 0.0),
- Vec4::new(0.0, 0.0, -1.0, 0.0),
- Vec4::new(-1.0, -1.0, 0.0, 1.0)
+ Transformation(Mat4::orthographic_rh_gl(
+ 0.0, width as f32,
+ height as f32, 0.0,
+ -1.0, 1.0
))
}
@@ -49,6 +48,6 @@ impl AsRef<[f32; 16]> for Transformation {
impl From<Transformation> for [f32; 16] {
fn from(t: Transformation) -> [f32; 16] {
- t.as_ref().clone()
+ *t.as_ref()
}
}
diff --git a/graphics/src/triangle.rs b/graphics/src/triangle.rs
new file mode 100644
index 00000000..05028f51
--- /dev/null
+++ b/graphics/src/triangle.rs
@@ -0,0 +1,25 @@
+//! Draw geometry using meshes of triangles.
+use bytemuck::{Pod, Zeroable};
+
+/// A set of [`Vertex2D`] and indices representing a list of triangles.
+#[derive(Clone, Debug)]
+pub struct Mesh2D {
+ /// The vertices of the mesh
+ pub vertices: Vec<Vertex2D>,
+
+ /// The list of vertex indices that defines the triangles of the mesh.
+ ///
+ /// Therefore, this list should always have a length that is a multiple of
+ /// 3.
+ pub indices: Vec<u32>,
+}
+
+/// A two-dimensional vertex with some color in __linear__ RGBA.
+#[derive(Copy, Clone, Debug, Zeroable, Pod)]
+#[repr(C)]
+pub struct Vertex2D {
+ /// The vertex position
+ pub position: [f32; 2],
+ /// The vertex color in __linear__ RGBA.
+ pub color: [f32; 4],
+}
diff --git a/graphics/src/viewport.rs b/graphics/src/viewport.rs
new file mode 100644
index 00000000..78d539af
--- /dev/null
+++ b/graphics/src/viewport.rs
@@ -0,0 +1,56 @@
+use crate::{Size, Transformation};
+
+/// A viewing region for displaying computer graphics.
+#[derive(Debug, Clone)]
+pub struct Viewport {
+ physical_size: Size<u32>,
+ logical_size: Size<f32>,
+ scale_factor: f64,
+ projection: Transformation,
+}
+
+impl Viewport {
+ /// Creates a new [`Viewport`] with the given physical dimensions and scale
+ /// factor.
+ pub fn with_physical_size(size: Size<u32>, scale_factor: f64) -> Viewport {
+ Viewport {
+ physical_size: size,
+ logical_size: Size::new(
+ (size.width as f64 / scale_factor) as f32,
+ (size.height as f64 / scale_factor) as f32,
+ ),
+ scale_factor,
+ projection: Transformation::orthographic(size.width, size.height),
+ }
+ }
+
+ /// Returns the physical size of the [`Viewport`].
+ pub fn physical_size(&self) -> Size<u32> {
+ self.physical_size
+ }
+
+ /// Returns the physical width of the [`Viewport`].
+ pub fn physical_width(&self) -> u32 {
+ self.physical_size.height
+ }
+
+ /// Returns the physical height of the [`Viewport`].
+ pub fn physical_height(&self) -> u32 {
+ self.physical_size.height
+ }
+
+ /// Returns the logical size of the [`Viewport`].
+ pub fn logical_size(&self) -> Size<f32> {
+ self.logical_size
+ }
+
+ /// Returns the scale factor of the [`Viewport`].
+ pub fn scale_factor(&self) -> f64 {
+ self.scale_factor
+ }
+
+ /// Returns the projection transformation of the [`Viewport`].
+ pub fn projection(&self) -> Transformation {
+ self.projection
+ }
+}
diff --git a/graphics/src/widget.rs b/graphics/src/widget.rs
new file mode 100644
index 00000000..159ca91b
--- /dev/null
+++ b/graphics/src/widget.rs
@@ -0,0 +1,73 @@
+//! Use the widgets supported out-of-the-box.
+//!
+//! # Re-exports
+//! For convenience, the contents of this module are available at the root
+//! module. Therefore, you can directly type:
+//!
+//! ```
+//! use iced_graphics::{button, Button};
+//! ```
+pub mod button;
+pub mod checkbox;
+pub mod container;
+pub mod image;
+pub mod pane_grid;
+pub mod pick_list;
+pub mod progress_bar;
+pub mod radio;
+pub mod rule;
+pub mod scrollable;
+pub mod slider;
+pub mod svg;
+pub mod text_input;
+
+mod column;
+mod row;
+mod space;
+mod text;
+
+#[doc(no_inline)]
+pub use button::Button;
+#[doc(no_inline)]
+pub use checkbox::Checkbox;
+#[doc(no_inline)]
+pub use container::Container;
+#[doc(no_inline)]
+pub use pane_grid::PaneGrid;
+#[doc(no_inline)]
+pub use pick_list::PickList;
+#[doc(no_inline)]
+pub use progress_bar::ProgressBar;
+#[doc(no_inline)]
+pub use radio::Radio;
+#[doc(no_inline)]
+pub use rule::Rule;
+#[doc(no_inline)]
+pub use scrollable::Scrollable;
+#[doc(no_inline)]
+pub use slider::Slider;
+#[doc(no_inline)]
+pub use text_input::TextInput;
+
+pub use column::Column;
+pub use image::Image;
+pub use row::Row;
+pub use space::Space;
+pub use svg::Svg;
+pub use text::Text;
+
+#[cfg(feature = "canvas")]
+#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))]
+pub mod canvas;
+
+#[cfg(feature = "canvas")]
+#[doc(no_inline)]
+pub use canvas::Canvas;
+
+#[cfg(feature = "qr_code")]
+#[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))]
+pub mod qr_code;
+
+#[cfg(feature = "qr_code")]
+#[doc(no_inline)]
+pub use qr_code::QRCode;
diff --git a/wgpu/src/renderer/widget/button.rs b/graphics/src/widget/button.rs
index 0de5bf5c..2e3f78ca 100644
--- a/wgpu/src/renderer/widget/button.rs
+++ b/graphics/src/widget/button.rs
@@ -1,16 +1,33 @@
-use crate::{button::StyleSheet, defaults, Defaults, Primitive, Renderer};
+//! Allow your users to perform actions by pressing a button.
+//!
+//! A [`Button`] has some local [`State`].
+use crate::defaults::{self, Defaults};
+use crate::{Backend, Primitive, Renderer};
+use iced_native::mouse;
use iced_native::{
- Background, Color, Element, Layout, MouseCursor, Point, Rectangle, Vector,
+ Background, Color, Element, Layout, Point, Rectangle, Vector,
};
-impl iced_native::button::Renderer for Renderer {
+pub use iced_native::button::State;
+pub use iced_style::button::{Style, StyleSheet};
+
+/// A widget that produces a message when clicked.
+///
+/// This is an alias of an `iced_native` button with an `iced_wgpu::Renderer`.
+pub type Button<'a, Message, Backend> =
+ iced_native::Button<'a, Message, Renderer<Backend>>;
+
+impl<B> iced_native::button::Renderer for Renderer<B>
+where
+ B: Backend,
+{
const DEFAULT_PADDING: u16 = 5;
type Style = Box<dyn StyleSheet>;
fn draw<Message>(
&mut self,
- defaults: &Defaults,
+ _defaults: &Defaults,
bounds: Rectangle,
cursor_position: Point,
is_disabled: bool,
@@ -39,14 +56,14 @@ impl iced_native::button::Renderer for Renderer {
text: defaults::Text {
color: styling.text_color,
},
- ..*defaults
},
content_layout,
cursor_position,
+ &bounds,
);
(
- if styling.background.is_some() || styling.border_width > 0 {
+ if styling.background.is_some() || styling.border_width > 0.0 {
let background = Primitive::Quad {
bounds,
background: styling
@@ -73,7 +90,7 @@ impl iced_native::button::Renderer for Renderer {
[0.0, 0.0, 0.0, 0.5].into(),
),
border_radius: styling.border_radius,
- border_width: 0,
+ border_width: 0.0,
border_color: Color::TRANSPARENT,
};
@@ -84,10 +101,10 @@ impl iced_native::button::Renderer for Renderer {
} else {
content
},
- if is_mouse_over {
- MouseCursor::Pointer
+ if is_mouse_over && !is_disabled {
+ mouse::Interaction::Pointer
} else {
- MouseCursor::OutOfBounds
+ mouse::Interaction::default()
},
)
}
diff --git a/graphics/src/widget/canvas.rs b/graphics/src/widget/canvas.rs
new file mode 100644
index 00000000..95ede50f
--- /dev/null
+++ b/graphics/src/widget/canvas.rs
@@ -0,0 +1,236 @@
+//! Draw 2D graphics for your users.
+//!
+//! A [`Canvas`] widget can be used to draw different kinds of 2D shapes in a
+//! [`Frame`]. It can be used for animation, data visualization, game graphics,
+//! and more!
+use crate::{Backend, Defaults, Primitive, Renderer};
+use iced_native::layout;
+use iced_native::mouse;
+use iced_native::{
+ Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Vector,
+ Widget,
+};
+use std::hash::Hash;
+use std::marker::PhantomData;
+
+pub mod event;
+pub mod path;
+
+mod cache;
+mod cursor;
+mod fill;
+mod frame;
+mod geometry;
+mod program;
+mod stroke;
+mod text;
+
+pub use cache::Cache;
+pub use cursor::Cursor;
+pub use event::Event;
+pub use fill::{Fill, FillRule};
+pub use frame::Frame;
+pub use geometry::Geometry;
+pub use path::Path;
+pub use program::Program;
+pub use stroke::{LineCap, LineJoin, Stroke};
+pub use text::Text;
+
+/// A widget capable of drawing 2D graphics.
+///
+/// # Examples
+/// The repository has a couple of [examples] showcasing how to use a
+/// [`Canvas`]:
+///
+/// - [`clock`], an application that uses the [`Canvas`] widget to draw a clock
+/// and its hands to display the current time.
+/// - [`game_of_life`], an interactive version of the Game of Life, invented by
+/// John Conway.
+/// - [`solar_system`], an animated solar system drawn using the [`Canvas`] widget
+/// and showcasing how to compose different transforms.
+///
+/// [examples]: https://github.com/hecrj/iced/tree/master/examples
+/// [`clock`]: https://github.com/hecrj/iced/tree/master/examples/clock
+/// [`game_of_life`]: https://github.com/hecrj/iced/tree/master/examples/game_of_life
+/// [`solar_system`]: https://github.com/hecrj/iced/tree/master/examples/solar_system
+///
+/// ## Drawing a simple circle
+/// If you want to get a quick overview, here's how we can draw a simple circle:
+///
+/// ```no_run
+/// # mod iced {
+/// # pub use iced_graphics::canvas;
+/// # pub use iced_native::{Color, Rectangle};
+/// # }
+/// use iced::canvas::{self, Canvas, Cursor, Fill, Frame, Geometry, Path, Program};
+/// use iced::{Color, Rectangle};
+///
+/// // First, we define the data we need for drawing
+/// #[derive(Debug)]
+/// struct Circle {
+/// radius: f32,
+/// }
+///
+/// // Then, we implement the `Program` trait
+/// impl Program<()> for Circle {
+/// fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry>{
+/// // We prepare a new `Frame`
+/// let mut frame = Frame::new(bounds.size());
+///
+/// // We create a `Path` representing a simple circle
+/// let circle = Path::circle(frame.center(), self.radius);
+///
+/// // And fill it with some color
+/// frame.fill(&circle, Color::BLACK);
+///
+/// // Finally, we produce the geometry
+/// vec![frame.into_geometry()]
+/// }
+/// }
+///
+/// // Finally, we simply use our `Circle` to create the `Canvas`!
+/// let canvas = Canvas::new(Circle { radius: 50.0 });
+/// ```
+#[derive(Debug)]
+pub struct Canvas<Message, P: Program<Message>> {
+ width: Length,
+ height: Length,
+ program: P,
+ phantom: PhantomData<Message>,
+}
+
+impl<Message, P: Program<Message>> Canvas<Message, P> {
+ const DEFAULT_SIZE: u16 = 100;
+
+ /// Creates a new [`Canvas`].
+ pub fn new(program: P) -> Self {
+ Canvas {
+ width: Length::Units(Self::DEFAULT_SIZE),
+ height: Length::Units(Self::DEFAULT_SIZE),
+ program,
+ phantom: PhantomData,
+ }
+ }
+
+ /// Sets the width of the [`Canvas`].
+ pub fn width(mut self, width: Length) -> Self {
+ self.width = width;
+ self
+ }
+
+ /// Sets the height of the [`Canvas`].
+ pub fn height(mut self, height: Length) -> Self {
+ self.height = height;
+ self
+ }
+}
+
+impl<Message, P, B> Widget<Message, Renderer<B>> for Canvas<Message, P>
+where
+ P: Program<Message>,
+ B: Backend,
+{
+ fn width(&self) -> Length {
+ self.width
+ }
+
+ fn height(&self) -> Length {
+ self.height
+ }
+
+ fn layout(
+ &self,
+ _renderer: &Renderer<B>,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ let limits = limits.width(self.width).height(self.height);
+ let size = limits.resolve(Size::ZERO);
+
+ layout::Node::new(size)
+ }
+
+ fn on_event(
+ &mut self,
+ event: iced_native::Event,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ messages: &mut Vec<Message>,
+ _renderer: &Renderer<B>,
+ _clipboard: Option<&dyn Clipboard>,
+ ) -> event::Status {
+ let bounds = layout.bounds();
+
+ let canvas_event = match event {
+ iced_native::Event::Mouse(mouse_event) => {
+ Some(Event::Mouse(mouse_event))
+ }
+ iced_native::Event::Keyboard(keyboard_event) => {
+ Some(Event::Keyboard(keyboard_event))
+ }
+ _ => None,
+ };
+
+ let cursor = Cursor::from_window_position(cursor_position);
+
+ if let Some(canvas_event) = canvas_event {
+ let (event_status, message) =
+ self.program.update(canvas_event, bounds, cursor);
+
+ if let Some(message) = message {
+ messages.push(message);
+ }
+
+ return event_status;
+ }
+
+ event::Status::Ignored
+ }
+
+ fn draw(
+ &self,
+ _renderer: &mut Renderer<B>,
+ _defaults: &Defaults,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ _viewport: &Rectangle,
+ ) -> (Primitive, mouse::Interaction) {
+ let bounds = layout.bounds();
+ let translation = Vector::new(bounds.x, bounds.y);
+ let cursor = Cursor::from_window_position(cursor_position);
+
+ (
+ Primitive::Translate {
+ translation,
+ content: Box::new(Primitive::Group {
+ primitives: self
+ .program
+ .draw(bounds, cursor)
+ .into_iter()
+ .map(Geometry::into_primitive)
+ .collect(),
+ }),
+ },
+ self.program.mouse_interaction(bounds, cursor),
+ )
+ }
+
+ fn hash_layout(&self, state: &mut Hasher) {
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
+ self.width.hash(state);
+ self.height.hash(state);
+ }
+}
+
+impl<'a, Message, P, B> From<Canvas<Message, P>>
+ for Element<'a, Message, Renderer<B>>
+where
+ Message: 'static,
+ P: Program<Message> + 'a,
+ B: Backend,
+{
+ fn from(canvas: Canvas<Message, P>) -> Element<'a, Message, Renderer<B>> {
+ Element::new(canvas)
+ }
+}
diff --git a/graphics/src/widget/canvas/cache.rs b/graphics/src/widget/canvas/cache.rs
new file mode 100644
index 00000000..a469417d
--- /dev/null
+++ b/graphics/src/widget/canvas/cache.rs
@@ -0,0 +1,98 @@
+use crate::{
+ canvas::{Frame, Geometry},
+ Primitive,
+};
+
+use iced_native::Size;
+use std::{cell::RefCell, sync::Arc};
+
+enum State {
+ Empty,
+ Filled {
+ bounds: Size,
+ primitive: Arc<Primitive>,
+ },
+}
+
+impl Default for State {
+ fn default() -> Self {
+ State::Empty
+ }
+}
+/// A simple cache that stores generated [`Geometry`] to avoid recomputation.
+///
+/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer
+/// change or it is explicitly cleared.
+#[derive(Debug, Default)]
+pub struct Cache {
+ state: RefCell<State>,
+}
+
+impl Cache {
+ /// Creates a new empty [`Cache`].
+ pub fn new() -> Self {
+ Cache {
+ state: Default::default(),
+ }
+ }
+
+ /// Clears the [`Cache`], forcing a redraw the next time it is used.
+ pub fn clear(&mut self) {
+ *self.state.borrow_mut() = State::Empty;
+ }
+
+ /// Draws [`Geometry`] using the provided closure and stores it in the
+ /// [`Cache`].
+ ///
+ /// The closure will only be called when
+ /// - the bounds have changed since the previous draw call.
+ /// - the [`Cache`] is empty or has been explicitly cleared.
+ ///
+ /// Otherwise, the previously stored [`Geometry`] will be returned. The
+ /// [`Cache`] is not cleared in this case. In other words, it will keep
+ /// returning the stored [`Geometry`] if needed.
+ pub fn draw(&self, bounds: Size, draw_fn: impl Fn(&mut Frame)) -> Geometry {
+ use std::ops::Deref;
+
+ if let State::Filled {
+ bounds: cached_bounds,
+ primitive,
+ } = self.state.borrow().deref()
+ {
+ if *cached_bounds == bounds {
+ return Geometry::from_primitive(Primitive::Cached {
+ cache: primitive.clone(),
+ });
+ }
+ }
+
+ let mut frame = Frame::new(bounds);
+ draw_fn(&mut frame);
+
+ let primitive = {
+ let geometry = frame.into_geometry();
+
+ Arc::new(geometry.into_primitive())
+ };
+
+ *self.state.borrow_mut() = State::Filled {
+ bounds,
+ primitive: primitive.clone(),
+ };
+
+ Geometry::from_primitive(Primitive::Cached { cache: primitive })
+ }
+}
+
+impl std::fmt::Debug for State {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ State::Empty => write!(f, "Empty"),
+ State::Filled { primitive, bounds } => f
+ .debug_struct("Filled")
+ .field("primitive", primitive)
+ .field("bounds", bounds)
+ .finish(),
+ }
+ }
+}
diff --git a/graphics/src/widget/canvas/cursor.rs b/graphics/src/widget/canvas/cursor.rs
new file mode 100644
index 00000000..9588d129
--- /dev/null
+++ b/graphics/src/widget/canvas/cursor.rs
@@ -0,0 +1,64 @@
+use iced_native::{Point, Rectangle};
+
+/// The mouse cursor state.
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum Cursor {
+ /// The cursor has a defined position.
+ Available(Point),
+
+ /// The cursor is currently unavailable (i.e. out of bounds or busy).
+ Unavailable,
+}
+
+impl Cursor {
+ // TODO: Remove this once this type is used in `iced_native` to encode
+ // proper cursor availability
+ pub(crate) fn from_window_position(position: Point) -> Self {
+ if position.x < 0.0 || position.y < 0.0 {
+ Cursor::Unavailable
+ } else {
+ Cursor::Available(position)
+ }
+ }
+
+ /// Returns the absolute position of the [`Cursor`], if available.
+ pub fn position(&self) -> Option<Point> {
+ match self {
+ Cursor::Available(position) => Some(*position),
+ Cursor::Unavailable => None,
+ }
+ }
+
+ /// Returns the relative position of the [`Cursor`] inside the given bounds,
+ /// if available.
+ ///
+ /// If the [`Cursor`] is not over the provided bounds, this method will
+ /// return `None`.
+ pub fn position_in(&self, bounds: &Rectangle) -> Option<Point> {
+ if self.is_over(bounds) {
+ self.position_from(bounds.position())
+ } else {
+ None
+ }
+ }
+
+ /// Returns the relative position of the [`Cursor`] from the given origin,
+ /// if available.
+ pub fn position_from(&self, origin: Point) -> Option<Point> {
+ match self {
+ Cursor::Available(position) => {
+ Some(Point::new(position.x - origin.x, position.y - origin.y))
+ }
+ Cursor::Unavailable => None,
+ }
+ }
+
+ /// Returns whether the [`Cursor`] is currently over the provided bounds
+ /// or not.
+ pub fn is_over(&self, bounds: &Rectangle) -> bool {
+ match self {
+ Cursor::Available(position) => bounds.contains(*position),
+ Cursor::Unavailable => false,
+ }
+ }
+}
diff --git a/graphics/src/widget/canvas/event.rs b/graphics/src/widget/canvas/event.rs
new file mode 100644
index 00000000..5bf6f7a6
--- /dev/null
+++ b/graphics/src/widget/canvas/event.rs
@@ -0,0 +1,17 @@
+//! Handle events of a canvas.
+use iced_native::keyboard;
+use iced_native::mouse;
+
+pub use iced_native::event::Status;
+
+/// A [`Canvas`] event.
+///
+/// [`Canvas`]: crate::widget::Canvas
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum Event {
+ /// A mouse event.
+ Mouse(mouse::Event),
+
+ /// A keyboard event.
+ Keyboard(keyboard::Event),
+}
diff --git a/graphics/src/widget/canvas/fill.rs b/graphics/src/widget/canvas/fill.rs
new file mode 100644
index 00000000..56495435
--- /dev/null
+++ b/graphics/src/widget/canvas/fill.rs
@@ -0,0 +1,60 @@
+use iced_native::Color;
+
+/// The style used to fill geometry.
+#[derive(Debug, Clone, Copy)]
+pub struct Fill {
+ /// The color used to fill geometry.
+ ///
+ /// By default, it is set to `BLACK`.
+ pub color: Color,
+
+ /// The fill rule defines how to determine what is inside and what is
+ /// outside of a shape.
+ ///
+ /// See the [SVG specification][1] for more details.
+ ///
+ /// By default, it is set to `NonZero`.
+ ///
+ /// [1]: https://www.w3.org/TR/SVG/painting.html#FillRuleProperty
+ pub rule: FillRule,
+}
+
+impl Default for Fill {
+ fn default() -> Fill {
+ Fill {
+ color: Color::BLACK,
+ rule: FillRule::NonZero,
+ }
+ }
+}
+
+impl From<Color> for Fill {
+ fn from(color: Color) -> Fill {
+ Fill {
+ color,
+ ..Fill::default()
+ }
+ }
+}
+
+/// The fill rule defines how to determine what is inside and what is outside of
+/// a shape.
+///
+/// See the [SVG specification][1].
+///
+/// [1]: https://www.w3.org/TR/SVG/painting.html#FillRuleProperty
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[allow(missing_docs)]
+pub enum FillRule {
+ NonZero,
+ EvenOdd,
+}
+
+impl From<FillRule> for lyon::tessellation::FillRule {
+ fn from(rule: FillRule) -> lyon::tessellation::FillRule {
+ match rule {
+ FillRule::NonZero => lyon::tessellation::FillRule::NonZero,
+ FillRule::EvenOdd => lyon::tessellation::FillRule::EvenOdd,
+ }
+ }
+}
diff --git a/wgpu/src/widget/canvas/frame.rs b/graphics/src/widget/canvas/frame.rs
index 7d7ce06a..b86f9e04 100644
--- a/wgpu/src/widget/canvas/frame.rs
+++ b/graphics/src/widget/canvas/frame.rs
@@ -1,17 +1,16 @@
use iced_native::{Point, Rectangle, Size, Vector};
use crate::{
- canvas::{Fill, Path, Stroke, Text},
+ canvas::{Fill, Geometry, Path, Stroke, Text},
triangle, Primitive,
};
/// The frame of a [`Canvas`].
///
-/// [`Canvas`]: struct.Canvas.html
+/// [`Canvas`]: crate::widget::Canvas
#[derive(Debug)]
pub struct Frame {
- width: f32,
- height: f32,
+ size: Size,
buffers: lyon::tessellation::VertexBuffers<triangle::Vertex2D, u32>,
primitives: Vec<Primitive>,
transforms: Transforms,
@@ -34,12 +33,9 @@ impl Frame {
///
/// The default coordinate system of a [`Frame`] has its origin at the
/// top-left corner of its bounds.
- ///
- /// [`Frame`]: struct.Frame.html
- pub fn new(width: f32, height: f32) -> Frame {
+ pub fn new(size: Size) -> Frame {
Frame {
- width,
- height,
+ size,
buffers: lyon::tessellation::VertexBuffers::new(),
primitives: Vec::new(),
transforms: Transforms {
@@ -53,85 +49,101 @@ impl Frame {
}
/// Returns the width of the [`Frame`].
- ///
- /// [`Frame`]: struct.Frame.html
#[inline]
pub fn width(&self) -> f32 {
- self.width
+ self.size.width
}
/// Returns the width of the [`Frame`].
- ///
- /// [`Frame`]: struct.Frame.html
#[inline]
pub fn height(&self) -> f32 {
- self.height
+ self.size.height
}
/// Returns the dimensions of the [`Frame`].
- ///
- /// [`Frame`]: struct.Frame.html
#[inline]
pub fn size(&self) -> Size {
- Size::new(self.width, self.height)
+ self.size
}
/// Returns the coordinate of the center of the [`Frame`].
- ///
- /// [`Frame`]: struct.Frame.html
#[inline]
pub fn center(&self) -> Point {
- Point::new(self.width / 2.0, self.height / 2.0)
+ Point::new(self.size.width / 2.0, self.size.height / 2.0)
}
/// Draws the given [`Path`] on the [`Frame`] by filling it with the
/// provided style.
- ///
- /// [`Path`]: path/struct.Path.html
- /// [`Frame`]: struct.Frame.html
- pub fn fill(&mut self, path: &Path, fill: Fill) {
+ pub fn fill(&mut self, path: &Path, fill: impl Into<Fill>) {
use lyon::tessellation::{
BuffersBuilder, FillOptions, FillTessellator,
};
+ let Fill { color, rule } = fill.into();
+
let mut buffers = BuffersBuilder::new(
&mut self.buffers,
- FillVertex(match fill {
- Fill::Color(color) => color.into_linear(),
- }),
+ FillVertex(color.into_linear()),
);
let mut tessellator = FillTessellator::new();
+ let options = FillOptions::default().with_fill_rule(rule.into());
let result = if self.transforms.current.is_identity {
- tessellator.tessellate_path(
- path.raw(),
- &FillOptions::default(),
- &mut buffers,
- )
+ tessellator.tessellate_path(path.raw(), &options, &mut buffers)
} else {
let path = path.transformed(&self.transforms.current.raw);
- tessellator.tessellate_path(
- path.raw(),
- &FillOptions::default(),
- &mut buffers,
- )
+ tessellator.tessellate_path(path.raw(), &options, &mut buffers)
};
let _ = result.expect("Tessellate path");
}
+ /// Draws an axis-aligned rectangle given its top-left corner coordinate and
+ /// its `Size` on the [`Frame`] by filling it with the provided style.
+ pub fn fill_rectangle(
+ &mut self,
+ top_left: Point,
+ size: Size,
+ fill: impl Into<Fill>,
+ ) {
+ use lyon::tessellation::{BuffersBuilder, FillOptions};
+
+ let Fill { color, rule } = fill.into();
+
+ let mut buffers = BuffersBuilder::new(
+ &mut self.buffers,
+ FillVertex(color.into_linear()),
+ );
+
+ let top_left =
+ self.transforms.current.raw.transform_point(
+ lyon::math::Point::new(top_left.x, top_left.y),
+ );
+
+ let size =
+ self.transforms.current.raw.transform_vector(
+ lyon::math::Vector::new(size.width, size.height),
+ );
+
+ let _ = lyon::tessellation::basic_shapes::fill_rectangle(
+ &lyon::math::Rect::new(top_left, size.into()),
+ &FillOptions::default().with_fill_rule(rule.into()),
+ &mut buffers,
+ )
+ .expect("Fill rectangle");
+ }
+
/// Draws the stroke of the given [`Path`] on the [`Frame`] with the
/// provided style.
- ///
- /// [`Path`]: path/struct.Path.html
- /// [`Frame`]: struct.Frame.html
- pub fn stroke(&mut self, path: &Path, stroke: Stroke) {
+ pub fn stroke(&mut self, path: &Path, stroke: impl Into<Stroke>) {
use lyon::tessellation::{
BuffersBuilder, StrokeOptions, StrokeTessellator,
};
+ let stroke = stroke.into();
+
let mut buffers = BuffersBuilder::new(
&mut self.buffers,
StrokeVertex(stroke.color.into_linear()),
@@ -170,11 +182,12 @@ impl Frame {
/// Support for vectorial text is planned, and should address all these
/// limitations.
///
- /// [`Text`]: struct.Text.html
- /// [`Frame`]: struct.Frame.html
- pub fn fill_text(&mut self, text: Text) {
+ /// [`Canvas`]: crate::widget::Canvas
+ pub fn fill_text(&mut self, text: impl Into<Text>) {
use std::f32;
+ let text = text.into();
+
let position = if self.transforms.current.is_identity {
text.position
} else {
@@ -207,8 +220,6 @@ impl Frame {
///
/// This method is useful to compose transforms and perform drawing
/// operations in different coordinate systems.
- ///
- /// [`Frame`]: struct.Frame.html
#[inline]
pub fn with_save(&mut self, f: impl FnOnce(&mut Frame)) {
self.transforms.previous.push(self.transforms.current);
@@ -219,8 +230,6 @@ impl Frame {
}
/// Applies a translation to the current transform of the [`Frame`].
- ///
- /// [`Frame`]: struct.Frame.html
#[inline]
pub fn translate(&mut self, translation: Vector) {
self.transforms.current.raw = self
@@ -235,21 +244,17 @@ impl Frame {
}
/// Applies a rotation to the current transform of the [`Frame`].
- ///
- /// [`Frame`]: struct.Frame.html
#[inline]
pub fn rotate(&mut self, angle: f32) {
self.transforms.current.raw = self
.transforms
.current
.raw
- .pre_rotate(lyon::math::Angle::radians(-angle));
+ .pre_rotate(lyon::math::Angle::radians(angle));
self.transforms.current.is_identity = false;
}
/// Applies a scaling to the current transform of the [`Frame`].
- ///
- /// [`Frame`]: struct.Frame.html
#[inline]
pub fn scale(&mut self, scale: f32) {
self.transforms.current.raw =
@@ -257,26 +262,40 @@ impl Frame {
self.transforms.current.is_identity = false;
}
- /// Produces the primitive representing everything drawn on the [`Frame`].
- ///
- /// [`Frame`]: struct.Frame.html
- pub fn into_primitive(mut self) -> Primitive {
- self.primitives.push(Primitive::Mesh2D {
- origin: Point::ORIGIN,
- buffers: triangle::Mesh2D {
- vertices: self.buffers.vertices,
- indices: self.buffers.indices,
- },
- });
+ /// Produces the [`Geometry`] representing everything drawn on the [`Frame`].
+ pub fn into_geometry(mut self) -> Geometry {
+ if !self.buffers.indices.is_empty() {
+ self.primitives.push(Primitive::Mesh2D {
+ buffers: triangle::Mesh2D {
+ vertices: self.buffers.vertices,
+ indices: self.buffers.indices,
+ },
+ size: self.size,
+ });
+ }
- Primitive::Group {
+ Geometry::from_primitive(Primitive::Group {
primitives: self.primitives,
- }
+ })
}
}
struct FillVertex([f32; 4]);
+impl lyon::tessellation::BasicVertexConstructor<triangle::Vertex2D>
+ for FillVertex
+{
+ fn new_vertex(
+ &mut self,
+ position: lyon::math::Point,
+ ) -> triangle::Vertex2D {
+ triangle::Vertex2D {
+ position: [position.x, position.y],
+ color: self.0,
+ }
+ }
+}
+
impl lyon::tessellation::FillVertexConstructor<triangle::Vertex2D>
for FillVertex
{
diff --git a/graphics/src/widget/canvas/geometry.rs b/graphics/src/widget/canvas/geometry.rs
new file mode 100644
index 00000000..8915cda1
--- /dev/null
+++ b/graphics/src/widget/canvas/geometry.rs
@@ -0,0 +1,30 @@
+use crate::Primitive;
+
+/// A bunch of shapes that can be drawn.
+///
+/// [`Geometry`] can be easily generated with a [`Frame`] or stored in a
+/// [`Cache`].
+///
+/// [`Frame`]: crate::widget::canvas::Frame
+/// [`Cache`]: crate::widget::canvas::Cache
+#[derive(Debug, Clone)]
+pub struct Geometry(Primitive);
+
+impl Geometry {
+ pub(crate) fn from_primitive(primitive: Primitive) -> Self {
+ Self(primitive)
+ }
+
+ /// Turns the [`Geometry`] into a [`Primitive`].
+ ///
+ /// This can be useful if you are building a custom widget.
+ pub fn into_primitive(self) -> Primitive {
+ self.0
+ }
+}
+
+impl From<Geometry> for Primitive {
+ fn from(geometry: Geometry) -> Primitive {
+ geometry.0
+ }
+}
diff --git a/wgpu/src/widget/canvas/path.rs b/graphics/src/widget/canvas/path.rs
index 15c2e853..6de19321 100644
--- a/wgpu/src/widget/canvas/path.rs
+++ b/graphics/src/widget/canvas/path.rs
@@ -3,14 +3,15 @@ pub mod arc;
mod builder;
+#[doc(no_inline)]
pub use arc::Arc;
pub use builder::Builder;
+use iced_native::{Point, Size};
+
/// An immutable set of points that may or may not be connected.
///
/// A single [`Path`] can represent different kinds of 2D shapes!
-///
-/// [`Path`]: struct.Path.html
#[derive(Debug, Clone)]
pub struct Path {
raw: lyon::path::Path,
@@ -20,9 +21,6 @@ impl Path {
/// Creates a new [`Path`] with the provided closure.
///
/// Use the [`Builder`] to configure your [`Path`].
- ///
- /// [`Path`]: struct.Path.html
- /// [`Builder`]: struct.Builder.html
pub fn new(f: impl FnOnce(&mut Builder)) -> Self {
let mut builder = Builder::new();
@@ -32,6 +30,27 @@ impl Path {
builder.build()
}
+ /// Creates a new [`Path`] representing a line segment given its starting
+ /// and end points.
+ pub fn line(from: Point, to: Point) -> Self {
+ Self::new(|p| {
+ p.move_to(from);
+ p.line_to(to);
+ })
+ }
+
+ /// Creates a new [`Path`] representing a rectangle given its top-left
+ /// corner coordinate and its `Size`.
+ pub fn rectangle(top_left: Point, size: Size) -> Self {
+ Self::new(|p| p.rectangle(top_left, size))
+ }
+
+ /// Creates a new [`Path`] representing a circle given its center
+ /// coordinate and its radius.
+ pub fn circle(center: Point, radius: f32) -> Self {
+ Self::new(|p| p.circle(center, radius))
+ }
+
#[inline]
pub(crate) fn raw(&self) -> &lyon::path::Path {
&self.raw
diff --git a/wgpu/src/widget/canvas/path/arc.rs b/graphics/src/widget/canvas/path/arc.rs
index 343191f1..b8e72daf 100644
--- a/wgpu/src/widget/canvas/path/arc.rs
+++ b/graphics/src/widget/canvas/path/arc.rs
@@ -15,8 +15,6 @@ pub struct Arc {
}
/// An elliptical [`Arc`].
-///
-/// [`Arc`]: struct.Arc.html
#[derive(Debug, Clone, Copy)]
pub struct Elliptical {
/// The center of the arc.
diff --git a/wgpu/src/widget/canvas/path/builder.rs b/graphics/src/widget/canvas/path/builder.rs
index a013149e..5ce0e02c 100644
--- a/wgpu/src/widget/canvas/path/builder.rs
+++ b/graphics/src/widget/canvas/path/builder.rs
@@ -6,8 +6,6 @@ use lyon::path::builder::{Build, FlatPathBuilder, PathBuilder, SvgBuilder};
/// A [`Path`] builder.
///
/// Once a [`Path`] is built, it can no longer be mutated.
-///
-/// [`Path`]: struct.Path.html
#[allow(missing_debug_implementations)]
pub struct Builder {
raw: lyon::path::builder::SvgPathBuilder<lyon::path::Builder>,
@@ -15,8 +13,6 @@ pub struct Builder {
impl Builder {
/// Creates a new [`Builder`].
- ///
- /// [`Builder`]: struct.Builder.html
pub fn new() -> Builder {
Builder {
raw: lyon::path::Path::builder().with_svg(),
@@ -31,8 +27,6 @@ impl Builder {
/// Connects the last point in the [`Path`] to the given `Point` with a
/// straight line.
- ///
- /// [`Path`]: struct.Path.html
#[inline]
pub fn line_to(&mut self, point: Point) {
let _ = self.raw.line_to(lyon::math::Point::new(point.x, point.y));
@@ -40,9 +34,6 @@ impl Builder {
/// Adds an [`Arc`] to the [`Path`] from `start_angle` to `end_angle` in
/// a clockwise direction.
- ///
- /// [`Arc`]: struct.Arc.html
- /// [`Path`]: struct.Path.html
#[inline]
pub fn arc(&mut self, arc: Arc) {
self.ellipse(arc.into());
@@ -53,8 +44,6 @@ impl Builder {
///
/// The arc is connected to the previous point by a straight line, if
/// necessary.
- ///
- /// [`Path`]: struct.Path.html
pub fn arc_to(&mut self, a: Point, b: Point, radius: f32) {
use lyon::{math, path};
@@ -72,10 +61,7 @@ impl Builder {
);
}
- /// Adds an [`Ellipse`] to the [`Path`] using a clockwise direction.
- ///
- /// [`Ellipse`]: struct.Arc.html
- /// [`Path`]: struct.Path.html
+ /// Adds an ellipse to the [`Path`] using a clockwise direction.
pub fn ellipse(&mut self, arc: arc::Elliptical) {
use lyon::{geom, math};
@@ -84,7 +70,7 @@ impl Builder {
radii: math::Vector::new(arc.radii.x, arc.radii.y),
x_rotation: math::Angle::radians(arc.rotation),
start_angle: math::Angle::radians(arc.start_angle),
- sweep_angle: math::Angle::radians(arc.end_angle),
+ sweep_angle: math::Angle::radians(arc.end_angle - arc.start_angle),
};
let _ = self.raw.move_to(arc.sample(0.0));
@@ -96,8 +82,6 @@ impl Builder {
/// Adds a cubic Bézier curve to the [`Path`] given its two control points
/// and its end point.
- ///
- /// [`Path`]: struct.Path.html
#[inline]
pub fn bezier_curve_to(
&mut self,
@@ -116,8 +100,6 @@ impl Builder {
/// Adds a quadratic Bézier curve to the [`Path`] given its control point
/// and its end point.
- ///
- /// [`Path`]: struct.Path.html
#[inline]
pub fn quadratic_curve_to(&mut self, control: Point, to: Point) {
use lyon::math;
@@ -130,21 +112,20 @@ impl Builder {
/// Adds a rectangle to the [`Path`] given its top-left corner coordinate
/// and its `Size`.
- ///
- /// [`Path`]: struct.Path.html
#[inline]
- pub fn rectangle(&mut self, p: Point, size: Size) {
- self.move_to(p);
- self.line_to(Point::new(p.x + size.width, p.y));
- self.line_to(Point::new(p.x + size.width, p.y + size.height));
- self.line_to(Point::new(p.x, p.y + size.height));
+ pub fn rectangle(&mut self, top_left: Point, size: Size) {
+ self.move_to(top_left);
+ self.line_to(Point::new(top_left.x + size.width, top_left.y));
+ self.line_to(Point::new(
+ top_left.x + size.width,
+ top_left.y + size.height,
+ ));
+ self.line_to(Point::new(top_left.x, top_left.y + size.height));
self.close();
}
/// Adds a circle to the [`Path`] given its center coordinate and its
/// radius.
- ///
- /// [`Path`]: struct.Path.html
#[inline]
pub fn circle(&mut self, center: Point, radius: f32) {
self.arc(Arc {
@@ -157,17 +138,12 @@ impl Builder {
/// Closes the current sub-path in the [`Path`] with a straight line to
/// the starting point.
- ///
- /// [`Path`]: struct.Path.html
#[inline]
pub fn close(&mut self) {
self.raw.close()
}
/// Builds the [`Path`] of this [`Builder`].
- ///
- /// [`Path`]: struct.Path.html
- /// [`Builder`]: struct.Builder.html
#[inline]
pub fn build(self) -> Path {
Path {
diff --git a/graphics/src/widget/canvas/program.rs b/graphics/src/widget/canvas/program.rs
new file mode 100644
index 00000000..d703caad
--- /dev/null
+++ b/graphics/src/widget/canvas/program.rs
@@ -0,0 +1,80 @@
+use crate::canvas::event::{self, Event};
+use crate::canvas::{Cursor, Geometry};
+use iced_native::{mouse, Rectangle};
+
+/// The state and logic of a [`Canvas`].
+///
+/// A [`Program`] can mutate internal state and produce messages for an
+/// application.
+///
+/// [`Canvas`]: crate::widget::Canvas
+pub trait Program<Message> {
+ /// Updates the state of the [`Program`].
+ ///
+ /// When a [`Program`] is used in a [`Canvas`], the runtime will call this
+ /// method for each [`Event`].
+ ///
+ /// This method can optionally return a `Message` to notify an application
+ /// of any meaningful interactions.
+ ///
+ /// By default, this method does and returns nothing.
+ ///
+ /// [`Canvas`]: crate::widget::Canvas
+ fn update(
+ &mut self,
+ _event: Event,
+ _bounds: Rectangle,
+ _cursor: Cursor,
+ ) -> (event::Status, Option<Message>) {
+ (event::Status::Ignored, None)
+ }
+
+ /// Draws the state of the [`Program`], producing a bunch of [`Geometry`].
+ ///
+ /// [`Geometry`] can be easily generated with a [`Frame`] or stored in a
+ /// [`Cache`].
+ ///
+ /// [`Frame`]: crate::widget::canvas::Cache
+ /// [`Cache`]: crate::widget::canvas::Cache
+ fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec<Geometry>;
+
+ /// Returns the current mouse interaction of the [`Program`].
+ ///
+ /// The interaction returned will be in effect even if the cursor position
+ /// is out of bounds of the program's [`Canvas`].
+ ///
+ /// [`Canvas`]: crate::widget::Canvas
+ fn mouse_interaction(
+ &self,
+ _bounds: Rectangle,
+ _cursor: Cursor,
+ ) -> mouse::Interaction {
+ mouse::Interaction::default()
+ }
+}
+
+impl<T, Message> Program<Message> for &mut T
+where
+ T: Program<Message>,
+{
+ fn update(
+ &mut self,
+ event: Event,
+ bounds: Rectangle,
+ cursor: Cursor,
+ ) -> (event::Status, Option<Message>) {
+ T::update(self, event, bounds, cursor)
+ }
+
+ fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Vec<Geometry> {
+ T::draw(self, bounds, cursor)
+ }
+
+ fn mouse_interaction(
+ &self,
+ bounds: Rectangle,
+ cursor: Cursor,
+ ) -> mouse::Interaction {
+ T::mouse_interaction(self, bounds, cursor)
+ }
+}
diff --git a/wgpu/src/widget/canvas/stroke.rs b/graphics/src/widget/canvas/stroke.rs
index 46d669c4..9f0449d0 100644
--- a/wgpu/src/widget/canvas/stroke.rs
+++ b/graphics/src/widget/canvas/stroke.rs
@@ -14,6 +14,28 @@ pub struct Stroke {
pub line_join: LineJoin,
}
+impl Stroke {
+ /// Sets the color of the [`Stroke`].
+ pub fn with_color(self, color: Color) -> Stroke {
+ Stroke { color, ..self }
+ }
+
+ /// Sets the width of the [`Stroke`].
+ pub fn with_width(self, width: f32) -> Stroke {
+ Stroke { width, ..self }
+ }
+
+ /// Sets the [`LineCap`] of the [`Stroke`].
+ pub fn with_line_cap(self, line_cap: LineCap) -> Stroke {
+ Stroke { line_cap, ..self }
+ }
+
+ /// Sets the [`LineJoin`] of the [`Stroke`].
+ pub fn with_line_join(self, line_join: LineJoin) -> Stroke {
+ Stroke { line_join, ..self }
+ }
+}
+
impl Default for Stroke {
fn default() -> Stroke {
Stroke {
diff --git a/wgpu/src/widget/canvas/text.rs b/graphics/src/widget/canvas/text.rs
index d1cf1a0f..c4cae30e 100644
--- a/wgpu/src/widget/canvas/text.rs
+++ b/graphics/src/widget/canvas/text.rs
@@ -32,3 +32,18 @@ impl Default for Text {
}
}
}
+
+impl From<String> for Text {
+ fn from(content: String) -> Text {
+ Text {
+ content,
+ ..Default::default()
+ }
+ }
+}
+
+impl From<&str> for Text {
+ fn from(content: &str) -> Text {
+ String::from(content).into()
+ }
+}
diff --git a/wgpu/src/renderer/widget/checkbox.rs b/graphics/src/widget/checkbox.rs
index 1a0585d3..cb7fd2cf 100644
--- a/wgpu/src/renderer/widget/checkbox.rs
+++ b/graphics/src/widget/checkbox.rs
@@ -1,9 +1,22 @@
-use crate::{checkbox::StyleSheet, Primitive, Renderer};
-use iced_native::{
- checkbox, HorizontalAlignment, MouseCursor, Rectangle, VerticalAlignment,
-};
+//! Show toggle controls using checkboxes.
+use crate::backend::{self, Backend};
+use crate::{Primitive, Renderer};
+use iced_native::checkbox;
+use iced_native::mouse;
+use iced_native::{HorizontalAlignment, Rectangle, VerticalAlignment};
-impl checkbox::Renderer for Renderer {
+pub use iced_style::checkbox::{Style, StyleSheet};
+
+/// A box that can be checked.
+///
+/// This is an alias of an `iced_native` checkbox with an `iced_wgpu::Renderer`.
+pub type Checkbox<Message, Backend> =
+ iced_native::Checkbox<Message, Renderer<Backend>>;
+
+impl<B> checkbox::Renderer for Renderer<B>
+where
+ B: Backend + backend::Text,
+{
type Style = Box<dyn StyleSheet>;
const DEFAULT_SIZE: u16 = 20;
@@ -35,10 +48,14 @@ impl checkbox::Renderer for Renderer {
Primitive::Group {
primitives: if is_checked {
let check = Primitive::Text {
- content: crate::text::CHECKMARK_ICON.to_string(),
- font: crate::text::BUILTIN_ICONS,
+ content: B::CHECKMARK_ICON.to_string(),
+ font: B::ICON_FONT,
size: bounds.height * 0.7,
- bounds: bounds,
+ bounds: Rectangle {
+ x: bounds.center_x(),
+ y: bounds.center_y(),
+ ..bounds
+ },
color: style.checkmark_color,
horizontal_alignment: HorizontalAlignment::Center,
vertical_alignment: VerticalAlignment::Center,
@@ -50,9 +67,9 @@ impl checkbox::Renderer for Renderer {
},
},
if is_mouse_over {
- MouseCursor::Pointer
+ mouse::Interaction::Pointer
} else {
- MouseCursor::OutOfBounds
+ mouse::Interaction::default()
},
)
}
diff --git a/graphics/src/widget/column.rs b/graphics/src/widget/column.rs
new file mode 100644
index 00000000..0cf56842
--- /dev/null
+++ b/graphics/src/widget/column.rs
@@ -0,0 +1,49 @@
+use crate::{Backend, Primitive, Renderer};
+use iced_native::column;
+use iced_native::mouse;
+use iced_native::{Element, Layout, Point, Rectangle};
+
+/// A container that distributes its contents vertically.
+pub type Column<'a, Message, Backend> =
+ iced_native::Column<'a, Message, Renderer<Backend>>;
+
+impl<B> column::Renderer for Renderer<B>
+where
+ B: Backend,
+{
+ fn draw<Message>(
+ &mut self,
+ defaults: &Self::Defaults,
+ content: &[Element<'_, Message, Self>],
+ layout: Layout<'_>,
+ cursor_position: Point,
+ viewport: &Rectangle,
+ ) -> Self::Output {
+ let mut mouse_interaction = mouse::Interaction::default();
+
+ (
+ Primitive::Group {
+ primitives: content
+ .iter()
+ .zip(layout.children())
+ .map(|(child, layout)| {
+ let (primitive, new_mouse_interaction) = child.draw(
+ self,
+ defaults,
+ layout,
+ cursor_position,
+ viewport,
+ );
+
+ if new_mouse_interaction > mouse_interaction {
+ mouse_interaction = new_mouse_interaction;
+ }
+
+ primitive
+ })
+ .collect(),
+ },
+ mouse_interaction,
+ )
+ }
+}
diff --git a/graphics/src/widget/container.rs b/graphics/src/widget/container.rs
new file mode 100644
index 00000000..aae3e1d8
--- /dev/null
+++ b/graphics/src/widget/container.rs
@@ -0,0 +1,78 @@
+//! Decorate content and apply alignment.
+use crate::container;
+use crate::defaults::{self, Defaults};
+use crate::{Backend, Primitive, Renderer};
+use iced_native::{Background, Color, Element, Layout, Point, Rectangle};
+
+pub use iced_style::container::{Style, StyleSheet};
+
+/// An element decorating some content.
+///
+/// This is an alias of an `iced_native` container with a default
+/// `Renderer`.
+pub type Container<'a, Message, Backend> =
+ iced_native::Container<'a, Message, Renderer<Backend>>;
+
+impl<B> iced_native::container::Renderer for Renderer<B>
+where
+ B: Backend,
+{
+ type Style = Box<dyn container::StyleSheet>;
+
+ fn draw<Message>(
+ &mut self,
+ defaults: &Defaults,
+ bounds: Rectangle,
+ cursor_position: Point,
+ viewport: &Rectangle,
+ style_sheet: &Self::Style,
+ content: &Element<'_, Message, Self>,
+ content_layout: Layout<'_>,
+ ) -> Self::Output {
+ let style = style_sheet.style();
+
+ let defaults = Defaults {
+ text: defaults::Text {
+ color: style.text_color.unwrap_or(defaults.text.color),
+ },
+ };
+
+ let (content, mouse_interaction) = content.draw(
+ self,
+ &defaults,
+ content_layout,
+ cursor_position,
+ viewport,
+ );
+
+ if let Some(background) = background(bounds, &style) {
+ (
+ Primitive::Group {
+ primitives: vec![background, content],
+ },
+ mouse_interaction,
+ )
+ } else {
+ (content, mouse_interaction)
+ }
+ }
+}
+
+pub(crate) fn background(
+ bounds: Rectangle,
+ style: &container::Style,
+) -> Option<Primitive> {
+ if style.background.is_some() || style.border_width > 0.0 {
+ Some(Primitive::Quad {
+ bounds,
+ background: style
+ .background
+ .unwrap_or(Background::Color(Color::TRANSPARENT)),
+ border_radius: style.border_radius,
+ border_width: style.border_width,
+ border_color: style.border_color,
+ })
+ } else {
+ None
+ }
+}
diff --git a/graphics/src/widget/image.rs b/graphics/src/widget/image.rs
new file mode 100644
index 00000000..30f446e8
--- /dev/null
+++ b/graphics/src/widget/image.rs
@@ -0,0 +1,31 @@
+//! Display images in your user interface.
+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};
+
+impl<B> image::Renderer for Renderer<B>
+where
+ B: Backend + backend::Image,
+{
+ fn dimensions(&self, handle: &image::Handle) -> (u32, u32) {
+ self.backend().dimensions(handle)
+ }
+
+ fn draw(
+ &mut self,
+ handle: image::Handle,
+ layout: Layout<'_>,
+ ) -> Self::Output {
+ (
+ Primitive::Image {
+ handle,
+ bounds: layout.bounds(),
+ },
+ mouse::Interaction::default(),
+ )
+ }
+}
diff --git a/graphics/src/widget/pane_grid.rs b/graphics/src/widget/pane_grid.rs
new file mode 100644
index 00000000..f09984fc
--- /dev/null
+++ b/graphics/src/widget/pane_grid.rs
@@ -0,0 +1,252 @@
+//! Let your users split regions of your application and organize layout dynamically.
+//!
+//! [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](https://gfycat.com/mixedflatjellyfish)
+//!
+//! # Example
+//! 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};
+use crate::defaults;
+use crate::{Primitive, Renderer};
+use iced_native::mouse;
+use iced_native::pane_grid;
+use iced_native::text;
+use iced_native::{
+ Element, HorizontalAlignment, Layout, Point, Rectangle, Vector,
+ VerticalAlignment,
+};
+
+pub use iced_native::pane_grid::{
+ Axis, Configuration, Content, Direction, DragEvent, Pane, ResizeEvent,
+ Split, State, TitleBar,
+};
+
+/// A collection of panes distributed using either vertical or horizontal splits
+/// to completely fill the space available.
+///
+/// [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](https://gfycat.com/mixedflatjellyfish)
+///
+/// This is an alias of an `iced_native` pane grid with an `iced_wgpu::Renderer`.
+pub type PaneGrid<'a, Message, Backend> =
+ iced_native::PaneGrid<'a, Message, Renderer<Backend>>;
+
+impl<B> pane_grid::Renderer for Renderer<B>
+where
+ B: Backend + backend::Text,
+{
+ fn draw<Message>(
+ &mut self,
+ defaults: &Self::Defaults,
+ content: &[(Pane, Content<'_, Message, Self>)],
+ dragging: Option<(Pane, Point)>,
+ resizing: Option<Axis>,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ ) -> Self::Output {
+ let pane_cursor_position = if dragging.is_some() {
+ // TODO: Remove once cursor availability is encoded in the type
+ // system
+ Point::new(-1.0, -1.0)
+ } else {
+ cursor_position
+ };
+
+ let mut mouse_interaction = mouse::Interaction::default();
+ let mut dragged_pane = None;
+
+ let mut panes: Vec<_> = content
+ .iter()
+ .zip(layout.children())
+ .enumerate()
+ .map(|(i, ((id, pane), layout))| {
+ let (primitive, new_mouse_interaction) =
+ pane.draw(self, defaults, layout, pane_cursor_position);
+
+ if new_mouse_interaction > mouse_interaction {
+ mouse_interaction = new_mouse_interaction;
+ }
+
+ if let Some((dragging, origin)) = dragging {
+ if *id == dragging {
+ dragged_pane = Some((i, layout, origin));
+ }
+ }
+
+ primitive
+ })
+ .collect();
+
+ let primitives = if let Some((index, layout, origin)) = dragged_pane {
+ let pane = panes.remove(index);
+ let bounds = layout.bounds();
+
+ // TODO: Fix once proper layering is implemented.
+ // This is a pretty hacky way to achieve layering.
+ let clip = Primitive::Clip {
+ bounds: Rectangle {
+ x: cursor_position.x - origin.x,
+ y: cursor_position.y - origin.y,
+ width: bounds.width + 0.5,
+ height: bounds.height + 0.5,
+ },
+ offset: Vector::new(0, 0),
+ content: Box::new(Primitive::Translate {
+ translation: Vector::new(
+ cursor_position.x - bounds.x - origin.x,
+ cursor_position.y - bounds.y - origin.y,
+ ),
+ content: Box::new(pane),
+ }),
+ };
+
+ panes.push(clip);
+
+ panes
+ } else {
+ panes
+ };
+
+ (
+ 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
+ },
+ )
+ }
+
+ fn draw_pane<Message>(
+ &mut self,
+ defaults: &Self::Defaults,
+ bounds: Rectangle,
+ style_sheet: &Self::Style,
+ title_bar: Option<(&TitleBar<'_, Message, Self>, Layout<'_>)>,
+ body: (&Element<'_, Message, Self>, Layout<'_>),
+ cursor_position: Point,
+ ) -> 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);
+
+ let background = crate::widget::container::background(bounds, &style);
+
+ if let Some((title_bar, title_bar_layout)) = title_bar {
+ let show_controls = bounds.contains(cursor_position);
+ let is_over_pick_area =
+ title_bar.is_over_pick_area(title_bar_layout, cursor_position);
+
+ let (title_bar_primitive, title_bar_interaction) = title_bar.draw(
+ self,
+ defaults,
+ title_bar_layout,
+ cursor_position,
+ show_controls,
+ );
+
+ (
+ Primitive::Group {
+ primitives: vec![
+ background.unwrap_or(Primitive::None),
+ title_bar_primitive,
+ body_primitive,
+ ],
+ },
+ if is_over_pick_area {
+ mouse::Interaction::Grab
+ } else if title_bar_interaction > body_interaction {
+ title_bar_interaction
+ } else {
+ body_interaction
+ },
+ )
+ } else {
+ (
+ if let Some(background) = background {
+ Primitive::Group {
+ primitives: vec![background, body_primitive],
+ }
+ } else {
+ body_primitive
+ },
+ body_interaction,
+ )
+ }
+ }
+
+ fn draw_title_bar<Message>(
+ &mut self,
+ defaults: &Self::Defaults,
+ bounds: Rectangle,
+ style_sheet: &Self::Style,
+ title: &str,
+ title_size: u16,
+ title_font: Self::Font,
+ title_bounds: Rectangle,
+ controls: Option<(&Element<'_, Message, Self>, Layout<'_>)>,
+ cursor_position: Point,
+ ) -> Self::Output {
+ let style = style_sheet.style();
+
+ let defaults = Self::Defaults {
+ text: defaults::Text {
+ color: style.text_color.unwrap_or(defaults.text.color),
+ },
+ };
+
+ let background = crate::widget::container::background(bounds, &style);
+
+ let (title_primitive, _) = text::Renderer::draw(
+ self,
+ &defaults,
+ title_bounds,
+ title,
+ title_size,
+ title_font,
+ None,
+ HorizontalAlignment::Left,
+ VerticalAlignment::Top,
+ );
+
+ if let Some((controls, controls_layout)) = controls {
+ let (controls_primitive, controls_interaction) = controls.draw(
+ self,
+ &defaults,
+ controls_layout,
+ cursor_position,
+ &bounds,
+ );
+
+ (
+ Primitive::Group {
+ primitives: vec![
+ background.unwrap_or(Primitive::None),
+ title_primitive,
+ controls_primitive,
+ ],
+ },
+ controls_interaction,
+ )
+ } else {
+ (
+ if let Some(background) = background {
+ Primitive::Group {
+ primitives: vec![background, title_primitive],
+ }
+ } else {
+ title_primitive
+ },
+ mouse::Interaction::default(),
+ )
+ }
+ }
+}
diff --git a/graphics/src/widget/pick_list.rs b/graphics/src/widget/pick_list.rs
new file mode 100644
index 00000000..f42a8707
--- /dev/null
+++ b/graphics/src/widget/pick_list.rs
@@ -0,0 +1,97 @@
+//! Display a dropdown list of selectable values.
+use crate::backend::{self, Backend};
+use crate::{Primitive, Renderer};
+use iced_native::{
+ mouse, Font, HorizontalAlignment, Point, Rectangle, VerticalAlignment,
+};
+use iced_style::menu;
+
+pub use iced_native::pick_list::State;
+pub use iced_style::pick_list::{Style, StyleSheet};
+
+/// A widget allowing the selection of a single value from a list of options.
+pub type PickList<'a, T, Message, Backend> =
+ iced_native::PickList<'a, T, Message, Renderer<Backend>>;
+
+impl<B> iced_native::pick_list::Renderer for Renderer<B>
+where
+ B: Backend + backend::Text,
+{
+ type Style = Box<dyn StyleSheet>;
+
+ const DEFAULT_PADDING: u16 = 5;
+
+ fn menu_style(style: &Box<dyn StyleSheet>) -> menu::Style {
+ style.menu()
+ }
+
+ fn draw(
+ &mut self,
+ bounds: Rectangle,
+ cursor_position: Point,
+ selected: Option<String>,
+ padding: u16,
+ text_size: u16,
+ font: Font,
+ style: &Box<dyn StyleSheet>,
+ ) -> Self::Output {
+ let is_mouse_over = bounds.contains(cursor_position);
+
+ let style = if is_mouse_over {
+ style.hovered()
+ } else {
+ style.active()
+ };
+
+ let background = Primitive::Quad {
+ bounds,
+ background: style.background,
+ border_color: style.border_color,
+ border_width: style.border_width,
+ border_radius: style.border_radius,
+ };
+
+ let arrow_down = Primitive::Text {
+ content: B::ARROW_DOWN_ICON.to_string(),
+ font: B::ICON_FONT,
+ size: bounds.height * style.icon_size,
+ bounds: Rectangle {
+ x: bounds.x + bounds.width - f32::from(padding) * 2.0,
+ y: bounds.center_y(),
+ ..bounds
+ },
+ color: style.text_color,
+ horizontal_alignment: HorizontalAlignment::Right,
+ vertical_alignment: VerticalAlignment::Center,
+ };
+
+ (
+ Primitive::Group {
+ primitives: if let Some(label) = selected {
+ let label = Primitive::Text {
+ content: label,
+ size: f32::from(text_size),
+ font,
+ color: style.text_color,
+ bounds: Rectangle {
+ x: bounds.x + f32::from(padding),
+ y: bounds.center_y(),
+ ..bounds
+ },
+ horizontal_alignment: HorizontalAlignment::Left,
+ vertical_alignment: VerticalAlignment::Center,
+ };
+
+ vec![background, label, arrow_down]
+ } else {
+ vec![background, arrow_down]
+ },
+ },
+ if is_mouse_over {
+ mouse::Interaction::Pointer
+ } else {
+ mouse::Interaction::default()
+ },
+ )
+ }
+}
diff --git a/wgpu/src/renderer/widget/progress_bar.rs b/graphics/src/widget/progress_bar.rs
index 34e33276..32ee42c6 100644
--- a/wgpu/src/renderer/widget/progress_bar.rs
+++ b/graphics/src/widget/progress_bar.rs
@@ -1,7 +1,24 @@
-use crate::{progress_bar::StyleSheet, Primitive, Renderer};
-use iced_native::{progress_bar, Color, MouseCursor, Rectangle};
+//! Allow your users to visually track the progress of a computation.
+//!
+//! A [`ProgressBar`] has a range of possible values and a current value,
+//! as well as a length, height and style.
+use crate::{Backend, Primitive, Renderer};
+use iced_native::mouse;
+use iced_native::progress_bar;
+use iced_native::{Color, Rectangle};
-impl progress_bar::Renderer for Renderer {
+pub use iced_style::progress_bar::{Style, StyleSheet};
+
+/// A bar that displays progress.
+///
+/// This is an alias of an `iced_native` progress bar with an
+/// `iced_wgpu::Renderer`.
+pub type ProgressBar<Backend> = iced_native::ProgressBar<Renderer<Backend>>;
+
+impl<B> progress_bar::Renderer for Renderer<B>
+where
+ B: Backend,
+{
type Style = Box<dyn StyleSheet>;
const DEFAULT_HEIGHT: u16 = 30;
@@ -14,17 +31,20 @@ impl progress_bar::Renderer for Renderer {
style_sheet: &Self::Style,
) -> Self::Output {
let style = style_sheet.style();
-
let (range_start, range_end) = range.into_inner();
- let active_progress_width = bounds.width
- * ((value - range_start) / (range_end - range_start).max(1.0));
+
+ let active_progress_width = if range_start >= range_end {
+ 0.0
+ } else {
+ bounds.width * (value - range_start) / (range_end - range_start)
+ };
let background = Primitive::Group {
primitives: vec![Primitive::Quad {
bounds: Rectangle { ..bounds },
background: style.background,
border_radius: style.border_radius,
- border_width: 0,
+ border_width: 0.0,
border_color: Color::TRANSPARENT,
}],
};
@@ -38,7 +58,7 @@ impl progress_bar::Renderer for Renderer {
},
background: style.bar,
border_radius: style.border_radius,
- border_width: 0,
+ border_width: 0.0,
border_color: Color::TRANSPARENT,
};
@@ -48,7 +68,7 @@ impl progress_bar::Renderer for Renderer {
} else {
background
},
- MouseCursor::OutOfBounds,
+ mouse::Interaction::default(),
)
}
}
diff --git a/graphics/src/widget/qr_code.rs b/graphics/src/widget/qr_code.rs
new file mode 100644
index 00000000..b3a01dd7
--- /dev/null
+++ b/graphics/src/widget/qr_code.rs
@@ -0,0 +1,305 @@
+//! Encode and display information in a QR code.
+use crate::canvas;
+use crate::{Backend, Defaults, Primitive, Renderer, Vector};
+
+use iced_native::{
+ layout, mouse, Color, Element, Hasher, Layout, Length, Point, Rectangle,
+ Size, Widget,
+};
+use thiserror::Error;
+
+const DEFAULT_CELL_SIZE: u16 = 4;
+const QUIET_ZONE: usize = 2;
+
+/// A type of matrix barcode consisting of squares arranged in a grid which
+/// can be read by an imaging device, such as a camera.
+#[derive(Debug)]
+pub struct QRCode<'a> {
+ state: &'a State,
+ dark: Color,
+ light: Color,
+ cell_size: u16,
+}
+
+impl<'a> QRCode<'a> {
+ /// Creates a new [`QRCode`] with the provided [`State`].
+ pub fn new(state: &'a State) -> Self {
+ Self {
+ cell_size: DEFAULT_CELL_SIZE,
+ dark: Color::BLACK,
+ light: Color::WHITE,
+ state,
+ }
+ }
+
+ /// Sets both the dark and light [`Color`]s of the [`QRCode`].
+ pub fn color(mut self, dark: Color, light: Color) -> Self {
+ self.dark = dark;
+ self.light = light;
+ self
+ }
+
+ /// Sets the size of the squares of the grid cell of the [`QRCode`].
+ pub fn cell_size(mut self, cell_size: u16) -> Self {
+ self.cell_size = cell_size;
+ self
+ }
+}
+
+impl<'a, Message, B> Widget<Message, Renderer<B>> for QRCode<'a>
+where
+ B: Backend,
+{
+ fn width(&self) -> Length {
+ Length::Shrink
+ }
+
+ fn height(&self) -> Length {
+ Length::Shrink
+ }
+
+ fn layout(
+ &self,
+ _renderer: &Renderer<B>,
+ _limits: &layout::Limits,
+ ) -> layout::Node {
+ let side_length = (self.state.width + 2 * QUIET_ZONE) as f32
+ * f32::from(self.cell_size);
+
+ layout::Node::new(Size::new(
+ f32::from(side_length),
+ f32::from(side_length),
+ ))
+ }
+
+ fn hash_layout(&self, state: &mut Hasher) {
+ use std::hash::Hash;
+
+ self.state.contents.hash(state);
+ }
+
+ fn draw(
+ &self,
+ _renderer: &mut Renderer<B>,
+ _defaults: &Defaults,
+ layout: Layout<'_>,
+ _cursor_position: Point,
+ _viewport: &Rectangle,
+ ) -> (Primitive, mouse::Interaction) {
+ let bounds = layout.bounds();
+ let side_length = self.state.width + 2 * QUIET_ZONE;
+
+ // Reuse cache if possible
+ let geometry = self.state.cache.draw(bounds.size(), |frame| {
+ // Scale units to cell size
+ frame.scale(f32::from(self.cell_size));
+
+ // Draw background
+ frame.fill_rectangle(
+ Point::ORIGIN,
+ Size::new(side_length as f32, side_length as f32),
+ self.light,
+ );
+
+ // Avoid drawing on the quiet zone
+ frame.translate(Vector::new(QUIET_ZONE as f32, QUIET_ZONE as f32));
+
+ // Draw contents
+ self.state
+ .contents
+ .iter()
+ .enumerate()
+ .filter(|(_, value)| **value == qrcode::Color::Dark)
+ .for_each(|(index, _)| {
+ let row = index / self.state.width;
+ let column = index % self.state.width;
+
+ frame.fill_rectangle(
+ Point::new(column as f32, row as f32),
+ Size::UNIT,
+ self.dark,
+ );
+ });
+ });
+
+ (
+ Primitive::Translate {
+ translation: Vector::new(bounds.x, bounds.y),
+ content: Box::new(geometry.into_primitive()),
+ },
+ mouse::Interaction::default(),
+ )
+ }
+}
+
+impl<'a, Message, B> Into<Element<'a, Message, Renderer<B>>> for QRCode<'a>
+where
+ B: Backend,
+{
+ fn into(self) -> Element<'a, Message, Renderer<B>> {
+ Element::new(self)
+ }
+}
+
+/// The state of a [`QRCode`].
+///
+/// It stores the data that will be displayed.
+#[derive(Debug)]
+pub struct State {
+ contents: Vec<qrcode::Color>,
+ width: usize,
+ cache: canvas::Cache,
+}
+
+impl State {
+ /// Creates a new [`State`] with the provided data.
+ ///
+ /// This method uses an [`ErrorCorrection::Medium`] and chooses the smallest
+ /// size to display the data.
+ pub fn new(data: impl AsRef<[u8]>) -> Result<Self, Error> {
+ let encoded = qrcode::QrCode::new(data)?;
+
+ Ok(Self::build(encoded))
+ }
+
+ /// Creates a new [`State`] with the provided [`ErrorCorrection`].
+ pub fn with_error_correction(
+ data: impl AsRef<[u8]>,
+ error_correction: ErrorCorrection,
+ ) -> Result<Self, Error> {
+ let encoded = qrcode::QrCode::with_error_correction_level(
+ data,
+ error_correction.into(),
+ )?;
+
+ Ok(Self::build(encoded))
+ }
+
+ /// Creates a new [`State`] with the provided [`Version`] and
+ /// [`ErrorCorrection`].
+ pub fn with_version(
+ data: impl AsRef<[u8]>,
+ version: Version,
+ error_correction: ErrorCorrection,
+ ) -> Result<Self, Error> {
+ let encoded = qrcode::QrCode::with_version(
+ data,
+ version.into(),
+ error_correction.into(),
+ )?;
+
+ Ok(Self::build(encoded))
+ }
+
+ fn build(encoded: qrcode::QrCode) -> Self {
+ let width = encoded.width();
+ let contents = encoded.into_colors();
+
+ Self {
+ contents,
+ width,
+ cache: canvas::Cache::new(),
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+/// The size of a [`QRCode`].
+///
+/// The higher the version the larger the grid of cells, and therefore the more
+/// information the [`QRCode`] can carry.
+pub enum Version {
+ /// A normal QR code version. It should be between 1 and 40.
+ Normal(u8),
+
+ /// A micro QR code version. It should be between 1 and 4.
+ Micro(u8),
+}
+
+impl From<Version> for qrcode::Version {
+ fn from(version: Version) -> Self {
+ match version {
+ Version::Normal(v) => qrcode::Version::Normal(i16::from(v)),
+ Version::Micro(v) => qrcode::Version::Micro(i16::from(v)),
+ }
+ }
+}
+
+/// The error correction level.
+///
+/// It controls the amount of data that can be damaged while still being able
+/// to recover the original information.
+///
+/// A higher error correction level allows for more corrupted data.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ErrorCorrection {
+ /// Low error correction. 7% of the data can be restored.
+ Low,
+ /// Medium error correction. 15% of the data can be restored.
+ Medium,
+ /// Quartile error correction. 25% of the data can be restored.
+ Quartile,
+ /// High error correction. 30% of the data can be restored.
+ High,
+}
+
+impl From<ErrorCorrection> for qrcode::EcLevel {
+ fn from(ec_level: ErrorCorrection) -> Self {
+ match ec_level {
+ ErrorCorrection::Low => qrcode::EcLevel::L,
+ ErrorCorrection::Medium => qrcode::EcLevel::M,
+ ErrorCorrection::Quartile => qrcode::EcLevel::Q,
+ ErrorCorrection::High => qrcode::EcLevel::H,
+ }
+ }
+}
+
+/// An error that occurred when building a [`State`] for a [`QRCode`].
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
+pub enum Error {
+ /// The data is too long to encode in a QR code for the chosen [`Version`].
+ #[error(
+ "The data is too long to encode in a QR code for the chosen version"
+ )]
+ DataTooLong,
+
+ /// The chosen [`Version`] and [`ErrorCorrection`] combination is invalid.
+ #[error(
+ "The chosen version and error correction level combination is invalid."
+ )]
+ InvalidVersion,
+
+ /// One or more characters in the provided data are not supported by the
+ /// chosen [`Version`].
+ #[error(
+ "One or more characters in the provided data are not supported by the \
+ chosen version"
+ )]
+ UnsupportedCharacterSet,
+
+ /// The chosen ECI designator is invalid. A valid designator should be
+ /// between 0 and 999999.
+ #[error(
+ "The chosen ECI designator is invalid. A valid designator should be \
+ between 0 and 999999."
+ )]
+ InvalidEciDesignator,
+
+ /// A character that does not belong to the character set was found.
+ #[error("A character that does not belong to the character set was found")]
+ InvalidCharacter,
+}
+
+impl From<qrcode::types::QrError> for Error {
+ fn from(error: qrcode::types::QrError) -> Self {
+ use qrcode::types::QrError;
+
+ match error {
+ QrError::DataTooLong => Error::DataTooLong,
+ QrError::InvalidVersion => Error::InvalidVersion,
+ QrError::UnsupportedCharacterSet => Error::UnsupportedCharacterSet,
+ QrError::InvalidEciDesignator => Error::InvalidEciDesignator,
+ QrError::InvalidCharacter => Error::InvalidCharacter,
+ }
+ }
+}
diff --git a/wgpu/src/renderer/widget/radio.rs b/graphics/src/widget/radio.rs
index 564f066b..fd3d8145 100644
--- a/wgpu/src/renderer/widget/radio.rs
+++ b/graphics/src/widget/radio.rs
@@ -1,15 +1,26 @@
-use crate::{radio::StyleSheet, Primitive, Renderer};
-use iced_native::{radio, Background, Color, MouseCursor, Rectangle};
+//! Create choices using radio buttons.
+use crate::{Backend, Primitive, Renderer};
+use iced_native::mouse;
+use iced_native::radio;
+use iced_native::{Background, Color, Rectangle};
-const SIZE: f32 = 28.0;
-const DOT_SIZE: f32 = SIZE / 2.0;
+pub use iced_style::radio::{Style, StyleSheet};
-impl radio::Renderer for Renderer {
+/// A circular button representing a choice.
+///
+/// This is an alias of an `iced_native` radio button with an
+/// `iced_wgpu::Renderer`.
+pub type Radio<Message, Backend> =
+ iced_native::Radio<Message, Renderer<Backend>>;
+
+impl<B> radio::Renderer for Renderer<B>
+where
+ B: Backend,
+{
type Style = Box<dyn StyleSheet>;
- fn default_size(&self) -> u32 {
- SIZE as u32
- }
+ const DEFAULT_SIZE: u16 = 28;
+ const DEFAULT_SPACING: u16 = 15;
fn draw(
&mut self,
@@ -25,10 +36,13 @@ impl radio::Renderer for Renderer {
style_sheet.active()
};
+ let size = bounds.width;
+ let dot_size = size / 2.0;
+
let radio = Primitive::Quad {
bounds,
background: style.background,
- border_radius: (SIZE / 2.0) as u16,
+ border_radius: size / 2.0,
border_width: style.border_width,
border_color: style.border_color,
};
@@ -38,14 +52,14 @@ impl radio::Renderer for Renderer {
primitives: if is_selected {
let radio_circle = Primitive::Quad {
bounds: Rectangle {
- x: bounds.x + DOT_SIZE / 2.0,
- y: bounds.y + DOT_SIZE / 2.0,
- width: bounds.width - DOT_SIZE,
- height: bounds.height - DOT_SIZE,
+ x: bounds.x + dot_size / 2.0,
+ y: bounds.y + dot_size / 2.0,
+ width: bounds.width - dot_size,
+ height: bounds.height - dot_size,
},
background: Background::Color(style.dot_color),
- border_radius: (DOT_SIZE / 2.0) as u16,
- border_width: 0,
+ border_radius: dot_size / 2.0,
+ border_width: 0.0,
border_color: Color::TRANSPARENT,
};
@@ -55,9 +69,9 @@ impl radio::Renderer for Renderer {
},
},
if is_mouse_over {
- MouseCursor::Pointer
+ mouse::Interaction::Pointer
} else {
- MouseCursor::OutOfBounds
+ mouse::Interaction::default()
},
)
}
diff --git a/graphics/src/widget/row.rs b/graphics/src/widget/row.rs
new file mode 100644
index 00000000..397d80bf
--- /dev/null
+++ b/graphics/src/widget/row.rs
@@ -0,0 +1,49 @@
+use crate::{Backend, Primitive, Renderer};
+use iced_native::mouse;
+use iced_native::row;
+use iced_native::{Element, Layout, Point, Rectangle};
+
+/// A container that distributes its contents horizontally.
+pub type Row<'a, Message, Backend> =
+ iced_native::Row<'a, Message, Renderer<Backend>>;
+
+impl<B> row::Renderer for Renderer<B>
+where
+ B: Backend,
+{
+ fn draw<Message>(
+ &mut self,
+ defaults: &Self::Defaults,
+ content: &[Element<'_, Message, Self>],
+ layout: Layout<'_>,
+ cursor_position: Point,
+ viewport: &Rectangle,
+ ) -> Self::Output {
+ let mut mouse_interaction = mouse::Interaction::default();
+
+ (
+ Primitive::Group {
+ primitives: content
+ .iter()
+ .zip(layout.children())
+ .map(|(child, layout)| {
+ let (primitive, new_mouse_interaction) = child.draw(
+ self,
+ defaults,
+ layout,
+ cursor_position,
+ viewport,
+ );
+
+ if new_mouse_interaction > mouse_interaction {
+ mouse_interaction = new_mouse_interaction;
+ }
+
+ primitive
+ })
+ .collect(),
+ },
+ mouse_interaction,
+ )
+ }
+}
diff --git a/graphics/src/widget/rule.rs b/graphics/src/widget/rule.rs
new file mode 100644
index 00000000..835ebed8
--- /dev/null
+++ b/graphics/src/widget/rule.rs
@@ -0,0 +1,73 @@
+//! Display a horizontal or vertical rule for dividing content.
+
+use crate::{Backend, Primitive, Renderer};
+use iced_native::mouse;
+use iced_native::rule;
+use iced_native::{Background, Color, Rectangle};
+
+pub use iced_style::rule::{FillMode, Style, StyleSheet};
+
+/// Display a horizontal or vertical rule for dividing content.
+///
+/// This is an alias of an `iced_native` rule with an `iced_graphics::Renderer`.
+pub type Rule<Backend> = iced_native::Rule<Renderer<Backend>>;
+
+impl<B> rule::Renderer for Renderer<B>
+where
+ B: Backend,
+{
+ type Style = Box<dyn StyleSheet>;
+
+ fn draw(
+ &mut self,
+ bounds: Rectangle,
+ style_sheet: &Self::Style,
+ is_horizontal: bool,
+ ) -> Self::Output {
+ let style = style_sheet.style();
+
+ let line = if is_horizontal {
+ let line_y = (bounds.y + (bounds.height / 2.0)
+ - (style.width as f32 / 2.0))
+ .round();
+
+ let (offset, line_width) = style.fill_mode.fill(bounds.width);
+ let line_x = bounds.x + offset;
+
+ Primitive::Quad {
+ bounds: Rectangle {
+ x: line_x,
+ y: line_y,
+ width: line_width,
+ height: style.width as f32,
+ },
+ background: Background::Color(style.color),
+ border_radius: style.radius,
+ border_width: 0.0,
+ border_color: Color::TRANSPARENT,
+ }
+ } else {
+ let line_x = (bounds.x + (bounds.width / 2.0)
+ - (style.width as f32 / 2.0))
+ .round();
+
+ let (offset, line_height) = style.fill_mode.fill(bounds.height);
+ let line_y = bounds.y + offset;
+
+ Primitive::Quad {
+ bounds: Rectangle {
+ x: line_x,
+ y: line_y,
+ width: style.width as f32,
+ height: line_height,
+ },
+ background: Background::Color(style.color),
+ border_radius: style.radius,
+ border_width: 0.0,
+ border_color: Color::TRANSPARENT,
+ }
+ };
+
+ (line, mouse::Interaction::default())
+ }
+}
diff --git a/wgpu/src/renderer/widget/scrollable.rs b/graphics/src/widget/scrollable.rs
index bfee7411..57065ba2 100644
--- a/wgpu/src/renderer/widget/scrollable.rs
+++ b/graphics/src/widget/scrollable.rs
@@ -1,12 +1,24 @@
-use crate::{Primitive, Renderer};
-use iced_native::{
- scrollable, Background, Color, MouseCursor, Rectangle, Vector,
-};
+//! Navigate an endless amount of content with a scrollbar.
+use crate::{Backend, Primitive, Renderer};
+use iced_native::mouse;
+use iced_native::scrollable;
+use iced_native::{Background, Color, Rectangle, Vector};
-const SCROLLBAR_WIDTH: u16 = 10;
-const SCROLLBAR_MARGIN: u16 = 2;
+pub use iced_native::scrollable::State;
+pub use iced_style::scrollable::{Scrollbar, Scroller, StyleSheet};
-impl scrollable::Renderer for Renderer {
+/// A widget that can vertically display an infinite amount of content
+/// with a scrollbar.
+///
+/// This is an alias of an `iced_native` scrollable with a default
+/// `Renderer`.
+pub type Scrollable<'a, Message, Backend> =
+ iced_native::Scrollable<'a, Message, Renderer<Backend>>;
+
+impl<B> scrollable::Renderer for Renderer<B>
+where
+ B: Backend,
+{
type Style = Box<dyn iced_style::scrollable::StyleSheet>;
fn scrollbar(
@@ -14,29 +26,45 @@ impl scrollable::Renderer for Renderer {
bounds: Rectangle,
content_bounds: Rectangle,
offset: u32,
+ scrollbar_width: u16,
+ scrollbar_margin: u16,
+ scroller_width: u16,
) -> Option<scrollable::Scrollbar> {
if content_bounds.height > bounds.height {
+ let outer_width =
+ scrollbar_width.max(scroller_width) + 2 * scrollbar_margin;
+
+ let outer_bounds = Rectangle {
+ x: bounds.x + bounds.width - outer_width as f32,
+ y: bounds.y,
+ width: outer_width as f32,
+ height: bounds.height,
+ };
+
let scrollbar_bounds = Rectangle {
x: bounds.x + bounds.width
- - f32::from(SCROLLBAR_WIDTH + 2 * SCROLLBAR_MARGIN),
+ - f32::from(outer_width / 2 + scrollbar_width / 2),
y: bounds.y,
- width: f32::from(SCROLLBAR_WIDTH + 2 * SCROLLBAR_MARGIN),
+ width: scrollbar_width as f32,
height: bounds.height,
};
let ratio = bounds.height / content_bounds.height;
- let scrollbar_height = bounds.height * ratio;
+ let scroller_height = bounds.height * ratio;
let y_offset = offset as f32 * ratio;
let scroller_bounds = Rectangle {
- x: scrollbar_bounds.x + f32::from(SCROLLBAR_MARGIN),
+ x: bounds.x + bounds.width
+ - f32::from(outer_width / 2 + scroller_width / 2),
y: scrollbar_bounds.y + y_offset,
- width: scrollbar_bounds.width - f32::from(2 * SCROLLBAR_MARGIN),
- height: scrollbar_height,
+ width: scroller_width as f32,
+ height: scroller_height,
};
Some(scrollable::Scrollbar {
+ outer_bounds,
bounds: scrollbar_bounds,
+ margin: scrollbar_margin,
scroller: scrollable::Scroller {
bounds: scroller_bounds,
},
@@ -56,16 +84,16 @@ impl scrollable::Renderer for Renderer {
scrollbar: Option<scrollable::Scrollbar>,
offset: u32,
style_sheet: &Self::Style,
- (content, mouse_cursor): Self::Output,
+ (content, mouse_interaction): Self::Output,
) -> Self::Output {
- let clip = Primitive::Clip {
- bounds,
- offset: Vector::new(0, offset),
- content: Box::new(content),
- };
-
(
if let Some(scrollbar) = scrollbar {
+ let clip = Primitive::Clip {
+ bounds,
+ offset: Vector::new(0, offset),
+ content: Box::new(content),
+ };
+
let style = if state.is_scroller_grabbed() {
style_sheet.dragging()
} else if is_mouse_over_scrollbar {
@@ -75,7 +103,7 @@ impl scrollable::Renderer for Renderer {
};
let is_scrollbar_visible =
- style.background.is_some() || style.border_width > 0;
+ style.background.is_some() || style.border_width > 0.0;
let scroller = if is_mouse_over
|| state.is_scroller_grabbed()
@@ -94,12 +122,7 @@ impl scrollable::Renderer for Renderer {
let scrollbar = if is_scrollbar_visible {
Primitive::Quad {
- bounds: Rectangle {
- x: scrollbar.bounds.x + f32::from(SCROLLBAR_MARGIN),
- width: scrollbar.bounds.width
- - f32::from(2 * SCROLLBAR_MARGIN),
- ..scrollbar.bounds
- },
+ bounds: scrollbar.bounds,
background: style
.background
.unwrap_or(Background::Color(Color::TRANSPARENT)),
@@ -115,12 +138,12 @@ impl scrollable::Renderer for Renderer {
primitives: vec![clip, scrollbar, scroller],
}
} else {
- clip
+ content
},
if is_mouse_over_scrollbar || state.is_scroller_grabbed() {
- MouseCursor::Idle
+ mouse::Interaction::Idle
} else {
- mouse_cursor
+ mouse_interaction
},
)
}
diff --git a/wgpu/src/renderer/widget/slider.rs b/graphics/src/widget/slider.rs
index c8ebd0da..aeceec3f 100644
--- a/wgpu/src/renderer/widget/slider.rs
+++ b/graphics/src/widget/slider.rs
@@ -1,17 +1,28 @@
-use crate::{
- slider::{HandleShape, StyleSheet},
- Primitive, Renderer,
-};
-use iced_native::{slider, Background, Color, MouseCursor, Point, Rectangle};
+//! Display an interactive selector of a single value from a range of values.
+//!
+//! A [`Slider`] has some local [`State`].
+use crate::{Backend, Primitive, Renderer};
+use iced_native::mouse;
+use iced_native::slider;
+use iced_native::{Background, Color, Point, Rectangle};
-const HANDLE_HEIGHT: f32 = 22.0;
+pub use iced_native::slider::State;
+pub use iced_style::slider::{Handle, HandleShape, Style, StyleSheet};
-impl slider::Renderer for Renderer {
+/// An horizontal bar and a handle that selects a single value from a range of
+/// values.
+///
+/// This is an alias of an `iced_native` slider with an `iced_wgpu::Renderer`.
+pub type Slider<'a, T, Message, Backend> =
+ iced_native::Slider<'a, T, Message, Renderer<Backend>>;
+
+impl<B> slider::Renderer for Renderer<B>
+where
+ B: Backend,
+{
type Style = Box<dyn StyleSheet>;
- fn height(&self) -> u32 {
- 30
- }
+ const DEFAULT_HEIGHT: u16 = 22;
fn draw(
&mut self,
@@ -43,8 +54,8 @@ impl slider::Renderer for Renderer {
height: 2.0,
},
background: Background::Color(style.rail_colors.0),
- border_radius: 0,
- border_width: 0,
+ border_radius: 0.0,
+ border_width: 0.0,
border_color: Color::TRANSPARENT,
},
Primitive::Quad {
@@ -55,27 +66,33 @@ impl slider::Renderer for Renderer {
height: 2.0,
},
background: Background::Color(style.rail_colors.1),
- border_radius: 0,
- border_width: 0,
+ border_radius: 0.0,
+ border_width: 0.0,
border_color: Color::TRANSPARENT,
},
);
- let (range_start, range_end) = range.into_inner();
+ let (handle_width, handle_height, handle_border_radius) = match style
+ .handle
+ .shape
+ {
+ HandleShape::Circle { radius } => {
+ (radius * 2.0, radius * 2.0, radius)
+ }
+ HandleShape::Rectangle {
+ width,
+ border_radius,
+ } => (f32::from(width), f32::from(bounds.height), border_radius),
+ };
- let (handle_width, handle_height, handle_border_radius) =
- match style.handle.shape {
- HandleShape::Circle { radius } => {
- (f32::from(radius * 2), f32::from(radius * 2), radius)
- }
- HandleShape::Rectangle {
- width,
- border_radius,
- } => (f32::from(width), HANDLE_HEIGHT, border_radius),
- };
+ let (range_start, range_end) = range.into_inner();
- let handle_offset = (bounds.width - handle_width)
- * ((value - range_start) / (range_end - range_start).max(1.0));
+ let handle_offset = if range_start >= range_end {
+ 0.0
+ } else {
+ (bounds.width - handle_width) * (value - range_start)
+ / (range_end - range_start)
+ };
let handle = Primitive::Quad {
bounds: Rectangle {
@@ -95,11 +112,11 @@ impl slider::Renderer for Renderer {
primitives: vec![rail_top, rail_bottom, handle],
},
if is_dragging {
- MouseCursor::Grabbing
+ mouse::Interaction::Grabbing
} else if is_mouse_over {
- MouseCursor::Grab
+ mouse::Interaction::Grab
} else {
- MouseCursor::OutOfBounds
+ mouse::Interaction::default()
},
)
}
diff --git a/graphics/src/widget/space.rs b/graphics/src/widget/space.rs
new file mode 100644
index 00000000..1f31eabe
--- /dev/null
+++ b/graphics/src/widget/space.rs
@@ -0,0 +1,15 @@
+use crate::{Backend, Primitive, Renderer};
+use iced_native::mouse;
+use iced_native::space;
+use iced_native::Rectangle;
+
+pub use iced_native::Space;
+
+impl<B> space::Renderer for Renderer<B>
+where
+ B: Backend,
+{
+ fn draw(&mut self, _bounds: Rectangle) -> Self::Output {
+ (Primitive::None, mouse::Interaction::default())
+ }
+}
diff --git a/wgpu/src/renderer/widget/svg.rs b/graphics/src/widget/svg.rs
index 67bc3fe1..8b5ed66a 100644
--- a/wgpu/src/renderer/widget/svg.rs
+++ b/graphics/src/widget/svg.rs
@@ -1,9 +1,16 @@
+//! Display vector graphics in your application.
+use crate::backend::{self, Backend};
use crate::{Primitive, Renderer};
-use iced_native::{svg, Layout, MouseCursor};
+use iced_native::{mouse, svg, Layout};
-impl svg::Renderer for Renderer {
+pub use iced_native::svg::{Handle, Svg};
+
+impl<B> svg::Renderer for Renderer<B>
+where
+ B: Backend + backend::Svg,
+{
fn dimensions(&self, handle: &svg::Handle) -> (u32, u32) {
- self.image_pipeline.viewport_dimensions(handle)
+ self.backend().viewport_dimensions(handle)
}
fn draw(
@@ -16,7 +23,7 @@ impl svg::Renderer for Renderer {
handle,
bounds: layout.bounds(),
},
- MouseCursor::OutOfBounds,
+ mouse::Interaction::default(),
)
}
}
diff --git a/graphics/src/widget/text.rs b/graphics/src/widget/text.rs
new file mode 100644
index 00000000..7e22e680
--- /dev/null
+++ b/graphics/src/widget/text.rs
@@ -0,0 +1,74 @@
+//! Write some text for your users to read.
+use crate::backend::{self, Backend};
+use crate::{Primitive, Renderer};
+use iced_native::mouse;
+use iced_native::text;
+use iced_native::{
+ Color, Font, HorizontalAlignment, Rectangle, Size, VerticalAlignment,
+};
+
+/// A paragraph of text.
+///
+/// This is an alias of an `iced_native` text with an `iced_wgpu::Renderer`.
+pub type Text<Backend> = iced_native::Text<Renderer<Backend>>;
+
+use std::f32;
+
+impl<B> text::Renderer for Renderer<B>
+where
+ B: Backend + backend::Text,
+{
+ type Font = Font;
+
+ fn default_size(&self) -> u16 {
+ self.backend().default_size()
+ }
+
+ fn measure(
+ &self,
+ content: &str,
+ size: u16,
+ font: Font,
+ bounds: Size,
+ ) -> (f32, f32) {
+ self.backend()
+ .measure(content, f32::from(size), font, bounds)
+ }
+
+ fn draw(
+ &mut self,
+ defaults: &Self::Defaults,
+ bounds: Rectangle,
+ content: &str,
+ size: u16,
+ font: Font,
+ color: Option<Color>,
+ horizontal_alignment: HorizontalAlignment,
+ vertical_alignment: VerticalAlignment,
+ ) -> Self::Output {
+ let x = match horizontal_alignment {
+ iced_native::HorizontalAlignment::Left => bounds.x,
+ iced_native::HorizontalAlignment::Center => bounds.center_x(),
+ iced_native::HorizontalAlignment::Right => bounds.x + bounds.width,
+ };
+
+ let y = match vertical_alignment {
+ iced_native::VerticalAlignment::Top => bounds.y,
+ iced_native::VerticalAlignment::Center => bounds.center_y(),
+ iced_native::VerticalAlignment::Bottom => bounds.y + bounds.height,
+ };
+
+ (
+ Primitive::Text {
+ content: content.to_string(),
+ size: f32::from(size),
+ bounds: Rectangle { x, y, ..bounds },
+ color: color.unwrap_or(defaults.text.color),
+ font,
+ horizontal_alignment,
+ vertical_alignment,
+ },
+ mouse::Interaction::default(),
+ )
+ }
+}
diff --git a/graphics/src/widget/text_input.rs b/graphics/src/widget/text_input.rs
new file mode 100644
index 00000000..c269022b
--- /dev/null
+++ b/graphics/src/widget/text_input.rs
@@ -0,0 +1,265 @@
+//! Display fields that can be filled with text.
+//!
+//! A [`TextInput`] has some local [`State`].
+use crate::backend::{self, Backend};
+use crate::{Primitive, Renderer};
+use iced_native::mouse;
+use iced_native::text_input::{self, cursor};
+use iced_native::{
+ Background, Color, Font, HorizontalAlignment, Point, Rectangle, Size,
+ Vector, VerticalAlignment,
+};
+use std::f32;
+
+pub use iced_native::text_input::State;
+pub use iced_style::text_input::{Style, StyleSheet};
+
+/// A field that can be filled with text.
+///
+/// This is an alias of an `iced_native` text input with an `iced_wgpu::Renderer`.
+pub type TextInput<'a, Message, Backend> =
+ iced_native::TextInput<'a, Message, Renderer<Backend>>;
+
+impl<B> text_input::Renderer for Renderer<B>
+where
+ B: Backend + backend::Text,
+{
+ type Style = Box<dyn StyleSheet>;
+
+ fn measure_value(&self, value: &str, size: u16, font: Font) -> f32 {
+ let backend = self.backend();
+
+ let (width, _) =
+ backend.measure(value, f32::from(size), font, Size::INFINITY);
+
+ width
+ }
+
+ fn offset(
+ &self,
+ text_bounds: Rectangle,
+ font: Font,
+ size: u16,
+ value: &text_input::Value,
+ state: &text_input::State,
+ ) -> f32 {
+ if state.is_focused() {
+ let cursor = state.cursor();
+
+ let focus_position = match cursor.state(value) {
+ cursor::State::Index(i) => i,
+ cursor::State::Selection { end, .. } => end,
+ };
+
+ let (_, offset) = measure_cursor_and_scroll_offset(
+ self,
+ text_bounds,
+ value,
+ size,
+ focus_position,
+ font,
+ );
+
+ offset
+ } else {
+ 0.0
+ }
+ }
+
+ fn draw(
+ &mut self,
+ bounds: Rectangle,
+ text_bounds: Rectangle,
+ cursor_position: Point,
+ font: Font,
+ size: u16,
+ placeholder: &str,
+ value: &text_input::Value,
+ state: &text_input::State,
+ style_sheet: &Self::Style,
+ ) -> Self::Output {
+ let is_mouse_over = bounds.contains(cursor_position);
+
+ let style = if state.is_focused() {
+ style_sheet.focused()
+ } else if is_mouse_over {
+ style_sheet.hovered()
+ } else {
+ style_sheet.active()
+ };
+
+ let input = Primitive::Quad {
+ bounds,
+ background: style.background,
+ border_radius: style.border_radius,
+ border_width: style.border_width,
+ border_color: style.border_color,
+ };
+
+ let text = value.to_string();
+
+ let text_value = Primitive::Text {
+ content: if text.is_empty() {
+ placeholder.to_string()
+ } else {
+ text.clone()
+ },
+ color: if text.is_empty() {
+ style_sheet.placeholder_color()
+ } else {
+ style_sheet.value_color()
+ },
+ font,
+ bounds: Rectangle {
+ y: text_bounds.center_y(),
+ width: f32::INFINITY,
+ ..text_bounds
+ },
+ size: f32::from(size),
+ horizontal_alignment: HorizontalAlignment::Left,
+ vertical_alignment: VerticalAlignment::Center,
+ };
+
+ let (contents_primitive, offset) = if state.is_focused() {
+ let cursor = state.cursor();
+
+ let (cursor_primitive, offset) = match cursor.state(value) {
+ cursor::State::Index(position) => {
+ let (text_value_width, offset) =
+ measure_cursor_and_scroll_offset(
+ self,
+ text_bounds,
+ value,
+ size,
+ position,
+ font,
+ );
+
+ (
+ Primitive::Quad {
+ bounds: Rectangle {
+ x: text_bounds.x + text_value_width,
+ y: text_bounds.y,
+ width: 1.0,
+ height: text_bounds.height,
+ },
+ background: Background::Color(
+ style_sheet.value_color(),
+ ),
+ border_radius: 0.0,
+ border_width: 0.0,
+ border_color: Color::TRANSPARENT,
+ },
+ offset,
+ )
+ }
+ cursor::State::Selection { start, end } => {
+ let left = start.min(end);
+ let right = end.max(start);
+
+ let (left_position, left_offset) =
+ measure_cursor_and_scroll_offset(
+ self,
+ text_bounds,
+ value,
+ size,
+ left,
+ font,
+ );
+
+ let (right_position, right_offset) =
+ measure_cursor_and_scroll_offset(
+ self,
+ text_bounds,
+ value,
+ size,
+ right,
+ font,
+ );
+
+ let width = right_position - left_position;
+
+ (
+ Primitive::Quad {
+ bounds: Rectangle {
+ x: text_bounds.x + left_position,
+ y: text_bounds.y,
+ width,
+ height: text_bounds.height,
+ },
+ background: Background::Color(
+ style_sheet.selection_color(),
+ ),
+ border_radius: 0.0,
+ border_width: 0.0,
+ border_color: Color::TRANSPARENT,
+ },
+ if end == right {
+ right_offset
+ } else {
+ left_offset
+ },
+ )
+ }
+ };
+
+ (
+ Primitive::Group {
+ primitives: vec![cursor_primitive, text_value],
+ },
+ Vector::new(offset as u32, 0),
+ )
+ } else {
+ (text_value, Vector::new(0, 0))
+ };
+
+ let text_width = self.measure_value(
+ if text.is_empty() { placeholder } else { &text },
+ size,
+ font,
+ );
+
+ let contents = if text_width > text_bounds.width {
+ Primitive::Clip {
+ bounds: text_bounds,
+ offset,
+ content: Box::new(contents_primitive),
+ }
+ } else {
+ contents_primitive
+ };
+
+ (
+ Primitive::Group {
+ primitives: vec![input, contents],
+ },
+ if is_mouse_over {
+ mouse::Interaction::Text
+ } else {
+ mouse::Interaction::default()
+ },
+ )
+ }
+}
+
+fn measure_cursor_and_scroll_offset<B>(
+ renderer: &Renderer<B>,
+ text_bounds: Rectangle,
+ value: &text_input::Value,
+ size: u16,
+ cursor_index: usize,
+ font: Font,
+) -> (f32, f32)
+where
+ B: Backend + backend::Text,
+{
+ use iced_native::text_input::Renderer;
+
+ let text_before_cursor = value.until(cursor_index).to_string();
+
+ let text_value_width =
+ renderer.measure_value(&text_before_cursor, size, font);
+ let offset = ((text_value_width + 5.0) - text_bounds.width).max(0.0);
+
+ (text_value_width, offset)
+}
diff --git a/graphics/src/window.rs b/graphics/src/window.rs
new file mode 100644
index 00000000..3e74db5f
--- /dev/null
+++ b/graphics/src/window.rs
@@ -0,0 +1,10 @@
+//! Draw graphics to window surfaces.
+mod compositor;
+
+#[cfg(feature = "opengl")]
+mod gl_compositor;
+
+pub use compositor::Compositor;
+
+#[cfg(feature = "opengl")]
+pub use gl_compositor::GLCompositor;
diff --git a/native/src/window/backend.rs b/graphics/src/window/compositor.rs
index 3bc691cd..0bc8cbc8 100644
--- a/native/src/window/backend.rs
+++ b/graphics/src/window/compositor.rs
@@ -1,14 +1,14 @@
-use crate::MouseCursor;
-
+use crate::{Color, Error, Viewport};
+use iced_native::mouse;
use raw_window_handle::HasRawWindowHandle;
-/// A graphics backend that can render to windows.
-pub trait Backend: Sized {
+/// A graphics compositor that can draw to windows.
+pub trait Compositor: Sized {
/// The settings of the backend.
type Settings: Default;
/// The iced renderer of the backend.
- type Renderer: crate::Renderer;
+ type Renderer: iced_native::Renderer;
/// The surface of the backend.
type Surface;
@@ -16,14 +16,12 @@ pub trait Backend: Sized {
/// The swap chain of the backend.
type SwapChain;
- /// Creates a new [`Backend`] and an associated iced renderer.
- ///
- /// [`Backend`]: trait.Backend.html
- fn new(settings: Self::Settings) -> (Self, Self::Renderer);
+ /// Creates a new [`Compositor`].
+ fn new(settings: Self::Settings) -> Result<(Self, Self::Renderer), Error>;
/// Crates a new [`Surface`] for the given window.
///
- /// [`Surface`]: #associatedtype.Surface
+ /// [`Surface`]: Self::Surface
fn create_surface<W: HasRawWindowHandle>(
&mut self,
window: &W,
@@ -31,8 +29,8 @@ pub trait Backend: Sized {
/// Crates a new [`SwapChain`] for the given [`Surface`].
///
- /// [`SwapChain`]: #associatedtype.SwapChain
- /// [`Surface`]: #associatedtype.Surface
+ /// [`SwapChain`]: Self::SwapChain
+ /// [`Surface`]: Self::Surface
fn create_swap_chain(
&mut self,
surface: &Self::Surface,
@@ -42,14 +40,14 @@ pub trait Backend: Sized {
/// Draws the output primitives to the next frame of the given [`SwapChain`].
///
- /// [`SwapChain`]: #associatedtype.SwapChain
- /// [`Surface`]: #associatedtype.Surface
+ /// [`SwapChain`]: Self::SwapChain
fn draw<T: AsRef<str>>(
&mut self,
renderer: &mut Self::Renderer,
swap_chain: &mut Self::SwapChain,
- output: &<Self::Renderer as crate::Renderer>::Output,
- scale_factor: f64,
+ viewport: &Viewport,
+ background_color: Color,
+ output: &<Self::Renderer as iced_native::Renderer>::Output,
overlay: &[T],
- ) -> MouseCursor;
+ ) -> mouse::Interaction;
}
diff --git a/graphics/src/window/gl_compositor.rs b/graphics/src/window/gl_compositor.rs
new file mode 100644
index 00000000..34d70be3
--- /dev/null
+++ b/graphics/src/window/gl_compositor.rs
@@ -0,0 +1,63 @@
+use crate::{Color, Error, Size, Viewport};
+use iced_native::mouse;
+
+use core::ffi::c_void;
+
+/// A basic OpenGL compositor.
+///
+/// A compositor is responsible for initializing a renderer and managing window
+/// surfaces.
+///
+/// For now, this compositor only deals with a single global surface
+/// for drawing. However, the trait will most likely change in the near future
+/// to handle multiple surfaces at once.
+///
+/// If you implement an OpenGL renderer, you can implement this trait to ease
+/// integration with existing windowing shells, like `iced_glutin`.
+pub trait GLCompositor: Sized {
+ /// The renderer of the [`GLCompositor`].
+ ///
+ /// This should point to your renderer type, which could be a type alias
+ /// of the [`Renderer`] provided in this crate with with a specific
+ /// [`Backend`].
+ ///
+ /// [`Renderer`]: crate::Renderer
+ /// [`Backend`]: crate::Backend
+ type Renderer: iced_native::Renderer;
+
+ /// The settings of the [`GLCompositor`].
+ ///
+ /// It's up to you to decide the configuration supported by your renderer!
+ type Settings: Default;
+
+ /// Creates a new [`GLCompositor`] and [`Renderer`] with the given
+ /// [`Settings`] and an OpenGL address loader function.
+ ///
+ /// [`Renderer`]: crate::Renderer
+ /// [`Backend`]: crate::Backend
+ /// [`Settings`]: Self::Settings
+ #[allow(unsafe_code)]
+ unsafe fn new(
+ settings: Self::Settings,
+ loader_function: impl FnMut(&str) -> *const c_void,
+ ) -> Result<(Self, Self::Renderer), Error>;
+
+ /// Returns the amount of samples that should be used when configuring
+ /// an OpenGL context for this [`GLCompositor`].
+ fn sample_count(settings: &Self::Settings) -> u32;
+
+ /// Resizes the viewport of the [`GLCompositor`].
+ fn resize_viewport(&mut self, physical_size: Size<u32>);
+
+ /// Draws the provided output with the given [`Renderer`].
+ ///
+ /// [`Renderer`]: crate::Renderer
+ fn draw<T: AsRef<str>>(
+ &mut self,
+ renderer: &mut Self::Renderer,
+ viewport: &Viewport,
+ background_color: Color,
+ output: &<Self::Renderer as iced_native::Renderer>::Output,
+ overlay: &[T],
+ ) -> mouse::Interaction;
+}
diff --git a/native/CHANGELOG.md b/native/CHANGELOG.md
deleted file mode 100644
index df8852b7..00000000
--- a/native/CHANGELOG.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# Changelog
-All notable changes to this project will be documented in this file.
-
-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]
-### Added
-- `image::Handle` type with `from_path` and `from_memory` methods. [#90]
-- `image::Data` enum representing different kinds of image data. [#90]
-- `text_input::Renderer::measure_value` required method to measure the width of a `TextInput` value. [#108]
-- Click-based cursor positioning for `TextInput`. [#108]
-- `Home` and `End` keys support for `TextInput`. [#108]
-- `Ctrl+Left` and `Ctrl+Right` cursor word jump for `TextInput`. [#108]
-- `keyboard::ModifiersState` struct which contains the state of the keyboard modifiers. [#108]
-- `TextInput::password` method to enable secure password input mode. [#113]
-- `Button::height` and `Button::min_height` methods to control the height of a button.
-
-### Changed
-- `Image::new` takes an `Into<image::Handle>` now instead of an `Into<String>`. [#90]
-- `Button::background` takes an `Into<Background>` now instead of a `Background`.
-- `keyboard::Event::Input` now contains key modifiers state. [#108]
-
-### Fixed
-- `Image` widget not keeping aspect ratio consistently. [#90]
-- `TextInput` not taking grapheme clusters into account. [#108]
-
-[#90]: https://github.com/hecrj/iced/pull/90
-[#108]: https://github.com/hecrj/iced/pull/108
-[#113]: https://github.com/hecrj/iced/pull/113
-
-
-## [0.1.0] - 2019-11-25
-### Added
-- First release! :tada:
-
-[Unreleased]: https://github.com/hecrj/iced/compare/native-0.1.0...HEAD
-[0.1.0]: https://github.com/hecrj/iced/releases/tag/native-0.1.0
diff --git a/native/Cargo.toml b/native/Cargo.toml
index 6276535e..2c99638a 100644
--- a/native/Cargo.toml
+++ b/native/Cargo.toml
@@ -1,22 +1,25 @@
[package]
name = "iced_native"
-version = "0.1.0"
+version = "0.3.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
description = "A renderer-agnostic library for native GUIs"
license = "MIT"
repository = "https://github.com/hecrj/iced"
+[features]
+debug = []
+
[dependencies]
twox-hash = "1.5"
-raw-window-handle = "0.3"
unicode-segmentation = "1.6"
+num-traits = "0.2"
[dependencies.iced_core]
-version = "0.1.0"
+version = "0.3"
path = "../core"
[dependencies.iced_futures]
-version = "0.1.0-alpha"
+version = "0.2"
path = "../futures"
features = ["thread-pool"]
diff --git a/native/README.md b/native/README.md
index 59ec5424..6323dd4f 100644
--- a/native/README.md
+++ b/native/README.md
@@ -14,7 +14,9 @@ To achieve this, it introduces a bunch of reusable interfaces:
- A bunch of `Renderer` traits, meant to keep the crate renderer-agnostic.
- A `Windowed` trait, leveraging [`raw-window-handle`], which can be implemented by graphical renderers that target _windows_. Window-based shells (like [`iced_winit`]) can use this trait to stay renderer-agnostic.
-![iced_native](../docs/graphs/native.png)
+<p align="center">
+ <img alt="The native target" src="../docs/graphs/native.png" width="80%">
+</p>
[documentation]: https://docs.rs/iced_native
[`iced_core`]: ../core
@@ -26,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.1.0"
+iced_native = "0.3"
```
__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 4c574590..ecdccabf 100644
--- a/native/src/clipboard.rs
+++ b/native/src/clipboard.rs
@@ -2,7 +2,5 @@
/// applications.
pub trait Clipboard {
/// Returns the current content of the [`Clipboard`] as text.
- ///
- /// [`Clipboard`]: trait.Clipboard.html
fn content(&self) -> Option<String>;
}
diff --git a/winit/src/debug/basic.rs b/native/src/debug/basic.rs
index d46edba6..a42f66ea 100644
--- a/winit/src/debug/basic.rs
+++ b/native/src/debug/basic.rs
@@ -1,5 +1,7 @@
+#![allow(missing_docs)]
use std::{collections::VecDeque, time};
+/// A bunch of time measurements for debugging purposes.
#[derive(Debug)]
pub struct Debug {
is_enabled: bool,
@@ -30,6 +32,7 @@ pub struct Debug {
}
impl Debug {
+ /// Creates a new [`struct@Debug`].
pub fn new() -> Self {
let now = time::Instant::now();
@@ -169,9 +172,13 @@ impl Debug {
lines.push(key_value("Render:", self.render_durations.average()));
lines.push(key_value("Message count:", self.message_count));
lines.push(String::from("Last messages:"));
- lines.extend(
- self.last_messages.iter().map(|msg| format!(" {}", msg)),
- );
+ lines.extend(self.last_messages.iter().map(|msg| {
+ if msg.len() <= 100 {
+ format!(" {}", msg)
+ } else {
+ format!(" {:.100}...", msg)
+ }
+ }));
lines
}
diff --git a/winit/src/debug/null.rs b/native/src/debug/null.rs
index 2a9430cd..60e6122d 100644
--- a/winit/src/debug/null.rs
+++ b/native/src/debug/null.rs
@@ -1,3 +1,4 @@
+#![allow(missing_docs)]
#[derive(Debug)]
pub struct Debug;
diff --git a/native/src/element.rs b/native/src/element.rs
index 276f7614..d6e9639a 100644
--- a/native/src/element.rs
+++ b/native/src/element.rs
@@ -1,5 +1,8 @@
+use crate::event::{self, Event};
+use crate::layout;
+use crate::overlay;
use crate::{
- layout, Clipboard, Color, Event, Hasher, Layout, Length, Point, Widget,
+ Clipboard, Color, Hasher, Layout, Length, Point, Rectangle, Widget,
};
/// A generic [`Widget`].
@@ -11,8 +14,6 @@ use crate::{
/// to turn it into an [`Element`].
///
/// [built-in widget]: widget/index.html#built-in-widgets
-/// [`Widget`]: widget/trait.Widget.html
-/// [`Element`]: struct.Element.html
#[allow(missing_debug_implementations)]
pub struct Element<'a, Message, Renderer> {
pub(crate) widget: Box<dyn Widget<Message, Renderer> + 'a>,
@@ -22,10 +23,7 @@ impl<'a, Message, Renderer> Element<'a, Message, Renderer>
where
Renderer: crate::Renderer,
{
- /// Create a new [`Element`] containing the given [`Widget`].
- ///
- /// [`Element`]: struct.Element.html
- /// [`Widget`]: widget/trait.Widget.html
+ /// Creates a new [`Element`] containing the given [`Widget`].
pub fn new(
widget: impl Widget<Message, Renderer> + 'a,
) -> Element<'a, Message, Renderer> {
@@ -39,8 +37,6 @@ where
/// This method is useful when you want to decouple different parts of your
/// UI and make them __composable__.
///
- /// [`Element`]: struct.Element.html
- ///
/// # Example
/// Imagine we want to use [our counter](index.html#usage). But instead of
/// showing a single counter, we want to display many of them. We can reuse
@@ -81,7 +77,7 @@ where
///
/// ```
/// # mod counter {
- /// # use iced_native::{text, Text};
+ /// # type Text = iced_native::Text<iced_native::renderer::Null>;
/// #
/// # #[derive(Debug, Clone, Copy)]
/// # pub enum Message {}
@@ -186,8 +182,7 @@ where
/// The [`Renderer`] will explain the layout of the [`Element`] graphically.
/// This can be very useful for debugging your layout!
///
- /// [`Element`]: struct.Element.html
- /// [`Renderer`]: trait.Renderer.html
+ /// [`Renderer`]: crate::Renderer
pub fn explain<C: Into<Color>>(
self,
color: C,
@@ -202,23 +197,18 @@ where
}
/// Returns the width of the [`Element`].
- ///
- /// [`Element`]: struct.Element.html
pub fn width(&self) -> Length {
self.widget.width()
}
/// Returns the height of the [`Element`].
- ///
- /// [`Element`]: struct.Element.html
pub fn height(&self) -> Length {
self.widget.height()
}
/// Computes the layout of the [`Element`] in the given [`Limits`].
///
- /// [`Element`]: struct.Element.html
- /// [`Limits`]: layout/struct.Limits.html
+ /// [`Limits`]: layout::Limits
pub fn layout(
&self,
renderer: &Renderer,
@@ -227,27 +217,51 @@ where
self.widget.layout(renderer, limits)
}
+ /// Processes a runtime [`Event`].
+ pub fn on_event(
+ &mut self,
+ event: Event,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ messages: &mut Vec<Message>,
+ renderer: &Renderer,
+ clipboard: Option<&dyn Clipboard>,
+ ) -> event::Status {
+ self.widget.on_event(
+ event,
+ layout,
+ cursor_position,
+ messages,
+ renderer,
+ clipboard,
+ )
+ }
+
/// Draws the [`Element`] and its children using the given [`Layout`].
- ///
- /// [`Element`]: struct.Element.html
- /// [`Layout`]: layout/struct.Layout.html
pub fn draw(
&self,
renderer: &mut Renderer,
defaults: &Renderer::Defaults,
layout: Layout<'_>,
cursor_position: Point,
+ viewport: &Rectangle,
) -> Renderer::Output {
self.widget
- .draw(renderer, defaults, layout, cursor_position)
+ .draw(renderer, defaults, layout, cursor_position, viewport)
}
/// Computes the _layout_ hash of the [`Element`].
- ///
- /// [`Element`]: struct.Element.html
pub fn hash_layout(&self, state: &mut Hasher) {
self.widget.hash_layout(state);
}
+
+ /// Returns the overlay of the [`Element`], if there is any.
+ pub fn overlay<'b>(
+ &'b mut self,
+ layout: Layout<'_>,
+ ) -> Option<overlay::Element<'b, Message, Renderer>> {
+ self.widget.overlay(layout)
+ }
}
struct Map<'a, A, B, Renderer> {
@@ -272,7 +286,9 @@ impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> {
impl<'a, A, B, Renderer> Widget<B, Renderer> for Map<'a, A, B, Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::Renderer + 'a,
+ A: 'static,
+ B: 'static,
{
fn width(&self) -> Length {
self.widget.width()
@@ -298,10 +314,10 @@ where
messages: &mut Vec<B>,
renderer: &Renderer,
clipboard: Option<&dyn Clipboard>,
- ) {
+ ) -> event::Status {
let mut original_messages = Vec::new();
- self.widget.on_event(
+ let status = self.widget.on_event(
event,
layout,
cursor_position,
@@ -313,6 +329,8 @@ where
original_messages
.drain(..)
.for_each(|message| messages.push((self.mapper)(message)));
+
+ status
}
fn draw(
@@ -321,14 +339,26 @@ where
defaults: &Renderer::Defaults,
layout: Layout<'_>,
cursor_position: Point,
+ viewport: &Rectangle,
) -> Renderer::Output {
self.widget
- .draw(renderer, defaults, layout, cursor_position)
+ .draw(renderer, defaults, layout, cursor_position, viewport)
}
fn hash_layout(&self, state: &mut Hasher) {
self.widget.hash_layout(state);
}
+
+ fn overlay(
+ &mut self,
+ layout: Layout<'_>,
+ ) -> Option<overlay::Element<'_, B, Renderer>> {
+ let mapper = &self.mapper;
+
+ self.widget
+ .overlay(layout)
+ .map(move |overlay| overlay.map(mapper))
+ }
}
struct Explain<'a, Message, Renderer: crate::Renderer> {
@@ -374,7 +404,7 @@ where
messages: &mut Vec<Message>,
renderer: &Renderer,
clipboard: Option<&dyn Clipboard>,
- ) {
+ ) -> event::Status {
self.element.widget.on_event(
event,
layout,
@@ -391,12 +421,14 @@ where
defaults: &Renderer::Defaults,
layout: Layout<'_>,
cursor_position: Point,
+ viewport: &Rectangle,
) -> Renderer::Output {
renderer.explain(
defaults,
self.element.widget.as_ref(),
layout,
cursor_position,
+ viewport,
self.color,
)
}
@@ -404,4 +436,11 @@ where
fn hash_layout(&self, state: &mut Hasher) {
self.element.widget.hash_layout(state);
}
+
+ fn overlay(
+ &mut self,
+ layout: Layout<'_>,
+ ) -> Option<overlay::Element<'_, Message, Renderer>> {
+ self.element.overlay(layout)
+ }
}
diff --git a/native/src/event.rs b/native/src/event.rs
index 99a8e880..f3c260c0 100644
--- a/native/src/event.rs
+++ b/native/src/event.rs
@@ -1,7 +1,8 @@
-use crate::{
- input::{keyboard, mouse, Touch},
- window,
-};
+//! Handle events of a user interface.
+use crate::keyboard;
+use crate::mouse;
+use crate::touch;
+use crate::window;
/// A user interface event.
///
@@ -21,5 +22,36 @@ pub enum Event {
Window(window::Event),
/// A touch event
- Touch(Touch),
+ Touch(touch::Touch),
+}
+
+/// The status of an [`Event`] after being processed.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Status {
+ /// The [`Event`] was **NOT** handled by any widget.
+ Ignored,
+
+ /// The [`Event`] was handled and processed by a widget.
+ Captured,
+}
+
+impl Status {
+ /// Merges two [`Status`] into one.
+ ///
+ /// `Captured` takes precedence over `Ignored`:
+ ///
+ /// ```
+ /// use iced_native::event::Status;
+ ///
+ /// assert_eq!(Status::Ignored.merge(Status::Ignored), Status::Ignored);
+ /// assert_eq!(Status::Ignored.merge(Status::Captured), Status::Captured);
+ /// assert_eq!(Status::Captured.merge(Status::Ignored), Status::Captured);
+ /// assert_eq!(Status::Captured.merge(Status::Captured), Status::Captured);
+ /// ```
+ pub fn merge(self, b: Self) -> Self {
+ match self {
+ Status::Ignored => b,
+ Status::Captured => Status::Captured,
+ }
+ }
}
diff --git a/native/src/input.rs b/native/src/input.rs
deleted file mode 100644
index 514a98ea..00000000
--- a/native/src/input.rs
+++ /dev/null
@@ -1,9 +0,0 @@
-//! Map your system events into input events that the runtime can understand.
-pub mod keyboard;
-pub mod mouse;
-pub mod touch;
-
-mod button_state;
-
-pub use button_state::ButtonState;
-pub use touch::Touch;
diff --git a/native/src/input/button_state.rs b/native/src/input/button_state.rs
deleted file mode 100644
index 988043ba..00000000
--- a/native/src/input/button_state.rs
+++ /dev/null
@@ -1,9 +0,0 @@
-/// The state of a button.
-#[derive(Debug, Hash, Ord, PartialOrd, PartialEq, Eq, Clone, Copy)]
-pub enum ButtonState {
- /// The button is pressed.
- Pressed,
-
- /// The button is __not__ pressed.
- Released,
-}
diff --git a/native/src/input/keyboard.rs b/native/src/input/keyboard.rs
deleted file mode 100644
index 432e75ba..00000000
--- a/native/src/input/keyboard.rs
+++ /dev/null
@@ -1,8 +0,0 @@
-//! Build keyboard events.
-mod event;
-mod key_code;
-mod modifiers_state;
-
-pub use event::Event;
-pub use key_code::KeyCode;
-pub use modifiers_state::ModifiersState;
diff --git a/native/src/input/keyboard/modifiers_state.rs b/native/src/input/keyboard/modifiers_state.rs
deleted file mode 100644
index 4e3794b3..00000000
--- a/native/src/input/keyboard/modifiers_state.rs
+++ /dev/null
@@ -1,15 +0,0 @@
-/// The current state of the keyboard modifiers.
-#[derive(Debug, Clone, Copy, PartialEq)]
-pub struct ModifiersState {
- /// Whether a shift key is pressed
- pub shift: bool,
-
- /// 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,
-}
diff --git a/native/src/keyboard.rs b/native/src/keyboard.rs
new file mode 100644
index 00000000..012538e3
--- /dev/null
+++ b/native/src/keyboard.rs
@@ -0,0 +1,2 @@
+//! Track keyboard events.
+pub use iced_core::keyboard::*;
diff --git a/native/src/layout.rs b/native/src/layout.rs
index 4a3ab94a..6d144902 100644
--- a/native/src/layout.rs
+++ b/native/src/layout.rs
@@ -12,8 +12,6 @@ pub use node::Node;
use crate::{Point, Rectangle, Vector};
/// The bounds of a [`Node`] and its children, using absolute coordinates.
-///
-/// [`Node`]: struct.Node.html
#[derive(Debug, Clone, Copy)]
pub struct Layout<'a> {
position: Point,
@@ -34,14 +32,15 @@ impl<'a> Layout<'a> {
}
}
- /// Gets the bounds of the [`Layout`].
+ /// Returns the position of the [`Layout`].
+ pub fn position(&self) -> Point {
+ self.position
+ }
+
+ /// Returns the bounds of the [`Layout`].
///
/// The returned [`Rectangle`] describes the position and size of a
/// [`Node`].
- ///
- /// [`Layout`]: struct.Layout.html
- /// [`Rectangle`]: struct.Rectangle.html
- /// [`Node`]: struct.Node.html
pub fn bounds(&self) -> Rectangle {
let bounds = self.node.bounds();
@@ -54,10 +53,7 @@ impl<'a> Layout<'a> {
}
/// Returns an iterator over the [`Layout`] of the children of a [`Node`].
- ///
- /// [`Layout`]: struct.Layout.html
- /// [`Node`]: struct.Node.html
- pub fn children(&'a self) -> impl Iterator<Item = Layout<'a>> {
+ pub fn children(self) -> impl Iterator<Item = Layout<'a>> {
self.node.children().iter().map(move |node| {
Layout::with_offset(
Vector::new(self.position.x, self.position.y),
diff --git a/native/src/layout/debugger.rs b/native/src/layout/debugger.rs
index e4b21609..0759613f 100644
--- a/native/src/layout/debugger.rs
+++ b/native/src/layout/debugger.rs
@@ -1,8 +1,6 @@
-use crate::{Color, Layout, Point, Renderer, Widget};
+use crate::{Color, Layout, Point, Rectangle, Renderer, Widget};
/// A renderer able to graphically explain a [`Layout`].
-///
-/// [`Layout`]: struct.Layout.html
pub trait Debugger: Renderer {
/// Explains the [`Layout`] of an [`Element`] for debugging purposes.
///
@@ -12,15 +10,15 @@ pub trait Debugger: Renderer {
/// A common approach consists in recursively rendering the bounds of the
/// [`Layout`] and its children.
///
- /// [`Layout`]: struct.Layout.html
- /// [`Element`]: ../struct.Element.html
- /// [`Element::explain`]: ../struct.Element.html#method.explain
+ /// [`Element`]: crate::Element
+ /// [`Element::explain`]: crate::Element::explain
fn explain<Message>(
&mut self,
defaults: &Self::Defaults,
widget: &dyn Widget<Message, Self>,
layout: Layout<'_>,
cursor_position: Point,
+ viewport: &Rectangle,
color: Color,
) -> Self::Output;
}
diff --git a/native/src/layout/flex.rs b/native/src/layout/flex.rs
index 2f65f1c1..4f6523fb 100644
--- a/native/src/layout/flex.rs
+++ b/native/src/layout/flex.rs
@@ -58,8 +58,6 @@ impl Axis {
/// padding and alignment to the items as needed.
///
/// It returns a new layout [`Node`].
-///
-/// [`Node`]: ../struct.Node.html
pub fn resolve<Message, Renderer>(
axis: Axis,
renderer: &Renderer,
@@ -171,8 +169,5 @@ where
let (width, height) = axis.pack(main - padding, cross);
let size = limits.resolve(Size::new(width, height));
- Node::with_children(
- Size::new(size.width + padding * 2.0, size.height + padding * 2.0),
- nodes,
- )
+ Node::with_children(size.pad(padding), nodes)
}
diff --git a/native/src/layout/limits.rs b/native/src/layout/limits.rs
index 664c881a..a7bb5c9c 100644
--- a/native/src/layout/limits.rs
+++ b/native/src/layout/limits.rs
@@ -17,9 +17,6 @@ impl Limits {
};
/// Creates new [`Limits`] with the given minimum and maximum [`Size`].
- ///
- /// [`Limits`]: struct.Limits.html
- /// [`Size`]: ../struct.Size.html
pub const fn new(min: Size, max: Size) -> Limits {
Limits {
min,
@@ -29,32 +26,21 @@ impl Limits {
}
/// Returns the minimum [`Size`] of the [`Limits`].
- ///
- /// [`Limits`]: struct.Limits.html
- /// [`Size`]: ../struct.Size.html
pub fn min(&self) -> Size {
self.min
}
/// Returns the maximum [`Size`] of the [`Limits`].
- ///
- /// [`Limits`]: struct.Limits.html
- /// [`Size`]: ../struct.Size.html
pub fn max(&self) -> Size {
self.max
}
/// Returns the fill [`Size`] of the [`Limits`].
- ///
- /// [`Limits`]: struct.Limits.html
- /// [`Size`]: ../struct.Size.html
pub fn fill(&self) -> Size {
self.fill
}
/// Applies a width constraint to the current [`Limits`].
- ///
- /// [`Limits`]: struct.Limits.html
pub fn width(mut self, width: Length) -> Limits {
match width {
Length::Shrink => {
@@ -77,8 +63,6 @@ impl Limits {
}
/// Applies a height constraint to the current [`Limits`].
- ///
- /// [`Limits`]: struct.Limits.html
pub fn height(mut self, height: Length) -> Limits {
match height {
Length::Shrink => {
@@ -101,8 +85,6 @@ impl Limits {
}
/// Applies a minimum width constraint to the current [`Limits`].
- ///
- /// [`Limits`]: struct.Limits.html
pub fn min_width(mut self, min_width: u32) -> Limits {
self.min.width =
self.min.width.max(min_width as f32).min(self.max.width);
@@ -111,8 +93,6 @@ impl Limits {
}
/// Applies a maximum width constraint to the current [`Limits`].
- ///
- /// [`Limits`]: struct.Limits.html
pub fn max_width(mut self, max_width: u32) -> Limits {
self.max.width =
self.max.width.min(max_width as f32).max(self.min.width);
@@ -121,8 +101,6 @@ impl Limits {
}
/// Applies a minimum height constraint to the current [`Limits`].
- ///
- /// [`Limits`]: struct.Limits.html
pub fn min_height(mut self, min_height: u32) -> Limits {
self.min.height =
self.min.height.max(min_height as f32).min(self.max.height);
@@ -131,8 +109,6 @@ impl Limits {
}
/// Applies a maximum height constraint to the current [`Limits`].
- ///
- /// [`Limits`]: struct.Limits.html
pub fn max_height(mut self, max_height: u32) -> Limits {
self.max.height =
self.max.height.min(max_height as f32).max(self.min.height);
@@ -141,16 +117,11 @@ impl Limits {
}
/// Shrinks the current [`Limits`] to account for the given padding.
- ///
- /// [`Limits`]: struct.Limits.html
pub fn pad(&self, padding: f32) -> Limits {
self.shrink(Size::new(padding * 2.0, padding * 2.0))
}
/// Shrinks the current [`Limits`] by the given [`Size`].
- ///
- /// [`Limits`]: struct.Limits.html
- /// [`Size`]: ../struct.Size.html
pub fn shrink(&self, size: Size) -> Limits {
let min = Size::new(
(self.min().width - size.width).max(0.0),
@@ -171,8 +142,6 @@ impl Limits {
}
/// Removes the minimum width constraint for the current [`Limits`].
- ///
- /// [`Limits`]: struct.Limits.html
pub fn loose(&self) -> Limits {
Limits {
min: Size::ZERO,
@@ -183,8 +152,6 @@ impl Limits {
/// Computes the resulting [`Size`] that fits the [`Limits`] given the
/// intrinsic size of some content.
- ///
- /// [`Limits`]: struct.Limits.html
pub fn resolve(&self, intrinsic_size: Size) -> Size {
Size::new(
intrinsic_size
diff --git a/native/src/layout/node.rs b/native/src/layout/node.rs
index a265c46a..d7666f31 100644
--- a/native/src/layout/node.rs
+++ b/native/src/layout/node.rs
@@ -9,17 +9,11 @@ pub struct Node {
impl Node {
/// Creates a new [`Node`] with the given [`Size`].
- ///
- /// [`Node`]: struct.Node.html
- /// [`Size`]: ../struct.Size.html
pub const fn new(size: Size) -> Self {
Self::with_children(size, Vec::new())
}
/// Creates a new [`Node`] with the given [`Size`] and children.
- ///
- /// [`Node`]: struct.Node.html
- /// [`Size`]: ../struct.Size.html
pub const fn with_children(size: Size, children: Vec<Node>) -> Self {
Node {
bounds: Rectangle {
@@ -33,30 +27,21 @@ impl Node {
}
/// Returns the [`Size`] of the [`Node`].
- ///
- /// [`Node`]: struct.Node.html
- /// [`Size`]: ../struct.Size.html
pub fn size(&self) -> Size {
Size::new(self.bounds.width, self.bounds.height)
}
/// Returns the bounds of the [`Node`].
- ///
- /// [`Node`]: struct.Node.html
pub fn bounds(&self) -> Rectangle {
self.bounds
}
/// Returns the children of the [`Node`].
- ///
- /// [`Node`]: struct.Node.html
pub fn children(&self) -> &[Node] {
&self.children
}
/// Aligns the [`Node`] in the given space.
- ///
- /// [`Node`]: struct.Node.html
pub fn align(
&mut self,
horizontal_alignment: Align,
@@ -85,8 +70,6 @@ impl Node {
}
/// Moves the [`Node`] to the given position.
- ///
- /// [`Node`]: struct.Node.html
pub fn move_to(&mut self, position: Point) {
self.bounds.x = position.x;
self.bounds.y = position.y;
diff --git a/native/src/lib.rs b/native/src/lib.rs
index e4e7baee..0890785b 100644
--- a/native/src/lib.rs
+++ b/native/src/lib.rs
@@ -1,6 +1,6 @@
//! A renderer-agnostic native GUI runtime.
//!
-//! ![`iced_native` crate graph](https://github.com/hecrj/iced/blob/cae26cb7bc627f4a5b3bcf1cd023a0c552e8c65e/docs/graphs/native.png?raw=true)
+//! ![The native path of the Iced ecosystem](https://github.com/hecrj/iced/raw/improvement/update-ecosystem-and-roadmap/docs/graphs/native.png)
//!
//! `iced_native` takes [`iced_core`] and builds a native runtime on top of it,
//! featuring:
@@ -9,20 +9,17 @@
//! - Event handling for all the built-in widgets
//! - A renderer-agnostic API
//!
-//! To achieve this, it introduces a bunch of reusable interfaces:
+//! To achieve this, it introduces a couple of reusable interfaces:
//!
//! - A [`Widget`] trait, which is used to implement new widgets: from layout
//! requirements to event and drawing logic.
//! - A bunch of `Renderer` traits, meant to keep the crate renderer-agnostic.
-//! - A [`window::Renderer`] trait, leveraging [`raw-window-handle`], which can be
-//! implemented by graphical renderers that target _windows_. Window-based
-//! shells (like [`iced_winit`]) can use this trait to stay renderer-agnostic.
//!
//! # Usage
//! The strategy to use this crate depends on your particular use case. If you
//! want to:
-//! - Implement a custom shell or integrate it in your own system, you should
-//! check out the [`UserInterface`] type.
+//! - Implement a custom shell or integrate it in your own system, check out the
+//! [`UserInterface`] type.
//! - Build a new renderer, see the [renderer] module.
//! - Build a custom widget, start at the [`Widget`] trait.
//!
@@ -30,30 +27,39 @@
//! [`iced_winit`]: https://github.com/hecrj/iced/tree/master/winit
//! [`druid`]: https://github.com/xi-editor/druid
//! [`raw-window-handle`]: https://github.com/rust-windowing/raw-window-handle
-//! [`Widget`]: widget/trait.Widget.html
-//! [`window::Renderer`]: window/trait.Renderer.html
-//! [`UserInterface`]: struct.UserInterface.html
-//! [renderer]: renderer/index.html
+//! [renderer]: crate::renderer
#![deny(missing_docs)]
#![deny(missing_debug_implementations)]
#![deny(unused_results)]
#![forbid(unsafe_code)]
#![forbid(rust_2018_idioms)]
-pub mod input;
+pub mod event;
+pub mod keyboard;
pub mod layout;
+pub mod mouse;
+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 event;
mod hasher;
-mod mouse_cursor;
mod runtime;
mod user_interface;
+// We disable debug capabilities on release builds unless the `debug` feature
+// is explicitly enabled.
+#[cfg(feature = "debug")]
+#[path = "debug/basic.rs"]
+mod debug;
+#[cfg(not(feature = "debug"))]
+#[path = "debug/null.rs"]
+mod debug;
+
pub use iced_core::{
Align, Background, Color, Font, HorizontalAlignment, Length, Point,
Rectangle, Size, Vector, VerticalAlignment,
@@ -64,11 +70,13 @@ pub use iced_futures::{executor, futures, Command};
pub use executor::Executor;
pub use clipboard::Clipboard;
+pub use debug::Debug;
pub use element::Element;
pub use event::Event;
pub use hasher::Hasher;
pub use layout::Layout;
-pub use mouse_cursor::MouseCursor;
+pub use overlay::Overlay;
+pub use program::Program;
pub use renderer::Renderer;
pub use runtime::Runtime;
pub use subscription::Subscription;
diff --git a/native/src/mouse.rs b/native/src/mouse.rs
new file mode 100644
index 00000000..9ee406cf
--- /dev/null
+++ b/native/src/mouse.rs
@@ -0,0 +1,6 @@
+//! Track mouse events.
+
+pub mod click;
+
+pub use click::Click;
+pub use iced_core::mouse::*;
diff --git a/native/src/mouse/click.rs b/native/src/mouse/click.rs
new file mode 100644
index 00000000..6c8b61a5
--- /dev/null
+++ b/native/src/mouse/click.rs
@@ -0,0 +1,71 @@
+//! Track mouse clicks.
+use crate::Point;
+use std::time::Instant;
+
+/// A mouse click.
+#[derive(Debug, Clone, Copy)]
+pub struct Click {
+ kind: Kind,
+ position: Point,
+ time: Instant,
+}
+
+/// The kind of mouse click.
+#[derive(Debug, Clone, Copy)]
+pub enum Kind {
+ /// A single click
+ Single,
+
+ /// A double click
+ Double,
+
+ /// A triple click
+ Triple,
+}
+
+impl Kind {
+ fn next(&self) -> Kind {
+ match self {
+ Kind::Single => Kind::Double,
+ Kind::Double => Kind::Triple,
+ Kind::Triple => Kind::Double,
+ }
+ }
+}
+
+impl Click {
+ /// Creates a new [`Click`] with the given position and previous last
+ /// [`Click`].
+ pub fn new(position: Point, previous: Option<Click>) -> Click {
+ let time = Instant::now();
+
+ let kind = if let Some(previous) = previous {
+ if previous.is_consecutive(position, time) {
+ previous.kind.next()
+ } else {
+ Kind::Single
+ }
+ } else {
+ Kind::Single
+ };
+
+ Click {
+ kind,
+ position,
+ time,
+ }
+ }
+
+ /// Returns the [`Kind`] of [`Click`].
+ pub fn kind(&self) -> Kind {
+ self.kind
+ }
+
+ fn is_consecutive(&self, new_position: Point, time: Instant) -> bool {
+ self.position == new_position
+ && time
+ .checked_duration_since(self.time)
+ .map(|duration| duration.as_millis() <= 300)
+ .unwrap_or(false)
+ }
+}
diff --git a/native/src/mouse_cursor.rs b/native/src/mouse_cursor.rs
deleted file mode 100644
index c7297e0e..00000000
--- a/native/src/mouse_cursor.rs
+++ /dev/null
@@ -1,30 +0,0 @@
-/// The state of the mouse cursor.
-#[derive(Debug, Eq, PartialEq, Clone, Copy, PartialOrd, Ord)]
-pub enum MouseCursor {
- /// The cursor is out of the bounds of the user interface.
- OutOfBounds,
-
- /// The cursor is over a non-interactive widget.
- Idle,
-
- /// The cursor is over a clickable widget.
- Pointer,
-
- /// The cursor is over a busy widget.
- Working,
-
- /// The cursor is over a grabbable widget.
- Grab,
-
- /// The cursor is grabbing a widget.
- Grabbing,
-
- /// The cursor is over a text widget.
- Text,
-}
-
-impl Default for MouseCursor {
- fn default() -> MouseCursor {
- MouseCursor::OutOfBounds
- }
-}
diff --git a/native/src/overlay.rs b/native/src/overlay.rs
new file mode 100644
index 00000000..ea8bb384
--- /dev/null
+++ b/native/src/overlay.rs
@@ -0,0 +1,76 @@
+//! Display interactive elements on top of other widgets.
+mod element;
+
+pub mod menu;
+
+pub use element::Element;
+pub use menu::Menu;
+
+use crate::event::{self, Event};
+use crate::layout;
+use crate::{Clipboard, Hasher, Layout, Point, Size};
+
+/// An interactive component that can be displayed on top of other widgets.
+pub trait Overlay<Message, Renderer>
+where
+ Renderer: crate::Renderer,
+{
+ /// Returns the layout [`Node`] of the [`Overlay`].
+ ///
+ /// This [`Node`] is used by the runtime to compute the [`Layout`] of the
+ /// user interface.
+ ///
+ /// [`Node`]: layout::Node
+ fn layout(
+ &self,
+ renderer: &Renderer,
+ bounds: Size,
+ position: Point,
+ ) -> layout::Node;
+
+ /// Draws the [`Overlay`] using the associated `Renderer`.
+ fn draw(
+ &self,
+ renderer: &mut Renderer,
+ defaults: &Renderer::Defaults,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ ) -> Renderer::Output;
+
+ /// Computes the _layout_ hash of the [`Overlay`].
+ ///
+ /// The produced hash is used by the runtime to decide if the [`Layout`]
+ /// needs to be recomputed between frames. Therefore, to ensure maximum
+ /// efficiency, the hash should only be affected by the properties of the
+ /// [`Overlay`] that can affect layouting.
+ ///
+ /// For example, the [`Text`] widget does not hash its color property, as
+ /// its value cannot affect the overall [`Layout`] of the user interface.
+ ///
+ /// [`Text`]: crate::widget::Text
+ fn hash_layout(&self, state: &mut Hasher, position: Point);
+
+ /// Processes a runtime [`Event`].
+ ///
+ /// It receives:
+ /// * an [`Event`] describing user interaction
+ /// * the computed [`Layout`] of the [`Overlay`]
+ /// * the current cursor position
+ /// * a mutable `Message` list, allowing the [`Overlay`] to produce
+ /// new messages based on user interaction.
+ /// * the `Renderer`
+ /// * a [`Clipboard`], if available
+ ///
+ /// By default, it does nothing.
+ fn on_event(
+ &mut self,
+ _event: Event,
+ _layout: Layout<'_>,
+ _cursor_position: Point,
+ _messages: &mut Vec<Message>,
+ _renderer: &Renderer,
+ _clipboard: Option<&dyn Clipboard>,
+ ) -> event::Status {
+ event::Status::Ignored
+ }
+}
diff --git a/native/src/overlay/element.rs b/native/src/overlay/element.rs
new file mode 100644
index 00000000..0f44a781
--- /dev/null
+++ b/native/src/overlay/element.rs
@@ -0,0 +1,156 @@
+pub use crate::Overlay;
+
+use crate::event::{self, Event};
+use crate::layout;
+use crate::{Clipboard, Hasher, Layout, Point, Size, Vector};
+
+/// A generic [`Overlay`].
+#[allow(missing_debug_implementations)]
+pub struct Element<'a, Message, Renderer> {
+ position: Point,
+ overlay: Box<dyn Overlay<Message, Renderer> + 'a>,
+}
+
+impl<'a, Message, Renderer> Element<'a, Message, Renderer>
+where
+ Renderer: crate::Renderer,
+{
+ /// Creates a new [`Element`] containing the given [`Overlay`].
+ pub fn new(
+ position: Point,
+ overlay: Box<dyn Overlay<Message, Renderer> + 'a>,
+ ) -> Self {
+ Self { position, overlay }
+ }
+
+ /// Translates the [`Element`].
+ pub fn translate(mut self, translation: Vector) -> Self {
+ self.position = self.position + translation;
+ self
+ }
+
+ /// Applies a transformation to the produced message of the [`Element`].
+ pub fn map<B>(self, f: &'a dyn Fn(Message) -> B) -> Element<'a, B, Renderer>
+ where
+ Message: 'a,
+ Renderer: 'a,
+ B: 'static,
+ {
+ Element {
+ position: self.position,
+ overlay: Box::new(Map::new(self.overlay, f)),
+ }
+ }
+
+ /// Computes the layout of the [`Element`] in the given bounds.
+ pub fn layout(&self, renderer: &Renderer, bounds: Size) -> layout::Node {
+ self.overlay.layout(renderer, bounds, self.position)
+ }
+
+ /// Processes a runtime [`Event`].
+ pub fn on_event(
+ &mut self,
+ event: Event,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ messages: &mut Vec<Message>,
+ renderer: &Renderer,
+ clipboard: Option<&dyn Clipboard>,
+ ) -> event::Status {
+ self.overlay.on_event(
+ event,
+ layout,
+ cursor_position,
+ messages,
+ renderer,
+ clipboard,
+ )
+ }
+
+ /// Draws the [`Element`] and its children using the given [`Layout`].
+ pub fn draw(
+ &self,
+ renderer: &mut Renderer,
+ defaults: &Renderer::Defaults,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ ) -> Renderer::Output {
+ self.overlay
+ .draw(renderer, defaults, layout, cursor_position)
+ }
+
+ /// Computes the _layout_ hash of the [`Element`].
+ pub fn hash_layout(&self, state: &mut Hasher) {
+ self.overlay.hash_layout(state, self.position);
+ }
+}
+
+struct Map<'a, A, B, Renderer> {
+ content: Box<dyn Overlay<A, Renderer> + 'a>,
+ mapper: &'a dyn Fn(A) -> B,
+}
+
+impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> {
+ pub fn new(
+ content: Box<dyn Overlay<A, Renderer> + 'a>,
+ mapper: &'a dyn Fn(A) -> B,
+ ) -> Map<'a, A, B, Renderer> {
+ Map { content, mapper }
+ }
+}
+
+impl<'a, A, B, Renderer> Overlay<B, Renderer> for Map<'a, A, B, Renderer>
+where
+ Renderer: crate::Renderer,
+{
+ fn layout(
+ &self,
+ renderer: &Renderer,
+ bounds: Size,
+ position: Point,
+ ) -> layout::Node {
+ self.content.layout(renderer, bounds, position)
+ }
+
+ fn on_event(
+ &mut self,
+ event: Event,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ messages: &mut Vec<B>,
+ renderer: &Renderer,
+ clipboard: Option<&dyn Clipboard>,
+ ) -> event::Status {
+ let mut original_messages = Vec::new();
+
+ let event_status = self.content.on_event(
+ event,
+ layout,
+ cursor_position,
+ &mut original_messages,
+ renderer,
+ clipboard,
+ );
+
+ original_messages
+ .drain(..)
+ .for_each(|message| messages.push((self.mapper)(message)));
+
+ event_status
+ }
+
+ fn draw(
+ &self,
+ renderer: &mut Renderer,
+ defaults: &Renderer::Defaults,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ ) -> Renderer::Output {
+ self.content
+ .draw(renderer, defaults, layout, cursor_position)
+ }
+
+ fn hash_layout(&self, state: &mut Hasher, position: Point) {
+ self.content.hash_layout(state, position);
+ }
+}
diff --git a/native/src/overlay/menu.rs b/native/src/overlay/menu.rs
new file mode 100644
index 00000000..abac849f
--- /dev/null
+++ b/native/src/overlay/menu.rs
@@ -0,0 +1,428 @@
+//! Build and show dropdown menus.
+use crate::container;
+use crate::event::{self, Event};
+use crate::layout;
+use crate::mouse;
+use crate::overlay;
+use crate::scrollable;
+use crate::text;
+use crate::{
+ Clipboard, Container, Element, Hasher, Layout, Length, Point, Rectangle,
+ Scrollable, Size, Vector, Widget,
+};
+
+/// A list of selectable options.
+#[allow(missing_debug_implementations)]
+pub struct Menu<'a, T, Renderer: self::Renderer> {
+ state: &'a mut State,
+ options: &'a [T],
+ hovered_option: &'a mut Option<usize>,
+ last_selection: &'a mut Option<T>,
+ width: u16,
+ padding: u16,
+ text_size: Option<u16>,
+ font: Renderer::Font,
+ style: <Renderer as self::Renderer>::Style,
+}
+
+impl<'a, T, Renderer> Menu<'a, T, Renderer>
+where
+ T: ToString + Clone,
+ Renderer: self::Renderer + 'a,
+{
+ /// Creates a new [`Menu`] with the given [`State`], a list of options, and
+ /// the message to produced when an option is selected.
+ pub fn new(
+ state: &'a mut State,
+ options: &'a [T],
+ hovered_option: &'a mut Option<usize>,
+ last_selection: &'a mut Option<T>,
+ ) -> Self {
+ Menu {
+ state,
+ options,
+ hovered_option,
+ last_selection,
+ width: 0,
+ padding: 0,
+ text_size: None,
+ font: Default::default(),
+ style: Default::default(),
+ }
+ }
+
+ /// Sets the width of the [`Menu`].
+ pub fn width(mut self, width: u16) -> Self {
+ self.width = width;
+ self
+ }
+
+ /// Sets the padding of the [`Menu`].
+ pub fn padding(mut self, padding: u16) -> Self {
+ self.padding = padding;
+ self
+ }
+
+ /// Sets the text size of the [`Menu`].
+ pub fn text_size(mut self, text_size: u16) -> Self {
+ self.text_size = Some(text_size);
+ self
+ }
+
+ /// Sets the font of the [`Menu`].
+ pub fn font(mut self, font: Renderer::Font) -> Self {
+ self.font = font;
+ self
+ }
+
+ /// Sets the style of the [`Menu`].
+ pub fn style(
+ mut self,
+ style: impl Into<<Renderer as self::Renderer>::Style>,
+ ) -> Self {
+ self.style = style.into();
+ self
+ }
+
+ /// Turns the [`Menu`] into an overlay [`Element`] at the given target
+ /// position.
+ ///
+ /// The `target_height` will be used to display the menu either on top
+ /// of the target or under it, depending on the screen position and the
+ /// dimensions of the [`Menu`].
+ pub fn overlay<Message: 'a>(
+ self,
+ position: Point,
+ target_height: f32,
+ ) -> overlay::Element<'a, Message, Renderer> {
+ overlay::Element::new(
+ position,
+ Box::new(Overlay::new(self, target_height)),
+ )
+ }
+}
+
+/// The local state of a [`Menu`].
+#[derive(Debug, Clone, Default)]
+pub struct State {
+ scrollable: scrollable::State,
+}
+
+impl State {
+ /// Creates a new [`State`] for a [`Menu`].
+ pub fn new() -> Self {
+ Self::default()
+ }
+}
+
+struct Overlay<'a, Message, Renderer: self::Renderer> {
+ container: Container<'a, Message, Renderer>,
+ width: u16,
+ target_height: f32,
+ style: <Renderer as self::Renderer>::Style,
+}
+
+impl<'a, Message, Renderer: self::Renderer> Overlay<'a, Message, Renderer>
+where
+ Message: 'a,
+ Renderer: 'a,
+{
+ pub fn new<T>(menu: Menu<'a, T, Renderer>, target_height: f32) -> Self
+ where
+ T: Clone + ToString,
+ {
+ let Menu {
+ state,
+ options,
+ hovered_option,
+ last_selection,
+ width,
+ padding,
+ font,
+ text_size,
+ style,
+ } = menu;
+
+ let container =
+ Container::new(Scrollable::new(&mut state.scrollable).push(List {
+ options,
+ hovered_option,
+ last_selection,
+ font,
+ text_size,
+ padding,
+ style: style.clone(),
+ }))
+ .padding(1);
+
+ Self {
+ container,
+ width: width,
+ target_height,
+ style: style,
+ }
+ }
+}
+
+impl<'a, Message, Renderer> crate::Overlay<Message, Renderer>
+ for Overlay<'a, Message, Renderer>
+where
+ Renderer: self::Renderer,
+{
+ fn layout(
+ &self,
+ renderer: &Renderer,
+ bounds: Size,
+ position: Point,
+ ) -> layout::Node {
+ let space_below = bounds.height - (position.y + self.target_height);
+ let space_above = position.y;
+
+ let limits = layout::Limits::new(
+ Size::ZERO,
+ Size::new(
+ bounds.width - position.x,
+ if space_below > space_above {
+ space_below
+ } else {
+ space_above
+ },
+ ),
+ )
+ .width(Length::Units(self.width));
+
+ let mut node = self.container.layout(renderer, &limits);
+
+ node.move_to(if space_below > space_above {
+ position + Vector::new(0.0, self.target_height)
+ } else {
+ position - Vector::new(0.0, node.size().height)
+ });
+
+ node
+ }
+
+ fn hash_layout(&self, state: &mut Hasher, position: Point) {
+ use std::hash::Hash;
+
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
+ (position.x as u32).hash(state);
+ (position.y as u32).hash(state);
+ self.container.hash_layout(state);
+ }
+
+ fn on_event(
+ &mut self,
+ event: Event,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ messages: &mut Vec<Message>,
+ renderer: &Renderer,
+ clipboard: Option<&dyn Clipboard>,
+ ) -> event::Status {
+ self.container.on_event(
+ event.clone(),
+ layout,
+ cursor_position,
+ messages,
+ renderer,
+ clipboard,
+ )
+ }
+
+ fn draw(
+ &self,
+ renderer: &mut Renderer,
+ defaults: &Renderer::Defaults,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ ) -> Renderer::Output {
+ let primitives = self.container.draw(
+ renderer,
+ defaults,
+ layout,
+ cursor_position,
+ &layout.bounds(),
+ );
+
+ renderer.decorate(
+ layout.bounds(),
+ cursor_position,
+ &self.style,
+ primitives,
+ )
+ }
+}
+
+struct List<'a, T, Renderer: self::Renderer> {
+ options: &'a [T],
+ hovered_option: &'a mut Option<usize>,
+ last_selection: &'a mut Option<T>,
+ padding: u16,
+ text_size: Option<u16>,
+ font: Renderer::Font,
+ style: <Renderer as self::Renderer>::Style,
+}
+
+impl<'a, T, Message, Renderer: self::Renderer> Widget<Message, Renderer>
+ for List<'a, T, Renderer>
+where
+ T: Clone + ToString,
+ Renderer: self::Renderer,
+{
+ fn width(&self) -> Length {
+ Length::Fill
+ }
+
+ fn height(&self) -> Length {
+ Length::Shrink
+ }
+
+ fn layout(
+ &self,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ use std::f32;
+
+ let limits = limits.width(Length::Fill).height(Length::Shrink);
+ let text_size = self.text_size.unwrap_or(renderer.default_size());
+
+ let size = {
+ let intrinsic = Size::new(
+ 0.0,
+ f32::from(text_size + self.padding * 2)
+ * self.options.len() as f32,
+ );
+
+ limits.resolve(intrinsic)
+ };
+
+ layout::Node::new(size)
+ }
+
+ fn hash_layout(&self, state: &mut Hasher) {
+ use std::hash::Hash as _;
+
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
+ self.options.len().hash(state);
+ self.text_size.hash(state);
+ self.padding.hash(state);
+ }
+
+ fn on_event(
+ &mut self,
+ event: Event,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ _messages: &mut Vec<Message>,
+ renderer: &Renderer,
+ _clipboard: Option<&dyn Clipboard>,
+ ) -> event::Status {
+ match event {
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
+ let bounds = layout.bounds();
+
+ if bounds.contains(cursor_position) {
+ if let Some(index) = *self.hovered_option {
+ if let Some(option) = self.options.get(index) {
+ *self.last_selection = Some(option.clone());
+ }
+ }
+ }
+ }
+ 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) {
+ *self.hovered_option = Some(
+ ((cursor_position.y - bounds.y)
+ / f32::from(text_size + self.padding * 2))
+ as usize,
+ );
+ }
+ }
+ _ => {}
+ }
+
+ event::Status::Ignored
+ }
+
+ fn draw(
+ &self,
+ renderer: &mut Renderer,
+ _defaults: &Renderer::Defaults,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ viewport: &Rectangle,
+ ) -> Renderer::Output {
+ self::Renderer::draw(
+ renderer,
+ layout.bounds(),
+ cursor_position,
+ viewport,
+ self.options,
+ *self.hovered_option,
+ self.padding,
+ self.text_size.unwrap_or(renderer.default_size()),
+ self.font,
+ &self.style,
+ )
+ }
+}
+
+/// The renderer of a [`Menu`].
+///
+/// Your [renderer] will need to implement this trait before being
+/// able to use a [`Menu`] in your user interface.
+///
+/// [renderer]: crate::renderer
+pub trait Renderer:
+ scrollable::Renderer + container::Renderer + text::Renderer
+{
+ /// The [`Menu`] style supported by this renderer.
+ type Style: Default + Clone;
+
+ /// Decorates a the list of options of a [`Menu`].
+ ///
+ /// This method can be used to draw a background for the [`Menu`].
+ fn decorate(
+ &mut self,
+ bounds: Rectangle,
+ cursor_position: Point,
+ style: &<Self as Renderer>::Style,
+ primitive: Self::Output,
+ ) -> Self::Output;
+
+ /// Draws the list of options of a [`Menu`].
+ fn draw<T: ToString>(
+ &mut self,
+ bounds: Rectangle,
+ cursor_position: Point,
+ viewport: &Rectangle,
+ options: &[T],
+ hovered_option: Option<usize>,
+ padding: u16,
+ text_size: u16,
+ font: Self::Font,
+ style: &<Self as Renderer>::Style,
+ ) -> Self::Output;
+}
+
+impl<'a, T, Message, Renderer> Into<Element<'a, Message, Renderer>>
+ for List<'a, T, Renderer>
+where
+ T: ToString + Clone,
+ Message: 'a,
+ Renderer: 'a + self::Renderer,
+{
+ fn into(self) -> Element<'a, Message, Renderer> {
+ Element::new(self)
+ }
+}
diff --git a/native/src/program.rs b/native/src/program.rs
new file mode 100644
index 00000000..9ee72703
--- /dev/null
+++ b/native/src/program.rs
@@ -0,0 +1,30 @@
+//! Build interactive programs using The Elm Architecture.
+use crate::{Command, Element, Renderer};
+
+mod state;
+
+pub use state::State;
+
+/// The core of a user interface application following The Elm Architecture.
+pub trait Program: Sized {
+ /// The graphics backend to use to draw the [`Program`].
+ type Renderer: Renderer;
+
+ /// The type of __messages__ your [`Program`] will produce.
+ type Message: std::fmt::Debug + Send;
+
+ /// Handles a __message__ and updates the state of the [`Program`].
+ ///
+ /// This is where you define your __update logic__. All the __messages__,
+ /// produced by either user interactions or commands, will be handled by
+ /// this method.
+ ///
+ /// Any [`Command`] returned will be executed immediately in the
+ /// background by shells.
+ fn update(&mut self, message: Self::Message) -> Command<Self::Message>;
+
+ /// Returns the widgets to display in the [`Program`].
+ ///
+ /// These widgets can produce __messages__ based on user interaction.
+ fn view(&mut self) -> Element<'_, Self::Message, Self::Renderer>;
+}
diff --git a/native/src/program/state.rs b/native/src/program/state.rs
new file mode 100644
index 00000000..e630890a
--- /dev/null
+++ b/native/src/program/state.rs
@@ -0,0 +1,180 @@
+use crate::{
+ Cache, Clipboard, Command, Debug, Event, Point, Program, Renderer, Size,
+ UserInterface,
+};
+
+/// The execution state of a [`Program`]. It leverages caching, event
+/// processing, and rendering primitive storage.
+#[allow(missing_debug_implementations)]
+pub struct State<P>
+where
+ P: Program + 'static,
+{
+ program: P,
+ cache: Option<Cache>,
+ primitive: <P::Renderer as Renderer>::Output,
+ queued_events: Vec<Event>,
+ queued_messages: Vec<P::Message>,
+}
+
+impl<P> State<P>
+where
+ P: Program + 'static,
+{
+ /// Creates a new [`State`] with the provided [`Program`], initializing its
+ /// primitive with the given logical bounds and renderer.
+ pub fn new(
+ mut program: P,
+ bounds: Size,
+ cursor_position: Point,
+ renderer: &mut P::Renderer,
+ debug: &mut Debug,
+ ) -> Self {
+ let mut user_interface = build_user_interface(
+ &mut program,
+ Cache::default(),
+ renderer,
+ bounds,
+ debug,
+ );
+
+ debug.draw_started();
+ let primitive = user_interface.draw(renderer, cursor_position);
+ debug.draw_finished();
+
+ let cache = Some(user_interface.into_cache());
+
+ State {
+ program,
+ cache,
+ primitive,
+ queued_events: Vec::new(),
+ queued_messages: Vec::new(),
+ }
+ }
+
+ /// Returns a reference to the [`Program`] of the [`State`].
+ pub fn program(&self) -> &P {
+ &self.program
+ }
+
+ /// Returns a reference to the current rendering primitive of the [`State`].
+ pub fn primitive(&self) -> &<P::Renderer as Renderer>::Output {
+ &self.primitive
+ }
+
+ /// Queues an event in the [`State`] for processing during an [`update`].
+ ///
+ /// [`update`]: Self::update
+ pub fn queue_event(&mut self, event: Event) {
+ self.queued_events.push(event);
+ }
+
+ /// Queues a message in the [`State`] for processing during an [`update`].
+ ///
+ /// [`update`]: Self::update
+ pub fn queue_message(&mut self, message: P::Message) {
+ self.queued_messages.push(message);
+ }
+
+ /// Returns whether the event queue of the [`State`] is empty or not.
+ pub fn is_queue_empty(&self) -> bool {
+ self.queued_events.is_empty() && self.queued_messages.is_empty()
+ }
+
+ /// Processes all the queued events and messages, rebuilding and redrawing
+ /// the widgets of the linked [`Program`] if necessary.
+ ///
+ /// Returns the [`Command`] obtained from [`Program`] after updating it,
+ /// only if an update was necessary.
+ pub fn update(
+ &mut self,
+ bounds: Size,
+ cursor_position: Point,
+ clipboard: Option<&dyn Clipboard>,
+ renderer: &mut P::Renderer,
+ debug: &mut Debug,
+ ) -> Option<Command<P::Message>> {
+ let mut user_interface = build_user_interface(
+ &mut self.program,
+ self.cache.take().unwrap(),
+ renderer,
+ bounds,
+ debug,
+ );
+
+ debug.event_processing_started();
+ let mut messages = Vec::new();
+
+ let _ = user_interface.update(
+ &self.queued_events,
+ cursor_position,
+ clipboard,
+ renderer,
+ &mut messages,
+ );
+
+ messages.extend(self.queued_messages.drain(..));
+ self.queued_events.clear();
+ debug.event_processing_finished();
+
+ if messages.is_empty() {
+ debug.draw_started();
+ self.primitive = user_interface.draw(renderer, cursor_position);
+ debug.draw_finished();
+
+ self.cache = Some(user_interface.into_cache());
+
+ None
+ } else {
+ // When there are messages, we are forced to rebuild twice
+ // for now :^)
+ let temp_cache = user_interface.into_cache();
+
+ let commands =
+ Command::batch(messages.into_iter().map(|message| {
+ debug.log_message(&message);
+
+ debug.update_started();
+ let command = self.program.update(message);
+ debug.update_finished();
+
+ command
+ }));
+
+ let mut user_interface = build_user_interface(
+ &mut self.program,
+ temp_cache,
+ renderer,
+ bounds,
+ debug,
+ );
+
+ debug.draw_started();
+ self.primitive = user_interface.draw(renderer, cursor_position);
+ debug.draw_finished();
+
+ self.cache = Some(user_interface.into_cache());
+
+ Some(commands)
+ }
+ }
+}
+
+fn build_user_interface<'a, P: Program>(
+ program: &'a mut P,
+ cache: Cache,
+ renderer: &mut P::Renderer,
+ size: Size,
+ debug: &mut Debug,
+) -> UserInterface<'a, P::Message, P::Renderer> {
+ debug.view_started();
+ let view = program.view();
+ debug.view_finished();
+
+ debug.layout_started();
+ let user_interface = UserInterface::build(view, size, cache, renderer);
+ debug.layout_finished();
+
+ user_interface
+}
diff --git a/native/src/renderer.rs b/native/src/renderer.rs
index a16df72b..39a6cff1 100644
--- a/native/src/renderer.rs
+++ b/native/src/renderer.rs
@@ -13,19 +13,19 @@
//! In the end, a __renderer__ satisfying all the constraints is
//! needed to build a [`UserInterface`].
//!
-//! [`Widget`]: ../widget/trait.Widget.html
-//! [`UserInterface`]: ../struct.UserInterface.html
-//! [`Text`]: ../widget/text/struct.Text.html
-//! [`text::Renderer`]: ../widget/text/trait.Renderer.html
-//! [`Checkbox`]: ../widget/checkbox/struct.Checkbox.html
-//! [`checkbox::Renderer`]: ../widget/checkbox/trait.Renderer.html
+//! [`Widget`]: crate::Widget
+//! [`UserInterface`]: crate::UserInterface
+//! [`Text`]: crate::widget::Text
+//! [`text::Renderer`]: crate::widget::text::Renderer
+//! [`Checkbox`]: crate::widget::Checkbox
+//! [`checkbox::Renderer`]: crate::widget::checkbox::Renderer
#[cfg(debug_assertions)]
mod null;
#[cfg(debug_assertions)]
pub use null::Null;
-use crate::{layout, Element};
+use crate::{layout, Element, Rectangle};
/// A component that can take the state of a user interface and produce an
/// output for its users.
@@ -34,15 +34,11 @@ pub trait Renderer: Sized {
///
/// If you are implementing a graphical renderer, your output will most
/// likely be a tree of visual primitives.
- ///
- /// [`Renderer`]: trait.Renderer.html
type Output;
/// The default styling attributes of the [`Renderer`].
///
/// This type can be leveraged to implement style inheritance.
- ///
- /// [`Renderer`]: trait.Renderer.html
type Defaults: Default;
/// Lays out the elements of a user interface.
@@ -56,4 +52,13 @@ pub trait Renderer: Sized {
) -> layout::Node {
element.layout(self, limits)
}
+
+ /// Overlays the `overlay` output with the given bounds on top of the `base`
+ /// output.
+ fn overlay(
+ &mut self,
+ base: Self::Output,
+ overlay: Self::Output,
+ overlay_bounds: Rectangle,
+ ) -> Self::Output;
}
diff --git a/native/src/renderer/null.rs b/native/src/renderer/null.rs
index 0fcce5ad..91ee9a28 100644
--- a/native/src/renderer/null.rs
+++ b/native/src/renderer/null.rs
@@ -1,7 +1,8 @@
use crate::{
- button, checkbox, column, progress_bar, radio, row, scrollable, slider,
- text, text_input, Color, Element, Font, HorizontalAlignment, Layout, Point,
- Rectangle, Renderer, Size, VerticalAlignment,
+ button, checkbox, column, container, pane_grid, progress_bar, radio, row,
+ scrollable, slider, text, text_input, Color, Element, Font,
+ HorizontalAlignment, Layout, Point, Rectangle, Renderer, Size,
+ VerticalAlignment,
};
/// A renderer that does nothing.
@@ -12,8 +13,6 @@ pub struct Null;
impl Null {
/// Creates a new [`Null`] renderer.
- ///
- /// [`Null`]: struct.Null.html
pub fn new() -> Null {
Null
}
@@ -22,6 +21,9 @@ impl Null {
impl Renderer for Null {
type Output = ();
type Defaults = ();
+
+ fn overlay(&mut self, _base: (), _overlay: (), _overlay_bounds: Rectangle) {
+ }
}
impl column::Renderer for Null {
@@ -31,6 +33,7 @@ impl column::Renderer for Null {
_content: &[Element<'_, Message, Self>],
_layout: Layout<'_>,
_cursor_position: Point,
+ _viewport: &Rectangle,
) {
}
}
@@ -42,12 +45,17 @@ impl row::Renderer for Null {
_content: &[Element<'_, Message, Self>],
_layout: Layout<'_>,
_cursor_position: Point,
+ _viewport: &Rectangle,
) {
}
}
impl text::Renderer for Null {
- const DEFAULT_SIZE: u16 = 20;
+ type Font = Font;
+
+ fn default_size(&self) -> u16 {
+ 20
+ }
fn measure(
&self,
@@ -81,6 +89,9 @@ impl scrollable::Renderer for Null {
_bounds: Rectangle,
_content_bounds: Rectangle,
_offset: u32,
+ _scrollbar_width: u16,
+ _scrollbar_margin: u16,
+ _scroller_width: u16,
) -> Option<scrollable::Scrollbar> {
None
}
@@ -103,10 +114,6 @@ impl scrollable::Renderer for Null {
impl text_input::Renderer for Null {
type Style = ();
- fn default_size(&self) -> u16 {
- 20
- }
-
fn measure_value(&self, _value: &str, _size: u16, _font: Font) -> f32 {
0.0
}
@@ -114,10 +121,10 @@ impl text_input::Renderer for Null {
fn offset(
&self,
_text_bounds: Rectangle,
+ _font: Font,
_size: u16,
_value: &text_input::Value,
_state: &text_input::State,
- _font: Font,
) -> f32 {
0.0
}
@@ -127,8 +134,8 @@ impl text_input::Renderer for Null {
_bounds: Rectangle,
_text_bounds: Rectangle,
_cursor_position: Point,
- _size: u16,
_font: Font,
+ _size: u16,
_placeholder: &str,
_value: &text_input::Value,
_state: &text_input::State,
@@ -159,9 +166,8 @@ impl button::Renderer for Null {
impl radio::Renderer for Null {
type Style = ();
- fn default_size(&self) -> u32 {
- 20
- }
+ const DEFAULT_SIZE: u16 = 20;
+ const DEFAULT_SPACING: u16 = 15;
fn draw(
&mut self,
@@ -194,9 +200,7 @@ impl checkbox::Renderer for Null {
impl slider::Renderer for Null {
type Style = ();
- fn height(&self) -> u32 {
- 30
- }
+ const DEFAULT_HEIGHT: u16 = 30;
fn draw(
&mut self,
@@ -224,3 +228,60 @@ impl progress_bar::Renderer for Null {
) {
}
}
+
+impl container::Renderer for Null {
+ type Style = ();
+
+ fn draw<Message>(
+ &mut self,
+ _defaults: &Self::Defaults,
+ _bounds: Rectangle,
+ _cursor_position: Point,
+ _viewport: &Rectangle,
+ _style: &Self::Style,
+ _content: &Element<'_, Message, Self>,
+ _content_layout: Layout<'_>,
+ ) {
+ }
+}
+
+impl pane_grid::Renderer for Null {
+ 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>,
+ _layout: Layout<'_>,
+ _cursor_position: Point,
+ ) {
+ }
+
+ fn draw_pane<Message>(
+ &mut self,
+ _defaults: &Self::Defaults,
+ _bounds: Rectangle,
+ _style: &Self::Style,
+ _title_bar: Option<(
+ &pane_grid::TitleBar<'_, Message, Self>,
+ Layout<'_>,
+ )>,
+ _body: (&Element<'_, Message, Self>, Layout<'_>),
+ _cursor_position: Point,
+ ) {
+ }
+
+ 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,
+ _controls: Option<(&Element<'_, Message, Self>, Layout<'_>)>,
+ _cursor_position: Point,
+ ) {
+ }
+}
diff --git a/native/src/runtime.rs b/native/src/runtime.rs
index 9fa031f4..5b0a6925 100644
--- a/native/src/runtime.rs
+++ b/native/src/runtime.rs
@@ -1,12 +1,18 @@
//! Run commands and subscriptions.
-use crate::{Event, Hasher};
+use crate::event::{self, Event};
+use crate::Hasher;
/// A native runtime with a generic executor and receiver of results.
///
/// It can be used by shells to easily spawn a [`Command`] or track a
/// [`Subscription`].
///
-/// [`Command`]: ../struct.Command.html
-/// [`Subscription`]: ../struct.Subscription.html
-pub type Runtime<Executor, Receiver, Message> =
- iced_futures::Runtime<Hasher, Event, Executor, Receiver, Message>;
+/// [`Command`]: crate::Command
+/// [`Subscription`]: crate::Subscription
+pub type Runtime<Executor, Receiver, Message> = iced_futures::Runtime<
+ Hasher,
+ (Event, event::Status),
+ Executor,
+ Receiver,
+ Message,
+>;
diff --git a/native/src/subscription.rs b/native/src/subscription.rs
index 0d002c6c..ff954382 100644
--- a/native/src/subscription.rs
+++ b/native/src/subscription.rs
@@ -1,5 +1,6 @@
//! Listen to external events in your application.
-use crate::{Event, Hasher};
+use crate::event::{self, Event};
+use crate::Hasher;
use iced_futures::futures::stream::BoxStream;
/// A request to listen to external events.
@@ -13,21 +14,18 @@ use iced_futures::futures::stream::BoxStream;
/// For instance, you can use a [`Subscription`] to listen to a WebSocket
/// connection, keyboard presses, mouse events, time ticks, etc.
///
-/// [`Command`]: ../struct.Command.html
-/// [`Subscription`]: struct.Subscription.html
-pub type Subscription<T> = iced_futures::Subscription<Hasher, Event, T>;
+/// [`Command`]: crate::Command
+pub type Subscription<T> =
+ iced_futures::Subscription<Hasher, (Event, event::Status), T>;
/// A stream of runtime events.
///
/// It is the input of a [`Subscription`] in the native runtime.
-///
-/// [`Subscription`]: type.Subscription.html
-pub type EventStream = BoxStream<'static, Event>;
+pub type EventStream = BoxStream<'static, (Event, event::Status)>;
/// A native [`Subscription`] tracker.
-///
-/// [`Subscription`]: type.Subscription.html
-pub type Tracker = iced_futures::subscription::Tracker<Hasher, Event>;
+pub type Tracker =
+ iced_futures::subscription::Tracker<Hasher, (Event, event::Status)>;
pub use iced_futures::subscription::Recipe;
@@ -37,11 +35,30 @@ use events::Events;
/// Returns a [`Subscription`] to all the runtime events.
///
-/// This subscription will notify your application of any [`Event`] handled by
-/// the runtime.
-///
-/// [`Subscription`]: type.Subscription.html
-/// [`Event`]: ../enum.Event.html
+/// This subscription will notify your application of any [`Event`] that was
+/// not captured by any widget.
pub fn events() -> Subscription<Event> {
- Subscription::from_recipe(Events)
+ Subscription::from_recipe(Events {
+ f: |event, status| match status {
+ event::Status::Ignored => Some(event),
+ event::Status::Captured => None,
+ },
+ })
+}
+
+/// Returns a [`Subscription`] that filters all the runtime events with the
+/// provided function, producing messages accordingly.
+///
+/// This subscription will call the provided function for every [`Event`]
+/// handled by the runtime. If the function:
+///
+/// - Returns `None`, the [`Event`] will be discarded.
+/// - Returns `Some` message, the `Message` will be produced.
+pub fn events_with<Message>(
+ f: fn(Event, event::Status) -> Option<Message>,
+) -> Subscription<Message>
+where
+ Message: 'static + Send,
+{
+ Subscription::from_recipe(Events { f })
}
diff --git a/native/src/subscription/events.rs b/native/src/subscription/events.rs
index 7d33166e..f689f3af 100644
--- a/native/src/subscription/events.rs
+++ b/native/src/subscription/events.rs
@@ -1,24 +1,36 @@
-use crate::{
- subscription::{EventStream, Recipe},
- Event, Hasher,
-};
-use iced_futures::futures::stream::BoxStream;
+use crate::event::{self, Event};
+use crate::subscription::{EventStream, Recipe};
+use crate::Hasher;
+use iced_futures::futures::future;
+use iced_futures::futures::StreamExt;
+use iced_futures::BoxStream;
-pub struct Events;
+pub struct Events<Message> {
+ pub(super) f: fn(Event, event::Status) -> Option<Message>,
+}
-impl Recipe<Hasher, Event> for Events {
- type Output = Event;
+impl<Message> Recipe<Hasher, (Event, event::Status)> for Events<Message>
+where
+ Message: 'static + Send,
+{
+ type Output = Message;
fn hash(&self, state: &mut Hasher) {
use std::hash::Hash;
- std::any::TypeId::of::<Self>().hash(state);
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+ self.f.hash(state);
}
fn stream(
self: Box<Self>,
event_stream: EventStream,
- ) -> BoxStream<'static, Self::Output> {
+ ) -> BoxStream<Self::Output> {
event_stream
+ .filter_map(move |(event, status)| {
+ future::ready((self.f)(event, status))
+ })
+ .boxed()
}
}
diff --git a/native/src/input/touch.rs b/native/src/touch.rs
index ea879427..88bd83bb 100644
--- a/native/src/input/touch.rs
+++ b/native/src/touch.rs
@@ -1,5 +1,4 @@
//! Build touch events.
-
use crate::Point;
/// A touch interaction.
diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs
index b71b9003..7a64ac59 100644
--- a/native/src/user_interface.rs
+++ b/native/src/user_interface.rs
@@ -1,7 +1,7 @@
-use crate::{
- input::{mouse, Touch},
- layout, Clipboard, Element, Event, Layout, Point, Size,
-};
+use crate::event::{self, Event};
+use crate::layout;
+use crate::overlay;
+use crate::{Clipboard, Element, Layout, Point, Rectangle, Size};
use std::hash::Hasher;
@@ -12,14 +12,17 @@ use std::hash::Hasher;
/// Iced tries to avoid dictating how to write your event loop. You are in
/// charge of using this type in your system in any way you want.
///
-/// [`Layout`]: struct.Layout.html
+/// # Example
+/// 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
#[allow(missing_debug_implementations)]
pub struct UserInterface<'a, Message, Renderer> {
- hash: u64,
root: Element<'a, Message, Renderer>,
- layout: layout::Node,
+ base: Layer,
+ overlay: Option<Layer>,
bounds: Size,
- cursor_position: Point,
}
impl<'a, Message, Renderer> UserInterface<'a, Message, Renderer>
@@ -31,10 +34,6 @@ where
/// It is able to avoid expensive computations when using a [`Cache`]
/// obtained from a previous instance of a [`UserInterface`].
///
- /// [`Element`]: struct.Element.html
- /// [`Cache`]: struct.Cache.html
- /// [`UserInterface`]: struct.UserInterface.html
- ///
/// # Example
/// Imagine we want to build a [`UserInterface`] for
/// [the counter example that we previously wrote](index.html#usage). Here
@@ -91,25 +90,37 @@ where
) -> Self {
let root = root.into();
- let hash = {
- let hasher = &mut crate::Hasher::default();
- root.hash_layout(hasher);
+ let (base, overlay) = {
+ let hash = {
+ let hasher = &mut crate::Hasher::default();
+ root.hash_layout(hasher);
- hasher.finish()
- };
+ hasher.finish()
+ };
- let layout = if hash == cache.hash && bounds == cache.bounds {
- cache.layout
- } else {
- renderer.layout(&root, &layout::Limits::new(Size::ZERO, bounds))
+ let layout_is_cached =
+ hash == cache.base.hash && bounds == cache.bounds;
+
+ let (layout, overlay) = if layout_is_cached {
+ (cache.base.layout, cache.overlay)
+ } else {
+ (
+ renderer.layout(
+ &root,
+ &layout::Limits::new(Size::ZERO, bounds),
+ ),
+ None,
+ )
+ };
+
+ (Layer { layout, hash }, overlay)
};
UserInterface {
- hash,
root,
- layout,
+ base,
+ overlay,
bounds,
- cursor_position: cache.cursor_position,
}
}
@@ -118,15 +129,12 @@ where
/// It returns __messages__ that may have been produced as a result of user
/// interactions. You should feed these to your __update logic__.
///
- /// [`UserInterface`]: struct.UserInterface.html
- /// [`Event`]: enum.Event.html
- ///
/// # Example
/// Let's allow our [counter](index.html#usage) to change state by
/// completing [the previous example](#example):
///
/// ```no_run
- /// use iced_native::{UserInterface, Cache, Size};
+ /// use iced_native::{UserInterface, Cache, Size, Point};
/// use iced_wgpu::Renderer;
///
/// # mod iced_wgpu {
@@ -148,12 +156,14 @@ where
/// let mut cache = Cache::new();
/// let mut renderer = Renderer::new();
/// let mut window_size = Size::new(1024.0, 768.0);
+ /// let mut cursor_position = Point::default();
///
/// // Initialize our event storage
/// let mut events = Vec::new();
+ /// let mut messages = Vec::new();
///
/// loop {
- /// // Process system events...
+ /// // Obtain system events...
///
/// let mut user_interface = UserInterface::build(
/// counter.view(),
@@ -163,61 +173,103 @@ where
/// );
///
/// // Update the user interface
- /// let messages = user_interface.update(events.drain(..), None, &renderer);
+ /// let event_statuses = user_interface.update(
+ /// &events,
+ /// cursor_position,
+ /// None,
+ /// &renderer,
+ /// &mut messages
+ /// );
///
/// cache = user_interface.into_cache();
///
/// // Process the produced messages
- /// for message in messages {
+ /// for message in messages.drain(..) {
/// counter.update(message);
/// }
/// }
/// ```
pub fn update(
&mut self,
- events: impl IntoIterator<Item = Event>,
+ events: &[Event],
+ cursor_position: Point,
clipboard: Option<&dyn Clipboard>,
renderer: &Renderer,
- ) -> Vec<Message> {
- let mut messages = Vec::new();
-
- for event in events {
- match event {
- Event::Mouse(mouse::Event::CursorMoved { position })
- | Event::Touch(Touch { position, .. }) => {
- self.cursor_position = position;
- }
- _ => {}
- }
-
- self.root.widget.on_event(
- event,
- Layout::new(&self.layout),
- self.cursor_position,
- &mut messages,
+ messages: &mut Vec<Message>,
+ ) -> Vec<event::Status> {
+ let (base_cursor, overlay_statuses) = if let Some(mut overlay) =
+ self.root.overlay(Layout::new(&self.base.layout))
+ {
+ let layer = Self::overlay_layer(
+ self.overlay.take(),
+ self.bounds,
+ &mut overlay,
renderer,
- clipboard,
);
- }
- messages
+ let event_statuses = events
+ .iter()
+ .cloned()
+ .map(|event| {
+ overlay.on_event(
+ event,
+ Layout::new(&layer.layout),
+ cursor_position,
+ messages,
+ renderer,
+ clipboard,
+ )
+ })
+ .collect();
+
+ let base_cursor = if layer.layout.bounds().contains(cursor_position)
+ {
+ // TODO: Type-safe cursor availability
+ Point::new(-1.0, -1.0)
+ } else {
+ cursor_position
+ };
+
+ self.overlay = Some(layer);
+
+ (base_cursor, event_statuses)
+ } else {
+ (cursor_position, vec![event::Status::Ignored; events.len()])
+ };
+
+ events
+ .iter()
+ .cloned()
+ .zip(overlay_statuses.into_iter())
+ .map(|(event, overlay_status)| {
+ let event_status = self.root.widget.on_event(
+ event,
+ Layout::new(&self.base.layout),
+ base_cursor,
+ messages,
+ renderer,
+ clipboard,
+ );
+
+ event_status.merge(overlay_status)
+ })
+ .collect()
}
/// Draws the [`UserInterface`] with the provided [`Renderer`].
///
- /// It returns the current state of the [`MouseCursor`]. You should update
- /// the icon of the mouse cursor accordingly in your system.
+ /// It returns the some [`Renderer::Output`]. You should update the icon of
+ /// the mouse cursor accordingly in your system.
///
- /// [`UserInterface`]: struct.UserInterface.html
- /// [`Renderer`]: trait.Renderer.html
- /// [`MouseCursor`]: enum.MouseCursor.html
+ /// [`Renderer`]: crate::Renderer
+ /// [`Renderer::Output`]: crate::Renderer::Output
///
/// # Example
/// We can finally draw our [counter](index.html#usage) by
/// [completing the last example](#example-1):
///
/// ```no_run
- /// use iced_native::{UserInterface, Cache, Size};
+ /// use iced_native::{UserInterface, Cache, Size, Point};
/// use iced_wgpu::Renderer;
///
/// # mod iced_wgpu {
@@ -239,10 +291,12 @@ where
/// let mut cache = Cache::new();
/// let mut renderer = Renderer::new();
/// let mut window_size = Size::new(1024.0, 768.0);
+ /// let mut cursor_position = Point::default();
/// let mut events = Vec::new();
+ /// let mut messages = Vec::new();
///
/// loop {
- /// // Process system events...
+ /// // Obtain system events...
///
/// let mut user_interface = UserInterface::build(
/// counter.view(),
@@ -251,14 +305,21 @@ where
/// &mut renderer,
/// );
///
- /// let messages = user_interface.update(events.drain(..), None, &renderer);
+ /// // Update the user interface
+ /// let event_statuses = user_interface.update(
+ /// &events,
+ /// cursor_position,
+ /// None,
+ /// &renderer,
+ /// &mut messages
+ /// );
///
/// // Draw the user interface
- /// let mouse_cursor = user_interface.draw(&mut renderer);
+ /// let mouse_cursor = user_interface.draw(&mut renderer, cursor_position);
///
/// cache = user_interface.into_cache();
///
- /// for message in messages {
+ /// for message in messages.drain(..) {
/// counter.update(message);
/// }
///
@@ -266,39 +327,132 @@ where
/// // Flush rendering operations...
/// }
/// ```
- pub fn draw(&self, renderer: &mut Renderer) -> Renderer::Output {
- self.root.widget.draw(
+ pub fn draw(
+ &mut self,
+ renderer: &mut Renderer,
+ cursor_position: Point,
+ ) -> Renderer::Output {
+ let viewport = Rectangle::with_size(self.bounds);
+
+ let overlay = if let Some(mut overlay) =
+ self.root.overlay(Layout::new(&self.base.layout))
+ {
+ let layer = Self::overlay_layer(
+ self.overlay.take(),
+ self.bounds,
+ &mut overlay,
+ renderer,
+ );
+
+ let overlay_bounds = layer.layout.bounds();
+
+ let overlay_primitives = overlay.draw(
+ renderer,
+ &Renderer::Defaults::default(),
+ Layout::new(&layer.layout),
+ cursor_position,
+ );
+
+ self.overlay = Some(layer);
+
+ Some((overlay_primitives, overlay_bounds))
+ } else {
+ None
+ };
+
+ if let Some((overlay_primitives, overlay_bounds)) = overlay {
+ let base_cursor = if overlay_bounds.contains(cursor_position) {
+ Point::new(-1.0, -1.0)
+ } else {
+ cursor_position
+ };
+
+ let base_primitives = self.root.widget.draw(
+ renderer,
+ &Renderer::Defaults::default(),
+ Layout::new(&self.base.layout),
+ base_cursor,
+ &viewport,
+ );
+
+ renderer.overlay(
+ base_primitives,
+ overlay_primitives,
+ overlay_bounds,
+ )
+ } else {
+ self.root.widget.draw(
+ renderer,
+ &Renderer::Defaults::default(),
+ Layout::new(&self.base.layout),
+ cursor_position,
+ &viewport,
+ )
+ }
+ }
+
+ /// Relayouts and returns a new [`UserInterface`] using the provided
+ /// bounds.
+ pub fn relayout(self, bounds: Size, renderer: &mut Renderer) -> Self {
+ Self::build(
+ self.root,
+ bounds,
+ Cache {
+ base: self.base,
+ overlay: self.overlay,
+ bounds: self.bounds,
+ },
renderer,
- &Renderer::Defaults::default(),
- Layout::new(&self.layout),
- self.cursor_position,
)
}
/// Extract the [`Cache`] of the [`UserInterface`], consuming it in the
/// process.
- ///
- /// [`Cache`]: struct.Cache.html
- /// [`UserInterface`]: struct.UserInterface.html
pub fn into_cache(self) -> Cache {
Cache {
- hash: self.hash,
- layout: self.layout,
+ base: self.base,
+ overlay: self.overlay,
bounds: self.bounds,
- cursor_position: self.cursor_position,
+ }
+ }
+
+ fn overlay_layer(
+ cache: Option<Layer>,
+ bounds: Size,
+ overlay: &mut overlay::Element<'_, Message, Renderer>,
+ renderer: &Renderer,
+ ) -> Layer {
+ let new_hash = {
+ let hasher = &mut crate::Hasher::default();
+ overlay.hash_layout(hasher);
+
+ hasher.finish()
+ };
+
+ let layout = match cache {
+ Some(Layer { hash, layout }) if new_hash == hash => layout,
+ _ => overlay.layout(renderer, bounds),
+ };
+
+ Layer {
+ layout,
+ hash: new_hash,
}
}
}
+#[derive(Debug, Clone)]
+struct Layer {
+ layout: layout::Node,
+ hash: u64,
+}
+
/// Reusable data of a specific [`UserInterface`].
-///
-/// [`UserInterface`]: struct.UserInterface.html
#[derive(Debug, Clone)]
pub struct Cache {
- hash: u64,
- layout: layout::Node,
+ base: Layer,
+ overlay: Option<Layer>,
bounds: Size,
- cursor_position: Point,
}
impl Cache {
@@ -306,15 +460,14 @@ impl Cache {
///
/// You should use this to initialize a [`Cache`] before building your first
/// [`UserInterface`].
- ///
- /// [`Cache`]: struct.Cache.html
- /// [`UserInterface`]: struct.UserInterface.html
pub fn new() -> Cache {
Cache {
- hash: 0,
- layout: layout::Node::new(Size::new(0.0, 0.0)),
+ base: Layer {
+ layout: layout::Node::new(Size::new(0.0, 0.0)),
+ hash: 0,
+ },
+ overlay: None,
bounds: Size::ZERO,
- cursor_position: Point::new(-1.0, -1.0),
}
}
}
@@ -324,11 +477,3 @@ impl Default for Cache {
Cache::new()
}
}
-
-impl PartialEq for Cache {
- fn eq(&self, other: &Cache) -> bool {
- self.hash == other.hash && self.cursor_position == other.cursor_position
- }
-}
-
-impl Eq for Cache {}
diff --git a/native/src/widget.rs b/native/src/widget.rs
index f9424b02..3677713a 100644
--- a/native/src/widget.rs
+++ b/native/src/widget.rs
@@ -18,16 +18,18 @@
//! use iced_native::{button, Button, Widget};
//! ```
//!
-//! [`Widget`]: trait.Widget.html
-//! [renderer]: ../renderer/index.html
+//! [renderer]: crate::renderer
pub mod button;
pub mod checkbox;
pub mod column;
pub mod container;
pub mod image;
+pub mod pane_grid;
+pub mod pick_list;
pub mod progress_bar;
pub mod radio;
pub mod row;
+pub mod rule;
pub mod scrollable;
pub mod slider;
pub mod space;
@@ -46,12 +48,18 @@ pub use container::Container;
#[doc(no_inline)]
pub use image::Image;
#[doc(no_inline)]
+pub use pane_grid::PaneGrid;
+#[doc(no_inline)]
+pub use pick_list::PickList;
+#[doc(no_inline)]
pub use progress_bar::ProgressBar;
#[doc(no_inline)]
pub use radio::Radio;
#[doc(no_inline)]
pub use row::Row;
#[doc(no_inline)]
+pub use rule::Rule;
+#[doc(no_inline)]
pub use scrollable::Scrollable;
#[doc(no_inline)]
pub use slider::Slider;
@@ -64,27 +72,41 @@ pub use text::Text;
#[doc(no_inline)]
pub use text_input::TextInput;
-use crate::{layout, Clipboard, Event, Hasher, Layout, Length, Point};
+use crate::event::{self, Event};
+use crate::layout;
+use crate::overlay;
+use crate::{Clipboard, Hasher, Layout, Length, Point, Rectangle};
/// A component that displays information and allows interaction.
///
/// If you want to build your own widgets, you will need to implement this
/// trait.
///
-/// [`Widget`]: trait.Widget.html
-/// [`Element`]: ../struct.Element.html
+/// # Examples
+/// The repository has some [examples] showcasing how to implement a custom
+/// widget:
+///
+/// - [`bezier_tool`], a Paint-like tool for drawing Bézier curves using
+/// [`lyon`].
+/// - [`custom_widget`], a demonstration of how to build a custom widget that
+/// draws a circle.
+/// - [`geometry`], a custom widget showcasing how to draw geometry with the
+/// `Mesh2D` primitive in [`iced_wgpu`].
+///
+/// [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
+/// [`lyon`]: https://github.com/nical/lyon
+/// [`iced_wgpu`]: https://github.com/hecrj/iced/tree/0.2/wgpu
pub trait Widget<Message, Renderer>
where
Renderer: crate::Renderer,
{
/// Returns the width of the [`Widget`].
- ///
- /// [`Widget`]: trait.Widget.html
fn width(&self) -> Length;
/// Returns the height of the [`Widget`].
- ///
- /// [`Widget`]: trait.Widget.html
fn height(&self) -> Length;
/// Returns the [`Node`] of the [`Widget`].
@@ -92,9 +114,7 @@ where
/// This [`Node`] is used by the runtime to compute the [`Layout`] of the
/// user interface.
///
- /// [`Node`]: ../layout/struct.Node.html
- /// [`Widget`]: trait.Widget.html
- /// [`Layout`]: ../layout/struct.Layout.html
+ /// [`Node`]: layout::Node
fn layout(
&self,
renderer: &Renderer,
@@ -102,14 +122,13 @@ where
) -> layout::Node;
/// Draws the [`Widget`] using the associated `Renderer`.
- ///
- /// [`Widget`]: trait.Widget.html
fn draw(
&self,
renderer: &mut Renderer,
defaults: &Renderer::Defaults,
layout: Layout<'_>,
cursor_position: Point,
+ viewport: &Rectangle,
) -> Renderer::Output;
/// Computes the _layout_ hash of the [`Widget`].
@@ -122,9 +141,7 @@ where
/// For example, the [`Text`] widget does not hash its color property, as
/// its value cannot affect the overall [`Layout`] of the user interface.
///
- /// [`Widget`]: trait.Widget.html
- /// [`Layout`]: ../layout/struct.Layout.html
- /// [`Text`]: text/struct.Text.html
+ /// [`Text`]: crate::widget::Text
fn hash_layout(&self, state: &mut Hasher);
/// Processes a runtime [`Event`].
@@ -136,12 +153,9 @@ where
/// * a mutable `Message` list, allowing the [`Widget`] to produce
/// new messages based on user interaction.
/// * the `Renderer`
+ /// * a [`Clipboard`], if available
///
/// By default, it does nothing.
- ///
- /// [`Event`]: ../enum.Event.html
- /// [`Widget`]: trait.Widget.html
- /// [`Layout`]: ../layout/struct.Layout.html
fn on_event(
&mut self,
_event: Event,
@@ -150,6 +164,15 @@ where
_messages: &mut Vec<Message>,
_renderer: &Renderer,
_clipboard: Option<&dyn Clipboard>,
- ) {
+ ) -> event::Status {
+ event::Status::Ignored
+ }
+
+ /// Returns the overlay of the [`Widget`], if there is any.
+ fn overlay(
+ &mut self,
+ _layout: Layout<'_>,
+ ) -> Option<overlay::Element<'_, Message, Renderer>> {
+ None
}
}
diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs
index 81dbe7c5..7d5eb30c 100644
--- a/native/src/widget/button.rs
+++ b/native/src/widget/button.rs
@@ -1,13 +1,12 @@
//! Allow your users to perform actions by pressing a button.
//!
//! A [`Button`] has some local [`State`].
-//!
-//! [`Button`]: struct.Button.html
-//! [`State`]: struct.State.html
+use crate::event::{self, Event};
+use crate::layout;
+use crate::mouse;
+use crate::touch::{self, Touch};
use crate::{
- input::{mouse, touch, ButtonState, Touch},
- layout, Clipboard, Element, Event, Hasher, Layout, Length, Point,
- Rectangle, Widget,
+ Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget,
};
use std::hash::Hash;
@@ -19,6 +18,7 @@ use std::hash::Hash;
/// # type Button<'a, Message> =
/// # iced_native::Button<'a, Message, iced_native::renderer::Null>;
/// #
+/// #[derive(Clone)]
/// enum Message {
/// ButtonPressed,
/// }
@@ -42,13 +42,11 @@ pub struct Button<'a, Message, Renderer: self::Renderer> {
impl<'a, Message, Renderer> Button<'a, Message, Renderer>
where
+ Message: Clone,
Renderer: self::Renderer,
{
/// Creates a new [`Button`] with some local [`State`] and the given
/// content.
- ///
- /// [`Button`]: struct.Button.html
- /// [`State`]: struct.State.html
pub fn new<E>(state: &'a mut State, content: E) -> Self
where
E: Into<Element<'a, Message, Renderer>>,
@@ -67,56 +65,42 @@ where
}
/// Sets the width of the [`Button`].
- ///
- /// [`Button`]: struct.Button.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`Button`].
- ///
- /// [`Button`]: struct.Button.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
/// Sets the minimum width of the [`Button`].
- ///
- /// [`Button`]: struct.Button.html
pub fn min_width(mut self, min_width: u32) -> Self {
self.min_width = min_width;
self
}
/// Sets the minimum height of the [`Button`].
- ///
- /// [`Button`]: struct.Button.html
pub fn min_height(mut self, min_height: u32) -> Self {
self.min_height = min_height;
self
}
/// Sets the padding of the [`Button`].
- ///
- /// [`Button`]: struct.Button.html
pub fn padding(mut self, padding: u16) -> Self {
self.padding = padding;
self
}
/// Sets the message that will be produced when the [`Button`] is pressed.
- ///
- /// [`Button`]: struct.Button.html
pub fn on_press(mut self, msg: Message) -> Self {
self.on_press = Some(msg);
self
}
/// Sets the style of the [`Button`].
- ///
- /// [`Button`]: struct.Button.html
pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {
self.style = style.into();
self
@@ -124,8 +108,6 @@ where
}
/// The local state of a [`Button`].
-///
-/// [`Button`]: struct.Button.html
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct State {
is_pressed: bool,
@@ -133,8 +115,6 @@ pub struct State {
impl State {
/// Creates a new [`State`].
- ///
- /// [`State`]: struct.State.html
pub fn new() -> State {
State::default()
}
@@ -143,8 +123,8 @@ impl State {
impl<'a, Message, Renderer> Widget<Message, Renderer>
for Button<'a, Message, Renderer>
where
- Renderer: self::Renderer,
Message: Clone,
+ Renderer: self::Renderer,
{
fn width(&self) -> Length {
self.width
@@ -183,12 +163,9 @@ where
messages: &mut Vec<Message>,
_renderer: &Renderer,
_clipboard: Option<&dyn Clipboard>,
- ) {
+ ) -> event::Status {
match event {
- Event::Mouse(mouse::Event::Input {
- button: mouse::Button::Left,
- state: ButtonState::Pressed,
- })
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(Touch {
phase: touch::Phase::Started,
..
@@ -196,26 +173,29 @@ where
if self.on_press.is_some() {
let bounds = layout.bounds();
- self.state.is_pressed = bounds.contains(cursor_position);
+ if bounds.contains(cursor_position) {
+ self.state.is_pressed = true;
+
+ return event::Status::Captured;
+ }
}
}
- Event::Mouse(mouse::Event::Input {
- button: mouse::Button::Left,
- state: ButtonState::Released,
- })
+ Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(Touch {
phase: touch::Phase::Ended,
..
}) => {
if let Some(on_press) = self.on_press.clone() {
let bounds = layout.bounds();
- let is_clicked = self.state.is_pressed
- && bounds.contains(cursor_position);
- self.state.is_pressed = false;
+ if self.state.is_pressed {
+ self.state.is_pressed = false;
+
+ if bounds.contains(cursor_position) {
+ messages.push(on_press);
+ }
- if is_clicked {
- messages.push(on_press);
+ return event::Status::Captured;
}
}
}
@@ -227,6 +207,8 @@ where
}
_ => {}
}
+
+ event::Status::Ignored
}
fn draw(
@@ -235,6 +217,7 @@ where
defaults: &Renderer::Defaults,
layout: Layout<'_>,
cursor_position: Point,
+ _viewport: &Rectangle,
) -> Renderer::Output {
renderer.draw(
defaults,
@@ -249,6 +232,9 @@ where
}
fn hash_layout(&self, state: &mut Hasher) {
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
self.width.hash(state);
self.content.hash_layout(state);
}
@@ -259,20 +245,15 @@ where
/// Your [renderer] will need to implement this trait before being
/// able to use a [`Button`] in your user interface.
///
-/// [`Button`]: struct.Button.html
-/// [renderer]: ../../renderer/index.html
+/// [renderer]: crate::renderer
pub trait Renderer: crate::Renderer + Sized {
/// The default padding of a [`Button`].
- ///
- /// [`Button`]: struct.Button.html
const DEFAULT_PADDING: u16;
/// The style supported by this renderer.
type Style: Default;
/// Draws a [`Button`].
- ///
- /// [`Button`]: struct.Button.html
fn draw<Message>(
&mut self,
defaults: &Self::Defaults,
@@ -289,8 +270,8 @@ pub trait Renderer: crate::Renderer + Sized {
impl<'a, Message, Renderer> From<Button<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
- Renderer: 'static + self::Renderer,
- Message: 'static + Clone,
+ Message: 'a + Clone,
+ Renderer: 'a + self::Renderer,
{
fn from(
button: Button<'a, Message, Renderer>,
diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs
index 7b2345de..92175b25 100644
--- a/native/src/widget/checkbox.rs
+++ b/native/src/widget/checkbox.rs
@@ -1,11 +1,15 @@
//! Show toggle controls using checkboxes.
use std::hash::Hash;
+use crate::event::{self, Event};
+use crate::layout;
+use crate::mouse;
+use crate::row;
+use crate::text;
+use crate::touch::{self, Touch};
use crate::{
- input::{mouse, touch, ButtonState, Touch},
- layout, row, text, Align, Clipboard, Element, Event, Font, Hasher,
- HorizontalAlignment, Layout, Length, Point, Rectangle, Row, Text,
- VerticalAlignment, Widget,
+ Align, Clipboard, Element, Hasher, HorizontalAlignment, Layout, Length,
+ Point, Rectangle, Row, Text, VerticalAlignment, Widget,
};
/// A box that can be checked.
@@ -33,7 +37,8 @@ pub struct Checkbox<Message, Renderer: self::Renderer + text::Renderer> {
width: Length,
size: u16,
spacing: u16,
- text_size: u16,
+ text_size: Option<u16>,
+ font: Renderer::Font,
style: Renderer::Style,
}
@@ -48,59 +53,56 @@ impl<Message, Renderer: self::Renderer + text::Renderer>
/// * a function that will be called when the [`Checkbox`] is toggled. It
/// will receive the new state of the [`Checkbox`] and must produce a
/// `Message`.
- ///
- /// [`Checkbox`]: struct.Checkbox.html
- pub fn new<F>(is_checked: bool, label: &str, f: F) -> Self
+ pub fn new<F>(is_checked: bool, label: impl Into<String>, f: F) -> Self
where
F: 'static + Fn(bool) -> Message,
{
Checkbox {
is_checked,
on_toggle: Box::new(f),
- label: String::from(label),
+ label: label.into(),
width: Length::Shrink,
size: <Renderer as self::Renderer>::DEFAULT_SIZE,
spacing: Renderer::DEFAULT_SPACING,
- text_size: <Renderer as text::Renderer>::DEFAULT_SIZE,
+ text_size: None,
+ font: Renderer::Font::default(),
style: Renderer::Style::default(),
}
}
/// Sets the size of the [`Checkbox`].
- ///
- /// [`Checkbox`]: struct.Checkbox.html
pub fn size(mut self, size: u16) -> Self {
self.size = size;
self
}
/// Sets the width of the [`Checkbox`].
- ///
- /// [`Checkbox`]: struct.Checkbox.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the spacing between the [`Checkbox`] and the text.
- ///
- /// [`Checkbox`]: struct.Checkbox.html
pub fn spacing(mut self, spacing: u16) -> Self {
self.spacing = spacing;
self
}
/// Sets the text size of the [`Checkbox`].
- ///
- /// [`Checkbox`]: struct.Checkbox.html
pub fn text_size(mut self, text_size: u16) -> Self {
- self.text_size = text_size;
+ self.text_size = Some(text_size);
self
}
- /// Sets the style of the [`Checkbox`].
+ /// Sets the [`Font`] of the text of the [`Checkbox`].
///
- /// [`Checkbox`]: struct.Checkbox.html
+ /// [`Font`]: crate::widget::text::Renderer::Font
+ pub fn font(mut self, font: Renderer::Font) -> Self {
+ self.font = font;
+ self
+ }
+
+ /// Sets the style of the [`Checkbox`].
pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {
self.style = style.into();
self
@@ -136,8 +138,9 @@ where
)
.push(
Text::new(&self.label)
+ .font(self.font)
.width(self.width)
- .size(self.text_size),
+ .size(self.text_size.unwrap_or(renderer.default_size())),
)
.layout(renderer, limits)
}
@@ -150,12 +153,9 @@ where
messages: &mut Vec<Message>,
_renderer: &Renderer,
_clipboard: Option<&dyn Clipboard>,
- ) {
+ ) -> event::Status {
match event {
- Event::Mouse(mouse::Event::Input {
- button: mouse::Button::Left,
- state: ButtonState::Pressed,
- })
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(Touch {
phase: touch::Phase::Started,
..
@@ -164,10 +164,14 @@ where
if mouse_over {
messages.push((self.on_toggle)(!self.is_checked));
+
+ return event::Status::Captured;
}
}
_ => {}
}
+
+ event::Status::Ignored
}
fn draw(
@@ -176,6 +180,7 @@ where
defaults: &Renderer::Defaults,
layout: Layout<'_>,
cursor_position: Point,
+ _viewport: &Rectangle,
) -> Renderer::Output {
let bounds = layout.bounds();
let mut children = layout.children();
@@ -189,8 +194,8 @@ where
defaults,
label_layout.bounds(),
&self.label,
- self.text_size,
- Font::Default,
+ self.text_size.unwrap_or(renderer.default_size()),
+ self.font,
None,
HorizontalAlignment::Left,
VerticalAlignment::Center,
@@ -209,6 +214,9 @@ where
}
fn hash_layout(&self, state: &mut Hasher) {
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
self.label.hash(state);
}
}
@@ -218,20 +226,15 @@ where
/// Your [renderer] will need to implement this trait before being
/// able to use a [`Checkbox`] in your user interface.
///
-/// [`Checkbox`]: struct.Checkbox.html
-/// [renderer]: ../../renderer/index.html
+/// [renderer]: crate::Renderer
pub trait Renderer: crate::Renderer {
/// The style supported by this renderer.
type Style: Default;
/// The default size of a [`Checkbox`].
- ///
- /// [`Checkbox`]: struct.Checkbox.html
const DEFAULT_SIZE: u16;
/// The default spacing of a [`Checkbox`].
- ///
- /// [`Checkbox`]: struct.Checkbox.html
const DEFAULT_SPACING: u16;
/// Draws a [`Checkbox`].
@@ -241,8 +244,6 @@ pub trait Renderer: crate::Renderer {
/// * whether the [`Checkbox`] is selected or not
/// * whether the mouse is over the [`Checkbox`] or not
/// * the drawn label of the [`Checkbox`]
- ///
- /// [`Checkbox`]: struct.Checkbox.html
fn draw(
&mut self,
bounds: Rectangle,
@@ -256,8 +257,8 @@ pub trait Renderer: crate::Renderer {
impl<'a, Message, Renderer> From<Checkbox<Message, Renderer>>
for Element<'a, Message, Renderer>
where
- Renderer: 'static + self::Renderer + text::Renderer + row::Renderer,
- Message: 'static,
+ Renderer: 'a + self::Renderer + text::Renderer + row::Renderer,
+ Message: 'a,
{
fn from(
checkbox: Checkbox<Message, Renderer>,
diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs
index a7a6f242..e0e88d31 100644
--- a/native/src/widget/column.rs
+++ b/native/src/widget/column.rs
@@ -1,18 +1,16 @@
//! Distribute content vertically.
use std::hash::Hash;
+use crate::event::{self, Event};
+use crate::layout;
+use crate::overlay;
use crate::{
- layout, Align, Clipboard, Element, Event, Hasher, Layout, Length, Point,
- Widget,
+ Align, Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget,
};
use std::u32;
/// A container that distributes its contents vertically.
-///
-/// A [`Column`] will try to fill the horizontal space of its container.
-///
-/// [`Column`]: struct.Column.html
#[allow(missing_debug_implementations)]
pub struct Column<'a, Message, Renderer> {
spacing: u16,
@@ -27,15 +25,11 @@ pub struct Column<'a, Message, Renderer> {
impl<'a, Message, Renderer> Column<'a, Message, Renderer> {
/// Creates an empty [`Column`].
- ///
- /// [`Column`]: struct.Column.html
pub fn new() -> Self {
Self::with_children(Vec::new())
}
/// Creates a [`Column`] with the given elements.
- ///
- /// [`Column`]: struct.Column.html
pub fn with_children(
children: Vec<Element<'a, Message, Renderer>>,
) -> Self {
@@ -62,56 +56,42 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> {
}
/// Sets the padding of the [`Column`].
- ///
- /// [`Column`]: struct.Column.html
pub fn padding(mut self, units: u16) -> Self {
self.padding = units;
self
}
/// Sets the width of the [`Column`].
- ///
- /// [`Column`]: struct.Column.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`Column`].
- ///
- /// [`Column`]: struct.Column.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
/// Sets the maximum width of the [`Column`].
- ///
- /// [`Column`]: struct.Column.html
pub fn max_width(mut self, max_width: u32) -> Self {
self.max_width = max_width;
self
}
/// Sets the maximum height of the [`Column`] in pixels.
- ///
- /// [`Column`]: struct.Column.html
pub fn max_height(mut self, max_height: u32) -> Self {
self.max_height = max_height;
self
}
/// Sets the horizontal alignment of the contents of the [`Column`] .
- ///
- /// [`Column`]: struct.Column.html
pub fn align_items(mut self, align: Align) -> Self {
self.align_items = align;
self
}
/// Adds an element to the [`Column`].
- ///
- /// [`Column`]: struct.Column.html
pub fn push<E>(mut self, child: E) -> Self
where
E: Into<Element<'a, Message, Renderer>>,
@@ -164,9 +144,11 @@ where
messages: &mut Vec<Message>,
renderer: &Renderer,
clipboard: Option<&dyn Clipboard>,
- ) {
- self.children.iter_mut().zip(layout.children()).for_each(
- |(child, layout)| {
+ ) -> event::Status {
+ self.children
+ .iter_mut()
+ .zip(layout.children())
+ .map(|(child, layout)| {
child.widget.on_event(
event.clone(),
layout,
@@ -175,8 +157,8 @@ where
renderer,
clipboard,
)
- },
- );
+ })
+ .fold(event::Status::Ignored, event::Status::merge)
}
fn draw(
@@ -185,23 +167,44 @@ where
defaults: &Renderer::Defaults,
layout: Layout<'_>,
cursor_position: Point,
+ viewport: &Rectangle,
) -> Renderer::Output {
- renderer.draw(defaults, &self.children, layout, cursor_position)
+ renderer.draw(
+ defaults,
+ &self.children,
+ layout,
+ cursor_position,
+ viewport,
+ )
}
fn hash_layout(&self, state: &mut Hasher) {
- 0.hash(state);
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
self.width.hash(state);
self.height.hash(state);
self.max_width.hash(state);
self.max_height.hash(state);
self.align_items.hash(state);
self.spacing.hash(state);
+ self.padding.hash(state);
for child in &self.children {
child.widget.hash_layout(state);
}
}
+
+ fn overlay(
+ &mut self,
+ layout: Layout<'_>,
+ ) -> Option<overlay::Element<'_, Message, Renderer>> {
+ self.children
+ .iter_mut()
+ .zip(layout.children())
+ .filter_map(|(child, layout)| child.widget.overlay(layout))
+ .next()
+ }
}
/// The renderer of a [`Column`].
@@ -209,8 +212,7 @@ where
/// Your [renderer] will need to implement this trait before being
/// able to use a [`Column`] in your user interface.
///
-/// [`Column`]: struct.Column.html
-/// [renderer]: ../../renderer/index.html
+/// [renderer]: crate::renderer
pub trait Renderer: crate::Renderer + Sized {
/// Draws a [`Column`].
///
@@ -218,15 +220,13 @@ pub trait Renderer: crate::Renderer + Sized {
/// - the children of the [`Column`]
/// - the [`Layout`] of the [`Column`] and its children
/// - the cursor position
- ///
- /// [`Column`]: struct.Row.html
- /// [`Layout`]: ../layout/struct.Layout.html
fn draw<Message>(
&mut self,
defaults: &Self::Defaults,
content: &[Element<'_, Message, Self>],
layout: Layout<'_>,
cursor_position: Point,
+ viewport: &Rectangle,
) -> Self::Output;
}
@@ -234,7 +234,7 @@ impl<'a, Message, Renderer> From<Column<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Renderer: 'a + self::Renderer,
- Message: 'static,
+ Message: 'a,
{
fn from(
column: Column<'a, Message, Renderer>,
diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs
index 3459a832..65764148 100644
--- a/native/src/widget/container.rs
+++ b/native/src/widget/container.rs
@@ -1,9 +1,11 @@
//! Decorate content and apply alignment.
use std::hash::Hash;
+use crate::event::{self, Event};
+use crate::layout;
+use crate::overlay;
use crate::{
- layout, Align, Clipboard, Element, Event, Hasher, Layout, Length, Point,
- Rectangle, Widget,
+ Align, Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget,
};
use std::u32;
@@ -13,6 +15,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,
width: Length,
height: Length,
max_width: u32,
@@ -28,13 +31,12 @@ where
Renderer: self::Renderer,
{
/// Creates an empty [`Container`].
- ///
- /// [`Container`]: struct.Container.html
pub fn new<T>(content: T) -> Self
where
T: Into<Element<'a, Message, Renderer>>,
{
Container {
+ padding: 0,
width: Length::Shrink,
height: Length::Shrink,
max_width: u32::MAX,
@@ -46,73 +48,61 @@ where
}
}
+ /// Sets the padding of the [`Container`].
+ pub fn padding(mut self, units: u16) -> Self {
+ self.padding = units;
+ self
+ }
+
/// Sets the width of the [`Container`].
- ///
- /// [`Container`]: struct.Container.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`Container`].
- ///
- /// [`Container`]: struct.Container.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
/// Sets the maximum width of the [`Container`].
- ///
- /// [`Container`]: struct.Container.html
pub fn max_width(mut self, max_width: u32) -> Self {
self.max_width = max_width;
self
}
/// Sets the maximum height of the [`Container`] in pixels.
- ///
- /// [`Container`]: struct.Container.html
pub fn max_height(mut self, max_height: u32) -> Self {
self.max_height = max_height;
self
}
-
+
/// Sets the content alignment for the horizontal axis of the [`Container`].
- ///
- /// [`Container`]: struct.Container.html
pub fn align_x(mut self, alignment: Align) -> Self {
self.horizontal_alignment = alignment;
self
}
/// Sets the content alignment for the vertical axis of the [`Container`].
- ///
- /// [`Container`]: struct.Container.html
pub fn align_y(mut self, alignment: Align) -> Self {
self.vertical_alignment = alignment;
self
}
/// Centers the contents in the horizontal axis of the [`Container`].
- ///
- /// [`Container`]: struct.Container.html
pub fn center_x(mut self) -> Self {
self.horizontal_alignment = Align::Center;
self
}
/// Centers the contents in the vertical axis of the [`Container`].
- ///
- /// [`Container`]: struct.Container.html
pub fn center_y(mut self) -> Self {
self.vertical_alignment = Align::Center;
self
}
/// Sets the style of the [`Container`].
- ///
- /// [`Container`]: struct.Container.html
pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {
self.style = style.into();
self
@@ -137,19 +127,23 @@ 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);
+ .height(self.height)
+ .pad(padding);
let mut content = self.content.layout(renderer, &limits.loose());
let size = limits.resolve(content.size());
+ content.move_to(Point::new(padding, padding));
content.align(self.horizontal_alignment, self.vertical_alignment, size);
- layout::Node::with_children(size, vec![content])
+ layout::Node::with_children(size.pad(padding), vec![content])
}
fn on_event(
@@ -160,7 +154,7 @@ where
messages: &mut Vec<Message>,
renderer: &Renderer,
clipboard: Option<&dyn Clipboard>,
- ) {
+ ) -> event::Status {
self.content.widget.on_event(
event,
layout.children().next().unwrap(),
@@ -177,11 +171,13 @@ where
defaults: &Renderer::Defaults,
layout: Layout<'_>,
cursor_position: Point,
+ viewport: &Rectangle,
) -> Renderer::Output {
renderer.draw(
defaults,
layout.bounds(),
cursor_position,
+ viewport,
&self.style,
&self.content,
layout.children().next().unwrap(),
@@ -189,7 +185,10 @@ where
}
fn hash_layout(&self, state: &mut Hasher) {
- 0.hash(state);
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
+ self.padding.hash(state);
self.width.hash(state);
self.height.hash(state);
self.max_width.hash(state);
@@ -197,6 +196,13 @@ where
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 [`Container`].
@@ -204,20 +210,18 @@ where
/// Your [renderer] will need to implement this trait before being
/// able to use a [`Container`] in your user interface.
///
-/// [`Container`]: struct.Container.html
-/// [renderer]: ../../renderer/index.html
+/// [renderer]: crate::renderer
pub trait Renderer: crate::Renderer {
/// The style supported by this renderer.
type Style: Default;
/// Draws a [`Container`].
- ///
- /// [`Container`]: struct.Container.html
fn draw<Message>(
&mut self,
defaults: &Self::Defaults,
bounds: Rectangle,
cursor_position: Point,
+ viewport: &Rectangle,
style: &Self::Style,
content: &Element<'_, Message, Self>,
content_layout: Layout<'_>,
@@ -228,7 +232,7 @@ impl<'a, Message, Renderer> From<Container<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Renderer: 'a + self::Renderer,
- Message: 'static,
+ Message: 'a,
{
fn from(
column: Container<'a, Message, Renderer>,
diff --git a/native/src/widget/image.rs b/native/src/widget/image.rs
index fbe38bfc..51d7ba26 100644
--- a/native/src/widget/image.rs
+++ b/native/src/widget/image.rs
@@ -1,5 +1,6 @@
//! Display images in your user interface.
-use crate::{layout, Element, Hasher, Layout, Length, Point, Size, Widget};
+use crate::layout;
+use crate::{Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget};
use std::{
hash::{Hash, Hasher as _},
@@ -27,8 +28,6 @@ pub struct Image {
impl Image {
/// Creates a new [`Image`] with the given path.
- ///
- /// [`Image`]: struct.Image.html
pub fn new<T: Into<Handle>>(handle: T) -> Self {
Image {
handle: handle.into(),
@@ -38,16 +37,12 @@ impl Image {
}
/// Sets the width of the [`Image`] boundaries.
- ///
- /// [`Image`]: struct.Image.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`Image`] boundaries.
- ///
- /// [`Image`]: struct.Image.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
@@ -97,11 +92,15 @@ where
_defaults: &Renderer::Defaults,
layout: Layout<'_>,
_cursor_position: Point,
+ _viewport: &Rectangle,
) -> Renderer::Output {
renderer.draw(self.handle.clone(), layout)
}
fn hash_layout(&self, state: &mut Hasher) {
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
self.handle.hash(state);
self.width.hash(state);
self.height.hash(state);
@@ -109,8 +108,6 @@ where
}
/// An [`Image`] handle.
-///
-/// [`Image`]: struct.Image.html
#[derive(Debug, Clone)]
pub struct Handle {
id: u64,
@@ -120,18 +117,16 @@ pub struct Handle {
impl Handle {
/// Creates an image [`Handle`] pointing to the image of the given path.
///
- /// [`Handle`]: struct.Handle.html
+ /// Makes an educated guess about the image format by examining the data in the file.
pub fn from_path<T: Into<PathBuf>>(path: T) -> Handle {
Self::from_data(Data::Path(path.into()))
}
/// Creates an image [`Handle`] containing the image pixels directly. This
- /// function expects the input data to be provided as a `Vec<u8>` of BGRA
+ /// function expects the input data to be provided as a `Vec<u8>` of BGRA
/// pixels.
///
/// This is useful if you have already decoded your image.
- ///
- /// [`Handle`]: struct.Handle.html
pub fn from_pixels(width: u32, height: u32, pixels: Vec<u8>) -> Handle {
Self::from_data(Data::Pixels {
width,
@@ -142,10 +137,10 @@ impl Handle {
/// Creates an image [`Handle`] containing the image data directly.
///
+ /// Makes an educated guess about the image format by examining the given data.
+ ///
/// This is useful if you already have your image loaded in-memory, maybe
/// because you downloaded or generated it procedurally.
- ///
- /// [`Handle`]: struct.Handle.html
pub fn from_memory(bytes: Vec<u8>) -> Handle {
Self::from_data(Data::Bytes(bytes))
}
@@ -161,29 +156,22 @@ impl Handle {
}
/// Returns the unique identifier of the [`Handle`].
- ///
- /// [`Handle`]: struct.Handle.html
pub fn id(&self) -> u64 {
self.id
}
/// Returns a reference to the image [`Data`].
- ///
- /// [`Data`]: enum.Data.html
pub fn data(&self) -> &Data {
&self.data
}
}
-impl From<String> for Handle {
- fn from(path: String) -> Handle {
- Handle::from_path(path)
- }
-}
-
-impl From<&str> for Handle {
- fn from(path: &str) -> Handle {
- Handle::from_path(path)
+impl<T> From<T> for Handle
+where
+ T: Into<PathBuf>,
+{
+ fn from(path: T) -> Handle {
+ Handle::from_path(path.into())
}
}
@@ -194,8 +182,6 @@ impl Hash for Handle {
}
/// The data of an [`Image`].
-///
-/// [`Image`]: struct.Image.html
#[derive(Clone, Hash)]
pub enum Data {
/// File data
@@ -232,17 +218,12 @@ impl std::fmt::Debug for Data {
/// Your [renderer] will need to implement this trait before being able to use
/// an [`Image`] in your user interface.
///
-/// [`Image`]: struct.Image.html
-/// [renderer]: ../../renderer/index.html
+/// [renderer]: crate::renderer
pub trait Renderer: crate::Renderer {
/// Returns the dimensions of an [`Image`] located on the given path.
- ///
- /// [`Image`]: struct.Image.html
fn dimensions(&self, handle: &Handle) -> (u32, u32);
/// Draws an [`Image`].
- ///
- /// [`Image`]: struct.Image.html
fn draw(&mut self, handle: Handle, layout: Layout<'_>) -> Self::Output;
}
diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs
new file mode 100644
index 00000000..ff19cbc2
--- /dev/null
+++ b/native/src/widget/pane_grid.rs
@@ -0,0 +1,639 @@
+//! Let your users split regions of your application and organize layout dynamically.
+//!
+//! [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](https://gfycat.com/mixedflatjellyfish)
+//!
+//! # Example
+//! 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
+mod axis;
+mod configuration;
+mod content;
+mod direction;
+mod node;
+mod pane;
+mod split;
+mod state;
+mod title_bar;
+
+pub use axis::Axis;
+pub use configuration::Configuration;
+pub use content::Content;
+pub use direction::Direction;
+pub use node::Node;
+pub use pane::Pane;
+pub use split::Split;
+pub use state::State;
+pub use title_bar::TitleBar;
+
+use crate::container;
+use crate::event::{self, Event};
+use crate::layout;
+use crate::mouse;
+use crate::overlay;
+use crate::row;
+use crate::text;
+use crate::{
+ Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Vector,
+ Widget,
+};
+
+/// A collection of panes distributed using either vertical or horizontal splits
+/// to completely fill the space available.
+///
+/// [![Pane grid - Iced](https://thumbs.gfycat.com/FrailFreshAiredaleterrier-small.gif)](https://gfycat.com/frailfreshairedaleterrier)
+///
+/// This distribution of space is common in tiling window managers (like
+/// [`awesome`](https://awesomewm.org/), [`i3`](https://i3wm.org/), or even
+/// [`tmux`](https://github.com/tmux/tmux)).
+///
+/// A [`PaneGrid`] supports:
+///
+/// * Vertical and horizontal splits
+/// * Tracking of the last active pane
+/// * Mouse-based resizing
+/// * Drag and drop to reorganize panes
+/// * Hotkey support
+/// * Configurable modifier keys
+/// * [`State`] API to perform actions programmatically (`split`, `swap`, `resize`, etc.)
+///
+/// ## Example
+///
+/// ```
+/// # use iced_native::{pane_grid, Text};
+/// #
+/// # type PaneGrid<'a, Message> =
+/// # iced_native::PaneGrid<'a, Message, iced_native::renderer::Null>;
+/// #
+/// enum PaneState {
+/// SomePane,
+/// AnotherKindOfPane,
+/// }
+///
+/// enum Message {
+/// PaneDragged(pane_grid::DragEvent),
+/// PaneResized(pane_grid::ResizeEvent),
+/// }
+///
+/// let (mut state, _) = pane_grid::State::new(PaneState::SomePane);
+///
+/// let pane_grid =
+/// PaneGrid::new(&mut state, |pane, state| {
+/// pane_grid::Content::new(match state {
+/// PaneState::SomePane => Text::new("This is some pane"),
+/// PaneState::AnotherKindOfPane => Text::new("This is another kind of pane"),
+/// })
+/// })
+/// .on_drag(Message::PaneDragged)
+/// .on_resize(10, Message::PaneResized);
+/// ```
+#[allow(missing_debug_implementations)]
+pub struct PaneGrid<'a, Message, Renderer: self::Renderer> {
+ state: &'a mut state::Internal,
+ elements: Vec<(Pane, Content<'a, Message, Renderer>)>,
+ width: Length,
+ height: Length,
+ spacing: u16,
+ 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>)>,
+}
+
+impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer>
+where
+ Renderer: self::Renderer,
+{
+ /// Creates a [`PaneGrid`] with the given [`State`] and view function.
+ ///
+ /// The view function will be called to display each [`Pane`] present in the
+ /// [`State`].
+ pub fn new<T>(
+ state: &'a mut State<T>,
+ view: impl Fn(Pane, &'a mut T) -> Content<'a, Message, Renderer>,
+ ) -> Self {
+ let elements = {
+ state
+ .panes
+ .iter_mut()
+ .map(|(pane, pane_state)| (*pane, view(*pane, pane_state)))
+ .collect()
+ };
+
+ Self {
+ state: &mut state.internal,
+ elements,
+ width: Length::Fill,
+ height: Length::Fill,
+ spacing: 0,
+ on_click: None,
+ on_drag: None,
+ on_resize: None,
+ }
+ }
+
+ /// Sets the width of the [`PaneGrid`].
+ pub fn width(mut self, width: Length) -> Self {
+ self.width = width;
+ self
+ }
+
+ /// Sets the height of the [`PaneGrid`].
+ pub fn height(mut self, height: Length) -> Self {
+ self.height = height;
+ self
+ }
+
+ /// Sets the spacing _between_ the panes of the [`PaneGrid`].
+ pub fn spacing(mut self, units: u16) -> Self {
+ self.spacing = units;
+ self
+ }
+
+ /// Sets the message that will be produced when a [`Pane`] of the
+ /// [`PaneGrid`] is clicked.
+ pub fn on_click<F>(mut self, f: F) -> Self
+ where
+ F: 'a + Fn(Pane) -> Message,
+ {
+ self.on_click = Some(Box::new(f));
+ self
+ }
+
+ /// Enables the drag and drop interactions of the [`PaneGrid`], which will
+ /// use the provided function to produce messages.
+ pub fn on_drag<F>(mut self, f: F) -> Self
+ where
+ F: 'a + Fn(DragEvent) -> Message,
+ {
+ self.on_drag = Some(Box::new(f));
+ self
+ }
+
+ /// Enables the resize interactions of the [`PaneGrid`], which will
+ /// use the provided function to produce messages.
+ ///
+ /// The `leeway` describes the amount of space around a split that can be
+ /// used to grab it.
+ ///
+ /// The grabbable area of a split will have a length of `spacing + leeway`,
+ /// properly centered. In other words, a length of
+ /// `(spacing + leeway) / 2.0` on either side of the split line.
+ pub fn on_resize<F>(mut self, leeway: u16, f: F) -> Self
+ where
+ F: 'a + Fn(ResizeEvent) -> Message,
+ {
+ self.on_resize = Some((leeway, Box::new(f)));
+ self
+ }
+}
+
+impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer>
+where
+ Renderer: self::Renderer,
+{
+ fn click_pane(
+ &mut self,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ messages: &mut Vec<Message>,
+ ) {
+ let mut clicked_region =
+ self.elements.iter().zip(layout.children()).filter(
+ |(_, layout)| layout.bounds().contains(cursor_position),
+ );
+
+ if let Some(((pane, content), layout)) = clicked_region.next() {
+ if let Some(on_click) = &self.on_click {
+ messages.push(on_click(*pane));
+ }
+
+ if let Some(on_drag) = &self.on_drag {
+ if content.can_be_picked_at(layout, cursor_position) {
+ let pane_position = layout.position();
+
+ let origin = cursor_position
+ - Vector::new(pane_position.x, pane_position.y);
+
+ self.state.pick_pane(pane, origin);
+
+ messages.push(on_drag(DragEvent::Picked { pane: *pane }));
+ }
+ }
+ }
+ }
+
+ fn trigger_resize(
+ &mut self,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ messages: &mut Vec<Message>,
+ ) -> event::Status {
+ if let Some((_, on_resize)) = &self.on_resize {
+ if let Some((split, _)) = self.state.picked_split() {
+ let bounds = layout.bounds();
+
+ let splits = self.state.split_regions(
+ f32::from(self.spacing),
+ Size::new(bounds.width, bounds.height),
+ );
+
+ if let Some((axis, rectangle, _)) = splits.get(&split) {
+ let ratio = match axis {
+ Axis::Horizontal => {
+ let position =
+ cursor_position.y - bounds.y - rectangle.y;
+
+ (position / rectangle.height).max(0.1).min(0.9)
+ }
+ Axis::Vertical => {
+ let position =
+ cursor_position.x - bounds.x - rectangle.x;
+
+ (position / rectangle.width).max(0.1).min(0.9)
+ }
+ };
+
+ messages.push(on_resize(ResizeEvent { split, ratio }));
+
+ return event::Status::Captured;
+ }
+ }
+ }
+
+ event::Status::Ignored
+ }
+}
+
+/// An event produced during a drag and drop interaction of a [`PaneGrid`].
+#[derive(Debug, Clone, Copy)]
+pub enum DragEvent {
+ /// A [`Pane`] was picked for dragging.
+ Picked {
+ /// The picked [`Pane`].
+ pane: Pane,
+ },
+
+ /// A [`Pane`] was dropped on top of another [`Pane`].
+ Dropped {
+ /// The picked [`Pane`].
+ pane: Pane,
+
+ /// The [`Pane`] where the picked one was dropped on.
+ target: Pane,
+ },
+
+ /// A [`Pane`] was picked and then dropped outside of other [`Pane`]
+ /// boundaries.
+ Canceled {
+ /// The picked [`Pane`].
+ pane: Pane,
+ },
+}
+
+/// An event produced during a resize interaction of a [`PaneGrid`].
+#[derive(Debug, Clone, Copy)]
+pub struct ResizeEvent {
+ /// The [`Split`] that is being dragged for resizing.
+ pub split: Split,
+
+ /// The new ratio of the [`Split`].
+ ///
+ /// The ratio is a value in [0, 1], representing the exact position of a
+ /// [`Split`] between two panes.
+ pub ratio: f32,
+}
+
+impl<'a, Message, Renderer> Widget<Message, Renderer>
+ for PaneGrid<'a, Message, Renderer>
+where
+ Renderer: self::Renderer + container::Renderer,
+{
+ fn width(&self) -> Length {
+ self.width
+ }
+
+ fn height(&self) -> Length {
+ self.height
+ }
+
+ fn layout(
+ &self,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ let limits = limits.width(self.width).height(self.height);
+ let size = limits.resolve(Size::ZERO);
+
+ let regions = self.state.pane_regions(f32::from(self.spacing), size);
+
+ let children = self
+ .elements
+ .iter()
+ .filter_map(|(pane, element)| {
+ let region = regions.get(pane)?;
+ let size = Size::new(region.width, region.height);
+
+ let mut node =
+ element.layout(renderer, &layout::Limits::new(size, size));
+
+ node.move_to(Point::new(region.x, region.y));
+
+ Some(node)
+ })
+ .collect();
+
+ layout::Node::with_children(size, children)
+ }
+
+ fn on_event(
+ &mut self,
+ event: Event,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ messages: &mut Vec<Message>,
+ renderer: &Renderer,
+ clipboard: Option<&dyn Clipboard>,
+ ) -> 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,
+ );
+
+ if let Some((split, axis)) = clicked_split {
+ self.state.pick_split(&split, axis);
+ } else {
+ self.click_pane(
+ layout,
+ cursor_position,
+ messages,
+ );
+ }
+ }
+ 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)| {
+ layout.bounds().contains(cursor_position)
+ });
+
+ let event = match dropped_region.next() {
+ Some(((target, _), _)) if pane != *target => {
+ DragEvent::Dropped {
+ pane,
+ target: *target,
+ }
+ }
+ _ => DragEvent::Canceled { pane },
+ };
+
+ messages.push(on_drag(event));
+ }
+
+ 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);
+ }
+ _ => {}
+ },
+ _ => {}
+ }
+
+ if self.state.picked_pane().is_none() {
+ self.elements
+ .iter_mut()
+ .zip(layout.children())
+ .map(|((_, pane), layout)| {
+ pane.on_event(
+ event.clone(),
+ layout,
+ cursor_position,
+ messages,
+ renderer,
+ clipboard,
+ )
+ })
+ .fold(event_status, event::Status::merge)
+ } else {
+ event::Status::Captured
+ }
+ }
+
+ fn draw(
+ &self,
+ renderer: &mut Renderer,
+ defaults: &Renderer::Defaults,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ _viewport: &Rectangle,
+ ) -> Renderer::Output {
+ let picked_split = self
+ .state
+ .picked_split()
+ .or_else(|| match self.on_resize {
+ Some((leeway, _)) => {
+ let bounds = layout.bounds();
+
+ 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), bounds.size());
+
+ hovered_split(
+ splits.iter(),
+ f32::from(self.spacing + leeway),
+ relative_cursor,
+ )
+ }
+ None => None,
+ })
+ .map(|(_, axis)| axis);
+
+ self::Renderer::draw(
+ renderer,
+ defaults,
+ &self.elements,
+ self.state.picked_pane(),
+ picked_split,
+ layout,
+ cursor_position,
+ )
+ }
+
+ fn hash_layout(&self, state: &mut Hasher) {
+ use std::hash::Hash;
+
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
+ self.width.hash(state);
+ self.height.hash(state);
+ self.state.hash_layout(state);
+
+ for (_, element) in &self.elements {
+ element.hash_layout(state);
+ }
+ }
+
+ fn overlay(
+ &mut self,
+ layout: Layout<'_>,
+ ) -> Option<overlay::Element<'_, Message, Renderer>> {
+ self.elements
+ .iter_mut()
+ .zip(layout.children())
+ .filter_map(|((_, pane), layout)| pane.overlay(layout))
+ .next()
+ }
+}
+
+/// The renderer of a [`PaneGrid`].
+///
+/// Your [renderer] will need to implement this trait before being
+/// able to use a [`PaneGrid`] in your user interface.
+///
+/// [renderer]: crate::renderer
+pub trait Renderer:
+ crate::Renderer + container::Renderer + text::Renderer + Sized
+{
+ /// Draws a [`PaneGrid`].
+ ///
+ /// It receives:
+ /// - the elements of the [`PaneGrid`]
+ /// - the [`Pane`] that is currently being dragged
+ /// - the [`Axis`] that is currently being resized
+ /// - the [`Layout`] of the [`PaneGrid`] and its elements
+ /// - the cursor position
+ fn draw<Message>(
+ &mut self,
+ defaults: &Self::Defaults,
+ content: &[(Pane, Content<'_, Message, Self>)],
+ dragging: Option<(Pane, Point)>,
+ resizing: Option<Axis>,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ ) -> Self::Output;
+
+ /// Draws a [`Pane`].
+ ///
+ /// It receives:
+ /// - the [`TitleBar`] of the [`Pane`], if any
+ /// - the [`Content`] of the [`Pane`]
+ /// - the [`Layout`] of the [`Pane`] and its elements
+ /// - the cursor position
+ fn draw_pane<Message>(
+ &mut self,
+ defaults: &Self::Defaults,
+ bounds: Rectangle,
+ style: &Self::Style,
+ title_bar: Option<(&TitleBar<'_, Message, Self>, Layout<'_>)>,
+ body: (&Element<'_, Message, Self>, Layout<'_>),
+ cursor_position: Point,
+ ) -> Self::Output;
+
+ /// Draws a [`TitleBar`].
+ ///
+ /// 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 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,
+ controls: Option<(&Element<'_, Message, Self>, Layout<'_>)>,
+ cursor_position: Point,
+ ) -> Self::Output;
+}
+
+impl<'a, Message, Renderer> From<PaneGrid<'a, Message, Renderer>>
+ for Element<'a, Message, Renderer>
+where
+ Renderer: 'a + self::Renderer + row::Renderer,
+ Message: 'a,
+{
+ fn from(
+ pane_grid: PaneGrid<'a, Message, Renderer>,
+ ) -> Element<'a, Message, Renderer> {
+ Element::new(pane_grid)
+ }
+}
+
+/*
+ * Helpers
+ */
+fn hovered_split<'a>(
+ splits: impl Iterator<Item = (&'a Split, &'a (Axis, Rectangle, f32))>,
+ spacing: f32,
+ cursor_position: Point,
+) -> Option<(Split, Axis)> {
+ 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))
+ } else {
+ None
+ }
+ })
+ .next()
+}
diff --git a/native/src/widget/pane_grid/axis.rs b/native/src/widget/pane_grid/axis.rs
new file mode 100644
index 00000000..2320cb7c
--- /dev/null
+++ b/native/src/widget/pane_grid/axis.rs
@@ -0,0 +1,238 @@
+use crate::Rectangle;
+
+/// A fixed reference line for the measurement of coordinates.
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
+pub enum Axis {
+ /// The horizontal axis: —
+ Horizontal,
+ /// The vertical axis: |
+ Vertical,
+}
+
+impl Axis {
+ pub(super) fn split(
+ &self,
+ rectangle: &Rectangle,
+ ratio: f32,
+ spacing: f32,
+ ) -> (Rectangle, Rectangle) {
+ match self {
+ Axis::Horizontal => {
+ let height_top =
+ (rectangle.height * ratio - spacing / 2.0).round();
+ let height_bottom = rectangle.height - height_top - spacing;
+
+ (
+ Rectangle {
+ height: height_top,
+ ..*rectangle
+ },
+ Rectangle {
+ y: rectangle.y + height_top + spacing,
+ height: height_bottom,
+ ..*rectangle
+ },
+ )
+ }
+ Axis::Vertical => {
+ let width_left =
+ (rectangle.width * ratio - spacing / 2.0).round();
+ let width_right = rectangle.width - width_left - spacing;
+
+ (
+ Rectangle {
+ width: width_left,
+ ..*rectangle
+ },
+ Rectangle {
+ x: rectangle.x + width_left + spacing,
+ width: width_right,
+ ..*rectangle
+ },
+ )
+ }
+ }
+ }
+
+ pub(super) fn split_line_bounds(
+ &self,
+ rectangle: Rectangle,
+ ratio: f32,
+ spacing: f32,
+ ) -> Rectangle {
+ match self {
+ Axis::Horizontal => Rectangle {
+ x: rectangle.x,
+ y: (rectangle.y + rectangle.height * ratio - spacing / 2.0)
+ .round(),
+ width: rectangle.width,
+ height: spacing,
+ },
+ Axis::Vertical => Rectangle {
+ x: (rectangle.x + rectangle.width * ratio - spacing / 2.0)
+ .round(),
+ y: rectangle.y,
+ width: spacing,
+ height: rectangle.height,
+ },
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ enum Case {
+ Horizontal {
+ overall_height: f32,
+ spacing: f32,
+ top_height: f32,
+ bottom_y: f32,
+ bottom_height: f32,
+ },
+ Vertical {
+ overall_width: f32,
+ spacing: f32,
+ left_width: f32,
+ right_x: f32,
+ right_width: f32,
+ },
+ }
+
+ #[test]
+ fn split() {
+ let cases = vec![
+ // Even height, even spacing
+ Case::Horizontal {
+ overall_height: 10.0,
+ spacing: 2.0,
+ top_height: 4.0,
+ bottom_y: 6.0,
+ bottom_height: 4.0,
+ },
+ // Odd height, even spacing
+ Case::Horizontal {
+ overall_height: 9.0,
+ spacing: 2.0,
+ top_height: 4.0,
+ bottom_y: 6.0,
+ bottom_height: 3.0,
+ },
+ // Even height, odd spacing
+ Case::Horizontal {
+ overall_height: 10.0,
+ spacing: 1.0,
+ top_height: 5.0,
+ bottom_y: 6.0,
+ bottom_height: 4.0,
+ },
+ // Odd height, odd spacing
+ Case::Horizontal {
+ overall_height: 9.0,
+ spacing: 1.0,
+ top_height: 4.0,
+ bottom_y: 5.0,
+ bottom_height: 4.0,
+ },
+ // Even width, even spacing
+ Case::Vertical {
+ overall_width: 10.0,
+ spacing: 2.0,
+ left_width: 4.0,
+ right_x: 6.0,
+ right_width: 4.0,
+ },
+ // Odd width, even spacing
+ Case::Vertical {
+ overall_width: 9.0,
+ spacing: 2.0,
+ left_width: 4.0,
+ right_x: 6.0,
+ right_width: 3.0,
+ },
+ // Even width, odd spacing
+ Case::Vertical {
+ overall_width: 10.0,
+ spacing: 1.0,
+ left_width: 5.0,
+ right_x: 6.0,
+ right_width: 4.0,
+ },
+ // Odd width, odd spacing
+ Case::Vertical {
+ overall_width: 9.0,
+ spacing: 1.0,
+ left_width: 4.0,
+ right_x: 5.0,
+ right_width: 4.0,
+ },
+ ];
+ for case in cases {
+ match case {
+ Case::Horizontal {
+ overall_height,
+ spacing,
+ top_height,
+ bottom_y,
+ bottom_height,
+ } => {
+ let a = Axis::Horizontal;
+ let r = Rectangle {
+ x: 0.0,
+ y: 0.0,
+ width: 10.0,
+ height: overall_height,
+ };
+ let (top, bottom) = a.split(&r, 0.5, spacing);
+ assert_eq!(
+ top,
+ Rectangle {
+ height: top_height,
+ ..r
+ }
+ );
+ assert_eq!(
+ bottom,
+ Rectangle {
+ y: bottom_y,
+ height: bottom_height,
+ ..r
+ }
+ );
+ }
+ Case::Vertical {
+ overall_width,
+ spacing,
+ left_width,
+ right_x,
+ right_width,
+ } => {
+ let a = Axis::Vertical;
+ let r = Rectangle {
+ x: 0.0,
+ y: 0.0,
+ width: overall_width,
+ height: 10.0,
+ };
+ let (left, right) = a.split(&r, 0.5, spacing);
+ assert_eq!(
+ left,
+ Rectangle {
+ width: left_width,
+ ..r
+ }
+ );
+ assert_eq!(
+ right,
+ Rectangle {
+ x: right_x,
+ width: right_width,
+ ..r
+ }
+ );
+ }
+ }
+ }
+ }
+}
diff --git a/native/src/widget/pane_grid/configuration.rs b/native/src/widget/pane_grid/configuration.rs
new file mode 100644
index 00000000..4c43826e
--- /dev/null
+++ b/native/src/widget/pane_grid/configuration.rs
@@ -0,0 +1,26 @@
+use crate::pane_grid::Axis;
+
+/// The arrangement of a [`PaneGrid`].
+///
+/// [`PaneGrid`]: crate::pane_grid::PaneGrid
+#[derive(Debug, Clone)]
+pub enum Configuration<T> {
+ /// A split of the available space.
+ Split {
+ /// The direction of the split.
+ axis: Axis,
+
+ /// The ratio of the split in [0.0, 1.0].
+ ratio: f32,
+
+ /// The left/top [`Configuration`] of the split.
+ a: Box<Configuration<T>>,
+
+ /// The right/bottom [`Configuration`] of the split.
+ b: Box<Configuration<T>>,
+ },
+ /// A [`Pane`].
+ ///
+ /// [`Pane`]: crate::pane_grid::Pane
+ Pane(T),
+}
diff --git a/native/src/widget/pane_grid/content.rs b/native/src/widget/pane_grid/content.rs
new file mode 100644
index 00000000..c9981903
--- /dev/null
+++ b/native/src/widget/pane_grid/content.rs
@@ -0,0 +1,213 @@
+use crate::container;
+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};
+
+/// The content of a [`Pane`].
+///
+/// [`Pane`]: crate::widget::pane_grid::Pane
+#[allow(missing_debug_implementations)]
+pub struct Content<'a, Message, Renderer: pane_grid::Renderer> {
+ title_bar: Option<TitleBar<'a, Message, Renderer>>,
+ body: Element<'a, Message, Renderer>,
+ style: Renderer::Style,
+}
+
+impl<'a, Message, Renderer> Content<'a, Message, Renderer>
+where
+ Renderer: pane_grid::Renderer,
+{
+ /// Creates a new [`Content`] with the provided body.
+ pub fn new(body: impl Into<Element<'a, Message, Renderer>>) -> Self {
+ Self {
+ title_bar: None,
+ body: body.into(),
+ style: Renderer::Style::default(),
+ }
+ }
+
+ /// Sets the [`TitleBar`] of this [`Content`].
+ pub fn title_bar(
+ mut self,
+ title_bar: TitleBar<'a, Message, Renderer>,
+ ) -> Self {
+ self.title_bar = Some(title_bar);
+ self
+ }
+
+ /// Sets the style of the [`Content`].
+ pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {
+ self.style = style.into();
+ self
+ }
+}
+
+impl<'a, Message, Renderer> Content<'a, Message, Renderer>
+where
+ Renderer: pane_grid::Renderer,
+{
+ /// Draws the [`Content`] with the provided [`Renderer`] and [`Layout`].
+ ///
+ /// [`Renderer`]: crate::widget::pane_grid::Renderer
+ pub fn draw(
+ &self,
+ renderer: &mut Renderer,
+ defaults: &Renderer::Defaults,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ ) -> Renderer::Output {
+ if let Some(title_bar) = &self.title_bar {
+ let mut children = layout.children();
+ let title_bar_layout = children.next().unwrap();
+ let body_layout = children.next().unwrap();
+
+ renderer.draw_pane(
+ defaults,
+ layout.bounds(),
+ &self.style,
+ Some((title_bar, title_bar_layout)),
+ (&self.body, body_layout),
+ cursor_position,
+ )
+ } else {
+ renderer.draw_pane(
+ defaults,
+ layout.bounds(),
+ &self.style,
+ None,
+ (&self.body, layout),
+ cursor_position,
+ )
+ }
+ }
+
+ /// Returns whether the [`Content`] with the given [`Layout`] can be picked
+ /// at the provided cursor position.
+ pub fn can_be_picked_at(
+ &self,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ ) -> bool {
+ if let Some(title_bar) = &self.title_bar {
+ let mut children = layout.children();
+ let title_bar_layout = children.next().unwrap();
+
+ title_bar.is_over_pick_area(title_bar_layout, cursor_position)
+ } else {
+ false
+ }
+ }
+
+ pub(crate) fn layout(
+ &self,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ if let Some(title_bar) = &self.title_bar {
+ let max_size = limits.max();
+
+ let title_bar_layout = title_bar
+ .layout(renderer, &layout::Limits::new(Size::ZERO, max_size));
+
+ let title_bar_size = title_bar_layout.size();
+
+ let mut body_layout = self.body.layout(
+ renderer,
+ &layout::Limits::new(
+ Size::ZERO,
+ Size::new(
+ max_size.width,
+ max_size.height - title_bar_size.height,
+ ),
+ ),
+ );
+
+ body_layout.move_to(Point::new(0.0, title_bar_size.height));
+
+ layout::Node::with_children(
+ max_size,
+ vec![title_bar_layout, body_layout],
+ )
+ } else {
+ self.body.layout(renderer, limits)
+ }
+ }
+
+ pub(crate) fn on_event(
+ &mut self,
+ event: Event,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ messages: &mut Vec<Message>,
+ renderer: &Renderer,
+ clipboard: Option<&dyn Clipboard>,
+ ) -> event::Status {
+ let mut event_status = event::Status::Ignored;
+
+ let body_layout = if let Some(title_bar) = &mut self.title_bar {
+ let mut children = layout.children();
+
+ event_status = title_bar.on_event(
+ event.clone(),
+ children.next().unwrap(),
+ cursor_position,
+ messages,
+ renderer,
+ clipboard,
+ );
+
+ children.next().unwrap()
+ } else {
+ layout
+ };
+
+ let body_status = self.body.on_event(
+ event,
+ body_layout,
+ cursor_position,
+ messages,
+ renderer,
+ clipboard,
+ );
+
+ event_status.merge(body_status)
+ }
+
+ pub(crate) fn hash_layout(&self, state: &mut Hasher) {
+ if let Some(title_bar) = &self.title_bar {
+ title_bar.hash_layout(state);
+ }
+
+ self.body.hash_layout(state);
+ }
+
+ pub(crate) fn overlay(
+ &mut self,
+ layout: Layout<'_>,
+ ) -> Option<overlay::Element<'_, Message, Renderer>> {
+ let body_layout = if self.title_bar.is_some() {
+ let mut children = layout.children();
+
+ // Overlays only allowed in the pane body, for now at least.
+ let _title_bar_layout = children.next();
+
+ children.next()?
+ } else {
+ layout
+ };
+
+ self.body.overlay(body_layout)
+ }
+}
+
+impl<'a, T, Message, Renderer> From<T> for Content<'a, Message, Renderer>
+where
+ T: Into<Element<'a, Message, Renderer>>,
+ Renderer: pane_grid::Renderer + container::Renderer,
+{
+ fn from(element: T) -> Self {
+ Self::new(element)
+ }
+}
diff --git a/native/src/widget/pane_grid/direction.rs b/native/src/widget/pane_grid/direction.rs
new file mode 100644
index 00000000..b31a8737
--- /dev/null
+++ b/native/src/widget/pane_grid/direction.rs
@@ -0,0 +1,12 @@
+/// A four cardinal direction.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Direction {
+ /// ↑
+ Up,
+ /// ↓
+ Down,
+ /// ←
+ Left,
+ /// →
+ Right,
+}
diff --git a/native/src/widget/pane_grid/node.rs b/native/src/widget/pane_grid/node.rs
new file mode 100644
index 00000000..319936fc
--- /dev/null
+++ b/native/src/widget/pane_grid/node.rs
@@ -0,0 +1,258 @@
+use crate::{
+ pane_grid::{Axis, Pane, Split},
+ Rectangle, Size,
+};
+
+use std::collections::HashMap;
+
+/// A layout node of a [`PaneGrid`].
+///
+/// [`PaneGrid`]: crate::widget::PaneGrid
+#[derive(Debug, Clone)]
+pub enum Node {
+ /// The region of this [`Node`] is split into two.
+ Split {
+ /// The [`Split`] of this [`Node`].
+ id: Split,
+
+ /// The direction of the split.
+ axis: Axis,
+
+ /// The ratio of the split in [0.0, 1.0].
+ ratio: f32,
+
+ /// The left/top [`Node`] of the split.
+ a: Box<Node>,
+
+ /// The right/bottom [`Node`] of the split.
+ b: Box<Node>,
+ },
+ /// The region of this [`Node`] is taken by a [`Pane`].
+ Pane(Pane),
+}
+
+impl Node {
+ /// Returns an iterator over each [`Split`] in this [`Node`].
+ pub fn splits(&self) -> impl Iterator<Item = &Split> {
+ let mut unvisited_nodes = vec![self];
+
+ std::iter::from_fn(move || {
+ while let Some(node) = unvisited_nodes.pop() {
+ match node {
+ Node::Split { id, a, b, .. } => {
+ unvisited_nodes.push(a);
+ unvisited_nodes.push(b);
+
+ return Some(id);
+ }
+ _ => {}
+ }
+ }
+
+ None
+ })
+ }
+
+ /// Returns the rectangular region for each [`Pane`] in the [`Node`] given
+ /// the spacing between panes and the total available space.
+ pub fn pane_regions(
+ &self,
+ spacing: f32,
+ size: Size,
+ ) -> HashMap<Pane, Rectangle> {
+ let mut regions = HashMap::new();
+
+ self.compute_regions(
+ spacing,
+ &Rectangle {
+ x: 0.0,
+ y: 0.0,
+ width: size.width,
+ height: size.height,
+ },
+ &mut regions,
+ );
+
+ regions
+ }
+
+ /// Returns the axis, rectangular region, and ratio for each [`Split`] in
+ /// the [`Node`] given the spacing between panes and the total available
+ /// space.
+ pub fn split_regions(
+ &self,
+ spacing: f32,
+ size: Size,
+ ) -> HashMap<Split, (Axis, Rectangle, f32)> {
+ let mut splits = HashMap::new();
+
+ self.compute_splits(
+ spacing,
+ &Rectangle {
+ x: 0.0,
+ y: 0.0,
+ width: size.width,
+ height: size.height,
+ },
+ &mut splits,
+ );
+
+ splits
+ }
+
+ pub(crate) fn find(&mut self, pane: &Pane) -> Option<&mut Node> {
+ match self {
+ Node::Split { a, b, .. } => {
+ a.find(pane).or_else(move || b.find(pane))
+ }
+ Node::Pane(p) => {
+ if p == pane {
+ Some(self)
+ } else {
+ None
+ }
+ }
+ }
+ }
+
+ pub(crate) fn split(&mut self, id: Split, axis: Axis, new_pane: Pane) {
+ *self = Node::Split {
+ id,
+ axis,
+ ratio: 0.5,
+ a: Box::new(self.clone()),
+ b: Box::new(Node::Pane(new_pane)),
+ };
+ }
+
+ pub(crate) fn update(&mut self, f: &impl Fn(&mut Node)) {
+ match self {
+ Node::Split { a, b, .. } => {
+ a.update(f);
+ b.update(f);
+ }
+ _ => {}
+ }
+
+ f(self);
+ }
+
+ pub(crate) fn resize(&mut self, split: &Split, percentage: f32) -> bool {
+ match self {
+ Node::Split {
+ id, ratio, a, b, ..
+ } => {
+ if id == split {
+ *ratio = percentage;
+
+ true
+ } else if a.resize(split, percentage) {
+ true
+ } else {
+ b.resize(split, percentage)
+ }
+ }
+ Node::Pane(_) => false,
+ }
+ }
+
+ pub(crate) fn remove(&mut self, pane: &Pane) -> Option<Pane> {
+ match self {
+ Node::Split { a, b, .. } => {
+ if a.pane() == Some(*pane) {
+ *self = *b.clone();
+ Some(self.first_pane())
+ } else if b.pane() == Some(*pane) {
+ *self = *a.clone();
+ Some(self.first_pane())
+ } else {
+ a.remove(pane).or_else(|| b.remove(pane))
+ }
+ }
+ Node::Pane(_) => None,
+ }
+ }
+
+ fn pane(&self) -> Option<Pane> {
+ match self {
+ Node::Split { .. } => None,
+ Node::Pane(pane) => Some(*pane),
+ }
+ }
+
+ fn first_pane(&self) -> Pane {
+ match self {
+ Node::Split { a, .. } => a.first_pane(),
+ Node::Pane(pane) => *pane,
+ }
+ }
+
+ fn compute_regions(
+ &self,
+ spacing: f32,
+ current: &Rectangle,
+ regions: &mut HashMap<Pane, Rectangle>,
+ ) {
+ match self {
+ Node::Split {
+ axis, ratio, a, b, ..
+ } => {
+ let (region_a, region_b) = axis.split(current, *ratio, spacing);
+
+ a.compute_regions(spacing, &region_a, regions);
+ b.compute_regions(spacing, &region_b, regions);
+ }
+ Node::Pane(pane) => {
+ let _ = regions.insert(*pane, *current);
+ }
+ }
+ }
+
+ fn compute_splits(
+ &self,
+ spacing: f32,
+ current: &Rectangle,
+ splits: &mut HashMap<Split, (Axis, Rectangle, f32)>,
+ ) {
+ match self {
+ Node::Split {
+ axis,
+ ratio,
+ a,
+ b,
+ id,
+ } => {
+ let (region_a, region_b) = axis.split(current, *ratio, spacing);
+
+ let _ = splits.insert(*id, (*axis, *current, *ratio));
+
+ a.compute_splits(spacing, &region_a, splits);
+ b.compute_splits(spacing, &region_b, splits);
+ }
+ Node::Pane(_) => {}
+ }
+ }
+}
+
+impl std::hash::Hash for Node {
+ fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+ match self {
+ Node::Split {
+ id,
+ axis,
+ ratio,
+ a,
+ b,
+ } => {
+ id.hash(state);
+ axis.hash(state);
+ ((ratio * 100_000.0) as u32).hash(state);
+ a.hash(state);
+ b.hash(state);
+ }
+ Node::Pane(pane) => {
+ pane.hash(state);
+ }
+ }
+ }
+}
diff --git a/native/src/widget/pane_grid/pane.rs b/native/src/widget/pane_grid/pane.rs
new file mode 100644
index 00000000..39d9f3ef
--- /dev/null
+++ b/native/src/widget/pane_grid/pane.rs
@@ -0,0 +1,5 @@
+/// A rectangular region in a [`PaneGrid`] used to display widgets.
+///
+/// [`PaneGrid`]: crate::widget::PaneGrid
+#[derive(Debug, Clone, Copy, PartialEq, Eq, 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
new file mode 100644
index 00000000..16975abc
--- /dev/null
+++ b/native/src/widget/pane_grid/split.rs
@@ -0,0 +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)]
+pub struct Split(pub(super) usize);
diff --git a/native/src/widget/pane_grid/state.rs b/native/src/widget/pane_grid/state.rs
new file mode 100644
index 00000000..666e1ca0
--- /dev/null
+++ b/native/src/widget/pane_grid/state.rs
@@ -0,0 +1,301 @@
+use crate::{
+ pane_grid::{Axis, Configuration, Direction, Node, Pane, Split},
+ Hasher, Point, Rectangle, Size,
+};
+
+use std::collections::HashMap;
+
+/// The state of a [`PaneGrid`].
+///
+/// It keeps track of the state of each [`Pane`] and the position of each
+/// [`Split`].
+///
+/// The [`State`] needs to own any mutable contents a [`Pane`] may need. This is
+/// why this struct is generic over the type `T`. Values of this type are
+/// provided to the view function of [`PaneGrid::new`] for displaying each
+/// [`Pane`].
+///
+/// [`PaneGrid`]: crate::widget::PaneGrid
+/// [`PaneGrid::new`]: crate::widget::PaneGrid::new
+#[derive(Debug, Clone)]
+pub struct State<T> {
+ pub(super) panes: HashMap<Pane, T>,
+ pub(super) internal: Internal,
+}
+
+impl<T> State<T> {
+ /// Creates a new [`State`], initializing the first pane with the provided
+ /// state.
+ ///
+ /// Alongside the [`State`], it returns the first [`Pane`] identifier.
+ pub fn new(first_pane_state: T) -> (Self, Pane) {
+ (
+ Self::with_configuration(Configuration::Pane(first_pane_state)),
+ Pane(0),
+ )
+ }
+
+ /// Creates a new [`State`] with the given [`Configuration`].
+ pub fn with_configuration(config: impl Into<Configuration<T>>) -> Self {
+ let mut panes = HashMap::new();
+
+ let (layout, last_id) =
+ Self::distribute_content(&mut panes, config.into(), 0);
+
+ State {
+ panes,
+ internal: Internal {
+ layout,
+ last_id,
+ action: Action::Idle,
+ },
+ }
+ }
+
+ /// Returns the total amount of panes in the [`State`].
+ pub fn len(&self) -> usize {
+ self.panes.len()
+ }
+
+ /// Returns the internal state of the given [`Pane`], if it exists.
+ pub fn get(&self, pane: &Pane) -> Option<&T> {
+ self.panes.get(pane)
+ }
+
+ /// Returns the internal state of the given [`Pane`] with mutability, if it
+ /// exists.
+ pub fn get_mut(&mut self, pane: &Pane) -> Option<&mut T> {
+ self.panes.get_mut(pane)
+ }
+
+ /// Returns an iterator over all the panes of the [`State`], alongside its
+ /// internal state.
+ pub fn iter(&self) -> impl Iterator<Item = (&Pane, &T)> {
+ self.panes.iter()
+ }
+
+ /// Returns a mutable iterator over all the panes of the [`State`],
+ /// alongside its internal state.
+ pub fn iter_mut(&mut self) -> impl Iterator<Item = (&Pane, &mut T)> {
+ self.panes.iter_mut()
+ }
+
+ /// Returns the layout of the [`State`].
+ pub fn layout(&self) -> &Node {
+ &self.internal.layout
+ }
+
+ /// Returns the adjacent [`Pane`] of another [`Pane`] in the given
+ /// direction, if there is one.
+ pub fn adjacent(&self, pane: &Pane, direction: Direction) -> Option<Pane> {
+ let regions = self
+ .internal
+ .layout
+ .pane_regions(0.0, Size::new(4096.0, 4096.0));
+
+ let current_region = regions.get(pane)?;
+
+ let target = match direction {
+ Direction::Left => {
+ Point::new(current_region.x - 1.0, current_region.y + 1.0)
+ }
+ Direction::Right => Point::new(
+ current_region.x + current_region.width + 1.0,
+ current_region.y + 1.0,
+ ),
+ Direction::Up => {
+ Point::new(current_region.x + 1.0, current_region.y - 1.0)
+ }
+ Direction::Down => Point::new(
+ current_region.x + 1.0,
+ current_region.y + current_region.height + 1.0,
+ ),
+ };
+
+ let mut colliding_regions =
+ regions.iter().filter(|(_, region)| region.contains(target));
+
+ let (pane, _) = colliding_regions.next()?;
+
+ Some(*pane)
+ }
+
+ /// Splits the given [`Pane`] into two in the given [`Axis`] and
+ /// initializing the new [`Pane`] with the provided internal state.
+ pub fn split(
+ &mut self,
+ axis: Axis,
+ pane: &Pane,
+ state: T,
+ ) -> Option<(Pane, Split)> {
+ let node = self.internal.layout.find(pane)?;
+
+ let new_pane = {
+ self.internal.last_id = self.internal.last_id.checked_add(1)?;
+
+ Pane(self.internal.last_id)
+ };
+
+ let new_split = {
+ self.internal.last_id = self.internal.last_id.checked_add(1)?;
+
+ Split(self.internal.last_id)
+ };
+
+ node.split(new_split, axis, new_pane);
+
+ let _ = self.panes.insert(new_pane, state);
+
+ Some((new_pane, new_split))
+ }
+
+ /// Swaps the position of the provided panes in the [`State`].
+ ///
+ /// If you want to swap panes on drag and drop in your [`PaneGrid`], you
+ /// will need to call this method when handling a [`DragEvent`].
+ ///
+ /// [`PaneGrid`]: crate::widget::PaneGrid
+ /// [`DragEvent`]: crate::widget::pane_grid::DragEvent
+ pub fn swap(&mut self, a: &Pane, b: &Pane) {
+ self.internal.layout.update(&|node| match node {
+ Node::Split { .. } => {}
+ Node::Pane(pane) => {
+ if pane == a {
+ *node = Node::Pane(*b);
+ } else if pane == b {
+ *node = Node::Pane(*a);
+ }
+ }
+ });
+ }
+
+ /// Resizes two panes by setting the position of the provided [`Split`].
+ ///
+ /// The ratio is a value in [0, 1], representing the exact position of a
+ /// [`Split`] between two panes.
+ ///
+ /// If you want to enable resize interactions in your [`PaneGrid`], you will
+ /// need to call this method when handling a [`ResizeEvent`].
+ ///
+ /// [`PaneGrid`]: crate::widget::PaneGrid
+ /// [`ResizeEvent`]: crate::widget::pane_grid::ResizeEvent
+ pub fn resize(&mut self, split: &Split, ratio: f32) {
+ let _ = self.internal.layout.resize(split, ratio);
+ }
+
+ /// Closes the given [`Pane`] and returns its internal state and its closest
+ /// sibling, if it exists.
+ pub fn close(&mut self, pane: &Pane) -> Option<(T, Pane)> {
+ if let Some(sibling) = self.internal.layout.remove(pane) {
+ self.panes.remove(pane).map(|state| (state, sibling))
+ } else {
+ None
+ }
+ }
+
+ fn distribute_content(
+ panes: &mut HashMap<Pane, T>,
+ content: Configuration<T>,
+ next_id: usize,
+ ) -> (Node, usize) {
+ match content {
+ Configuration::Split { axis, ratio, a, b } => {
+ let (a, next_id) = Self::distribute_content(panes, *a, next_id);
+ let (b, next_id) = Self::distribute_content(panes, *b, next_id);
+
+ (
+ Node::Split {
+ id: Split(next_id),
+ axis,
+ ratio,
+ a: Box::new(a),
+ b: Box::new(b),
+ },
+ next_id + 1,
+ )
+ }
+ Configuration::Pane(state) => {
+ let id = Pane(next_id);
+ let _ = panes.insert(id, state);
+
+ (Node::Pane(id), next_id + 1)
+ }
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct Internal {
+ layout: Node,
+ last_id: usize,
+ action: Action,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum Action {
+ Idle,
+ Dragging { pane: Pane, origin: Point },
+ Resizing { split: Split, axis: Axis },
+}
+
+impl Internal {
+ pub fn picked_pane(&self) -> Option<(Pane, Point)> {
+ match self.action {
+ Action::Dragging { pane, origin, .. } => Some((pane, origin)),
+ _ => None,
+ }
+ }
+
+ pub fn picked_split(&self) -> Option<(Split, Axis)> {
+ match self.action {
+ Action::Resizing { split, axis, .. } => Some((split, axis)),
+ _ => None,
+ }
+ }
+
+ pub fn pane_regions(
+ &self,
+ spacing: f32,
+ size: Size,
+ ) -> HashMap<Pane, Rectangle> {
+ self.layout.pane_regions(spacing, size)
+ }
+
+ pub fn split_regions(
+ &self,
+ spacing: f32,
+ size: Size,
+ ) -> HashMap<Split, (Axis, Rectangle, f32)> {
+ self.layout.split_regions(spacing, size)
+ }
+
+ pub fn pick_pane(&mut self, pane: &Pane, origin: Point) {
+ self.action = Action::Dragging {
+ pane: *pane,
+ origin,
+ };
+ }
+
+ pub fn pick_split(&mut self, split: &Split, axis: Axis) {
+ // TODO: Obtain `axis` from layout itself. Maybe we should implement
+ // `Node::find_split`
+ if self.picked_pane().is_some() {
+ return;
+ }
+
+ self.action = Action::Resizing {
+ split: *split,
+ axis,
+ };
+ }
+
+ pub fn idle(&mut self) {
+ self.action = Action::Idle;
+ }
+
+ pub fn hash_layout(&self, hasher: &mut Hasher) {
+ use std::hash::Hash;
+
+ self.layout.hash(hasher);
+ }
+}
diff --git a/native/src/widget/pane_grid/title_bar.rs b/native/src/widget/pane_grid/title_bar.rs
new file mode 100644
index 00000000..475cb9ae
--- /dev/null
+++ b/native/src/widget/pane_grid/title_bar.rs
@@ -0,0 +1,253 @@
+use crate::event::{self, Event};
+use crate::layout;
+use crate::pane_grid;
+use crate::{Clipboard, Element, Hasher, Layout, 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>,
+ controls: Option<Element<'a, Message, Renderer>>,
+ padding: u16,
+ always_show_controls: bool,
+ style: 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 {
+ Self {
+ title: title.into(),
+ title_size: None,
+ controls: None,
+ padding: 0,
+ always_show_controls: false,
+ style: Renderer::Style::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,
+ controls: impl Into<Element<'a, Message, Renderer>>,
+ ) -> Self {
+ self.controls = Some(controls.into());
+ self
+ }
+
+ /// Sets the padding of the [`TitleBar`].
+ pub fn padding(mut self, units: u16) -> Self {
+ self.padding = units;
+ self
+ }
+
+ /// Sets the style of the [`TitleBar`].
+ pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {
+ self.style = style.into();
+ self
+ }
+
+ /// Sets whether or not the [`controls`] attached to this [`TitleBar`] are
+ /// always visible.
+ ///
+ /// By default, the controls are only visible when the [`Pane`] of this
+ /// [`TitleBar`] is hovered.
+ ///
+ /// [`controls`]: Self::controls
+ /// [`Pane`]: crate::widget::pane_grid::Pane
+ pub fn always_show_controls(mut self) -> Self {
+ self.always_show_controls = true;
+ self
+ }
+}
+
+impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer>
+where
+ Renderer: pane_grid::Renderer,
+{
+ /// Draws the [`TitleBar`] with the provided [`Renderer`] and [`Layout`].
+ ///
+ /// [`Renderer`]: crate::widget::pane_grid::Renderer
+ pub fn draw(
+ &self,
+ renderer: &mut Renderer,
+ defaults: &Renderer::Defaults,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ 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 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,
+ )
+ } 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,
+ )
+ }
+ }
+
+ /// Returns whether the mouse cursor is over the pick area of the
+ /// [`TitleBar`] or not.
+ ///
+ /// The whole [`TitleBar`] is a pick area, except its controls.
+ pub fn is_over_pick_area(
+ &self,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ ) -> bool {
+ if layout.bounds().contains(cursor_position) {
+ let mut children = layout.children();
+ let padded = 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)
+ } else {
+ true
+ }
+ } else {
+ false
+ }
+ }
+
+ pub(crate) fn hash_layout(&self, hasher: &mut Hasher) {
+ use std::hash::Hash;
+
+ self.title.hash(hasher);
+ self.title_size.hash(hasher);
+ self.padding.hash(hasher);
+ }
+
+ pub(crate) fn layout(
+ &self,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ let padding = f32::from(self.padding);
+ let limits = limits.pad(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 mut node = if let Some(controls) = &self.controls {
+ let mut controls_layout = controls
+ .layout(renderer, &layout::Limits::new(Size::ZERO, max_size));
+
+ 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(
+ Size::new(max_size.width, height),
+ vec![title_layout, controls_layout],
+ )
+ } else {
+ layout::Node::new(Size::new(max_size.width, title_height))
+ };
+
+ node.move_to(Point::new(padding, padding));
+
+ layout::Node::with_children(node.size().pad(padding), vec![node])
+ }
+
+ pub(crate) fn on_event(
+ &mut self,
+ event: Event,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ messages: &mut Vec<Message>,
+ renderer: &Renderer,
+ clipboard: Option<&dyn Clipboard>,
+ ) -> event::Status {
+ if let Some(controls) = &mut self.controls {
+ let mut children = layout.children();
+ let padded = children.next().unwrap();
+
+ let mut children = padded.children();
+ let _ = children.next();
+ let controls_layout = children.next().unwrap();
+
+ controls.on_event(
+ event,
+ controls_layout,
+ cursor_position,
+ messages,
+ renderer,
+ clipboard,
+ )
+ } else {
+ event::Status::Ignored
+ }
+ }
+}
diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs
new file mode 100644
index 00000000..58c0dfe1
--- /dev/null
+++ b/native/src/widget/pick_list.rs
@@ -0,0 +1,343 @@
+//! Display a dropdown list of selectable values.
+use crate::event::{self, Event};
+use crate::layout;
+use crate::mouse;
+use crate::overlay;
+use crate::overlay::menu::{self, Menu};
+use crate::scrollable;
+use crate::text;
+use crate::{
+ Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget,
+};
+use std::borrow::Cow;
+
+/// A widget for selecting a single value from a list of options.
+#[allow(missing_debug_implementations)]
+pub struct PickList<'a, T, Message, Renderer: self::Renderer>
+where
+ [T]: ToOwned<Owned = Vec<T>>,
+{
+ menu: &'a mut menu::State,
+ is_open: &'a mut bool,
+ hovered_option: &'a mut Option<usize>,
+ last_selection: &'a mut Option<T>,
+ on_selected: Box<dyn Fn(T) -> Message>,
+ options: Cow<'a, [T]>,
+ selected: Option<T>,
+ width: Length,
+ padding: u16,
+ text_size: Option<u16>,
+ font: Renderer::Font,
+ style: <Renderer as self::Renderer>::Style,
+}
+
+/// The local state of a [`PickList`].
+#[derive(Debug, Clone)]
+pub struct State<T> {
+ menu: menu::State,
+ is_open: bool,
+ hovered_option: Option<usize>,
+ last_selection: Option<T>,
+}
+
+impl<T> Default for State<T> {
+ fn default() -> Self {
+ Self {
+ menu: menu::State::default(),
+ is_open: bool::default(),
+ hovered_option: Option::default(),
+ last_selection: Option::default(),
+ }
+ }
+}
+
+impl<'a, T: 'a, Message, Renderer: self::Renderer>
+ PickList<'a, T, Message, Renderer>
+where
+ T: ToString + Eq,
+ [T]: ToOwned<Owned = Vec<T>>,
+{
+ /// Creates a new [`PickList`] with the given [`State`], a list of options,
+ /// the current selected value, and the message to produce when an option is
+ /// selected.
+ pub fn new(
+ state: &'a mut State<T>,
+ options: impl Into<Cow<'a, [T]>>,
+ selected: Option<T>,
+ on_selected: impl Fn(T) -> Message + 'static,
+ ) -> Self {
+ let State {
+ menu,
+ is_open,
+ hovered_option,
+ last_selection,
+ } = state;
+
+ Self {
+ menu,
+ is_open,
+ hovered_option,
+ last_selection,
+ on_selected: Box::new(on_selected),
+ options: options.into(),
+ selected,
+ width: Length::Shrink,
+ text_size: None,
+ padding: Renderer::DEFAULT_PADDING,
+ font: Default::default(),
+ style: Default::default(),
+ }
+ }
+
+ /// 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;
+ self
+ }
+
+ /// Sets the text size of the [`PickList`].
+ pub fn text_size(mut self, size: u16) -> Self {
+ self.text_size = Some(size);
+ self
+ }
+
+ /// Sets the font of the [`PickList`].
+ pub fn font(mut self, font: Renderer::Font) -> Self {
+ self.font = font;
+ self
+ }
+
+ /// Sets the style of the [`PickList`].
+ pub fn style(
+ mut self,
+ style: impl Into<<Renderer as self::Renderer>::Style>,
+ ) -> Self {
+ self.style = style.into();
+ self
+ }
+}
+
+impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer>
+ for PickList<'a, T, Message, Renderer>
+where
+ T: Clone + ToString + Eq,
+ [T]: ToOwned<Owned = Vec<T>>,
+ Message: 'static,
+ Renderer: self::Renderer + scrollable::Renderer + 'a,
+{
+ fn width(&self) -> Length {
+ Length::Shrink
+ }
+
+ fn height(&self) -> Length {
+ Length::Shrink
+ }
+
+ fn layout(
+ &self,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ use std::f32;
+
+ let limits = limits
+ .width(self.width)
+ .height(Length::Shrink)
+ .pad(f32::from(self.padding));
+
+ let text_size = self.text_size.unwrap_or(renderer.default_size());
+
+ let max_width = match self.width {
+ Length::Shrink => {
+ 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)
+ }
+ _ => 0,
+ };
+
+ let size = {
+ let intrinsic = Size::new(
+ max_width as f32
+ + f32::from(text_size)
+ + f32::from(self.padding),
+ f32::from(text_size),
+ );
+
+ limits.resolve(intrinsic).pad(f32::from(self.padding))
+ };
+
+ layout::Node::new(size)
+ }
+
+ fn hash_layout(&self, state: &mut Hasher) {
+ use std::hash::Hash as _;
+
+ match self.width {
+ Length::Shrink => {
+ self.options
+ .iter()
+ .map(ToString::to_string)
+ .for_each(|label| label.hash(state));
+ }
+ _ => {
+ self.width.hash(state);
+ }
+ }
+ }
+
+ fn on_event(
+ &mut self,
+ event: Event,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ messages: &mut Vec<Message>,
+ _renderer: &Renderer,
+ _clipboard: Option<&dyn Clipboard>,
+ ) -> event::Status {
+ match event {
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
+ let event_status = if *self.is_open {
+ // TODO: Encode cursor availability in the type system
+ *self.is_open =
+ cursor_position.x < 0.0 || cursor_position.y < 0.0;
+
+ event::Status::Captured
+ } else if layout.bounds().contains(cursor_position) {
+ let selected = self.selected.as_ref();
+
+ *self.is_open = true;
+ *self.hovered_option = self
+ .options
+ .iter()
+ .position(|option| Some(option) == selected);
+
+ event::Status::Captured
+ } else {
+ event::Status::Ignored
+ };
+
+ if let Some(last_selection) = self.last_selection.take() {
+ messages.push((self.on_selected)(last_selection));
+
+ *self.is_open = false;
+
+ event::Status::Captured
+ } else {
+ event_status
+ }
+ }
+ _ => event::Status::Ignored,
+ }
+ }
+
+ fn draw(
+ &self,
+ renderer: &mut Renderer,
+ _defaults: &Renderer::Defaults,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ _viewport: &Rectangle,
+ ) -> Renderer::Output {
+ self::Renderer::draw(
+ renderer,
+ layout.bounds(),
+ cursor_position,
+ self.selected.as_ref().map(ToString::to_string),
+ self.padding,
+ self.text_size.unwrap_or(renderer.default_size()),
+ self.font,
+ &self.style,
+ )
+ }
+
+ fn overlay(
+ &mut self,
+ layout: Layout<'_>,
+ ) -> Option<overlay::Element<'_, Message, Renderer>> {
+ if *self.is_open {
+ let bounds = layout.bounds();
+
+ let mut menu = Menu::new(
+ &mut self.menu,
+ &self.options,
+ &mut self.hovered_option,
+ &mut self.last_selection,
+ )
+ .width(bounds.width.round() as u16)
+ .padding(self.padding)
+ .font(self.font)
+ .style(Renderer::menu_style(&self.style));
+
+ if let Some(text_size) = self.text_size {
+ menu = menu.text_size(text_size);
+ }
+
+ Some(menu.overlay(layout.position(), bounds.height))
+ } else {
+ None
+ }
+ }
+}
+
+/// The renderer of a [`PickList`].
+///
+/// Your [renderer] will need to implement this trait before being
+/// able to use a [`PickList`] in your user interface.
+///
+/// [renderer]: crate::renderer
+pub trait Renderer: text::Renderer + menu::Renderer {
+ /// The default padding of a [`PickList`].
+ const DEFAULT_PADDING: u16;
+
+ /// The [`PickList`] style supported by this renderer.
+ type Style: Default;
+
+ /// Returns the style of the [`Menu`] of the [`PickList`].
+ fn menu_style(
+ style: &<Self as Renderer>::Style,
+ ) -> <Self as menu::Renderer>::Style;
+
+ /// Draws a [`PickList`].
+ fn draw(
+ &mut self,
+ bounds: Rectangle,
+ cursor_position: Point,
+ selected: Option<String>,
+ padding: u16,
+ text_size: u16,
+ font: Self::Font,
+ style: &<Self as Renderer>::Style,
+ ) -> Self::Output;
+}
+
+impl<'a, T: 'a, Message, Renderer> Into<Element<'a, Message, Renderer>>
+ for PickList<'a, T, Message, Renderer>
+where
+ T: Clone + ToString + Eq,
+ [T]: ToOwned<Owned = Vec<T>>,
+ Renderer: self::Renderer + 'a,
+ Message: 'static,
+{
+ fn into(self) -> Element<'a, Message, Renderer> {
+ Element::new(self)
+ }
+}
diff --git a/native/src/widget/progress_bar.rs b/native/src/widget/progress_bar.rs
index 67d1ab83..d294f198 100644
--- a/native/src/widget/progress_bar.rs
+++ b/native/src/widget/progress_bar.rs
@@ -33,8 +33,6 @@ impl<Renderer: self::Renderer> ProgressBar<Renderer> {
/// It expects:
/// * an inclusive range of possible values
/// * the current value of the [`ProgressBar`]
- ///
- /// [`ProgressBar`]: struct.ProgressBar.html
pub fn new(range: RangeInclusive<f32>, value: f32) -> Self {
ProgressBar {
value: value.max(*range.start()).min(*range.end()),
@@ -46,24 +44,18 @@ impl<Renderer: self::Renderer> ProgressBar<Renderer> {
}
/// Sets the width of the [`ProgressBar`].
- ///
- /// [`ProgressBar`]: struct.ProgressBar.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`ProgressBar`].
- ///
- /// [`ProgressBar`]: struct.ProgressBar.html
pub fn height(mut self, height: Length) -> Self {
self.height = Some(height);
self
}
/// Sets the style of the [`ProgressBar`].
- ///
- /// [`ProgressBar`]: struct.ProgressBar.html
pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {
self.style = style.into();
self
@@ -104,6 +96,7 @@ where
_defaults: &Renderer::Defaults,
layout: Layout<'_>,
_cursor_position: Point,
+ _viewport: &Rectangle,
) -> Renderer::Output {
renderer.draw(
layout.bounds(),
@@ -114,6 +107,9 @@ where
}
fn hash_layout(&self, state: &mut Hasher) {
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
self.width.hash(state);
self.height.hash(state);
}
@@ -124,15 +120,12 @@ where
/// Your [renderer] will need to implement this trait before being
/// able to use a [`ProgressBar`] in your user interface.
///
-/// [`ProgressBar`]: struct.ProgressBar.html
-/// [renderer]: ../../renderer/index.html
+/// [renderer]: crate::renderer
pub trait Renderer: crate::Renderer {
/// The style supported by this renderer.
type Style: Default;
/// The default height of a [`ProgressBar`].
- ///
- /// [`ProgressBar`]: struct.ProgressBar.html
const DEFAULT_HEIGHT: u16;
/// Draws a [`ProgressBar`].
@@ -143,8 +136,6 @@ pub trait Renderer: crate::Renderer {
/// * the current value of the [`ProgressBar`]
/// * maybe a specific background of the [`ProgressBar`]
/// * maybe a specific active color of the [`ProgressBar`]
- ///
- /// [`ProgressBar`]: struct.ProgressBar.html
fn draw(
&self,
bounds: Rectangle,
@@ -157,8 +148,8 @@ pub trait Renderer: crate::Renderer {
impl<'a, Message, Renderer> From<ProgressBar<Renderer>>
for Element<'a, Message, Renderer>
where
- Renderer: 'static + self::Renderer,
- Message: 'static,
+ Renderer: 'a + self::Renderer,
+ Message: 'a,
{
fn from(
progress_bar: ProgressBar<Renderer>,
diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs
index 46983db3..3a1dd386 100644
--- a/native/src/widget/radio.rs
+++ b/native/src/widget/radio.rs
@@ -1,9 +1,13 @@
//! Create choices using radio buttons.
+use crate::event::{self, Event};
+use crate::layout;
+use crate::mouse;
+use crate::row;
+use crate::text;
+use crate::touch::{self, Touch};
use crate::{
- input::{mouse, touch, ButtonState, Touch},
- layout, row, text, Align, Clipboard, Element, Event, Font, Hasher,
- HorizontalAlignment, Layout, Length, Point, Rectangle, Row, Text,
- VerticalAlignment, Widget,
+ Align, Clipboard, Element, Hasher, HorizontalAlignment, Layout, Length,
+ Point, Rectangle, Row, Text, VerticalAlignment, Widget,
};
use std::hash::Hash;
@@ -35,14 +39,22 @@ use std::hash::Hash;
///
/// ![Radio buttons drawn by `iced_wgpu`](https://github.com/hecrj/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/radio.png?raw=true)
#[allow(missing_debug_implementations)]
-pub struct Radio<Message, Renderer: self::Renderer> {
+pub struct Radio<Message, Renderer: self::Renderer + text::Renderer> {
is_selected: bool,
on_click: Message,
label: String,
+ width: Length,
+ size: u16,
+ spacing: u16,
+ text_size: Option<u16>,
style: Renderer::Style,
}
-impl<Message, Renderer: self::Renderer> Radio<Message, Renderer> {
+impl<Message, Renderer: self::Renderer + text::Renderer>
+ Radio<Message, Renderer>
+where
+ Message: Clone,
+{
/// Creates a new [`Radio`] button.
///
/// It expects:
@@ -51,9 +63,12 @@ impl<Message, Renderer: self::Renderer> Radio<Message, Renderer> {
/// * the current selected value
/// * a function that will be called when the [`Radio`] is selected. It
/// receives the value of the radio and must produce a `Message`.
- ///
- /// [`Radio`]: struct.Radio.html
- pub fn new<F, V>(value: V, label: &str, selected: Option<V>, f: F) -> Self
+ pub fn new<F, V>(
+ value: V,
+ label: impl Into<String>,
+ selected: Option<V>,
+ f: F,
+ ) -> Self
where
V: Eq + Copy,
F: 'static + Fn(V) -> Message,
@@ -61,14 +76,40 @@ impl<Message, Renderer: self::Renderer> Radio<Message, Renderer> {
Radio {
is_selected: Some(value) == selected,
on_click: f(value),
- label: String::from(label),
+ label: label.into(),
+ width: Length::Shrink,
+ size: <Renderer as self::Renderer>::DEFAULT_SIZE,
+ spacing: Renderer::DEFAULT_SPACING, //15
+ text_size: None,
style: Renderer::Style::default(),
}
}
+ /// Sets the size of the [`Radio`] button.
+ pub fn size(mut self, size: u16) -> Self {
+ self.size = size;
+ self
+ }
+
+ /// Sets the width of the [`Radio`] button.
+ pub fn width(mut self, width: Length) -> Self {
+ self.width = width;
+ self
+ }
+
+ /// Sets the spacing between the [`Radio`] button and the text.
+ pub fn spacing(mut self, spacing: u16) -> Self {
+ self.spacing = spacing;
+ self
+ }
+
+ /// Sets the text size of the [`Radio`] button.
+ pub fn text_size(mut self, text_size: u16) -> Self {
+ self.text_size = Some(text_size);
+ self
+ }
+
/// Sets the style of the [`Radio`] button.
- ///
- /// [`Radio`]: struct.Radio.html
pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {
self.style = style.into();
self
@@ -77,11 +118,11 @@ impl<Message, Renderer: self::Renderer> Radio<Message, Renderer> {
impl<Message, Renderer> Widget<Message, Renderer> for Radio<Message, Renderer>
where
- Renderer: self::Renderer + text::Renderer + row::Renderer,
Message: Clone,
+ Renderer: self::Renderer + text::Renderer + row::Renderer,
{
fn width(&self) -> Length {
- Length::Fill
+ self.width
}
fn height(&self) -> Length {
@@ -93,18 +134,20 @@ where
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let size = self::Renderer::default_size(renderer);
-
Row::<(), Renderer>::new()
- .width(Length::Fill)
- .spacing(15)
+ .width(self.width)
+ .spacing(self.spacing)
.align_items(Align::Center)
.push(
Row::new()
- .width(Length::Units(size as u16))
- .height(Length::Units(size as u16)),
+ .width(Length::Units(self.size))
+ .height(Length::Units(self.size)),
+ )
+ .push(
+ Text::new(&self.label)
+ .width(self.width)
+ .size(self.text_size.unwrap_or(renderer.default_size())),
)
- .push(Text::new(&self.label))
.layout(renderer, limits)
}
@@ -116,22 +159,23 @@ where
messages: &mut Vec<Message>,
_renderer: &Renderer,
_clipboard: Option<&dyn Clipboard>,
- ) {
+ ) -> event::Status {
match event {
- Event::Mouse(mouse::Event::Input {
- button: mouse::Button::Left,
- state: ButtonState::Pressed,
- })
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(Touch {
phase: touch::Phase::Started,
..
}) => {
if layout.bounds().contains(cursor_position) {
messages.push(self.on_click.clone());
+
+ return event::Status::Captured;
}
}
_ => {}
}
+
+ event::Status::Ignored
}
fn draw(
@@ -140,6 +184,7 @@ where
defaults: &Renderer::Defaults,
layout: Layout<'_>,
cursor_position: Point,
+ _viewport: &Rectangle,
) -> Renderer::Output {
let bounds = layout.bounds();
let mut children = layout.children();
@@ -153,8 +198,8 @@ where
defaults,
label_layout.bounds(),
&self.label,
- <Renderer as text::Renderer>::DEFAULT_SIZE,
- Font::Default,
+ self.text_size.unwrap_or(renderer.default_size()),
+ Default::default(),
None,
HorizontalAlignment::Left,
VerticalAlignment::Center,
@@ -173,6 +218,9 @@ where
}
fn hash_layout(&self, state: &mut Hasher) {
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
self.label.hash(state);
}
}
@@ -182,16 +230,16 @@ where
/// Your [renderer] will need to implement this trait before being
/// able to use a [`Radio`] button in your user interface.
///
-/// [`Radio`]: struct.Radio.html
-/// [renderer]: ../../renderer/index.html
+/// [renderer]: crate::renderer
pub trait Renderer: crate::Renderer {
/// The style supported by this renderer.
type Style: Default;
- /// Returns the default size of a [`Radio`] button.
- ///
- /// [`Radio`]: struct.Radio.html
- fn default_size(&self) -> u32;
+ /// The default size of a [`Radio`] button.
+ const DEFAULT_SIZE: u16;
+
+ /// The default spacing of a [`Radio`] button.
+ const DEFAULT_SPACING: u16;
/// Draws a [`Radio`] button.
///
@@ -200,8 +248,6 @@ pub trait Renderer: crate::Renderer {
/// * whether the [`Radio`] is selected or not
/// * whether the mouse is over the [`Radio`] or not
/// * the drawn label of the [`Radio`]
- ///
- /// [`Radio`]: struct.Radio.html
fn draw(
&mut self,
bounds: Rectangle,
@@ -215,8 +261,8 @@ pub trait Renderer: crate::Renderer {
impl<'a, Message, Renderer> From<Radio<Message, Renderer>>
for Element<'a, Message, Renderer>
where
- Renderer: 'static + self::Renderer + row::Renderer + text::Renderer,
- Message: 'static + Clone,
+ Message: 'a + Clone,
+ Renderer: 'a + self::Renderer + row::Renderer + text::Renderer,
{
fn from(radio: Radio<Message, Renderer>) -> Element<'a, Message, Renderer> {
Element::new(radio)
diff --git a/native/src/widget/row.rs b/native/src/widget/row.rs
index c8812ea2..b71663bd 100644
--- a/native/src/widget/row.rs
+++ b/native/src/widget/row.rs
@@ -1,18 +1,15 @@
//! Distribute content horizontally.
-use std::hash::Hash;
-
+use crate::event::{self, Event};
+use crate::layout;
+use crate::overlay;
use crate::{
- layout, Align, Clipboard, Element, Event, Hasher, Layout, Length, Point,
- Widget,
+ Align, Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Widget,
};
+use std::hash::Hash;
use std::u32;
/// A container that distributes its contents horizontally.
-///
-/// A [`Row`] will try to fill the horizontal space of its container.
-///
-/// [`Row`]: struct.Row.html
#[allow(missing_debug_implementations)]
pub struct Row<'a, Message, Renderer> {
spacing: u16,
@@ -27,15 +24,11 @@ pub struct Row<'a, Message, Renderer> {
impl<'a, Message, Renderer> Row<'a, Message, Renderer> {
/// Creates an empty [`Row`].
- ///
- /// [`Row`]: struct.Row.html
pub fn new() -> Self {
Self::with_children(Vec::new())
}
/// Creates a [`Row`] with the given elements.
- ///
- /// [`Row`]: struct.Row.html
pub fn with_children(
children: Vec<Element<'a, Message, Renderer>>,
) -> Self {
@@ -62,57 +55,42 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> {
}
/// Sets the padding of the [`Row`].
- ///
- /// [`Row`]: struct.Row.html
pub fn padding(mut self, units: u16) -> Self {
self.padding = units;
self
}
/// Sets the width of the [`Row`].
- ///
- /// [`Row`]: struct.Row.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`Row`].
- ///
- /// [`Row`]: struct.Row.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
/// Sets the maximum width of the [`Row`].
- ///
- /// [`Row`]: struct.Row.html
pub fn max_width(mut self, max_width: u32) -> Self {
self.max_width = max_width;
self
}
/// Sets the maximum height of the [`Row`].
- ///
- /// [`Row`]: struct.Row.html
pub fn max_height(mut self, max_height: u32) -> Self {
self.max_height = max_height;
self
}
/// Sets the vertical alignment of the contents of the [`Row`] .
- ///
- /// [`Row`]: struct.Row.html
pub fn align_items(mut self, align: Align) -> Self {
self.align_items = align;
self
}
/// Adds an [`Element`] to the [`Row`].
- ///
- /// [`Element`]: ../struct.Element.html
- /// [`Row`]: struct.Row.html
pub fn push<E>(mut self, child: E) -> Self
where
E: Into<Element<'a, Message, Renderer>>,
@@ -165,9 +143,11 @@ where
messages: &mut Vec<Message>,
renderer: &Renderer,
clipboard: Option<&dyn Clipboard>,
- ) {
- self.children.iter_mut().zip(layout.children()).for_each(
- |(child, layout)| {
+ ) -> event::Status {
+ self.children
+ .iter_mut()
+ .zip(layout.children())
+ .map(|(child, layout)| {
child.widget.on_event(
event.clone(),
layout,
@@ -176,8 +156,8 @@ where
renderer,
clipboard,
)
- },
- );
+ })
+ .fold(event::Status::Ignored, event::Status::merge)
}
fn draw(
@@ -186,24 +166,44 @@ where
defaults: &Renderer::Defaults,
layout: Layout<'_>,
cursor_position: Point,
+ viewport: &Rectangle,
) -> Renderer::Output {
- renderer.draw(defaults, &self.children, layout, cursor_position)
+ renderer.draw(
+ defaults,
+ &self.children,
+ layout,
+ cursor_position,
+ viewport,
+ )
}
fn hash_layout(&self, state: &mut Hasher) {
- 1.hash(state);
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
self.width.hash(state);
self.height.hash(state);
self.max_width.hash(state);
self.max_height.hash(state);
self.align_items.hash(state);
self.spacing.hash(state);
- self.spacing.hash(state);
+ self.padding.hash(state);
for child in &self.children {
child.widget.hash_layout(state);
}
}
+
+ fn overlay(
+ &mut self,
+ layout: Layout<'_>,
+ ) -> Option<overlay::Element<'_, Message, Renderer>> {
+ self.children
+ .iter_mut()
+ .zip(layout.children())
+ .filter_map(|(child, layout)| child.widget.overlay(layout))
+ .next()
+ }
}
/// The renderer of a [`Row`].
@@ -211,8 +211,7 @@ where
/// Your [renderer] will need to implement this trait before being
/// able to use a [`Row`] in your user interface.
///
-/// [`Row`]: struct.Row.html
-/// [renderer]: ../../renderer/index.html
+/// [renderer]: crate::renderer
pub trait Renderer: crate::Renderer + Sized {
/// Draws a [`Row`].
///
@@ -220,15 +219,13 @@ pub trait Renderer: crate::Renderer + Sized {
/// - the children of the [`Row`]
/// - the [`Layout`] of the [`Row`] and its children
/// - the cursor position
- ///
- /// [`Row`]: struct.Row.html
- /// [`Layout`]: ../layout/struct.Layout.html
fn draw<Message>(
&mut self,
defaults: &Self::Defaults,
children: &[Element<'_, Message, Self>],
layout: Layout<'_>,
cursor_position: Point,
+ viewport: &Rectangle,
) -> Self::Output;
}
@@ -236,7 +233,7 @@ impl<'a, Message, Renderer> From<Row<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Renderer: 'a + self::Renderer,
- Message: 'static,
+ Message: 'a,
{
fn from(row: Row<'a, Message, Renderer>) -> Element<'a, Message, Renderer> {
Element::new(row)
diff --git a/native/src/widget/rule.rs b/native/src/widget/rule.rs
new file mode 100644
index 00000000..18c88658
--- /dev/null
+++ b/native/src/widget/rule.rs
@@ -0,0 +1,116 @@
+//! Display a horizontal or vertical rule for dividing content.
+
+use std::hash::Hash;
+
+use crate::{
+ layout, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget,
+};
+
+/// Display a horizontal or vertical rule for dividing content.
+#[derive(Debug, Copy, Clone)]
+pub struct Rule<Renderer: self::Renderer> {
+ width: Length,
+ height: Length,
+ style: Renderer::Style,
+ is_horizontal: bool,
+}
+
+impl<Renderer: self::Renderer> Rule<Renderer> {
+ /// Creates a horizontal [`Rule`] for dividing content by the given vertical spacing.
+ pub fn horizontal(spacing: u16) -> Self {
+ Rule {
+ width: Length::Fill,
+ height: Length::from(Length::Units(spacing)),
+ style: Renderer::Style::default(),
+ is_horizontal: true,
+ }
+ }
+
+ /// Creates a vertical [`Rule`] for dividing content by the given horizontal spacing.
+ pub fn vertical(spacing: u16) -> Self {
+ Rule {
+ width: Length::from(Length::Units(spacing)),
+ height: Length::Fill,
+ style: Renderer::Style::default(),
+ is_horizontal: false,
+ }
+ }
+
+ /// Sets the style of the [`Rule`].
+ pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {
+ self.style = style.into();
+ self
+ }
+}
+
+impl<Message, Renderer> Widget<Message, Renderer> for Rule<Renderer>
+where
+ Renderer: self::Renderer,
+{
+ fn width(&self) -> Length {
+ self.width
+ }
+
+ fn height(&self) -> Length {
+ self.height
+ }
+
+ fn layout(
+ &self,
+ _renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ let limits = limits.width(self.width).height(self.height);
+
+ layout::Node::new(limits.resolve(Size::ZERO))
+ }
+
+ fn draw(
+ &self,
+ renderer: &mut Renderer,
+ _defaults: &Renderer::Defaults,
+ layout: Layout<'_>,
+ _cursor_position: Point,
+ _viewport: &Rectangle,
+ ) -> Renderer::Output {
+ renderer.draw(layout.bounds(), &self.style, self.is_horizontal)
+ }
+
+ fn hash_layout(&self, state: &mut Hasher) {
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
+ self.width.hash(state);
+ self.height.hash(state);
+ }
+}
+
+/// The renderer of a [`Rule`].
+pub trait Renderer: crate::Renderer {
+ /// The style supported by this renderer.
+ type Style: Default;
+
+ /// Draws a [`Rule`].
+ ///
+ /// It receives:
+ /// * the bounds of the [`Rule`]
+ /// * the style of the [`Rule`]
+ /// * whether the [`Rule`] is horizontal (true) or vertical (false)
+ fn draw(
+ &mut self,
+ bounds: Rectangle,
+ style: &Self::Style,
+ is_horizontal: bool,
+ ) -> Self::Output;
+}
+
+impl<'a, Message, Renderer> From<Rule<Renderer>>
+ for Element<'a, Message, Renderer>
+where
+ Renderer: 'a + self::Renderer,
+ Message: 'a,
+{
+ fn from(rule: Rule<Renderer>) -> Element<'a, Message, Renderer> {
+ Element::new(rule)
+ }
+}
diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs
index eb1722ed..8c321ee5 100644
--- a/native/src/widget/scrollable.rs
+++ b/native/src/widget/scrollable.rs
@@ -1,9 +1,13 @@
//! Navigate an endless amount of content with a scrollbar.
+use crate::column;
+use crate::event::{self, Event};
+use crate::layout;
+use crate::mouse;
+use crate::overlay;
+use crate::touch::{self, Touch};
use crate::{
- column,
- input::{mouse, touch, ButtonState, Touch},
- layout, Align, Clipboard, Column, Element, Event, Hasher, Layout, Length,
- Point, Rectangle, Size, Widget,
+ Align, Clipboard, Column, Element, Hasher, Layout, Length, Point,
+ Rectangle, Size, Vector, Widget,
};
use std::{f32, hash::Hash, u32};
@@ -15,20 +19,23 @@ pub struct Scrollable<'a, Message, Renderer: self::Renderer> {
state: &'a mut State,
height: Length,
max_height: u32,
+ scrollbar_width: u16,
+ scrollbar_margin: u16,
+ scroller_width: u16,
content: Column<'a, Message, Renderer>,
style: Renderer::Style,
}
impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> {
/// Creates a new [`Scrollable`] with the given [`State`].
- ///
- /// [`Scrollable`]: struct.Scrollable.html
- /// [`State`]: struct.State.html
pub fn new(state: &'a mut State) -> Self {
Scrollable {
state,
height: Length::Shrink,
max_height: u32::MAX,
+ scrollbar_width: 10,
+ scrollbar_margin: 0,
+ scroller_width: 10,
content: Column::new(),
style: Renderer::Style::default(),
}
@@ -45,64 +52,68 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> {
}
/// Sets the padding of the [`Scrollable`].
- ///
- /// [`Scrollable`]: struct.Scrollable.html
pub fn padding(mut self, units: u16) -> Self {
self.content = self.content.padding(units);
self
}
/// Sets the width of the [`Scrollable`].
- ///
- /// [`Scrollable`]: struct.Scrollable.html
pub fn width(mut self, width: Length) -> Self {
self.content = self.content.width(width);
self
}
/// Sets the height of the [`Scrollable`].
- ///
- /// [`Scrollable`]: struct.Scrollable.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
/// Sets the maximum width of the [`Scrollable`].
- ///
- /// [`Scrollable`]: struct.Scrollable.html
pub fn max_width(mut self, max_width: u32) -> Self {
self.content = self.content.max_width(max_width);
self
}
/// Sets the maximum height of the [`Scrollable`] in pixels.
- ///
- /// [`Scrollable`]: struct.Scrollable.html
pub fn max_height(mut self, max_height: u32) -> Self {
self.max_height = max_height;
self
}
/// Sets the horizontal alignment of the contents of the [`Scrollable`] .
- ///
- /// [`Scrollable`]: struct.Scrollable.html
pub fn align_items(mut self, align_items: Align) -> Self {
self.content = self.content.align_items(align_items);
self
}
+ /// Sets the scrollbar width of the [`Scrollable`] .
+ /// Silently enforces a minimum value of 1.
+ pub fn scrollbar_width(mut self, scrollbar_width: u16) -> Self {
+ self.scrollbar_width = scrollbar_width.max(1);
+ self
+ }
+
+ /// Sets the scrollbar margin of the [`Scrollable`] .
+ pub fn scrollbar_margin(mut self, scrollbar_margin: u16) -> Self {
+ self.scrollbar_margin = scrollbar_margin;
+ self
+ }
+
+ /// Sets the scroller width of the [`Scrollable`] .
+ /// 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 the style of the [`Scrollable`] .
- ///
- /// [`Scrollable`]: struct.Scrollable.html
pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {
self.style = style.into();
self
}
/// Adds an element to the [`Scrollable`].
- ///
- /// [`Scrollable`]: struct.Scrollable.html
pub fn push<E>(mut self, child: E) -> Self
where
E: Into<Element<'a, Message, Renderer>>,
@@ -115,7 +126,7 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> {
impl<'a, Message, Renderer> Widget<Message, Renderer>
for Scrollable<'a, Message, Renderer>
where
- Renderer: 'static + self::Renderer + column::Renderer,
+ Renderer: self::Renderer,
{
fn width(&self) -> Length {
Widget::<Message, Renderer>::width(&self.content)
@@ -154,14 +165,56 @@ where
messages: &mut Vec<Message>,
renderer: &Renderer,
clipboard: Option<&dyn Clipboard>,
- ) {
+ ) -> event::Status {
let bounds = layout.bounds();
let is_mouse_over = bounds.contains(cursor_position);
let content = layout.children().next().unwrap();
let content_bounds = content.bounds();
- // TODO: Event capture. Nested scrollables should capture scroll events.
+ let offset = self.state.offset(bounds, content_bounds);
+ let scrollbar = renderer.scrollbar(
+ bounds,
+ content_bounds,
+ offset,
+ self.scrollbar_width,
+ self.scrollbar_margin,
+ self.scroller_width,
+ );
+ let is_mouse_over_scrollbar = scrollbar
+ .as_ref()
+ .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
+ .unwrap_or(false);
+
+ let event_status = {
+ let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar {
+ Point::new(
+ cursor_position.x,
+ cursor_position.y
+ + self.state.offset(bounds, content_bounds) as f32,
+ )
+ } else {
+ // TODO: Make `cursor_position` an `Option<Point>` so we can encode
+ // cursor availability.
+ // This will probably happen naturally once we add multi-window
+ // support.
+ Point::new(cursor_position.x, -1.0)
+ };
+
+ self.content.on_event(
+ event.clone(),
+ content,
+ cursor_position,
+ messages,
+ renderer,
+ clipboard,
+ )
+ };
+
+ if let event::Status::Captured = event_status {
+ return event::Status::Captured;
+ }
+
if is_mouse_over {
match event {
Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
@@ -174,6 +227,8 @@ where
self.state.scroll(y, bounds, content_bounds);
}
}
+
+ return event::Status::Captured;
}
Event::Touch(Touch { phase, .. }) => match phase {
touch::Phase::Started => {
@@ -199,19 +254,11 @@ where
}
}
- let offset = self.state.offset(bounds, content_bounds);
- let scrollbar = renderer.scrollbar(bounds, content_bounds, offset);
- let is_mouse_over_scrollbar = scrollbar
- .as_ref()
- .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
- .unwrap_or(false);
-
if self.state.is_scroller_grabbed() {
match event {
- Event::Mouse(mouse::Event::Input {
- button: mouse::Button::Left,
- state: ButtonState::Released,
- })
+ Event::Mouse(mouse::Event::ButtonReleased(
+ mouse::Button::Left,
+ ))
| Event::Touch(Touch {
phase: touch::Phase::Ended,
..
@@ -221,6 +268,8 @@ where
..
}) => {
self.state.scroller_grabbed_at = None;
+
+ return event::Status::Captured;
}
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(Touch {
@@ -238,16 +287,17 @@ where
bounds,
content_bounds,
);
+
+ return event::Status::Captured;
}
}
_ => {}
}
} else if is_mouse_over_scrollbar {
match event {
- Event::Mouse(mouse::Event::Input {
- button: mouse::Button::Left,
- state: ButtonState::Pressed,
- })
+ Event::Mouse(mouse::Event::ButtonPressed(
+ mouse::Button::Left,
+ ))
| Event::Touch(Touch {
phase: touch::Phase::Started,
..
@@ -267,6 +317,8 @@ where
self.state.scroller_grabbed_at =
Some(scroller_grabbed_at);
+
+ return event::Status::Captured;
}
}
}
@@ -274,28 +326,7 @@ where
}
}
- let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar {
- Point::new(
- cursor_position.x,
- cursor_position.y
- + self.state.offset(bounds, content_bounds) as f32,
- )
- } else {
- // TODO: Make `cursor_position` an `Option<Point>` so we can encode
- // cursor availability.
- // This will probably happen naturally once we add multi-window
- // support.
- Point::new(cursor_position.x, -1.0)
- };
-
- self.content.on_event(
- event,
- content,
- cursor_position,
- messages,
- renderer,
- clipboard,
- )
+ event::Status::Ignored
}
fn draw(
@@ -304,12 +335,20 @@ where
defaults: &Renderer::Defaults,
layout: Layout<'_>,
cursor_position: Point,
+ _viewport: &Rectangle,
) -> Renderer::Output {
let bounds = layout.bounds();
let content_layout = layout.children().next().unwrap();
let content_bounds = content_layout.bounds();
let offset = self.state.offset(bounds, content_bounds);
- let scrollbar = renderer.scrollbar(bounds, content_bounds, offset);
+ let scrollbar = renderer.scrollbar(
+ bounds,
+ content_bounds,
+ offset,
+ self.scrollbar_width,
+ self.scrollbar_margin,
+ self.scroller_width,
+ );
let is_mouse_over = bounds.contains(cursor_position);
let is_mouse_over_scrollbar = scrollbar
@@ -329,6 +368,10 @@ where
defaults,
content_layout,
cursor_position,
+ &Rectangle {
+ y: bounds.y + offset as f32,
+ ..bounds
+ },
)
};
@@ -347,18 +390,35 @@ where
}
fn hash_layout(&self, state: &mut Hasher) {
- std::any::TypeId::of::<Scrollable<'static, (), Renderer>>().hash(state);
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
self.height.hash(state);
self.max_height.hash(state);
self.content.hash_layout(state)
}
+
+ fn overlay(
+ &mut self,
+ layout: Layout<'_>,
+ ) -> Option<overlay::Element<'_, Message, Renderer>> {
+ let Self { content, state, .. } = self;
+
+ content
+ .overlay(layout.children().next().unwrap())
+ .map(|overlay| {
+ let bounds = layout.bounds();
+ let content_layout = layout.children().next().unwrap();
+ let content_bounds = content_layout.bounds();
+ let offset = state.offset(bounds, content_bounds);
+
+ overlay.translate(Vector::new(0.0, -(offset as f32)))
+ })
+ }
}
/// The local state of a [`Scrollable`].
-///
-/// [`Scrollable`]: struct.Scrollable.html
#[derive(Debug, Clone, Copy, Default)]
pub struct State {
scroller_grabbed_at: Option<f32>,
@@ -368,17 +428,12 @@ pub struct State {
impl State {
/// Creates a new [`State`] with the scrollbar located at the top.
- ///
- /// [`State`]: struct.State.html
pub fn new() -> Self {
State::default()
}
/// Apply a scrolling offset to the current [`State`], given the bounds of
/// the [`Scrollable`] and its contents.
- ///
- /// [`Scrollable`]: struct.Scrollable.html
- /// [`State`]: struct.State.html
pub fn scroll(
&mut self,
delta_y: f32,
@@ -399,9 +454,6 @@ impl State {
///
/// `0` represents scrollbar at the top, while `1` represents scrollbar at
/// the bottom.
- ///
- /// [`Scrollable`]: struct.Scrollable.html
- /// [`State`]: struct.State.html
pub fn scroll_to(
&mut self,
percentage: f32,
@@ -414,9 +466,6 @@ impl State {
/// Returns the current scrolling offset of the [`State`], given the bounds
/// of the [`Scrollable`] and its contents.
- ///
- /// [`Scrollable`]: struct.Scrollable.html
- /// [`State`]: struct.State.html
pub fn offset(&self, bounds: Rectangle, content_bounds: Rectangle) -> u32 {
let hidden_content =
(content_bounds.height - bounds.height).max(0.0).round() as u32;
@@ -436,28 +485,29 @@ impl State {
}
/// The scrollbar of a [`Scrollable`].
-///
-/// [`Scrollable`]: struct.Scrollable.html
#[derive(Debug)]
pub struct Scrollbar {
+ /// The outer bounds of the scrollable, including the [`Scrollbar`] and
+ /// [`Scroller`].
+ pub outer_bounds: Rectangle,
+
/// The bounds of the [`Scrollbar`].
- ///
- /// [`Scrollbar`]: struct.Scrollbar.html
pub bounds: Rectangle,
+ /// The margin within the [`Scrollbar`].
+ pub margin: u16,
+
/// The bounds of the [`Scroller`].
- ///
- /// [`Scroller`]: struct.Scroller.html
pub scroller: Scroller,
}
impl Scrollbar {
fn is_mouse_over(&self, cursor_position: Point) -> bool {
- self.bounds.contains(cursor_position)
+ self.outer_bounds.contains(cursor_position)
}
fn grab_scroller(&self, cursor_position: Point) -> Option<f32> {
- if self.bounds.contains(cursor_position) {
+ if self.outer_bounds.contains(cursor_position) {
Some(if self.scroller.bounds.contains(cursor_position) {
(cursor_position.y - self.scroller.bounds.y)
/ self.scroller.bounds.height
@@ -482,13 +532,9 @@ impl Scrollbar {
}
/// The handle of a [`Scrollbar`].
-///
-/// [`Scrollbar`]: struct.Scrollbar.html
#[derive(Debug, Clone, Copy)]
pub struct Scroller {
/// The bounds of the [`Scroller`].
- ///
- /// [`Scroller`]: struct.Scrollbar.html
pub bounds: Rectangle,
}
@@ -497,22 +543,21 @@ pub struct Scroller {
/// Your [renderer] will need to implement this trait before being
/// able to use a [`Scrollable`] in your user interface.
///
-/// [`Scrollable`]: struct.Scrollable.html
-/// [renderer]: ../../renderer/index.html
-pub trait Renderer: crate::Renderer + Sized {
+/// [renderer]: crate::renderer
+pub trait Renderer: column::Renderer + Sized {
/// The style supported by this renderer.
type Style: Default;
/// Returns the [`Scrollbar`] given the bounds and content bounds of a
/// [`Scrollable`].
- ///
- /// [`Scrollbar`]: struct.Scrollbar.html
- /// [`Scrollable`]: struct.Scrollable.html
fn scrollbar(
&self,
bounds: Rectangle,
content_bounds: Rectangle,
offset: u32,
+ scrollbar_width: u16,
+ scrollbar_margin: u16,
+ scroller_width: u16,
) -> Option<Scrollbar>;
/// Draws the [`Scrollable`].
@@ -526,10 +571,6 @@ pub trait Renderer: crate::Renderer + Sized {
/// - a optional [`Scrollbar`] to be rendered
/// - the scrolling offset
/// - the drawn content
- ///
- /// [`Scrollbar`]: struct.Scrollbar.html
- /// [`Scrollable`]: struct.Scrollable.html
- /// [`State`]: struct.State.html
fn draw(
&mut self,
scrollable: &State,
@@ -547,8 +588,8 @@ pub trait Renderer: crate::Renderer + Sized {
impl<'a, Message, Renderer> From<Scrollable<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
- Renderer: 'static + self::Renderer + column::Renderer,
- Message: 'static,
+ Renderer: 'a + self::Renderer,
+ Message: 'a,
{
fn from(
scrollable: Scrollable<'a, Message, Renderer>,
diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs
index c98cebb6..755e6b2b 100644
--- a/native/src/widget/slider.rs
+++ b/native/src/widget/slider.rs
@@ -1,13 +1,12 @@
//! Display an interactive selector of a single value from a range of values.
//!
//! A [`Slider`] has some local [`State`].
-//!
-//! [`Slider`]: struct.Slider.html
-//! [`State`]: struct.State.html
+use crate::event::{self, Event};
+use crate::layout;
+use crate::mouse;
+use crate::touch::{self, Touch};
use crate::{
- input::{mouse, touch, ButtonState, Touch},
- layout, Clipboard, Element, Event, Hasher, Layout, Length, Point,
- Rectangle, Size, Widget,
+ Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget,
};
use std::{hash::Hash, ops::RangeInclusive};
@@ -17,13 +16,15 @@ use std::{hash::Hash, ops::RangeInclusive};
///
/// A [`Slider`] will try to fill the horizontal space of its container.
///
-/// [`Slider`]: struct.Slider.html
+/// The [`Slider`] range of numeric values is generic and its step size defaults
+/// to 1 unit.
///
/// # Example
/// ```
/// # use iced_native::{slider, renderer::Null};
/// #
-/// # pub type Slider<'a, Message> = iced_native::Slider<'a, Message, Null>;
+/// # pub type Slider<'a, T, Message> = iced_native::Slider<'a, T, Message, Null>;
+/// #[derive(Clone)]
/// pub enum Message {
/// SliderChanged(f32),
/// }
@@ -36,16 +37,24 @@ use std::{hash::Hash, ops::RangeInclusive};
///
/// ![Slider drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/slider.png?raw=true)
#[allow(missing_debug_implementations)]
-pub struct Slider<'a, Message, Renderer: self::Renderer> {
+pub struct Slider<'a, T, Message, Renderer: self::Renderer> {
state: &'a mut State,
- range: RangeInclusive<f32>,
- value: f32,
- on_change: Box<dyn Fn(f32) -> Message>,
+ range: RangeInclusive<T>,
+ step: T,
+ value: T,
+ on_change: Box<dyn Fn(T) -> Message>,
+ on_release: Option<Message>,
width: Length,
+ height: u16,
style: Renderer::Style,
}
-impl<'a, Message, Renderer: self::Renderer> Slider<'a, Message, Renderer> {
+impl<'a, T, Message, Renderer> Slider<'a, T, Message, Renderer>
+where
+ T: Copy + From<u8> + std::cmp::PartialOrd,
+ Message: Clone,
+ Renderer: self::Renderer,
+{
/// Creates a new [`Slider`].
///
/// It expects:
@@ -55,48 +64,77 @@ impl<'a, Message, Renderer: self::Renderer> Slider<'a, Message, Renderer> {
/// * a function that will be called when the [`Slider`] is dragged.
/// It receives the new value of the [`Slider`] and must produce a
/// `Message`.
- ///
- /// [`Slider`]: struct.Slider.html
- /// [`State`]: struct.State.html
pub fn new<F>(
state: &'a mut State,
- range: RangeInclusive<f32>,
- value: f32,
+ range: RangeInclusive<T>,
+ value: T,
on_change: F,
) -> Self
where
- F: 'static + Fn(f32) -> Message,
+ F: 'static + Fn(T) -> Message,
{
+ let value = if value >= *range.start() {
+ value
+ } else {
+ *range.start()
+ };
+
+ let value = if value <= *range.end() {
+ value
+ } else {
+ *range.end()
+ };
+
Slider {
state,
- value: value.max(*range.start()).min(*range.end()),
+ value,
range,
+ step: T::from(1),
on_change: Box::new(on_change),
+ on_release: None,
width: Length::Fill,
+ height: Renderer::DEFAULT_HEIGHT,
style: Renderer::Style::default(),
}
}
- /// Sets the width of the [`Slider`].
+ /// Sets the release message of the [`Slider`].
+ /// This is called when the mouse is released from the slider.
///
- /// [`Slider`]: struct.Slider.html
+ /// Typically, the user's interaction with the slider is finished when this message is produced.
+ /// This is useful if you need to spawn a long-running task from the slider's result, where
+ /// the default on_change message could create too many events.
+ pub fn on_release(mut self, on_release: Message) -> Self {
+ self.on_release = Some(on_release);
+ self
+ }
+
+ /// Sets the width of the [`Slider`].
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
+ /// Sets the height of the [`Slider`].
+ pub fn height(mut self, height: u16) -> Self {
+ self.height = height;
+ self
+ }
+
/// Sets the style of the [`Slider`].
- ///
- /// [`Slider`]: struct.Slider.html
pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {
self.style = style.into();
self
}
+
+ /// Sets the step size of the [`Slider`].
+ pub fn step(mut self, step: T) -> Self {
+ self.step = step;
+ self
+ }
}
/// The local state of a [`Slider`].
-///
-/// [`Slider`]: struct.Slider.html
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct State {
is_dragging: bool,
@@ -104,16 +142,16 @@ pub struct State {
impl State {
/// Creates a new [`State`].
- ///
- /// [`State`]: struct.State.html
pub fn new() -> State {
State::default()
}
}
-impl<'a, Message, Renderer> Widget<Message, Renderer>
- for Slider<'a, Message, Renderer>
+impl<'a, T, Message, Renderer> Widget<Message, Renderer>
+ for Slider<'a, T, Message, Renderer>
where
+ T: Copy + Into<f64> + num_traits::FromPrimitive,
+ Message: Clone,
Renderer: self::Renderer,
{
fn width(&self) -> Length {
@@ -126,12 +164,11 @@ where
fn layout(
&self,
- renderer: &Renderer,
+ _renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- let limits = limits
- .width(self.width)
- .height(Length::Units(renderer.height() as u16));
+ let limits =
+ limits.width(self.width).height(Length::Units(self.height));
let size = limits.resolve(Size::ZERO);
@@ -146,28 +183,32 @@ where
messages: &mut Vec<Message>,
_renderer: &Renderer,
_clipboard: Option<&dyn Clipboard>,
- ) {
+ ) -> event::Status {
let mut change = || {
let bounds = layout.bounds();
-
if cursor_position.x <= bounds.x {
messages.push((self.on_change)(*self.range.start()));
} else if cursor_position.x >= bounds.x + bounds.width {
messages.push((self.on_change)(*self.range.end()));
} else {
- let percent = (cursor_position.x - bounds.x) / bounds.width;
- let value = (self.range.end() - self.range.start()) * percent
- + self.range.start();
+ let step = self.step.into();
+ let start = (*self.range.start()).into();
+ let end = (*self.range.end()).into();
+
+ let percent = f64::from(cursor_position.x - bounds.x)
+ / f64::from(bounds.width);
- messages.push((self.on_change)(value));
+ let steps = (percent * (end - start) / step).round();
+ let value = steps * step + start;
+
+ if let Some(value) = T::from_f64(value) {
+ messages.push((self.on_change)(value));
+ }
}
};
match event {
- Event::Mouse(mouse::Event::Input {
- button: mouse::Button::Left,
- state: ButtonState::Pressed,
- })
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(Touch {
phase: touch::Phase::Started,
..
@@ -175,17 +216,23 @@ where
if layout.bounds().contains(cursor_position) {
change();
self.state.is_dragging = true;
+
+ return event::Status::Captured;
}
}
- Event::Mouse(mouse::Event::Input {
- button: mouse::Button::Left,
- state: ButtonState::Released,
- })
+ Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(Touch {
phase: touch::Phase::Ended,
..
}) => {
- self.state.is_dragging = false;
+ 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::CursorMoved { .. })
| Event::Touch(Touch {
@@ -194,10 +241,14 @@ where
}) => {
if self.state.is_dragging {
change();
+
+ return event::Status::Captured;
}
}
_ => {}
}
+
+ event::Status::Ignored
}
fn draw(
@@ -206,18 +257,25 @@ where
_defaults: &Renderer::Defaults,
layout: Layout<'_>,
cursor_position: Point,
+ _viewport: &Rectangle,
) -> Renderer::Output {
+ let start = *self.range.start();
+ let end = *self.range.end();
+
renderer.draw(
layout.bounds(),
cursor_position,
- self.range.clone(),
- self.value,
+ start.into() as f32..=end.into() as f32,
+ self.value.into() as f32,
self.state.is_dragging,
&self.style,
)
}
fn hash_layout(&self, state: &mut Hasher) {
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
self.width.hash(state);
}
}
@@ -227,16 +285,13 @@ where
/// Your [renderer] will need to implement this trait before being
/// able to use a [`Slider`] in your user interface.
///
-/// [`Slider`]: struct.Slider.html
-/// [renderer]: ../../renderer/index.html
+/// [renderer]: crate::renderer
pub trait Renderer: crate::Renderer {
/// The style supported by this renderer.
type Style: Default;
- /// Returns the height of the [`Slider`].
- ///
- /// [`Slider`]: struct.Slider.html
- fn height(&self) -> u32;
+ /// The default height of a [`Slider`].
+ const DEFAULT_HEIGHT: u16;
/// Draws a [`Slider`].
///
@@ -246,10 +301,6 @@ pub trait Renderer: crate::Renderer {
/// * the local state of the [`Slider`]
/// * the range of values of the [`Slider`]
/// * the current value of the [`Slider`]
- ///
- /// [`Slider`]: struct.Slider.html
- /// [`State`]: struct.State.html
- /// [`Class`]: enum.Class.html
fn draw(
&mut self,
bounds: Rectangle,
@@ -261,14 +312,15 @@ pub trait Renderer: crate::Renderer {
) -> Self::Output;
}
-impl<'a, Message, Renderer> From<Slider<'a, Message, Renderer>>
+impl<'a, T, Message, Renderer> From<Slider<'a, T, Message, Renderer>>
for Element<'a, Message, Renderer>
where
- Renderer: 'static + self::Renderer,
- Message: 'static,
+ T: 'a + Copy + Into<f64> + num_traits::FromPrimitive,
+ Message: 'a + Clone,
+ Renderer: 'a + self::Renderer,
{
fn from(
- slider: Slider<'a, Message, Renderer>,
+ slider: Slider<'a, T, Message, Renderer>,
) -> Element<'a, Message, Renderer> {
Element::new(slider)
}
diff --git a/native/src/widget/space.rs b/native/src/widget/space.rs
index 24c94bf6..6b34ece8 100644
--- a/native/src/widget/space.rs
+++ b/native/src/widget/space.rs
@@ -16,15 +16,11 @@ pub struct Space {
impl Space {
/// Creates an amount of empty [`Space`] with the given width and height.
- ///
- /// [`Space`]: struct.Space.html
pub fn new(width: Length, height: Length) -> Self {
Space { width, height }
}
/// Creates an amount of horizontal [`Space`].
- ///
- /// [`Space`]: struct.Space.html
pub fn with_width(width: Length) -> Self {
Space {
width,
@@ -33,8 +29,6 @@ impl Space {
}
/// Creates an amount of vertical [`Space`].
- ///
- /// [`Space`]: struct.Space.html
pub fn with_height(height: Length) -> Self {
Space {
width: Length::Shrink,
@@ -43,7 +37,7 @@ impl Space {
}
}
-impl<'a, Message, Renderer> Widget<Message, Renderer> for Space
+impl<Message, Renderer> Widget<Message, Renderer> for Space
where
Renderer: self::Renderer,
{
@@ -71,33 +65,31 @@ where
_defaults: &Renderer::Defaults,
layout: Layout<'_>,
_cursor_position: Point,
+ _viewport: &Rectangle,
) -> Renderer::Output {
renderer.draw(layout.bounds())
}
fn hash_layout(&self, state: &mut Hasher) {
std::any::TypeId::of::<Space>().hash(state);
+
self.width.hash(state);
self.height.hash(state);
}
}
/// The renderer of an amount of [`Space`].
-///
-/// [`Space`]: struct.Space.html
pub trait Renderer: crate::Renderer {
/// Draws an amount of empty [`Space`].
///
/// You should most likely return an empty primitive here.
- ///
- /// [`Space`]: struct.Space.html
fn draw(&mut self, bounds: Rectangle) -> Self::Output;
}
impl<'a, Message, Renderer> From<Space> for Element<'a, Message, Renderer>
where
Renderer: self::Renderer,
- Message: 'static,
+ Message: 'a,
{
fn from(space: Space) -> Element<'a, Message, Renderer> {
Element::new(space)
diff --git a/native/src/widget/svg.rs b/native/src/widget/svg.rs
index 063730bb..9cd61918 100644
--- a/native/src/widget/svg.rs
+++ b/native/src/widget/svg.rs
@@ -1,9 +1,11 @@
//! Display vector graphics in your application.
-use crate::{layout, Element, Hasher, Layout, Length, Point, Size, Widget};
+use crate::layout;
+use crate::{Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget};
use std::{
- hash::Hash,
- path::{Path, PathBuf},
+ hash::{Hash, Hasher as _},
+ path::PathBuf,
+ sync::Arc,
};
/// A vector graphics image.
@@ -12,8 +14,6 @@ use std::{
///
/// [`Svg`] images can have a considerable rendering cost when resized,
/// specially when they are complex.
-///
-/// [`Svg`]: struct.Svg.html
#[derive(Debug, Clone)]
pub struct Svg {
handle: Handle,
@@ -23,9 +23,6 @@ pub struct Svg {
impl Svg {
/// Creates a new [`Svg`] from the given [`Handle`].
- ///
- /// [`Svg`]: struct.Svg.html
- /// [`Handle`]: struct.Handle.html
pub fn new(handle: impl Into<Handle>) -> Self {
Svg {
handle: handle.into(),
@@ -34,17 +31,19 @@ impl Svg {
}
}
+ /// Creates a new [`Svg`] that will display the contents of the file at the
+ /// provided path.
+ pub fn from_path(path: impl Into<PathBuf>) -> Self {
+ Self::new(Handle::from_path(path))
+ }
+
/// Sets the width of the [`Svg`].
- ///
- /// [`Svg`]: struct.Svg.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`Svg`].
- ///
- /// [`Svg`]: struct.Svg.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
@@ -94,68 +93,88 @@ where
_defaults: &Renderer::Defaults,
layout: Layout<'_>,
_cursor_position: Point,
+ _viewport: &Rectangle,
) -> Renderer::Output {
renderer.draw(self.handle.clone(), layout)
}
fn hash_layout(&self, state: &mut Hasher) {
+ std::any::TypeId::of::<Svg>().hash(state);
+
+ self.handle.hash(state);
self.width.hash(state);
self.height.hash(state);
}
}
/// An [`Svg`] handle.
-///
-/// [`Svg`]: struct.Svg.html
#[derive(Debug, Clone)]
pub struct Handle {
id: u64,
- path: PathBuf,
+ data: Arc<Data>,
}
impl Handle {
/// Creates an SVG [`Handle`] pointing to the vector image of the given
/// path.
- ///
- /// [`Handle`]: struct.Handle.html
- pub fn from_path<T: Into<PathBuf>>(path: T) -> Handle {
- use std::hash::Hasher as _;
+ pub fn from_path(path: impl Into<PathBuf>) -> Handle {
+ Self::from_data(Data::Path(path.into()))
+ }
- let path = path.into();
+ /// Creates an SVG [`Handle`] from raw bytes containing either an SVG string
+ /// or gzip compressed data.
+ ///
+ /// This is useful if you already have your SVG data in-memory, maybe
+ /// because you downloaded or generated it procedurally.
+ pub fn from_memory(bytes: impl Into<Vec<u8>>) -> Handle {
+ Self::from_data(Data::Bytes(bytes.into()))
+ }
+ fn from_data(data: Data) -> Handle {
let mut hasher = Hasher::default();
- path.hash(&mut hasher);
+ data.hash(&mut hasher);
Handle {
id: hasher.finish(),
- path,
+ data: Arc::new(data),
}
}
/// Returns the unique identifier of the [`Handle`].
- ///
- /// [`Handle`]: struct.Handle.html
pub fn id(&self) -> u64 {
self.id
}
- /// Returns a reference to the path of the [`Handle`].
- ///
- /// [`Handle`]: enum.Handle.html
- pub fn path(&self) -> &Path {
- &self.path
+ /// Returns a reference to the SVG [`Data`].
+ pub fn data(&self) -> &Data {
+ &self.data
}
}
-impl From<String> for Handle {
- fn from(path: String) -> Handle {
- Handle::from_path(path)
+impl Hash for Handle {
+ fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+ self.id.hash(state);
}
}
-impl From<&str> for Handle {
- fn from(path: &str) -> Handle {
- Handle::from_path(path)
+/// The data of an [`Svg`].
+#[derive(Clone, Hash)]
+pub enum Data {
+ /// File data
+ Path(PathBuf),
+
+ /// In-memory data
+ ///
+ /// Can contain an SVG string or a gzip compressed data.
+ Bytes(Vec<u8>),
+}
+
+impl std::fmt::Debug for Data {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Data::Path(path) => write!(f, "Path({:?})", path),
+ Data::Bytes(_) => write!(f, "Bytes(...)"),
+ }
}
}
@@ -164,17 +183,12 @@ impl From<&str> for Handle {
/// Your [renderer] will need to implement this trait before being able to use
/// an [`Svg`] in your user interface.
///
-/// [`Svg`]: struct.Svg.html
-/// [renderer]: ../../renderer/index.html
+/// [renderer]: crate::renderer
pub trait Renderer: crate::Renderer {
- /// Returns the default dimensions of an [`Svg`] located on the given path.
- ///
- /// [`Svg`]: struct.Svg.html
+ /// Returns the default dimensions of an [`Svg`] for the given [`Handle`].
fn dimensions(&self, handle: &Handle) -> (u32, u32);
/// Draws an [`Svg`].
- ///
- /// [`Svg`]: struct.Svg.html
fn draw(&mut self, handle: Handle, layout: Layout<'_>) -> Self::Output;
}
diff --git a/native/src/widget/text.rs b/native/src/widget/text.rs
index 7d8cad6e..6cc18e6c 100644
--- a/native/src/widget/text.rs
+++ b/native/src/widget/text.rs
@@ -1,7 +1,7 @@
//! Write some text for your users to read.
use crate::{
- layout, Color, Element, Font, Hasher, HorizontalAlignment, Layout, Length,
- Point, Rectangle, Size, VerticalAlignment, Widget,
+ layout, Color, Element, Hasher, HorizontalAlignment, Layout, Length, Point,
+ Rectangle, Size, VerticalAlignment, Widget,
};
use std::hash::Hash;
@@ -11,7 +11,7 @@ use std::hash::Hash;
/// # Example
///
/// ```
-/// # use iced_native::Text;
+/// # type Text = iced_native::Text<iced_native::renderer::Null>;
/// #
/// Text::new("I <3 iced!")
/// .color([0.0, 0.0, 1.0])
@@ -19,28 +19,26 @@ use std::hash::Hash;
/// ```
///
/// ![Text drawn by `iced_wgpu`](https://github.com/hecrj/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/text.png?raw=true)
-#[derive(Debug, Clone)]
-pub struct Text {
+#[derive(Debug)]
+pub struct Text<Renderer: self::Renderer> {
content: String,
size: Option<u16>,
color: Option<Color>,
- font: Font,
+ font: Renderer::Font,
width: Length,
height: Length,
horizontal_alignment: HorizontalAlignment,
vertical_alignment: VerticalAlignment,
}
-impl Text {
+impl<Renderer: self::Renderer> Text<Renderer> {
/// Create a new fragment of [`Text`] with the given contents.
- ///
- /// [`Text`]: struct.Text.html
pub fn new<T: Into<String>>(label: T) -> Self {
Text {
content: label.into(),
size: None,
color: None,
- font: Font::Default,
+ font: Default::default(),
width: Length::Shrink,
height: Length::Shrink,
horizontal_alignment: HorizontalAlignment::Left,
@@ -49,17 +47,12 @@ impl Text {
}
/// Sets the size of the [`Text`].
- ///
- /// [`Text`]: struct.Text.html
pub fn size(mut self, size: u16) -> Self {
self.size = Some(size);
self
}
/// Sets the [`Color`] of the [`Text`].
- ///
- /// [`Text`]: struct.Text.html
- /// [`Color`]: ../../struct.Color.html
pub fn color<C: Into<Color>>(mut self, color: C) -> Self {
self.color = Some(color.into());
self
@@ -67,33 +60,25 @@ impl Text {
/// Sets the [`Font`] of the [`Text`].
///
- /// [`Text`]: struct.Text.html
- /// [`Font`]: ../../struct.Font.html
- pub fn font(mut self, font: Font) -> Self {
- self.font = font;
+ /// [`Font`]: Renderer::Font
+ pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
+ self.font = font.into();
self
}
/// Sets the width of the [`Text`] boundaries.
- ///
- /// [`Text`]: struct.Text.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`Text`] boundaries.
- ///
- /// [`Text`]: struct.Text.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
/// Sets the [`HorizontalAlignment`] of the [`Text`].
- ///
- /// [`Text`]: struct.Text.html
- /// [`HorizontalAlignment`]: enum.HorizontalAlignment.html
pub fn horizontal_alignment(
mut self,
alignment: HorizontalAlignment,
@@ -103,16 +88,13 @@ impl Text {
}
/// Sets the [`VerticalAlignment`] of the [`Text`].
- ///
- /// [`Text`]: struct.Text.html
- /// [`VerticalAlignment`]: enum.VerticalAlignment.html
pub fn vertical_alignment(mut self, alignment: VerticalAlignment) -> Self {
self.vertical_alignment = alignment;
self
}
}
-impl<Message, Renderer> Widget<Message, Renderer> for Text
+impl<Message, Renderer> Widget<Message, Renderer> for Text<Renderer>
where
Renderer: self::Renderer,
{
@@ -131,7 +113,7 @@ where
) -> layout::Node {
let limits = limits.width(self.width).height(self.height);
- let size = self.size.unwrap_or(Renderer::DEFAULT_SIZE);
+ let size = self.size.unwrap_or(renderer.default_size());
let bounds = limits.max();
@@ -149,12 +131,13 @@ where
defaults: &Renderer::Defaults,
layout: Layout<'_>,
_cursor_position: Point,
+ _viewport: &Rectangle,
) -> Renderer::Output {
renderer.draw(
defaults,
layout.bounds(),
&self.content,
- self.size.unwrap_or(Renderer::DEFAULT_SIZE),
+ self.size.unwrap_or(renderer.default_size()),
self.font,
self.color,
self.horizontal_alignment,
@@ -163,6 +146,9 @@ where
}
fn hash_layout(&self, state: &mut Hasher) {
+ struct Marker;
+ std::any::TypeId::of::<Marker>().hash(state);
+
self.content.hash(state);
self.size.hash(state);
self.width.hash(state);
@@ -173,26 +159,23 @@ where
/// The renderer of a [`Text`] fragment.
///
/// Your [renderer] will need to implement this trait before being
-/// able to use [`Text`] in your [`UserInterface`].
+/// able to use [`Text`] in your user interface.
///
-/// [`Text`]: struct.Text.html
-/// [renderer]: ../../renderer/index.html
-/// [`UserInterface`]: ../../struct.UserInterface.html
+/// [renderer]: crate::Renderer
pub trait Renderer: crate::Renderer {
- /// The default size of [`Text`].
- ///
- /// [`Text`]: struct.Text.html
- const DEFAULT_SIZE: u16;
+ /// The font type used for [`Text`].
+ type Font: Default + Copy;
+
+ /// Returns the default size of [`Text`].
+ fn default_size(&self) -> u16;
/// Measures the [`Text`] in the given bounds and returns the minimum
/// boundaries that can fit the contents.
- ///
- /// [`Text`]: struct.Text.html
fn measure(
&self,
content: &str,
size: u16,
- font: Font,
+ font: Self::Font,
bounds: Size,
) -> (f32, f32);
@@ -205,28 +188,40 @@ pub trait Renderer: crate::Renderer {
/// * the color of the [`Text`]
/// * the [`HorizontalAlignment`] of the [`Text`]
/// * the [`VerticalAlignment`] of the [`Text`]
- ///
- /// [`Text`]: struct.Text.html
- /// [`HorizontalAlignment`]: enum.HorizontalAlignment.html
- /// [`VerticalAlignment`]: enum.VerticalAlignment.html
fn draw(
&mut self,
defaults: &Self::Defaults,
bounds: Rectangle,
content: &str,
size: u16,
- font: Font,
+ font: Self::Font,
color: Option<Color>,
horizontal_alignment: HorizontalAlignment,
vertical_alignment: VerticalAlignment,
) -> Self::Output;
}
-impl<'a, Message, Renderer> From<Text> for Element<'a, Message, Renderer>
+impl<'a, Message, Renderer> From<Text<Renderer>>
+ for Element<'a, Message, Renderer>
where
- Renderer: self::Renderer,
+ Renderer: self::Renderer + 'a,
{
- fn from(text: Text) -> Element<'a, Message, Renderer> {
+ fn from(text: Text<Renderer>) -> Element<'a, Message, Renderer> {
Element::new(text)
}
}
+
+impl<Renderer: self::Renderer> Clone for Text<Renderer> {
+ fn clone(&self) -> Self {
+ Self {
+ content: self.content.clone(),
+ size: self.size,
+ color: self.color,
+ font: self.font,
+ width: self.width,
+ height: self.height,
+ horizontal_alignment: self.horizontal_alignment,
+ vertical_alignment: self.vertical_alignment,
+ }
+ }
+}
diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs
index c06a8cce..ca71c20c 100644
--- a/native/src/widget/text_input.rs
+++ b/native/src/widget/text_input.rs
@@ -1,17 +1,27 @@
//! Display fields that can be filled with text.
//!
//! A [`TextInput`] has some local [`State`].
-//!
-//! [`TextInput`]: struct.TextInput.html
-//! [`State`]: struct.State.html
+mod editor;
+mod value;
+
+pub mod cursor;
+
+pub use cursor::Cursor;
+pub use value::Value;
+
+use editor::Editor;
+
+use crate::event::{self, Event};
+use crate::keyboard;
+use crate::layout;
+use crate::mouse::{self, click};
+use crate::text;
+use crate::touch::{self, Touch};
use crate::{
- input::{keyboard, mouse, touch, ButtonState, Touch},
- layout, Clipboard, Element, Event, Font, Hasher, Layout, Length, Point,
- Rectangle, Size, Widget,
+ Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget,
};
use std::u32;
-use unicode_segmentation::UnicodeSegmentation;
/// A field that can be filled with text.
///
@@ -43,7 +53,7 @@ pub struct TextInput<'a, Message, Renderer: self::Renderer> {
placeholder: String,
value: Value,
is_secure: bool,
- font: Font,
+ font: Renderer::Font,
width: Length,
max_width: u32,
padding: u16,
@@ -53,7 +63,11 @@ pub struct TextInput<'a, Message, Renderer: self::Renderer> {
style: Renderer::Style,
}
-impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> {
+impl<'a, Message, Renderer> TextInput<'a, Message, Renderer>
+where
+ Message: Clone,
+ Renderer: self::Renderer,
+{
/// Creates a new [`TextInput`].
///
/// It expects:
@@ -61,9 +75,6 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> {
/// - a placeholder
/// - the current value
/// - a function that produces a message when the [`TextInput`] changes
- ///
- /// [`TextInput`]: struct.TextInput.html
- /// [`State`]: struct.State.html
pub fn new<F>(
state: &'a mut State,
placeholder: &str,
@@ -78,7 +89,7 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> {
placeholder: String::from(placeholder),
value: Value::new(value),
is_secure: false,
- font: Font::Default,
+ font: Default::default(),
width: Length::Fill,
max_width: u32::MAX,
padding: 0,
@@ -90,8 +101,6 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> {
}
/// Converts the [`TextInput`] into a secure password input.
- ///
- /// [`TextInput`]: struct.TextInput.html
pub fn password(mut self) -> Self {
self.is_secure = true;
self
@@ -99,39 +108,31 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> {
/// Sets the [`Font`] of the [`Text`].
///
- /// [`Text`]: struct.Text.html
- /// [`Font`]: ../../struct.Font.html
- pub fn font(mut self, font: Font) -> Self {
+ /// [`Font`]: crate::widget::text::Renderer::Font
+ /// [`Text`]: crate::widget::Text
+ pub fn font(mut self, font: Renderer::Font) -> Self {
self.font = font;
self
}
/// Sets the width of the [`TextInput`].
- ///
- /// [`TextInput`]: struct.TextInput.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the maximum width of the [`TextInput`].
- ///
- /// [`TextInput`]: struct.TextInput.html
pub fn max_width(mut self, max_width: u32) -> Self {
self.max_width = max_width;
self
}
/// Sets the padding of the [`TextInput`].
- ///
- /// [`TextInput`]: struct.TextInput.html
pub fn padding(mut self, units: u16) -> Self {
self.padding = units;
self
}
/// Sets the text size of the [`TextInput`].
- ///
- /// [`TextInput`]: struct.TextInput.html
pub fn size(mut self, size: u16) -> Self {
self.size = Some(size);
self
@@ -139,27 +140,75 @@ impl<'a, Message, Renderer: self::Renderer> TextInput<'a, Message, Renderer> {
/// Sets the message that should be produced when the [`TextInput`] is
/// focused and the enter key is pressed.
- ///
- /// [`TextInput`]: struct.TextInput.html
pub fn on_submit(mut self, message: Message) -> Self {
self.on_submit = Some(message);
self
}
/// Sets the style of the [`TextInput`].
- ///
- /// [`TextInput`]: struct.TextInput.html
pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {
self.style = style.into();
self
}
+
+ /// Returns the current [`State`] of the [`TextInput`].
+ pub fn state(&self) -> &State {
+ self.state
+ }
+}
+
+impl<'a, Message, Renderer> TextInput<'a, Message, Renderer>
+where
+ Renderer: self::Renderer,
+{
+ /// Draws the [`TextInput`] with the given [`Renderer`], overriding its
+ /// [`Value`] if provided.
+ pub fn draw(
+ &self,
+ renderer: &mut Renderer,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ value: Option<&Value>,
+ ) -> Renderer::Output {
+ let value = value.unwrap_or(&self.value);
+ let bounds = layout.bounds();
+ let text_bounds = layout.children().next().unwrap().bounds();
+
+ if self.is_secure {
+ self::Renderer::draw(
+ renderer,
+ bounds,
+ text_bounds,
+ cursor_position,
+ self.font,
+ self.size.unwrap_or(renderer.default_size()),
+ &self.placeholder,
+ &value.secure(),
+ &self.state,
+ &self.style,
+ )
+ } else {
+ self::Renderer::draw(
+ renderer,
+ bounds,
+ text_bounds,
+ cursor_position,
+ self.font,
+ self.size.unwrap_or(renderer.default_size()),
+ &self.placeholder,
+ value,
+ &self.state,
+ &self.style,
+ )
+ }
+ }
}
impl<'a, Message, Renderer> Widget<Message, Renderer>
for TextInput<'a, Message, Renderer>
where
- Renderer: 'static + self::Renderer,
- Message: Clone + std::fmt::Debug,
+ Message: Clone,
+ Renderer: self::Renderer,
{
fn width(&self) -> Length {
self.width
@@ -197,22 +246,91 @@ where
messages: &mut Vec<Message>,
renderer: &Renderer,
clipboard: Option<&dyn Clipboard>,
- ) {
+ ) -> event::Status {
match event {
- Event::Mouse(mouse::Event::Input {
- button: mouse::Button::Left,
- state: ButtonState::Pressed,
- })
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(Touch {
phase: touch::Phase::Started,
..
}) => {
let is_clicked = layout.bounds().contains(cursor_position);
+ self.state.is_focused = is_clicked;
+
if is_clicked {
let text_layout = layout.children().next().unwrap();
let target = cursor_position.x - text_layout.bounds().x;
+ let click = mouse::Click::new(
+ cursor_position,
+ self.state.last_click,
+ );
+
+ match click.kind() {
+ click::Kind::Single => {
+ if target > 0.0 {
+ let value = if self.is_secure {
+ self.value.secure()
+ } else {
+ self.value.clone()
+ };
+
+ let position = renderer.find_cursor_position(
+ text_layout.bounds(),
+ self.font,
+ self.size,
+ &value,
+ &self.state,
+ target,
+ );
+
+ self.state.cursor.move_to(position);
+ } else {
+ self.state.cursor.move_to(0);
+ }
+
+ self.state.is_dragging = true;
+ }
+ click::Kind::Double => {
+ if self.is_secure {
+ self.state.cursor.select_all(&self.value);
+ } else {
+ let position = renderer.find_cursor_position(
+ text_layout.bounds(),
+ self.font,
+ self.size,
+ &self.value,
+ &self.state,
+ target,
+ );
+
+ self.state.cursor.select_range(
+ self.value.previous_start_of_word(position),
+ self.value.next_end_of_word(position),
+ );
+ }
+
+ self.state.is_dragging = false;
+ }
+ click::Kind::Triple => {
+ self.state.cursor.select_all(&self.value);
+ self.state.is_dragging = false;
+ }
+ }
+
+ self.state.last_click = Some(click);
+
+ return event::Status::Captured;
+ }
+ }
+ Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
+ self.state.is_dragging = false;
+ }
+ Event::Mouse(mouse::Event::CursorMoved { position }) => {
+ if self.state.is_dragging {
+ let text_layout = layout.children().next().unwrap();
+ let target = position.x - text_layout.bounds().x;
+
if target > 0.0 {
let value = if self.is_secure {
self.value.secure()
@@ -220,154 +338,243 @@ where
self.value.clone()
};
- let size = self.size.unwrap_or(renderer.default_size());
-
- let offset = renderer.offset(
+ let position = renderer.find_cursor_position(
text_layout.bounds(),
- size,
+ self.font,
+ self.size,
&value,
&self.state,
- self.font,
+ target,
);
- self.state.cursor_position = find_cursor_position(
- renderer,
- target + offset,
- &value,
- size,
- 0,
- self.value.len(),
- self.font,
+ self.state.cursor.select_range(
+ self.state.cursor.start(&value),
+ position,
);
- } else {
- self.state.cursor_position = 0;
}
- }
- self.state.is_focused = is_clicked;
+ return event::Status::Captured;
+ }
}
Event::Keyboard(keyboard::Event::CharacterReceived(c))
if self.state.is_focused
&& self.state.is_pasting.is_none()
+ && !self.state.keyboard_modifiers.is_command_pressed()
&& !c.is_control() =>
{
- let cursor_position = self.state.cursor_position(&self.value);
+ let mut editor =
+ Editor::new(&mut self.value, &mut self.state.cursor);
- self.value.insert(cursor_position, c);
- self.state.move_cursor_right(&self.value);
+ editor.insert(c);
- let message = (self.on_change)(self.value.to_string());
+ let message = (self.on_change)(editor.contents());
messages.push(message);
+
+ return event::Status::Captured;
}
- Event::Keyboard(keyboard::Event::Input {
- key_code,
- state: ButtonState::Pressed,
- modifiers,
- }) if self.state.is_focused => match key_code {
- keyboard::KeyCode::Enter => {
- if let Some(on_submit) = self.on_submit.clone() {
- messages.push(on_submit);
+ Event::Keyboard(keyboard::Event::KeyPressed {
+ key_code, ..
+ }) if self.state.is_focused => {
+ let modifiers = self.state.keyboard_modifiers;
+
+ match key_code {
+ keyboard::KeyCode::Enter => {
+ if let Some(on_submit) = self.on_submit.clone() {
+ messages.push(on_submit);
+ }
}
- }
- keyboard::KeyCode::Backspace => {
- let cursor_position =
- self.state.cursor_position(&self.value);
+ keyboard::KeyCode::Backspace => {
+ if platform::is_jump_modifier_pressed(modifiers)
+ && self
+ .state
+ .cursor
+ .selection(&self.value)
+ .is_none()
+ {
+ if self.is_secure {
+ let cursor_pos =
+ self.state.cursor.end(&self.value);
+ self.state.cursor.select_range(0, cursor_pos);
+ } else {
+ self.state
+ .cursor
+ .select_left_by_words(&self.value);
+ }
+ }
- if cursor_position > 0 {
- self.state.move_cursor_left(&self.value);
+ let mut editor = Editor::new(
+ &mut self.value,
+ &mut self.state.cursor,
+ );
- let _ = self.value.remove(cursor_position - 1);
+ editor.backspace();
- let message = (self.on_change)(self.value.to_string());
+ let message = (self.on_change)(editor.contents());
messages.push(message);
}
- }
- keyboard::KeyCode::Delete => {
- let cursor_position =
- self.state.cursor_position(&self.value);
+ keyboard::KeyCode::Delete => {
+ if platform::is_jump_modifier_pressed(modifiers)
+ && self
+ .state
+ .cursor
+ .selection(&self.value)
+ .is_none()
+ {
+ if self.is_secure {
+ let cursor_pos =
+ self.state.cursor.end(&self.value);
+ self.state
+ .cursor
+ .select_range(cursor_pos, self.value.len());
+ } else {
+ self.state
+ .cursor
+ .select_right_by_words(&self.value);
+ }
+ }
+
+ let mut editor = Editor::new(
+ &mut self.value,
+ &mut self.state.cursor,
+ );
- if cursor_position < self.value.len() {
- let _ = self.value.remove(cursor_position);
+ editor.delete();
- let message = (self.on_change)(self.value.to_string());
+ let message = (self.on_change)(editor.contents());
messages.push(message);
}
- }
- keyboard::KeyCode::Left => {
- if platform::is_jump_modifier_pressed(modifiers)
- && !self.is_secure
- {
- self.state.move_cursor_left_by_words(&self.value);
- } else {
- self.state.move_cursor_left(&self.value);
+ keyboard::KeyCode::Left => {
+ if platform::is_jump_modifier_pressed(modifiers)
+ && !self.is_secure
+ {
+ if modifiers.shift {
+ self.state
+ .cursor
+ .select_left_by_words(&self.value);
+ } else {
+ self.state
+ .cursor
+ .move_left_by_words(&self.value);
+ }
+ } else if modifiers.shift {
+ self.state.cursor.select_left(&self.value)
+ } else {
+ self.state.cursor.move_left(&self.value);
+ }
}
- }
- keyboard::KeyCode::Right => {
- if platform::is_jump_modifier_pressed(modifiers)
- && !self.is_secure
- {
- self.state.move_cursor_right_by_words(&self.value);
- } else {
- self.state.move_cursor_right(&self.value);
+ keyboard::KeyCode::Right => {
+ if platform::is_jump_modifier_pressed(modifiers)
+ && !self.is_secure
+ {
+ if modifiers.shift {
+ self.state
+ .cursor
+ .select_right_by_words(&self.value);
+ } else {
+ self.state
+ .cursor
+ .move_right_by_words(&self.value);
+ }
+ } else if modifiers.shift {
+ self.state.cursor.select_right(&self.value)
+ } else {
+ self.state.cursor.move_right(&self.value);
+ }
}
- }
- keyboard::KeyCode::Home => {
- self.state.cursor_position = 0;
- }
- keyboard::KeyCode::End => {
- self.state.move_cursor_to_end(&self.value);
- }
- keyboard::KeyCode::V => {
- if platform::is_copy_paste_modifier_pressed(modifiers) {
- 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 cursor_position =
- self.state.cursor_position(&self.value);
-
- self.value
- .insert_many(cursor_position, content.clone());
-
- self.state.move_cursor_right_by_amount(
- &self.value,
- content.len(),
+ keyboard::KeyCode::Home => {
+ if modifiers.shift {
+ self.state.cursor.select_range(
+ self.state.cursor.start(&self.value),
+ 0,
);
- self.state.is_pasting = Some(content);
-
- let message =
- (self.on_change)(self.value.to_string());
- messages.push(message);
+ } else {
+ self.state.cursor.move_to(0);
+ }
+ }
+ keyboard::KeyCode::End => {
+ if modifiers.shift {
+ self.state.cursor.select_range(
+ self.state.cursor.start(&self.value),
+ self.value.len(),
+ );
+ } else {
+ 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,
+ );
+
+ 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);
}
- } else {
+ }
+ keyboard::KeyCode::Escape => {
+ self.state.is_focused = false;
+ self.state.is_dragging = false;
self.state.is_pasting = None;
+
+ self.state.keyboard_modifiers =
+ keyboard::Modifiers::default();
}
+ _ => {}
}
- _ => {}
- },
- Event::Keyboard(keyboard::Event::Input {
- key_code,
- state: ButtonState::Released,
- ..
- }) => match key_code {
- keyboard::KeyCode::V => {
- self.state.is_pasting = None;
+
+ return event::Status::Captured;
+ }
+ Event::Keyboard(keyboard::Event::KeyReleased {
+ key_code, ..
+ }) if self.state.is_focused => {
+ match key_code {
+ keyboard::KeyCode::V => {
+ self.state.is_pasting = None;
+ }
+ _ => {}
}
- _ => {}
- },
+
+ return event::Status::Captured;
+ }
+ Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers))
+ if self.state.is_focused =>
+ {
+ self.state.keyboard_modifiers = modifiers;
+ }
_ => {}
}
+
+ event::Status::Ignored
}
fn draw(
@@ -376,41 +583,15 @@ where
_defaults: &Renderer::Defaults,
layout: Layout<'_>,
cursor_position: Point,
+ _viewport: &Rectangle,
) -> Renderer::Output {
- let bounds = layout.bounds();
- let text_bounds = layout.children().next().unwrap().bounds();
-
- if self.is_secure {
- renderer.draw(
- bounds,
- text_bounds,
- cursor_position,
- self.size.unwrap_or(renderer.default_size()),
- self.font,
- &self.placeholder,
- &self.value.secure(),
- &self.state,
- &self.style,
- )
- } else {
- renderer.draw(
- bounds,
- text_bounds,
- cursor_position,
- self.size.unwrap_or(renderer.default_size()),
- self.font,
- &self.placeholder,
- &self.value,
- &self.state,
- &self.style,
- )
- }
+ self.draw(renderer, layout, cursor_position, None)
}
fn hash_layout(&self, state: &mut Hasher) {
use std::{any::TypeId, hash::Hash};
-
- TypeId::of::<TextInput<'static, (), Renderer>>().hash(state);
+ struct Marker;
+ TypeId::of::<Marker>().hash(state);
self.width.hash(state);
self.max_width.hash(state);
@@ -424,71 +605,82 @@ where
/// Your [renderer] will need to implement this trait before being
/// able to use a [`TextInput`] in your user interface.
///
-/// [`TextInput`]: struct.TextInput.html
-/// [renderer]: ../../renderer/index.html
-pub trait Renderer: crate::Renderer + Sized {
+/// [renderer]: crate::renderer
+pub trait Renderer: text::Renderer + Sized {
/// The style supported by this renderer.
type Style: Default;
- /// Returns the default size of the text of the [`TextInput`].
- ///
- /// [`TextInput`]: struct.TextInput.html
- fn default_size(&self) -> u16;
-
/// Returns the width of the value of the [`TextInput`].
- ///
- /// [`TextInput`]: struct.TextInput.html
- fn measure_value(&self, value: &str, size: u16, font: Font) -> f32;
+ fn measure_value(&self, value: &str, size: u16, font: Self::Font) -> f32;
/// Returns the current horizontal offset of the value of the
/// [`TextInput`].
///
/// This is the amount of horizontal scrolling applied when the [`Value`]
/// does not fit the [`TextInput`].
- ///
- /// [`TextInput`]: struct.TextInput.html
- /// [`Value`]: struct.Value.html
fn offset(
&self,
text_bounds: Rectangle,
+ font: Self::Font,
size: u16,
value: &Value,
state: &State,
- font: Font,
) -> f32;
/// Draws a [`TextInput`].
///
/// It receives:
- /// - its bounds of the [`TextInput`]
+ /// - the bounds of the [`TextInput`]
/// - the bounds of the text (i.e. the current value)
/// - the cursor position
/// - the placeholder to show when the value is empty
/// - the current [`Value`]
/// - the current [`State`]
- ///
- /// [`TextInput`]: struct.TextInput.html
- /// [`Value`]: struct.Value.html
- /// [`State`]: struct.State.html
fn draw(
&mut self,
bounds: Rectangle,
text_bounds: Rectangle,
cursor_position: Point,
+ font: Self::Font,
size: u16,
- font: Font,
placeholder: &str,
value: &Value,
state: &State,
style: &Self::Style,
) -> Self::Output;
+
+ /// Computes the position of the text cursor at the given X coordinate of
+ /// a [`TextInput`].
+ fn find_cursor_position(
+ &self,
+ text_bounds: Rectangle,
+ font: Self::Font,
+ size: Option<u16>,
+ value: &Value,
+ state: &State,
+ x: f32,
+ ) -> usize {
+ let size = size.unwrap_or(self.default_size());
+
+ let offset = self.offset(text_bounds, font, size, &value, &state);
+
+ find_cursor_position(
+ self,
+ &value,
+ font,
+ size,
+ x + offset,
+ 0,
+ value.len(),
+ )
+ }
}
impl<'a, Message, Renderer> From<TextInput<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
- Renderer: 'static + self::Renderer,
- Message: 'static + Clone + std::fmt::Debug,
+ Message: 'a + Clone,
+ Renderer: 'a + self::Renderer,
{
fn from(
text_input: TextInput<'a, Message, Renderer>,
@@ -498,245 +690,80 @@ where
}
/// The state of a [`TextInput`].
-///
-/// [`TextInput`]: struct.TextInput.html
#[derive(Debug, Default, Clone)]
pub struct State {
is_focused: bool,
+ is_dragging: bool,
is_pasting: Option<Value>,
- cursor_position: usize,
+ last_click: Option<mouse::Click>,
+ cursor: Cursor,
+ keyboard_modifiers: keyboard::Modifiers,
// TODO: Add stateful horizontal scrolling offset
}
impl State {
/// Creates a new [`State`], representing an unfocused [`TextInput`].
- ///
- /// [`State`]: struct.State.html
pub fn new() -> Self {
Self::default()
}
/// Creates a new [`State`], representing a focused [`TextInput`].
- ///
- /// [`State`]: struct.State.html
pub fn focused() -> Self {
- use std::usize;
-
Self {
is_focused: true,
+ is_dragging: false,
is_pasting: None,
- cursor_position: usize::MAX,
+ last_click: None,
+ cursor: Cursor::default(),
+ keyboard_modifiers: keyboard::Modifiers::default(),
}
}
/// Returns whether the [`TextInput`] is currently focused or not.
- ///
- /// [`TextInput`]: struct.TextInput.html
pub fn is_focused(&self) -> bool {
self.is_focused
}
- /// Returns the cursor position of a [`TextInput`].
- ///
- /// [`TextInput`]: struct.TextInput.html
- pub fn cursor_position(&self, value: &Value) -> usize {
- self.cursor_position.min(value.len())
- }
-
- /// Moves the cursor of a [`TextInput`] to the left.
- ///
- /// [`TextInput`]: struct.TextInput.html
- pub(crate) fn move_cursor_left(&mut self, value: &Value) {
- let current = self.cursor_position(value);
-
- if current > 0 {
- self.cursor_position = current - 1;
- }
- }
-
- /// Moves the cursor of a [`TextInput`] to the right.
- ///
- /// [`TextInput`]: struct.TextInput.html
- pub(crate) fn move_cursor_right(&mut self, value: &Value) {
- self.move_cursor_right_by_amount(value, 1)
- }
-
- pub(crate) fn move_cursor_right_by_amount(
- &mut self,
- value: &Value,
- amount: usize,
- ) {
- let current = self.cursor_position(value);
- let new_position = current.saturating_add(amount);
-
- if new_position < value.len() + 1 {
- self.cursor_position = new_position;
- }
- }
-
- /// Moves the cursor of a [`TextInput`] to the previous start of a word.
- ///
- /// [`TextInput`]: struct.TextInput.html
- pub(crate) fn move_cursor_left_by_words(&mut self, value: &Value) {
- let current = self.cursor_position(value);
-
- self.cursor_position = value.previous_start_of_word(current);
+ /// Returns the [`Cursor`] of the [`TextInput`].
+ pub fn cursor(&self) -> Cursor {
+ self.cursor
}
- /// Moves the cursor of a [`TextInput`] to the next end of a word.
- ///
- /// [`TextInput`]: struct.TextInput.html
- pub(crate) fn move_cursor_right_by_words(&mut self, value: &Value) {
- let current = self.cursor_position(value);
-
- self.cursor_position = value.next_end_of_word(current);
+ /// Focuses the [`TextInput`].
+ pub fn focus(&mut self) {
+ self.is_focused = true;
}
- /// Moves the cursor of a [`TextInput`] to the end.
- ///
- /// [`TextInput`]: struct.TextInput.html
- pub(crate) fn move_cursor_to_end(&mut self, value: &Value) {
- self.cursor_position = value.len();
+ /// Unfocuses the [`TextInput`].
+ pub fn unfocus(&mut self) {
+ self.is_focused = false;
}
-}
-/// The value of a [`TextInput`].
-///
-/// [`TextInput`]: struct.TextInput.html
-// TODO: Reduce allocations, cache results (?)
-#[derive(Debug, Clone)]
-pub struct Value {
- graphemes: Vec<String>,
-}
-
-impl Value {
- /// Creates a new [`Value`] from a string slice.
- ///
- /// [`Value`]: struct.Value.html
- pub fn new(string: &str) -> Self {
- let graphemes = UnicodeSegmentation::graphemes(string, true)
- .map(String::from)
- .collect();
-
- Self { graphemes }
- }
-
- /// Returns the total amount of graphemes in the [`Value`].
- ///
- /// [`Value`]: struct.Value.html
- pub fn len(&self) -> usize {
- self.graphemes.len()
- }
-
- /// Returns the position of the previous start of a word from the given
- /// grapheme `index`.
- ///
- /// [`Value`]: struct.Value.html
- pub fn previous_start_of_word(&self, index: usize) -> usize {
- let previous_string =
- &self.graphemes[..index.min(self.graphemes.len())].concat();
-
- UnicodeSegmentation::split_word_bound_indices(&previous_string as &str)
- .filter(|(_, word)| !word.trim_start().is_empty())
- .next_back()
- .map(|(i, previous_word)| {
- index
- - UnicodeSegmentation::graphemes(previous_word, true)
- .count()
- - UnicodeSegmentation::graphemes(
- &previous_string[i + previous_word.len()..] as &str,
- true,
- )
- .count()
- })
- .unwrap_or(0)
+ /// Moves the [`Cursor`] of the [`TextInput`] to the front of the input text.
+ pub fn move_cursor_to_front(&mut self) {
+ self.cursor.move_to(0);
}
- /// Returns the position of the next end of a word from the given grapheme
- /// `index`.
- ///
- /// [`Value`]: struct.Value.html
- pub fn next_end_of_word(&self, index: usize) -> usize {
- let next_string = &self.graphemes[index..].concat();
-
- UnicodeSegmentation::split_word_bound_indices(&next_string as &str)
- .filter(|(_, word)| !word.trim_start().is_empty())
- .next()
- .map(|(i, next_word)| {
- index
- + UnicodeSegmentation::graphemes(next_word, true).count()
- + UnicodeSegmentation::graphemes(
- &next_string[..i] as &str,
- true,
- )
- .count()
- })
- .unwrap_or(self.len())
+ /// Moves the [`Cursor`] of the [`TextInput`] to the end of the input text.
+ pub fn move_cursor_to_end(&mut self) {
+ self.cursor.move_to(usize::MAX);
}
- /// Returns a new [`Value`] containing the graphemes until the given
- /// `index`.
- ///
- /// [`Value`]: struct.Value.html
- pub fn until(&self, index: usize) -> Self {
- let graphemes = self.graphemes[..index.min(self.len())].to_vec();
-
- Self { graphemes }
- }
-
- /// Converts the [`Value`] into a `String`.
- ///
- /// [`Value`]: struct.Value.html
- pub fn to_string(&self) -> String {
- self.graphemes.concat()
- }
-
- /// Inserts a new `char` at the given grapheme `index`.
- pub fn insert(&mut self, index: usize, c: char) {
- self.graphemes.insert(index, c.to_string());
-
- self.graphemes =
- UnicodeSegmentation::graphemes(&self.to_string() as &str, true)
- .map(String::from)
- .collect();
- }
-
- /// Inserts a bunch of graphemes at the given grapheme `index`.
- pub fn insert_many(&mut self, index: usize, mut value: Value) {
- let _ = self
- .graphemes
- .splice(index..index, value.graphemes.drain(..));
- }
-
- /// Removes the grapheme at the given `index`.
- ///
- /// [`Value`]: struct.Value.html
- pub fn remove(&mut self, index: usize) {
- let _ = self.graphemes.remove(index);
- }
-
- /// Returns a new [`Value`] with all its graphemes replaced with the
- /// dot ('•') character.
- ///
- /// [`Value`]: struct.Value.html
- pub fn secure(&self) -> Self {
- Self {
- graphemes: std::iter::repeat(String::from("•"))
- .take(self.graphemes.len())
- .collect(),
- }
+ /// Moves the [`Cursor`] of the [`TextInput`] to an arbitrary location.
+ pub fn move_cursor_to(&mut self, position: usize) {
+ self.cursor.move_to(position);
}
}
// TODO: Reduce allocations
fn find_cursor_position<Renderer: self::Renderer>(
renderer: &Renderer,
- target: f32,
value: &Value,
+ font: Renderer::Font,
size: u16,
+ target: f32,
start: usize,
end: usize,
- font: Font,
) -> usize {
if start >= end {
if start == 0 {
@@ -764,46 +791,34 @@ fn find_cursor_position<Renderer: self::Renderer>(
if width > target {
find_cursor_position(
renderer,
- target,
value,
+ font,
size,
+ target,
start,
start + index,
- font,
)
} else {
find_cursor_position(
renderer,
- target,
value,
+ font,
size,
+ target,
start + index + 1,
end,
- font,
)
}
}
mod platform {
- use crate::input::keyboard;
+ use crate::keyboard;
- pub fn is_jump_modifier_pressed(
- modifiers: keyboard::ModifiersState,
- ) -> bool {
+ pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool {
if cfg!(target_os = "macos") {
modifiers.alt
} else {
modifiers.control
}
}
-
- pub fn is_copy_paste_modifier_pressed(
- modifiers: keyboard::ModifiersState,
- ) -> bool {
- if cfg!(target_os = "macos") {
- modifiers.logo
- } else {
- modifiers.control
- }
- }
}
diff --git a/native/src/widget/text_input/cursor.rs b/native/src/widget/text_input/cursor.rs
new file mode 100644
index 00000000..e630e293
--- /dev/null
+++ b/native/src/widget/text_input/cursor.rs
@@ -0,0 +1,186 @@
+//! Track the cursor of a text input.
+use crate::widget::text_input::Value;
+
+/// The cursor of a text input.
+#[derive(Debug, Copy, Clone)]
+pub struct Cursor {
+ state: State,
+}
+
+/// The state of a [`Cursor`].
+#[derive(Debug, Copy, Clone)]
+pub enum State {
+ /// Cursor without a selection
+ Index(usize),
+
+ /// Cursor selecting a range of text
+ Selection {
+ /// The start of the selection
+ start: usize,
+ /// The end of the selection
+ end: usize,
+ },
+}
+
+impl Default for Cursor {
+ fn default() -> Self {
+ Cursor {
+ state: State::Index(0),
+ }
+ }
+}
+
+impl Cursor {
+ /// Returns the [`State`] of the [`Cursor`].
+ pub fn state(&self, value: &Value) -> State {
+ match self.state {
+ State::Index(index) => State::Index(index.min(value.len())),
+ State::Selection { start, end } => {
+ let start = start.min(value.len());
+ let end = end.min(value.len());
+
+ if start == end {
+ State::Index(start)
+ } else {
+ State::Selection { start, end }
+ }
+ }
+ }
+ }
+
+ pub(crate) fn move_to(&mut self, position: usize) {
+ self.state = State::Index(position);
+ }
+
+ pub(crate) fn move_right(&mut self, value: &Value) {
+ self.move_right_by_amount(value, 1)
+ }
+
+ pub(crate) fn move_right_by_words(&mut self, value: &Value) {
+ self.move_to(value.next_end_of_word(self.right(value)))
+ }
+
+ pub(crate) fn move_right_by_amount(
+ &mut self,
+ value: &Value,
+ amount: usize,
+ ) {
+ match self.state(value) {
+ State::Index(index) => {
+ self.move_to(index.saturating_add(amount).min(value.len()))
+ }
+ State::Selection { start, end } => self.move_to(end.max(start)),
+ }
+ }
+
+ pub(crate) fn move_left(&mut self, value: &Value) {
+ match self.state(value) {
+ State::Index(index) if index > 0 => self.move_to(index - 1),
+ State::Selection { start, end } => self.move_to(start.min(end)),
+ _ => self.move_to(0),
+ }
+ }
+
+ pub(crate) fn move_left_by_words(&mut self, value: &Value) {
+ self.move_to(value.previous_start_of_word(self.left(value)));
+ }
+
+ pub(crate) fn select_range(&mut self, start: usize, end: usize) {
+ if start == end {
+ self.state = State::Index(start);
+ } else {
+ self.state = State::Selection { start, end };
+ }
+ }
+
+ pub(crate) fn select_left(&mut self, value: &Value) {
+ match self.state(value) {
+ State::Index(index) if index > 0 => {
+ self.select_range(index, index - 1)
+ }
+ State::Selection { start, end } if end > 0 => {
+ self.select_range(start, end - 1)
+ }
+ _ => (),
+ }
+ }
+
+ pub(crate) fn select_right(&mut self, value: &Value) {
+ match self.state(value) {
+ State::Index(index) if index < value.len() => {
+ self.select_range(index, index + 1)
+ }
+ State::Selection { start, end } if end < value.len() => {
+ self.select_range(start, end + 1)
+ }
+ _ => (),
+ }
+ }
+
+ pub(crate) fn select_left_by_words(&mut self, value: &Value) {
+ match self.state(value) {
+ State::Index(index) => {
+ self.select_range(index, value.previous_start_of_word(index))
+ }
+ State::Selection { start, end } => {
+ self.select_range(start, value.previous_start_of_word(end))
+ }
+ }
+ }
+
+ pub(crate) fn select_right_by_words(&mut self, value: &Value) {
+ match self.state(value) {
+ State::Index(index) => {
+ self.select_range(index, value.next_end_of_word(index))
+ }
+ State::Selection { start, end } => {
+ self.select_range(start, value.next_end_of_word(end))
+ }
+ }
+ }
+
+ pub(crate) fn select_all(&mut self, value: &Value) {
+ self.select_range(0, value.len());
+ }
+
+ pub(crate) fn start(&self, value: &Value) -> usize {
+ let start = match self.state {
+ State::Index(index) => index,
+ State::Selection { start, .. } => start,
+ };
+
+ start.min(value.len())
+ }
+
+ pub(crate) fn end(&self, value: &Value) -> usize {
+ let end = match self.state {
+ State::Index(index) => index,
+ State::Selection { end, .. } => end,
+ };
+
+ 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,
+ State::Selection { start, end } => start.min(end),
+ }
+ }
+
+ fn right(&self, value: &Value) -> usize {
+ match self.state(value) {
+ State::Index(index) => index,
+ State::Selection { start, end } => start.max(end),
+ }
+ }
+}
diff --git a/native/src/widget/text_input/editor.rs b/native/src/widget/text_input/editor.rs
new file mode 100644
index 00000000..20e42567
--- /dev/null
+++ b/native/src/widget/text_input/editor.rs
@@ -0,0 +1,77 @@
+use crate::text_input::{Cursor, Value};
+
+pub struct Editor<'a> {
+ value: &'a mut Value,
+ cursor: &'a mut Cursor,
+}
+
+impl<'a> Editor<'a> {
+ pub fn new(value: &'a mut Value, cursor: &'a mut Cursor) -> Editor<'a> {
+ Editor { value, cursor }
+ }
+
+ pub fn contents(&self) -> String {
+ self.value.to_string()
+ }
+
+ pub fn insert(&mut self, character: char) {
+ match self.cursor.selection(self.value) {
+ Some((left, right)) => {
+ self.cursor.move_left(self.value);
+ self.value.remove_many(left, right);
+ }
+ _ => (),
+ }
+
+ self.value.insert(self.cursor.end(self.value), character);
+ self.cursor.move_right(self.value);
+ }
+
+ pub fn paste(&mut self, content: Value) {
+ let length = content.len();
+
+ match self.cursor.selection(self.value) {
+ Some((left, right)) => {
+ self.cursor.move_left(self.value);
+ self.value.remove_many(left, right);
+ }
+ _ => (),
+ }
+
+ self.value.insert_many(self.cursor.end(self.value), content);
+
+ self.cursor.move_right_by_amount(self.value, length);
+ }
+
+ pub fn backspace(&mut self) {
+ match self.cursor.selection(self.value) {
+ Some((start, end)) => {
+ self.cursor.move_left(self.value);
+ self.value.remove_many(start, end);
+ }
+ None => {
+ let start = self.cursor.start(self.value);
+
+ if start > 0 {
+ self.cursor.move_left(self.value);
+ self.value.remove(start - 1);
+ }
+ }
+ }
+ }
+
+ pub fn delete(&mut self) {
+ match self.cursor.selection(self.value) {
+ Some(_) => {
+ self.backspace();
+ }
+ None => {
+ let end = self.cursor.end(self.value);
+
+ if end < self.value.len() {
+ self.value.remove(end);
+ }
+ }
+ }
+ }
+}
diff --git a/native/src/widget/text_input/value.rs b/native/src/widget/text_input/value.rs
new file mode 100644
index 00000000..86be2790
--- /dev/null
+++ b/native/src/widget/text_input/value.rs
@@ -0,0 +1,125 @@
+use unicode_segmentation::UnicodeSegmentation;
+
+/// The value of a [`TextInput`].
+///
+/// [`TextInput`]: crate::widget::TextInput
+// TODO: Reduce allocations, cache results (?)
+#[derive(Debug, Clone)]
+pub struct Value {
+ graphemes: Vec<String>,
+}
+
+impl Value {
+ /// Creates a new [`Value`] from a string slice.
+ pub fn new(string: &str) -> Self {
+ let graphemes = UnicodeSegmentation::graphemes(string, true)
+ .map(String::from)
+ .collect();
+
+ Self { graphemes }
+ }
+
+ /// Returns whether the [`Value`] is empty or not.
+ ///
+ /// A [`Value`] is empty when it contains no graphemes.
+ pub fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+
+ /// Returns the total amount of graphemes in the [`Value`].
+ pub fn len(&self) -> usize {
+ self.graphemes.len()
+ }
+
+ /// Returns the position of the previous start of a word from the given
+ /// grapheme `index`.
+ pub fn previous_start_of_word(&self, index: usize) -> usize {
+ let previous_string =
+ &self.graphemes[..index.min(self.graphemes.len())].concat();
+
+ UnicodeSegmentation::split_word_bound_indices(&previous_string as &str)
+ .filter(|(_, word)| !word.trim_start().is_empty())
+ .next_back()
+ .map(|(i, previous_word)| {
+ index
+ - UnicodeSegmentation::graphemes(previous_word, true)
+ .count()
+ - UnicodeSegmentation::graphemes(
+ &previous_string[i + previous_word.len()..] as &str,
+ true,
+ )
+ .count()
+ })
+ .unwrap_or(0)
+ }
+
+ /// Returns the position of the next end of a word from the given grapheme
+ /// `index`.
+ pub fn next_end_of_word(&self, index: usize) -> usize {
+ let next_string = &self.graphemes[index..].concat();
+
+ UnicodeSegmentation::split_word_bound_indices(&next_string as &str)
+ .filter(|(_, word)| !word.trim_start().is_empty())
+ .next()
+ .map(|(i, next_word)| {
+ index
+ + UnicodeSegmentation::graphemes(next_word, true).count()
+ + UnicodeSegmentation::graphemes(
+ &next_string[..i] as &str,
+ true,
+ )
+ .count()
+ })
+ .unwrap_or(self.len())
+ }
+
+ /// Returns a new [`Value`] containing the graphemes until the given
+ /// `index`.
+ pub fn until(&self, index: usize) -> Self {
+ let graphemes = self.graphemes[..index.min(self.len())].to_vec();
+
+ Self { graphemes }
+ }
+
+ /// Converts the [`Value`] into a `String`.
+ pub fn to_string(&self) -> String {
+ self.graphemes.concat()
+ }
+
+ /// Inserts a new `char` at the given grapheme `index`.
+ pub fn insert(&mut self, index: usize, c: char) {
+ self.graphemes.insert(index, c.to_string());
+
+ self.graphemes =
+ UnicodeSegmentation::graphemes(&self.to_string() as &str, true)
+ .map(String::from)
+ .collect();
+ }
+
+ /// Inserts a bunch of graphemes at the given grapheme `index`.
+ pub fn insert_many(&mut self, index: usize, mut value: Value) {
+ let _ = self
+ .graphemes
+ .splice(index..index, value.graphemes.drain(..));
+ }
+
+ /// Removes the grapheme at the given `index`.
+ pub fn remove(&mut self, index: usize) {
+ let _ = self.graphemes.remove(index);
+ }
+
+ /// Removes the graphemes from `start` to `end`.
+ pub fn remove_many(&mut self, start: usize, end: usize) {
+ let _ = self.graphemes.splice(start..end, std::iter::empty());
+ }
+
+ /// Returns a new [`Value`] with all its graphemes replaced with the
+ /// dot ('•') character.
+ pub fn secure(&self) -> Self {
+ Self {
+ graphemes: std::iter::repeat(String::from("•"))
+ .take(self.graphemes.len())
+ .collect(),
+ }
+ }
+}
diff --git a/native/src/window.rs b/native/src/window.rs
index 4dcae62f..220bb3be 100644
--- a/native/src/window.rs
+++ b/native/src/window.rs
@@ -1,6 +1,4 @@
//! Build window-based GUI applications.
-mod backend;
mod event;
-pub use backend::Backend;
pub use event::Event;
diff --git a/rustfmt.toml b/rustfmt.toml
index 7e5dded7..d979d317 100644
--- a/rustfmt.toml
+++ b/rustfmt.toml
@@ -1,3 +1 @@
max_width=80
-wrap_comments=true
-merge_imports=true
diff --git a/src/application.rs b/src/application.rs
index 2ee3337f..3b690a7c 100644
--- a/src/application.rs
+++ b/src/application.rs
@@ -1,4 +1,5 @@
-use crate::{window, Command, Element, Executor, Settings, Subscription};
+use crate::window;
+use crate::{Color, Command, Element, Executor, Settings, Subscription};
/// An interactive cross-platform application.
///
@@ -9,109 +10,114 @@ use crate::{window, Command, Element, Executor, Settings, Subscription};
/// - On the web, it will take control of the `<title>` and the `<body>` of the
/// document.
///
-/// An [`Application`](trait.Application.html) can execute asynchronous actions
-/// by returning a [`Command`](struct.Command.html) in some of its methods. If
-/// you do not intend to perform any background work in your program, the
-/// [`Sandbox`](trait.Sandbox.html) trait offers a simplified interface.
+/// An [`Application`] can execute asynchronous actions by returning a
+/// [`Command`] in some of its methods. If you do not intend to perform any
+/// background work in your program, the [`Sandbox`] trait offers a simplified
+/// interface.
///
-/// # Example
-/// Let's say we want to run the [`Counter` example we implemented
-/// before](index.html#overview). We just need to fill in the gaps:
+/// When using an [`Application`] with the `debug` feature enabled, a debug view
+/// can be toggled by pressing `F12`.
///
-/// ```no_run
-/// use iced::{button, executor, Application, Button, Column, Command, Element, Settings, Text};
+/// # Examples
+/// [The repository has a bunch of examples] that use the [`Application`] trait:
///
-/// pub fn main() {
-/// Counter::run(Settings::default())
-/// }
+/// - [`clock`], an application that uses the [`Canvas`] widget to draw a clock
+/// and its hands to display the current time.
+/// - [`download_progress`], a basic application that asynchronously downloads
+/// a dummy file of 100 MB and tracks the download progress.
+/// - [`events`], a log of native events displayed using a conditional
+/// [`Subscription`].
+/// - [`game_of_life`], an interactive version of the [Game of Life], invented
+/// by [John Horton Conway].
+/// - [`pokedex`], an application that displays a random Pokédex entry (sprite
+/// included!) by using the [PokéAPI].
+/// - [`solar_system`], an animated solar system drawn using the [`Canvas`] widget
+/// and showcasing how to compose different transforms.
+/// - [`stopwatch`], a watch with start/stop and reset buttons showcasing how
+/// to listen to time.
+/// - [`todos`], a todos tracker inspired by [TodoMVC].
///
-/// #[derive(Default)]
-/// struct Counter {
-/// value: i32,
-/// increment_button: button::State,
-/// decrement_button: button::State,
-/// }
+/// [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
+/// [`Sandbox`]: crate::Sandbox
+/// [`Canvas`]: crate::widget::Canvas
+/// [PokéAPI]: https://pokeapi.co/
+/// [TodoMVC]: http://todomvc.com/
+///
+/// ## A simple "Hello, world!"
+///
+/// If you just want to get started, here is a simple [`Application`] that
+/// says "Hello, world!":
+///
+/// ```no_run
+/// use iced::{executor, Application, Command, Element, Settings, Text};
///
-/// #[derive(Debug, Clone, Copy)]
-/// enum Message {
-/// IncrementPressed,
-/// DecrementPressed,
+/// pub fn main() -> iced::Result {
+/// Hello::run(Settings::default())
/// }
///
-/// impl Application for Counter {
-/// type Executor = executor::Null;
-/// type Message = Message;
+/// struct Hello;
///
-/// fn new() -> (Self, Command<Message>) {
-/// (Self::default(), Command::none())
+/// impl Application for Hello {
+/// type Executor = executor::Default;
+/// type Message = ();
+/// type Flags = ();
+///
+/// fn new(_flags: ()) -> (Hello, Command<Self::Message>) {
+/// (Hello, Command::none())
/// }
///
/// fn title(&self) -> String {
-/// String::from("A simple counter")
+/// String::from("A cool application")
/// }
///
-/// fn update(&mut self, message: Message) -> Command<Message> {
-/// match message {
-/// Message::IncrementPressed => {
-/// self.value += 1;
-/// }
-/// Message::DecrementPressed => {
-/// self.value -= 1;
-/// }
-/// }
-///
+/// fn update(&mut self, _message: Self::Message) -> Command<Self::Message> {
/// Command::none()
/// }
///
-/// fn view(&mut self) -> Element<Message> {
-/// Column::new()
-/// .push(
-/// Button::new(&mut self.increment_button, Text::new("Increment"))
-/// .on_press(Message::IncrementPressed),
-/// )
-/// .push(
-/// Text::new(self.value.to_string()).size(50),
-/// )
-/// .push(
-/// Button::new(&mut self.decrement_button, Text::new("Decrement"))
-/// .on_press(Message::DecrementPressed),
-/// )
-/// .into()
+/// fn view(&mut self) -> Element<Self::Message> {
+/// Text::new("Hello, world!").into()
/// }
/// }
/// ```
pub trait Application: Sized {
/// The [`Executor`] that will run commands and subscriptions.
///
- /// The [`executor::Default`] can be a good starting point!
+ /// The [default executor] can be a good starting point!
///
- /// [`Executor`]: trait.Executor.html
- /// [`executor::Default`]: executor/struct.Default.html
+ /// [`Executor`]: Self::Executor
+ /// [default executor]: crate::executor::Default
type Executor: Executor;
/// The type of __messages__ your [`Application`] will produce.
- ///
- /// [`Application`]: trait.Application.html
type Message: std::fmt::Debug + Send;
- /// Initializes the [`Application`].
+ /// The data needed to initialize your [`Application`].
+ type Flags;
+
+ /// Initializes the [`Application`] with the flags provided to
+ /// [`run`] as part of the [`Settings`].
///
/// Here is where you should return the initial state of your app.
///
- /// Additionally, you can return a [`Command`](struct.Command.html) if you
- /// need to perform some async action in the background on startup. This is
- /// useful if you want to load state from a file, perform an initial HTTP
- /// request, etc.
+ /// Additionally, you can return a [`Command`] if you need to perform some
+ /// async action in the background on startup. This is useful if you want to
+ /// load state from a file, perform an initial HTTP request, etc.
///
- /// [`Application`]: trait.Application.html
- fn new() -> (Self, Command<Self::Message>);
+ /// [`run`]: Self::run
+ fn new(flags: Self::Flags) -> (Self, Command<Self::Message>);
/// Returns the current title of the [`Application`].
///
/// This title can be dynamic! The runtime will automatically update the
/// title of your application when necessary.
- ///
- /// [`Application`]: trait.Application.html
fn title(&self) -> String;
/// Handles a __message__ and updates the state of the [`Application`].
@@ -121,9 +127,6 @@ pub trait Application: Sized {
/// this method.
///
/// Any [`Command`] returned will be executed immediately in the background.
- ///
- /// [`Application`]: trait.Application.html
- /// [`Command`]: struct.Command.html
fn update(&mut self, message: Self::Message) -> Command<Self::Message>;
/// Returns the event [`Subscription`] for the current state of the
@@ -134,8 +137,6 @@ pub trait Application: Sized {
/// [`update`](#tymethod.update).
///
/// By default, this method returns an empty [`Subscription`].
- ///
- /// [`Subscription`]: struct.Subscription.html
fn subscription(&self) -> Subscription<Self::Message> {
Subscription::none()
}
@@ -143,8 +144,6 @@ pub trait Application: Sized {
/// Returns the widgets to display in the [`Application`].
///
/// These widgets can produce __messages__ based on user interaction.
- ///
- /// [`Application`]: trait.Application.html
fn view(&mut self) -> Element<'_, Self::Message>;
/// Returns the current [`Application`] mode.
@@ -155,56 +154,99 @@ pub trait Application: Sized {
/// Currently, the mode only has an effect in native platforms.
///
/// By default, an application will run in windowed mode.
- ///
- /// [`Application`]: trait.Application.html
fn mode(&self) -> window::Mode {
window::Mode::Windowed
}
+ /// Returns the background color of the [`Application`].
+ ///
+ /// By default, it returns [`Color::WHITE`].
+ fn background_color(&self) -> Color {
+ Color::WHITE
+ }
+
+ /// Returns the scale factor of the [`Application`].
+ ///
+ /// It can be used to dynamically control the size of the UI at runtime
+ /// (i.e. zooming).
+ ///
+ /// For instance, a scale factor of `2.0` will make widgets twice as big,
+ /// while a scale factor of `0.5` will shrink them to half their size.
+ ///
+ /// By default, it returns `1.0`.
+ fn scale_factor(&self) -> f64 {
+ 1.0
+ }
+
/// Runs the [`Application`].
///
- /// This method will take control of the current thread and __will NOT
- /// return__.
+ /// On native platforms, this method will take control of the current thread
+ /// and __will NOT return__ unless there is an [`Error`] during startup.
///
/// It should probably be that last thing you call in your `main` function.
///
- /// [`Application`]: trait.Application.html
- fn run(_settings: Settings)
+ /// [`Error`]: crate::Error
+ fn run(settings: Settings<Self::Flags>) -> crate::Result
where
Self: 'static,
{
#[cfg(not(target_arch = "wasm32"))]
- <Instance<Self> as iced_winit::Application>::run(
- _settings.into(),
- iced_wgpu::Settings {
- default_font: _settings.default_font,
- antialiasing: if _settings.antialiasing {
- Some(iced_wgpu::settings::Antialiasing::MSAAx4)
+ {
+ let renderer_settings = crate::renderer::Settings {
+ default_font: settings.default_font,
+ default_text_size: settings.default_text_size,
+ antialiasing: if settings.antialiasing {
+ Some(crate::renderer::settings::Antialiasing::MSAAx4)
} else {
None
},
- ..iced_wgpu::Settings::default()
- },
- );
+ ..crate::renderer::Settings::default()
+ };
+
+ Ok(crate::runtime::application::run::<
+ Instance<Self>,
+ Self::Executor,
+ crate::renderer::window::Compositor,
+ >(settings.into(), renderer_settings)?)
+ }
#[cfg(target_arch = "wasm32")]
- <Instance<Self> as iced_web::Application>::run();
+ {
+ <Instance<Self> as iced_web::Application>::run(settings.flags);
+
+ Ok(())
+ }
}
}
struct Instance<A: Application>(A);
#[cfg(not(target_arch = "wasm32"))]
-impl<A> iced_winit::Application for Instance<A>
+impl<A> iced_winit::Program for Instance<A>
where
A: Application,
{
- type Backend = iced_wgpu::window::Backend;
- type Executor = A::Executor;
+ type Renderer = crate::renderer::Renderer;
type Message = A::Message;
- fn new() -> (Self, Command<A::Message>) {
- let (app, command) = A::new();
+ fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
+ self.0.update(message)
+ }
+
+ fn view(&mut self) -> Element<'_, Self::Message> {
+ self.0.view()
+ }
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+impl<A> crate::runtime::Application for Instance<A>
+where
+ A: Application,
+{
+ type Flags = A::Flags;
+
+ fn new(flags: Self::Flags) -> (Self, Command<A::Message>) {
+ let (app, command) = A::new(flags);
(Instance(app), command)
}
@@ -220,16 +262,16 @@ where
}
}
- fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
- self.0.update(message)
- }
-
fn subscription(&self) -> Subscription<Self::Message> {
self.0.subscription()
}
- fn view(&mut self) -> Element<'_, Self::Message> {
- self.0.view()
+ fn background_color(&self) -> Color {
+ self.0.background_color()
+ }
+
+ fn scale_factor(&self) -> f64 {
+ self.0.scale_factor()
}
}
@@ -238,11 +280,12 @@ impl<A> iced_web::Application for Instance<A>
where
A: Application,
{
- type Message = A::Message;
type Executor = A::Executor;
+ type Message = A::Message;
+ type Flags = A::Flags;
- fn new() -> (Self, Command<A::Message>) {
- let (app, command) = A::new();
+ fn new(flags: Self::Flags) -> (Self, Command<A::Message>) {
+ let (app, command) = A::new(flags);
(Instance(app), command)
}
diff --git a/src/element.rs b/src/element.rs
index e5356fb6..6f47c701 100644
--- a/src/element.rs
+++ b/src/element.rs
@@ -3,7 +3,7 @@
/// This is an alias of an `iced_native` element with a default `Renderer`.
#[cfg(not(target_arch = "wasm32"))]
pub type Element<'a, Message> =
- iced_winit::Element<'a, Message, iced_wgpu::Renderer>;
+ crate::runtime::Element<'a, Message, crate::renderer::Renderer>;
#[cfg(target_arch = "wasm32")]
pub use iced_web::Element;
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 00000000..31b87d17
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,34 @@
+use iced_futures::futures;
+
+/// An error that occurred while running an application.
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+ /// The futures executor could not be created.
+ #[error("the futures executor could not be created")]
+ ExecutorCreationFailed(futures::io::Error),
+
+ /// The application window could not be created.
+ #[error("the application window could not be created")]
+ WindowCreationFailed(Box<dyn std::error::Error>),
+
+ /// A suitable graphics adapter or device could not be found.
+ #[error("a suitable graphics adapter or device could not be found")]
+ GraphicsAdapterNotFound,
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+impl From<iced_winit::Error> for Error {
+ fn from(error: iced_winit::Error) -> Error {
+ match error {
+ iced_winit::Error::ExecutorCreationFailed(error) => {
+ Error::ExecutorCreationFailed(error)
+ }
+ iced_winit::Error::WindowCreationFailed(error) => {
+ Error::WindowCreationFailed(Box::new(error))
+ }
+ iced_winit::Error::GraphicsAdapterNotFound => {
+ Error::GraphicsAdapterNotFound
+ }
+ }
+ }
+}
diff --git a/src/executor.rs b/src/executor.rs
index b4be5264..0333bc1d 100644
--- a/src/executor.rs
+++ b/src/executor.rs
@@ -1,5 +1,5 @@
//! Choose your preferred executor to power your application.
-pub use crate::common::{executor::Null, Executor};
+pub use crate::runtime::Executor;
pub use platform::Default;
@@ -7,13 +7,23 @@ pub use platform::Default;
mod platform {
use iced_futures::{executor, futures};
- #[cfg(feature = "tokio")]
+ #[cfg(feature = "tokio_old")]
+ type Executor = executor::TokioOld;
+
+ #[cfg(all(not(feature = "tokio_old"), feature = "tokio"))]
type Executor = executor::Tokio;
- #[cfg(all(not(feature = "tokio"), feature = "async-std"))]
+ #[cfg(all(
+ not(any(feature = "tokio_old", feature = "tokio")),
+ feature = "async-std"
+ ))]
type Executor = executor::AsyncStd;
- #[cfg(not(any(feature = "tokio", feature = "async-std")))]
+ #[cfg(not(any(
+ feature = "tokio_old",
+ feature = "tokio",
+ feature = "async-std"
+ )))]
type Executor = executor::ThreadPool;
/// A default cross-platform executor.
@@ -40,7 +50,7 @@ mod platform {
}
fn enter<R>(&self, f: impl FnOnce() -> R) -> R {
- self.0.enter(f)
+ super::Executor::enter(&self.0, f)
}
}
}
@@ -51,7 +61,11 @@ mod platform {
/// A default cross-platform executor.
///
- /// - On native platforms, it will use `iced_futures::executor::ThreadPool`.
+ /// - On native platforms, it will use:
+ /// - `iced_futures::executor::Tokio` when the `tokio` feature is enabled.
+ /// - `iced_futures::executor::AsyncStd` when the `async-std` feature is
+ /// enabled.
+ /// - `iced_futures::executor::ThreadPool` otherwise.
/// - On the Web, it will use `iced_futures::executor::WasmBindgen`.
#[derive(Debug)]
pub struct Default(WasmBindgen);
@@ -64,5 +78,9 @@ mod platform {
fn spawn(&self, future: impl futures::Future<Output = ()> + 'static) {
self.0.spawn(future);
}
+
+ fn enter<R>(&self, f: impl FnOnce() -> R) -> R {
+ self.0.enter(f)
+ }
}
}
diff --git a/src/keyboard.rs b/src/keyboard.rs
new file mode 100644
index 00000000..2134a66b
--- /dev/null
+++ b/src/keyboard.rs
@@ -0,0 +1,2 @@
+//! Listen and react to keyboard events.
+pub use crate::runtime::keyboard::{Event, KeyCode, Modifiers};
diff --git a/src/lib.rs b/src/lib.rs
index d492db02..3578ea82 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/master/examples
+//! [examples]: https://github.com/hecrj/iced/tree/0.2/examples
//! [repository]: https://github.com/hecrj/iced
//!
//! # Overview
@@ -166,43 +166,79 @@
//! 1. Draw the resulting user interface.
//!
//! # Usage
-//! Take a look at the [`Application`] trait, which streamlines all the process
-//! described above for you!
+//! The [`Application`] and [`Sandbox`] traits should get you started quickly,
+//! streamlining all the process described above!
//!
//! [Elm]: https://elm-lang.org/
//! [The Elm Architecture]: https://guide.elm-lang.org/architecture/
-//! [examples]: https://github.com/hecrj/iced/tree/master/examples
-//! [`Application`]: trait.Application.html
#![deny(missing_docs)]
#![deny(missing_debug_implementations)]
#![deny(unused_results)]
#![forbid(unsafe_code)]
#![forbid(rust_2018_idioms)]
+#![cfg_attr(docsrs, feature(doc_cfg))]
mod application;
mod element;
+mod error;
+mod result;
mod sandbox;
pub mod executor;
+pub mod keyboard;
+pub mod mouse;
pub mod settings;
pub mod widget;
pub mod window;
+#[cfg(all(
+ any(feature = "tokio", feature = "tokio_old", feature = "async-std"),
+ not(target_arch = "wasm32")
+))]
+#[cfg_attr(
+ docsrs,
+ doc(cfg(any(
+ feature = "tokio",
+ feature = "tokio_old",
+ feature = "async-std"
+ )))
+)]
+pub mod time;
+
+#[cfg(all(
+ not(target_arch = "wasm32"),
+ not(feature = "glow"),
+ feature = "wgpu"
+))]
+use iced_winit as runtime;
+
+#[cfg(all(not(target_arch = "wasm32"), feature = "glow"))]
+use iced_glutin as runtime;
+
+#[cfg(all(
+ not(target_arch = "wasm32"),
+ not(feature = "glow"),
+ feature = "wgpu"
+))]
+use iced_wgpu as renderer;
+
+#[cfg(all(not(target_arch = "wasm32"), feature = "glow"))]
+use iced_glow as renderer;
+
+#[cfg(target_arch = "wasm32")]
+use iced_web as runtime;
+
#[doc(no_inline)]
pub use widget::*;
pub use application::Application;
pub use element::Element;
+pub use error::Error;
pub use executor::Executor;
+pub use result::Result;
pub use sandbox::Sandbox;
pub use settings::Settings;
-#[cfg(not(target_arch = "wasm32"))]
-use iced_winit as common;
-
-#[cfg(target_arch = "wasm32")]
-use iced_web as common;
-
-pub use common::{
+pub use runtime::{
futures, Align, Background, Color, Command, Font, HorizontalAlignment,
- Length, Point, Size, Space, Subscription, Vector, VerticalAlignment,
+ Length, Point, Rectangle, Size, Subscription, Vector, VerticalAlignment,
};
diff --git a/src/mouse.rs b/src/mouse.rs
new file mode 100644
index 00000000..d61ed09a
--- /dev/null
+++ b/src/mouse.rs
@@ -0,0 +1,2 @@
+//! Listen and react to mouse events.
+pub use crate::runtime::mouse::{Button, Event, Interaction, ScrollDelta};
diff --git a/src/result.rs b/src/result.rs
new file mode 100644
index 00000000..ef565bd6
--- /dev/null
+++ b/src/result.rs
@@ -0,0 +1,6 @@
+use crate::Error;
+
+/// The result of running an [`Application`].
+///
+/// [`Application`]: crate::Application
+pub type Result = std::result::Result<(), Error>;
diff --git a/src/sandbox.rs b/src/sandbox.rs
index 2c0332ff..dbaa02f1 100644
--- a/src/sandbox.rs
+++ b/src/sandbox.rs
@@ -1,127 +1,143 @@
-use crate::{executor, Application, Command, Element, Settings, Subscription};
+use crate::{
+ Application, Color, Command, Element, Error, Settings, Subscription,
+};
/// A sandboxed [`Application`].
///
-/// A [`Sandbox`] is just an [`Application`] that cannot run any asynchronous
-/// actions.
+/// If you are a just getting started with the library, this trait offers a
+/// simpler interface than [`Application`].
///
-/// If you do not need to leverage a [`Command`], you can use a [`Sandbox`]
-/// instead of returning a [`Command::none`] everywhere.
+/// Unlike an [`Application`], a [`Sandbox`] cannot run any asynchronous
+/// actions or be initialized with some external flags. However, both traits
+/// are very similar and upgrading from a [`Sandbox`] is very straightforward.
///
-/// [`Application`]: trait.Application.html
-/// [`Sandbox`]: trait.Sandbox.html
-/// [`Command`]: struct.Command.html
-/// [`Command::none`]: struct.Command.html#method.none
+/// Therefore, it is recommended to always start by implementing this trait and
+/// upgrade only once necessary.
///
-/// # Example
-/// We can use a [`Sandbox`] to run the [`Counter` example we implemented
-/// before](index.html#overview), instead of an [`Application`]. We just need
-/// to remove the use of [`Command`]:
+/// # Examples
+/// [The repository has a bunch of examples] that use the [`Sandbox`] trait:
///
-/// ```no_run
-/// use iced::{button, Button, Column, Element, Sandbox, Settings, Text};
+/// - [`bezier_tool`], a Paint-like tool for drawing Bézier curves using
+/// [`lyon`].
+/// - [`counter`], the classic counter example explained in [the overview].
+/// - [`custom_widget`], a demonstration of how to build a custom widget that
+/// draws a circle.
+/// - [`geometry`], a custom widget showcasing how to draw geometry with the
+/// `Mesh2D` primitive in [`iced_wgpu`].
+/// - [`pane_grid`], a grid of panes that can be split, resized, and
+/// reorganized.
+/// - [`progress_bar`], a simple progress bar that can be filled by using a
+/// slider.
+/// - [`styling`], an example showcasing custom styling with a light and dark
+/// theme.
+/// - [`svg`], an application that renders the [Ghostscript Tiger] by leveraging
+/// the [`Svg` widget].
+/// - [`tour`], a simple UI tour that can run both on native platforms and the
+/// web!
///
-/// pub fn main() {
-/// Counter::run(Settings::default())
-/// }
+/// [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 overview]: index.html#overview
+/// [`iced_wgpu`]: https://github.com/hecrj/iced/tree/0.2/wgpu
+/// [`Svg` widget]: crate::widget::Svg
+/// [Ghostscript Tiger]: https://commons.wikimedia.org/wiki/File:Ghostscript_Tiger.svg
///
-/// #[derive(Default)]
-/// struct Counter {
-/// value: i32,
-/// increment_button: button::State,
-/// decrement_button: button::State,
-/// }
+/// ## A simple "Hello, world!"
+///
+/// If you just want to get started, here is a simple [`Sandbox`] that
+/// says "Hello, world!":
///
-/// #[derive(Debug, Clone, Copy)]
-/// enum Message {
-/// IncrementPressed,
-/// DecrementPressed,
+/// ```no_run
+/// use iced::{Element, Sandbox, Settings, Text};
+///
+/// pub fn main() -> iced::Result {
+/// Hello::run(Settings::default())
/// }
///
-/// impl Sandbox for Counter {
-/// type Message = Message;
+/// struct Hello;
+///
+/// impl Sandbox for Hello {
+/// type Message = ();
///
-/// fn new() -> Self {
-/// Self::default()
+/// fn new() -> Hello {
+/// Hello
/// }
///
/// fn title(&self) -> String {
-/// String::from("A simple counter")
+/// String::from("A cool application")
/// }
///
-/// fn update(&mut self, message: Message) {
-/// match message {
-/// Message::IncrementPressed => {
-/// self.value += 1;
-/// }
-/// Message::DecrementPressed => {
-/// self.value -= 1;
-/// }
-/// }
+/// fn update(&mut self, _message: Self::Message) {
+/// // This application has no interactions
/// }
///
-/// fn view(&mut self) -> Element<Message> {
-/// Column::new()
-/// .push(
-/// Button::new(&mut self.increment_button, Text::new("Increment"))
-/// .on_press(Message::IncrementPressed),
-/// )
-/// .push(
-/// Text::new(self.value.to_string()).size(50),
-/// )
-/// .push(
-/// Button::new(&mut self.decrement_button, Text::new("Decrement"))
-/// .on_press(Message::DecrementPressed),
-/// )
-/// .into()
+/// fn view(&mut self) -> Element<Self::Message> {
+/// Text::new("Hello, world!").into()
/// }
/// }
/// ```
pub trait Sandbox {
/// The type of __messages__ your [`Sandbox`] will produce.
- ///
- /// [`Sandbox`]: trait.Sandbox.html
type Message: std::fmt::Debug + Send;
/// Initializes the [`Sandbox`].
///
/// Here is where you should return the initial state of your app.
- ///
- /// [`Sandbox`]: trait.Sandbox.html
fn new() -> Self;
/// Returns the current title of the [`Sandbox`].
///
/// This title can be dynamic! The runtime will automatically update the
/// title of your application when necessary.
- ///
- /// [`Sandbox`]: trait.Sandbox.html
fn title(&self) -> String;
/// Handles a __message__ and updates the state of the [`Sandbox`].
///
/// This is where you define your __update logic__. All the __messages__,
/// produced by user interactions, will be handled by this method.
- ///
- /// [`Sandbox`]: trait.Sandbox.html
fn update(&mut self, message: Self::Message);
/// Returns the widgets to display in the [`Sandbox`].
///
/// These widgets can produce __messages__ based on user interaction.
- ///
- /// [`Sandbox`]: trait.Sandbox.html
fn view(&mut self) -> Element<'_, Self::Message>;
+ /// Returns the background color of the [`Sandbox`].
+ ///
+ /// By default, it returns [`Color::WHITE`].
+ fn background_color(&self) -> Color {
+ Color::WHITE
+ }
+
+ /// Returns the scale factor of the [`Sandbox`].
+ ///
+ /// It can be used to dynamically control the size of the UI at runtime
+ /// (i.e. zooming).
+ ///
+ /// For instance, a scale factor of `2.0` will make widgets twice as big,
+ /// while a scale factor of `0.5` will shrink them to half their size.
+ ///
+ /// By default, it returns `1.0`.
+ fn scale_factor(&self) -> f64 {
+ 1.0
+ }
+
/// Runs the [`Sandbox`].
///
- /// This method will take control of the current thread and __will NOT
- /// return__.
+ /// On native platforms, this method will take control of the current thread
+ /// and __will NOT return__.
///
/// It should probably be that last thing you call in your `main` function.
- ///
- /// [`Sandbox`]: trait.Sandbox.html
- fn run(settings: Settings)
+ fn run(settings: Settings<()>) -> Result<(), Error>
where
Self: 'static + Sized,
{
@@ -133,10 +149,11 @@ impl<T> Application for T
where
T: Sandbox,
{
- type Executor = executor::Null;
+ type Executor = crate::runtime::executor::Null;
+ type Flags = ();
type Message = T::Message;
- fn new() -> (Self, Command<T::Message>) {
+ fn new(_flags: ()) -> (Self, Command<T::Message>) {
(T::new(), Command::none())
}
@@ -157,4 +174,12 @@ where
fn view(&mut self) -> Element<'_, T::Message> {
T::view(self)
}
+
+ fn background_color(&self) -> Color {
+ T::background_color(self)
+ }
+
+ fn scale_factor(&self) -> f64 {
+ T::scale_factor(self)
+ }
}
diff --git a/src/settings.rs b/src/settings.rs
index 32ec583c..c82a1354 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -2,41 +2,79 @@
use crate::window;
/// The settings of an application.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
-pub struct Settings {
+#[derive(Debug, Clone)]
+pub struct Settings<Flags> {
/// The window settings.
///
/// They will be ignored on the Web.
- ///
- /// [`Window`]: struct.Window.html
pub window: window::Settings,
+ /// The data needed to initialize an [`Application`].
+ ///
+ /// [`Application`]: crate::Application
+ pub flags: Flags,
+
/// The bytes of the font that will be used by default.
///
/// If `None` is provided, a default system font will be chosen.
// TODO: Add `name` for web compatibility
pub default_font: Option<&'static [u8]>,
+ /// The text size that will be used by default.
+ ///
+ /// The default value is 20.
+ pub default_text_size: u16,
+
/// If set to true, the renderer will try to perform antialiasing for some
/// primitives.
///
/// Enabling it can produce a smoother result in some widgets, like the
- /// `Canvas`, at a performance cost.
+ /// [`Canvas`], at a performance cost.
///
/// By default, it is disabled.
+ ///
+ /// [`Canvas`]: crate::widget::Canvas
pub antialiasing: bool,
}
+impl<Flags> Settings<Flags> {
+ /// Initialize [`Application`] settings using the given data.
+ ///
+ /// [`Application`]: crate::Application
+ pub fn with_flags(flags: Flags) -> Self {
+ let default_settings = Settings::<()>::default();
+
+ Self {
+ flags,
+ antialiasing: default_settings.antialiasing,
+ default_font: default_settings.default_font,
+ default_text_size: default_settings.default_text_size,
+ window: default_settings.window,
+ }
+ }
+}
+
+impl<Flags> Default for Settings<Flags>
+where
+ Flags: Default,
+{
+ fn default() -> Self {
+ Self {
+ flags: Default::default(),
+ antialiasing: Default::default(),
+ default_font: Default::default(),
+ default_text_size: 20,
+ window: Default::default(),
+ }
+ }
+}
+
#[cfg(not(target_arch = "wasm32"))]
-impl From<Settings> for iced_winit::Settings {
- fn from(settings: Settings) -> iced_winit::Settings {
+impl<Flags> From<Settings<Flags>> for iced_winit::Settings<Flags> {
+ fn from(settings: Settings<Flags>) -> iced_winit::Settings<Flags> {
iced_winit::Settings {
- window: iced_winit::settings::Window {
- size: settings.window.size,
- resizable: settings.window.resizable,
- decorations: settings.window.decorations,
- platform_specific: Default::default(),
- },
+ window: settings.window.into(),
+ flags: settings.flags,
}
}
}
diff --git a/src/time.rs b/src/time.rs
new file mode 100644
index 00000000..b8432895
--- /dev/null
+++ b/src/time.rs
@@ -0,0 +1,12 @@
+//! Listen and react to time.
+use crate::Subscription;
+
+/// Returns a [`Subscription`] that produces messages at a set interval.
+///
+/// The first message is produced after a `duration`, and then continues to
+/// produce more messages every `duration` after that.
+pub fn every(
+ duration: std::time::Duration,
+) -> Subscription<std::time::Instant> {
+ iced_futures::time::every(duration)
+}
diff --git a/src/widget.rs b/src/widget.rs
index 7d3a1cef..b9b65499 100644
--- a/src/widget.rs
+++ b/src/widget.rs
@@ -13,43 +13,54 @@
//!
//! These widgets have their own module with a `State` type. For instance, a
//! [`TextInput`] has some [`text_input::State`].
-//!
-//! [`TextInput`]: text_input/struct.TextInput.html
-//! [`text_input::State`]: text_input/struct.State.html
#[cfg(not(target_arch = "wasm32"))]
mod platform {
- pub use iced_wgpu::widget::*;
+ pub use crate::renderer::widget::{
+ button, checkbox, container, pane_grid, pick_list, progress_bar, radio,
+ rule, scrollable, slider, text_input, Column, Row, Space, Text,
+ };
+
+ #[cfg(any(feature = "canvas", feature = "glow_canvas"))]
+ #[cfg_attr(
+ docsrs,
+ doc(cfg(any(feature = "canvas", feature = "glow_canvas")))
+ )]
+ pub use crate::renderer::widget::canvas;
+
+ #[cfg(any(feature = "qr_code", feature = "glow_qr_code"))]
+ #[cfg_attr(
+ docsrs,
+ doc(cfg(any(feature = "qr_code", feature = "glow_qr_code")))
+ )]
+ pub use crate::renderer::widget::qr_code;
+ #[cfg_attr(docsrs, doc(cfg(feature = "image")))]
pub mod image {
//! Display images in your user interface.
- pub use iced_winit::image::{Handle, Image};
+ pub use crate::runtime::image::{Handle, Image};
}
+ #[cfg_attr(docsrs, doc(cfg(feature = "svg")))]
pub mod svg {
//! Display vector graphics in your user interface.
- pub use iced_winit::svg::{Handle, Svg};
+ pub use crate::runtime::svg::{Handle, Svg};
}
- pub use iced_winit::Text;
-
#[doc(no_inline)]
pub use {
button::Button, checkbox::Checkbox, container::Container, image::Image,
- progress_bar::ProgressBar, radio::Radio, scrollable::Scrollable,
- slider::Slider, svg::Svg, text_input::TextInput,
+ pane_grid::PaneGrid, pick_list::PickList, progress_bar::ProgressBar,
+ radio::Radio, rule::Rule, scrollable::Scrollable, slider::Slider,
+ svg::Svg, text_input::TextInput,
};
- /// A container that distributes its contents vertically.
- ///
- /// This is an alias of an `iced_native` column with a default `Renderer`.
- pub type Column<'a, Message> =
- iced_winit::Column<'a, Message, iced_wgpu::Renderer>;
-
- /// A container that distributes its contents horizontally.
- ///
- /// This is an alias of an `iced_native` row with a default `Renderer`.
- pub type Row<'a, Message> =
- iced_winit::Row<'a, Message, iced_wgpu::Renderer>;
+ #[cfg(any(feature = "canvas", feature = "glow_canvas"))]
+ #[doc(no_inline)]
+ pub use canvas::Canvas;
+
+ #[cfg(any(feature = "qr_code", feature = "glow_qr_code"))]
+ #[doc(no_inline)]
+ pub use qr_code::QRCode;
}
#[cfg(target_arch = "wasm32")]
diff --git a/src/window.rs b/src/window.rs
index 54ea2a02..a2883b62 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -2,5 +2,8 @@
mod mode;
mod settings;
+pub mod icon;
+
+pub use icon::Icon;
pub use mode::Mode;
pub use settings::Settings;
diff --git a/src/window/icon.rs b/src/window/icon.rs
new file mode 100644
index 00000000..0d27b00e
--- /dev/null
+++ b/src/window/icon.rs
@@ -0,0 +1,132 @@
+//! Attach an icon to the window of your application.
+use std::fmt;
+use std::io;
+
+/// The icon of a window.
+#[cfg(not(target_arch = "wasm32"))]
+#[derive(Debug, Clone)]
+pub struct Icon(iced_winit::winit::window::Icon);
+
+/// The icon of a window.
+#[cfg(target_arch = "wasm32")]
+#[derive(Debug, Clone)]
+pub struct Icon;
+
+impl Icon {
+ /// Creates an icon from 32bpp RGBA data.
+ #[cfg(not(target_arch = "wasm32"))]
+ pub fn from_rgba(
+ rgba: Vec<u8>,
+ width: u32,
+ height: u32,
+ ) -> Result<Self, Error> {
+ let raw =
+ iced_winit::winit::window::Icon::from_rgba(rgba, width, height)?;
+
+ Ok(Icon(raw))
+ }
+
+ /// Creates an icon from 32bpp RGBA data.
+ #[cfg(target_arch = "wasm32")]
+ pub fn from_rgba(
+ _rgba: Vec<u8>,
+ _width: u32,
+ _height: u32,
+ ) -> Result<Self, Error> {
+ Ok(Icon)
+ }
+}
+
+/// An error produced when using `Icon::from_rgba` with invalid arguments.
+#[derive(Debug)]
+pub enum Error {
+ /// The provided RGBA data isn't divisble by 4.
+ ///
+ /// Therefore, it cannot be safely interpreted as 32bpp RGBA pixels.
+ InvalidData {
+ /// The length of the provided RGBA data.
+ byte_count: usize,
+ },
+
+ /// The number of RGBA pixels does not match the provided dimensions.
+ DimensionsMismatch {
+ /// The provided width.
+ width: u32,
+ /// The provided height.
+ height: u32,
+ /// The amount of pixels of the provided RGBA data.
+ pixel_count: usize,
+ },
+
+ /// The underlying OS failed to create the icon.
+ OsError(io::Error),
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+impl From<iced_winit::winit::window::BadIcon> for Error {
+ fn from(error: iced_winit::winit::window::BadIcon) -> Self {
+ use iced_winit::winit::window::BadIcon;
+
+ match error {
+ BadIcon::ByteCountNotDivisibleBy4 { byte_count } => {
+ Error::InvalidData { byte_count }
+ }
+ BadIcon::DimensionsVsPixelCount {
+ width,
+ height,
+ pixel_count,
+ ..
+ } => Error::DimensionsMismatch {
+ width,
+ height,
+ pixel_count,
+ },
+ BadIcon::OsError(os_error) => Error::OsError(os_error),
+ }
+ }
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+impl From<Icon> for iced_winit::winit::window::Icon {
+ fn from(icon: Icon) -> Self {
+ icon.0
+ }
+}
+
+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 \
+ 4. Therefore, it cannot be safely interpreted as 32bpp RGBA \
+ pixels.",
+ byte_count,
+ )
+ }
+ Error::DimensionsMismatch {
+ width,
+ height,
+ pixel_count,
+ } => {
+ write!(f,
+ "The number of RGBA pixels ({:?}) does not match the provided \
+ dimensions ({:?}x{:?}).",
+ pixel_count, width, height,
+ )
+ }
+ Error::OsError(e) => write!(
+ f,
+ "The underlying OS failed to create the window \
+ icon: {:?}",
+ e
+ ),
+ }
+ }
+}
+
+impl std::error::Error for Error {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ Some(self)
+ }
+}
diff --git a/src/window/settings.rs b/src/window/settings.rs
index a31d2af2..6b5d2985 100644
--- a/src/window/settings.rs
+++ b/src/window/settings.rs
@@ -1,22 +1,61 @@
+use crate::window::Icon;
+
/// The window settings of an application.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[derive(Debug, Clone)]
pub struct Settings {
- /// The size of the window.
+ /// The initial size of the window.
pub size: (u32, u32),
+ /// The minimum size of the window.
+ pub min_size: Option<(u32, u32)>,
+
+ /// The maximum size of the window.
+ pub max_size: Option<(u32, u32)>,
+
/// Whether the window should be resizable or not.
pub resizable: bool,
/// Whether the window should have a border, a title bar, etc. or not.
pub decorations: bool,
+
+ /// Whether the window should be transparent.
+ pub transparent: bool,
+
+ /// Whether the window will always be on top of other windows.
+ pub always_on_top: bool,
+
+ /// The icon of the window.
+ pub icon: Option<Icon>,
}
impl Default for Settings {
fn default() -> Settings {
Settings {
size: (1024, 768),
+ min_size: None,
+ max_size: None,
resizable: true,
decorations: true,
+ transparent: false,
+ always_on_top: false,
+ icon: None,
+ }
+ }
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+impl From<Settings> for iced_winit::settings::Window {
+ fn from(settings: Settings) -> Self {
+ Self {
+ size: settings.size,
+ min_size: settings.min_size,
+ max_size: settings.max_size,
+ resizable: settings.resizable,
+ decorations: settings.decorations,
+ transparent: settings.transparent,
+ always_on_top: settings.always_on_top,
+ icon: settings.icon.map(Icon::into),
+ platform_specific: Default::default(),
}
}
}
diff --git a/style/Cargo.toml b/style/Cargo.toml
index 5928c60d..ac16f8ee 100644
--- a/style/Cargo.toml
+++ b/style/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "iced_style"
-version = "0.1.0-alpha"
+version = "0.2.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
description = "The default set of styles of Iced"
@@ -11,4 +11,4 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"]
categories = ["gui"]
[dependencies]
-iced_core = { version = "0.1.0", path = "../core" }
+iced_core = { version = "0.3", path = "../core" }
diff --git a/style/src/button.rs b/style/src/button.rs
index 1e3844f9..43d27216 100644
--- a/style/src/button.rs
+++ b/style/src/button.rs
@@ -6,8 +6,8 @@ use iced_core::{Background, Color, Vector};
pub struct Style {
pub shadow_offset: Vector,
pub background: Option<Background>,
- pub border_radius: u16,
- pub border_width: u16,
+ pub border_radius: f32,
+ pub border_width: f32,
pub border_color: Color,
pub text_color: Color,
}
@@ -17,8 +17,8 @@ impl std::default::Default for Style {
Self {
shadow_offset: Vector::default(),
background: None,
- border_radius: 0,
- border_width: 0,
+ border_radius: 0.0,
+ border_width: 0.0,
border_color: Color::TRANSPARENT,
text_color: Color::BLACK,
}
@@ -72,8 +72,8 @@ impl StyleSheet for Default {
Style {
shadow_offset: Vector::new(0.0, 0.0),
background: Some(Background::Color([0.87, 0.87, 0.87].into())),
- border_radius: 2,
- border_width: 1,
+ border_radius: 2.0,
+ border_width: 1.0,
border_color: [0.7, 0.7, 0.7].into(),
text_color: Color::BLACK,
}
diff --git a/style/src/checkbox.rs b/style/src/checkbox.rs
index 3c645f15..1c5f2460 100644
--- a/style/src/checkbox.rs
+++ b/style/src/checkbox.rs
@@ -6,8 +6,8 @@ use iced_core::{Background, Color};
pub struct Style {
pub background: Background,
pub checkmark_color: Color,
- pub border_radius: u16,
- pub border_width: u16,
+ pub border_radius: f32,
+ pub border_width: f32,
pub border_color: Color,
}
@@ -25,8 +25,8 @@ impl StyleSheet for Default {
Style {
background: Background::Color(Color::from_rgb(0.95, 0.95, 0.95)),
checkmark_color: Color::from_rgb(0.3, 0.3, 0.3),
- border_radius: 5,
- border_width: 1,
+ border_radius: 5.0,
+ border_width: 1.0,
border_color: Color::from_rgb(0.6, 0.6, 0.6),
}
}
diff --git a/style/src/container.rs b/style/src/container.rs
index d2247342..1ce6a7ca 100644
--- a/style/src/container.rs
+++ b/style/src/container.rs
@@ -6,8 +6,8 @@ use iced_core::{Background, Color};
pub struct Style {
pub text_color: Option<Color>,
pub background: Option<Background>,
- pub border_radius: u16,
- pub border_width: u16,
+ pub border_radius: f32,
+ pub border_width: f32,
pub border_color: Color,
}
@@ -16,8 +16,8 @@ impl std::default::Default for Style {
Self {
text_color: None,
background: None,
- border_radius: 0,
- border_width: 0,
+ border_radius: 0.0,
+ border_width: 0.0,
border_color: Color::TRANSPARENT,
}
}
@@ -36,8 +36,8 @@ impl StyleSheet for Default {
Style {
text_color: None,
background: None,
- border_radius: 0,
- border_width: 0,
+ border_radius: 0.0,
+ border_width: 0.0,
border_color: Color::TRANSPARENT,
}
}
diff --git a/style/src/lib.rs b/style/src/lib.rs
index 2c5977b5..7e0a9f49 100644
--- a/style/src/lib.rs
+++ b/style/src/lib.rs
@@ -2,11 +2,18 @@
//!
//! It contains a set of styles and stylesheets for most of the built-in
//! widgets.
+//!
+//! ![The foundations of the Iced ecosystem](https://github.com/hecrj/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/foundations.png?raw=true)
+pub use iced_core::{Background, Color};
+
pub mod button;
pub mod checkbox;
pub mod container;
+pub mod menu;
+pub mod pick_list;
pub mod progress_bar;
pub mod radio;
+pub mod rule;
pub mod scrollable;
pub mod slider;
pub mod text_input;
diff --git a/style/src/menu.rs b/style/src/menu.rs
new file mode 100644
index 00000000..90985b8f
--- /dev/null
+++ b/style/src/menu.rs
@@ -0,0 +1,25 @@
+use iced_core::{Background, Color};
+
+/// The appearance of a menu.
+#[derive(Debug, Clone, Copy)]
+pub struct Style {
+ pub text_color: Color,
+ pub background: Background,
+ pub border_width: f32,
+ pub border_color: Color,
+ pub selected_text_color: Color,
+ pub selected_background: Background,
+}
+
+impl std::default::Default for Style {
+ fn default() -> Self {
+ Self {
+ text_color: Color::BLACK,
+ background: Background::Color([0.87, 0.87, 0.87].into()),
+ border_width: 1.0,
+ border_color: [0.7, 0.7, 0.7].into(),
+ selected_text_color: Color::WHITE,
+ selected_background: Background::Color([0.4, 0.4, 1.0].into()),
+ }
+ }
+}
diff --git a/style/src/pick_list.rs b/style/src/pick_list.rs
new file mode 100644
index 00000000..a757ba98
--- /dev/null
+++ b/style/src/pick_list.rs
@@ -0,0 +1,70 @@
+use crate::menu;
+use iced_core::{Background, Color};
+
+/// The appearance of a pick list.
+#[derive(Debug, Clone, Copy)]
+pub struct Style {
+ pub text_color: Color,
+ pub background: Background,
+ pub border_radius: f32,
+ pub border_width: f32,
+ pub border_color: Color,
+ pub icon_size: f32,
+}
+
+impl std::default::Default for Style {
+ fn default() -> Self {
+ Self {
+ text_color: Color::BLACK,
+ background: Background::Color([0.87, 0.87, 0.87].into()),
+ border_radius: 0.0,
+ border_width: 1.0,
+ border_color: [0.7, 0.7, 0.7].into(),
+ icon_size: 0.7,
+ }
+ }
+}
+
+/// A set of rules that dictate the style of a container.
+pub trait StyleSheet {
+ fn menu(&self) -> menu::Style;
+
+ fn active(&self) -> Style;
+
+ /// Produces the style of a container.
+ fn hovered(&self) -> Style;
+}
+
+struct Default;
+
+impl StyleSheet for Default {
+ fn menu(&self) -> menu::Style {
+ menu::Style::default()
+ }
+
+ fn active(&self) -> Style {
+ Style::default()
+ }
+
+ fn hovered(&self) -> Style {
+ Style {
+ border_color: Color::BLACK,
+ ..self.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/style/src/progress_bar.rs b/style/src/progress_bar.rs
index 73503fa8..36be63f9 100644
--- a/style/src/progress_bar.rs
+++ b/style/src/progress_bar.rs
@@ -6,7 +6,7 @@ use iced_core::{Background, Color};
pub struct Style {
pub background: Background,
pub bar: Background,
- pub border_radius: u16,
+ pub border_radius: f32,
}
/// A set of rules that dictate the style of a progress bar.
@@ -21,7 +21,7 @@ impl StyleSheet for Default {
Style {
background: Background::Color(Color::from_rgb(0.6, 0.6, 0.6)),
bar: Background::Color(Color::from_rgb(0.3, 0.9, 0.3)),
- border_radius: 5,
+ border_radius: 5.0,
}
}
}
diff --git a/style/src/radio.rs b/style/src/radio.rs
index 1f0689b9..83310e05 100644
--- a/style/src/radio.rs
+++ b/style/src/radio.rs
@@ -6,7 +6,7 @@ use iced_core::{Background, Color};
pub struct Style {
pub background: Background,
pub dot_color: Color,
- pub border_width: u16,
+ pub border_width: f32,
pub border_color: Color,
}
@@ -24,7 +24,7 @@ impl StyleSheet for Default {
Style {
background: Background::Color(Color::from_rgb(0.95, 0.95, 0.95)),
dot_color: Color::from_rgb(0.3, 0.3, 0.3),
- border_width: 1,
+ border_width: 1.0,
border_color: Color::from_rgb(0.6, 0.6, 0.6),
}
}
diff --git a/style/src/rule.rs b/style/src/rule.rs
new file mode 100644
index 00000000..5021340b
--- /dev/null
+++ b/style/src/rule.rs
@@ -0,0 +1,114 @@
+//! Display a horizontal or vertical rule for dividing content.
+
+use iced_core::Color;
+
+/// The fill mode of a rule.
+#[derive(Debug, Clone, Copy)]
+pub enum FillMode {
+ /// Fill the whole length of the container.
+ Full,
+ /// Fill a percent of the length of the container. The rule
+ /// will be centered in that container.
+ ///
+ /// The range is `[0.0, 100.0]`.
+ Percent(f32),
+ /// Uniform offset from each end, length units.
+ Padded(u16),
+ /// Different offset on each end of the rule, length units.
+ /// First = top or left.
+ AsymmetricPadding(u16, u16),
+}
+
+impl FillMode {
+ /// Return the starting offset and length of the rule.
+ ///
+ /// * `space` - The space to fill.
+ ///
+ /// # Returns
+ ///
+ /// * (starting_offset, length)
+ pub fn fill(&self, space: f32) -> (f32, f32) {
+ match *self {
+ FillMode::Full => (0.0, space),
+ FillMode::Percent(percent) => {
+ if percent >= 100.0 {
+ (0.0, space)
+ } else {
+ let percent_width = (space * percent / 100.0).round();
+
+ (((space - percent_width) / 2.0).round(), percent_width)
+ }
+ }
+ FillMode::Padded(padding) => {
+ if padding == 0 {
+ (0.0, space)
+ } else {
+ let padding = padding as f32;
+ let mut line_width = space - (padding * 2.0);
+ if line_width < 0.0 {
+ line_width = 0.0;
+ }
+
+ (padding, line_width)
+ }
+ }
+ FillMode::AsymmetricPadding(first_pad, second_pad) => {
+ let first_pad = first_pad as f32;
+ let second_pad = second_pad as f32;
+ let mut line_width = space - first_pad - second_pad;
+ if line_width < 0.0 {
+ line_width = 0.0;
+ }
+
+ (first_pad, line_width)
+ }
+ }
+ }
+}
+
+/// The appearance of a rule.
+#[derive(Debug, Clone, Copy)]
+pub struct Style {
+ /// The color of the rule.
+ pub color: Color,
+ /// The width (thickness) of the rule line.
+ pub width: u16,
+ /// The radius of the line corners.
+ pub radius: f32,
+ /// The [`FillMode`] of the rule.
+ pub fill_mode: FillMode,
+}
+
+/// A set of rules that dictate the style of a rule.
+pub trait StyleSheet {
+ /// Produces the style of a rule.
+ fn style(&self) -> Style;
+}
+
+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),
+ }
+ }
+}
+
+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/scrollable.rs b/style/src/scrollable.rs
index 690c14a2..65da9803 100644
--- a/style/src/scrollable.rs
+++ b/style/src/scrollable.rs
@@ -5,8 +5,8 @@ use iced_core::{Background, Color};
#[derive(Debug, Clone, Copy)]
pub struct Scrollbar {
pub background: Option<Background>,
- pub border_radius: u16,
- pub border_width: u16,
+ pub border_radius: f32,
+ pub border_width: f32,
pub border_color: Color,
pub scroller: Scroller,
}
@@ -15,8 +15,8 @@ pub struct Scrollbar {
#[derive(Debug, Clone, Copy)]
pub struct Scroller {
pub color: Color,
- pub border_radius: u16,
- pub border_width: u16,
+ pub border_radius: f32,
+ pub border_width: f32,
pub border_color: Color,
}
@@ -40,13 +40,13 @@ impl StyleSheet for Default {
fn active(&self) -> Scrollbar {
Scrollbar {
background: None,
- border_radius: 5,
- border_width: 0,
+ border_radius: 5.0,
+ border_width: 0.0,
border_color: Color::TRANSPARENT,
scroller: Scroller {
color: [0.0, 0.0, 0.0, 0.7].into(),
- border_radius: 5,
- border_width: 0,
+ border_radius: 5.0,
+ border_width: 0.0,
border_color: Color::TRANSPARENT,
},
}
diff --git a/style/src/slider.rs b/style/src/slider.rs
index 776e180c..9148fcbe 100644
--- a/style/src/slider.rs
+++ b/style/src/slider.rs
@@ -13,15 +13,15 @@ pub struct Style {
pub struct Handle {
pub shape: HandleShape,
pub color: Color,
- pub border_width: u16,
+ pub border_width: f32,
pub border_color: Color,
}
/// The shape of the handle of a slider.
#[derive(Debug, Clone, Copy)]
pub enum HandleShape {
- Circle { radius: u16 },
- Rectangle { width: u16, border_radius: u16 },
+ Circle { radius: f32 },
+ Rectangle { width: u16, border_radius: f32 },
}
/// A set of rules that dictate the style of a slider.
@@ -45,11 +45,11 @@ impl StyleSheet for Default {
handle: Handle {
shape: HandleShape::Rectangle {
width: 8,
- border_radius: 4,
+ border_radius: 4.0,
},
color: Color::from_rgb(0.95, 0.95, 0.95),
border_color: Color::from_rgb(0.6, 0.6, 0.6),
- border_width: 1,
+ border_width: 1.0,
},
}
}
diff --git a/style/src/text_input.rs b/style/src/text_input.rs
index c5123b20..19acea65 100644
--- a/style/src/text_input.rs
+++ b/style/src/text_input.rs
@@ -5,8 +5,8 @@ use iced_core::{Background, Color};
#[derive(Debug, Clone, Copy)]
pub struct Style {
pub background: Background,
- pub border_radius: u16,
- pub border_width: u16,
+ pub border_radius: f32,
+ pub border_width: f32,
pub border_color: Color,
}
@@ -14,8 +14,8 @@ impl std::default::Default for Style {
fn default() -> Self {
Self {
background: Background::Color(Color::WHITE),
- border_radius: 0,
- border_width: 0,
+ border_radius: 0.0,
+ border_width: 0.0,
border_color: Color::TRANSPARENT,
}
}
@@ -33,6 +33,8 @@ pub trait StyleSheet {
fn value_color(&self) -> Color;
+ fn selection_color(&self) -> Color;
+
/// Produces the style of an hovered text input.
fn hovered(&self) -> Style {
self.focused()
@@ -45,8 +47,8 @@ impl StyleSheet for Default {
fn active(&self) -> Style {
Style {
background: Background::Color(Color::WHITE),
- border_radius: 5,
- border_width: 1,
+ border_radius: 5.0,
+ border_width: 1.0,
border_color: Color::from_rgb(0.7, 0.7, 0.7),
}
}
@@ -65,6 +67,10 @@ impl StyleSheet for Default {
fn value_color(&self) -> Color {
Color::from_rgb(0.3, 0.3, 0.3)
}
+
+ fn selection_color(&self) -> Color {
+ Color::from_rgb(0.8, 0.8, 1.0)
+ }
}
impl std::default::Default for Box<dyn StyleSheet> {
diff --git a/web/CHANGELOG.md b/web/CHANGELOG.md
deleted file mode 100644
index ed02519a..00000000
--- a/web/CHANGELOG.md
+++ /dev/null
@@ -1,19 +0,0 @@
-# Changelog
-All notable changes to this project will be documented in this file.
-
-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]
-### Changed
-- `Button::background` takes an `Into<Background>` now instead of a `Background`.
-
-### Fixed
-- Render not being scheduled after `Command` futures finishing.
-
-## [0.1.0] - 2019-11-25
-### Added
-- First release! :tada:
-
-[Unreleased]: https://github.com/hecrj/iced/compare/web-0.1.0...HEAD
-[0.1.0]: https://github.com/hecrj/iced/releases/tag/web-0.1.0
diff --git a/web/Cargo.toml b/web/Cargo.toml
index c043c697..e063a021 100644
--- a/web/Cargo.toml
+++ b/web/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "iced_web"
-version = "0.1.0"
+version = "0.3.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
description = "A web backend for Iced"
@@ -15,20 +15,24 @@ categories = ["web-programming"]
maintenance = { status = "actively-developed" }
[dependencies]
-iced_style = { version = "0.1.0-alpha", path = "../style" }
-dodrio = "0.1.0"
-wasm-bindgen = "0.2.51"
+dodrio = "0.2"
+wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
url = "2.0"
+num-traits = "0.2"
[dependencies.iced_core]
-version = "0.1.0"
+version = "0.3"
path = "../core"
[dependencies.iced_futures]
-version = "0.1.0-alpha"
+version = "0.2"
path = "../futures"
+[dependencies.iced_style]
+version = "0.2"
+path = "../style"
+
[dependencies.web-sys]
version = "0.3.27"
features = [
diff --git a/web/README.md b/web/README.md
index aceb4a97..0e770589 100644
--- a/web/README.md
+++ b/web/README.md
@@ -8,8 +8,6 @@
The crate is currently a __very experimental__, simple abstraction layer over [`dodrio`].
-![iced_core](../docs/graphs/web.png)
-
[documentation]: https://docs.rs/iced_web
[`iced_core`]: ../core
[`dodrio`]: https://github.com/fitzgen/dodrio
@@ -18,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.1.0"
+iced_web = "0.3"
```
__Iced moves fast and the `master` branch can contain breaking changes!__ If
@@ -36,7 +34,7 @@ For instance, let's say we want to build the [`tour` example]:
```
cd examples
cargo build --package tour --target wasm32-unknown-unknown
-wasm-bindgen ../target/wasm32-unknown-unknown/debug/examples/tour.wasm --out-dir tour --web
+wasm-bindgen ../target/wasm32-unknown-unknown/debug/tour.wasm --out-dir tour --web
```
*__Note:__ Keep in mind that Iced is still in early exploration stages and most of the work needs to happen on the native side of the ecosystem. At this stage, it is important to be able to batch work without having to constantly jump back and forth. Because of this, there is currently no requirement for the `master` branch to contain a cross-platform API at all times. If you hit an issue when building an example and want to help, it may be a good way to [start contributing]!*
@@ -50,6 +48,7 @@ Once the example is compiled, we need to create an `.html` file to load our appl
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
+ <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Tour - Iced</title>
</head>
<body>
diff --git a/web/src/bus.rs b/web/src/bus.rs
index c66e9659..5ce8e810 100644
--- a/web/src/bus.rs
+++ b/web/src/bus.rs
@@ -5,7 +5,7 @@ use std::rc::Rc;
///
/// It can be used to route messages back to the [`Application`].
///
-/// [`Application`]: trait.Application.html
+/// [`Application`]: crate::Application
#[allow(missing_debug_implementations)]
pub struct Bus<Message> {
publish: Rc<Box<dyn Fn(Message) -> ()>>,
@@ -33,15 +33,13 @@ where
/// Publishes a new message for the [`Application`].
///
- /// [`Application`]: trait.Application.html
+ /// [`Application`]: crate::Application
pub fn publish(&self, message: Message) {
(self.publish)(message)
}
/// Creates a new [`Bus`] that applies the given function to the messages
/// before publishing.
- ///
- /// [`Bus`]: struct.Bus.html
pub fn map<B>(&self, mapper: Rc<Box<dyn Fn(B) -> Message>>) -> Bus<B>
where
B: 'static,
diff --git a/web/src/css.rs b/web/src/css.rs
index 6a307770..bdde23f3 100644
--- a/web/src/css.rs
+++ b/web/src/css.rs
@@ -20,9 +20,7 @@ pub enum Rule {
}
impl Rule {
- /// Returns the class name of the [`Style`].
- ///
- /// [`Style`]: enum.Style.html
+ /// Returns the class name of the [`Rule`].
pub fn class<'a>(&self) -> String {
match self {
Rule::Column => String::from("c"),
@@ -32,9 +30,7 @@ impl Rule {
}
}
- /// Returns the declaration of the [`Style`].
- ///
- /// [`Style`]: enum.Style.html
+ /// Returns the declaration of the [`Rule`].
pub fn declaration<'a>(&self, bump: &'a bumpalo::Bump) -> &'a str {
let class = self.class();
@@ -81,22 +77,17 @@ pub struct Css<'a> {
}
impl<'a> Css<'a> {
- /// Creates an empty style [`Sheet`].
- ///
- /// [`Sheet`]: struct.Sheet.html
+ /// Creates an empty [`Css`].
pub fn new() -> Self {
Css {
rules: BTreeMap::new(),
}
}
- /// Inserts the [`rule`] in the [`Sheet`], if it was not previously
+ /// Inserts the [`Rule`] in the [`Css`], if it was not previously
/// inserted.
///
/// It returns the class name of the provided [`Rule`].
- ///
- /// [`Sheet`]: struct.Sheet.html
- /// [`Rule`]: enum.Rule.html
pub fn insert(&mut self, bump: &'a bumpalo::Bump, rule: Rule) -> String {
let class = rule.class();
@@ -107,9 +98,7 @@ impl<'a> Css<'a> {
class
}
- /// Produces the VDOM node of the style [`Sheet`].
- ///
- /// [`Sheet`]: struct.Sheet.html
+ /// Produces the VDOM node of the [`Css`].
pub fn node(self, bump: &'a bumpalo::Bump) -> dodrio::Node<'a> {
use dodrio::builder::*;
@@ -133,8 +122,6 @@ impl<'a> Css<'a> {
}
/// Returns the style value for the given [`Length`].
-///
-/// [`Length`]: ../enum.Length.html
pub fn length(length: Length) -> String {
match length {
Length::Shrink => String::from("auto"),
@@ -164,15 +151,11 @@ pub fn min_length(units: u32) -> String {
}
/// Returns the style value for the given [`Color`].
-///
-/// [`Color`]: ../struct.Color.html
pub fn color(Color { r, g, b, a }: Color) -> String {
format!("rgba({}, {}, {}, {})", 255.0 * r, 255.0 * g, 255.0 * b, a)
}
/// Returns the style value for the given [`Background`].
-///
-/// [`Background`]: ../struct.Background.html
pub fn background(background: Background) -> String {
match background {
Background::Color(c) => color(c),
@@ -180,8 +163,6 @@ pub fn background(background: Background) -> String {
}
/// Returns the style value for the given [`Align`].
-///
-/// [`Align`]: ../enum.Align.html
pub fn align(align: Align) -> &'static str {
match align {
Align::Start => "flex-start",
diff --git a/web/src/element.rs b/web/src/element.rs
index 93e73713..6bb90177 100644
--- a/web/src/element.rs
+++ b/web/src/element.rs
@@ -11,9 +11,7 @@ use std::rc::Rc;
/// If you have a [built-in widget], you should be able to use `Into<Element>`
/// to turn it into an [`Element`].
///
-/// [built-in widget]: widget/index.html
-/// [`Widget`]: widget/trait.Widget.html
-/// [`Element`]: struct.Element.html
+/// [built-in widget]: mod@crate::widget
#[allow(missing_debug_implementations)]
pub struct Element<'a, Message> {
pub(crate) widget: Box<dyn Widget<Message> + 'a>,
@@ -21,9 +19,6 @@ pub struct Element<'a, Message> {
impl<'a, Message> Element<'a, Message> {
/// Create a new [`Element`] containing the given [`Widget`].
- ///
- /// [`Element`]: struct.Element.html
- /// [`Widget`]: widget/trait.Widget.html
pub fn new(widget: impl Widget<Message> + 'a) -> Self {
Self {
widget: Box::new(widget),
@@ -34,8 +29,6 @@ impl<'a, Message> Element<'a, Message> {
///
/// This method is useful when you want to decouple different parts of your
/// UI and make them __composable__.
- ///
- /// [`Element`]: struct.Element.html
pub fn map<F, B>(self, f: F) -> Element<'a, B>
where
Message: 'static,
diff --git a/web/src/lib.rs b/web/src/lib.rs
index 258ad9e7..58f6591d 100644
--- a/web/src/lib.rs
+++ b/web/src/lib.rs
@@ -1,7 +1,5 @@
//! A web runtime for Iced, targetting the DOM.
//!
-//! ![`iced_web` crate graph](https://github.com/hecrj/iced/blob/cae26cb7bc627f4a5b3bcf1cd023a0c552e8c65e/docs/graphs/web.png?raw=true)
-//!
//! `iced_web` takes [`iced_core`] and builds a WebAssembly runtime on top. It
//! achieves this by introducing a `Widget` trait that can be used to produce
//! VDOM nodes.
@@ -23,8 +21,8 @@
//!
//! ```bash
//! cd examples
-//! cargo build --example tour --target wasm32-unknown-unknown
-//! wasm-bindgen ../target/wasm32-unknown-unknown/debug/examples/tour.wasm --out-dir tour --web
+//! cargo build --package tour --target wasm32-unknown-unknown
+//! wasm-bindgen ../target/wasm32-unknown-unknown/debug/tour.wasm --out-dir tour --web
//! ```
//!
//! Then, we need to create an `.html` file to load our application:
@@ -34,6 +32,7 @@
//! <html>
//! <head>
//! <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
+//! <meta name="viewport" content="width=device-width, initial-scale=1">
//! <title>Tour - Iced</title>
//! </head>
//! <body>
@@ -50,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/blob/master/examples/tour.rs
+//! [`tour` example]: https://github.com/hecrj/iced/tree/0.2/examples/tour
#![deny(missing_docs)]
#![deny(missing_debug_implementations)]
#![deny(unused_results)]
@@ -73,8 +72,8 @@ pub use dodrio;
pub use element::Element;
pub use hasher::Hasher;
pub use iced_core::{
- Align, Background, Color, Font, HorizontalAlignment, Length, Point, Size,
- Vector, VerticalAlignment,
+ keyboard, mouse, Align, Background, Color, Font, HorizontalAlignment,
+ Length, Point, Rectangle, Size, Vector, VerticalAlignment,
};
pub use iced_futures::{executor, futures, Command};
pub use subscription::Subscription;
@@ -94,30 +93,23 @@ pub use executor::Executor;
/// An [`Application`](trait.Application.html) can execute asynchronous actions
/// by returning a [`Command`](struct.Command.html) in some of its methods.
pub trait Application {
+ /// The [`Executor`] that will run commands and subscriptions.
+ type Executor: Executor;
+
/// The type of __messages__ your [`Application`] will produce.
- ///
- /// [`Application`]: trait.Application.html
type Message: Send;
- /// The [`Executor`] that will run commands and subscriptions.
- ///
- /// The [`executor::WasmBindgen`] can be a good choice for the Web.
- ///
- /// [`Executor`]: trait.Executor.html
- /// [`executor::Default`]: executor/struct.Default.html
- type Executor: Executor;
+ /// The data needed to initialize your [`Application`].
+ type Flags;
/// Initializes the [`Application`].
///
/// Here is where you should return the initial state of your app.
///
- /// Additionally, you can return a [`Command`](struct.Command.html) if you
- /// need to perform some async action in the background on startup. This is
- /// useful if you want to load state from a file, perform an initial HTTP
- /// request, etc.
- ///
- /// [`Application`]: trait.Application.html
- fn new() -> (Self, Command<Self::Message>)
+ /// Additionally, you can return a [`Command`] if you need to perform some
+ /// async action in the background on startup. This is useful if you want to
+ /// load state from a file, perform an initial HTTP request, etc.
+ fn new(flags: Self::Flags) -> (Self, Command<Self::Message>)
where
Self: Sized;
@@ -125,8 +117,6 @@ pub trait Application {
///
/// This title can be dynamic! The runtime will automatically update the
/// title of your application when necessary.
- ///
- /// [`Application`]: trait.Application.html
fn title(&self) -> String;
/// Handles a __message__ and updates the state of the [`Application`].
@@ -136,16 +126,11 @@ pub trait Application {
/// this method.
///
/// Any [`Command`] returned will be executed immediately in the background.
- ///
- /// [`Application`]: trait.Application.html
- /// [`Command`]: struct.Command.html
fn update(&mut self, message: Self::Message) -> Command<Self::Message>;
/// Returns the widgets to display in the [`Application`].
///
/// These widgets can produce __messages__ based on user interaction.
- ///
- /// [`Application`]: trait.Application.html
fn view(&mut self) -> Element<'_, Self::Message>;
/// Returns the event [`Subscription`] for the current state of the
@@ -156,30 +141,21 @@ pub trait Application {
/// [`update`](#tymethod.update).
///
/// By default, this method returns an empty [`Subscription`].
- ///
- /// [`Subscription`]: struct.Subscription.html
fn subscription(&self) -> Subscription<Self::Message> {
Subscription::none()
}
/// Runs the [`Application`].
- ///
- /// [`Application`]: trait.Application.html
- fn run()
+ fn run(flags: Self::Flags)
where
Self: 'static + Sized,
{
use futures::stream::StreamExt;
- let (app, command) = Self::new();
-
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().unwrap();
- let mut title = app.title();
- document.set_title(&title);
-
let (sender, receiver) =
iced_futures::futures::channel::mpsc::unbounded();
@@ -187,6 +163,12 @@ pub trait Application {
Self::Executor::new().expect("Create executor"),
sender.clone(),
);
+
+ let (app, command) = runtime.enter(|| Self::new(flags));
+
+ let mut title = app.title();
+ document.set_title(&title);
+
runtime.spawn(command);
let application = Rc::new(RefCell::new(app));
@@ -199,8 +181,13 @@ pub trait Application {
let vdom = dodrio::Vdom::new(&body, instance);
let event_loop = receiver.for_each(move |message| {
- let command = application.borrow_mut().update(message);
- let subscription = application.borrow().subscription();
+ let (command, subscription) = runtime.enter(|| {
+ let command = application.borrow_mut().update(message);
+ let subscription = application.borrow().subscription();
+
+ (command, subscription)
+ });
+
let new_title = application.borrow().title();
runtime.spawn(command);
@@ -226,28 +213,25 @@ struct Instance<A: Application> {
bus: Bus<A::Message>,
}
-impl<A> dodrio::Render for Instance<A>
+impl<'a, A> dodrio::Render<'a> for Instance<A>
where
A: Application,
{
- fn render<'a, 'bump>(
- &'a self,
- bump: &'bump bumpalo::Bump,
- ) -> dodrio::Node<'bump>
- where
- 'a: 'bump,
- {
+ fn render(
+ &self,
+ context: &mut dodrio::RenderContext<'a>,
+ ) -> dodrio::Node<'a> {
use dodrio::builder::*;
let mut ui = self.application.borrow_mut();
let element = ui.view();
let mut css = Css::new();
- let node = element.widget.node(bump, &self.bus, &mut css);
+ let node = element.widget.node(context.bump, &self.bus, &mut css);
- div(bump)
+ div(context.bump)
.attr("style", "width: 100%; height: 100%")
- .children(vec![css.node(bump), node])
+ .children(vec![css.node(context.bump), node])
.finish()
}
}
diff --git a/web/src/subscription.rs b/web/src/subscription.rs
index 6b8415c0..fb54f7e3 100644
--- a/web/src/subscription.rs
+++ b/web/src/subscription.rs
@@ -12,8 +12,7 @@ use crate::Hasher;
/// For instance, you can use a [`Subscription`] to listen to a WebSocket
/// connection, keyboard presses, mouse events, time ticks, etc.
///
-/// [`Command`]: ../struct.Command.html
-/// [`Subscription`]: struct.Subscription.html
+/// [`Command`]: crate::Command
pub type Subscription<T> = iced_futures::Subscription<Hasher, (), T>;
pub use iced_futures::subscription::Recipe;
diff --git a/web/src/widget.rs b/web/src/widget.rs
index 025cf22f..023f5f13 100644
--- a/web/src/widget.rs
+++ b/web/src/widget.rs
@@ -12,8 +12,6 @@
//! ```
//! use iced_web::{button, Button, Widget};
//! ```
-//!
-//! [`Widget`]: trait.Widget.html
use crate::{Bus, Css};
use dodrio::bumpalo;
@@ -56,12 +54,8 @@ pub use space::Space;
///
/// If you want to build your own widgets, you will need to implement this
/// trait.
-///
-/// [`Widget`]: trait.Widget.html
pub trait Widget<Message> {
/// Produces a VDOM node for the [`Widget`].
- ///
- /// [`Widget`]: trait.Widget.html
fn node<'b>(
&self,
bump: &'b bumpalo::Bump,
diff --git a/web/src/widget/button.rs b/web/src/widget/button.rs
index 3a5afe60..e7cff6a0 100644
--- a/web/src/widget/button.rs
+++ b/web/src/widget/button.rs
@@ -1,9 +1,6 @@
//! Allow your users to perform actions by pressing a button.
//!
//! A [`Button`] has some local [`State`].
-//!
-//! [`Button`]: struct.Button.html
-//! [`State`]: struct.State.html
use crate::{css, Background, Bus, Css, Element, Length, Widget};
pub use iced_style::button::{Style, StyleSheet};
@@ -38,9 +35,6 @@ pub struct Button<'a, Message> {
impl<'a, Message> Button<'a, Message> {
/// Creates a new [`Button`] with some local [`State`] and the given
/// content.
- ///
- /// [`Button`]: struct.Button.html
- /// [`State`]: struct.State.html
pub fn new<E>(_state: &'a mut State, content: E) -> Self
where
E: Into<Element<'a, Message>>,
@@ -58,56 +52,42 @@ impl<'a, Message> Button<'a, Message> {
}
/// Sets the width of the [`Button`].
- ///
- /// [`Button`]: struct.Button.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`Button`].
- ///
- /// [`Button`]: struct.Button.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
/// Sets the minimum width of the [`Button`].
- ///
- /// [`Button`]: struct.Button.html
pub fn min_width(mut self, min_width: u32) -> Self {
self.min_width = min_width;
self
}
/// Sets the minimum height of the [`Button`].
- ///
- /// [`Button`]: struct.Button.html
pub fn min_height(mut self, min_height: u32) -> Self {
self.min_height = min_height;
self
}
/// Sets the padding of the [`Button`].
- ///
- /// [`Button`]: struct.Button.html
pub fn padding(mut self, padding: u16) -> Self {
self.padding = padding;
self
}
/// Sets the style of the [`Button`].
- ///
- /// [`Button`]: struct.Button.html
pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self {
self.style = style.into();
self
}
/// Sets the message that will be produced when the [`Button`] is pressed.
- ///
- /// [`Button`]: struct.Button.html
pub fn on_press(mut self, msg: Message) -> Self {
self.on_press = Some(msg);
self
@@ -115,15 +95,11 @@ impl<'a, Message> Button<'a, Message> {
}
/// The local state of a [`Button`].
-///
-/// [`Button`]: struct.Button.html
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct State;
impl State {
/// Creates a new [`State`].
- ///
- /// [`State`]: struct.State.html
pub fn new() -> State {
State::default()
}
@@ -154,16 +130,20 @@ 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",
- bumpalo::format!(in bump, "{}", padding_class).into_bump_str(),
- )
+ .attr("class", class)
.attr(
"style",
bumpalo::format!(
in bump,
- "background: {}; border-radius: {}px; width:{}; min-width: {}; color: {}",
+ "background: {}; border-radius: {}px; width:{}; \
+ min-width: {}; color: {}",
background,
style.border_radius,
css::length(self.width),
diff --git a/web/src/widget/checkbox.rs b/web/src/widget/checkbox.rs
index 0657ccfb..543af99a 100644
--- a/web/src/widget/checkbox.rs
+++ b/web/src/widget/checkbox.rs
@@ -28,6 +28,7 @@ pub struct Checkbox<Message> {
is_checked: bool,
on_toggle: Rc<dyn Fn(bool) -> Message>,
label: String,
+ id: Option<String>,
width: Length,
style: Box<dyn StyleSheet>,
}
@@ -41,36 +42,37 @@ impl<Message> Checkbox<Message> {
/// * a function that will be called when the [`Checkbox`] is toggled. It
/// will receive the new state of the [`Checkbox`] and must produce a
/// `Message`.
- ///
- /// [`Checkbox`]: struct.Checkbox.html
- pub fn new<F>(is_checked: bool, label: &str, f: F) -> Self
+ pub fn new<F>(is_checked: bool, label: impl Into<String>, f: F) -> Self
where
F: 'static + Fn(bool) -> Message,
{
Checkbox {
is_checked,
on_toggle: Rc::new(f),
- label: String::from(label),
+ label: label.into(),
+ id: None,
width: Length::Shrink,
style: Default::default(),
}
}
/// Sets the width of the [`Checkbox`].
- ///
- /// [`Checkbox`]: struct.Checkbox.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the style of the [`Checkbox`].
- ///
- /// [`Checkbox`]: struct.Checkbox.html
pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self {
self.style = style.into();
self
}
+
+ /// Sets the id of the [`Checkbox`].
+ pub fn id(mut self, id: impl Into<String>) -> Self {
+ self.id = Some(id.into());
+ self
+ }
}
impl<Message> Widget<Message> for Checkbox<Message>
@@ -84,8 +86,10 @@ where
style_sheet: &mut Css<'b>,
) -> dodrio::Node<'b> {
use dodrio::builder::*;
+ use dodrio::bumpalo::collections::String;
- let checkbox_label = bumpalo::format!(in bump, "{}", self.label);
+ let checkbox_label =
+ String::from_str_in(&self.label, bump).into_bump_str();
let event_bus = bus.clone();
let on_toggle = self.on_toggle.clone();
@@ -95,7 +99,15 @@ where
let spacing_class = style_sheet.insert(bump, css::Rule::Spacing(5));
- label(bump)
+ 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))
+ };
+
+ label
.attr(
"class",
bumpalo::format!(in bump, "{} {}", row_class, spacing_class)
@@ -108,7 +120,7 @@ where
)
.children(vec![
// TODO: Checkbox styling
- input(bump)
+ input
.attr("type", "checkbox")
.bool_attr("checked", self.is_checked)
.on("click", move |_root, vdom, _event| {
@@ -118,8 +130,7 @@ where
vdom.schedule_render();
})
.finish(),
- span(bump).children(vec![
- text(checkbox_label.into_bump_str())]).finish(),
+ text(checkbox_label),
])
.finish()
}
diff --git a/web/src/widget/column.rs b/web/src/widget/column.rs
index 25b88b0e..d832fdcb 100644
--- a/web/src/widget/column.rs
+++ b/web/src/widget/column.rs
@@ -6,8 +6,6 @@ use std::u32;
/// A container that distributes its contents vertically.
///
/// A [`Column`] will try to fill the horizontal space of its container.
-///
-/// [`Column`]: struct.Column.html
#[allow(missing_debug_implementations)]
pub struct Column<'a, Message> {
spacing: u16,
@@ -22,15 +20,11 @@ pub struct Column<'a, Message> {
impl<'a, Message> Column<'a, Message> {
/// Creates an empty [`Column`].
- ///
- /// [`Column`]: struct.Column.html
pub fn new() -> Self {
Self::with_children(Vec::new())
}
/// Creates a [`Column`] with the given elements.
- ///
- /// [`Column`]: struct.Column.html
pub fn with_children(children: Vec<Element<'a, Message>>) -> Self {
Column {
spacing: 0,
@@ -55,56 +49,42 @@ impl<'a, Message> Column<'a, Message> {
}
/// Sets the padding of the [`Column`].
- ///
- /// [`Column`]: struct.Column.html
pub fn padding(mut self, units: u16) -> Self {
self.padding = units;
self
}
/// Sets the width of the [`Column`].
- ///
- /// [`Column`]: struct.Column.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`Column`].
- ///
- /// [`Column`]: struct.Column.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
/// Sets the maximum width of the [`Column`].
- ///
- /// [`Column`]: struct.Column.html
pub fn max_width(mut self, max_width: u32) -> Self {
self.max_width = max_width;
self
}
/// Sets the maximum height of the [`Column`] in pixels.
- ///
- /// [`Column`]: struct.Column.html
pub fn max_height(mut self, max_height: u32) -> Self {
self.max_height = max_height;
self
}
/// Sets the horizontal alignment of the contents of the [`Column`] .
- ///
- /// [`Column`]: struct.Column.html
pub fn align_items(mut self, align: Align) -> Self {
self.align_items = align;
self
}
/// Adds an element to the [`Column`].
- ///
- /// [`Column`]: struct.Column.html
pub fn push<E>(mut self, child: E) -> Self
where
E: Into<Element<'a, Message>>,
diff --git a/web/src/widget/container.rs b/web/src/widget/container.rs
index 8e4318f9..7187a4f0 100644
--- a/web/src/widget/container.rs
+++ b/web/src/widget/container.rs
@@ -8,6 +8,7 @@ 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,
width: Length,
height: Length,
max_width: u32,
@@ -20,8 +21,6 @@ pub struct Container<'a, Message> {
impl<'a, Message> Container<'a, Message> {
/// Creates an empty [`Container`].
- ///
- /// [`Container`]: struct.Container.html
pub fn new<T>(content: T) -> Self
where
T: Into<Element<'a, Message>>,
@@ -29,6 +28,7 @@ impl<'a, Message> Container<'a, Message> {
use std::u32;
Container {
+ padding: 0,
width: Length::Shrink,
height: Length::Shrink,
max_width: u32::MAX,
@@ -40,41 +40,37 @@ impl<'a, Message> Container<'a, Message> {
}
}
+ /// Sets the padding of the [`Container`].
+ pub fn padding(mut self, units: u16) -> Self {
+ self.padding = units;
+ self
+ }
+
/// Sets the width of the [`Container`].
- ///
- /// [`Container`]: struct.Container.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`Container`].
- ///
- /// [`Container`]: struct.Container.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
/// Sets the maximum width of the [`Container`].
- ///
- /// [`Container`]: struct.Container.html
pub fn max_width(mut self, max_width: u32) -> Self {
self.max_width = max_width;
self
}
/// Sets the maximum height of the [`Container`] in pixels.
- ///
- /// [`Container`]: struct.Container.html
pub fn max_height(mut self, max_height: u32) -> Self {
self.max_height = max_height;
self
}
/// Centers the contents in the horizontal axis of the [`Container`].
- ///
- /// [`Container`]: struct.Container.html
pub fn center_x(mut self) -> Self {
self.horizontal_alignment = Align::Center;
@@ -82,8 +78,6 @@ impl<'a, Message> Container<'a, Message> {
}
/// Centers the contents in the vertical axis of the [`Container`].
- ///
- /// [`Container`]: struct.Container.html
pub fn center_y(mut self) -> Self {
self.vertical_alignment = Align::Center;
@@ -91,8 +85,6 @@ impl<'a, Message> Container<'a, Message> {
}
/// Sets the style of the [`Container`].
- ///
- /// [`Container`]: struct.Container.html
pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self {
self.style_sheet = style.into();
self
@@ -113,12 +105,15 @@ 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).into_bump_str(),
+ bumpalo::format!(in bump, "{} {}", column_class, padding_class).into_bump_str(),
)
.attr(
"style",
diff --git a/web/src/widget/image.rs b/web/src/widget/image.rs
index 029ab352..05c89ea5 100644
--- a/web/src/widget/image.rs
+++ b/web/src/widget/image.rs
@@ -22,6 +22,9 @@ pub struct Image {
/// The image path
pub handle: Handle,
+ /// The alt text of the image
+ pub alt: String,
+
/// The width of the image
pub width: Length,
@@ -31,31 +34,32 @@ pub struct Image {
impl Image {
/// Creates a new [`Image`] with the given path.
- ///
- /// [`Image`]: struct.Image.html
pub fn new<T: Into<Handle>>(handle: T) -> Self {
Image {
handle: handle.into(),
+ alt: Default::default(),
width: Length::Shrink,
height: Length::Shrink,
}
}
/// Sets the width of the [`Image`] boundaries.
- ///
- /// [`Image`]: struct.Image.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`Image`] boundaries.
- ///
- /// [`Image`]: struct.Image.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
+
+ /// Sets the alt text of the [`Image`].
+ pub fn alt(mut self, alt: impl Into<String>) -> Self {
+ self.alt = alt.into();
+ self
+ }
}
impl<Message> Widget<Message> for Image {
@@ -66,12 +70,19 @@ impl<Message> Widget<Message> for Image {
_style_sheet: &mut Css<'b>,
) -> dodrio::Node<'b> {
use dodrio::builder::*;
+ use dodrio::bumpalo::collections::String;
- let src = bumpalo::format!(in bump, "{}", match self.handle.data.as_ref() {
- Data::Path(path) => path.to_str().unwrap_or("")
- });
+ let src = String::from_str_in(
+ match self.handle.data.as_ref() {
+ Data::Path(path) => path.to_str().unwrap_or(""),
+ },
+ bump,
+ )
+ .into_bump_str();
- let mut image = img(bump).attr("src", src.into_bump_str());
+ let alt = String::from_str_in(&self.alt, bump).into_bump_str();
+
+ let mut image = img(bump).attr("src", src).attr("alt", alt);
match self.width {
Length::Shrink => {}
@@ -99,8 +110,6 @@ impl<'a, Message> From<Image> for Element<'a, Message> {
}
/// An [`Image`] handle.
-///
-/// [`Image`]: struct.Image.html
#[derive(Debug, Clone)]
pub struct Handle {
id: u64,
@@ -109,8 +118,6 @@ pub struct Handle {
impl Handle {
/// Creates an image [`Handle`] pointing to the image of the given path.
- ///
- /// [`Handle`]: struct.Handle.html
pub fn from_path<T: Into<PathBuf>>(path: T) -> Handle {
Self::from_data(Data::Path(path.into()))
}
@@ -126,15 +133,11 @@ impl Handle {
}
/// Returns the unique identifier of the [`Handle`].
- ///
- /// [`Handle`]: struct.Handle.html
pub fn id(&self) -> u64 {
self.id
}
/// Returns a reference to the image [`Data`].
- ///
- /// [`Data`]: enum.Data.html
pub fn data(&self) -> &Data {
&self.data
}
@@ -153,8 +156,6 @@ impl From<&str> for Handle {
}
/// The data of an [`Image`].
-///
-/// [`Image`]: struct.Image.html
#[derive(Clone, Hash)]
pub enum Data {
/// A remote image
diff --git a/web/src/widget/progress_bar.rs b/web/src/widget/progress_bar.rs
index 856203c0..7d77616e 100644
--- a/web/src/widget/progress_bar.rs
+++ b/web/src/widget/progress_bar.rs
@@ -32,8 +32,6 @@ impl ProgressBar {
/// It expects:
/// * an inclusive range of possible values
/// * the current value of the [`ProgressBar`]
- ///
- /// [`ProgressBar`]: struct.ProgressBar.html
pub fn new(range: RangeInclusive<f32>, value: f32) -> Self {
ProgressBar {
value: value.max(*range.start()).min(*range.end()),
@@ -45,24 +43,18 @@ impl ProgressBar {
}
/// Sets the width of the [`ProgressBar`].
- ///
- /// [`ProgressBar`]: struct.ProgressBar.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`ProgressBar`].
- ///
- /// [`ProgressBar`]: struct.ProgressBar.html
pub fn height(mut self, height: Length) -> Self {
self.height = Some(height);
self
}
/// Sets the style of the [`ProgressBar`].
- ///
- /// [`ProgressBar`]: struct.ProgressBar.html
pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self {
self.style = style.into();
self
diff --git a/web/src/widget/radio.rs b/web/src/widget/radio.rs
index e00e26db..5a9bc379 100644
--- a/web/src/widget/radio.rs
+++ b/web/src/widget/radio.rs
@@ -35,6 +35,8 @@ pub struct Radio<Message> {
is_selected: bool,
on_click: Message,
label: String,
+ id: Option<String>,
+ name: Option<String>,
style: Box<dyn StyleSheet>,
}
@@ -47,9 +49,12 @@ impl<Message> Radio<Message> {
/// * the current selected value
/// * a function that will be called when the [`Radio`] is selected. It
/// receives the value of the radio and must produce a `Message`.
- ///
- /// [`Radio`]: struct.Radio.html
- pub fn new<F, V>(value: V, label: &str, selected: Option<V>, f: F) -> Self
+ pub fn new<F, V>(
+ value: V,
+ label: impl Into<String>,
+ selected: Option<V>,
+ f: F,
+ ) -> Self
where
V: Eq + Copy,
F: 'static + Fn(V) -> Message,
@@ -57,18 +62,30 @@ impl<Message> Radio<Message> {
Radio {
is_selected: Some(value) == selected,
on_click: f(value),
- label: String::from(label),
+ label: label.into(),
+ id: None,
+ name: None,
style: Default::default(),
}
}
/// Sets the style of the [`Radio`] button.
- ///
- /// [`Radio`]: struct.Radio.html
pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self {
self.style = style.into();
self
}
+
+ /// Sets the name attribute of the [`Radio`] button.
+ pub fn name(mut self, name: impl Into<String>) -> Self {
+ self.name = Some(name.into());
+ self
+ }
+
+ /// Sets the id of the [`Radio`] button.
+ pub fn id(mut self, id: impl Into<String>) -> Self {
+ self.id = Some(id.into());
+ self
+ }
}
impl<Message> Widget<Message> for Radio<Message>
@@ -82,17 +99,35 @@ where
_style_sheet: &mut Css<'b>,
) -> dodrio::Node<'b> {
use dodrio::builder::*;
+ use dodrio::bumpalo::collections::String;
- let radio_label = bumpalo::format!(in bump, "{}", self.label);
+ let radio_label =
+ String::from_str_in(&self.label, bump).into_bump_str();
let event_bus = bus.clone();
let on_click = self.on_click.clone();
+ 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 input = if let Some(name) = &self.name {
+ let name = String::from_str_in(name, bump).into_bump_str();
+
+ dodrio::builder::input(bump).attr("name", name)
+ } else {
+ input
+ };
+
// TODO: Complete styling
- label(bump)
+ label
.attr("style", "display: block; font-size: 20px")
.children(vec![
- input(bump)
+ input
.attr("type", "radio")
.attr("style", "margin-right: 10px")
.bool_attr("checked", self.is_selected)
@@ -100,7 +135,7 @@ where
event_bus.publish(on_click.clone());
})
.finish(),
- text(radio_label.into_bump_str()),
+ text(radio_label),
])
.finish()
}
diff --git a/web/src/widget/row.rs b/web/src/widget/row.rs
index cfa10fdf..f00a544a 100644
--- a/web/src/widget/row.rs
+++ b/web/src/widget/row.rs
@@ -6,8 +6,6 @@ use std::u32;
/// A container that distributes its contents horizontally.
///
/// A [`Row`] will try to fill the horizontal space of its container.
-///
-/// [`Row`]: struct.Row.html
#[allow(missing_debug_implementations)]
pub struct Row<'a, Message> {
spacing: u16,
@@ -22,15 +20,11 @@ pub struct Row<'a, Message> {
impl<'a, Message> Row<'a, Message> {
/// Creates an empty [`Row`].
- ///
- /// [`Row`]: struct.Row.html
pub fn new() -> Self {
Self::with_children(Vec::new())
}
/// Creates a [`Row`] with the given elements.
- ///
- /// [`Row`]: struct.Row.html
pub fn with_children(children: Vec<Element<'a, Message>>) -> Self {
Row {
spacing: 0,
@@ -55,57 +49,42 @@ impl<'a, Message> Row<'a, Message> {
}
/// Sets the padding of the [`Row`].
- ///
- /// [`Row`]: struct.Row.html
pub fn padding(mut self, units: u16) -> Self {
self.padding = units;
self
}
/// Sets the width of the [`Row`].
- ///
- /// [`Row`]: struct.Row.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`Row`].
- ///
- /// [`Row`]: struct.Row.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
/// Sets the maximum width of the [`Row`].
- ///
- /// [`Row`]: struct.Row.html
pub fn max_width(mut self, max_width: u32) -> Self {
self.max_width = max_width;
self
}
/// Sets the maximum height of the [`Row`].
- ///
- /// [`Row`]: struct.Row.html
pub fn max_height(mut self, max_height: u32) -> Self {
self.max_height = max_height;
self
}
/// Sets the vertical alignment of the contents of the [`Row`] .
- ///
- /// [`Row`]: struct.Row.html
pub fn align_items(mut self, align: Align) -> Self {
self.align_items = align;
self
}
/// Adds an [`Element`] to the [`Row`].
- ///
- /// [`Element`]: ../struct.Element.html
- /// [`Row`]: struct.Row.html
pub fn push<E>(mut self, child: E) -> Self
where
E: Into<Element<'a, Message>>,
diff --git a/web/src/widget/scrollable.rs b/web/src/widget/scrollable.rs
index 07b38aad..f9135dd6 100644
--- a/web/src/widget/scrollable.rs
+++ b/web/src/widget/scrollable.rs
@@ -16,9 +16,6 @@ pub struct Scrollable<'a, Message> {
impl<'a, Message> Scrollable<'a, Message> {
/// Creates a new [`Scrollable`] with the given [`State`].
- ///
- /// [`Scrollable`]: struct.Scrollable.html
- /// [`State`]: struct.State.html
pub fn new(_state: &'a mut State) -> Self {
use std::u32;
@@ -42,64 +39,48 @@ impl<'a, Message> Scrollable<'a, Message> {
}
/// Sets the padding of the [`Scrollable`].
- ///
- /// [`Scrollable`]: struct.Scrollable.html
pub fn padding(mut self, units: u16) -> Self {
self.content = self.content.padding(units);
self
}
/// Sets the width of the [`Scrollable`].
- ///
- /// [`Scrollable`]: struct.Scrollable.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`Scrollable`].
- ///
- /// [`Scrollable`]: struct.Scrollable.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
/// Sets the maximum width of the [`Scrollable`].
- ///
- /// [`Scrollable`]: struct.Scrollable.html
pub fn max_width(mut self, max_width: u32) -> Self {
self.content = self.content.max_width(max_width);
self
}
/// Sets the maximum height of the [`Scrollable`] in pixels.
- ///
- /// [`Scrollable`]: struct.Scrollable.html
pub fn max_height(mut self, max_height: u32) -> Self {
self.max_height = max_height;
self
}
/// Sets the horizontal alignment of the contents of the [`Scrollable`] .
- ///
- /// [`Scrollable`]: struct.Scrollable.html
pub fn align_items(mut self, align_items: Align) -> Self {
self.content = self.content.align_items(align_items);
self
}
/// Sets the style of the [`Scrollable`] .
- ///
- /// [`Scrollable`]: struct.Scrollable.html
pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self {
self.style = style.into();
self
}
/// Adds an element to the [`Scrollable`].
- ///
- /// [`Scrollable`]: struct.Scrollable.html
pub fn push<E>(mut self, child: E) -> Self
where
E: Into<Element<'a, Message>>,
@@ -154,15 +135,11 @@ where
}
/// The local state of a [`Scrollable`].
-///
-/// [`Scrollable`]: struct.Scrollable.html
#[derive(Debug, Clone, Copy, Default)]
pub struct State;
impl State {
/// Creates a new [`State`] with the scrollbar located at the top.
- ///
- /// [`State`]: struct.State.html
pub fn new() -> Self {
State::default()
}
diff --git a/web/src/widget/slider.rs b/web/src/widget/slider.rs
index 5aa6439e..91a4d2ec 100644
--- a/web/src/widget/slider.rs
+++ b/web/src/widget/slider.rs
@@ -1,9 +1,6 @@
//! Display an interactive selector of a single value from a range of values.
//!
//! A [`Slider`] has some local [`State`].
-//!
-//! [`Slider`]: struct.Slider.html
-//! [`State`]: struct.State.html
use crate::{Bus, Css, Element, Length, Widget};
pub use iced_style::slider::{Handle, HandleShape, Style, StyleSheet};
@@ -16,7 +13,8 @@ use std::{ops::RangeInclusive, rc::Rc};
///
/// A [`Slider`] will try to fill the horizontal space of its container.
///
-/// [`Slider`]: struct.Slider.html
+/// The [`Slider`] range of numeric values is generic and its step size defaults
+/// to 1 unit.
///
/// # Example
/// ```
@@ -34,16 +32,20 @@ use std::{ops::RangeInclusive, rc::Rc};
///
/// ![Slider drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/slider.png?raw=true)
#[allow(missing_debug_implementations)]
-pub struct Slider<'a, Message> {
+pub struct Slider<'a, T, Message> {
_state: &'a mut State,
- range: RangeInclusive<f32>,
- value: f32,
- on_change: Rc<Box<dyn Fn(f32) -> Message>>,
+ range: RangeInclusive<T>,
+ step: T,
+ value: T,
+ on_change: Rc<Box<dyn Fn(T) -> Message>>,
width: Length,
style: Box<dyn StyleSheet>,
}
-impl<'a, Message> Slider<'a, Message> {
+impl<'a, T, Message> Slider<'a, T, Message>
+where
+ T: Copy + From<u8> + std::cmp::PartialOrd,
+{
/// Creates a new [`Slider`].
///
/// It expects:
@@ -53,22 +55,32 @@ impl<'a, Message> Slider<'a, Message> {
/// * a function that will be called when the [`Slider`] is dragged.
/// It receives the new value of the [`Slider`] and must produce a
/// `Message`.
- ///
- /// [`Slider`]: struct.Slider.html
- /// [`State`]: struct.State.html
pub fn new<F>(
state: &'a mut State,
- range: RangeInclusive<f32>,
- value: f32,
+ range: RangeInclusive<T>,
+ value: T,
on_change: F,
) -> Self
where
- F: 'static + Fn(f32) -> Message,
+ F: 'static + Fn(T) -> Message,
{
+ let value = if value >= *range.start() {
+ value
+ } else {
+ *range.start()
+ };
+
+ let value = if value <= *range.end() {
+ value
+ } else {
+ *range.end()
+ };
+
Slider {
_state: state,
- value: value.max(*range.start()).min(*range.end()),
+ value,
range,
+ step: T::from(1),
on_change: Rc::new(Box::new(on_change)),
width: Length::Fill,
style: Default::default(),
@@ -76,24 +88,27 @@ impl<'a, Message> Slider<'a, Message> {
}
/// Sets the width of the [`Slider`].
- ///
- /// [`Slider`]: struct.Slider.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the style of the [`Slider`].
- ///
- /// [`Slider`]: struct.Slider.html
pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self {
self.style = style.into();
self
}
+
+ /// Sets the step size of the [`Slider`].
+ pub fn step(mut self, step: T) -> Self {
+ self.step = step;
+ self
+ }
}
-impl<'a, Message> Widget<Message> for Slider<'a, Message>
+impl<'a, T, Message> Widget<Message> for Slider<'a, T, Message>
where
+ T: 'static + Copy + Into<f64> + num_traits::FromPrimitive,
Message: 'static,
{
fn node<'b>(
@@ -107,18 +122,18 @@ where
let (start, end) = self.range.clone().into_inner();
- let min = bumpalo::format!(in bump, "{}", start);
- let max = bumpalo::format!(in bump, "{}", end);
- let value = bumpalo::format!(in bump, "{}", self.value);
+ let min = bumpalo::format!(in bump, "{}", start.into());
+ let max = bumpalo::format!(in bump, "{}", end.into());
+ let value = bumpalo::format!(in bump, "{}", self.value.into());
+ let step = bumpalo::format!(in bump, "{}", self.step.into());
let on_change = self.on_change.clone();
let event_bus = bus.clone();
- // TODO: Make `step` configurable
// TODO: Styling
input(bump)
.attr("type", "range")
- .attr("step", "0.01")
+ .attr("step", step.into_bump_str())
.attr("min", min.into_bump_str())
.attr("max", max.into_bump_str())
.attr("value", value.into_bump_str())
@@ -131,33 +146,32 @@ where
Some(slider) => slider,
};
- if let Ok(value) = slider.value().parse::<f32>() {
- event_bus.publish(on_change(value));
+ if let Ok(value) = slider.value().parse::<f64>() {
+ if let Some(value) = T::from_f64(value) {
+ event_bus.publish(on_change(value));
+ }
}
})
.finish()
}
}
-impl<'a, Message> From<Slider<'a, Message>> for Element<'a, Message>
+impl<'a, T, Message> From<Slider<'a, T, Message>> for Element<'a, Message>
where
+ T: 'static + Copy + Into<f64> + num_traits::FromPrimitive,
Message: 'static,
{
- fn from(slider: Slider<'a, Message>) -> Element<'a, Message> {
+ fn from(slider: Slider<'a, T, Message>) -> Element<'a, Message> {
Element::new(slider)
}
}
/// The local state of a [`Slider`].
-///
-/// [`Slider`]: struct.Slider.html
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct State;
impl State {
/// Creates a new [`State`].
- ///
- /// [`State`]: struct.State.html
pub fn new() -> Self {
Self
}
diff --git a/web/src/widget/space.rs b/web/src/widget/space.rs
index 4ce52595..a8571fdb 100644
--- a/web/src/widget/space.rs
+++ b/web/src/widget/space.rs
@@ -12,15 +12,11 @@ pub struct Space {
impl Space {
/// Creates an amount of empty [`Space`] with the given width and height.
- ///
- /// [`Space`]: struct.Space.html
pub fn new(width: Length, height: Length) -> Self {
Space { width, height }
}
/// Creates an amount of horizontal [`Space`].
- ///
- /// [`Space`]: struct.Space.html
pub fn with_width(width: Length) -> Self {
Space {
width,
@@ -29,8 +25,6 @@ impl Space {
}
/// Creates an amount of vertical [`Space`].
- ///
- /// [`Space`]: struct.Space.html
pub fn with_height(height: Length) -> Self {
Space {
width: Length::Shrink,
diff --git a/web/src/widget/text.rs b/web/src/widget/text.rs
index 3ec565a8..72232dc0 100644
--- a/web/src/widget/text.rs
+++ b/web/src/widget/text.rs
@@ -28,8 +28,6 @@ pub struct Text {
impl Text {
/// Create a new fragment of [`Text`] with the given contents.
- ///
- /// [`Text`]: struct.Text.html
pub fn new<T: Into<String>>(label: T) -> Self {
Text {
content: label.into(),
@@ -44,51 +42,36 @@ impl Text {
}
/// Sets the size of the [`Text`].
- ///
- /// [`Text`]: struct.Text.html
pub fn size(mut self, size: u16) -> Self {
self.size = Some(size);
self
}
/// Sets the [`Color`] of the [`Text`].
- ///
- /// [`Text`]: struct.Text.html
- /// [`Color`]: ../../struct.Color.html
pub fn color<C: Into<Color>>(mut self, color: C) -> Self {
self.color = Some(color.into());
self
}
/// Sets the [`Font`] of the [`Text`].
- ///
- /// [`Text`]: struct.Text.html
- /// [`Font`]: ../../struct.Font.html
pub fn font(mut self, font: Font) -> Self {
self.font = font;
self
}
/// Sets the width of the [`Text`] boundaries.
- ///
- /// [`Text`]: struct.Text.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the height of the [`Text`] boundaries.
- ///
- /// [`Text`]: struct.Text.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
/// Sets the [`HorizontalAlignment`] of the [`Text`].
- ///
- /// [`Text`]: struct.Text.html
- /// [`HorizontalAlignment`]: enum.HorizontalAlignment.html
pub fn horizontal_alignment(
mut self,
alignment: HorizontalAlignment,
@@ -98,9 +81,6 @@ impl Text {
}
/// Sets the [`VerticalAlignment`] of the [`Text`].
- ///
- /// [`Text`]: struct.Text.html
- /// [`VerticalAlignment`]: enum.VerticalAlignment.html
pub fn vertical_alignment(mut self, alignment: VerticalAlignment) -> Self {
self.vertical_alignment = alignment;
self
@@ -116,7 +96,12 @@ impl<'a, Message> Widget<Message> for Text {
) -> dodrio::Node<'b> {
use dodrio::builder::*;
- let content = bumpalo::format!(in bump, "{}", self.content);
+ let content = {
+ use dodrio::bumpalo::collections::String;
+
+ String::from_str_in(&self.content, bump)
+ };
+
let color = self
.color
.map(css::color)
@@ -133,7 +118,8 @@ impl<'a, Message> Widget<Message> for Text {
let style = bumpalo::format!(
in bump,
- "width: {}; height: {}; font-size: {}px; color: {}; text-align: {}; font-family: {}",
+ "width: {}; height: {}; font-size: {}px; color: {}; \
+ text-align: {}; font-family: {}",
width,
height,
self.size.unwrap_or(20),
diff --git a/web/src/widget/text_input.rs b/web/src/widget/text_input.rs
index 3fa458bd..bc2048a8 100644
--- a/web/src/widget/text_input.rs
+++ b/web/src/widget/text_input.rs
@@ -1,9 +1,6 @@
//! Display fields that can be filled with text.
//!
//! A [`TextInput`] has some local [`State`].
-//!
-//! [`TextInput`]: struct.TextInput.html
-//! [`State`]: struct.State.html
use crate::{bumpalo, css, Bus, Css, Element, Length, Widget};
pub use iced_style::text_input::{Style, StyleSheet};
@@ -53,9 +50,6 @@ impl<'a, Message> TextInput<'a, Message> {
/// - a placeholder
/// - the current value
/// - a function that produces a message when the [`TextInput`] changes
- ///
- /// [`TextInput`]: struct.TextInput.html
- /// [`State`]: struct.State.html
pub fn new<F>(
state: &'a mut State,
placeholder: &str,
@@ -81,40 +75,30 @@ impl<'a, Message> TextInput<'a, Message> {
}
/// Converts the [`TextInput`] into a secure password input.
- ///
- /// [`TextInput`]: struct.TextInput.html
pub fn password(mut self) -> Self {
self.is_secure = true;
self
}
/// Sets the width of the [`TextInput`].
- ///
- /// [`TextInput`]: struct.TextInput.html
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
/// Sets the maximum width of the [`TextInput`].
- ///
- /// [`TextInput`]: struct.TextInput.html
pub fn max_width(mut self, max_width: u32) -> Self {
self.max_width = max_width;
self
}
/// Sets the padding of the [`TextInput`].
- ///
- /// [`TextInput`]: struct.TextInput.html
pub fn padding(mut self, units: u16) -> Self {
self.padding = units;
self
}
/// Sets the text size of the [`TextInput`].
- ///
- /// [`TextInput`]: struct.TextInput.html
pub fn size(mut self, size: u16) -> Self {
self.size = Some(size);
self
@@ -122,16 +106,12 @@ impl<'a, Message> TextInput<'a, Message> {
/// Sets the message that should be produced when the [`TextInput`] is
/// focused and the enter key is pressed.
- ///
- /// [`TextInput`]: struct.TextInput.html
pub fn on_submit(mut self, message: Message) -> Self {
self.on_submit = Some(message);
self
}
/// Sets the style of the [`TextInput`].
- ///
- /// [`TextInput`]: struct.TextInput.html
pub fn style(mut self, style: impl Into<Box<dyn StyleSheet>>) -> Self {
self.style_sheet = style.into();
self
@@ -151,8 +131,26 @@ where
use dodrio::builder::*;
use wasm_bindgen::JsCast;
- let padding_class =
- style_sheet.insert(bump, css::Rule::Padding(self.padding));
+ 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;
+
+ String::from_str_in(&self.placeholder, bump).into_bump_str()
+ };
+
+ let value = {
+ use dodrio::bumpalo::collections::String;
+
+ String::from_str_in(&self.value, bump).into_bump_str()
+ };
let on_change = self.on_change.clone();
let on_submit = self.on_submit.clone();
@@ -161,15 +159,14 @@ where
let style = self.style_sheet.active();
input(bump)
- .attr(
- "class",
- bumpalo::format!(in bump, "{}", padding_class).into_bump_str(),
- )
+ .attr("class", class)
.attr(
"style",
bumpalo::format!(
in bump,
- "width: {}; max-width: {}; font-size: {}px; background: {}; border-width: {}px; border-color: {}; border-radius: {}px; color: {}",
+ "width: {}; max-width: {}; font-size: {}px; \
+ background: {}; border-width: {}px; border-color: {}; \
+ border-radius: {}px; color: {}",
css::length(self.width),
css::max_length(self.max_width),
self.size.unwrap_or(20),
@@ -181,19 +178,9 @@ where
)
.into_bump_str(),
)
- .attr(
- "placeholder",
- bumpalo::format!(in bump, "{}", self.placeholder)
- .into_bump_str(),
- )
- .attr(
- "value",
- bumpalo::format!(in bump, "{}", self.value).into_bump_str(),
- )
- .attr(
- "type",
- bumpalo::format!(in bump, "{}", if self.is_secure { "password" } else { "text" }).into_bump_str(),
- )
+ .attr("placeholder", placeholder)
+ .attr("value", value)
+ .attr("type", if self.is_secure { "password" } else { "text" })
.on("input", move |_root, _vdom, event| {
let text_input = match event.target().and_then(|t| {
t.dyn_into::<web_sys::HtmlInputElement>().ok()
@@ -206,10 +193,13 @@ where
})
.on("keypress", move |_root, _vdom, event| {
if let Some(on_submit) = on_submit.clone() {
- let event = event.unchecked_into::<web_sys::KeyboardEvent>();
+ let event =
+ event.unchecked_into::<web_sys::KeyboardEvent>();
match event.key_code() {
- 13 => { submit_event_bus.publish(on_submit); }
+ 13 => {
+ submit_event_bus.publish(on_submit);
+ }
_ => {}
}
}
@@ -228,22 +218,16 @@ where
}
/// The state of a [`TextInput`].
-///
-/// [`TextInput`]: struct.TextInput.html
#[derive(Debug, Clone, Copy, Default)]
pub struct State;
impl State {
/// Creates a new [`State`], representing an unfocused [`TextInput`].
- ///
- /// [`State`]: struct.State.html
pub fn new() -> Self {
Self::default()
}
/// Creates a new [`State`], representing a focused [`TextInput`].
- ///
- /// [`State`]: struct.State.html
pub fn focused() -> Self {
// TODO
Self::default()
diff --git a/wgpu/CHANGELOG.md b/wgpu/CHANGELOG.md
deleted file mode 100644
index f9708308..00000000
--- a/wgpu/CHANGELOG.md
+++ /dev/null
@@ -1,14 +0,0 @@
-# Changelog
-All notable changes to this project will be documented in this file.
-
-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.1.0] - 2019-11-25
-### Added
-- First release! :tada:
-
-[Unreleased]: https://github.com/hecrj/iced/compare/wgpu-0.1.0...HEAD
-[0.1.0]: https://github.com/hecrj/iced/releases/tag/wgpu-0.1.0
diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml
index 17dfb4a3..5f4699a8 100644
--- a/wgpu/Cargo.toml
+++ b/wgpu/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "iced_wgpu"
-version = "0.1.0"
+version = "0.3.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
description = "A wgpu renderer for Iced"
@@ -9,29 +9,41 @@ repository = "https://github.com/hecrj/iced"
[features]
svg = ["resvg"]
-canvas = ["lyon"]
+canvas = ["iced_graphics/canvas"]
+qr_code = ["iced_graphics/qr_code"]
+default_system_font = ["iced_graphics/font-source"]
[dependencies]
-iced_native = { version = "0.1.0", path = "../native" }
-iced_style = { version = "0.1.0-alpha", path = "../style" }
-wgpu = "0.4"
-wgpu_glyph = "0.7"
-glyph_brush = "0.6"
+wgpu = "0.6"
+wgpu_glyph = "0.10"
+glyph_brush = "0.7"
raw-window-handle = "0.3"
-glam = "0.8"
-font-kit = "0.5"
log = "0.4"
-guillotiere = "0.4"
+guillotiere = "0.6"
+futures = "0.3"
+
+[dependencies.bytemuck]
+version = "1.4"
+features = ["derive"]
+
+[dependencies.iced_native]
+version = "0.3"
+path = "../native"
+
+[dependencies.iced_graphics]
+version = "0.1"
+path = "../graphics"
+features = ["font-fallback", "font-icons"]
[dependencies.image]
-version = "0.22"
+version = "0.23"
optional = true
[dependencies.resvg]
-version = "0.8"
+version = "0.9"
features = ["raqote-backend"]
optional = true
-[dependencies.lyon]
-version = "0.15"
-optional = true
+[package.metadata.docs.rs]
+rustdoc-args = ["--cfg", "docsrs"]
+all-features = true
diff --git a/wgpu/README.md b/wgpu/README.md
index 38c6ddb6..e8cb0a43 100644
--- a/wgpu/README.md
+++ b/wgpu/README.md
@@ -11,10 +11,13 @@
Currently, `iced_wgpu` supports the following primitives:
- Text, which is rendered using [`wgpu_glyph`]. No shaping at all.
- Quads or rectangles, with rounded borders and a solid background color.
-- Images, lazily loaded from the filesystem.
- Clip areas, useful to implement scrollables or hide overflowing content.
+- Images and SVG, loaded from memory or the file system.
+- Meshes of triangles, useful to draw geometry freely.
-![iced_wgpu](../docs/graphs/wgpu.png)
+<p align="center">
+ <img alt="The native target" src="../docs/graphs/native.png" width="80%">
+</p>
[documentation]: https://docs.rs/iced_wgpu
[`iced_native`]: ../native
@@ -26,7 +29,7 @@ Currently, `iced_wgpu` supports the following primitives:
Add `iced_wgpu` as a dependency in your `Cargo.toml`:
```toml
-iced_wgpu = "0.1.0"
+iced_wgpu = "0.3"
```
__Iced moves fast and the `master` branch can contain breaking changes!__ If
diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs
new file mode 100644
index 00000000..fccb5ac7
--- /dev/null
+++ b/wgpu/src/backend.rs
@@ -0,0 +1,288 @@
+use crate::quad;
+use crate::text;
+use crate::triangle;
+use crate::{Settings, Transformation};
+use iced_graphics::backend;
+use iced_graphics::font;
+use iced_graphics::layer::Layer;
+use iced_graphics::{Primitive, Viewport};
+use iced_native::mouse;
+use iced_native::{Font, HorizontalAlignment, Size, VerticalAlignment};
+
+#[cfg(any(feature = "image", feature = "svg"))]
+use crate::image;
+
+/// A [`wgpu`] graphics backend for [`iced`].
+///
+/// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs
+/// [`iced`]: https://github.com/hecrj/iced
+#[derive(Debug)]
+pub struct Backend {
+ quad_pipeline: quad::Pipeline,
+ text_pipeline: text::Pipeline,
+ triangle_pipeline: triangle::Pipeline,
+
+ #[cfg(any(feature = "image", feature = "svg"))]
+ image_pipeline: image::Pipeline,
+
+ default_text_size: u16,
+}
+
+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 quad_pipeline = quad::Pipeline::new(device, settings.format);
+ let triangle_pipeline = triangle::Pipeline::new(
+ device,
+ settings.format,
+ settings.antialiasing,
+ );
+
+ #[cfg(any(feature = "image", feature = "svg"))]
+ let image_pipeline = image::Pipeline::new(device, settings.format);
+
+ Self {
+ quad_pipeline,
+ text_pipeline,
+ triangle_pipeline,
+
+ #[cfg(any(feature = "image", feature = "svg"))]
+ image_pipeline,
+
+ default_text_size: settings.default_text_size,
+ }
+ }
+
+ /// Draws the provided primitives in the given `TextureView`.
+ ///
+ /// The text provided as overlay will be rendered on top of the primitives.
+ /// This is useful for rendering debug information.
+ pub fn draw<T: AsRef<str>>(
+ &mut self,
+ device: &wgpu::Device,
+ staging_belt: &mut wgpu::util::StagingBelt,
+ encoder: &mut wgpu::CommandEncoder,
+ frame: &wgpu::TextureView,
+ viewport: &Viewport,
+ (primitive, mouse_interaction): &(Primitive, mouse::Interaction),
+ overlay_text: &[T],
+ ) -> mouse::Interaction {
+ log::debug!("Drawing");
+
+ let target_size = viewport.physical_size();
+ let scale_factor = viewport.scale_factor() as f32;
+ let transformation = viewport.projection();
+
+ let mut layers = Layer::generate(primitive, viewport);
+ layers.push(Layer::overlay(overlay_text, viewport));
+
+ for layer in layers {
+ self.flush(
+ device,
+ scale_factor,
+ transformation,
+ &layer,
+ staging_belt,
+ encoder,
+ &frame,
+ target_size.width,
+ target_size.height,
+ );
+ }
+
+ #[cfg(any(feature = "image", feature = "svg"))]
+ self.image_pipeline.trim_cache();
+
+ *mouse_interaction
+ }
+
+ fn flush(
+ &mut self,
+ device: &wgpu::Device,
+ scale_factor: f32,
+ transformation: Transformation,
+ layer: &Layer<'_>,
+ staging_belt: &mut wgpu::util::StagingBelt,
+ encoder: &mut wgpu::CommandEncoder,
+ target: &wgpu::TextureView,
+ target_width: u32,
+ target_height: u32,
+ ) {
+ let bounds = (layer.bounds * scale_factor).snap();
+
+ if !layer.quads.is_empty() {
+ self.quad_pipeline.draw(
+ device,
+ staging_belt,
+ encoder,
+ &layer.quads,
+ transformation,
+ scale_factor,
+ bounds,
+ target,
+ );
+ }
+
+ if !layer.meshes.is_empty() {
+ let scaled = transformation
+ * Transformation::scale(scale_factor, scale_factor);
+
+ self.triangle_pipeline.draw(
+ device,
+ staging_belt,
+ encoder,
+ target,
+ target_width,
+ target_height,
+ scaled,
+ scale_factor,
+ &layer.meshes,
+ );
+ }
+
+ #[cfg(any(feature = "image", feature = "svg"))]
+ {
+ if !layer.images.is_empty() {
+ let scaled = transformation
+ * Transformation::scale(scale_factor, scale_factor);
+
+ self.image_pipeline.draw(
+ device,
+ staging_belt,
+ encoder,
+ &layer.images,
+ scaled,
+ bounds,
+ target,
+ scale_factor,
+ );
+ }
+ }
+
+ if !layer.text.is_empty() {
+ for text in layer.text.iter() {
+ // Target physical coordinates directly to avoid blurry text
+ let text = wgpu_glyph::Section {
+ // TODO: We `round` here to avoid rerasterizing text when
+ // its position changes slightly. This can make text feel a
+ // bit "jumpy". We may be able to do better once we improve
+ // our text rendering/caching pipeline.
+ screen_position: (
+ (text.bounds.x * scale_factor).round(),
+ (text.bounds.y * scale_factor).round(),
+ ),
+ // TODO: Fix precision issues with some scale factors.
+ //
+ // The `ceil` here can cause some words to render on the
+ // same line when they should not.
+ //
+ // Ideally, `wgpu_glyph` should be able to compute layout
+ // using logical positions, and then apply the proper
+ // scaling when rendering. This would ensure that both
+ // measuring and rendering follow the same layout rules.
+ bounds: (
+ (text.bounds.width * scale_factor).ceil(),
+ (text.bounds.height * scale_factor).ceil(),
+ ),
+ text: vec![wgpu_glyph::Text {
+ text: text.content,
+ scale: wgpu_glyph::ab_glyph::PxScale {
+ x: text.size * scale_factor,
+ y: text.size * scale_factor,
+ },
+ font_id: self.text_pipeline.find_font(text.font),
+ extra: wgpu_glyph::Extra {
+ color: text.color,
+ z: 0.0,
+ },
+ }],
+ layout: wgpu_glyph::Layout::default()
+ .h_align(match text.horizontal_alignment {
+ HorizontalAlignment::Left => {
+ wgpu_glyph::HorizontalAlign::Left
+ }
+ HorizontalAlignment::Center => {
+ wgpu_glyph::HorizontalAlign::Center
+ }
+ HorizontalAlignment::Right => {
+ wgpu_glyph::HorizontalAlign::Right
+ }
+ })
+ .v_align(match text.vertical_alignment {
+ VerticalAlignment::Top => {
+ wgpu_glyph::VerticalAlign::Top
+ }
+ VerticalAlignment::Center => {
+ wgpu_glyph::VerticalAlign::Center
+ }
+ VerticalAlignment::Bottom => {
+ wgpu_glyph::VerticalAlign::Bottom
+ }
+ }),
+ ..Default::default()
+ };
+
+ self.text_pipeline.queue(text);
+ }
+
+ self.text_pipeline.draw_queued(
+ device,
+ staging_belt,
+ encoder,
+ target,
+ transformation,
+ wgpu_glyph::Region {
+ x: bounds.x,
+ y: bounds.y,
+ width: bounds.width,
+ height: bounds.height,
+ },
+ );
+ }
+ }
+}
+
+impl iced_graphics::Backend for Backend {
+ fn trim_measurements(&mut self) {
+ self.text_pipeline.trim_measurement_cache()
+ }
+}
+
+impl backend::Text for Backend {
+ const ICON_FONT: Font = font::ICONS;
+ const CHECKMARK_ICON: char = font::CHECKMARK_ICON;
+ const ARROW_DOWN_ICON: char = font::ARROW_DOWN_ICON;
+
+ fn default_size(&self) -> u16 {
+ self.default_text_size
+ }
+
+ fn measure(
+ &self,
+ contents: &str,
+ size: f32,
+ font: Font,
+ bounds: Size,
+ ) -> (f32, f32) {
+ self.text_pipeline.measure(contents, size, font, bounds)
+ }
+}
+
+#[cfg(feature = "image")]
+impl backend::Image for Backend {
+ fn dimensions(&self, handle: &iced_native::image::Handle) -> (u32, u32) {
+ self.image_pipeline.dimensions(handle)
+ }
+}
+
+#[cfg(feature = "svg")]
+impl backend::Svg for Backend {
+ fn viewport_dimensions(
+ &self,
+ handle: &iced_native::svg::Handle,
+ ) -> (u32, u32) {
+ self.image_pipeline.viewport_dimensions(handle)
+ }
+}
diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs
index d3603676..c256ca7e 100644
--- a/wgpu/src/image.rs
+++ b/wgpu/src/image.rs
@@ -9,10 +9,13 @@ mod vector;
use crate::Transformation;
use atlas::Atlas;
+use iced_graphics::layer;
use iced_native::Rectangle;
use std::cell::RefCell;
use std::mem;
+use bytemuck::{Pod, Zeroable};
+
#[cfg(feature = "image")]
use iced_native::image;
@@ -40,6 +43,8 @@ pub struct Pipeline {
impl Pipeline {
pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
+ use wgpu::util::DeviceExt;
+
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
@@ -47,50 +52,52 @@ impl Pipeline {
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::FilterMode::Linear,
- lod_min_clamp: -100.0,
- lod_max_clamp: 100.0,
- compare_function: wgpu::CompareFunction::Always,
+ ..Default::default()
});
let constant_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
- bindings: &[
- wgpu::BindGroupLayoutBinding {
+ label: Some("iced_wgpu::image constants layout"),
+ entries: &[
+ wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStage::VERTEX,
- ty: wgpu::BindingType::UniformBuffer { dynamic: false },
+ ty: wgpu::BindingType::UniformBuffer {
+ dynamic: false,
+ min_binding_size: wgpu::BufferSize::new(
+ mem::size_of::<Uniforms>() as u64,
+ ),
+ },
+ count: None,
},
- wgpu::BindGroupLayoutBinding {
+ wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStage::FRAGMENT,
- ty: wgpu::BindingType::Sampler,
+ ty: wgpu::BindingType::Sampler { comparison: false },
+ count: None,
},
],
});
- let uniforms = Uniforms {
- transform: Transformation::identity().into(),
- };
-
- let uniforms_buffer = device
- .create_buffer_mapped(
- 1,
- wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST,
- )
- .fill_from_slice(&[uniforms]);
+ let uniforms_buffer = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("iced_wgpu::image uniforms buffer"),
+ size: mem::size_of::<Uniforms>() as u64,
+ usage: wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST,
+ mapped_at_creation: false,
+ });
let constant_bind_group =
device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("iced_wgpu::image constants bind group"),
layout: &constant_layout,
- bindings: &[
- wgpu::Binding {
+ entries: &[
+ wgpu::BindGroupEntry {
binding: 0,
- resource: wgpu::BindingResource::Buffer {
- buffer: &uniforms_buffer,
- range: 0..std::mem::size_of::<Uniforms>() as u64,
- },
+ resource: wgpu::BindingResource::Buffer(
+ uniforms_buffer.slice(..),
+ ),
},
- wgpu::Binding {
+ wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler),
},
@@ -99,36 +106,38 @@ impl Pipeline {
let texture_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
- bindings: &[wgpu::BindGroupLayoutBinding {
+ label: Some("iced_wgpu::image texture atlas layout"),
+ entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStage::FRAGMENT,
ty: wgpu::BindingType::SampledTexture {
- multisampled: false,
dimension: wgpu::TextureViewDimension::D2,
+ component_type: wgpu::TextureComponentType::Float,
+ multisampled: false,
},
+ count: None,
}],
});
let layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("iced_wgpu::image pipeline layout"),
+ push_constant_ranges: &[],
bind_group_layouts: &[&constant_layout, &texture_layout],
});
- let vs = include_bytes!("shader/image.vert.spv");
- let vs_module = device.create_shader_module(
- &wgpu::read_spirv(std::io::Cursor::new(&vs[..]))
- .expect("Read image vertex shader as SPIR-V"),
- );
+ let vs_module = device.create_shader_module(wgpu::include_spirv!(
+ "shader/image.vert.spv"
+ ));
- let fs = include_bytes!("shader/image.frag.spv");
- let fs_module = device.create_shader_module(
- &wgpu::read_spirv(std::io::Cursor::new(&fs[..]))
- .expect("Read image fragment shader as SPIR-V"),
- );
+ let fs_module = device.create_shader_module(wgpu::include_spirv!(
+ "shader/image.frag.spv"
+ ));
let pipeline =
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
- layout: &layout,
+ label: Some("iced_wgpu::image pipeline"),
+ layout: Some(&layout),
vertex_stage: wgpu::ProgrammableStageDescriptor {
module: &vs_module,
entry_point: "main",
@@ -140,9 +149,7 @@ impl Pipeline {
rasterization_state: Some(wgpu::RasterizationStateDescriptor {
front_face: wgpu::FrontFace::Cw,
cull_mode: wgpu::CullMode::None,
- depth_bias: 0,
- depth_bias_slope_scale: 0.0,
- depth_bias_clamp: 0.0,
+ ..Default::default()
}),
primitive_topology: wgpu::PrimitiveTopology::TriangleList,
color_states: &[wgpu::ColorStateDescriptor {
@@ -160,72 +167,83 @@ impl Pipeline {
write_mask: wgpu::ColorWrite::ALL,
}],
depth_stencil_state: None,
- index_format: wgpu::IndexFormat::Uint16,
- vertex_buffers: &[
- wgpu::VertexBufferDescriptor {
- stride: mem::size_of::<Vertex>() as u64,
- step_mode: wgpu::InputStepMode::Vertex,
- attributes: &[wgpu::VertexAttributeDescriptor {
- shader_location: 0,
- format: wgpu::VertexFormat::Float2,
- offset: 0,
- }],
- },
- wgpu::VertexBufferDescriptor {
- stride: mem::size_of::<Instance>() as u64,
- step_mode: wgpu::InputStepMode::Instance,
- attributes: &[
- wgpu::VertexAttributeDescriptor {
- shader_location: 1,
+ vertex_state: wgpu::VertexStateDescriptor {
+ index_format: wgpu::IndexFormat::Uint16,
+ vertex_buffers: &[
+ wgpu::VertexBufferDescriptor {
+ stride: mem::size_of::<Vertex>() as u64,
+ step_mode: wgpu::InputStepMode::Vertex,
+ attributes: &[wgpu::VertexAttributeDescriptor {
+ shader_location: 0,
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,
- },
- ],
- },
- ],
+ }],
+ },
+ wgpu::VertexBufferDescriptor {
+ 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,
+ },
+ ],
+ },
+ ],
+ },
sample_count: 1,
sample_mask: !0,
alpha_to_coverage_enabled: false,
});
- let vertices = device
- .create_buffer_mapped(QUAD_VERTS.len(), wgpu::BufferUsage::VERTEX)
- .fill_from_slice(&QUAD_VERTS);
+ let vertices =
+ device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+ label: Some("iced_wgpu::image vertex buffer"),
+ contents: bytemuck::cast_slice(&QUAD_VERTS),
+ usage: wgpu::BufferUsage::VERTEX,
+ });
- let indices = device
- .create_buffer_mapped(QUAD_INDICES.len(), wgpu::BufferUsage::INDEX)
- .fill_from_slice(&QUAD_INDICES);
+ let indices =
+ device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+ label: Some("iced_wgpu::image index buffer"),
+ contents: bytemuck::cast_slice(&QUAD_INDICES),
+ usage: wgpu::BufferUsage::INDEX,
+ });
let instances = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("iced_wgpu::image instance buffer"),
size: mem::size_of::<Instance>() as u64 * Instance::MAX as u64,
usage: wgpu::BufferUsage::VERTEX | wgpu::BufferUsage::COPY_DST,
+ mapped_at_creation: false,
});
let texture_atlas = Atlas::new(device);
let texture = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("iced_wgpu::image texture atlas bind group"),
layout: &texture_layout,
- bindings: &[wgpu::Binding {
+ entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(
&texture_atlas.view(),
@@ -271,9 +289,10 @@ impl Pipeline {
pub fn draw(
&mut self,
- device: &mut wgpu::Device,
+ device: &wgpu::Device,
+ staging_belt: &mut wgpu::util::StagingBelt,
encoder: &mut wgpu::CommandEncoder,
- images: &[Image],
+ images: &[layer::Image],
transformation: Transformation,
bounds: Rectangle<u32>,
target: &wgpu::TextureView,
@@ -288,31 +307,48 @@ impl Pipeline {
let mut vector_cache = self.vector_cache.borrow_mut();
for image in images {
- match &image.handle {
+ match &image {
#[cfg(feature = "image")]
- Handle::Raster(handle) => {
+ layer::Image::Raster { handle, bounds } => {
if let Some(atlas_entry) = raster_cache.upload(
handle,
device,
encoder,
&mut self.texture_atlas,
) {
- add_instances(image, atlas_entry, instances);
+ add_instances(
+ [bounds.x, bounds.y],
+ [bounds.width, bounds.height],
+ atlas_entry,
+ instances,
+ );
}
}
+ #[cfg(not(feature = "image"))]
+ layer::Image::Raster { .. } => {}
+
#[cfg(feature = "svg")]
- Handle::Vector(handle) => {
+ layer::Image::Vector { handle, bounds } => {
+ let size = [bounds.width, bounds.height];
+
if let Some(atlas_entry) = vector_cache.upload(
handle,
- image.size,
+ size,
_scale,
device,
encoder,
&mut self.texture_atlas,
) {
- add_instances(image, atlas_entry, instances);
+ add_instances(
+ [bounds.x, bounds.y],
+ size,
+ atlas_entry,
+ instances,
+ );
}
}
+ #[cfg(not(feature = "svg"))]
+ layer::Image::Vector { .. } => {}
}
}
@@ -327,8 +363,9 @@ impl Pipeline {
self.texture =
device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("iced_wgpu::image texture atlas bind group"),
layout: &self.texture_layout,
- bindings: &[wgpu::Binding {
+ entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(
&self.texture_atlas.view(),
@@ -339,23 +376,20 @@ impl Pipeline {
self.texture_version = texture_version;
}
- let uniforms_buffer = device
- .create_buffer_mapped(1, wgpu::BufferUsage::COPY_SRC)
- .fill_from_slice(&[Uniforms {
- transform: transformation.into(),
- }]);
-
- encoder.copy_buffer_to_buffer(
- &uniforms_buffer,
- 0,
- &self.uniforms,
- 0,
- std::mem::size_of::<Uniforms>() as u64,
- );
+ {
+ let mut uniforms_buffer = staging_belt.write_buffer(
+ encoder,
+ &self.uniforms,
+ 0,
+ wgpu::BufferSize::new(mem::size_of::<Uniforms>() as u64)
+ .unwrap(),
+ device,
+ );
- let instances_buffer = device
- .create_buffer_mapped(instances.len(), wgpu::BufferUsage::COPY_SRC)
- .fill_from_slice(&instances);
+ uniforms_buffer.copy_from_slice(bytemuck::bytes_of(&Uniforms {
+ transform: transformation.into(),
+ }));
+ }
let mut i = 0;
let total = instances.len();
@@ -364,27 +398,30 @@ impl Pipeline {
let end = (i + Instance::MAX).min(total);
let amount = end - i;
- encoder.copy_buffer_to_buffer(
- &instances_buffer,
- (i * std::mem::size_of::<Instance>()) as u64,
+ let mut instances_buffer = staging_belt.write_buffer(
+ encoder,
&self.instances,
0,
- (amount * std::mem::size_of::<Instance>()) as u64,
+ wgpu::BufferSize::new(
+ (amount * std::mem::size_of::<Instance>()) as u64,
+ )
+ .unwrap(),
+ device,
);
+ instances_buffer.copy_from_slice(bytemuck::cast_slice(
+ &instances[i..i + amount],
+ ));
+
let mut render_pass =
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &[
wgpu::RenderPassColorAttachmentDescriptor {
attachment: target,
resolve_target: None,
- load_op: wgpu::LoadOp::Load,
- store_op: wgpu::StoreOp::Store,
- clear_color: wgpu::Color {
- r: 0.0,
- g: 0.0,
- b: 0.0,
- a: 0.0,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Load,
+ store: true,
},
},
],
@@ -394,11 +431,9 @@ impl Pipeline {
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, 0);
- render_pass.set_vertex_buffers(
- 0,
- &[(&self.vertices, 0), (&self.instances, 0)],
- );
+ render_pass.set_index_buffer(self.indices.slice(..));
+ 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,
@@ -426,22 +461,8 @@ impl Pipeline {
}
}
-pub struct Image {
- pub handle: Handle,
- pub position: [f32; 2],
- pub size: [f32; 2],
-}
-
-pub enum Handle {
- #[cfg(feature = "image")]
- Raster(image::Handle),
-
- #[cfg(feature = "svg")]
- Vector(svg::Handle),
-}
-
#[repr(C)]
-#[derive(Clone, Copy)]
+#[derive(Clone, Copy, Zeroable, Pod)]
pub struct Vertex {
_position: [f32; 2],
}
@@ -464,7 +485,7 @@ const QUAD_VERTS: [Vertex; 4] = [
];
#[repr(C)]
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone, Copy, Zeroable, Pod)]
struct Instance {
_position: [f32; 2],
_size: [f32; 2],
@@ -478,28 +499,29 @@ impl Instance {
}
#[repr(C)]
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone, Copy, Zeroable, Pod)]
struct Uniforms {
transform: [f32; 16],
}
fn add_instances(
- image: &Image,
+ image_position: [f32; 2],
+ image_size: [f32; 2],
entry: &atlas::Entry,
instances: &mut Vec<Instance>,
) {
match entry {
atlas::Entry::Contiguous(allocation) => {
- add_instance(image.position, image.size, allocation, instances);
+ add_instance(image_position, image_size, allocation, instances);
}
atlas::Entry::Fragmented { fragments, size } => {
- let scaling_x = image.size[0] / size.0 as f32;
- let scaling_y = image.size[1] / size.1 as f32;
+ let scaling_x = image_size[0] / size.0 as f32;
+ let scaling_y = image_size[1] / size.1 as f32;
for fragment in fragments {
let allocation = &fragment.allocation;
- let [x, y] = image.position;
+ let [x, y] = image_position;
let (fragment_x, fragment_y) = fragment.position;
let (fragment_width, fragment_height) = allocation.size();
diff --git a/wgpu/src/image/atlas.rs b/wgpu/src/image/atlas.rs
index 86a5ff49..660ebe44 100644
--- a/wgpu/src/image/atlas.rs
+++ b/wgpu/src/image/atlas.rs
@@ -28,8 +28,8 @@ impl Atlas {
};
let texture = device.create_texture(&wgpu::TextureDescriptor {
+ label: Some("iced_wgpu::image texture atlas"),
size: extent,
- array_layer_count: 2,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
@@ -39,12 +39,15 @@ impl Atlas {
| wgpu::TextureUsage::SAMPLED,
});
- let texture_view = texture.create_default_view();
+ let texture_view = texture.create_view(&wgpu::TextureViewDescriptor {
+ dimension: Some(wgpu::TextureViewDimension::D2Array),
+ ..Default::default()
+ });
Atlas {
texture,
texture_view,
- layers: vec![Layer::Empty, Layer::Empty],
+ layers: vec![Layer::Empty],
}
}
@@ -56,17 +59,16 @@ impl Atlas {
self.layers.len()
}
- pub fn upload<C>(
+ pub fn upload(
&mut self,
width: u32,
height: u32,
- data: &[C],
+ data: &[u8],
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
- ) -> Option<Entry>
- where
- C: Copy + 'static,
- {
+ ) -> Option<Entry> {
+ use wgpu::util::DeviceExt;
+
let entry = {
let current_size = self.layers.len();
let entry = self.allocate(width, height)?;
@@ -80,9 +82,31 @@ impl Atlas {
log::info!("Allocated atlas entry: {:?}", entry);
- let buffer = device
- .create_buffer_mapped(data.len(), wgpu::BufferUsage::COPY_SRC)
- .fill_from_slice(data);
+ // It is a webgpu requirement that:
+ // BufferCopyView.layout.bytes_per_row % wgpu::COPY_BYTES_PER_ROW_ALIGNMENT == 0
+ // So we calculate padded_width by rounding width up to the next
+ // multiple of wgpu::COPY_BYTES_PER_ROW_ALIGNMENT.
+ let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
+ let padding = (align - (4 * width) % align) % align;
+ let padded_width = (4 * width + padding) as usize;
+ let padded_data_size = padded_width * height as usize;
+
+ let mut padded_data = vec![0; padded_data_size];
+
+ for row in 0..height as usize {
+ let offset = row * padded_width;
+
+ padded_data[offset..offset + 4 * width as usize].copy_from_slice(
+ &data[row * 4 * width as usize..(row + 1) * 4 * width as usize],
+ )
+ }
+
+ let buffer =
+ device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+ label: Some("iced_wgpu::image staging buffer"),
+ contents: &padded_data,
+ usage: wgpu::BufferUsage::COPY_SRC,
+ });
match &entry {
Entry::Contiguous(allocation) => {
@@ -90,6 +114,7 @@ impl Atlas {
&buffer,
width,
height,
+ padding,
0,
&allocation,
encoder,
@@ -98,12 +123,13 @@ impl Atlas {
Entry::Fragmented { fragments, .. } => {
for fragment in fragments {
let (x, y) = fragment.position;
- let offset = (y * width + x) as usize * 4;
+ let offset = (y * padded_width as u32 + 4 * x) as usize;
self.upload_allocation(
&buffer,
width,
height,
+ padding,
offset,
&fragment.allocation,
encoder,
@@ -256,6 +282,7 @@ impl Atlas {
buffer: &wgpu::Buffer,
image_width: u32,
image_height: u32,
+ padding: u32,
offset: usize,
allocation: &Allocation,
encoder: &mut wgpu::CommandEncoder,
@@ -273,18 +300,19 @@ impl Atlas {
encoder.copy_buffer_to_texture(
wgpu::BufferCopyView {
buffer,
- offset: offset as u64,
- row_pitch: 4 * image_width,
- image_height,
+ layout: wgpu::TextureDataLayout {
+ offset: offset as u64,
+ bytes_per_row: 4 * image_width + padding,
+ rows_per_image: image_height,
+ },
},
wgpu::TextureCopyView {
texture: &self.texture,
- array_layer: layer as u32,
mip_level: 0,
origin: wgpu::Origin3d {
- x: x as f32,
- y: y as f32,
- z: 0.0,
+ x,
+ y,
+ z: layer as u32,
},
},
extent,
@@ -302,12 +330,12 @@ impl Atlas {
}
let new_texture = device.create_texture(&wgpu::TextureDescriptor {
+ label: Some("iced_wgpu::image texture atlas"),
size: wgpu::Extent3d {
width: SIZE,
height: SIZE,
- depth: 1,
+ depth: self.layers.len() as u32,
},
- array_layer_count: self.layers.len() as u32,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
@@ -329,22 +357,20 @@ impl Atlas {
encoder.copy_texture_to_texture(
wgpu::TextureCopyView {
texture: &self.texture,
- array_layer: i as u32,
mip_level: 0,
origin: wgpu::Origin3d {
- x: 0.0,
- y: 0.0,
- z: 0.0,
+ x: 0,
+ y: 0,
+ z: i as u32,
},
},
wgpu::TextureCopyView {
texture: &new_texture,
- array_layer: i as u32,
mip_level: 0,
origin: wgpu::Origin3d {
- x: 0.0,
- y: 0.0,
- z: 0.0,
+ x: 0,
+ y: 0,
+ z: i as u32,
},
},
wgpu::Extent3d {
@@ -356,6 +382,10 @@ impl Atlas {
}
self.texture = new_texture;
- self.texture_view = self.texture.create_default_view();
+ self.texture_view =
+ self.texture.create_view(&wgpu::TextureViewDescriptor {
+ dimension: Some(wgpu::TextureViewDimension::D2Array),
+ ..Default::default()
+ });
}
}
diff --git a/wgpu/src/image/raster.rs b/wgpu/src/image/raster.rs
index 4f69df8c..25607dab 100644
--- a/wgpu/src/image/raster.rs
+++ b/wgpu/src/image/raster.rs
@@ -43,14 +43,14 @@ impl Cache {
let memory = match handle.data() {
image::Data::Path(path) => {
if let Ok(image) = ::image::open(path) {
- Memory::Host(image.to_bgra())
+ Memory::Host(image.to_bgra8())
} else {
Memory::NotFound
}
}
image::Data::Bytes(bytes) => {
if let Ok(image) = ::image::load_from_memory(&bytes) {
- Memory::Host(image.to_bgra())
+ Memory::Host(image.to_bgra8())
} else {
Memory::Invalid
}
diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs
index bae0f82f..95df2e99 100644
--- a/wgpu/src/image/vector.rs
+++ b/wgpu/src/image/vector.rs
@@ -45,9 +45,19 @@ impl Cache {
let opt = resvg::Options::default();
- let svg = match resvg::usvg::Tree::from_file(handle.path(), &opt.usvg) {
- Ok(tree) => Svg::Loaded(tree),
- Err(_) => Svg::NotFound,
+ let svg = match handle.data() {
+ svg::Data::Path(path) => {
+ match resvg::usvg::Tree::from_file(path, &opt.usvg) {
+ Ok(tree) => Svg::Loaded(tree),
+ Err(_) => Svg::NotFound,
+ }
+ }
+ svg::Data::Bytes(bytes) => {
+ match resvg::usvg::Tree::from_data(&bytes, &opt.usvg) {
+ Ok(tree) => Svg::Loaded(tree),
+ Err(_) => Svg::NotFound,
+ }
+ }
};
let _ = self.svgs.insert(handle.id(), svg);
@@ -107,7 +117,7 @@ impl Cache {
let allocation = texture_atlas.upload(
width,
height,
- canvas.get_data(),
+ bytemuck::cast_slice(canvas.get_data()),
device,
encoder,
)?;
diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs
index 4e0cbc60..a4c2ac0e 100644
--- a/wgpu/src/lib.rs
+++ b/wgpu/src/lib.rs
@@ -1,6 +1,6 @@
//! A [`wgpu`] renderer for [`iced_native`].
//!
-//! ![`iced_wgpu` crate graph](https://github.com/hecrj/iced/blob/cae26cb7bc627f4a5b3bcf1cd023a0c552e8c65e/docs/graphs/wgpu.png?raw=true)
+//! ![The native path of the Iced ecosystem](https://github.com/hecrj/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/native.png?raw=true)
//!
//! For now, it is the default renderer of [Iced] in native platforms.
//!
@@ -11,8 +11,9 @@
//! Currently, `iced_wgpu` supports the following primitives:
//! - Text, which is rendered using [`wgpu_glyph`]. No shaping at all.
//! - Quads or rectangles, with rounded borders and a solid background color.
-//! - Images, lazily loaded from the filesystem.
//! - Clip areas, useful to implement scrollables or hide overflowing content.
+//! - Images and SVG, loaded from memory or the file system.
+//! - Meshes of triangles, useful to draw geometry freely.
//!
//! [Iced]: https://github.com/hecrj/iced
//! [`iced_native`]: https://github.com/hecrj/iced/tree/master/native
@@ -22,36 +23,37 @@
#![deny(missing_docs)]
#![deny(missing_debug_implementations)]
#![deny(unused_results)]
-#![forbid(unsafe_code)]
+#![deny(unsafe_code)]
#![forbid(rust_2018_idioms)]
-pub mod defaults;
+#![cfg_attr(docsrs, feature(doc_cfg))]
+
pub mod settings;
pub mod triangle;
pub mod widget;
pub mod window;
-mod primitive;
+mod backend;
mod quad;
-mod renderer;
-mod target;
mod text;
-mod transformation;
-mod viewport;
+pub use iced_graphics::{
+ Antialiasing, Color, Defaults, Error, Primitive, Viewport,
+};
pub use wgpu;
-pub use defaults::Defaults;
-pub use primitive::Primitive;
-pub use renderer::Renderer;
+pub use backend::Backend;
pub use settings::Settings;
-pub use target::Target;
-pub use viewport::Viewport;
#[doc(no_inline)]
pub use widget::*;
-pub(crate) use quad::Quad;
-pub(crate) use transformation::Transformation;
+pub(crate) use iced_graphics::Transformation;
#[cfg(any(feature = "image", feature = "svg"))]
mod image;
+
+/// A [`wgpu`] graphics renderer for [`iced`].
+///
+/// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs
+/// [`iced`]: https://github.com/hecrj/iced
+pub type Renderer = iced_graphics::Renderer<Backend>;
diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs
index 9047080d..24d20cfa 100644
--- a/wgpu/src/quad.rs
+++ b/wgpu/src/quad.rs
@@ -1,7 +1,10 @@
use crate::Transformation;
+use iced_graphics::layer;
use iced_native::Rectangle;
+use bytemuck::{Pod, Zeroable};
use std::mem;
+use wgpu::util::DeviceExt;
#[derive(Debug)]
pub struct Pipeline {
@@ -14,57 +17,58 @@ pub struct Pipeline {
}
impl Pipeline {
- pub fn new(
- device: &mut wgpu::Device,
- format: wgpu::TextureFormat,
- ) -> Pipeline {
+ pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Pipeline {
let constant_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
- bindings: &[wgpu::BindGroupLayoutBinding {
+ label: Some("iced_wgpu::quad uniforms layout"),
+ entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStage::VERTEX,
- ty: wgpu::BindingType::UniformBuffer { dynamic: false },
+ ty: wgpu::BindingType::UniformBuffer {
+ dynamic: false,
+ min_binding_size: wgpu::BufferSize::new(
+ mem::size_of::<Uniforms>() as u64,
+ ),
+ },
+ count: None,
}],
});
- let constants_buffer = device
- .create_buffer_mapped(
- 1,
- wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST,
- )
- .fill_from_slice(&[Uniforms::default()]);
+ let constants_buffer = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("iced_wgpu::quad uniforms buffer"),
+ size: mem::size_of::<Uniforms>() as u64,
+ usage: wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST,
+ mapped_at_creation: false,
+ });
let constants = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("iced_wgpu::quad uniforms bind group"),
layout: &constant_layout,
- bindings: &[wgpu::Binding {
+ entries: &[wgpu::BindGroupEntry {
binding: 0,
- resource: wgpu::BindingResource::Buffer {
- buffer: &constants_buffer,
- range: 0..std::mem::size_of::<Uniforms>() as u64,
- },
+ resource: wgpu::BindingResource::Buffer(
+ constants_buffer.slice(..),
+ ),
}],
});
let layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("iced_wgpu::quad pipeline layout"),
+ push_constant_ranges: &[],
bind_group_layouts: &[&constant_layout],
});
- let vs = include_bytes!("shader/quad.vert.spv");
- let vs_module = device.create_shader_module(
- &wgpu::read_spirv(std::io::Cursor::new(&vs[..]))
- .expect("Read quad vertex shader as SPIR-V"),
- );
+ let vs_module = device
+ .create_shader_module(wgpu::include_spirv!("shader/quad.vert.spv"));
- let fs = include_bytes!("shader/quad.frag.spv");
- let fs_module = device.create_shader_module(
- &wgpu::read_spirv(std::io::Cursor::new(&fs[..]))
- .expect("Read quad fragment shader as SPIR-V"),
- );
+ let fs_module = device
+ .create_shader_module(wgpu::include_spirv!("shader/quad.frag.spv"));
let pipeline =
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
- layout: &layout,
+ label: Some("iced_wgpu::quad pipeline"),
+ layout: Some(&layout),
vertex_stage: wgpu::ProgrammableStageDescriptor {
module: &vs_module,
entry_point: "main",
@@ -76,9 +80,7 @@ impl Pipeline {
rasterization_state: Some(wgpu::RasterizationStateDescriptor {
front_face: wgpu::FrontFace::Cw,
cull_mode: wgpu::CullMode::None,
- depth_bias: 0,
- depth_bias_slope_scale: 0.0,
- depth_bias_clamp: 0.0,
+ ..Default::default()
}),
primitive_topology: wgpu::PrimitiveTopology::TriangleList,
color_states: &[wgpu::ColorStateDescriptor {
@@ -96,70 +98,80 @@ impl Pipeline {
write_mask: wgpu::ColorWrite::ALL,
}],
depth_stencil_state: None,
- index_format: wgpu::IndexFormat::Uint16,
- vertex_buffers: &[
- wgpu::VertexBufferDescriptor {
- stride: mem::size_of::<Vertex>() as u64,
- step_mode: wgpu::InputStepMode::Vertex,
- attributes: &[wgpu::VertexAttributeDescriptor {
- shader_location: 0,
- format: wgpu::VertexFormat::Float2,
- offset: 0,
- }],
- },
- wgpu::VertexBufferDescriptor {
- stride: mem::size_of::<Quad>() as u64,
- step_mode: wgpu::InputStepMode::Instance,
- attributes: &[
- wgpu::VertexAttributeDescriptor {
- shader_location: 1,
+ vertex_state: wgpu::VertexStateDescriptor {
+ index_format: wgpu::IndexFormat::Uint16,
+ vertex_buffers: &[
+ wgpu::VertexBufferDescriptor {
+ stride: mem::size_of::<Vertex>() as u64,
+ step_mode: wgpu::InputStepMode::Vertex,
+ attributes: &[wgpu::VertexAttributeDescriptor {
+ shader_location: 0,
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),
- },
- ],
- },
- ],
+ }],
+ },
+ wgpu::VertexBufferDescriptor {
+ 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),
+ },
+ ],
+ },
+ ],
+ },
sample_count: 1,
sample_mask: !0,
alpha_to_coverage_enabled: false,
});
- let vertices = device
- .create_buffer_mapped(QUAD_VERTS.len(), wgpu::BufferUsage::VERTEX)
- .fill_from_slice(&QUAD_VERTS);
+ let vertices =
+ device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+ label: Some("iced_wgpu::quad vertex buffer"),
+ contents: bytemuck::cast_slice(&QUAD_VERTS),
+ usage: wgpu::BufferUsage::VERTEX,
+ });
- let indices = device
- .create_buffer_mapped(QUAD_INDICES.len(), wgpu::BufferUsage::INDEX)
- .fill_from_slice(&QUAD_INDICES);
+ let indices =
+ device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+ label: Some("iced_wgpu::quad index buffer"),
+ contents: bytemuck::cast_slice(&QUAD_INDICES),
+ usage: wgpu::BufferUsage::INDEX,
+ });
let instances = device.create_buffer(&wgpu::BufferDescriptor {
- size: mem::size_of::<Quad>() as u64 * Quad::MAX as u64,
+ label: Some("iced_wgpu::quad instance buffer"),
+ size: mem::size_of::<layer::Quad>() as u64 * MAX_INSTANCES as u64,
usage: wgpu::BufferUsage::VERTEX | wgpu::BufferUsage::COPY_DST,
+ mapped_at_creation: false,
});
Pipeline {
@@ -174,9 +186,10 @@ impl Pipeline {
pub fn draw(
&mut self,
- device: &mut wgpu::Device,
+ device: &wgpu::Device,
+ staging_belt: &mut wgpu::util::StagingBelt,
encoder: &mut wgpu::CommandEncoder,
- instances: &[Quad],
+ instances: &[layer::Quad],
transformation: Transformation,
scale: f32,
bounds: Rectangle<u32>,
@@ -184,37 +197,38 @@ impl Pipeline {
) {
let uniforms = Uniforms::new(transformation, scale);
- let constants_buffer = device
- .create_buffer_mapped(1, wgpu::BufferUsage::COPY_SRC)
- .fill_from_slice(&[uniforms]);
+ {
+ let mut constants_buffer = staging_belt.write_buffer(
+ encoder,
+ &self.constants_buffer,
+ 0,
+ wgpu::BufferSize::new(mem::size_of::<Uniforms>() as u64)
+ .unwrap(),
+ device,
+ );
- encoder.copy_buffer_to_buffer(
- &constants_buffer,
- 0,
- &self.constants_buffer,
- 0,
- std::mem::size_of::<Uniforms>() as u64,
- );
+ constants_buffer.copy_from_slice(bytemuck::bytes_of(&uniforms));
+ }
let mut i = 0;
let total = instances.len();
while i < total {
- let end = (i + Quad::MAX).min(total);
+ let end = (i + MAX_INSTANCES).min(total);
let amount = end - i;
- let instance_buffer = device
- .create_buffer_mapped(amount, wgpu::BufferUsage::COPY_SRC)
- .fill_from_slice(&instances[i..end]);
+ let instance_bytes = bytemuck::cast_slice(&instances[i..end]);
- encoder.copy_buffer_to_buffer(
- &instance_buffer,
- 0,
+ let mut instance_buffer = staging_belt.write_buffer(
+ encoder,
&self.instances,
0,
- (mem::size_of::<Quad>() * amount) as u64,
+ wgpu::BufferSize::new(instance_bytes.len() as u64).unwrap(),
+ device,
);
+ instance_buffer.copy_from_slice(instance_bytes);
+
{
let mut render_pass =
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
@@ -222,13 +236,9 @@ impl Pipeline {
wgpu::RenderPassColorAttachmentDescriptor {
attachment: target,
resolve_target: None,
- load_op: wgpu::LoadOp::Load,
- store_op: wgpu::StoreOp::Store,
- clear_color: wgpu::Color {
- r: 0.0,
- g: 0.0,
- b: 0.0,
- a: 0.0,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Load,
+ store: true,
},
},
],
@@ -237,11 +247,9 @@ impl Pipeline {
render_pass.set_pipeline(&self.pipeline);
render_pass.set_bind_group(0, &self.constants, &[]);
- render_pass.set_index_buffer(&self.indices, 0);
- render_pass.set_vertex_buffers(
- 0,
- &[(&self.vertices, 0), (&self.instances, 0)],
- );
+ render_pass.set_index_buffer(self.indices.slice(..));
+ 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,
@@ -257,13 +265,13 @@ impl Pipeline {
);
}
- i += Quad::MAX;
+ i += MAX_INSTANCES;
}
}
}
#[repr(C)]
-#[derive(Clone, Copy)]
+#[derive(Clone, Copy, Zeroable, Pod)]
pub struct Vertex {
_position: [f32; 2],
}
@@ -285,23 +293,10 @@ const QUAD_VERTS: [Vertex; 4] = [
},
];
-#[repr(C)]
-#[derive(Debug, Clone, Copy)]
-pub struct Quad {
- pub position: [f32; 2],
- pub scale: [f32; 2],
- pub color: [f32; 4],
- pub border_color: [f32; 4],
- pub border_radius: f32,
- pub border_width: f32,
-}
-
-impl Quad {
- const MAX: usize = 100_000;
-}
+const MAX_INSTANCES: usize = 100_000;
#[repr(C)]
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone, Copy, Zeroable, Pod)]
struct Uniforms {
transform: [f32; 16],
scale: f32,
diff --git a/wgpu/src/renderer.rs b/wgpu/src/renderer.rs
deleted file mode 100644
index c06af339..00000000
--- a/wgpu/src/renderer.rs
+++ /dev/null
@@ -1,509 +0,0 @@
-use crate::{
- quad, text, triangle, Defaults, Primitive, Quad, Settings, Target,
- Transformation,
-};
-
-#[cfg(any(feature = "image", feature = "svg"))]
-use crate::image::{self, Image};
-
-use iced_native::{
- layout, Background, Color, Layout, MouseCursor, Point, Rectangle, Vector,
- Widget,
-};
-
-mod widget;
-
-/// A [`wgpu`] renderer.
-///
-/// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs
-#[derive(Debug)]
-pub struct Renderer {
- quad_pipeline: quad::Pipeline,
- text_pipeline: text::Pipeline,
- triangle_pipeline: triangle::Pipeline,
-
- #[cfg(any(feature = "image", feature = "svg"))]
- image_pipeline: image::Pipeline,
-}
-
-struct Layer<'a> {
- bounds: Rectangle<u32>,
- quads: Vec<Quad>,
- meshes: Vec<(Point, &'a triangle::Mesh2D)>,
- text: Vec<wgpu_glyph::Section<'a>>,
-
- #[cfg(any(feature = "image", feature = "svg"))]
- images: Vec<Image>,
-}
-
-impl<'a> Layer<'a> {
- pub fn new(bounds: Rectangle<u32>) -> Self {
- Self {
- bounds,
- quads: Vec::new(),
- text: Vec::new(),
- meshes: Vec::new(),
-
- #[cfg(any(feature = "image", feature = "svg"))]
- images: Vec::new(),
- }
- }
-}
-
-impl Renderer {
- /// Creates a new [`Renderer`].
- ///
- /// [`Renderer`]: struct.Renderer.html
- pub fn new(device: &mut wgpu::Device, settings: Settings) -> Self {
- let text_pipeline =
- text::Pipeline::new(device, settings.format, settings.default_font);
- let quad_pipeline = quad::Pipeline::new(device, settings.format);
- let triangle_pipeline = triangle::Pipeline::new(
- device,
- settings.format,
- settings.antialiasing,
- );
-
- #[cfg(any(feature = "image", feature = "svg"))]
- let image_pipeline = image::Pipeline::new(device, settings.format);
-
- Self {
- quad_pipeline,
- text_pipeline,
- triangle_pipeline,
-
- #[cfg(any(feature = "image", feature = "svg"))]
- image_pipeline,
- }
- }
-
- /// Draws the provided primitives in the given [`Target`].
- ///
- /// The text provided as overlay will be renderer on top of the primitives.
- /// This is useful for rendering debug information.
- ///
- /// [`Target`]: struct.Target.html
- pub fn draw<T: AsRef<str>>(
- &mut self,
- device: &mut wgpu::Device,
- encoder: &mut wgpu::CommandEncoder,
- target: Target<'_>,
- (primitive, mouse_cursor): &(Primitive, MouseCursor),
- scale_factor: f64,
- overlay: &[T],
- ) -> MouseCursor {
- log::debug!("Drawing");
-
- let (width, height) = target.viewport.dimensions();
- let scale_factor = scale_factor as f32;
- let transformation = target.viewport.transformation();
-
- let mut layers = Vec::new();
-
- layers.push(Layer::new(Rectangle {
- x: 0,
- y: 0,
- width: u32::from(width),
- height: u32::from(height),
- }));
-
- self.draw_primitive(Vector::new(0.0, 0.0), primitive, &mut layers);
- self.draw_overlay(overlay, &mut layers);
-
- for layer in layers {
- self.flush(
- device,
- scale_factor,
- transformation,
- &layer,
- encoder,
- target.texture,
- width,
- height,
- );
- }
-
- #[cfg(any(feature = "image", feature = "svg"))]
- self.image_pipeline.trim_cache();
-
- *mouse_cursor
- }
-
- fn draw_primitive<'a>(
- &mut self,
- translation: Vector,
- primitive: &'a Primitive,
- layers: &mut Vec<Layer<'a>>,
- ) {
- match primitive {
- Primitive::None => {}
- Primitive::Group { primitives } => {
- // TODO: Inspect a bit and regroup (?)
- for primitive in primitives {
- self.draw_primitive(translation, primitive, layers)
- }
- }
- Primitive::Text {
- content,
- bounds,
- size,
- color,
- font,
- horizontal_alignment,
- vertical_alignment,
- } => {
- let x = match horizontal_alignment {
- iced_native::HorizontalAlignment::Left => bounds.x,
- iced_native::HorizontalAlignment::Center => {
- bounds.x + bounds.width / 2.0
- }
- iced_native::HorizontalAlignment::Right => {
- bounds.x + bounds.width
- }
- };
-
- let y = match vertical_alignment {
- iced_native::VerticalAlignment::Top => bounds.y,
- iced_native::VerticalAlignment::Center => {
- bounds.y + bounds.height / 2.0
- }
- iced_native::VerticalAlignment::Bottom => {
- bounds.y + bounds.height
- }
- };
-
- let layer = layers.last_mut().unwrap();
-
- layer.text.push(wgpu_glyph::Section {
- text: &content,
- screen_position: (x + translation.x, y + translation.y),
- bounds: (bounds.width, bounds.height),
- scale: wgpu_glyph::Scale { x: *size, y: *size },
- color: color.into_linear(),
- font_id: self.text_pipeline.find_font(*font),
- layout: wgpu_glyph::Layout::default()
- .h_align(match horizontal_alignment {
- iced_native::HorizontalAlignment::Left => {
- wgpu_glyph::HorizontalAlign::Left
- }
- iced_native::HorizontalAlignment::Center => {
- wgpu_glyph::HorizontalAlign::Center
- }
- iced_native::HorizontalAlignment::Right => {
- wgpu_glyph::HorizontalAlign::Right
- }
- })
- .v_align(match vertical_alignment {
- iced_native::VerticalAlignment::Top => {
- wgpu_glyph::VerticalAlign::Top
- }
- iced_native::VerticalAlignment::Center => {
- wgpu_glyph::VerticalAlign::Center
- }
- iced_native::VerticalAlignment::Bottom => {
- wgpu_glyph::VerticalAlign::Bottom
- }
- }),
- ..Default::default()
- })
- }
- Primitive::Quad {
- bounds,
- background,
- border_radius,
- border_width,
- border_color,
- } => {
- let layer = layers.last_mut().unwrap();
-
- // TODO: Move some of these computations to the GPU (?)
- layer.quads.push(Quad {
- position: [
- bounds.x + translation.x,
- bounds.y + translation.y,
- ],
- scale: [bounds.width, bounds.height],
- color: match background {
- Background::Color(color) => color.into_linear(),
- },
- border_radius: *border_radius as f32,
- border_width: *border_width as f32,
- border_color: border_color.into_linear(),
- });
- }
- Primitive::Mesh2D { origin, buffers } => {
- let layer = layers.last_mut().unwrap();
-
- layer.meshes.push((*origin + translation, buffers));
- }
- Primitive::Clip {
- bounds,
- offset,
- content,
- } => {
- let layer = layers.last_mut().unwrap();
-
- let layer_bounds: Rectangle<f32> = layer.bounds.into();
-
- let clip = Rectangle {
- x: bounds.x + translation.x,
- y: bounds.y + translation.y,
- ..*bounds
- };
-
- // Only draw visible content
- if let Some(clip_bounds) = layer_bounds.intersection(&clip) {
- let clip_layer = Layer::new(clip_bounds.into());
- let new_layer = Layer::new(layer.bounds);
-
- layers.push(clip_layer);
- self.draw_primitive(
- translation
- - Vector::new(offset.x as f32, offset.y as f32),
- content,
- layers,
- );
- layers.push(new_layer);
- }
- }
-
- Primitive::Cached { origin, cache } => {
- self.draw_primitive(
- translation + Vector::new(origin.x, origin.y),
- &cache,
- layers,
- );
- }
-
- #[cfg(feature = "image")]
- Primitive::Image { handle, bounds } => {
- let layer = layers.last_mut().unwrap();
-
- layer.images.push(Image {
- handle: image::Handle::Raster(handle.clone()),
- position: [
- bounds.x + translation.x,
- bounds.y + translation.y,
- ],
- size: [bounds.width, bounds.height],
- });
- }
- #[cfg(not(feature = "image"))]
- Primitive::Image { .. } => {}
-
- #[cfg(feature = "svg")]
- Primitive::Svg { handle, bounds } => {
- let layer = layers.last_mut().unwrap();
-
- layer.images.push(Image {
- handle: image::Handle::Vector(handle.clone()),
- position: [
- bounds.x + translation.x,
- bounds.y + translation.y,
- ],
- size: [bounds.width, bounds.height],
- });
- }
- #[cfg(not(feature = "svg"))]
- Primitive::Svg { .. } => {}
- }
- }
-
- fn draw_overlay<'a, T: AsRef<str>>(
- &mut self,
- lines: &'a [T],
- layers: &mut Vec<Layer<'a>>,
- ) {
- let first = layers.first().unwrap();
- let mut overlay = Layer::new(first.bounds);
-
- let font_id = self.text_pipeline.overlay_font();
- let scale = wgpu_glyph::Scale { x: 20.0, y: 20.0 };
-
- for (i, line) in lines.iter().enumerate() {
- overlay.text.push(wgpu_glyph::Section {
- text: line.as_ref(),
- screen_position: (11.0, 11.0 + 25.0 * i as f32),
- color: [0.9, 0.9, 0.9, 1.0],
- scale,
- font_id,
- ..wgpu_glyph::Section::default()
- });
-
- overlay.text.push(wgpu_glyph::Section {
- text: line.as_ref(),
- screen_position: (10.0, 10.0 + 25.0 * i as f32),
- color: [0.0, 0.0, 0.0, 1.0],
- scale,
- font_id,
- ..wgpu_glyph::Section::default()
- });
- }
-
- layers.push(overlay);
- }
-
- fn flush(
- &mut self,
- device: &mut wgpu::Device,
- scale_factor: f32,
- transformation: Transformation,
- layer: &Layer<'_>,
- encoder: &mut wgpu::CommandEncoder,
- target: &wgpu::TextureView,
- target_width: u32,
- target_height: u32,
- ) {
- let bounds = layer.bounds * scale_factor;
-
- if layer.meshes.len() > 0 {
- let scaled = transformation
- * Transformation::scale(scale_factor, scale_factor);
-
- self.triangle_pipeline.draw(
- device,
- encoder,
- target,
- target_width,
- target_height,
- scaled,
- &layer.meshes,
- bounds,
- );
- }
-
- if layer.quads.len() > 0 {
- self.quad_pipeline.draw(
- device,
- encoder,
- &layer.quads,
- transformation,
- scale_factor,
- bounds,
- target,
- );
- }
-
- #[cfg(any(feature = "image", feature = "svg"))]
- {
- if layer.images.len() > 0 {
- let scaled = transformation
- * Transformation::scale(scale_factor, scale_factor);
-
- self.image_pipeline.draw(
- device,
- encoder,
- &layer.images,
- scaled,
- bounds,
- target,
- scale_factor,
- );
- }
- }
-
- if layer.text.len() > 0 {
- for text in layer.text.iter() {
- // Target physical coordinates directly to avoid blurry text
- let text = wgpu_glyph::Section {
- // TODO: We `round` here to avoid rerasterizing text when
- // its position changes slightly. This can make text feel a
- // bit "jumpy". We may be able to do better once we improve
- // our text rendering/caching pipeline.
- screen_position: (
- (text.screen_position.0 * scale_factor).round(),
- (text.screen_position.1 * scale_factor).round(),
- ),
- // TODO: Fix precision issues with some scale factors.
- //
- // The `ceil` here can cause some words to render on the
- // same line when they should not.
- //
- // Ideally, `wgpu_glyph` should be able to compute layout
- // using logical positions, and then apply the proper
- // scaling when rendering. This would ensure that both
- // measuring and rendering follow the same layout rules.
- bounds: (
- (text.bounds.0 * scale_factor).ceil(),
- (text.bounds.1 * scale_factor).ceil(),
- ),
- scale: wgpu_glyph::Scale {
- x: text.scale.x * scale_factor,
- y: text.scale.y * scale_factor,
- },
- ..*text
- };
-
- self.text_pipeline.queue(text);
- }
-
- self.text_pipeline.draw_queued(
- device,
- encoder,
- target,
- transformation,
- wgpu_glyph::Region {
- x: bounds.x,
- y: bounds.y,
- width: bounds.width,
- height: bounds.height,
- },
- );
- }
- }
-}
-
-impl iced_native::Renderer for Renderer {
- type Output = (Primitive, MouseCursor);
- type Defaults = Defaults;
-
- fn layout<'a, Message>(
- &mut self,
- element: &iced_native::Element<'a, Message, Self>,
- limits: &iced_native::layout::Limits,
- ) -> iced_native::layout::Node {
- let node = element.layout(self, limits);
-
- self.text_pipeline.clear_measurement_cache();
-
- node
- }
-}
-
-impl layout::Debugger for Renderer {
- fn explain<Message>(
- &mut self,
- defaults: &Defaults,
- widget: &dyn Widget<Message, Self>,
- layout: Layout<'_>,
- cursor_position: Point,
- color: Color,
- ) -> Self::Output {
- let mut primitives = Vec::new();
- let (primitive, cursor) =
- widget.draw(self, defaults, layout, cursor_position);
-
- explain_layout(layout, color, &mut primitives);
- primitives.push(primitive);
-
- (Primitive::Group { primitives }, cursor)
- }
-}
-
-fn explain_layout(
- layout: Layout<'_>,
- color: Color,
- primitives: &mut Vec<Primitive>,
-) {
- primitives.push(Primitive::Quad {
- bounds: layout.bounds(),
- background: Background::Color(Color::TRANSPARENT),
- border_radius: 0,
- border_width: 1,
- border_color: [0.6, 0.6, 0.6, 0.5].into(),
- });
-
- for child in layout.children() {
- explain_layout(child, color, primitives);
- }
-}
diff --git a/wgpu/src/renderer/widget.rs b/wgpu/src/renderer/widget.rs
deleted file mode 100644
index 84f908e7..00000000
--- a/wgpu/src/renderer/widget.rs
+++ /dev/null
@@ -1,18 +0,0 @@
-mod button;
-mod checkbox;
-mod column;
-mod container;
-mod progress_bar;
-mod radio;
-mod row;
-mod scrollable;
-mod slider;
-mod space;
-mod text;
-mod text_input;
-
-#[cfg(feature = "svg")]
-mod svg;
-
-#[cfg(feature = "image")]
-mod image;
diff --git a/wgpu/src/renderer/widget/column.rs b/wgpu/src/renderer/widget/column.rs
deleted file mode 100644
index 95a7463a..00000000
--- a/wgpu/src/renderer/widget/column.rs
+++ /dev/null
@@ -1,34 +0,0 @@
-use crate::{Primitive, Renderer};
-use iced_native::{column, Element, Layout, MouseCursor, Point};
-
-impl column::Renderer for Renderer {
- fn draw<Message>(
- &mut self,
- defaults: &Self::Defaults,
- content: &[Element<'_, Message, Self>],
- layout: Layout<'_>,
- cursor_position: Point,
- ) -> Self::Output {
- let mut mouse_cursor = MouseCursor::OutOfBounds;
-
- (
- Primitive::Group {
- primitives: content
- .iter()
- .zip(layout.children())
- .map(|(child, layout)| {
- let (primitive, new_mouse_cursor) =
- child.draw(self, defaults, layout, cursor_position);
-
- if new_mouse_cursor > mouse_cursor {
- mouse_cursor = new_mouse_cursor;
- }
-
- primitive
- })
- .collect(),
- },
- mouse_cursor,
- )
- }
-}
diff --git a/wgpu/src/renderer/widget/container.rs b/wgpu/src/renderer/widget/container.rs
deleted file mode 100644
index 2d4d1db8..00000000
--- a/wgpu/src/renderer/widget/container.rs
+++ /dev/null
@@ -1,49 +0,0 @@
-use crate::{container, defaults, Defaults, Primitive, Renderer};
-use iced_native::{Background, Color, Element, Layout, Point, Rectangle};
-
-impl iced_native::container::Renderer for Renderer {
- type Style = Box<dyn container::StyleSheet>;
-
- fn draw<Message>(
- &mut self,
- defaults: &Defaults,
- bounds: Rectangle,
- cursor_position: Point,
- style_sheet: &Self::Style,
- content: &Element<'_, Message, Self>,
- content_layout: Layout<'_>,
- ) -> Self::Output {
- let style = style_sheet.style();
-
- let defaults = Defaults {
- text: defaults::Text {
- color: style.text_color.unwrap_or(defaults.text.color),
- },
- ..*defaults
- };
-
- let (content, mouse_cursor) =
- content.draw(self, &defaults, content_layout, cursor_position);
-
- if style.background.is_some() || style.border_width > 0 {
- let quad = Primitive::Quad {
- bounds,
- background: style
- .background
- .unwrap_or(Background::Color(Color::TRANSPARENT)),
- border_radius: style.border_radius,
- border_width: style.border_width,
- border_color: style.border_color,
- };
-
- (
- Primitive::Group {
- primitives: vec![quad, content],
- },
- mouse_cursor,
- )
- } else {
- (content, mouse_cursor)
- }
- }
-}
diff --git a/wgpu/src/renderer/widget/image.rs b/wgpu/src/renderer/widget/image.rs
deleted file mode 100644
index 70dc5d97..00000000
--- a/wgpu/src/renderer/widget/image.rs
+++ /dev/null
@@ -1,22 +0,0 @@
-use crate::{Primitive, Renderer};
-use iced_native::{image, Layout, MouseCursor};
-
-impl image::Renderer for Renderer {
- fn dimensions(&self, handle: &image::Handle) -> (u32, u32) {
- self.image_pipeline.dimensions(handle)
- }
-
- fn draw(
- &mut self,
- handle: image::Handle,
- layout: Layout<'_>,
- ) -> Self::Output {
- (
- Primitive::Image {
- handle,
- bounds: layout.bounds(),
- },
- MouseCursor::OutOfBounds,
- )
- }
-}
diff --git a/wgpu/src/renderer/widget/row.rs b/wgpu/src/renderer/widget/row.rs
deleted file mode 100644
index bd9f1a04..00000000
--- a/wgpu/src/renderer/widget/row.rs
+++ /dev/null
@@ -1,34 +0,0 @@
-use crate::{Primitive, Renderer};
-use iced_native::{row, Element, Layout, MouseCursor, Point};
-
-impl row::Renderer for Renderer {
- fn draw<Message>(
- &mut self,
- defaults: &Self::Defaults,
- children: &[Element<'_, Message, Self>],
- layout: Layout<'_>,
- cursor_position: Point,
- ) -> Self::Output {
- let mut mouse_cursor = MouseCursor::OutOfBounds;
-
- (
- Primitive::Group {
- primitives: children
- .iter()
- .zip(layout.children())
- .map(|(child, layout)| {
- let (primitive, new_mouse_cursor) =
- child.draw(self, defaults, layout, cursor_position);
-
- if new_mouse_cursor > mouse_cursor {
- mouse_cursor = new_mouse_cursor;
- }
-
- primitive
- })
- .collect(),
- },
- mouse_cursor,
- )
- }
-}
diff --git a/wgpu/src/renderer/widget/space.rs b/wgpu/src/renderer/widget/space.rs
deleted file mode 100644
index 28e05437..00000000
--- a/wgpu/src/renderer/widget/space.rs
+++ /dev/null
@@ -1,8 +0,0 @@
-use crate::{Primitive, Renderer};
-use iced_native::{space, MouseCursor, Rectangle};
-
-impl space::Renderer for Renderer {
- fn draw(&mut self, _bounds: Rectangle) -> Self::Output {
- (Primitive::None, MouseCursor::OutOfBounds)
- }
-}
diff --git a/wgpu/src/renderer/widget/text.rs b/wgpu/src/renderer/widget/text.rs
deleted file mode 100644
index 33e549cd..00000000
--- a/wgpu/src/renderer/widget/text.rs
+++ /dev/null
@@ -1,47 +0,0 @@
-use crate::{Primitive, Renderer};
-use iced_native::{
- text, Color, Font, HorizontalAlignment, MouseCursor, Rectangle, Size,
- VerticalAlignment,
-};
-
-use std::f32;
-
-impl text::Renderer for Renderer {
- const DEFAULT_SIZE: u16 = 20;
-
- fn measure(
- &self,
- content: &str,
- size: u16,
- font: Font,
- bounds: Size,
- ) -> (f32, f32) {
- self.text_pipeline
- .measure(content, f32::from(size), font, bounds)
- }
-
- fn draw(
- &mut self,
- defaults: &Self::Defaults,
- bounds: Rectangle,
- content: &str,
- size: u16,
- font: Font,
- color: Option<Color>,
- horizontal_alignment: HorizontalAlignment,
- vertical_alignment: VerticalAlignment,
- ) -> Self::Output {
- (
- Primitive::Text {
- content: content.to_string(),
- size: f32::from(size),
- bounds,
- color: color.unwrap_or(defaults.text.color),
- font,
- horizontal_alignment,
- vertical_alignment,
- },
- MouseCursor::OutOfBounds,
- )
- }
-}
diff --git a/wgpu/src/renderer/widget/text_input.rs b/wgpu/src/renderer/widget/text_input.rs
deleted file mode 100644
index e2a1b3a9..00000000
--- a/wgpu/src/renderer/widget/text_input.rs
+++ /dev/null
@@ -1,182 +0,0 @@
-use crate::{text_input::StyleSheet, Primitive, Renderer};
-
-use iced_native::{
- text_input, Background, Color, Font, HorizontalAlignment, MouseCursor,
- Point, Rectangle, Size, Vector, VerticalAlignment,
-};
-use std::f32;
-
-impl text_input::Renderer for Renderer {
- type Style = Box<dyn StyleSheet>;
-
- fn default_size(&self) -> u16 {
- // TODO: Make this configurable
- 20
- }
-
- fn measure_value(&self, value: &str, size: u16, font: Font) -> f32 {
- let (mut width, _) = self.text_pipeline.measure(
- value,
- f32::from(size),
- font,
- Size::INFINITY,
- );
-
- let spaces_at_the_end = value.len() - value.trim_end().len();
-
- if spaces_at_the_end > 0 {
- let space_width = self.text_pipeline.space_width(size as f32);
- width += spaces_at_the_end as f32 * space_width;
- }
-
- width
- }
-
- fn offset(
- &self,
- text_bounds: Rectangle,
- size: u16,
- value: &text_input::Value,
- state: &text_input::State,
- font: Font,
- ) -> f32 {
- if state.is_focused() {
- let (_, offset) = measure_cursor_and_scroll_offset(
- self,
- text_bounds,
- value,
- size,
- state.cursor_position(value),
- font,
- );
-
- offset
- } else {
- 0.0
- }
- }
-
- fn draw(
- &mut self,
- bounds: Rectangle,
- text_bounds: Rectangle,
- cursor_position: Point,
- size: u16,
- font: Font,
- placeholder: &str,
- value: &text_input::Value,
- state: &text_input::State,
- style_sheet: &Self::Style,
- ) -> Self::Output {
- let is_mouse_over = bounds.contains(cursor_position);
-
- let style = if state.is_focused() {
- style_sheet.focused()
- } else if is_mouse_over {
- style_sheet.hovered()
- } else {
- style_sheet.active()
- };
-
- let input = Primitive::Quad {
- bounds,
- background: style.background,
- border_radius: style.border_radius,
- border_width: style.border_width,
- border_color: style.border_color,
- };
-
- let text = value.to_string();
-
- let text_value = Primitive::Text {
- content: if text.is_empty() {
- placeholder.to_string()
- } else {
- text.clone()
- },
- color: if text.is_empty() {
- style_sheet.placeholder_color()
- } else {
- style_sheet.value_color()
- }
- .into(),
- font,
- bounds: Rectangle {
- width: f32::INFINITY,
- ..text_bounds
- },
- size: f32::from(size),
- horizontal_alignment: HorizontalAlignment::Left,
- vertical_alignment: VerticalAlignment::Center,
- };
-
- let (contents_primitive, offset) = if state.is_focused() {
- let (text_value_width, offset) = measure_cursor_and_scroll_offset(
- self,
- text_bounds,
- value,
- size,
- state.cursor_position(value),
- font,
- );
-
- let cursor = Primitive::Quad {
- bounds: Rectangle {
- x: text_bounds.x + text_value_width,
- y: text_bounds.y,
- width: 1.0,
- height: text_bounds.height,
- },
- background: Background::Color(style_sheet.value_color()),
- border_radius: 0,
- border_width: 0,
- border_color: Color::TRANSPARENT,
- };
-
- (
- Primitive::Group {
- primitives: vec![text_value, cursor],
- },
- Vector::new(offset as u32, 0),
- )
- } else {
- (text_value, Vector::new(0, 0))
- };
-
- let contents = Primitive::Clip {
- bounds: text_bounds,
- offset,
- content: Box::new(contents_primitive),
- };
-
- (
- Primitive::Group {
- primitives: vec![input, contents],
- },
- if is_mouse_over {
- MouseCursor::Text
- } else {
- MouseCursor::OutOfBounds
- },
- )
- }
-}
-
-fn measure_cursor_and_scroll_offset(
- renderer: &Renderer,
- text_bounds: Rectangle,
- value: &text_input::Value,
- size: u16,
- cursor_index: usize,
- font: Font,
-) -> (f32, f32) {
- use iced_native::text_input::Renderer;
-
- let text_before_cursor = value.until(cursor_index).to_string();
-
- let text_value_width =
- renderer.measure_value(&text_before_cursor, size, font);
- let offset = ((text_value_width + 5.0) - text_bounds.width).max(0.0);
-
- (text_value_width, offset)
-}
diff --git a/wgpu/src/settings.rs b/wgpu/src/settings.rs
index f946ce0d..26763e22 100644
--- a/wgpu/src/settings.rs
+++ b/wgpu/src/settings.rs
@@ -1,22 +1,31 @@
-//! Configure a [`Renderer`].
-//!
-//! [`Renderer`]: struct.Renderer.html
+//! Configure a renderer.
+pub use crate::Antialiasing;
-/// The settings of a [`Renderer`].
+/// The settings of a [`Backend`].
///
-/// [`Renderer`]: ../struct.Renderer.html
+/// [`Backend`]: crate::Backend
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Settings {
- /// The output format of the [`Renderer`].
+ /// The output format of the [`Backend`].
///
- /// [`Renderer`]: ../struct.Renderer.html
+ /// [`Backend`]: crate::Backend
pub format: wgpu::TextureFormat,
+ /// The present mode of the [`Backend`].
+ ///
+ /// [`Backend`]: crate::Backend
+ pub present_mode: wgpu::PresentMode,
+
/// The bytes of the font that will be used by default.
///
/// If `None` is provided, a default system font will be chosen.
pub default_font: Option<&'static [u8]>,
+ /// The default size of text.
+ ///
+ /// By default, it will be set to 20.
+ pub default_text_size: u16,
+
/// The antialiasing strategy that will be used for triangle primitives.
pub antialiasing: Option<Antialiasing>,
}
@@ -25,32 +34,10 @@ impl Default for Settings {
fn default() -> Settings {
Settings {
format: wgpu::TextureFormat::Bgra8UnormSrgb,
+ present_mode: wgpu::PresentMode::Mailbox,
default_font: None,
+ default_text_size: 20,
antialiasing: None,
}
}
}
-
-/// An antialiasing strategy.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum Antialiasing {
- /// Multisample AA with 2 samples
- MSAAx2,
- /// Multisample AA with 4 samples
- MSAAx4,
- /// Multisample AA with 8 samples
- MSAAx8,
- /// Multisample AA with 16 samples
- MSAAx16,
-}
-
-impl Antialiasing {
- pub(crate) fn sample_count(&self) -> u32 {
- match self {
- Antialiasing::MSAAx2 => 2,
- Antialiasing::MSAAx4 => 4,
- Antialiasing::MSAAx8 => 8,
- Antialiasing::MSAAx16 => 16,
- }
- }
-}
diff --git a/wgpu/src/shader/blit.vert b/wgpu/src/shader/blit.vert
index 1c081b9e..899cd39d 100644
--- a/wgpu/src/shader/blit.vert
+++ b/wgpu/src/shader/blit.vert
@@ -3,12 +3,12 @@
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)
+ vec2(-1.0, 1.0),
+ vec2(1.0, 1.0),
+ vec2(1.0, -1.0)
);
const vec2 uvs[6] = vec2[6](
diff --git a/wgpu/src/shader/blit.vert.spv b/wgpu/src/shader/blit.vert.spv
index ad697d48..e0b436ce 100644
--- a/wgpu/src/shader/blit.vert.spv
+++ b/wgpu/src/shader/blit.vert.spv
Binary files differ
diff --git a/wgpu/src/shader/quad.vert b/wgpu/src/shader/quad.vert
index 1d9a4fd2..09a278b1 100644
--- a/wgpu/src/shader/quad.vert
+++ b/wgpu/src/shader/quad.vert
@@ -24,6 +24,11 @@ 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),
diff --git a/wgpu/src/shader/quad.vert.spv b/wgpu/src/shader/quad.vert.spv
index 7059b51b..fa71ba1e 100644
--- a/wgpu/src/shader/quad.vert.spv
+++ b/wgpu/src/shader/quad.vert.spv
Binary files differ
diff --git a/wgpu/src/target.rs b/wgpu/src/target.rs
deleted file mode 100644
index 1e72c0c3..00000000
--- a/wgpu/src/target.rs
+++ /dev/null
@@ -1,14 +0,0 @@
-use crate::Viewport;
-
-/// A rendering target.
-#[derive(Debug)]
-pub struct Target<'a> {
- /// The texture where graphics will be rendered.
- pub texture: &'a wgpu::TextureView,
-
- /// The viewport of the target.
- ///
- /// Most of the time, you will want this to match the dimensions of the
- /// texture.
- pub viewport: &'a Viewport,
-}
diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs
index c5670102..78999cf8 100644
--- a/wgpu/src/text.rs
+++ b/wgpu/src/text.rs
@@ -1,84 +1,71 @@
-mod font;
-
use crate::Transformation;
-
+use iced_graphics::font;
use std::{cell::RefCell, collections::HashMap};
-
-pub const BUILTIN_ICONS: iced_native::Font = iced_native::Font::External {
- name: "iced_wgpu icons",
- bytes: include_bytes!("text/icons.ttf"),
-};
-
-pub const CHECKMARK_ICON: char = '\u{F00C}';
-
-const FALLBACK_FONT: &[u8] = include_bytes!("../fonts/Lato-Regular.ttf");
+use wgpu_glyph::ab_glyph;
#[derive(Debug)]
pub struct Pipeline {
- draw_brush: RefCell<wgpu_glyph::GlyphBrush<'static, ()>>,
+ draw_brush: RefCell<wgpu_glyph::GlyphBrush<()>>,
draw_font_map: RefCell<HashMap<String, wgpu_glyph::FontId>>,
-
- measure_brush: RefCell<glyph_brush::GlyphBrush<'static, ()>>,
+ measure_brush: RefCell<glyph_brush::GlyphBrush<()>>,
}
impl Pipeline {
pub fn new(
- device: &mut wgpu::Device,
+ device: &wgpu::Device,
format: wgpu::TextureFormat,
default_font: Option<&[u8]>,
) -> Self {
- // TODO: Font customization
- let font_source = font::Source::new();
+ let default_font = default_font.map(|slice| slice.to_vec());
- let default_font =
- default_font.map(|slice| slice.to_vec()).unwrap_or_else(|| {
- font_source
+ // TODO: Font customization
+ #[cfg(feature = "default_system_font")]
+ let default_font = {
+ default_font.or_else(|| {
+ font::Source::new()
.load(&[font::Family::SansSerif, font::Family::Serif])
- .unwrap_or_else(|_| FALLBACK_FONT.to_vec())
- });
-
- let load_glyph_brush = |font: Vec<u8>| {
- let builder =
- wgpu_glyph::GlyphBrushBuilder::using_fonts_bytes(vec![
- font.clone()
- ])?;
-
- Ok((
- builder,
- glyph_brush::GlyphBrushBuilder::using_font_bytes(font).build(),
- ))
+ .ok()
+ })
};
- let (brush_builder, measure_brush) = load_glyph_brush(default_font)
- .unwrap_or_else(|_: wgpu_glyph::rusttype::Error| {
- log::warn!("System font failed to load. Falling back to embedded font...");
+ let default_font =
+ default_font.unwrap_or_else(|| font::FALLBACK.to_vec());
- load_glyph_brush(FALLBACK_FONT.to_vec()).expect("Load fallback font")
+ let font = ab_glyph::FontArc::try_from_vec(default_font)
+ .unwrap_or_else(|_| {
+ log::warn!(
+ "System font failed to load. Falling back to \
+ embedded font..."
+ );
+
+ ab_glyph::FontArc::try_from_slice(font::FALLBACK)
+ .expect("Load fallback font")
});
- let draw_brush = brush_builder
- .initial_cache_size((2048, 2048))
- .build(device, format);
+ 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
+ .build(device, format);
+
+ let measure_brush =
+ glyph_brush::GlyphBrushBuilder::using_font(font).build();
Pipeline {
draw_brush: RefCell::new(draw_brush),
draw_font_map: RefCell::new(HashMap::new()),
-
measure_brush: RefCell::new(measure_brush),
}
}
- pub fn overlay_font(&self) -> wgpu_glyph::FontId {
- wgpu_glyph::FontId(0)
- }
-
pub fn queue(&mut self, section: wgpu_glyph::Section<'_>) {
self.draw_brush.borrow_mut().queue(section);
}
pub fn draw_queued(
&mut self,
- device: &mut wgpu::Device,
+ device: &wgpu::Device,
+ staging_belt: &mut wgpu::util::StagingBelt,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
transformation: Transformation,
@@ -88,6 +75,7 @@ impl Pipeline {
.borrow_mut()
.draw_queued_with_transform_and_scissoring(
device,
+ staging_belt,
encoder,
target,
transformation.into(),
@@ -108,10 +96,13 @@ impl Pipeline {
let wgpu_glyph::FontId(font_id) = self.find_font(font);
let section = wgpu_glyph::Section {
- text: content,
- scale: wgpu_glyph::Scale { x: size, y: size },
bounds: (bounds.width, bounds.height),
- font_id: wgpu_glyph::FontId(font_id),
+ text: vec![wgpu_glyph::Text {
+ text: content,
+ scale: size.into(),
+ font_id: wgpu_glyph::FontId(font_id),
+ extra: wgpu_glyph::Extra::default(),
+ }],
..Default::default()
};
@@ -124,21 +115,7 @@ impl Pipeline {
}
}
- pub fn space_width(&self, size: f32) -> f32 {
- use wgpu_glyph::GlyphCruncher;
-
- let glyph_brush = self.measure_brush.borrow();
-
- // TODO: Select appropriate font
- let font = &glyph_brush.fonts()[0];
-
- font.glyph(' ')
- .scaled(wgpu_glyph::Scale { x: size, y: size })
- .h_metrics()
- .advance_width
- }
-
- pub fn clear_measurement_cache(&mut self) {
+ pub fn trim_measurement_cache(&mut self) {
// TODO: We should probably use a `GlyphCalculator` for this. However,
// it uses a lifetimed `GlyphCalculatorGuard` with side-effects on drop.
// This makes stuff quite inconvenient. A manual method for trimming the
@@ -170,11 +147,12 @@ impl Pipeline {
return *font_id;
}
- // TODO: Find a way to share font data
- let _ = self.measure_brush.borrow_mut().add_font_bytes(bytes);
+ let font = ab_glyph::FontArc::try_from_slice(bytes)
+ .expect("Load font");
+
+ let _ = self.measure_brush.borrow_mut().add_font(font.clone());
- let font_id =
- self.draw_brush.borrow_mut().add_font_bytes(bytes);
+ let font_id = self.draw_brush.borrow_mut().add_font(font);
let _ = self
.draw_font_map
diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs
index 51a6f954..61a771d8 100644
--- a/wgpu/src/triangle.rs
+++ b/wgpu/src/triangle.rs
@@ -1,18 +1,23 @@
//! Draw meshes of triangles.
use crate::{settings, Transformation};
-use iced_native::{Point, Rectangle};
+use iced_graphics::layer;
+
+use bytemuck::{Pod, Zeroable};
use std::mem;
+pub use iced_graphics::triangle::{Mesh2D, Vertex2D};
+
mod msaa;
-const UNIFORM_BUFFER_SIZE: usize = 100;
-const VERTEX_BUFFER_SIZE: usize = 100_000;
-const INDEX_BUFFER_SIZE: usize = 100_000;
+const UNIFORM_BUFFER_SIZE: usize = 50;
+const VERTEX_BUFFER_SIZE: usize = 10_000;
+const INDEX_BUFFER_SIZE: usize = 10_000;
#[derive(Debug)]
pub(crate) struct Pipeline {
pipeline: wgpu::RenderPipeline,
blit: Option<msaa::Blit>,
+ constants_layout: wgpu::BindGroupLayout,
constants: wgpu::BindGroup,
uniforms_buffer: Buffer<Uniforms>,
vertex_buffer: Buffer<Vertex2D>,
@@ -21,6 +26,7 @@ pub(crate) struct Pipeline {
#[derive(Debug)]
struct Buffer<T> {
+ label: &'static str,
raw: wgpu::Buffer,
size: usize,
usage: wgpu::BufferUsage,
@@ -29,16 +35,20 @@ struct Buffer<T> {
impl<T> Buffer<T> {
pub fn new(
+ label: &'static str,
device: &wgpu::Device,
size: usize,
usage: wgpu::BufferUsage,
) -> Self {
let raw = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some(label),
size: (std::mem::size_of::<T>() * size) as u64,
usage,
+ mapped_at_creation: false,
});
Buffer {
+ label,
raw,
size,
usage,
@@ -46,34 +56,48 @@ impl<T> Buffer<T> {
}
}
- pub fn ensure_capacity(&mut self, device: &wgpu::Device, size: usize) {
- if self.size < size {
+ pub fn expand(&mut self, device: &wgpu::Device, size: usize) -> bool {
+ let needs_resize = self.size < size;
+
+ if needs_resize {
self.raw = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some(self.label),
size: (std::mem::size_of::<T>() * size) as u64,
usage: self.usage,
+ mapped_at_creation: false,
});
self.size = size;
}
+
+ needs_resize
}
}
impl Pipeline {
pub fn new(
- device: &mut wgpu::Device,
+ device: &wgpu::Device,
format: wgpu::TextureFormat,
antialiasing: Option<settings::Antialiasing>,
) -> Pipeline {
- let constant_layout =
+ let constants_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
- bindings: &[wgpu::BindGroupLayoutBinding {
+ label: Some("iced_wgpu::triangle uniforms layout"),
+ entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStage::VERTEX,
- ty: wgpu::BindingType::UniformBuffer { dynamic: true },
+ ty: wgpu::BindingType::UniformBuffer {
+ dynamic: true,
+ min_binding_size: wgpu::BufferSize::new(
+ mem::size_of::<Uniforms>() as u64,
+ ),
+ },
+ count: None,
}],
});
let constants_buffer = Buffer::new(
+ "iced_wgpu::triangle uniforms buffer",
device,
UNIFORM_BUFFER_SIZE,
wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST,
@@ -81,36 +105,37 @@ impl Pipeline {
let constant_bind_group =
device.create_bind_group(&wgpu::BindGroupDescriptor {
- layout: &constant_layout,
- bindings: &[wgpu::Binding {
+ label: Some("iced_wgpu::triangle uniforms bind group"),
+ layout: &constants_layout,
+ entries: &[wgpu::BindGroupEntry {
binding: 0,
- resource: wgpu::BindingResource::Buffer {
- buffer: &constants_buffer.raw,
- range: 0..std::mem::size_of::<Uniforms>() as u64,
- },
+ resource: wgpu::BindingResource::Buffer(
+ constants_buffer
+ .raw
+ .slice(0..std::mem::size_of::<Uniforms>() as u64),
+ ),
}],
});
let layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
- bind_group_layouts: &[&constant_layout],
+ label: Some("iced_wgpu::triangle pipeline layout"),
+ push_constant_ranges: &[],
+ bind_group_layouts: &[&constants_layout],
});
- let vs = include_bytes!("shader/triangle.vert.spv");
- let vs_module = device.create_shader_module(
- &wgpu::read_spirv(std::io::Cursor::new(&vs[..]))
- .expect("Read triangle vertex shader as SPIR-V"),
- );
+ let vs_module = device.create_shader_module(wgpu::include_spirv!(
+ "shader/triangle.vert.spv"
+ ));
- let fs = include_bytes!("shader/triangle.frag.spv");
- let fs_module = device.create_shader_module(
- &wgpu::read_spirv(std::io::Cursor::new(&fs[..]))
- .expect("Read triangle fragment shader as SPIR-V"),
- );
+ let fs_module = device.create_shader_module(wgpu::include_spirv!(
+ "shader/triangle.frag.spv"
+ ));
let pipeline =
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
- layout: &layout,
+ label: Some("iced_wgpu::triangle pipeline"),
+ layout: Some(&layout),
vertex_stage: wgpu::ProgrammableStageDescriptor {
module: &vs_module,
entry_point: "main",
@@ -122,9 +147,7 @@ impl Pipeline {
rasterization_state: Some(wgpu::RasterizationStateDescriptor {
front_face: wgpu::FrontFace::Cw,
cull_mode: wgpu::CullMode::None,
- depth_bias: 0,
- depth_bias_slope_scale: 0.0,
- depth_bias_clamp: 0.0,
+ ..Default::default()
}),
primitive_topology: wgpu::PrimitiveTopology::TriangleList,
color_states: &[wgpu::ColorStateDescriptor {
@@ -142,28 +165,30 @@ impl Pipeline {
write_mask: wgpu::ColorWrite::ALL,
}],
depth_stencil_state: None,
- index_format: wgpu::IndexFormat::Uint32,
- vertex_buffers: &[wgpu::VertexBufferDescriptor {
- stride: mem::size_of::<Vertex2D>() as u64,
- step_mode: wgpu::InputStepMode::Vertex,
- attributes: &[
- // Position
- wgpu::VertexAttributeDescriptor {
- shader_location: 0,
- format: wgpu::VertexFormat::Float2,
- offset: 0,
- },
- // Color
- wgpu::VertexAttributeDescriptor {
- shader_location: 1,
- format: wgpu::VertexFormat::Float4,
- offset: 4 * 2,
- },
- ],
- }],
- sample_count: antialiasing
- .map(|a| a.sample_count())
- .unwrap_or(1),
+ vertex_state: wgpu::VertexStateDescriptor {
+ index_format: wgpu::IndexFormat::Uint32,
+ vertex_buffers: &[wgpu::VertexBufferDescriptor {
+ stride: mem::size_of::<Vertex2D>() as u64,
+ step_mode: wgpu::InputStepMode::Vertex,
+ attributes: &[
+ // Position
+ wgpu::VertexAttributeDescriptor {
+ shader_location: 0,
+ format: wgpu::VertexFormat::Float2,
+ offset: 0,
+ },
+ // Color
+ wgpu::VertexAttributeDescriptor {
+ shader_location: 1,
+ format: wgpu::VertexFormat::Float4,
+ offset: 4 * 2,
+ },
+ ],
+ }],
+ },
+ sample_count: u32::from(
+ antialiasing.map(|a| a.sample_count()).unwrap_or(1),
+ ),
sample_mask: !0,
alpha_to_coverage_enabled: false,
});
@@ -171,14 +196,17 @@ impl Pipeline {
Pipeline {
pipeline,
blit: antialiasing.map(|a| msaa::Blit::new(device, format, a)),
+ constants_layout,
constants: constant_bind_group,
uniforms_buffer: constants_buffer,
vertex_buffer: Buffer::new(
+ "iced_wgpu::triangle vertex buffer",
device,
VERTEX_BUFFER_SIZE,
wgpu::BufferUsage::VERTEX | wgpu::BufferUsage::COPY_DST,
),
index_buffer: Buffer::new(
+ "iced_wgpu::triangle index buffer",
device,
INDEX_BUFFER_SIZE,
wgpu::BufferUsage::INDEX | wgpu::BufferUsage::COPY_DST,
@@ -188,30 +216,50 @@ impl Pipeline {
pub fn draw(
&mut self,
- device: &mut wgpu::Device,
+ device: &wgpu::Device,
+ staging_belt: &mut wgpu::util::StagingBelt,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
target_width: u32,
target_height: u32,
transformation: Transformation,
- meshes: &[(Point, &Mesh2D)],
- bounds: Rectangle<u32>,
+ scale_factor: f32,
+ meshes: &[layer::Mesh<'_>],
) {
// This looks a bit crazy, but we are just counting how many vertices
// and indices we will need to handle.
// TODO: Improve readability
let (total_vertices, total_indices) = meshes
.iter()
- .map(|(_, mesh)| (mesh.vertices.len(), mesh.indices.len()))
+ .map(|layer::Mesh { buffers, .. }| {
+ (buffers.vertices.len(), buffers.indices.len())
+ })
.fold((0, 0), |(total_v, total_i), (v, i)| {
(total_v + v, total_i + i)
});
// Then we ensure the current buffers are big enough, resizing if
// necessary
- self.uniforms_buffer.ensure_capacity(device, meshes.len());
- self.vertex_buffer.ensure_capacity(device, total_vertices);
- self.index_buffer.ensure_capacity(device, total_indices);
+ let _ = self.vertex_buffer.expand(device, total_vertices);
+ let _ = self.index_buffer.expand(device, total_indices);
+
+ // If the uniforms buffer is resized, then we need to recreate its
+ // bind group.
+ if self.uniforms_buffer.expand(device, meshes.len()) {
+ self.constants =
+ device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("iced_wgpu::triangle uniforms buffer"),
+ layout: &self.constants_layout,
+ entries: &[wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::Buffer(
+ self.uniforms_buffer.raw.slice(
+ 0..std::mem::size_of::<Uniforms>() as u64,
+ ),
+ ),
+ }],
+ });
+ }
let mut uniforms: Vec<Uniforms> = Vec::with_capacity(meshes.len());
let mut offsets: Vec<(
@@ -223,73 +271,85 @@ impl Pipeline {
let mut last_index = 0;
// We upload everything upfront
- for (origin, mesh) in meshes {
- let transform = Uniforms {
- transform: (transformation
- * Transformation::translate(origin.x, origin.y))
- .into(),
- };
-
- let vertex_buffer = device
- .create_buffer_mapped(
- mesh.vertices.len(),
- wgpu::BufferUsage::COPY_SRC,
- )
- .fill_from_slice(&mesh.vertices);
-
- let index_buffer = device
- .create_buffer_mapped(
- mesh.indices.len(),
- wgpu::BufferUsage::COPY_SRC,
- )
- .fill_from_slice(&mesh.indices);
-
- encoder.copy_buffer_to_buffer(
- &vertex_buffer,
- 0,
- &self.vertex_buffer.raw,
- (std::mem::size_of::<Vertex2D>() * last_vertex) as u64,
- (std::mem::size_of::<Vertex2D>() * mesh.vertices.len()) as u64,
- );
+ for mesh in meshes {
+ let transform = (transformation
+ * Transformation::translate(mesh.origin.x, mesh.origin.y))
+ .into();
+
+ let vertices = bytemuck::cast_slice(&mesh.buffers.vertices);
+ let indices = bytemuck::cast_slice(&mesh.buffers.indices);
+
+ match (
+ wgpu::BufferSize::new(vertices.len() as u64),
+ wgpu::BufferSize::new(indices.len() as u64),
+ ) {
+ (Some(vertices_size), Some(indices_size)) => {
+ {
+ let mut vertex_buffer = staging_belt.write_buffer(
+ encoder,
+ &self.vertex_buffer.raw,
+ (std::mem::size_of::<Vertex2D>() * last_vertex)
+ as u64,
+ vertices_size,
+ device,
+ );
+
+ vertex_buffer.copy_from_slice(vertices);
+ }
+
+ {
+ let mut index_buffer = staging_belt.write_buffer(
+ encoder,
+ &self.index_buffer.raw,
+ (std::mem::size_of::<u32>() * last_index) as u64,
+ indices_size,
+ device,
+ );
+
+ index_buffer.copy_from_slice(indices);
+ }
+
+ uniforms.push(transform);
+ offsets.push((
+ last_vertex as u64,
+ last_index as u64,
+ mesh.buffers.indices.len(),
+ ));
+
+ last_vertex += mesh.buffers.vertices.len();
+ last_index += mesh.buffers.indices.len();
+ }
+ _ => {}
+ }
+ }
+
+ let uniforms = bytemuck::cast_slice(&uniforms);
- encoder.copy_buffer_to_buffer(
- &index_buffer,
+ if let Some(uniforms_size) =
+ wgpu::BufferSize::new(uniforms.len() as u64)
+ {
+ let mut uniforms_buffer = staging_belt.write_buffer(
+ encoder,
+ &self.uniforms_buffer.raw,
0,
- &self.index_buffer.raw,
- (std::mem::size_of::<u32>() * last_index) as u64,
- (std::mem::size_of::<u32>() * mesh.indices.len()) as u64,
+ uniforms_size,
+ device,
);
- uniforms.push(transform);
- offsets.push((
- last_vertex as u64,
- last_index as u64,
- mesh.indices.len(),
- ));
-
- last_vertex += mesh.vertices.len();
- last_index += mesh.indices.len();
+ uniforms_buffer.copy_from_slice(uniforms);
}
- let uniforms_buffer = device
- .create_buffer_mapped(uniforms.len(), wgpu::BufferUsage::COPY_SRC)
- .fill_from_slice(&uniforms);
-
- encoder.copy_buffer_to_buffer(
- &uniforms_buffer,
- 0,
- &self.uniforms_buffer.raw,
- 0,
- (std::mem::size_of::<Uniforms>() * uniforms.len()) as u64,
- );
-
{
- let (attachment, resolve_target, load_op) =
+ let (attachment, resolve_target, load) =
if let Some(blit) = &mut self.blit {
let (attachment, resolve_target) =
blit.targets(device, target_width, target_height);
- (attachment, Some(resolve_target), wgpu::LoadOp::Clear)
+ (
+ attachment,
+ Some(resolve_target),
+ wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
+ )
} else {
(target, None, wgpu::LoadOp::Load)
};
@@ -300,47 +360,43 @@ impl Pipeline {
wgpu::RenderPassColorAttachmentDescriptor {
attachment,
resolve_target,
- load_op,
- store_op: wgpu::StoreOp::Store,
- clear_color: wgpu::Color {
- r: 0.0,
- g: 0.0,
- b: 0.0,
- a: 0.0,
- },
+ ops: wgpu::Operations { load, store: true },
},
],
depth_stencil_attachment: None,
});
render_pass.set_pipeline(&self.pipeline);
- render_pass.set_scissor_rect(
- bounds.x,
- bounds.y,
- bounds.width,
- bounds.height,
- );
for (i, (vertex_offset, index_offset, indices)) in
offsets.into_iter().enumerate()
{
+ let clip_bounds = (meshes[i].clip_bounds * scale_factor).snap();
+
+ render_pass.set_scissor_rect(
+ clip_bounds.x,
+ clip_bounds.y,
+ clip_bounds.width,
+ clip_bounds.height,
+ );
+
render_pass.set_bind_group(
0,
&self.constants,
- &[(std::mem::size_of::<Uniforms>() * i) as u64],
+ &[(std::mem::size_of::<Uniforms>() * i) as u32],
);
render_pass.set_index_buffer(
- &self.index_buffer.raw,
- index_offset * std::mem::size_of::<u32>() as u64,
+ self.index_buffer
+ .raw
+ .slice(index_offset * mem::size_of::<u32>() as u64..),
);
- render_pass.set_vertex_buffers(
+ render_pass.set_vertex_buffer(
0,
- &[(
- &self.vertex_buffer.raw,
- vertex_offset * std::mem::size_of::<Vertex2D>() as u64,
- )],
+ self.vertex_buffer.raw.slice(
+ vertex_offset * mem::size_of::<Vertex2D>() as u64..,
+ ),
);
render_pass.draw_indexed(0..indices as u32, 0, 0..1);
@@ -354,38 +410,31 @@ impl Pipeline {
}
#[repr(C)]
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone, Copy, Zeroable, Pod)]
struct Uniforms {
transform: [f32; 16],
+ // We need to align this to 256 bytes to please `wgpu`...
+ // TODO: Be smarter and stop wasting memory!
+ _padding_a: [f32; 32],
+ _padding_b: [f32; 16],
}
impl Default for Uniforms {
fn default() -> Self {
Self {
transform: *Transformation::identity().as_ref(),
+ _padding_a: [0.0; 32],
+ _padding_b: [0.0; 16],
}
}
}
-/// A two-dimensional vertex with some color in __linear__ RGBA.
-#[repr(C)]
-#[derive(Copy, Clone, Debug)]
-pub struct Vertex2D {
- /// The vertex position
- pub position: [f32; 2],
- /// The vertex color in __linear__ RGBA.
- pub color: [f32; 4],
-}
-
-/// A set of [`Vertex2D`] and indices representing a list of triangles.
-///
-/// [`Vertex2D`]: struct.Vertex2D.html
-#[derive(Clone, Debug)]
-pub struct Mesh2D {
- /// The vertices of the mesh
- pub vertices: Vec<Vertex2D>,
- /// The list of vertex indices that defines the triangles of the mesh.
- ///
- /// Therefore, this list should always have a length that is a multiple of 3.
- pub indices: Vec<u32>,
+impl From<Transformation> for Uniforms {
+ fn from(transformation: Transformation) -> Uniforms {
+ Self {
+ transform: transformation.into(),
+ _padding_a: [0.0; 32],
+ _padding_b: [0.0; 16],
+ }
+ }
}
diff --git a/wgpu/src/triangle/msaa.rs b/wgpu/src/triangle/msaa.rs
index 7ccfb062..db86f748 100644
--- a/wgpu/src/triangle/msaa.rs
+++ b/wgpu/src/triangle/msaa.rs
@@ -23,24 +23,25 @@ impl Blit {
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::FilterMode::Linear,
- lod_min_clamp: -100.0,
- lod_max_clamp: 100.0,
- compare_function: wgpu::CompareFunction::Always,
+ ..Default::default()
});
let constant_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
- bindings: &[wgpu::BindGroupLayoutBinding {
+ label: Some("iced_wgpu::triangle:msaa uniforms layout"),
+ entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStage::FRAGMENT,
- ty: wgpu::BindingType::Sampler,
+ ty: wgpu::BindingType::Sampler { comparison: false },
+ count: None,
}],
});
let constant_bind_group =
device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("iced_wgpu::triangle::msaa uniforms bind group"),
layout: &constant_layout,
- bindings: &[wgpu::Binding {
+ entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Sampler(&sampler),
}],
@@ -48,36 +49,38 @@ impl Blit {
let texture_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
- bindings: &[wgpu::BindGroupLayoutBinding {
+ label: Some("iced_wgpu::triangle::msaa texture layout"),
+ entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStage::FRAGMENT,
ty: wgpu::BindingType::SampledTexture {
- multisampled: false,
dimension: wgpu::TextureViewDimension::D2,
+ component_type: wgpu::TextureComponentType::Float,
+ multisampled: false,
},
+ count: None,
}],
});
let layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("iced_wgpu::triangle::msaa pipeline layout"),
+ push_constant_ranges: &[],
bind_group_layouts: &[&constant_layout, &texture_layout],
});
- let vs = include_bytes!("../shader/blit.vert.spv");
- let vs_module = device.create_shader_module(
- &wgpu::read_spirv(std::io::Cursor::new(&vs[..]))
- .expect("Read blit vertex shader as SPIR-V"),
- );
+ let vs_module = device.create_shader_module(wgpu::include_spirv!(
+ "../shader/blit.vert.spv"
+ ));
- let fs = include_bytes!("../shader/blit.frag.spv");
- let fs_module = device.create_shader_module(
- &wgpu::read_spirv(std::io::Cursor::new(&fs[..]))
- .expect("Read blit fragment shader as SPIR-V"),
- );
+ let fs_module = device.create_shader_module(wgpu::include_spirv!(
+ "../shader/blit.frag.spv"
+ ));
let pipeline =
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
- layout: &layout,
+ label: Some("iced_wgpu::triangle::msaa pipeline"),
+ layout: Some(&layout),
vertex_stage: wgpu::ProgrammableStageDescriptor {
module: &vs_module,
entry_point: "main",
@@ -89,9 +92,7 @@ impl Blit {
rasterization_state: Some(wgpu::RasterizationStateDescriptor {
front_face: wgpu::FrontFace::Cw,
cull_mode: wgpu::CullMode::None,
- depth_bias: 0,
- depth_bias_slope_scale: 0.0,
- depth_bias_clamp: 0.0,
+ ..Default::default()
}),
primitive_topology: wgpu::PrimitiveTopology::TriangleList,
color_states: &[wgpu::ColorStateDescriptor {
@@ -109,8 +110,10 @@ impl Blit {
write_mask: wgpu::ColorWrite::ALL,
}],
depth_stencil_state: None,
- index_format: wgpu::IndexFormat::Uint16,
- vertex_buffers: &[],
+ vertex_state: wgpu::VertexStateDescriptor {
+ index_format: wgpu::IndexFormat::Uint16,
+ vertex_buffers: &[],
+ },
sample_count: 1,
sample_mask: !0,
alpha_to_coverage_enabled: false,
@@ -120,7 +123,7 @@ impl Blit {
format,
pipeline,
constants: constant_bind_group,
- texture_layout: texture_layout,
+ texture_layout,
sample_count: antialiasing.sample_count(),
targets: None,
}
@@ -173,13 +176,9 @@ impl Blit {
wgpu::RenderPassColorAttachmentDescriptor {
attachment: target,
resolve_target: None,
- load_op: wgpu::LoadOp::Load,
- store_op: wgpu::StoreOp::Store,
- clear_color: wgpu::Color {
- r: 0.0,
- g: 0.0,
- b: 0.0,
- a: 0.0,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Load,
+ store: true,
},
},
],
@@ -222,8 +221,8 @@ impl Targets {
};
let attachment = device.create_texture(&wgpu::TextureDescriptor {
+ label: Some("iced_wgpu::triangle::msaa attachment"),
size: extent,
- array_layer_count: 1,
mip_level_count: 1,
sample_count,
dimension: wgpu::TextureDimension::D2,
@@ -232,8 +231,8 @@ impl Targets {
});
let resolve = device.create_texture(&wgpu::TextureDescriptor {
+ label: Some("iced_wgpu::triangle::msaa resolve target"),
size: extent,
- array_layer_count: 1,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
@@ -242,12 +241,16 @@ impl Targets {
| wgpu::TextureUsage::SAMPLED,
});
- let attachment = attachment.create_default_view();
- let resolve = resolve.create_default_view();
+ let attachment =
+ attachment.create_view(&wgpu::TextureViewDescriptor::default());
+
+ let resolve =
+ resolve.create_view(&wgpu::TextureViewDescriptor::default());
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("iced_wgpu::triangle::msaa texture bind group"),
layout: texture_layout,
- bindings: &[wgpu::Binding {
+ entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&resolve),
}],
diff --git a/wgpu/src/viewport.rs b/wgpu/src/viewport.rs
deleted file mode 100644
index 66242468..00000000
--- a/wgpu/src/viewport.rs
+++ /dev/null
@@ -1,29 +0,0 @@
-use crate::Transformation;
-
-/// A viewing region for displaying computer graphics.
-#[derive(Debug)]
-pub struct Viewport {
- width: u32,
- height: u32,
- transformation: Transformation,
-}
-
-impl Viewport {
- /// Creates a new [`Viewport`] with the given dimensions.
- pub fn new(width: u32, height: u32) -> Viewport {
- Viewport {
- width,
- height,
- transformation: Transformation::orthographic(width, height),
- }
- }
-
- /// Returns the dimensions of the [`Viewport`].
- pub fn dimensions(&self) -> (u32, u32) {
- (self.width, self.height)
- }
-
- pub(crate) fn transformation(&self) -> Transformation {
- self.transformation
- }
-}
diff --git a/wgpu/src/widget.rs b/wgpu/src/widget.rs
index 73cce7e2..177ae1b6 100644
--- a/wgpu/src/widget.rs
+++ b/wgpu/src/widget.rs
@@ -7,11 +7,16 @@
//! ```
//! use iced_wgpu::{button, Button};
//! ```
+use crate::Renderer;
+
pub mod button;
pub mod checkbox;
pub mod container;
+pub mod pane_grid;
+pub mod pick_list;
pub mod progress_bar;
pub mod radio;
+pub mod rule;
pub mod scrollable;
pub mod slider;
pub mod text_input;
@@ -23,10 +28,16 @@ pub use checkbox::Checkbox;
#[doc(no_inline)]
pub use container::Container;
#[doc(no_inline)]
+pub use pane_grid::PaneGrid;
+#[doc(no_inline)]
+pub use pick_list::PickList;
+#[doc(no_inline)]
pub use progress_bar::ProgressBar;
#[doc(no_inline)]
pub use radio::Radio;
#[doc(no_inline)]
+pub use rule::Rule;
+#[doc(no_inline)]
pub use scrollable::Scrollable;
#[doc(no_inline)]
pub use slider::Slider;
@@ -34,8 +45,28 @@ pub use slider::Slider;
pub use text_input::TextInput;
#[cfg(feature = "canvas")]
+#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))]
pub mod canvas;
#[cfg(feature = "canvas")]
#[doc(no_inline)]
pub use canvas::Canvas;
+
+#[cfg(feature = "qr_code")]
+#[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))]
+pub mod qr_code;
+
+#[cfg(feature = "qr_code")]
+#[doc(no_inline)]
+pub use qr_code::QRCode;
+
+pub use iced_native::Space;
+
+/// A container that distributes its contents vertically.
+pub type Column<'a, Message> = iced_native::Column<'a, Message, Renderer>;
+
+/// A container that distributes its contents horizontally.
+pub type Row<'a, Message> = iced_native::Row<'a, Message, Renderer>;
+
+/// A paragraph of text.
+pub type Text = iced_native::Text<Renderer>;
diff --git a/wgpu/src/widget/button.rs b/wgpu/src/widget/button.rs
index b738c55e..fc729cd5 100644
--- a/wgpu/src/widget/button.rs
+++ b/wgpu/src/widget/button.rs
@@ -1,13 +1,10 @@
//! Allow your users to perform actions by pressing a button.
//!
//! A [`Button`] has some local [`State`].
-//!
-//! [`Button`]: type.Button.html
-//! [`State`]: struct.State.html
use crate::Renderer;
+pub use iced_graphics::button::{Style, StyleSheet};
pub use iced_native::button::State;
-pub use iced_style::button::{Style, StyleSheet};
/// A widget that produces a message when clicked.
///
diff --git a/wgpu/src/widget/canvas.rs b/wgpu/src/widget/canvas.rs
index 3a9605c9..399dd19c 100644
--- a/wgpu/src/widget/canvas.rs
+++ b/wgpu/src/widget/canvas.rs
@@ -3,149 +3,4 @@
//! A [`Canvas`] widget can be used to draw different kinds of 2D shapes in a
//! [`Frame`]. It can be used for animation, data visualization, game graphics,
//! and more!
-//!
-//! [`Canvas`]: struct.Canvas.html
-//! [`Frame`]: struct.Frame.html
-use crate::{Defaults, Primitive, Renderer};
-
-use iced_native::{
- layout, Element, Hasher, Layout, Length, MouseCursor, Point, Size, Widget,
-};
-use std::hash::Hash;
-
-pub mod layer;
-pub mod path;
-
-mod drawable;
-mod fill;
-mod frame;
-mod stroke;
-mod text;
-
-pub use drawable::Drawable;
-pub use fill::Fill;
-pub use frame::Frame;
-pub use layer::Layer;
-pub use path::Path;
-pub use stroke::{LineCap, LineJoin, Stroke};
-pub use text::Text;
-
-/// A widget capable of drawing 2D graphics.
-///
-/// A [`Canvas`] may contain multiple layers. A [`Layer`] is drawn using the
-/// painter's algorithm. In other words, layers will be drawn on top of each in
-/// the same order they are pushed into the [`Canvas`].
-///
-/// [`Canvas`]: struct.Canvas.html
-/// [`Layer`]: layer/trait.Layer.html
-#[derive(Debug)]
-pub struct Canvas<'a> {
- width: Length,
- height: Length,
- layers: Vec<Box<dyn Layer + 'a>>,
-}
-
-impl<'a> Canvas<'a> {
- const DEFAULT_SIZE: u16 = 100;
-
- /// Creates a new [`Canvas`] with no layers.
- ///
- /// [`Canvas`]: struct.Canvas.html
- pub fn new() -> Self {
- Canvas {
- width: Length::Units(Self::DEFAULT_SIZE),
- height: Length::Units(Self::DEFAULT_SIZE),
- layers: Vec::new(),
- }
- }
-
- /// Sets the width of the [`Canvas`].
- ///
- /// [`Canvas`]: struct.Canvas.html
- pub fn width(mut self, width: Length) -> Self {
- self.width = width;
- self
- }
-
- /// Sets the height of the [`Canvas`].
- ///
- /// [`Canvas`]: struct.Canvas.html
- pub fn height(mut self, height: Length) -> Self {
- self.height = height;
- self
- }
-
- /// Adds a [`Layer`] to the [`Canvas`].
- ///
- /// It will be drawn on top of previous layers.
- ///
- /// [`Layer`]: layer/trait.Layer.html
- /// [`Canvas`]: struct.Canvas.html
- pub fn push(mut self, layer: impl Layer + 'a) -> Self {
- self.layers.push(Box::new(layer));
- self
- }
-}
-
-impl<'a, Message> Widget<Message, Renderer> for Canvas<'a> {
- fn width(&self) -> Length {
- self.width
- }
-
- fn height(&self) -> Length {
- self.height
- }
-
- fn layout(
- &self,
- _renderer: &Renderer,
- limits: &layout::Limits,
- ) -> layout::Node {
- let limits = limits.width(self.width).height(self.height);
- let size = limits.resolve(Size::ZERO);
-
- layout::Node::new(size)
- }
-
- fn draw(
- &self,
- _renderer: &mut Renderer,
- _defaults: &Defaults,
- layout: Layout<'_>,
- _cursor_position: Point,
- ) -> (Primitive, MouseCursor) {
- let bounds = layout.bounds();
- let origin = Point::new(bounds.x, bounds.y);
- let size = Size::new(bounds.width, bounds.height);
-
- (
- Primitive::Group {
- primitives: self
- .layers
- .iter()
- .map(|layer| Primitive::Cached {
- origin,
- cache: layer.draw(size),
- })
- .collect(),
- },
- MouseCursor::Idle,
- )
- }
-
- fn hash_layout(&self, state: &mut Hasher) {
- std::any::TypeId::of::<Canvas<'static>>().hash(state);
-
- self.width.hash(state);
- self.height.hash(state);
- }
-}
-
-impl<'a, Message> From<Canvas<'a>> for Element<'a, Message, Renderer>
-where
- Message: 'static,
-{
- fn from(canvas: Canvas<'a>) -> Element<'a, Message, Renderer> {
- Element::new(canvas)
- }
-}
+pub use iced_graphics::canvas::*;
diff --git a/wgpu/src/widget/canvas/drawable.rs b/wgpu/src/widget/canvas/drawable.rs
deleted file mode 100644
index 6c74071c..00000000
--- a/wgpu/src/widget/canvas/drawable.rs
+++ /dev/null
@@ -1,12 +0,0 @@
-use crate::canvas::Frame;
-
-/// A type that can be drawn on a [`Frame`].
-///
-/// [`Frame`]: struct.Frame.html
-pub trait Drawable {
- /// Draws the [`Drawable`] on the given [`Frame`].
- ///
- /// [`Drawable`]: trait.Drawable.html
- /// [`Frame`]: struct.Frame.html
- fn draw(&self, frame: &mut Frame);
-}
diff --git a/wgpu/src/widget/canvas/fill.rs b/wgpu/src/widget/canvas/fill.rs
deleted file mode 100644
index 5ce24cf3..00000000
--- a/wgpu/src/widget/canvas/fill.rs
+++ /dev/null
@@ -1,14 +0,0 @@
-use iced_native::Color;
-
-/// The style used to fill geometry.
-#[derive(Debug, Clone, Copy)]
-pub enum Fill {
- /// Fill with a color.
- Color(Color),
-}
-
-impl Default for Fill {
- fn default() -> Fill {
- Fill::Color(Color::BLACK)
- }
-}
diff --git a/wgpu/src/widget/canvas/layer.rs b/wgpu/src/widget/canvas/layer.rs
deleted file mode 100644
index a46b7fb1..00000000
--- a/wgpu/src/widget/canvas/layer.rs
+++ /dev/null
@@ -1,25 +0,0 @@
-//! Produce, store, and reuse geometry.
-mod cache;
-
-pub use cache::Cache;
-
-use crate::Primitive;
-use iced_native::Size;
-
-use std::sync::Arc;
-
-/// A layer that can be presented at a [`Canvas`].
-///
-/// [`Canvas`]: ../struct.Canvas.html
-pub trait Layer: std::fmt::Debug {
- /// Draws the [`Layer`] in the given bounds and produces a [`Primitive`] as
- /// a result.
- ///
- /// The [`Layer`] may choose to store the produced [`Primitive`] locally and
- /// only recompute it when the bounds change, its contents change, or is
- /// otherwise explicitly cleared by other means.
- ///
- /// [`Layer`]: trait.Layer.html
- /// [`Primitive`]: ../../../enum.Primitive.html
- fn draw(&self, bounds: Size) -> Arc<Primitive>;
-}
diff --git a/wgpu/src/widget/canvas/layer/cache.rs b/wgpu/src/widget/canvas/layer/cache.rs
deleted file mode 100644
index 6b69f01e..00000000
--- a/wgpu/src/widget/canvas/layer/cache.rs
+++ /dev/null
@@ -1,99 +0,0 @@
-use crate::{
- canvas::{Drawable, Frame, Layer},
- Primitive,
-};
-
-use iced_native::Size;
-use std::{cell::RefCell, marker::PhantomData, sync::Arc};
-
-/// A simple cache that stores generated geometry to avoid recomputation.
-///
-/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer
-/// change or it is explicitly cleared.
-///
-/// [`Layer`]: ../trait.Layer.html
-/// [`Cached`]: struct.Cached.html
-#[derive(Debug)]
-pub struct Cache<T: Drawable> {
- input: PhantomData<T>,
- state: RefCell<State>,
-}
-
-#[derive(Debug)]
-enum State {
- Empty,
- Filled {
- bounds: Size,
- primitive: Arc<Primitive>,
- },
-}
-
-impl<T> Cache<T>
-where
- T: Drawable + std::fmt::Debug,
-{
- /// Creates a new empty [`Cache`].
- ///
- /// [`Cache`]: struct.Cache.html
- pub fn new() -> Self {
- Cache {
- input: PhantomData,
- state: RefCell::new(State::Empty),
- }
- }
-
- /// Clears the cache, forcing a redraw the next time it is used.
- ///
- /// [`Cached`]: struct.Cached.html
- pub fn clear(&mut self) {
- *self.state.borrow_mut() = State::Empty;
- }
-
- /// Binds the [`Cache`] with some data, producing a [`Layer`] that can be
- /// added to a [`Canvas`].
- ///
- /// [`Cache`]: struct.Cache.html
- /// [`Layer`]: ../trait.Layer.html
- /// [`Canvas`]: ../../struct.Canvas.html
- pub fn with<'a>(&'a self, input: &'a T) -> impl Layer + 'a {
- Bind {
- cache: self,
- input: input,
- }
- }
-}
-
-#[derive(Debug)]
-struct Bind<'a, T: Drawable> {
- cache: &'a Cache<T>,
- input: &'a T,
-}
-
-impl<'a, T> Layer for Bind<'a, T>
-where
- T: Drawable + std::fmt::Debug,
-{
- fn draw(&self, current_bounds: Size) -> Arc<Primitive> {
- use std::ops::Deref;
-
- if let State::Filled { bounds, primitive } =
- self.cache.state.borrow().deref()
- {
- if *bounds == current_bounds {
- return primitive.clone();
- }
- }
-
- let mut frame = Frame::new(current_bounds.width, current_bounds.height);
- self.input.draw(&mut frame);
-
- let primitive = Arc::new(frame.into_primitive());
-
- *self.cache.state.borrow_mut() = State::Filled {
- bounds: current_bounds,
- primitive: primitive.clone(),
- };
-
- primitive
- }
-}
diff --git a/wgpu/src/widget/checkbox.rs b/wgpu/src/widget/checkbox.rs
index da0d7a84..d27d77cc 100644
--- a/wgpu/src/widget/checkbox.rs
+++ b/wgpu/src/widget/checkbox.rs
@@ -1,7 +1,7 @@
//! Show toggle controls using checkboxes.
use crate::Renderer;
-pub use iced_style::checkbox::{Style, StyleSheet};
+pub use iced_graphics::checkbox::{Style, StyleSheet};
/// A box that can be checked.
///
diff --git a/wgpu/src/widget/container.rs b/wgpu/src/widget/container.rs
index 9a93a246..bc26cef2 100644
--- a/wgpu/src/widget/container.rs
+++ b/wgpu/src/widget/container.rs
@@ -1,7 +1,7 @@
//! Decorate content and apply alignment.
use crate::Renderer;
-pub use iced_style::container::{Style, StyleSheet};
+pub use iced_graphics::container::{Style, StyleSheet};
/// An element decorating some content.
///
diff --git a/wgpu/src/widget/pane_grid.rs b/wgpu/src/widget/pane_grid.rs
new file mode 100644
index 00000000..c26dde48
--- /dev/null
+++ b/wgpu/src/widget/pane_grid.rs
@@ -0,0 +1,31 @@
+//! Let your users split regions of your application and organize layout dynamically.
+//!
+//! [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](https://gfycat.com/mixedflatjellyfish)
+//!
+//! # Example
+//! 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::Renderer;
+
+pub use iced_native::pane_grid::{
+ Axis, Configuration, Direction, DragEvent, Node, Pane, ResizeEvent, Split,
+ State,
+};
+
+/// A collection of panes distributed using either vertical or horizontal splits
+/// to completely fill the space available.
+///
+/// [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](https://gfycat.com/mixedflatjellyfish)
+///
+/// This is an alias of an `iced_native` pane grid with an `iced_wgpu::Renderer`.
+pub type PaneGrid<'a, Message> = iced_native::PaneGrid<'a, Message, Renderer>;
+
+/// The content of a [`Pane`].
+pub type Content<'a, Message> =
+ iced_native::pane_grid::Content<'a, Message, Renderer>;
+
+/// The title bar of a [`Pane`].
+pub type TitleBar<'a, Message> =
+ iced_native::pane_grid::TitleBar<'a, Message, Renderer>;
diff --git a/wgpu/src/widget/pick_list.rs b/wgpu/src/widget/pick_list.rs
new file mode 100644
index 00000000..fccc68c9
--- /dev/null
+++ b/wgpu/src/widget/pick_list.rs
@@ -0,0 +1,9 @@
+//! Display a dropdown list of selectable values.
+pub use iced_native::pick_list::State;
+
+pub use iced_graphics::overlay::menu::Style as Menu;
+pub use iced_graphics::pick_list::{Style, StyleSheet};
+
+/// A widget allowing the selection of a single value from a list of options.
+pub type PickList<'a, T, Message> =
+ iced_native::PickList<'a, T, Message, crate::Renderer>;
diff --git a/wgpu/src/widget/progress_bar.rs b/wgpu/src/widget/progress_bar.rs
index 34450b5e..45a25d00 100644
--- a/wgpu/src/widget/progress_bar.rs
+++ b/wgpu/src/widget/progress_bar.rs
@@ -1,12 +1,10 @@
-//! Allow your users to perform actions by pressing a button.
+//! Allow your users to visually track the progress of a computation.
//!
-//! A [`Button`] has some local [`State`].
-//!
-//! [`Button`]: type.Button.html
-//! [`State`]: struct.State.html
+//! A [`ProgressBar`] has a range of possible values and a current value,
+//! as well as a length, height and style.
use crate::Renderer;
-pub use iced_style::progress_bar::{Style, StyleSheet};
+pub use iced_graphics::progress_bar::{Style, StyleSheet};
/// A bar that displays progress.
///
diff --git a/wgpu/src/widget/qr_code.rs b/wgpu/src/widget/qr_code.rs
new file mode 100644
index 00000000..7b1c2408
--- /dev/null
+++ b/wgpu/src/widget/qr_code.rs
@@ -0,0 +1,2 @@
+//! Encode and display information in a QR code.
+pub use iced_graphics::qr_code::*;
diff --git a/wgpu/src/widget/radio.rs b/wgpu/src/widget/radio.rs
index 6e5cf042..0b843d1f 100644
--- a/wgpu/src/widget/radio.rs
+++ b/wgpu/src/widget/radio.rs
@@ -1,7 +1,7 @@
//! Create choices using radio buttons.
use crate::Renderer;
-pub use iced_style::radio::{Style, StyleSheet};
+pub use iced_graphics::radio::{Style, StyleSheet};
/// A circular button representing a choice.
///
diff --git a/wgpu/src/widget/rule.rs b/wgpu/src/widget/rule.rs
new file mode 100644
index 00000000..3f7bc67a
--- /dev/null
+++ b/wgpu/src/widget/rule.rs
@@ -0,0 +1,10 @@
+//! Display a horizontal or vertical rule for dividing content.
+
+use crate::Renderer;
+
+pub use iced_graphics::rule::{FillMode, Style, StyleSheet};
+
+/// Display a horizontal or vertical rule for dividing content.
+///
+/// This is an alias of an `iced_native` rule with an `iced_wgpu::Renderer`.
+pub type Rule = iced_native::Rule<Renderer>;
diff --git a/wgpu/src/widget/scrollable.rs b/wgpu/src/widget/scrollable.rs
index 1d236105..fabb4318 100644
--- a/wgpu/src/widget/scrollable.rs
+++ b/wgpu/src/widget/scrollable.rs
@@ -1,8 +1,8 @@
//! Navigate an endless amount of content with a scrollbar.
use crate::Renderer;
+pub use iced_graphics::scrollable::{Scrollbar, Scroller, StyleSheet};
pub use iced_native::scrollable::State;
-pub use iced_style::scrollable::{Scrollbar, Scroller, StyleSheet};
/// A widget that can vertically display an infinite amount of content
/// with a scrollbar.
diff --git a/wgpu/src/widget/slider.rs b/wgpu/src/widget/slider.rs
index 4e47978f..9a269858 100644
--- a/wgpu/src/widget/slider.rs
+++ b/wgpu/src/widget/slider.rs
@@ -1,16 +1,13 @@
//! Display an interactive selector of a single value from a range of values.
//!
//! A [`Slider`] has some local [`State`].
-//!
-//! [`Slider`]: struct.Slider.html
-//! [`State`]: struct.State.html
use crate::Renderer;
+pub use iced_graphics::slider::{Handle, HandleShape, Style, StyleSheet};
pub use iced_native::slider::State;
-pub use iced_style::slider::{Handle, HandleShape, Style, StyleSheet};
/// An horizontal bar and a handle that selects a single value from a range of
/// values.
///
/// This is an alias of an `iced_native` slider with an `iced_wgpu::Renderer`.
-pub type Slider<'a, Message> = iced_native::Slider<'a, Message, Renderer>;
+pub type Slider<'a, T, Message> = iced_native::Slider<'a, T, Message, Renderer>;
diff --git a/wgpu/src/widget/text_input.rs b/wgpu/src/widget/text_input.rs
index 260fe3a6..db18b1cc 100644
--- a/wgpu/src/widget/text_input.rs
+++ b/wgpu/src/widget/text_input.rs
@@ -1,13 +1,10 @@
//! Display fields that can be filled with text.
//!
//! A [`TextInput`] has some local [`State`].
-//!
-//! [`TextInput`]: struct.TextInput.html
-//! [`State`]: struct.State.html
use crate::Renderer;
+pub use iced_graphics::text_input::{Style, StyleSheet};
pub use iced_native::text_input::State;
-pub use iced_style::text_input::{Style, StyleSheet};
/// A field that can be filled with text.
///
diff --git a/wgpu/src/window.rs b/wgpu/src/window.rs
index b7adad82..aac5fb9e 100644
--- a/wgpu/src/window.rs
+++ b/wgpu/src/window.rs
@@ -1,6 +1,4 @@
//! Display rendering results on windows.
-mod backend;
-mod swap_chain;
+mod compositor;
-pub use backend::Backend;
-pub use swap_chain::SwapChain;
+pub use compositor::Compositor;
diff --git a/wgpu/src/window/backend.rs b/wgpu/src/window/backend.rs
deleted file mode 100644
index 5b269f36..00000000
--- a/wgpu/src/window/backend.rs
+++ /dev/null
@@ -1,113 +0,0 @@
-use crate::{window::SwapChain, Renderer, Settings, Target};
-
-use iced_native::MouseCursor;
-use raw_window_handle::HasRawWindowHandle;
-
-/// A window graphics backend for iced powered by `wgpu`.
-#[derive(Debug)]
-pub struct Backend {
- device: wgpu::Device,
- queue: wgpu::Queue,
- format: wgpu::TextureFormat,
-}
-
-impl iced_native::window::Backend for Backend {
- type Settings = Settings;
- type Renderer = Renderer;
- type Surface = wgpu::Surface;
- type SwapChain = SwapChain;
-
- fn new(settings: Self::Settings) -> (Backend, Renderer) {
- let adapter = wgpu::Adapter::request(&wgpu::RequestAdapterOptions {
- power_preference: if settings.antialiasing.is_none() {
- wgpu::PowerPreference::Default
- } else {
- wgpu::PowerPreference::HighPerformance
- },
- backends: wgpu::BackendBit::all(),
- })
- .expect("Request adapter");
-
- let (mut device, queue) =
- adapter.request_device(&wgpu::DeviceDescriptor {
- extensions: wgpu::Extensions {
- anisotropic_filtering: false,
- },
- limits: wgpu::Limits { max_bind_groups: 2 },
- });
-
- let renderer = Renderer::new(&mut device, settings);
-
- (
- Backend {
- device,
- queue,
- format: settings.format,
- },
- renderer,
- )
- }
-
- fn create_surface<W: HasRawWindowHandle>(
- &mut self,
- window: &W,
- ) -> wgpu::Surface {
- wgpu::Surface::create(window)
- }
-
- fn create_swap_chain(
- &mut self,
- surface: &Self::Surface,
- width: u32,
- height: u32,
- ) -> SwapChain {
- SwapChain::new(&self.device, surface, self.format, width, height)
- }
-
- fn draw<T: AsRef<str>>(
- &mut self,
- renderer: &mut Self::Renderer,
- swap_chain: &mut SwapChain,
- output: &<Self::Renderer as iced_native::Renderer>::Output,
- scale_factor: f64,
- overlay: &[T],
- ) -> MouseCursor {
- let (frame, viewport) = swap_chain.next_frame();
-
- let mut encoder = self.device.create_command_encoder(
- &wgpu::CommandEncoderDescriptor { todo: 0 },
- );
-
- let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
- color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor {
- attachment: &frame.view,
- resolve_target: None,
- load_op: wgpu::LoadOp::Clear,
- store_op: wgpu::StoreOp::Store,
- clear_color: wgpu::Color {
- r: 1.0,
- g: 1.0,
- b: 1.0,
- a: 1.0,
- },
- }],
- depth_stencil_attachment: None,
- });
-
- let mouse_cursor = renderer.draw(
- &mut self.device,
- &mut encoder,
- Target {
- texture: &frame.view,
- viewport,
- },
- output,
- scale_factor,
- overlay,
- );
-
- self.queue.submit(&[encoder.finish()]);
-
- mouse_cursor
- }
-}
diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs
new file mode 100644
index 00000000..492efb42
--- /dev/null
+++ b/wgpu/src/window/compositor.rs
@@ -0,0 +1,177 @@
+use crate::{Backend, Color, Error, Renderer, Settings, Viewport};
+
+use futures::task::SpawnExt;
+use iced_native::{futures, mouse};
+use raw_window_handle::HasRawWindowHandle;
+
+/// A window graphics backend for iced powered by `wgpu`.
+#[allow(missing_debug_implementations)]
+pub struct Compositor {
+ settings: Settings,
+ instance: wgpu::Instance,
+ device: wgpu::Device,
+ queue: wgpu::Queue,
+ staging_belt: wgpu::util::StagingBelt,
+ local_pool: futures::executor::LocalPool,
+}
+
+impl Compositor {
+ const CHUNK_SIZE: u64 = 10 * 1024;
+
+ /// 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);
+
+ let adapter = instance
+ .request_adapter(&wgpu::RequestAdapterOptions {
+ power_preference: if settings.antialiasing.is_none() {
+ wgpu::PowerPreference::Default
+ } else {
+ wgpu::PowerPreference::HighPerformance
+ },
+ compatible_surface: None,
+ })
+ .await?;
+
+ let (device, queue) = adapter
+ .request_device(
+ &wgpu::DeviceDescriptor {
+ features: wgpu::Features::empty(),
+ limits: wgpu::Limits {
+ max_bind_groups: 2,
+ ..wgpu::Limits::default()
+ },
+ shader_validation: false,
+ },
+ None,
+ )
+ .await
+ .ok()?;
+
+ let staging_belt = wgpu::util::StagingBelt::new(Self::CHUNK_SIZE);
+ let local_pool = futures::executor::LocalPool::new();
+
+ Some(Compositor {
+ instance,
+ settings,
+ device,
+ queue,
+ staging_belt,
+ local_pool,
+ })
+ }
+
+ /// Creates a new rendering [`Backend`] for this [`Compositor`].
+ pub fn create_backend(&self) -> Backend {
+ Backend::new(&self.device, self.settings)
+ }
+}
+
+impl iced_graphics::window::Compositor for Compositor {
+ type Settings = Settings;
+ type Renderer = Renderer;
+ 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)?;
+
+ let backend = compositor.create_backend();
+
+ Ok((compositor, Renderer::new(backend)))
+ }
+
+ fn create_surface<W: HasRawWindowHandle>(
+ &mut self,
+ window: &W,
+ ) -> wgpu::Surface {
+ #[allow(unsafe_code)]
+ unsafe {
+ self.instance.create_surface(window)
+ }
+ }
+
+ fn create_swap_chain(
+ &mut self,
+ surface: &Self::Surface,
+ width: u32,
+ height: u32,
+ ) -> Self::SwapChain {
+ self.device.create_swap_chain(
+ surface,
+ &wgpu::SwapChainDescriptor {
+ usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT,
+ format: self.settings.format,
+ present_mode: self.settings.present_mode,
+ width,
+ height,
+ },
+ )
+ }
+
+ fn draw<T: AsRef<str>>(
+ &mut self,
+ renderer: &mut Self::Renderer,
+ swap_chain: &mut Self::SwapChain,
+ viewport: &Viewport,
+ background_color: Color,
+ output: &<Self::Renderer as iced_native::Renderer>::Output,
+ overlay: &[T],
+ ) -> mouse::Interaction {
+ let frame = swap_chain.get_current_frame().expect("Next frame");
+
+ let mut encoder = self.device.create_command_encoder(
+ &wgpu::CommandEncoderDescriptor {
+ label: Some("iced_wgpu encoder"),
+ },
+ );
+
+ 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 mouse_interaction = renderer.backend_mut().draw(
+ &mut self.device,
+ &mut self.staging_belt,
+ &mut encoder,
+ &frame.output.view,
+ viewport,
+ 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();
+
+ mouse_interaction
+ }
+}
diff --git a/wgpu/src/window/swap_chain.rs b/wgpu/src/window/swap_chain.rs
deleted file mode 100644
index 4ca2901b..00000000
--- a/wgpu/src/window/swap_chain.rs
+++ /dev/null
@@ -1,57 +0,0 @@
-use crate::Viewport;
-
-/// The rendering target of a window.
-///
-/// It represents a series of virtual framebuffers with a scale factor.
-#[derive(Debug)]
-pub struct SwapChain {
- raw: wgpu::SwapChain,
- viewport: Viewport,
-}
-
-impl SwapChain {}
-
-impl SwapChain {
- /// Creates a new [`SwapChain`] for the given surface.
- ///
- /// [`SwapChain`]: struct.SwapChain.html
- pub fn new(
- device: &wgpu::Device,
- surface: &wgpu::Surface,
- format: wgpu::TextureFormat,
- width: u32,
- height: u32,
- ) -> SwapChain {
- SwapChain {
- raw: new_swap_chain(surface, format, width, height, device),
- viewport: Viewport::new(width, height),
- }
- }
-
- /// Returns the next frame of the [`SwapChain`] alongside its [`Viewport`].
- ///
- /// [`SwapChain`]: struct.SwapChain.html
- /// [`Viewport`]: ../struct.Viewport.html
- pub fn next_frame(&mut self) -> (wgpu::SwapChainOutput<'_>, &Viewport) {
- (self.raw.get_next_texture(), &self.viewport)
- }
-}
-
-fn new_swap_chain(
- surface: &wgpu::Surface,
- format: wgpu::TextureFormat,
- width: u32,
- height: u32,
- device: &wgpu::Device,
-) -> wgpu::SwapChain {
- device.create_swap_chain(
- &surface,
- &wgpu::SwapChainDescriptor {
- usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT,
- format,
- width,
- height,
- present_mode: wgpu::PresentMode::Vsync,
- },
- )
-}
diff --git a/winit/CHANGELOG.md b/winit/CHANGELOG.md
deleted file mode 100644
index 1289a45a..00000000
--- a/winit/CHANGELOG.md
+++ /dev/null
@@ -1,14 +0,0 @@
-# Changelog
-All notable changes to this project will be documented in this file.
-
-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.1.0-alpha] - 2019-11-25
-### Added
-- First release! :tada:
-
-[Unreleased]: https://github.com/hecrj/iced/compare/winit-0.1.0-alpha...HEAD
-[0.1.0-alpha]: https://github.com/hecrj/iced/releases/tag/winit-0.1.0-alpha
diff --git a/winit/Cargo.toml b/winit/Cargo.toml
index ca2018c7..39a6a5fa 100644
--- a/winit/Cargo.toml
+++ b/winit/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "iced_winit"
-version = "0.1.0-alpha"
+version = "0.2.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2018"
description = "A winit runtime for Iced"
@@ -11,16 +11,25 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"]
categories = ["gui"]
[features]
-debug = []
+debug = ["iced_native/debug"]
[dependencies]
-winit = "0.22"
+winit = "0.24"
window_clipboard = "0.1"
log = "0.4"
+thiserror = "1.0"
[dependencies.iced_native]
-version = "0.1.0-alpha"
+version = "0.3"
path = "../native"
+[dependencies.iced_graphics]
+version = "0.1"
+path = "../graphics"
+
+[dependencies.iced_futures]
+version = "0.2"
+path = "../futures"
+
[target.'cfg(target_os = "windows")'.dependencies.winapi]
version = "0.3.6"
diff --git a/winit/README.md b/winit/README.md
index d3309e49..721baa14 100644
--- a/winit/README.md
+++ b/winit/README.md
@@ -8,9 +8,11 @@
It exposes a renderer-agnostic `Application` trait that can be implemented and then run with a simple call. The use of this trait is optional. A `conversion` module is provided for users that decide to implement a custom event loop.
-![iced_winit](../docs/graphs/winit.png)
+<p align="center">
+ <img alt="The native target" src="../docs/graphs/native.png" width="80%">
+</p>
-[documentation]: https://docs.rs/iced_winit/0.1.0-alpha.1/iced_winit/
+[documentation]: https://docs.rs/iced_winit
[`iced_native`]: ../native
[`winit`]: https://github.com/rust-windowing/winit
@@ -18,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.1.0-alpha"
+iced_winit = "0.2"
```
__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 65bffd5b..d1a94864 100644
--- a/winit/src/application.rs
+++ b/winit/src/application.rs
@@ -1,65 +1,54 @@
+//! Create interactive, native cross-platform applications.
+mod state;
+
+pub use state::State;
+
+use crate::conversion;
+use crate::mouse;
use crate::{
- conversion, size::Size, window, Cache, Clipboard, Command, Debug, Element,
- Executor, Mode, MouseCursor, Proxy, Runtime, Settings, Subscription,
- UserInterface,
+ Clipboard, Color, Command, Debug, Error, Executor, Mode, Proxy, Runtime,
+ Settings, Size, Subscription,
};
+use iced_futures::futures;
+use iced_futures::futures::channel::mpsc;
+use iced_graphics::window;
+use iced_native::program::Program;
+use iced_native::{Cache, UserInterface};
+
+use std::mem::ManuallyDrop;
+
/// An interactive, native cross-platform application.
///
/// This trait is the main entrypoint of Iced. Once implemented, you can run
/// your GUI application by simply calling [`run`](#method.run). It will run in
/// its own window.
///
-/// An [`Application`](trait.Application.html) can execute asynchronous actions
-/// by returning a [`Command`](struct.Command.html) in some of its methods.
-pub trait Application: Sized {
- /// The graphics backend to use to draw the [`Application`].
- ///
- /// [`Application`]: trait.Application.html
- type Backend: window::Backend;
-
- /// The [`Executor`] that will run commands and subscriptions.
- ///
- /// [`Executor`]: trait.Executor.html
- type Executor: Executor;
-
- /// The type of __messages__ your [`Application`] will produce.
- ///
- /// [`Application`]: trait.Application.html
- type Message: std::fmt::Debug + Send;
-
- /// Initializes the [`Application`].
+/// An [`Application`] can execute asynchronous actions by returning a
+/// [`Command`] in some of its methods.
+///
+/// When using an [`Application`] with the `debug` feature enabled, a debug view
+/// can be toggled by pressing `F12`.
+pub trait Application: Program {
+ /// The data needed to initialize your [`Application`].
+ type Flags;
+
+ /// Initializes the [`Application`] with the flags provided to
+ /// [`run`] as part of the [`Settings`].
///
/// Here is where you should return the initial state of your app.
///
- /// Additionally, you can return a [`Command`](struct.Command.html) if you
- /// need to perform some async action in the background on startup. This is
- /// useful if you want to load state from a file, perform an initial HTTP
- /// request, etc.
- ///
- /// [`Application`]: trait.Application.html
- fn new() -> (Self, Command<Self::Message>);
+ /// Additionally, you can return a [`Command`] if you need to perform some
+ /// async action in the background on startup. This is useful if you want to
+ /// load state from a file, perform an initial HTTP request, etc.
+ fn new(flags: Self::Flags) -> (Self, Command<Self::Message>);
/// Returns the current title of the [`Application`].
///
/// This title can be dynamic! The runtime will automatically update the
/// title of your application when necessary.
- ///
- /// [`Application`]: trait.Application.html
fn title(&self) -> String;
- /// Handles a __message__ and updates the state of the [`Application`].
- ///
- /// This is where you define your __update logic__. All the __messages__,
- /// produced by either user interactions or commands, will be handled by
- /// this method.
- ///
- /// Any [`Command`] returned will be executed immediately in the background.
- ///
- /// [`Application`]: trait.Application.html
- /// [`Command`]: struct.Command.html
- fn update(&mut self, message: Self::Message) -> Command<Self::Message>;
-
/// Returns the event `Subscription` for the current state of the
/// application.
///
@@ -67,16 +56,11 @@ pub trait Application: Sized {
/// [`update`](#tymethod.update).
///
/// A `Subscription` will be kept alive as long as you keep returning it!
- fn subscription(&self) -> Subscription<Self::Message>;
-
- /// Returns the widgets to display in the [`Application`].
///
- /// These widgets can produce __messages__ based on user interaction.
- ///
- /// [`Application`]: trait.Application.html
- fn view(
- &mut self,
- ) -> Element<'_, Self::Message, <Self::Backend as window::Backend>::Renderer>;
+ /// By default, it returns an empty subscription.
+ fn subscription(&self) -> Subscription<Self::Message> {
+ Subscription::none()
+ }
/// Returns the current [`Application`] mode.
///
@@ -84,251 +68,266 @@ pub trait Application: Sized {
/// is returned.
///
/// By default, an application will run in windowed mode.
- ///
- /// [`Application`]: trait.Application.html
fn mode(&self) -> Mode {
Mode::Windowed
}
- /// Runs the [`Application`].
+ /// Returns the background [`Color`] of the [`Application`].
///
- /// This method will take control of the current thread and __will NOT
- /// return__.
+ /// By default, it returns [`Color::WHITE`].
+ fn background_color(&self) -> Color {
+ Color::WHITE
+ }
+
+ /// Returns the scale factor of the [`Application`].
///
- /// It should probably be that last thing you call in your `main` function.
+ /// It can be used to dynamically control the size of the UI at runtime
+ /// (i.e. zooming).
///
- /// [`Application`]: trait.Application.html
- fn run(
- settings: Settings,
- backend_settings: <Self::Backend as window::Backend>::Settings,
- ) where
- Self: 'static,
- {
- use window::Backend as _;
- use winit::{
- event::{self, WindowEvent},
- event_loop::{ControlFlow, EventLoop},
- window::WindowBuilder,
- };
-
- let mut debug = Debug::new();
-
- debug.startup_started();
- let event_loop = EventLoop::with_user_event();
- let mut external_messages = Vec::new();
-
- let mut runtime = {
- let executor = Self::Executor::new().expect("Create executor");
-
- Runtime::new(executor, Proxy::new(event_loop.create_proxy()))
- };
-
- let (mut application, init_command) = runtime.enter(|| Self::new());
- runtime.spawn(init_command);
-
- let subscription = application.subscription();
- runtime.track(subscription);
-
- let mut title = application.title();
- let mut mode = application.mode();
-
- let window = {
- let mut window_builder = WindowBuilder::new();
-
- let (width, height) = settings.window.size;
-
- window_builder = window_builder
- .with_title(&title)
- .with_inner_size(winit::dpi::LogicalSize { width, height })
- .with_resizable(settings.window.resizable)
- .with_decorations(settings.window.decorations)
- .with_fullscreen(conversion::fullscreen(
- event_loop.primary_monitor(),
- mode,
- ));
-
- #[cfg(target_os = "windows")]
- {
- use winit::platform::windows::WindowBuilderExtWindows;
-
- if let Some(parent) = settings.window.platform_specific.parent {
- window_builder = window_builder.with_parent_window(parent);
- }
- }
-
- window_builder.build(&event_loop).expect("Open window")
- };
-
- let mut size = Size::new(window.inner_size(), window.scale_factor());
- let mut resized = false;
-
- let clipboard = Clipboard::new(&window);
- let (mut backend, mut renderer) = Self::Backend::new(backend_settings);
+ /// For instance, a scale factor of `2.0` will make widgets twice as big,
+ /// while a scale factor of `0.5` will shrink them to half their size.
+ ///
+ /// By default, it returns `1.0`.
+ fn scale_factor(&self) -> f64 {
+ 1.0
+ }
+}
- let surface = backend.create_surface(&window);
+/// Runs an [`Application`] with an executor, compositor, and the provided
+/// settings.
+pub fn run<A, E, C>(
+ settings: Settings<A::Flags>,
+ compositor_settings: C::Settings,
+) -> Result<(), Error>
+where
+ A: Application + 'static,
+ E: Executor + 'static,
+ C: window::Compositor<Renderer = A::Renderer> + 'static,
+{
+ use futures::task;
+ use futures::Future;
+ use winit::event_loop::EventLoop;
+
+ 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 = {
+ let proxy = Proxy::new(event_loop.create_proxy());
+ let executor = E::new().map_err(Error::ExecutorCreationFailed)?;
+
+ Runtime::new(executor, proxy)
+ };
+
+ let (application, init_command) = {
+ let flags = settings.flags;
+
+ runtime.enter(|| A::new(flags))
+ };
+
+ let subscription = application.subscription();
+
+ runtime.spawn(init_command);
+ runtime.track(subscription);
+
+ let window = settings
+ .window
+ .into_builder(
+ &application.title(),
+ application.mode(),
+ event_loop.primary_monitor(),
+ )
+ .build(&event_loop)
+ .map_err(Error::WindowCreationFailed)?;
+
+ let (mut sender, receiver) = mpsc::unbounded();
+
+ let mut instance = Box::pin(run_instance::<A, E, C>(
+ application,
+ compositor,
+ renderer,
+ window,
+ runtime,
+ debug,
+ receiver,
+ ));
- let mut swap_chain = {
- let physical_size = size.physical();
+ let mut context = task::Context::from_waker(task::noop_waker_ref());
- backend.create_swap_chain(
- &surface,
- physical_size.width,
- physical_size.height,
- )
- };
+ event_loop.run(move |event, _, control_flow| {
+ use winit::event_loop::ControlFlow;
- let user_interface = build_user_interface(
- &mut application,
- Cache::default(),
- &mut renderer,
- size.logical(),
- &mut debug,
- );
+ if let ControlFlow::Exit = control_flow {
+ return;
+ }
- debug.draw_started();
- let mut primitive = user_interface.draw(&mut renderer);
- debug.draw_finished();
+ if let Some(event) = event.to_static() {
+ sender.start_send(event).expect("Send event");
- let mut cache = Some(user_interface.into_cache());
- let mut events = Vec::new();
- let mut mouse_cursor = MouseCursor::OutOfBounds;
- let mut modifiers = winit::event::ModifiersState::default();
- debug.startup_finished();
+ let poll = instance.as_mut().poll(&mut context);
- window.request_redraw();
+ *control_flow = match poll {
+ task::Poll::Pending => ControlFlow::Wait,
+ task::Poll::Ready(_) => ControlFlow::Exit,
+ };
+ }
+ });
+}
- event_loop.run(move |event, _, control_flow| match event {
+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>>,
+) where
+ A: Application + 'static,
+ E: Executor + 'static,
+ C: window::Compositor<Renderer = A::Renderer> + 'static,
+{
+ use iced_futures::futures::stream::StreamExt;
+ use winit::event;
+
+ let surface = compositor.create_surface(&window);
+ let clipboard = Clipboard::new(&window);
+
+ let mut state = State::new(&application, &window);
+ let mut viewport_version = state.viewport_version();
+ let mut swap_chain = {
+ let physical_size = state.physical_size();
+
+ compositor.create_swap_chain(
+ &surface,
+ physical_size.width,
+ physical_size.height,
+ )
+ };
+
+ let mut user_interface = ManuallyDrop::new(build_user_interface(
+ &mut application,
+ Cache::default(),
+ &mut renderer,
+ state.logical_size(),
+ &mut debug,
+ ));
+
+ let mut primitive =
+ user_interface.draw(&mut renderer, state.cursor_position());
+ let mut mouse_interaction = mouse::Interaction::default();
+
+ let mut events = Vec::new();
+ let mut messages = Vec::new();
+
+ debug.startup_finished();
+
+ while let Some(event) = receiver.next().await {
+ match event {
event::Event::MainEventsCleared => {
- if events.is_empty() && external_messages.is_empty() {
- return;
+ if events.is_empty() && messages.is_empty() {
+ continue;
}
- // TODO: We should be able to keep a user interface alive
- // between events once we remove state references.
- //
- // This will allow us to rebuild it only when a message is
- // handled.
- let mut user_interface = build_user_interface(
- &mut application,
- cache.take().unwrap(),
+ debug.event_processing_started();
+
+ let statuses = user_interface.update(
+ &events,
+ state.cursor_position(),
+ clipboard.as_ref().map(|c| c as _),
&mut renderer,
- size.logical(),
- &mut debug,
+ &mut messages,
);
- debug.event_processing_started();
- events
- .iter()
- .cloned()
- .for_each(|event| runtime.broadcast(event));
-
- let mut messages = user_interface.update(
- events.drain(..),
- clipboard
- .as_ref()
- .map(|c| c as &dyn iced_native::Clipboard),
- &renderer,
- );
- messages.extend(external_messages.drain(..));
debug.event_processing_finished();
- if messages.is_empty() {
- debug.draw_started();
- primitive = user_interface.draw(&mut renderer);
- debug.draw_finished();
-
- cache = Some(user_interface.into_cache());
- } else {
- // When there are messages, we are forced to rebuild twice
- // for now :^)
- let temp_cache = user_interface.into_cache();
-
- for message in messages {
- log::debug!("Updating");
-
- debug.log_message(&message);
-
- debug.update_started();
- let command =
- runtime.enter(|| application.update(message));
- runtime.spawn(command);
- debug.update_finished();
- }
-
- let subscription = application.subscription();
- runtime.track(subscription);
-
- // Update window title
- let new_title = application.title();
-
- if title != new_title {
- window.set_title(&new_title);
-
- title = new_title;
- }
-
- // Update window mode
- let new_mode = application.mode();
-
- if mode != new_mode {
- window.set_fullscreen(conversion::fullscreen(
- window.current_monitor(),
- new_mode,
- ));
+ for event in events.drain(..).zip(statuses.into_iter()) {
+ runtime.broadcast(event);
+ }
- mode = new_mode;
- }
+ if !messages.is_empty() {
+ let cache =
+ ManuallyDrop::into_inner(user_interface).into_cache();
- let user_interface = build_user_interface(
+ // Update application
+ update(
&mut application,
- temp_cache,
- &mut renderer,
- size.logical(),
+ &mut runtime,
&mut debug,
+ &mut messages,
);
- debug.draw_started();
- primitive = user_interface.draw(&mut renderer);
- debug.draw_finished();
+ // Update window
+ state.synchronize(&application, &window);
- cache = Some(user_interface.into_cache());
+ user_interface = ManuallyDrop::new(build_user_interface(
+ &mut application,
+ cache,
+ &mut renderer,
+ state.logical_size(),
+ &mut debug,
+ ));
}
+ debug.draw_started();
+ primitive =
+ user_interface.draw(&mut renderer, state.cursor_position());
+ debug.draw_finished();
+
window.request_redraw();
}
event::Event::UserEvent(message) => {
- external_messages.push(message);
+ messages.push(message);
}
event::Event::RedrawRequested(_) => {
debug.render_started();
+ let current_viewport_version = state.viewport_version();
- if resized {
- let physical_size = size.physical();
+ if viewport_version != current_viewport_version {
+ let physical_size = state.physical_size();
+ let logical_size = state.logical_size();
- swap_chain = backend.create_swap_chain(
+ debug.layout_started();
+ user_interface = ManuallyDrop::new(
+ ManuallyDrop::into_inner(user_interface)
+ .relayout(logical_size, &mut renderer),
+ );
+ debug.layout_finished();
+
+ debug.draw_started();
+ primitive = user_interface
+ .draw(&mut renderer, state.cursor_position());
+ debug.draw_finished();
+
+ swap_chain = compositor.create_swap_chain(
&surface,
physical_size.width,
physical_size.height,
);
- resized = false;
+
+ viewport_version = current_viewport_version;
}
- let new_mouse_cursor = backend.draw(
+ let new_mouse_interaction = compositor.draw(
&mut renderer,
&mut swap_chain,
+ state.viewport(),
+ state.background_color(),
&primitive,
- size.scale_factor(),
&debug.overlay(),
);
debug.render_finished();
- if new_mouse_cursor != mouse_cursor {
- window.set_cursor_icon(conversion::mouse_cursor(
- new_mouse_cursor,
+ if new_mouse_interaction != mouse_interaction {
+ window.set_cursor_icon(conversion::mouse_interaction(
+ new_mouse_interaction,
));
- mouse_cursor = new_mouse_cursor;
+ mouse_interaction = new_mouse_interaction;
}
// TODO: Handle animations!
@@ -338,81 +337,90 @@ pub trait Application: Sized {
event: window_event,
..
} => {
- match window_event {
- WindowEvent::Resized(new_size) => {
- size = Size::new(new_size, window.scale_factor());
- resized = true;
- }
- WindowEvent::CloseRequested => {
- *control_flow = ControlFlow::Exit;
- }
- WindowEvent::ModifiersChanged(new_modifiers) => {
- modifiers = new_modifiers;
- }
- #[cfg(target_os = "macos")]
- WindowEvent::KeyboardInput {
- input:
- winit::event::KeyboardInput {
- virtual_keycode:
- Some(winit::event::VirtualKeyCode::Q),
- state: winit::event::ElementState::Pressed,
- ..
- },
- ..
- } if modifiers.logo() => {
- *control_flow = ControlFlow::Exit;
- }
- #[cfg(feature = "debug")]
- WindowEvent::KeyboardInput {
- input:
- winit::event::KeyboardInput {
- virtual_keycode:
- Some(winit::event::VirtualKeyCode::F12),
- state: winit::event::ElementState::Pressed,
- ..
- },
- ..
- } => debug.toggle(),
- _ => {}
+ if requests_exit(&window_event, state.modifiers()) {
+ break;
}
+ state.update(&window, &window_event, &mut debug);
+
if let Some(event) = conversion::window_event(
- window_event,
- size.scale_factor(),
- modifiers,
+ &window_event,
+ state.scale_factor(),
+ state.modifiers(),
) {
events.push(event);
}
}
- _ => {
- *control_flow = ControlFlow::Wait;
- }
- })
+ _ => {}
+ }
}
+
+ // Manually drop the user interface
+ drop(ManuallyDrop::into_inner(user_interface));
}
-fn build_user_interface<'a, A: Application>(
+/// Returns true if the provided event should cause an [`Application`] to
+/// exit.
+pub fn requests_exit(
+ event: &winit::event::WindowEvent<'_>,
+ _modifiers: winit::event::ModifiersState,
+) -> bool {
+ use winit::event::WindowEvent;
+
+ match event {
+ WindowEvent::CloseRequested => true,
+ #[cfg(target_os = "macos")]
+ WindowEvent::KeyboardInput {
+ input:
+ winit::event::KeyboardInput {
+ virtual_keycode: Some(winit::event::VirtualKeyCode::Q),
+ state: winit::event::ElementState::Pressed,
+ ..
+ },
+ ..
+ } if _modifiers.logo() => true,
+ _ => false,
+ }
+}
+
+/// Builds a [`UserInterface`] for the provided [`Application`], logging
+/// [`struct@Debug`] information accordingly.
+pub fn build_user_interface<'a, A: Application>(
application: &'a mut A,
cache: Cache,
- renderer: &mut <A::Backend as window::Backend>::Renderer,
- size: winit::dpi::LogicalSize<f64>,
+ renderer: &mut A::Renderer,
+ size: Size,
debug: &mut Debug,
-) -> UserInterface<'a, A::Message, <A::Backend as window::Backend>::Renderer> {
+) -> UserInterface<'a, A::Message, A::Renderer> {
debug.view_started();
let view = application.view();
debug.view_finished();
debug.layout_started();
- let user_interface = UserInterface::build(
- view,
- iced_native::Size::new(
- size.width.round() as f32,
- size.height.round() as f32,
- ),
- cache,
- renderer,
- );
+ let user_interface = UserInterface::build(view, size, cache, renderer);
debug.layout_finished();
user_interface
}
+
+/// Updates an [`Application`] by feeding it the provided messages, spawning any
+/// resulting [`Command`], and tracking its [`Subscription`].
+pub fn update<A: Application, E: Executor>(
+ application: &mut A,
+ runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>,
+ debug: &mut Debug,
+ messages: &mut Vec<A::Message>,
+) {
+ for message in messages.drain(..) {
+ debug.log_message(&message);
+
+ debug.update_started();
+ let command = runtime.enter(|| application.update(message));
+ debug.update_finished();
+
+ runtime.spawn(command);
+ }
+
+ let subscription = application.subscription();
+ runtime.track(subscription);
+}
diff --git a/winit/src/application/state.rs b/winit/src/application/state.rs
new file mode 100644
index 00000000..58bc7ed6
--- /dev/null
+++ b/winit/src/application/state.rs
@@ -0,0 +1,202 @@
+use crate::conversion;
+use crate::{Application, Color, Debug, Mode, Point, Size, Viewport};
+
+use std::marker::PhantomData;
+use winit::event::WindowEvent;
+use winit::window::Window;
+
+/// The state of a windowed [`Application`].
+#[derive(Debug, Clone)]
+pub struct State<A: Application> {
+ title: String,
+ mode: Mode,
+ background_color: Color,
+ scale_factor: f64,
+ viewport: Viewport,
+ viewport_version: usize,
+ cursor_position: winit::dpi::PhysicalPosition<f64>,
+ modifiers: winit::event::ModifiersState,
+ application: PhantomData<A>,
+}
+
+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 mode = application.mode();
+ let background_color = application.background_color();
+ let scale_factor = application.scale_factor();
+
+ let viewport = {
+ let physical_size = window.inner_size();
+
+ Viewport::with_physical_size(
+ Size::new(physical_size.width, physical_size.height),
+ window.scale_factor() * scale_factor,
+ )
+ };
+
+ Self {
+ title,
+ mode,
+ background_color,
+ scale_factor,
+ viewport,
+ viewport_version: 0,
+ // TODO: Encode cursor availability in the type-system
+ cursor_position: winit::dpi::PhysicalPosition::new(-1.0, -1.0),
+ modifiers: winit::event::ModifiersState::default(),
+ application: PhantomData,
+ }
+ }
+
+ /// Returns the current background [`Color`] of the [`State`].
+ pub fn background_color(&self) -> Color {
+ self.background_color
+ }
+
+ /// Returns the current [`Viewport`] of the [`State`].
+ pub fn viewport(&self) -> &Viewport {
+ &self.viewport
+ }
+
+ /// Returns the version of the [`Viewport`] of the [`State`].
+ ///
+ /// The version is incremented every time the [`Viewport`] changes.
+ pub fn viewport_version(&self) -> usize {
+ self.viewport_version
+ }
+
+ /// Returns the physical [`Size`] of the [`Viewport`] of the [`State`].
+ pub fn physical_size(&self) -> Size<u32> {
+ self.viewport.physical_size()
+ }
+
+ /// Returns the logical [`Size`] of the [`Viewport`] of the [`State`].
+ pub fn logical_size(&self) -> Size<f32> {
+ self.viewport.logical_size()
+ }
+
+ /// Returns the current scale factor of the [`Viewport`] of the [`State`].
+ pub fn scale_factor(&self) -> f64 {
+ self.viewport.scale_factor()
+ }
+
+ /// Returns the current cursor position of the [`State`].
+ pub fn cursor_position(&self) -> Point {
+ conversion::cursor_position(
+ self.cursor_position,
+ self.viewport.scale_factor(),
+ )
+ }
+
+ /// Returns the current keyboard modifiers of the [`State`].
+ pub fn modifiers(&self) -> winit::event::ModifiersState {
+ self.modifiers
+ }
+
+ /// Processes the provided window event and updates the [`State`]
+ /// accordingly.
+ pub fn update(
+ &mut self,
+ window: &Window,
+ event: &WindowEvent<'_>,
+ _debug: &mut Debug,
+ ) {
+ match event {
+ WindowEvent::Resized(new_size) => {
+ let size = Size::new(new_size.width, new_size.height);
+
+ self.viewport = Viewport::with_physical_size(
+ size,
+ window.scale_factor() * self.scale_factor,
+ );
+
+ self.viewport_version = self.viewport_version.wrapping_add(1);
+ }
+ WindowEvent::ScaleFactorChanged {
+ scale_factor: new_scale_factor,
+ new_inner_size,
+ } => {
+ let size =
+ Size::new(new_inner_size.width, new_inner_size.height);
+
+ self.viewport = Viewport::with_physical_size(
+ size,
+ new_scale_factor * self.scale_factor,
+ );
+
+ self.viewport_version = self.viewport_version.wrapping_add(1);
+ }
+ WindowEvent::CursorMoved { position, .. } => {
+ self.cursor_position = *position;
+ }
+ WindowEvent::CursorLeft { .. } => {
+ // TODO: Encode cursor availability in the type-system
+ self.cursor_position =
+ winit::dpi::PhysicalPosition::new(-1.0, -1.0);
+ }
+ WindowEvent::ModifiersChanged(new_modifiers) => {
+ self.modifiers = *new_modifiers;
+ }
+ #[cfg(feature = "debug")]
+ WindowEvent::KeyboardInput {
+ input:
+ winit::event::KeyboardInput {
+ virtual_keycode: Some(winit::event::VirtualKeyCode::F12),
+ state: winit::event::ElementState::Pressed,
+ ..
+ },
+ ..
+ } => _debug.toggle(),
+ _ => {}
+ }
+ }
+
+ /// Synchronizes the [`State`] with its [`Application`] and its respective
+ /// window.
+ ///
+ /// Normally an [`Application`] should be synchronized with its [`State`]
+ /// and window after calling [`Application::update`].
+ ///
+ /// [`Application::update`]: crate::Program::update
+ pub fn synchronize(&mut self, application: &A, window: &Window) {
+ // Update window title
+ let new_title = application.title();
+
+ if self.title != new_title {
+ window.set_title(&new_title);
+
+ self.title = new_title;
+ }
+
+ // Update window mode
+ let new_mode = application.mode();
+
+ if self.mode != new_mode {
+ window.set_fullscreen(conversion::fullscreen(
+ window.current_monitor(),
+ new_mode,
+ ));
+
+ self.mode = new_mode;
+ }
+
+ // Update background color
+ self.background_color = application.background_color();
+
+ // Update scale factor
+ let new_scale_factor = application.scale_factor();
+
+ if self.scale_factor != new_scale_factor {
+ let size = window.inner_size();
+
+ self.viewport = Viewport::with_physical_size(
+ Size::new(size.width, size.height),
+ window.scale_factor() * new_scale_factor,
+ );
+
+ self.scale_factor = new_scale_factor;
+ }
+ }
+}
diff --git a/winit/src/clipboard.rs b/winit/src/clipboard.rs
index 1ff029ab..93d53b11 100644
--- a/winit/src/clipboard.rs
+++ b/winit/src/clipboard.rs
@@ -5,8 +5,6 @@ pub struct Clipboard(window_clipboard::Clipboard);
impl Clipboard {
/// Creates a new [`Clipboard`] for the given window.
- ///
- /// [`Clipboard`]: struct.Clipboard.html
pub fn new(window: &winit::window::Window) -> Option<Clipboard> {
window_clipboard::Clipboard::new(window).map(Clipboard).ok()
}
diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs
index 1d008d05..e6fc4b96 100644
--- a/winit/src/conversion.rs
+++ b/winit/src/conversion.rs
@@ -1,18 +1,17 @@
-//! Convert [`winit`] types to [`iced_native`] types, and viceversa.
+//! Convert [`winit`] types into [`iced_native`] types, and viceversa.
//!
//! [`winit`]: https://github.com/rust-windowing/winit
//! [`iced_native`]: https://github.com/hecrj/iced/tree/master/native
use crate::{
- input::{
- keyboard::{self, KeyCode, ModifiersState},
- mouse, touch, ButtonState, Touch,
- },
- window, Event, Mode, MouseCursor, Point,
+ keyboard::{self, KeyCode, Modifiers},
+ mouse,
+ touch::{self, Touch},
+ window, Event, Mode, Point,
};
/// Converts a winit window event into an iced event.
pub fn window_event(
- event: winit::event::WindowEvent<'_>,
+ event: &winit::event::WindowEvent<'_>,
scale_factor: f64,
modifiers: winit::event::ModifiersState,
) -> Option<Event> {
@@ -27,6 +26,14 @@ pub fn window_event(
height: logical_size.height,
}))
}
+ WindowEvent::ScaleFactorChanged { new_inner_size, .. } => {
+ let logical_size = new_inner_size.to_logical(scale_factor);
+
+ Some(Event::Window(window::Event::Resized {
+ width: logical_size.width,
+ height: logical_size.height,
+ }))
+ }
WindowEvent::CursorMoved { position, .. } => {
let position = position.to_logical::<f64>(scale_factor);
@@ -34,18 +41,30 @@ pub fn window_event(
position: Point::new(position.x as f32, position.y as f32),
}))
}
+ WindowEvent::CursorEntered { .. } => {
+ Some(Event::Mouse(mouse::Event::CursorEntered))
+ }
+ WindowEvent::CursorLeft { .. } => {
+ Some(Event::Mouse(mouse::Event::CursorLeft))
+ }
WindowEvent::MouseInput { button, state, .. } => {
- Some(Event::Mouse(mouse::Event::Input {
- button: mouse_button(button),
- state: button_state(state),
+ let button = mouse_button(*button);
+
+ Some(Event::Mouse(match state {
+ winit::event::ElementState::Pressed => {
+ mouse::Event::ButtonPressed(button)
+ }
+ winit::event::ElementState::Released => {
+ mouse::Event::ButtonReleased(button)
+ }
}))
}
WindowEvent::MouseWheel { delta, .. } => match delta {
winit::event::MouseScrollDelta::LineDelta(delta_x, delta_y) => {
Some(Event::Mouse(mouse::Event::WheelScrolled {
delta: mouse::ScrollDelta::Lines {
- x: delta_x,
- y: delta_y,
+ x: *delta_x,
+ y: *delta_y,
},
}))
}
@@ -58,8 +77,8 @@ pub fn window_event(
}))
}
},
- WindowEvent::ReceivedCharacter(c) if !is_private_use_character(c) => {
- Some(Event::Keyboard(keyboard::Event::CharacterReceived(c)))
+ WindowEvent::ReceivedCharacter(c) if !is_private_use_character(*c) => {
+ Some(Event::Keyboard(keyboard::Event::CharacterReceived(*c)))
}
WindowEvent::KeyboardInput {
input:
@@ -69,30 +88,47 @@ pub fn window_event(
..
},
..
- } => Some(Event::Keyboard(keyboard::Event::Input {
- key_code: key_code(virtual_keycode),
- state: button_state(state),
- modifiers: modifiers_state(modifiers),
+ } => Some(Event::Keyboard({
+ let key_code = key_code(*virtual_keycode);
+ let modifiers = self::modifiers(modifiers);
+
+ match state {
+ winit::event::ElementState::Pressed => {
+ keyboard::Event::KeyPressed {
+ key_code,
+ modifiers,
+ }
+ }
+ winit::event::ElementState::Released => {
+ keyboard::Event::KeyReleased {
+ key_code,
+ modifiers,
+ }
+ }
+ }
})),
+ WindowEvent::ModifiersChanged(new_modifiers) => Some(Event::Keyboard(
+ keyboard::Event::ModifiersChanged(self::modifiers(*new_modifiers)),
+ )),
WindowEvent::HoveredFile(path) => {
- Some(Event::Window(window::Event::FileHovered(path)))
+ Some(Event::Window(window::Event::FileHovered(path.clone())))
}
WindowEvent::DroppedFile(path) => {
- Some(Event::Window(window::Event::FileDropped(path)))
+ Some(Event::Window(window::Event::FileDropped(path.clone())))
}
WindowEvent::HoveredFileCancelled => {
Some(Event::Window(window::Event::FilesHoveredLeft))
}
- WindowEvent::Touch(touch) => Some(Event::Touch(touch_event(touch))),
+ WindowEvent::Touch(touch) => Some(Event::Touch(touch_event(*touch))),
_ => None,
}
}
/// Converts a [`Mode`] to a [`winit`] fullscreen mode.
///
-/// [`Mode`]:
+/// [`winit`]: https://github.com/rust-windowing/winit
pub fn fullscreen(
- monitor: winit::monitor::MonitorHandle,
+ monitor: Option<winit::monitor::MonitorHandle>,
mode: Mode,
) -> Option<winit::window::Fullscreen> {
match mode {
@@ -107,15 +143,23 @@ pub fn fullscreen(
///
/// [`winit`]: https://github.com/rust-windowing/winit
/// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native
-pub fn mouse_cursor(mouse_cursor: MouseCursor) -> winit::window::CursorIcon {
- match mouse_cursor {
- MouseCursor::OutOfBounds => winit::window::CursorIcon::Default,
- MouseCursor::Idle => winit::window::CursorIcon::Default,
- MouseCursor::Pointer => winit::window::CursorIcon::Hand,
- MouseCursor::Working => winit::window::CursorIcon::Progress,
- MouseCursor::Grab => winit::window::CursorIcon::Grab,
- MouseCursor::Grabbing => winit::window::CursorIcon::Grabbing,
- MouseCursor::Text => winit::window::CursorIcon::Text,
+pub fn mouse_interaction(
+ interaction: mouse::Interaction,
+) -> winit::window::CursorIcon {
+ use mouse::Interaction;
+
+ match interaction {
+ Interaction::Idle => winit::window::CursorIcon::Default,
+ Interaction::Pointer => winit::window::CursorIcon::Hand,
+ Interaction::Working => winit::window::CursorIcon::Progress,
+ Interaction::Grab => winit::window::CursorIcon::Grab,
+ Interaction::Grabbing => winit::window::CursorIcon::Grabbing,
+ Interaction::Crosshair => winit::window::CursorIcon::Crosshair,
+ Interaction::Text => winit::window::CursorIcon::Text,
+ Interaction::ResizingHorizontally => {
+ winit::window::CursorIcon::EwResize
+ }
+ Interaction::ResizingVertically => winit::window::CursorIcon::NsResize,
}
}
@@ -128,18 +172,9 @@ pub fn mouse_button(mouse_button: winit::event::MouseButton) -> mouse::Button {
winit::event::MouseButton::Left => mouse::Button::Left,
winit::event::MouseButton::Right => mouse::Button::Right,
winit::event::MouseButton::Middle => mouse::Button::Middle,
- winit::event::MouseButton::Other(other) => mouse::Button::Other(other),
- }
-}
-
-/// Converts an `ElementState` from [`winit`] to an [`iced_native`] button state.
-///
-/// [`winit`]: https://github.com/rust-windowing/winit
-/// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native
-pub fn button_state(element_state: winit::event::ElementState) -> ButtonState {
- match element_state {
- winit::event::ElementState::Pressed => ButtonState::Pressed,
- winit::event::ElementState::Released => ButtonState::Released,
+ winit::event::MouseButton::Other(other) => {
+ mouse::Button::Other(other as u8)
+ }
}
}
@@ -148,10 +183,8 @@ pub fn button_state(element_state: winit::event::ElementState) -> ButtonState {
///
/// [`winit`]: https://github.com/rust-windowing/winit
/// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native
-pub fn modifiers_state(
- modifiers: winit::event::ModifiersState,
-) -> ModifiersState {
- ModifiersState {
+pub fn modifiers(modifiers: winit::event::ModifiersState) -> Modifiers {
+ Modifiers {
shift: modifiers.shift(),
control: modifiers.ctrl(),
alt: modifiers.alt(),
@@ -159,6 +192,16 @@ pub fn modifiers_state(
}
}
+/// Converts a physical cursor position to a logical `Point`.
+pub fn cursor_position(
+ position: winit::dpi::PhysicalPosition<f64>,
+ scale_factor: f64,
+) -> Point {
+ let logical_position = position.to_logical(scale_factor);
+
+ 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
@@ -276,7 +319,8 @@ pub fn key_code(virtual_keycode: winit::event::VirtualKeyCode) -> KeyCode {
winit::event::VirtualKeyCode::Numpad9 => KeyCode::Numpad9,
winit::event::VirtualKeyCode::AbntC1 => KeyCode::AbntC1,
winit::event::VirtualKeyCode::AbntC2 => KeyCode::AbntC2,
- winit::event::VirtualKeyCode::Add => KeyCode::Add,
+ winit::event::VirtualKeyCode::NumpadAdd => KeyCode::NumpadAdd,
+ winit::event::VirtualKeyCode::Plus => KeyCode::Plus,
winit::event::VirtualKeyCode::Apostrophe => KeyCode::Apostrophe,
winit::event::VirtualKeyCode::Apps => KeyCode::Apps,
winit::event::VirtualKeyCode::At => KeyCode::At,
@@ -287,8 +331,8 @@ pub fn key_code(virtual_keycode: winit::event::VirtualKeyCode) -> KeyCode {
winit::event::VirtualKeyCode::Colon => KeyCode::Colon,
winit::event::VirtualKeyCode::Comma => KeyCode::Comma,
winit::event::VirtualKeyCode::Convert => KeyCode::Convert,
- winit::event::VirtualKeyCode::Decimal => KeyCode::Decimal,
- winit::event::VirtualKeyCode::Divide => KeyCode::Divide,
+ winit::event::VirtualKeyCode::NumpadDecimal => KeyCode::NumpadDecimal,
+ winit::event::VirtualKeyCode::NumpadDivide => KeyCode::NumpadDivide,
winit::event::VirtualKeyCode::Equals => KeyCode::Equals,
winit::event::VirtualKeyCode::Grave => KeyCode::Grave,
winit::event::VirtualKeyCode::Kana => KeyCode::Kana,
@@ -302,7 +346,7 @@ pub fn key_code(virtual_keycode: winit::event::VirtualKeyCode) -> KeyCode {
winit::event::VirtualKeyCode::MediaSelect => KeyCode::MediaSelect,
winit::event::VirtualKeyCode::MediaStop => KeyCode::MediaStop,
winit::event::VirtualKeyCode::Minus => KeyCode::Minus,
- winit::event::VirtualKeyCode::Multiply => KeyCode::Multiply,
+ winit::event::VirtualKeyCode::NumpadMultiply => KeyCode::NumpadMultiply,
winit::event::VirtualKeyCode::Mute => KeyCode::Mute,
winit::event::VirtualKeyCode::MyComputer => KeyCode::MyComputer,
winit::event::VirtualKeyCode::NavigateForward => {
@@ -330,7 +374,7 @@ pub fn key_code(virtual_keycode: winit::event::VirtualKeyCode) -> KeyCode {
winit::event::VirtualKeyCode::Slash => KeyCode::Slash,
winit::event::VirtualKeyCode::Sleep => KeyCode::Sleep,
winit::event::VirtualKeyCode::Stop => KeyCode::Stop,
- winit::event::VirtualKeyCode::Subtract => KeyCode::Subtract,
+ winit::event::VirtualKeyCode::NumpadSubtract => KeyCode::NumpadSubtract,
winit::event::VirtualKeyCode::Sysrq => KeyCode::Sysrq,
winit::event::VirtualKeyCode::Tab => KeyCode::Tab,
winit::event::VirtualKeyCode::Underline => KeyCode::Underline,
@@ -349,6 +393,7 @@ pub fn key_code(virtual_keycode: winit::event::VirtualKeyCode) -> KeyCode {
winit::event::VirtualKeyCode::Copy => KeyCode::Copy,
winit::event::VirtualKeyCode::Paste => KeyCode::Paste,
winit::event::VirtualKeyCode::Cut => KeyCode::Cut,
+ winit::event::VirtualKeyCode::Asterisk => KeyCode::Asterisk,
}
}
diff --git a/winit/src/error.rs b/winit/src/error.rs
new file mode 100644
index 00000000..8e1d20e8
--- /dev/null
+++ b/winit/src/error.rs
@@ -0,0 +1,27 @@
+use iced_futures::futures;
+
+/// An error that occurred while running an application.
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+ /// The futures executor could not be created.
+ #[error("the futures executor could not be created")]
+ ExecutorCreationFailed(futures::io::Error),
+
+ /// The application window could not be created.
+ #[error("the application window could not be created")]
+ WindowCreationFailed(winit::error::OsError),
+
+ /// A suitable graphics adapter or device could not be found.
+ #[error("a suitable graphics adapter or device could not be found")]
+ GraphicsAdapterNotFound,
+}
+
+impl From<iced_graphics::Error> for Error {
+ fn from(error: iced_graphics::Error) -> Error {
+ match error {
+ iced_graphics::Error::AdapterNotFound => {
+ Error::GraphicsAdapterNotFound
+ }
+ }
+ }
+}
diff --git a/winit/src/lib.rs b/winit/src/lib.rs
index f99e1290..c9f324dd 100644
--- a/winit/src/lib.rs
+++ b/winit/src/lib.rs
@@ -1,6 +1,6 @@
//! A windowing shell for Iced, on top of [`winit`].
//!
-//! ![`iced_winit` crate graph](https://github.com/hecrj/iced/blob/cae26cb7bc627f4a5b3bcf1cd023a0c552e8c65e/docs/graphs/winit.png?raw=true)
+//! ![The native path of the Iced ecosystem](https://github.com/hecrj/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/native.png?raw=true)
//!
//! `iced_winit` offers some convenient abstractions on top of [`iced_native`]
//! to quickstart development when using [`winit`].
@@ -13,8 +13,7 @@
//!
//! [`iced_native`]: https://github.com/hecrj/iced/tree/master/native
//! [`winit`]: https://github.com/rust-windowing/winit
-//! [`Application`]: trait.Application.html
-//! [`conversion`]: conversion
+//! [`conversion`]: crate::conversion
#![deny(missing_docs)]
#![deny(missing_debug_implementations)]
#![deny(unused_results)]
@@ -25,28 +24,20 @@
pub use iced_native::*;
pub use winit;
+pub mod application;
pub mod conversion;
pub mod settings;
-mod application;
mod clipboard;
+mod error;
mod mode;
mod proxy;
-mod size;
-
-// We disable debug capabilities on release builds unless the `debug` feature
-// is explicitly enabled.
-#[cfg(feature = "debug")]
-#[path = "debug/basic.rs"]
-mod debug;
-#[cfg(not(feature = "debug"))]
-#[path = "debug/null.rs"]
-mod debug;
pub use application::Application;
pub use clipboard::Clipboard;
+pub use error::Error;
pub use mode::Mode;
+pub use proxy::Proxy;
pub use settings::Settings;
-use debug::Debug;
-use proxy::Proxy;
+pub use iced_graphics::Viewport;
diff --git a/winit/src/proxy.rs b/winit/src/proxy.rs
index cff6ca72..7b9074d7 100644
--- a/winit/src/proxy.rs
+++ b/winit/src/proxy.rs
@@ -5,6 +5,8 @@ use iced_native::futures::{
};
use std::pin::Pin;
+/// An event loop proxy that implements `Sink`.
+#[derive(Debug)]
pub struct Proxy<Message: 'static> {
raw: winit::event_loop::EventLoopProxy<Message>,
}
@@ -18,6 +20,7 @@ impl<Message: 'static> Clone for Proxy<Message> {
}
impl<Message: 'static> Proxy<Message> {
+ /// Creates a new [`Proxy`] from an `EventLoopProxy`.
pub fn new(raw: winit::event_loop::EventLoopProxy<Message>) -> Self {
Self { raw }
}
diff --git a/winit/src/settings.rs b/winit/src/settings.rs
new file mode 100644
index 00000000..2e8715cd
--- /dev/null
+++ b/winit/src/settings.rs
@@ -0,0 +1,118 @@
+//! Configure your application.
+#[cfg(target_os = "windows")]
+#[path = "settings/windows.rs"]
+mod platform;
+#[cfg(not(target_os = "windows"))]
+#[path = "settings/not_windows.rs"]
+mod platform;
+
+pub use platform::PlatformSpecific;
+
+use crate::conversion;
+use crate::Mode;
+use winit::monitor::MonitorHandle;
+use winit::window::WindowBuilder;
+
+/// The settings of an application.
+#[derive(Debug, Clone, Default)]
+pub struct Settings<Flags> {
+ /// The [`Window`] settings
+ pub window: Window,
+
+ /// The data needed to initialize an [`Application`].
+ ///
+ /// [`Application`]: crate::Application
+ pub flags: Flags,
+}
+
+/// The window settings of an application.
+#[derive(Debug, Clone)]
+pub struct Window {
+ /// The size of the window.
+ pub size: (u32, u32),
+
+ /// The minimum size of the window.
+ pub min_size: Option<(u32, u32)>,
+
+ /// The maximum size of the window.
+ pub max_size: Option<(u32, u32)>,
+
+ /// Whether the window should be resizable or not.
+ pub resizable: bool,
+
+ /// Whether the window should have a border, a title bar, etc.
+ pub decorations: bool,
+
+ /// Whether the window should be transparent.
+ pub transparent: bool,
+
+ /// Whether the window will always be on top of other windows.
+ pub always_on_top: bool,
+
+ /// The window icon, which is also usually used in the taskbar
+ pub icon: Option<winit::window::Icon>,
+
+ /// Platform specific settings.
+ pub platform_specific: platform::PlatformSpecific,
+}
+
+impl Window {
+ /// Converts the window settings into a `WindowBuilder` from `winit`.
+ pub fn into_builder(
+ self,
+ title: &str,
+ mode: Mode,
+ primary_monitor: Option<MonitorHandle>,
+ ) -> WindowBuilder {
+ let mut window_builder = WindowBuilder::new();
+
+ let (width, height) = self.size;
+
+ window_builder = window_builder
+ .with_title(title)
+ .with_inner_size(winit::dpi::LogicalSize { width, height })
+ .with_resizable(self.resizable)
+ .with_decorations(self.decorations)
+ .with_transparent(self.transparent)
+ .with_window_icon(self.icon)
+ .with_always_on_top(self.always_on_top)
+ .with_fullscreen(conversion::fullscreen(primary_monitor, mode));
+
+ if let Some((width, height)) = self.min_size {
+ window_builder = window_builder
+ .with_min_inner_size(winit::dpi::LogicalSize { width, height });
+ }
+
+ if let Some((width, height)) = self.max_size {
+ window_builder = window_builder
+ .with_max_inner_size(winit::dpi::LogicalSize { width, height });
+ }
+
+ #[cfg(target_os = "windows")]
+ {
+ use winit::platform::windows::WindowBuilderExtWindows;
+
+ if let Some(parent) = self.platform_specific.parent {
+ window_builder = window_builder.with_parent_window(parent);
+ }
+ }
+
+ window_builder
+ }
+}
+
+impl Default for Window {
+ fn default() -> Window {
+ Window {
+ size: (1024, 768),
+ min_size: None,
+ max_size: None,
+ resizable: true,
+ decorations: true,
+ transparent: false,
+ always_on_top: false,
+ icon: None,
+ platform_specific: Default::default(),
+ }
+ }
+}
diff --git a/winit/src/settings/mod.rs b/winit/src/settings/mod.rs
deleted file mode 100644
index b2290b46..00000000
--- a/winit/src/settings/mod.rs
+++ /dev/null
@@ -1,53 +0,0 @@
-//! Configure your application.
-#[cfg(target_os = "windows")]
-#[path = "windows.rs"]
-mod platform;
-#[cfg(not(target_os = "windows"))]
-#[path = "not_windows.rs"]
-mod platform;
-
-pub use platform::PlatformSpecific;
-
-/// The settings of an application.
-#[derive(Debug, Clone, Copy, PartialEq)]
-pub struct Settings {
- /// The [`Window`] settings
- ///
- /// [`Window`]: struct.Window.html
- pub window: Window,
-}
-
-impl Default for Settings {
- fn default() -> Settings {
- Settings {
- window: Window::default(),
- }
- }
-}
-
-/// The window settings of an application.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub struct Window {
- /// The size of the window.
- pub size: (u32, u32),
-
- /// Whether the window should be resizable or not.
- pub resizable: bool,
-
- /// Whether the window should have a border, a title bar, etc.
- pub decorations: bool,
-
- /// Platform specific settings.
- pub platform_specific: platform::PlatformSpecific,
-}
-
-impl Default for Window {
- fn default() -> Window {
- Window {
- size: (1024, 768),
- resizable: true,
- decorations: true,
- platform_specific: Default::default(),
- }
- }
-}
diff --git a/winit/src/size.rs b/winit/src/size.rs
deleted file mode 100644
index 7e3056d4..00000000
--- a/winit/src/size.rs
+++ /dev/null
@@ -1,30 +0,0 @@
-pub struct Size {
- physical: winit::dpi::PhysicalSize<u32>,
- logical: winit::dpi::LogicalSize<f64>,
- scale_factor: f64,
-}
-
-impl Size {
- pub fn new(
- physical: winit::dpi::PhysicalSize<u32>,
- scale_factor: f64,
- ) -> Size {
- Size {
- logical: physical.to_logical(scale_factor),
- physical,
- scale_factor,
- }
- }
-
- pub fn physical(&self) -> winit::dpi::PhysicalSize<u32> {
- self.physical
- }
-
- pub fn logical(&self) -> winit::dpi::LogicalSize<f64> {
- self.logical
- }
-
- pub fn scale_factor(&self) -> f64 {
- self.scale_factor
- }
-}