summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Bingus <shankern@protonmail.com>2023-07-12 12:23:18 -0700
committerLibravatar Bingus <shankern@protonmail.com>2023-07-12 12:23:18 -0700
commit633f405f3f78bc7f82d2b2061491b0e011137451 (patch)
tree5ebfc1f45d216a5c14a90492563599e6969eab4d
parent41836dd80d0534608e7aedfbf2319c540a23de1a (diff)
parent21bd51426d900e271206f314e0c915dd41065521 (diff)
downloadiced-633f405f3f78bc7f82d2b2061491b0e011137451.tar.gz
iced-633f405f3f78bc7f82d2b2061491b0e011137451.tar.bz2
iced-633f405f3f78bc7f82d2b2061491b0e011137451.zip
Merge remote-tracking branch 'origin/master' into feat/multi-window-support
# Conflicts: # Cargo.toml # core/src/window/icon.rs # core/src/window/id.rs # core/src/window/position.rs # core/src/window/settings.rs # examples/integration/src/main.rs # examples/integration_opengl/src/main.rs # glutin/src/application.rs # native/src/subscription.rs # native/src/window.rs # runtime/src/window/action.rs # src/lib.rs # src/window.rs # winit/Cargo.toml # winit/src/application.rs # winit/src/icon.rs # winit/src/settings.rs # winit/src/window.rs
-rw-r--r--.github/ISSUE_TEMPLATE/BUG-REPORT.yml6
-rw-r--r--.github/workflows/document.yml8
-rw-r--r--.github/workflows/test.yml4
-rw-r--r--CHANGELOG.md56
-rw-r--r--Cargo.toml57
-rw-r--r--DEPENDENCIES.md33
-rw-r--r--README.md38
-rw-r--r--ROADMAP.md14
-rw-r--r--core/Cargo.toml10
-rw-r--r--core/README.md2
-rw-r--r--core/src/angle.rs33
-rw-r--r--core/src/background.rs19
-rw-r--r--core/src/border_radius.rs22
-rw-r--r--core/src/clipboard.rs23
-rw-r--r--core/src/color.rs40
-rw-r--r--core/src/element.rs (renamed from native/src/element.rs)139
-rw-r--r--core/src/event.rs (renamed from native/src/event.rs)2
-rw-r--r--core/src/font.rs116
-rw-r--r--core/src/gradient.rs105
-rw-r--r--core/src/hasher.rs (renamed from native/src/hasher.rs)0
-rw-r--r--core/src/image.rs (renamed from native/src/image.rs)12
-rw-r--r--core/src/layout.rs (renamed from native/src/layout.rs)0
-rw-r--r--core/src/layout/DRUID_LICENSE (renamed from native/src/layout/DRUID_LICENSE)0
-rw-r--r--core/src/layout/flex.rs (renamed from native/src/layout/flex.rs)0
-rw-r--r--core/src/layout/limits.rs (renamed from native/src/layout/limits.rs)0
-rw-r--r--core/src/layout/node.rs (renamed from native/src/layout/node.rs)0
-rw-r--r--core/src/length.rs8
-rw-r--r--core/src/lib.rs34
-rw-r--r--core/src/mouse.rs5
-rw-r--r--core/src/mouse/button.rs2
-rw-r--r--core/src/mouse/click.rs (renamed from native/src/mouse/click.rs)0
-rw-r--r--core/src/mouse/cursor.rs52
-rw-r--r--core/src/mouse/interaction.rs10
-rw-r--r--core/src/overlay.rs (renamed from native/src/overlay.rs)25
-rw-r--r--core/src/overlay/element.rs (renamed from native/src/overlay/element.rs)83
-rw-r--r--core/src/overlay/group.rs (renamed from native/src/overlay/group.rs)47
-rw-r--r--core/src/pixels.rs6
-rw-r--r--core/src/rectangle.rs48
-rw-r--r--core/src/renderer.rs (renamed from native/src/renderer.rs)26
-rw-r--r--core/src/renderer/null.rs (renamed from native/src/renderer/null.rs)24
-rw-r--r--core/src/shell.rs (renamed from native/src/shell.rs)0
-rw-r--r--core/src/size.rs2
-rw-r--r--core/src/svg.rs (renamed from native/src/svg.rs)4
-rw-r--r--core/src/text.rs212
-rw-r--r--core/src/touch.rs (renamed from native/src/touch.rs)0
-rw-r--r--core/src/widget.rs (renamed from native/src/widget.rs)103
-rw-r--r--core/src/widget/id.rs (renamed from native/src/widget/id.rs)2
-rw-r--r--core/src/widget/operation.rs226
-rw-r--r--core/src/widget/operation/focusable.rs (renamed from native/src/widget/operation/focusable.rs)0
-rw-r--r--core/src/widget/operation/scrollable.rs (renamed from native/src/widget/operation/scrollable.rs)41
-rw-r--r--core/src/widget/operation/text_input.rs (renamed from native/src/widget/operation/text_input.rs)0
-rw-r--r--core/src/widget/text.rs (renamed from native/src/widget/text.rs)115
-rw-r--r--core/src/widget/tree.rs (renamed from native/src/widget/tree.rs)0
-rw-r--r--core/src/window.rs15
-rw-r--r--core/src/window/event.rs (renamed from native/src/window/event.rs)0
-rw-r--r--core/src/window/icon.rs80
-rw-r--r--core/src/window/id.rs (renamed from native/src/window/id.rs)0
-rw-r--r--core/src/window/level.rs19
-rw-r--r--core/src/window/mode.rs (renamed from native/src/window/mode.rs)0
-rw-r--r--core/src/window/position.rs0
-rw-r--r--core/src/window/redraw_request.rs (renamed from native/src/window/redraw_request.rs)0
-rw-r--r--core/src/window/settings.rs (renamed from native/src/window/settings.rs)32
-rw-r--r--core/src/window/user_attention.rs (renamed from native/src/window/user_attention.rs)0
-rw-r--r--docs/release_summary.py52
-rw-r--r--examples/README.md3
-rw-r--r--examples/arc/src/main.rs12
-rw-r--r--examples/bezier_tool/src/main.rs39
-rw-r--r--examples/checkbox/fonts/icons.ttfbin1272 -> 1784 bytes
-rw-r--r--examples/checkbox/src/main.rs32
-rw-r--r--examples/clock/src/main.rs16
-rw-r--r--examples/color_palette/Cargo.toml2
-rw-r--r--examples/color_palette/src/main.rs63
-rw-r--r--examples/component/Cargo.toml4
-rw-r--r--examples/component/src/main.rs24
-rw-r--r--examples/custom_quad/Cargo.toml3
-rw-r--r--examples/custom_quad/src/main.rs11
-rw-r--r--examples/custom_widget/Cargo.toml3
-rw-r--r--examples/custom_widget/src/main.rs11
-rw-r--r--examples/download_progress/Cargo.toml2
-rw-r--r--examples/download_progress/src/download.rs19
-rw-r--r--examples/events/Cargo.toml1
-rw-r--r--examples/events/src/main.rs9
-rw-r--r--examples/game_of_life/src/main.rs95
-rw-r--r--examples/geometry/Cargo.toml4
-rw-r--r--examples/geometry/src/main.rs121
-rw-r--r--examples/integration/.gitignore (renamed from examples/integration_wgpu/.gitignore)0
-rw-r--r--examples/integration/Cargo.toml (renamed from examples/integration_wgpu/Cargo.toml)8
-rw-r--r--examples/integration/README.md (renamed from examples/integration_wgpu/README.md)0
-rw-r--r--examples/integration/index.html (renamed from examples/integration_wgpu/index.html)4
-rw-r--r--examples/integration/src/controls.rs (renamed from examples/integration_wgpu/src/controls.rs)19
-rw-r--r--examples/integration/src/main.rs (renamed from examples/integration_wgpu/src/main.rs)182
-rw-r--r--examples/integration/src/scene.rs (renamed from examples/integration_wgpu/src/scene.rs)2
-rw-r--r--examples/integration/src/shader/frag.wgsl (renamed from examples/integration_wgpu/src/shader/frag.wgsl)0
-rw-r--r--examples/integration/src/shader/vert.wgsl (renamed from examples/integration_wgpu/src/shader/vert.wgsl)0
-rw-r--r--examples/integration_opengl/Cargo.toml12
-rw-r--r--examples/integration_opengl/README.md16
-rw-r--r--examples/integration_opengl/src/controls.rs101
-rw-r--r--examples/integration_opengl/src/main.rs188
-rw-r--r--examples/integration_opengl/src/scene.rs102
-rw-r--r--examples/lazy/Cargo.toml3
-rw-r--r--examples/lazy/src/main.rs12
-rw-r--r--examples/loading_spinners/Cargo.toml11
-rw-r--r--examples/loading_spinners/README.md14
-rw-r--r--examples/loading_spinners/src/circular.rs421
-rw-r--r--examples/loading_spinners/src/easing.rs133
-rw-r--r--examples/loading_spinners/src/linear.rs326
-rw-r--r--examples/loading_spinners/src/main.rs118
-rw-r--r--examples/modal/Cargo.toml3
-rw-r--r--examples/modal/src/main.rs152
-rw-r--r--examples/modern_art/Cargo.toml11
-rw-r--r--examples/modern_art/src/main.rs142
-rw-r--r--examples/multitouch/src/main.rs14
-rw-r--r--examples/pane_grid/Cargo.toml4
-rw-r--r--examples/pane_grid/src/main.rs13
-rw-r--r--examples/pick_list/src/main.rs9
-rw-r--r--examples/qr_code/src/main.rs12
-rw-r--r--examples/screenshot/Cargo.toml11
-rw-r--r--examples/screenshot/src/main.rs320
-rw-r--r--examples/scrollable/src/main.rs149
-rw-r--r--examples/sierpinski_triangle/src/main.rs24
-rw-r--r--examples/solar_system/Cargo.toml1
-rw-r--r--examples/solar_system/src/main.rs50
-rw-r--r--examples/styling/src/main.rs15
-rw-r--r--examples/toast/Cargo.toml3
-rw-r--r--examples/toast/src/main.rs81
-rw-r--r--examples/todos/fonts/icons.ttfbin5596 -> 5732 bytes
-rw-r--r--examples/todos/src/main.rs76
-rw-r--r--examples/tour/Cargo.toml2
-rw-r--r--examples/tour/src/main.rs91
-rw-r--r--examples/url_handler/Cargo.toml1
-rw-r--r--examples/url_handler/src/main.rs10
-rw-r--r--examples/websocket/Cargo.toml2
-rw-r--r--examples/websocket/src/echo.rs104
-rw-r--r--examples/websocket/src/echo/server.rs2
-rw-r--r--examples/websocket/src/main.rs9
-rw-r--r--futures/Cargo.toml4
-rw-r--r--futures/src/backend/native.rs4
-rw-r--r--futures/src/backend/native/async_std.rs14
-rw-r--r--futures/src/backend/native/smol.rs16
-rw-r--r--futures/src/backend/native/thread_pool.rs1
-rw-r--r--futures/src/backend/native/tokio.rs14
-rw-r--r--futures/src/backend/wasm/wasm_bindgen.rs14
-rw-r--r--futures/src/command.rs70
-rw-r--r--futures/src/lib.rs5
-rw-r--r--futures/src/runtime.rs25
-rw-r--r--futures/src/subscription.rs355
-rw-r--r--futures/src/subscription/tracker.rs51
-rw-r--r--glow/Cargo.toml51
-rw-r--r--glow/README.md51
-rw-r--r--glow/src/backend.rs280
-rw-r--r--glow/src/image.rs254
-rw-r--r--glow/src/image/storage.rs78
-rw-r--r--glow/src/lib.rs53
-rw-r--r--glow/src/program.rs133
-rw-r--r--glow/src/quad.rs74
-rw-r--r--glow/src/quad/compatibility.rs349
-rw-r--r--glow/src/quad/core.rs244
-rw-r--r--glow/src/settings.rs61
-rw-r--r--glow/src/shader/common/gradient.frag59
-rw-r--r--glow/src/shader/common/gradient.vert9
-rw-r--r--glow/src/shader/common/image.frag22
-rw-r--r--glow/src/shader/common/image.vert9
-rw-r--r--glow/src/shader/common/solid.frag18
-rw-r--r--glow/src/shader/common/solid.vert11
-rw-r--r--glow/src/shader/compatibility/quad.frag83
-rw-r--r--glow/src/shader/compatibility/quad.vert46
-rw-r--r--glow/src/shader/core/quad.frag95
-rw-r--r--glow/src/shader/core/quad.vert52
-rw-r--r--glow/src/text.rs257
-rw-r--r--glow/src/triangle.rs595
-rw-r--r--glow/src/window.rs4
-rw-r--r--glow/src/window/compositor.rs111
-rw-r--r--glutin/Cargo.toml42
-rw-r--r--glutin/README.md29
-rw-r--r--glutin/src/application.rs510
-rw-r--r--glutin/src/lib.rs33
-rw-r--r--graphics/Cargo.toml60
-rw-r--r--graphics/fonts/Lato-Regular.ttfbin75136 -> 0 bytes
-rw-r--r--graphics/fonts/OFL.txt93
-rw-r--r--graphics/src/backend.rs25
-rw-r--r--graphics/src/color.rs46
-rw-r--r--graphics/src/compositor.rs (renamed from graphics/src/window/compositor.rs)23
-rw-r--r--graphics/src/damage.rs205
-rw-r--r--graphics/src/font.rs35
-rw-r--r--graphics/src/font/source.rs45
-rw-r--r--graphics/src/geometry.rs24
-rw-r--r--graphics/src/geometry/fill.rs (renamed from graphics/src/widget/canvas/fill.rs)29
-rw-r--r--graphics/src/geometry/path.rs (renamed from graphics/src/widget/canvas/path.rs)54
-rw-r--r--graphics/src/geometry/path/arc.rs (renamed from graphics/src/widget/canvas/path/arc.rs)2
-rw-r--r--graphics/src/geometry/path/builder.rs (renamed from graphics/src/widget/canvas/path/builder.rs)27
-rw-r--r--graphics/src/geometry/stroke.rs (renamed from graphics/src/widget/canvas/stroke.rs)42
-rw-r--r--graphics/src/geometry/style.rs (renamed from graphics/src/widget/canvas/style.rs)3
-rw-r--r--graphics/src/geometry/text.rs (renamed from graphics/src/widget/canvas/text.rs)13
-rw-r--r--graphics/src/gradient.rs257
-rw-r--r--graphics/src/gradient/linear.rs112
-rw-r--r--graphics/src/image.rs99
-rw-r--r--graphics/src/image/raster.rs242
-rw-r--r--graphics/src/image/storage.rs2
-rw-r--r--graphics/src/layer/quad.rs30
-rw-r--r--graphics/src/lib.rs29
-rw-r--r--graphics/src/mesh.rs76
-rw-r--r--graphics/src/overlay/menu.rs3
-rw-r--r--graphics/src/primitive.rs97
-rw-r--r--graphics/src/renderer.rs132
-rw-r--r--graphics/src/triangle.rs33
-rw-r--r--graphics/src/viewport.rs4
-rw-r--r--graphics/src/widget/canvas/cache.rs100
-rw-r--r--graphics/src/widget/canvas/cursor.rs64
-rw-r--r--graphics/src/widget/canvas/geometry.rs24
-rw-r--r--graphics/src/window.rs10
-rw-r--r--graphics/src/window/gl_compositor.rs71
-rw-r--r--lazy/Cargo.toml18
-rw-r--r--native/README.md37
-rw-r--r--native/src/mouse.rs6
-rw-r--r--native/src/runtime.rs18
-rw-r--r--native/src/subscription.rs248
-rw-r--r--native/src/text.rs114
-rw-r--r--native/src/widget/action.rs154
-rw-r--r--native/src/widget/helpers.rs317
-rw-r--r--native/src/widget/operation.rs112
-rw-r--r--native/src/window.rs38
-rw-r--r--native/src/window/icon.rs12
-rw-r--r--native/src/window/position.rs22
-rw-r--r--renderer/Cargo.toml30
-rw-r--r--renderer/src/backend.rs100
-rw-r--r--renderer/src/compositor.rs262
-rw-r--r--renderer/src/geometry.rs184
-rw-r--r--renderer/src/geometry/cache.rs125
-rw-r--r--renderer/src/lib.rs269
-rw-r--r--renderer/src/settings.rs31
-rw-r--r--renderer/src/widget.rs (renamed from graphics/src/widget.rs)5
-rw-r--r--runtime/Cargo.toml (renamed from native/Cargo.toml)14
-rw-r--r--runtime/README.md18
-rw-r--r--runtime/src/clipboard.rs (renamed from native/src/clipboard.rs)37
-rw-r--r--runtime/src/command.rs (renamed from native/src/command.rs)55
-rw-r--r--runtime/src/command/action.rs (renamed from native/src/command/action.rs)24
-rw-r--r--runtime/src/debug/basic.rs (renamed from native/src/debug/basic.rs)2
-rw-r--r--runtime/src/debug/null.rs (renamed from native/src/debug/null.rs)0
-rw-r--r--runtime/src/font.rs19
-rw-r--r--runtime/src/keyboard.rs (renamed from native/src/keyboard.rs)0
-rw-r--r--runtime/src/lib.rs (renamed from native/src/lib.rs)50
-rw-r--r--runtime/src/overlay.rs4
-rw-r--r--runtime/src/overlay/nested.rs353
-rw-r--r--runtime/src/program.rs (renamed from native/src/program.rs)7
-rw-r--r--runtime/src/program/state.rs (renamed from native/src/program/state.rs)64
-rw-r--r--runtime/src/system.rs (renamed from native/src/system.rs)0
-rw-r--r--runtime/src/system/action.rs (renamed from native/src/system/action.rs)0
-rw-r--r--runtime/src/system/information.rs (renamed from native/src/system/information.rs)0
-rw-r--r--runtime/src/user_interface.rs (renamed from native/src/user_interface.rs)224
-rw-r--r--runtime/src/window.rs135
-rw-r--r--runtime/src/window/action.rs (renamed from native/src/window/action.rs)73
-rw-r--r--runtime/src/window/screenshot.rs92
-rw-r--r--src/advanced.rs16
-rw-r--r--src/application.rs33
-rw-r--r--src/clipboard.rs3
-rw-r--r--src/element.rs5
-rw-r--r--src/error.rs16
-rw-r--r--src/executor.rs14
-rw-r--r--src/keyboard.rs2
-rw-r--r--src/lib.rs159
-rw-r--r--src/mouse.rs2
-rw-r--r--src/overlay.rs18
-rw-r--r--src/result.rs6
-rw-r--r--src/sandbox.rs22
-rw-r--r--src/settings.rs32
-rw-r--r--src/touch.rs2
-rw-r--r--src/widget.rs238
-rw-r--r--src/window.rs12
-rw-r--r--style/Cargo.toml6
-rw-r--r--style/src/button.rs9
-rw-r--r--style/src/checkbox.rs4
-rw-r--r--style/src/container.rs6
-rw-r--r--style/src/lib.rs3
-rw-r--r--style/src/menu.rs4
-rw-r--r--style/src/pane_grid.rs38
-rw-r--r--style/src/pick_list.rs4
-rw-r--r--style/src/progress_bar.rs4
-rw-r--r--style/src/rule.rs4
-rw-r--r--style/src/scrollable.rs28
-rw-r--r--style/src/slider.rs17
-rw-r--r--style/src/text.rs20
-rw-r--r--style/src/text_input.rs12
-rw-r--r--style/src/theme.rs182
-rw-r--r--style/src/theme/palette.rs22
-rw-r--r--tiny_skia/Cargo.toml38
-rw-r--r--tiny_skia/fonts/Iced-Icons.ttf (renamed from graphics/fonts/Icons.ttf)bin5032 -> 5108 bytes
-rw-r--r--tiny_skia/src/backend.rs856
-rw-r--r--tiny_skia/src/geometry.rs315
-rw-r--r--tiny_skia/src/lib.rs28
-rw-r--r--tiny_skia/src/primitive.rs48
-rw-r--r--tiny_skia/src/raster.rs116
-rw-r--r--tiny_skia/src/settings.rs24
-rw-r--r--tiny_skia/src/text.rs483
-rw-r--r--tiny_skia/src/vector.rs195
-rw-r--r--tiny_skia/src/window.rs3
-rw-r--r--tiny_skia/src/window/compositor.rs228
-rw-r--r--wgpu/Cargo.toml69
-rw-r--r--wgpu/README.md2
-rw-r--r--wgpu/fonts/Iced-Icons.ttfbin0 -> 5108 bytes
-rw-r--r--wgpu/src/backend.rs429
-rw-r--r--wgpu/src/buffer.rs109
-rw-r--r--wgpu/src/buffer/dynamic.rs219
-rw-r--r--wgpu/src/buffer/static.rs117
-rw-r--r--wgpu/src/color.rs165
-rw-r--r--wgpu/src/geometry.rs (renamed from graphics/src/widget/canvas/frame.rs)261
-rw-r--r--wgpu/src/image.rs287
-rw-r--r--wgpu/src/image/atlas.rs227
-rw-r--r--wgpu/src/image/atlas/allocation.rs3
-rw-r--r--wgpu/src/image/atlas/allocator.rs4
-rw-r--r--wgpu/src/image/atlas/entry.rs9
-rw-r--r--wgpu/src/image/raster.rs119
-rw-r--r--wgpu/src/image/vector.rs (renamed from graphics/src/image/vector.rs)77
-rw-r--r--wgpu/src/layer.rs (renamed from graphics/src/layer.rs)180
-rw-r--r--wgpu/src/layer/image.rs (renamed from graphics/src/layer/image.rs)6
-rw-r--r--wgpu/src/layer/mesh.rs (renamed from graphics/src/layer/mesh.rs)19
-rw-r--r--wgpu/src/layer/text.rs (renamed from graphics/src/layer/text.rs)14
-rw-r--r--wgpu/src/lib.rs25
-rw-r--r--wgpu/src/primitive.rs21
-rw-r--r--wgpu/src/quad.rs455
-rw-r--r--wgpu/src/quad/gradient.rs165
-rw-r--r--wgpu/src/quad/solid.rs150
-rw-r--r--wgpu/src/settings.rs55
-rw-r--r--wgpu/src/shader/gradient.wgsl88
-rw-r--r--wgpu/src/shader/quad.wgsl293
-rw-r--r--wgpu/src/shader/solid.wgsl30
-rw-r--r--wgpu/src/shader/triangle.wgsl165
-rw-r--r--wgpu/src/text.rs661
-rw-r--r--wgpu/src/triangle.rs900
-rw-r--r--wgpu/src/triangle/msaa.rs19
-rw-r--r--wgpu/src/window.rs3
-rw-r--r--wgpu/src/window/compositor.rs373
-rw-r--r--widget/Cargo.toml37
-rw-r--r--widget/src/button.rs (renamed from native/src/widget/button.rs)92
-rw-r--r--widget/src/canvas.rs (renamed from graphics/src/widget/canvas.rs)145
-rw-r--r--widget/src/canvas/event.rs (renamed from graphics/src/widget/canvas/event.rs)8
-rw-r--r--widget/src/canvas/program.rs (renamed from graphics/src/widget/canvas/program.rs)42
-rw-r--r--widget/src/checkbox.rs (renamed from native/src/widget/checkbox.rs)130
-rw-r--r--widget/src/column.rs (renamed from native/src/widget/column.rs)50
-rw-r--r--widget/src/container.rs (renamed from native/src/widget/container.rs)42
-rw-r--r--widget/src/helpers.rs368
-rw-r--r--widget/src/image.rs (renamed from native/src/widget/image.rs)22
-rw-r--r--widget/src/image/viewer.rs (renamed from native/src/widget/image/viewer.rs)39
-rw-r--r--widget/src/lazy.rs (renamed from lazy/src/lazy.rs)134
-rw-r--r--widget/src/lazy/cache.rs (renamed from lazy/src/cache.rs)4
-rw-r--r--widget/src/lazy/component.rs (renamed from lazy/src/component.rs)356
-rw-r--r--widget/src/lazy/helpers.rs (renamed from lazy/src/lib.rs)45
-rw-r--r--widget/src/lazy/responsive.rs (renamed from lazy/src/responsive.rs)181
-rw-r--r--widget/src/lib.rs126
-rw-r--r--widget/src/mouse_area.rs311
-rw-r--r--widget/src/overlay.rs (renamed from graphics/src/overlay.rs)0
-rw-r--r--widget/src/overlay/menu.rs (renamed from native/src/overlay/menu.rs)219
-rw-r--r--widget/src/pane_grid.rs (renamed from native/src/widget/pane_grid.rs)500
-rw-r--r--widget/src/pane_grid/axis.rs (renamed from native/src/widget/pane_grid/axis.rs)2
-rw-r--r--widget/src/pane_grid/configuration.rs (renamed from native/src/widget/pane_grid/configuration.rs)2
-rw-r--r--widget/src/pane_grid/content.rs (renamed from native/src/widget/pane_grid/content.rs)102
-rw-r--r--widget/src/pane_grid/direction.rs (renamed from native/src/widget/pane_grid/direction.rs)0
-rw-r--r--widget/src/pane_grid/draggable.rs (renamed from native/src/widget/pane_grid/draggable.rs)8
-rw-r--r--widget/src/pane_grid/node.rs (renamed from native/src/widget/pane_grid/node.rs)14
-rw-r--r--widget/src/pane_grid/pane.rs (renamed from native/src/widget/pane_grid/pane.rs)0
-rw-r--r--widget/src/pane_grid/split.rs (renamed from native/src/widget/pane_grid/split.rs)0
-rw-r--r--widget/src/pane_grid/state.rs (renamed from native/src/widget/pane_grid/state.rs)107
-rw-r--r--widget/src/pane_grid/title_bar.rs (renamed from native/src/widget/pane_grid/title_bar.rs)42
-rw-r--r--widget/src/pick_list.rs (renamed from native/src/widget/pick_list.rs)228
-rw-r--r--widget/src/progress_bar.rs (renamed from native/src/widget/progress_bar.rs)31
-rw-r--r--widget/src/qr_code.rs (renamed from graphics/src/widget/qr_code.rs)104
-rw-r--r--widget/src/radio.rs (renamed from native/src/widget/radio.rs)131
-rw-r--r--widget/src/row.rs (renamed from native/src/widget/row.rs)50
-rw-r--r--widget/src/rule.rs (renamed from native/src/widget/rule.rs)27
-rw-r--r--widget/src/scrollable.rs (renamed from native/src/widget/scrollable.rs)843
-rw-r--r--widget/src/slider.rs (renamed from native/src/widget/slider.rs)155
-rw-r--r--widget/src/space.rs (renamed from native/src/widget/space.rs)16
-rw-r--r--widget/src/svg.rs (renamed from native/src/widget/svg.rs)19
-rw-r--r--widget/src/text.rs6
-rw-r--r--widget/src/text_input.rs (renamed from native/src/widget/text_input.rs)374
-rw-r--r--widget/src/text_input/cursor.rs (renamed from native/src/widget/text_input/cursor.rs)2
-rw-r--r--widget/src/text_input/editor.rs (renamed from native/src/widget/text_input/editor.rs)2
-rw-r--r--widget/src/text_input/value.rs (renamed from native/src/widget/text_input/value.rs)0
-rw-r--r--widget/src/toggler.rs (renamed from native/src/widget/toggler.rs)89
-rw-r--r--widget/src/tooltip.rs (renamed from native/src/widget/tooltip.rs)313
-rw-r--r--widget/src/vertical_slider.rs (renamed from native/src/widget/vertical_slider.rs)155
-rw-r--r--winit/Cargo.toml31
-rw-r--r--winit/README.md2
-rw-r--r--winit/src/application.rs208
-rw-r--r--winit/src/application/state.rs41
-rw-r--r--winit/src/clipboard.rs17
-rw-r--r--winit/src/conversion.rs50
-rw-r--r--winit/src/error.rs7
-rw-r--r--winit/src/icon.rs217
-rw-r--r--winit/src/lib.rs18
-rw-r--r--winit/src/proxy.rs2
-rw-r--r--winit/src/settings.rs61
-rw-r--r--winit/src/settings/windows.rs3
-rw-r--r--winit/src/system.rs7
-rw-r--r--winit/src/window.rs128
394 files changed, 17244 insertions, 13256 deletions
diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml
index e93a01ae..d4c94fcd 100644
--- a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml
+++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml
@@ -62,15 +62,15 @@ body:
If you are using an older release, please upgrade to the latest one before filing an issue.
options:
+ - crates.io release
- master
- - 0.7
validations:
required: true
- type: dropdown
id: os
attributes:
- label: Operative System
- description: Which operative system are you using?
+ label: Operating System
+ description: Which operating system are you using?
options:
- Windows
- macOS
diff --git a/.github/workflows/document.yml b/.github/workflows/document.yml
index e69f4d63..09a7a4d5 100644
--- a/.github/workflows/document.yml
+++ b/.github/workflows/document.yml
@@ -20,13 +20,13 @@ jobs:
-p iced_core \
-p iced_style \
-p iced_futures \
- -p iced_native \
- -p iced_lazy \
+ -p iced_runtime \
-p iced_graphics \
-p iced_wgpu \
- -p iced_glow \
+ -p iced_tiny_skia \
+ -p iced_renderer \
+ -p iced_widget \
-p iced_winit \
- -p iced_glutin \
-p iced
- name: Write CNAME file
run: echo 'docs.iced.rs' > ./target/doc/CNAME
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 38b81842..a9a9b3f9 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -37,5 +37,5 @@ jobs:
run: cargo build --package tour --target wasm32-unknown-unknown
- name: Check compilation of `todos` example
run: cargo build --package todos --target wasm32-unknown-unknown
- - name: Check compilation of `integration_wgpu` example
- run: cargo build --package integration_wgpu --target wasm32-unknown-unknown
+ - name: Check compilation of `integration` example
+ run: cargo build --package integration --target wasm32-unknown-unknown
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1d9d1fb3..077f4af3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,59 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [0.9.0] - 2023-04-13
+### Added
+- `MouseArea` widget. [#1594](https://github.com/iced-rs/iced/pull/1594)
+- `channel` helper in `subscription`. [#1786](https://github.com/iced-rs/iced/pull/1786)
+- Configurable `width` for `Scrollable`. [#1749](https://github.com/iced-rs/iced/pull/1749)
+- Support for disabled `TextInput`. [#1744](https://github.com/iced-rs/iced/pull/1744)
+- Platform-specific window settings. [#1730](https://github.com/iced-rs/iced/pull/1730)
+- Left and right colors for sliders. [#1643](https://github.com/iced-rs/iced/pull/1643)
+- Icon for `TextInput`. [#1702](https://github.com/iced-rs/iced/pull/1702)
+- Mouse over scrollbar flag for `scrollable::StyleSheet`. [#1669](https://github.com/iced-rs/iced/pull/1669)
+- Better example for `Radio`. [#1762](https://github.com/iced-rs/iced/pull/1762)
+
+### Changed
+- `wgpu` has been updated to `0.15` in `iced_wgpu`. [#1789](https://github.com/iced-rs/iced/pull/1789)
+- `resvg` has been updated to `0.29` in `iced_graphics`. [#1733](https://github.com/iced-rs/iced/pull/1733)
+- `subscription::run` now takes a function pointer. [#1723](https://github.com/iced-rs/iced/pull/1723)
+
+### Fixed
+- Redundant `on_scroll` messages for `Scrollable`. [#1788](https://github.com/iced-rs/iced/pull/1788)
+- Outdated items in `ROADMAP.md` [#1782](https://github.com/iced-rs/iced/pull/1782)
+- Colons in shader labels causing compilation issues in `iced_wgpu`. [#1779](https://github.com/iced-rs/iced/pull/1779)
+- Re-expose winit features for window servers in Linux. [#1777](https://github.com/iced-rs/iced/pull/1777)
+- Replacement of application node in Wasm. [#1765](https://github.com/iced-rs/iced/pull/1765)
+- `clippy` lints for Rust 1.68. [#1755](https://github.com/iced-rs/iced/pull/1755)
+- Unnecessary `Component` rebuilds. [#1754](https://github.com/iced-rs/iced/pull/1754)
+- Incorrect package name in checkbox example docs. [#1750](https://github.com/iced-rs/iced/pull/1750)
+- Fullscreen only working on primary monitor. [#1742](https://github.com/iced-rs/iced/pull/1742)
+- `Padding::fit` on irregular values for an axis. [#1734](https://github.com/iced-rs/iced/pull/1734)
+- `Debug` implementation of `Font` displaying its bytes. [#1731](https://github.com/iced-rs/iced/pull/1731)
+- Sliders bleeding over their rail. [#1721](https://github.com/iced-rs/iced/pull/1721)
+
+### Removed
+- `Fill` variant for `Alignment`. [#1735](https://github.com/iced-rs/iced/pull/1735)
+
+Many thanks to...
+
+- @ahoneybun
+- @bq-wrongway
+- @bungoboingo
+- @casperstorm
+- @Davidster
+- @ElhamAryanpur
+- @FinnPerry
+- @GyulyVGC
+- @JungleTryne
+- @lupd
+- @mmstick
+- @nicksenger
+- @Night-Hunter-NF
+- @tarkah
+- @traxys
+- @Xaeroxe
+
## [0.8.0] - 2023-02-18
### Added
- Generic pixel units. [#1711](https://github.com/iced-rs/iced/pull/1711)
@@ -414,7 +467,8 @@ Many thanks to...
### Added
- First release! :tada:
-[Unreleased]: https://github.com/iced-rs/iced/compare/0.8.0...HEAD
+[Unreleased]: https://github.com/iced-rs/iced/compare/0.9.0...HEAD
+[0.9.0]: https://github.com/iced-rs/iced/compare/0.8.0...0.9.0
[0.8.0]: https://github.com/iced-rs/iced/compare/0.7.0...0.8.0
[0.7.0]: https://github.com/iced-rs/iced/compare/0.6.0...0.7.0
[0.6.0]: https://github.com/iced-rs/iced/compare/0.5.0...0.6.0
diff --git a/Cargo.toml b/Cargo.toml
index 7f89b05e..4a82c923 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "iced"
-version = "0.8.0"
+version = "0.9.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
description = "A cross-platform GUI library inspired by Elm"
@@ -13,20 +13,18 @@ categories = ["gui"]
[features]
default = ["wgpu"]
+# Enable the `wgpu` GPU-accelerated renderer backend
+wgpu = ["iced_renderer/wgpu"]
# Enables the `Image` widget
-image = ["iced_wgpu?/image", "iced_glow?/image", "image_rs"]
+image = ["iced_widget/image", "image_rs"]
# Enables the `Svg` widget
-svg = ["iced_wgpu?/svg", "iced_glow?/svg"]
+svg = ["iced_widget/svg"]
# Enables the `Canvas` widget
-canvas = ["iced_graphics/canvas"]
+canvas = ["iced_widget/canvas"]
# Enables the `QRCode` widget
-qr_code = ["iced_graphics/qr_code"]
-# Enables the `iced_wgpu` renderer
-wgpu = ["iced_wgpu"]
-# Enables using system fonts
-default_system_font = ["iced_wgpu?/default_system_font", "iced_glow?/default_system_font"]
-# Enables the `iced_glow` renderer. Overrides `iced_wgpu`
-glow = ["iced_glow", "iced_glutin"]
+qr_code = ["iced_widget/qr_code"]
+# Enables lazy widgets
+lazy = ["iced_widget/lazy"]
# Enables a debug view in native platforms (press F12)
debug = ["iced_winit/debug"]
# Enables `tokio` as the `executor::Default` on native platforms
@@ -39,13 +37,10 @@ smol = ["iced_futures/smol"]
palette = ["iced_core/palette"]
# Enables querying system information
system = ["iced_winit/system"]
-# Enables chrome traces
-chrome-trace = [
- "iced_winit/chrome-trace",
- "iced_glutin?/trace",
- "iced_wgpu?/tracing",
- "iced_glow?/tracing",
-]
+# Enables broken "sRGB linear" blending to reproduce color management of the Web
+web-colors = ["iced_renderer/web-colors"]
+# Enables the advanced module
+advanced = []
# Enables experimental multi-window support for iced_winit + wgpu.
multi-window = ["iced_winit/multi-window"]
@@ -57,37 +52,29 @@ members = [
"core",
"futures",
"graphics",
- "glow",
- "glutin",
- "lazy",
- "native",
+ "runtime",
+ "renderer",
"style",
+ "tiny_skia",
"wgpu",
+ "widget",
"winit",
"examples/*",
]
[dependencies]
-iced_core = { version = "0.8", path = "core" }
+iced_core = { version = "0.9", path = "core" }
iced_futures = { version = "0.6", path = "futures" }
-iced_native = { version = "0.9", path = "native" }
-iced_graphics = { version = "0.7", path = "graphics" }
-iced_winit = { version = "0.8", path = "winit", features = ["application"] }
-iced_glutin = { version = "0.7", path = "glutin", optional = true }
-iced_glow = { version = "0.7", path = "glow", optional = true }
-thiserror = "1.0"
+iced_renderer = { version = "0.1", path = "renderer" }
+iced_widget = { version = "0.1", path = "widget" }
+iced_winit = { version = "0.9", path = "winit", features = ["application"] }
+thiserror = "1"
[dependencies.image_rs]
version = "0.24"
package = "image"
optional = true
-[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
-iced_wgpu = { version = "0.9", path = "wgpu", optional = true }
-
-[target.'cfg(target_arch = "wasm32")'.dependencies]
-iced_wgpu = { version = "0.9", path = "wgpu", features = ["webgl"], optional = true }
-
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
features = ["image", "svg", "canvas", "qr_code"]
diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md
new file mode 100644
index 00000000..809371cb
--- /dev/null
+++ b/DEPENDENCIES.md
@@ -0,0 +1,33 @@
+# Dependencies
+
+Iced requires some system dependencies to work, and not
+all operating systems come with them installed.
+
+You can follow the provided instructions for your system to
+get them, if your system isn't here, add it!
+
+## NixOS
+
+You can add this `shell.nix` to your project and use it by running `nix-shell`:
+
+```nix
+{ pkgs ? import <nixpkgs> {} }:
+
+pkgs.mkShell rec {
+ buildInputs = with pkgs; [
+ expat
+ fontconfig
+ freetype
+ freetype.dev
+ libGL
+ pkgconfig
+ xorg.libX11
+ xorg.libXcursor
+ xorg.libXi
+ xorg.libXrandr
+ ];
+
+ LD_LIBRARY_PATH =
+ builtins.foldl' (a: b: "${a}:${b}/lib") "${pkgs.vulkan-loader}/lib" buildInputs;
+}
+```
diff --git a/README.md b/README.md
index a5ebf230..c72fd770 100644
--- a/README.md
+++ b/README.md
@@ -35,9 +35,9 @@ Inspired by [Elm].
* First-class support for async actions (use futures!)
* [Modular ecosystem] split into reusable parts:
* A [renderer-agnostic native runtime] enabling integration with existing systems
- * Two [built-in renderers] leveraging [`wgpu`] and [`glow`]
+ * Two [built-in renderers] leveraging [`wgpu`] and [`tiny-skia`]
* [`iced_wgpu`] supporting Vulkan, Metal and DX12
- * [`iced_glow`] supporting OpenGL 2.1+ and OpenGL ES 2.0+
+ * [`iced_tiny_skia`] offering a software alternative as a fallback
* A [windowing shell]
* A [web runtime] leveraging the DOM
@@ -52,9 +52,9 @@ __Iced is currently experimental software.__ [Take a look at the roadmap],
[Modular ecosystem]: ECOSYSTEM.md
[renderer-agnostic native runtime]: native/
[`wgpu`]: https://github.com/gfx-rs/wgpu
-[`glow`]: https://github.com/grovesNL/glow
+[`tiny-skia`]: https://github.com/RazrFalcon/tiny-skia
[`iced_wgpu`]: wgpu/
-[`iced_glow`]: glow/
+[`iced_tiny_skia`]: tiny_skia/
[built-in renderers]: ECOSYSTEM.md#Renderers
[windowing shell]: winit/
[`dodrio`]: https://github.com/fitzgen/dodrio
@@ -68,7 +68,7 @@ __Iced is currently experimental software.__ [Take a look at the roadmap],
Add `iced` as a dependency in your `Cargo.toml`:
```toml
-iced = "0.8"
+iced = "0.9"
```
If your project is using a Rust edition older than 2021, then you will need to
@@ -196,34 +196,6 @@ end-user-oriented GUI library, while keeping [the ecosystem] modular:
[`ggez`]: https://github.com/ggez/ggez
[the ecosystem]: ECOSYSTEM.md
-## Troubleshooting
-
-### `GraphicsAdapterNotFound`
-
-This occurs when the selected [built-in renderer] is not able to create a context.
-
-Often this will occur while using [`iced_wgpu`] as the renderer without
-supported hardware (needs Vulkan, Metal or DX12). In this case, you could try using the
-[`iced_glow`] renderer:
-
-First, check if it works with
-
-```console
-cargo run --features iced/glow --package game_of_life
-```
-
-and then use it in your project with
-
-```toml
-iced = { version = "0.8", default-features = false, features = ["glow"] }
-```
-
-__NOTE:__ Chances are you have hardware that supports at least OpenGL 2.1 or OpenGL ES 2.0,
-but if you don't, right now there's no software fallback, so it means your hardware
-doesn't support Iced.
-
-[built-in renderer]: https://github.com/iced-rs/iced/blob/master/ECOSYSTEM.md#Renderers
-
## Contributing / Feedback
Contributions are greatly appreciated! If you want to contribute, please
diff --git a/ROADMAP.md b/ROADMAP.md
index ef6df0e4..f1893664 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -19,6 +19,7 @@ Once a step is completed, it is collapsed and added to this list:
* [x] Custom styling ([#146])
* [x] Canvas for 2D graphics ([#193])
* [x] Basic overlay support ([#444])
+ * [x] Animations [#31]
[#24]: https://github.com/iced-rs/iced/issues/24
[#25]: https://github.com/iced-rs/iced/issues/25
@@ -29,6 +30,7 @@ Once a step is completed, it is collapsed and added to this list:
[#146]: https://github.com/iced-rs/iced/pull/146
[#193]: https://github.com/iced-rs/iced/pull/193
[#444]: https://github.com/iced-rs/iced/pull/444
+[#31]: https://github.com/iced-rs/iced/issues/31
### Multi-window support ([#27])
Open and control multiple windows at runtime.
@@ -39,16 +41,7 @@ This approach should also allow us to perform custom optimizations for this part
[#27]: https://github.com/iced-rs/iced/issues/27
-### Animations ([#31])
-Allow widgets to request a redraw at a specific time.
-
-This is a necessary feature to render loading spinners, a blinking text cursor, GIF images, etc.
-
-[`winit`] allows flexible control of its event loop. We may be able to use [`ControlFlow::WaitUntil`](https://docs.rs/winit/0.20.0-alpha3/winit/event_loop/enum.ControlFlow.html#variant.WaitUntil) for this purpose.
-
-[#31]: https://github.com/iced-rs/iced/issues/31
-
-### Canvas widget for 3D graphics ([#32])
+### Canvas widget for 3D graphics (~~[#32]~~ [#343])
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.
@@ -56,6 +49,7 @@ As a first approach, we could expose the underlying renderer directly here, and
In the long run, we could expose a renderer-agnostic abstraction to perform the drawing.
[#32]: https://github.com/iced-rs/iced/issues/32
+[#343]: https://github.com/iced-rs/iced/issues/343
### Text shaping and font fallback ([#33])
[`wgpu_glyph`] uses [`glyph_brush`], which in turn uses [`rusttype`]. While the current implementation is able to layout text quite nicely, it does not perform any [text shaping].
diff --git a/core/Cargo.toml b/core/Cargo.toml
index 0d6310d3..55f2e85f 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "iced_core"
-version = "0.8.1"
+version = "0.9.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
description = "The essential concepts of Iced"
@@ -9,10 +9,16 @@ repository = "https://github.com/iced-rs/iced"
[dependencies]
bitflags = "1.2"
+thiserror = "1"
+log = "0.4.17"
+twox-hash = { version = "1.5", default-features = false }
[dependencies.palette]
-version = "0.6"
+version = "0.7"
optional = true
[target.'cfg(target_arch = "wasm32")'.dependencies]
instant = "0.1"
+
+[dev-dependencies]
+approx = "0.5"
diff --git a/core/README.md b/core/README.md
index 64d92e78..519e0608 100644
--- a/core/README.md
+++ b/core/README.md
@@ -18,7 +18,7 @@ This crate is meant to be a starting point for an Iced runtime.
Add `iced_core` as a dependency in your `Cargo.toml`:
```toml
-iced_core = "0.8"
+iced_core = "0.9"
```
__Iced moves fast and the `master` branch can contain breaking changes!__ If
diff --git a/core/src/angle.rs b/core/src/angle.rs
new file mode 100644
index 00000000..75a57c76
--- /dev/null
+++ b/core/src/angle.rs
@@ -0,0 +1,33 @@
+use crate::{Point, Rectangle, Vector};
+use std::f32::consts::PI;
+
+#[derive(Debug, Copy, Clone, PartialEq)]
+/// Degrees
+pub struct Degrees(pub f32);
+
+#[derive(Debug, Copy, Clone, PartialEq)]
+/// Radians
+pub struct Radians(pub f32);
+
+impl From<Degrees> for Radians {
+ fn from(degrees: Degrees) -> Self {
+ Radians(degrees.0 * PI / 180.0)
+ }
+}
+
+impl Radians {
+ /// Calculates the line in which the [`Angle`] intercepts the `bounds`.
+ pub fn to_distance(&self, bounds: &Rectangle) -> (Point, Point) {
+ let v1 = Vector::new(f32::cos(self.0), f32::sin(self.0));
+
+ let distance_to_rect = f32::min(
+ f32::abs((bounds.y - bounds.center().y) / v1.y),
+ f32::abs(((bounds.x + bounds.width) - bounds.center().x) / v1.x),
+ );
+
+ let start = bounds.center() + v1 * distance_to_rect;
+ let end = bounds.center() - v1 * distance_to_rect;
+
+ (start, end)
+ }
+}
diff --git a/core/src/background.rs b/core/src/background.rs
index cfb95867..347c52c0 100644
--- a/core/src/background.rs
+++ b/core/src/background.rs
@@ -1,11 +1,14 @@
+use crate::gradient::{self, Gradient};
use crate::Color;
/// The background of some element.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Background {
- /// A solid color
+ /// A solid color.
Color(Color),
- // TODO: Add gradient and image variants
+ /// Linearly interpolate between several colors.
+ Gradient(Gradient),
+ // TODO: Add image variant
}
impl From<Color> for Background {
@@ -14,8 +17,14 @@ impl From<Color> for Background {
}
}
-impl From<Color> for Option<Background> {
- fn from(color: Color) -> Self {
- Some(Background::from(color))
+impl From<Gradient> for Background {
+ fn from(gradient: Gradient) -> Self {
+ Background::Gradient(gradient)
+ }
+}
+
+impl From<gradient::Linear> for Background {
+ fn from(gradient: gradient::Linear) -> Self {
+ Background::Gradient(Gradient::Linear(gradient))
}
}
diff --git a/core/src/border_radius.rs b/core/src/border_radius.rs
new file mode 100644
index 00000000..a444dd74
--- /dev/null
+++ b/core/src/border_radius.rs
@@ -0,0 +1,22 @@
+/// The border radii for the corners of a graphics primitive in the order:
+/// top-left, top-right, bottom-right, bottom-left.
+#[derive(Debug, Clone, Copy, PartialEq, Default)]
+pub struct BorderRadius([f32; 4]);
+
+impl From<f32> for BorderRadius {
+ fn from(w: f32) -> Self {
+ Self([w; 4])
+ }
+}
+
+impl From<[f32; 4]> for BorderRadius {
+ fn from(radi: [f32; 4]) -> Self {
+ Self(radi)
+ }
+}
+
+impl From<BorderRadius> for [f32; 4] {
+ fn from(radi: BorderRadius) -> Self {
+ radi.0
+ }
+}
diff --git a/core/src/clipboard.rs b/core/src/clipboard.rs
new file mode 100644
index 00000000..081b4004
--- /dev/null
+++ b/core/src/clipboard.rs
@@ -0,0 +1,23 @@
+//! Access the clipboard.
+
+/// A buffer for short-term storage and transfer within and between
+/// applications.
+pub trait Clipboard {
+ /// Reads the current content of the [`Clipboard`] as text.
+ fn read(&self) -> Option<String>;
+
+ /// Writes the given text contents to the [`Clipboard`].
+ fn write(&mut self, contents: String);
+}
+
+/// A null implementation of the [`Clipboard`] trait.
+#[derive(Debug, Clone, Copy)]
+pub struct Null;
+
+impl Clipboard for Null {
+ fn read(&self) -> Option<String> {
+ None
+ }
+
+ fn write(&mut self, _contents: String) {}
+}
diff --git a/core/src/color.rs b/core/src/color.rs
index fe0a1856..1392f28b 100644
--- a/core/src/color.rs
+++ b/core/src/color.rs
@@ -183,15 +183,15 @@ macro_rules! color {
}
#[cfg(feature = "palette")]
-/// Converts from palette's `Srgba` type to a [`Color`].
+/// Converts from palette's `Rgba` type to a [`Color`].
impl From<Srgba> for Color {
- fn from(srgba: Srgba) -> Self {
- Color::new(srgba.red, srgba.green, srgba.blue, srgba.alpha)
+ fn from(rgba: Srgba) -> Self {
+ Color::new(rgba.red, rgba.green, rgba.blue, rgba.alpha)
}
}
#[cfg(feature = "palette")]
-/// Converts from [`Color`] to palette's `Srgba` type.
+/// Converts from [`Color`] to palette's `Rgba` type.
impl From<Color> for Srgba {
fn from(c: Color) -> Self {
Srgba::new(c.r, c.g, c.b, c.a)
@@ -199,15 +199,15 @@ impl From<Color> for Srgba {
}
#[cfg(feature = "palette")]
-/// Converts from palette's `Srgb` type to a [`Color`].
+/// Converts from palette's `Rgb` type to a [`Color`].
impl From<Srgb> for Color {
- fn from(srgb: Srgb) -> Self {
- Color::new(srgb.red, srgb.green, srgb.blue, 1.0)
+ fn from(rgb: Srgb) -> Self {
+ Color::new(rgb.red, rgb.green, rgb.blue, 1.0)
}
}
#[cfg(feature = "palette")]
-/// Converts from [`Color`] to palette's `Srgb` type.
+/// Converts from [`Color`] to palette's `Rgb` type.
impl From<Color> for Srgb {
fn from(c: Color) -> Self {
Srgb::new(c.r, c.g, c.b)
@@ -218,12 +218,12 @@ impl From<Color> for Srgb {
#[cfg(test)]
mod tests {
use super::*;
- use palette::Blend;
+ use palette::blend::Blend;
#[test]
fn srgba_traits() {
let c = Color::from_rgb(0.5, 0.4, 0.3);
- // Round-trip conversion to the palette:Srgba type
+ // Round-trip conversion to the palette::Srgba type
let s: Srgba = c.into();
let r: Color = s.into();
assert_eq!(c, r);
@@ -231,6 +231,8 @@ mod tests {
#[test]
fn color_manipulation() {
+ use approx::assert_relative_eq;
+
let c1 = Color::from_rgb(0.5, 0.4, 0.3);
let c2 = Color::from_rgb(0.2, 0.5, 0.3);
@@ -238,19 +240,15 @@ mod tests {
let l1 = Srgba::from(c1).into_linear();
let l2 = Srgba::from(c2).into_linear();
- // Take the lighter of each of the RGB components
+ // Take the lighter of each of the sRGB 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
- }
- );
+ let result: Color = Srgba::from_linear(lighter).into();
+
+ assert_relative_eq!(result.r, 0.5);
+ assert_relative_eq!(result.g, 0.5);
+ assert_relative_eq!(result.b, 0.3);
+ assert_relative_eq!(result.a, 1.0);
}
}
diff --git a/native/src/element.rs b/core/src/element.rs
index 0a677d20..3268f14b 100644
--- a/native/src/element.rs
+++ b/core/src/element.rs
@@ -5,9 +5,7 @@ use crate::overlay;
use crate::renderer;
use crate::widget;
use crate::widget::tree::{self, Tree};
-use crate::{
- Clipboard, Color, Layout, Length, Point, Rectangle, Shell, Widget,
-};
+use crate::{Clipboard, Color, Layout, Length, Rectangle, Shell, Widget};
use std::any::Any;
use std::borrow::Borrow;
@@ -90,41 +88,65 @@ impl<'a, Message, Renderer> Element<'a, Message, Renderer> {
/// We compose the previous __messages__ with the index of the counter
/// producing them. Let's implement our __view logic__ now:
///
- /// ```
+ /// ```no_run
/// # mod counter {
- /// # type Text<'a> = iced_native::widget::Text<'a, iced_native::renderer::Null>;
- /// #
/// # #[derive(Debug, Clone, Copy)]
/// # pub enum Message {}
/// # pub struct Counter;
/// #
/// # impl Counter {
- /// # pub fn view(&mut self) -> Text {
- /// # Text::new("")
+ /// # pub fn view(
+ /// # &self,
+ /// # ) -> iced_core::Element<Message, iced_core::renderer::Null> {
+ /// # unimplemented!()
/// # }
/// # }
/// # }
/// #
- /// # mod iced_wgpu {
- /// # pub use iced_native::renderer::Null as Renderer;
- /// # }
+ /// # mod iced {
+ /// # pub use iced_core::renderer::Null as Renderer;
+ /// # pub use iced_core::Element;
/// #
- /// # use counter::Counter;
+ /// # pub mod widget {
+ /// # pub struct Row<Message> {
+ /// # _t: std::marker::PhantomData<Message>,
+ /// # }
/// #
- /// # struct ManyCounters {
- /// # counters: Vec<Counter>,
- /// # }
+ /// # impl<Message> Row<Message> {
+ /// # pub fn new() -> Self {
+ /// # unimplemented!()
+ /// # }
/// #
- /// # #[derive(Debug, Clone, Copy)]
- /// # pub enum Message {
- /// # Counter(usize, counter::Message)
+ /// # pub fn spacing(mut self, _: u32) -> Self {
+ /// # unimplemented!()
+ /// # }
+ /// #
+ /// # pub fn push(
+ /// # mut self,
+ /// # _: iced_core::Element<Message, iced_core::renderer::Null>,
+ /// # ) -> Self {
+ /// # unimplemented!()
+ /// # }
+ /// # }
+ /// # }
/// # }
- /// use iced_native::Element;
- /// use iced_native::widget::Row;
- /// use iced_wgpu::Renderer;
+ /// #
+ /// use counter::Counter;
+ ///
+ /// use iced::widget::Row;
+ /// use iced::{Element, Renderer};
+ ///
+ /// struct ManyCounters {
+ /// counters: Vec<Counter>,
+ /// }
+ ///
+ /// #[derive(Debug, Clone, Copy)]
+ /// pub enum Message {
+ /// Counter(usize, counter::Message),
+ /// }
///
/// impl ManyCounters {
- /// pub fn view(&mut self) -> Row<Message, Renderer> {
+ /// pub fn view(&mut self) -> Row<Message> {
/// // We can quickly populate a `Row` by folding over our counters
/// self.counters.iter_mut().enumerate().fold(
/// Row::new().spacing(20),
@@ -137,9 +159,10 @@ impl<'a, Message, Renderer> Element<'a, Message, Renderer> {
/// // Here we turn our `Element<counter::Message>` into
/// // an `Element<Message>` by combining the `index` and the
/// // message of the `element`.
- /// element.map(move |message| Message::Counter(index, message))
+ /// element
+ /// .map(move |message| Message::Counter(index, message)),
/// )
- /// }
+ /// },
/// )
/// }
/// }
@@ -353,7 +376,7 @@ where
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, B>,
@@ -365,7 +388,7 @@ where
tree,
event,
layout,
- cursor_position,
+ cursor,
renderer,
clipboard,
&mut local_shell,
@@ -383,35 +406,23 @@ where
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
) {
- self.widget.draw(
- tree,
- renderer,
- theme,
- style,
- layout,
- cursor_position,
- viewport,
- )
+ self.widget
+ .draw(tree, renderer, theme, style, layout, cursor, viewport)
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
- self.widget.mouse_interaction(
- tree,
- layout,
- cursor_position,
- viewport,
- renderer,
- )
+ self.widget
+ .mouse_interaction(tree, layout, cursor, viewport, renderer)
}
fn overlay<'b>(
@@ -496,20 +507,14 @@ where
state: &mut Tree,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
- self.element.widget.on_event(
- state,
- event,
- layout,
- cursor_position,
- renderer,
- clipboard,
- shell,
- )
+ self.element
+ .widget
+ .on_event(state, event, layout, cursor, renderer, clipboard, shell)
}
fn draw(
@@ -519,7 +524,7 @@ where
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
) {
fn explain_layout<Renderer: crate::Renderer>(
@@ -542,15 +547,9 @@ where
}
}
- self.element.widget.draw(
- state,
- renderer,
- theme,
- style,
- layout,
- cursor_position,
- viewport,
- );
+ self.element
+ .widget
+ .draw(state, renderer, theme, style, layout, cursor, viewport);
explain_layout(renderer, self.color, layout);
}
@@ -559,17 +558,13 @@ where
&self,
state: &Tree,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
- self.element.widget.mouse_interaction(
- state,
- layout,
- cursor_position,
- viewport,
- renderer,
- )
+ self.element
+ .widget
+ .mouse_interaction(state, layout, cursor, viewport, renderer)
}
fn overlay<'b>(
diff --git a/native/src/event.rs b/core/src/event.rs
index eb826399..870b3074 100644
--- a/native/src/event.rs
+++ b/core/src/event.rs
@@ -62,7 +62,7 @@ impl Status {
/// `Captured` takes precedence over `Ignored`:
///
/// ```
- /// use iced_native::event::Status;
+ /// use iced_core::event::Status;
///
/// assert_eq!(Status::Ignored.merge(Status::Ignored), Status::Ignored);
/// assert_eq!(Status::Ignored.merge(Status::Captured), Status::Captured);
diff --git a/core/src/font.rs b/core/src/font.rs
index 3f9ad2b5..bb425fd6 100644
--- a/core/src/font.rs
+++ b/core/src/font.rs
@@ -1,24 +1,102 @@
+//! Load and use fonts.
+use std::hash::Hash;
+
/// A font.
-#[derive(Debug, Clone, Copy)]
-pub enum Font {
- /// The default font.
- ///
- /// This is normally a font configured in a renderer or loaded from the
- /// system.
- Default,
-
- /// An external font.
- External {
- /// The name of the external font
- name: &'static str,
-
- /// The bytes of the external font
- bytes: &'static [u8],
- },
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
+pub struct Font {
+ /// The [`Family`] of the [`Font`].
+ pub family: Family,
+ /// The [`Weight`] of the [`Font`].
+ pub weight: Weight,
+ /// The [`Stretch`] of the [`Font`].
+ pub stretch: Stretch,
+ /// Whether if the [`Font`] is monospaced or not.
+ pub monospaced: bool,
}
-impl Default for Font {
- fn default() -> Font {
- Font::Default
+impl Font {
+ /// A non-monospaced sans-serif font with normal [`Weight`].
+ pub const DEFAULT: Font = Font {
+ family: Family::SansSerif,
+ weight: Weight::Normal,
+ stretch: Stretch::Normal,
+ monospaced: false,
+ };
+
+ /// A monospaced font with normal [`Weight`].
+ pub const MONOSPACE: Font = Font {
+ family: Family::Monospace,
+ monospaced: true,
+ ..Self::DEFAULT
+ };
+
+ /// Creates a non-monospaced [`Font`] with the given [`Family::Name`] and
+ /// normal [`Weight`].
+ pub const fn with_name(name: &'static str) -> Self {
+ Font {
+ family: Family::Name(name),
+ ..Self::DEFAULT
+ }
}
}
+
+/// A font family.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
+pub enum Family {
+ /// The name of a font family of choice.
+ Name(&'static str),
+
+ /// Serif fonts represent the formal text style for a script.
+ Serif,
+
+ /// Glyphs in sans-serif fonts, as the term is used in CSS, are generally low
+ /// contrast and have stroke endings that are plain — without any flaring,
+ /// cross stroke, or other ornamentation.
+ #[default]
+ SansSerif,
+
+ /// Glyphs in cursive fonts generally use a more informal script style, and
+ /// the result looks more like handwritten pen or brush writing than printed
+ /// letterwork.
+ Cursive,
+
+ /// Fantasy fonts are primarily decorative or expressive fonts that contain
+ /// decorative or expressive representations of characters.
+ Fantasy,
+
+ /// The sole criterion of a monospace font is that all glyphs have the same
+ /// fixed width.
+ Monospace,
+}
+
+/// The weight of some text.
+#[allow(missing_docs)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
+pub enum Weight {
+ Thin,
+ ExtraLight,
+ Light,
+ #[default]
+ Normal,
+ Medium,
+ Semibold,
+ Bold,
+ ExtraBold,
+ Black,
+}
+
+/// The width of some text.
+#[allow(missing_docs)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
+pub enum Stretch {
+ UltraCondensed,
+ ExtraCondensed,
+ Condensed,
+ SemiCondensed,
+ #[default]
+ Normal,
+ SemiExpanded,
+ Expanded,
+ ExtraExpanded,
+ UltraExpanded,
+}
diff --git a/core/src/gradient.rs b/core/src/gradient.rs
new file mode 100644
index 00000000..e19622fb
--- /dev/null
+++ b/core/src/gradient.rs
@@ -0,0 +1,105 @@
+//! Colors that transition progressively.
+use crate::{Color, Radians};
+
+use std::cmp::Ordering;
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+/// A fill which transitions colors progressively along a direction, either linearly, radially (TBD),
+/// or conically (TBD).
+///
+/// For a gradient which can be used as a fill on a canvas, see [`iced_graphics::Gradient`].
+pub enum Gradient {
+ /// A linear gradient interpolates colors along a direction at a specific [`Angle`].
+ Linear(Linear),
+}
+
+impl Gradient {
+ /// Adjust the opacity of the gradient by a multiplier applied to each color stop.
+ pub fn mul_alpha(mut self, alpha_multiplier: f32) -> Self {
+ match &mut self {
+ Gradient::Linear(linear) => {
+ for stop in linear.stops.iter_mut().flatten() {
+ stop.color.a *= alpha_multiplier;
+ }
+ }
+ }
+
+ self
+ }
+}
+
+impl From<Linear> for Gradient {
+ fn from(gradient: Linear) -> Self {
+ Self::Linear(gradient)
+ }
+}
+
+#[derive(Debug, Default, Clone, Copy, PartialEq)]
+/// A point along the gradient vector where the specified [`color`] is unmixed.
+///
+/// [`color`]: Self::color
+pub struct ColorStop {
+ /// Offset along the gradient vector.
+ pub offset: f32,
+
+ /// The color of the gradient at the specified [`offset`].
+ ///
+ /// [`offset`]: Self::offset
+ pub color: Color,
+}
+
+/// A linear gradient.
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Linear {
+ /// How the [`Gradient`] is angled within its bounds.
+ pub angle: Radians,
+ /// [`ColorStop`]s along the linear gradient path.
+ pub stops: [Option<ColorStop>; 8],
+}
+
+impl Linear {
+ /// Creates a new [`Linear`] gradient with the given angle in [`Radians`].
+ pub fn new(angle: impl Into<Radians>) -> Self {
+ Self {
+ angle: angle.into(),
+ stops: [None; 8],
+ }
+ }
+
+ /// Adds a new [`ColorStop`], defined by an offset and a color, to the gradient.
+ ///
+ /// Any `offset` that is not within `0.0..=1.0` will be silently ignored.
+ ///
+ /// Any stop added after the 8th will be silently ignored.
+ pub fn add_stop(mut self, offset: f32, color: Color) -> Self {
+ if offset.is_finite() && (0.0..=1.0).contains(&offset) {
+ let (Ok(index) | Err(index)) =
+ self.stops.binary_search_by(|stop| match stop {
+ None => Ordering::Greater,
+ Some(stop) => stop.offset.partial_cmp(&offset).unwrap(),
+ });
+
+ if index < 8 {
+ self.stops[index] = Some(ColorStop { offset, color });
+ }
+ } else {
+ log::warn!("Gradient color stop must be within 0.0..=1.0 range.");
+ };
+
+ self
+ }
+
+ /// Adds multiple [`ColorStop`]s to the gradient.
+ ///
+ /// Any stop added after the 8th will be silently ignored.
+ pub fn add_stops(
+ mut self,
+ stops: impl IntoIterator<Item = ColorStop>,
+ ) -> Self {
+ for stop in stops.into_iter() {
+ self = self.add_stop(stop.offset, stop.color)
+ }
+
+ self
+ }
+}
diff --git a/native/src/hasher.rs b/core/src/hasher.rs
index fa52f16d..fa52f16d 100644
--- a/native/src/hasher.rs
+++ b/core/src/hasher.rs
diff --git a/native/src/image.rs b/core/src/image.rs
index 70fbade0..85d9d475 100644
--- a/native/src/image.rs
+++ b/core/src/image.rs
@@ -6,7 +6,7 @@ use std::path::PathBuf;
use std::sync::Arc;
/// A handle of some image data.
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Handle {
id: u64,
data: Data,
@@ -110,6 +110,14 @@ impl std::hash::Hash for Bytes {
}
}
+impl PartialEq for Bytes {
+ fn eq(&self, other: &Self) -> bool {
+ self.as_ref() == other.as_ref()
+ }
+}
+
+impl Eq for Bytes {}
+
impl AsRef<[u8]> for Bytes {
fn as_ref(&self) -> &[u8] {
self.0.as_ref().as_ref()
@@ -125,7 +133,7 @@ impl std::ops::Deref for Bytes {
}
/// The data of a raster image.
-#[derive(Clone, Hash)]
+#[derive(Clone, PartialEq, Eq, Hash)]
pub enum Data {
/// File data
Path(PathBuf),
diff --git a/native/src/layout.rs b/core/src/layout.rs
index 04954fb9..04954fb9 100644
--- a/native/src/layout.rs
+++ b/core/src/layout.rs
diff --git a/native/src/layout/DRUID_LICENSE b/core/src/layout/DRUID_LICENSE
index d6456956..d6456956 100644
--- a/native/src/layout/DRUID_LICENSE
+++ b/core/src/layout/DRUID_LICENSE
diff --git a/native/src/layout/flex.rs b/core/src/layout/flex.rs
index 8b967849..8b967849 100644
--- a/native/src/layout/flex.rs
+++ b/core/src/layout/flex.rs
diff --git a/native/src/layout/limits.rs b/core/src/layout/limits.rs
index 5d3c1556..5d3c1556 100644
--- a/native/src/layout/limits.rs
+++ b/core/src/layout/limits.rs
diff --git a/native/src/layout/node.rs b/core/src/layout/node.rs
index 2b44a7d5..2b44a7d5 100644
--- a/native/src/layout/node.rs
+++ b/core/src/layout/node.rs
diff --git a/core/src/length.rs b/core/src/length.rs
index bb925c4b..3adb996e 100644
--- a/core/src/length.rs
+++ b/core/src/length.rs
@@ -1,3 +1,5 @@
+use crate::Pixels;
+
/// The strategy used to fill space in a specific dimension.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Length {
@@ -36,6 +38,12 @@ impl Length {
}
}
+impl From<Pixels> for Length {
+ fn from(amount: Pixels) -> Self {
+ Length::Fixed(f32::from(amount))
+ }
+}
+
impl From<f32> for Length {
fn from(amount: f32) -> Self {
Length::Fixed(amount)
diff --git a/core/src/lib.rs b/core/src/lib.rs
index d3596b4d..76d775e7 100644
--- a/core/src/lib.rs
+++ b/core/src/lib.rs
@@ -7,7 +7,7 @@
//! ![The foundations of the Iced ecosystem](https://github.com/iced-rs/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/foundations.png?raw=true)
//!
//! [Iced]: https://github.com/iced-rs/iced
-//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.8/native
+//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.9/native
//! [`iced_web`]: https://github.com/iced-rs/iced_web
#![doc(
html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"
@@ -25,31 +25,61 @@
#![forbid(unsafe_code, rust_2018_idioms)]
#![allow(clippy::inherent_to_string, clippy::type_complexity)]
pub mod alignment;
+pub mod clipboard;
+pub mod event;
+pub mod font;
+pub mod gradient;
+pub mod image;
pub mod keyboard;
+pub mod layout;
pub mod mouse;
+pub mod overlay;
+pub mod renderer;
+pub mod svg;
+pub mod text;
pub mod time;
+pub mod touch;
+pub mod widget;
+pub mod window;
+mod angle;
mod background;
+mod border_radius;
mod color;
mod content_fit;
-mod font;
+mod element;
+mod hasher;
mod length;
mod padding;
mod pixels;
mod point;
mod rectangle;
+mod shell;
mod size;
mod vector;
pub use alignment::Alignment;
+pub use angle::{Degrees, Radians};
pub use background::Background;
+pub use border_radius::BorderRadius;
+pub use clipboard::Clipboard;
pub use color::Color;
pub use content_fit::ContentFit;
+pub use element::Element;
+pub use event::Event;
pub use font::Font;
+pub use gradient::Gradient;
+pub use hasher::Hasher;
+pub use layout::Layout;
pub use length::Length;
+pub use overlay::Overlay;
pub use padding::Padding;
pub use pixels::Pixels;
pub use point::Point;
pub use rectangle::Rectangle;
+pub use renderer::Renderer;
+pub use shell::Shell;
pub use size::Size;
+pub use text::Text;
pub use vector::Vector;
+pub use widget::Widget;
diff --git a/core/src/mouse.rs b/core/src/mouse.rs
index 48214f65..d13a60fb 100644
--- a/core/src/mouse.rs
+++ b/core/src/mouse.rs
@@ -1,8 +1,13 @@
//! Handle mouse events.
+pub mod click;
+
mod button;
+mod cursor;
mod event;
mod interaction;
pub use button::Button;
+pub use click::Click;
+pub use cursor::Cursor;
pub use event::{Event, ScrollDelta};
pub use interaction::Interaction;
diff --git a/core/src/mouse/button.rs b/core/src/mouse/button.rs
index aeb8a55d..3eec7f42 100644
--- a/core/src/mouse/button.rs
+++ b/core/src/mouse/button.rs
@@ -11,5 +11,5 @@ pub enum Button {
Middle,
/// Some other button.
- Other(u8),
+ Other(u16),
}
diff --git a/native/src/mouse/click.rs b/core/src/mouse/click.rs
index 4a7d796c..4a7d796c 100644
--- a/native/src/mouse/click.rs
+++ b/core/src/mouse/click.rs
diff --git a/core/src/mouse/cursor.rs b/core/src/mouse/cursor.rs
new file mode 100644
index 00000000..203526e9
--- /dev/null
+++ b/core/src/mouse/cursor.rs
@@ -0,0 +1,52 @@
+use crate::{Point, Rectangle, Vector};
+
+/// The mouse cursor state.
+#[derive(Debug, Clone, Copy, PartialEq, Default)]
+pub enum Cursor {
+ /// The cursor has a defined position.
+ Available(Point),
+
+ /// The cursor is currently unavailable (i.e. out of bounds or busy).
+ #[default]
+ Unavailable,
+}
+
+impl Cursor {
+ /// 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 absolute position of the [`Cursor`], if available and inside
+ /// the given bounds.
+ ///
+ /// If the [`Cursor`] is not over the provided bounds, this method will
+ /// return `None`.
+ pub fn position_over(self, bounds: Rectangle) -> Option<Point> {
+ self.position().filter(|p| bounds.contains(*p))
+ }
+
+ /// 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> {
+ self.position_over(bounds)
+ .map(|p| p - Vector::new(bounds.x, bounds.y))
+ }
+
+ /// Returns the relative position of the [`Cursor`] from the given origin,
+ /// if available.
+ pub fn position_from(self, origin: Point) -> Option<Point> {
+ self.position().map(|p| p - Vector::new(origin.x, origin.y))
+ }
+
+ /// Returns true if the [`Cursor`] is over the given `bounds`.
+ pub fn is_over(self, bounds: Rectangle) -> bool {
+ self.position_over(bounds).is_some()
+ }
+}
diff --git a/core/src/mouse/interaction.rs b/core/src/mouse/interaction.rs
index 664147a7..072033fd 100644
--- a/core/src/mouse/interaction.rs
+++ b/core/src/mouse/interaction.rs
@@ -1,7 +1,8 @@
/// The interaction of a mouse cursor.
-#[derive(Debug, Eq, PartialEq, Clone, Copy, PartialOrd, Ord)]
+#[derive(Debug, Eq, PartialEq, Clone, Copy, PartialOrd, Ord, Default)]
#[allow(missing_docs)]
pub enum Interaction {
+ #[default]
Idle,
Pointer,
Grab,
@@ -11,10 +12,5 @@ pub enum Interaction {
Grabbing,
ResizingHorizontally,
ResizingVertically,
-}
-
-impl Default for Interaction {
- fn default() -> Interaction {
- Interaction::Idle
- }
+ NotAllowed,
}
diff --git a/native/src/overlay.rs b/core/src/overlay.rs
index 6cada416..2e05db93 100644
--- a/native/src/overlay.rs
+++ b/core/src/overlay.rs
@@ -2,11 +2,8 @@
mod element;
mod group;
-pub mod menu;
-
pub use element::Element;
pub use group::Group;
-pub use menu::Menu;
use crate::event::{self, Event};
use crate::layout;
@@ -41,7 +38,7 @@ where
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
);
/// Applies a [`widget::Operation`] to the [`Overlay`].
@@ -69,7 +66,7 @@ where
&mut self,
_event: Event,
_layout: Layout<'_>,
- _cursor_position: Point,
+ _cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
_shell: &mut Shell<'_, Message>,
@@ -83,7 +80,7 @@ where
fn mouse_interaction(
&self,
_layout: Layout<'_>,
- _cursor_position: Point,
+ _cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
@@ -94,9 +91,23 @@ where
///
/// By default, it returns true if the bounds of the `layout` contain
/// the `cursor_position`.
- fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool {
+ fn is_over(
+ &self,
+ layout: Layout<'_>,
+ _renderer: &Renderer,
+ cursor_position: Point,
+ ) -> bool {
layout.bounds().contains(cursor_position)
}
+
+ /// Returns the nested overlay of the [`Overlay`], if there is any.
+ fn overlay<'a>(
+ &'a mut self,
+ _layout: Layout<'_>,
+ _renderer: &Renderer,
+ ) -> Option<Element<'a, Message, Renderer>> {
+ None
+ }
}
/// Returns a [`Group`] of overlay [`Element`] children.
diff --git a/native/src/overlay/element.rs b/core/src/overlay/element.rs
index 237d25d1..c2134343 100644
--- a/native/src/overlay/element.rs
+++ b/core/src/overlay/element.rs
@@ -68,35 +68,25 @@ where
&mut self,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
- self.overlay.on_event(
- event,
- layout,
- cursor_position,
- renderer,
- clipboard,
- shell,
- )
+ self.overlay
+ .on_event(event, layout, cursor, renderer, clipboard, shell)
}
/// Returns the current [`mouse::Interaction`] of the [`Element`].
pub fn mouse_interaction(
&self,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
- self.overlay.mouse_interaction(
- layout,
- cursor_position,
- viewport,
- renderer,
- )
+ self.overlay
+ .mouse_interaction(layout, cursor, viewport, renderer)
}
/// Draws the [`Element`] and its children using the given [`Layout`].
@@ -106,10 +96,9 @@ where
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
) {
- self.overlay
- .draw(renderer, theme, style, layout, cursor_position)
+ self.overlay.draw(renderer, theme, style, layout, cursor)
}
/// Applies a [`widget::Operation`] to the [`Element`].
@@ -123,8 +112,22 @@ where
}
/// Returns true if the cursor is over the [`Element`].
- pub fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool {
- self.overlay.is_over(layout, cursor_position)
+ pub fn is_over(
+ &self,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ cursor_position: Point,
+ ) -> bool {
+ self.overlay.is_over(layout, renderer, cursor_position)
+ }
+
+ /// Returns the nested overlay of the [`Element`], if there is any.
+ pub fn overlay<'b>(
+ &'b mut self,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ ) -> Option<Element<'b, Message, Renderer>> {
+ self.overlay.overlay(layout, renderer)
}
}
@@ -215,7 +218,7 @@ where
&mut self,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, B>,
@@ -226,7 +229,7 @@ where
let event_status = self.content.on_event(
event,
layout,
- cursor_position,
+ cursor,
renderer,
clipboard,
&mut local_shell,
@@ -240,16 +243,12 @@ where
fn mouse_interaction(
&self,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
- self.content.mouse_interaction(
- layout,
- cursor_position,
- viewport,
- renderer,
- )
+ self.content
+ .mouse_interaction(layout, cursor, viewport, renderer)
}
fn draw(
@@ -258,13 +257,27 @@ where
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
) {
- self.content
- .draw(renderer, theme, style, layout, cursor_position)
+ self.content.draw(renderer, theme, style, layout, cursor)
}
- fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool {
- self.content.is_over(layout, cursor_position)
+ fn is_over(
+ &self,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ cursor_position: Point,
+ ) -> bool {
+ self.content.is_over(layout, renderer, cursor_position)
+ }
+
+ fn overlay<'b>(
+ &'b mut self,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ ) -> Option<Element<'b, B, Renderer>> {
+ self.content
+ .overlay(layout, renderer)
+ .map(|overlay| overlay.map(self.mapper))
}
}
diff --git a/native/src/overlay/group.rs b/core/src/overlay/group.rs
index 1126f0cf..deffaad0 100644
--- a/native/src/overlay/group.rs
+++ b/core/src/overlay/group.rs
@@ -1,12 +1,10 @@
-use iced_core::{Point, Rectangle, Size};
-
use crate::event;
use crate::layout;
use crate::mouse;
use crate::overlay;
use crate::renderer;
use crate::widget;
-use crate::{Clipboard, Event, Layout, Overlay, Shell};
+use crate::{Clipboard, Event, Layout, Overlay, Point, Rectangle, Shell, Size};
/// An [`Overlay`] container that displays multiple overlay [`overlay::Element`]
/// children.
@@ -83,7 +81,7 @@ where
&mut self,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
@@ -95,7 +93,7 @@ where
child.on_event(
event.clone(),
layout,
- cursor_position,
+ cursor,
renderer,
clipboard,
shell,
@@ -110,17 +108,17 @@ where
theme: &<Renderer as crate::Renderer>::Theme,
style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
) {
for (child, layout) in self.children.iter().zip(layout.children()) {
- child.draw(renderer, theme, style, layout, cursor_position);
+ child.draw(renderer, theme, style, layout, cursor);
}
}
fn mouse_interaction(
&self,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
@@ -128,12 +126,7 @@ where
.iter()
.zip(layout.children())
.map(|(child, layout)| {
- child.mouse_interaction(
- layout,
- cursor_position,
- viewport,
- renderer,
- )
+ child.mouse_interaction(layout, cursor, viewport, renderer)
})
.max()
.unwrap_or_default()
@@ -154,11 +147,33 @@ where
});
}
- fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool {
+ fn is_over(
+ &self,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ cursor_position: Point,
+ ) -> bool {
self.children
.iter()
.zip(layout.children())
- .any(|(child, layout)| child.is_over(layout, cursor_position))
+ .any(|(child, layout)| {
+ child.is_over(layout, renderer, cursor_position)
+ })
+ }
+
+ fn overlay<'b>(
+ &'b mut self,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ ) -> Option<overlay::Element<'b, Message, Renderer>> {
+ let children = self
+ .children
+ .iter_mut()
+ .zip(layout.children())
+ .filter_map(|(child, layout)| child.overlay(layout, renderer))
+ .collect::<Vec<_>>();
+
+ (!children.is_empty()).then(|| Group::with_children(children).overlay())
}
}
diff --git a/core/src/pixels.rs b/core/src/pixels.rs
index e42cd9f9..6a9e5c88 100644
--- a/core/src/pixels.rs
+++ b/core/src/pixels.rs
@@ -20,3 +20,9 @@ impl From<u16> for Pixels {
Self(f32::from(amount))
}
}
+
+impl From<Pixels> for f32 {
+ fn from(pixels: Pixels) -> Self {
+ pixels.0
+ }
+}
diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs
index 4fe91519..7ff324cb 100644
--- a/core/src/rectangle.rs
+++ b/core/src/rectangle.rs
@@ -66,6 +66,11 @@ impl Rectangle<f32> {
Size::new(self.width, self.height)
}
+ /// Returns the area of the [`Rectangle`].
+ pub fn area(&self) -> f32 {
+ self.width * self.height
+ }
+
/// Returns true if the given [`Point`] is contained in the [`Rectangle`].
pub fn contains(&self, point: Point) -> bool {
self.x <= point.x
@@ -74,6 +79,15 @@ impl Rectangle<f32> {
&& point.y <= self.y + self.height
}
+ /// Returns true if the current [`Rectangle`] is completely within the given
+ /// `container`.
+ pub fn is_within(&self, container: &Rectangle) -> bool {
+ container.contains(self.position())
+ && container.contains(
+ self.position() + Vector::new(self.width, self.height),
+ )
+ }
+
/// Computes the intersection with the given [`Rectangle`].
pub fn intersection(
&self,
@@ -100,6 +114,30 @@ impl Rectangle<f32> {
}
}
+ /// Returns whether the [`Rectangle`] intersects with the given one.
+ pub fn intersects(&self, other: &Self) -> bool {
+ self.intersection(other).is_some()
+ }
+
+ /// Computes the union with the given [`Rectangle`].
+ pub fn union(&self, other: &Self) -> Self {
+ let x = self.x.min(other.x);
+ let y = self.y.min(other.y);
+
+ let lower_right_x = (self.x + self.width).max(other.x + other.width);
+ let lower_right_y = (self.y + self.height).max(other.y + other.height);
+
+ let width = lower_right_x - x;
+ let height = lower_right_y - y;
+
+ Rectangle {
+ x,
+ y,
+ width,
+ height,
+ }
+ }
+
/// Snaps the [`Rectangle`] to __unsigned__ integer coordinates.
pub fn snap(self) -> Rectangle<u32> {
Rectangle {
@@ -109,6 +147,16 @@ impl Rectangle<f32> {
height: self.height as u32,
}
}
+
+ /// Expands the [`Rectangle`] a given amount.
+ pub fn expand(self, amount: f32) -> Self {
+ Self {
+ x: self.x - amount,
+ y: self.y - amount,
+ width: self.width + amount * 2.0,
+ height: self.height + amount * 2.0,
+ }
+ }
}
impl std::ops::Mul<f32> for Rectangle<f32> {
diff --git a/native/src/renderer.rs b/core/src/renderer.rs
index 2ac78982..7c73d2e4 100644
--- a/native/src/renderer.rs
+++ b/core/src/renderer.rs
@@ -1,11 +1,12 @@
//! Write your own renderer.
#[cfg(debug_assertions)]
mod null;
+
#[cfg(debug_assertions)]
pub use null::Null;
use crate::layout;
-use crate::{Background, Color, Element, Rectangle, Vector};
+use crate::{Background, BorderRadius, Color, Element, Rectangle, Vector};
/// A component that can be used by widgets to draw themselves on a screen.
pub trait Renderer: Sized {
@@ -59,29 +60,6 @@ pub struct Quad {
pub border_color: Color,
}
-/// The border radi for the corners of a graphics primitive in the order:
-/// top-left, top-right, bottom-right, bottom-left.
-#[derive(Debug, Clone, Copy, PartialEq, Default)]
-pub struct BorderRadius([f32; 4]);
-
-impl From<f32> for BorderRadius {
- fn from(w: f32) -> Self {
- Self([w; 4])
- }
-}
-
-impl From<[f32; 4]> for BorderRadius {
- fn from(radi: [f32; 4]) -> Self {
- Self(radi)
- }
-}
-
-impl From<BorderRadius> for [f32; 4] {
- fn from(radi: BorderRadius) -> Self {
- radi.0
- }
-}
-
/// The styling attributes of a [`Renderer`].
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Style {
diff --git a/native/src/renderer/null.rs b/core/src/renderer/null.rs
index 9376d540..5d49699e 100644
--- a/native/src/renderer/null.rs
+++ b/core/src/renderer/null.rs
@@ -1,6 +1,8 @@
use crate::renderer::{self, Renderer};
use crate::text::{self, Text};
-use crate::{Background, Font, Point, Rectangle, Size, Theme, Vector};
+use crate::{Background, Font, Point, Rectangle, Size, Vector};
+
+use std::borrow::Cow;
/// A renderer that does nothing.
///
@@ -16,7 +18,7 @@ impl Null {
}
impl Renderer for Null {
- type Theme = Theme;
+ type Theme = ();
fn with_layer(&mut self, _bounds: Rectangle, _f: impl FnOnce(&mut Self)) {}
@@ -40,30 +42,40 @@ impl Renderer for Null {
impl text::Renderer for Null {
type Font = Font;
- const ICON_FONT: Font = Font::Default;
+ const ICON_FONT: Font = Font::DEFAULT;
const CHECKMARK_ICON: char = '0';
const ARROW_DOWN_ICON: char = '0';
+ fn default_font(&self) -> Self::Font {
+ Font::default()
+ }
+
fn default_size(&self) -> f32 {
- 20.0
+ 16.0
}
+ fn load_font(&mut self, _font: Cow<'static, [u8]>) {}
+
fn measure(
&self,
_content: &str,
_size: f32,
+ _line_height: text::LineHeight,
_font: Font,
_bounds: Size,
- ) -> (f32, f32) {
- (0.0, 20.0)
+ _shaping: text::Shaping,
+ ) -> Size {
+ Size::new(0.0, 20.0)
}
fn hit_test(
&self,
_contents: &str,
_size: f32,
+ _line_height: text::LineHeight,
_font: Self::Font,
_bounds: Size,
+ _shaping: text::Shaping,
_point: Point,
_nearest_only: bool,
) -> Option<text::Hit> {
diff --git a/native/src/shell.rs b/core/src/shell.rs
index 74a5c616..74a5c616 100644
--- a/native/src/shell.rs
+++ b/core/src/shell.rs
diff --git a/core/src/size.rs b/core/src/size.rs
index fbe940ef..7ef2f602 100644
--- a/core/src/size.rs
+++ b/core/src/size.rs
@@ -1,7 +1,7 @@
use crate::{Padding, Vector};
/// An amount of space in 2 dimensions.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Size<T = f32> {
/// The width.
pub width: T,
diff --git a/native/src/svg.rs b/core/src/svg.rs
index 9b98877a..54e9434e 100644
--- a/native/src/svg.rs
+++ b/core/src/svg.rs
@@ -7,7 +7,7 @@ use std::path::PathBuf;
use std::sync::Arc;
/// A handle of Svg data.
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Handle {
id: u64,
data: Arc<Data>,
@@ -57,7 +57,7 @@ impl Hash for Handle {
}
/// The data of a vectorial image.
-#[derive(Clone, Hash)]
+#[derive(Clone, Hash, PartialEq, Eq)]
pub enum Data {
/// File data
Path(PathBuf),
diff --git a/core/src/text.rs b/core/src/text.rs
new file mode 100644
index 00000000..fc8aa20e
--- /dev/null
+++ b/core/src/text.rs
@@ -0,0 +1,212 @@
+//! Draw and interact with text.
+use crate::alignment;
+use crate::{Color, Pixels, Point, Rectangle, Size};
+
+use std::borrow::Cow;
+use std::hash::{Hash, Hasher};
+
+/// A paragraph.
+#[derive(Debug, Clone, Copy)]
+pub struct Text<'a, Font> {
+ /// The content of the paragraph.
+ pub content: &'a str,
+
+ /// The bounds of the paragraph.
+ pub bounds: Rectangle,
+
+ /// The size of the [`Text`] in logical pixels.
+ pub size: f32,
+
+ /// The line height of the [`Text`].
+ pub line_height: LineHeight,
+
+ /// The color of the [`Text`].
+ pub color: Color,
+
+ /// The font of the [`Text`].
+ pub font: Font,
+
+ /// The horizontal alignment of the [`Text`].
+ pub horizontal_alignment: alignment::Horizontal,
+
+ /// The vertical alignment of the [`Text`].
+ pub vertical_alignment: alignment::Vertical,
+
+ /// The [`Shaping`] strategy of the [`Text`].
+ pub shaping: Shaping,
+}
+
+/// The shaping strategy of some text.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
+pub enum Shaping {
+ /// No shaping and no font fallback.
+ ///
+ /// This shaping strategy is very cheap, but it will not display complex
+ /// scripts properly nor try to find missing glyphs in your system fonts.
+ ///
+ /// You should use this strategy when you have complete control of the text
+ /// and the font you are displaying in your application.
+ ///
+ /// This is the default.
+ #[default]
+ Basic,
+ /// Advanced text shaping and font fallback.
+ ///
+ /// You will need to enable this flag if the text contains a complex
+ /// script, the font used needs it, and/or multiple fonts in your system
+ /// may be needed to display all of the glyphs.
+ ///
+ /// Advanced shaping is expensive! You should only enable it when necessary.
+ Advanced,
+}
+
+/// The height of a line of text in a paragraph.
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum LineHeight {
+ /// A factor of the size of the text.
+ Relative(f32),
+
+ /// An absolute height in logical pixels.
+ Absolute(Pixels),
+}
+
+impl LineHeight {
+ /// Returns the [`LineHeight`] in absolute logical pixels.
+ pub fn to_absolute(self, text_size: Pixels) -> Pixels {
+ match self {
+ Self::Relative(factor) => Pixels(factor * text_size.0),
+ Self::Absolute(pixels) => pixels,
+ }
+ }
+}
+
+impl Default for LineHeight {
+ fn default() -> Self {
+ Self::Relative(1.3)
+ }
+}
+
+impl From<f32> for LineHeight {
+ fn from(factor: f32) -> Self {
+ Self::Relative(factor)
+ }
+}
+
+impl From<Pixels> for LineHeight {
+ fn from(pixels: Pixels) -> Self {
+ Self::Absolute(pixels)
+ }
+}
+
+impl Hash for LineHeight {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ match self {
+ Self::Relative(factor) => {
+ state.write_u8(0);
+ factor.to_bits().hash(state);
+ }
+ Self::Absolute(pixels) => {
+ state.write_u8(1);
+ f32::from(*pixels).to_bits().hash(state);
+ }
+ }
+ }
+}
+
+/// The result of hit testing on text.
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum Hit {
+ /// The point was within the bounds of the returned character index.
+ CharOffset(usize),
+}
+
+impl Hit {
+ /// Computes the cursor position of the [`Hit`] .
+ pub fn cursor(self) -> usize {
+ match self {
+ Self::CharOffset(i) => i,
+ }
+ }
+}
+
+/// A renderer capable of measuring and drawing [`Text`].
+pub trait Renderer: crate::Renderer {
+ /// The font type used.
+ type Font: Copy;
+
+ /// The icon font of the backend.
+ const ICON_FONT: Self::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 [`Self::Font`].
+ fn default_font(&self) -> Self::Font;
+
+ /// Returns the default size of [`Text`].
+ fn default_size(&self) -> f32;
+
+ /// Measures the text in the given bounds and returns the minimum boundaries
+ /// that can fit the contents.
+ fn measure(
+ &self,
+ content: &str,
+ size: f32,
+ line_height: LineHeight,
+ font: Self::Font,
+ bounds: Size,
+ shaping: Shaping,
+ ) -> Size;
+
+ /// Measures the width of the text as if it were laid out in a single line.
+ fn measure_width(
+ &self,
+ content: &str,
+ size: f32,
+ font: Self::Font,
+ shaping: Shaping,
+ ) -> f32 {
+ let bounds = self.measure(
+ content,
+ size,
+ LineHeight::Absolute(Pixels(size)),
+ font,
+ Size::INFINITY,
+ shaping,
+ );
+
+ bounds.width
+ }
+
+ /// Tests whether the provided point is within the boundaries of text
+ /// laid out with the given parameters, returning information about
+ /// the nearest character.
+ ///
+ /// If `nearest_only` is true, the hit test does not consider whether the
+ /// the point is interior to any glyph bounds, returning only the character
+ /// with the nearest centeroid.
+ fn hit_test(
+ &self,
+ contents: &str,
+ size: f32,
+ line_height: LineHeight,
+ font: Self::Font,
+ bounds: Size,
+ shaping: Shaping,
+ point: Point,
+ nearest_only: bool,
+ ) -> Option<Hit>;
+
+ /// Loads a [`Self::Font`] from its bytes.
+ fn load_font(&mut self, font: Cow<'static, [u8]>);
+
+ /// Draws the given [`Text`].
+ fn fill_text(&mut self, text: Text<'_, Self::Font>);
+}
diff --git a/native/src/touch.rs b/core/src/touch.rs
index 18120644..18120644 100644
--- a/native/src/touch.rs
+++ b/core/src/touch.rs
diff --git a/native/src/widget.rs b/core/src/widget.rs
index 2b3ca7be..79d86444 100644
--- a/native/src/widget.rs
+++ b/core/src/widget.rs
@@ -1,98 +1,21 @@
-//! Use the built-in widgets or create your own.
-//!
-//! # Built-in widgets
-//! Every built-in drawable widget has its own module with a `Renderer` trait
-//! that must be implemented by a [renderer] before being able to use it as
-//! a [`Widget`].
-//!
-//! # Custom widgets
-//! If you want to implement a custom widget, you simply need to implement the
-//! [`Widget`] trait. You can use the API of the built-in widgets as a guide or
-//! source of inspiration.
-//!
-//! [renderer]: crate::renderer
-pub mod button;
-pub mod checkbox;
-pub mod column;
-pub mod container;
-pub mod helpers;
-pub mod image;
+//! Create custom widgets and operate on them.
pub mod operation;
-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;
-pub mod svg;
pub mod text;
-pub mod text_input;
-pub mod toggler;
-pub mod tooltip;
pub mod tree;
-pub mod vertical_slider;
-mod action;
mod id;
-#[doc(no_inline)]
-pub use button::Button;
-#[doc(no_inline)]
-pub use checkbox::Checkbox;
-#[doc(no_inline)]
-pub use column::Column;
-#[doc(no_inline)]
-pub use container::Container;
-#[doc(no_inline)]
-pub use helpers::*;
-#[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;
-#[doc(no_inline)]
-pub use space::Space;
-#[doc(no_inline)]
-pub use svg::Svg;
-#[doc(no_inline)]
-pub use text::Text;
-#[doc(no_inline)]
-pub use text_input::TextInput;
-#[doc(no_inline)]
-pub use toggler::Toggler;
-#[doc(no_inline)]
-pub use tooltip::Tooltip;
-#[doc(no_inline)]
-pub use tree::Tree;
-#[doc(no_inline)]
-pub use vertical_slider::VerticalSlider;
-
-pub use action::Action;
pub use id::Id;
pub use operation::Operation;
+pub use text::Text;
+pub use tree::Tree;
use crate::event::{self, Event};
-use crate::layout;
+use crate::layout::{self, Layout};
use crate::mouse;
use crate::overlay;
use crate::renderer;
-use crate::{Clipboard, Layout, Length, Point, Rectangle, Shell};
+use crate::{Clipboard, Length, Rectangle, Shell};
/// A component that displays information and allows interaction.
///
@@ -110,12 +33,12 @@ use crate::{Clipboard, Layout, Length, Point, Rectangle, Shell};
/// - [`geometry`], a custom widget showcasing how to draw geometry with the
/// `Mesh2D` primitive in [`iced_wgpu`].
///
-/// [examples]: https://github.com/iced-rs/iced/tree/0.8/examples
-/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.8/examples/bezier_tool
-/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.8/examples/custom_widget
-/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.8/examples/geometry
+/// [examples]: https://github.com/iced-rs/iced/tree/0.9/examples
+/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.9/examples/bezier_tool
+/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.9/examples/custom_widget
+/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.9/examples/geometry
/// [`lyon`]: https://github.com/nical/lyon
-/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.8/wgpu
+/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.9/wgpu
pub trait Widget<Message, Renderer>
where
Renderer: crate::Renderer,
@@ -144,7 +67,7 @@ where
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
);
@@ -188,7 +111,7 @@ where
_state: &mut Tree,
_event: Event,
_layout: Layout<'_>,
- _cursor_position: Point,
+ _cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
_shell: &mut Shell<'_, Message>,
@@ -203,7 +126,7 @@ where
&self,
_state: &Tree,
_layout: Layout<'_>,
- _cursor_position: Point,
+ _cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
diff --git a/native/src/widget/id.rs b/core/src/widget/id.rs
index 4b8fedf1..ae739bb7 100644
--- a/native/src/widget/id.rs
+++ b/core/src/widget/id.rs
@@ -24,7 +24,7 @@ impl Id {
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub enum Internal {
+enum Internal {
Unique(usize),
Custom(borrow::Cow<'static, str>),
}
diff --git a/core/src/widget/operation.rs b/core/src/widget/operation.rs
new file mode 100644
index 00000000..ad188c36
--- /dev/null
+++ b/core/src/widget/operation.rs
@@ -0,0 +1,226 @@
+//! Query or update internal widget state.
+pub mod focusable;
+pub mod scrollable;
+pub mod text_input;
+
+pub use focusable::Focusable;
+pub use scrollable::Scrollable;
+pub use text_input::TextInput;
+
+use crate::widget::Id;
+
+use std::any::Any;
+use std::fmt;
+use std::rc::Rc;
+
+/// A piece of logic that can traverse the widget tree of an application in
+/// order to query or update some widget state.
+pub trait Operation<T> {
+ /// Operates on a widget that contains other widgets.
+ ///
+ /// The `operate_on_children` function can be called to return control to
+ /// the widget tree and keep traversing it.
+ fn container(
+ &mut self,
+ id: Option<&Id>,
+ operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
+ );
+
+ /// Operates on a widget that can be focused.
+ fn focusable(&mut self, _state: &mut dyn Focusable, _id: Option<&Id>) {}
+
+ /// Operates on a widget that can be scrolled.
+ fn scrollable(&mut self, _state: &mut dyn Scrollable, _id: Option<&Id>) {}
+
+ /// Operates on a widget that has text input.
+ fn text_input(&mut self, _state: &mut dyn TextInput, _id: Option<&Id>) {}
+
+ /// Operates on a custom widget with some state.
+ fn custom(&mut self, _state: &mut dyn Any, _id: Option<&Id>) {}
+
+ /// Finishes the [`Operation`] and returns its [`Outcome`].
+ fn finish(&self) -> Outcome<T> {
+ Outcome::None
+ }
+}
+
+/// The result of an [`Operation`].
+pub enum Outcome<T> {
+ /// The [`Operation`] produced no result.
+ None,
+
+ /// The [`Operation`] produced some result.
+ Some(T),
+
+ /// The [`Operation`] needs to be followed by another [`Operation`].
+ Chain(Box<dyn Operation<T>>),
+}
+
+impl<T> fmt::Debug for Outcome<T>
+where
+ T: fmt::Debug,
+{
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::None => write!(f, "Outcome::None"),
+ Self::Some(output) => write!(f, "Outcome::Some({output:?})"),
+ Self::Chain(_) => write!(f, "Outcome::Chain(...)"),
+ }
+ }
+}
+
+/// Maps the output of an [`Operation`] using the given function.
+pub fn map<A, B>(
+ operation: Box<dyn Operation<A>>,
+ f: impl Fn(A) -> B + 'static,
+) -> impl Operation<B>
+where
+ A: 'static,
+ B: 'static,
+{
+ #[allow(missing_debug_implementations)]
+ struct Map<A, B> {
+ operation: Box<dyn Operation<A>>,
+ f: Rc<dyn Fn(A) -> B>,
+ }
+
+ impl<A, B> Operation<B> for Map<A, B>
+ where
+ A: 'static,
+ B: 'static,
+ {
+ fn container(
+ &mut self,
+ id: Option<&Id>,
+ operate_on_children: &mut dyn FnMut(&mut dyn Operation<B>),
+ ) {
+ struct MapRef<'a, A> {
+ operation: &'a mut dyn Operation<A>,
+ }
+
+ impl<'a, A, B> Operation<B> for MapRef<'a, A> {
+ fn container(
+ &mut self,
+ id: Option<&Id>,
+ operate_on_children: &mut dyn FnMut(&mut dyn Operation<B>),
+ ) {
+ let Self { operation, .. } = self;
+
+ operation.container(id, &mut |operation| {
+ operate_on_children(&mut MapRef { operation });
+ });
+ }
+
+ fn scrollable(
+ &mut self,
+ state: &mut dyn Scrollable,
+ id: Option<&Id>,
+ ) {
+ self.operation.scrollable(state, id);
+ }
+
+ fn focusable(
+ &mut self,
+ state: &mut dyn Focusable,
+ id: Option<&Id>,
+ ) {
+ self.operation.focusable(state, id);
+ }
+
+ fn text_input(
+ &mut self,
+ state: &mut dyn TextInput,
+ id: Option<&Id>,
+ ) {
+ self.operation.text_input(state, id);
+ }
+
+ fn custom(&mut self, state: &mut dyn Any, id: Option<&Id>) {
+ self.operation.custom(state, id);
+ }
+ }
+
+ let Self { operation, .. } = self;
+
+ MapRef {
+ operation: operation.as_mut(),
+ }
+ .container(id, operate_on_children);
+ }
+
+ fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) {
+ self.operation.focusable(state, id);
+ }
+
+ fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) {
+ self.operation.scrollable(state, id);
+ }
+
+ fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
+ self.operation.text_input(state, id);
+ }
+
+ fn custom(&mut self, state: &mut dyn Any, id: Option<&Id>) {
+ self.operation.custom(state, id);
+ }
+
+ fn finish(&self) -> Outcome<B> {
+ match self.operation.finish() {
+ Outcome::None => Outcome::None,
+ Outcome::Some(output) => Outcome::Some((self.f)(output)),
+ Outcome::Chain(next) => Outcome::Chain(Box::new(Map {
+ operation: next,
+ f: self.f.clone(),
+ })),
+ }
+ }
+ }
+
+ Map {
+ operation,
+ f: Rc::new(f),
+ }
+}
+
+/// Produces an [`Operation`] that applies the given [`Operation`] to the
+/// children of a container with the given [`Id`].
+pub fn scope<T: 'static>(
+ target: Id,
+ operation: impl Operation<T> + 'static,
+) -> impl Operation<T> {
+ struct ScopedOperation<Message> {
+ target: Id,
+ operation: Box<dyn Operation<Message>>,
+ }
+
+ impl<Message: 'static> Operation<Message> for ScopedOperation<Message> {
+ fn container(
+ &mut self,
+ id: Option<&Id>,
+ operate_on_children: &mut dyn FnMut(&mut dyn Operation<Message>),
+ ) {
+ if id == Some(&self.target) {
+ operate_on_children(self.operation.as_mut());
+ } else {
+ operate_on_children(self);
+ }
+ }
+
+ fn finish(&self) -> Outcome<Message> {
+ match self.operation.finish() {
+ Outcome::Chain(next) => {
+ Outcome::Chain(Box::new(ScopedOperation {
+ target: self.target.clone(),
+ operation: next,
+ }))
+ }
+ outcome => outcome,
+ }
+ }
+ }
+
+ ScopedOperation {
+ target,
+ operation: Box::new(operation),
+ }
+}
diff --git a/native/src/widget/operation/focusable.rs b/core/src/widget/operation/focusable.rs
index 312e4894..312e4894 100644
--- a/native/src/widget/operation/focusable.rs
+++ b/core/src/widget/operation/focusable.rs
diff --git a/native/src/widget/operation/scrollable.rs b/core/src/widget/operation/scrollable.rs
index 3b20631f..f947344d 100644
--- a/native/src/widget/operation/scrollable.rs
+++ b/core/src/widget/operation/scrollable.rs
@@ -5,6 +5,9 @@ use crate::widget::{Id, Operation};
pub trait Scrollable {
/// Snaps the scroll of the widget to the given `percentage` along the horizontal & vertical axis.
fn snap_to(&mut self, offset: RelativeOffset);
+
+ /// Scroll the widget to the given [`AbsoluteOffset`] along the horizontal & vertical axis.
+ fn scroll_to(&mut self, offset: AbsoluteOffset);
}
/// Produces an [`Operation`] that snaps the widget with the given [`Id`] to
@@ -34,7 +37,43 @@ pub fn snap_to<T>(target: Id, offset: RelativeOffset) -> impl Operation<T> {
SnapTo { target, offset }
}
-/// The amount of offset in each direction of a [`Scrollable`].
+/// Produces an [`Operation`] that scrolls the widget with the given [`Id`] to
+/// the provided [`AbsoluteOffset`].
+pub fn scroll_to<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {
+ struct ScrollTo {
+ target: Id,
+ offset: AbsoluteOffset,
+ }
+
+ impl<T> Operation<T> for ScrollTo {
+ fn container(
+ &mut self,
+ _id: Option<&Id>,
+ operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
+ ) {
+ operate_on_children(self)
+ }
+
+ fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) {
+ if Some(&self.target) == id {
+ state.scroll_to(self.offset);
+ }
+ }
+ }
+
+ ScrollTo { target, offset }
+}
+
+/// The amount of absolute offset in each direction of a [`Scrollable`].
+#[derive(Debug, Clone, Copy, PartialEq, Default)]
+pub struct AbsoluteOffset {
+ /// The amount of horizontal offset
+ pub x: f32,
+ /// The amount of vertical offset
+ pub y: f32,
+}
+
+/// The amount of relative offset in each direction of a [`Scrollable`].
///
/// A value of `0.0` means start, while `1.0` means end.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
diff --git a/native/src/widget/operation/text_input.rs b/core/src/widget/operation/text_input.rs
index 4c773e99..4c773e99 100644
--- a/native/src/widget/operation/text_input.rs
+++ b/core/src/widget/operation/text_input.rs
diff --git a/native/src/widget/text.rs b/core/src/widget/text.rs
index 3fee48f2..79df2b02 100644
--- a/native/src/widget/text.rs
+++ b/core/src/widget/text.rs
@@ -1,30 +1,17 @@
//! Write some text for your users to read.
use crate::alignment;
use crate::layout;
+use crate::mouse;
use crate::renderer;
use crate::text;
use crate::widget::Tree;
-use crate::{Element, Layout, Length, Pixels, Point, Rectangle, Size, Widget};
+use crate::{Color, Element, Layout, Length, Pixels, Rectangle, Widget};
use std::borrow::Cow;
-pub use iced_style::text::{Appearance, StyleSheet};
+pub use text::{LineHeight, Shaping};
/// A paragraph of text.
-///
-/// # Example
-///
-/// ```
-/// # use iced_native::Color;
-/// #
-/// # type Text<'a> = iced_native::widget::Text<'a, iced_native::renderer::Null>;
-/// #
-/// Text::new("I <3 iced!")
-/// .size(40)
-/// .style(Color::from([0.0, 0.0, 1.0]));
-/// ```
-///
-/// ![Text drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/text.png?raw=true)
#[allow(missing_debug_implementations)]
pub struct Text<'a, Renderer>
where
@@ -33,11 +20,13 @@ where
{
content: Cow<'a, str>,
size: Option<f32>,
+ line_height: LineHeight,
width: Length,
height: Length,
horizontal_alignment: alignment::Horizontal,
vertical_alignment: alignment::Vertical,
- font: Renderer::Font,
+ font: Option<Renderer::Font>,
+ shaping: Shaping,
style: <Renderer::Theme as StyleSheet>::Style,
}
@@ -51,11 +40,13 @@ where
Text {
content: content.into(),
size: None,
- font: Default::default(),
+ line_height: LineHeight::default(),
+ font: None,
width: Length::Shrink,
height: Length::Shrink,
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
+ shaping: Shaping::Basic,
style: Default::default(),
}
}
@@ -66,11 +57,17 @@ where
self
}
+ /// Sets the [`LineHeight`] of the [`Text`].
+ pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
+ self.line_height = line_height.into();
+ self
+ }
+
/// Sets the [`Font`] of the [`Text`].
///
/// [`Font`]: crate::text::Renderer::Font
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
- self.font = font.into();
+ self.font = Some(font.into());
self
}
@@ -112,6 +109,12 @@ where
self.vertical_alignment = alignment;
self
}
+
+ /// Sets the [`Shaping`] strategy of the [`Text`].
+ pub fn shaping(mut self, shaping: Shaping) -> Self {
+ self.shaping = shaping;
+ self
+ }
}
impl<'a, Message, Renderer> Widget<Message, Renderer> for Text<'a, Renderer>
@@ -136,12 +139,16 @@ where
let size = self.size.unwrap_or_else(|| renderer.default_size());
- let bounds = limits.max();
-
- let (width, height) =
- renderer.measure(&self.content, size, self.font.clone(), bounds);
+ let bounds = renderer.measure(
+ &self.content,
+ size,
+ self.line_height,
+ self.font.unwrap_or_else(|| renderer.default_font()),
+ limits.max(),
+ self.shaping,
+ );
- let size = limits.resolve(Size::new(width, height));
+ let size = limits.resolve(bounds);
layout::Node::new(size)
}
@@ -153,7 +160,7 @@ where
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
- _cursor_position: Point,
+ _cursor_position: mouse::Cursor,
_viewport: &Rectangle,
) {
draw(
@@ -162,10 +169,12 @@ where
layout,
&self.content,
self.size,
- self.font.clone(),
- theme.appearance(self.style),
+ self.line_height,
+ self.font,
+ theme.appearance(self.style.clone()),
self.horizontal_alignment,
self.vertical_alignment,
+ self.shaping,
);
}
}
@@ -186,10 +195,12 @@ pub fn draw<Renderer>(
layout: Layout<'_>,
content: &str,
size: Option<f32>,
- font: Renderer::Font,
+ line_height: LineHeight,
+ font: Option<Renderer::Font>,
appearance: Appearance,
horizontal_alignment: alignment::Horizontal,
vertical_alignment: alignment::Vertical,
+ shaping: Shaping,
) where
Renderer: text::Renderer,
{
@@ -207,14 +218,18 @@ pub fn draw<Renderer>(
alignment::Vertical::Bottom => bounds.y + bounds.height,
};
- renderer.fill_text(crate::text::Text {
+ let size = size.unwrap_or_else(|| renderer.default_size());
+
+ renderer.fill_text(crate::Text {
content,
- size: size.unwrap_or_else(|| renderer.default_size()),
+ size,
+ line_height,
bounds: Rectangle { x, y, ..bounds },
color: appearance.color.unwrap_or(style.text_color),
- font,
+ font: font.unwrap_or_else(|| renderer.default_font()),
horizontal_alignment,
vertical_alignment,
+ shaping,
});
}
@@ -238,22 +253,52 @@ where
Self {
content: self.content.clone(),
size: self.size,
+ line_height: self.line_height,
width: self.width,
height: self.height,
horizontal_alignment: self.horizontal_alignment,
vertical_alignment: self.vertical_alignment,
- font: self.font.clone(),
- style: self.style,
+ font: self.font,
+ style: self.style.clone(),
+ shaping: self.shaping,
}
}
}
+impl<'a, Renderer> From<&'a str> for Text<'a, Renderer>
+where
+ Renderer: text::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ fn from(content: &'a str) -> Self {
+ Self::new(content)
+ }
+}
+
impl<'a, Message, Renderer> From<&'a str> for Element<'a, Message, Renderer>
where
Renderer: text::Renderer + 'a,
Renderer::Theme: StyleSheet,
{
- fn from(contents: &'a str) -> Self {
- Text::new(contents).into()
+ fn from(content: &'a str) -> Self {
+ Text::from(content).into()
}
}
+
+/// The style sheet of some text.
+pub trait StyleSheet {
+ /// The supported style of the [`StyleSheet`].
+ type Style: Default + Clone;
+
+ /// Produces the [`Appearance`] of some text.
+ fn appearance(&self, style: Self::Style) -> Appearance;
+}
+
+/// The apperance of some text.
+#[derive(Debug, Clone, Copy, Default)]
+pub struct Appearance {
+ /// The [`Color`] of the text.
+ ///
+ /// The default, `None`, means using the inherited color.
+ pub color: Option<Color>,
+}
diff --git a/native/src/widget/tree.rs b/core/src/widget/tree.rs
index da269632..da269632 100644
--- a/native/src/widget/tree.rs
+++ b/core/src/widget/tree.rs
diff --git a/core/src/window.rs b/core/src/window.rs
new file mode 100644
index 00000000..a6dbdfb4
--- /dev/null
+++ b/core/src/window.rs
@@ -0,0 +1,15 @@
+//! Build window-based GUI applications.
+pub mod icon;
+
+mod event;
+mod level;
+mod mode;
+mod redraw_request;
+mod user_attention;
+
+pub use event::Event;
+pub use icon::Icon;
+pub use level::Level;
+pub use mode::Mode;
+pub use redraw_request::RedrawRequest;
+pub use user_attention::UserAttention;
diff --git a/native/src/window/event.rs b/core/src/window/event.rs
index e2fb5e66..e2fb5e66 100644
--- a/native/src/window/event.rs
+++ b/core/src/window/event.rs
diff --git a/core/src/window/icon.rs b/core/src/window/icon.rs
new file mode 100644
index 00000000..31868ecf
--- /dev/null
+++ b/core/src/window/icon.rs
@@ -0,0 +1,80 @@
+//! Change the icon of a window.
+use crate::Size;
+
+use std::mem;
+
+/// Builds an [`Icon`] from its RGBA pixels in the sRGB color space.
+pub fn from_rgba(
+ rgba: Vec<u8>,
+ width: u32,
+ height: u32,
+) -> Result<Icon, Error> {
+ const PIXEL_SIZE: usize = mem::size_of::<u8>() * 4;
+
+ if rgba.len() % PIXEL_SIZE != 0 {
+ return Err(Error::ByteCountNotDivisibleBy4 {
+ byte_count: rgba.len(),
+ });
+ }
+
+ let pixel_count = rgba.len() / PIXEL_SIZE;
+
+ if pixel_count != (width * height) as usize {
+ return Err(Error::DimensionsVsPixelCount {
+ width,
+ height,
+ width_x_height: (width * height) as usize,
+ pixel_count,
+ });
+ }
+
+ Ok(Icon {
+ rgba,
+ size: Size::new(width, height),
+ })
+}
+
+/// An window icon normally used for the titlebar or taskbar.
+#[derive(Debug, Clone)]
+pub struct Icon {
+ rgba: Vec<u8>,
+ size: Size<u32>,
+}
+
+impl Icon {
+ /// Returns the raw data of the [`Icon`].
+ pub fn into_raw(self) -> (Vec<u8>, Size<u32>) {
+ (self.rgba, self.size)
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+/// An error produced when using [`Icon::from_rgba`] with invalid arguments.
+pub enum Error {
+ /// Produced when the length of the `rgba` argument isn't divisible by 4, thus `rgba` can't be
+ /// safely interpreted as 32bpp RGBA pixels.
+ #[error(
+ "The provided RGBA data (with length {byte_count}) isn't divisible \
+ by 4. Therefore, it cannot be safely interpreted as 32bpp RGBA pixels"
+ )]
+ ByteCountNotDivisibleBy4 {
+ /// The length of the provided RGBA data.
+ byte_count: usize,
+ },
+ /// Produced when the number of pixels (`rgba.len() / 4`) isn't equal to `width * height`.
+ /// At least one of your arguments is incorrect.
+ #[error(
+ "The number of RGBA pixels ({pixel_count}) does not match the \
+ provided dimensions ({width}x{height})."
+ )]
+ DimensionsVsPixelCount {
+ /// The provided width.
+ width: u32,
+ /// The provided height.
+ height: u32,
+ /// The product of `width` and `height`.
+ width_x_height: usize,
+ /// The amount of pixels of the provided RGBA data.
+ pixel_count: usize,
+ },
+}
diff --git a/native/src/window/id.rs b/core/src/window/id.rs
index 0a11b1aa..0a11b1aa 100644
--- a/native/src/window/id.rs
+++ b/core/src/window/id.rs
diff --git a/core/src/window/level.rs b/core/src/window/level.rs
new file mode 100644
index 00000000..3878ecac
--- /dev/null
+++ b/core/src/window/level.rs
@@ -0,0 +1,19 @@
+/// A window level groups windows with respect to their z-position.
+///
+/// The relative ordering between windows in different window levels is fixed.
+/// The z-order of a window within the same window level may change dynamically
+/// on user interaction.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub enum Level {
+ /// The default behavior.
+ #[default]
+ Normal,
+
+ /// The window will always be below normal windows.
+ ///
+ /// This is useful for a widget-based app.
+ AlwaysOnBottom,
+
+ /// The window will always be on top of normal windows.
+ AlwaysOnTop,
+}
diff --git a/native/src/window/mode.rs b/core/src/window/mode.rs
index fdce8e23..fdce8e23 100644
--- a/native/src/window/mode.rs
+++ b/core/src/window/mode.rs
diff --git a/core/src/window/position.rs b/core/src/window/position.rs
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/core/src/window/position.rs
diff --git a/native/src/window/redraw_request.rs b/core/src/window/redraw_request.rs
index 3b4f0fd3..3b4f0fd3 100644
--- a/native/src/window/redraw_request.rs
+++ b/core/src/window/redraw_request.rs
diff --git a/native/src/window/settings.rs b/core/src/window/settings.rs
index 67798fbe..458b9232 100644
--- a/native/src/window/settings.rs
+++ b/core/src/window/settings.rs
@@ -1,4 +1,6 @@
-use crate::window::{Icon, Position};
+use crate::window::{Icon, Level, Position};
+
+pub use iced_winit::settings::PlatformSpecific;
/// The window settings of an application.
#[derive(Debug, Clone)]
@@ -27,11 +29,14 @@ pub struct Settings {
/// 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 [`Level`].
+ pub level: Level,
/// The icon of the window.
pub icon: Option<Icon>,
+
+ /// Platform specific settings.
+ pub platform_specific: PlatformSpecific,
}
impl Default for Settings {
@@ -45,8 +50,27 @@ impl Default for Settings {
resizable: true,
decorations: true,
transparent: false,
- always_on_top: false,
+ level: Level::default(),
icon: None,
+ platform_specific: Default::default(),
+ }
+ }
+}
+
+impl From<Settings> for iced_winit::settings::Window {
+ fn from(settings: Settings) -> Self {
+ Self {
+ size: settings.size,
+ position: iced_winit::Position::from(settings.position),
+ min_size: settings.min_size,
+ max_size: settings.max_size,
+ visible: settings.visible,
+ resizable: settings.resizable,
+ decorations: settings.decorations,
+ transparent: settings.transparent,
+ level: settings.level,
+ icon: settings.icon.map(Icon::into),
+ platform_specific: settings.platform_specific,
}
}
}
diff --git a/native/src/window/user_attention.rs b/core/src/window/user_attention.rs
index b03dfeef..b03dfeef 100644
--- a/native/src/window/user_attention.rs
+++ b/core/src/window/user_attention.rs
diff --git a/docs/release_summary.py b/docs/release_summary.py
new file mode 100644
index 00000000..cd4593b5
--- /dev/null
+++ b/docs/release_summary.py
@@ -0,0 +1,52 @@
+import re
+import sys
+import requests
+from typing import List, Tuple
+
+if len(sys.argv) < 3:
+ print("Usage: python release_summary.py <personal_access_token> <previous_release_branch>")
+ exit(1)
+
+TOKEN = sys.argv[1]
+HEADERS = {"Authorization": f"Bearer {TOKEN}"}
+PR_COMMIT_REGEX = re.compile(r"(?i)Merge pull request #(\d+).*")
+
+def get_merged_prs_since_release(repo: str, previous_release_branch: str) -> List[Tuple[str, int, str, str]]:
+ prs = []
+ compare_url = f"https://api.github.com/repos/{repo}/compare/{previous_release_branch}...master"
+ compare_response = requests.get(compare_url, headers=HEADERS)
+
+ if compare_response.status_code == 200:
+ compare_data = compare_response.json()
+ for commit in compare_data["commits"]:
+ match = PR_COMMIT_REGEX.search(commit["commit"]["message"])
+ if match:
+ pr_number = int(match.group(1))
+ pr_url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}"
+ pr_response = requests.get(pr_url, headers=HEADERS)
+ if pr_response.status_code == 200:
+ pr_data = pr_response.json()
+ prs.append((pr_data["title"], pr_number, pr_data["html_url"], pr_data["user"]["login"]))
+ else:
+ print(f"Error fetching PR {pr_number}: {pr_response.status_code}")
+ else:
+ print(f"Error comparing branches: {compare_response.status_code}")
+
+ return prs
+
+def print_pr_list(prs: List[Tuple[str, int, str, str]]):
+ for pr in prs:
+ print(f"- {pr[0]}. [#{pr[1]}]({pr[2]})")
+
+def print_authors(prs: List[Tuple[str, int, str, str]]):
+ authors = set(pr[3] for pr in prs)
+ print("\nAuthors:")
+ for author in sorted(authors, key=str.casefold):
+ print(f"- @{author}")
+
+if __name__ == "__main__":
+ repo = "iced-rs/iced"
+ previous_release_branch = sys.argv[2]
+ merged_prs = get_merged_prs_since_release(repo, previous_release_branch)
+ print_pr_list(merged_prs)
+ print_authors(merged_prs)
diff --git a/examples/README.md b/examples/README.md
index 74cf145b..111e8910 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -93,8 +93,7 @@ A bunch of simpler examples exist:
- [`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_opengl`](integration_opengl), a demonstration of how to integrate Iced in an existing OpenGL application.
-- [`integration_wgpu`](integration_wgpu), a demonstration of how to integrate Iced in an existing [`wgpu`] application.
+- [`integration`](integration), a demonstration of how to integrate Iced in an existing [`wgpu`] 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].
diff --git a/examples/arc/src/main.rs b/examples/arc/src/main.rs
index 7b6ea0e1..df565859 100644
--- a/examples/arc/src/main.rs
+++ b/examples/arc/src/main.rs
@@ -1,12 +1,13 @@
use std::{f32::consts::PI, time::Instant};
use iced::executor;
+use iced::mouse;
use iced::widget::canvas::{
- self, stroke, Cache, Canvas, Cursor, Geometry, Path, Stroke,
+ self, stroke, Cache, Canvas, Geometry, Path, Stroke,
};
use iced::{
- Application, Command, Element, Length, Point, Rectangle, Settings,
- Subscription, Theme,
+ Application, Command, Element, Length, Point, Rectangle, Renderer,
+ Settings, Subscription, Theme,
};
pub fn main() -> iced::Result {
@@ -75,11 +76,12 @@ impl<Message> canvas::Program<Message> for Arc {
fn draw(
&self,
_state: &Self::State,
+ renderer: &Renderer,
theme: &Theme,
bounds: Rectangle,
- _cursor: Cursor,
+ _cursor: mouse::Cursor,
) -> Vec<Geometry> {
- let geometry = self.cache.draw(bounds.size(), |frame| {
+ let geometry = self.cache.draw(renderer, bounds.size(), |frame| {
let palette = theme.palette();
let center = frame.center();
diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs
index 7c3916d4..310be28f 100644
--- a/examples/bezier_tool/src/main.rs
+++ b/examples/bezier_tool/src/main.rs
@@ -61,10 +61,8 @@ impl Sandbox for Example {
mod bezier {
use iced::mouse;
use iced::widget::canvas::event::{self, Event};
- use iced::widget::canvas::{
- self, Canvas, Cursor, Frame, Geometry, Path, Stroke,
- };
- use iced::{Element, Length, Point, Rectangle, Theme};
+ use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path, Stroke};
+ use iced::{Element, Length, Point, Rectangle, Renderer, Theme};
#[derive(Default)]
pub struct State {
@@ -100,10 +98,10 @@ mod bezier {
state: &mut Self::State,
event: Event,
bounds: Rectangle,
- cursor: Cursor,
+ cursor: mouse::Cursor,
) -> (event::Status, Option<Curve>) {
let cursor_position =
- if let Some(position) = cursor.position_in(&bounds) {
+ if let Some(position) = cursor.position_in(bounds) {
position
} else {
return (event::Status::Ignored, None);
@@ -152,22 +150,26 @@ mod bezier {
fn draw(
&self,
state: &Self::State,
+ renderer: &Renderer,
_theme: &Theme,
bounds: Rectangle,
- cursor: Cursor,
+ cursor: mouse::Cursor,
) -> Vec<Geometry> {
- let content =
- self.state.cache.draw(bounds.size(), |frame: &mut Frame| {
+ let content = self.state.cache.draw(
+ renderer,
+ bounds.size(),
+ |frame: &mut Frame| {
Curve::draw_all(self.curves, frame);
frame.stroke(
&Path::rectangle(Point::ORIGIN, frame.size()),
Stroke::default().with_width(2.0),
);
- });
+ },
+ );
if let Some(pending) = state {
- let pending_curve = pending.draw(bounds, cursor);
+ let pending_curve = pending.draw(renderer, bounds, cursor);
vec![content, pending_curve]
} else {
@@ -179,9 +181,9 @@ mod bezier {
&self,
_state: &Self::State,
bounds: Rectangle,
- cursor: Cursor,
+ cursor: mouse::Cursor,
) -> mouse::Interaction {
- if cursor.is_over(&bounds) {
+ if cursor.is_over(bounds) {
mouse::Interaction::Crosshair
} else {
mouse::Interaction::default()
@@ -216,10 +218,15 @@ mod bezier {
}
impl Pending {
- fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Geometry {
- let mut frame = Frame::new(bounds.size());
+ fn draw(
+ &self,
+ renderer: &Renderer,
+ bounds: Rectangle,
+ cursor: mouse::Cursor,
+ ) -> Geometry {
+ let mut frame = Frame::new(renderer, bounds.size());
- if let Some(cursor_position) = cursor.position_in(&bounds) {
+ if let Some(cursor_position) = cursor.position_in(bounds) {
match *self {
Pending::One { from } => {
let line = Path::line(from, cursor_position);
diff --git a/examples/checkbox/fonts/icons.ttf b/examples/checkbox/fonts/icons.ttf
index a2046844..82f28481 100644
--- a/examples/checkbox/fonts/icons.ttf
+++ b/examples/checkbox/fonts/icons.ttf
Binary files differ
diff --git a/examples/checkbox/src/main.rs b/examples/checkbox/src/main.rs
index 09950bb8..ef1a054d 100644
--- a/examples/checkbox/src/main.rs
+++ b/examples/checkbox/src/main.rs
@@ -1,10 +1,9 @@
-use iced::widget::{checkbox, column, container};
-use iced::{Element, Font, Length, Sandbox, Settings};
+use iced::executor;
+use iced::font::{self, Font};
+use iced::widget::{checkbox, column, container, text};
+use iced::{Application, Command, Element, Length, Settings, Theme};
-const ICON_FONT: Font = Font::External {
- name: "Icons",
- bytes: include_bytes!("../fonts/icons.ttf"),
-};
+const ICON_FONT: Font = Font::with_name("icons");
pub fn main() -> iced::Result {
Example::run(Settings::default())
@@ -20,24 +19,35 @@ struct Example {
enum Message {
DefaultChecked(bool),
CustomChecked(bool),
+ FontLoaded(Result<(), font::Error>),
}
-impl Sandbox for Example {
+impl Application for Example {
type Message = Message;
+ type Flags = ();
+ type Executor = executor::Default;
+ type Theme = Theme;
- fn new() -> Self {
- Default::default()
+ fn new(_flags: Self::Flags) -> (Self, Command<Message>) {
+ (
+ Self::default(),
+ font::load(include_bytes!("../fonts/icons.ttf").as_slice())
+ .map(Message::FontLoaded),
+ )
}
fn title(&self) -> String {
String::from("Checkbox - Iced")
}
- fn update(&mut self, message: Message) {
+ fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::DefaultChecked(value) => self.default_checkbox = value,
Message::CustomChecked(value) => self.custom_checkbox = value,
+ Message::FontLoaded(_) => (),
}
+
+ Command::none()
}
fn view(&self) -> Element<Message> {
@@ -49,6 +59,8 @@ impl Sandbox for Example {
font: ICON_FONT,
code_point: '\u{e901}',
size: None,
+ line_height: text::LineHeight::Relative(1.0),
+ shaping: text::Shaping::Basic,
});
let content = column![default_checkbox, custom_checkbox].spacing(22);
diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs
index a389c54f..fae77bc0 100644
--- a/examples/clock/src/main.rs
+++ b/examples/clock/src/main.rs
@@ -1,11 +1,10 @@
use iced::executor;
-use iced::widget::canvas::{
- stroke, Cache, Cursor, Geometry, LineCap, Path, Stroke,
-};
+use iced::mouse;
+use iced::widget::canvas::{stroke, Cache, Geometry, LineCap, Path, Stroke};
use iced::widget::{canvas, container};
use iced::{
- Application, Color, Command, Element, Length, Point, Rectangle, Settings,
- Subscription, Theme, Vector,
+ Application, Color, Command, Element, Length, Point, Rectangle, Renderer,
+ Settings, Subscription, Theme, Vector,
};
pub fn main() -> iced::Result {
@@ -83,17 +82,18 @@ impl Application for Clock {
}
}
-impl<Message> canvas::Program<Message> for Clock {
+impl<Message> canvas::Program<Message, Renderer> for Clock {
type State = ();
fn draw(
&self,
_state: &Self::State,
+ renderer: &Renderer,
_theme: &Theme,
bounds: Rectangle,
- _cursor: Cursor,
+ _cursor: mouse::Cursor,
) -> Vec<Geometry> {
- let clock = self.clock.draw(bounds.size(), |frame| {
+ let clock = self.clock.draw(renderer, bounds.size(), |frame| {
let center = frame.center();
let radius = frame.width().min(frame.height()) / 2.0;
diff --git a/examples/color_palette/Cargo.toml b/examples/color_palette/Cargo.toml
index 8fd37202..3be732bb 100644
--- a/examples/color_palette/Cargo.toml
+++ b/examples/color_palette/Cargo.toml
@@ -7,4 +7,4 @@ publish = false
[dependencies]
iced = { path = "../..", features = ["canvas", "palette"] }
-palette = "0.6.0"
+palette = "0.7.0"
diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs
index a2df36c2..736a9d53 100644
--- a/examples/color_palette/src/main.rs
+++ b/examples/color_palette/src/main.rs
@@ -1,10 +1,14 @@
-use iced::widget::canvas::{self, Canvas, Cursor, Frame, Geometry, Path};
+use iced::alignment::{self, Alignment};
+use iced::mouse;
+use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path};
use iced::widget::{column, row, text, Slider};
use iced::{
- alignment, Alignment, Color, Element, Length, Point, Rectangle, Sandbox,
- Settings, Size, Vector,
+ Color, Element, Length, Point, Rectangle, Renderer, Sandbox, Settings,
+ Size, Vector,
+};
+use palette::{
+ self, convert::FromColor, rgb::Rgb, Darken, Hsl, Lighten, ShiftHue,
};
-use palette::{self, convert::FromColor, Hsl, Srgb};
use std::marker::PhantomData;
use std::ops::RangeInclusive;
@@ -49,12 +53,12 @@ impl Sandbox for ColorPalette {
fn update(&mut self, message: Message) {
let srgb = match message {
- Message::RgbColorChanged(rgb) => palette::Srgb::from(rgb),
- Message::HslColorChanged(hsl) => palette::Srgb::from_color(hsl),
- Message::HsvColorChanged(hsv) => palette::Srgb::from_color(hsv),
- Message::HwbColorChanged(hwb) => palette::Srgb::from_color(hwb),
- Message::LabColorChanged(lab) => palette::Srgb::from_color(lab),
- Message::LchColorChanged(lch) => palette::Srgb::from_color(lch),
+ Message::RgbColorChanged(rgb) => Rgb::from(rgb),
+ Message::HslColorChanged(hsl) => Rgb::from_color(hsl),
+ Message::HsvColorChanged(hsv) => Rgb::from_color(hsv),
+ Message::HwbColorChanged(hwb) => Rgb::from_color(hwb),
+ Message::LabColorChanged(lab) => Rgb::from_color(lab),
+ Message::LchColorChanged(lch) => Rgb::from_color(lch),
};
self.theme = Theme::new(srgb);
@@ -63,7 +67,7 @@ impl Sandbox for ColorPalette {
fn view(&self) -> Element<Message> {
let base = self.theme.base;
- let srgb = palette::Srgb::from(base);
+ let srgb = Rgb::from(base);
let hsl = palette::Hsl::from_color(srgb);
let hsv = palette::Hsv::from_color(srgb);
let hwb = palette::Hwb::from_color(srgb);
@@ -95,12 +99,10 @@ struct Theme {
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_color(Srgb::from(base));
+ let hsl = Hsl::from_color(Rgb::from(base));
let lower = [
hsl.shift_hue(-135.0).lighten(0.075),
@@ -119,12 +121,12 @@ impl Theme {
Theme {
lower: lower
.iter()
- .map(|&color| Srgb::from_color(color).into())
+ .map(|&color| Rgb::from_color(color).into())
.collect(),
base,
higher: higher
.iter()
- .map(|&color| Srgb::from_color(color).into())
+ .map(|&color| Rgb::from_color(color).into())
.collect(),
canvas_cache: canvas::Cache::default(),
}
@@ -209,14 +211,14 @@ impl Theme {
text.vertical_alignment = alignment::Vertical::Bottom;
- let hsl = Hsl::from_color(Srgb::from(self.base));
+ let hsl = Hsl::from_color(Rgb::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_color(graded).into();
+ let color: Color = Rgb::from_color(graded).into();
let anchor = Point {
x: (i as f32) * box_size.width,
@@ -243,11 +245,12 @@ impl<Message> canvas::Program<Message> for Theme {
fn draw(
&self,
_state: &Self::State,
+ renderer: &Renderer,
_theme: &iced::Theme,
bounds: Rectangle,
- _cursor: Cursor,
+ _cursor: mouse::Cursor,
) -> Vec<Geometry> {
- let theme = self.canvas_cache.draw(bounds.size(), |frame| {
+ let theme = self.canvas_cache.draw(renderer, bounds.size(), |frame| {
self.draw(frame);
});
@@ -351,7 +354,7 @@ impl ColorSpace for palette::Hsl {
fn components(&self) -> [f32; 3] {
[
- self.hue.to_positive_degrees(),
+ self.hue.into_positive_degrees(),
self.saturation,
self.lightness,
]
@@ -360,7 +363,7 @@ impl ColorSpace for palette::Hsl {
fn to_string(&self) -> String {
format!(
"hsl({:.1}, {:.1}%, {:.1}%)",
- self.hue.to_positive_degrees(),
+ self.hue.into_positive_degrees(),
100.0 * self.saturation,
100.0 * self.lightness
)
@@ -377,13 +380,17 @@ impl ColorSpace for palette::Hsv {
}
fn components(&self) -> [f32; 3] {
- [self.hue.to_positive_degrees(), self.saturation, self.value]
+ [
+ self.hue.into_positive_degrees(),
+ self.saturation,
+ self.value,
+ ]
}
fn to_string(&self) -> String {
format!(
"hsv({:.1}, {:.1}%, {:.1}%)",
- self.hue.to_positive_degrees(),
+ self.hue.into_positive_degrees(),
100.0 * self.saturation,
100.0 * self.value
)
@@ -405,7 +412,7 @@ impl ColorSpace for palette::Hwb {
fn components(&self) -> [f32; 3] {
[
- self.hue.to_positive_degrees(),
+ self.hue.into_positive_degrees(),
self.whiteness,
self.blackness,
]
@@ -414,7 +421,7 @@ impl ColorSpace for palette::Hwb {
fn to_string(&self) -> String {
format!(
"hwb({:.1}, {:.1}%, {:.1}%)",
- self.hue.to_positive_degrees(),
+ self.hue.into_positive_degrees(),
100.0 * self.whiteness,
100.0 * self.blackness
)
@@ -449,7 +456,7 @@ impl ColorSpace for palette::Lch {
}
fn components(&self) -> [f32; 3] {
- [self.l, self.chroma, self.hue.to_positive_degrees()]
+ [self.l, self.chroma, self.hue.into_positive_degrees()]
}
fn to_string(&self) -> String {
@@ -457,7 +464,7 @@ impl ColorSpace for palette::Lch {
"Lch({:.1}, {:.1}, {:.1})",
self.l,
self.chroma,
- self.hue.to_positive_degrees()
+ self.hue.into_positive_degrees()
)
}
}
diff --git a/examples/component/Cargo.toml b/examples/component/Cargo.toml
index dd435201..9db1e6b4 100644
--- a/examples/component/Cargo.toml
+++ b/examples/component/Cargo.toml
@@ -6,6 +6,4 @@ edition = "2021"
publish = false
[dependencies]
-iced = { path = "../..", features = ["debug"] }
-iced_native = { path = "../../native" }
-iced_lazy = { path = "../../lazy" }
+iced = { path = "../..", features = ["debug", "lazy"] }
diff --git a/examples/component/src/main.rs b/examples/component/src/main.rs
index bbf549e7..010321a9 100644
--- a/examples/component/src/main.rs
+++ b/examples/component/src/main.rs
@@ -47,9 +47,8 @@ impl Sandbox for Component {
mod numeric_input {
use iced::alignment::{self, Alignment};
- use iced::widget::{self, button, row, text, text_input};
- use iced::{Element, Length};
- use iced_lazy::{self, Component};
+ use iced::widget::{button, component, row, text, text_input, Component};
+ use iced::{Element, Length, Renderer};
pub struct NumericInput<Message> {
value: Option<u32>,
@@ -82,13 +81,7 @@ mod numeric_input {
}
}
- impl<Message, Renderer> Component<Message, Renderer> for NumericInput<Message>
- where
- Renderer: iced_native::text::Renderer + 'static,
- Renderer::Theme: widget::button::StyleSheet
- + widget::text_input::StyleSheet
- + widget::text::StyleSheet,
- {
+ impl<Message> Component<Message, Renderer> for NumericInput<Message> {
type State = ();
type Event = Event;
@@ -141,8 +134,8 @@ mod numeric_input {
.map(u32::to_string)
.as_deref()
.unwrap_or(""),
- Event::InputChanged,
)
+ .on_input(Event::InputChanged)
.padding(10),
button("+", Event::IncrementPressed),
]
@@ -152,17 +145,12 @@ mod numeric_input {
}
}
- impl<'a, Message, Renderer> From<NumericInput<Message>>
- for Element<'a, Message, Renderer>
+ impl<'a, Message> From<NumericInput<Message>> for Element<'a, Message, Renderer>
where
Message: 'a,
- Renderer: 'static + iced_native::text::Renderer,
- Renderer::Theme: widget::button::StyleSheet
- + widget::text_input::StyleSheet
- + widget::text::StyleSheet,
{
fn from(numeric_input: NumericInput<Message>) -> Self {
- iced_lazy::component(numeric_input)
+ component(numeric_input)
}
}
}
diff --git a/examples/custom_quad/Cargo.toml b/examples/custom_quad/Cargo.toml
index 39154786..f097c2dd 100644
--- a/examples/custom_quad/Cargo.toml
+++ b/examples/custom_quad/Cargo.toml
@@ -6,5 +6,4 @@ edition = "2021"
publish = false
[dependencies]
-iced = { path = "../.." }
-iced_native = { path = "../../native" }
+iced = { path = "../..", features = ["advanced"] }
diff --git a/examples/custom_quad/src/main.rs b/examples/custom_quad/src/main.rs
index 6509887c..4b300116 100644
--- a/examples/custom_quad/src/main.rs
+++ b/examples/custom_quad/src/main.rs
@@ -1,9 +1,10 @@
//! This example showcases a drawing a quad.
mod quad {
- use iced_native::layout::{self, Layout};
- use iced_native::renderer;
- use iced_native::widget::{self, Widget};
- use iced_native::{Color, Element, Length, Point, Rectangle, Size};
+ use iced::advanced::layout::{self, Layout};
+ use iced::advanced::renderer;
+ use iced::advanced::widget::{self, Widget};
+ use iced::mouse;
+ use iced::{Color, Element, Length, Rectangle, Size};
pub struct CustomQuad {
size: f32,
@@ -48,7 +49,7 @@ mod quad {
_theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
- _cursor_position: Point,
+ _cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
renderer.fill_quad(
diff --git a/examples/custom_widget/Cargo.toml b/examples/custom_widget/Cargo.toml
index 067aab4a..dda0efe8 100644
--- a/examples/custom_widget/Cargo.toml
+++ b/examples/custom_widget/Cargo.toml
@@ -6,5 +6,4 @@ edition = "2021"
publish = false
[dependencies]
-iced = { path = "../.." }
-iced_native = { path = "../../native" }
+iced = { path = "../..", features = ["advanced"] }
diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs
index f6bb3b1e..713bc62d 100644
--- a/examples/custom_widget/src/main.rs
+++ b/examples/custom_widget/src/main.rs
@@ -9,10 +9,11 @@ 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_native::layout::{self, Layout};
- use iced_native::renderer;
- use iced_native::widget::{self, Widget};
- use iced_native::{Color, Element, Length, Point, Rectangle, Size};
+ use iced::advanced::layout::{self, Layout};
+ use iced::advanced::renderer;
+ use iced::advanced::widget::{self, Widget};
+ use iced::mouse;
+ use iced::{Color, Element, Length, Rectangle, Size};
pub struct Circle {
radius: f32,
@@ -55,7 +56,7 @@ mod circle {
_theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
- _cursor_position: Point,
+ _cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
renderer.fill_quad(
diff --git a/examples/download_progress/Cargo.toml b/examples/download_progress/Cargo.toml
index f38679ea..212832f4 100644
--- a/examples/download_progress/Cargo.toml
+++ b/examples/download_progress/Cargo.toml
@@ -7,8 +7,6 @@ publish = false
[dependencies]
iced = { path = "../..", features = ["tokio"] }
-iced_native = { path = "../../native" }
-iced_futures = { path = "../../futures" }
[dependencies.reqwest]
version = "0.11"
diff --git a/examples/download_progress/src/download.rs b/examples/download_progress/src/download.rs
index 39dd843f..3b11cb76 100644
--- a/examples/download_progress/src/download.rs
+++ b/examples/download_progress/src/download.rs
@@ -1,4 +1,4 @@
-use iced_native::subscription;
+use iced::subscription;
use std::hash::Hash;
@@ -18,10 +18,7 @@ pub struct Download<I> {
url: String,
}
-async fn download<I: Copy>(
- id: I,
- state: State,
-) -> (Option<(I, Progress)>, State) {
+async fn download<I: Copy>(id: I, state: State) -> ((I, Progress), State) {
match state {
State::Ready(url) => {
let response = reqwest::get(&url).await;
@@ -30,7 +27,7 @@ async fn download<I: Copy>(
Ok(response) => {
if let Some(total) = response.content_length() {
(
- Some((id, Progress::Started)),
+ (id, Progress::Started),
State::Downloading {
response,
total,
@@ -38,10 +35,10 @@ async fn download<I: Copy>(
},
)
} else {
- (Some((id, Progress::Errored)), State::Finished)
+ ((id, Progress::Errored), State::Finished)
}
}
- Err(_) => (Some((id, Progress::Errored)), State::Finished),
+ Err(_) => ((id, Progress::Errored), State::Finished),
}
}
State::Downloading {
@@ -55,7 +52,7 @@ async fn download<I: Copy>(
let percentage = (downloaded as f32 / total as f32) * 100.0;
(
- Some((id, Progress::Advanced(percentage))),
+ (id, Progress::Advanced(percentage)),
State::Downloading {
response,
total,
@@ -63,8 +60,8 @@ async fn download<I: Copy>(
},
)
}
- Ok(None) => (Some((id, Progress::Finished)), State::Finished),
- Err(_) => (Some((id, Progress::Errored)), State::Finished),
+ Ok(None) => ((id, Progress::Finished), State::Finished),
+ Err(_) => ((id, Progress::Errored), State::Finished),
},
State::Finished => {
// We do not let the stream die, as it would start a
diff --git a/examples/events/Cargo.toml b/examples/events/Cargo.toml
index 8c56e471..15ffc0af 100644
--- a/examples/events/Cargo.toml
+++ b/examples/events/Cargo.toml
@@ -7,4 +7,3 @@ publish = false
[dependencies]
iced = { path = "../..", features = ["debug"] }
-iced_native = { path = "../../native" }
diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs
index b57010c7..70659f52 100644
--- a/examples/events/src/main.rs
+++ b/examples/events/src/main.rs
@@ -1,12 +1,13 @@
use iced::alignment;
use iced::executor;
+use iced::subscription;
use iced::widget::{button, checkbox, container, text, Column};
use iced::window;
+use iced::Event;
use iced::{
Alignment, Application, Command, Element, Length, Settings, Subscription,
Theme,
};
-use iced_native::Event;
pub fn main() -> iced::Result {
Events::run(Settings {
@@ -17,13 +18,13 @@ pub fn main() -> iced::Result {
#[derive(Debug, Default)]
struct Events {
- last: Vec<iced_native::Event>,
+ last: Vec<Event>,
enabled: bool,
}
#[derive(Debug, Clone)]
enum Message {
- EventOccurred(iced_native::Event),
+ EventOccurred(Event),
Toggled(bool),
Exit(window::Id),
}
@@ -70,7 +71,7 @@ impl Application for Events {
}
fn subscription(&self) -> Subscription<Message> {
- iced_native::subscription::events().map(Message::EventOccurred)
+ subscription::events().map(Message::EventOccurred)
}
fn view(&self) -> Element<Message> {
diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs
index ed911160..e951d734 100644
--- a/examples/game_of_life/src/main.rs
+++ b/examples/game_of_life/src/main.rs
@@ -145,7 +145,7 @@ impl Application for GameOfLife {
self.grid
.view()
.map(move |message| Message::Grid(message, version)),
- controls
+ controls,
];
container(content)
@@ -204,15 +204,14 @@ fn view_controls<'a>(
mod grid {
use crate::Preset;
+ use iced::alignment;
+ use iced::mouse;
use iced::touch;
use iced::widget::canvas;
use iced::widget::canvas::event::{self, Event};
- use iced::widget::canvas::{
- Cache, Canvas, Cursor, Frame, Geometry, Path, Text,
- };
+ use iced::widget::canvas::{Cache, Canvas, Frame, Geometry, Path, Text};
use iced::{
- alignment, mouse, Color, Element, Length, Point, Rectangle, Size,
- Theme, Vector,
+ Color, Element, Length, Point, Rectangle, Renderer, Size, Theme, Vector,
};
use rustc_hash::{FxHashMap, FxHashSet};
use std::future::Future;
@@ -401,14 +400,14 @@ mod grid {
interaction: &mut Interaction,
event: Event,
bounds: Rectangle,
- cursor: Cursor,
+ cursor: mouse::Cursor,
) -> (event::Status, Option<Message>) {
if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event {
*interaction = Interaction::None;
}
let cursor_position =
- if let Some(position) = cursor.position_in(&bounds) {
+ if let Some(position) = cursor.position_in(bounds) {
position
} else {
return (event::Status::Ignored, None);
@@ -536,13 +535,14 @@ mod grid {
fn draw(
&self,
_interaction: &Interaction,
+ renderer: &Renderer,
_theme: &Theme,
bounds: Rectangle,
- cursor: Cursor,
+ cursor: mouse::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 life = self.life_cache.draw(renderer, bounds.size(), |frame| {
let background = Path::rectangle(Point::ORIGIN, frame.size());
frame.fill(&background, Color::from_rgb8(0x40, 0x44, 0x4B));
@@ -565,12 +565,11 @@ mod grid {
});
let overlay = {
- let mut frame = Frame::new(bounds.size());
+ let mut frame = Frame::new(renderer, bounds.size());
- let hovered_cell =
- cursor.position_in(&bounds).map(|position| {
- Cell::at(self.project(position, frame.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| {
@@ -626,38 +625,40 @@ mod grid {
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));
+ let grid =
+ self.grid_cache.draw(renderer, bounds.size(), |frame| {
+ frame.translate(center);
+ frame.scale(self.scaling);
+ frame.translate(self.translation);
+ frame.scale(Cell::SIZE as f32);
- 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,
- );
- }
+ 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,
- );
- }
- });
+ 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]
}
@@ -667,13 +668,13 @@ mod grid {
&self,
interaction: &Interaction,
bounds: Rectangle,
- cursor: Cursor,
+ cursor: mouse::Cursor,
) -> mouse::Interaction {
match interaction {
Interaction::Drawing => mouse::Interaction::Crosshair,
Interaction::Erasing => mouse::Interaction::Crosshair,
Interaction::Panning { .. } => mouse::Interaction::Grabbing,
- Interaction::None if cursor.is_over(&bounds) => {
+ Interaction::None if cursor.is_over(bounds) => {
mouse::Interaction::Crosshair
}
_ => mouse::Interaction::default(),
diff --git a/examples/geometry/Cargo.toml b/examples/geometry/Cargo.toml
index 22ede0e0..6068d651 100644
--- a/examples/geometry/Cargo.toml
+++ b/examples/geometry/Cargo.toml
@@ -6,6 +6,4 @@ edition = "2021"
publish = false
[dependencies]
-iced = { path = "../.." }
-iced_native = { path = "../../native" }
-iced_graphics = { path = "../../graphics" }
+iced = { path = "../..", features = ["advanced"] }
diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs
index 9bacce7f..3bc7f46b 100644
--- a/examples/geometry/src/main.rs
+++ b/examples/geometry/src/main.rs
@@ -1,24 +1,12 @@
//! This example showcases a simple native custom widget that renders using
//! arbitrary low-level geometry.
mod rainbow {
- // 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_graphics::renderer::{self, Renderer};
- use iced_graphics::triangle::ColoredVertex2D;
- use iced_graphics::{Backend, Primitive};
-
- use iced_native::layout;
- use iced_native::widget::{self, Widget};
- use iced_native::{
- Element, Layout, Length, Point, Rectangle, Size, Vector,
- };
+ use iced::advanced::graphics::color;
+ use iced::advanced::layout::{self, Layout};
+ use iced::advanced::renderer;
+ use iced::advanced::widget::{self, Widget};
+ use iced::mouse;
+ use iced::{Element, Length, Rectangle, Renderer, Size, Theme, Vector};
#[derive(Debug, Clone, Copy, Default)]
pub struct Rainbow;
@@ -27,10 +15,7 @@ mod rainbow {
Rainbow
}
- impl<Message, B, T> Widget<Message, Renderer<B, T>> for Rainbow
- where
- B: Backend,
- {
+ impl<Message> Widget<Message, Renderer> for Rainbow {
fn width(&self) -> Length {
Length::Fill
}
@@ -41,7 +26,7 @@ mod rainbow {
fn layout(
&self,
- _renderer: &Renderer<B, T>,
+ _renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let size = limits.width(Length::Fill).resolve(Size::ZERO);
@@ -52,17 +37,17 @@ mod rainbow {
fn draw(
&self,
_tree: &widget::Tree,
- renderer: &mut Renderer<B, T>,
- _theme: &T,
+ renderer: &mut Renderer,
+ _theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
- use iced_graphics::triangle::Mesh2D;
- use iced_native::Renderer as _;
+ use iced::advanced::graphics::mesh::{self, Mesh, SolidVertex2D};
+ use iced::advanced::Renderer as _;
- let b = layout.bounds();
+ let bounds = layout.bounds();
// R O Y G B I V
let color_r = [1.0, 0.0, 0.0, 1.0];
@@ -75,61 +60,61 @@ mod rainbow {
let color_v = [0.75, 0.0, 0.5, 1.0];
let posn_center = {
- if b.contains(cursor_position) {
- [cursor_position.x - b.x, cursor_position.y - b.y]
+ if let Some(cursor_position) = cursor.position_in(bounds) {
+ [cursor_position.x, cursor_position.y]
} else {
- [b.width / 2.0, b.height / 2.0]
+ [bounds.width / 2.0, bounds.height / 2.0]
}
};
let posn_tl = [0.0, 0.0];
- let posn_t = [b.width / 2.0, 0.0];
- let posn_tr = [b.width, 0.0];
- let posn_r = [b.width, b.height / 2.0];
- let posn_br = [b.width, b.height];
- let posn_b = [(b.width / 2.0), b.height];
- let posn_bl = [0.0, b.height];
- let posn_l = [0.0, b.height / 2.0];
-
- let mesh = Primitive::SolidMesh {
- size: b.size(),
- buffers: Mesh2D {
+ let posn_t = [bounds.width / 2.0, 0.0];
+ let posn_tr = [bounds.width, 0.0];
+ let posn_r = [bounds.width, bounds.height / 2.0];
+ let posn_br = [bounds.width, bounds.height];
+ let posn_b = [(bounds.width / 2.0), bounds.height];
+ let posn_bl = [0.0, bounds.height];
+ let posn_l = [0.0, bounds.height / 2.0];
+
+ let mesh = Mesh::Solid {
+ size: bounds.size(),
+ buffers: mesh::Indexed {
vertices: vec![
- ColoredVertex2D {
+ SolidVertex2D {
position: posn_center,
- color: [1.0, 1.0, 1.0, 1.0],
+ color: color::pack([1.0, 1.0, 1.0, 1.0]),
},
- ColoredVertex2D {
+ SolidVertex2D {
position: posn_tl,
- color: color_r,
+ color: color::pack(color_r),
},
- ColoredVertex2D {
+ SolidVertex2D {
position: posn_t,
- color: color_o,
+ color: color::pack(color_o),
},
- ColoredVertex2D {
+ SolidVertex2D {
position: posn_tr,
- color: color_y,
+ color: color::pack(color_y),
},
- ColoredVertex2D {
+ SolidVertex2D {
position: posn_r,
- color: color_g,
+ color: color::pack(color_g),
},
- ColoredVertex2D {
+ SolidVertex2D {
position: posn_br,
- color: color_gb,
+ color: color::pack(color_gb),
},
- ColoredVertex2D {
+ SolidVertex2D {
position: posn_b,
- color: color_b,
+ color: color::pack(color_b),
},
- ColoredVertex2D {
+ SolidVertex2D {
position: posn_bl,
- color: color_i,
+ color: color::pack(color_i),
},
- ColoredVertex2D {
+ SolidVertex2D {
position: posn_l,
- color: color_v,
+ color: color::pack(color_v),
},
],
indices: vec![
@@ -145,16 +130,16 @@ mod rainbow {
},
};
- renderer.with_translation(Vector::new(b.x, b.y), |renderer| {
- renderer.draw_primitive(mesh);
- });
+ renderer.with_translation(
+ Vector::new(bounds.x, bounds.y),
+ |renderer| {
+ renderer.draw_mesh(mesh);
+ },
+ );
}
}
- impl<'a, Message, B, T> From<Rainbow> for Element<'a, Message, Renderer<B, T>>
- where
- B: Backend,
- {
+ impl<'a, Message> From<Rainbow> for Element<'a, Message, Renderer> {
fn from(rainbow: Rainbow) -> Self {
Self::new(rainbow)
}
diff --git a/examples/integration_wgpu/.gitignore b/examples/integration/.gitignore
index e188dc28..e188dc28 100644
--- a/examples/integration_wgpu/.gitignore
+++ b/examples/integration/.gitignore
diff --git a/examples/integration_wgpu/Cargo.toml b/examples/integration/Cargo.toml
index eaa1df7e..22914742 100644
--- a/examples/integration_wgpu/Cargo.toml
+++ b/examples/integration/Cargo.toml
@@ -1,5 +1,5 @@
[package]
-name = "integration_wgpu"
+name = "integration"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
@@ -7,8 +7,10 @@ publish = false
[dependencies]
iced_winit = { path = "../../winit" }
-iced_wgpu = { path = "../../wgpu", features = ["webgl"] }
-env_logger = "0.8"
+iced_wgpu = { path = "../../wgpu" }
+iced_widget = { path = "../../widget" }
+iced_renderer = { path = "../../renderer", features = ["wgpu"] }
+env_logger = "0.10"
[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.7"
diff --git a/examples/integration_wgpu/README.md b/examples/integration/README.md
index ece9ba1e..ece9ba1e 100644
--- a/examples/integration_wgpu/README.md
+++ b/examples/integration/README.md
diff --git a/examples/integration_wgpu/index.html b/examples/integration/index.html
index 461e67a4..920bc4a0 100644
--- a/examples/integration_wgpu/index.html
+++ b/examples/integration/index.html
@@ -8,8 +8,8 @@
<h1>integration_wgpu</h1>
<canvas id="iced_canvas"></canvas>
<script type="module">
- import init from "./integration_wgpu.js";
- init('./integration_wgpu_bg.wasm');
+ import init from "./integration.js";
+ init('./integration_bg.wasm');
</script>
<style>
body {
diff --git a/examples/integration_wgpu/src/controls.rs b/examples/integration/src/controls.rs
index 533cb6e2..14e53ede 100644
--- a/examples/integration_wgpu/src/controls.rs
+++ b/examples/integration/src/controls.rs
@@ -1,6 +1,8 @@
use iced_wgpu::Renderer;
-use iced_winit::widget::{slider, text_input, Column, Row, Text};
-use iced_winit::{Alignment, Color, Command, Element, Length, Program};
+use iced_widget::{slider, text_input, Column, Row, Text};
+use iced_winit::core::{Alignment, Color, Element, Length};
+use iced_winit::runtime::{Command, Program};
+use iced_winit::style::Theme;
pub struct Controls {
background_color: Color,
@@ -27,7 +29,7 @@ impl Controls {
}
impl Program for Controls {
- type Renderer = Renderer;
+ type Renderer = Renderer<Theme>;
type Message = Message;
fn update(&mut self, message: Message) -> Command<Message> {
@@ -43,7 +45,7 @@ impl Program for Controls {
Command::none()
}
- fn view(&self) -> Element<Message, Renderer> {
+ fn view(&self) -> Element<Message, Renderer<Theme>> {
let background_color = self.background_color;
let text = &self.text;
@@ -100,11 +102,10 @@ impl Program for Controls {
.size(14)
.style(Color::WHITE),
)
- .push(text_input(
- "Placeholder",
- text,
- Message::TextChanged,
- )),
+ .push(
+ text_input("Placeholder", text)
+ .on_input(Message::TextChanged),
+ ),
),
)
.into()
diff --git a/examples/integration_wgpu/src/main.rs b/examples/integration/src/main.rs
index 1f42013b..a560959a 100644
--- a/examples/integration_wgpu/src/main.rs
+++ b/examples/integration/src/main.rs
@@ -4,14 +4,17 @@ mod scene;
use controls::Controls;
use scene::Scene;
-use iced_wgpu::{wgpu, Backend, Renderer, Settings, Viewport};
-use iced_winit::{
- conversion, futures, program, renderer, window, winit, Clipboard, Color,
- Debug, Size,
-};
+use iced_wgpu::graphics::Viewport;
+use iced_wgpu::{wgpu, Backend, Renderer, Settings};
+use iced_winit::core::mouse;
+use iced_winit::core::renderer;
+use iced_winit::core::{Color, Size};
+use iced_winit::runtime::program;
+use iced_winit::runtime::Debug;
+use iced_winit::style::Theme;
+use iced_winit::{conversion, futures, winit, Clipboard};
use winit::{
- dpi::PhysicalPosition,
event::{Event, ModifiersState, WindowEvent},
event_loop::{ControlFlow, EventLoop},
};
@@ -23,19 +26,20 @@ use web_sys::HtmlCanvasElement;
#[cfg(target_arch = "wasm32")]
use winit::platform::web::WindowBuilderExtWebSys;
-pub fn main() {
+pub fn main() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(target_arch = "wasm32")]
let canvas_element = {
- console_log::init_with_level(log::Level::Debug)
- .expect("could not initialize logger");
+ console_log::init_with_level(log::Level::Debug)?;
+
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
web_sys::window()
.and_then(|win| win.document())
.and_then(|doc| doc.get_element_by_id("iced_canvas"))
.and_then(|element| element.dyn_into::<HtmlCanvasElement>().ok())
- .expect("Canvas with id `iced_canvas` is missing")
+ .expect("Get canvas element")
};
+
#[cfg(not(target_arch = "wasm32"))]
env_logger::init();
@@ -45,23 +49,21 @@ pub fn main() {
#[cfg(target_arch = "wasm32")]
let window = winit::window::WindowBuilder::new()
.with_canvas(Some(canvas_element))
- .build(&event_loop)
- .expect("Failed to build winit window");
+ .build(&event_loop)?;
#[cfg(not(target_arch = "wasm32"))]
- let window = winit::window::Window::new(&event_loop).unwrap();
+ let window = winit::window::Window::new(&event_loop)?;
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 cursor_position = None;
let mut modifiers = ModifiersState::default();
let mut clipboard = Clipboard::connect(&window);
// Initialize wgpu
-
#[cfg(target_arch = "wasm32")]
let default_backend = wgpu::Backends::GL;
#[cfg(not(target_arch = "wasm32"))]
@@ -70,46 +72,55 @@ pub fn main() {
let backend =
wgpu::util::backend_bits_from_env().unwrap_or(default_backend);
- let instance = wgpu::Instance::new(backend);
- let surface = unsafe { instance.create_surface(&window) };
-
- let (format, (device, queue)) = futures::executor::block_on(async {
- let adapter = wgpu::util::initialize_adapter_from_env_or_default(
- &instance,
- backend,
- Some(&surface),
- )
- .await
- .expect("No suitable GPU adapters found on the system!");
-
- let adapter_features = adapter.features();
-
- #[cfg(target_arch = "wasm32")]
- let needed_limits = wgpu::Limits::downlevel_webgl2_defaults()
- .using_resolution(adapter.limits());
-
- #[cfg(not(target_arch = "wasm32"))]
- let needed_limits = wgpu::Limits::default();
-
- (
- surface
- .get_supported_formats(&adapter)
- .first()
- .copied()
- .expect("Get preferred format"),
- adapter
- .request_device(
- &wgpu::DeviceDescriptor {
- label: None,
- features: adapter_features & wgpu::Features::default(),
- limits: needed_limits,
- },
- None,
- )
- .await
- .expect("Request device"),
- )
+ let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
+ backends: backend,
+ ..Default::default()
});
+ let surface = unsafe { instance.create_surface(&window) }?;
+
+ let (format, (device, queue)) =
+ futures::futures::executor::block_on(async {
+ let adapter = wgpu::util::initialize_adapter_from_env_or_default(
+ &instance,
+ backend,
+ Some(&surface),
+ )
+ .await
+ .expect("Create adapter");
+
+ let adapter_features = adapter.features();
+
+ #[cfg(target_arch = "wasm32")]
+ let needed_limits = wgpu::Limits::downlevel_webgl2_defaults()
+ .using_resolution(adapter.limits());
+
+ #[cfg(not(target_arch = "wasm32"))]
+ let needed_limits = wgpu::Limits::default();
+
+ let capabilities = surface.get_capabilities(&adapter);
+
+ (
+ capabilities
+ .formats
+ .iter()
+ .copied()
+ .find(wgpu::TextureFormat::is_srgb)
+ .or_else(|| capabilities.formats.first().copied())
+ .expect("Get preferred format"),
+ adapter
+ .request_device(
+ &wgpu::DeviceDescriptor {
+ label: None,
+ features: adapter_features
+ & wgpu::Features::default(),
+ limits: needed_limits,
+ },
+ None,
+ )
+ .await
+ .expect("Request device"),
+ )
+ });
surface.configure(
&device,
@@ -120,22 +131,24 @@ pub fn main() {
height: physical_size.height,
present_mode: wgpu::PresentMode::AutoVsync,
alpha_mode: wgpu::CompositeAlphaMode::Auto,
+ view_formats: vec![],
},
);
let mut resized = false;
- // Initialize staging belt
- let mut staging_belt = wgpu::util::StagingBelt::new(5 * 1024);
-
// Initialize scene and GUI controls
let scene = Scene::new(&device, format);
let controls = Controls::new();
// Initialize iced
let mut debug = Debug::new();
- let mut renderer =
- Renderer::new(Backend::new(&device, Settings::default(), format));
+ let mut renderer = Renderer::new(Backend::new(
+ &device,
+ &queue,
+ Settings::default(),
+ format,
+ ));
let mut state = program::State::new(
controls,
@@ -153,7 +166,7 @@ pub fn main() {
Event::WindowEvent { event, .. } => {
match event {
WindowEvent::CursorMoved { position, .. } => {
- cursor_position = position;
+ cursor_position = Some(position);
}
WindowEvent::ModifiersChanged(new_modifiers) => {
modifiers = new_modifiers;
@@ -183,13 +196,20 @@ pub fn main() {
// We update iced
let _ = state.update(
viewport.logical_size(),
- conversion::cursor_position(
- cursor_position,
- viewport.scale_factor(),
- ),
+ cursor_position
+ .map(|p| {
+ conversion::cursor_position(
+ p,
+ viewport.scale_factor(),
+ )
+ })
+ .map(mouse::Cursor::Available)
+ .unwrap_or(mouse::Cursor::Unavailable),
&mut renderer,
- &iced_wgpu::Theme::Dark,
- &renderer::Style { text_color: Color::WHITE },
+ &Theme::Dark,
+ &renderer::Style {
+ text_color: Color::WHITE,
+ },
&mut clipboard,
&mut debug,
);
@@ -215,7 +235,8 @@ pub fn main() {
width: size.width,
height: size.height,
present_mode: wgpu::PresentMode::AutoVsync,
- alpha_mode: wgpu::CompositeAlphaMode::Auto
+ alpha_mode: wgpu::CompositeAlphaMode::Auto,
+ view_formats: vec![],
},
);
@@ -230,7 +251,9 @@ pub fn main() {
let program = state.program();
- let view = frame.texture.create_view(&wgpu::TextureViewDescriptor::default());
+ let view = frame.texture.create_view(
+ &wgpu::TextureViewDescriptor::default(),
+ );
{
// We clear the frame
@@ -248,8 +271,9 @@ pub fn main() {
renderer.with_primitives(|backend, primitive| {
backend.present(
&device,
- &mut staging_belt,
+ &queue,
&mut encoder,
+ None,
&view,
primitive,
&viewport,
@@ -258,24 +282,22 @@ pub fn main() {
});
// Then we submit the work
- staging_belt.finish();
queue.submit(Some(encoder.finish()));
frame.present();
// Update the mouse cursor
- window.set_cursor_icon(
- iced_winit::conversion::mouse_interaction(
- state.mouse_interaction(),
- ),
- );
-
- // And recall staging buffers
- staging_belt.recall();
-
+ window.set_cursor_icon(
+ iced_winit::conversion::mouse_interaction(
+ state.mouse_interaction(),
+ ),
+ );
}
Err(error) => match error {
wgpu::SurfaceError::OutOfMemory => {
- panic!("Swapchain error: {error}. Rendering cannot continue.")
+ panic!(
+ "Swapchain error: {error}. \
+ Rendering cannot continue."
+ )
}
_ => {
// Try rendering again next frame.
diff --git a/examples/integration_wgpu/src/scene.rs b/examples/integration/src/scene.rs
index 3e41fbda..90c7efbf 100644
--- a/examples/integration_wgpu/src/scene.rs
+++ b/examples/integration/src/scene.rs
@@ -1,5 +1,5 @@
use iced_wgpu::wgpu;
-use iced_winit::Color;
+use iced_winit::core::Color;
pub struct Scene {
pipeline: wgpu::RenderPipeline,
diff --git a/examples/integration_wgpu/src/shader/frag.wgsl b/examples/integration/src/shader/frag.wgsl
index cf27bb56..cf27bb56 100644
--- a/examples/integration_wgpu/src/shader/frag.wgsl
+++ b/examples/integration/src/shader/frag.wgsl
diff --git a/examples/integration_wgpu/src/shader/vert.wgsl b/examples/integration/src/shader/vert.wgsl
index e353e6ba..e353e6ba 100644
--- a/examples/integration_wgpu/src/shader/vert.wgsl
+++ b/examples/integration/src/shader/vert.wgsl
diff --git a/examples/integration_opengl/Cargo.toml b/examples/integration_opengl/Cargo.toml
deleted file mode 100644
index 6dac999c..00000000
--- a/examples/integration_opengl/Cargo.toml
+++ /dev/null
@@ -1,12 +0,0 @@
-[package]
-name = "integration_opengl"
-version = "0.1.0"
-authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
-edition = "2021"
-publish = false
-
-[dependencies]
-iced_glutin = { path = "../../glutin" }
-iced_glow = { path = "../../glow" }
-iced_winit = { path = "../../winit" }
-env_logger = "0.8"
diff --git a/examples/integration_opengl/README.md b/examples/integration_opengl/README.md
deleted file mode 100644
index b7c2c074..00000000
--- a/examples/integration_opengl/README.md
+++ /dev/null
@@ -1,16 +0,0 @@
-## OpenGL integration
-
-A demonstration of how to integrate Iced in an existing graphical OpenGL application.
-
-The __[`main`]__ file contains all the code of the example.
-
-<div align="center">
- <a href="https://imgbox.com/9P9ETcod" target="_blank"><img src="https://images2.imgbox.com/2a/51/9P9ETcod_o.gif" alt="image host"/></a>
-</div>
-
-You can run it with `cargo run`:
-```
-cargo run --package integration_opengl
-```
-
-[`main`]: src/main.rs
diff --git a/examples/integration_opengl/src/controls.rs b/examples/integration_opengl/src/controls.rs
deleted file mode 100644
index c3648f44..00000000
--- a/examples/integration_opengl/src/controls.rs
+++ /dev/null
@@ -1,101 +0,0 @@
-use iced_glow::Renderer;
-use iced_glutin::widget::Slider;
-use iced_glutin::widget::{Column, Row, Text};
-use iced_glutin::{Alignment, Color, Command, Element, Length, Program};
-
-pub struct Controls {
- background_color: Color,
-}
-
-#[derive(Debug, Clone)]
-pub enum Message {
- BackgroundColorChanged(Color),
-}
-
-impl Controls {
- pub fn new() -> Controls {
- Controls {
- background_color: Color::BLACK,
- }
- }
-
- 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) => {
- self.background_color = color;
- }
- }
-
- Command::none()
- }
-
- fn view(&self) -> Element<Message, Renderer> {
- let background_color = self.background_color;
-
- let sliders = Row::new()
- .width(500)
- .spacing(20)
- .push(
- Slider::new(0.0..=1.0, background_color.r, move |r| {
- Message::BackgroundColorChanged(Color {
- r,
- ..background_color
- })
- })
- .step(0.01),
- )
- .push(
- Slider::new(0.0..=1.0, background_color.g, move |g| {
- Message::BackgroundColorChanged(Color {
- g,
- ..background_color
- })
- })
- .step(0.01),
- )
- .push(
- Slider::new(0.0..=1.0, background_color.b, move |b| {
- Message::BackgroundColorChanged(Color {
- b,
- ..background_color
- })
- })
- .step(0.01),
- );
-
- Row::new()
- .width(Length::Fill)
- .height(Length::Fill)
- .align_items(Alignment::End)
- .push(
- Column::new()
- .width(Length::Fill)
- .align_items(Alignment::End)
- .push(
- Column::new()
- .padding(10)
- .spacing(10)
- .push(
- Text::new("Background color")
- .style(Color::WHITE),
- )
- .push(sliders)
- .push(
- Text::new(format!("{background_color:?}"))
- .size(14)
- .style(Color::WHITE),
- ),
- ),
- )
- .into()
- }
-}
diff --git a/examples/integration_opengl/src/main.rs b/examples/integration_opengl/src/main.rs
index 4dd3a4a9..e69de29b 100644
--- a/examples/integration_opengl/src/main.rs
+++ b/examples/integration_opengl/src/main.rs
@@ -1,188 +0,0 @@
-mod controls;
-mod scene;
-
-use controls::Controls;
-use scene::Scene;
-
-use glow::*;
-use glutin::dpi::PhysicalPosition;
-use glutin::event::{Event, ModifiersState, WindowEvent};
-use glutin::event_loop::ControlFlow;
-use iced_glow::glow;
-use iced_glow::{Backend, Renderer, Settings, Viewport};
-use iced_glutin::conversion;
-use iced_glutin::glutin;
-use iced_glutin::renderer;
-use iced_glutin::{program, Clipboard, Color, Debug, Size};
-
-pub fn main() {
- env_logger::init();
- let (gl, event_loop, windowed_context, shader_version) = {
- let el = glutin::event_loop::EventLoop::new();
-
- let wb = glutin::window::WindowBuilder::new()
- .with_title("OpenGL integration example")
- .with_inner_size(glutin::dpi::LogicalSize::new(1024.0, 768.0));
-
- let windowed_context = glutin::ContextBuilder::new()
- .with_vsync(true)
- .build_windowed(wb, &el)
- .unwrap();
-
- unsafe {
- let windowed_context = windowed_context.make_current().unwrap();
-
- let gl = glow::Context::from_loader_function(|s| {
- windowed_context.get_proc_address(s) as *const _
- });
-
- // 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);
-
- (gl, el, windowed_context, "#version 410")
- }
- };
-
- let physical_size = windowed_context.window().inner_size();
- let mut viewport = Viewport::with_physical_size(
- Size::new(physical_size.width, physical_size.height),
- windowed_context.window().scale_factor(),
- );
-
- let mut cursor_position = PhysicalPosition::new(-1.0, -1.0);
- let mut modifiers = ModifiersState::default();
- let mut clipboard = Clipboard::connect(windowed_context.window());
-
- let mut renderer = Renderer::new(Backend::new(&gl, Settings::default()));
-
- let mut debug = Debug::new();
-
- let controls = Controls::new();
- let mut state = program::State::new(
- controls,
- viewport.logical_size(),
- &mut renderer,
- &mut debug,
- );
- let mut resized = false;
-
- let scene = Scene::new(&gl, shader_version);
-
- event_loop.run(move |event, _, control_flow| {
- *control_flow = ControlFlow::Wait;
-
- match event {
- Event::WindowEvent { event, .. } => {
- match event {
- WindowEvent::CursorMoved { position, .. } => {
- cursor_position = position;
- }
- WindowEvent::ModifiersChanged(new_modifiers) => {
- modifiers = new_modifiers;
- }
- WindowEvent::Resized(physical_size) => {
- viewport = Viewport::with_physical_size(
- Size::new(
- physical_size.width,
- physical_size.height,
- ),
- windowed_context.window().scale_factor(),
- );
-
- resized = true;
- }
- WindowEvent::CloseRequested => {
- scene.cleanup(&gl);
- *control_flow = ControlFlow::Exit
- }
- _ => (),
- }
-
- // Map window event to iced event
- if let Some(event) = iced_winit::conversion::window_event(
- iced_winit::window::Id::MAIN,
- &event,
- windowed_context.window().scale_factor(),
- modifiers,
- ) {
- state.queue_event(event);
- }
- }
- Event::MainEventsCleared => {
- // If there are events pending
- if !state.is_queue_empty() {
- // We update iced
- let _ = state.update(
- viewport.logical_size(),
- conversion::cursor_position(
- cursor_position,
- viewport.scale_factor(),
- ),
- &mut renderer,
- &iced_glow::Theme::Dark,
- &renderer::Style {
- text_color: Color::WHITE,
- },
- &mut clipboard,
- &mut debug,
- );
-
- // and request a redraw
- windowed_context.window().request_redraw();
- }
- }
- Event::RedrawRequested(_) => {
- if resized {
- let size = windowed_context.window().inner_size();
-
- unsafe {
- gl.viewport(
- 0,
- 0,
- size.width as i32,
- size.height as i32,
- );
- }
-
- resized = false;
- }
-
- let program = state.program();
- {
- // We clear the frame
- scene.clear(&gl, program.background_color());
-
- // Draw the scene
- scene.draw(&gl);
- }
-
- // And then iced on top
- renderer.with_primitives(|backend, primitive| {
- backend.present(
- &gl,
- primitive,
- &viewport,
- &debug.overlay(),
- );
- });
-
- // Update the mouse cursor
- windowed_context.window().set_cursor_icon(
- iced_winit::conversion::mouse_interaction(
- state.mouse_interaction(),
- ),
- );
-
- windowed_context.swap_buffers().unwrap();
- }
- _ => (),
- }
- });
-}
diff --git a/examples/integration_opengl/src/scene.rs b/examples/integration_opengl/src/scene.rs
deleted file mode 100644
index c1d05b65..00000000
--- a/examples/integration_opengl/src/scene.rs
+++ /dev/null
@@ -1,102 +0,0 @@
-use glow::*;
-use iced_glow::glow;
-use iced_glow::Color;
-
-pub struct Scene {
- program: glow::Program,
- vertex_array: glow::VertexArray,
-}
-
-impl Scene {
- pub fn new(gl: &glow::Context, shader_version: &str) -> Self {
- unsafe {
- let vertex_array = gl
- .create_vertex_array()
- .expect("Cannot create vertex array");
- gl.bind_vertex_array(Some(vertex_array));
-
- let program = gl.create_program().expect("Cannot create program");
-
- let (vertex_shader_source, fragment_shader_source) = (
- r#"const vec2 verts[3] = vec2[3](
- vec2(0.5f, 1.0f),
- vec2(0.0f, 0.0f),
- vec2(1.0f, 0.0f)
- );
- out vec2 vert;
- void main() {
- vert = verts[gl_VertexID];
- gl_Position = vec4(vert - 0.5, 0.0, 1.0);
- }"#,
- r#"precision highp float;
- in vec2 vert;
- out vec4 color;
- void main() {
- color = vec4(vert, 0.5, 1.0);
- }"#,
- );
-
- let shader_sources = [
- (glow::VERTEX_SHADER, vertex_shader_source),
- (glow::FRAGMENT_SHADER, fragment_shader_source),
- ];
-
- 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,
- &format!("{shader_version}\n{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);
- }
-
- gl.use_program(Some(program));
- Self {
- program,
- vertex_array,
- }
- }
- }
-
- pub fn clear(&self, gl: &glow::Context, background_color: Color) {
- let [r, g, b, a] = background_color.into_linear();
- unsafe {
- gl.clear_color(r, g, b, a);
- gl.clear(glow::COLOR_BUFFER_BIT);
- }
- }
-
- pub fn draw(&self, gl: &glow::Context) {
- unsafe {
- gl.bind_vertex_array(Some(self.vertex_array));
- gl.use_program(Some(self.program));
- gl.draw_arrays(glow::TRIANGLES, 0, 3);
- }
- }
-
- pub fn cleanup(&self, gl: &glow::Context) {
- unsafe {
- gl.delete_program(self.program);
- gl.delete_vertex_array(self.vertex_array);
- }
- }
-}
diff --git a/examples/lazy/Cargo.toml b/examples/lazy/Cargo.toml
index 79255c25..e03e89a9 100644
--- a/examples/lazy/Cargo.toml
+++ b/examples/lazy/Cargo.toml
@@ -6,5 +6,4 @@ edition = "2021"
publish = false
[dependencies]
-iced = { path = "../..", features = ["debug"] }
-iced_lazy = { path = "../../lazy" }
+iced = { path = "../..", features = ["debug", "lazy"] }
diff --git a/examples/lazy/src/main.rs b/examples/lazy/src/main.rs
index 6512106f..c6baa6a1 100644
--- a/examples/lazy/src/main.rs
+++ b/examples/lazy/src/main.rs
@@ -1,10 +1,9 @@
use iced::theme;
use iced::widget::{
- button, column, horizontal_space, pick_list, row, scrollable, text,
+ button, column, horizontal_space, lazy, pick_list, row, scrollable, text,
text_input,
};
use iced::{Element, Length, Sandbox, Settings};
-use iced_lazy::lazy;
use std::collections::HashSet;
use std::hash::Hash;
@@ -214,12 +213,9 @@ impl Sandbox for App {
column![
scrollable(options).height(Length::Fill),
row![
- text_input(
- "Add a new option",
- &self.input,
- Message::InputChanged,
- )
- .on_submit(Message::AddItem(self.input.clone())),
+ text_input("Add a new option", &self.input)
+ .on_input(Message::InputChanged)
+ .on_submit(Message::AddItem(self.input.clone())),
button(text(format!("Toggle Order ({})", self.order)))
.on_press(Message::ToggleOrder)
]
diff --git a/examples/loading_spinners/Cargo.toml b/examples/loading_spinners/Cargo.toml
new file mode 100644
index 00000000..ee9a48aa
--- /dev/null
+++ b/examples/loading_spinners/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "loading_spinners"
+version = "0.1.0"
+authors = ["Nick Senger <dev@nsenger.com>"]
+edition = "2021"
+publish = false
+
+[dependencies]
+iced = { path = "../..", features = ["advanced", "canvas"] }
+lyon_algorithms = "1"
+once_cell = "1"
diff --git a/examples/loading_spinners/README.md b/examples/loading_spinners/README.md
new file mode 100644
index 00000000..3573c6f6
--- /dev/null
+++ b/examples/loading_spinners/README.md
@@ -0,0 +1,14 @@
+## Loading Spinners
+
+Example implementation of animated indeterminate loading spinners.
+
+<div align="center">
+ <a href="https://gfycat.com/importantdevotedhammerheadbird">
+ <img src="https://thumbs.gfycat.com/ImportantDevotedHammerheadbird-small.gif">
+ </a>
+</div>
+
+You can run it with `cargo run`:
+```
+cargo run --package loading_spinners
+```
diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs
new file mode 100644
index 00000000..3a35e029
--- /dev/null
+++ b/examples/loading_spinners/src/circular.rs
@@ -0,0 +1,421 @@
+//! Show a circular progress indicator.
+use iced::advanced::layout;
+use iced::advanced::renderer;
+use iced::advanced::widget::tree::{self, Tree};
+use iced::advanced::{Clipboard, Layout, Renderer, Shell, Widget};
+use iced::event;
+use iced::mouse;
+use iced::time::Instant;
+use iced::widget::canvas;
+use iced::window::{self, RedrawRequest};
+use iced::{
+ Background, Color, Element, Event, Length, Rectangle, Size, Vector,
+};
+
+use super::easing::{self, Easing};
+
+use std::f32::consts::PI;
+use std::time::Duration;
+
+const MIN_RADIANS: f32 = PI / 8.0;
+const WRAP_RADIANS: f32 = 2.0 * PI - PI / 4.0;
+const BASE_ROTATION_SPEED: u32 = u32::MAX / 80;
+
+#[allow(missing_debug_implementations)]
+pub struct Circular<'a, Theme>
+where
+ Theme: StyleSheet,
+{
+ size: f32,
+ bar_height: f32,
+ style: <Theme as StyleSheet>::Style,
+ easing: &'a Easing,
+ cycle_duration: Duration,
+ rotation_duration: Duration,
+}
+
+impl<'a, Theme> Circular<'a, Theme>
+where
+ Theme: StyleSheet,
+{
+ /// Creates a new [`Circular`] with the given content.
+ pub fn new() -> Self {
+ Circular {
+ size: 40.0,
+ bar_height: 4.0,
+ style: <Theme as StyleSheet>::Style::default(),
+ easing: &easing::STANDARD,
+ cycle_duration: Duration::from_millis(600),
+ rotation_duration: Duration::from_secs(2),
+ }
+ }
+
+ /// Sets the size of the [`Circular`].
+ pub fn size(mut self, size: f32) -> Self {
+ self.size = size;
+ self
+ }
+
+ /// Sets the bar height of the [`Circular`].
+ pub fn bar_height(mut self, bar_height: f32) -> Self {
+ self.bar_height = bar_height;
+ self
+ }
+
+ /// Sets the style variant of this [`Circular`].
+ pub fn style(mut self, style: <Theme as StyleSheet>::Style) -> Self {
+ self.style = style;
+ self
+ }
+
+ /// Sets the easing of this [`Circular`].
+ pub fn easing(mut self, easing: &'a Easing) -> Self {
+ self.easing = easing;
+ self
+ }
+
+ /// Sets the cycle duration of this [`Circular`].
+ pub fn cycle_duration(mut self, duration: Duration) -> Self {
+ self.cycle_duration = duration / 2;
+ self
+ }
+
+ /// Sets the base rotation duration of this [`Circular`]. This is the duration that a full
+ /// rotation would take if the cycle rotation were set to 0.0 (no expanding or contracting)
+ pub fn rotation_duration(mut self, duration: Duration) -> Self {
+ self.rotation_duration = duration;
+ self
+ }
+}
+
+impl<'a, Theme> Default for Circular<'a, Theme>
+where
+ Theme: StyleSheet,
+{
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+#[derive(Clone, Copy)]
+enum Animation {
+ Expanding {
+ start: Instant,
+ progress: f32,
+ rotation: u32,
+ last: Instant,
+ },
+ Contracting {
+ start: Instant,
+ progress: f32,
+ rotation: u32,
+ last: Instant,
+ },
+}
+
+impl Default for Animation {
+ fn default() -> Self {
+ Self::Expanding {
+ start: Instant::now(),
+ progress: 0.0,
+ rotation: 0,
+ last: Instant::now(),
+ }
+ }
+}
+
+impl Animation {
+ fn next(&self, additional_rotation: u32, now: Instant) -> Self {
+ match self {
+ Self::Expanding { rotation, .. } => Self::Contracting {
+ start: now,
+ progress: 0.0,
+ rotation: rotation.wrapping_add(additional_rotation),
+ last: now,
+ },
+ Self::Contracting { rotation, .. } => Self::Expanding {
+ start: now,
+ progress: 0.0,
+ rotation: rotation.wrapping_add(
+ BASE_ROTATION_SPEED.wrapping_add(
+ ((WRAP_RADIANS / (2.0 * PI)) * u32::MAX as f32) as u32,
+ ),
+ ),
+ last: now,
+ },
+ }
+ }
+
+ fn start(&self) -> Instant {
+ match self {
+ Self::Expanding { start, .. } | Self::Contracting { start, .. } => {
+ *start
+ }
+ }
+ }
+
+ fn last(&self) -> Instant {
+ match self {
+ Self::Expanding { last, .. } | Self::Contracting { last, .. } => {
+ *last
+ }
+ }
+ }
+
+ fn timed_transition(
+ &self,
+ cycle_duration: Duration,
+ rotation_duration: Duration,
+ now: Instant,
+ ) -> Self {
+ let elapsed = now.duration_since(self.start());
+ let additional_rotation = ((now - self.last()).as_secs_f32()
+ / rotation_duration.as_secs_f32()
+ * (u32::MAX) as f32) as u32;
+
+ match elapsed {
+ elapsed if elapsed > cycle_duration => {
+ self.next(additional_rotation, now)
+ }
+ _ => self.with_elapsed(
+ cycle_duration,
+ additional_rotation,
+ elapsed,
+ now,
+ ),
+ }
+ }
+
+ fn with_elapsed(
+ &self,
+ cycle_duration: Duration,
+ additional_rotation: u32,
+ elapsed: Duration,
+ now: Instant,
+ ) -> Self {
+ let progress = elapsed.as_secs_f32() / cycle_duration.as_secs_f32();
+ match self {
+ Self::Expanding {
+ start, rotation, ..
+ } => Self::Expanding {
+ start: *start,
+ progress,
+ rotation: rotation.wrapping_add(additional_rotation),
+ last: now,
+ },
+ Self::Contracting {
+ start, rotation, ..
+ } => Self::Contracting {
+ start: *start,
+ progress,
+ rotation: rotation.wrapping_add(additional_rotation),
+ last: now,
+ },
+ }
+ }
+
+ fn rotation(&self) -> f32 {
+ match self {
+ Self::Expanding { rotation, .. }
+ | Self::Contracting { rotation, .. } => {
+ *rotation as f32 / u32::MAX as f32
+ }
+ }
+ }
+}
+
+#[derive(Default)]
+struct State {
+ animation: Animation,
+ cache: canvas::Cache,
+}
+
+impl<'a, Message, Theme> Widget<Message, iced::Renderer<Theme>>
+ for Circular<'a, Theme>
+where
+ Message: 'a + Clone,
+ Theme: StyleSheet,
+{
+ fn tag(&self) -> tree::Tag {
+ tree::Tag::of::<State>()
+ }
+
+ fn state(&self) -> tree::State {
+ tree::State::new(State::default())
+ }
+
+ fn width(&self) -> Length {
+ Length::Fixed(self.size)
+ }
+
+ fn height(&self) -> Length {
+ Length::Fixed(self.size)
+ }
+
+ fn layout(
+ &self,
+ _renderer: &iced::Renderer<Theme>,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ let limits = limits.width(self.size).height(self.size);
+ let size = limits.resolve(Size::ZERO);
+
+ layout::Node::new(size)
+ }
+
+ fn on_event(
+ &mut self,
+ tree: &mut Tree,
+ event: Event,
+ _layout: Layout<'_>,
+ _cursor: mouse::Cursor,
+ _renderer: &iced::Renderer<Theme>,
+ _clipboard: &mut dyn Clipboard,
+ shell: &mut Shell<'_, Message>,
+ ) -> event::Status {
+ const FRAME_RATE: u64 = 60;
+
+ let state = tree.state.downcast_mut::<State>();
+
+ if let Event::Window(window::Event::RedrawRequested(now)) = event {
+ state.animation = state.animation.timed_transition(
+ self.cycle_duration,
+ self.rotation_duration,
+ now,
+ );
+
+ state.cache.clear();
+ shell.request_redraw(RedrawRequest::At(
+ now + Duration::from_millis(1000 / FRAME_RATE),
+ ));
+ }
+
+ event::Status::Ignored
+ }
+
+ fn draw(
+ &self,
+ tree: &Tree,
+ renderer: &mut iced::Renderer<Theme>,
+ theme: &Theme,
+ _style: &renderer::Style,
+ layout: Layout<'_>,
+ _cursor: mouse::Cursor,
+ _viewport: &Rectangle,
+ ) {
+ let state = tree.state.downcast_ref::<State>();
+ let bounds = layout.bounds();
+ let custom_style =
+ <Theme as StyleSheet>::appearance(theme, &self.style);
+
+ let geometry = state.cache.draw(renderer, bounds.size(), |frame| {
+ let track_radius = frame.width() / 2.0 - self.bar_height;
+ let track_path = canvas::Path::circle(frame.center(), track_radius);
+
+ frame.stroke(
+ &track_path,
+ canvas::Stroke::default()
+ .with_color(custom_style.track_color)
+ .with_width(self.bar_height),
+ );
+
+ let mut builder = canvas::path::Builder::new();
+
+ let start = state.animation.rotation() * 2.0 * PI;
+
+ match state.animation {
+ Animation::Expanding { progress, .. } => {
+ builder.arc(canvas::path::Arc {
+ center: frame.center(),
+ radius: track_radius,
+ start_angle: start,
+ end_angle: start
+ + MIN_RADIANS
+ + WRAP_RADIANS * (self.easing.y_at_x(progress)),
+ });
+ }
+ Animation::Contracting { progress, .. } => {
+ builder.arc(canvas::path::Arc {
+ center: frame.center(),
+ radius: track_radius,
+ start_angle: start
+ + WRAP_RADIANS * (self.easing.y_at_x(progress)),
+ end_angle: start + MIN_RADIANS + WRAP_RADIANS,
+ });
+ }
+ }
+
+ let bar_path = builder.build();
+
+ frame.stroke(
+ &bar_path,
+ canvas::Stroke::default()
+ .with_color(custom_style.bar_color)
+ .with_width(self.bar_height),
+ );
+ });
+
+ renderer.with_translation(
+ Vector::new(bounds.x, bounds.y),
+ |renderer| {
+ use iced::advanced::graphics::geometry::Renderer as _;
+
+ renderer.draw(vec![geometry]);
+ },
+ );
+ }
+}
+
+impl<'a, Message, Theme> From<Circular<'a, Theme>>
+ for Element<'a, Message, iced::Renderer<Theme>>
+where
+ Message: Clone + 'a,
+ Theme: StyleSheet + 'a,
+{
+ fn from(circular: Circular<'a, Theme>) -> Self {
+ Self::new(circular)
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct Appearance {
+ /// The [`Background`] of the progress indicator.
+ pub background: Option<Background>,
+ /// The track [`Color`] of the progress indicator.
+ pub track_color: Color,
+ /// The bar [`Color`] of the progress indicator.
+ pub bar_color: Color,
+}
+
+impl std::default::Default for Appearance {
+ fn default() -> Self {
+ Self {
+ background: None,
+ track_color: Color::TRANSPARENT,
+ bar_color: Color::BLACK,
+ }
+ }
+}
+
+/// A set of rules that dictate the style of an indicator.
+pub trait StyleSheet {
+ /// The supported style of the [`StyleSheet`].
+ type Style: Default;
+
+ /// Produces the active [`Appearance`] of a indicator.
+ fn appearance(&self, style: &Self::Style) -> Appearance;
+}
+
+impl StyleSheet for iced::Theme {
+ type Style = ();
+
+ fn appearance(&self, _style: &Self::Style) -> Appearance {
+ let palette = self.extended_palette();
+
+ Appearance {
+ background: None,
+ track_color: palette.background.weak.color,
+ bar_color: palette.primary.base.color,
+ }
+ }
+}
diff --git a/examples/loading_spinners/src/easing.rs b/examples/loading_spinners/src/easing.rs
new file mode 100644
index 00000000..665b3329
--- /dev/null
+++ b/examples/loading_spinners/src/easing.rs
@@ -0,0 +1,133 @@
+use iced::Point;
+
+use lyon_algorithms::measure::PathMeasurements;
+use lyon_algorithms::path::{builder::NoAttributes, path::BuilderImpl, Path};
+use once_cell::sync::Lazy;
+
+pub static EMPHASIZED: Lazy<Easing> = Lazy::new(|| {
+ Easing::builder()
+ .cubic_bezier_to([0.05, 0.0], [0.133333, 0.06], [0.166666, 0.4])
+ .cubic_bezier_to([0.208333, 0.82], [0.25, 1.0], [1.0, 1.0])
+ .build()
+});
+
+pub static EMPHASIZED_DECELERATE: Lazy<Easing> = Lazy::new(|| {
+ Easing::builder()
+ .cubic_bezier_to([0.05, 0.7], [0.1, 1.0], [1.0, 1.0])
+ .build()
+});
+
+pub static EMPHASIZED_ACCELERATE: Lazy<Easing> = Lazy::new(|| {
+ Easing::builder()
+ .cubic_bezier_to([0.3, 0.0], [0.8, 0.15], [1.0, 1.0])
+ .build()
+});
+
+pub static STANDARD: Lazy<Easing> = Lazy::new(|| {
+ Easing::builder()
+ .cubic_bezier_to([0.2, 0.0], [0.0, 1.0], [1.0, 1.0])
+ .build()
+});
+
+pub static STANDARD_DECELERATE: Lazy<Easing> = Lazy::new(|| {
+ Easing::builder()
+ .cubic_bezier_to([0.0, 0.0], [0.0, 1.0], [1.0, 1.0])
+ .build()
+});
+
+pub static STANDARD_ACCELERATE: Lazy<Easing> = Lazy::new(|| {
+ Easing::builder()
+ .cubic_bezier_to([0.3, 0.0], [1.0, 1.0], [1.0, 1.0])
+ .build()
+});
+
+pub struct Easing {
+ path: Path,
+ measurements: PathMeasurements,
+}
+
+impl Easing {
+ pub fn builder() -> Builder {
+ Builder::new()
+ }
+
+ pub fn y_at_x(&self, x: f32) -> f32 {
+ let mut sampler = self.measurements.create_sampler(
+ &self.path,
+ lyon_algorithms::measure::SampleType::Normalized,
+ );
+ let sample = sampler.sample(x);
+
+ sample.position().y
+ }
+}
+
+pub struct Builder(NoAttributes<BuilderImpl>);
+
+impl Builder {
+ pub fn new() -> Self {
+ let mut builder = Path::builder();
+ builder.begin(lyon_algorithms::geom::point(0.0, 0.0));
+
+ Self(builder)
+ }
+
+ /// Adds a line segment. Points must be between 0,0 and 1,1
+ pub fn line_to(mut self, to: impl Into<Point>) -> Self {
+ self.0.line_to(Self::point(to));
+
+ self
+ }
+
+ /// Adds a quadratic bézier curve. Points must be between 0,0 and 1,1
+ pub fn quadratic_bezier_to(
+ mut self,
+ ctrl: impl Into<Point>,
+ to: impl Into<Point>,
+ ) -> Self {
+ self.0
+ .quadratic_bezier_to(Self::point(ctrl), Self::point(to));
+
+ self
+ }
+
+ /// Adds a cubic bézier curve. Points must be between 0,0 and 1,1
+ pub fn cubic_bezier_to(
+ mut self,
+ ctrl1: impl Into<Point>,
+ ctrl2: impl Into<Point>,
+ to: impl Into<Point>,
+ ) -> Self {
+ self.0.cubic_bezier_to(
+ Self::point(ctrl1),
+ Self::point(ctrl2),
+ Self::point(to),
+ );
+
+ self
+ }
+
+ pub fn build(mut self) -> Easing {
+ self.0.line_to(lyon_algorithms::geom::point(1.0, 1.0));
+ self.0.end(false);
+
+ let path = self.0.build();
+ let measurements = PathMeasurements::from_path(&path, 0.0);
+
+ Easing { path, measurements }
+ }
+
+ fn point(p: impl Into<Point>) -> lyon_algorithms::geom::Point<f32> {
+ let p: Point = p.into();
+ lyon_algorithms::geom::point(
+ p.x.min(1.0).max(0.0),
+ p.y.min(1.0).max(0.0),
+ )
+ }
+}
+
+impl Default for Builder {
+ fn default() -> Self {
+ Self::new()
+ }
+}
diff --git a/examples/loading_spinners/src/linear.rs b/examples/loading_spinners/src/linear.rs
new file mode 100644
index 00000000..3d95729b
--- /dev/null
+++ b/examples/loading_spinners/src/linear.rs
@@ -0,0 +1,326 @@
+//! Show a linear progress indicator.
+use iced::advanced::layout;
+use iced::advanced::renderer::{self, Quad};
+use iced::advanced::widget::tree::{self, Tree};
+use iced::advanced::{Clipboard, Layout, Shell, Widget};
+use iced::event;
+use iced::mouse;
+use iced::time::Instant;
+use iced::window::{self, RedrawRequest};
+use iced::{Background, Color, Element, Event, Length, Rectangle, Size};
+
+use super::easing::{self, Easing};
+
+use std::time::Duration;
+
+#[allow(missing_debug_implementations)]
+pub struct Linear<'a, Renderer>
+where
+ Renderer: iced::advanced::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ width: Length,
+ height: Length,
+ style: <Renderer::Theme as StyleSheet>::Style,
+ easing: &'a Easing,
+ cycle_duration: Duration,
+}
+
+impl<'a, Renderer> Linear<'a, Renderer>
+where
+ Renderer: iced::advanced::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ /// Creates a new [`Linear`] with the given content.
+ pub fn new() -> Self {
+ Linear {
+ width: Length::Fixed(100.0),
+ height: Length::Fixed(4.0),
+ style: <Renderer::Theme as StyleSheet>::Style::default(),
+ easing: &easing::STANDARD,
+ cycle_duration: Duration::from_millis(600),
+ }
+ }
+
+ /// Sets the width of the [`Linear`].
+ pub fn width(mut self, width: impl Into<Length>) -> Self {
+ self.width = width.into();
+ self
+ }
+
+ /// Sets the height of the [`Linear`].
+ pub fn height(mut self, height: impl Into<Length>) -> Self {
+ self.height = height.into();
+ self
+ }
+
+ /// Sets the style variant of this [`Linear`].
+ pub fn style(
+ mut self,
+ style: <Renderer::Theme as StyleSheet>::Style,
+ ) -> Self {
+ self.style = style;
+ self
+ }
+
+ /// Sets the motion easing of this [`Linear`].
+ pub fn easing(mut self, easing: &'a Easing) -> Self {
+ self.easing = easing;
+ self
+ }
+
+ /// Sets the cycle duration of this [`Linear`].
+ pub fn cycle_duration(mut self, duration: Duration) -> Self {
+ self.cycle_duration = duration / 2;
+ self
+ }
+}
+
+impl<'a, Renderer> Default for Linear<'a, Renderer>
+where
+ Renderer: iced::advanced::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+#[derive(Clone, Copy)]
+enum State {
+ Expanding { start: Instant, progress: f32 },
+ Contracting { start: Instant, progress: f32 },
+}
+
+impl Default for State {
+ fn default() -> Self {
+ Self::Expanding {
+ start: Instant::now(),
+ progress: 0.0,
+ }
+ }
+}
+
+impl State {
+ fn next(&self, now: Instant) -> Self {
+ match self {
+ Self::Expanding { .. } => Self::Contracting {
+ start: now,
+ progress: 0.0,
+ },
+ Self::Contracting { .. } => Self::Expanding {
+ start: now,
+ progress: 0.0,
+ },
+ }
+ }
+
+ fn start(&self) -> Instant {
+ match self {
+ Self::Expanding { start, .. } | Self::Contracting { start, .. } => {
+ *start
+ }
+ }
+ }
+
+ fn timed_transition(&self, cycle_duration: Duration, now: Instant) -> Self {
+ let elapsed = now.duration_since(self.start());
+
+ match elapsed {
+ elapsed if elapsed > cycle_duration => self.next(now),
+ _ => self.with_elapsed(cycle_duration, elapsed),
+ }
+ }
+
+ fn with_elapsed(
+ &self,
+ cycle_duration: Duration,
+ elapsed: Duration,
+ ) -> Self {
+ let progress = elapsed.as_secs_f32() / cycle_duration.as_secs_f32();
+ match self {
+ Self::Expanding { start, .. } => Self::Expanding {
+ start: *start,
+ progress,
+ },
+ Self::Contracting { start, .. } => Self::Contracting {
+ start: *start,
+ progress,
+ },
+ }
+ }
+}
+
+impl<'a, Message, Renderer> Widget<Message, Renderer> for Linear<'a, Renderer>
+where
+ Message: 'a + Clone,
+ Renderer: 'a + iced::advanced::Renderer,
+ Renderer::Theme: StyleSheet,
+{
+ fn tag(&self) -> tree::Tag {
+ tree::Tag::of::<State>()
+ }
+
+ fn state(&self) -> tree::State {
+ tree::State::new(State::default())
+ }
+
+ 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 on_event(
+ &mut self,
+ tree: &mut Tree,
+ event: Event,
+ _layout: Layout<'_>,
+ _cursor: mouse::Cursor,
+ _renderer: &Renderer,
+ _clipboard: &mut dyn Clipboard,
+ shell: &mut Shell<'_, Message>,
+ ) -> event::Status {
+ const FRAME_RATE: u64 = 60;
+
+ let state = tree.state.downcast_mut::<State>();
+
+ if let Event::Window(window::Event::RedrawRequested(now)) = event {
+ *state = state.timed_transition(self.cycle_duration, now);
+
+ shell.request_redraw(RedrawRequest::At(
+ now + Duration::from_millis(1000 / FRAME_RATE),
+ ));
+ }
+
+ event::Status::Ignored
+ }
+
+ fn draw(
+ &self,
+ tree: &Tree,
+ renderer: &mut Renderer,
+ theme: &Renderer::Theme,
+ _style: &renderer::Style,
+ layout: Layout<'_>,
+ _cursor: mouse::Cursor,
+ _viewport: &Rectangle,
+ ) {
+ let bounds = layout.bounds();
+ let custom_style = theme.appearance(&self.style);
+ let state = tree.state.downcast_ref::<State>();
+
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: Rectangle {
+ x: bounds.x,
+ y: bounds.y,
+ width: bounds.width,
+ height: bounds.height,
+ },
+ border_radius: 0.0.into(),
+ border_width: 0.0,
+ border_color: Color::TRANSPARENT,
+ },
+ Background::Color(custom_style.track_color),
+ );
+
+ match state {
+ State::Expanding { progress, .. } => renderer.fill_quad(
+ renderer::Quad {
+ bounds: Rectangle {
+ x: bounds.x,
+ y: bounds.y,
+ width: self.easing.y_at_x(*progress) * bounds.width,
+ height: bounds.height,
+ },
+ border_radius: 0.0.into(),
+ border_width: 0.0,
+ border_color: Color::TRANSPARENT,
+ },
+ Background::Color(custom_style.bar_color),
+ ),
+
+ State::Contracting { progress, .. } => renderer.fill_quad(
+ Quad {
+ bounds: Rectangle {
+ x: bounds.x
+ + self.easing.y_at_x(*progress) * bounds.width,
+ y: bounds.y,
+ width: (1.0 - self.easing.y_at_x(*progress))
+ * bounds.width,
+ height: bounds.height,
+ },
+ border_radius: 0.0.into(),
+ border_width: 0.0,
+ border_color: Color::TRANSPARENT,
+ },
+ Background::Color(custom_style.bar_color),
+ ),
+ }
+ }
+}
+
+impl<'a, Message, Renderer> From<Linear<'a, Renderer>>
+ for Element<'a, Message, Renderer>
+where
+ Message: Clone + 'a,
+ Renderer: iced::advanced::Renderer + 'a,
+ Renderer::Theme: StyleSheet,
+{
+ fn from(linear: Linear<'a, Renderer>) -> Self {
+ Self::new(linear)
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct Appearance {
+ /// The track [`Color`] of the progress indicator.
+ pub track_color: Color,
+ /// The bar [`Color`] of the progress indicator.
+ pub bar_color: Color,
+}
+
+impl std::default::Default for Appearance {
+ fn default() -> Self {
+ Self {
+ track_color: Color::TRANSPARENT,
+ bar_color: Color::BLACK,
+ }
+ }
+}
+
+/// A set of rules that dictate the style of an indicator.
+pub trait StyleSheet {
+ /// The supported style of the [`StyleSheet`].
+ type Style: Default;
+
+ /// Produces the active [`Appearance`] of a indicator.
+ fn appearance(&self, style: &Self::Style) -> Appearance;
+}
+
+impl StyleSheet for iced::Theme {
+ type Style = ();
+
+ fn appearance(&self, _style: &Self::Style) -> Appearance {
+ let palette = self.extended_palette();
+
+ Appearance {
+ track_color: palette.background.weak.color,
+ bar_color: palette.primary.base.color,
+ }
+ }
+}
diff --git a/examples/loading_spinners/src/main.rs b/examples/loading_spinners/src/main.rs
new file mode 100644
index 00000000..a78e9590
--- /dev/null
+++ b/examples/loading_spinners/src/main.rs
@@ -0,0 +1,118 @@
+use iced::executor;
+use iced::widget::{column, container, row, slider, text};
+use iced::{Application, Command, Element, Length, Settings, Theme};
+
+use std::time::Duration;
+
+mod circular;
+mod easing;
+mod linear;
+
+use circular::Circular;
+use linear::Linear;
+
+pub fn main() -> iced::Result {
+ LoadingSpinners::run(Settings {
+ antialiasing: true,
+ ..Default::default()
+ })
+}
+
+struct LoadingSpinners {
+ cycle_duration: f32,
+}
+
+impl Default for LoadingSpinners {
+ fn default() -> Self {
+ Self {
+ cycle_duration: 2.0,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+enum Message {
+ CycleDurationChanged(f32),
+}
+
+impl Application for LoadingSpinners {
+ type Message = Message;
+ type Flags = ();
+ type Executor = executor::Default;
+ type Theme = Theme;
+
+ fn new(_flags: Self::Flags) -> (Self, Command<Message>) {
+ (Self::default(), Command::none())
+ }
+
+ fn title(&self) -> String {
+ String::from("Loading Spinners - Iced")
+ }
+
+ fn update(&mut self, message: Message) -> Command<Message> {
+ match message {
+ Message::CycleDurationChanged(duration) => {
+ self.cycle_duration = duration;
+ }
+ }
+
+ Command::none()
+ }
+
+ fn view(&self) -> Element<Message> {
+ let column = [
+ &easing::EMPHASIZED,
+ &easing::EMPHASIZED_DECELERATE,
+ &easing::EMPHASIZED_ACCELERATE,
+ &easing::STANDARD,
+ &easing::STANDARD_DECELERATE,
+ &easing::STANDARD_ACCELERATE,
+ ]
+ .iter()
+ .zip([
+ "Emphasized:",
+ "Emphasized Decelerate:",
+ "Emphasized Accelerate:",
+ "Standard:",
+ "Standard Decelerate:",
+ "Standard Accelerate:",
+ ])
+ .fold(column![], |column, (easing, label)| {
+ column.push(
+ row![
+ text(label).width(250),
+ Linear::new().easing(easing).cycle_duration(
+ Duration::from_secs_f32(self.cycle_duration)
+ ),
+ Circular::new().easing(easing).cycle_duration(
+ Duration::from_secs_f32(self.cycle_duration)
+ )
+ ]
+ .align_items(iced::Alignment::Center)
+ .spacing(20.0),
+ )
+ })
+ .spacing(20);
+
+ container(
+ column.push(
+ row(vec![
+ text("Cycle duration:").into(),
+ slider(1.0..=1000.0, self.cycle_duration * 100.0, |x| {
+ Message::CycleDurationChanged(x / 100.0)
+ })
+ .width(200.0)
+ .into(),
+ text(format!("{:.2}s", self.cycle_duration)).into(),
+ ])
+ .align_items(iced::Alignment::Center)
+ .spacing(20.0),
+ ),
+ )
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .center_x()
+ .center_y()
+ .into()
+ }
+}
diff --git a/examples/modal/Cargo.toml b/examples/modal/Cargo.toml
index 8770acac..3ac61e6a 100644
--- a/examples/modal/Cargo.toml
+++ b/examples/modal/Cargo.toml
@@ -6,5 +6,4 @@ edition = "2021"
publish = false
[dependencies]
-iced = { path = "../..", features = [] }
-iced_native = { path = "../../native" }
+iced = { path = "../..", features = ["advanced"] }
diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs
index 54555684..7fcbbfe4 100644
--- a/examples/modal/src/main.rs
+++ b/examples/modal/src/main.rs
@@ -1,12 +1,15 @@
+use iced::executor;
+use iced::keyboard;
+use iced::subscription::{self, Subscription};
+use iced::theme;
use iced::widget::{
- self, button, column, container, horizontal_space, row, text, text_input,
-};
-use iced::{
- executor, keyboard, subscription, theme, Alignment, Application, Command,
- Element, Event, Length, Settings, Subscription,
+ self, button, column, container, horizontal_space, pick_list, row, text,
+ text_input,
};
+use iced::{Alignment, Application, Command, Element, Event, Length, Settings};
-use self::modal::Modal;
+use modal::Modal;
+use std::fmt;
pub fn main() -> iced::Result {
App::run(Settings::default())
@@ -17,6 +20,7 @@ struct App {
show_modal: bool,
email: String,
password: String,
+ plan: Plan,
}
#[derive(Debug, Clone)]
@@ -25,6 +29,7 @@ enum Message {
HideModal,
Email(String),
Password(String),
+ Plan(Plan),
Submit,
Event(Event),
}
@@ -65,6 +70,10 @@ impl Application for App {
self.password = password;
Command::none()
}
+ Message::Plan(plan) => {
+ self.plan = plan;
+ Command::none()
+ }
Message::Submit => {
if !self.email.is_empty() && !self.password.is_empty() {
self.hide_modal();
@@ -133,23 +142,31 @@ impl Application for App {
column![
column![
text("Email").size(12),
- text_input(
- "abc@123.com",
- &self.email,
- Message::Email
- )
- .on_submit(Message::Submit)
- .padding(5),
+ text_input("abc@123.com", &self.email,)
+ .on_input(Message::Email)
+ .on_submit(Message::Submit)
+ .padding(5),
]
.spacing(5),
column![
text("Password").size(12),
- text_input("", &self.password, Message::Password)
+ text_input("", &self.password)
+ .on_input(Message::Password)
.on_submit(Message::Submit)
.password()
.padding(5),
]
.spacing(5),
+ column![
+ text("Plan").size(12),
+ pick_list(
+ Plan::ALL,
+ Some(self.plan),
+ Message::Plan
+ )
+ .padding(5),
+ ]
+ .spacing(5),
button(text("Submit")).on_press(Message::HideModal),
]
.spacing(10)
@@ -177,13 +194,39 @@ impl App {
}
}
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+enum Plan {
+ #[default]
+ Basic,
+ Pro,
+ Enterprise,
+}
+
+impl Plan {
+ pub const ALL: &[Self] = &[Self::Basic, Self::Pro, Self::Enterprise];
+}
+
+impl fmt::Display for Plan {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Plan::Basic => "Basic",
+ Plan::Pro => "Pro",
+ Plan::Enterprise => "Enterprise",
+ }
+ .fmt(f)
+ }
+}
+
mod modal {
- use iced_native::alignment::Alignment;
- use iced_native::widget::{self, Tree};
- use iced_native::{
- event, layout, mouse, overlay, renderer, Clipboard, Color, Element,
- Event, Layout, Length, Point, Rectangle, Shell, Size, Widget,
- };
+ use iced::advanced::layout::{self, Layout};
+ use iced::advanced::overlay;
+ use iced::advanced::renderer;
+ use iced::advanced::widget::{self, Widget};
+ use iced::advanced::{self, Clipboard, Shell};
+ use iced::alignment::Alignment;
+ use iced::event;
+ use iced::mouse;
+ use iced::{Color, Element, Event, Length, Point, Rectangle, Size};
/// A widget that centers a modal element over some base element
pub struct Modal<'a, Message, Renderer> {
@@ -218,14 +261,17 @@ mod modal {
impl<'a, Message, Renderer> Widget<Message, Renderer>
for Modal<'a, Message, Renderer>
where
- Renderer: iced_native::Renderer,
+ Renderer: advanced::Renderer,
Message: Clone,
{
- fn children(&self) -> Vec<Tree> {
- vec![Tree::new(&self.base), Tree::new(&self.modal)]
+ fn children(&self) -> Vec<widget::Tree> {
+ vec![
+ widget::Tree::new(&self.base),
+ widget::Tree::new(&self.modal),
+ ]
}
- fn diff(&self, tree: &mut Tree) {
+ fn diff(&self, tree: &mut widget::Tree) {
tree.diff_children(&[&self.base, &self.modal]);
}
@@ -247,10 +293,10 @@ mod modal {
fn on_event(
&mut self,
- state: &mut Tree,
+ state: &mut widget::Tree,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
@@ -259,7 +305,7 @@ mod modal {
&mut state.children[0],
event,
layout,
- cursor_position,
+ cursor,
renderer,
clipboard,
shell,
@@ -268,12 +314,12 @@ mod modal {
fn draw(
&self,
- state: &Tree,
+ state: &widget::Tree,
renderer: &mut Renderer,
- theme: &<Renderer as iced_native::Renderer>::Theme,
+ theme: &<Renderer as advanced::Renderer>::Theme,
style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
) {
self.base.as_widget().draw(
@@ -282,14 +328,14 @@ mod modal {
theme,
style,
layout,
- cursor_position,
+ cursor,
viewport,
);
}
fn overlay<'b>(
&'b mut self,
- state: &'b mut Tree,
+ state: &'b mut widget::Tree,
layout: Layout<'_>,
_renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> {
@@ -306,16 +352,16 @@ mod modal {
fn mouse_interaction(
&self,
- state: &Tree,
+ state: &widget::Tree,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.base.as_widget().mouse_interaction(
&state.children[0],
layout,
- cursor_position,
+ cursor,
viewport,
renderer,
)
@@ -323,7 +369,7 @@ mod modal {
fn operate(
&self,
- state: &mut Tree,
+ state: &mut widget::Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation<Message>,
@@ -339,7 +385,7 @@ mod modal {
struct Overlay<'a, 'b, Message, Renderer> {
content: &'b mut Element<'a, Message, Renderer>,
- tree: &'b mut Tree,
+ tree: &'b mut widget::Tree,
size: Size,
on_blur: Option<Message>,
}
@@ -347,7 +393,7 @@ mod modal {
impl<'a, 'b, Message, Renderer> overlay::Overlay<Message, Renderer>
for Overlay<'a, 'b, Message, Renderer>
where
- Renderer: iced_native::Renderer,
+ Renderer: advanced::Renderer,
Message: Clone,
{
fn layout(
@@ -373,7 +419,7 @@ mod modal {
&mut self,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
@@ -385,7 +431,7 @@ mod modal {
mouse::Button::Left,
)) = &event
{
- if !content_bounds.contains(cursor_position) {
+ if !cursor.is_over(content_bounds) {
shell.publish(message.clone());
return event::Status::Captured;
}
@@ -396,7 +442,7 @@ mod modal {
self.tree,
event,
layout.children().next().unwrap(),
- cursor_position,
+ cursor,
renderer,
clipboard,
shell,
@@ -409,12 +455,12 @@ mod modal {
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
) {
renderer.fill_quad(
renderer::Quad {
bounds: layout.bounds(),
- border_radius: renderer::BorderRadius::from(0.0),
+ border_radius: Default::default(),
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
@@ -430,7 +476,7 @@ mod modal {
theme,
style,
layout.children().next().unwrap(),
- cursor_position,
+ cursor,
&layout.bounds(),
);
}
@@ -452,24 +498,36 @@ mod modal {
fn mouse_interaction(
&self,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.content.as_widget().mouse_interaction(
self.tree,
layout.children().next().unwrap(),
- cursor_position,
+ cursor,
viewport,
renderer,
)
}
+
+ fn overlay<'c>(
+ &'c mut self,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ ) -> Option<overlay::Element<'c, Message, Renderer>> {
+ self.content.as_widget_mut().overlay(
+ self.tree,
+ layout.children().next().unwrap(),
+ renderer,
+ )
+ }
}
impl<'a, Message, Renderer> From<Modal<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
- Renderer: 'a + iced_native::Renderer,
+ Renderer: 'a + advanced::Renderer,
Message: 'a + Clone,
{
fn from(modal: Modal<'a, Message, Renderer>) -> Self {
diff --git a/examples/modern_art/Cargo.toml b/examples/modern_art/Cargo.toml
deleted file mode 100644
index 4242d209..00000000
--- a/examples/modern_art/Cargo.toml
+++ /dev/null
@@ -1,11 +0,0 @@
-[package]
-name = "modern_art"
-version = "0.1.0"
-authors = ["Bingus <shankern@protonmail.com>"]
-edition = "2021"
-publish = false
-
-[dependencies]
-iced = { path = "../..", features = ["canvas", "tokio", "debug"] }
-rand = "0.8.5"
-env_logger = "0.9"
diff --git a/examples/modern_art/src/main.rs b/examples/modern_art/src/main.rs
deleted file mode 100644
index 28ed3e21..00000000
--- a/examples/modern_art/src/main.rs
+++ /dev/null
@@ -1,142 +0,0 @@
-use iced::widget::canvas::{
- self, gradient::Location, gradient::Position, Cache, Canvas, Cursor, Frame,
- Geometry, Gradient,
-};
-use iced::{
- executor, Application, Color, Command, Element, Length, Point, Rectangle,
- Renderer, Settings, Size, Theme,
-};
-use rand::{thread_rng, Rng};
-
-fn main() -> iced::Result {
- env_logger::builder().format_timestamp(None).init();
-
- ModernArt::run(Settings {
- antialiasing: true,
- ..Settings::default()
- })
-}
-
-#[derive(Debug, Clone, Copy)]
-enum Message {}
-
-struct ModernArt {
- cache: Cache,
-}
-
-impl Application for ModernArt {
- type Executor = executor::Default;
- type Message = Message;
- type Theme = Theme;
- type Flags = ();
-
- fn new(_flags: Self::Flags) -> (Self, Command<Self::Message>) {
- (
- ModernArt {
- cache: Default::default(),
- },
- Command::none(),
- )
- }
-
- fn title(&self) -> String {
- String::from("Modern Art")
- }
-
- fn update(&mut self, _message: Message) -> Command<Message> {
- Command::none()
- }
-
- fn view(&self) -> Element<'_, Self::Message, Renderer<Self::Theme>> {
- Canvas::new(self)
- .width(Length::Fill)
- .height(Length::Fill)
- .into()
- }
-}
-
-impl<Message> canvas::Program<Message> for ModernArt {
- type State = ();
-
- fn draw(
- &self,
- _state: &Self::State,
- _theme: &Theme,
- bounds: Rectangle,
- _cursor: Cursor,
- ) -> Vec<Geometry> {
- let geometry = self.cache.draw(bounds.size(), |frame| {
- let num_squares = thread_rng().gen_range(0..1200);
-
- let mut i = 0;
- while i <= num_squares {
- generate_box(frame, bounds.size());
- i += 1;
- }
- });
-
- vec![geometry]
- }
-}
-
-fn random_direction() -> Location {
- match thread_rng().gen_range(0..8) {
- 0 => Location::TopLeft,
- 1 => Location::Top,
- 2 => Location::TopRight,
- 3 => Location::Right,
- 4 => Location::BottomRight,
- 5 => Location::Bottom,
- 6 => Location::BottomLeft,
- 7 => Location::Left,
- _ => Location::TopLeft,
- }
-}
-
-fn generate_box(frame: &mut Frame, bounds: Size) -> bool {
- let solid = rand::random::<bool>();
-
- let random_color = || -> Color {
- Color::from_rgb(
- thread_rng().gen_range(0.0..1.0),
- thread_rng().gen_range(0.0..1.0),
- thread_rng().gen_range(0.0..1.0),
- )
- };
-
- let gradient = |top_left: Point, size: Size| -> Gradient {
- let mut builder = Gradient::linear(Position::Relative {
- top_left,
- size,
- start: random_direction(),
- end: random_direction(),
- });
- let stops = thread_rng().gen_range(1..15u32);
-
- let mut i = 0;
- while i <= stops {
- builder = builder.add_stop(i as f32 / stops as f32, random_color());
- i += 1;
- }
-
- builder.build().unwrap()
- };
-
- let top_left = Point::new(
- thread_rng().gen_range(0.0..bounds.width),
- thread_rng().gen_range(0.0..bounds.height),
- );
-
- let size = Size::new(
- thread_rng().gen_range(50.0..200.0),
- thread_rng().gen_range(50.0..200.0),
- );
-
- if solid {
- frame.fill_rectangle(top_left, size, random_color());
- } else {
- frame.fill_rectangle(top_left, size, gradient(top_left, size));
- };
-
- solid
-}
diff --git a/examples/multitouch/src/main.rs b/examples/multitouch/src/main.rs
index f5faae0f..2830b78d 100644
--- a/examples/multitouch/src/main.rs
+++ b/examples/multitouch/src/main.rs
@@ -1,12 +1,13 @@
//! This example shows how to use touch events in `Canvas` to draw
//! a circle around each fingertip. This only works on touch-enabled
//! computers like Microsoft Surface.
+use iced::mouse;
use iced::widget::canvas::event;
use iced::widget::canvas::stroke::{self, Stroke};
-use iced::widget::canvas::{self, Canvas, Cursor, Geometry};
+use iced::widget::canvas::{self, Canvas, Geometry};
use iced::{
executor, touch, window, Application, Color, Command, Element, Length,
- Point, Rectangle, Settings, Subscription, Theme,
+ Point, Rectangle, Renderer, Settings, Subscription, Theme,
};
use std::collections::HashMap;
@@ -95,7 +96,7 @@ impl Application for Multitouch {
}
}
-impl canvas::Program<Message> for State {
+impl canvas::Program<Message, Renderer> for State {
type State = ();
fn update(
@@ -103,7 +104,7 @@ impl canvas::Program<Message> for State {
_state: &mut Self::State,
event: event::Event,
_bounds: Rectangle,
- _cursor: Cursor,
+ _cursor: mouse::Cursor,
) -> (event::Status, Option<Message>) {
match event {
event::Event::Touch(touch_event) => match touch_event {
@@ -125,11 +126,12 @@ impl canvas::Program<Message> for State {
fn draw(
&self,
_state: &Self::State,
+ renderer: &Renderer,
_theme: &Theme,
bounds: Rectangle,
- _cursor: Cursor,
+ _cursor: mouse::Cursor,
) -> Vec<Geometry> {
- let fingerweb = self.cache.draw(bounds.size(), |frame| {
+ let fingerweb = self.cache.draw(renderer, bounds.size(), |frame| {
if self.fingers.len() < 2 {
return;
}
diff --git a/examples/pane_grid/Cargo.toml b/examples/pane_grid/Cargo.toml
index dfd6dfa9..4c0bf072 100644
--- a/examples/pane_grid/Cargo.toml
+++ b/examples/pane_grid/Cargo.toml
@@ -6,6 +6,4 @@ edition = "2021"
publish = false
[dependencies]
-iced = { path = "../..", features = ["debug"] }
-iced_native = { path = "../../native" }
-iced_lazy = { path = "../../lazy" }
+iced = { path = "../..", features = ["debug", "lazy"] }
diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs
index c9f1376c..04896e20 100644
--- a/examples/pane_grid/src/main.rs
+++ b/examples/pane_grid/src/main.rs
@@ -1,14 +1,16 @@
use iced::alignment::{self, Alignment};
+use iced::event::{self, Event};
use iced::executor;
use iced::keyboard;
+use iced::subscription;
use iced::theme::{self, Theme};
use iced::widget::pane_grid::{self, PaneGrid};
-use iced::widget::{button, column, container, row, scrollable, text};
+use iced::widget::{
+ button, column, container, responsive, row, scrollable, text,
+};
use iced::{
Application, Color, Command, Element, Length, Settings, Size, Subscription,
};
-use iced_lazy::responsive;
-use iced_native::{event, subscription, Event};
pub fn main() -> iced::Result {
Example::run(Settings::default())
@@ -107,7 +109,7 @@ impl Application for Example {
pane,
target,
}) => {
- self.panes.swap(&pane, &target);
+ self.panes.drop(&pane, target);
}
Message::Dragged(_) => {}
Message::TogglePin(pane) => {
@@ -253,6 +255,7 @@ fn handle_hotkey(key_code: keyboard::KeyCode) -> Option<Message> {
}
}
+#[derive(Clone, Copy)]
struct Pane {
id: usize,
pub is_pinned: bool,
@@ -296,7 +299,7 @@ fn view_content<'a>(
)
]
.spacing(5)
- .max_width(150);
+ .max_width(160);
if total_panes > 1 && !is_pinned {
controls = controls.push(
diff --git a/examples/pick_list/src/main.rs b/examples/pick_list/src/main.rs
index 62a4ef88..21200621 100644
--- a/examples/pick_list/src/main.rs
+++ b/examples/pick_list/src/main.rs
@@ -61,8 +61,9 @@ impl Sandbox for Example {
}
}
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Language {
+ #[default]
Rust,
Elm,
Ruby,
@@ -84,12 +85,6 @@ impl Language {
];
}
-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!(
diff --git a/examples/qr_code/src/main.rs b/examples/qr_code/src/main.rs
index d8041745..867ebfa4 100644
--- a/examples/qr_code/src/main.rs
+++ b/examples/qr_code/src/main.rs
@@ -49,13 +49,11 @@ impl Sandbox for QRGenerator {
.size(70)
.style(Color::from([0.5, 0.5, 0.5]));
- let input = text_input(
- "Type the data of your QR code here...",
- &self.data,
- Message::DataChanged,
- )
- .size(30)
- .padding(15);
+ let input =
+ text_input("Type the data of your QR code here...", &self.data)
+ .on_input(Message::DataChanged)
+ .size(30)
+ .padding(15);
let mut content = column![title, input]
.width(700)
diff --git a/examples/screenshot/Cargo.toml b/examples/screenshot/Cargo.toml
new file mode 100644
index 00000000..b79300b7
--- /dev/null
+++ b/examples/screenshot/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "screenshot"
+version = "0.1.0"
+authors = ["Bingus <shankern@protonmail.com>"]
+edition = "2021"
+publish = false
+
+[dependencies]
+iced = { path = "../..", features = ["debug", "image", "advanced"] }
+image = { version = "0.24.6", features = ["png"]}
+env_logger = "0.10.0"
diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs
new file mode 100644
index 00000000..83824535
--- /dev/null
+++ b/examples/screenshot/src/main.rs
@@ -0,0 +1,320 @@
+use iced::alignment;
+use iced::keyboard::KeyCode;
+use iced::theme::{Button, Container};
+use iced::widget::{button, column, container, image, row, text, text_input};
+use iced::window::screenshot::{self, Screenshot};
+use iced::{
+ event, executor, keyboard, subscription, Alignment, Application, Command,
+ ContentFit, Element, Event, Length, Rectangle, Renderer, Subscription,
+ Theme,
+};
+
+use ::image as img;
+use ::image::ColorType;
+
+fn main() -> iced::Result {
+ env_logger::builder().format_timestamp(None).init();
+
+ Example::run(iced::Settings::default())
+}
+
+struct Example {
+ screenshot: Option<Screenshot>,
+ saved_png_path: Option<Result<String, PngError>>,
+ png_saving: bool,
+ crop_error: Option<screenshot::CropError>,
+ x_input_value: Option<u32>,
+ y_input_value: Option<u32>,
+ width_input_value: Option<u32>,
+ height_input_value: Option<u32>,
+}
+
+#[derive(Clone, Debug)]
+enum Message {
+ Crop,
+ Screenshot,
+ ScreenshotData(Screenshot),
+ Png,
+ PngSaved(Result<String, PngError>),
+ XInputChanged(Option<u32>),
+ YInputChanged(Option<u32>),
+ WidthInputChanged(Option<u32>),
+ HeightInputChanged(Option<u32>),
+}
+
+impl Application for Example {
+ type Executor = executor::Default;
+ type Message = Message;
+ type Theme = Theme;
+ type Flags = ();
+
+ fn new(_flags: Self::Flags) -> (Self, Command<Self::Message>) {
+ (
+ Example {
+ screenshot: None,
+ saved_png_path: None,
+ png_saving: false,
+ crop_error: None,
+ x_input_value: None,
+ y_input_value: None,
+ width_input_value: None,
+ height_input_value: None,
+ },
+ Command::none(),
+ )
+ }
+
+ fn title(&self) -> String {
+ "Screenshot".to_string()
+ }
+
+ fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
+ match message {
+ Message::Screenshot => {
+ return iced::window::screenshot(Message::ScreenshotData);
+ }
+ Message::ScreenshotData(screenshot) => {
+ self.screenshot = Some(screenshot);
+ }
+ Message::Png => {
+ if let Some(screenshot) = &self.screenshot {
+ self.png_saving = true;
+
+ return Command::perform(
+ save_to_png(screenshot.clone()),
+ Message::PngSaved,
+ );
+ }
+ }
+ Message::PngSaved(res) => {
+ self.png_saving = false;
+ self.saved_png_path = Some(res);
+ }
+ Message::XInputChanged(new_value) => {
+ self.x_input_value = new_value;
+ }
+ Message::YInputChanged(new_value) => {
+ self.y_input_value = new_value;
+ }
+ Message::WidthInputChanged(new_value) => {
+ self.width_input_value = new_value;
+ }
+ Message::HeightInputChanged(new_value) => {
+ self.height_input_value = new_value;
+ }
+ Message::Crop => {
+ if let Some(screenshot) = &self.screenshot {
+ let cropped = screenshot.crop(Rectangle::<u32> {
+ x: self.x_input_value.unwrap_or(0),
+ y: self.y_input_value.unwrap_or(0),
+ width: self.width_input_value.unwrap_or(0),
+ height: self.height_input_value.unwrap_or(0),
+ });
+
+ match cropped {
+ Ok(screenshot) => {
+ self.screenshot = Some(screenshot);
+ self.crop_error = None;
+ }
+ Err(crop_error) => {
+ self.crop_error = Some(crop_error);
+ }
+ }
+ }
+ }
+ }
+
+ Command::none()
+ }
+
+ fn view(&self) -> Element<'_, Self::Message, Renderer<Self::Theme>> {
+ let image: Element<Message> = if let Some(screenshot) = &self.screenshot
+ {
+ image(image::Handle::from_pixels(
+ screenshot.size.width,
+ screenshot.size.height,
+ screenshot.clone(),
+ ))
+ .content_fit(ContentFit::Contain)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .into()
+ } else {
+ text("Press the button to take a screenshot!").into()
+ };
+
+ let image = container(image)
+ .padding(10)
+ .style(Container::Box)
+ .width(Length::FillPortion(2))
+ .height(Length::Fill)
+ .center_x()
+ .center_y();
+
+ let crop_origin_controls = row![
+ text("X:")
+ .vertical_alignment(alignment::Vertical::Center)
+ .width(30),
+ numeric_input("0", self.x_input_value).map(Message::XInputChanged),
+ text("Y:")
+ .vertical_alignment(alignment::Vertical::Center)
+ .width(30),
+ numeric_input("0", self.y_input_value).map(Message::YInputChanged)
+ ]
+ .spacing(10)
+ .align_items(Alignment::Center);
+
+ let crop_dimension_controls = row![
+ text("W:")
+ .vertical_alignment(alignment::Vertical::Center)
+ .width(30),
+ numeric_input("0", self.width_input_value)
+ .map(Message::WidthInputChanged),
+ text("H:")
+ .vertical_alignment(alignment::Vertical::Center)
+ .width(30),
+ numeric_input("0", self.height_input_value)
+ .map(Message::HeightInputChanged)
+ ]
+ .spacing(10)
+ .align_items(Alignment::Center);
+
+ let mut crop_controls =
+ column![crop_origin_controls, crop_dimension_controls]
+ .spacing(10)
+ .align_items(Alignment::Center);
+
+ if let Some(crop_error) = &self.crop_error {
+ crop_controls = crop_controls
+ .push(text(format!("Crop error! \n{}", crop_error)));
+ }
+
+ let mut controls = column![
+ column![
+ button(centered_text("Screenshot!"))
+ .padding([10, 20, 10, 20])
+ .width(Length::Fill)
+ .on_press(Message::Screenshot),
+ if !self.png_saving {
+ button(centered_text("Save as png")).on_press_maybe(
+ self.screenshot.is_some().then(|| Message::Png),
+ )
+ } else {
+ button(centered_text("Saving...")).style(Button::Secondary)
+ }
+ .style(Button::Secondary)
+ .padding([10, 20, 10, 20])
+ .width(Length::Fill)
+ ]
+ .spacing(10),
+ column![
+ crop_controls,
+ button(centered_text("Crop"))
+ .on_press(Message::Crop)
+ .style(Button::Destructive)
+ .padding([10, 20, 10, 20])
+ .width(Length::Fill),
+ ]
+ .spacing(10)
+ .align_items(Alignment::Center),
+ ]
+ .spacing(40);
+
+ if let Some(png_result) = &self.saved_png_path {
+ let msg = match png_result {
+ Ok(path) => format!("Png saved as: {:?}!", path),
+ Err(msg) => {
+ format!("Png could not be saved due to:\n{:?}", msg)
+ }
+ };
+
+ controls = controls.push(text(msg));
+ }
+
+ let side_content = container(controls)
+ .align_x(alignment::Horizontal::Center)
+ .width(Length::FillPortion(1))
+ .height(Length::Fill)
+ .center_y()
+ .center_x();
+
+ let content = row![side_content, image]
+ .spacing(10)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .align_items(Alignment::Center);
+
+ container(content)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .padding(10)
+ .center_x()
+ .center_y()
+ .into()
+ }
+
+ fn subscription(&self) -> Subscription<Self::Message> {
+ subscription::events_with(|event, status| {
+ if let event::Status::Captured = status {
+ return None;
+ }
+
+ if let Event::Keyboard(keyboard::Event::KeyPressed {
+ key_code: KeyCode::F5,
+ ..
+ }) = event
+ {
+ Some(Message::Screenshot)
+ } else {
+ None
+ }
+ })
+ }
+}
+
+async fn save_to_png(screenshot: Screenshot) -> Result<String, PngError> {
+ let path = "screenshot.png".to_string();
+ img::save_buffer(
+ &path,
+ &screenshot.bytes,
+ screenshot.size.width,
+ screenshot.size.height,
+ ColorType::Rgba8,
+ )
+ .map(|_| path)
+ .map_err(|err| PngError(format!("{:?}", err)))
+}
+
+#[derive(Clone, Debug)]
+struct PngError(String);
+
+fn numeric_input(
+ placeholder: &str,
+ value: Option<u32>,
+) -> Element<'_, Option<u32>> {
+ text_input(
+ placeholder,
+ &value
+ .as_ref()
+ .map(ToString::to_string)
+ .unwrap_or_else(String::new),
+ )
+ .on_input(move |text| {
+ if text.is_empty() {
+ None
+ } else if let Ok(new_value) = text.parse() {
+ Some(new_value)
+ } else {
+ value
+ }
+ })
+ .width(40)
+ .into()
+}
+
+fn centered_text(content: &str) -> Element<'_, Message> {
+ text(content)
+ .width(Length::Fill)
+ .horizontal_alignment(alignment::Horizontal::Center)
+ .into()
+}
diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs
index a3ade54f..8c08d993 100644
--- a/examples/scrollable/src/main.rs
+++ b/examples/scrollable/src/main.rs
@@ -5,6 +5,7 @@ use iced::widget::{
};
use iced::{executor, theme, Alignment, Color};
use iced::{Application, Command, Element, Length, Settings, Theme};
+
use once_cell::sync::Lazy;
static SCROLLABLE_ID: Lazy<scrollable::Id> = Lazy::new(scrollable::Id::unique);
@@ -19,6 +20,7 @@ struct ScrollableDemo {
scrollbar_margin: u16,
scroller_width: u16,
current_scroll_offset: scrollable::RelativeOffset,
+ alignment: scrollable::Alignment,
}
#[derive(Debug, Clone, Eq, PartialEq, Copy)]
@@ -31,12 +33,13 @@ enum Direction {
#[derive(Debug, Clone)]
enum Message {
SwitchDirection(Direction),
+ AlignmentChanged(scrollable::Alignment),
ScrollbarWidthChanged(u16),
ScrollbarMarginChanged(u16),
ScrollerWidthChanged(u16),
ScrollToBeginning,
ScrollToEnd,
- Scrolled(scrollable::RelativeOffset),
+ Scrolled(scrollable::Viewport),
}
impl Application for ScrollableDemo {
@@ -53,6 +56,7 @@ impl Application for ScrollableDemo {
scrollbar_margin: 0,
scroller_width: 10,
current_scroll_offset: scrollable::RelativeOffset::START,
+ alignment: scrollable::Alignment::Start,
},
Command::none(),
)
@@ -73,6 +77,15 @@ impl Application for ScrollableDemo {
self.current_scroll_offset,
)
}
+ Message::AlignmentChanged(alignment) => {
+ self.current_scroll_offset = scrollable::RelativeOffset::START;
+ self.alignment = alignment;
+
+ scrollable::snap_to(
+ SCROLLABLE_ID.clone(),
+ self.current_scroll_offset,
+ )
+ }
Message::ScrollbarWidthChanged(width) => {
self.scrollbar_width = width;
@@ -104,8 +117,8 @@ impl Application for ScrollableDemo {
self.current_scroll_offset,
)
}
- Message::Scrolled(offset) => {
- self.current_scroll_offset = offset;
+ Message::Scrolled(viewport) => {
+ self.current_scroll_offset = viewport.relative_offset();
Command::none()
}
@@ -164,10 +177,33 @@ impl Application for ScrollableDemo {
.spacing(10)
.width(Length::Fill);
- let scroll_controls =
- row![scroll_slider_controls, scroll_orientation_controls]
- .spacing(20)
- .width(Length::Fill);
+ let scroll_alignment_controls = column(vec![
+ text("Scrollable alignment:").into(),
+ radio(
+ "Start",
+ scrollable::Alignment::Start,
+ Some(self.alignment),
+ Message::AlignmentChanged,
+ )
+ .into(),
+ radio(
+ "End",
+ scrollable::Alignment::End,
+ Some(self.alignment),
+ Message::AlignmentChanged,
+ )
+ .into(),
+ ])
+ .spacing(10)
+ .width(Length::Fill);
+
+ let scroll_controls = row![
+ scroll_slider_controls,
+ scroll_orientation_controls,
+ scroll_alignment_controls
+ ]
+ .spacing(20)
+ .width(Length::Fill);
let scroll_to_end_button = || {
button("Scroll to end")
@@ -199,12 +235,13 @@ impl Application for ScrollableDemo {
.spacing(40),
)
.height(Length::Fill)
- .vertical_scroll(
+ .direction(scrollable::Direction::Vertical(
Properties::new()
.width(self.scrollbar_width)
.margin(self.scrollbar_margin)
- .scroller_width(self.scroller_width),
- )
+ .scroller_width(self.scroller_width)
+ .alignment(self.alignment),
+ ))
.id(SCROLLABLE_ID.clone())
.on_scroll(Message::Scrolled),
Direction::Horizontal => scrollable(
@@ -223,12 +260,13 @@ impl Application for ScrollableDemo {
.spacing(40),
)
.height(Length::Fill)
- .horizontal_scroll(
+ .direction(scrollable::Direction::Horizontal(
Properties::new()
.width(self.scrollbar_width)
.margin(self.scrollbar_margin)
- .scroller_width(self.scroller_width),
- )
+ .scroller_width(self.scroller_width)
+ .alignment(self.alignment),
+ ))
.style(theme::Scrollable::custom(ScrollbarCustomStyle))
.id(SCROLLABLE_ID.clone())
.on_scroll(Message::Scrolled),
@@ -264,18 +302,18 @@ impl Application for ScrollableDemo {
.spacing(40),
)
.height(Length::Fill)
- .vertical_scroll(
- Properties::new()
- .width(self.scrollbar_width)
- .margin(self.scrollbar_margin)
- .scroller_width(self.scroller_width),
- )
- .horizontal_scroll(
- Properties::new()
+ .direction({
+ let properties = Properties::new()
.width(self.scrollbar_width)
.margin(self.scrollbar_margin)
- .scroller_width(self.scroller_width),
- )
+ .scroller_width(self.scroller_width)
+ .alignment(self.alignment);
+
+ scrollable::Direction::Both {
+ horizontal: properties,
+ vertical: properties,
+ }
+ })
.style(theme::Scrollable::Custom(Box::new(
ScrollbarCustomStyle,
)))
@@ -289,18 +327,13 @@ impl Application for ScrollableDemo {
}
Direction::Horizontal => {
progress_bar(0.0..=1.0, self.current_scroll_offset.x)
- .style(theme::ProgressBar::Custom(Box::new(
- ProgressBarCustomStyle,
- )))
+ .style(progress_bar_custom_style)
.into()
}
Direction::Multi => column![
progress_bar(0.0..=1.0, self.current_scroll_offset.y),
- progress_bar(0.0..=1.0, self.current_scroll_offset.x).style(
- theme::ProgressBar::Custom(Box::new(
- ProgressBarCustomStyle,
- ))
- )
+ progress_bar(0.0..=1.0, self.current_scroll_offset.x)
+ .style(progress_bar_custom_style)
]
.spacing(10)
.into(),
@@ -338,36 +371,44 @@ impl scrollable::StyleSheet for ScrollbarCustomStyle {
style.active(&theme::Scrollable::Default)
}
- fn hovered(&self, style: &Self::Style) -> Scrollbar {
- style.hovered(&theme::Scrollable::Default)
+ fn hovered(
+ &self,
+ style: &Self::Style,
+ is_mouse_over_scrollbar: bool,
+ ) -> Scrollbar {
+ style.hovered(&theme::Scrollable::Default, is_mouse_over_scrollbar)
}
- fn hovered_horizontal(&self, style: &Self::Style) -> Scrollbar {
- Scrollbar {
- background: style.active(&theme::Scrollable::default()).background,
- border_radius: 0.0,
- border_width: 0.0,
- border_color: Default::default(),
- scroller: Scroller {
- color: Color::from_rgb8(250, 85, 134),
- border_radius: 0.0,
+ fn hovered_horizontal(
+ &self,
+ style: &Self::Style,
+ is_mouse_over_scrollbar: bool,
+ ) -> Scrollbar {
+ if is_mouse_over_scrollbar {
+ Scrollbar {
+ background: style
+ .active(&theme::Scrollable::default())
+ .background,
+ border_radius: 0.0.into(),
border_width: 0.0,
border_color: Default::default(),
- },
+ scroller: Scroller {
+ color: Color::from_rgb8(250, 85, 134),
+ border_radius: 0.0.into(),
+ border_width: 0.0,
+ border_color: Default::default(),
+ },
+ }
+ } else {
+ self.active(style)
}
}
}
-struct ProgressBarCustomStyle;
-
-impl progress_bar::StyleSheet for ProgressBarCustomStyle {
- type Style = Theme;
-
- fn appearance(&self, style: &Self::Style) -> progress_bar::Appearance {
- progress_bar::Appearance {
- background: style.extended_palette().background.strong.color.into(),
- bar: Color::from_rgb8(250, 85, 134).into(),
- border_radius: 0.0,
- }
+fn progress_bar_custom_style(theme: &Theme) -> progress_bar::Appearance {
+ progress_bar::Appearance {
+ background: theme.extended_palette().background.strong.color.into(),
+ bar: Color::from_rgb8(250, 85, 134).into(),
+ border_radius: 0.0.into(),
}
}
diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs
index 1d25d171..885d3c63 100644
--- a/examples/sierpinski_triangle/src/main.rs
+++ b/examples/sierpinski_triangle/src/main.rs
@@ -1,12 +1,13 @@
use std::fmt::Debug;
use iced::executor;
+use iced::mouse;
use iced::widget::canvas::event::{self, Event};
use iced::widget::canvas::{self, Canvas};
use iced::widget::{column, row, slider, text};
use iced::{
- Application, Color, Command, Length, Point, Rectangle, Settings, Size,
- Theme,
+ Application, Color, Command, Length, Point, Rectangle, Renderer, Settings,
+ Size, Theme,
};
use rand::Rng;
@@ -105,14 +106,14 @@ impl canvas::Program<Message> for SierpinskiGraph {
_state: &mut Self::State,
event: Event,
bounds: Rectangle,
- cursor: canvas::Cursor,
+ cursor: mouse::Cursor,
) -> (event::Status, Option<Message>) {
- let cursor_position =
- if let Some(position) = cursor.position_in(&bounds) {
- position
- } else {
- return (event::Status::Ignored, None);
- };
+ let cursor_position = if let Some(position) = cursor.position_in(bounds)
+ {
+ position
+ } else {
+ return (event::Status::Ignored, None);
+ };
match event {
Event::Mouse(mouse_event) => {
@@ -134,11 +135,12 @@ impl canvas::Program<Message> for SierpinskiGraph {
fn draw(
&self,
_state: &Self::State,
+ renderer: &Renderer,
_theme: &Theme,
bounds: Rectangle,
- _cursor: canvas::Cursor,
+ _cursor: mouse::Cursor,
) -> Vec<canvas::Geometry> {
- let geom = self.cache.draw(bounds.size(), |frame| {
+ let geom = self.cache.draw(renderer, bounds.size(), |frame| {
frame.stroke(
&canvas::Path::rectangle(Point::ORIGIN, frame.size()),
canvas::Stroke::default(),
diff --git a/examples/solar_system/Cargo.toml b/examples/solar_system/Cargo.toml
index 835396b0..1a98a87e 100644
--- a/examples/solar_system/Cargo.toml
+++ b/examples/solar_system/Cargo.toml
@@ -7,4 +7,5 @@ publish = false
[dependencies]
iced = { path = "../..", features = ["canvas", "tokio", "debug"] }
+env_logger = "0.10.0"
rand = "0.8.3"
diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs
index 9a4ee754..58d06206 100644
--- a/examples/solar_system/src/main.rs
+++ b/examples/solar_system/src/main.rs
@@ -8,20 +8,23 @@
//! [1]: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Basic_animations#An_animated_solar_system
use iced::application;
use iced::executor;
+use iced::mouse;
use iced::theme::{self, Theme};
use iced::widget::canvas;
-use iced::widget::canvas::gradient::{self, Gradient};
+use iced::widget::canvas::gradient;
use iced::widget::canvas::stroke::{self, Stroke};
-use iced::widget::canvas::{Cursor, Path};
+use iced::widget::canvas::Path;
use iced::window;
use iced::{
- Application, Color, Command, Element, Length, Point, Rectangle, Settings,
- Size, Subscription, Vector,
+ Application, Color, Command, Element, Length, Point, Rectangle, Renderer,
+ Settings, Size, Subscription, Vector,
};
use std::time::Instant;
pub fn main() -> iced::Result {
+ env_logger::builder().format_timestamp(None).init();
+
SolarSystem::run(Settings {
antialiasing: true,
..Settings::default()
@@ -156,24 +159,26 @@ impl<Message> canvas::Program<Message> for State {
fn draw(
&self,
_state: &Self::State,
+ renderer: &Renderer,
_theme: &Theme,
bounds: Rectangle,
- _cursor: Cursor,
+ _cursor: mouse::Cursor,
) -> Vec<canvas::Geometry> {
use std::f32::consts::PI;
- let background = self.space_cache.draw(bounds.size(), |frame| {
- let stars = Path::new(|path| {
- for (p, size) in &self.stars {
- path.rectangle(*p, Size::new(*size, *size));
- }
- });
+ let background =
+ self.space_cache.draw(renderer, bounds.size(), |frame| {
+ let stars = Path::new(|path| {
+ for (p, size) in &self.stars {
+ path.rectangle(*p, Size::new(*size, *size));
+ }
+ });
- frame.translate(frame.center() - Point::ORIGIN);
- frame.fill(&stars, Color::WHITE);
- });
+ frame.translate(frame.center() - Point::ORIGIN);
+ frame.fill(&stars, Color::WHITE);
+ });
- let system = self.system_cache.draw(bounds.size(), |frame| {
+ let system = self.system_cache.draw(renderer, bounds.size(), |frame| {
let center = frame.center();
let sun = Path::circle(center, Self::SUN_RADIUS);
@@ -206,15 +211,12 @@ impl<Message> canvas::Program<Message> for State {
let earth = Path::circle(Point::ORIGIN, Self::EARTH_RADIUS);
- let earth_fill =
- Gradient::linear(gradient::Position::Absolute {
- start: Point::new(-Self::EARTH_RADIUS, 0.0),
- end: Point::new(Self::EARTH_RADIUS, 0.0),
- })
- .add_stop(0.2, Color::from_rgb(0.15, 0.50, 1.0))
- .add_stop(0.8, Color::from_rgb(0.0, 0.20, 0.47))
- .build()
- .expect("Build Earth fill gradient");
+ let earth_fill = gradient::Linear::new(
+ Point::new(-Self::EARTH_RADIUS, 0.0),
+ Point::new(Self::EARTH_RADIUS, 0.0),
+ )
+ .add_stop(0.2, Color::from_rgb(0.15, 0.50, 1.0))
+ .add_stop(0.8, Color::from_rgb(0.0, 0.20, 0.47));
frame.fill(&earth, earth_fill);
diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs
index 448c9792..f8a4c80a 100644
--- a/examples/styling/src/main.rs
+++ b/examples/styling/src/main.rs
@@ -90,13 +90,10 @@ impl Sandbox for Styling {
},
);
- let text_input = text_input(
- "Type something...",
- &self.input_value,
- Message::InputChanged,
- )
- .padding(10)
- .size(20);
+ let text_input = text_input("Type something...", &self.input_value)
+ .on_input(Message::InputChanged)
+ .padding(10)
+ .size(20);
let button = button("Submit")
.padding(10)
@@ -130,7 +127,9 @@ impl Sandbox for Styling {
let content = column![
choose_theme,
horizontal_rule(38),
- row![text_input, button].spacing(10),
+ row![text_input, button]
+ .spacing(10)
+ .align_items(Alignment::Center),
slider,
progress_bar,
row![
diff --git a/examples/toast/Cargo.toml b/examples/toast/Cargo.toml
index f1f986aa..f703572c 100644
--- a/examples/toast/Cargo.toml
+++ b/examples/toast/Cargo.toml
@@ -6,5 +6,4 @@ edition = "2021"
publish = false
[dependencies]
-iced = { path = "../..", features = [] }
-iced_native = { path = "../../native" }
+iced = { path = "../..", features = ["advanced"] }
diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs
index e74b3ee6..4282ddcf 100644
--- a/examples/toast/src/main.rs
+++ b/examples/toast/src/main.rs
@@ -1,10 +1,10 @@
+use iced::executor;
+use iced::keyboard;
+use iced::subscription::{self, Subscription};
use iced::widget::{
self, button, column, container, pick_list, row, slider, text, text_input,
};
-use iced::{
- executor, keyboard, subscription, Alignment, Application, Command, Element,
- Event, Length, Settings, Subscription,
-};
+use iced::{Alignment, Application, Command, Element, Event, Length, Settings};
use toast::{Status, Toast};
@@ -119,13 +119,15 @@ impl Application for App {
column![
subtitle(
"Title",
- text_input("", &self.editing.title, Message::Title)
+ text_input("", &self.editing.title)
+ .on_input(Message::Title)
.on_submit(Message::Add)
.into()
),
subtitle(
"Message",
- text_input("", &self.editing.body, Message::Body)
+ text_input("", &self.editing.body)
+ .on_input(Message::Body)
.on_submit(Message::Add)
.into()
),
@@ -176,17 +178,23 @@ mod toast {
use std::fmt;
use std::time::{Duration, Instant};
+ use iced::advanced;
+ use iced::advanced::layout::{self, Layout};
+ use iced::advanced::overlay;
+ use iced::advanced::renderer;
+ use iced::advanced::widget::{self, Operation, Tree};
+ use iced::advanced::{Clipboard, Shell, Widget};
+ use iced::event::{self, Event};
+ use iced::mouse;
use iced::theme;
use iced::widget::{
button, column, container, horizontal_rule, horizontal_space, row, text,
};
+ use iced::window;
use iced::{
Alignment, Element, Length, Point, Rectangle, Renderer, Size, Theme,
Vector,
};
- use iced_native::widget::{tree, Operation, Tree};
- use iced_native::{event, layout, mouse, overlay, renderer, window};
- use iced_native::{Clipboard, Event, Layout, Shell, Widget};
pub const DEFAULT_TIMEOUT: u64 = 5;
@@ -218,7 +226,7 @@ mod toast {
};
container::Appearance {
- background: pair.color.into(),
+ background: Some(pair.color.into()),
text_color: pair.text.into(),
..Default::default()
}
@@ -324,13 +332,13 @@ mod toast {
self.content.as_widget().layout(renderer, limits)
}
- fn tag(&self) -> tree::Tag {
+ fn tag(&self) -> widget::tree::Tag {
struct Marker(Vec<Instant>);
- iced_native::widget::tree::Tag::of::<Marker>()
+ widget::tree::Tag::of::<Marker>()
}
- fn state(&self) -> tree::State {
- iced_native::widget::tree::State::new(Vec::<Option<Instant>>::new())
+ fn state(&self) -> widget::tree::State {
+ widget::tree::State::new(Vec::<Option<Instant>>::new())
}
fn children(&self) -> Vec<Tree> {
@@ -388,7 +396,7 @@ mod toast {
state: &mut Tree,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
@@ -397,7 +405,7 @@ mod toast {
&mut state.children[0],
event,
layout,
- cursor_position,
+ cursor,
renderer,
clipboard,
shell,
@@ -411,7 +419,7 @@ mod toast {
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
) {
self.content.as_widget().draw(
@@ -420,7 +428,7 @@ mod toast {
theme,
style,
layout,
- cursor_position,
+ cursor,
viewport,
);
}
@@ -429,14 +437,14 @@ mod toast {
&self,
state: &Tree,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.content.as_widget().mouse_interaction(
&state.children[0],
layout,
- cursor_position,
+ cursor,
viewport,
renderer,
)
@@ -515,7 +523,7 @@ mod toast {
&mut self,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
@@ -564,7 +572,7 @@ mod toast {
state,
event.clone(),
layout,
- cursor_position,
+ cursor,
renderer,
clipboard,
&mut local_shell,
@@ -584,10 +592,10 @@ mod toast {
fn draw(
&self,
renderer: &mut Renderer,
- theme: &<Renderer as iced_native::Renderer>::Theme,
+ theme: &<Renderer as advanced::Renderer>::Theme,
style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
) {
let viewport = layout.bounds();
@@ -598,13 +606,7 @@ mod toast {
.zip(layout.children())
{
child.as_widget().draw(
- state,
- renderer,
- theme,
- style,
- layout,
- cursor_position,
- &viewport,
+ state, renderer, theme, style, layout, cursor, &viewport,
);
}
}
@@ -613,7 +615,7 @@ mod toast {
&mut self,
layout: Layout<'_>,
renderer: &Renderer,
- operation: &mut dyn iced_native::widget::Operation<Message>,
+ operation: &mut dyn widget::Operation<Message>,
) {
operation.container(None, &mut |operation| {
self.toasts
@@ -631,7 +633,7 @@ mod toast {
fn mouse_interaction(
&self,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
@@ -641,18 +643,19 @@ mod toast {
.zip(layout.children())
.map(|((child, state), layout)| {
child.as_widget().mouse_interaction(
- state,
- layout,
- cursor_position,
- viewport,
- renderer,
+ state, layout, cursor, viewport, renderer,
)
})
.max()
.unwrap_or_default()
}
- fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool {
+ fn is_over(
+ &self,
+ layout: Layout<'_>,
+ _renderer: &Renderer,
+ cursor_position: Point,
+ ) -> bool {
layout
.children()
.any(|layout| layout.bounds().contains(cursor_position))
diff --git a/examples/todos/fonts/icons.ttf b/examples/todos/fonts/icons.ttf
index 4498299d..7b65fd36 100644
--- a/examples/todos/fonts/icons.ttf
+++ b/examples/todos/fonts/icons.ttf
Binary files differ
diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs
index 6a87f58c..6ad7b4fb 100644
--- a/examples/todos/src/main.rs
+++ b/examples/todos/src/main.rs
@@ -1,5 +1,6 @@
use iced::alignment::{self, Alignment};
use iced::event::{self, Event};
+use iced::font::{self, Font};
use iced::keyboard::{self, KeyCode, Modifiers};
use iced::subscription;
use iced::theme::{self, Theme};
@@ -9,7 +10,7 @@ use iced::widget::{
};
use iced::window;
use iced::{Application, Element};
-use iced::{Color, Command, Font, Length, Settings, Subscription};
+use iced::{Color, Command, Length, Settings, Subscription};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
@@ -44,6 +45,7 @@ struct State {
#[derive(Debug, Clone)]
enum Message {
Loaded(Result<SavedState, LoadError>),
+ FontLoaded(Result<(), font::Error>),
Saved(Result<(), SaveError>),
InputChanged(String),
CreateTask,
@@ -62,7 +64,11 @@ impl Application for Todos {
fn new(_flags: ()) -> (Todos, Command<Message>) {
(
Todos::Loading,
- Command::perform(SavedState::load(), Message::Loaded),
+ Command::batch(vec![
+ font::load(include_bytes!("../fonts/icons.ttf").as_slice())
+ .map(Message::FontLoaded),
+ Command::perform(SavedState::load(), Message::Loaded),
+ ]),
)
}
@@ -204,15 +210,12 @@ impl Application for Todos {
.style(Color::from([0.5, 0.5, 0.5]))
.horizontal_alignment(alignment::Horizontal::Center);
- let input = text_input(
- "What needs to be done?",
- input_value,
- Message::InputChanged,
- )
- .id(INPUT_ID.clone())
- .padding(15)
- .size(30)
- .on_submit(Message::CreateTask);
+ let input = text_input("What needs to be done?", input_value)
+ .id(INPUT_ID.clone())
+ .on_input(Message::InputChanged)
+ .on_submit(Message::CreateTask)
+ .padding(15)
+ .size(30);
let controls = view_controls(tasks, *filter);
let filtered_tasks =
@@ -361,7 +364,8 @@ impl Task {
self.completed,
TaskMessage::Completed,
)
- .width(Length::Fill);
+ .width(Length::Fill)
+ .text_shaping(text::Shaping::Advanced);
row![
checkbox,
@@ -375,21 +379,23 @@ impl Task {
.into()
}
TaskState::Editing => {
- let text_input = text_input(
- "Describe your task...",
- &self.description,
- TaskMessage::DescriptionEdited,
- )
- .id(Self::text_input_id(i))
- .on_submit(TaskMessage::FinishEdition)
- .padding(10);
+ let text_input =
+ text_input("Describe your task...", &self.description)
+ .id(Self::text_input_id(i))
+ .on_input(TaskMessage::DescriptionEdited)
+ .on_submit(TaskMessage::FinishEdition)
+ .padding(10);
row![
text_input,
- button(row![delete_icon(), "Delete"].spacing(10))
- .on_press(TaskMessage::Delete)
- .padding(10)
- .style(theme::Button::Destructive)
+ button(
+ row![delete_icon(), "Delete"]
+ .spacing(10)
+ .align_items(Alignment::Center)
+ )
+ .on_press(TaskMessage::Delete)
+ .padding(10)
+ .style(theme::Button::Destructive)
]
.spacing(20)
.align_items(Alignment::Center)
@@ -403,7 +409,7 @@ fn view_controls(tasks: &[Task], current_filter: Filter) -> Element<Message> {
let tasks_left = tasks.iter().filter(|task| !task.completed).count();
let filter_button = |label, filter, current_filter| {
- let label = text(label).size(16);
+ let label = text(label);
let button = button(label).style(if filter == current_filter {
theme::Button::Primary
@@ -420,8 +426,7 @@ fn view_controls(tasks: &[Task], current_filter: Filter) -> Element<Message> {
tasks_left,
if tasks_left == 1 { "task" } else { "tasks" }
))
- .width(Length::Fill)
- .size(16),
+ .width(Length::Fill),
row![
filter_button("All", Filter::All, current_filter),
filter_button("Active", Filter::Active, current_filter),
@@ -435,19 +440,16 @@ fn view_controls(tasks: &[Task], current_filter: Filter) -> Element<Message> {
.into()
}
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(
+ Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize,
+)]
pub enum Filter {
+ #[default]
All,
Active,
Completed,
}
-impl Default for Filter {
- fn default() -> Self {
- Filter::All
- }
-}
-
impl Filter {
fn matches(&self, task: &Task) -> bool {
match self {
@@ -485,17 +487,13 @@ fn empty_message(message: &str) -> Element<'_, Message> {
}
// Fonts
-const ICONS: Font = Font::External {
- name: "Icons",
- bytes: include_bytes!("../../todos/fonts/icons.ttf"),
-};
+const ICONS: Font = Font::with_name("Iced-Todos-Icons");
fn icon(unicode: char) -> Text<'static> {
text(unicode.to_string())
.font(ICONS)
.width(20)
.horizontal_alignment(alignment::Horizontal::Center)
- .size(20)
}
fn edit_icon() -> Text<'static> {
diff --git a/examples/tour/Cargo.toml b/examples/tour/Cargo.toml
index 39e83671..48471f2d 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.8"
+env_logger = "0.10.0"
diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs
index de063d00..13bcd5ff 100644
--- a/examples/tour/src/main.rs
+++ b/examples/tour/src/main.rs
@@ -5,7 +5,7 @@ use iced::widget::{
scrollable, slider, text, text_input, toggler, vertical_space,
};
use iced::widget::{Button, Column, Container, Slider};
-use iced::{Color, Element, Length, Renderer, Sandbox, Settings};
+use iced::{Color, Element, Font, Length, Renderer, Sandbox, Settings};
pub fn main() -> iced::Result {
env_logger::init();
@@ -62,11 +62,8 @@ impl Sandbox for Tour {
controls = controls.push(horizontal_space(Length::Fill));
if steps.can_continue() {
- controls = controls.push(
- button("Next")
- .on_press(Message::NextPressed)
- .style(theme::Button::Primary),
- );
+ controls =
+ controls.push(button("Next").on_press(Message::NextPressed));
}
let content: Element<_> = column![
@@ -127,6 +124,7 @@ impl Steps {
Step::TextInput {
value: String::new(),
is_secure: false,
+ is_showing_icon: false,
},
Step::Debugger,
Step::End,
@@ -171,14 +169,32 @@ impl Steps {
enum Step {
Welcome,
- Slider { value: u8 },
- RowsAndColumns { layout: Layout, spacing: u16 },
- Text { size: u16, color: Color },
- Radio { selection: Option<Language> },
- Toggler { can_continue: bool },
- Image { width: u16 },
+ Slider {
+ value: u8,
+ },
+ RowsAndColumns {
+ layout: Layout,
+ spacing: u16,
+ },
+ Text {
+ size: u16,
+ color: Color,
+ },
+ Radio {
+ selection: Option<Language>,
+ },
+ Toggler {
+ can_continue: bool,
+ },
+ Image {
+ width: u16,
+ },
Scrollable,
- TextInput { value: String, is_secure: bool },
+ TextInput {
+ value: String,
+ is_secure: bool,
+ is_showing_icon: bool,
+ },
Debugger,
End,
}
@@ -194,6 +210,7 @@ pub enum StepMessage {
ImageWidthChanged(u16),
InputChanged(String),
ToggleSecureInput(bool),
+ ToggleTextInputIcon(bool),
DebugToggled(bool),
TogglerChanged(bool),
}
@@ -256,6 +273,14 @@ impl<'a> Step {
*can_continue = value;
}
}
+ StepMessage::ToggleTextInputIcon(toggle) => {
+ if let Step::TextInput {
+ is_showing_icon, ..
+ } = self
+ {
+ *is_showing_icon = toggle
+ }
+ }
};
}
@@ -303,9 +328,11 @@ impl<'a> Step {
Self::rows_and_columns(*layout, *spacing)
}
Step::Scrollable => Self::scrollable(),
- Step::TextInput { value, is_secure } => {
- Self::text_input(value, *is_secure)
- }
+ Step::TextInput {
+ value,
+ is_secure,
+ is_showing_icon,
+ } => Self::text_input(value, *is_secure, *is_showing_icon),
Step::Debugger => Self::debugger(debug),
Step::End => Self::end(),
}
@@ -530,14 +557,25 @@ impl<'a> Step {
)
}
- fn text_input(value: &str, is_secure: bool) -> Column<'a, StepMessage> {
- let text_input = text_input(
- "Type something to continue...",
- value,
- StepMessage::InputChanged,
- )
- .padding(10)
- .size(30);
+ fn text_input(
+ value: &str,
+ is_secure: bool,
+ is_showing_icon: bool,
+ ) -> Column<'a, StepMessage> {
+ let mut text_input = text_input("Type something to continue...", value)
+ .on_input(StepMessage::InputChanged)
+ .padding(10)
+ .size(30);
+
+ if is_showing_icon {
+ text_input = text_input.icon(text_input::Icon {
+ font: Font::default(),
+ code_point: '🚀',
+ size: Some(28.0),
+ spacing: 10.0,
+ side: text_input::Side::Right,
+ });
+ }
Self::container("Text input")
.push("Use a text input to ask for different kinds of information.")
@@ -551,6 +589,11 @@ impl<'a> Step {
is_secure,
StepMessage::ToggleSecureInput,
))
+ .push(checkbox(
+ "Show icon",
+ is_showing_icon,
+ StepMessage::ToggleTextInputIcon,
+ ))
.push(
"A text input produces a message every time it changes. It is \
very easy to keep track of its contents:",
diff --git a/examples/url_handler/Cargo.toml b/examples/url_handler/Cargo.toml
index 63c7ec27..4dcff92d 100644
--- a/examples/url_handler/Cargo.toml
+++ b/examples/url_handler/Cargo.toml
@@ -7,4 +7,3 @@ publish = false
[dependencies]
iced = { path = "../.." }
-iced_native = { path = "../../native" }
diff --git a/examples/url_handler/src/main.rs b/examples/url_handler/src/main.rs
index 3257b519..f63fa06a 100644
--- a/examples/url_handler/src/main.rs
+++ b/examples/url_handler/src/main.rs
@@ -1,12 +1,10 @@
+use iced::event::{Event, MacOS, PlatformSpecific};
use iced::executor;
+use iced::subscription;
use iced::widget::{container, text};
use iced::{
Application, Command, Element, Length, Settings, Subscription, Theme,
};
-use iced_native::{
- event::{MacOS, PlatformSpecific},
- Event,
-};
pub fn main() -> iced::Result {
App::run(Settings::default())
@@ -19,7 +17,7 @@ struct App {
#[derive(Debug, Clone)]
enum Message {
- EventOccurred(iced_native::Event),
+ EventOccurred(Event),
}
impl Application for App {
@@ -52,7 +50,7 @@ impl Application for App {
}
fn subscription(&self) -> Subscription<Message> {
- iced_native::subscription::events().map(Message::EventOccurred)
+ subscription::events().map(Message::EventOccurred)
}
fn view(&self) -> Element<Message> {
diff --git a/examples/websocket/Cargo.toml b/examples/websocket/Cargo.toml
index c25f067b..03b240c6 100644
--- a/examples/websocket/Cargo.toml
+++ b/examples/websocket/Cargo.toml
@@ -7,8 +7,6 @@ publish = false
[dependencies]
iced = { path = "../..", features = ["tokio", "debug"] }
-iced_native = { path = "../../native" }
-iced_futures = { path = "../../futures" }
once_cell = "1.15"
[dependencies.async-tungstenite]
diff --git a/examples/websocket/src/echo.rs b/examples/websocket/src/echo.rs
index e74768a6..281ed4bd 100644
--- a/examples/websocket/src/echo.rs
+++ b/examples/websocket/src/echo.rs
@@ -1,7 +1,7 @@
pub mod server;
-use iced_futures::futures;
-use iced_native::subscription::{self, Subscription};
+use iced::futures;
+use iced::subscription::{self, Subscription};
use futures::channel::mpsc;
use futures::sink::SinkExt;
@@ -13,63 +13,67 @@ use std::fmt;
pub fn connect() -> Subscription<Event> {
struct Connect;
- subscription::unfold(
+ subscription::channel(
std::any::TypeId::of::<Connect>(),
- State::Disconnected,
- |state| async move {
- match state {
- State::Disconnected => {
- const ECHO_SERVER: &str = "ws://localhost:3030";
-
- match async_tungstenite::tokio::connect_async(ECHO_SERVER)
+ 100,
+ |mut output| async move {
+ let mut state = State::Disconnected;
+
+ loop {
+ match &mut state {
+ State::Disconnected => {
+ const ECHO_SERVER: &str = "ws://127.0.0.1:3030";
+
+ match async_tungstenite::tokio::connect_async(
+ ECHO_SERVER,
+ )
.await
- {
- Ok((websocket, _)) => {
- let (sender, receiver) = mpsc::channel(100);
-
- (
- Some(Event::Connected(Connection(sender))),
- State::Connected(websocket, receiver),
- )
- }
- Err(_) => {
- tokio::time::sleep(
- tokio::time::Duration::from_secs(1),
- )
- .await;
+ {
+ Ok((websocket, _)) => {
+ let (sender, receiver) = mpsc::channel(100);
+
+ let _ = output
+ .send(Event::Connected(Connection(sender)))
+ .await;
- (Some(Event::Disconnected), State::Disconnected)
+ state = State::Connected(websocket, receiver);
+ }
+ Err(_) => {
+ tokio::time::sleep(
+ tokio::time::Duration::from_secs(1),
+ )
+ .await;
+
+ let _ = output.send(Event::Disconnected).await;
+ }
}
}
- }
- State::Connected(mut websocket, mut input) => {
- let mut fused_websocket = websocket.by_ref().fuse();
-
- futures::select! {
- received = fused_websocket.select_next_some() => {
- match received {
- Ok(tungstenite::Message::Text(message)) => {
- (
- Some(Event::MessageReceived(Message::User(message))),
- State::Connected(websocket, input)
- )
- }
- Ok(_) => {
- (None, State::Connected(websocket, input))
- }
- Err(_) => {
- (Some(Event::Disconnected), State::Disconnected)
+ State::Connected(websocket, input) => {
+ let mut fused_websocket = websocket.by_ref().fuse();
+
+ futures::select! {
+ received = fused_websocket.select_next_some() => {
+ match received {
+ Ok(tungstenite::Message::Text(message)) => {
+ let _ = output.send(Event::MessageReceived(Message::User(message))).await;
+ }
+ Err(_) => {
+ let _ = output.send(Event::Disconnected).await;
+
+ state = State::Disconnected;
+ }
+ Ok(_) => continue,
}
}
- }
- message = input.select_next_some() => {
- let result = websocket.send(tungstenite::Message::Text(message.to_string())).await;
+ message = input.select_next_some() => {
+ let result = websocket.send(tungstenite::Message::Text(message.to_string())).await;
+
+ if result.is_err() {
+ let _ = output.send(Event::Disconnected).await;
- if result.is_ok() {
- (None, State::Connected(websocket, input))
- } else {
- (Some(Event::Disconnected), State::Disconnected)
+ state = State::Disconnected;
+ }
}
}
}
diff --git a/examples/websocket/src/echo/server.rs b/examples/websocket/src/echo/server.rs
index dd234984..168a635e 100644
--- a/examples/websocket/src/echo/server.rs
+++ b/examples/websocket/src/echo/server.rs
@@ -1,4 +1,4 @@
-use iced_futures::futures;
+use iced::futures;
use futures::channel::mpsc;
use futures::{SinkExt, StreamExt};
diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs
index e617b8ce..920189f5 100644
--- a/examples/websocket/src/main.rs
+++ b/examples/websocket/src/main.rs
@@ -125,12 +125,9 @@ impl Application for WebSocket {
};
let new_message_input = {
- let mut input = text_input(
- "Type a message...",
- &self.new_message,
- Message::NewMessageChanged,
- )
- .padding(10);
+ let mut input = text_input("Type a message...", &self.new_message)
+ .on_input(Message::NewMessageChanged)
+ .padding(10);
let mut button = button(
text("Send")
diff --git a/futures/Cargo.toml b/futures/Cargo.toml
index e4d355ee..f636a304 100644
--- a/futures/Cargo.toml
+++ b/futures/Cargo.toml
@@ -16,6 +16,10 @@ thread-pool = ["futures/thread-pool"]
[dependencies]
log = "0.4"
+[dependencies.iced_core]
+version = "0.9"
+path = "../core"
+
[dependencies.futures]
version = "0.3"
diff --git a/futures/src/backend/native.rs b/futures/src/backend/native.rs
index 4199ad16..85af2c88 100644
--- a/futures/src/backend/native.rs
+++ b/futures/src/backend/native.rs
@@ -1,16 +1,12 @@
//! Backends that are only available in native platforms: Windows, macOS, or Linux.
-#[cfg_attr(docsrs, doc(cfg(feature = "tokio",)))]
#[cfg(feature = "tokio")]
pub mod tokio;
-#[cfg_attr(docsrs, doc(cfg(feature = "async-std",)))]
#[cfg(feature = "async-std")]
pub mod async_std;
-#[cfg_attr(docsrs, doc(cfg(feature = "smol",)))]
#[cfg(feature = "smol")]
pub mod smol;
-#[cfg_attr(docsrs, doc(cfg(feature = "thread-pool",)))]
#[cfg(feature = "thread-pool")]
pub mod thread_pool;
diff --git a/futures/src/backend/native/async_std.rs b/futures/src/backend/native/async_std.rs
index b324dbf1..52b0e914 100644
--- a/futures/src/backend/native/async_std.rs
+++ b/futures/src/backend/native/async_std.rs
@@ -18,28 +18,26 @@ impl crate::Executor for Executor {
pub mod time {
//! Listen and react to time.
+ use crate::core::Hasher;
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>(
+ pub fn every(
duration: std::time::Duration,
- ) -> Subscription<H, E, std::time::Instant> {
+ ) -> Subscription<std::time::Instant> {
Subscription::from_recipe(Every(duration))
}
#[derive(Debug)]
struct Every(std::time::Duration);
- impl<H, E> subscription::Recipe<H, E> for Every
- where
- H: std::hash::Hasher,
- {
+ impl subscription::Recipe for Every {
type Output = std::time::Instant;
- fn hash(&self, state: &mut H) {
+ fn hash(&self, state: &mut Hasher) {
use std::hash::Hash;
std::any::TypeId::of::<Self>().hash(state);
@@ -48,7 +46,7 @@ pub mod time {
fn stream(
self: Box<Self>,
- _input: futures::stream::BoxStream<'static, E>,
+ _input: subscription::EventStream,
) -> futures::stream::BoxStream<'static, Self::Output> {
use futures::stream::StreamExt;
diff --git a/futures/src/backend/native/smol.rs b/futures/src/backend/native/smol.rs
index d5201cde..00d13d35 100644
--- a/futures/src/backend/native/smol.rs
+++ b/futures/src/backend/native/smol.rs
@@ -1,9 +1,7 @@
//! A `smol` backend.
-
use futures::Future;
/// A `smol` executor.
-#[cfg_attr(docsrs, doc(cfg(feature = "smol")))]
#[derive(Debug)]
pub struct Executor;
@@ -19,28 +17,26 @@ impl crate::Executor for Executor {
pub mod time {
//! Listen and react to time.
+ use crate::core::Hasher;
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>(
+ pub fn every(
duration: std::time::Duration,
- ) -> Subscription<H, E, std::time::Instant> {
+ ) -> Subscription<std::time::Instant> {
Subscription::from_recipe(Every(duration))
}
#[derive(Debug)]
struct Every(std::time::Duration);
- impl<H, E> subscription::Recipe<H, E> for Every
- where
- H: std::hash::Hasher,
- {
+ impl subscription::Recipe for Every {
type Output = std::time::Instant;
- fn hash(&self, state: &mut H) {
+ fn hash(&self, state: &mut Hasher) {
use std::hash::Hash;
std::any::TypeId::of::<Self>().hash(state);
@@ -49,7 +45,7 @@ pub mod time {
fn stream(
self: Box<Self>,
- _input: futures::stream::BoxStream<'static, E>,
+ _input: subscription::EventStream,
) -> futures::stream::BoxStream<'static, Self::Output> {
use futures::stream::StreamExt;
diff --git a/futures/src/backend/native/thread_pool.rs b/futures/src/backend/native/thread_pool.rs
index da5d4b9b..c96f2682 100644
--- a/futures/src/backend/native/thread_pool.rs
+++ b/futures/src/backend/native/thread_pool.rs
@@ -2,7 +2,6 @@
use futures::Future;
/// A thread pool executor for futures.
-#[cfg_attr(docsrs, doc(cfg(feature = "thread-pool")))]
pub type Executor = futures::executor::ThreadPool;
impl crate::Executor for Executor {
diff --git a/futures/src/backend/native/tokio.rs b/futures/src/backend/native/tokio.rs
index dd818bd1..4698a105 100644
--- a/futures/src/backend/native/tokio.rs
+++ b/futures/src/backend/native/tokio.rs
@@ -22,28 +22,26 @@ impl crate::Executor for Executor {
pub mod time {
//! Listen and react to time.
+ use crate::core::Hasher;
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>(
+ pub fn every(
duration: std::time::Duration,
- ) -> Subscription<H, E, std::time::Instant> {
+ ) -> Subscription<std::time::Instant> {
Subscription::from_recipe(Every(duration))
}
#[derive(Debug)]
struct Every(std::time::Duration);
- impl<H, E> subscription::Recipe<H, E> for Every
- where
- H: std::hash::Hasher,
- {
+ impl subscription::Recipe for Every {
type Output = std::time::Instant;
- fn hash(&self, state: &mut H) {
+ fn hash(&self, state: &mut Hasher) {
use std::hash::Hash;
std::any::TypeId::of::<Self>().hash(state);
@@ -52,7 +50,7 @@ pub mod time {
fn stream(
self: Box<Self>,
- _input: futures::stream::BoxStream<'static, E>,
+ _input: subscription::EventStream,
) -> futures::stream::BoxStream<'static, Self::Output> {
use futures::stream::StreamExt;
diff --git a/futures/src/backend/wasm/wasm_bindgen.rs b/futures/src/backend/wasm/wasm_bindgen.rs
index b726501a..2666f1b4 100644
--- a/futures/src/backend/wasm/wasm_bindgen.rs
+++ b/futures/src/backend/wasm/wasm_bindgen.rs
@@ -16,6 +16,7 @@ impl crate::Executor for Executor {
pub mod time {
//! Listen and react to time.
+ use crate::core::Hasher;
use crate::subscription::{self, Subscription};
use crate::BoxStream;
@@ -23,22 +24,19 @@ pub mod time {
///
/// 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>(
+ pub fn every(
duration: std::time::Duration,
- ) -> Subscription<H, E, wasm_timer::Instant> {
+ ) -> Subscription<wasm_timer::Instant> {
Subscription::from_recipe(Every(duration))
}
#[derive(Debug)]
struct Every(std::time::Duration);
- impl<H, E> subscription::Recipe<H, E> for Every
- where
- H: std::hash::Hasher,
- {
+ impl subscription::Recipe for Every {
type Output = wasm_timer::Instant;
- fn hash(&self, state: &mut H) {
+ fn hash(&self, state: &mut Hasher) {
use std::hash::Hash;
std::any::TypeId::of::<Self>().hash(state);
@@ -47,7 +45,7 @@ pub mod time {
fn stream(
self: Box<Self>,
- _input: BoxStream<E>,
+ _input: subscription::EventStream,
) -> BoxStream<Self::Output> {
use futures::stream::StreamExt;
diff --git a/futures/src/command.rs b/futures/src/command.rs
deleted file mode 100644
index 3d1ec3f9..00000000
--- a/futures/src/command.rs
+++ /dev/null
@@ -1,70 +0,0 @@
-/// A set of asynchronous actions to be performed by some runtime.
-#[must_use = "`Command` must be returned to runtime to take effect"]
-#[derive(Debug)]
-pub struct Command<T>(Internal<T>);
-
-#[derive(Debug)]
-enum Internal<T> {
- None,
- Single(T),
- Batch(Vec<T>),
-}
-
-impl<T> Command<T> {
- /// Creates an empty [`Command`].
- ///
- /// In other words, a [`Command`] that does nothing.
- pub const fn none() -> Self {
- Self(Internal::None)
- }
-
- /// Creates a [`Command`] that performs a single action.
- pub const fn single(action: T) -> Self {
- Self(Internal::Single(action))
- }
-
- /// 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 {
- let mut batch = Vec::new();
-
- for Command(command) in commands {
- match command {
- Internal::None => {}
- Internal::Single(command) => batch.push(command),
- Internal::Batch(commands) => batch.extend(commands),
- }
- }
-
- Self(Internal::Batch(batch))
- }
-
- /// Applies a transformation to the result of a [`Command`].
- pub fn map<A>(self, f: impl Fn(T) -> A) -> Command<A>
- where
- T: 'static,
- {
- let Command(command) = self;
-
- match command {
- Internal::None => Command::none(),
- Internal::Single(action) => Command::single(f(action)),
- Internal::Batch(batch) => {
- Command(Internal::Batch(batch.into_iter().map(f).collect()))
- }
- }
- }
-
- /// Returns all of the actions of the [`Command`].
- pub fn actions(self) -> Vec<T> {
- let Command(command) = self;
-
- match command {
- Internal::None => Vec::new(),
- Internal::Single(action) => vec![action],
- Internal::Batch(batch) => batch,
- }
- }
-}
diff --git a/futures/src/lib.rs b/futures/src/lib.rs
index c0982db7..34d81e1e 100644
--- a/futures/src/lib.rs
+++ b/futures/src/lib.rs
@@ -16,10 +16,10 @@
)]
#![forbid(unsafe_code, rust_2018_idioms)]
#![allow(clippy::inherent_to_string, clippy::type_complexity)]
-#![cfg_attr(docsrs, feature(doc_cfg))]
+#![cfg_attr(docsrs, feature(doc_auto_cfg))]
pub use futures;
+pub use iced_core as core;
-mod command;
mod maybe_send;
mod runtime;
@@ -27,7 +27,6 @@ pub mod backend;
pub mod executor;
pub mod subscription;
-pub use command::Command;
pub use executor::Executor;
pub use maybe_send::MaybeSend;
pub use platform::*;
diff --git a/futures/src/runtime.rs b/futures/src/runtime.rs
index 24f9f241..2241a494 100644
--- a/futures/src/runtime.rs
+++ b/futures/src/runtime.rs
@@ -1,6 +1,7 @@
//! Run commands and keep track of subscriptions.
+use crate::core::event::{self, Event};
use crate::subscription;
-use crate::{BoxFuture, Executor, MaybeSend, Subscription};
+use crate::{BoxFuture, Executor, MaybeSend};
use futures::{channel::mpsc, Sink};
use std::marker::PhantomData;
@@ -12,18 +13,15 @@ use std::marker::PhantomData;
///
/// [`Command`]: crate::Command
#[derive(Debug)]
-pub struct Runtime<Hasher, Event, Executor, Sender, Message> {
+pub struct Runtime<Executor, Sender, Message> {
executor: Executor,
sender: Sender,
- subscriptions: subscription::Tracker<Hasher, Event>,
+ subscriptions: subscription::Tracker,
_message: PhantomData<Message>,
}
-impl<Hasher, Event, Executor, Sender, Message>
- Runtime<Hasher, Event, Executor, Sender, Message>
+impl<Executor, Sender, Message> Runtime<Executor, Sender, Message>
where
- Hasher: std::hash::Hasher + Default,
- Event: Send + Clone + 'static,
Executor: self::Executor,
Sender: Sink<Message, Error = mpsc::SendError>
+ Unpin
@@ -79,7 +77,9 @@ where
/// [`Tracker::update`]: subscription::Tracker::update
pub fn track(
&mut self,
- subscription: Subscription<Hasher, Event, Message>,
+ recipes: impl IntoIterator<
+ Item = Box<dyn subscription::Recipe<Output = Message>>,
+ >,
) {
let Runtime {
executor,
@@ -88,8 +88,9 @@ where
..
} = self;
- let futures = executor
- .enter(|| subscriptions.update(subscription, sender.clone()));
+ let futures = executor.enter(|| {
+ subscriptions.update(recipes.into_iter(), sender.clone())
+ });
for future in futures {
executor.spawn(future);
@@ -102,7 +103,7 @@ where
/// See [`Tracker::broadcast`] to learn more.
///
/// [`Tracker::broadcast`]: subscription::Tracker::broadcast
- pub fn broadcast(&mut self, event: Event) {
- self.subscriptions.broadcast(event);
+ pub fn broadcast(&mut self, event: Event, status: event::Status) {
+ self.subscriptions.broadcast(event, status);
}
}
diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs
index fe53fd7e..0642a924 100644
--- a/futures/src/subscription.rs
+++ b/futures/src/subscription.rs
@@ -3,7 +3,20 @@ mod tracker;
pub use tracker::Tracker;
-use crate::BoxStream;
+use crate::core::event::{self, Event};
+use crate::core::window;
+use crate::core::Hasher;
+use crate::futures::{Future, Stream};
+use crate::{BoxStream, MaybeSend};
+
+use futures::channel::mpsc;
+use futures::never::Never;
+use std::hash::Hash;
+
+/// A stream of runtime events.
+///
+/// It is the input of a [`Subscription`].
+pub type EventStream = BoxStream<(Event, event::Status)>;
/// A request to listen to external events.
///
@@ -16,19 +29,13 @@ use crate::BoxStream;
/// For instance, you can use a [`Subscription`] to listen to a WebSocket
/// connection, keyboard presses, mouse events, time ticks, etc.
///
-/// This type is normally aliased by runtimes with a specific `Event` and/or
-/// `Hasher`.
-///
/// [`Command`]: crate::Command
#[must_use = "`Subscription` must be returned to runtime to take effect"]
-pub struct Subscription<Hasher, Event, Output> {
- recipes: Vec<Box<dyn Recipe<Hasher, Event, Output = Output>>>,
+pub struct Subscription<Message> {
+ recipes: Vec<Box<dyn Recipe<Output = Message>>>,
}
-impl<H, E, O> Subscription<H, E, O>
-where
- H: std::hash::Hasher,
-{
+impl<Message> Subscription<Message> {
/// Returns an empty [`Subscription`] that will not produce any output.
pub fn none() -> Self {
Self {
@@ -38,7 +45,7 @@ where
/// Creates a [`Subscription`] from a [`Recipe`] describing it.
pub fn from_recipe(
- recipe: impl Recipe<H, E, Output = O> + 'static,
+ recipe: impl Recipe<Output = Message> + 'static,
) -> Self {
Self {
recipes: vec![Box::new(recipe)],
@@ -48,7 +55,7 @@ where
/// Batches all the provided subscriptions and returns the resulting
/// [`Subscription`].
pub fn batch(
- subscriptions: impl IntoIterator<Item = Subscription<H, E, O>>,
+ subscriptions: impl IntoIterator<Item = Subscription<Message>>,
) -> Self {
Self {
recipes: subscriptions
@@ -59,18 +66,16 @@ where
}
/// Returns the different recipes of the [`Subscription`].
- pub fn recipes(self) -> Vec<Box<dyn Recipe<H, E, Output = O>>> {
+ pub fn into_recipes(self) -> Vec<Box<dyn Recipe<Output = Message>>> {
self.recipes
}
/// Adds a value to the [`Subscription`] context.
///
/// The value will be part of the identity of a [`Subscription`].
- pub fn with<T>(mut self, value: T) -> Subscription<H, E, (T, O)>
+ pub fn with<T>(mut self, value: T) -> Subscription<(T, Message)>
where
- H: 'static,
- E: 'static,
- O: 'static,
+ Message: 'static,
T: std::hash::Hash + Clone + Send + Sync + 'static,
{
Subscription {
@@ -79,18 +84,16 @@ where
.drain(..)
.map(|recipe| {
Box::new(With::new(recipe, value.clone()))
- as Box<dyn Recipe<H, E, Output = (T, O)>>
+ as Box<dyn Recipe<Output = (T, Message)>>
})
.collect(),
}
}
/// Transforms the [`Subscription`] output with the given function.
- pub fn map<A>(mut self, f: fn(O) -> A) -> Subscription<H, E, A>
+ pub fn map<A>(mut self, f: fn(Message) -> A) -> Subscription<A>
where
- H: 'static,
- E: 'static,
- O: 'static,
+ Message: 'static,
A: 'static,
{
Subscription {
@@ -98,15 +101,14 @@ where
.recipes
.drain(..)
.map(|recipe| {
- Box::new(Map::new(recipe, f))
- as Box<dyn Recipe<H, E, Output = A>>
+ Box::new(Map::new(recipe, f)) as Box<dyn Recipe<Output = A>>
})
.collect(),
}
}
}
-impl<I, O, H> std::fmt::Debug for Subscription<I, O, H> {
+impl<Message> std::fmt::Debug for Subscription<Message> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Subscription").finish()
}
@@ -126,10 +128,10 @@ impl<I, O, H> std::fmt::Debug for Subscription<I, O, H> {
/// - [`stopwatch`], a watch with start/stop and reset buttons showcasing how
/// to listen to time.
///
-/// [examples]: https://github.com/iced-rs/iced/tree/0.8/examples
-/// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.8/examples/download_progress
-/// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.8/examples/stopwatch
-pub trait Recipe<Hasher: std::hash::Hasher, Event> {
+/// [examples]: https://github.com/iced-rs/iced/tree/0.9/examples
+/// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.9/examples/download_progress
+/// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.9/examples/stopwatch
+pub trait Recipe {
/// The events that will be produced by a [`Subscription`] with this
/// [`Recipe`].
type Output;
@@ -141,45 +143,33 @@ pub trait Recipe<Hasher: std::hash::Hasher, Event> {
/// Executes the [`Recipe`] and produces the stream of events of its
/// [`Subscription`].
- ///
- /// It receives some stream of generic events, which is normally defined by
- /// shells.
- fn stream(
- self: Box<Self>,
- input: BoxStream<Event>,
- ) -> BoxStream<Self::Output>;
+ fn stream(self: Box<Self>, input: EventStream) -> BoxStream<Self::Output>;
}
-struct Map<Hasher, Event, A, B> {
- recipe: Box<dyn Recipe<Hasher, Event, Output = A>>,
+struct Map<A, B> {
+ recipe: Box<dyn Recipe<Output = A>>,
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: fn(A) -> B,
- ) -> Self {
+impl<A, B> Map<A, B> {
+ fn new(recipe: Box<dyn Recipe<Output = A>>, mapper: fn(A) -> B) -> Self {
Map { recipe, mapper }
}
}
-impl<H, E, A, B> Recipe<H, E> for Map<H, E, A, B>
+impl<A, B> Recipe for Map<A, B>
where
A: 'static,
B: 'static,
- H: std::hash::Hasher,
{
type Output = B;
- fn hash(&self, state: &mut H) {
- use std::hash::Hash;
-
+ fn hash(&self, state: &mut Hasher) {
self.recipe.hash(state);
self.mapper.hash(state);
}
- fn stream(self: Box<Self>, input: BoxStream<E>) -> BoxStream<Self::Output> {
+ fn stream(self: Box<Self>, input: EventStream) -> BoxStream<Self::Output> {
use futures::StreamExt;
let mapper = self.mapper;
@@ -188,34 +178,31 @@ where
}
}
-struct With<Hasher, Event, A, B> {
- recipe: Box<dyn Recipe<Hasher, Event, Output = A>>,
+struct With<A, B> {
+ recipe: Box<dyn Recipe<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 {
+impl<A, B> With<A, B> {
+ fn new(recipe: Box<dyn Recipe<Output = A>>, value: B) -> Self {
With { recipe, value }
}
}
-impl<H, E, A, B> Recipe<H, E> for With<H, E, A, B>
+impl<A, B> Recipe for With<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;
-
+ fn hash(&self, state: &mut Hasher) {
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> {
+ fn stream(self: Box<Self>, input: EventStream) -> BoxStream<Self::Output> {
use futures::StreamExt;
let value = self.value;
@@ -227,3 +214,253 @@ where
)
}
}
+
+/// Returns a [`Subscription`] to all the ignored runtime events.
+///
+/// This subscription will notify your application of any [`Event`] that was
+/// not captured by any widget.
+pub fn events() -> Subscription<Event> {
+ events_with(|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 + MaybeSend,
+{
+ #[derive(Hash)]
+ struct EventsWith;
+
+ Subscription::from_recipe(Runner {
+ id: (EventsWith, f),
+ spawn: move |events| {
+ use futures::future;
+ use futures::stream::StreamExt;
+
+ events.filter_map(move |(event, status)| {
+ future::ready(match event {
+ Event::Window(window::Event::RedrawRequested(_)) => None,
+ _ => f(event, status),
+ })
+ })
+ },
+ })
+}
+
+/// Returns a [`Subscription`] that produces a message for every runtime event,
+/// including the redraw request events.
+///
+/// **Warning:** This [`Subscription`], if unfiltered, may produce messages in
+/// an infinite loop.
+pub fn raw_events<Message>(
+ f: fn(Event, event::Status) -> Option<Message>,
+) -> Subscription<Message>
+where
+ Message: 'static + MaybeSend,
+{
+ #[derive(Hash)]
+ struct RawEvents;
+
+ Subscription::from_recipe(Runner {
+ id: (RawEvents, f),
+ spawn: move |events| {
+ use futures::future;
+ use futures::stream::StreamExt;
+
+ events.filter_map(move |(event, status)| {
+ future::ready(f(event, status))
+ })
+ },
+ })
+}
+
+/// Returns a [`Subscription`] that will call the given function to create and
+/// asynchronously run the given [`Stream`].
+pub fn run<S, Message>(builder: fn() -> S) -> Subscription<Message>
+where
+ S: Stream<Item = Message> + MaybeSend + 'static,
+ Message: 'static,
+{
+ Subscription::from_recipe(Runner {
+ id: builder,
+ spawn: move |_| builder(),
+ })
+}
+
+/// Returns a [`Subscription`] that will create and asynchronously run the
+/// given [`Stream`].
+///
+/// The `id` will be used to uniquely identify the [`Subscription`].
+pub fn run_with_id<I, S, Message>(id: I, stream: S) -> Subscription<Message>
+where
+ I: Hash + 'static,
+ S: Stream<Item = Message> + MaybeSend + 'static,
+ Message: 'static,
+{
+ Subscription::from_recipe(Runner {
+ id,
+ spawn: move |_| stream,
+ })
+}
+
+/// Returns a [`Subscription`] that will create and asynchronously run a
+/// [`Stream`] that will call the provided closure to produce every `Message`.
+///
+/// The `id` will be used to uniquely identify the [`Subscription`].
+pub fn unfold<I, T, Fut, Message>(
+ id: I,
+ initial: T,
+ mut f: impl FnMut(T) -> Fut + MaybeSend + Sync + 'static,
+) -> Subscription<Message>
+where
+ I: Hash + 'static,
+ T: MaybeSend + 'static,
+ Fut: Future<Output = (Message, T)> + MaybeSend + 'static,
+ Message: 'static + MaybeSend,
+{
+ use futures::future::FutureExt;
+
+ run_with_id(
+ id,
+ futures::stream::unfold(initial, move |state| f(state).map(Some)),
+ )
+}
+
+/// Creates a [`Subscription`] that publishes the events sent from a [`Future`]
+/// to an [`mpsc::Sender`] with the given bounds.
+///
+/// # Creating an asynchronous worker with bidirectional communication
+/// You can leverage this helper to create a [`Subscription`] that spawns
+/// an asynchronous worker in the background and establish a channel of
+/// communication with an `iced` application.
+///
+/// You can achieve this by creating an `mpsc` channel inside the closure
+/// and returning the `Sender` as a `Message` for the `Application`:
+///
+/// ```
+/// use iced_futures::subscription::{self, Subscription};
+/// use iced_futures::futures::channel::mpsc;
+/// use iced_futures::futures::sink::SinkExt;
+///
+/// pub enum Event {
+/// Ready(mpsc::Sender<Input>),
+/// WorkFinished,
+/// // ...
+/// }
+///
+/// enum Input {
+/// DoSomeWork,
+/// // ...
+/// }
+///
+/// enum State {
+/// Starting,
+/// Ready(mpsc::Receiver<Input>),
+/// }
+///
+/// fn some_worker() -> Subscription<Event> {
+/// struct SomeWorker;
+///
+/// subscription::channel(std::any::TypeId::of::<SomeWorker>(), 100, |mut output| async move {
+/// let mut state = State::Starting;
+///
+/// loop {
+/// match &mut state {
+/// State::Starting => {
+/// // Create channel
+/// let (sender, receiver) = mpsc::channel(100);
+///
+/// // Send the sender back to the application
+/// output.send(Event::Ready(sender)).await;
+///
+/// // We are ready to receive messages
+/// state = State::Ready(receiver);
+/// }
+/// State::Ready(receiver) => {
+/// use iced_futures::futures::StreamExt;
+///
+/// // Read next input sent from `Application`
+/// let input = receiver.select_next_some().await;
+///
+/// match input {
+/// Input::DoSomeWork => {
+/// // Do some async work...
+///
+/// // Finally, we can optionally produce a message to tell the
+/// // `Application` the work is done
+/// output.send(Event::WorkFinished).await;
+/// }
+/// }
+/// }
+/// }
+/// }
+/// })
+/// }
+/// ```
+///
+/// Check out the [`websocket`] example, which showcases this pattern to maintain a WebSocket
+/// connection open.
+///
+/// [`websocket`]: https://github.com/iced-rs/iced/tree/0.9/examples/websocket
+pub fn channel<I, Fut, Message>(
+ id: I,
+ size: usize,
+ f: impl FnOnce(mpsc::Sender<Message>) -> Fut + MaybeSend + 'static,
+) -> Subscription<Message>
+where
+ I: Hash + 'static,
+ Fut: Future<Output = Never> + MaybeSend + 'static,
+ Message: 'static + MaybeSend,
+{
+ use futures::stream::{self, StreamExt};
+
+ Subscription::from_recipe(Runner {
+ id,
+ spawn: move |_| {
+ let (sender, receiver) = mpsc::channel(size);
+
+ let runner = stream::once(f(sender)).map(|_| unreachable!());
+
+ stream::select(receiver, runner)
+ },
+ })
+}
+
+struct Runner<I, F, S, Message>
+where
+ F: FnOnce(EventStream) -> S,
+ S: Stream<Item = Message>,
+{
+ id: I,
+ spawn: F,
+}
+
+impl<I, S, F, Message> Recipe for Runner<I, F, S, Message>
+where
+ I: Hash + 'static,
+ F: FnOnce(EventStream) -> S,
+ S: Stream<Item = Message> + MaybeSend + 'static,
+{
+ type Output = Message;
+
+ fn hash(&self, state: &mut Hasher) {
+ std::any::TypeId::of::<I>().hash(state);
+ self.id.hash(state);
+ }
+
+ fn stream(self: Box<Self>, input: EventStream) -> BoxStream<Self::Output> {
+ crate::boxed_stream((self.spawn)(input))
+ }
+}
diff --git a/futures/src/subscription/tracker.rs b/futures/src/subscription/tracker.rs
index 9fe110b0..ae71cd25 100644
--- a/futures/src/subscription/tracker.rs
+++ b/futures/src/subscription/tracker.rs
@@ -1,38 +1,35 @@
-use crate::{BoxFuture, MaybeSend, Subscription};
+use crate::core::event::{self, Event};
+use crate::core::Hasher;
+use crate::subscription::Recipe;
+use crate::{BoxFuture, MaybeSend};
-use futures::{
- channel::mpsc,
- sink::{Sink, SinkExt},
-};
-use std::{collections::HashMap, marker::PhantomData};
+use futures::channel::mpsc;
+use futures::sink::{Sink, SinkExt};
+
+use std::collections::HashMap;
+use std::hash::Hasher as _;
/// A registry of subscription streams.
///
/// If you have an application that continuously returns a [`Subscription`],
/// you can use a [`Tracker`] to keep track of the different recipes and keep
/// its executions alive.
-#[derive(Debug)]
-pub struct Tracker<Hasher, Event> {
- subscriptions: HashMap<u64, Execution<Event>>,
- _hasher: PhantomData<Hasher>,
+#[derive(Debug, Default)]
+pub struct Tracker {
+ subscriptions: HashMap<u64, Execution>,
}
#[derive(Debug)]
-pub struct Execution<Event> {
+pub struct Execution {
_cancel: futures::channel::oneshot::Sender<()>,
- listener: Option<futures::channel::mpsc::Sender<Event>>,
+ listener: Option<futures::channel::mpsc::Sender<(Event, event::Status)>>,
}
-impl<Hasher, Event> Tracker<Hasher, Event>
-where
- Hasher: std::hash::Hasher + Default,
- Event: 'static + Send + Clone,
-{
+impl Tracker {
/// Creates a new empty [`Tracker`].
pub fn new() -> Self {
Self {
subscriptions: HashMap::new(),
- _hasher: PhantomData,
}
}
@@ -56,7 +53,7 @@ where
/// [`Recipe`]: crate::subscription::Recipe
pub fn update<Message, Receiver>(
&mut self,
- subscription: Subscription<Hasher, Event, Message>,
+ recipes: impl Iterator<Item = Box<dyn Recipe<Output = Message>>>,
receiver: Receiver,
) -> Vec<BoxFuture<()>>
where
@@ -70,8 +67,6 @@ where
use futures::stream::StreamExt;
let mut futures: Vec<BoxFuture<()>> = Vec::new();
-
- let recipes = subscription.recipes();
let mut alive = std::collections::HashSet::new();
for recipe in recipes {
@@ -142,12 +137,12 @@ where
/// currently open.
///
/// [`Recipe::stream`]: crate::subscription::Recipe::stream
- pub fn broadcast(&mut self, event: Event) {
+ pub fn broadcast(&mut self, event: Event, status: event::Status) {
self.subscriptions
.values_mut()
.filter_map(|connection| connection.listener.as_mut())
.for_each(|listener| {
- if let Err(error) = listener.try_send(event.clone()) {
+ if let Err(error) = listener.try_send((event.clone(), status)) {
log::warn!(
"Error sending event to subscription: {:?}",
error
@@ -156,13 +151,3 @@ where
});
}
}
-
-impl<Hasher, Event> Default for Tracker<Hasher, Event>
-where
- Hasher: std::hash::Hasher + Default,
- Event: 'static + Send + Clone,
-{
- fn default() -> Self {
- Self::new()
- }
-}
diff --git a/glow/Cargo.toml b/glow/Cargo.toml
deleted file mode 100644
index 1a848ab7..00000000
--- a/glow/Cargo.toml
+++ /dev/null
@@ -1,51 +0,0 @@
-[package]
-name = "iced_glow"
-version = "0.7.0"
-authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
-edition = "2021"
-description = "A glow renderer for iced"
-license = "MIT AND OFL-1.1"
-repository = "https://github.com/iced-rs/iced"
-
-[features]
-svg = ["iced_graphics/svg"]
-image = ["iced_graphics/image"]
-png = ["iced_graphics/png"]
-jpeg = ["iced_graphics/jpeg"]
-jpeg_rayon = ["iced_graphics/jpeg_rayon"]
-gif = ["iced_graphics/gif"]
-webp = ["iced_graphics/webp"]
-pnm = ["iced_graphics/pnm"]
-ico = ["iced_graphics/ico"]
-bmp = ["iced_graphics/bmp"]
-hdr = ["iced_graphics/hdr"]
-dds = ["iced_graphics/dds"]
-farbfeld = ["iced_graphics/farbfeld"]
-canvas = ["iced_graphics/canvas"]
-qr_code = ["iced_graphics/qr_code"]
-default_system_font = ["iced_graphics/font-source"]
-
-[dependencies]
-glow = "0.11.1"
-glow_glyph = "0.5.0"
-glyph_brush = "0.7"
-euclid = "0.22"
-bytemuck = "1.4"
-log = "0.4"
-
-[dependencies.iced_native]
-version = "0.9"
-path = "../native"
-
-[dependencies.iced_graphics]
-version = "0.7"
-path = "../graphics"
-features = ["font-fallback", "font-icons", "opengl"]
-
-[dependencies.tracing]
-version = "0.1.6"
-optional = true
-
-[package.metadata.docs.rs]
-rustdoc-args = ["--cfg", "docsrs"]
-all-features = true
diff --git a/glow/README.md b/glow/README.md
deleted file mode 100644
index 95c9d62a..00000000
--- a/glow/README.md
+++ /dev/null
@@ -1,51 +0,0 @@
-# `iced_glow`
-[![Documentation](https://docs.rs/iced_glow/badge.svg)][documentation]
-[![Crates.io](https://img.shields.io/crates/v/iced_glow.svg)](https://crates.io/crates/iced_glow)
-[![License](https://img.shields.io/crates/l/iced_glow.svg)](https://github.com/iced-rs/iced/blob/master/LICENSE)
-[![Discord Server](https://img.shields.io/discord/628993209984614400?label=&labelColor=6A7EC2&logo=discord&logoColor=ffffff&color=7389D8)](https://discord.gg/3xZJ65GAhd)
-
-`iced_glow` is a [`glow`] renderer for [`iced_native`]. This renderer supports OpenGL 3.0+ and OpenGL ES 2.0.
-
-This renderer is mostly used as a fallback for hardware that doesn't support [`wgpu`] (Vulkan, Metal or DX12).
-
-Currently, `iced_glow` supports the following primitives:
-- Text, which is rendered using [`glow_glyph`]. No shaping at all.
-- Quads or rectangles, with rounded borders and a solid background color.
-- Clip areas, useful to implement scrollables or hide overflowing content.
-- Meshes of triangles, useful to draw geometry freely.
-
-<p align="center">
- <img alt="The native target" src="../docs/graphs/native.png" width="80%">
-</p>
-
-[documentation]: https://docs.rs/iced_glow
-[`iced_native`]: ../native
-[`glow`]: https://github.com/grovesNL/glow
-[`wgpu`]: https://github.com/gfx-rs/wgpu
-[`glow_glyph`]: https://github.com/hecrj/glow_glyph
-
-## Installation
-Add `iced_glow` as a dependency in your `Cargo.toml`:
-
-```toml
-iced_glow = "0.7"
-```
-
-__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/iced-rs/iced/releases
-
-## Current limitations
-
-The current implementation is quite naive, it uses:
-
-- A different pipeline/shader for each primitive
-- A very simplistic layer model: every `Clip` primitive will generate new layers
-- _Many_ render passes instead of preparing everything upfront
-- A glyph cache that is trimmed incorrectly when there are multiple layers (a [`glyph_brush`] limitation)
-
-Some of these issues are already being worked on! If you want to help, [get in touch!]
-
-[get in touch!]: ../CONTRIBUTING.md
-[`glyph_brush`]: https://github.com/alexheretic/glyph-brush
diff --git a/glow/src/backend.rs b/glow/src/backend.rs
deleted file mode 100644
index 36a34eda..00000000
--- a/glow/src/backend.rs
+++ /dev/null
@@ -1,280 +0,0 @@
-#[cfg(any(feature = "image", feature = "svg"))]
-use crate::image;
-use crate::quad;
-use crate::text;
-use crate::{program, triangle};
-use crate::{Settings, Transformation, Viewport};
-
-use iced_graphics::backend;
-use iced_graphics::font;
-use iced_graphics::{Layer, Primitive};
-use iced_native::alignment;
-use iced_native::{Font, Size};
-
-/// A [`glow`] graphics backend for [`iced`].
-///
-/// [`glow`]: https://github.com/grovesNL/glow
-/// [`iced`]: https://github.com/iced-rs/iced
-#[derive(Debug)]
-pub struct Backend {
- #[cfg(any(feature = "image", feature = "svg"))]
- image_pipeline: image::Pipeline,
- quad_pipeline: quad::Pipeline,
- text_pipeline: text::Pipeline,
- triangle_pipeline: triangle::Pipeline,
- default_text_size: f32,
-}
-
-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,
- settings.text_multithreading,
- );
-
- let shader_version = program::Version::new(gl);
-
- #[cfg(any(feature = "image", feature = "svg"))]
- let image_pipeline = image::Pipeline::new(gl, &shader_version);
- let quad_pipeline = quad::Pipeline::new(gl, &shader_version);
- let triangle_pipeline = triangle::Pipeline::new(gl, &shader_version);
-
- Self {
- #[cfg(any(feature = "image", feature = "svg"))]
- image_pipeline,
- 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 present<T: AsRef<str>>(
- &mut self,
- gl: &glow::Context,
- primitives: &[Primitive],
- viewport: &Viewport,
- overlay_text: &[T],
- ) {
- let viewport_size = viewport.physical_size();
- let scale_factor = viewport.scale_factor() as f32;
- let projection = viewport.projection();
-
- let mut layers = Layer::generate(primitives, viewport);
- layers.push(Layer::overlay(overlay_text, viewport));
-
- for layer in layers {
- self.flush(
- gl,
- scale_factor,
- projection,
- &layer,
- viewport_size.height,
- );
- }
-
- #[cfg(any(feature = "image", feature = "svg"))]
- self.image_pipeline.trim_cache(gl);
- }
-
- 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();
-
- if bounds.width < 1 || bounds.height < 1 {
- return;
- }
-
- 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(
- &layer.meshes,
- gl,
- target_height,
- scaled,
- scale_factor,
- );
- }
-
- #[cfg(any(feature = "image", feature = "svg"))]
- if !layer.images.is_empty() {
- let scaled = transformation
- * Transformation::scale(scale_factor, scale_factor);
-
- self.image_pipeline.draw(
- gl,
- target_height,
- scaled,
- scale_factor,
- &layer.images,
- bounds,
- );
- }
-
- 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 {
- alignment::Horizontal::Left => {
- glow_glyph::HorizontalAlign::Left
- }
- alignment::Horizontal::Center => {
- glow_glyph::HorizontalAlign::Center
- }
- alignment::Horizontal::Right => {
- glow_glyph::HorizontalAlign::Right
- }
- })
- .v_align(match text.vertical_alignment {
- alignment::Vertical::Top => {
- glow_glyph::VerticalAlign::Top
- }
- alignment::Vertical::Center => {
- glow_glyph::VerticalAlign::Center
- }
- alignment::Vertical::Bottom => {
- glow_glyph::VerticalAlign::Bottom
- }
- }),
- };
-
- 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) -> f32 {
- 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)
- }
-
- fn hit_test(
- &self,
- contents: &str,
- size: f32,
- font: Font,
- bounds: Size,
- point: iced_native::Point,
- nearest_only: bool,
- ) -> Option<text::Hit> {
- self.text_pipeline.hit_test(
- contents,
- size,
- font,
- bounds,
- point,
- nearest_only,
- )
- }
-}
-
-#[cfg(feature = "image")]
-impl backend::Image for Backend {
- fn dimensions(&self, handle: &iced_native::image::Handle) -> Size<u32> {
- self.image_pipeline.dimensions(handle)
- }
-}
-
-#[cfg(feature = "svg")]
-impl backend::Svg for Backend {
- fn viewport_dimensions(
- &self,
- handle: &iced_native::svg::Handle,
- ) -> Size<u32> {
- self.image_pipeline.viewport_dimensions(handle)
- }
-}
diff --git a/glow/src/image.rs b/glow/src/image.rs
deleted file mode 100644
index d3a25b5b..00000000
--- a/glow/src/image.rs
+++ /dev/null
@@ -1,254 +0,0 @@
-mod storage;
-
-use storage::Storage;
-
-pub use iced_graphics::triangle::{Mesh2D, Vertex2D};
-
-use crate::program::{self, Shader};
-use crate::Transformation;
-
-#[cfg(feature = "image")]
-use iced_graphics::image::raster;
-
-#[cfg(feature = "svg")]
-use iced_graphics::image::vector;
-
-use iced_graphics::layer;
-use iced_graphics::Rectangle;
-use iced_graphics::Size;
-
-use glow::HasContext;
-
-use std::cell::RefCell;
-
-#[cfg(feature = "tracing")]
-use tracing::info_span;
-
-#[derive(Debug)]
-pub(crate) struct Pipeline {
- program: <glow::Context as HasContext>::Program,
- vertex_array: <glow::Context as HasContext>::VertexArray,
- vertex_buffer: <glow::Context as HasContext>::Buffer,
- transform_location: <glow::Context as HasContext>::UniformLocation,
- storage: Storage,
- #[cfg(feature = "image")]
- raster_cache: RefCell<raster::Cache<Storage>>,
- #[cfg(feature = "svg")]
- vector_cache: RefCell<vector::Cache<Storage>>,
-}
-
-impl Pipeline {
- pub fn new(
- gl: &glow::Context,
- shader_version: &program::Version,
- ) -> Pipeline {
- let program = unsafe {
- let vertex_shader = Shader::vertex(
- gl,
- shader_version,
- include_str!("shader/common/image.vert"),
- );
- let fragment_shader = Shader::fragment(
- gl,
- shader_version,
- include_str!("shader/common/image.frag"),
- );
-
- program::create(
- gl,
- &[vertex_shader, fragment_shader],
- &[(0, "i_Position")],
- )
- };
-
- 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_buffer =
- unsafe { gl.create_buffer().expect("Create vertex buffer") };
- let vertex_array =
- unsafe { gl.create_vertex_array().expect("Create vertex array") };
-
- unsafe {
- gl.bind_vertex_array(Some(vertex_array));
- gl.bind_buffer(glow::ARRAY_BUFFER, Some(vertex_buffer));
-
- let vertices = &[0u8, 0, 1, 0, 0, 1, 1, 1];
- gl.buffer_data_size(
- glow::ARRAY_BUFFER,
- vertices.len() as i32,
- glow::STATIC_DRAW,
- );
- gl.buffer_sub_data_u8_slice(
- glow::ARRAY_BUFFER,
- 0,
- bytemuck::cast_slice(vertices),
- );
-
- gl.enable_vertex_attrib_array(0);
- gl.vertex_attrib_pointer_f32(
- 0,
- 2,
- glow::UNSIGNED_BYTE,
- false,
- 0,
- 0,
- );
-
- gl.bind_buffer(glow::ARRAY_BUFFER, None);
- gl.bind_vertex_array(None);
- }
-
- Pipeline {
- program,
- vertex_array,
- vertex_buffer,
- transform_location,
- storage: Storage::default(),
- #[cfg(feature = "image")]
- raster_cache: RefCell::new(raster::Cache::default()),
- #[cfg(feature = "svg")]
- vector_cache: RefCell::new(vector::Cache::default()),
- }
- }
-
- #[cfg(feature = "image")]
- pub fn dimensions(&self, handle: &iced_native::image::Handle) -> Size<u32> {
- self.raster_cache.borrow_mut().load(handle).dimensions()
- }
-
- #[cfg(feature = "svg")]
- pub fn viewport_dimensions(
- &self,
- handle: &iced_native::svg::Handle,
- ) -> Size<u32> {
- let mut cache = self.vector_cache.borrow_mut();
- let svg = cache.load(handle);
-
- svg.viewport_dimensions()
- }
-
- pub fn draw(
- &mut self,
- mut gl: &glow::Context,
- target_height: u32,
- transformation: Transformation,
- _scale_factor: f32,
- images: &[layer::Image],
- layer_bounds: Rectangle<u32>,
- ) {
- #[cfg(feature = "tracing")]
- let _ = info_span!("Glow::Image", "DRAW").entered();
-
- unsafe {
- gl.use_program(Some(self.program));
- gl.bind_vertex_array(Some(self.vertex_array));
- gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.vertex_buffer));
- gl.enable(glow::SCISSOR_TEST);
- }
-
- #[cfg(feature = "image")]
- let mut raster_cache = self.raster_cache.borrow_mut();
-
- #[cfg(feature = "svg")]
- let mut vector_cache = self.vector_cache.borrow_mut();
-
- for image in images {
- let (entry, bounds) = match &image {
- #[cfg(feature = "image")]
- layer::Image::Raster { handle, bounds } => (
- raster_cache.upload(handle, &mut gl, &mut self.storage),
- bounds,
- ),
- #[cfg(not(feature = "image"))]
- layer::Image::Raster { handle: _, bounds } => (None, bounds),
-
- #[cfg(feature = "svg")]
- layer::Image::Vector {
- handle,
- color,
- bounds,
- } => {
- let size = [bounds.width, bounds.height];
- (
- vector_cache.upload(
- handle,
- *color,
- size,
- _scale_factor,
- &mut gl,
- &mut self.storage,
- ),
- bounds,
- )
- }
-
- #[cfg(not(feature = "svg"))]
- layer::Image::Vector { bounds, .. } => (None, bounds),
- };
-
- unsafe {
- gl.scissor(
- layer_bounds.x as i32,
- (target_height - (layer_bounds.y + layer_bounds.height))
- as i32,
- layer_bounds.width as i32,
- layer_bounds.height as i32,
- );
-
- if let Some(storage::Entry { texture, .. }) = entry {
- gl.bind_texture(glow::TEXTURE_2D, Some(*texture))
- } else {
- continue;
- }
-
- let translate = Transformation::translate(bounds.x, bounds.y);
- let scale = Transformation::scale(bounds.width, bounds.height);
- let transformation = transformation * translate * scale;
- let matrix: [f32; 16] = transformation.into();
- gl.uniform_matrix_4_f32_slice(
- Some(&self.transform_location),
- false,
- &matrix,
- );
-
- gl.draw_arrays(glow::TRIANGLE_STRIP, 0, 4);
-
- gl.bind_texture(glow::TEXTURE_2D, None);
- }
- }
-
- unsafe {
- gl.bind_buffer(glow::ARRAY_BUFFER, None);
- gl.bind_vertex_array(None);
- gl.use_program(None);
- gl.disable(glow::SCISSOR_TEST);
- }
- }
-
- pub fn trim_cache(&mut self, mut gl: &glow::Context) {
- #[cfg(feature = "image")]
- self.raster_cache
- .borrow_mut()
- .trim(&mut self.storage, &mut gl);
-
- #[cfg(feature = "svg")]
- self.vector_cache
- .borrow_mut()
- .trim(&mut self.storage, &mut gl);
- }
-}
diff --git a/glow/src/image/storage.rs b/glow/src/image/storage.rs
deleted file mode 100644
index 9bc20641..00000000
--- a/glow/src/image/storage.rs
+++ /dev/null
@@ -1,78 +0,0 @@
-use iced_graphics::image;
-use iced_graphics::Size;
-
-use glow::HasContext;
-
-#[derive(Debug, Default)]
-pub struct Storage;
-
-impl image::Storage for Storage {
- type Entry = Entry;
- type State<'a> = &'a glow::Context;
-
- fn upload(
- &mut self,
- width: u32,
- height: u32,
- data: &[u8],
- gl: &mut &glow::Context,
- ) -> Option<Self::Entry> {
- unsafe {
- let texture = gl.create_texture().expect("create texture");
- gl.bind_texture(glow::TEXTURE_2D, Some(texture));
- gl.tex_image_2d(
- glow::TEXTURE_2D,
- 0,
- glow::SRGB8_ALPHA8 as i32,
- width as i32,
- height as i32,
- 0,
- glow::RGBA,
- glow::UNSIGNED_BYTE,
- Some(data),
- );
- gl.tex_parameter_i32(
- glow::TEXTURE_2D,
- glow::TEXTURE_WRAP_S,
- glow::CLAMP_TO_EDGE as _,
- );
- gl.tex_parameter_i32(
- glow::TEXTURE_2D,
- glow::TEXTURE_WRAP_T,
- glow::CLAMP_TO_EDGE as _,
- );
- gl.tex_parameter_i32(
- glow::TEXTURE_2D,
- glow::TEXTURE_MIN_FILTER,
- glow::LINEAR as _,
- );
- gl.tex_parameter_i32(
- glow::TEXTURE_2D,
- glow::TEXTURE_MAG_FILTER,
- glow::LINEAR as _,
- );
- gl.bind_texture(glow::TEXTURE_2D, None);
-
- Some(Entry {
- size: Size::new(width, height),
- texture,
- })
- }
- }
-
- fn remove(&mut self, entry: &Entry, gl: &mut &glow::Context) {
- unsafe { gl.delete_texture(entry.texture) }
- }
-}
-
-#[derive(Debug)]
-pub struct Entry {
- size: Size<u32>,
- pub(super) texture: glow::NativeTexture,
-}
-
-impl image::storage::Entry for Entry {
- fn size(&self) -> Size<u32> {
- self.size
- }
-}
diff --git a/glow/src/lib.rs b/glow/src/lib.rs
deleted file mode 100644
index 9e7de0d9..00000000
--- a/glow/src/lib.rs
+++ /dev/null
@@ -1,53 +0,0 @@
-//! A [`glow`] renderer for [`iced_native`].
-//!
-//! ![The native path of the Iced ecosystem](https://github.com/iced-rs/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/native.png?raw=true)
-//!
-//! [`glow`]: https://github.com/grovesNL/glow
-//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.8/native
-#![doc(
- html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"
-)]
-#![deny(
- missing_debug_implementations,
- missing_docs,
- unused_results,
- clippy::extra_unused_lifetimes,
- clippy::from_over_into,
- clippy::needless_borrow,
- clippy::new_without_default,
- clippy::useless_conversion
-)]
-#![forbid(rust_2018_idioms)]
-#![allow(clippy::inherent_to_string, clippy::type_complexity)]
-#![cfg_attr(docsrs, feature(doc_cfg))]
-
-pub use glow;
-
-mod backend;
-#[cfg(any(feature = "image", feature = "svg"))]
-mod image;
-mod program;
-mod quad;
-mod text;
-mod triangle;
-
-pub mod settings;
-pub mod window;
-
-pub use backend::Backend;
-pub use settings::Settings;
-
-pub(crate) use iced_graphics::Transformation;
-
-pub use iced_graphics::{Error, Viewport};
-pub use iced_native::Theme;
-
-pub use iced_native::alignment;
-pub use iced_native::{Alignment, Background, Color, Command, Length, Vector};
-
-/// A [`glow`] graphics renderer for [`iced`].
-///
-/// [`glow`]: https://github.com/grovesNL/glow
-/// [`iced`]: https://github.com/iced-rs/iced
-pub type Renderer<Theme = iced_native::Theme> =
- iced_graphics::Renderer<Backend, Theme>;
diff --git a/glow/src/program.rs b/glow/src/program.rs
deleted file mode 100644
index 95437fcd..00000000
--- a/glow/src/program.rs
+++ /dev/null
@@ -1,133 +0,0 @@
-use glow::HasContext;
-
-/// The [`Version`] of a `Program`.
-pub struct Version {
- vertex: String,
- fragment: String,
-}
-
-impl Version {
- pub fn new(gl: &glow::Context) -> Version {
- let version = gl.version();
-
- let (vertex, fragment) = match (
- version.major,
- version.minor,
- version.is_embedded,
- ) {
- // OpenGL 3.0+
- (3, 0 | 1 | 2, false) => (
- format!("#version 1{}0\n#extension GL_ARB_explicit_attrib_location : enable", version.minor + 3),
- format!(
- "#version 1{}0\n#extension GL_ARB_explicit_attrib_location : enable\n#define HIGHER_THAN_300 1",
- version.minor + 3
- ),
- ),
- // OpenGL 3.3+
- (3 | 4, _, false) => (
- format!("#version {}{}0\n#extension GL_ARB_explicit_attrib_location : enable", version.major, version.minor),
- format!(
- "#version {}{}0\n#extension GL_ARB_explicit_attrib_location : enable\n#define HIGHER_THAN_300 1",
- version.major, version.minor
- ),
- ),
- // OpenGL ES 3.0+
- (3, _, true) => (
- format!("#version 3{}0 es", version.minor),
- format!(
- "#version 3{}0 es\n#define HIGHER_THAN_300 1",
- version.minor
- ),
- ),
- // OpenGL ES 2.0+
- (2, _, true) => (
- String::from(
- "#version 100\n#define in attribute\n#define out varying",
- ),
- String::from("#version 100\n#define in varying"),
- ),
- // OpenGL 2.1
- (2, _, false) => (
- String::from(
- "#version 120\n#define in attribute\n#define out varying",
- ),
- String::from("#version 120\n#define in varying"),
- ),
- // OpenGL 1.1+
- _ => panic!("Incompatible context version: {version:?}"),
- };
-
- log::info!("Shader directive: {}", vertex.lines().next().unwrap());
-
- Version { vertex, fragment }
- }
-}
-
-pub struct Shader(<glow::Context as HasContext>::Shader);
-
-impl Shader {
- fn compile(gl: &glow::Context, stage: u32, content: &str) -> Shader {
- unsafe {
- let shader = gl.create_shader(stage).expect("Cannot create shader");
-
- gl.shader_source(shader, content);
- gl.compile_shader(shader);
-
- if !gl.get_shader_compile_status(shader) {
- panic!("{}", gl.get_shader_info_log(shader));
- }
-
- Shader(shader)
- }
- }
-
- /// Creates a vertex [`Shader`].
- pub fn vertex(
- gl: &glow::Context,
- version: &Version,
- content: &'static str,
- ) -> Self {
- let content = format!("{}\n{}", version.vertex, content);
-
- Shader::compile(gl, glow::VERTEX_SHADER, &content)
- }
-
- /// Creates a fragment [`Shader`].
- pub fn fragment(
- gl: &glow::Context,
- version: &Version,
- content: &'static str,
- ) -> Self {
- let content = format!("{}\n{}", version.fragment, content);
-
- Shader::compile(gl, glow::FRAGMENT_SHADER, &content)
- }
-}
-
-pub unsafe fn create(
- gl: &glow::Context,
- shaders: &[Shader],
- attributes: &[(u32, &str)],
-) -> <glow::Context as HasContext>::Program {
- let program = gl.create_program().expect("Cannot create program");
-
- for shader in shaders {
- gl.attach_shader(program, shader.0);
- }
-
- for (i, name) in attributes {
- gl.bind_attrib_location(program, *i, name);
- }
-
- 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.0);
- gl.delete_shader(shader.0);
- }
-
- program
-}
diff --git a/glow/src/quad.rs b/glow/src/quad.rs
deleted file mode 100644
index 67d9a098..00000000
--- a/glow/src/quad.rs
+++ /dev/null
@@ -1,74 +0,0 @@
-mod compatibility;
-mod core;
-
-use crate::program;
-use crate::Transformation;
-use glow::HasContext;
-use iced_graphics::layer;
-use iced_native::Rectangle;
-
-#[cfg(feature = "tracing")]
-use tracing::info_span;
-
-#[derive(Debug)]
-pub enum Pipeline {
- Core(core::Pipeline),
- Compatibility(compatibility::Pipeline),
-}
-
-impl Pipeline {
- pub fn new(
- gl: &glow::Context,
- shader_version: &program::Version,
- ) -> Pipeline {
- let gl_version = gl.version();
-
- // OpenGL 3.0+ and OpenGL ES 3.0+ have instancing (which is what separates `core` from `compatibility`)
- if gl_version.major >= 3 {
- log::info!("Mode: core");
- Pipeline::Core(core::Pipeline::new(gl, shader_version))
- } else {
- log::info!("Mode: compatibility");
- Pipeline::Compatibility(compatibility::Pipeline::new(
- gl,
- shader_version,
- ))
- }
- }
-
- pub fn draw(
- &mut self,
- gl: &glow::Context,
- target_height: u32,
- instances: &[layer::Quad],
- transformation: Transformation,
- scale: f32,
- bounds: Rectangle<u32>,
- ) {
- #[cfg(feature = "tracing")]
- let _ = info_span!("Glow::Quad", "DRAW").enter();
-
- match self {
- Pipeline::Core(pipeline) => {
- pipeline.draw(
- gl,
- target_height,
- instances,
- transformation,
- scale,
- bounds,
- );
- }
- Pipeline::Compatibility(pipeline) => {
- pipeline.draw(
- gl,
- target_height,
- instances,
- transformation,
- scale,
- bounds,
- );
- }
- }
- }
-}
diff --git a/glow/src/quad/compatibility.rs b/glow/src/quad/compatibility.rs
deleted file mode 100644
index e909162c..00000000
--- a/glow/src/quad/compatibility.rs
+++ /dev/null
@@ -1,349 +0,0 @@
-use crate::program::{self, Shader};
-use crate::Transformation;
-use glow::HasContext;
-use iced_graphics::layer;
-use iced_native::Rectangle;
-
-// Only change `MAX_QUADS`, otherwise you could cause problems
-// by splitting a triangle into different render passes.
-const MAX_QUADS: usize = 100_000;
-const MAX_VERTICES: usize = MAX_QUADS * 4;
-const MAX_INDICES: usize = MAX_QUADS * 6;
-
-#[derive(Debug)]
-pub struct Pipeline {
- program: <glow::Context as HasContext>::Program,
- vertex_array: <glow::Context as HasContext>::VertexArray,
- vertex_buffer: <glow::Context as HasContext>::Buffer,
- index_buffer: <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,
- shader_version: &program::Version,
- ) -> Pipeline {
- let program = unsafe {
- let vertex_shader = Shader::vertex(
- gl,
- shader_version,
- include_str!("../shader/compatibility/quad.vert"),
- );
- let fragment_shader = Shader::fragment(
- gl,
- shader_version,
- include_str!("../shader/compatibility/quad.frag"),
- );
-
- program::create(
- gl,
- &[vertex_shader, fragment_shader],
- &[
- (0, "i_Pos"),
- (1, "i_Scale"),
- (2, "i_Color"),
- (3, "i_BorderColor"),
- (4, "i_BorderRadius"),
- (5, "i_BorderWidth"),
- ],
- )
- };
-
- 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));
-
- gl.uniform_matrix_4_f32_slice(
- Some(&transform_location),
- false,
- Transformation::identity().as_ref(),
- );
-
- 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, vertex_buffer, index_buffer) =
- unsafe { create_buffers(gl, MAX_VERTICES) };
-
- Pipeline {
- program,
- vertex_array,
- vertex_buffer,
- index_buffer,
- 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>,
- ) {
- // TODO: Remove this allocation (probably by changing the shader and removing the need of two `position`)
- let vertices: Vec<Vertex> =
- instances.iter().flat_map(Vertex::from_quad).collect();
-
- // TODO: Remove this allocation (or allocate only when needed)
- let indices: Vec<i32> = (0..instances.len().min(MAX_QUADS) as i32)
- .flat_map(|i| {
- [i * 4, 1 + i * 4, 2 + i * 4, 2 + i * 4, 1 + i * 4, 3 + i * 4]
- })
- .cycle()
- .take(instances.len() * 6)
- .collect();
-
- 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.vertex_buffer));
- gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(self.index_buffer));
- }
-
- if transformation != self.current_transform {
- unsafe {
- gl.uniform_matrix_4_f32_slice(
- Some(&self.transform_location),
- false,
- transformation.as_ref(),
- );
-
- 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 passes = vertices
- .chunks(MAX_VERTICES)
- .zip(indices.chunks(MAX_INDICES));
-
- for (vertices, indices) in passes {
- unsafe {
- gl.buffer_sub_data_u8_slice(
- glow::ARRAY_BUFFER,
- 0,
- bytemuck::cast_slice(vertices),
- );
-
- gl.buffer_sub_data_u8_slice(
- glow::ELEMENT_ARRAY_BUFFER,
- 0,
- bytemuck::cast_slice(indices),
- );
-
- gl.draw_elements(
- glow::TRIANGLES,
- indices.len() as i32,
- glow::UNSIGNED_INT,
- 0,
- );
- }
- }
-
- unsafe {
- gl.bind_vertex_array(None);
- gl.use_program(None);
- gl.disable(glow::SCISSOR_TEST);
- }
- }
-}
-
-unsafe fn create_buffers(
- gl: &glow::Context,
- size: usize,
-) -> (
- <glow::Context as HasContext>::VertexArray,
- <glow::Context as HasContext>::Buffer,
- <glow::Context as HasContext>::Buffer,
-) {
- let vertex_array = gl.create_vertex_array().expect("Create vertex array");
- let vertex_buffer = gl.create_buffer().expect("Create vertex buffer");
- let index_buffer = gl.create_buffer().expect("Create index buffer");
-
- gl.bind_vertex_array(Some(vertex_array));
-
- gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(index_buffer));
- gl.buffer_data_size(
- glow::ELEMENT_ARRAY_BUFFER,
- 12 * size as i32,
- glow::DYNAMIC_DRAW,
- );
-
- gl.bind_buffer(glow::ARRAY_BUFFER, Some(vertex_buffer));
- gl.buffer_data_size(
- glow::ARRAY_BUFFER,
- (size * Vertex::SIZE) as i32,
- glow::DYNAMIC_DRAW,
- );
-
- let stride = Vertex::SIZE 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, 2, glow::FLOAT, false, stride, 4 * 2);
-
- gl.enable_vertex_attrib_array(2);
- gl.vertex_attrib_pointer_f32(2, 4, glow::FLOAT, false, stride, 4 * (2 + 2));
-
- gl.enable_vertex_attrib_array(3);
- gl.vertex_attrib_pointer_f32(
- 3,
- 4,
- glow::FLOAT,
- false,
- stride,
- 4 * (2 + 2 + 4),
- );
-
- gl.enable_vertex_attrib_array(4);
- gl.vertex_attrib_pointer_f32(
- 4,
- 4,
- glow::FLOAT,
- false,
- stride,
- 4 * (2 + 2 + 4 + 4),
- );
-
- gl.enable_vertex_attrib_array(5);
- gl.vertex_attrib_pointer_f32(
- 5,
- 1,
- glow::FLOAT,
- false,
- stride,
- 4 * (2 + 2 + 4 + 4 + 4),
- );
-
- gl.enable_vertex_attrib_array(6);
- gl.vertex_attrib_pointer_f32(
- 6,
- 2,
- glow::FLOAT,
- false,
- stride,
- 4 * (2 + 2 + 4 + 4 + 4 + 1),
- );
-
- gl.bind_vertex_array(None);
- gl.bind_buffer(glow::ARRAY_BUFFER, None);
- gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, None);
-
- (vertex_array, vertex_buffer, index_buffer)
-}
-
-/// The vertex of a colored rectangle with a border.
-///
-/// This type can be directly uploaded to GPU memory.
-#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
-#[repr(C)]
-pub struct Vertex {
- /// The position of the [`Vertex`].
- pub position: [f32; 2],
-
- /// The size of the [`Vertex`].
- pub size: [f32; 2],
-
- /// The color of the [`Vertex`], in __linear RGB__.
- pub color: [f32; 4],
-
- /// The border color of the [`Vertex`], in __linear RGB__.
- pub border_color: [f32; 4],
-
- /// The border radius of the [`Vertex`].
- pub border_radius: [f32; 4],
-
- /// The border width of the [`Vertex`].
- pub border_width: f32,
-
- /// The __quad__ position of the [`Vertex`].
- pub q_position: [f32; 2],
-}
-
-impl Vertex {
- const SIZE: usize = std::mem::size_of::<Self>();
-
- fn from_quad(quad: &layer::Quad) -> [Vertex; 4] {
- let base = Vertex {
- position: quad.position,
- size: quad.size,
- color: quad.color,
- border_color: quad.color,
- border_radius: quad.border_radius,
- border_width: quad.border_width,
- q_position: [0.0, 0.0],
- };
-
- [
- base,
- Self {
- q_position: [0.0, 1.0],
- ..base
- },
- Self {
- q_position: [1.0, 0.0],
- ..base
- },
- Self {
- q_position: [1.0, 1.0],
- ..base
- },
- ]
- }
-}
diff --git a/glow/src/quad/core.rs b/glow/src/quad/core.rs
deleted file mode 100644
index 89036530..00000000
--- a/glow/src/quad/core.rs
+++ /dev/null
@@ -1,244 +0,0 @@
-use crate::program::{self, Shader};
-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,
- shader_version: &program::Version,
- ) -> Pipeline {
- let program = unsafe {
- let vertex_shader = Shader::vertex(
- gl,
- shader_version,
- include_str!("../shader/core/quad.vert"),
- );
- let fragment_shader = Shader::fragment(
- gl,
- shader_version,
- include_str!("../shader/core/quad.frag"),
- );
-
- program::create(
- gl,
- &[vertex_shader, fragment_shader],
- &[
- (0, "i_Pos"),
- (1, "i_Scale"),
- (2, "i_Color"),
- (3, "i_BorderColor"),
- (4, "i_BorderRadius"),
- (5, "i_BorderWidth"),
- ],
- )
- };
-
- 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));
-
- gl.uniform_matrix_4_f32_slice(
- Some(&transform_location),
- false,
- Transformation::identity().as_ref(),
- );
-
- 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 {
- gl.uniform_matrix_4_f32_slice(
- Some(&self.transform_location),
- false,
- transformation.as_ref(),
- );
-
- 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;
- }
-
- for instances in instances.chunks(MAX_INSTANCES) {
- unsafe {
- gl.buffer_sub_data_u8_slice(
- glow::ARRAY_BUFFER,
- 0,
- bytemuck::cast_slice(instances),
- );
-
- gl.draw_arrays_instanced(
- glow::TRIANGLE_STRIP,
- 0,
- 4,
- instances.len() as i32,
- );
- }
- }
-
- 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,
- 4,
- 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 + 4),
- );
- 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
deleted file mode 100644
index 6aaa0d55..00000000
--- a/glow/src/settings.rs
+++ /dev/null
@@ -1,61 +0,0 @@
-//! Configure a renderer.
-pub use iced_graphics::Antialiasing;
-
-/// The settings of a [`Backend`].
-///
-/// [`Backend`]: crate::Backend
-#[derive(Clone, Copy, PartialEq)]
-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.0`.
- pub default_text_size: f32,
-
- /// If enabled, spread text workload in multiple threads when multiple cores
- /// are available.
- ///
- /// By default, it is disabled.
- pub text_multithreading: bool,
-
- /// The antialiasing strategy that will be used for triangle primitives.
- ///
- /// By default, it is `None`.
- pub antialiasing: Option<Antialiasing>,
-}
-
-impl Default for Settings {
- fn default() -> Settings {
- Settings {
- default_font: None,
- default_text_size: 20.0,
- text_multithreading: false,
- antialiasing: None,
- }
- }
-}
-
-impl std::fmt::Debug for Settings {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.debug_struct("Settings")
- // Instead of printing the font bytes, we simply show a `bool` indicating if using a default font or not.
- .field("default_font", &self.default_font.is_some())
- .field("default_text_size", &self.default_text_size)
- .field("text_multithreading", &self.text_multithreading)
- .field("antialiasing", &self.antialiasing)
- .finish()
- }
-}
-
-impl Settings {
- /// Creates new [`Settings`] using environment configuration.
- ///
- /// Currently, this is equivalent to calling [`Settings::default`].
- pub fn from_env() -> Self {
- Self::default()
- }
-}
diff --git a/glow/src/shader/common/gradient.frag b/glow/src/shader/common/gradient.frag
deleted file mode 100644
index 9af0cb6e..00000000
--- a/glow/src/shader/common/gradient.frag
+++ /dev/null
@@ -1,59 +0,0 @@
-#ifdef GL_ES
-#ifdef GL_FRAGMENT_PRECISION_HIGH
-precision highp float;
-#else
-precision mediump float;
-#endif
-#endif
-
-#ifdef HIGHER_THAN_300
-layout (location = 0) out vec4 fragColor;
-#define gl_FragColor fragColor
-#endif
-
-in vec2 raw_position;
-
-uniform vec4 gradient_direction;
-uniform int color_stops_size;
-// GLSL does not support dynamically sized arrays without SSBOs so this is capped to 16 stops
-//stored as color(vec4) -> offset(vec4) sequentially;
-uniform vec4 color_stops[32];
-
-//TODO: rewrite without branching to make ALUs happy
-void main() {
- vec2 start = gradient_direction.xy;
- vec2 end = gradient_direction.zw;
- vec2 gradient_vec = vec2(end - start);
- vec2 current_vec = vec2(raw_position.xy - start);
- vec2 unit = normalize(gradient_vec);
- float coord_offset = dot(unit, current_vec) / length(gradient_vec);
- //if a gradient has a start/end stop that is identical, the mesh will have a transparent fill
- gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
-
- float min_offset = color_stops[1].x;
- float max_offset = color_stops[color_stops_size - 1].x;
-
- for (int i = 0; i < color_stops_size - 2; i += 2) {
- float curr_offset = color_stops[i+1].x;
- float next_offset = color_stops[i+3].x;
-
- if (coord_offset <= min_offset) {
- //current coordinate is before the first defined offset, set it to the start color
- gl_FragColor = color_stops[0];
- }
-
- if (curr_offset <= coord_offset && coord_offset <= next_offset) {
- //current fragment is between the current offset processing & the next one, interpolate colors
- gl_FragColor = mix(color_stops[i], color_stops[i+2], smoothstep(
- curr_offset,
- next_offset,
- coord_offset
- ));
- }
-
- if (coord_offset >= max_offset) {
- //current coordinate is before the last defined offset, set it to the last color
- gl_FragColor = color_stops[color_stops_size - 2];
- }
- }
-}
diff --git a/glow/src/shader/common/gradient.vert b/glow/src/shader/common/gradient.vert
deleted file mode 100644
index fe505997..00000000
--- a/glow/src/shader/common/gradient.vert
+++ /dev/null
@@ -1,9 +0,0 @@
-uniform mat4 u_Transform;
-
-in vec2 i_Position;
-out vec2 raw_position;
-
-void main() {
- gl_Position = u_Transform * vec4(i_Position, 0.0, 1.0);
- raw_position = i_Position;
-}
diff --git a/glow/src/shader/common/image.frag b/glow/src/shader/common/image.frag
deleted file mode 100644
index 5e05abdf..00000000
--- a/glow/src/shader/common/image.frag
+++ /dev/null
@@ -1,22 +0,0 @@
-#ifdef GL_ES
-#ifdef GL_FRAGMENT_PRECISION_HIGH
-precision highp float;
-#else
-precision mediump float;
-#endif
-#endif
-
-uniform sampler2D tex;
-in vec2 tex_pos;
-
-#ifdef HIGHER_THAN_300
-out vec4 fragColor;
-#define gl_FragColor fragColor
-#endif
-#ifdef GL_ES
-#define texture texture2D
-#endif
-
-void main() {
- gl_FragColor = texture(tex, tex_pos);
-}
diff --git a/glow/src/shader/common/image.vert b/glow/src/shader/common/image.vert
deleted file mode 100644
index 93e541f2..00000000
--- a/glow/src/shader/common/image.vert
+++ /dev/null
@@ -1,9 +0,0 @@
-uniform mat4 u_Transform;
-
-in vec2 i_Position;
-out vec2 tex_pos;
-
-void main() {
- gl_Position = u_Transform * vec4(i_Position, 0.0, 1.0);
- tex_pos = i_Position;
-}
diff --git a/glow/src/shader/common/solid.frag b/glow/src/shader/common/solid.frag
deleted file mode 100644
index 174ffdd3..00000000
--- a/glow/src/shader/common/solid.frag
+++ /dev/null
@@ -1,18 +0,0 @@
-#ifdef GL_ES
-#ifdef GL_FRAGMENT_PRECISION_HIGH
-precision highp float;
-#else
-precision mediump float;
-#endif
-#endif
-
-#ifdef HIGHER_THAN_300
-out vec4 fragColor;
-#define gl_FragColor fragColor
-#endif
-
-in vec4 v_Color;
-
-void main() {
- gl_FragColor = v_Color;
-}
diff --git a/glow/src/shader/common/solid.vert b/glow/src/shader/common/solid.vert
deleted file mode 100644
index 59ed88e5..00000000
--- a/glow/src/shader/common/solid.vert
+++ /dev/null
@@ -1,11 +0,0 @@
-uniform mat4 u_Transform;
-
-in vec2 i_Position;
-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/shader/compatibility/quad.frag b/glow/src/shader/compatibility/quad.frag
deleted file mode 100644
index bb9d8122..00000000
--- a/glow/src/shader/compatibility/quad.frag
+++ /dev/null
@@ -1,83 +0,0 @@
-#ifdef GL_ES
-#ifdef GL_FRAGMENT_PRECISION_HIGH
-precision highp float;
-#else
-precision mediump float;
-#endif
-#endif
-
-uniform float u_ScreenHeight;
-
-varying vec4 v_Color;
-varying vec4 v_BorderColor;
-varying vec2 v_Pos;
-varying vec2 v_Scale;
-varying vec4 v_BorderRadius;
-varying float v_BorderWidth;
-
-float _distance(vec2 frag_coord, vec2 position, 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);
-}
-
-float selectBorderRadius(vec4 radi, vec2 position, vec2 center)
-{
- float rx = radi.x;
- float ry = radi.y;
- rx = position.x > center.x ? radi.y : radi.x;
- ry = position.x > center.x ? radi.z : radi.w;
- rx = position.y > center.y ? ry : rx;
- return rx;
-}
-
-void main() {
- vec2 fragCoord = vec2(gl_FragCoord.x, u_ScreenHeight - gl_FragCoord.y);
-
- float border_radius = selectBorderRadius(
- v_BorderRadius,
- fragCoord,
- (v_Pos + v_Scale * 0.5).xy
- );
-
- float internal_border = max(border_radius - 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
- );
-
- vec4 mixed_color = mix(v_Color, v_BorderColor, border_mix);
-
- float d = _distance(
- fragCoord,
- v_Pos,
- v_Scale,
- border_radius
- );
-
- float radius_alpha =
- 1.0 - smoothstep(max(border_radius - 0.5, 0.0), border_radius + 0.5, d);
-
- gl_FragColor = vec4(mixed_color.xyz, mixed_color.w * radius_alpha);
-}
diff --git a/glow/src/shader/compatibility/quad.vert b/glow/src/shader/compatibility/quad.vert
deleted file mode 100644
index 89931f06..00000000
--- a/glow/src/shader/compatibility/quad.vert
+++ /dev/null
@@ -1,46 +0,0 @@
-uniform mat4 u_Transform;
-uniform float u_Scale;
-
-attribute vec2 i_Pos;
-attribute vec2 i_Scale;
-attribute vec4 i_Color;
-attribute vec4 i_BorderColor;
-attribute vec4 i_BorderRadius;
-attribute float i_BorderWidth;
-attribute vec2 q_Pos;
-
-varying vec4 v_Color;
-varying vec4 v_BorderColor;
-varying vec2 v_Pos;
-varying vec2 v_Scale;
-varying vec4 v_BorderRadius;
-varying float v_BorderWidth;
-
-
-void main() {
- vec2 p_Pos = i_Pos * u_Scale;
- vec2 p_Scale = i_Scale * u_Scale;
-
- vec4 i_BorderRadius = vec4(
- min(i_BorderRadius.x, min(i_Scale.x, i_Scale.y) / 2.0),
- min(i_BorderRadius.y, min(i_Scale.x, i_Scale.y) / 2.0),
- min(i_BorderRadius.z, min(i_Scale.x, i_Scale.y) / 2.0),
- min(i_BorderRadius.w, 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/core/quad.frag b/glow/src/shader/core/quad.frag
deleted file mode 100644
index 71147aa5..00000000
--- a/glow/src/shader/core/quad.frag
+++ /dev/null
@@ -1,95 +0,0 @@
-#ifdef GL_ES
-#ifdef GL_FRAGMENT_PRECISION_HIGH
-precision highp float;
-#else
-precision mediump float;
-#endif
-#endif
-
-#ifdef HIGHER_THAN_300
-out vec4 fragColor;
-#define gl_FragColor fragColor
-#endif
-
-uniform float u_ScreenHeight;
-
-in vec4 v_Color;
-in vec4 v_BorderColor;
-in vec2 v_Pos;
-in vec2 v_Scale;
-in vec4 v_BorderRadius;
-in float v_BorderWidth;
-
-float fDistance(vec2 frag_coord, vec2 position, 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);
-}
-
-float selectBorderRadius(vec4 radi, vec2 position, vec2 center)
-{
- float rx = radi.x;
- float ry = radi.y;
- rx = position.x > center.x ? radi.y : radi.x;
- ry = position.x > center.x ? radi.z : radi.w;
- rx = position.y > center.y ? ry : rx;
- return rx;
-}
-
-void main() {
- vec4 mixed_color;
-
- vec2 fragCoord = vec2(gl_FragCoord.x, u_ScreenHeight - gl_FragCoord.y);
-
- float border_radius = selectBorderRadius(
- v_BorderRadius,
- fragCoord,
- (v_Pos + v_Scale * 0.5).xy
- );
-
- // TODO: Remove branching (?)
- if(v_BorderWidth > 0.0) {
- float internal_border = max(border_radius - v_BorderWidth, 0.0);
-
- float internal_distance = fDistance(
- 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 = fDistance(
- fragCoord,
- v_Pos,
- v_Scale,
- border_radius
- );
-
- float radius_alpha =
- 1.0 - smoothstep(max(border_radius - 0.5, 0.0), border_radius + 0.5, d);
-
- gl_FragColor = vec4(mixed_color.xyz, mixed_color.w * radius_alpha);
-}
diff --git a/glow/src/shader/core/quad.vert b/glow/src/shader/core/quad.vert
deleted file mode 100644
index 17c3e641..00000000
--- a/glow/src/shader/core/quad.vert
+++ /dev/null
@@ -1,52 +0,0 @@
-uniform mat4 u_Transform;
-uniform float u_Scale;
-
-in vec2 i_Pos;
-in vec2 i_Scale;
-in vec4 i_Color;
-in vec4 i_BorderColor;
-in vec4 i_BorderRadius;
-in float i_BorderWidth;
-
-out vec4 v_Color;
-out vec4 v_BorderColor;
-out vec2 v_Pos;
-out vec2 v_Scale;
-out vec4 v_BorderRadius;
-out float v_BorderWidth;
-
-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;
-
- vec4 i_BorderRadius = vec4(
- min(i_BorderRadius.x, min(i_Scale.x, i_Scale.y) / 2.0),
- min(i_BorderRadius.y, min(i_Scale.x, i_Scale.y) / 2.0),
- min(i_BorderRadius.z, min(i_Scale.x, i_Scale.y) / 2.0),
- min(i_BorderRadius.w, 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/text.rs b/glow/src/text.rs
deleted file mode 100644
index 37ccdece..00000000
--- a/glow/src/text.rs
+++ /dev/null
@@ -1,257 +0,0 @@
-use crate::Transformation;
-
-use iced_graphics::font;
-
-use glow_glyph::ab_glyph;
-use std::{cell::RefCell, collections::HashMap};
-
-pub use iced_native::text::Hit;
-
-#[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]>,
- multithreading: bool,
- ) -> 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_builder =
- glow_glyph::GlyphBrushBuilder::using_font(font.clone())
- .initial_cache_size((2048, 2048))
- .draw_cache_multithread(multithreading);
-
- #[cfg(target_arch = "wasm32")]
- let draw_brush_builder = draw_brush_builder.draw_cache_align_4x4(true);
-
- let draw_brush = draw_brush_builder.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 hit_test(
- &self,
- content: &str,
- size: f32,
- font: iced_native::Font,
- bounds: iced_native::Size,
- point: iced_native::Point,
- nearest_only: bool,
- ) -> Option<Hit> {
- 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()
- };
-
- let mut mb = self.measure_brush.borrow_mut();
-
- // The underlying type is FontArc, so clones are cheap.
- use ab_glyph::{Font, ScaleFont};
- let font = mb.fonts()[font_id].clone().into_scaled(size);
-
- // Implements an iterator over the glyph bounding boxes.
- let bounds = mb.glyphs(section).map(
- |glow_glyph::SectionGlyph {
- byte_index, glyph, ..
- }| {
- (
- *byte_index,
- iced_native::Rectangle::new(
- iced_native::Point::new(
- glyph.position.x - font.h_side_bearing(glyph.id),
- glyph.position.y - font.ascent(),
- ),
- iced_native::Size::new(
- font.h_advance(glyph.id),
- font.ascent() - font.descent(),
- ),
- ),
- )
- },
- );
-
- // Implements computation of the character index based on the byte index
- // within the input string.
- let char_index = |byte_index| {
- let mut b_count = 0;
- for (i, utf8_len) in
- content.chars().map(|c| c.len_utf8()).enumerate()
- {
- if byte_index < (b_count + utf8_len) {
- return i;
- }
- b_count += utf8_len;
- }
-
- byte_index
- };
-
- if !nearest_only {
- for (idx, bounds) in bounds.clone() {
- if bounds.contains(point) {
- return Some(Hit::CharOffset(char_index(idx)));
- }
- }
- }
-
- let nearest = bounds
- .map(|(index, bounds)| (index, bounds.center()))
- .min_by(|(_, center_a), (_, center_b)| {
- center_a
- .distance(point)
- .partial_cmp(&center_b.distance(point))
- .unwrap_or(std::cmp::Ordering::Greater)
- });
-
- nearest.map(|(idx, center)| {
- Hit::NearestCharOffset(char_index(idx), point - center)
- })
- }
-
- 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
deleted file mode 100644
index 42c88455..00000000
--- a/glow/src/triangle.rs
+++ /dev/null
@@ -1,595 +0,0 @@
-//! Draw meshes of triangles.
-use crate::program;
-use crate::Transformation;
-
-use iced_graphics::gradient::Gradient;
-use iced_graphics::layer::mesh::{self, Mesh};
-use iced_graphics::triangle::{ColoredVertex2D, Vertex2D};
-
-use glow::HasContext;
-use std::marker::PhantomData;
-
-#[cfg(feature = "tracing")]
-use tracing::info_span;
-
-const DEFAULT_VERTICES: usize = 1_000;
-const DEFAULT_INDICES: usize = 1_000;
-
-#[derive(Debug)]
-pub(crate) struct Pipeline {
- indices: Buffer<u32>,
- solid: solid::Program,
- gradient: gradient::Program,
-}
-
-impl Pipeline {
- pub fn new(gl: &glow::Context, shader_version: &program::Version) -> Self {
- let mut indices = unsafe {
- Buffer::new(
- gl,
- glow::ELEMENT_ARRAY_BUFFER,
- glow::DYNAMIC_DRAW,
- DEFAULT_INDICES,
- )
- };
-
- let solid = solid::Program::new(gl, shader_version);
- let gradient = gradient::Program::new(gl, shader_version);
-
- unsafe {
- gl.bind_vertex_array(Some(solid.vertex_array));
- indices.bind(gl, 0);
-
- gl.bind_vertex_array(Some(gradient.vertex_array));
- indices.bind(gl, 0);
-
- gl.bind_vertex_array(None);
- }
-
- Self {
- indices,
- solid,
- gradient,
- }
- }
-
- pub fn draw(
- &mut self,
- meshes: &[Mesh<'_>],
- gl: &glow::Context,
- target_height: u32,
- transformation: Transformation,
- scale_factor: f32,
- ) {
- #[cfg(feature = "tracing")]
- let _ = info_span!("Glow::Triangle", "DRAW").enter();
-
- unsafe {
- gl.enable(glow::MULTISAMPLE);
- gl.enable(glow::SCISSOR_TEST);
- }
-
- // Count the total amount of vertices & indices we need to handle
- let count = mesh::attribute_count_of(meshes);
-
- // Then we ensure the current attribute buffers are big enough, resizing if necessary
- unsafe {
- self.indices.bind(gl, count.indices);
- }
-
- // We upload all the vertices and indices upfront
- let mut solid_vertex_offset = 0;
- let mut gradient_vertex_offset = 0;
- let mut index_offset = 0;
-
- for mesh in meshes {
- let indices = mesh.indices();
-
- unsafe {
- gl.buffer_sub_data_u8_slice(
- glow::ELEMENT_ARRAY_BUFFER,
- (index_offset * std::mem::size_of::<u32>()) as i32,
- bytemuck::cast_slice(indices),
- );
-
- index_offset += indices.len();
- }
-
- match mesh {
- Mesh::Solid { buffers, .. } => {
- unsafe {
- self.solid.vertices.bind(gl, count.solid_vertices);
-
- gl.buffer_sub_data_u8_slice(
- glow::ARRAY_BUFFER,
- (solid_vertex_offset
- * std::mem::size_of::<ColoredVertex2D>())
- as i32,
- bytemuck::cast_slice(&buffers.vertices),
- );
- }
-
- solid_vertex_offset += buffers.vertices.len();
- }
- Mesh::Gradient { buffers, .. } => {
- unsafe {
- self.gradient
- .vertices
- .bind(gl, count.gradient_vertices);
-
- gl.buffer_sub_data_u8_slice(
- glow::ARRAY_BUFFER,
- (gradient_vertex_offset
- * std::mem::size_of::<Vertex2D>())
- as i32,
- bytemuck::cast_slice(&buffers.vertices),
- );
- }
-
- gradient_vertex_offset += buffers.vertices.len();
- }
- }
- }
-
- // Then we draw each mesh using offsets
- let mut last_solid_vertex = 0;
- let mut last_gradient_vertex = 0;
- let mut last_index = 0;
-
- for mesh in meshes {
- let indices = mesh.indices();
- let origin = mesh.origin();
-
- let transform =
- transformation * Transformation::translate(origin.x, origin.y);
-
- let clip_bounds = (mesh.clip_bounds() * scale_factor).snap();
-
- unsafe {
- 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,
- );
- }
-
- match mesh {
- Mesh::Solid { buffers, .. } => unsafe {
- gl.use_program(Some(self.solid.program));
- gl.bind_vertex_array(Some(self.solid.vertex_array));
-
- if transform != self.solid.uniforms.transform {
- gl.uniform_matrix_4_f32_slice(
- Some(&self.solid.uniforms.transform_location),
- false,
- transform.as_ref(),
- );
-
- self.solid.uniforms.transform = transform;
- }
-
- gl.draw_elements_base_vertex(
- glow::TRIANGLES,
- indices.len() as i32,
- glow::UNSIGNED_INT,
- (last_index * std::mem::size_of::<u32>()) as i32,
- last_solid_vertex as i32,
- );
-
- last_solid_vertex += buffers.vertices.len();
- },
- Mesh::Gradient {
- buffers, gradient, ..
- } => unsafe {
- gl.use_program(Some(self.gradient.program));
- gl.bind_vertex_array(Some(self.gradient.vertex_array));
-
- if transform != self.gradient.uniforms.transform {
- gl.uniform_matrix_4_f32_slice(
- Some(&self.gradient.uniforms.locations.transform),
- false,
- transform.as_ref(),
- );
-
- self.gradient.uniforms.transform = transform;
- }
-
- if &self.gradient.uniforms.gradient != *gradient {
- match gradient {
- Gradient::Linear(linear) => {
- gl.uniform_4_f32(
- Some(
- &self
- .gradient
- .uniforms
- .locations
- .gradient_direction,
- ),
- linear.start.x,
- linear.start.y,
- linear.end.x,
- linear.end.y,
- );
-
- gl.uniform_1_i32(
- Some(
- &self
- .gradient
- .uniforms
- .locations
- .color_stops_size,
- ),
- (linear.color_stops.len() * 2) as i32,
- );
-
- let mut stops = [0.0; 128];
-
- for (index, stop) in linear
- .color_stops
- .iter()
- .enumerate()
- .take(16)
- {
- let [r, g, b, a] = stop.color.into_linear();
-
- stops[index * 8] = r;
- stops[(index * 8) + 1] = g;
- stops[(index * 8) + 2] = b;
- stops[(index * 8) + 3] = a;
- stops[(index * 8) + 4] = stop.offset;
- stops[(index * 8) + 5] = 0.;
- stops[(index * 8) + 6] = 0.;
- stops[(index * 8) + 7] = 0.;
- }
-
- gl.uniform_4_f32_slice(
- Some(
- &self
- .gradient
- .uniforms
- .locations
- .color_stops,
- ),
- &stops,
- );
- }
- }
-
- self.gradient.uniforms.gradient = (*gradient).clone();
- }
-
- gl.draw_elements_base_vertex(
- glow::TRIANGLES,
- indices.len() as i32,
- glow::UNSIGNED_INT,
- (last_index * std::mem::size_of::<u32>()) as i32,
- last_gradient_vertex as i32,
- );
-
- last_gradient_vertex += buffers.vertices.len();
- },
- }
-
- last_index += indices.len();
- }
-
- unsafe {
- gl.bind_vertex_array(None);
- gl.disable(glow::SCISSOR_TEST);
- gl.disable(glow::MULTISAMPLE);
- }
- }
-}
-
-#[derive(Debug)]
-pub 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;
- }
- }
-}
-
-mod solid {
- use crate::program;
- use crate::triangle;
- use glow::{Context, HasContext, NativeProgram};
- use iced_graphics::triangle::ColoredVertex2D;
- use iced_graphics::Transformation;
-
- #[derive(Debug)]
- pub struct Program {
- pub program: <Context as HasContext>::Program,
- pub vertex_array: <glow::Context as HasContext>::VertexArray,
- pub vertices: triangle::Buffer<ColoredVertex2D>,
- pub uniforms: Uniforms,
- }
-
- impl Program {
- pub fn new(gl: &Context, shader_version: &program::Version) -> Self {
- let program = unsafe {
- let vertex_shader = program::Shader::vertex(
- gl,
- shader_version,
- include_str!("shader/common/solid.vert"),
- );
-
- let fragment_shader = program::Shader::fragment(
- gl,
- shader_version,
- include_str!("shader/common/solid.frag"),
- );
-
- program::create(
- gl,
- &[vertex_shader, fragment_shader],
- &[(0, "i_Position"), (1, "i_Color")],
- )
- };
-
- let vertex_array = unsafe {
- gl.create_vertex_array().expect("Create vertex array")
- };
-
- let vertices = unsafe {
- triangle::Buffer::new(
- gl,
- glow::ARRAY_BUFFER,
- glow::DYNAMIC_DRAW,
- super::DEFAULT_VERTICES,
- )
- };
-
- unsafe {
- gl.bind_vertex_array(Some(vertex_array));
-
- let stride = std::mem::size_of::<ColoredVertex2D>() 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);
- };
-
- Self {
- program,
- vertex_array,
- vertices,
- uniforms: Uniforms::new(gl, program),
- }
- }
- }
-
- #[derive(Debug)]
- pub struct Uniforms {
- pub transform: Transformation,
- pub transform_location: <Context as HasContext>::UniformLocation,
- }
-
- impl Uniforms {
- fn new(gl: &Context, program: NativeProgram) -> Self {
- let transform = Transformation::identity();
- let transform_location =
- unsafe { gl.get_uniform_location(program, "u_Transform") }
- .expect("Solid - Get u_Transform.");
-
- unsafe {
- gl.use_program(Some(program));
-
- gl.uniform_matrix_4_f32_slice(
- Some(&transform_location),
- false,
- transform.as_ref(),
- );
-
- gl.use_program(None);
- }
-
- Self {
- transform,
- transform_location,
- }
- }
- }
-}
-
-mod gradient {
- use crate::program;
- use crate::triangle;
- use glow::{Context, HasContext, NativeProgram};
- use iced_graphics::gradient::{self, Gradient};
- use iced_graphics::triangle::Vertex2D;
- use iced_graphics::Transformation;
-
- #[derive(Debug)]
- pub struct Program {
- pub program: <Context as HasContext>::Program,
- pub vertex_array: <glow::Context as HasContext>::VertexArray,
- pub vertices: triangle::Buffer<Vertex2D>,
- pub uniforms: Uniforms,
- }
-
- impl Program {
- pub fn new(gl: &Context, shader_version: &program::Version) -> Self {
- let program = unsafe {
- let vertex_shader = program::Shader::vertex(
- gl,
- shader_version,
- include_str!("shader/common/gradient.vert"),
- );
-
- let fragment_shader = program::Shader::fragment(
- gl,
- shader_version,
- include_str!("shader/common/gradient.frag"),
- );
-
- program::create(
- gl,
- &[vertex_shader, fragment_shader],
- &[(0, "i_Position")],
- )
- };
-
- let vertex_array = unsafe {
- gl.create_vertex_array().expect("Create vertex array")
- };
-
- let vertices = unsafe {
- triangle::Buffer::new(
- gl,
- glow::ARRAY_BUFFER,
- glow::DYNAMIC_DRAW,
- super::DEFAULT_VERTICES,
- )
- };
-
- unsafe {
- gl.bind_vertex_array(Some(vertex_array));
-
- 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.bind_vertex_array(None);
- };
-
- Self {
- program,
- vertex_array,
- vertices,
- uniforms: Uniforms::new(gl, program),
- }
- }
- }
-
- #[derive(Debug)]
- pub struct Uniforms {
- pub gradient: Gradient,
- pub transform: Transformation,
- pub locations: Locations,
- }
-
- #[derive(Debug)]
- pub struct Locations {
- pub gradient_direction: <Context as HasContext>::UniformLocation,
- pub color_stops_size: <Context as HasContext>::UniformLocation,
- //currently the maximum number of stops is 16 due to lack of SSBO in GL2.1
- pub color_stops: <Context as HasContext>::UniformLocation,
- pub transform: <Context as HasContext>::UniformLocation,
- }
-
- impl Uniforms {
- fn new(gl: &Context, program: NativeProgram) -> Self {
- let gradient_direction = unsafe {
- gl.get_uniform_location(program, "gradient_direction")
- }
- .expect("Gradient - Get gradient_direction.");
-
- let color_stops_size =
- unsafe { gl.get_uniform_location(program, "color_stops_size") }
- .expect("Gradient - Get color_stops_size.");
-
- let color_stops = unsafe {
- gl.get_uniform_location(program, "color_stops")
- .expect("Gradient - Get color_stops.")
- };
-
- let transform = Transformation::identity();
- let transform_location =
- unsafe { gl.get_uniform_location(program, "u_Transform") }
- .expect("Solid - Get u_Transform.");
-
- unsafe {
- gl.use_program(Some(program));
-
- gl.uniform_matrix_4_f32_slice(
- Some(&transform_location),
- false,
- transform.as_ref(),
- );
-
- gl.use_program(None);
- }
-
- Self {
- gradient: Gradient::Linear(gradient::Linear {
- start: Default::default(),
- end: Default::default(),
- color_stops: vec![],
- }),
- transform: Transformation::identity(),
- locations: Locations {
- gradient_direction,
- color_stops_size,
- color_stops,
- transform: transform_location,
- },
- }
- }
- }
-}
diff --git a/glow/src/window.rs b/glow/src/window.rs
deleted file mode 100644
index aac5fb9e..00000000
--- a/glow/src/window.rs
+++ /dev/null
@@ -1,4 +0,0 @@
-//! 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
deleted file mode 100644
index 20756032..00000000
--- a/glow/src/window/compositor.rs
+++ /dev/null
@@ -1,111 +0,0 @@
-use crate::{Backend, Color, Error, Renderer, Settings, Viewport};
-
-use glow::HasContext;
-use iced_graphics::{compositor, Antialiasing, Size};
-
-use core::ffi::c_void;
-use std::marker::PhantomData;
-
-/// A window graphics backend for iced powered by `glow`.
-#[allow(missing_debug_implementations)]
-pub struct Compositor<Theme> {
- gl: glow::Context,
- theme: PhantomData<Theme>,
-}
-
-impl<Theme> iced_graphics::window::GLCompositor for Compositor<Theme> {
- type Settings = Settings;
- type Renderer = Renderer<Theme>;
-
- 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);
-
- log::info!("{:#?}", settings);
-
- let version = gl.version();
- log::info!(
- "OpenGL version: {:?} (Embedded: {})",
- version,
- version.is_embedded
- );
-
- let renderer = gl.get_parameter_string(glow::RENDERER);
- log::info!("Renderer: {}", renderer);
-
- // Enable auto-conversion from/to sRGB
- gl.enable(glow::FRAMEBUFFER_SRGB);
-
- // Enable alpha blending
- gl.enable(glow::BLEND);
- gl.blend_func_separate(
- glow::SRC_ALPHA,
- glow::ONE_MINUS_SRC_ALPHA,
- glow::ONE,
- glow::ONE_MINUS_SRC_ALPHA,
- );
-
- // Disable multisampling by default
- gl.disable(glow::MULTISAMPLE);
-
- let renderer = Renderer::new(Backend::new(&gl, settings));
-
- Ok((
- Self {
- gl,
- theme: PhantomData,
- },
- 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 fetch_information(&self) -> compositor::Information {
- let adapter = unsafe { self.gl.get_parameter_string(glow::RENDERER) };
-
- compositor::Information {
- backend: format!("{:?}", self.gl.version()),
- adapter,
- }
- }
-
- fn present<T: AsRef<str>>(
- &mut self,
- renderer: &mut Self::Renderer,
- viewport: &Viewport,
- color: Color,
- overlay: &[T],
- ) {
- 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.with_primitives(|backend, primitive| {
- backend.present(gl, primitive, viewport, overlay);
- });
- }
-}
diff --git a/glutin/Cargo.toml b/glutin/Cargo.toml
deleted file mode 100644
index 10d3778b..00000000
--- a/glutin/Cargo.toml
+++ /dev/null
@@ -1,42 +0,0 @@
-[package]
-name = "iced_glutin"
-version = "0.7.0"
-authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
-edition = "2021"
-description = "A glutin runtime for Iced"
-license = "MIT"
-repository = "https://github.com/iced-rs/iced"
-documentation = "https://docs.rs/iced_glutin"
-keywords = ["gui", "ui", "graphics", "interface", "widgets"]
-categories = ["gui"]
-
-[features]
-trace = ["iced_winit/trace"]
-debug = ["iced_winit/debug"]
-system = ["iced_winit/system"]
-
-[dependencies]
-log = "0.4"
-
-[dependencies.glutin]
-version = "0.29"
-git = "https://github.com/iced-rs/glutin"
-rev = "da8d291486b4c9bec12487a46c119c4b1d386abf"
-
-[dependencies.iced_native]
-version = "0.9"
-path = "../native"
-
-[dependencies.iced_winit]
-version = "0.8"
-path = "../winit"
-features = ["application"]
-
-[dependencies.iced_graphics]
-version = "0.7"
-path = "../graphics"
-features = ["opengl"]
-
-[dependencies.tracing]
-version = "0.1.6"
-optional = true
diff --git a/glutin/README.md b/glutin/README.md
deleted file mode 100644
index 45e8ee6b..00000000
--- a/glutin/README.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# `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/iced-rs/iced/blob/master/LICENSE)
-[![Discord Server](https://img.shields.io/discord/628993209984614400?label=&labelColor=6A7EC2&logo=discord&logoColor=ffffff&color=7389D8)](https://discord.gg/3xZJ65GAhd)
-
-`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.7"
-```
-
-__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/iced-rs/iced/releases
diff --git a/glutin/src/application.rs b/glutin/src/application.rs
index 24f38e7b..e69de29b 100644
--- a/glutin/src/application.rs
+++ b/glutin/src/application.rs
@@ -1,510 +0,0 @@
-//! Create interactive, native cross-platform applications.
-use crate::mouse;
-use crate::{Error, Executor, Runtime};
-
-pub use iced_winit::application::StyleSheet;
-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::renderer;
-use iced_winit::time::Instant;
-use iced_winit::user_interface;
-use iced_winit::{Clipboard, Command, Debug, Event, Proxy, Settings};
-
-use glutin::window::Window;
-use std::mem::ManuallyDrop;
-
-#[cfg(feature = "tracing")]
-use tracing::{info_span, instrument::Instrument};
-
-/// 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,
- <A::Renderer as iced_native::Renderer>::Theme: StyleSheet,
-{
- use futures::task;
- use futures::Future;
- use glutin::event_loop::EventLoopBuilder;
- use glutin::platform::run_return::EventLoopExtRunReturn;
- use glutin::ContextBuilder;
-
- #[cfg(feature = "trace")]
- let _guard = iced_winit::Profiler::init();
-
- let mut debug = Debug::new();
- debug.startup_started();
-
- #[cfg(feature = "tracing")]
- let _ = info_span!("Application::Glutin", "RUN").entered();
-
- let mut event_loop = EventLoopBuilder::with_user_event().build();
- let proxy = event_loop.create_proxy();
-
- let 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 context = {
- let builder = settings.window.into_builder(
- &application.title(),
- event_loop.primary_monitor(),
- settings.id,
- );
-
- log::debug!("Window builder: {:#?}", builder);
-
- let opengl_builder = ContextBuilder::new()
- .with_vsync(true)
- .with_multisampling(C::sample_count(&compositor_settings) as u16);
-
- let opengles_builder = opengl_builder.clone().with_gl(
- glutin::GlRequest::Specific(glutin::Api::OpenGlEs, (2, 0)),
- );
-
- let (first_builder, second_builder) = if settings.try_opengles_first {
- (opengles_builder, opengl_builder)
- } else {
- (opengl_builder, opengles_builder)
- };
-
- log::info!("Trying first builder: {:#?}", first_builder);
-
- let context = first_builder
- .build_windowed(builder.clone(), &event_loop)
- .or_else(|_| {
- log::info!("Trying second builder: {:#?}", second_builder);
- second_builder.build_windowed(builder, &event_loop)
- })
- .map_err(|error| {
- use glutin::CreationError;
- use iced_graphics::Error as ContextError;
-
- match error {
- CreationError::Window(error) => {
- Error::WindowCreationFailed(error)
- }
- CreationError::OpenGlVersionNotSupported => {
- Error::GraphicsCreationFailed(
- ContextError::VersionNotSupported,
- )
- }
- CreationError::NoAvailablePixelFormat => {
- Error::GraphicsCreationFailed(
- ContextError::NoAvailablePixelFormat,
- )
- }
- error => Error::GraphicsCreationFailed(
- ContextError::BackendError(error.to_string()),
- ),
- }
- })?;
-
- #[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 event_sender, event_receiver) = mpsc::unbounded();
- let (control_sender, mut control_receiver) = mpsc::unbounded();
-
- let mut instance = Box::pin({
- let run_instance = run_instance::<A, E, C>(
- application,
- compositor,
- renderer,
- runtime,
- proxy,
- debug,
- event_receiver,
- control_sender,
- context,
- init_command,
- settings.exit_on_close_request,
- );
-
- #[cfg(feature = "tracing")]
- let run_instance =
- run_instance.instrument(info_span!("Application", "LOOP"));
-
- run_instance
- });
-
- let mut context = task::Context::from_waker(task::noop_waker_ref());
-
- let _ = event_loop.run_return(move |event, _, control_flow| {
- use glutin::event_loop::ControlFlow;
-
- if let ControlFlow::ExitWithCode(_) = control_flow {
- return;
- }
-
- let event = match event {
- glutin::event::Event::WindowEvent {
- event:
- glutin::event::WindowEvent::ScaleFactorChanged {
- new_inner_size,
- ..
- },
- window_id,
- } => Some(glutin::event::Event::WindowEvent {
- event: glutin::event::WindowEvent::Resized(*new_inner_size),
- window_id,
- }),
- _ => event.to_static(),
- };
-
- if let Some(event) = event {
- event_sender.start_send(event).expect("Send event");
-
- let poll = instance.as_mut().poll(&mut context);
-
- match poll {
- task::Poll::Pending => {
- if let Ok(Some(flow)) = control_receiver.try_next() {
- *control_flow = flow;
- }
- }
- task::Poll::Ready(_) => {
- *control_flow = ControlFlow::Exit;
- }
- }
- }
- });
-
- Ok(())
-}
-
-async fn run_instance<A, E, C>(
- mut application: A,
- mut compositor: C,
- mut renderer: A::Renderer,
- mut runtime: Runtime<E, Proxy<A::Message>, A::Message>,
- mut proxy: glutin::event_loop::EventLoopProxy<A::Message>,
- mut debug: Debug,
- mut event_receiver: mpsc::UnboundedReceiver<
- glutin::event::Event<'_, A::Message>,
- >,
- mut control_sender: mpsc::UnboundedSender<glutin::event_loop::ControlFlow>,
- mut context: glutin::ContextWrapper<glutin::PossiblyCurrent, Window>,
- init_command: Command<A::Message>,
- exit_on_close_request: bool,
-) where
- A: Application + 'static,
- E: Executor + 'static,
- C: window::GLCompositor<Renderer = A::Renderer> + 'static,
- <A::Renderer as iced_native::Renderer>::Theme: StyleSheet,
-{
- use glutin::event;
- use glutin::event_loop::ControlFlow;
- use iced_winit::futures::stream::StreamExt;
-
- let mut clipboard = Clipboard::connect(context.window());
- let mut cache = user_interface::Cache::default();
- let mut state = application::State::new(&application, context.window());
- let mut viewport_version = state.viewport_version();
- let mut should_exit = false;
-
- application::run_command(
- &application,
- &mut cache,
- &state,
- &mut renderer,
- init_command,
- &mut runtime,
- &mut clipboard,
- &mut should_exit,
- &mut proxy,
- &mut debug,
- context.window(),
- || compositor.fetch_information(),
- );
- runtime.track(application.subscription());
-
- let mut user_interface =
- ManuallyDrop::new(application::build_user_interface(
- &application,
- user_interface::Cache::default(),
- &mut renderer,
- state.logical_size(),
- &mut debug,
- ));
-
- let mut mouse_interaction = mouse::Interaction::default();
- let mut events = Vec::new();
- let mut messages = Vec::new();
- let mut redraw_pending = false;
-
- debug.startup_finished();
-
- while let Some(event) = event_receiver.next().await {
- match event {
- event::Event::NewEvents(start_cause) => {
- redraw_pending = matches!(
- start_cause,
- event::StartCause::Init
- | event::StartCause::Poll
- | event::StartCause::ResumeTimeReached { .. }
- );
- }
- event::Event::MainEventsCleared => {
- if !redraw_pending && events.is_empty() && messages.is_empty() {
- continue;
- }
-
- debug.event_processing_started();
-
- let (interface_state, statuses) = user_interface.update(
- &events,
- state.cursor_position(),
- &mut renderer,
- &mut clipboard,
- &mut messages,
- );
-
- debug.event_processing_finished();
-
- for event in events.drain(..).zip(statuses.into_iter()) {
- runtime.broadcast(event);
- }
-
- if !messages.is_empty()
- || matches!(
- interface_state,
- user_interface::State::Outdated
- )
- {
- let mut cache =
- ManuallyDrop::into_inner(user_interface).into_cache();
-
- // Update application
- application::update(
- &mut application,
- &mut cache,
- &state,
- &mut renderer,
- &mut runtime,
- &mut clipboard,
- &mut should_exit,
- &mut proxy,
- &mut debug,
- &mut messages,
- context.window(),
- || compositor.fetch_information(),
- );
-
- // Update window
- state.synchronize(&application, context.window());
-
- user_interface =
- ManuallyDrop::new(application::build_user_interface(
- &application,
- cache,
- &mut renderer,
- state.logical_size(),
- &mut debug,
- ));
-
- if should_exit {
- break;
- }
- }
-
- // TODO: Avoid redrawing all the time by forcing widgets to
- // request redraws on state changes
- //
- // Then, we can use the `interface_state` here to decide if a redraw
- // is needed right away, or simply wait until a specific time.
- let redraw_event = Event::Window(
- crate::window::Id::MAIN,
- crate::window::Event::RedrawRequested(Instant::now()),
- );
-
- let (interface_state, _) = user_interface.update(
- &[redraw_event.clone()],
- state.cursor_position(),
- &mut renderer,
- &mut clipboard,
- &mut messages,
- );
-
- debug.draw_started();
- let new_mouse_interaction = user_interface.draw(
- &mut renderer,
- state.theme(),
- &renderer::Style {
- text_color: state.text_color(),
- },
- state.cursor_position(),
- );
- debug.draw_finished();
-
- if new_mouse_interaction != mouse_interaction {
- context.window().set_cursor_icon(
- conversion::mouse_interaction(new_mouse_interaction),
- );
-
- mouse_interaction = new_mouse_interaction;
- }
-
- context.window().request_redraw();
- runtime
- .broadcast((redraw_event, crate::event::Status::Ignored));
-
- let _ = control_sender.start_send(match interface_state {
- user_interface::State::Updated {
- redraw_request: Some(redraw_request),
- } => match redraw_request {
- crate::window::RedrawRequest::NextFrame => {
- ControlFlow::Poll
- }
- crate::window::RedrawRequest::At(at) => {
- ControlFlow::WaitUntil(at)
- }
- },
- _ => ControlFlow::Wait,
- });
-
- redraw_pending = false;
- }
- event::Event::PlatformSpecific(event::PlatformSpecific::MacOS(
- event::MacOS::ReceivedUrl(url),
- )) => {
- use iced_native::event;
- events.push(iced_native::Event::PlatformSpecific(
- event::PlatformSpecific::MacOS(event::MacOS::ReceivedUrl(
- url,
- )),
- ));
- }
- event::Event::UserEvent(message) => {
- messages.push(message);
- }
- event::Event::RedrawRequested(_) => {
- #[cfg(feature = "tracing")]
- let _ = info_span!("Application", "FRAME").entered();
-
- debug.render_started();
-
- #[allow(unsafe_code)]
- unsafe {
- if !context.is_current() {
- context = context
- .make_current()
- .expect("Make OpenGL context current");
- }
- }
-
- 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();
- let new_mouse_interaction = user_interface.draw(
- &mut renderer,
- state.theme(),
- &renderer::Style {
- text_color: state.text_color(),
- },
- state.cursor_position(),
- );
- debug.draw_finished();
-
- if new_mouse_interaction != mouse_interaction {
- context.window().set_cursor_icon(
- conversion::mouse_interaction(
- new_mouse_interaction,
- ),
- );
-
- mouse_interaction = new_mouse_interaction;
- }
-
- context.resize(glutin::dpi::PhysicalSize::new(
- physical_size.width,
- physical_size.height,
- ));
-
- compositor.resize_viewport(physical_size);
-
- viewport_version = current_viewport_version;
- }
-
- compositor.present(
- &mut renderer,
- state.viewport(),
- state.background_color(),
- &debug.overlay(),
- );
-
- context.swap_buffers().expect("Swap buffers");
-
- debug.render_finished();
-
- // 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())
- && exit_on_close_request
- {
- break;
- }
-
- state.update(context.window(), &window_event, &mut debug);
-
- if let Some(event) = conversion::window_event(
- crate::window::Id::MAIN,
- &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
deleted file mode 100644
index 33afd664..00000000
--- a/glutin/src/lib.rs
+++ /dev/null
@@ -1,33 +0,0 @@
-//! A windowing shell for [`iced`], on top of [`glutin`].
-//!
-//! ![The native path of the Iced ecosystem](https://github.com/iced-rs/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/native.png?raw=true)
-//!
-//! [`iced`]: https://github.com/iced-rs/iced
-//! [`glutin`]: https://github.com/rust-windowing/glutin
-#![doc(
- html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"
-)]
-#![deny(
- missing_docs,
- missing_debug_implementations,
- unsafe_code,
- unused_results,
- clippy::extra_unused_lifetimes,
- clippy::from_over_into,
- clippy::needless_borrow,
- clippy::new_without_default,
- clippy::useless_conversion
-)]
-#![forbid(rust_2018_idioms)]
-#![allow(clippy::inherent_to_string, clippy::type_complexity)]
-#![cfg_attr(docsrs, feature(doc_cfg))]
-
-pub use glutin;
-
-#[doc(no_inline)]
-pub use iced_winit::*;
-
-pub mod application;
-
-#[doc(no_inline)]
-pub use application::Application;
diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml
index a37c99a2..7a9e6aee 100644
--- a/graphics/Cargo.toml
+++ b/graphics/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "iced_graphics"
-version = "0.7.0"
+version = "0.8.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
description = "A bunch of backend-agnostic types that can be leveraged to build a renderer for Iced"
@@ -11,29 +11,14 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"]
categories = ["gui"]
[features]
-svg = ["resvg"]
-image = ["png", "jpeg", "jpeg_rayon", "gif", "webp", "bmp"]
-png = ["image_rs/png"]
-jpeg = ["image_rs/jpeg"]
-jpeg_rayon = ["image_rs/jpeg_rayon"]
-gif = ["image_rs/gif"]
-webp = ["image_rs/webp"]
-pnm = ["image_rs/pnm"]
-ico = ["image_rs/ico"]
-bmp = ["image_rs/bmp"]
-hdr = ["image_rs/hdr"]
-dds = ["image_rs/dds"]
-farbfeld = ["image_rs/farbfeld"]
-canvas = ["lyon"]
-qr_code = ["qrcode", "canvas"]
-font-source = ["font-kit"]
-font-fallback = []
-font-icons = []
+geometry = ["lyon_path"]
opengl = []
-image_rs = ["kamadak-exif"]
+image = ["dep:image", "kamadak-exif"]
+web-colors = []
[dependencies]
-glam = "0.21.3"
+glam = "0.24"
+half = "2.2.1"
log = "0.4"
raw-window-handle = "0.5"
thiserror = "1.0"
@@ -43,41 +28,22 @@ bitflags = "1.2"
version = "1.4"
features = ["derive"]
-[dependencies.iced_native]
+[dependencies.iced_core]
version = "0.9"
-path = "../native"
+path = "../core"
-[dependencies.iced_style]
-version = "0.7"
-path = "../style"
-
-[dependencies.lyon]
-version = "1.0"
-optional = true
-
-[dependencies.qrcode]
-version = "0.12"
-optional = true
-default-features = false
-
-[dependencies.font-kit]
-version = "0.10"
-optional = true
-
-[dependencies.image_rs]
+[dependencies.image]
version = "0.24"
-package = "image"
-default-features = false
-optional = true
-
-[dependencies.resvg]
-version = "0.29"
optional = true
[dependencies.kamadak-exif]
version = "0.5"
optional = true
+[dependencies.lyon_path]
+version = "1"
+optional = true
+
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
all-features = true
diff --git a/graphics/fonts/Lato-Regular.ttf b/graphics/fonts/Lato-Regular.ttf
deleted file mode 100644
index 33eba8b1..00000000
--- a/graphics/fonts/Lato-Regular.ttf
+++ /dev/null
Binary files differ
diff --git a/graphics/fonts/OFL.txt b/graphics/fonts/OFL.txt
deleted file mode 100644
index dfca0da4..00000000
--- a/graphics/fonts/OFL.txt
+++ /dev/null
@@ -1,93 +0,0 @@
-Copyright (c) 2010-2014 by tyPoland Lukasz Dziedzic (team@latofonts.com) with Reserved Font Name "Lato"
-
-This Font Software is licensed under the SIL Open Font License, Version 1.1.
-This license is copied below, and is also available with a FAQ at:
-http://scripts.sil.org/OFL
-
-
------------------------------------------------------------
-SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
------------------------------------------------------------
-
-PREAMBLE
-The goals of the Open Font License (OFL) are to stimulate worldwide
-development of collaborative font projects, to support the font creation
-efforts of academic and linguistic communities, and to provide a free and
-open framework in which fonts may be shared and improved in partnership
-with others.
-
-The OFL allows the licensed fonts to be used, studied, modified and
-redistributed freely as long as they are not sold by themselves. The
-fonts, including any derivative works, can be bundled, embedded,
-redistributed and/or sold with any software provided that any reserved
-names are not used by derivative works. The fonts and derivatives,
-however, cannot be released under any other type of license. The
-requirement for fonts to remain under this license does not apply
-to any document created using the fonts or their derivatives.
-
-DEFINITIONS
-"Font Software" refers to the set of files released by the Copyright
-Holder(s) under this license and clearly marked as such. This may
-include source files, build scripts and documentation.
-
-"Reserved Font Name" refers to any names specified as such after the
-copyright statement(s).
-
-"Original Version" refers to the collection of Font Software components as
-distributed by the Copyright Holder(s).
-
-"Modified Version" refers to any derivative made by adding to, deleting,
-or substituting -- in part or in whole -- any of the components of the
-Original Version, by changing formats or by porting the Font Software to a
-new environment.
-
-"Author" refers to any designer, engineer, programmer, technical
-writer or other person who contributed to the Font Software.
-
-PERMISSION & CONDITIONS
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of the Font Software, to use, study, copy, merge, embed, modify,
-redistribute, and sell modified and unmodified copies of the Font
-Software, subject to the following conditions:
-
-1) Neither the Font Software nor any of its individual components,
-in Original or Modified Versions, may be sold by itself.
-
-2) Original or Modified Versions of the Font Software may be bundled,
-redistributed and/or sold with any software, provided that each copy
-contains the above copyright notice and this license. These can be
-included either as stand-alone text files, human-readable headers or
-in the appropriate machine-readable metadata fields within text or
-binary files as long as those fields can be easily viewed by the user.
-
-3) No Modified Version of the Font Software may use the Reserved Font
-Name(s) unless explicit written permission is granted by the corresponding
-Copyright Holder. This restriction only applies to the primary font name as
-presented to the users.
-
-4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
-Software shall not be used to promote, endorse or advertise any
-Modified Version, except to acknowledge the contribution(s) of the
-Copyright Holder(s) and the Author(s) or with their explicit written
-permission.
-
-5) The Font Software, modified or unmodified, in part or in whole,
-must be distributed entirely under this license, and must not be
-distributed under any other license. The requirement for fonts to
-remain under this license does not apply to any document created
-using the Font Software.
-
-TERMINATION
-This license becomes null and void if any of the above conditions are
-not met.
-
-DISCLAIMER
-THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
-OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
-COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
-DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
-OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs
index 256b7ab5..59e95bf8 100644
--- a/graphics/src/backend.rs
+++ b/graphics/src/backend.rs
@@ -1,13 +1,18 @@
//! Write a graphics backend.
-use iced_native::image;
-use iced_native::svg;
-use iced_native::text;
-use iced_native::{Font, Point, Size};
+use iced_core::image;
+use iced_core::svg;
+use iced_core::text;
+use iced_core::{Font, Point, Size};
+
+use std::borrow::Cow;
/// The graphics backend of a [`Renderer`].
///
/// [`Renderer`]: crate::Renderer
pub trait Backend {
+ /// The custom kind of primitives this [`Backend`] supports.
+ type Primitive;
+
/// Trims the measurements cache.
///
/// This method is currently necessary to properly trim the text cache in
@@ -31,6 +36,9 @@ pub trait Text {
/// [`ICON_FONT`]: Self::ICON_FONT
const ARROW_DOWN_ICON: char;
+ /// Returns the default [`Font`].
+ fn default_font(&self) -> Font;
+
/// Returns the default size of text.
fn default_size(&self) -> f32;
@@ -41,9 +49,11 @@ pub trait Text {
&self,
contents: &str,
size: f32,
+ line_height: text::LineHeight,
font: Font,
bounds: Size,
- ) -> (f32, f32);
+ shaping: text::Shaping,
+ ) -> Size;
/// Tests whether the provided point is within the boundaries of [`Text`]
/// laid out with the given parameters, returning information about
@@ -56,11 +66,16 @@ pub trait Text {
&self,
contents: &str,
size: f32,
+ line_height: text::LineHeight,
font: Font,
bounds: Size,
+ shaping: text::Shaping,
point: Point,
nearest_only: bool,
) -> Option<text::Hit>;
+
+ /// Loads a [`Font`] from its bytes.
+ fn load_font(&mut self, font: Cow<'static, [u8]>);
}
/// A graphics backend that supports image rendering.
diff --git a/graphics/src/color.rs b/graphics/src/color.rs
new file mode 100644
index 00000000..92448a68
--- /dev/null
+++ b/graphics/src/color.rs
@@ -0,0 +1,46 @@
+//! Manage colors for shaders.
+use crate::core::Color;
+
+use bytemuck::{Pod, Zeroable};
+
+/// A color packed as 4 floats representing RGBA channels.
+#[derive(Debug, Clone, Copy, PartialEq, Zeroable, Pod)]
+#[repr(C)]
+pub struct Packed([f32; 4]);
+
+impl Packed {
+ /// Returns the internal components of the [`Packed`] color.
+ pub fn components(self) -> [f32; 4] {
+ self.0
+ }
+}
+
+/// A flag that indicates whether the renderer should perform gamma correction.
+pub const GAMMA_CORRECTION: bool = internal::GAMMA_CORRECTION;
+
+/// Packs a [`Color`].
+pub fn pack(color: impl Into<Color>) -> Packed {
+ Packed(internal::pack(color.into()))
+}
+
+#[cfg(not(feature = "web-colors"))]
+mod internal {
+ use crate::core::Color;
+
+ pub const GAMMA_CORRECTION: bool = true;
+
+ pub fn pack(color: Color) -> [f32; 4] {
+ color.into_linear()
+ }
+}
+
+#[cfg(feature = "web-colors")]
+mod internal {
+ use crate::core::Color;
+
+ pub const GAMMA_CORRECTION: bool = false;
+
+ pub fn pack(color: Color) -> [f32; 4] {
+ [color.r, color.g, color.b, color.a]
+ }
+}
diff --git a/graphics/src/window/compositor.rs b/graphics/src/compositor.rs
index db4ba45d..f7b86045 100644
--- a/graphics/src/window/compositor.rs
+++ b/graphics/src/compositor.rs
@@ -1,6 +1,8 @@
//! A compositor is responsible for initializing a renderer and managing window
//! surfaces.
-use crate::{Color, Error, Viewport};
+use crate::{Error, Viewport};
+
+use iced_core::Color;
use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle};
use thiserror::Error;
@@ -11,7 +13,7 @@ pub trait Compositor: Sized {
type Settings: Default;
/// The iced renderer of the backend.
- type Renderer: iced_native::Renderer;
+ type Renderer: iced_core::Renderer;
/// The surface of the backend.
type Surface;
@@ -28,6 +30,8 @@ pub trait Compositor: Sized {
fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>(
&mut self,
window: &W,
+ width: u32,
+ height: u32,
) -> Self::Surface;
/// Configures a new [`Surface`] with the given dimensions.
@@ -55,6 +59,19 @@ pub trait Compositor: Sized {
background_color: Color,
overlay: &[T],
) -> Result<(), SurfaceError>;
+
+ /// Screenshots the current [`Renderer`] primitives to an offscreen texture, and returns the bytes of
+ /// the texture ordered as `RGBA` in the sRGB color space.
+ ///
+ /// [`Renderer`]: Self::Renderer;
+ fn screenshot<T: AsRef<str>>(
+ &mut self,
+ renderer: &mut Self::Renderer,
+ surface: &mut Self::Surface,
+ viewport: &Viewport,
+ background_color: Color,
+ overlay: &[T],
+ ) -> Vec<u8>;
}
/// Result of an unsuccessful call to [`Compositor::present`].
@@ -78,7 +95,7 @@ pub enum SurfaceError {
OutOfMemory,
}
-/// Contains informations about the graphics (e.g. graphics adapter, graphics backend).
+/// Contains information about the graphics (e.g. graphics adapter, graphics backend).
#[derive(Debug)]
pub struct Information {
/// Contains the graphics adapter.
diff --git a/graphics/src/damage.rs b/graphics/src/damage.rs
new file mode 100644
index 00000000..2f29956e
--- /dev/null
+++ b/graphics/src/damage.rs
@@ -0,0 +1,205 @@
+//! Track and compute the damage of graphical primitives.
+use crate::core::alignment;
+use crate::core::{Rectangle, Size};
+use crate::Primitive;
+
+use std::sync::Arc;
+
+/// A type that has some damage bounds.
+pub trait Damage: PartialEq {
+ /// Returns the bounds of the [`Damage`].
+ fn bounds(&self) -> Rectangle;
+}
+
+impl<T: Damage> Damage for Primitive<T> {
+ fn bounds(&self) -> Rectangle {
+ match self {
+ Self::Text {
+ bounds,
+ horizontal_alignment,
+ vertical_alignment,
+ ..
+ } => {
+ let mut bounds = *bounds;
+
+ bounds.x = match horizontal_alignment {
+ alignment::Horizontal::Left => bounds.x,
+ alignment::Horizontal::Center => {
+ bounds.x - bounds.width / 2.0
+ }
+ alignment::Horizontal::Right => bounds.x - bounds.width,
+ };
+
+ bounds.y = match vertical_alignment {
+ alignment::Vertical::Top => bounds.y,
+ alignment::Vertical::Center => {
+ bounds.y - bounds.height / 2.0
+ }
+ alignment::Vertical::Bottom => bounds.y - bounds.height,
+ };
+
+ bounds.expand(1.5)
+ }
+ Self::Quad { bounds, .. }
+ | Self::Image { bounds, .. }
+ | Self::Svg { bounds, .. } => bounds.expand(1.0),
+ Self::Clip { bounds, .. } => bounds.expand(1.0),
+ Self::Group { primitives } => primitives
+ .iter()
+ .map(Self::bounds)
+ .fold(Rectangle::with_size(Size::ZERO), |a, b| {
+ Rectangle::union(&a, &b)
+ }),
+ Self::Translate {
+ translation,
+ content,
+ } => content.bounds() + *translation,
+ Self::Cache { content } => content.bounds(),
+ Self::Custom(custom) => custom.bounds(),
+ }
+ }
+}
+
+fn regions<T: Damage>(a: &Primitive<T>, b: &Primitive<T>) -> Vec<Rectangle> {
+ match (a, b) {
+ (
+ Primitive::Group {
+ primitives: primitives_a,
+ },
+ Primitive::Group {
+ primitives: primitives_b,
+ },
+ ) => return list(primitives_a, primitives_b),
+ (
+ Primitive::Clip {
+ bounds: bounds_a,
+ content: content_a,
+ ..
+ },
+ Primitive::Clip {
+ bounds: bounds_b,
+ content: content_b,
+ ..
+ },
+ ) => {
+ if bounds_a == bounds_b {
+ return regions(content_a, content_b)
+ .into_iter()
+ .filter_map(|r| r.intersection(&bounds_a.expand(1.0)))
+ .collect();
+ } else {
+ return vec![bounds_a.expand(1.0), bounds_b.expand(1.0)];
+ }
+ }
+ (
+ Primitive::Translate {
+ translation: translation_a,
+ content: content_a,
+ },
+ Primitive::Translate {
+ translation: translation_b,
+ content: content_b,
+ },
+ ) => {
+ if translation_a == translation_b {
+ return regions(content_a, content_b)
+ .into_iter()
+ .map(|r| r + *translation_a)
+ .collect();
+ }
+ }
+ (
+ Primitive::Cache { content: content_a },
+ Primitive::Cache { content: content_b },
+ ) => {
+ if Arc::ptr_eq(content_a, content_b) {
+ return vec![];
+ }
+ }
+ _ if a == b => return vec![],
+ _ => {}
+ }
+
+ let bounds_a = a.bounds();
+ let bounds_b = b.bounds();
+
+ if bounds_a == bounds_b {
+ vec![bounds_a]
+ } else {
+ vec![bounds_a, bounds_b]
+ }
+}
+
+/// Computes the damage regions between the two given lists of primitives.
+pub fn list<T: Damage>(
+ previous: &[Primitive<T>],
+ current: &[Primitive<T>],
+) -> Vec<Rectangle> {
+ let damage = previous
+ .iter()
+ .zip(current)
+ .flat_map(|(a, b)| regions(a, b));
+
+ if previous.len() == current.len() {
+ damage.collect()
+ } else {
+ let (smaller, bigger) = if previous.len() < current.len() {
+ (previous, current)
+ } else {
+ (current, previous)
+ };
+
+ // Extend damage by the added/removed primitives
+ damage
+ .chain(bigger[smaller.len()..].iter().map(Damage::bounds))
+ .collect()
+ }
+}
+
+/// Groups the given damage regions that are close together inside the given
+/// bounds.
+pub fn group(
+ mut damage: Vec<Rectangle>,
+ scale_factor: f32,
+ bounds: Size<u32>,
+) -> Vec<Rectangle> {
+ use std::cmp::Ordering;
+
+ const AREA_THRESHOLD: f32 = 20_000.0;
+
+ let bounds = Rectangle {
+ x: 0.0,
+ y: 0.0,
+ width: bounds.width as f32,
+ height: bounds.height as f32,
+ };
+
+ damage.sort_by(|a, b| {
+ a.x.partial_cmp(&b.x)
+ .unwrap_or(Ordering::Equal)
+ .then_with(|| a.y.partial_cmp(&b.y).unwrap_or(Ordering::Equal))
+ });
+
+ let mut output = Vec::new();
+ let mut scaled = damage
+ .into_iter()
+ .filter_map(|region| (region * scale_factor).intersection(&bounds))
+ .filter(|region| region.width >= 1.0 && region.height >= 1.0);
+
+ if let Some(mut current) = scaled.next() {
+ for region in scaled {
+ let union = current.union(&region);
+
+ if union.area() - current.area() - region.area() <= AREA_THRESHOLD {
+ current = union;
+ } else {
+ output.push(current);
+ current = region;
+ }
+ }
+
+ output.push(current);
+ }
+
+ output
+}
diff --git a/graphics/src/font.rs b/graphics/src/font.rs
deleted file mode 100644
index d55d0faf..00000000
--- a/graphics/src/font.rs
+++ /dev/null
@@ -1,35 +0,0 @@
-//! 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/graphics/src/font/source.rs b/graphics/src/font/source.rs
deleted file mode 100644
index c0b50e1d..00000000
--- a/graphics/src/font/source.rs
+++ /dev/null
@@ -1,45 +0,0 @@
-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,
- &font_kit::properties::Properties::default(),
- )?;
-
- match font {
- font_kit::handle::Handle::Path { path, .. } => {
- use std::io::Read;
-
- let mut buf = Vec::new();
- let mut reader = std::fs::File::open(path).expect("Read font");
- let _ = reader.read_to_end(&mut buf);
-
- Ok(buf)
- }
- font_kit::handle::Handle::Memory { bytes, .. } => {
- Ok(bytes.as_ref().clone())
- }
- }
- }
-}
-
-impl Default for Source {
- fn default() -> Self {
- Self::new()
- }
-}
diff --git a/graphics/src/geometry.rs b/graphics/src/geometry.rs
new file mode 100644
index 00000000..7cd3dd3a
--- /dev/null
+++ b/graphics/src/geometry.rs
@@ -0,0 +1,24 @@
+//! Build and draw geometry.
+pub mod fill;
+pub mod path;
+pub mod stroke;
+
+mod style;
+mod text;
+
+pub use fill::Fill;
+pub use path::Path;
+pub use stroke::{LineCap, LineDash, LineJoin, Stroke};
+pub use style::Style;
+pub use text::Text;
+
+pub use crate::gradient::{self, Gradient};
+
+/// A renderer capable of drawing some [`Geometry`].
+pub trait Renderer: crate::core::Renderer {
+ /// The kind of geometry this renderer can draw.
+ type Geometry;
+
+ /// Draws the given layers of [`Geometry`].
+ fn draw(&mut self, layers: Vec<Self::Geometry>);
+}
diff --git a/graphics/src/widget/canvas/fill.rs b/graphics/src/geometry/fill.rs
index e954ebb5..b773c99b 100644
--- a/graphics/src/widget/canvas/fill.rs
+++ b/graphics/src/geometry/fill.rs
@@ -1,7 +1,8 @@
//! Fill [crate::widget::canvas::Geometry] with a certain style.
-use crate::{Color, Gradient};
+pub use crate::geometry::Style;
-pub use crate::widget::canvas::Style;
+use crate::core::Color;
+use crate::gradient::{self, Gradient};
/// The style used to fill geometry.
#[derive(Debug, Clone)]
@@ -19,14 +20,14 @@ pub struct Fill {
/// By default, it is set to `NonZero`.
///
/// [1]: https://www.w3.org/TR/SVG/painting.html#FillRuleProperty
- pub rule: FillRule,
+ pub rule: Rule,
}
impl Default for Fill {
fn default() -> Self {
Self {
style: Style::Solid(Color::BLACK),
- rule: FillRule::NonZero,
+ rule: Rule::NonZero,
}
}
}
@@ -49,6 +50,15 @@ impl From<Gradient> for Fill {
}
}
+impl From<gradient::Linear> for Fill {
+ fn from(gradient: gradient::Linear) -> Self {
+ Fill {
+ style: Style::Gradient(Gradient::Linear(gradient)),
+ ..Default::default()
+ }
+ }
+}
+
/// The fill rule defines how to determine what is inside and what is outside of
/// a shape.
///
@@ -57,16 +67,7 @@ impl From<Gradient> for Fill {
/// [1]: https://www.w3.org/TR/SVG/painting.html#FillRuleProperty
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(missing_docs)]
-pub enum FillRule {
+pub enum Rule {
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/graphics/src/widget/canvas/path.rs b/graphics/src/geometry/path.rs
index aeb2589e..3d8fc6fa 100644
--- a/graphics/src/widget/canvas/path.rs
+++ b/graphics/src/geometry/path.rs
@@ -7,18 +7,16 @@ mod builder;
pub use arc::Arc;
pub use builder::Builder;
-use crate::widget::canvas::LineDash;
+pub use lyon_path;
-use iced_native::{Point, Size};
-use lyon::algorithms::walk::{walk_along_path, RepeatedPattern, WalkerEvent};
-use lyon::path::iterator::PathIterator;
+use iced_core::{Point, Size};
/// An immutable set of points that may or may not be connected.
///
/// A single [`Path`] can represent different kinds of 2D shapes!
#[derive(Debug, Clone)]
pub struct Path {
- raw: lyon::path::Path,
+ raw: lyon_path::Path,
}
impl Path {
@@ -55,55 +53,17 @@ impl Path {
Self::new(|p| p.circle(center, radius))
}
+ /// Returns the internal [`lyon_path::Path`].
#[inline]
- pub(crate) fn raw(&self) -> &lyon::path::Path {
+ pub fn raw(&self) -> &lyon_path::Path {
&self.raw
}
+ /// Returns the current [`Path`] with the given transform applied to it.
#[inline]
- pub(crate) fn transformed(
- &self,
- transform: &lyon::math::Transform,
- ) -> Path {
+ pub fn transform(&self, transform: &lyon_path::math::Transform) -> Path {
Path {
raw: self.raw.clone().transformed(transform),
}
}
}
-
-pub(super) fn dashed(path: &Path, line_dash: LineDash<'_>) -> Path {
- Path::new(|builder| {
- let segments_odd = (line_dash.segments.len() % 2 == 1)
- .then(|| [line_dash.segments, line_dash.segments].concat());
-
- let mut draw_line = false;
-
- walk_along_path(
- path.raw().iter().flattened(0.01),
- 0.0,
- lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE,
- &mut RepeatedPattern {
- callback: |event: WalkerEvent<'_>| {
- let point = Point {
- x: event.position.x,
- y: event.position.y,
- };
-
- if draw_line {
- builder.line_to(point);
- } else {
- builder.move_to(point);
- }
-
- draw_line = !draw_line;
-
- true
- },
- index: line_dash.offset,
- intervals: segments_odd
- .as_deref()
- .unwrap_or(line_dash.segments),
- },
- );
- })
-}
diff --git a/graphics/src/widget/canvas/path/arc.rs b/graphics/src/geometry/path/arc.rs
index b8e72daf..2cdebb66 100644
--- a/graphics/src/widget/canvas/path/arc.rs
+++ b/graphics/src/geometry/path/arc.rs
@@ -1,5 +1,5 @@
//! Build and draw curves.
-use iced_native::{Point, Vector};
+use iced_core::{Point, Vector};
/// A segment of a differentiable curve.
#[derive(Debug, Clone, Copy)]
diff --git a/graphics/src/widget/canvas/path/builder.rs b/graphics/src/geometry/path/builder.rs
index 5121aa68..794dd3bc 100644
--- a/graphics/src/widget/canvas/path/builder.rs
+++ b/graphics/src/geometry/path/builder.rs
@@ -1,35 +1,38 @@
-use crate::widget::canvas::path::{arc, Arc, Path};
+use crate::geometry::path::{arc, Arc, Path};
-use iced_native::{Point, Size};
-use lyon::path::builder::SvgPathBuilder;
+use iced_core::{Point, Size};
+
+use lyon_path::builder::{self, SvgPathBuilder};
+use lyon_path::geom;
+use lyon_path::math;
/// A [`Path`] builder.
///
/// Once a [`Path`] is built, it can no longer be mutated.
#[allow(missing_debug_implementations)]
pub struct Builder {
- raw: lyon::path::builder::WithSvg<lyon::path::path::BuilderImpl>,
+ raw: builder::WithSvg<lyon_path::path::BuilderImpl>,
}
impl Builder {
/// Creates a new [`Builder`].
pub fn new() -> Builder {
Builder {
- raw: lyon::path::Path::builder().with_svg(),
+ raw: lyon_path::Path::builder().with_svg(),
}
}
/// Moves the starting point of a new sub-path to the given `Point`.
#[inline]
pub fn move_to(&mut self, point: Point) {
- let _ = self.raw.move_to(lyon::math::Point::new(point.x, point.y));
+ let _ = self.raw.move_to(math::Point::new(point.x, point.y));
}
/// Connects the last point in the [`Path`] to the given `Point` with a
/// straight line.
#[inline]
pub fn line_to(&mut self, point: Point) {
- let _ = self.raw.line_to(lyon::math::Point::new(point.x, point.y));
+ let _ = self.raw.line_to(math::Point::new(point.x, point.y));
}
/// Adds an [`Arc`] to the [`Path`] from `start_angle` to `end_angle` in
@@ -53,8 +56,6 @@ impl Builder {
/// See [the HTML5 specification of `arcTo`](https://html.spec.whatwg.org/multipage/canvas.html#building-paths:dom-context-2d-arcto)
/// for more details and examples.
pub fn arc_to(&mut self, a: Point, b: Point, radius: f32) {
- use lyon::{math, path};
-
let start = self.raw.current_position();
let mid = math::Point::new(a.x, a.y);
let end = math::Point::new(b.x, b.y);
@@ -92,7 +93,7 @@ impl Builder {
self.raw.arc_to(
math::Vector::new(radius, radius),
math::Angle::radians(0.0),
- path::ArcFlags {
+ lyon_path::ArcFlags {
large_arc: false,
sweep,
},
@@ -102,8 +103,6 @@ impl Builder {
/// Adds an ellipse to the [`Path`] using a clockwise direction.
pub fn ellipse(&mut self, arc: arc::Elliptical) {
- use lyon::{geom, math};
-
let arc = geom::Arc {
center: math::Point::new(arc.center.x, arc.center.y),
radii: math::Vector::new(arc.radii.x, arc.radii.y),
@@ -128,8 +127,6 @@ impl Builder {
control_b: Point,
to: Point,
) {
- use lyon::math;
-
let _ = self.raw.cubic_bezier_to(
math::Point::new(control_a.x, control_a.y),
math::Point::new(control_b.x, control_b.y),
@@ -141,8 +138,6 @@ impl Builder {
/// and its end point.
#[inline]
pub fn quadratic_curve_to(&mut self, control: Point, to: Point) {
- use lyon::math;
-
let _ = self.raw.quadratic_bezier_to(
math::Point::new(control.x, control.y),
math::Point::new(to.x, to.y),
diff --git a/graphics/src/widget/canvas/stroke.rs b/graphics/src/geometry/stroke.rs
index 4c19251d..69a76e1c 100644
--- a/graphics/src/widget/canvas/stroke.rs
+++ b/graphics/src/geometry/stroke.rs
@@ -1,7 +1,7 @@
//! Create lines from a [crate::widget::canvas::Path] and assigns them various attributes/styles.
-pub use crate::widget::canvas::Style;
+pub use crate::geometry::Style;
-use iced_native::Color;
+use iced_core::Color;
/// The style of a stroke.
#[derive(Debug, Clone)]
@@ -59,9 +59,10 @@ impl<'a> Default for Stroke<'a> {
}
/// The shape used at the end of open subpaths when they are stroked.
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone, Copy, Default)]
pub enum LineCap {
/// The stroke for each sub-path does not extend beyond its two endpoints.
+ #[default]
Butt,
/// At the end of each sub-path, the shape representing the stroke will be
/// extended by a square.
@@ -71,27 +72,12 @@ pub enum LineCap {
Round,
}
-impl Default for LineCap {
- fn default() -> LineCap {
- LineCap::Butt
- }
-}
-
-impl From<LineCap> for lyon::tessellation::LineCap {
- fn from(line_cap: LineCap) -> lyon::tessellation::LineCap {
- match line_cap {
- LineCap::Butt => lyon::tessellation::LineCap::Butt,
- LineCap::Square => lyon::tessellation::LineCap::Square,
- LineCap::Round => lyon::tessellation::LineCap::Round,
- }
- }
-}
-
/// The shape used at the corners of paths or basic shapes when they are
/// stroked.
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone, Copy, Default)]
pub enum LineJoin {
/// A sharp corner.
+ #[default]
Miter,
/// A round corner.
Round,
@@ -99,22 +85,6 @@ pub enum LineJoin {
Bevel,
}
-impl Default for LineJoin {
- fn default() -> LineJoin {
- LineJoin::Miter
- }
-}
-
-impl From<LineJoin> for lyon::tessellation::LineJoin {
- fn from(line_join: LineJoin) -> lyon::tessellation::LineJoin {
- match line_join {
- LineJoin::Miter => lyon::tessellation::LineJoin::Miter,
- LineJoin::Round => lyon::tessellation::LineJoin::Round,
- LineJoin::Bevel => lyon::tessellation::LineJoin::Bevel,
- }
- }
-}
-
/// The dash pattern used when stroking the line.
#[derive(Debug, Clone, Copy, Default)]
pub struct LineDash<'a> {
diff --git a/graphics/src/widget/canvas/style.rs b/graphics/src/geometry/style.rs
index 6794f2e7..a0f4b08a 100644
--- a/graphics/src/widget/canvas/style.rs
+++ b/graphics/src/geometry/style.rs
@@ -1,4 +1,5 @@
-use crate::{Color, Gradient};
+use crate::core::Color;
+use crate::geometry::Gradient;
/// The coloring style of some drawing.
#[derive(Debug, Clone, PartialEq)]
diff --git a/graphics/src/widget/canvas/text.rs b/graphics/src/geometry/text.rs
index 056f8204..c584f3cd 100644
--- a/graphics/src/widget/canvas/text.rs
+++ b/graphics/src/geometry/text.rs
@@ -1,5 +1,6 @@
-use crate::alignment;
-use crate::{Color, Font, Point};
+use crate::core::alignment;
+use crate::core::text::{LineHeight, Shaping};
+use crate::core::{Color, Font, Point};
/// A bunch of text that can be drawn to a canvas
#[derive(Debug, Clone)]
@@ -19,12 +20,16 @@ pub struct Text {
pub color: Color,
/// The size of the text
pub size: f32,
+ /// The line height of the text.
+ pub line_height: LineHeight,
/// The font of the text
pub font: Font,
/// The horizontal alignment of the text
pub horizontal_alignment: alignment::Horizontal,
/// The vertical alignment of the text
pub vertical_alignment: alignment::Vertical,
+ /// The shaping strategy of the text.
+ pub shaping: Shaping,
}
impl Default for Text {
@@ -34,9 +39,11 @@ impl Default for Text {
position: Point::ORIGIN,
color: Color::BLACK,
size: 16.0,
- font: Font::Default,
+ line_height: LineHeight::Relative(1.2),
+ font: Font::default(),
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
+ shaping: Shaping::Basic,
}
}
}
diff --git a/graphics/src/gradient.rs b/graphics/src/gradient.rs
index 61e919d6..4db565d8 100644
--- a/graphics/src/gradient.rs
+++ b/graphics/src/gradient.rs
@@ -1,117 +1,196 @@
-//! For creating a Gradient.
-pub mod linear;
+//! A gradient that can be used as a [`Fill`] for some geometry.
+//!
+//! For a gradient that you can use as a background variant for a widget, see [`Gradient`].
+//!
+//! [`Gradient`]: crate::core::Gradient;
+use crate::color;
+use crate::core::gradient::ColorStop;
+use crate::core::{self, Color, Point, Rectangle};
-pub use linear::Linear;
-
-use crate::{Color, Point, Size};
+use bytemuck::{Pod, Zeroable};
+use half::f16;
+use std::cmp::Ordering;
#[derive(Debug, Clone, PartialEq)]
-/// A fill which transitions colors progressively along a direction, either linearly, radially (TBD),
-/// or conically (TBD).
+/// A fill which linearly interpolates colors along a direction.
+///
+/// For a gradient which can be used as a fill for a background of a widget, see [`crate::core::Gradient`].
pub enum Gradient {
/// A linear gradient interpolates colors along a direction from its `start` to its `end`
/// point.
Linear(Linear),
}
+impl From<Linear> for Gradient {
+ fn from(gradient: Linear) -> Self {
+ Self::Linear(gradient)
+ }
+}
+
impl Gradient {
- /// Creates a new linear [`linear::Builder`].
- pub fn linear(position: impl Into<Position>) -> linear::Builder {
- linear::Builder::new(position.into())
+ /// Packs the [`Gradient`] for use in shader code.
+ pub fn pack(&self) -> Packed {
+ match self {
+ Gradient::Linear(linear) => linear.pack(),
+ }
}
}
-#[derive(Debug, Clone, Copy, PartialEq)]
-/// A point along the gradient vector where the specified [`color`] is unmixed.
+/// A linear gradient that can be used in the style of [`Fill`] or [`Stroke`].
///
-/// [`color`]: Self::color
-pub struct ColorStop {
- /// Offset along the gradient vector.
- pub offset: f32,
+/// [`Fill`]: crate::geometry::Fill;
+/// [`Stroke`]: crate::geometry::Stroke;
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Linear {
+ /// The absolute starting position of the gradient.
+ pub start: Point,
- /// The color of the gradient at the specified [`offset`].
- ///
- /// [`offset`]: Self::offset
- pub color: Color,
-}
+ /// The absolute ending position of the gradient.
+ pub end: Point,
-#[derive(Debug)]
-/// The position of the gradient within its bounds.
-pub enum Position {
- /// The gradient will be positioned with respect to two points.
- Absolute {
- /// The starting point of the gradient.
- start: Point,
- /// The ending point of the gradient.
- end: Point,
- },
- /// The gradient will be positioned relative to the provided bounds.
- Relative {
- /// The top left position of the bounds.
- top_left: Point,
- /// The width & height of the bounds.
- size: Size,
- /// The start [Location] of the gradient.
- start: Location,
- /// The end [Location] of the gradient.
- end: Location,
- },
+ /// [`ColorStop`]s along the linear gradient direction.
+ pub stops: [Option<ColorStop>; 8],
}
-impl From<(Point, Point)> for Position {
- fn from((start, end): (Point, Point)) -> Self {
- Self::Absolute { start, end }
+impl Linear {
+ /// Creates a new [`Builder`].
+ pub fn new(start: Point, end: Point) -> Self {
+ Self {
+ start,
+ end,
+ stops: [None; 8],
+ }
+ }
+
+ /// Adds a new [`ColorStop`], defined by an offset and a color, to the gradient.
+ ///
+ /// Any `offset` that is not within `0.0..=1.0` will be silently ignored.
+ ///
+ /// Any stop added after the 8th will be silently ignored.
+ pub fn add_stop(mut self, offset: f32, color: Color) -> Self {
+ if offset.is_finite() && (0.0..=1.0).contains(&offset) {
+ let (Ok(index) | Err(index)) =
+ self.stops.binary_search_by(|stop| match stop {
+ None => Ordering::Greater,
+ Some(stop) => stop.offset.partial_cmp(&offset).unwrap(),
+ });
+
+ if index < 8 {
+ self.stops[index] = Some(ColorStop { offset, color });
+ }
+ } else {
+ log::warn!("Gradient: ColorStop must be within 0.0..=1.0 range.");
+ };
+
+ self
+ }
+
+ /// Adds multiple [`ColorStop`]s to the gradient.
+ ///
+ /// Any stop added after the 8th will be silently ignored.
+ pub fn add_stops(
+ mut self,
+ stops: impl IntoIterator<Item = ColorStop>,
+ ) -> Self {
+ for stop in stops.into_iter() {
+ self = self.add_stop(stop.offset, stop.color)
+ }
+
+ self
+ }
+
+ /// Packs the [`Gradient`] for use in shader code.
+ pub fn pack(&self) -> Packed {
+ let mut colors = [[0u32; 2]; 8];
+ let mut offsets = [f16::from(0u8); 8];
+
+ for (index, stop) in self.stops.iter().enumerate() {
+ let [r, g, b, a] =
+ color::pack(stop.map_or(Color::default(), |s| s.color))
+ .components();
+
+ colors[index] = [
+ pack_f16s([f16::from_f32(r), f16::from_f32(g)]),
+ pack_f16s([f16::from_f32(b), f16::from_f32(a)]),
+ ];
+
+ offsets[index] =
+ stop.map_or(f16::from_f32(2.0), |s| f16::from_f32(s.offset));
+ }
+
+ let offsets = [
+ pack_f16s([offsets[0], offsets[1]]),
+ pack_f16s([offsets[2], offsets[3]]),
+ pack_f16s([offsets[4], offsets[5]]),
+ pack_f16s([offsets[6], offsets[7]]),
+ ];
+
+ let direction = [self.start.x, self.start.y, self.end.x, self.end.y];
+
+ Packed {
+ colors,
+ offsets,
+ direction,
+ }
}
}
-#[derive(Debug, Clone, Copy)]
-/// The location of a relatively-positioned gradient.
-pub enum Location {
- /// Top left.
- TopLeft,
- /// Top.
- Top,
- /// Top right.
- TopRight,
- /// Right.
- Right,
- /// Bottom right.
- BottomRight,
- /// Bottom.
- Bottom,
- /// Bottom left.
- BottomLeft,
- /// Left.
- Left,
+/// Packed [`Gradient`] data for use in shader code.
+#[derive(Debug, Copy, Clone, PartialEq, Zeroable, Pod)]
+#[repr(C)]
+pub struct Packed {
+ // 8 colors, each channel = 16 bit float, 2 colors packed into 1 u32
+ colors: [[u32; 2]; 8],
+ // 8 offsets, 8x 16 bit floats packed into 4 u32s
+ offsets: [u32; 4],
+ direction: [f32; 4],
}
-impl Location {
- fn to_absolute(self, top_left: Point, size: Size) -> Point {
- match self {
- Location::TopLeft => top_left,
- Location::Top => {
- Point::new(top_left.x + size.width / 2.0, top_left.y)
- }
- Location::TopRight => {
- Point::new(top_left.x + size.width, top_left.y)
- }
- Location::Right => Point::new(
- top_left.x + size.width,
- top_left.y + size.height / 2.0,
- ),
- Location::BottomRight => {
- Point::new(top_left.x + size.width, top_left.y + size.height)
- }
- Location::Bottom => Point::new(
- top_left.x + size.width / 2.0,
- top_left.y + size.height,
- ),
- Location::BottomLeft => {
- Point::new(top_left.x, top_left.y + size.height)
+/// Creates a new [`Packed`] gradient for use in shader code.
+pub fn pack(gradient: &core::Gradient, bounds: Rectangle) -> Packed {
+ match gradient {
+ core::Gradient::Linear(linear) => {
+ let mut colors = [[0u32; 2]; 8];
+ let mut offsets = [f16::from(0u8); 8];
+
+ for (index, stop) in linear.stops.iter().enumerate() {
+ let [r, g, b, a] =
+ color::pack(stop.map_or(Color::default(), |s| s.color))
+ .components();
+
+ colors[index] = [
+ pack_f16s([f16::from_f32(r), f16::from_f32(g)]),
+ pack_f16s([f16::from_f32(b), f16::from_f32(a)]),
+ ];
+
+ offsets[index] = stop
+ .map_or(f16::from_f32(2.0), |s| f16::from_f32(s.offset));
}
- Location::Left => {
- Point::new(top_left.x, top_left.y + size.height / 2.0)
+
+ let offsets = [
+ pack_f16s([offsets[0], offsets[1]]),
+ pack_f16s([offsets[2], offsets[3]]),
+ pack_f16s([offsets[4], offsets[5]]),
+ pack_f16s([offsets[6], offsets[7]]),
+ ];
+
+ let (start, end) = linear.angle.to_distance(&bounds);
+
+ let direction = [start.x, start.y, end.x, end.y];
+
+ Packed {
+ colors,
+ offsets,
+ direction,
}
}
}
}
+
+/// Packs two f16s into one u32.
+fn pack_f16s(f: [f16; 2]) -> u32 {
+ let one = (f[0].to_bits() as u32) << 16;
+ let two = f[1].to_bits() as u32;
+
+ one | two
+}
diff --git a/graphics/src/gradient/linear.rs b/graphics/src/gradient/linear.rs
deleted file mode 100644
index c886db47..00000000
--- a/graphics/src/gradient/linear.rs
+++ /dev/null
@@ -1,112 +0,0 @@
-//! Linear gradient builder & definition.
-use crate::gradient::{ColorStop, Gradient, Position};
-use crate::{Color, Point};
-
-/// A linear gradient that can be used in the style of [`Fill`] or [`Stroke`].
-///
-/// [`Fill`]: crate::widget::canvas::Fill
-/// [`Stroke`]: crate::widget::canvas::Stroke
-#[derive(Debug, Clone, PartialEq)]
-pub struct Linear {
- /// The point where the linear gradient begins.
- pub start: Point,
- /// The point where the linear gradient ends.
- pub end: Point,
- /// [`ColorStop`]s along the linear gradient path.
- pub color_stops: Vec<ColorStop>,
-}
-
-/// A [`Linear`] builder.
-#[derive(Debug)]
-pub struct Builder {
- start: Point,
- end: Point,
- stops: Vec<ColorStop>,
- error: Option<BuilderError>,
-}
-
-impl Builder {
- /// Creates a new [`Builder`].
- pub fn new(position: Position) -> Self {
- let (start, end) = match position {
- Position::Absolute { start, end } => (start, end),
- Position::Relative {
- top_left,
- size,
- start,
- end,
- } => (
- start.to_absolute(top_left, size),
- end.to_absolute(top_left, size),
- ),
- };
-
- Self {
- start,
- end,
- stops: vec![],
- error: None,
- }
- }
-
- /// Adds a new stop, defined by an offset and a color, to the gradient.
- ///
- /// `offset` must be between `0.0` and `1.0` or the gradient cannot be built.
- ///
- /// Note: when using the [`glow`] backend, any color stop added after the 16th
- /// will not be displayed.
- ///
- /// On the [`wgpu`] backend this limitation does not exist (technical limit is 524,288 stops).
- ///
- /// [`glow`]: https://docs.rs/iced_glow
- /// [`wgpu`]: https://docs.rs/iced_wgpu
- pub fn add_stop(mut self, offset: f32, color: Color) -> Self {
- if offset.is_finite() && (0.0..=1.0).contains(&offset) {
- match self.stops.binary_search_by(|stop| {
- stop.offset.partial_cmp(&offset).unwrap()
- }) {
- Ok(_) => {
- self.error = Some(BuilderError::DuplicateOffset(offset))
- }
- Err(index) => {
- self.stops.insert(index, ColorStop { offset, color });
- }
- }
- } else {
- self.error = Some(BuilderError::InvalidOffset(offset))
- };
-
- self
- }
-
- /// Builds the linear [`Gradient`] of this [`Builder`].
- ///
- /// Returns `BuilderError` if gradient in invalid.
- pub fn build(self) -> Result<Gradient, BuilderError> {
- if self.stops.is_empty() {
- Err(BuilderError::MissingColorStop)
- } else if let Some(error) = self.error {
- Err(error)
- } else {
- Ok(Gradient::Linear(Linear {
- start: self.start,
- end: self.end,
- color_stops: self.stops,
- }))
- }
- }
-}
-
-/// An error that happened when building a [`Linear`] gradient.
-#[derive(Debug, thiserror::Error)]
-pub enum BuilderError {
- #[error("Gradients must contain at least one color stop.")]
- /// Gradients must contain at least one color stop.
- MissingColorStop,
- #[error("Offset {0} must be a unique, finite number.")]
- /// Offsets in a gradient must all be unique & finite.
- DuplicateOffset(f32),
- #[error("Offset {0} must be between 0.0..=1.0.")]
- /// Offsets in a gradient must be between 0.0..=1.0.
- InvalidOffset(f32),
-}
diff --git a/graphics/src/image.rs b/graphics/src/image.rs
index 04f4ff9d..6b43f4a8 100644
--- a/graphics/src/image.rs
+++ b/graphics/src/image.rs
@@ -1,10 +1,95 @@
-//! Render images.
-#[cfg(feature = "image_rs")]
-pub mod raster;
+//! Load and operate on images.
+use crate::core::image::{Data, Handle};
-#[cfg(feature = "svg")]
-pub mod vector;
+use bitflags::bitflags;
-pub mod storage;
+pub use ::image as image_rs;
-pub use storage::Storage;
+/// Tries to load an image by its [`Handle`].
+pub fn load(handle: &Handle) -> image_rs::ImageResult<image_rs::DynamicImage> {
+ match handle.data() {
+ Data::Path(path) => {
+ let image = ::image::open(path)?;
+
+ let operation = std::fs::File::open(path)
+ .ok()
+ .map(std::io::BufReader::new)
+ .and_then(|mut reader| Operation::from_exif(&mut reader).ok())
+ .unwrap_or_else(Operation::empty);
+
+ Ok(operation.perform(image))
+ }
+ Data::Bytes(bytes) => {
+ let image = ::image::load_from_memory(bytes)?;
+ let operation =
+ Operation::from_exif(&mut std::io::Cursor::new(bytes))
+ .ok()
+ .unwrap_or_else(Operation::empty);
+
+ Ok(operation.perform(image))
+ }
+ Data::Rgba {
+ width,
+ height,
+ pixels,
+ } => {
+ if let Some(image) = image_rs::ImageBuffer::from_vec(
+ *width,
+ *height,
+ pixels.to_vec(),
+ ) {
+ Ok(image_rs::DynamicImage::ImageRgba8(image))
+ } else {
+ Err(image_rs::error::ImageError::Limits(
+ image_rs::error::LimitError::from_kind(
+ image_rs::error::LimitErrorKind::DimensionError,
+ ),
+ ))
+ }
+ }
+ }
+}
+
+bitflags! {
+ struct Operation: u8 {
+ const FLIP_HORIZONTALLY = 0b001;
+ const ROTATE_180 = 0b010;
+ const FLIP_DIAGONALLY = 0b100;
+ }
+}
+
+impl Operation {
+ // Meaning of the returned value is described e.g. at:
+ // https://magnushoff.com/articles/jpeg-orientation/
+ fn from_exif<R>(reader: &mut R) -> Result<Self, exif::Error>
+ where
+ R: std::io::BufRead + std::io::Seek,
+ {
+ let exif = exif::Reader::new().read_from_container(reader)?;
+
+ Ok(exif
+ .get_field(exif::Tag::Orientation, exif::In::PRIMARY)
+ .and_then(|field| field.value.get_uint(0))
+ .and_then(|value| u8::try_from(value).ok())
+ .and_then(|value| Self::from_bits(value.saturating_sub(1)))
+ .unwrap_or_else(Self::empty))
+ }
+
+ fn perform(self, mut image: image::DynamicImage) -> image::DynamicImage {
+ use image::imageops;
+
+ if self.contains(Self::FLIP_DIAGONALLY) {
+ imageops::flip_vertical_in_place(&mut image)
+ }
+
+ if self.contains(Self::ROTATE_180) {
+ imageops::rotate180_in_place(&mut image);
+ }
+
+ if self.contains(Self::FLIP_HORIZONTALLY) {
+ imageops::flip_horizontal_in_place(&mut image);
+ }
+
+ image
+ }
+}
diff --git a/graphics/src/image/raster.rs b/graphics/src/image/raster.rs
deleted file mode 100644
index da46c30f..00000000
--- a/graphics/src/image/raster.rs
+++ /dev/null
@@ -1,242 +0,0 @@
-//! Raster image loading and caching.
-use crate::image::Storage;
-use crate::Size;
-
-use iced_native::image;
-
-use bitflags::bitflags;
-use std::collections::{HashMap, HashSet};
-
-/// Entry in cache corresponding to an image handle
-#[derive(Debug)]
-pub enum Memory<T: Storage> {
- /// Image data on host
- Host(::image_rs::ImageBuffer<::image_rs::Rgba<u8>, Vec<u8>>),
- /// Storage entry
- Device(T::Entry),
- /// Image not found
- NotFound,
- /// Invalid image data
- Invalid,
-}
-
-impl<T: Storage> Memory<T> {
- /// Width and height of image
- pub fn dimensions(&self) -> Size<u32> {
- use crate::image::storage::Entry;
-
- match self {
- Memory::Host(image) => {
- let (width, height) = image.dimensions();
-
- Size::new(width, height)
- }
- Memory::Device(entry) => entry.size(),
- Memory::NotFound => Size::new(1, 1),
- Memory::Invalid => Size::new(1, 1),
- }
- }
-}
-
-/// Caches image raster data
-#[derive(Debug)]
-pub struct Cache<T: Storage> {
- map: HashMap<u64, Memory<T>>,
- hits: HashSet<u64>,
-}
-
-impl<T: Storage> Cache<T> {
- /// Load image
- pub fn load(&mut self, handle: &image::Handle) -> &mut Memory<T> {
- if self.contains(handle) {
- return self.get(handle).unwrap();
- }
-
- let memory = match handle.data() {
- image::Data::Path(path) => {
- if let Ok(image) = image_rs::open(path) {
- let operation = std::fs::File::open(path)
- .ok()
- .map(std::io::BufReader::new)
- .and_then(|mut reader| {
- Operation::from_exif(&mut reader).ok()
- })
- .unwrap_or_else(Operation::empty);
-
- Memory::Host(operation.perform(image.to_rgba8()))
- } else {
- Memory::NotFound
- }
- }
- image::Data::Bytes(bytes) => {
- if let Ok(image) = image_rs::load_from_memory(bytes) {
- let operation =
- Operation::from_exif(&mut std::io::Cursor::new(bytes))
- .ok()
- .unwrap_or_else(Operation::empty);
-
- Memory::Host(operation.perform(image.to_rgba8()))
- } else {
- Memory::Invalid
- }
- }
- image::Data::Rgba {
- width,
- height,
- pixels,
- } => {
- if let Some(image) = image_rs::ImageBuffer::from_vec(
- *width,
- *height,
- pixels.to_vec(),
- ) {
- Memory::Host(image)
- } else {
- Memory::Invalid
- }
- }
- };
-
- self.insert(handle, memory);
- self.get(handle).unwrap()
- }
-
- /// Load image and upload raster data
- pub fn upload(
- &mut self,
- handle: &image::Handle,
- state: &mut T::State<'_>,
- storage: &mut T,
- ) -> Option<&T::Entry> {
- let memory = self.load(handle);
-
- if let Memory::Host(image) = memory {
- let (width, height) = image.dimensions();
-
- let entry = storage.upload(width, height, image, state)?;
-
- *memory = Memory::Device(entry);
- }
-
- if let Memory::Device(allocation) = memory {
- Some(allocation)
- } else {
- None
- }
- }
-
- /// Trim cache misses from cache
- pub fn trim(&mut self, storage: &mut T, state: &mut T::State<'_>) {
- let hits = &self.hits;
-
- self.map.retain(|k, memory| {
- let retain = hits.contains(k);
-
- if !retain {
- if let Memory::Device(entry) = memory {
- storage.remove(entry, state);
- }
- }
-
- retain
- });
-
- self.hits.clear();
- }
-
- fn get(&mut self, handle: &image::Handle) -> Option<&mut Memory<T>> {
- let _ = self.hits.insert(handle.id());
-
- self.map.get_mut(&handle.id())
- }
-
- fn insert(&mut self, handle: &image::Handle, memory: Memory<T>) {
- let _ = self.map.insert(handle.id(), memory);
- }
-
- fn contains(&self, handle: &image::Handle) -> bool {
- self.map.contains_key(&handle.id())
- }
-}
-
-impl<T: Storage> Default for Cache<T> {
- fn default() -> Self {
- Self {
- map: HashMap::new(),
- hits: HashSet::new(),
- }
- }
-}
-
-bitflags! {
- struct Operation: u8 {
- const FLIP_HORIZONTALLY = 0b001;
- const ROTATE_180 = 0b010;
- const FLIP_DIAGONALLY = 0b100;
- }
-}
-
-impl Operation {
- // Meaning of the returned value is described e.g. at:
- // https://magnushoff.com/articles/jpeg-orientation/
- fn from_exif<R>(reader: &mut R) -> Result<Self, exif::Error>
- where
- R: std::io::BufRead + std::io::Seek,
- {
- let exif = exif::Reader::new().read_from_container(reader)?;
-
- Ok(exif
- .get_field(exif::Tag::Orientation, exif::In::PRIMARY)
- .and_then(|field| field.value.get_uint(0))
- .and_then(|value| u8::try_from(value).ok())
- .and_then(|value| Self::from_bits(value.saturating_sub(1)))
- .unwrap_or_else(Self::empty))
- }
-
- fn perform<P>(
- self,
- image: image_rs::ImageBuffer<P, Vec<P::Subpixel>>,
- ) -> image_rs::ImageBuffer<P, Vec<P::Subpixel>>
- where
- P: image_rs::Pixel + 'static,
- {
- use image_rs::imageops;
-
- let mut image = if self.contains(Self::FLIP_DIAGONALLY) {
- flip_diagonally(image)
- } else {
- image
- };
-
- if self.contains(Self::ROTATE_180) {
- imageops::rotate180_in_place(&mut image);
- }
-
- if self.contains(Self::FLIP_HORIZONTALLY) {
- imageops::flip_horizontal_in_place(&mut image);
- }
-
- image
- }
-}
-
-fn flip_diagonally<I>(
- image: I,
-) -> image_rs::ImageBuffer<I::Pixel, Vec<<I::Pixel as image_rs::Pixel>::Subpixel>>
-where
- I: image_rs::GenericImage,
- I::Pixel: 'static,
-{
- let (width, height) = image.dimensions();
- let mut out = image_rs::ImageBuffer::new(height, width);
-
- for x in 0..width {
- for y in 0..height {
- let p = image.get_pixel(x, y);
-
- out.put_pixel(y, x, p);
- }
- }
-
- out
-}
diff --git a/graphics/src/image/storage.rs b/graphics/src/image/storage.rs
index 1b5b5c35..4caa6141 100644
--- a/graphics/src/image/storage.rs
+++ b/graphics/src/image/storage.rs
@@ -1,5 +1,5 @@
//! Store images.
-use crate::Size;
+use iced_core::Size;
use std::fmt::Debug;
diff --git a/graphics/src/layer/quad.rs b/graphics/src/layer/quad.rs
deleted file mode 100644
index 0d8bde9d..00000000
--- a/graphics/src/layer/quad.rs
+++ /dev/null
@@ -1,30 +0,0 @@
-/// 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; 4],
-
- /// The border width of the [`Quad`].
- pub border_width: f32,
-}
-
-#[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
index d39dd90c..af374a2f 100644
--- a/graphics/src/lib.rs
+++ b/graphics/src/lib.rs
@@ -20,7 +20,7 @@
)]
#![forbid(rust_2018_idioms)]
#![allow(clippy::inherent_to_string, clippy::type_complexity)]
-#![cfg_attr(docsrs, feature(doc_cfg))]
+#![cfg_attr(docsrs, feature(doc_auto_cfg))]
mod antialiasing;
mod error;
mod primitive;
@@ -28,28 +28,29 @@ mod transformation;
mod viewport;
pub mod backend;
-pub mod font;
+pub mod color;
+pub mod compositor;
+pub mod damage;
pub mod gradient;
-pub mod image;
-pub mod layer;
-pub mod overlay;
+pub mod mesh;
pub mod renderer;
-pub mod triangle;
-pub mod widget;
-pub mod window;
+
+#[cfg(feature = "geometry")]
+pub mod geometry;
+
+#[cfg(feature = "image")]
+pub mod image;
pub use antialiasing::Antialiasing;
pub use backend::Backend;
+pub use compositor::Compositor;
+pub use damage::Damage;
pub use error::Error;
pub use gradient::Gradient;
-pub use layer::Layer;
+pub use mesh::Mesh;
pub use primitive::Primitive;
pub use renderer::Renderer;
pub use transformation::Transformation;
pub use viewport::Viewport;
-pub use window::compositor;
-pub use iced_native::alignment;
-pub use iced_native::{
- Alignment, Background, Color, Font, Point, Rectangle, Size, Vector,
-};
+pub use iced_core as core;
diff --git a/graphics/src/mesh.rs b/graphics/src/mesh.rs
new file mode 100644
index 00000000..cfb5a60f
--- /dev/null
+++ b/graphics/src/mesh.rs
@@ -0,0 +1,76 @@
+//! Draw triangles!
+use crate::color;
+use crate::core::{Rectangle, Size};
+use crate::gradient;
+use crate::Damage;
+
+use bytemuck::{Pod, Zeroable};
+
+/// A low-level primitive to render a mesh of triangles.
+#[derive(Debug, Clone, PartialEq)]
+pub enum Mesh {
+ /// A mesh with a solid color.
+ Solid {
+ /// The vertices and indices of the mesh.
+ buffers: Indexed<SolidVertex2D>,
+
+ /// The size of the drawable region of the mesh.
+ ///
+ /// Any geometry that falls out of this region will be clipped.
+ size: Size,
+ },
+ /// A mesh with a gradient.
+ Gradient {
+ /// The vertices and indices of the mesh.
+ buffers: Indexed<GradientVertex2D>,
+
+ /// The size of the drawable region of the mesh.
+ ///
+ /// Any geometry that falls out of this region will be clipped.
+ size: Size,
+ },
+}
+
+impl Damage for Mesh {
+ fn bounds(&self) -> Rectangle {
+ match self {
+ Self::Solid { size, .. } | Self::Gradient { size, .. } => {
+ Rectangle::with_size(*size)
+ }
+ }
+ }
+}
+
+/// A set of [`Vertex2D`] and indices representing a list of triangles.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Indexed<T> {
+ /// The vertices of the mesh
+ pub vertices: Vec<T>,
+
+ /// 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 a color.
+#[derive(Copy, Clone, Debug, PartialEq, Zeroable, Pod)]
+#[repr(C)]
+pub struct SolidVertex2D {
+ /// The vertex position in 2D space.
+ pub position: [f32; 2],
+
+ /// The color of the vertex in __linear__ RGBA.
+ pub color: color::Packed,
+}
+
+/// A vertex which contains 2D position & packed gradient data.
+#[derive(Copy, Clone, Debug, PartialEq, Zeroable, Pod)]
+#[repr(C)]
+pub struct GradientVertex2D {
+ /// The vertex position in 2D space.
+ pub position: [f32; 2],
+
+ /// The packed vertex data of the gradient.
+ pub gradient: gradient::Packed,
+}
diff --git a/graphics/src/overlay/menu.rs b/graphics/src/overlay/menu.rs
deleted file mode 100644
index 8b489e5e..00000000
--- a/graphics/src/overlay/menu.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-//! Build and show dropdown menus.
-
-pub use iced_style::menu::{Appearance, StyleSheet};
diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs
index 5a163a2f..7592a410 100644
--- a/graphics/src/primitive.rs
+++ b/graphics/src/primitive.rs
@@ -1,23 +1,15 @@
-use iced_native::image;
-use iced_native::svg;
-use iced_native::{Background, Color, Font, Rectangle, Size, Vector};
-
-use crate::alignment;
-use crate::gradient::Gradient;
-use crate::triangle;
+//! Draw using different graphical primitives.
+use crate::core::alignment;
+use crate::core::image;
+use crate::core::svg;
+use crate::core::text;
+use crate::core::{Background, Color, Font, Rectangle, Vector};
use std::sync::Arc;
/// A rendering primitive.
-#[derive(Debug, Clone)]
-pub enum Primitive {
- /// An empty primitive
- None,
- /// A group of primitives
- Group {
- /// The primitives of the group
- primitives: Vec<Primitive>,
- },
+#[derive(Debug, Clone, PartialEq)]
+pub enum Primitive<T> {
/// A text primitive
Text {
/// The contents of the text
@@ -26,14 +18,18 @@ pub enum Primitive {
bounds: Rectangle,
/// The color of the text
color: Color,
- /// The size of the text
+ /// The size of the text in logical pixels
size: f32,
+ /// The line height of the text
+ line_height: text::LineHeight,
/// The font of the text
font: Font,
/// The horizontal alignment of the text
horizontal_alignment: alignment::Horizontal,
/// The vertical alignment of the text
vertical_alignment: alignment::Vertical,
+ /// The shaping strategy of the text.
+ shaping: text::Shaping,
},
/// A quad primitive
Quad {
@@ -41,7 +37,7 @@ pub enum Primitive {
bounds: Rectangle,
/// The background of the quad
background: Background,
- /// The border radius of the quad
+ /// The border radii of the quad
border_radius: [f32; 4],
/// The border width of the quad
border_width: f32,
@@ -66,12 +62,17 @@ pub enum Primitive {
/// The bounds of the viewport
bounds: Rectangle,
},
+ /// A group of primitives
+ Group {
+ /// The primitives of the group
+ primitives: Vec<Primitive<T>>,
+ },
/// A clip primitive
Clip {
/// The bounds of the clip
bounds: Rectangle,
/// The content of the clip
- content: Box<Primitive>,
+ content: Box<Primitive<T>>,
},
/// A primitive that applies a translation
Translate {
@@ -79,47 +80,39 @@ pub enum Primitive {
translation: Vector,
/// The primitive to translate
- content: Box<Primitive>,
- },
- /// A low-level primitive to render a mesh of triangles with a solid color.
- ///
- /// It can be used to render many kinds of geometry freely.
- SolidMesh {
- /// The vertices and indices of the mesh.
- buffers: triangle::Mesh2D<triangle::ColoredVertex2D>,
-
- /// The size of the drawable region of the mesh.
- ///
- /// Any geometry that falls out of this region will be clipped.
- size: Size,
- },
- /// A low-level primitive to render a mesh of triangles with a gradient.
- ///
- /// It can be used to render many kinds of geometry freely.
- GradientMesh {
- /// The vertices and indices of the mesh.
- buffers: triangle::Mesh2D<triangle::Vertex2D>,
-
- /// The size of the drawable region of the mesh.
- ///
- /// Any geometry that falls out of this region will be clipped.
- size: Size,
-
- /// The [`Gradient`] to apply to the mesh.
- gradient: Gradient,
+ content: Box<Primitive<T>>,
},
/// A cached primitive.
///
/// This can be useful if you are implementing a widget where primitive
/// generation is expensive.
- Cached {
+ Cache {
/// The cached primitive
- cache: Arc<Primitive>,
+ content: Arc<Primitive<T>>,
},
+ /// A backend-specific primitive.
+ Custom(T),
}
-impl Default for Primitive {
- fn default() -> Primitive {
- Primitive::None
+impl<T> Primitive<T> {
+ /// Creates a [`Primitive::Group`].
+ pub fn group(primitives: Vec<Self>) -> Self {
+ Self::Group { primitives }
+ }
+
+ /// Creates a [`Primitive::Clip`].
+ pub fn clip(self, bounds: Rectangle) -> Self {
+ Self::Clip {
+ bounds,
+ content: Box::new(self),
+ }
+ }
+
+ /// Creates a [`Primitive::Translate`].
+ pub fn translate(self, translation: Vector) -> Self {
+ Self::Translate {
+ translation,
+ content: Box::new(self),
+ }
}
}
diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs
index 34b6eb1d..c0cec60a 100644
--- a/graphics/src/renderer.rs
+++ b/graphics/src/renderer.rs
@@ -1,22 +1,24 @@
//! Create a renderer from a [`Backend`].
use crate::backend::{self, Backend};
-use crate::{Primitive, Vector};
-use iced_native::image;
-use iced_native::layout;
-use iced_native::renderer;
-use iced_native::svg;
-use iced_native::text::{self, Text};
-use iced_native::{Background, Color, Element, Font, Point, Rectangle, Size};
-
-pub use iced_native::renderer::Style;
-
+use crate::Primitive;
+
+use iced_core::image;
+use iced_core::layout;
+use iced_core::renderer;
+use iced_core::svg;
+use iced_core::text::{self, Text};
+use iced_core::{
+ Background, Color, Element, Font, Point, Rectangle, Size, Vector,
+};
+
+use std::borrow::Cow;
use std::marker::PhantomData;
/// A backend-agnostic renderer that supports all the built-in widgets.
#[derive(Debug)]
pub struct Renderer<B: Backend, Theme> {
backend: B,
- primitives: Vec<Primitive>,
+ primitives: Vec<Primitive<B::Primitive>>,
theme: PhantomData<Theme>,
}
@@ -30,27 +32,60 @@ impl<B: Backend, T> Renderer<B, T> {
}
}
- /// Returns the [`Backend`] of the [`Renderer`].
+ /// Returns a reference to the [`Backend`] of the [`Renderer`].
pub fn backend(&self) -> &B {
&self.backend
}
/// Enqueues the given [`Primitive`] in the [`Renderer`] for drawing.
- pub fn draw_primitive(&mut self, primitive: Primitive) {
+ pub fn draw_primitive(&mut self, primitive: Primitive<B::Primitive>) {
self.primitives.push(primitive);
}
/// Runs the given closure with the [`Backend`] and the recorded primitives
/// of the [`Renderer`].
- pub fn with_primitives(&mut self, f: impl FnOnce(&mut B, &[Primitive])) {
- f(&mut self.backend, &self.primitives);
+ pub fn with_primitives<O>(
+ &mut self,
+ f: impl FnOnce(&mut B, &[Primitive<B::Primitive>]) -> O,
+ ) -> O {
+ f(&mut self.backend, &self.primitives)
+ }
+
+ /// Starts recording a new layer.
+ pub fn start_layer(&mut self) -> Vec<Primitive<B::Primitive>> {
+ std::mem::take(&mut self.primitives)
+ }
+
+ /// Ends the recording of a layer.
+ pub fn end_layer(
+ &mut self,
+ primitives: Vec<Primitive<B::Primitive>>,
+ bounds: Rectangle,
+ ) {
+ let layer = std::mem::replace(&mut self.primitives, primitives);
+
+ self.primitives.push(Primitive::group(layer).clip(bounds));
+ }
+
+ /// Starts recording a translation.
+ pub fn start_translation(&mut self) -> Vec<Primitive<B::Primitive>> {
+ std::mem::take(&mut self.primitives)
+ }
+
+ /// Ends the recording of a translation.
+ pub fn end_translation(
+ &mut self,
+ primitives: Vec<Primitive<B::Primitive>>,
+ translation: Vector,
+ ) {
+ let layer = std::mem::replace(&mut self.primitives, primitives);
+
+ self.primitives
+ .push(Primitive::group(layer).translate(translation));
}
}
-impl<B, T> iced_native::Renderer for Renderer<B, T>
-where
- B: Backend,
-{
+impl<B: Backend, T> iced_core::Renderer for Renderer<B, T> {
type Theme = T;
fn layout<Message>(
@@ -58,27 +93,17 @@ where
element: &Element<'_, Message, Self>,
limits: &layout::Limits,
) -> layout::Node {
- let layout = element.as_widget().layout(self, limits);
-
self.backend.trim_measurements();
- layout
+ element.as_widget().layout(self, limits)
}
fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self)) {
- let current_primitives = std::mem::take(&mut self.primitives);
+ let current = self.start_layer();
f(self);
- let layer_primitives =
- std::mem::replace(&mut self.primitives, current_primitives);
-
- self.primitives.push(Primitive::Clip {
- bounds,
- content: Box::new(Primitive::Group {
- primitives: layer_primitives,
- }),
- });
+ self.end_layer(current, bounds);
}
fn with_translation(
@@ -86,19 +111,11 @@ where
translation: Vector,
f: impl FnOnce(&mut Self),
) {
- let current_primitives = std::mem::take(&mut self.primitives);
+ let current = self.start_translation();
f(self);
- let layer_primitives =
- std::mem::replace(&mut self.primitives, current_primitives);
-
- self.primitives.push(Primitive::Translate {
- translation,
- content: Box::new(Primitive::Group {
- primitives: layer_primitives,
- }),
- });
+ self.end_translation(current, translation);
}
fn fill_quad(
@@ -130,6 +147,10 @@ where
const CHECKMARK_ICON: char = B::CHECKMARK_ICON;
const ARROW_DOWN_ICON: char = B::ARROW_DOWN_ICON;
+ fn default_font(&self) -> Self::Font {
+ self.backend().default_font()
+ }
+
fn default_size(&self) -> f32 {
self.backend().default_size()
}
@@ -138,40 +159,59 @@ where
&self,
content: &str,
size: f32,
+ line_height: text::LineHeight,
font: Font,
bounds: Size,
- ) -> (f32, f32) {
- self.backend().measure(content, size, font, bounds)
+ shaping: text::Shaping,
+ ) -> Size {
+ self.backend().measure(
+ content,
+ size,
+ line_height,
+ font,
+ bounds,
+ shaping,
+ )
}
fn hit_test(
&self,
content: &str,
size: f32,
+ line_height: text::LineHeight,
font: Font,
bounds: Size,
+ shaping: text::Shaping,
point: Point,
nearest_only: bool,
) -> Option<text::Hit> {
self.backend().hit_test(
content,
size,
+ line_height,
font,
bounds,
+ shaping,
point,
nearest_only,
)
}
+ fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
+ self.backend.load_font(bytes);
+ }
+
fn fill_text(&mut self, text: Text<'_, Self::Font>) {
self.primitives.push(Primitive::Text {
content: text.content.to_string(),
bounds: text.bounds,
size: text.size,
+ line_height: text.line_height,
color: text.color,
font: text.font,
horizontal_alignment: text.horizontal_alignment,
vertical_alignment: text.vertical_alignment,
+ shaping: text.shaping,
});
}
}
@@ -187,7 +227,7 @@ where
}
fn draw(&mut self, handle: image::Handle, bounds: Rectangle) {
- self.draw_primitive(Primitive::Image { handle, bounds })
+ self.primitives.push(Primitive::Image { handle, bounds })
}
}
@@ -205,7 +245,7 @@ where
color: Option<Color>,
bounds: Rectangle,
) {
- self.draw_primitive(Primitive::Svg {
+ self.primitives.push(Primitive::Svg {
handle,
color,
bounds,
diff --git a/graphics/src/triangle.rs b/graphics/src/triangle.rs
deleted file mode 100644
index f52b2339..00000000
--- a/graphics/src/triangle.rs
+++ /dev/null
@@ -1,33 +0,0 @@
-//! 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<T> {
- /// The vertices of the mesh
- pub vertices: Vec<T>,
-
- /// 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.
-#[derive(Copy, Clone, Debug, Zeroable, Pod)]
-#[repr(C)]
-pub struct Vertex2D {
- /// The vertex position in 2D space.
- pub position: [f32; 2],
-}
-
-/// A two-dimensional vertex with a color.
-#[derive(Copy, Clone, Debug, Zeroable, Pod)]
-#[repr(C)]
-pub struct ColoredVertex2D {
- /// The vertex position in 2D space.
- pub position: [f32; 2],
-
- /// The color of the vertex in __linear__ RGBA.
- pub color: [f32; 4],
-}
diff --git a/graphics/src/viewport.rs b/graphics/src/viewport.rs
index 2c0b541a..5792555d 100644
--- a/graphics/src/viewport.rs
+++ b/graphics/src/viewport.rs
@@ -1,4 +1,6 @@
-use crate::{Size, Transformation};
+use crate::Transformation;
+
+use iced_core::Size;
/// A viewing region for displaying computer graphics.
#[derive(Debug, Clone)]
diff --git a/graphics/src/widget/canvas/cache.rs b/graphics/src/widget/canvas/cache.rs
deleted file mode 100644
index 52217bbb..00000000
--- a/graphics/src/widget/canvas/cache.rs
+++ /dev/null
@@ -1,100 +0,0 @@
-use crate::widget::canvas::{Frame, Geometry};
-use crate::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(&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 FnOnce(&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
deleted file mode 100644
index 9588d129..00000000
--- a/graphics/src/widget/canvas/cursor.rs
+++ /dev/null
@@ -1,64 +0,0 @@
-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/geometry.rs b/graphics/src/widget/canvas/geometry.rs
deleted file mode 100644
index e8ac621d..00000000
--- a/graphics/src/widget/canvas/geometry.rs
+++ /dev/null
@@ -1,24 +0,0 @@
-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
- }
-}
diff --git a/graphics/src/window.rs b/graphics/src/window.rs
deleted file mode 100644
index a38b81f3..00000000
--- a/graphics/src/window.rs
+++ /dev/null
@@ -1,10 +0,0 @@
-//! Draw graphics to window surfaces.
-pub mod compositor;
-
-#[cfg(feature = "opengl")]
-pub mod gl_compositor;
-
-pub use compositor::Compositor;
-
-#[cfg(feature = "opengl")]
-pub use gl_compositor::GLCompositor;
diff --git a/graphics/src/window/gl_compositor.rs b/graphics/src/window/gl_compositor.rs
deleted file mode 100644
index a45a7ca1..00000000
--- a/graphics/src/window/gl_compositor.rs
+++ /dev/null
@@ -1,71 +0,0 @@
-//! A compositor is responsible for initializing a renderer and managing window
-//! surfaces.
-use crate::compositor::Information;
-use crate::{Color, Error, Size, Viewport};
-
-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.
- ///
- /// # Safety
- /// The `loader_function` should resolve to valid OpenGL bindings.
- ///
- /// [`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>);
-
- /// Returns [`Information`] used by this [`GLCompositor`].
- fn fetch_information(&self) -> Information;
-
- /// Presents the primitives of the [`Renderer`] to the next frame of the
- /// [`GLCompositor`].
- ///
- /// [`Renderer`]: crate::Renderer
- fn present<T: AsRef<str>>(
- &mut self,
- renderer: &mut Self::Renderer,
- viewport: &Viewport,
- background_color: Color,
- overlay: &[T],
- );
-}
diff --git a/lazy/Cargo.toml b/lazy/Cargo.toml
deleted file mode 100644
index c739b312..00000000
--- a/lazy/Cargo.toml
+++ /dev/null
@@ -1,18 +0,0 @@
-[package]
-name = "iced_lazy"
-version = "0.5.0"
-authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
-edition = "2021"
-description = "Lazy widgets for Iced"
-license = "MIT"
-repository = "https://github.com/iced-rs/iced"
-documentation = "https://docs.rs/iced_lazy"
-keywords = ["gui", "ui", "graphics", "interface", "widgets"]
-categories = ["gui"]
-
-[dependencies]
-ouroboros = "0.13"
-
-[dependencies.iced_native]
-version = "0.9"
-path = "../native"
diff --git a/native/README.md b/native/README.md
deleted file mode 100644
index 996daa76..00000000
--- a/native/README.md
+++ /dev/null
@@ -1,37 +0,0 @@
-# `iced_native`
-[![Documentation](https://docs.rs/iced_native/badge.svg)][documentation]
-[![Crates.io](https://img.shields.io/crates/v/iced_native.svg)](https://crates.io/crates/iced_native)
-[![License](https://img.shields.io/crates/l/iced_native.svg)](https://github.com/iced-rs/iced/blob/master/LICENSE)
-[![Discord Server](https://img.shields.io/discord/628993209984614400?label=&labelColor=6A7EC2&logo=discord&logoColor=ffffff&color=7389D8)](https://discord.gg/3xZJ65GAhd)
-
-`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
-
-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>
-
-[documentation]: https://docs.rs/iced_native
-[`iced_core`]: ../core
-[`iced_winit`]: ../winit
-[`druid`]: https://github.com/xi-editor/druid
-[`raw-window-handle`]: https://github.com/rust-windowing/raw-window-handle
-
-## Installation
-Add `iced_native` as a dependency in your `Cargo.toml`:
-
-```toml
-iced_native = "0.9"
-```
-
-__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/iced-rs/iced/releases
diff --git a/native/src/mouse.rs b/native/src/mouse.rs
deleted file mode 100644
index 9ee406cf..00000000
--- a/native/src/mouse.rs
+++ /dev/null
@@ -1,6 +0,0 @@
-//! Track mouse events.
-
-pub mod click;
-
-pub use click::Click;
-pub use iced_core::mouse::*;
diff --git a/native/src/runtime.rs b/native/src/runtime.rs
deleted file mode 100644
index 5b0a6925..00000000
--- a/native/src/runtime.rs
+++ /dev/null
@@ -1,18 +0,0 @@
-//! Run commands and subscriptions.
-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`]: 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 2a216ebe..e69de29b 100644
--- a/native/src/subscription.rs
+++ b/native/src/subscription.rs
@@ -1,248 +0,0 @@
-//! Listen to external events in your application.
-use crate::event::{self, Event};
-use crate::window;
-use crate::Hasher;
-
-use iced_futures::futures::{self, Future, Stream};
-use iced_futures::{BoxStream, MaybeSend};
-
-use std::hash::Hash;
-
-/// A request to listen to external events.
-///
-/// Besides performing async actions on demand with [`Command`], most
-/// applications also need to listen to external events passively.
-///
-/// A [`Subscription`] is normally provided to some runtime, like a [`Command`],
-/// and it will generate events as long as the user keeps requesting it.
-///
-/// For instance, you can use a [`Subscription`] to listen to a WebSocket
-/// connection, keyboard presses, mouse events, time ticks, etc.
-///
-/// [`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.
-pub type EventStream = BoxStream<(Event, event::Status)>;
-
-/// A native [`Subscription`] tracker.
-pub type Tracker =
- iced_futures::subscription::Tracker<Hasher, (Event, event::Status)>;
-
-pub use iced_futures::subscription::Recipe;
-
-/// Returns a [`Subscription`] to all the ignored runtime events.
-///
-/// This subscription will notify your application of any [`Event`] that was
-/// not captured by any widget.
-pub fn events() -> Subscription<Event> {
- events_with(|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 + MaybeSend,
-{
- #[derive(Hash)]
- struct EventsWith;
-
- Subscription::from_recipe(Runner {
- id: (EventsWith, f),
- spawn: move |events| {
- use futures::future;
- use futures::stream::StreamExt;
-
- events.filter_map(move |(event, status)| {
- future::ready(match event {
- Event::Window(_, window::Event::RedrawRequested(_)) => None,
- _ => f(event, status),
- })
- })
- },
- })
-}
-
-pub(crate) fn raw_events<Message>(
- f: fn(Event, event::Status) -> Option<Message>,
-) -> Subscription<Message>
-where
- Message: 'static + MaybeSend,
-{
- #[derive(Hash)]
- struct RawEvents;
-
- Subscription::from_recipe(Runner {
- id: (RawEvents, f),
- spawn: move |events| {
- use futures::future;
- use futures::stream::StreamExt;
-
- events.filter_map(move |(event, status)| {
- future::ready(f(event, status))
- })
- },
- })
-}
-
-/// Returns a [`Subscription`] that will call the given function to create and
-/// asynchronously run the given [`Stream`].
-pub fn run<S, Message>(builder: fn() -> S) -> Subscription<Message>
-where
- S: Stream<Item = Message> + MaybeSend + 'static,
- Message: 'static,
-{
- Subscription::from_recipe(Runner {
- id: builder,
- spawn: move |_| builder(),
- })
-}
-
-/// Returns a [`Subscription`] that will create and asynchronously run the
-/// given [`Stream`].
-///
-/// The `id` will be used to uniquely identify the [`Subscription`].
-pub fn run_with_id<I, S, Message>(id: I, stream: S) -> Subscription<Message>
-where
- I: Hash + 'static,
- S: Stream<Item = Message> + MaybeSend + 'static,
- Message: 'static,
-{
- Subscription::from_recipe(Runner {
- id,
- spawn: move |_| stream,
- })
-}
-
-/// Returns a [`Subscription`] that will create and asynchronously run a
-/// [`Stream`] that will call the provided closure to produce every `Message`.
-///
-/// The `id` will be used to uniquely identify the [`Subscription`].
-///
-/// # Creating an asynchronous worker with bidirectional communication
-/// You can leverage this helper to create a [`Subscription`] that spawns
-/// an asynchronous worker in the background and establish a channel of
-/// communication with an `iced` application.
-///
-/// You can achieve this by creating an `mpsc` channel inside the closure
-/// and returning the `Sender` as a `Message` for the `Application`:
-///
-/// ```
-/// use iced_native::subscription::{self, Subscription};
-/// use iced_native::futures::channel::mpsc;
-///
-/// pub enum Event {
-/// Ready(mpsc::Sender<Input>),
-/// WorkFinished,
-/// // ...
-/// }
-///
-/// enum Input {
-/// DoSomeWork,
-/// // ...
-/// }
-///
-/// enum State {
-/// Starting,
-/// Ready(mpsc::Receiver<Input>),
-/// }
-///
-/// fn some_worker() -> Subscription<Event> {
-/// struct SomeWorker;
-///
-/// subscription::unfold(std::any::TypeId::of::<SomeWorker>(), State::Starting, |state| async move {
-/// match state {
-/// State::Starting => {
-/// // Create channel
-/// let (sender, receiver) = mpsc::channel(100);
-///
-/// (Some(Event::Ready(sender)), State::Ready(receiver))
-/// }
-/// State::Ready(mut receiver) => {
-/// use iced_native::futures::StreamExt;
-///
-/// // Read next input sent from `Application`
-/// let input = receiver.select_next_some().await;
-///
-/// match input {
-/// Input::DoSomeWork => {
-/// // Do some async work...
-///
-/// // Finally, we can optionally return a message to tell the
-/// // `Application` the work is done
-/// (Some(Event::WorkFinished), State::Ready(receiver))
-/// }
-/// }
-/// }
-/// }
-/// })
-/// }
-/// ```
-///
-/// Check out the [`websocket`] example, which showcases this pattern to maintain a WebSocket
-/// connection open.
-///
-/// [`websocket`]: https://github.com/iced-rs/iced/tree/0.8/examples/websocket
-pub fn unfold<I, T, Fut, Message>(
- id: I,
- initial: T,
- mut f: impl FnMut(T) -> Fut + MaybeSend + Sync + 'static,
-) -> Subscription<Message>
-where
- I: Hash + 'static,
- T: MaybeSend + 'static,
- Fut: Future<Output = (Option<Message>, T)> + MaybeSend + 'static,
- Message: 'static + MaybeSend,
-{
- use futures::future::{self, FutureExt};
- use futures::stream::StreamExt;
-
- run_with_id(
- id,
- futures::stream::unfold(initial, move |state| f(state).map(Some))
- .filter_map(future::ready),
- )
-}
-
-struct Runner<I, F, S, Message>
-where
- F: FnOnce(EventStream) -> S,
- S: Stream<Item = Message>,
-{
- id: I,
- spawn: F,
-}
-
-impl<I, S, F, Message> Recipe<Hasher, (Event, event::Status)>
- for Runner<I, F, S, Message>
-where
- I: Hash + 'static,
- F: FnOnce(EventStream) -> S,
- S: Stream<Item = Message> + MaybeSend + 'static,
-{
- type Output = Message;
-
- fn hash(&self, state: &mut Hasher) {
- std::any::TypeId::of::<I>().hash(state);
- self.id.hash(state);
- }
-
- fn stream(self: Box<Self>, input: EventStream) -> BoxStream<Self::Output> {
- iced_futures::boxed_stream((self.spawn)(input))
- }
-}
diff --git a/native/src/text.rs b/native/src/text.rs
deleted file mode 100644
index 55c3cfd3..00000000
--- a/native/src/text.rs
+++ /dev/null
@@ -1,114 +0,0 @@
-//! Draw and interact with text.
-use crate::alignment;
-use crate::{Color, Point, Rectangle, Size, Vector};
-
-/// A paragraph.
-#[derive(Debug, Clone, Copy)]
-pub struct Text<'a, Font> {
- /// The content of the paragraph.
- pub content: &'a str,
-
- /// The bounds of the paragraph.
- pub bounds: Rectangle,
-
- /// The size of the [`Text`].
- pub size: f32,
-
- /// The color of the [`Text`].
- pub color: Color,
-
- /// The font of the [`Text`].
- pub font: Font,
-
- /// The horizontal alignment of the [`Text`].
- pub horizontal_alignment: alignment::Horizontal,
-
- /// The vertical alignment of the [`Text`].
- pub vertical_alignment: alignment::Vertical,
-}
-
-/// The result of hit testing on text.
-#[derive(Debug, Clone, Copy, PartialEq)]
-pub enum Hit {
- /// The point was within the bounds of the returned character index.
- CharOffset(usize),
- /// The provided point was not within the bounds of a glyph. The index
- /// of the character with the closest centeroid position is returned,
- /// as well as its delta.
- NearestCharOffset(usize, Vector),
-}
-
-impl Hit {
- /// Computes the cursor position of the [`Hit`] .
- pub fn cursor(self) -> usize {
- match self {
- Self::CharOffset(i) => i,
- Self::NearestCharOffset(i, delta) => {
- if delta.x > f32::EPSILON {
- i + 1
- } else {
- i
- }
- }
- }
- }
-}
-
-/// A renderer capable of measuring and drawing [`Text`].
-pub trait Renderer: crate::Renderer {
- /// The font type used.
- type Font: Default + Clone;
-
- /// The icon font of the backend.
- const ICON_FONT: Self::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) -> f32;
-
- /// Measures the text in the given bounds and returns the minimum boundaries
- /// that can fit the contents.
- fn measure(
- &self,
- content: &str,
- size: f32,
- font: Self::Font,
- bounds: Size,
- ) -> (f32, f32);
-
- /// Measures the width of the text as if it were laid out in a single line.
- fn measure_width(&self, content: &str, size: f32, font: Self::Font) -> f32 {
- let (width, _) = self.measure(content, size, font, Size::INFINITY);
-
- width
- }
-
- /// Tests whether the provided point is within the boundaries of text
- /// laid out with the given parameters, returning information about
- /// the nearest character.
- ///
- /// If `nearest_only` is true, the hit test does not consider whether the
- /// the point is interior to any glyph bounds, returning only the character
- /// with the nearest centeroid.
- fn hit_test(
- &self,
- contents: &str,
- size: f32,
- font: Self::Font,
- bounds: Size,
- point: Point,
- nearest_only: bool,
- ) -> Option<Hit>;
-
- /// Draws the given [`Text`].
- fn fill_text(&mut self, text: Text<'_, Self::Font>);
-}
diff --git a/native/src/widget/action.rs b/native/src/widget/action.rs
deleted file mode 100644
index 3f1b6b6c..00000000
--- a/native/src/widget/action.rs
+++ /dev/null
@@ -1,154 +0,0 @@
-use crate::widget::operation::{
- self, Focusable, Operation, Scrollable, TextInput,
-};
-use crate::widget::Id;
-
-use iced_futures::MaybeSend;
-
-use std::any::Any;
-use std::rc::Rc;
-
-/// An operation to be performed on the widget tree.
-#[allow(missing_debug_implementations)]
-pub struct Action<T>(Box<dyn Operation<T>>);
-
-impl<T> Action<T> {
- /// Creates a new [`Action`] with the given [`Operation`].
- pub fn new(operation: impl Operation<T> + 'static) -> Self {
- Self(Box::new(operation))
- }
-
- /// Maps the output of an [`Action`] using the given function.
- pub fn map<A>(
- self,
- f: impl Fn(T) -> A + 'static + MaybeSend + Sync,
- ) -> Action<A>
- where
- T: 'static,
- A: 'static,
- {
- Action(Box::new(Map {
- operation: self.0,
- f: Rc::new(f),
- }))
- }
-
- /// Consumes the [`Action`] and returns the internal [`Operation`].
- pub fn into_operation(self) -> Box<dyn Operation<T>> {
- self.0
- }
-}
-
-#[allow(missing_debug_implementations)]
-struct Map<A, B> {
- operation: Box<dyn Operation<A>>,
- f: Rc<dyn Fn(A) -> B>,
-}
-
-impl<A, B> Operation<B> for Map<A, B>
-where
- A: 'static,
- B: 'static,
-{
- fn container(
- &mut self,
- id: Option<&Id>,
- operate_on_children: &mut dyn FnMut(&mut dyn Operation<B>),
- ) {
- struct MapRef<'a, A> {
- operation: &'a mut dyn Operation<A>,
- }
-
- impl<'a, A, B> Operation<B> for MapRef<'a, A> {
- fn container(
- &mut self,
- id: Option<&Id>,
- operate_on_children: &mut dyn FnMut(&mut dyn Operation<B>),
- ) {
- let Self { operation, .. } = self;
-
- operation.container(id, &mut |operation| {
- operate_on_children(&mut MapRef { operation });
- });
- }
-
- fn scrollable(
- &mut self,
- state: &mut dyn Scrollable,
- id: Option<&Id>,
- ) {
- self.operation.scrollable(state, id);
- }
-
- fn focusable(
- &mut self,
- state: &mut dyn Focusable,
- id: Option<&Id>,
- ) {
- self.operation.focusable(state, id);
- }
-
- fn text_input(
- &mut self,
- state: &mut dyn TextInput,
- id: Option<&Id>,
- ) {
- self.operation.text_input(state, id);
- }
-
- fn custom(&mut self, state: &mut dyn Any, id: Option<&Id>) {
- self.operation.custom(state, id);
- }
- }
-
- let Self { operation, .. } = self;
-
- MapRef {
- operation: operation.as_mut(),
- }
- .container(id, operate_on_children);
- }
-
- fn focusable(
- &mut self,
- state: &mut dyn operation::Focusable,
- id: Option<&Id>,
- ) {
- self.operation.focusable(state, id);
- }
-
- fn scrollable(
- &mut self,
- state: &mut dyn operation::Scrollable,
- id: Option<&Id>,
- ) {
- self.operation.scrollable(state, id);
- }
-
- fn text_input(
- &mut self,
- state: &mut dyn operation::TextInput,
- id: Option<&Id>,
- ) {
- self.operation.text_input(state, id);
- }
-
- fn custom(&mut self, state: &mut dyn Any, id: Option<&Id>) {
- self.operation.custom(state, id);
- }
-
- fn finish(&self) -> operation::Outcome<B> {
- match self.operation.finish() {
- operation::Outcome::None => operation::Outcome::None,
- operation::Outcome::Some(output) => {
- operation::Outcome::Some((self.f)(output))
- }
- operation::Outcome::Chain(next) => {
- operation::Outcome::Chain(Box::new(Map {
- operation: next,
- f: self.f.clone(),
- }))
- }
- }
- }
-}
diff --git a/native/src/widget/helpers.rs b/native/src/widget/helpers.rs
deleted file mode 100644
index d13eca75..00000000
--- a/native/src/widget/helpers.rs
+++ /dev/null
@@ -1,317 +0,0 @@
-//! Helper functions to create pure widgets.
-use crate::overlay;
-use crate::widget;
-use crate::{Element, Length, Pixels};
-
-use std::borrow::Cow;
-use std::ops::RangeInclusive;
-
-/// Creates a [`Column`] with the given children.
-///
-/// [`Column`]: widget::Column
-#[macro_export]
-macro_rules! column {
- () => (
- $crate::widget::Column::new()
- );
- ($($x:expr),+ $(,)?) => (
- $crate::widget::Column::with_children(vec![$($crate::Element::from($x)),+])
- );
-}
-
-/// Creates a [`Row`] with the given children.
-///
-/// [`Row`]: widget::Row
-#[macro_export]
-macro_rules! row {
- () => (
- $crate::widget::Row::new()
- );
- ($($x:expr),+ $(,)?) => (
- $crate::widget::Row::with_children(vec![$($crate::Element::from($x)),+])
- );
-}
-
-/// Creates a new [`Container`] with the provided content.
-///
-/// [`Container`]: widget::Container
-pub fn container<'a, Message, Renderer>(
- content: impl Into<Element<'a, Message, Renderer>>,
-) -> widget::Container<'a, Message, Renderer>
-where
- Renderer: crate::Renderer,
- Renderer::Theme: widget::container::StyleSheet,
-{
- widget::Container::new(content)
-}
-
-/// Creates a new [`Column`] with the given children.
-///
-/// [`Column`]: widget::Column
-pub fn column<Message, Renderer>(
- children: Vec<Element<'_, Message, Renderer>>,
-) -> widget::Column<'_, Message, Renderer> {
- widget::Column::with_children(children)
-}
-
-/// Creates a new [`Row`] with the given children.
-///
-/// [`Row`]: widget::Row
-pub fn row<Message, Renderer>(
- children: Vec<Element<'_, Message, Renderer>>,
-) -> widget::Row<'_, Message, Renderer> {
- widget::Row::with_children(children)
-}
-
-/// Creates a new [`Scrollable`] with the provided content.
-///
-/// [`Scrollable`]: widget::Scrollable
-pub fn scrollable<'a, Message, Renderer>(
- content: impl Into<Element<'a, Message, Renderer>>,
-) -> widget::Scrollable<'a, Message, Renderer>
-where
- Renderer: crate::Renderer,
- Renderer::Theme: widget::scrollable::StyleSheet,
-{
- widget::Scrollable::new(content)
-}
-
-/// Creates a new [`Button`] with the provided content.
-///
-/// [`Button`]: widget::Button
-pub fn button<'a, Message, Renderer>(
- content: impl Into<Element<'a, Message, Renderer>>,
-) -> widget::Button<'a, Message, Renderer>
-where
- Renderer: crate::Renderer,
- Renderer::Theme: widget::button::StyleSheet,
- <Renderer::Theme as widget::button::StyleSheet>::Style: Default,
-{
- widget::Button::new(content)
-}
-
-/// Creates a new [`Tooltip`] with the provided content, tooltip text, and [`tooltip::Position`].
-///
-/// [`Tooltip`]: widget::Tooltip
-/// [`tooltip::Position`]: widget::tooltip::Position
-pub fn tooltip<'a, Message, Renderer>(
- content: impl Into<Element<'a, Message, Renderer>>,
- tooltip: impl ToString,
- position: widget::tooltip::Position,
-) -> widget::Tooltip<'a, Message, Renderer>
-where
- Renderer: crate::text::Renderer,
- Renderer::Theme: widget::container::StyleSheet + widget::text::StyleSheet,
-{
- widget::Tooltip::new(content, tooltip.to_string(), position)
-}
-
-/// Creates a new [`Text`] widget with the provided content.
-///
-/// [`Text`]: widget::Text
-pub fn text<'a, Renderer>(text: impl ToString) -> widget::Text<'a, Renderer>
-where
- Renderer: crate::text::Renderer,
- Renderer::Theme: widget::text::StyleSheet,
-{
- widget::Text::new(text.to_string())
-}
-
-/// Creates a new [`Checkbox`].
-///
-/// [`Checkbox`]: widget::Checkbox
-pub fn checkbox<'a, Message, Renderer>(
- label: impl Into<String>,
- is_checked: bool,
- f: impl Fn(bool) -> Message + 'a,
-) -> widget::Checkbox<'a, Message, Renderer>
-where
- Renderer: crate::text::Renderer,
- Renderer::Theme: widget::checkbox::StyleSheet + widget::text::StyleSheet,
-{
- widget::Checkbox::new(label, is_checked, f)
-}
-
-/// Creates a new [`Radio`].
-///
-/// [`Radio`]: widget::Radio
-pub fn radio<Message, Renderer, V>(
- label: impl Into<String>,
- value: V,
- selected: Option<V>,
- on_click: impl FnOnce(V) -> Message,
-) -> widget::Radio<Message, Renderer>
-where
- Message: Clone,
- Renderer: crate::text::Renderer,
- Renderer::Theme: widget::radio::StyleSheet,
- V: Copy + Eq,
-{
- widget::Radio::new(value, label, selected, on_click)
-}
-
-/// Creates a new [`Toggler`].
-///
-/// [`Toggler`]: widget::Toggler
-pub fn toggler<'a, Message, Renderer>(
- label: impl Into<Option<String>>,
- is_checked: bool,
- f: impl Fn(bool) -> Message + 'a,
-) -> widget::Toggler<'a, Message, Renderer>
-where
- Renderer: crate::text::Renderer,
- Renderer::Theme: widget::toggler::StyleSheet,
-{
- widget::Toggler::new(label, is_checked, f)
-}
-
-/// Creates a new [`TextInput`].
-///
-/// [`TextInput`]: widget::TextInput
-pub fn text_input<'a, Message, Renderer>(
- placeholder: &str,
- value: &str,
- on_change: impl Fn(String) -> Message + 'a,
-) -> widget::TextInput<'a, Message, Renderer>
-where
- Message: Clone,
- Renderer: crate::text::Renderer,
- Renderer::Theme: widget::text_input::StyleSheet,
-{
- widget::TextInput::new(placeholder, value, on_change)
-}
-
-/// Creates a new [`Slider`].
-///
-/// [`Slider`]: widget::Slider
-pub fn slider<'a, T, Message, Renderer>(
- range: std::ops::RangeInclusive<T>,
- value: T,
- on_change: impl Fn(T) -> Message + 'a,
-) -> widget::Slider<'a, T, Message, Renderer>
-where
- T: Copy + From<u8> + std::cmp::PartialOrd,
- Message: Clone,
- Renderer: crate::Renderer,
- Renderer::Theme: widget::slider::StyleSheet,
-{
- widget::Slider::new(range, value, on_change)
-}
-
-/// Creates a new [`VerticalSlider`].
-///
-/// [`VerticalSlider`]: widget::VerticalSlider
-pub fn vertical_slider<'a, T, Message, Renderer>(
- range: std::ops::RangeInclusive<T>,
- value: T,
- on_change: impl Fn(T) -> Message + 'a,
-) -> widget::VerticalSlider<'a, T, Message, Renderer>
-where
- T: Copy + From<u8> + std::cmp::PartialOrd,
- Message: Clone,
- Renderer: crate::Renderer,
- Renderer::Theme: widget::slider::StyleSheet,
-{
- widget::VerticalSlider::new(range, value, on_change)
-}
-
-/// Creates a new [`PickList`].
-///
-/// [`PickList`]: widget::PickList
-pub fn pick_list<'a, Message, Renderer, T>(
- options: impl Into<Cow<'a, [T]>>,
- selected: Option<T>,
- on_selected: impl Fn(T) -> Message + 'a,
-) -> widget::PickList<'a, T, Message, Renderer>
-where
- T: ToString + Eq + 'static,
- [T]: ToOwned<Owned = Vec<T>>,
- Renderer: crate::text::Renderer,
- Renderer::Theme: widget::pick_list::StyleSheet
- + widget::scrollable::StyleSheet
- + overlay::menu::StyleSheet
- + widget::container::StyleSheet,
- <Renderer::Theme as overlay::menu::StyleSheet>::Style:
- From<<Renderer::Theme as widget::pick_list::StyleSheet>::Style>,
-{
- widget::PickList::new(options, selected, on_selected)
-}
-
-/// Creates a new [`Image`].
-///
-/// [`Image`]: widget::Image
-pub fn image<Handle>(handle: impl Into<Handle>) -> widget::Image<Handle> {
- widget::Image::new(handle.into())
-}
-
-/// Creates a new horizontal [`Space`] with the given [`Length`].
-///
-/// [`Space`]: widget::Space
-pub fn horizontal_space(width: impl Into<Length>) -> widget::Space {
- widget::Space::with_width(width)
-}
-
-/// Creates a new vertical [`Space`] with the given [`Length`].
-///
-/// [`Space`]: widget::Space
-pub fn vertical_space(height: impl Into<Length>) -> widget::Space {
- widget::Space::with_height(height)
-}
-
-/// Creates a horizontal [`Rule`] with the given height.
-///
-/// [`Rule`]: widget::Rule
-pub fn horizontal_rule<Renderer>(
- height: impl Into<Pixels>,
-) -> widget::Rule<Renderer>
-where
- Renderer: crate::Renderer,
- Renderer::Theme: widget::rule::StyleSheet,
-{
- widget::Rule::horizontal(height)
-}
-
-/// Creates a vertical [`Rule`] with the given width.
-///
-/// [`Rule`]: widget::Rule
-pub fn vertical_rule<Renderer>(
- width: impl Into<Pixels>,
-) -> widget::Rule<Renderer>
-where
- Renderer: crate::Renderer,
- Renderer::Theme: widget::rule::StyleSheet,
-{
- widget::Rule::vertical(width)
-}
-
-/// Creates a new [`ProgressBar`].
-///
-/// It expects:
-/// * an inclusive range of possible values, and
-/// * the current value of the [`ProgressBar`].
-///
-/// [`ProgressBar`]: widget::ProgressBar
-pub fn progress_bar<Renderer>(
- range: RangeInclusive<f32>,
- value: f32,
-) -> widget::ProgressBar<Renderer>
-where
- Renderer: crate::Renderer,
- Renderer::Theme: widget::progress_bar::StyleSheet,
-{
- widget::ProgressBar::new(range, value)
-}
-
-/// Creates a new [`Svg`] widget from the given [`Handle`].
-///
-/// [`Svg`]: widget::Svg
-/// [`Handle`]: widget::svg::Handle
-pub fn svg<Renderer>(
- handle: impl Into<widget::svg::Handle>,
-) -> widget::Svg<Renderer>
-where
- Renderer: crate::svg::Renderer,
- Renderer::Theme: widget::svg::StyleSheet,
-{
- widget::Svg::new(handle)
-}
diff --git a/native/src/widget/operation.rs b/native/src/widget/operation.rs
deleted file mode 100644
index 53688a21..00000000
--- a/native/src/widget/operation.rs
+++ /dev/null
@@ -1,112 +0,0 @@
-//! Query or update internal widget state.
-pub mod focusable;
-pub mod scrollable;
-pub mod text_input;
-
-pub use focusable::Focusable;
-pub use scrollable::Scrollable;
-pub use text_input::TextInput;
-
-use crate::widget::Id;
-
-use std::any::Any;
-use std::fmt;
-
-/// A piece of logic that can traverse the widget tree of an application in
-/// order to query or update some widget state.
-pub trait Operation<T> {
- /// Operates on a widget that contains other widgets.
- ///
- /// The `operate_on_children` function can be called to return control to
- /// the widget tree and keep traversing it.
- fn container(
- &mut self,
- id: Option<&Id>,
- operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
- );
-
- /// Operates on a widget that can be focused.
- fn focusable(&mut self, _state: &mut dyn Focusable, _id: Option<&Id>) {}
-
- /// Operates on a widget that can be scrolled.
- fn scrollable(&mut self, _state: &mut dyn Scrollable, _id: Option<&Id>) {}
-
- /// Operates on a widget that has text input.
- fn text_input(&mut self, _state: &mut dyn TextInput, _id: Option<&Id>) {}
-
- /// Operates on a custom widget with some state.
- fn custom(&mut self, _state: &mut dyn Any, _id: Option<&Id>) {}
-
- /// Finishes the [`Operation`] and returns its [`Outcome`].
- fn finish(&self) -> Outcome<T> {
- Outcome::None
- }
-}
-
-/// The result of an [`Operation`].
-pub enum Outcome<T> {
- /// The [`Operation`] produced no result.
- None,
-
- /// The [`Operation`] produced some result.
- Some(T),
-
- /// The [`Operation`] needs to be followed by another [`Operation`].
- Chain(Box<dyn Operation<T>>),
-}
-
-impl<T> fmt::Debug for Outcome<T>
-where
- T: fmt::Debug,
-{
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::None => write!(f, "Outcome::None"),
- Self::Some(output) => write!(f, "Outcome::Some({output:?})"),
- Self::Chain(_) => write!(f, "Outcome::Chain(...)"),
- }
- }
-}
-
-/// Produces an [`Operation`] that applies the given [`Operation`] to the
-/// children of a container with the given [`Id`].
-pub fn scoped<T: 'static>(
- target: Id,
- operation: impl Operation<T> + 'static,
-) -> impl Operation<T> {
- struct ScopedOperation<Message> {
- target: Id,
- operation: Box<dyn Operation<Message>>,
- }
-
- impl<Message: 'static> Operation<Message> for ScopedOperation<Message> {
- fn container(
- &mut self,
- id: Option<&Id>,
- operate_on_children: &mut dyn FnMut(&mut dyn Operation<Message>),
- ) {
- if id == Some(&self.target) {
- operate_on_children(self.operation.as_mut());
- } else {
- operate_on_children(self);
- }
- }
-
- fn finish(&self) -> Outcome<Message> {
- match self.operation.finish() {
- Outcome::Chain(next) => {
- Outcome::Chain(Box::new(ScopedOperation {
- target: self.target.clone(),
- operation: next,
- }))
- }
- outcome => outcome,
- }
- }
- }
-
- ScopedOperation {
- target,
- operation: Box::new(operation),
- }
-}
diff --git a/native/src/window.rs b/native/src/window.rs
index 660cd54f..e69de29b 100644
--- a/native/src/window.rs
+++ b/native/src/window.rs
@@ -1,38 +0,0 @@
-//! Build window-based GUI applications.
-mod action;
-mod event;
-mod icon;
-mod id;
-mod mode;
-mod position;
-mod redraw_request;
-mod settings;
-mod user_attention;
-
-pub use action::Action;
-pub use event::Event;
-pub use icon::Icon;
-pub use id::Id;
-pub use mode::Mode;
-pub use position::Position;
-pub use redraw_request::RedrawRequest;
-pub use settings::Settings;
-pub use user_attention::UserAttention;
-
-use crate::subscription::{self, Subscription};
-use crate::time::Instant;
-
-/// Subscribes to the frames of the window of the running application.
-///
-/// The resulting [`Subscription`] will produce items at a rate equal to the
-/// refresh rate of the window. Note that this rate may be variable, as it is
-/// normally managed by the graphics driver and/or the OS.
-///
-/// In any case, this [`Subscription`] is useful to smoothly draw application-driven
-/// animations without missing any frames.
-pub fn frames() -> Subscription<Instant> {
- subscription::raw_events(|event, _status| match event {
- crate::Event::Window(_, Event::RedrawRequested(at)) => Some(at),
- _ => None,
- })
-}
diff --git a/native/src/window/icon.rs b/native/src/window/icon.rs
deleted file mode 100644
index 08a6acfd..00000000
--- a/native/src/window/icon.rs
+++ /dev/null
@@ -1,12 +0,0 @@
-//! Attach an icon to the window of your application.
-
-/// The icon of a window.
-#[derive(Debug, Clone)]
-pub struct Icon {
- /// The __rgba__ color data of the window [`Icon`].
- pub rgba: Vec<u8>,
- /// The width of the window [`Icon`].
- pub width: u32,
- /// The height of the window [`Icon`].
- pub height: u32,
-}
diff --git a/native/src/window/position.rs b/native/src/window/position.rs
deleted file mode 100644
index c260c29e..00000000
--- a/native/src/window/position.rs
+++ /dev/null
@@ -1,22 +0,0 @@
-/// The position of a window in a given screen.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum Position {
- /// The platform-specific default position for a new window.
- Default,
- /// The window is completely centered on the screen.
- Centered,
- /// The window is positioned with specific coordinates: `(X, Y)`.
- ///
- /// When the decorations of the window are enabled, Windows 10 will add some
- /// invisible padding to the window. This padding gets included in the
- /// position. So if you have decorations enabled and want the window to be
- /// at (0, 0) you would have to set the position to
- /// `(PADDING_X, PADDING_Y)`.
- Specific(i32, i32),
-}
-
-impl Default for Position {
- fn default() -> Self {
- Self::Default
- }
-}
diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml
new file mode 100644
index 00000000..fda2bc7b
--- /dev/null
+++ b/renderer/Cargo.toml
@@ -0,0 +1,30 @@
+[package]
+name = "iced_renderer"
+version = "0.1.0"
+edition = "2021"
+
+[features]
+wgpu = ["iced_wgpu"]
+image = ["iced_tiny_skia/image", "iced_wgpu?/image"]
+svg = ["iced_tiny_skia/svg", "iced_wgpu?/svg"]
+geometry = ["iced_graphics/geometry", "iced_tiny_skia/geometry", "iced_wgpu?/geometry"]
+tracing = ["iced_wgpu?/tracing"]
+web-colors = ["iced_wgpu?/web-colors"]
+
+[dependencies]
+raw-window-handle = "0.5"
+thiserror = "1"
+log = "0.4"
+
+[dependencies.iced_graphics]
+version = "0.8"
+path = "../graphics"
+
+[dependencies.iced_tiny_skia]
+version = "0.1"
+path = "../tiny_skia"
+
+[dependencies.iced_wgpu]
+version = "0.10"
+path = "../wgpu"
+optional = true
diff --git a/renderer/src/backend.rs b/renderer/src/backend.rs
new file mode 100644
index 00000000..3f229b52
--- /dev/null
+++ b/renderer/src/backend.rs
@@ -0,0 +1,100 @@
+use crate::core::text;
+use crate::core::{Font, Point, Size};
+use crate::graphics::backend;
+
+use std::borrow::Cow;
+
+#[allow(clippy::large_enum_variant)]
+pub enum Backend {
+ TinySkia(iced_tiny_skia::Backend),
+ #[cfg(feature = "wgpu")]
+ Wgpu(iced_wgpu::Backend),
+}
+
+macro_rules! delegate {
+ ($backend:expr, $name:ident, $body:expr) => {
+ match $backend {
+ Self::TinySkia($name) => $body,
+ #[cfg(feature = "wgpu")]
+ Self::Wgpu($name) => $body,
+ }
+ };
+}
+
+impl backend::Text for Backend {
+ const ICON_FONT: Font = Font::with_name("Iced-Icons");
+ const CHECKMARK_ICON: char = '\u{f00c}';
+ const ARROW_DOWN_ICON: char = '\u{e800}';
+
+ fn default_font(&self) -> Font {
+ delegate!(self, backend, backend.default_font())
+ }
+
+ fn default_size(&self) -> f32 {
+ delegate!(self, backend, backend.default_size())
+ }
+
+ fn measure(
+ &self,
+ contents: &str,
+ size: f32,
+ line_height: text::LineHeight,
+ font: Font,
+ bounds: Size,
+ shaping: text::Shaping,
+ ) -> Size {
+ delegate!(
+ self,
+ backend,
+ backend.measure(contents, size, line_height, font, bounds, shaping)
+ )
+ }
+
+ fn hit_test(
+ &self,
+ contents: &str,
+ size: f32,
+ line_height: text::LineHeight,
+ font: Font,
+ bounds: Size,
+ shaping: text::Shaping,
+ position: Point,
+ nearest_only: bool,
+ ) -> Option<text::Hit> {
+ delegate!(
+ self,
+ backend,
+ backend.hit_test(
+ contents,
+ size,
+ line_height,
+ font,
+ bounds,
+ shaping,
+ position,
+ nearest_only,
+ )
+ )
+ }
+
+ fn load_font(&mut self, font: Cow<'static, [u8]>) {
+ delegate!(self, backend, backend.load_font(font));
+ }
+}
+
+#[cfg(feature = "image")]
+impl backend::Image for Backend {
+ fn dimensions(&self, handle: &crate::core::image::Handle) -> Size<u32> {
+ delegate!(self, backend, backend.dimensions(handle))
+ }
+}
+
+#[cfg(feature = "svg")]
+impl backend::Svg for Backend {
+ fn viewport_dimensions(
+ &self,
+ handle: &crate::core::svg::Handle,
+ ) -> Size<u32> {
+ delegate!(self, backend, backend.viewport_dimensions(handle))
+ }
+}
diff --git a/renderer/src/compositor.rs b/renderer/src/compositor.rs
new file mode 100644
index 00000000..8b17a4b0
--- /dev/null
+++ b/renderer/src/compositor.rs
@@ -0,0 +1,262 @@
+use crate::core::Color;
+use crate::graphics::compositor::{Information, SurfaceError};
+use crate::graphics::{Error, Viewport};
+use crate::{Renderer, Settings};
+
+use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle};
+use std::env;
+
+pub enum Compositor<Theme> {
+ TinySkia(iced_tiny_skia::window::Compositor<Theme>),
+ #[cfg(feature = "wgpu")]
+ Wgpu(iced_wgpu::window::Compositor<Theme>),
+}
+
+pub enum Surface {
+ TinySkia(iced_tiny_skia::window::Surface),
+ #[cfg(feature = "wgpu")]
+ Wgpu(iced_wgpu::window::Surface),
+}
+
+impl<Theme> crate::graphics::Compositor for Compositor<Theme> {
+ type Settings = Settings;
+ type Renderer = Renderer<Theme>;
+ type Surface = Surface;
+
+ fn new<W: HasRawWindowHandle + HasRawDisplayHandle>(
+ settings: Self::Settings,
+ compatible_window: Option<&W>,
+ ) -> Result<(Self, Self::Renderer), Error> {
+ let candidates =
+ Candidate::list_from_env().unwrap_or(Candidate::default_list());
+
+ let mut error = Error::GraphicsAdapterNotFound;
+
+ for candidate in candidates {
+ match candidate.build(settings, compatible_window) {
+ Ok((compositor, renderer)) => {
+ return Ok((compositor, renderer))
+ }
+ Err(new_error) => {
+ error = new_error;
+ }
+ }
+ }
+
+ Err(error)
+ }
+
+ fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>(
+ &mut self,
+ window: &W,
+ width: u32,
+ height: u32,
+ ) -> Surface {
+ match self {
+ Self::TinySkia(compositor) => Surface::TinySkia(
+ compositor.create_surface(window, width, height),
+ ),
+ #[cfg(feature = "wgpu")]
+ Self::Wgpu(compositor) => {
+ Surface::Wgpu(compositor.create_surface(window, width, height))
+ }
+ }
+ }
+
+ fn configure_surface(
+ &mut self,
+ surface: &mut Surface,
+ width: u32,
+ height: u32,
+ ) {
+ match (self, surface) {
+ (Self::TinySkia(compositor), Surface::TinySkia(surface)) => {
+ compositor.configure_surface(surface, width, height);
+ }
+ #[cfg(feature = "wgpu")]
+ (Self::Wgpu(compositor), Surface::Wgpu(surface)) => {
+ compositor.configure_surface(surface, width, height);
+ }
+ #[allow(unreachable_patterns)]
+ _ => panic!(
+ "The provided surface is not compatible with the compositor."
+ ),
+ }
+ }
+
+ fn fetch_information(&self) -> Information {
+ match self {
+ Self::TinySkia(compositor) => compositor.fetch_information(),
+ #[cfg(feature = "wgpu")]
+ Self::Wgpu(compositor) => compositor.fetch_information(),
+ }
+ }
+
+ fn present<T: AsRef<str>>(
+ &mut self,
+ renderer: &mut Self::Renderer,
+ surface: &mut Self::Surface,
+ viewport: &Viewport,
+ background_color: Color,
+ overlay: &[T],
+ ) -> Result<(), SurfaceError> {
+ match (self, renderer, surface) {
+ (
+ Self::TinySkia(_compositor),
+ crate::Renderer::TinySkia(renderer),
+ Surface::TinySkia(surface),
+ ) => renderer.with_primitives(|backend, primitives| {
+ iced_tiny_skia::window::compositor::present(
+ backend,
+ surface,
+ primitives,
+ viewport,
+ background_color,
+ overlay,
+ )
+ }),
+ #[cfg(feature = "wgpu")]
+ (
+ Self::Wgpu(compositor),
+ crate::Renderer::Wgpu(renderer),
+ Surface::Wgpu(surface),
+ ) => renderer.with_primitives(|backend, primitives| {
+ iced_wgpu::window::compositor::present(
+ compositor,
+ backend,
+ surface,
+ primitives,
+ viewport,
+ background_color,
+ overlay,
+ )
+ }),
+ #[allow(unreachable_patterns)]
+ _ => panic!(
+ "The provided renderer or surface are not compatible \
+ with the compositor."
+ ),
+ }
+ }
+
+ fn screenshot<T: AsRef<str>>(
+ &mut self,
+ renderer: &mut Self::Renderer,
+ surface: &mut Self::Surface,
+ viewport: &Viewport,
+ background_color: Color,
+ overlay: &[T],
+ ) -> Vec<u8> {
+ match (self, renderer, surface) {
+ (
+ Self::TinySkia(_compositor),
+ Renderer::TinySkia(renderer),
+ Surface::TinySkia(surface),
+ ) => renderer.with_primitives(|backend, primitives| {
+ iced_tiny_skia::window::compositor::screenshot(
+ surface,
+ backend,
+ primitives,
+ viewport,
+ background_color,
+ overlay,
+ )
+ }),
+ #[cfg(feature = "wgpu")]
+ (
+ Self::Wgpu(compositor),
+ Renderer::Wgpu(renderer),
+ Surface::Wgpu(_),
+ ) => renderer.with_primitives(|backend, primitives| {
+ iced_wgpu::window::compositor::screenshot(
+ compositor,
+ backend,
+ primitives,
+ viewport,
+ background_color,
+ overlay,
+ )
+ }),
+ #[allow(unreachable_patterns)]
+ _ => panic!(
+ "The provided renderer or backend are not compatible \
+ with the compositor."
+ ),
+ }
+ }
+}
+
+enum Candidate {
+ Wgpu,
+ TinySkia,
+}
+
+impl Candidate {
+ fn default_list() -> Vec<Self> {
+ vec![
+ #[cfg(feature = "wgpu")]
+ Self::Wgpu,
+ Self::TinySkia,
+ ]
+ }
+
+ fn list_from_env() -> Option<Vec<Self>> {
+ let backends = env::var("ICED_BACKEND").ok()?;
+
+ Some(
+ backends
+ .split(',')
+ .map(str::trim)
+ .map(|backend| match backend {
+ "wgpu" => Self::Wgpu,
+ "tiny-skia" => Self::TinySkia,
+ _ => panic!("unknown backend value: \"{backend}\""),
+ })
+ .collect(),
+ )
+ }
+
+ fn build<Theme, W: HasRawWindowHandle + HasRawDisplayHandle>(
+ self,
+ settings: Settings,
+ _compatible_window: Option<&W>,
+ ) -> Result<(Compositor<Theme>, Renderer<Theme>), Error> {
+ match self {
+ Self::TinySkia => {
+ let (compositor, backend) =
+ iced_tiny_skia::window::compositor::new(
+ iced_tiny_skia::Settings {
+ default_font: settings.default_font,
+ default_text_size: settings.default_text_size,
+ },
+ );
+
+ Ok((
+ Compositor::TinySkia(compositor),
+ Renderer::TinySkia(iced_tiny_skia::Renderer::new(backend)),
+ ))
+ }
+ #[cfg(feature = "wgpu")]
+ Self::Wgpu => {
+ let (compositor, backend) = iced_wgpu::window::compositor::new(
+ iced_wgpu::Settings {
+ default_font: settings.default_font,
+ default_text_size: settings.default_text_size,
+ antialiasing: settings.antialiasing,
+ ..iced_wgpu::Settings::from_env()
+ },
+ _compatible_window,
+ )?;
+
+ Ok((
+ Compositor::Wgpu(compositor),
+ Renderer::Wgpu(iced_wgpu::Renderer::new(backend)),
+ ))
+ }
+ #[cfg(not(feature = "wgpu"))]
+ Self::Wgpu => {
+ panic!("`wgpu` feature was not enabled in `iced_renderer`")
+ }
+ }
+ }
+}
diff --git a/renderer/src/geometry.rs b/renderer/src/geometry.rs
new file mode 100644
index 00000000..04b5d9e6
--- /dev/null
+++ b/renderer/src/geometry.rs
@@ -0,0 +1,184 @@
+mod cache;
+
+pub use cache::Cache;
+
+use crate::core::{Point, Rectangle, Size, Vector};
+use crate::graphics::geometry::{Fill, Path, Stroke, Text};
+use crate::Renderer;
+
+pub enum Frame {
+ TinySkia(iced_tiny_skia::geometry::Frame),
+ #[cfg(feature = "wgpu")]
+ Wgpu(iced_wgpu::geometry::Frame),
+}
+
+pub enum Geometry {
+ TinySkia(iced_tiny_skia::Primitive),
+ #[cfg(feature = "wgpu")]
+ Wgpu(iced_wgpu::Primitive),
+}
+
+macro_rules! delegate {
+ ($frame:expr, $name:ident, $body:expr) => {
+ match $frame {
+ Self::TinySkia($name) => $body,
+ #[cfg(feature = "wgpu")]
+ Self::Wgpu($name) => $body,
+ }
+ };
+}
+
+impl Frame {
+ pub fn new<Theme>(renderer: &Renderer<Theme>, size: Size) -> Self {
+ match renderer {
+ Renderer::TinySkia(_) => {
+ Frame::TinySkia(iced_tiny_skia::geometry::Frame::new(size))
+ }
+ #[cfg(feature = "wgpu")]
+ Renderer::Wgpu(_) => {
+ Frame::Wgpu(iced_wgpu::geometry::Frame::new(size))
+ }
+ }
+ }
+
+ /// Returns the width of the [`Frame`].
+ #[inline]
+ pub fn width(&self) -> f32 {
+ delegate!(self, frame, frame.width())
+ }
+
+ /// Returns the height of the [`Frame`].
+ #[inline]
+ pub fn height(&self) -> f32 {
+ delegate!(self, frame, frame.height())
+ }
+
+ /// Returns the dimensions of the [`Frame`].
+ #[inline]
+ pub fn size(&self) -> Size {
+ delegate!(self, frame, frame.size())
+ }
+
+ /// Returns the coordinate of the center of the [`Frame`].
+ #[inline]
+ pub fn center(&self) -> Point {
+ delegate!(self, frame, frame.center())
+ }
+
+ /// Draws the given [`Path`] on the [`Frame`] by filling it with the
+ /// provided style.
+ pub fn fill(&mut self, path: &Path, fill: impl Into<Fill>) {
+ delegate!(self, frame, frame.fill(path, fill));
+ }
+
+ /// Draws an axis-aligned rectangle given its top-left corner coordinate and
+ /// its `Size` on the [`Frame`] by filling it with the provided style.
+ pub fn fill_rectangle(
+ &mut self,
+ top_left: Point,
+ size: Size,
+ fill: impl Into<Fill>,
+ ) {
+ delegate!(self, frame, frame.fill_rectangle(top_left, size, fill));
+ }
+
+ /// Draws the stroke of the given [`Path`] on the [`Frame`] with the
+ /// provided style.
+ pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) {
+ delegate!(self, frame, frame.stroke(path, stroke));
+ }
+
+ /// Draws the characters of the given [`Text`] on the [`Frame`], filling
+ /// them with the given color.
+ ///
+ /// __Warning:__ Text currently does not work well with rotations and scale
+ /// transforms! The position will be correctly transformed, but the
+ /// resulting glyphs will not be rotated or scaled properly.
+ ///
+ /// Additionally, all text will be rendered on top of all the layers of
+ /// a [`Canvas`]. Therefore, it is currently only meant to be used for
+ /// overlays, which is the most common use case.
+ ///
+ /// Support for vectorial text is planned, and should address all these
+ /// limitations.
+ ///
+ /// [`Canvas`]: crate::widget::Canvas
+ pub fn fill_text(&mut self, text: impl Into<Text>) {
+ delegate!(self, frame, frame.fill_text(text));
+ }
+
+ /// Stores the current transform of the [`Frame`] and executes the given
+ /// drawing operations, restoring the transform afterwards.
+ ///
+ /// This method is useful to compose transforms and perform drawing
+ /// operations in different coordinate systems.
+ #[inline]
+ pub fn with_save(&mut self, f: impl FnOnce(&mut Frame)) {
+ delegate!(self, frame, frame.push_transform());
+
+ f(self);
+
+ delegate!(self, frame, frame.pop_transform());
+ }
+
+ /// Executes the given drawing operations within a [`Rectangle`] region,
+ /// clipping any geometry that overflows its bounds. Any transformations
+ /// performed are local to the provided closure.
+ ///
+ /// This method is useful to perform drawing operations that need to be
+ /// clipped.
+ #[inline]
+ pub fn with_clip(&mut self, region: Rectangle, f: impl FnOnce(&mut Frame)) {
+ let mut frame = match self {
+ Self::TinySkia(_) => Self::TinySkia(
+ iced_tiny_skia::geometry::Frame::new(region.size()),
+ ),
+ #[cfg(feature = "wgpu")]
+ Self::Wgpu(_) => {
+ Self::Wgpu(iced_wgpu::geometry::Frame::new(region.size()))
+ }
+ };
+
+ f(&mut frame);
+
+ let origin = Point::new(region.x, region.y);
+
+ match (self, frame) {
+ (Self::TinySkia(target), Self::TinySkia(frame)) => {
+ target.clip(frame, origin);
+ }
+ #[cfg(feature = "wgpu")]
+ (Self::Wgpu(target), Self::Wgpu(frame)) => {
+ target.clip(frame, origin);
+ }
+ #[allow(unreachable_patterns)]
+ _ => unreachable!(),
+ };
+ }
+
+ /// Applies a translation to the current transform of the [`Frame`].
+ #[inline]
+ pub fn translate(&mut self, translation: Vector) {
+ delegate!(self, frame, frame.translate(translation));
+ }
+
+ /// Applies a rotation in radians to the current transform of the [`Frame`].
+ #[inline]
+ pub fn rotate(&mut self, angle: f32) {
+ delegate!(self, frame, frame.rotate(angle));
+ }
+
+ /// Applies a scaling to the current transform of the [`Frame`].
+ #[inline]
+ pub fn scale(&mut self, scale: f32) {
+ delegate!(self, frame, frame.scale(scale));
+ }
+
+ pub fn into_geometry(self) -> Geometry {
+ match self {
+ Self::TinySkia(frame) => Geometry::TinySkia(frame.into_primitive()),
+ #[cfg(feature = "wgpu")]
+ Self::Wgpu(frame) => Geometry::Wgpu(frame.into_primitive()),
+ }
+ }
+}
diff --git a/renderer/src/geometry/cache.rs b/renderer/src/geometry/cache.rs
new file mode 100644
index 00000000..d82e7f69
--- /dev/null
+++ b/renderer/src/geometry/cache.rs
@@ -0,0 +1,125 @@
+use crate::core::Size;
+use crate::geometry::{Frame, Geometry};
+use crate::Renderer;
+
+use std::cell::RefCell;
+use std::sync::Arc;
+
+/// A simple cache that stores generated [`Geometry`] to avoid recomputation.
+///
+/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer
+/// change or it is explicitly cleared.
+#[derive(Debug, Default)]
+pub struct Cache {
+ state: RefCell<State>,
+}
+
+#[derive(Debug, Default)]
+enum State {
+ #[default]
+ Empty,
+ Filled {
+ bounds: Size,
+ primitive: Internal,
+ },
+}
+
+#[derive(Debug, Clone)]
+enum Internal {
+ TinySkia(Arc<iced_tiny_skia::Primitive>),
+ #[cfg(feature = "wgpu")]
+ Wgpu(Arc<iced_wgpu::Primitive>),
+}
+
+impl Cache {
+ /// Creates a new empty [`Cache`].
+ pub fn new() -> Self {
+ Cache {
+ state: Default::default(),
+ }
+ }
+
+ /// Clears the [`Cache`], forcing a redraw the next time it is used.
+ pub fn clear(&self) {
+ *self.state.borrow_mut() = State::Empty;
+ }
+
+ /// Draws [`Geometry`] using the provided closure and stores it in the
+ /// [`Cache`].
+ ///
+ /// The closure will only be called when
+ /// - the bounds have changed since the previous draw call.
+ /// - the [`Cache`] is empty or has been explicitly cleared.
+ ///
+ /// Otherwise, the previously stored [`Geometry`] will be returned. The
+ /// [`Cache`] is not cleared in this case. In other words, it will keep
+ /// returning the stored [`Geometry`] if needed.
+ pub fn draw<Theme>(
+ &self,
+ renderer: &Renderer<Theme>,
+ bounds: Size,
+ draw_fn: impl FnOnce(&mut Frame),
+ ) -> Geometry {
+ use std::ops::Deref;
+
+ if let State::Filled {
+ bounds: cached_bounds,
+ primitive,
+ } = self.state.borrow().deref()
+ {
+ if *cached_bounds == bounds {
+ match primitive {
+ Internal::TinySkia(primitive) => {
+ return Geometry::TinySkia(
+ iced_tiny_skia::Primitive::Cache {
+ content: primitive.clone(),
+ },
+ );
+ }
+ #[cfg(feature = "wgpu")]
+ Internal::Wgpu(primitive) => {
+ return Geometry::Wgpu(iced_wgpu::Primitive::Cache {
+ content: primitive.clone(),
+ });
+ }
+ }
+ }
+ }
+
+ let mut frame = Frame::new(renderer, bounds);
+ draw_fn(&mut frame);
+
+ let primitive = {
+ let geometry = frame.into_geometry();
+
+ match geometry {
+ Geometry::TinySkia(primitive) => {
+ Internal::TinySkia(Arc::new(primitive))
+ }
+ #[cfg(feature = "wgpu")]
+ Geometry::Wgpu(primitive) => {
+ Internal::Wgpu(Arc::new(primitive))
+ }
+ }
+ };
+
+ *self.state.borrow_mut() = State::Filled {
+ bounds,
+ primitive: primitive.clone(),
+ };
+
+ match primitive {
+ Internal::TinySkia(primitive) => {
+ Geometry::TinySkia(iced_tiny_skia::Primitive::Cache {
+ content: primitive,
+ })
+ }
+ #[cfg(feature = "wgpu")]
+ Internal::Wgpu(primitive) => {
+ Geometry::Wgpu(iced_wgpu::Primitive::Cache {
+ content: primitive,
+ })
+ }
+ }
+ }
+}
diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs
new file mode 100644
index 00000000..7d1a02c2
--- /dev/null
+++ b/renderer/src/lib.rs
@@ -0,0 +1,269 @@
+pub mod compositor;
+
+#[cfg(feature = "geometry")]
+pub mod geometry;
+
+mod settings;
+
+pub use iced_graphics as graphics;
+pub use iced_graphics::core;
+
+pub use compositor::Compositor;
+pub use settings::Settings;
+
+#[cfg(feature = "geometry")]
+pub use geometry::Geometry;
+
+use crate::core::renderer;
+use crate::core::text::{self, Text};
+use crate::core::{Background, Font, Point, Rectangle, Size, Vector};
+use crate::graphics::Mesh;
+
+use std::borrow::Cow;
+
+/// The default graphics renderer for [`iced`].
+///
+/// [`iced`]: https://github.com/iced-rs/iced
+pub enum Renderer<Theme> {
+ TinySkia(iced_tiny_skia::Renderer<Theme>),
+ #[cfg(feature = "wgpu")]
+ Wgpu(iced_wgpu::Renderer<Theme>),
+}
+
+macro_rules! delegate {
+ ($renderer:expr, $name:ident, $body:expr) => {
+ match $renderer {
+ Self::TinySkia($name) => $body,
+ #[cfg(feature = "wgpu")]
+ Self::Wgpu($name) => $body,
+ }
+ };
+}
+
+impl<T> Renderer<T> {
+ pub fn draw_mesh(&mut self, mesh: Mesh) {
+ match self {
+ Self::TinySkia(_) => {
+ log::warn!("Unsupported mesh primitive: {:?}", mesh)
+ }
+ #[cfg(feature = "wgpu")]
+ Self::Wgpu(renderer) => {
+ renderer.draw_primitive(iced_wgpu::Primitive::Custom(
+ iced_wgpu::primitive::Custom::Mesh(mesh),
+ ));
+ }
+ }
+ }
+}
+
+impl<T> core::Renderer for Renderer<T> {
+ type Theme = T;
+
+ fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self)) {
+ match self {
+ Self::TinySkia(renderer) => {
+ let primitives = renderer.start_layer();
+
+ f(self);
+
+ match self {
+ Self::TinySkia(renderer) => {
+ renderer.end_layer(primitives, bounds);
+ }
+ #[cfg(feature = "wgpu")]
+ _ => unreachable!(),
+ }
+ }
+ #[cfg(feature = "wgpu")]
+ Self::Wgpu(renderer) => {
+ let primitives = renderer.start_layer();
+
+ f(self);
+
+ match self {
+ #[cfg(feature = "wgpu")]
+ Self::Wgpu(renderer) => {
+ renderer.end_layer(primitives, bounds);
+ }
+ _ => unreachable!(),
+ }
+ }
+ }
+ }
+
+ fn with_translation(
+ &mut self,
+ translation: Vector,
+ f: impl FnOnce(&mut Self),
+ ) {
+ match self {
+ Self::TinySkia(renderer) => {
+ let primitives = renderer.start_translation();
+
+ f(self);
+
+ match self {
+ Self::TinySkia(renderer) => {
+ renderer.end_translation(primitives, translation);
+ }
+ #[cfg(feature = "wgpu")]
+ _ => unreachable!(),
+ }
+ }
+ #[cfg(feature = "wgpu")]
+ Self::Wgpu(renderer) => {
+ let primitives = renderer.start_translation();
+
+ f(self);
+
+ match self {
+ #[cfg(feature = "wgpu")]
+ Self::Wgpu(renderer) => {
+ renderer.end_translation(primitives, translation);
+ }
+ _ => unreachable!(),
+ }
+ }
+ }
+ }
+
+ fn fill_quad(
+ &mut self,
+ quad: renderer::Quad,
+ background: impl Into<Background>,
+ ) {
+ delegate!(self, renderer, renderer.fill_quad(quad, background));
+ }
+
+ fn clear(&mut self) {
+ delegate!(self, renderer, renderer.clear());
+ }
+}
+
+impl<T> text::Renderer for Renderer<T> {
+ type Font = Font;
+
+ const ICON_FONT: Font = iced_tiny_skia::Renderer::<T>::ICON_FONT;
+ const CHECKMARK_ICON: char = iced_tiny_skia::Renderer::<T>::CHECKMARK_ICON;
+ const ARROW_DOWN_ICON: char =
+ iced_tiny_skia::Renderer::<T>::ARROW_DOWN_ICON;
+
+ fn default_font(&self) -> Self::Font {
+ delegate!(self, renderer, renderer.default_font())
+ }
+
+ fn default_size(&self) -> f32 {
+ delegate!(self, renderer, renderer.default_size())
+ }
+
+ fn measure(
+ &self,
+ content: &str,
+ size: f32,
+ line_height: text::LineHeight,
+ font: Font,
+ bounds: Size,
+ shaping: text::Shaping,
+ ) -> Size {
+ delegate!(
+ self,
+ renderer,
+ renderer.measure(content, size, line_height, font, bounds, shaping)
+ )
+ }
+
+ fn hit_test(
+ &self,
+ content: &str,
+ size: f32,
+ line_height: text::LineHeight,
+ font: Font,
+ bounds: Size,
+ shaping: text::Shaping,
+ point: Point,
+ nearest_only: bool,
+ ) -> Option<text::Hit> {
+ delegate!(
+ self,
+ renderer,
+ renderer.hit_test(
+ content,
+ size,
+ line_height,
+ font,
+ bounds,
+ shaping,
+ point,
+ nearest_only
+ )
+ )
+ }
+
+ fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
+ delegate!(self, renderer, renderer.load_font(bytes));
+ }
+
+ fn fill_text(&mut self, text: Text<'_, Self::Font>) {
+ delegate!(self, renderer, renderer.fill_text(text));
+ }
+}
+
+#[cfg(feature = "image")]
+impl<T> crate::core::image::Renderer for Renderer<T> {
+ type Handle = crate::core::image::Handle;
+
+ fn dimensions(&self, handle: &crate::core::image::Handle) -> Size<u32> {
+ delegate!(self, renderer, renderer.dimensions(handle))
+ }
+
+ fn draw(&mut self, handle: crate::core::image::Handle, bounds: Rectangle) {
+ delegate!(self, renderer, renderer.draw(handle, bounds));
+ }
+}
+
+#[cfg(feature = "svg")]
+impl<T> crate::core::svg::Renderer for Renderer<T> {
+ fn dimensions(&self, handle: &crate::core::svg::Handle) -> Size<u32> {
+ delegate!(self, renderer, renderer.dimensions(handle))
+ }
+
+ fn draw(
+ &mut self,
+ handle: crate::core::svg::Handle,
+ color: Option<crate::core::Color>,
+ bounds: Rectangle,
+ ) {
+ delegate!(self, renderer, renderer.draw(handle, color, bounds))
+ }
+}
+
+#[cfg(feature = "geometry")]
+impl<T> crate::graphics::geometry::Renderer for Renderer<T> {
+ type Geometry = crate::Geometry;
+
+ fn draw(&mut self, layers: Vec<Self::Geometry>) {
+ match self {
+ Self::TinySkia(renderer) => {
+ for layer in layers {
+ match layer {
+ crate::Geometry::TinySkia(primitive) => {
+ renderer.draw_primitive(primitive);
+ }
+ _ => unreachable!(),
+ }
+ }
+ }
+ #[cfg(feature = "wgpu")]
+ Self::Wgpu(renderer) => {
+ for layer in layers {
+ match layer {
+ crate::Geometry::Wgpu(primitive) => {
+ renderer.draw_primitive(primitive);
+ }
+ _ => unreachable!(),
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/renderer/src/settings.rs b/renderer/src/settings.rs
new file mode 100644
index 00000000..2e51f339
--- /dev/null
+++ b/renderer/src/settings.rs
@@ -0,0 +1,31 @@
+use crate::core::Font;
+use crate::graphics::Antialiasing;
+
+/// The settings of a [`Backend`].
+///
+/// [`Backend`]: crate::Backend
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Settings {
+ /// The default [`Font`] to use.
+ pub default_font: Font,
+
+ /// The default size of text.
+ ///
+ /// By default, it will be set to `16.0`.
+ pub default_text_size: f32,
+
+ /// The antialiasing strategy that will be used for triangle primitives.
+ ///
+ /// By default, it is `None`.
+ pub antialiasing: Option<Antialiasing>,
+}
+
+impl Default for Settings {
+ fn default() -> Settings {
+ Settings {
+ default_font: Font::default(),
+ default_text_size: 16.0,
+ antialiasing: None,
+ }
+ }
+}
diff --git a/graphics/src/widget.rs b/renderer/src/widget.rs
index e7fab97c..6c0c2a83 100644
--- a/graphics/src/widget.rs
+++ b/renderer/src/widget.rs
@@ -1,16 +1,11 @@
-//! Use the graphical widgets supported out-of-the-box.
#[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/native/Cargo.toml b/runtime/Cargo.toml
index 3f92783e..a65f07f2 100644
--- a/native/Cargo.toml
+++ b/runtime/Cargo.toml
@@ -1,6 +1,6 @@
[package]
-name = "iced_native"
-version = "0.9.1"
+name = "iced_runtime"
+version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
description = "A renderer-agnostic library for native GUIs"
@@ -11,19 +11,13 @@ repository = "https://github.com/iced-rs/iced"
debug = []
[dependencies]
-twox-hash = { version = "1.5", default-features = false }
-unicode-segmentation = "1.6"
-num-traits = "0.2"
+thiserror = "1"
[dependencies.iced_core]
-version = "0.8"
+version = "0.9"
path = "../core"
[dependencies.iced_futures]
version = "0.6"
path = "../futures"
features = ["thread-pool"]
-
-[dependencies.iced_style]
-version = "0.7"
-path = "../style"
diff --git a/runtime/README.md b/runtime/README.md
new file mode 100644
index 00000000..1b0fa857
--- /dev/null
+++ b/runtime/README.md
@@ -0,0 +1,18 @@
+# `iced_runtime`
+[![Documentation](https://docs.rs/iced_native/badge.svg)][documentation]
+[![Crates.io](https://img.shields.io/crates/v/iced_native.svg)](https://crates.io/crates/iced_native)
+[![License](https://img.shields.io/crates/l/iced_native.svg)](https://github.com/iced-rs/iced/blob/master/LICENSE)
+[![Discord Server](https://img.shields.io/discord/628993209984614400?label=&labelColor=6A7EC2&logo=discord&logoColor=ffffff&color=7389D8)](https://discord.gg/3xZJ65GAhd)
+
+`iced_runtime` takes [`iced_core`] and builds a native runtime on top of it.
+
+[documentation]: https://docs.rs/iced_native
+[`iced_core`]: ../core
+[`iced_winit`]: ../winit
+[`druid`]: https://github.com/xi-editor/druid
+[`raw-window-handle`]: https://github.com/rust-windowing/raw-window-handle
+
+__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/iced-rs/iced/releases
diff --git a/native/src/clipboard.rs b/runtime/src/clipboard.rs
index c9105bc0..bc450912 100644
--- a/native/src/clipboard.rs
+++ b/runtime/src/clipboard.rs
@@ -1,30 +1,9 @@
//! Access the clipboard.
-use iced_futures::MaybeSend;
+use crate::command::{self, Command};
+use crate::futures::MaybeSend;
use std::fmt;
-/// A buffer for short-term storage and transfer within and between
-/// applications.
-pub trait Clipboard {
- /// Reads the current content of the [`Clipboard`] as text.
- fn read(&self) -> Option<String>;
-
- /// Writes the given text contents to the [`Clipboard`].
- fn write(&mut self, contents: String);
-}
-
-/// A null implementation of the [`Clipboard`] trait.
-#[derive(Debug, Clone, Copy)]
-pub struct Null;
-
-impl Clipboard for Null {
- fn read(&self) -> Option<String> {
- None
- }
-
- fn write(&mut self, _contents: String) {}
-}
-
/// A clipboard action to be performed by some [`Command`].
///
/// [`Command`]: crate::Command
@@ -60,3 +39,15 @@ impl<T> fmt::Debug for Action<T> {
}
}
}
+
+/// Read the current contents of the clipboard.
+pub fn read<Message>(
+ f: impl Fn(Option<String>) -> Message + 'static,
+) -> Command<Message> {
+ Command::single(command::Action::Clipboard(Action::Read(Box::new(f))))
+}
+
+/// Write the given contents to the clipboard.
+pub fn write<Message>(contents: String) -> Command<Message> {
+ Command::single(command::Action::Clipboard(Action::Write(contents)))
+}
diff --git a/native/src/command.rs b/runtime/src/command.rs
index ca9d0b64..cd4c51ff 100644
--- a/native/src/command.rs
+++ b/runtime/src/command.rs
@@ -3,35 +3,39 @@ mod action;
pub use action::Action;
-use crate::widget;
-
-use iced_futures::MaybeSend;
+use crate::core::widget;
+use crate::futures::MaybeSend;
use std::fmt;
use std::future::Future;
/// A set of asynchronous actions to be performed by some runtime.
#[must_use = "`Command` must be returned to runtime to take effect"]
-pub struct Command<T>(iced_futures::Command<Action<T>>);
+pub struct Command<T>(Internal<Action<T>>);
+
+#[derive(Debug)]
+enum Internal<T> {
+ None,
+ Single(T),
+ Batch(Vec<T>),
+}
impl<T> Command<T> {
/// Creates an empty [`Command`].
///
/// In other words, a [`Command`] that does nothing.
pub const fn none() -> Self {
- Self(iced_futures::Command::none())
+ Self(Internal::None)
}
/// Creates a [`Command`] that performs a single [`Action`].
pub const fn single(action: Action<T>) -> Self {
- Self(iced_futures::Command::single(action))
+ Self(Internal::Single(action))
}
/// Creates a [`Command`] that performs a [`widget::Operation`].
pub fn widget(operation: impl widget::Operation<T> + 'static) -> Self {
- Self(iced_futures::Command::single(Action::Widget(
- widget::Action::new(operation),
- )))
+ Self::single(Action::Widget(Box::new(operation)))
}
/// Creates a [`Command`] that performs the action of the given future.
@@ -49,9 +53,17 @@ impl<T> Command<T> {
///
/// Once this command is run, all the commands will be executed at once.
pub fn batch(commands: impl IntoIterator<Item = Command<T>>) -> Self {
- Self(iced_futures::Command::batch(
- commands.into_iter().map(|Command(command)| command),
- ))
+ let mut batch = Vec::new();
+
+ for Command(command) in commands {
+ match command {
+ Internal::None => {}
+ Internal::Single(command) => batch.push(command),
+ Internal::Batch(commands) => batch.extend(commands),
+ }
+ }
+
+ Self(Internal::Batch(batch))
}
/// Applies a transformation to the result of a [`Command`].
@@ -63,16 +75,27 @@ impl<T> Command<T> {
T: 'static,
A: 'static,
{
- let Command(command) = self;
-
- Command(command.map(move |action| action.map(f.clone())))
+ match self.0 {
+ Internal::None => Command::none(),
+ Internal::Single(action) => Command::single(action.map(f)),
+ Internal::Batch(batch) => Command(Internal::Batch(
+ batch
+ .into_iter()
+ .map(|action| action.map(f.clone()))
+ .collect(),
+ )),
+ }
}
/// Returns all of the actions of the [`Command`].
pub fn actions(self) -> Vec<Action<T>> {
let Command(command) = self;
- command.actions()
+ match command {
+ Internal::None => Vec::new(),
+ Internal::Single(action) => vec![action],
+ Internal::Batch(batch) => batch,
+ }
}
}
diff --git a/native/src/command/action.rs b/runtime/src/command/action.rs
index dcb79672..b2594379 100644
--- a/native/src/command/action.rs
+++ b/runtime/src/command/action.rs
@@ -1,10 +1,12 @@
use crate::clipboard;
+use crate::core::widget;
+use crate::font;
use crate::system;
-use crate::widget;
use crate::window;
use iced_futures::MaybeSend;
+use std::borrow::Cow;
use std::fmt;
/// An action that a [`Command`] can perform.
@@ -26,7 +28,16 @@ pub enum Action<T> {
System(system::Action<T>),
/// Run a widget action.
- Widget(widget::Action<T>),
+ Widget(Box<dyn widget::Operation<T>>),
+
+ /// Load a font from its bytes.
+ LoadFont {
+ /// The bytes of the font to load.
+ bytes: Cow<'static, [u8]>,
+
+ /// The message to produce when the font has been loaded.
+ tagger: Box<dyn Fn(Result<(), font::Error>) -> T>,
+ },
}
impl<T> Action<T> {
@@ -48,7 +59,13 @@ impl<T> Action<T> {
Self::Clipboard(action) => Action::Clipboard(action.map(f)),
Self::Window(id, window) => Action::Window(id, window.map(f)),
Self::System(system) => Action::System(system.map(f)),
- Self::Widget(widget) => Action::Widget(widget.map(f)),
+ Self::Widget(operation) => {
+ Action::Widget(Box::new(widget::operation::map(operation, f)))
+ }
+ Self::LoadFont { bytes, tagger } => Action::LoadFont {
+ bytes,
+ tagger: Box::new(move |result| f(tagger(result))),
+ },
}
}
}
@@ -65,6 +82,7 @@ impl<T> fmt::Debug for Action<T> {
}
Self::System(action) => write!(f, "Action::System({action:?})"),
Self::Widget(_action) => write!(f, "Action::Widget"),
+ Self::LoadFont { .. } => write!(f, "Action::LoadFont"),
}
}
}
diff --git a/native/src/debug/basic.rs b/runtime/src/debug/basic.rs
index 92f614da..32f725a1 100644
--- a/native/src/debug/basic.rs
+++ b/runtime/src/debug/basic.rs
@@ -1,5 +1,5 @@
#![allow(missing_docs)]
-use crate::time;
+use crate::core::time;
use std::collections::VecDeque;
diff --git a/native/src/debug/null.rs b/runtime/src/debug/null.rs
index 2db0eebb..2db0eebb 100644
--- a/native/src/debug/null.rs
+++ b/runtime/src/debug/null.rs
diff --git a/runtime/src/font.rs b/runtime/src/font.rs
new file mode 100644
index 00000000..15359694
--- /dev/null
+++ b/runtime/src/font.rs
@@ -0,0 +1,19 @@
+//! Load and use fonts.
+pub use iced_core::font::*;
+
+use crate::command::{self, Command};
+use std::borrow::Cow;
+
+/// An error while loading a font.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Error {}
+
+/// Load a font from its bytes.
+pub fn load(
+ bytes: impl Into<Cow<'static, [u8]>>,
+) -> Command<Result<(), Error>> {
+ Command::single(command::Action::LoadFont {
+ bytes: bytes.into(),
+ tagger: Box::new(std::convert::identity),
+ })
+}
diff --git a/native/src/keyboard.rs b/runtime/src/keyboard.rs
index 012538e3..012538e3 100644
--- a/native/src/keyboard.rs
+++ b/runtime/src/keyboard.rs
diff --git a/native/src/lib.rs b/runtime/src/lib.rs
index ebdc8490..4bbf9687 100644
--- a/native/src/lib.rs
+++ b/runtime/src/lib.rs
@@ -23,8 +23,8 @@
//! - Build a new renderer, see the [renderer] module.
//! - Build a custom widget, start at the [`Widget`] trait.
//!
-//! [`iced_core`]: https://github.com/iced-rs/iced/tree/0.8/core
-//! [`iced_winit`]: https://github.com/iced-rs/iced/tree/0.8/winit
+//! [`iced_core`]: https://github.com/iced-rs/iced/tree/0.9/core
+//! [`iced_winit`]: https://github.com/iced-rs/iced/tree/0.9/winit
//! [`druid`]: https://github.com/xi-editor/druid
//! [`raw-window-handle`]: https://github.com/rust-windowing/raw-window-handle
//! [renderer]: crate::renderer
@@ -42,32 +42,17 @@
clippy::useless_conversion
)]
#![forbid(unsafe_code, rust_2018_idioms)]
-#![allow(clippy::inherent_to_string, clippy::type_complexity)]
-#![cfg_attr(docsrs, feature(doc_cfg))]
+#![cfg_attr(docsrs, feature(doc_auto_cfg))]
pub mod clipboard;
pub mod command;
-pub mod event;
-pub mod image;
+pub mod font;
pub mod keyboard;
-pub mod layout;
-pub mod mouse;
pub mod overlay;
pub mod program;
-pub mod renderer;
-pub mod subscription;
-pub mod svg;
pub mod system;
-pub mod text;
-pub mod touch;
pub mod user_interface;
-pub mod widget;
pub mod window;
-mod element;
-mod hasher;
-mod runtime;
-mod shell;
-
// We disable debug capabilities on release builds unless the `debug` feature
// is explicitly enabled.
#[cfg(feature = "debug")]
@@ -77,32 +62,11 @@ mod debug;
#[path = "debug/null.rs"]
mod debug;
-pub use iced_core::alignment;
-pub use iced_core::time;
-pub use iced_core::{
- color, Alignment, Background, Color, ContentFit, Font, Length, Padding,
- Pixels, Point, Rectangle, Size, Vector,
-};
-pub use iced_futures::{executor, futures};
-pub use iced_style::application;
-pub use iced_style::theme;
-
-#[doc(no_inline)]
-pub use executor::Executor;
+pub use iced_core as core;
+pub use iced_futures as futures;
-pub use clipboard::Clipboard;
pub use command::Command;
pub use debug::Debug;
-pub use element::Element;
-pub use event::Event;
-pub use hasher::Hasher;
-pub use layout::Layout;
-pub use overlay::Overlay;
+pub use font::Font;
pub use program::Program;
-pub use renderer::Renderer;
-pub use runtime::Runtime;
-pub use shell::Shell;
-pub use subscription::Subscription;
-pub use theme::Theme;
pub use user_interface::UserInterface;
-pub use widget::Widget;
diff --git a/runtime/src/overlay.rs b/runtime/src/overlay.rs
new file mode 100644
index 00000000..03390980
--- /dev/null
+++ b/runtime/src/overlay.rs
@@ -0,0 +1,4 @@
+//! Overlays for user interfaces.
+mod nested;
+
+pub use nested::Nested;
diff --git a/runtime/src/overlay/nested.rs b/runtime/src/overlay/nested.rs
new file mode 100644
index 00000000..b729f769
--- /dev/null
+++ b/runtime/src/overlay/nested.rs
@@ -0,0 +1,353 @@
+use crate::core::event;
+use crate::core::layout;
+use crate::core::mouse;
+use crate::core::overlay;
+use crate::core::renderer;
+use crate::core::widget;
+use crate::core::{Clipboard, Event, Layout, Point, Rectangle, Shell, Size};
+
+/// An [`Overlay`] container that displays nested overlays
+#[allow(missing_debug_implementations)]
+pub struct Nested<'a, Message, Renderer> {
+ overlay: overlay::Element<'a, Message, Renderer>,
+}
+
+impl<'a, Message, Renderer> Nested<'a, Message, Renderer>
+where
+ Renderer: renderer::Renderer,
+{
+ /// Creates a nested overlay from the provided [`overlay::Element`]
+ pub fn new(element: overlay::Element<'a, Message, Renderer>) -> Self {
+ Self { overlay: element }
+ }
+
+ /// Returns the position of the [`Nested`] overlay.
+ pub fn position(&self) -> Point {
+ self.overlay.position()
+ }
+
+ /// Returns the layout [`Node`] of the [`Nested`] overlay.
+ pub fn layout(
+ &mut self,
+ renderer: &Renderer,
+ bounds: Size,
+ position: Point,
+ ) -> layout::Node {
+ fn recurse<Message, Renderer>(
+ element: &mut overlay::Element<'_, Message, Renderer>,
+ renderer: &Renderer,
+ bounds: Size,
+ position: Point,
+ ) -> layout::Node
+ where
+ Renderer: renderer::Renderer,
+ {
+ let translation = position - Point::ORIGIN;
+
+ let node = element.layout(renderer, bounds, translation);
+
+ if let Some(mut nested) =
+ element.overlay(Layout::new(&node), renderer)
+ {
+ layout::Node::with_children(
+ node.size(),
+ vec![
+ node,
+ recurse(&mut nested, renderer, bounds, position),
+ ],
+ )
+ } else {
+ layout::Node::with_children(node.size(), vec![node])
+ }
+ }
+
+ recurse(&mut self.overlay, renderer, bounds, position)
+ }
+
+ /// Draws the [`Nested`] overlay using the associated `Renderer`.
+ pub fn draw(
+ &mut self,
+ renderer: &mut Renderer,
+ theme: &<Renderer as renderer::Renderer>::Theme,
+ style: &renderer::Style,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ ) {
+ fn recurse<Message, Renderer>(
+ element: &mut overlay::Element<'_, Message, Renderer>,
+ layout: Layout<'_>,
+ renderer: &mut Renderer,
+ theme: &<Renderer as renderer::Renderer>::Theme,
+ style: &renderer::Style,
+ cursor: mouse::Cursor,
+ ) where
+ Renderer: renderer::Renderer,
+ {
+ let mut layouts = layout.children();
+
+ if let Some(layout) = layouts.next() {
+ let nested_layout = layouts.next();
+
+ let is_over = cursor
+ .position()
+ .zip(nested_layout)
+ .and_then(|(cursor_position, nested_layout)| {
+ element.overlay(layout, renderer).map(|nested| {
+ nested.is_over(
+ nested_layout.children().next().unwrap(),
+ renderer,
+ cursor_position,
+ )
+ })
+ })
+ .unwrap_or_default();
+
+ renderer.with_layer(layout.bounds(), |renderer| {
+ element.draw(
+ renderer,
+ theme,
+ style,
+ layout,
+ if is_over {
+ mouse::Cursor::Unavailable
+ } else {
+ cursor
+ },
+ );
+ });
+
+ if let Some((mut nested, nested_layout)) =
+ element.overlay(layout, renderer).zip(nested_layout)
+ {
+ recurse(
+ &mut nested,
+ nested_layout,
+ renderer,
+ theme,
+ style,
+ cursor,
+ );
+ }
+ }
+ }
+
+ recurse(&mut self.overlay, layout, renderer, theme, style, cursor);
+ }
+
+ /// Applies a [`widget::Operation`] to the [`Nested`] overlay.
+ pub fn operate(
+ &mut self,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ operation: &mut dyn widget::Operation<Message>,
+ ) {
+ fn recurse<Message, Renderer>(
+ element: &mut overlay::Element<'_, Message, Renderer>,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ operation: &mut dyn widget::Operation<Message>,
+ ) where
+ Renderer: renderer::Renderer,
+ {
+ let mut layouts = layout.children();
+
+ if let Some(layout) = layouts.next() {
+ element.operate(layout, renderer, operation);
+
+ if let Some((mut nested, nested_layout)) =
+ element.overlay(layout, renderer).zip(layouts.next())
+ {
+ recurse(&mut nested, nested_layout, renderer, operation);
+ }
+ }
+ }
+
+ recurse(&mut self.overlay, layout, renderer, operation)
+ }
+
+ /// Processes a runtime [`Event`].
+ pub fn on_event(
+ &mut self,
+ event: Event,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ renderer: &Renderer,
+ clipboard: &mut dyn Clipboard,
+ shell: &mut Shell<'_, Message>,
+ ) -> event::Status {
+ fn recurse<Message, Renderer>(
+ element: &mut overlay::Element<'_, Message, Renderer>,
+ layout: Layout<'_>,
+ event: Event,
+ cursor: mouse::Cursor,
+ renderer: &Renderer,
+ clipboard: &mut dyn Clipboard,
+ shell: &mut Shell<'_, Message>,
+ ) -> (event::Status, bool)
+ where
+ Renderer: renderer::Renderer,
+ {
+ let mut layouts = layout.children();
+
+ if let Some(layout) = layouts.next() {
+ let (nested_status, nested_is_over) =
+ if let Some((mut nested, nested_layout)) =
+ element.overlay(layout, renderer).zip(layouts.next())
+ {
+ recurse(
+ &mut nested,
+ nested_layout,
+ event.clone(),
+ cursor,
+ renderer,
+ clipboard,
+ shell,
+ )
+ } else {
+ (event::Status::Ignored, false)
+ };
+
+ if matches!(nested_status, event::Status::Ignored) {
+ let is_over = nested_is_over
+ || cursor
+ .position()
+ .map(|cursor_position| {
+ element.is_over(
+ layout,
+ renderer,
+ cursor_position,
+ )
+ })
+ .unwrap_or_default();
+
+ (
+ element.on_event(
+ event,
+ layout,
+ if nested_is_over {
+ mouse::Cursor::Unavailable
+ } else {
+ cursor
+ },
+ renderer,
+ clipboard,
+ shell,
+ ),
+ is_over,
+ )
+ } else {
+ (nested_status, nested_is_over)
+ }
+ } else {
+ (event::Status::Ignored, false)
+ }
+ }
+
+ let (status, _) = recurse(
+ &mut self.overlay,
+ layout,
+ event,
+ cursor,
+ renderer,
+ clipboard,
+ shell,
+ );
+
+ status
+ }
+
+ /// Returns the current [`mouse::Interaction`] of the [`Nested`] overlay.
+ pub fn mouse_interaction(
+ &mut self,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ viewport: &Rectangle,
+ renderer: &Renderer,
+ ) -> mouse::Interaction {
+ fn recurse<Message, Renderer>(
+ element: &mut overlay::Element<'_, Message, Renderer>,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ viewport: &Rectangle,
+ renderer: &Renderer,
+ ) -> Option<mouse::Interaction>
+ where
+ Renderer: renderer::Renderer,
+ {
+ let mut layouts = layout.children();
+
+ let layout = layouts.next()?;
+ let cursor_position = cursor.position()?;
+
+ if !element.is_over(layout, renderer, cursor_position) {
+ return None;
+ }
+
+ Some(
+ element
+ .overlay(layout, renderer)
+ .zip(layouts.next())
+ .and_then(|(mut overlay, layout)| {
+ recurse(
+ &mut overlay,
+ layout,
+ cursor,
+ viewport,
+ renderer,
+ )
+ })
+ .unwrap_or_else(|| {
+ element.mouse_interaction(
+ layout, cursor, viewport, renderer,
+ )
+ }),
+ )
+ }
+
+ recurse(&mut self.overlay, layout, cursor, viewport, renderer)
+ .unwrap_or_default()
+ }
+
+ /// Returns true if the cursor is over the [`Nested`] overlay.
+ pub fn is_over(
+ &mut self,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ cursor_position: Point,
+ ) -> bool {
+ fn recurse<Message, Renderer>(
+ element: &mut overlay::Element<'_, Message, Renderer>,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ cursor_position: Point,
+ ) -> bool
+ where
+ Renderer: renderer::Renderer,
+ {
+ let mut layouts = layout.children();
+
+ if let Some(layout) = layouts.next() {
+ if element.is_over(layout, renderer, cursor_position) {
+ return true;
+ }
+
+ if let Some((mut nested, nested_layout)) =
+ element.overlay(layout, renderer).zip(layouts.next())
+ {
+ recurse(
+ &mut nested,
+ nested_layout,
+ renderer,
+ cursor_position,
+ )
+ } else {
+ false
+ }
+ } else {
+ false
+ }
+ }
+
+ recurse(&mut self.overlay, layout, renderer, cursor_position)
+ }
+}
diff --git a/native/src/program.rs b/runtime/src/program.rs
index c71c237f..44585cc5 100644
--- a/native/src/program.rs
+++ b/runtime/src/program.rs
@@ -1,5 +1,8 @@
//! Build interactive programs using The Elm Architecture.
-use crate::{Command, Element, Renderer};
+use crate::Command;
+
+use iced_core::text;
+use iced_core::{Element, Renderer};
mod state;
@@ -8,7 +11,7 @@ pub use state::State;
/// The core of a user interface application following The Elm Architecture.
pub trait Program: Sized {
/// The graphics backend to use to draw the [`Program`].
- type Renderer: Renderer;
+ type Renderer: Renderer + text::Renderer;
/// The type of __messages__ your [`Program`] will produce.
type Message: std::fmt::Debug + Send;
diff --git a/native/src/program/state.rs b/runtime/src/program/state.rs
index 8ae1cacb..35df6078 100644
--- a/native/src/program/state.rs
+++ b/runtime/src/program/state.rs
@@ -1,9 +1,10 @@
-use crate::application;
-use crate::event::{self, Event};
-use crate::mouse;
-use crate::renderer;
+use crate::core::event::{self, Event};
+use crate::core::mouse;
+use crate::core::renderer;
+use crate::core::widget::operation::{self, Operation};
+use crate::core::{Clipboard, Size};
use crate::user_interface::{self, UserInterface};
-use crate::{Clipboard, Command, Debug, Point, Program, Size};
+use crate::{Command, Debug, Program};
/// The execution state of a [`Program`]. It leverages caching, event
/// processing, and rendering primitive storage.
@@ -22,7 +23,6 @@ where
impl<P> State<P>
where
P: Program + 'static,
- <P::Renderer as crate::Renderer>::Theme: application::StyleSheet,
{
/// Creates a new [`State`] with the provided [`Program`], initializing its
/// primitive with the given logical bounds and renderer.
@@ -89,9 +89,9 @@ where
pub fn update(
&mut self,
bounds: Size,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &mut P::Renderer,
- theme: &<P::Renderer as crate::Renderer>::Theme,
+ theme: &<P::Renderer as iced_core::Renderer>::Theme,
style: &renderer::Style,
clipboard: &mut dyn Clipboard,
debug: &mut Debug,
@@ -109,7 +109,7 @@ where
let (_, event_statuses) = user_interface.update(
&self.queued_events,
- cursor_position,
+ cursor,
renderer,
clipboard,
&mut messages,
@@ -132,7 +132,7 @@ where
let command = if messages.is_empty() {
debug.draw_started();
self.mouse_interaction =
- user_interface.draw(renderer, theme, style, cursor_position);
+ user_interface.draw(renderer, theme, style, cursor);
debug.draw_finished();
self.cache = Some(user_interface.into_cache());
@@ -164,7 +164,7 @@ where
debug.draw_started();
self.mouse_interaction =
- user_interface.draw(renderer, theme, style, cursor_position);
+ user_interface.draw(renderer, theme, style, cursor);
debug.draw_finished();
self.cache = Some(user_interface.into_cache());
@@ -174,6 +174,43 @@ where
(uncaptured_events, command)
}
+
+ /// Applies [`widget::Operation`]s to the [`State`]
+ pub fn operate(
+ &mut self,
+ renderer: &mut P::Renderer,
+ operations: impl Iterator<Item = Box<dyn Operation<P::Message>>>,
+ bounds: Size,
+ debug: &mut Debug,
+ ) {
+ let mut user_interface = build_user_interface(
+ &mut self.program,
+ self.cache.take().unwrap(),
+ renderer,
+ bounds,
+ debug,
+ );
+
+ for operation in operations {
+ let mut current_operation = Some(operation);
+
+ while let Some(mut operation) = current_operation.take() {
+ user_interface.operate(renderer, operation.as_mut());
+
+ match operation.finish() {
+ operation::Outcome::None => {}
+ operation::Outcome::Some(message) => {
+ self.queued_messages.push(message)
+ }
+ operation::Outcome::Chain(next) => {
+ current_operation = Some(next);
+ }
+ };
+ }
+ }
+
+ self.cache = Some(user_interface.into_cache());
+ }
}
fn build_user_interface<'a, P: Program>(
@@ -182,10 +219,7 @@ fn build_user_interface<'a, P: Program>(
renderer: &mut P::Renderer,
size: Size,
debug: &mut Debug,
-) -> UserInterface<'a, P::Message, P::Renderer>
-where
- <P::Renderer as crate::Renderer>::Theme: application::StyleSheet,
-{
+) -> UserInterface<'a, P::Message, P::Renderer> {
debug.view_started();
let view = program.view();
debug.view_finished();
diff --git a/native/src/system.rs b/runtime/src/system.rs
index 61c8ff29..61c8ff29 100644
--- a/native/src/system.rs
+++ b/runtime/src/system.rs
diff --git a/native/src/system/action.rs b/runtime/src/system/action.rs
index dea9536f..dea9536f 100644
--- a/native/src/system/action.rs
+++ b/runtime/src/system/action.rs
diff --git a/native/src/system/information.rs b/runtime/src/system/information.rs
index 93e7a5a4..93e7a5a4 100644
--- a/native/src/system/information.rs
+++ b/runtime/src/system/information.rs
diff --git a/native/src/user_interface.rs b/runtime/src/user_interface.rs
index 68ccda55..619423fd 100644
--- a/native/src/user_interface.rs
+++ b/runtime/src/user_interface.rs
@@ -1,14 +1,12 @@
//! Implement your own event loop to drive a user interface.
-use crate::application;
-use crate::event::{self, Event};
-use crate::layout;
-use crate::mouse;
-use crate::renderer;
-use crate::widget;
-use crate::window;
-use crate::{
- Clipboard, Element, Layout, Point, Rectangle, Shell, Size, Vector,
-};
+use crate::core::event::{self, Event};
+use crate::core::layout;
+use crate::core::mouse;
+use crate::core::renderer;
+use crate::core::widget;
+use crate::core::window;
+use crate::core::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size};
+use crate::overlay;
/// A set of interactive graphical elements with a specific [`Layout`].
///
@@ -18,11 +16,10 @@ use crate::{
/// charge of using this type in your system in any way you want.
///
/// # Example
-/// The [`integration_opengl`] & [`integration_wgpu`] examples use a
-/// [`UserInterface`] to integrate Iced in an existing graphical application.
+/// The [`integration`] example uses a [`UserInterface`] to integrate Iced in an
+/// existing graphical application.
///
-/// [`integration_opengl`]: https://github.com/iced-rs/iced/tree/0.8/examples/integration_opengl
-/// [`integration_wgpu`]: https://github.com/iced-rs/iced/tree/0.8/examples/integration_wgpu
+/// [`integration`]: https://github.com/iced-rs/iced/tree/0.9/examples/integration
#[allow(missing_debug_implementations)]
pub struct UserInterface<'a, Message, Renderer> {
root: Element<'a, Message, Renderer>,
@@ -34,8 +31,7 @@ pub struct UserInterface<'a, Message, Renderer> {
impl<'a, Message, Renderer> UserInterface<'a, Message, Renderer>
where
- Renderer: crate::Renderer,
- Renderer::Theme: application::StyleSheet,
+ Renderer: crate::core::Renderer,
{
/// Builds a user interface for an [`Element`].
///
@@ -48,24 +44,21 @@ where
/// is naive way to set up our application loop:
///
/// ```no_run
- /// use iced_native::Size;
- /// use iced_native::user_interface::{self, UserInterface};
- /// use iced_wgpu::Renderer;
- ///
/// # mod iced_wgpu {
- /// # pub use iced_native::renderer::Null as Renderer;
+ /// # pub use iced_runtime::core::renderer::Null as Renderer;
/// # }
/// #
- /// # use iced_native::widget::Column;
- /// #
/// # pub struct Counter;
/// #
/// # impl Counter {
/// # pub fn new() -> Self { Counter }
- /// # pub fn view(&self) -> Column<(), Renderer> {
- /// # Column::new()
- /// # }
+ /// # pub fn view(&self) -> iced_core::Element<(), Renderer> { unimplemented!() }
+ /// # pub fn update(&mut self, _: ()) {}
/// # }
+ /// use iced_runtime::core::Size;
+ /// use iced_runtime::user_interface::{self, UserInterface};
+ /// use iced_wgpu::Renderer;
+ ///
/// // Initialization
/// let mut counter = Counter::new();
/// let mut cache = user_interface::Cache::new();
@@ -124,30 +117,28 @@ where
/// completing [the previous example](#example):
///
/// ```no_run
- /// use iced_native::{clipboard, Size, Point};
- /// use iced_native::user_interface::{self, UserInterface};
- /// use iced_wgpu::Renderer;
- ///
/// # mod iced_wgpu {
- /// # pub use iced_native::renderer::Null as Renderer;
+ /// # pub use iced_runtime::core::renderer::Null as Renderer;
/// # }
/// #
- /// # use iced_native::widget::Column;
- /// #
/// # pub struct Counter;
/// #
/// # impl Counter {
/// # pub fn new() -> Self { Counter }
- /// # pub fn view(&self) -> Column<(), Renderer> {
- /// # Column::new()
- /// # }
- /// # pub fn update(&mut self, message: ()) {}
+ /// # pub fn view(&self) -> iced_core::Element<(), Renderer> { unimplemented!() }
+ /// # pub fn update(&mut self, _: ()) {}
/// # }
+ /// use iced_runtime::core::clipboard;
+ /// use iced_runtime::core::mouse;
+ /// use iced_runtime::core::Size;
+ /// use iced_runtime::user_interface::{self, UserInterface};
+ /// use iced_wgpu::Renderer;
+ ///
/// let mut counter = Counter::new();
/// let mut cache = user_interface::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 cursor = mouse::Cursor::default();
/// let mut clipboard = clipboard::Null;
///
/// // Initialize our event storage
@@ -167,7 +158,7 @@ where
/// // Update the user interface
/// let (state, event_statuses) = user_interface.update(
/// &events,
- /// cursor_position,
+ /// cursor,
/// &mut renderer,
/// &mut clipboard,
/// &mut messages
@@ -184,7 +175,7 @@ where
pub fn update(
&mut self,
events: &[Event],
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &mut Renderer,
clipboard: &mut dyn Clipboard,
messages: &mut Vec<Message>,
@@ -194,18 +185,18 @@ where
let mut outdated = false;
let mut redraw_request = None;
- let mut manual_overlay =
- ManuallyDrop::new(self.root.as_widget_mut().overlay(
- &mut self.state,
- Layout::new(&self.base),
- renderer,
- ));
+ let mut manual_overlay = ManuallyDrop::new(
+ self.root
+ .as_widget_mut()
+ .overlay(&mut self.state, Layout::new(&self.base), renderer)
+ .map(overlay::Nested::new),
+ );
let (base_cursor, overlay_statuses) = if manual_overlay.is_some() {
let bounds = self.bounds;
let mut overlay = manual_overlay.as_mut().unwrap();
- let mut layout = overlay.layout(renderer, bounds, Vector::ZERO);
+ let mut layout = overlay.layout(renderer, bounds, Point::ORIGIN);
let mut event_statuses = Vec::new();
for event in events.iter().cloned() {
@@ -214,7 +205,7 @@ where
let event_status = overlay.on_event(
event,
Layout::new(&layout),
- cursor_position,
+ cursor,
renderer,
clipboard,
&mut shell,
@@ -240,12 +231,16 @@ where
&layout::Limits::new(Size::ZERO, self.bounds),
);
- manual_overlay =
- ManuallyDrop::new(self.root.as_widget_mut().overlay(
- &mut self.state,
- Layout::new(&self.base),
- renderer,
- ));
+ manual_overlay = ManuallyDrop::new(
+ self.root
+ .as_widget_mut()
+ .overlay(
+ &mut self.state,
+ Layout::new(&self.base),
+ renderer,
+ )
+ .map(overlay::Nested::new),
+ );
if manual_overlay.is_none() {
break;
@@ -254,7 +249,8 @@ where
overlay = manual_overlay.as_mut().unwrap();
shell.revalidate_layout(|| {
- layout = overlay.layout(renderer, bounds, Vector::ZERO);
+ layout =
+ overlay.layout(renderer, bounds, Point::ORIGIN);
});
}
@@ -263,22 +259,29 @@ where
}
}
- let base_cursor = manual_overlay
- .as_ref()
- .filter(|overlay| {
- overlay.is_over(Layout::new(&layout), cursor_position)
- })
- .map(|_| {
- // TODO: Type-safe cursor availability
- Point::new(-1.0, -1.0)
+ let base_cursor = if manual_overlay
+ .as_mut()
+ .and_then(|overlay| {
+ cursor.position().map(|cursor_position| {
+ overlay.is_over(
+ Layout::new(&layout),
+ renderer,
+ cursor_position,
+ )
+ })
})
- .unwrap_or(cursor_position);
+ .unwrap_or_default()
+ {
+ mouse::Cursor::Unavailable
+ } else {
+ cursor
+ };
self.overlay = Some(layout);
(base_cursor, event_statuses)
} else {
- (cursor_position, vec![event::Status::Ignored; events.len()])
+ (cursor, vec![event::Status::Ignored; events.len()])
};
let _ = ManuallyDrop::into_inner(manual_overlay);
@@ -357,35 +360,34 @@ where
/// [completing the last example](#example-1):
///
/// ```no_run
- /// use iced_native::clipboard;
- /// use iced_native::renderer;
- /// use iced_native::user_interface::{self, UserInterface};
- /// use iced_native::{Size, Point, Theme};
- /// use iced_wgpu::Renderer;
- ///
/// # mod iced_wgpu {
- /// # pub use iced_native::renderer::Null as Renderer;
+ /// # pub use iced_runtime::core::renderer::Null as Renderer;
+ /// # pub type Theme = ();
/// # }
/// #
- /// # use iced_native::widget::Column;
- /// #
/// # pub struct Counter;
/// #
/// # impl Counter {
/// # pub fn new() -> Self { Counter }
- /// # pub fn view(&self) -> Column<(), Renderer> {
- /// # Column::new()
- /// # }
- /// # pub fn update(&mut self, message: ()) {}
+ /// # pub fn view(&self) -> Element<(), Renderer> { unimplemented!() }
+ /// # pub fn update(&mut self, _: ()) {}
/// # }
+ /// use iced_runtime::core::clipboard;
+ /// use iced_runtime::core::mouse;
+ /// use iced_runtime::core::renderer;
+ /// use iced_runtime::core::{Element, Size};
+ /// use iced_runtime::user_interface::{self, UserInterface};
+ /// use iced_wgpu::{Renderer, Theme};
+ ///
/// let mut counter = Counter::new();
/// let mut cache = user_interface::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 cursor = mouse::Cursor::default();
/// let mut clipboard = clipboard::Null;
/// let mut events = Vec::new();
/// let mut messages = Vec::new();
+ /// let mut theme = Theme::default();
///
/// loop {
/// // Obtain system events...
@@ -400,14 +402,14 @@ where
/// // Update the user interface
/// let event_statuses = user_interface.update(
/// &events,
- /// cursor_position,
+ /// cursor,
/// &mut renderer,
/// &mut clipboard,
/// &mut messages
/// );
///
/// // Draw the user interface
- /// let mouse_cursor = user_interface.draw(&mut renderer, &Theme::default(), &renderer::Style::default(), cursor_position);
+ /// let mouse_interaction = user_interface.draw(&mut renderer, &theme, &renderer::Style::default(), cursor);
///
/// cache = user_interface.into_cache();
///
@@ -424,35 +426,44 @@ where
renderer: &mut Renderer,
theme: &Renderer::Theme,
style: &renderer::Style,
- cursor_position: Point,
+ cursor: mouse::Cursor,
) -> mouse::Interaction {
// TODO: Move to shell level (?)
renderer.clear();
let viewport = Rectangle::with_size(self.bounds);
- let base_cursor = if let Some(overlay) = self
+ let base_cursor = if let Some(mut overlay) = self
.root
.as_widget_mut()
.overlay(&mut self.state, Layout::new(&self.base), renderer)
+ .map(overlay::Nested::new)
{
let overlay_layout = self.overlay.take().unwrap_or_else(|| {
- overlay.layout(renderer, self.bounds, Vector::ZERO)
+ overlay.layout(renderer, self.bounds, Point::ORIGIN)
});
- let new_cursor_position = if overlay
- .is_over(Layout::new(&overlay_layout), cursor_position)
+ let cursor = if cursor
+ .position()
+ .map(|cursor_position| {
+ overlay.is_over(
+ Layout::new(&overlay_layout),
+ renderer,
+ cursor_position,
+ )
+ })
+ .unwrap_or_default()
{
- Point::new(-1.0, -1.0)
+ mouse::Cursor::Unavailable
} else {
- cursor_position
+ cursor
};
self.overlay = Some(overlay_layout);
- new_cursor_position
+ cursor
} else {
- cursor_position
+ cursor
};
self.root.as_widget().draw(
@@ -468,7 +479,7 @@ where
let base_interaction = self.root.as_widget().mouse_interaction(
&self.state,
Layout::new(&self.base),
- cursor_position,
+ base_cursor,
&viewport,
renderer,
);
@@ -490,10 +501,11 @@ where
.and_then(|layout| {
root.as_widget_mut()
.overlay(&mut self.state, Layout::new(base), renderer)
- .map(|overlay| {
+ .map(overlay::Nested::new)
+ .map(|mut overlay| {
let overlay_interaction = overlay.mouse_interaction(
Layout::new(layout),
- cursor_position,
+ cursor,
&viewport,
renderer,
);
@@ -506,11 +518,20 @@ where
theme,
style,
Layout::new(layout),
- cursor_position,
+ cursor,
);
});
- if overlay.is_over(Layout::new(layout), cursor_position)
+ if cursor
+ .position()
+ .map(|cursor_position| {
+ overlay.is_over(
+ Layout::new(layout),
+ renderer,
+ cursor_position,
+ )
+ })
+ .unwrap_or_default()
{
overlay_interaction
} else {
@@ -534,14 +555,15 @@ where
operation,
);
- if let Some(mut overlay) = self.root.as_widget_mut().overlay(
- &mut self.state,
- Layout::new(&self.base),
- renderer,
- ) {
+ if let Some(mut overlay) = self
+ .root
+ .as_widget_mut()
+ .overlay(&mut self.state, Layout::new(&self.base), renderer)
+ .map(overlay::Nested::new)
+ {
if self.overlay.is_none() {
self.overlay =
- Some(overlay.layout(renderer, self.bounds, Vector::ZERO));
+ Some(overlay.layout(renderer, self.bounds, Point::ORIGIN));
}
overlay.operate(
diff --git a/runtime/src/window.rs b/runtime/src/window.rs
new file mode 100644
index 00000000..5219fbfd
--- /dev/null
+++ b/runtime/src/window.rs
@@ -0,0 +1,135 @@
+//! Build window-based GUI applications.
+mod action;
+
+pub mod screenshot;
+
+pub use action::Action;
+pub use screenshot::Screenshot;
+
+use crate::command::{self, Command};
+use crate::core::time::Instant;
+use crate::core::window::{Event, Icon, Level, Mode, UserAttention};
+use crate::core::Size;
+use crate::futures::subscription::{self, Subscription};
+
+/// Subscribes to the frames of the window of the running application.
+///
+/// The resulting [`Subscription`] will produce items at a rate equal to the
+/// refresh rate of the window. Note that this rate may be variable, as it is
+/// normally managed by the graphics driver and/or the OS.
+///
+/// In any case, this [`Subscription`] is useful to smoothly draw application-driven
+/// animations without missing any frames.
+pub fn frames() -> Subscription<Instant> {
+ subscription::raw_events(|event, _status| match event {
+ iced_core::Event::Window(Event::RedrawRequested(at)) => Some(at),
+ _ => None,
+ })
+}
+
+/// Closes the current window and exits the application.
+pub fn close<Message>() -> Command<Message> {
+ Command::single(command::Action::Window(Action::Close))
+}
+
+/// Begins dragging the window while the left mouse button is held.
+pub fn drag<Message>() -> Command<Message> {
+ Command::single(command::Action::Window(Action::Drag))
+}
+
+/// Resizes the window to the given logical dimensions.
+pub fn resize<Message>(new_size: Size<u32>) -> Command<Message> {
+ Command::single(command::Action::Window(Action::Resize(new_size)))
+}
+
+/// Fetches the current window size in logical dimensions.
+pub fn fetch_size<Message>(
+ f: impl FnOnce(Size<u32>) -> Message + 'static,
+) -> Command<Message> {
+ Command::single(command::Action::Window(Action::FetchSize(Box::new(f))))
+}
+
+/// Maximizes the window.
+pub fn maximize<Message>(maximized: bool) -> Command<Message> {
+ Command::single(command::Action::Window(Action::Maximize(maximized)))
+}
+
+/// Minimes the window.
+pub fn minimize<Message>(minimized: bool) -> Command<Message> {
+ Command::single(command::Action::Window(Action::Minimize(minimized)))
+}
+
+/// Moves a window to the given logical coordinates.
+pub fn move_to<Message>(x: i32, y: i32) -> Command<Message> {
+ Command::single(command::Action::Window(Action::Move { x, y }))
+}
+
+/// Changes the [`Mode`] of the window.
+pub fn change_mode<Message>(mode: Mode) -> Command<Message> {
+ Command::single(command::Action::Window(Action::ChangeMode(mode)))
+}
+
+/// Fetches the current [`Mode`] of the window.
+pub fn fetch_mode<Message>(
+ f: impl FnOnce(Mode) -> Message + 'static,
+) -> Command<Message> {
+ Command::single(command::Action::Window(Action::FetchMode(Box::new(f))))
+}
+
+/// Toggles the window to maximized or back.
+pub fn toggle_maximize<Message>() -> Command<Message> {
+ Command::single(command::Action::Window(Action::ToggleMaximize))
+}
+
+/// Toggles the window decorations.
+pub fn toggle_decorations<Message>() -> Command<Message> {
+ Command::single(command::Action::Window(Action::ToggleDecorations))
+}
+
+/// Request user attention to the window, this has no effect if the application
+/// is already focused. How requesting for user attention manifests is platform dependent,
+/// see [`UserAttention`] for details.
+///
+/// Providing `None` will unset the request for user attention. Unsetting the request for
+/// user attention might not be done automatically by the WM when the window receives input.
+pub fn request_user_attention<Message>(
+ user_attention: Option<UserAttention>,
+) -> Command<Message> {
+ Command::single(command::Action::Window(Action::RequestUserAttention(
+ user_attention,
+ )))
+}
+
+/// Brings the window to the front and sets input focus. Has no effect if the window is
+/// already in focus, minimized, or not visible.
+///
+/// This [`Command`] steals input focus from other applications. Do not use this method unless
+/// you are certain that's what the user wants. Focus stealing can cause an extremely disruptive
+/// user experience.
+pub fn gain_focus<Message>() -> Command<Message> {
+ Command::single(command::Action::Window(Action::GainFocus))
+}
+
+/// Changes the window [`Level`].
+pub fn change_level<Message>(level: Level) -> Command<Message> {
+ Command::single(command::Action::Window(Action::ChangeLevel(level)))
+}
+
+/// Fetches an identifier unique to the window.
+pub fn fetch_id<Message>(
+ f: impl FnOnce(u64) -> Message + 'static,
+) -> Command<Message> {
+ Command::single(command::Action::Window(Action::FetchId(Box::new(f))))
+}
+
+/// Changes the [`Icon`] of the window.
+pub fn change_icon<Message>(icon: Icon) -> Command<Message> {
+ Command::single(command::Action::Window(Action::ChangeIcon(icon)))
+}
+
+/// Captures a [`Screenshot`] from the window.
+pub fn screenshot<Message>(
+ f: impl FnOnce(Screenshot) -> Message + Send + 'static,
+) -> Command<Message> {
+ Command::single(command::Action::Window(Action::Screenshot(Box::new(f))))
+}
diff --git a/native/src/window/action.rs b/runtime/src/window/action.rs
index 5751bf97..cebec4ae 100644
--- a/native/src/window/action.rs
+++ b/runtime/src/window/action.rs
@@ -1,13 +1,15 @@
-use crate::window::{Mode, UserAttention, Settings};
+use crate::core::window::{Icon, Level, Mode, UserAttention, Settings};
+use crate::core::Size;
+use crate::futures::MaybeSend;
+use crate::window::Screenshot;
-use iced_futures::MaybeSend;
use std::fmt;
/// An operation to be performed on some window.
pub enum Action<T> {
- /// Closes the current window and exits the application.
+ /// Close the current window and exits the application.
Close,
- /// Moves the window with the left mouse button until the button is
+ /// Move the window with the left mouse button until the button is
/// released.
///
/// There’s no guarantee that this will work unless the left mouse
@@ -19,13 +21,10 @@ pub enum Action<T> {
settings: Settings,
},
/// Resize the window.
- Resize {
- /// The new logical width of the window
- width: u32,
- /// The new logical height of the window
- height: u32,
- },
- /// Sets the window to maximized or back
+ Resize(Size<u32>),
+ /// Fetch the current size of the window.
+ FetchSize(Box<dyn FnOnce(Size<u32>) -> T + 'static>),
+ /// Set the window to maximized or back
Maximize(bool),
/// Set the window to minimized or back
Minimize(bool),
@@ -75,14 +74,27 @@ pub enum Action<T> {
///
/// - **Web / Wayland:** Unsupported.
GainFocus,
- /// Change whether or not the window will always be on top of other windows.
+ /// Change the window [`Level`].
+ ChangeLevel(Level),
+ /// Fetch an identifier unique to the window.
+ FetchId(Box<dyn FnOnce(u64) -> T + 'static>),
+ /// Change the window [`Icon`].
+ ///
+ /// On Windows and X11, this is typically the small icon in the top-left
+ /// corner of the titlebar.
///
/// ## Platform-specific
///
- /// - **Web / Wayland:** Unsupported.
- ChangeAlwaysOnTop(bool),
- /// Fetch an identifier unique to the window.
- FetchId(Box<dyn FnOnce(u64) -> T + 'static>),
+ /// - **Web / Wayland / macOS:** Unsupported.
+ ///
+ /// - **Windows:** Sets `ICON_SMALL`. The base size for a window icon is 16x16, but it's
+ /// recommended to account for screen scaling and pick a multiple of that, i.e. 32x32.
+ ///
+ /// - **X11:** Has no universal guidelines for icon sizes, so you're at the whims of the WM. That
+ /// said, it's usually in the same ballpark as on Windows.
+ ChangeIcon(Icon),
+ /// Screenshot the viewport of the window.
+ Screenshot(Box<dyn FnOnce(Screenshot) -> T + 'static>),
}
impl<T> Action<T> {
@@ -95,10 +107,11 @@ impl<T> Action<T> {
T: 'static,
{
match self {
- Self::Spawn { settings } => Action::Spawn { settings },
Self::Close => Action::Close,
Self::Drag => Action::Drag,
- Self::Resize { width, height } => Action::Resize { width, height },
+ Self::Spawn { settings } => Action::Spawn { settings },
+ Self::Resize(size) => Action::Resize(size),
+ Self::FetchSize(o) => Action::FetchSize(Box::new(move |s| f(o(s)))),
Self::Maximize(maximized) => Action::Maximize(maximized),
Self::Minimize(minimized) => Action::Minimize(minimized),
Self::Move { x, y } => Action::Move { x, y },
@@ -110,10 +123,14 @@ impl<T> Action<T> {
Action::RequestUserAttention(attention_type)
}
Self::GainFocus => Action::GainFocus,
- Self::ChangeAlwaysOnTop(on_top) => {
- Action::ChangeAlwaysOnTop(on_top)
- }
+ Self::ChangeLevel(level) => Action::ChangeLevel(level),
Self::FetchId(o) => Action::FetchId(Box::new(move |s| f(o(s)))),
+ Self::ChangeIcon(icon) => Action::ChangeIcon(icon),
+ Self::Screenshot(tag) => {
+ Action::Screenshot(Box::new(move |screenshot| {
+ f(tag(screenshot))
+ }))
+ }
}
}
}
@@ -126,10 +143,8 @@ impl<T> fmt::Debug for Action<T> {
Self::Spawn { settings } => {
write!(f, "Action::Spawn {{ settings: {:?} }}", settings)
}
- Self::Resize { width, height } => write!(
- f,
- "Action::Resize {{ widget: {width}, height: {height} }}"
- ),
+ Self::Resize(size) => write!(f, "Action::Resize({size:?})"),
+ Self::FetchSize(_) => write!(f, "Action::FetchSize"),
Self::Maximize(maximized) => {
write!(f, "Action::Maximize({maximized})")
}
@@ -147,10 +162,14 @@ impl<T> fmt::Debug for Action<T> {
write!(f, "Action::RequestUserAttention")
}
Self::GainFocus => write!(f, "Action::GainFocus"),
- Self::ChangeAlwaysOnTop(on_top) => {
- write!(f, "Action::AlwaysOnTop({on_top})")
+ Self::ChangeLevel(level) => {
+ write!(f, "Action::ChangeLevel({level:?})")
}
Self::FetchId(_) => write!(f, "Action::FetchId"),
+ Self::ChangeIcon(_icon) => {
+ write!(f, "Action::ChangeIcon(icon)")
+ }
+ Self::Screenshot(_) => write!(f, "Action::Screenshot"),
}
}
}
diff --git a/runtime/src/window/screenshot.rs b/runtime/src/window/screenshot.rs
new file mode 100644
index 00000000..c84286b6
--- /dev/null
+++ b/runtime/src/window/screenshot.rs
@@ -0,0 +1,92 @@
+//! Take screenshots of a window.
+use crate::core::{Rectangle, Size};
+
+use std::fmt::{Debug, Formatter};
+use std::sync::Arc;
+
+/// Data of a screenshot, captured with `window::screenshot()`.
+///
+/// The `bytes` of this screenshot will always be ordered as `RGBA` in the sRGB color space.
+#[derive(Clone)]
+pub struct Screenshot {
+ /// The bytes of the [`Screenshot`].
+ pub bytes: Arc<Vec<u8>>,
+ /// The size of the [`Screenshot`].
+ pub size: Size<u32>,
+}
+
+impl Debug for Screenshot {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "Screenshot: {{ \n bytes: {}\n size: {:?} }}",
+ self.bytes.len(),
+ self.size
+ )
+ }
+}
+
+impl Screenshot {
+ /// Creates a new [`Screenshot`].
+ pub fn new(bytes: Vec<u8>, size: Size<u32>) -> Self {
+ Self {
+ bytes: Arc::new(bytes),
+ size,
+ }
+ }
+
+ /// Crops a [`Screenshot`] to the provided `region`. This will always be relative to the
+ /// top-left corner of the [`Screenshot`].
+ pub fn crop(&self, region: Rectangle<u32>) -> Result<Self, CropError> {
+ if region.width == 0 || region.height == 0 {
+ return Err(CropError::Zero);
+ }
+
+ if region.x + region.width > self.size.width
+ || region.y + region.height > self.size.height
+ {
+ return Err(CropError::OutOfBounds);
+ }
+
+ // Image is always RGBA8 = 4 bytes per pixel
+ const PIXEL_SIZE: usize = 4;
+
+ let bytes_per_row = self.size.width as usize * PIXEL_SIZE;
+ let row_range = region.y as usize..(region.y + region.height) as usize;
+ let column_range = region.x as usize * PIXEL_SIZE
+ ..(region.x + region.width) as usize * PIXEL_SIZE;
+
+ let chopped = self.bytes.chunks(bytes_per_row).enumerate().fold(
+ vec![],
+ |mut acc, (row, bytes)| {
+ if row_range.contains(&row) {
+ acc.extend(&bytes[column_range.clone()]);
+ }
+
+ acc
+ },
+ );
+
+ Ok(Self {
+ bytes: Arc::new(chopped),
+ size: Size::new(region.width, region.height),
+ })
+ }
+}
+
+impl AsRef<[u8]> for Screenshot {
+ fn as_ref(&self) -> &[u8] {
+ &self.bytes
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+/// Errors that can occur when cropping a [`Screenshot`].
+pub enum CropError {
+ #[error("The cropped region is out of bounds.")]
+ /// The cropped region's size is out of bounds.
+ OutOfBounds,
+ #[error("The cropped region is not visible.")]
+ /// The cropped region's size is zero.
+ Zero,
+}
diff --git a/src/advanced.rs b/src/advanced.rs
new file mode 100644
index 00000000..2071aed0
--- /dev/null
+++ b/src/advanced.rs
@@ -0,0 +1,16 @@
+//! Leverage advanced concepts like custom widgets.
+pub use crate::core::image;
+pub use crate::core::layout::{self, Layout};
+pub use crate::core::mouse;
+pub use crate::core::overlay::{self, Overlay};
+pub use crate::core::renderer::{self, Renderer};
+pub use crate::core::svg;
+pub use crate::core::text::{self, Text};
+pub use crate::core::widget::{self, Widget};
+pub use crate::core::{Clipboard, Hasher, Shell};
+pub use crate::renderer::graphics;
+
+pub mod subscription {
+ //! Write your own subscriptions.
+ pub use crate::runtime::futures::subscription::{EventStream, Recipe};
+}
diff --git a/src/application.rs b/src/application.rs
index 1db5c93f..abf58fa3 100644
--- a/src/application.rs
+++ b/src/application.rs
@@ -1,7 +1,7 @@
//! Build interactive cross-platform applications.
use crate::{Command, Element, Executor, Settings, Subscription};
-pub use iced_native::application::{Appearance, StyleSheet};
+pub use crate::style::application::{Appearance, StyleSheet};
/// An interactive cross-platform application.
///
@@ -39,15 +39,15 @@ pub use iced_native::application::{Appearance, StyleSheet};
/// to listen to time.
/// - [`todos`], a todos tracker inspired by [TodoMVC].
///
-/// [The repository has a bunch of examples]: https://github.com/iced-rs/iced/tree/0.8/examples
-/// [`clock`]: https://github.com/iced-rs/iced/tree/0.8/examples/clock
-/// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.8/examples/download_progress
-/// [`events`]: https://github.com/iced-rs/iced/tree/0.8/examples/events
-/// [`game_of_life`]: https://github.com/iced-rs/iced/tree/0.8/examples/game_of_life
-/// [`pokedex`]: https://github.com/iced-rs/iced/tree/0.8/examples/pokedex
-/// [`solar_system`]: https://github.com/iced-rs/iced/tree/0.8/examples/solar_system
-/// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.8/examples/stopwatch
-/// [`todos`]: https://github.com/iced-rs/iced/tree/0.8/examples/todos
+/// [The repository has a bunch of examples]: https://github.com/iced-rs/iced/tree/0.9/examples
+/// [`clock`]: https://github.com/iced-rs/iced/tree/0.9/examples/clock
+/// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.9/examples/download_progress
+/// [`events`]: https://github.com/iced-rs/iced/tree/0.9/examples/events
+/// [`game_of_life`]: https://github.com/iced-rs/iced/tree/0.9/examples/game_of_life
+/// [`pokedex`]: https://github.com/iced-rs/iced/tree/0.9/examples/pokedex
+/// [`solar_system`]: https://github.com/iced-rs/iced/tree/0.9/examples/solar_system
+/// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.9/examples/stopwatch
+/// [`todos`]: https://github.com/iced-rs/iced/tree/0.9/examples/todos
/// [`Sandbox`]: crate::Sandbox
/// [`Canvas`]: crate::widget::Canvas
/// [PokéAPI]: https://pokeapi.co/
@@ -197,26 +197,25 @@ pub trait Application: Sized {
let renderer_settings = crate::renderer::Settings {
default_font: settings.default_font,
default_text_size: settings.default_text_size,
- text_multithreading: settings.text_multithreading,
antialiasing: if settings.antialiasing {
- Some(crate::renderer::settings::Antialiasing::MSAAx4)
+ Some(crate::graphics::Antialiasing::MSAAx4)
} else {
None
},
- ..crate::renderer::Settings::from_env()
+ ..crate::renderer::Settings::default()
};
- Ok(crate::runtime::application::run::<
+ Ok(crate::shell::application::run::<
Instance<Self>,
Self::Executor,
- crate::renderer::window::Compositor<Self::Theme>,
+ crate::renderer::Compositor<Self::Theme>,
>(settings.into(), renderer_settings)?)
}
}
struct Instance<A: Application>(A);
-impl<A> iced_winit::Program for Instance<A>
+impl<A> crate::runtime::Program for Instance<A>
where
A: Application,
{
@@ -232,7 +231,7 @@ where
}
}
-impl<A> crate::runtime::Application for Instance<A>
+impl<A> crate::shell::Application for Instance<A>
where
A: Application,
{
diff --git a/src/clipboard.rs b/src/clipboard.rs
deleted file mode 100644
index dde17051..00000000
--- a/src/clipboard.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-//! Access the clipboard.
-#[cfg(not(target_arch = "wasm32"))]
-pub use crate::runtime::clipboard::{read, write};
diff --git a/src/element.rs b/src/element.rs
deleted file mode 100644
index 2eb1bb4d..00000000
--- a/src/element.rs
+++ /dev/null
@@ -1,5 +0,0 @@
-/// A generic widget.
-///
-/// This is an alias of an `iced_native` element with a default `Renderer`.
-pub type Element<'a, Message, Renderer = crate::Renderer> =
- crate::runtime::Element<'a, Message, Renderer>;
diff --git a/src/error.rs b/src/error.rs
index 0bfa3ff1..111bedf2 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -1,4 +1,6 @@
-use iced_futures::futures;
+use crate::futures;
+use crate::graphics;
+use crate::shell;
/// An error that occurred while running an application.
#[derive(Debug, thiserror::Error)]
@@ -13,19 +15,19 @@ pub enum Error {
/// The application graphics context could not be created.
#[error("the application graphics context could not be created")]
- GraphicsCreationFailed(iced_graphics::Error),
+ GraphicsCreationFailed(graphics::Error),
}
-impl From<iced_winit::Error> for Error {
- fn from(error: iced_winit::Error) -> Error {
+impl From<shell::Error> for Error {
+ fn from(error: shell::Error) -> Error {
match error {
- iced_winit::Error::ExecutorCreationFailed(error) => {
+ shell::Error::ExecutorCreationFailed(error) => {
Error::ExecutorCreationFailed(error)
}
- iced_winit::Error::WindowCreationFailed(error) => {
+ shell::Error::WindowCreationFailed(error) => {
Error::WindowCreationFailed(Box::new(error))
}
- iced_winit::Error::GraphicsCreationFailed(error) => {
+ shell::Error::GraphicsCreationFailed(error) => {
Error::GraphicsCreationFailed(error)
}
}
diff --git a/src/executor.rs b/src/executor.rs
deleted file mode 100644
index 36ae274e..00000000
--- a/src/executor.rs
+++ /dev/null
@@ -1,14 +0,0 @@
-//! Choose your preferred executor to power your application.
-pub use crate::runtime::Executor;
-
-/// A default cross-platform executor.
-///
-/// - On native platforms, it will use:
-/// - `iced_futures::backend::native::tokio` when the `tokio` feature is enabled.
-/// - `iced_futures::backend::native::async-std` when the `async-std` feature is
-/// enabled.
-/// - `iced_futures::backend::native::smol` when the `smol` feature is enabled.
-/// - `iced_futures::backend::native::thread_pool` otherwise.
-///
-/// - On Wasm, it will use `iced_futures::backend::wasm::wasm_bindgen`.
-pub type Default = iced_futures::backend::default::Executor;
diff --git a/src/keyboard.rs b/src/keyboard.rs
deleted file mode 100644
index 2134a66b..00000000
--- a/src/keyboard.rs
+++ /dev/null
@@ -1,2 +0,0 @@
-//! 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 e7481c77..4ddcd9d8 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -24,13 +24,13 @@
//! [scrollables]: https://gfycat.com/perkybaggybaboon-rust-gui
//! [Debug overlay with performance metrics]: https://gfycat.com/incredibledarlingbee
//! [Modular ecosystem]: https://github.com/iced-rs/iced/blob/master/ECOSYSTEM.md
-//! [renderer-agnostic native runtime]: https://github.com/iced-rs/iced/tree/0.8/native
+//! [renderer-agnostic native runtime]: https://github.com/iced-rs/iced/tree/0.9/native
//! [`wgpu`]: https://github.com/gfx-rs/wgpu-rs
-//! [built-in renderer]: https://github.com/iced-rs/iced/tree/0.8/wgpu
-//! [windowing shell]: https://github.com/iced-rs/iced/tree/0.8/winit
+//! [built-in renderer]: https://github.com/iced-rs/iced/tree/0.9/wgpu
+//! [windowing shell]: https://github.com/iced-rs/iced/tree/0.9/winit
//! [`dodrio`]: https://github.com/fitzgen/dodrio
//! [web runtime]: https://github.com/iced-rs/iced_web
-//! [examples]: https://github.com/iced-rs/iced/tree/0.8/examples
+//! [examples]: https://github.com/iced-rs/iced/tree/0.9/examples
//! [repository]: https://github.com/iced-rs/iced
//!
//! # Overview
@@ -163,62 +163,147 @@
)]
#![forbid(rust_2018_idioms, unsafe_code)]
#![allow(clippy::inherent_to_string, clippy::type_complexity)]
-#![cfg_attr(docsrs, feature(doc_cfg))]
+#![cfg_attr(docsrs, feature(doc_auto_cfg))]
+use iced_widget::graphics;
+use iced_widget::renderer;
+use iced_widget::style;
+use iced_winit as shell;
+use iced_winit::core;
+use iced_winit::runtime;
+
+pub use iced_futures::futures;
-mod element;
mod error;
-mod result;
mod sandbox;
pub mod application;
-pub mod clipboard;
-pub mod executor;
-pub mod keyboard;
-pub mod mouse;
-pub mod overlay;
pub mod settings;
pub mod time;
-pub mod touch;
-pub mod widget;
pub mod window;
-#[cfg(all(not(feature = "glow"), feature = "multi-window"))]
+#[cfg(feature = "advanced")]
+pub mod advanced;
+
+#[cfg(feature = "multi-window")]
pub mod multi_window;
-#[cfg(all(not(feature = "glow"), feature = "wgpu"))]
-use iced_winit as runtime;
+pub use style::theme;
+
+pub use crate::core::alignment;
+pub use crate::core::event;
+pub use crate::core::gradient;
+pub use crate::core::{
+ color, Alignment, Background, Color, ContentFit, Degrees, Gradient, Length,
+ Padding, Pixels, Point, Radians, Rectangle, Size, Vector,
+};
+pub use crate::runtime::Command;
+
+pub mod clipboard {
+ //! Access the clipboard.
+ pub use crate::runtime::clipboard::{read, write};
+}
+
+pub mod executor {
+ //! Choose your preferred executor to power your application.
+ pub use iced_futures::Executor;
+
+ /// A default cross-platform executor.
+ ///
+ /// - On native platforms, it will use:
+ /// - `iced_futures::backend::native::tokio` when the `tokio` feature is enabled.
+ /// - `iced_futures::backend::native::async-std` when the `async-std` feature is
+ /// enabled.
+ /// - `iced_futures::backend::native::smol` when the `smol` feature is enabled.
+ /// - `iced_futures::backend::native::thread_pool` otherwise.
+ ///
+ /// - On Wasm, it will use `iced_futures::backend::wasm::wasm_bindgen`.
+ pub type Default = iced_futures::backend::default::Executor;
+}
+
+pub mod font {
+ //! Load and use fonts.
+ pub use crate::core::font::*;
+ pub use crate::runtime::font::*;
+}
+
+pub mod keyboard {
+ //! Listen and react to keyboard events.
+ pub use crate::core::keyboard::{Event, KeyCode, Modifiers};
+}
-#[cfg(feature = "glow")]
-use iced_glutin as runtime;
+pub mod mouse {
+ //! Listen and react to mouse events.
+ pub use crate::core::mouse::{
+ Button, Cursor, Event, Interaction, ScrollDelta,
+ };
+}
-#[cfg(all(not(feature = "glow"), feature = "wgpu"))]
-use iced_wgpu as renderer;
+pub mod subscription {
+ //! Listen to external events in your application.
+ pub use iced_futures::subscription::{
+ channel, events, events_with, run, run_with_id, unfold, Subscription,
+ };
+}
-#[cfg(feature = "glow")]
-use iced_glow as renderer;
+#[cfg(feature = "system")]
+pub mod system {
+ //! Retrieve system information.
+ pub use crate::runtime::system::Information;
+ pub use crate::shell::system::*;
+}
+
+pub mod overlay {
+ //! Display interactive elements on top of other widgets.
+
+ /// A generic [`Overlay`].
+ ///
+ /// This is an alias of an `iced_native` element with a default `Renderer`.
+ ///
+ /// [`Overlay`]: iced_native::Overlay
+ pub type Element<'a, Message, Renderer = crate::Renderer> =
+ crate::core::overlay::Element<'a, Message, Renderer>;
+
+ pub use iced_widget::overlay::*;
+}
+
+pub mod touch {
+ //! Listen and react to touch events.
+ pub use crate::core::touch::{Event, Finger};
+}
-pub use iced_native::theme;
-pub use runtime::event;
-pub use runtime::subscription;
+pub mod widget {
+ //! Use the built-in widgets or create your own.
+ pub use iced_widget::*;
+
+ // We hide the re-exported modules by `iced_widget`
+ mod core {}
+ mod graphics {}
+ mod native {}
+ mod renderer {}
+ mod style {}
+ mod runtime {}
+}
pub use application::Application;
-pub use element::Element;
pub use error::Error;
pub use event::Event;
pub use executor::Executor;
-pub use renderer::Renderer;
-pub use result::Result;
+pub use font::Font;
pub use sandbox::Sandbox;
pub use settings::Settings;
pub use subscription::Subscription;
pub use theme::Theme;
-pub use runtime::alignment;
-pub use runtime::futures;
-pub use runtime::{
- color, Alignment, Background, Color, Command, ContentFit, Font, Length,
- Padding, Point, Rectangle, Size, Vector,
-};
+/// The default renderer.
+pub type Renderer<Theme = style::Theme> = renderer::Renderer<Theme>;
-#[cfg(feature = "system")]
-pub use runtime::system;
+/// A generic widget.
+///
+/// This is an alias of an `iced_native` element with a default `Renderer`.
+pub type Element<'a, Message, Renderer = crate::Renderer> =
+ crate::core::Element<'a, Message, Renderer>;
+
+/// The result of running an [`Application`].
+///
+/// [`Application`]: crate::Application
+pub type Result = std::result::Result<(), Error>;
diff --git a/src/mouse.rs b/src/mouse.rs
deleted file mode 100644
index d61ed09a..00000000
--- a/src/mouse.rs
+++ /dev/null
@@ -1,2 +0,0 @@
-//! Listen and react to mouse events.
-pub use crate::runtime::mouse::{Button, Event, Interaction, ScrollDelta};
diff --git a/src/overlay.rs b/src/overlay.rs
deleted file mode 100644
index c0f4c492..00000000
--- a/src/overlay.rs
+++ /dev/null
@@ -1,18 +0,0 @@
-//! Display interactive elements on top of other widgets.
-
-/// A generic [`Overlay`].
-///
-/// This is an alias of an `iced_native` element with a default `Renderer`.
-///
-/// [`Overlay`]: iced_native::Overlay
-pub type Element<'a, Message, Renderer = crate::Renderer> =
- iced_native::overlay::Element<'a, Message, Renderer>;
-
-pub mod menu {
- //! Build and show dropdown menus.
- pub use iced_native::overlay::menu::{Appearance, State, StyleSheet};
-
- /// A widget that produces a message when clicked.
- pub type Menu<'a, Message, Renderer = crate::Renderer> =
- iced_native::overlay::Menu<'a, Message, Renderer>;
-}
diff --git a/src/result.rs b/src/result.rs
deleted file mode 100644
index ef565bd6..00000000
--- a/src/result.rs
+++ /dev/null
@@ -1,6 +0,0 @@
-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 e8ed0f81..cca327b6 100644
--- a/src/sandbox.rs
+++ b/src/sandbox.rs
@@ -34,19 +34,19 @@ use crate::{Application, Command, Element, Error, Settings, Subscription};
/// - [`tour`], a simple UI tour that can run both on native platforms and the
/// web!
///
-/// [The repository has a bunch of examples]: https://github.com/iced-rs/iced/tree/0.8/examples
-/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.8/examples/bezier_tool
-/// [`counter`]: https://github.com/iced-rs/iced/tree/0.8/examples/counter
-/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.8/examples/custom_widget
-/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.8/examples/geometry
-/// [`pane_grid`]: https://github.com/iced-rs/iced/tree/0.8/examples/pane_grid
-/// [`progress_bar`]: https://github.com/iced-rs/iced/tree/0.8/examples/progress_bar
-/// [`styling`]: https://github.com/iced-rs/iced/tree/0.8/examples/styling
-/// [`svg`]: https://github.com/iced-rs/iced/tree/0.8/examples/svg
-/// [`tour`]: https://github.com/iced-rs/iced/tree/0.8/examples/tour
+/// [The repository has a bunch of examples]: https://github.com/iced-rs/iced/tree/0.9/examples
+/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.9/examples/bezier_tool
+/// [`counter`]: https://github.com/iced-rs/iced/tree/0.9/examples/counter
+/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.9/examples/custom_widget
+/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.9/examples/geometry
+/// [`pane_grid`]: https://github.com/iced-rs/iced/tree/0.9/examples/pane_grid
+/// [`progress_bar`]: https://github.com/iced-rs/iced/tree/0.9/examples/progress_bar
+/// [`styling`]: https://github.com/iced-rs/iced/tree/0.9/examples/styling
+/// [`svg`]: https://github.com/iced-rs/iced/tree/0.9/examples/svg
+/// [`tour`]: https://github.com/iced-rs/iced/tree/0.9/examples/tour
/// [`Canvas widget`]: crate::widget::Canvas
/// [the overview]: index.html#overview
-/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.8/wgpu
+/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.9/wgpu
/// [`Svg` widget]: crate::widget::Svg
/// [Ghostscript Tiger]: https://commons.wikimedia.org/wiki/File:Ghostscript_Tiger.svg
///
diff --git a/src/settings.rs b/src/settings.rs
index 0eb3e62d..0dd46584 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -1,5 +1,6 @@
//! Configure your application.
use crate::window;
+use crate::Font;
/// The settings of an application.
#[derive(Debug, Clone)]
@@ -20,23 +21,16 @@ pub struct Settings<Flags> {
/// [`Application`]: crate::Application
pub flags: Flags,
- /// The bytes of the font that will be used by default.
+ /// The default [`Font`] to be used.
///
- /// If `None` is provided, a default system font will be chosen.
- // TODO: Add `name` for web compatibility
- pub default_font: Option<&'static [u8]>,
+ /// By default, it uses [`Font::SansSerif`].
+ pub default_font: Font,
/// The text size that will be used by default.
///
- /// The default value is `20.0`.
+ /// The default value is `16.0`.
pub default_text_size: f32,
- /// If enabled, spread text workload in multiple threads when multiple cores
- /// are available.
- ///
- /// By default, it is disabled.
- pub text_multithreading: bool,
-
/// If set to true, the renderer will try to perform antialiasing for some
/// primitives.
///
@@ -55,15 +49,6 @@ pub struct Settings<Flags> {
///
/// [`Application`]: crate::Application
pub exit_on_close_request: bool,
-
- /// Whether the [`Application`] should try to build the context
- /// using OpenGL ES first then OpenGL.
- ///
- /// By default, it is disabled.
- /// **Note:** Only works for the `glow` backend.
- ///
- /// [`Application`]: crate::Application
- pub try_opengles_first: bool,
}
impl<Flags> Settings<Flags> {
@@ -79,10 +64,8 @@ impl<Flags> Settings<Flags> {
window: default_settings.window,
default_font: default_settings.default_font,
default_text_size: default_settings.default_text_size,
- text_multithreading: default_settings.text_multithreading,
antialiasing: default_settings.antialiasing,
exit_on_close_request: default_settings.exit_on_close_request,
- try_opengles_first: default_settings.try_opengles_first,
}
}
}
@@ -97,11 +80,9 @@ where
window: Default::default(),
flags: Default::default(),
default_font: Default::default(),
- default_text_size: 20.0,
- text_multithreading: false,
+ default_text_size: 16.0,
antialiasing: false,
exit_on_close_request: true,
- try_opengles_first: false,
}
}
}
@@ -113,7 +94,6 @@ impl<Flags> From<Settings<Flags>> for iced_winit::Settings<Flags> {
window: settings.window.into(),
flags: settings.flags,
exit_on_close_request: settings.exit_on_close_request,
- try_opengles_first: settings.try_opengles_first,
}
}
}
diff --git a/src/touch.rs b/src/touch.rs
index 0b77c386..f2bdfca8 100644
--- a/src/touch.rs
+++ b/src/touch.rs
@@ -1,2 +1,2 @@
//! Listen and react to touch events.
-pub use crate::runtime::touch::{Event, Finger};
+pub use crate::core::touch::{Event, Finger};
diff --git a/src/widget.rs b/src/widget.rs
deleted file mode 100644
index e2b0537e..00000000
--- a/src/widget.rs
+++ /dev/null
@@ -1,238 +0,0 @@
-//! Display information and interactive controls in your application.
-pub use iced_native::widget::helpers::*;
-
-pub use iced_native::{column, row};
-
-/// A container that distributes its contents vertically.
-pub type Column<'a, Message, Renderer = crate::Renderer> =
- iced_native::widget::Column<'a, Message, Renderer>;
-
-/// A container that distributes its contents horizontally.
-pub type Row<'a, Message, Renderer = crate::Renderer> =
- iced_native::widget::Row<'a, Message, Renderer>;
-
-pub mod text {
- //! Write some text for your users to read.
- pub use iced_native::widget::text::{Appearance, StyleSheet};
-
- /// A paragraph of text.
- pub type Text<'a, Renderer = crate::Renderer> =
- iced_native::widget::Text<'a, Renderer>;
-}
-
-pub mod button {
- //! Allow your users to perform actions by pressing a button.
- pub use iced_native::widget::button::{Appearance, StyleSheet};
-
- /// A widget that produces a message when clicked.
- pub type Button<'a, Message, Renderer = crate::Renderer> =
- iced_native::widget::Button<'a, Message, Renderer>;
-}
-
-pub mod checkbox {
- //! Show toggle controls using checkboxes.
- pub use iced_native::widget::checkbox::{Appearance, Icon, StyleSheet};
-
- /// A box that can be checked.
- pub type Checkbox<'a, Message, Renderer = crate::Renderer> =
- iced_native::widget::Checkbox<'a, Message, Renderer>;
-}
-
-pub mod container {
- //! Decorate content and apply alignment.
- pub use iced_native::widget::container::{Appearance, StyleSheet};
-
- /// An element decorating some content.
- pub type Container<'a, Message, Renderer = crate::Renderer> =
- iced_native::widget::Container<'a, Message, Renderer>;
-}
-
-pub mod pane_grid {
- //! 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/iced-rs/iced/tree/0.8/examples/pane_grid
- pub use iced_native::widget::pane_grid::{
- Axis, Configuration, Direction, DragEvent, Line, Node, Pane,
- ResizeEvent, Split, State, StyleSheet,
- };
-
- /// 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)
- pub type PaneGrid<'a, Message, Renderer = crate::Renderer> =
- iced_native::widget::PaneGrid<'a, Message, Renderer>;
-
- /// The content of a [`Pane`].
- pub type Content<'a, Message, Renderer = crate::Renderer> =
- iced_native::widget::pane_grid::Content<'a, Message, Renderer>;
-
- /// The title bar of a [`Pane`].
- pub type TitleBar<'a, Message, Renderer = crate::Renderer> =
- iced_native::widget::pane_grid::TitleBar<'a, Message, Renderer>;
-}
-
-pub mod pick_list {
- //! Display a dropdown list of selectable values.
- pub use iced_native::widget::pick_list::{
- Appearance, Handle, Icon, StyleSheet,
- };
-
- /// A widget allowing the selection of a single value from a list of options.
- pub type PickList<'a, T, Message, Renderer = crate::Renderer> =
- iced_native::widget::PickList<'a, T, Message, Renderer>;
-}
-
-pub mod radio {
- //! Create choices using radio buttons.
- pub use iced_native::widget::radio::{Appearance, StyleSheet};
-
- /// A circular button representing a choice.
- pub type Radio<Message, Renderer = crate::Renderer> =
- iced_native::widget::Radio<Message, Renderer>;
-}
-
-pub mod scrollable {
- //! Navigate an endless amount of content with a scrollbar.
- pub use iced_native::widget::scrollable::{
- snap_to, style::Scrollbar, style::Scroller, Id, Properties,
- RelativeOffset, StyleSheet,
- };
-
- /// A widget that can vertically display an infinite amount of content
- /// with a scrollbar.
- pub type Scrollable<'a, Message, Renderer = crate::Renderer> =
- iced_native::widget::Scrollable<'a, Message, Renderer>;
-}
-
-pub mod toggler {
- //! Show toggle controls using togglers.
- pub use iced_native::widget::toggler::{Appearance, StyleSheet};
-
- /// A toggler widget.
- pub type Toggler<'a, Message, Renderer = crate::Renderer> =
- iced_native::widget::Toggler<'a, Message, Renderer>;
-}
-
-pub mod text_input {
- //! Display fields that can be filled with text.
- pub use iced_native::widget::text_input::{
- focus, move_cursor_to, move_cursor_to_end, move_cursor_to_front,
- select_all, Appearance, Id, StyleSheet,
- };
-
- /// A field that can be filled with text.
- pub type TextInput<'a, Message, Renderer = crate::Renderer> =
- iced_native::widget::TextInput<'a, Message, Renderer>;
-}
-
-pub mod tooltip {
- //! Display a widget over another.
- pub use iced_native::widget::tooltip::Position;
-
- /// A widget allowing the selection of a single value from a list of options.
- pub type Tooltip<'a, Message, Renderer = crate::Renderer> =
- iced_native::widget::Tooltip<'a, Message, Renderer>;
-}
-
-pub use iced_native::widget::progress_bar;
-pub use iced_native::widget::rule;
-pub use iced_native::widget::slider;
-pub use iced_native::widget::vertical_slider;
-pub use iced_native::widget::Space;
-
-pub use button::Button;
-pub use checkbox::Checkbox;
-pub use container::Container;
-pub use pane_grid::PaneGrid;
-pub use pick_list::PickList;
-pub use progress_bar::ProgressBar;
-pub use radio::Radio;
-pub use rule::Rule;
-pub use scrollable::Scrollable;
-pub use slider::Slider;
-pub use text::Text;
-pub use text_input::TextInput;
-pub use toggler::Toggler;
-pub use tooltip::Tooltip;
-pub use vertical_slider::VerticalSlider;
-
-#[cfg(feature = "canvas")]
-#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))]
-pub use iced_graphics::widget::canvas;
-
-#[cfg(feature = "canvas")]
-#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))]
-/// Creates a new [`Canvas`].
-pub fn canvas<P, Message, Theme>(program: P) -> Canvas<Message, Theme, P>
-where
- P: canvas::Program<Message, Theme>,
-{
- Canvas::new(program)
-}
-
-#[cfg(feature = "image")]
-#[cfg_attr(docsrs, doc(cfg(feature = "image")))]
-pub mod image {
- //! Display images in your user interface.
- pub use iced_native::image::Handle;
-
- /// A frame that displays an image.
- pub type Image = iced_native::widget::Image<Handle>;
-
- pub use iced_native::widget::image::viewer;
- pub use viewer::Viewer;
-}
-
-#[cfg(feature = "qr_code")]
-#[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))]
-pub use iced_graphics::widget::qr_code;
-
-#[cfg(feature = "svg")]
-#[cfg_attr(docsrs, doc(cfg(feature = "svg")))]
-pub mod svg {
- //! Display vector graphics in your application.
- pub use iced_native::svg::Handle;
- pub use iced_native::widget::svg::{Appearance, StyleSheet, Svg};
-}
-
-#[cfg(feature = "canvas")]
-#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))]
-pub use canvas::Canvas;
-
-#[cfg(feature = "image")]
-#[cfg_attr(docsrs, doc(cfg(feature = "image")))]
-pub use image::Image;
-
-#[cfg(feature = "qr_code")]
-#[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))]
-pub use qr_code::QRCode;
-
-#[cfg(feature = "svg")]
-#[cfg_attr(docsrs, doc(cfg(feature = "svg")))]
-pub use svg::Svg;
-
-use crate::Command;
-use iced_native::widget::operation;
-
-/// Focuses the previous focusable widget.
-pub fn focus_previous<Message>() -> Command<Message>
-where
- Message: 'static,
-{
- Command::widget(operation::focusable::focus_previous())
-}
-
-/// Focuses the next focusable widget.
-pub fn focus_next<Message>() -> Command<Message>
-where
- Message: 'static,
-{
- Command::widget(operation::focusable::focus_next())
-}
diff --git a/src/window.rs b/src/window.rs
index c8f5c3ae..e4601575 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -1,6 +1,12 @@
//! Configure the window of your application in native platforms.
-pub use iced_native::window::Icon;
-pub use iced_native::window::Position;
-pub use iced_native::window::Settings;
+mod position;
+mod settings;
+pub mod icon;
+
+pub use icon::Icon;
+pub use position::Position;
+pub use settings::{PlatformSpecific, Settings};
+
+pub use crate::core::window::*;
pub use crate::runtime::window::*;
diff --git a/style/Cargo.toml b/style/Cargo.toml
index 3b54f1ec..8af4a9b3 100644
--- a/style/Cargo.toml
+++ b/style/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "iced_style"
-version = "0.7.0"
+version = "0.8.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
description = "The default set of styles of Iced"
@@ -11,12 +11,12 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"]
categories = ["gui"]
[dependencies.iced_core]
-version = "0.8"
+version = "0.9"
path = "../core"
features = ["palette"]
[dependencies.palette]
-version = "0.6"
+version = "0.7"
[dependencies.once_cell]
version = "1.15"
diff --git a/style/src/button.rs b/style/src/button.rs
index a564a2b7..e49ad94a 100644
--- a/style/src/button.rs
+++ b/style/src/button.rs
@@ -1,5 +1,5 @@
//! Change the apperance of a button.
-use iced_core::{Background, Color, Vector};
+use iced_core::{Background, BorderRadius, Color, Vector};
/// The appearance of a button.
#[derive(Debug, Clone, Copy)]
@@ -9,7 +9,7 @@ pub struct Appearance {
/// The [`Background`] of the button.
pub background: Option<Background>,
/// The border radius of the button.
- pub border_radius: f32,
+ pub border_radius: BorderRadius,
/// The border width of the button.
pub border_width: f32,
/// The border [`Color`] of the button.
@@ -23,7 +23,7 @@ impl std::default::Default for Appearance {
Self {
shadow_offset: Vector::default(),
background: None,
- border_radius: 0.0,
+ border_radius: 0.0.into(),
border_width: 0.0,
border_color: Color::TRANSPARENT,
text_color: Color::BLACK,
@@ -68,6 +68,9 @@ pub trait StyleSheet {
a: color.a * 0.5,
..color
}),
+ Background::Gradient(gradient) => {
+ Background::Gradient(gradient.mul_alpha(0.5))
+ }
}),
text_color: Color {
a: active.text_color.a * 0.5,
diff --git a/style/src/checkbox.rs b/style/src/checkbox.rs
index 52b90ec9..cf52c05d 100644
--- a/style/src/checkbox.rs
+++ b/style/src/checkbox.rs
@@ -1,5 +1,5 @@
//! Change the appearance of a checkbox.
-use iced_core::{Background, Color};
+use iced_core::{Background, BorderRadius, Color};
/// The appearance of a checkbox.
#[derive(Debug, Clone, Copy)]
@@ -9,7 +9,7 @@ pub struct Appearance {
/// The icon [`Color`] of the checkbox.
pub icon_color: Color,
/// The border radius of the checkbox.
- pub border_radius: f32,
+ pub border_radius: BorderRadius,
/// The border width of the checkbox.
pub border_width: f32,
/// The border [`Color`] of the checkbox.
diff --git a/style/src/container.rs b/style/src/container.rs
index 560b2d5b..ec543ae4 100644
--- a/style/src/container.rs
+++ b/style/src/container.rs
@@ -1,5 +1,5 @@
//! Change the appearance of a container.
-use iced_core::{Background, Color};
+use iced_core::{Background, BorderRadius, Color};
/// The appearance of a container.
#[derive(Debug, Clone, Copy)]
@@ -9,7 +9,7 @@ pub struct Appearance {
/// The [`Background`] of the container.
pub background: Option<Background>,
/// The border radius of the container.
- pub border_radius: f32,
+ pub border_radius: BorderRadius,
/// The border width of the container.
pub border_width: f32,
/// The border [`Color`] of the container.
@@ -21,7 +21,7 @@ impl std::default::Default for Appearance {
Self {
text_color: None,
background: None,
- border_radius: 0.0,
+ border_radius: 0.0.into(),
border_width: 0.0,
border_color: Color::TRANSPARENT,
}
diff --git a/style/src/lib.rs b/style/src/lib.rs
index 59eb1eb8..286ff9db 100644
--- a/style/src/lib.rs
+++ b/style/src/lib.rs
@@ -18,7 +18,7 @@
#![deny(missing_docs, unused_results)]
#![forbid(unsafe_code, rust_2018_idioms)]
#![allow(clippy::inherent_to_string, clippy::type_complexity)]
-pub use iced_core::{Background, Color};
+pub use iced_core as core;
pub mod application;
pub mod button;
@@ -33,7 +33,6 @@ pub mod rule;
pub mod scrollable;
pub mod slider;
pub mod svg;
-pub mod text;
pub mod text_input;
pub mod theme;
pub mod toggler;
diff --git a/style/src/menu.rs b/style/src/menu.rs
index 7d878748..dbf19dae 100644
--- a/style/src/menu.rs
+++ b/style/src/menu.rs
@@ -1,5 +1,5 @@
//! Change the appearance of menus.
-use iced_core::{Background, Color};
+use iced_core::{Background, BorderRadius, Color};
/// The appearance of a menu.
#[derive(Debug, Clone, Copy)]
@@ -11,7 +11,7 @@ pub struct Appearance {
/// The border width of the menu.
pub border_width: f32,
/// The border radius of the menu.
- pub border_radius: f32,
+ pub border_radius: BorderRadius,
/// The border [`Color`] of the menu.
pub border_color: Color,
/// The text [`Color`] of a selected option in the menu.
diff --git a/style/src/pane_grid.rs b/style/src/pane_grid.rs
index fd8fc05f..dfdc9186 100644
--- a/style/src/pane_grid.rs
+++ b/style/src/pane_grid.rs
@@ -1,16 +1,17 @@
//! Change the appearance of a pane grid.
-use iced_core::Color;
+use iced_core::{Background, BorderRadius, Color};
-/// A set of rules that dictate the style of a container.
-pub trait StyleSheet {
- /// The supported style of the [`StyleSheet`].
- type Style: Default;
-
- /// The [`Line`] to draw when a split is picked.
- fn picked_split(&self, style: &Self::Style) -> Option<Line>;
-
- /// The [`Line`] to draw when a split is hovered.
- fn hovered_split(&self, style: &Self::Style) -> Option<Line>;
+/// The appearance of the hovered region of a pane grid.
+#[derive(Debug, Clone, Copy)]
+pub struct Appearance {
+ /// The [`Background`] of the hovered pane region.
+ pub background: Background,
+ /// The border width of the hovered pane region.
+ pub border_width: f32,
+ /// The border [`Color`] of the hovered pane region.
+ pub border_color: Color,
+ /// The border radius of the hovered pane region.
+ pub border_radius: BorderRadius,
}
/// A line.
@@ -24,3 +25,18 @@ pub struct Line {
/// The width of the [`Line`].
pub width: f32,
}
+
+/// A set of rules that dictate the style of a container.
+pub trait StyleSheet {
+ /// The supported style of the [`StyleSheet`].
+ type Style: Default;
+
+ /// The [`Appearance`] to draw when a pane is hovered.
+ fn hovered_region(&self, style: &Self::Style) -> Appearance;
+
+ /// The [`Line`] to draw when a split is picked.
+ fn picked_split(&self, style: &Self::Style) -> Option<Line>;
+
+ /// The [`Line`] to draw when a split is hovered.
+ fn hovered_split(&self, style: &Self::Style) -> Option<Line>;
+}
diff --git a/style/src/pick_list.rs b/style/src/pick_list.rs
index 11e13b01..961c1e93 100644
--- a/style/src/pick_list.rs
+++ b/style/src/pick_list.rs
@@ -1,5 +1,5 @@
//! Change the appearance of a pick list.
-use iced_core::{Background, Color};
+use iced_core::{Background, BorderRadius, Color};
/// The appearance of a pick list.
#[derive(Debug, Clone, Copy)]
@@ -13,7 +13,7 @@ pub struct Appearance {
/// The [`Background`] of the pick list.
pub background: Background,
/// The border radius of the pick list.
- pub border_radius: f32,
+ pub border_radius: BorderRadius,
/// The border width of the pick list.
pub border_width: f32,
/// The border color of the pick list.
diff --git a/style/src/progress_bar.rs b/style/src/progress_bar.rs
index fb1819fc..c05a6ee4 100644
--- a/style/src/progress_bar.rs
+++ b/style/src/progress_bar.rs
@@ -1,5 +1,5 @@
//! Change the appearance of a progress bar.
-use iced_core::Background;
+use iced_core::{Background, BorderRadius};
/// The appearance of a progress bar.
#[derive(Debug, Clone, Copy)]
@@ -9,7 +9,7 @@ pub struct Appearance {
/// The [`Background`] of the bar of the progress bar.
pub bar: Background,
/// The border radius of the progress bar.
- pub border_radius: f32,
+ pub border_radius: BorderRadius,
}
/// A set of rules that dictate the style of a progress bar.
diff --git a/style/src/rule.rs b/style/src/rule.rs
index b7380747..afae085c 100644
--- a/style/src/rule.rs
+++ b/style/src/rule.rs
@@ -1,5 +1,5 @@
//! Change the appearance of a rule.
-use iced_core::Color;
+use iced_core::{BorderRadius, Color};
/// The appearance of a rule.
#[derive(Debug, Clone, Copy)]
@@ -9,7 +9,7 @@ pub struct Appearance {
/// The width (thickness) of the rule line.
pub width: u16,
/// The radius of the line corners.
- pub radius: f32,
+ pub radius: BorderRadius,
/// The [`FillMode`] of the rule.
pub fill_mode: FillMode,
}
diff --git a/style/src/scrollable.rs b/style/src/scrollable.rs
index 64ed8462..952c11e1 100644
--- a/style/src/scrollable.rs
+++ b/style/src/scrollable.rs
@@ -1,5 +1,5 @@
//! Change the appearance of a scrollable.
-use iced_core::{Background, Color};
+use iced_core::{Background, BorderRadius, Color};
/// The appearance of a scrollable.
#[derive(Debug, Clone, Copy)]
@@ -7,7 +7,7 @@ pub struct Scrollbar {
/// The [`Background`] of a scrollable.
pub background: Option<Background>,
/// The border radius of a scrollable.
- pub border_radius: f32,
+ pub border_radius: BorderRadius,
/// The border width of a scrollable.
pub border_width: f32,
/// The border [`Color`] of a scrollable.
@@ -22,7 +22,7 @@ pub struct Scroller {
/// The [`Color`] of the scroller.
pub color: Color,
/// The border radius of the scroller.
- pub border_radius: f32,
+ pub border_radius: BorderRadius,
/// The border width of the scroller.
pub border_width: f32,
/// The border [`Color`] of the scroller.
@@ -37,12 +37,16 @@ pub trait StyleSheet {
/// Produces the style of an active scrollbar.
fn active(&self, style: &Self::Style) -> Scrollbar;
- /// Produces the style of a hovered scrollbar.
- fn hovered(&self, style: &Self::Style) -> Scrollbar;
+ /// Produces the style of a scrollbar when the scrollable is being hovered.
+ fn hovered(
+ &self,
+ style: &Self::Style,
+ is_mouse_over_scrollbar: bool,
+ ) -> Scrollbar;
/// Produces the style of a scrollbar that is being dragged.
fn dragging(&self, style: &Self::Style) -> Scrollbar {
- self.hovered(style)
+ self.hovered(style, true)
}
/// Produces the style of an active horizontal scrollbar.
@@ -50,13 +54,17 @@ pub trait StyleSheet {
self.active(style)
}
- /// Produces the style of a hovered horizontal scrollbar.
- fn hovered_horizontal(&self, style: &Self::Style) -> Scrollbar {
- self.hovered(style)
+ /// Produces the style of a horizontal scrollbar when the scrollable is being hovered.
+ fn hovered_horizontal(
+ &self,
+ style: &Self::Style,
+ is_mouse_over_scrollbar: bool,
+ ) -> Scrollbar {
+ self.hovered(style, is_mouse_over_scrollbar)
}
/// Produces the style of a horizontal scrollbar that is being dragged.
fn dragging_horizontal(&self, style: &Self::Style) -> Scrollbar {
- self.hovered_horizontal(style)
+ self.hovered_horizontal(style, true)
}
}
diff --git a/style/src/slider.rs b/style/src/slider.rs
index 4b52fad3..f0068558 100644
--- a/style/src/slider.rs
+++ b/style/src/slider.rs
@@ -1,15 +1,26 @@
//! Change the apperance of a slider.
-use iced_core::Color;
+use iced_core::{BorderRadius, Color};
/// The appearance of a slider.
#[derive(Debug, Clone, Copy)]
pub struct Appearance {
/// The colors of the rail of the slider.
- pub rail_colors: (Color, Color),
+ pub rail: Rail,
/// The appearance of the [`Handle`] of the slider.
pub handle: Handle,
}
+/// The appearance of a slider rail
+#[derive(Debug, Clone, Copy)]
+pub struct Rail {
+ /// The colors of the rail of the slider.
+ pub colors: (Color, Color),
+ /// The width of the stroke of a slider rail.
+ pub width: f32,
+ /// The border radius of the corners of the rail.
+ pub border_radius: BorderRadius,
+}
+
/// The appearance of the handle of a slider.
#[derive(Debug, Clone, Copy)]
pub struct Handle {
@@ -36,7 +47,7 @@ pub enum HandleShape {
/// The width of the rectangle.
width: u16,
/// The border radius of the corners of the rectangle.
- border_radius: f32,
+ border_radius: BorderRadius,
},
}
diff --git a/style/src/text.rs b/style/src/text.rs
deleted file mode 100644
index f31c2306..00000000
--- a/style/src/text.rs
+++ /dev/null
@@ -1,20 +0,0 @@
-//! Change the appearance of text.
-use iced_core::Color;
-
-/// The style sheet of some text.
-pub trait StyleSheet {
- /// The supported style of the [`StyleSheet`].
- type Style: Default + Copy;
-
- /// Produces the [`Appearance`] of some text.
- fn appearance(&self, style: Self::Style) -> Appearance;
-}
-
-/// The apperance of some text.
-#[derive(Debug, Clone, Copy, Default)]
-pub struct Appearance {
- /// The [`Color`] of the text.
- ///
- /// The default, `None`, means using the inherited color.
- pub color: Option<Color>,
-}
diff --git a/style/src/text_input.rs b/style/src/text_input.rs
index d97016dc..90251b5c 100644
--- a/style/src/text_input.rs
+++ b/style/src/text_input.rs
@@ -1,5 +1,5 @@
//! Change the appearance of a text input.
-use iced_core::{Background, Color};
+use iced_core::{Background, BorderRadius, Color};
/// The appearance of a text input.
#[derive(Debug, Clone, Copy)]
@@ -7,11 +7,13 @@ pub struct Appearance {
/// The [`Background`] of the text input.
pub background: Background,
/// The border radius of the text input.
- pub border_radius: f32,
+ pub border_radius: BorderRadius,
/// The border width of the text input.
pub border_width: f32,
/// The border [`Color`] of the text input.
pub border_color: Color,
+ /// The icon [`Color`] of the text input.
+ pub icon_color: Color,
}
/// A set of rules that dictate the style of a text input.
@@ -31,6 +33,9 @@ pub trait StyleSheet {
/// Produces the [`Color`] of the value of a text input.
fn value_color(&self, style: &Self::Style) -> Color;
+ /// Produces the [`Color`] of the value of a disabled text input.
+ fn disabled_color(&self, style: &Self::Style) -> Color;
+
/// Produces the [`Color`] of the selection of a text input.
fn selection_color(&self, style: &Self::Style) -> Color;
@@ -38,4 +43,7 @@ pub trait StyleSheet {
fn hovered(&self, style: &Self::Style) -> Appearance {
self.focused(style)
}
+
+ /// Produces the style of a disabled text input.
+ fn disabled(&self, style: &Self::Style) -> Appearance;
}
diff --git a/style/src/theme.rs b/style/src/theme.rs
index 4ba4facf..64497181 100644
--- a/style/src/theme.rs
+++ b/style/src/theme.rs
@@ -8,6 +8,7 @@ use crate::application;
use crate::button;
use crate::checkbox;
use crate::container;
+use crate::core::widget::text;
use crate::menu;
use crate::pane_grid;
use crate::pick_list;
@@ -17,7 +18,6 @@ use crate::rule;
use crate::scrollable;
use crate::slider;
use crate::svg;
-use crate::text;
use crate::text_input;
use crate::toggler;
@@ -105,7 +105,7 @@ impl application::StyleSheet for Theme {
}
}
-impl application::StyleSheet for fn(&Theme) -> application::Appearance {
+impl<T: Fn(&Theme) -> application::Appearance> application::StyleSheet for T {
type Style = Theme;
fn appearance(&self, style: &Self::Style) -> application::Appearance {
@@ -113,8 +113,10 @@ impl application::StyleSheet for fn(&Theme) -> application::Appearance {
}
}
-impl From<fn(&Theme) -> application::Appearance> for Application {
- fn from(f: fn(&Theme) -> application::Appearance) -> Self {
+impl<T: Fn(&Theme) -> application::Appearance + 'static> From<T>
+ for Application
+{
+ fn from(f: T) -> Self {
Self::Custom(Box::new(f))
}
}
@@ -139,6 +141,15 @@ pub enum Button {
Custom(Box<dyn button::StyleSheet<Style = Theme>>),
}
+impl Button {
+ /// Creates a custom [`Button`] style variant.
+ pub fn custom(
+ style_sheet: impl button::StyleSheet<Style = Theme> + 'static,
+ ) -> Self {
+ Self::Custom(Box::new(style_sheet))
+ }
+}
+
impl button::StyleSheet for Theme {
type Style = Button;
@@ -146,7 +157,7 @@ impl button::StyleSheet for Theme {
let palette = self.extended_palette();
let appearance = button::Appearance {
- border_radius: 2.0,
+ border_radius: 2.0.into(),
..button::Appearance::default()
};
@@ -217,6 +228,9 @@ impl button::StyleSheet for Theme {
a: color.a * 0.5,
..color
}),
+ Background::Gradient(gradient) => {
+ Background::Gradient(gradient.mul_alpha(0.5))
+ }
}),
text_color: Color {
a: active.text_color.a * 0.5,
@@ -332,7 +346,7 @@ fn checkbox_appearance(
base.color
}),
icon_color,
- border_radius: 2.0,
+ border_radius: 2.0.into(),
border_width: 1.0,
border_color: accent.color,
text_color: None,
@@ -351,8 +365,8 @@ pub enum Container {
Custom(Box<dyn container::StyleSheet<Style = Theme>>),
}
-impl From<fn(&Theme) -> container::Appearance> for Container {
- fn from(f: fn(&Theme) -> container::Appearance) -> Self {
+impl<T: Fn(&Theme) -> container::Appearance + 'static> From<T> for Container {
+ fn from(f: T) -> Self {
Self::Custom(Box::new(f))
}
}
@@ -368,8 +382,8 @@ impl container::StyleSheet for Theme {
container::Appearance {
text_color: None,
- background: palette.background.weak.color.into(),
- border_radius: 2.0,
+ background: Some(palette.background.weak.color.into()),
+ border_radius: 2.0.into(),
border_width: 0.0,
border_color: Color::TRANSPARENT,
}
@@ -379,7 +393,7 @@ impl container::StyleSheet for Theme {
}
}
-impl container::StyleSheet for fn(&Theme) -> container::Appearance {
+impl<T: Fn(&Theme) -> container::Appearance> container::StyleSheet for T {
type Style = Theme;
fn appearance(&self, style: &Self::Style) -> container::Appearance {
@@ -408,7 +422,7 @@ impl slider::StyleSheet for Theme {
let handle = slider::Handle {
shape: slider::HandleShape::Rectangle {
width: 8,
- border_radius: 4.0,
+ border_radius: 4.0.into(),
},
color: Color::WHITE,
border_color: Color::WHITE,
@@ -416,10 +430,14 @@ impl slider::StyleSheet for Theme {
};
slider::Appearance {
- rail_colors: (
- palette.primary.base.color,
- Color::TRANSPARENT,
- ),
+ rail: slider::Rail {
+ colors: (
+ palette.primary.base.color,
+ palette.secondary.base.color,
+ ),
+ width: 4.0,
+ border_radius: 2.0.into(),
+ },
handle: slider::Handle {
color: palette.background.base.color,
border_color: palette.primary.base.color,
@@ -490,7 +508,7 @@ impl menu::StyleSheet for Theme {
text_color: palette.background.weak.text,
background: palette.background.weak.color.into(),
border_width: 1.0,
- border_radius: 0.0,
+ border_radius: 0.0.into(),
border_color: palette.background.strong.color,
selected_text_color: palette.primary.strong.text,
selected_background: palette.primary.strong.color.into(),
@@ -536,7 +554,7 @@ impl pick_list::StyleSheet for Theme {
background: palette.background.weak.color.into(),
placeholder_color: palette.background.strong.color,
handle_color: palette.background.weak.text,
- border_radius: 2.0,
+ border_radius: 2.0.into(),
border_width: 1.0,
border_color: palette.background.strong.color,
}
@@ -555,7 +573,7 @@ impl pick_list::StyleSheet for Theme {
background: palette.background.weak.color.into(),
placeholder_color: palette.background.strong.color,
handle_color: palette.background.weak.text,
- border_radius: 2.0,
+ border_radius: 2.0.into(),
border_width: 1.0,
border_color: palette.primary.strong.color,
}
@@ -700,6 +718,25 @@ pub enum PaneGrid {
impl pane_grid::StyleSheet for Theme {
type Style = PaneGrid;
+ fn hovered_region(&self, style: &Self::Style) -> pane_grid::Appearance {
+ match style {
+ PaneGrid::Default => {
+ let palette = self.extended_palette();
+
+ pane_grid::Appearance {
+ background: Background::Color(Color {
+ a: 0.5,
+ ..palette.primary.base.color
+ }),
+ border_width: 2.0,
+ border_color: palette.primary.strong.color,
+ border_radius: 0.0.into(),
+ }
+ }
+ PaneGrid::Custom(custom) => custom.hovered_region(self),
+ }
+ }
+
fn picked_split(&self, style: &Self::Style) -> Option<pane_grid::Line> {
match style {
PaneGrid::Default => {
@@ -743,8 +780,10 @@ pub enum ProgressBar {
Custom(Box<dyn progress_bar::StyleSheet<Style = Theme>>),
}
-impl From<fn(&Theme) -> progress_bar::Appearance> for ProgressBar {
- fn from(f: fn(&Theme) -> progress_bar::Appearance) -> Self {
+impl<T: Fn(&Theme) -> progress_bar::Appearance + 'static> From<T>
+ for ProgressBar
+{
+ fn from(f: T) -> Self {
Self::Custom(Box::new(f))
}
}
@@ -762,7 +801,7 @@ impl progress_bar::StyleSheet for Theme {
let from_palette = |bar: Color| progress_bar::Appearance {
background: palette.background.strong.color.into(),
bar: bar.into(),
- border_radius: 2.0,
+ border_radius: 2.0.into(),
};
match style {
@@ -774,7 +813,7 @@ impl progress_bar::StyleSheet for Theme {
}
}
-impl progress_bar::StyleSheet for fn(&Theme) -> progress_bar::Appearance {
+impl<T: Fn(&Theme) -> progress_bar::Appearance> progress_bar::StyleSheet for T {
type Style = Theme;
fn appearance(&self, style: &Self::Style) -> progress_bar::Appearance {
@@ -792,8 +831,8 @@ pub enum Rule {
Custom(Box<dyn rule::StyleSheet<Style = Theme>>),
}
-impl From<fn(&Theme) -> rule::Appearance> for Rule {
- fn from(f: fn(&Theme) -> rule::Appearance) -> Self {
+impl<T: Fn(&Theme) -> rule::Appearance + 'static> From<T> for Rule {
+ fn from(f: T) -> Self {
Self::Custom(Box::new(f))
}
}
@@ -808,7 +847,7 @@ impl rule::StyleSheet for Theme {
Rule::Default => rule::Appearance {
color: palette.background.strong.color,
width: 1,
- radius: 0.0,
+ radius: 0.0.into(),
fill_mode: rule::FillMode::Full,
},
Rule::Custom(custom) => custom.appearance(self),
@@ -816,7 +855,7 @@ impl rule::StyleSheet for Theme {
}
}
-impl rule::StyleSheet for fn(&Theme) -> rule::Appearance {
+impl<T: Fn(&Theme) -> rule::Appearance> rule::StyleSheet for T {
type Style = Theme;
fn appearance(&self, style: &Self::Style) -> rule::Appearance {
@@ -890,13 +929,13 @@ impl scrollable::StyleSheet for Theme {
let palette = self.extended_palette();
scrollable::Scrollbar {
- background: palette.background.weak.color.into(),
- border_radius: 2.0,
+ background: Some(palette.background.weak.color.into()),
+ border_radius: 2.0.into(),
border_width: 0.0,
border_color: Color::TRANSPARENT,
scroller: scrollable::Scroller {
color: palette.background.strong.color,
- border_radius: 2.0,
+ border_radius: 2.0.into(),
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
@@ -906,31 +945,41 @@ impl scrollable::StyleSheet for Theme {
}
}
- fn hovered(&self, style: &Self::Style) -> scrollable::Scrollbar {
+ fn hovered(
+ &self,
+ style: &Self::Style,
+ is_mouse_over_scrollbar: bool,
+ ) -> scrollable::Scrollbar {
match style {
Scrollable::Default => {
- let palette = self.extended_palette();
+ if is_mouse_over_scrollbar {
+ let palette = self.extended_palette();
- scrollable::Scrollbar {
- background: palette.background.weak.color.into(),
- border_radius: 2.0,
- border_width: 0.0,
- border_color: Color::TRANSPARENT,
- scroller: scrollable::Scroller {
- color: palette.primary.strong.color,
- border_radius: 2.0,
+ scrollable::Scrollbar {
+ background: Some(palette.background.weak.color.into()),
+ border_radius: 2.0.into(),
border_width: 0.0,
border_color: Color::TRANSPARENT,
- },
+ scroller: scrollable::Scroller {
+ color: palette.primary.strong.color,
+ border_radius: 2.0.into(),
+ border_width: 0.0,
+ border_color: Color::TRANSPARENT,
+ },
+ }
+ } else {
+ self.active(style)
}
}
- Scrollable::Custom(custom) => custom.hovered(self),
+ Scrollable::Custom(custom) => {
+ custom.hovered(self, is_mouse_over_scrollbar)
+ }
}
}
fn dragging(&self, style: &Self::Style) -> scrollable::Scrollbar {
match style {
- Scrollable::Default => self.hovered(style),
+ Scrollable::Default => self.hovered(style, true),
Scrollable::Custom(custom) => custom.dragging(self),
}
}
@@ -942,10 +991,16 @@ impl scrollable::StyleSheet for Theme {
}
}
- fn hovered_horizontal(&self, style: &Self::Style) -> scrollable::Scrollbar {
+ fn hovered_horizontal(
+ &self,
+ style: &Self::Style,
+ is_mouse_over_scrollbar: bool,
+ ) -> scrollable::Scrollbar {
match style {
- Scrollable::Default => self.hovered(style),
- Scrollable::Custom(custom) => custom.hovered_horizontal(self),
+ Scrollable::Default => self.hovered(style, is_mouse_over_scrollbar),
+ Scrollable::Custom(custom) => {
+ custom.hovered_horizontal(self, is_mouse_over_scrollbar)
+ }
}
}
@@ -954,7 +1009,7 @@ impl scrollable::StyleSheet for Theme {
style: &Self::Style,
) -> scrollable::Scrollbar {
match style {
- Scrollable::Default => self.hovered_horizontal(style),
+ Scrollable::Default => self.hovered_horizontal(style, true),
Scrollable::Custom(custom) => custom.dragging_horizontal(self),
}
}
@@ -1009,9 +1064,10 @@ impl text_input::StyleSheet for Theme {
text_input::Appearance {
background: palette.background.base.color.into(),
- border_radius: 2.0,
+ border_radius: 2.0.into(),
border_width: 1.0,
border_color: palette.background.strong.color,
+ icon_color: palette.background.weak.text,
}
}
@@ -1024,9 +1080,10 @@ impl text_input::StyleSheet for Theme {
text_input::Appearance {
background: palette.background.base.color.into(),
- border_radius: 2.0,
+ border_radius: 2.0.into(),
border_width: 1.0,
border_color: palette.background.base.text,
+ icon_color: palette.background.weak.text,
}
}
@@ -1039,9 +1096,10 @@ impl text_input::StyleSheet for Theme {
text_input::Appearance {
background: palette.background.base.color.into(),
- border_radius: 2.0,
+ border_radius: 2.0.into(),
border_width: 1.0,
border_color: palette.primary.strong.color,
+ icon_color: palette.background.weak.text,
}
}
@@ -1074,4 +1132,28 @@ impl text_input::StyleSheet for Theme {
palette.primary.weak.color
}
+
+ fn disabled(&self, style: &Self::Style) -> text_input::Appearance {
+ if let TextInput::Custom(custom) = style {
+ return custom.disabled(self);
+ }
+
+ let palette = self.extended_palette();
+
+ text_input::Appearance {
+ background: palette.background.weak.color.into(),
+ border_radius: 2.0.into(),
+ border_width: 1.0,
+ border_color: palette.background.strong.color,
+ icon_color: palette.background.strong.color,
+ }
+ }
+
+ fn disabled_color(&self, style: &Self::Style) -> Color {
+ if let TextInput::Custom(custom) = style {
+ return custom.disabled_color(self);
+ }
+
+ self.placeholder_color(style)
+ }
}
diff --git a/style/src/theme/palette.rs b/style/src/theme/palette.rs
index 0f15494b..aaeb799d 100644
--- a/style/src/theme/palette.rs
+++ b/style/src/theme/palette.rs
@@ -2,7 +2,9 @@
use iced_core::Color;
use once_cell::sync::Lazy;
-use palette::{FromColor, Hsl, Mix, RelativeContrast, Srgb};
+use palette::color_difference::Wcag21RelativeContrast;
+use palette::rgb::Rgb;
+use palette::{FromColor, Hsl, Mix};
/// A color palette.
#[derive(Debug, Clone, Copy, PartialEq)]
@@ -298,11 +300,11 @@ fn deviate(color: Color, amount: f32) -> Color {
}
fn mix(a: Color, b: Color, factor: f32) -> Color {
- let a_lin = Srgb::from(a).into_linear();
- let b_lin = Srgb::from(b).into_linear();
+ let a_lin = Rgb::from(a).into_linear();
+ let b_lin = Rgb::from(b).into_linear();
- let mixed = a_lin.mix(&b_lin, factor);
- Srgb::from_linear(mixed).into()
+ let mixed = a_lin.mix(b_lin, factor);
+ Rgb::from_linear(mixed).into()
}
fn readable(background: Color, text: Color) -> Color {
@@ -320,16 +322,16 @@ fn is_dark(color: Color) -> bool {
}
fn is_readable(a: Color, b: Color) -> bool {
- let a_srgb = Srgb::from(a);
- let b_srgb = Srgb::from(b);
+ let a_srgb = Rgb::from(a);
+ let b_srgb = Rgb::from(b);
- a_srgb.has_enhanced_contrast_text(&b_srgb)
+ a_srgb.has_enhanced_contrast_text(b_srgb)
}
fn to_hsl(color: Color) -> Hsl {
- Hsl::from_color(Srgb::from(color))
+ Hsl::from_color(Rgb::from(color))
}
fn from_hsl(hsl: Hsl) -> Color {
- Srgb::from_color(hsl).into()
+ Rgb::from_color(hsl).into()
}
diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml
new file mode 100644
index 00000000..d9276ea5
--- /dev/null
+++ b/tiny_skia/Cargo.toml
@@ -0,0 +1,38 @@
+[package]
+name = "iced_tiny_skia"
+version = "0.1.0"
+edition = "2021"
+
+[features]
+image = ["iced_graphics/image"]
+svg = ["resvg"]
+geometry = ["iced_graphics/geometry"]
+
+[dependencies]
+raw-window-handle = "0.5"
+softbuffer = "0.2"
+tiny-skia = "0.10"
+bytemuck = "1"
+rustc-hash = "1.1"
+kurbo = "0.9"
+log = "0.4"
+
+[dependencies.iced_graphics]
+version = "0.8"
+path = "../graphics"
+
+[dependencies.cosmic-text]
+git = "https://github.com/hecrj/cosmic-text.git"
+rev = "c3cd24dc972bb8fd55d016c81ac9fa637e0a4ada"
+
+[dependencies.twox-hash]
+version = "1.6"
+default-features = false
+
+[target.'cfg(not(target_arch = "wasm32"))'.dependencies.twox-hash]
+version = "1.6.1"
+features = ["std"]
+
+[dependencies.resvg]
+version = "0.35"
+optional = true
diff --git a/graphics/fonts/Icons.ttf b/tiny_skia/fonts/Iced-Icons.ttf
index 5e455b69..e3273141 100644
--- a/graphics/fonts/Icons.ttf
+++ b/tiny_skia/fonts/Iced-Icons.ttf
Binary files differ
diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs
new file mode 100644
index 00000000..a8add70b
--- /dev/null
+++ b/tiny_skia/src/backend.rs
@@ -0,0 +1,856 @@
+use crate::core::text;
+use crate::core::Gradient;
+use crate::core::{Background, Color, Font, Point, Rectangle, Size, Vector};
+use crate::graphics::backend;
+use crate::graphics::{Damage, Viewport};
+use crate::primitive::{self, Primitive};
+use crate::Settings;
+
+use std::borrow::Cow;
+
+pub struct Backend {
+ default_font: Font,
+ default_text_size: f32,
+ text_pipeline: crate::text::Pipeline,
+
+ #[cfg(feature = "image")]
+ raster_pipeline: crate::raster::Pipeline,
+
+ #[cfg(feature = "svg")]
+ vector_pipeline: crate::vector::Pipeline,
+}
+
+impl Backend {
+ pub fn new(settings: Settings) -> Self {
+ Self {
+ default_font: settings.default_font,
+ default_text_size: settings.default_text_size,
+ text_pipeline: crate::text::Pipeline::new(),
+
+ #[cfg(feature = "image")]
+ raster_pipeline: crate::raster::Pipeline::new(),
+
+ #[cfg(feature = "svg")]
+ vector_pipeline: crate::vector::Pipeline::new(),
+ }
+ }
+
+ pub fn draw<T: AsRef<str>>(
+ &mut self,
+ pixels: &mut tiny_skia::PixmapMut<'_>,
+ clip_mask: &mut tiny_skia::Mask,
+ primitives: &[Primitive],
+ viewport: &Viewport,
+ damage: &[Rectangle],
+ background_color: Color,
+ overlay: &[T],
+ ) {
+ let physical_size = viewport.physical_size();
+ let scale_factor = viewport.scale_factor() as f32;
+
+ if !overlay.is_empty() {
+ let path = tiny_skia::PathBuilder::from_rect(
+ tiny_skia::Rect::from_xywh(
+ 0.0,
+ 0.0,
+ physical_size.width as f32,
+ physical_size.height as f32,
+ )
+ .expect("Create damage rectangle"),
+ );
+
+ pixels.fill_path(
+ &path,
+ &tiny_skia::Paint {
+ shader: tiny_skia::Shader::SolidColor(into_color(Color {
+ a: 0.1,
+ ..background_color
+ })),
+ anti_alias: false,
+ ..Default::default()
+ },
+ tiny_skia::FillRule::default(),
+ tiny_skia::Transform::identity(),
+ None,
+ );
+ }
+
+ for &region in damage {
+ let path = tiny_skia::PathBuilder::from_rect(
+ tiny_skia::Rect::from_xywh(
+ region.x,
+ region.y,
+ region.width,
+ region.height,
+ )
+ .expect("Create damage rectangle"),
+ );
+
+ pixels.fill_path(
+ &path,
+ &tiny_skia::Paint {
+ shader: tiny_skia::Shader::SolidColor(into_color(
+ background_color,
+ )),
+ anti_alias: false,
+ blend_mode: tiny_skia::BlendMode::Source,
+ ..Default::default()
+ },
+ tiny_skia::FillRule::default(),
+ tiny_skia::Transform::identity(),
+ None,
+ );
+
+ adjust_clip_mask(clip_mask, region);
+
+ for primitive in primitives {
+ self.draw_primitive(
+ primitive,
+ pixels,
+ clip_mask,
+ region,
+ scale_factor,
+ Vector::ZERO,
+ );
+ }
+
+ if !overlay.is_empty() {
+ pixels.stroke_path(
+ &path,
+ &tiny_skia::Paint {
+ shader: tiny_skia::Shader::SolidColor(into_color(
+ Color::from_rgb(1.0, 0.0, 0.0),
+ )),
+ anti_alias: false,
+ ..tiny_skia::Paint::default()
+ },
+ &tiny_skia::Stroke {
+ width: 1.0,
+ ..tiny_skia::Stroke::default()
+ },
+ tiny_skia::Transform::identity(),
+ None,
+ );
+ }
+ }
+
+ self.text_pipeline.trim_cache();
+
+ #[cfg(feature = "image")]
+ self.raster_pipeline.trim_cache();
+
+ #[cfg(feature = "svg")]
+ self.vector_pipeline.trim_cache();
+ }
+
+ fn draw_primitive(
+ &mut self,
+ primitive: &Primitive,
+ pixels: &mut tiny_skia::PixmapMut<'_>,
+ clip_mask: &mut tiny_skia::Mask,
+ clip_bounds: Rectangle,
+ scale_factor: f32,
+ translation: Vector,
+ ) {
+ match primitive {
+ Primitive::Quad {
+ bounds,
+ background,
+ border_radius,
+ border_width,
+ border_color,
+ } => {
+ let physical_bounds = (*bounds + translation) * scale_factor;
+
+ if !clip_bounds.intersects(&physical_bounds) {
+ return;
+ }
+
+ let clip_mask = (!physical_bounds.is_within(&clip_bounds))
+ .then_some(clip_mask as &_);
+
+ let transform = tiny_skia::Transform::from_translate(
+ translation.x,
+ translation.y,
+ )
+ .post_scale(scale_factor, scale_factor);
+
+ // Make sure the border radius is not larger than the bounds
+ let border_width = border_width
+ .min(bounds.width / 2.0)
+ .min(bounds.height / 2.0);
+
+ let mut fill_border_radius = *border_radius;
+ for radius in &mut fill_border_radius {
+ *radius = (*radius)
+ .min(bounds.width / 2.0)
+ .min(bounds.height / 2.0);
+ }
+ let path = rounded_rectangle(*bounds, fill_border_radius);
+
+ pixels.fill_path(
+ &path,
+ &tiny_skia::Paint {
+ shader: match background {
+ Background::Color(color) => {
+ tiny_skia::Shader::SolidColor(into_color(
+ *color,
+ ))
+ }
+ Background::Gradient(Gradient::Linear(linear)) => {
+ let (start, end) =
+ linear.angle.to_distance(bounds);
+
+ let stops: Vec<tiny_skia::GradientStop> =
+ linear
+ .stops
+ .into_iter()
+ .flatten()
+ .map(|stop| {
+ tiny_skia::GradientStop::new(
+ stop.offset,
+ tiny_skia::Color::from_rgba(
+ stop.color.b,
+ stop.color.g,
+ stop.color.r,
+ stop.color.a,
+ )
+ .expect("Create color"),
+ )
+ })
+ .collect();
+
+ tiny_skia::LinearGradient::new(
+ tiny_skia::Point {
+ x: start.x,
+ y: start.y,
+ },
+ tiny_skia::Point { x: end.x, y: end.y },
+ if stops.is_empty() {
+ vec![tiny_skia::GradientStop::new(
+ 0.0,
+ tiny_skia::Color::BLACK,
+ )]
+ } else {
+ stops
+ },
+ tiny_skia::SpreadMode::Pad,
+ tiny_skia::Transform::identity(),
+ )
+ .expect("Create linear gradient")
+ }
+ },
+ anti_alias: true,
+ ..tiny_skia::Paint::default()
+ },
+ tiny_skia::FillRule::EvenOdd,
+ transform,
+ clip_mask,
+ );
+
+ if border_width > 0.0 {
+ // Border path is offset by half the border width
+ let border_bounds = Rectangle {
+ x: bounds.x + border_width / 2.0,
+ y: bounds.y + border_width / 2.0,
+ width: bounds.width - border_width,
+ height: bounds.height - border_width,
+ };
+
+ // Make sure the border radius is correct
+ let mut border_radius = *border_radius;
+ let mut is_simple_border = true;
+
+ for radius in &mut border_radius {
+ *radius = if *radius == 0.0 {
+ // Path should handle this fine
+ 0.0
+ } else if *radius > border_width / 2.0 {
+ *radius - border_width / 2.0
+ } else {
+ is_simple_border = false;
+ 0.0
+ }
+ .min(border_bounds.width / 2.0)
+ .min(border_bounds.height / 2.0);
+ }
+
+ // Stroking a path works well in this case
+ if is_simple_border {
+ let border_path =
+ rounded_rectangle(border_bounds, border_radius);
+
+ pixels.stroke_path(
+ &border_path,
+ &tiny_skia::Paint {
+ shader: tiny_skia::Shader::SolidColor(
+ into_color(*border_color),
+ ),
+ anti_alias: true,
+ ..tiny_skia::Paint::default()
+ },
+ &tiny_skia::Stroke {
+ width: border_width,
+ ..tiny_skia::Stroke::default()
+ },
+ transform,
+ clip_mask,
+ );
+ } else {
+ // Draw corners that have too small border radii as having no border radius,
+ // but mask them with the rounded rectangle with the correct border radius.
+ let mut temp_pixmap = tiny_skia::Pixmap::new(
+ bounds.width as u32,
+ bounds.height as u32,
+ )
+ .unwrap();
+
+ let mut quad_mask = tiny_skia::Mask::new(
+ bounds.width as u32,
+ bounds.height as u32,
+ )
+ .unwrap();
+
+ let zero_bounds = Rectangle {
+ x: 0.0,
+ y: 0.0,
+ width: bounds.width,
+ height: bounds.height,
+ };
+ let path =
+ rounded_rectangle(zero_bounds, fill_border_radius);
+
+ quad_mask.fill_path(
+ &path,
+ tiny_skia::FillRule::EvenOdd,
+ true,
+ transform,
+ );
+ let path_bounds = Rectangle {
+ x: border_width / 2.0,
+ y: border_width / 2.0,
+ width: bounds.width - border_width,
+ height: bounds.height - border_width,
+ };
+
+ let border_radius_path =
+ rounded_rectangle(path_bounds, border_radius);
+
+ temp_pixmap.stroke_path(
+ &border_radius_path,
+ &tiny_skia::Paint {
+ shader: tiny_skia::Shader::SolidColor(
+ into_color(*border_color),
+ ),
+ anti_alias: true,
+ ..tiny_skia::Paint::default()
+ },
+ &tiny_skia::Stroke {
+ width: border_width,
+ ..tiny_skia::Stroke::default()
+ },
+ transform,
+ Some(&quad_mask),
+ );
+
+ pixels.draw_pixmap(
+ bounds.x as i32,
+ bounds.y as i32,
+ temp_pixmap.as_ref(),
+ &tiny_skia::PixmapPaint::default(),
+ transform,
+ clip_mask,
+ );
+ }
+ }
+ }
+ Primitive::Text {
+ content,
+ bounds,
+ color,
+ size,
+ line_height,
+ font,
+ horizontal_alignment,
+ vertical_alignment,
+ shaping,
+ } => {
+ let physical_bounds =
+ (primitive.bounds() + translation) * scale_factor;
+
+ if !clip_bounds.intersects(&physical_bounds) {
+ return;
+ }
+
+ let clip_mask = (!physical_bounds.is_within(&clip_bounds))
+ .then_some(clip_mask as &_);
+
+ self.text_pipeline.draw(
+ content,
+ *bounds + translation,
+ *color,
+ *size,
+ *line_height,
+ *font,
+ *horizontal_alignment,
+ *vertical_alignment,
+ *shaping,
+ scale_factor,
+ pixels,
+ clip_mask,
+ );
+ }
+ #[cfg(feature = "image")]
+ Primitive::Image { handle, bounds } => {
+ let physical_bounds = (*bounds + translation) * scale_factor;
+
+ if !clip_bounds.intersects(&physical_bounds) {
+ return;
+ }
+
+ let clip_mask = (!physical_bounds.is_within(&clip_bounds))
+ .then_some(clip_mask as &_);
+
+ let transform = tiny_skia::Transform::from_translate(
+ translation.x,
+ translation.y,
+ )
+ .post_scale(scale_factor, scale_factor);
+
+ self.raster_pipeline
+ .draw(handle, *bounds, pixels, transform, clip_mask);
+ }
+ #[cfg(not(feature = "image"))]
+ Primitive::Image { .. } => {
+ log::warn!(
+ "Unsupported primitive in `iced_tiny_skia`: {:?}",
+ primitive
+ );
+ }
+ #[cfg(feature = "svg")]
+ Primitive::Svg {
+ handle,
+ bounds,
+ color,
+ } => {
+ let physical_bounds = (*bounds + translation) * scale_factor;
+
+ if !clip_bounds.intersects(&physical_bounds) {
+ return;
+ }
+
+ let clip_mask = (!physical_bounds.is_within(&clip_bounds))
+ .then_some(clip_mask as &_);
+
+ self.vector_pipeline.draw(
+ handle,
+ *color,
+ (*bounds + translation) * scale_factor,
+ pixels,
+ clip_mask,
+ );
+ }
+ #[cfg(not(feature = "svg"))]
+ Primitive::Svg { .. } => {
+ log::warn!(
+ "Unsupported primitive in `iced_tiny_skia`: {:?}",
+ primitive
+ );
+ }
+ Primitive::Custom(primitive::Custom::Fill {
+ path,
+ paint,
+ rule,
+ transform,
+ }) => {
+ let bounds = path.bounds();
+
+ let physical_bounds = (Rectangle {
+ x: bounds.x(),
+ y: bounds.y(),
+ width: bounds.width(),
+ height: bounds.height(),
+ } + translation)
+ * scale_factor;
+
+ if !clip_bounds.intersects(&physical_bounds) {
+ return;
+ }
+
+ let clip_mask = (!physical_bounds.is_within(&clip_bounds))
+ .then_some(clip_mask as &_);
+
+ pixels.fill_path(
+ path,
+ paint,
+ *rule,
+ transform
+ .post_translate(translation.x, translation.y)
+ .post_scale(scale_factor, scale_factor),
+ clip_mask,
+ );
+ }
+ Primitive::Custom(primitive::Custom::Stroke {
+ path,
+ paint,
+ stroke,
+ transform,
+ }) => {
+ let bounds = path.bounds();
+
+ let physical_bounds = (Rectangle {
+ x: bounds.x(),
+ y: bounds.y(),
+ width: bounds.width(),
+ height: bounds.height(),
+ } + translation)
+ * scale_factor;
+
+ if !clip_bounds.intersects(&physical_bounds) {
+ return;
+ }
+
+ let clip_mask = (!physical_bounds.is_within(&clip_bounds))
+ .then_some(clip_mask as &_);
+
+ pixels.stroke_path(
+ path,
+ paint,
+ stroke,
+ transform
+ .post_translate(translation.x, translation.y)
+ .post_scale(scale_factor, scale_factor),
+ clip_mask,
+ );
+ }
+ Primitive::Group { primitives } => {
+ for primitive in primitives {
+ self.draw_primitive(
+ primitive,
+ pixels,
+ clip_mask,
+ clip_bounds,
+ scale_factor,
+ translation,
+ );
+ }
+ }
+ Primitive::Translate {
+ translation: offset,
+ content,
+ } => {
+ self.draw_primitive(
+ content,
+ pixels,
+ clip_mask,
+ clip_bounds,
+ scale_factor,
+ translation + *offset,
+ );
+ }
+ Primitive::Clip { bounds, content } => {
+ let bounds = (*bounds + translation) * scale_factor;
+
+ if bounds == clip_bounds {
+ self.draw_primitive(
+ content,
+ pixels,
+ clip_mask,
+ bounds,
+ scale_factor,
+ translation,
+ );
+ } else if let Some(bounds) = clip_bounds.intersection(&bounds) {
+ if bounds.x + bounds.width <= 0.0
+ || bounds.y + bounds.height <= 0.0
+ || bounds.x as u32 >= pixels.width()
+ || bounds.y as u32 >= pixels.height()
+ || bounds.width <= 1.0
+ || bounds.height <= 1.0
+ {
+ return;
+ }
+
+ adjust_clip_mask(clip_mask, bounds);
+
+ self.draw_primitive(
+ content,
+ pixels,
+ clip_mask,
+ bounds,
+ scale_factor,
+ translation,
+ );
+
+ adjust_clip_mask(clip_mask, clip_bounds);
+ }
+ }
+ Primitive::Cache { content } => {
+ self.draw_primitive(
+ content,
+ pixels,
+ clip_mask,
+ clip_bounds,
+ scale_factor,
+ translation,
+ );
+ }
+ }
+ }
+}
+
+fn into_color(color: Color) -> tiny_skia::Color {
+ tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a)
+ .expect("Convert color from iced to tiny_skia")
+}
+
+fn rounded_rectangle(
+ bounds: Rectangle,
+ border_radius: [f32; 4],
+) -> tiny_skia::Path {
+ let [top_left, top_right, bottom_right, bottom_left] = border_radius;
+
+ if top_left == 0.0
+ && top_right == 0.0
+ && bottom_right == 0.0
+ && bottom_left == 0.0
+ {
+ return tiny_skia::PathBuilder::from_rect(
+ tiny_skia::Rect::from_xywh(
+ bounds.x,
+ bounds.y,
+ bounds.width,
+ bounds.height,
+ )
+ .expect("Build quad rectangle"),
+ );
+ }
+
+ if top_left == top_right
+ && top_left == bottom_right
+ && top_left == bottom_left
+ && top_left == bounds.width / 2.0
+ && top_left == bounds.height / 2.0
+ {
+ return tiny_skia::PathBuilder::from_circle(
+ bounds.x + bounds.width / 2.0,
+ bounds.y + bounds.height / 2.0,
+ top_left,
+ )
+ .expect("Build circle path");
+ }
+
+ let mut builder = tiny_skia::PathBuilder::new();
+
+ builder.move_to(bounds.x + top_left, bounds.y);
+ builder.line_to(bounds.x + bounds.width - top_right, bounds.y);
+
+ if top_right > 0.0 {
+ arc_to(
+ &mut builder,
+ bounds.x + bounds.width - top_right,
+ bounds.y,
+ bounds.x + bounds.width,
+ bounds.y + top_right,
+ top_right,
+ );
+ }
+
+ maybe_line_to(
+ &mut builder,
+ bounds.x + bounds.width,
+ bounds.y + bounds.height - bottom_right,
+ );
+
+ if bottom_right > 0.0 {
+ arc_to(
+ &mut builder,
+ bounds.x + bounds.width,
+ bounds.y + bounds.height - bottom_right,
+ bounds.x + bounds.width - bottom_right,
+ bounds.y + bounds.height,
+ bottom_right,
+ );
+ }
+
+ maybe_line_to(
+ &mut builder,
+ bounds.x + bottom_left,
+ bounds.y + bounds.height,
+ );
+
+ if bottom_left > 0.0 {
+ arc_to(
+ &mut builder,
+ bounds.x + bottom_left,
+ bounds.y + bounds.height,
+ bounds.x,
+ bounds.y + bounds.height - bottom_left,
+ bottom_left,
+ );
+ }
+
+ maybe_line_to(&mut builder, bounds.x, bounds.y + top_left);
+
+ if top_left > 0.0 {
+ arc_to(
+ &mut builder,
+ bounds.x,
+ bounds.y + top_left,
+ bounds.x + top_left,
+ bounds.y,
+ top_left,
+ );
+ }
+
+ builder.finish().expect("Build rounded rectangle path")
+}
+
+fn maybe_line_to(path: &mut tiny_skia::PathBuilder, x: f32, y: f32) {
+ if path.last_point() != Some(tiny_skia::Point { x, y }) {
+ path.line_to(x, y);
+ }
+}
+
+fn arc_to(
+ path: &mut tiny_skia::PathBuilder,
+ x_from: f32,
+ y_from: f32,
+ x_to: f32,
+ y_to: f32,
+ radius: f32,
+) {
+ let svg_arc = kurbo::SvgArc {
+ from: kurbo::Point::new(f64::from(x_from), f64::from(y_from)),
+ to: kurbo::Point::new(f64::from(x_to), f64::from(y_to)),
+ radii: kurbo::Vec2::new(f64::from(radius), f64::from(radius)),
+ x_rotation: 0.0,
+ large_arc: false,
+ sweep: true,
+ };
+
+ match kurbo::Arc::from_svg_arc(&svg_arc) {
+ Some(arc) => {
+ arc.to_cubic_beziers(0.1, |p1, p2, p| {
+ path.cubic_to(
+ p1.x as f32,
+ p1.y as f32,
+ p2.x as f32,
+ p2.y as f32,
+ p.x as f32,
+ p.y as f32,
+ );
+ });
+ }
+ None => {
+ path.line_to(x_to, y_to);
+ }
+ }
+}
+
+fn adjust_clip_mask(clip_mask: &mut tiny_skia::Mask, bounds: Rectangle) {
+ clip_mask.clear();
+
+ let path = {
+ let mut builder = tiny_skia::PathBuilder::new();
+ builder.push_rect(
+ tiny_skia::Rect::from_xywh(
+ bounds.x,
+ bounds.y,
+ bounds.width,
+ bounds.height,
+ )
+ .unwrap(),
+ );
+
+ builder.finish().unwrap()
+ };
+
+ clip_mask.fill_path(
+ &path,
+ tiny_skia::FillRule::EvenOdd,
+ false,
+ tiny_skia::Transform::default(),
+ );
+}
+
+impl iced_graphics::Backend for Backend {
+ type Primitive = primitive::Custom;
+}
+
+impl backend::Text for Backend {
+ const ICON_FONT: Font = Font::with_name("Iced-Icons");
+ const CHECKMARK_ICON: char = '\u{f00c}';
+ const ARROW_DOWN_ICON: char = '\u{e800}';
+
+ fn default_font(&self) -> Font {
+ self.default_font
+ }
+
+ fn default_size(&self) -> f32 {
+ self.default_text_size
+ }
+
+ fn measure(
+ &self,
+ contents: &str,
+ size: f32,
+ line_height: text::LineHeight,
+ font: Font,
+ bounds: Size,
+ shaping: text::Shaping,
+ ) -> Size {
+ self.text_pipeline.measure(
+ contents,
+ size,
+ line_height,
+ font,
+ bounds,
+ shaping,
+ )
+ }
+
+ fn hit_test(
+ &self,
+ contents: &str,
+ size: f32,
+ line_height: text::LineHeight,
+ font: Font,
+ bounds: Size,
+ shaping: text::Shaping,
+ point: Point,
+ nearest_only: bool,
+ ) -> Option<text::Hit> {
+ self.text_pipeline.hit_test(
+ contents,
+ size,
+ line_height,
+ font,
+ bounds,
+ shaping,
+ point,
+ nearest_only,
+ )
+ }
+
+ fn load_font(&mut self, font: Cow<'static, [u8]>) {
+ self.text_pipeline.load_font(font);
+ }
+}
+
+#[cfg(feature = "image")]
+impl backend::Image for Backend {
+ fn dimensions(&self, handle: &crate::core::image::Handle) -> Size<u32> {
+ self.raster_pipeline.dimensions(handle)
+ }
+}
+
+#[cfg(feature = "svg")]
+impl backend::Svg for Backend {
+ fn viewport_dimensions(
+ &self,
+ handle: &crate::core::svg::Handle,
+ ) -> Size<u32> {
+ self.vector_pipeline.viewport_dimensions(handle)
+ }
+}
diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs
new file mode 100644
index 00000000..9bd47556
--- /dev/null
+++ b/tiny_skia/src/geometry.rs
@@ -0,0 +1,315 @@
+use crate::core::{Point, Rectangle, Size, Vector};
+use crate::graphics::geometry::fill::{self, Fill};
+use crate::graphics::geometry::stroke::{self, Stroke};
+use crate::graphics::geometry::{Path, Style, Text};
+use crate::graphics::Gradient;
+use crate::primitive::{self, Primitive};
+
+pub struct Frame {
+ size: Size,
+ transform: tiny_skia::Transform,
+ stack: Vec<tiny_skia::Transform>,
+ primitives: Vec<Primitive>,
+}
+
+impl Frame {
+ pub fn new(size: Size) -> Self {
+ Self {
+ size,
+ transform: tiny_skia::Transform::identity(),
+ stack: Vec::new(),
+ primitives: Vec::new(),
+ }
+ }
+
+ pub fn width(&self) -> f32 {
+ self.size.width
+ }
+
+ pub fn height(&self) -> f32 {
+ self.size.height
+ }
+
+ pub fn size(&self) -> Size {
+ self.size
+ }
+
+ pub fn center(&self) -> Point {
+ Point::new(self.size.width / 2.0, self.size.height / 2.0)
+ }
+
+ pub fn fill(&mut self, path: &Path, fill: impl Into<Fill>) {
+ let Some(path) = convert_path(path) else { return };
+ let fill = fill.into();
+
+ self.primitives
+ .push(Primitive::Custom(primitive::Custom::Fill {
+ path,
+ paint: into_paint(fill.style),
+ rule: into_fill_rule(fill.rule),
+ transform: self.transform,
+ }));
+ }
+
+ pub fn fill_rectangle(
+ &mut self,
+ top_left: Point,
+ size: Size,
+ fill: impl Into<Fill>,
+ ) {
+ let Some(path) = convert_path(&Path::rectangle(top_left, size)) else { return };
+ let fill = fill.into();
+
+ self.primitives
+ .push(Primitive::Custom(primitive::Custom::Fill {
+ path,
+ paint: tiny_skia::Paint {
+ anti_alias: false,
+ ..into_paint(fill.style)
+ },
+ rule: into_fill_rule(fill.rule),
+ transform: self.transform,
+ }));
+ }
+
+ pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) {
+ let Some(path) = convert_path(path) else { return };
+
+ let stroke = stroke.into();
+ let skia_stroke = into_stroke(&stroke);
+
+ self.primitives
+ .push(Primitive::Custom(primitive::Custom::Stroke {
+ path,
+ paint: into_paint(stroke.style),
+ stroke: skia_stroke,
+ transform: self.transform,
+ }));
+ }
+
+ pub fn fill_text(&mut self, text: impl Into<Text>) {
+ let text = text.into();
+
+ let position = if self.transform.is_identity() {
+ text.position
+ } else {
+ let mut transformed = [tiny_skia::Point {
+ x: text.position.x,
+ y: text.position.y,
+ }];
+
+ self.transform.map_points(&mut transformed);
+
+ Point::new(transformed[0].x, transformed[0].y)
+ };
+
+ // TODO: Use vectorial text instead of primitive
+ self.primitives.push(Primitive::Text {
+ content: text.content,
+ bounds: Rectangle {
+ x: position.x,
+ y: position.y,
+ width: f32::INFINITY,
+ height: f32::INFINITY,
+ },
+ color: text.color,
+ size: text.size,
+ line_height: text.line_height,
+ font: text.font,
+ horizontal_alignment: text.horizontal_alignment,
+ vertical_alignment: text.vertical_alignment,
+ shaping: text.shaping,
+ });
+ }
+
+ pub fn push_transform(&mut self) {
+ self.stack.push(self.transform);
+ }
+
+ pub fn pop_transform(&mut self) {
+ self.transform = self.stack.pop().expect("Pop transform");
+ }
+
+ pub fn clip(&mut self, frame: Self, at: Point) {
+ self.primitives.push(Primitive::Translate {
+ translation: Vector::new(at.x, at.y),
+ content: Box::new(frame.into_primitive()),
+ });
+ }
+
+ pub fn translate(&mut self, translation: Vector) {
+ self.transform =
+ self.transform.pre_translate(translation.x, translation.y);
+ }
+
+ pub fn rotate(&mut self, angle: f32) {
+ self.transform = self
+ .transform
+ .pre_concat(tiny_skia::Transform::from_rotate(angle.to_degrees()));
+ }
+
+ pub fn scale(&mut self, scale: f32) {
+ self.transform = self.transform.pre_scale(scale, scale);
+ }
+
+ pub fn into_primitive(self) -> Primitive {
+ Primitive::Clip {
+ bounds: Rectangle::new(Point::ORIGIN, self.size),
+ content: Box::new(Primitive::Group {
+ primitives: self.primitives,
+ }),
+ }
+ }
+}
+
+fn convert_path(path: &Path) -> Option<tiny_skia::Path> {
+ use iced_graphics::geometry::path::lyon_path;
+
+ let mut builder = tiny_skia::PathBuilder::new();
+ let mut last_point = Default::default();
+
+ for event in path.raw().iter() {
+ match event {
+ lyon_path::Event::Begin { at } => {
+ builder.move_to(at.x, at.y);
+
+ last_point = at;
+ }
+ lyon_path::Event::Line { from, to } => {
+ if last_point != from {
+ builder.move_to(from.x, from.y);
+ }
+
+ builder.line_to(to.x, to.y);
+
+ last_point = to;
+ }
+ lyon_path::Event::Quadratic { from, ctrl, to } => {
+ if last_point != from {
+ builder.move_to(from.x, from.y);
+ }
+
+ builder.quad_to(ctrl.x, ctrl.y, to.x, to.y);
+
+ last_point = to;
+ }
+ lyon_path::Event::Cubic {
+ from,
+ ctrl1,
+ ctrl2,
+ to,
+ } => {
+ if last_point != from {
+ builder.move_to(from.x, from.y);
+ }
+
+ builder
+ .cubic_to(ctrl1.x, ctrl1.y, ctrl2.x, ctrl2.y, to.x, to.y);
+
+ last_point = to;
+ }
+ lyon_path::Event::End { close, .. } => {
+ if close {
+ builder.close();
+ }
+ }
+ }
+ }
+
+ let result = builder.finish();
+
+ #[cfg(debug_assertions)]
+ if result.is_none() {
+ log::warn!("Invalid path: {:?}", path.raw());
+ }
+
+ result
+}
+
+pub fn into_paint(style: Style) -> tiny_skia::Paint<'static> {
+ tiny_skia::Paint {
+ shader: match style {
+ Style::Solid(color) => tiny_skia::Shader::SolidColor(
+ tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a)
+ .expect("Create color"),
+ ),
+ Style::Gradient(gradient) => match gradient {
+ Gradient::Linear(linear) => {
+ let stops: Vec<tiny_skia::GradientStop> = linear
+ .stops
+ .into_iter()
+ .flatten()
+ .map(|stop| {
+ tiny_skia::GradientStop::new(
+ stop.offset,
+ tiny_skia::Color::from_rgba(
+ stop.color.b,
+ stop.color.g,
+ stop.color.r,
+ stop.color.a,
+ )
+ .expect("Create color"),
+ )
+ })
+ .collect();
+
+ tiny_skia::LinearGradient::new(
+ tiny_skia::Point {
+ x: linear.start.x,
+ y: linear.start.y,
+ },
+ tiny_skia::Point {
+ x: linear.end.x,
+ y: linear.end.y,
+ },
+ if stops.is_empty() {
+ vec![tiny_skia::GradientStop::new(
+ 0.0,
+ tiny_skia::Color::BLACK,
+ )]
+ } else {
+ stops
+ },
+ tiny_skia::SpreadMode::Pad,
+ tiny_skia::Transform::identity(),
+ )
+ .expect("Create linear gradient")
+ }
+ },
+ },
+ anti_alias: true,
+ ..Default::default()
+ }
+}
+
+pub fn into_fill_rule(rule: fill::Rule) -> tiny_skia::FillRule {
+ match rule {
+ fill::Rule::EvenOdd => tiny_skia::FillRule::EvenOdd,
+ fill::Rule::NonZero => tiny_skia::FillRule::Winding,
+ }
+}
+
+pub fn into_stroke(stroke: &Stroke) -> tiny_skia::Stroke {
+ tiny_skia::Stroke {
+ width: stroke.width,
+ line_cap: match stroke.line_cap {
+ stroke::LineCap::Butt => tiny_skia::LineCap::Butt,
+ stroke::LineCap::Square => tiny_skia::LineCap::Square,
+ stroke::LineCap::Round => tiny_skia::LineCap::Round,
+ },
+ line_join: match stroke.line_join {
+ stroke::LineJoin::Miter => tiny_skia::LineJoin::Miter,
+ stroke::LineJoin::Round => tiny_skia::LineJoin::Round,
+ stroke::LineJoin::Bevel => tiny_skia::LineJoin::Bevel,
+ },
+ dash: if stroke.line_dash.segments.is_empty() {
+ None
+ } else {
+ tiny_skia::StrokeDash::new(
+ stroke.line_dash.segments.into(),
+ stroke.line_dash.offset as f32,
+ )
+ },
+ ..Default::default()
+ }
+}
diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs
new file mode 100644
index 00000000..15de6ce2
--- /dev/null
+++ b/tiny_skia/src/lib.rs
@@ -0,0 +1,28 @@
+pub mod window;
+
+mod backend;
+mod primitive;
+mod settings;
+mod text;
+
+#[cfg(feature = "image")]
+mod raster;
+
+#[cfg(feature = "svg")]
+mod vector;
+
+#[cfg(feature = "geometry")]
+pub mod geometry;
+
+pub use iced_graphics as graphics;
+pub use iced_graphics::core;
+
+pub use backend::Backend;
+pub use primitive::Primitive;
+pub use settings::Settings;
+
+/// A [`tiny-skia`] graphics renderer for [`iced`].
+///
+/// [`tiny-skia`]: https://github.com/RazrFalcon/tiny-skia
+/// [`iced`]: https://github.com/iced-rs/iced
+pub type Renderer<Theme> = iced_graphics::Renderer<Backend, Theme>;
diff --git a/tiny_skia/src/primitive.rs b/tiny_skia/src/primitive.rs
new file mode 100644
index 00000000..0ed24969
--- /dev/null
+++ b/tiny_skia/src/primitive.rs
@@ -0,0 +1,48 @@
+use crate::core::Rectangle;
+use crate::graphics::Damage;
+
+pub type Primitive = crate::graphics::Primitive<Custom>;
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum Custom {
+ /// A path filled with some paint.
+ Fill {
+ /// The path to fill.
+ path: tiny_skia::Path,
+ /// The paint to use.
+ paint: tiny_skia::Paint<'static>,
+ /// The fill rule to follow.
+ rule: tiny_skia::FillRule,
+ /// The transform to apply to the path.
+ transform: tiny_skia::Transform,
+ },
+ /// A path stroked with some paint.
+ Stroke {
+ /// The path to stroke.
+ path: tiny_skia::Path,
+ /// The paint to use.
+ paint: tiny_skia::Paint<'static>,
+ /// The stroke settings.
+ stroke: tiny_skia::Stroke,
+ /// The transform to apply to the path.
+ transform: tiny_skia::Transform,
+ },
+}
+
+impl Damage for Custom {
+ fn bounds(&self) -> Rectangle {
+ match self {
+ Self::Fill { path, .. } | Self::Stroke { path, .. } => {
+ let bounds = path.bounds();
+
+ Rectangle {
+ x: bounds.x(),
+ y: bounds.y(),
+ width: bounds.width(),
+ height: bounds.height(),
+ }
+ .expand(1.0)
+ }
+ }
+ }
+}
diff --git a/tiny_skia/src/raster.rs b/tiny_skia/src/raster.rs
new file mode 100644
index 00000000..dedb127c
--- /dev/null
+++ b/tiny_skia/src/raster.rs
@@ -0,0 +1,116 @@
+use crate::core::image as raster;
+use crate::core::{Rectangle, Size};
+use crate::graphics;
+
+use rustc_hash::{FxHashMap, FxHashSet};
+use std::cell::RefCell;
+use std::collections::hash_map;
+
+pub struct Pipeline {
+ cache: RefCell<Cache>,
+}
+
+impl Pipeline {
+ pub fn new() -> Self {
+ Self {
+ cache: RefCell::new(Cache::default()),
+ }
+ }
+
+ pub fn dimensions(&self, handle: &raster::Handle) -> Size<u32> {
+ if let Some(image) = self.cache.borrow_mut().allocate(handle) {
+ Size::new(image.width(), image.height())
+ } else {
+ Size::new(0, 0)
+ }
+ }
+
+ pub fn draw(
+ &mut self,
+ handle: &raster::Handle,
+ bounds: Rectangle,
+ pixels: &mut tiny_skia::PixmapMut<'_>,
+ transform: tiny_skia::Transform,
+ clip_mask: Option<&tiny_skia::Mask>,
+ ) {
+ if let Some(image) = self.cache.borrow_mut().allocate(handle) {
+ let width_scale = bounds.width / image.width() as f32;
+ let height_scale = bounds.height / image.height() as f32;
+
+ let transform = transform.pre_scale(width_scale, height_scale);
+
+ pixels.draw_pixmap(
+ (bounds.x / width_scale) as i32,
+ (bounds.y / height_scale) as i32,
+ image,
+ &tiny_skia::PixmapPaint {
+ quality: tiny_skia::FilterQuality::Bilinear,
+ ..Default::default()
+ },
+ transform,
+ clip_mask,
+ );
+ }
+ }
+
+ pub fn trim_cache(&mut self) {
+ self.cache.borrow_mut().trim();
+ }
+}
+
+#[derive(Default)]
+struct Cache {
+ entries: FxHashMap<u64, Option<Entry>>,
+ hits: FxHashSet<u64>,
+}
+
+impl Cache {
+ pub fn allocate(
+ &mut self,
+ handle: &raster::Handle,
+ ) -> Option<tiny_skia::PixmapRef<'_>> {
+ let id = handle.id();
+
+ if let hash_map::Entry::Vacant(entry) = self.entries.entry(id) {
+ let image = graphics::image::load(handle).ok()?.into_rgba8();
+
+ let mut buffer =
+ vec![0u32; image.width() as usize * image.height() as usize];
+
+ for (i, pixel) in image.pixels().enumerate() {
+ let [r, g, b, a] = pixel.0;
+
+ buffer[i] = bytemuck::cast(
+ tiny_skia::ColorU8::from_rgba(b, g, r, a).premultiply(),
+ );
+ }
+
+ entry.insert(Some(Entry {
+ width: image.width(),
+ height: image.height(),
+ pixels: buffer,
+ }));
+ }
+
+ self.hits.insert(id);
+ self.entries.get(&id).unwrap().as_ref().map(|entry| {
+ tiny_skia::PixmapRef::from_bytes(
+ bytemuck::cast_slice(&entry.pixels),
+ entry.width,
+ entry.height,
+ )
+ .expect("Build pixmap from image bytes")
+ })
+ }
+
+ fn trim(&mut self) {
+ self.entries.retain(|key, _| self.hits.contains(key));
+ self.hits.clear();
+ }
+}
+
+struct Entry {
+ width: u32,
+ height: u32,
+ pixels: Vec<u32>,
+}
diff --git a/tiny_skia/src/settings.rs b/tiny_skia/src/settings.rs
new file mode 100644
index 00000000..abffbfe6
--- /dev/null
+++ b/tiny_skia/src/settings.rs
@@ -0,0 +1,24 @@
+use crate::core::Font;
+
+/// The settings of a [`Backend`].
+///
+/// [`Backend`]: crate::Backend
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Settings {
+ /// The default [`Font`] to use.
+ pub default_font: Font,
+
+ /// The default size of text.
+ ///
+ /// By default, it will be set to `16.0`.
+ pub default_text_size: f32,
+}
+
+impl Default for Settings {
+ fn default() -> Settings {
+ Settings {
+ default_font: Font::default(),
+ default_text_size: 16.0,
+ }
+ }
+}
diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs
new file mode 100644
index 00000000..15f25740
--- /dev/null
+++ b/tiny_skia/src/text.rs
@@ -0,0 +1,483 @@
+use crate::core::alignment;
+use crate::core::font::{self, Font};
+use crate::core::text::{Hit, LineHeight, Shaping};
+use crate::core::{Color, Pixels, Point, Rectangle, Size};
+
+use rustc_hash::{FxHashMap, FxHashSet};
+use std::borrow::Cow;
+use std::cell::RefCell;
+use std::collections::hash_map;
+use std::hash::{BuildHasher, Hash, Hasher};
+use std::sync::Arc;
+
+#[allow(missing_debug_implementations)]
+pub struct Pipeline {
+ font_system: RefCell<cosmic_text::FontSystem>,
+ glyph_cache: GlyphCache,
+ cache: RefCell<Cache>,
+}
+
+impl Pipeline {
+ pub fn new() -> Self {
+ Pipeline {
+ font_system: RefCell::new(cosmic_text::FontSystem::new_with_fonts(
+ [cosmic_text::fontdb::Source::Binary(Arc::new(
+ include_bytes!("../fonts/Iced-Icons.ttf").as_slice(),
+ ))]
+ .into_iter(),
+ )),
+ glyph_cache: GlyphCache::new(),
+ cache: RefCell::new(Cache::new()),
+ }
+ }
+
+ pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
+ self.font_system.get_mut().db_mut().load_font_source(
+ cosmic_text::fontdb::Source::Binary(Arc::new(bytes.into_owned())),
+ );
+
+ self.cache = RefCell::new(Cache::new());
+ }
+
+ pub fn draw(
+ &mut self,
+ content: &str,
+ bounds: Rectangle,
+ color: Color,
+ size: f32,
+ line_height: LineHeight,
+ font: Font,
+ horizontal_alignment: alignment::Horizontal,
+ vertical_alignment: alignment::Vertical,
+ shaping: Shaping,
+ scale_factor: f32,
+ pixels: &mut tiny_skia::PixmapMut<'_>,
+ clip_mask: Option<&tiny_skia::Mask>,
+ ) {
+ let line_height = f32::from(line_height.to_absolute(Pixels(size)));
+
+ let font_system = self.font_system.get_mut();
+ let key = Key {
+ bounds: bounds.size(),
+ content,
+ font,
+ size,
+ line_height,
+ shaping,
+ };
+
+ let (_, entry) = self.cache.get_mut().allocate(font_system, key);
+
+ let max_width = entry.bounds.width * scale_factor;
+ let total_height = entry.bounds.height * scale_factor;
+
+ let bounds = bounds * scale_factor;
+
+ let x = match horizontal_alignment {
+ alignment::Horizontal::Left => bounds.x,
+ alignment::Horizontal::Center => bounds.x - max_width / 2.0,
+ alignment::Horizontal::Right => bounds.x - max_width,
+ };
+
+ let y = match vertical_alignment {
+ alignment::Vertical::Top => bounds.y,
+ alignment::Vertical::Center => bounds.y - total_height / 2.0,
+ alignment::Vertical::Bottom => bounds.y - total_height,
+ };
+
+ let mut swash = cosmic_text::SwashCache::new();
+
+ for run in entry.buffer.layout_runs() {
+ for glyph in run.glyphs {
+ let physical_glyph = glyph.physical((x, y), scale_factor);
+
+ if let Some((buffer, placement)) = self.glyph_cache.allocate(
+ physical_glyph.cache_key,
+ color,
+ font_system,
+ &mut swash,
+ ) {
+ let pixmap = tiny_skia::PixmapRef::from_bytes(
+ buffer,
+ placement.width,
+ placement.height,
+ )
+ .expect("Create glyph pixel map");
+
+ pixels.draw_pixmap(
+ physical_glyph.x + placement.left,
+ physical_glyph.y - placement.top
+ + (run.line_y * scale_factor).round() as i32,
+ pixmap,
+ &tiny_skia::PixmapPaint::default(),
+ tiny_skia::Transform::identity(),
+ clip_mask,
+ );
+ }
+ }
+ }
+ }
+
+ pub fn trim_cache(&mut self) {
+ self.cache.get_mut().trim();
+ self.glyph_cache.trim();
+ }
+
+ pub fn measure(
+ &self,
+ content: &str,
+ size: f32,
+ line_height: LineHeight,
+ font: Font,
+ bounds: Size,
+ shaping: Shaping,
+ ) -> Size {
+ let mut measurement_cache = self.cache.borrow_mut();
+
+ let line_height = f32::from(line_height.to_absolute(Pixels(size)));
+
+ let (_, entry) = measurement_cache.allocate(
+ &mut self.font_system.borrow_mut(),
+ Key {
+ content,
+ size,
+ line_height,
+ font,
+ bounds,
+ shaping,
+ },
+ );
+
+ entry.bounds
+ }
+
+ pub fn hit_test(
+ &self,
+ content: &str,
+ size: f32,
+ line_height: LineHeight,
+ font: Font,
+ bounds: Size,
+ shaping: Shaping,
+ point: Point,
+ _nearest_only: bool,
+ ) -> Option<Hit> {
+ let mut measurement_cache = self.cache.borrow_mut();
+
+ let line_height = f32::from(line_height.to_absolute(Pixels(size)));
+
+ let (_, entry) = measurement_cache.allocate(
+ &mut self.font_system.borrow_mut(),
+ Key {
+ content,
+ size,
+ line_height,
+ font,
+ bounds,
+ shaping,
+ },
+ );
+
+ let cursor = entry.buffer.hit(point.x, point.y)?;
+
+ Some(Hit::CharOffset(cursor.index))
+ }
+}
+
+fn measure(buffer: &cosmic_text::Buffer) -> Size {
+ let (width, total_lines) = buffer
+ .layout_runs()
+ .fold((0.0, 0usize), |(width, total_lines), run| {
+ (run.line_w.max(width), total_lines + 1)
+ });
+
+ Size::new(width, total_lines as f32 * buffer.metrics().line_height)
+}
+
+fn to_family(family: font::Family) -> cosmic_text::Family<'static> {
+ match family {
+ font::Family::Name(name) => cosmic_text::Family::Name(name),
+ font::Family::SansSerif => cosmic_text::Family::SansSerif,
+ font::Family::Serif => cosmic_text::Family::Serif,
+ font::Family::Cursive => cosmic_text::Family::Cursive,
+ font::Family::Fantasy => cosmic_text::Family::Fantasy,
+ font::Family::Monospace => cosmic_text::Family::Monospace,
+ }
+}
+
+fn to_weight(weight: font::Weight) -> cosmic_text::Weight {
+ match weight {
+ font::Weight::Thin => cosmic_text::Weight::THIN,
+ font::Weight::ExtraLight => cosmic_text::Weight::EXTRA_LIGHT,
+ font::Weight::Light => cosmic_text::Weight::LIGHT,
+ font::Weight::Normal => cosmic_text::Weight::NORMAL,
+ font::Weight::Medium => cosmic_text::Weight::MEDIUM,
+ font::Weight::Semibold => cosmic_text::Weight::SEMIBOLD,
+ font::Weight::Bold => cosmic_text::Weight::BOLD,
+ font::Weight::ExtraBold => cosmic_text::Weight::EXTRA_BOLD,
+ font::Weight::Black => cosmic_text::Weight::BLACK,
+ }
+}
+
+fn to_stretch(stretch: font::Stretch) -> cosmic_text::Stretch {
+ match stretch {
+ font::Stretch::UltraCondensed => cosmic_text::Stretch::UltraCondensed,
+ font::Stretch::ExtraCondensed => cosmic_text::Stretch::ExtraCondensed,
+ font::Stretch::Condensed => cosmic_text::Stretch::Condensed,
+ font::Stretch::SemiCondensed => cosmic_text::Stretch::SemiCondensed,
+ font::Stretch::Normal => cosmic_text::Stretch::Normal,
+ font::Stretch::SemiExpanded => cosmic_text::Stretch::SemiExpanded,
+ font::Stretch::Expanded => cosmic_text::Stretch::Expanded,
+ font::Stretch::ExtraExpanded => cosmic_text::Stretch::ExtraExpanded,
+ font::Stretch::UltraExpanded => cosmic_text::Stretch::UltraExpanded,
+ }
+}
+
+fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping {
+ match shaping {
+ Shaping::Basic => cosmic_text::Shaping::Basic,
+ Shaping::Advanced => cosmic_text::Shaping::Advanced,
+ }
+}
+
+#[derive(Debug, Clone, Default)]
+struct GlyphCache {
+ entries: FxHashMap<
+ (cosmic_text::CacheKey, [u8; 3]),
+ (Vec<u32>, cosmic_text::Placement),
+ >,
+ recently_used: FxHashSet<(cosmic_text::CacheKey, [u8; 3])>,
+ trim_count: usize,
+}
+
+impl GlyphCache {
+ const TRIM_INTERVAL: usize = 300;
+
+ fn new() -> Self {
+ GlyphCache::default()
+ }
+
+ fn allocate(
+ &mut self,
+ cache_key: cosmic_text::CacheKey,
+ color: Color,
+ font_system: &mut cosmic_text::FontSystem,
+ swash: &mut cosmic_text::SwashCache,
+ ) -> Option<(&[u8], cosmic_text::Placement)> {
+ let [r, g, b, _a] = color.into_rgba8();
+ let key = (cache_key, [r, g, b]);
+
+ if let hash_map::Entry::Vacant(entry) = self.entries.entry(key) {
+ // TODO: Outline support
+ let image = swash.get_image_uncached(font_system, cache_key)?;
+
+ let glyph_size = image.placement.width as usize
+ * image.placement.height as usize;
+
+ if glyph_size == 0 {
+ return None;
+ }
+
+ let mut buffer = vec![0u32; glyph_size];
+
+ match image.content {
+ cosmic_text::SwashContent::Mask => {
+ let mut i = 0;
+
+ // TODO: Blend alpha
+
+ for _y in 0..image.placement.height {
+ for _x in 0..image.placement.width {
+ buffer[i] = bytemuck::cast(
+ tiny_skia::ColorU8::from_rgba(
+ b,
+ g,
+ r,
+ image.data[i],
+ )
+ .premultiply(),
+ );
+
+ i += 1;
+ }
+ }
+ }
+ cosmic_text::SwashContent::Color => {
+ let mut i = 0;
+
+ for _y in 0..image.placement.height {
+ for _x in 0..image.placement.width {
+ // TODO: Blend alpha
+ buffer[i >> 2] = bytemuck::cast(
+ tiny_skia::ColorU8::from_rgba(
+ image.data[i + 2],
+ image.data[i + 1],
+ image.data[i],
+ image.data[i + 3],
+ )
+ .premultiply(),
+ );
+
+ i += 4;
+ }
+ }
+ }
+ cosmic_text::SwashContent::SubpixelMask => {
+ // TODO
+ }
+ }
+
+ entry.insert((buffer, image.placement));
+ }
+
+ self.recently_used.insert(key);
+
+ self.entries.get(&key).map(|(buffer, placement)| {
+ (bytemuck::cast_slice(buffer.as_slice()), *placement)
+ })
+ }
+
+ pub fn trim(&mut self) {
+ if self.trim_count > Self::TRIM_INTERVAL {
+ self.entries
+ .retain(|key, _| self.recently_used.contains(key));
+
+ self.recently_used.clear();
+
+ self.trim_count = 0;
+ } else {
+ self.trim_count += 1;
+ }
+ }
+}
+
+struct Cache {
+ entries: FxHashMap<KeyHash, Entry>,
+ measurements: FxHashMap<KeyHash, KeyHash>,
+ recently_used: FxHashSet<KeyHash>,
+ hasher: HashBuilder,
+ trim_count: usize,
+}
+
+struct Entry {
+ buffer: cosmic_text::Buffer,
+ bounds: Size,
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+type HashBuilder = twox_hash::RandomXxHashBuilder64;
+
+#[cfg(target_arch = "wasm32")]
+type HashBuilder = std::hash::BuildHasherDefault<twox_hash::XxHash64>;
+
+impl Cache {
+ const TRIM_INTERVAL: usize = 300;
+
+ fn new() -> Self {
+ Self {
+ entries: FxHashMap::default(),
+ measurements: FxHashMap::default(),
+ recently_used: FxHashSet::default(),
+ hasher: HashBuilder::default(),
+ trim_count: 0,
+ }
+ }
+
+ fn allocate(
+ &mut self,
+ font_system: &mut cosmic_text::FontSystem,
+ key: Key<'_>,
+ ) -> (KeyHash, &mut Entry) {
+ let hash = key.hash(self.hasher.build_hasher());
+
+ if let Some(hash) = self.measurements.get(&hash) {
+ let _ = self.recently_used.insert(*hash);
+
+ return (*hash, self.entries.get_mut(hash).unwrap());
+ }
+
+ if let hash_map::Entry::Vacant(entry) = self.entries.entry(hash) {
+ let metrics = cosmic_text::Metrics::new(key.size, key.size * 1.2);
+ let mut buffer = cosmic_text::Buffer::new(font_system, metrics);
+
+ buffer.set_size(
+ font_system,
+ key.bounds.width,
+ key.bounds.height.max(key.size * 1.2),
+ );
+ buffer.set_text(
+ font_system,
+ key.content,
+ cosmic_text::Attrs::new()
+ .family(to_family(key.font.family))
+ .weight(to_weight(key.font.weight))
+ .stretch(to_stretch(key.font.stretch)),
+ to_shaping(key.shaping),
+ );
+
+ let bounds = measure(&buffer);
+
+ let _ = entry.insert(Entry { buffer, bounds });
+
+ for bounds in [
+ bounds,
+ Size {
+ width: key.bounds.width,
+ ..bounds
+ },
+ ] {
+ if key.bounds != bounds {
+ let _ = self.measurements.insert(
+ Key { bounds, ..key }.hash(self.hasher.build_hasher()),
+ hash,
+ );
+ }
+ }
+ }
+
+ let _ = self.recently_used.insert(hash);
+
+ (hash, self.entries.get_mut(&hash).unwrap())
+ }
+
+ fn trim(&mut self) {
+ if self.trim_count > Self::TRIM_INTERVAL {
+ self.entries
+ .retain(|key, _| self.recently_used.contains(key));
+ self.measurements
+ .retain(|_, value| self.recently_used.contains(value));
+
+ self.recently_used.clear();
+
+ self.trim_count = 0;
+ } else {
+ self.trim_count += 1;
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+struct Key<'a> {
+ content: &'a str,
+ size: f32,
+ line_height: f32,
+ font: Font,
+ bounds: Size,
+ shaping: Shaping,
+}
+
+impl Key<'_> {
+ fn hash<H: Hasher>(self, mut hasher: H) -> KeyHash {
+ self.content.hash(&mut hasher);
+ self.size.to_bits().hash(&mut hasher);
+ self.line_height.to_bits().hash(&mut hasher);
+ self.font.hash(&mut hasher);
+ self.bounds.width.to_bits().hash(&mut hasher);
+ self.bounds.height.to_bits().hash(&mut hasher);
+ self.shaping.hash(&mut hasher);
+
+ hasher.finish()
+ }
+}
+
+type KeyHash = u64;
diff --git a/tiny_skia/src/vector.rs b/tiny_skia/src/vector.rs
new file mode 100644
index 00000000..433ca0f5
--- /dev/null
+++ b/tiny_skia/src/vector.rs
@@ -0,0 +1,195 @@
+use crate::core::svg::{Data, Handle};
+use crate::core::{Color, Rectangle, Size};
+
+use resvg::usvg;
+use rustc_hash::{FxHashMap, FxHashSet};
+
+use std::cell::RefCell;
+use std::collections::hash_map;
+use std::fs;
+
+pub struct Pipeline {
+ cache: RefCell<Cache>,
+}
+
+impl Pipeline {
+ pub fn new() -> Self {
+ Self {
+ cache: RefCell::new(Cache::default()),
+ }
+ }
+
+ pub fn viewport_dimensions(&self, handle: &Handle) -> Size<u32> {
+ self.cache
+ .borrow_mut()
+ .viewport_dimensions(handle)
+ .unwrap_or(Size::new(0, 0))
+ }
+
+ pub fn draw(
+ &mut self,
+ handle: &Handle,
+ color: Option<Color>,
+ bounds: Rectangle,
+ pixels: &mut tiny_skia::PixmapMut<'_>,
+ clip_mask: Option<&tiny_skia::Mask>,
+ ) {
+ if let Some(image) = self.cache.borrow_mut().draw(
+ handle,
+ color,
+ Size::new(bounds.width as u32, bounds.height as u32),
+ ) {
+ pixels.draw_pixmap(
+ bounds.x as i32,
+ bounds.y as i32,
+ image,
+ &tiny_skia::PixmapPaint::default(),
+ tiny_skia::Transform::identity(),
+ clip_mask,
+ );
+ }
+ }
+
+ pub fn trim_cache(&mut self) {
+ self.cache.borrow_mut().trim();
+ }
+}
+
+#[derive(Default)]
+struct Cache {
+ trees: FxHashMap<u64, Option<resvg::usvg::Tree>>,
+ tree_hits: FxHashSet<u64>,
+ rasters: FxHashMap<RasterKey, tiny_skia::Pixmap>,
+ raster_hits: FxHashSet<RasterKey>,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+struct RasterKey {
+ id: u64,
+ color: Option<[u8; 4]>,
+ size: Size<u32>,
+}
+
+impl Cache {
+ fn load(&mut self, handle: &Handle) -> Option<&usvg::Tree> {
+ use usvg::TreeParsing;
+
+ let id = handle.id();
+
+ if let hash_map::Entry::Vacant(entry) = self.trees.entry(id) {
+ let svg = match handle.data() {
+ Data::Path(path) => {
+ fs::read_to_string(path).ok().and_then(|contents| {
+ usvg::Tree::from_str(
+ &contents,
+ &usvg::Options::default(),
+ )
+ .ok()
+ })
+ }
+ Data::Bytes(bytes) => {
+ usvg::Tree::from_data(bytes, &usvg::Options::default()).ok()
+ }
+ };
+
+ entry.insert(svg);
+ }
+
+ self.tree_hits.insert(id);
+ self.trees.get(&id).unwrap().as_ref()
+ }
+
+ fn viewport_dimensions(&mut self, handle: &Handle) -> Option<Size<u32>> {
+ let tree = self.load(handle)?;
+
+ Some(Size::new(
+ tree.size.width() as u32,
+ tree.size.height() as u32,
+ ))
+ }
+
+ fn draw(
+ &mut self,
+ handle: &Handle,
+ color: Option<Color>,
+ size: Size<u32>,
+ ) -> Option<tiny_skia::PixmapRef<'_>> {
+ if size.width == 0 || size.height == 0 {
+ return None;
+ }
+
+ let key = RasterKey {
+ id: handle.id(),
+ color: color.map(Color::into_rgba8),
+ size,
+ };
+
+ #[allow(clippy::map_entry)]
+ if !self.rasters.contains_key(&key) {
+ let tree = self.load(handle)?;
+
+ let mut image = tiny_skia::Pixmap::new(size.width, size.height)?;
+
+ let tree_size = tree.size.to_int_size();
+
+ let target_size = if size.width > size.height {
+ tree_size.scale_to_width(size.width)
+ } else {
+ tree_size.scale_to_height(size.height)
+ };
+
+ let transform = if let Some(target_size) = target_size {
+ let tree_size = tree_size.to_size();
+ let target_size = target_size.to_size();
+
+ tiny_skia::Transform::from_scale(
+ target_size.width() / tree_size.width(),
+ target_size.height() / tree_size.height(),
+ )
+ } else {
+ tiny_skia::Transform::default()
+ };
+
+ resvg::Tree::from_usvg(tree).render(transform, &mut image.as_mut());
+
+ if let Some([r, g, b, _]) = key.color {
+ // Apply color filter
+ for pixel in
+ bytemuck::cast_slice_mut::<u8, u32>(image.data_mut())
+ {
+ *pixel = bytemuck::cast(
+ tiny_skia::ColorU8::from_rgba(
+ b,
+ g,
+ r,
+ (*pixel >> 24) as u8,
+ )
+ .premultiply(),
+ );
+ }
+ } else {
+ // Swap R and B channels for `softbuffer` presentation
+ for pixel in
+ bytemuck::cast_slice_mut::<u8, u32>(image.data_mut())
+ {
+ *pixel = *pixel & 0xFF00FF00
+ | ((0x000000FF & *pixel) << 16)
+ | ((0x00FF0000 & *pixel) >> 16);
+ }
+ }
+
+ self.rasters.insert(key, image);
+ }
+
+ self.raster_hits.insert(key);
+ self.rasters.get(&key).map(tiny_skia::Pixmap::as_ref)
+ }
+
+ fn trim(&mut self) {
+ self.trees.retain(|key, _| self.tree_hits.contains(key));
+ self.rasters.retain(|key, _| self.raster_hits.contains(key));
+
+ self.tree_hits.clear();
+ self.raster_hits.clear();
+ }
+}
diff --git a/tiny_skia/src/window.rs b/tiny_skia/src/window.rs
new file mode 100644
index 00000000..d8d9378e
--- /dev/null
+++ b/tiny_skia/src/window.rs
@@ -0,0 +1,3 @@
+pub mod compositor;
+
+pub use compositor::{Compositor, Surface};
diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs
new file mode 100644
index 00000000..775cf9e5
--- /dev/null
+++ b/tiny_skia/src/window/compositor.rs
@@ -0,0 +1,228 @@
+use crate::core::{Color, Rectangle, Size};
+use crate::graphics::compositor::{self, Information};
+use crate::graphics::damage;
+use crate::graphics::{Error, Viewport};
+use crate::{Backend, Primitive, Renderer, Settings};
+
+use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle};
+use std::marker::PhantomData;
+
+pub struct Compositor<Theme> {
+ _theme: PhantomData<Theme>,
+}
+
+pub struct Surface {
+ window: softbuffer::GraphicsContext,
+ buffer: Vec<u32>,
+ clip_mask: tiny_skia::Mask,
+ primitives: Option<Vec<Primitive>>,
+ background_color: Color,
+}
+
+impl<Theme> crate::graphics::Compositor for Compositor<Theme> {
+ type Settings = Settings;
+ type Renderer = Renderer<Theme>;
+ type Surface = Surface;
+
+ fn new<W: HasRawWindowHandle + HasRawDisplayHandle>(
+ settings: Self::Settings,
+ _compatible_window: Option<&W>,
+ ) -> Result<(Self, Self::Renderer), Error> {
+ let (compositor, backend) = new(settings);
+
+ Ok((compositor, Renderer::new(backend)))
+ }
+
+ fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>(
+ &mut self,
+ window: &W,
+ width: u32,
+ height: u32,
+ ) -> Surface {
+ let window =
+ unsafe { softbuffer::GraphicsContext::new(window, window) }
+ .expect("Create softbuffer for window");
+
+ Surface {
+ window,
+ buffer: vec![0; width as usize * height as usize],
+ clip_mask: tiny_skia::Mask::new(width, height)
+ .expect("Create clip mask"),
+ primitives: None,
+ background_color: Color::BLACK,
+ }
+ }
+
+ fn configure_surface(
+ &mut self,
+ surface: &mut Surface,
+ width: u32,
+ height: u32,
+ ) {
+ surface.buffer.resize((width * height) as usize, 0);
+ surface.clip_mask =
+ tiny_skia::Mask::new(width, height).expect("Create clip mask");
+ surface.primitives = None;
+ }
+
+ fn fetch_information(&self) -> Information {
+ Information {
+ adapter: String::from("CPU"),
+ backend: String::from("tiny-skia"),
+ }
+ }
+
+ fn present<T: AsRef<str>>(
+ &mut self,
+ renderer: &mut Self::Renderer,
+ surface: &mut Self::Surface,
+ viewport: &Viewport,
+ background_color: Color,
+ overlay: &[T],
+ ) -> Result<(), compositor::SurfaceError> {
+ renderer.with_primitives(|backend, primitives| {
+ present(
+ backend,
+ surface,
+ primitives,
+ viewport,
+ background_color,
+ overlay,
+ )
+ })
+ }
+
+ fn screenshot<T: AsRef<str>>(
+ &mut self,
+ renderer: &mut Self::Renderer,
+ surface: &mut Self::Surface,
+ viewport: &Viewport,
+ background_color: Color,
+ overlay: &[T],
+ ) -> Vec<u8> {
+ renderer.with_primitives(|backend, primitives| {
+ screenshot(
+ surface,
+ backend,
+ primitives,
+ viewport,
+ background_color,
+ overlay,
+ )
+ })
+ }
+}
+
+pub fn new<Theme>(settings: Settings) -> (Compositor<Theme>, Backend) {
+ (
+ Compositor {
+ _theme: PhantomData,
+ },
+ Backend::new(settings),
+ )
+}
+
+pub fn present<T: AsRef<str>>(
+ backend: &mut Backend,
+ surface: &mut Surface,
+ primitives: &[Primitive],
+ viewport: &Viewport,
+ background_color: Color,
+ overlay: &[T],
+) -> Result<(), compositor::SurfaceError> {
+ let physical_size = viewport.physical_size();
+ let scale_factor = viewport.scale_factor() as f32;
+
+ let mut pixels = tiny_skia::PixmapMut::from_bytes(
+ bytemuck::cast_slice_mut(&mut surface.buffer),
+ physical_size.width,
+ physical_size.height,
+ )
+ .expect("Create pixel map");
+
+ let damage = surface
+ .primitives
+ .as_deref()
+ .and_then(|last_primitives| {
+ (surface.background_color == background_color)
+ .then(|| damage::list(last_primitives, primitives))
+ })
+ .unwrap_or_else(|| vec![Rectangle::with_size(viewport.logical_size())]);
+
+ if damage.is_empty() {
+ return Ok(());
+ }
+
+ surface.primitives = Some(primitives.to_vec());
+ surface.background_color = background_color;
+
+ let damage = damage::group(damage, scale_factor, physical_size);
+
+ backend.draw(
+ &mut pixels,
+ &mut surface.clip_mask,
+ primitives,
+ viewport,
+ &damage,
+ background_color,
+ overlay,
+ );
+
+ surface.window.set_buffer(
+ &surface.buffer,
+ physical_size.width as u16,
+ physical_size.height as u16,
+ );
+
+ Ok(())
+}
+
+pub fn screenshot<T: AsRef<str>>(
+ surface: &mut Surface,
+ backend: &mut Backend,
+ primitives: &[Primitive],
+ viewport: &Viewport,
+ background_color: Color,
+ overlay: &[T],
+) -> Vec<u8> {
+ let size = viewport.physical_size();
+
+ let mut offscreen_buffer: Vec<u32> =
+ vec![0; size.width as usize * size.height as usize];
+
+ backend.draw(
+ &mut tiny_skia::PixmapMut::from_bytes(
+ bytemuck::cast_slice_mut(&mut offscreen_buffer),
+ size.width,
+ size.height,
+ )
+ .expect("Create offscreen pixel map"),
+ &mut surface.clip_mask,
+ primitives,
+ viewport,
+ &[Rectangle::with_size(Size::new(
+ size.width as f32,
+ size.height as f32,
+ ))],
+ background_color,
+ overlay,
+ );
+
+ offscreen_buffer.iter().fold(
+ Vec::with_capacity(offscreen_buffer.len() * 4),
+ |mut acc, pixel| {
+ const A_MASK: u32 = 0xFF_00_00_00;
+ const R_MASK: u32 = 0x00_FF_00_00;
+ const G_MASK: u32 = 0x00_00_FF_00;
+ const B_MASK: u32 = 0x00_00_00_FF;
+
+ let a = ((A_MASK & pixel) >> 24) as u8;
+ let r = ((R_MASK & pixel) >> 16) as u8;
+ let g = ((G_MASK & pixel) >> 8) as u8;
+ let b = (B_MASK & pixel) as u8;
+
+ acc.extend([r, g, b, a]);
+ acc
+ },
+ )
+}
diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml
index f1e22cf6..22cfad55 100644
--- a/wgpu/Cargo.toml
+++ b/wgpu/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "iced_wgpu"
-version = "0.9.0"
+version = "0.10.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
description = "A wgpu renderer for Iced"
@@ -8,58 +8,59 @@ license = "MIT AND OFL-1.1"
repository = "https://github.com/iced-rs/iced"
[features]
-svg = ["iced_graphics/svg"]
+geometry = ["iced_graphics/geometry", "lyon"]
image = ["iced_graphics/image"]
-png = ["iced_graphics/png"]
-jpeg = ["iced_graphics/jpeg"]
-jpeg_rayon = ["iced_graphics/jpeg_rayon"]
-gif = ["iced_graphics/gif"]
-webp = ["iced_graphics/webp"]
-pnm = ["iced_graphics/pnm"]
-ico = ["iced_graphics/ico"]
-bmp = ["iced_graphics/bmp"]
-hdr = ["iced_graphics/hdr"]
-dds = ["iced_graphics/dds"]
-farbfeld = ["iced_graphics/farbfeld"]
-canvas = ["iced_graphics/canvas"]
-qr_code = ["iced_graphics/qr_code"]
-default_system_font = ["iced_graphics/font-source"]
-spirv = ["wgpu/spirv"]
-webgl = ["wgpu/webgl"]
+svg = ["resvg"]
+web-colors = ["iced_graphics/web-colors"]
[dependencies]
-wgpu = "0.14"
-wgpu_glyph = "0.18"
-glyph_brush = "0.7"
+wgpu = "0.16"
raw-window-handle = "0.5"
log = "0.4"
guillotiere = "0.6"
futures = "0.3"
bitflags = "1.2"
+once_cell = "1.0"
+rustc-hash = "1.1"
+
+[target.'cfg(target_arch = "wasm32")'.dependencies]
+wgpu = { version = "0.16", features = ["webgl"] }
+
+[dependencies.twox-hash]
+version = "1.6"
+default-features = false
+
+[target.'cfg(not(target_arch = "wasm32"))'.dependencies.twox-hash]
+version = "1.6.1"
+features = ["std"]
[dependencies.bytemuck]
version = "1.9"
features = ["derive"]
-[dependencies.iced_native]
-version = "0.9"
-path = "../native"
-
[dependencies.iced_graphics]
-version = "0.7"
+version = "0.8"
path = "../graphics"
-features = ["font-fallback", "font-icons"]
-[dependencies.tracing]
-version = "0.1.6"
+[dependencies.glyphon]
+version = "0.2"
+git = "https://github.com/hecrj/glyphon.git"
+rev = "8324f20158a62f8520bad4ed09f6aa5552f8f2a6"
+
+[dependencies.glam]
+version = "0.24"
+
+[dependencies.lyon]
+version = "1.0"
optional = true
-[dependencies.encase]
-version = "0.3.0"
-features = ["glam"]
+[dependencies.resvg]
+version = "0.35"
+optional = true
-[dependencies.glam]
-version = "0.21.3"
+[dependencies.tracing]
+version = "0.1.6"
+optional = true
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
diff --git a/wgpu/README.md b/wgpu/README.md
index 3e6af103..f8c88374 100644
--- a/wgpu/README.md
+++ b/wgpu/README.md
@@ -30,7 +30,7 @@ Currently, `iced_wgpu` supports the following primitives:
Add `iced_wgpu` as a dependency in your `Cargo.toml`:
```toml
-iced_wgpu = "0.9"
+iced_wgpu = "0.10"
```
__Iced moves fast and the `master` branch can contain breaking changes!__ If
diff --git a/wgpu/fonts/Iced-Icons.ttf b/wgpu/fonts/Iced-Icons.ttf
new file mode 100644
index 00000000..e3273141
--- /dev/null
+++ b/wgpu/fonts/Iced-Icons.ttf
Binary files differ
diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs
index 6a299425..4a0c54f0 100644
--- a/wgpu/src/backend.rs
+++ b/wgpu/src/backend.rs
@@ -1,14 +1,13 @@
+use crate::core;
+use crate::core::{Color, Font, Point, Size};
+use crate::graphics::backend;
+use crate::graphics::color;
+use crate::graphics::{Transformation, Viewport};
+use crate::primitive::{self, Primitive};
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::alignment;
-use iced_native::{Font, Size};
+use crate::{Layer, Settings};
#[cfg(feature = "tracing")]
use tracing::info_span;
@@ -16,11 +15,13 @@ use tracing::info_span;
#[cfg(any(feature = "image", feature = "svg"))]
use crate::image;
+use std::borrow::Cow;
+
/// A [`wgpu`] graphics backend for [`iced`].
///
/// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs
/// [`iced`]: https://github.com/iced-rs/iced
-#[derive(Debug)]
+#[allow(missing_debug_implementations)]
pub struct Backend {
quad_pipeline: quad::Pipeline,
text_pipeline: text::Pipeline,
@@ -29,6 +30,7 @@ pub struct Backend {
#[cfg(any(feature = "image", feature = "svg"))]
image_pipeline: image::Pipeline,
+ default_font: Font,
default_text_size: f32,
}
@@ -36,16 +38,11 @@ impl Backend {
/// Creates a new [`Backend`].
pub fn new(
device: &wgpu::Device,
+ queue: &wgpu::Queue,
settings: Settings,
format: wgpu::TextureFormat,
) -> Self {
- let text_pipeline = text::Pipeline::new(
- device,
- format,
- settings.default_font,
- settings.text_multithreading,
- );
-
+ let text_pipeline = text::Pipeline::new(device, queue, format);
let quad_pipeline = quad::Pipeline::new(device, format);
let triangle_pipeline =
triangle::Pipeline::new(device, format, settings.antialiasing);
@@ -61,6 +58,7 @@ impl Backend {
#[cfg(any(feature = "image", feature = "svg"))]
image_pipeline,
+ default_font: settings.default_font,
default_text_size: settings.default_text_size,
}
}
@@ -72,8 +70,9 @@ impl Backend {
pub fn present<T: AsRef<str>>(
&mut self,
device: &wgpu::Device,
- staging_belt: &mut wgpu::util::StagingBelt,
+ queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
+ clear_color: Option<Color>,
frame: &wgpu::TextureView,
primitives: &[Primitive],
viewport: &Viewport,
@@ -90,180 +89,268 @@ impl Backend {
let mut layers = Layer::generate(primitives, viewport);
layers.push(Layer::overlay(overlay_text, viewport));
+ self.prepare(
+ device,
+ queue,
+ encoder,
+ scale_factor,
+ transformation,
+ &layers,
+ );
+
+ while !self.prepare_text(
+ device,
+ queue,
+ scale_factor,
+ target_size,
+ &layers,
+ ) {}
+
+ self.render(
+ device,
+ encoder,
+ frame,
+ clear_color,
+ scale_factor,
+ target_size,
+ &layers,
+ );
+
+ self.quad_pipeline.end_frame();
+ self.text_pipeline.end_frame();
+ self.triangle_pipeline.end_frame();
+
+ #[cfg(any(feature = "image", feature = "svg"))]
+ self.image_pipeline.end_frame();
+ }
+
+ fn prepare_text(
+ &mut self,
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ scale_factor: f32,
+ target_size: Size<u32>,
+ layers: &[Layer<'_>],
+ ) -> bool {
for layer in layers {
- self.flush(
- device,
- scale_factor,
- transformation,
- &layer,
- staging_belt,
- encoder,
- frame,
- target_size,
- );
+ let bounds = (layer.bounds * scale_factor).snap();
+
+ if bounds.width < 1 || bounds.height < 1 {
+ continue;
+ }
+
+ if !layer.text.is_empty()
+ && !self.text_pipeline.prepare(
+ device,
+ queue,
+ &layer.text,
+ layer.bounds,
+ scale_factor,
+ target_size,
+ )
+ {
+ return false;
+ }
}
- #[cfg(any(feature = "image", feature = "svg"))]
- self.image_pipeline.trim_cache(device, encoder);
+ true
}
- fn flush(
+ fn prepare(
&mut self,
device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ _encoder: &mut wgpu::CommandEncoder,
scale_factor: f32,
transformation: Transformation,
- layer: &Layer<'_>,
- staging_belt: &mut wgpu::util::StagingBelt,
+ layers: &[Layer<'_>],
+ ) {
+ for layer in layers {
+ let bounds = (layer.bounds * scale_factor).snap();
+
+ if bounds.width < 1 || bounds.height < 1 {
+ continue;
+ }
+
+ if !layer.quads.is_empty() {
+ self.quad_pipeline.prepare(
+ device,
+ queue,
+ &layer.quads,
+ transformation,
+ scale_factor,
+ );
+ }
+
+ if !layer.meshes.is_empty() {
+ let scaled = transformation
+ * Transformation::scale(scale_factor, scale_factor);
+
+ self.triangle_pipeline.prepare(
+ device,
+ queue,
+ &layer.meshes,
+ scaled,
+ );
+ }
+
+ #[cfg(any(feature = "image", feature = "svg"))]
+ {
+ if !layer.images.is_empty() {
+ let scaled = transformation
+ * Transformation::scale(scale_factor, scale_factor);
+
+ self.image_pipeline.prepare(
+ device,
+ queue,
+ _encoder,
+ &layer.images,
+ scaled,
+ scale_factor,
+ );
+ }
+ }
+ }
+ }
+
+ fn render(
+ &mut self,
+ device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
+ clear_color: Option<Color>,
+ scale_factor: f32,
target_size: Size<u32>,
+ layers: &[Layer<'_>],
) {
- let bounds = (layer.bounds * scale_factor).snap();
+ use std::mem::ManuallyDrop;
- if bounds.width < 1 || bounds.height < 1 {
- return;
- }
+ let mut quad_layer = 0;
+ let mut triangle_layer = 0;
+ #[cfg(any(feature = "image", feature = "svg"))]
+ let mut image_layer = 0;
+ let mut text_layer = 0;
+
+ let mut render_pass = ManuallyDrop::new(encoder.begin_render_pass(
+ &wgpu::RenderPassDescriptor {
+ label: Some("iced_wgpu::quad render pass"),
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+ view: target,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: match clear_color {
+ Some(background_color) => wgpu::LoadOp::Clear({
+ let [r, g, b, a] =
+ color::pack(background_color).components();
+
+ wgpu::Color {
+ r: f64::from(r),
+ g: f64::from(g),
+ b: f64::from(b),
+ a: f64::from(a),
+ }
+ }),
+ None => wgpu::LoadOp::Load,
+ },
+ store: true,
+ },
+ })],
+ depth_stencil_attachment: None,
+ },
+ ));
- if !layer.quads.is_empty() {
- self.quad_pipeline.draw(
- device,
- staging_belt,
- encoder,
- &layer.quads,
- transformation,
- scale_factor,
- bounds,
- target,
- );
- }
+ for layer in layers {
+ let bounds = (layer.bounds * scale_factor).snap();
- if !layer.meshes.is_empty() {
- let scaled = transformation
- * Transformation::scale(scale_factor, scale_factor);
-
- self.triangle_pipeline.draw(
- device,
- staging_belt,
- encoder,
- target,
- target_size,
- scaled,
- scale_factor,
- &layer.meshes,
- );
- }
+ if bounds.width < 1 || bounds.height < 1 {
+ return;
+ }
- #[cfg(any(feature = "image", feature = "svg"))]
- {
- if !layer.images.is_empty() {
- let scaled = transformation
- * Transformation::scale(scale_factor, scale_factor);
+ if !layer.quads.is_empty() {
+ self.quad_pipeline.render(
+ quad_layer,
+ bounds,
+ &layer.quads,
+ &mut render_pass,
+ );
+
+ quad_layer += 1;
+ }
- self.image_pipeline.draw(
+ if !layer.meshes.is_empty() {
+ let _ = ManuallyDrop::into_inner(render_pass);
+
+ self.triangle_pipeline.render(
device,
- staging_belt,
encoder,
- &layer.images,
- scaled,
- bounds,
target,
+ triangle_layer,
+ target_size,
+ &layer.meshes,
scale_factor,
);
+
+ triangle_layer += 1;
+
+ render_pass = ManuallyDrop::new(encoder.begin_render_pass(
+ &wgpu::RenderPassDescriptor {
+ label: Some("iced_wgpu::quad render pass"),
+ color_attachments: &[Some(
+ wgpu::RenderPassColorAttachment {
+ view: target,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Load,
+ store: true,
+ },
+ },
+ )],
+ depth_stencil_attachment: None,
+ },
+ ));
}
- }
- 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 {
- alignment::Horizontal::Left => {
- wgpu_glyph::HorizontalAlign::Left
- }
- alignment::Horizontal::Center => {
- wgpu_glyph::HorizontalAlign::Center
- }
- alignment::Horizontal::Right => {
- wgpu_glyph::HorizontalAlign::Right
- }
- })
- .v_align(match text.vertical_alignment {
- alignment::Vertical::Top => {
- wgpu_glyph::VerticalAlign::Top
- }
- alignment::Vertical::Center => {
- wgpu_glyph::VerticalAlign::Center
- }
- alignment::Vertical::Bottom => {
- wgpu_glyph::VerticalAlign::Bottom
- }
- }),
- };
-
- self.text_pipeline.queue(text);
+ #[cfg(any(feature = "image", feature = "svg"))]
+ {
+ if !layer.images.is_empty() {
+ self.image_pipeline.render(
+ image_layer,
+ bounds,
+ &mut render_pass,
+ );
+
+ image_layer += 1;
+ }
}
- 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,
- },
- );
+ if !layer.text.is_empty() {
+ self.text_pipeline
+ .render(text_layer, bounds, &mut render_pass);
+
+ text_layer += 1;
+ }
}
+
+ let _ = ManuallyDrop::into_inner(render_pass);
}
}
-impl iced_graphics::Backend for Backend {
+impl crate::graphics::Backend for Backend {
+ type Primitive = primitive::Custom;
+
fn trim_measurements(&mut self) {
- self.text_pipeline.trim_measurement_cache()
+ self.text_pipeline.trim_measurements();
}
}
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;
+ const ICON_FONT: Font = Font::with_name("Iced-Icons");
+ const CHECKMARK_ICON: char = '\u{f00c}';
+ const ARROW_DOWN_ICON: char = '\u{e800}';
+
+ fn default_font(&self) -> Font {
+ self.default_font
+ }
fn default_size(&self) -> f32 {
self.default_text_size
@@ -273,45 +360,59 @@ impl backend::Text for Backend {
&self,
contents: &str,
size: f32,
+ line_height: core::text::LineHeight,
font: Font,
bounds: Size,
- ) -> (f32, f32) {
- self.text_pipeline.measure(contents, size, font, bounds)
+ shaping: core::text::Shaping,
+ ) -> Size {
+ self.text_pipeline.measure(
+ contents,
+ size,
+ line_height,
+ font,
+ bounds,
+ shaping,
+ )
}
fn hit_test(
&self,
contents: &str,
size: f32,
+ line_height: core::text::LineHeight,
font: Font,
bounds: Size,
- point: iced_native::Point,
+ shaping: core::text::Shaping,
+ point: Point,
nearest_only: bool,
- ) -> Option<text::Hit> {
+ ) -> Option<core::text::Hit> {
self.text_pipeline.hit_test(
contents,
size,
+ line_height,
font,
bounds,
+ shaping,
point,
nearest_only,
)
}
+
+ fn load_font(&mut self, font: Cow<'static, [u8]>) {
+ self.text_pipeline.load_font(font);
+ }
}
#[cfg(feature = "image")]
impl backend::Image for Backend {
- fn dimensions(&self, handle: &iced_native::image::Handle) -> Size<u32> {
+ fn dimensions(&self, handle: &core::image::Handle) -> Size<u32> {
self.image_pipeline.dimensions(handle)
}
}
#[cfg(feature = "svg")]
impl backend::Svg for Backend {
- fn viewport_dimensions(
- &self,
- handle: &iced_native::svg::Handle,
- ) -> Size<u32> {
+ fn viewport_dimensions(&self, handle: &core::svg::Handle) -> Size<u32> {
self.image_pipeline.viewport_dimensions(handle)
}
}
diff --git a/wgpu/src/buffer.rs b/wgpu/src/buffer.rs
index 7c092d0b..94122187 100644
--- a/wgpu/src/buffer.rs
+++ b/wgpu/src/buffer.rs
@@ -1,3 +1,106 @@
-//! Utilities for buffer operations.
-pub mod dynamic;
-pub mod r#static;
+use std::marker::PhantomData;
+use std::ops::RangeBounds;
+
+#[derive(Debug)]
+pub struct Buffer<T> {
+ label: &'static str,
+ size: u64,
+ usage: wgpu::BufferUsages,
+ pub(crate) raw: wgpu::Buffer,
+ offsets: Vec<wgpu::BufferAddress>,
+ type_: PhantomData<T>,
+}
+
+impl<T: bytemuck::Pod> Buffer<T> {
+ pub fn new(
+ device: &wgpu::Device,
+ label: &'static str,
+ amount: usize,
+ usage: wgpu::BufferUsages,
+ ) -> Self {
+ let size = next_copy_size::<T>(amount);
+
+ let raw = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some(label),
+ size,
+ usage,
+ mapped_at_creation: false,
+ });
+
+ Self {
+ label,
+ size,
+ usage,
+ raw,
+ offsets: Vec::new(),
+ type_: PhantomData,
+ }
+ }
+
+ pub fn resize(&mut self, device: &wgpu::Device, new_count: usize) -> bool {
+ let new_size = (std::mem::size_of::<T>() * new_count) as u64;
+
+ if self.size < new_size {
+ self.offsets.clear();
+
+ self.raw = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some(self.label),
+ size: new_size,
+ usage: self.usage,
+ mapped_at_creation: false,
+ });
+
+ self.size = new_size;
+
+ true
+ } else {
+ false
+ }
+ }
+
+ /// Returns the size of the written bytes.
+ pub fn write(
+ &mut self,
+ queue: &wgpu::Queue,
+ offset: usize,
+ contents: &[T],
+ ) -> usize {
+ let bytes: &[u8] = bytemuck::cast_slice(contents);
+ queue.write_buffer(&self.raw, offset as u64, bytes);
+
+ self.offsets.push(offset as u64);
+
+ bytes.len()
+ }
+
+ pub fn slice(
+ &self,
+ bounds: impl RangeBounds<wgpu::BufferAddress>,
+ ) -> wgpu::BufferSlice<'_> {
+ self.raw.slice(bounds)
+ }
+
+ /// Returns the slice calculated from the offset stored at the given index.
+ pub fn slice_from_index(&self, index: usize) -> wgpu::BufferSlice<'_> {
+ self.raw.slice(self.offset_at(index)..)
+ }
+
+ /// Clears any temporary data (i.e. offsets) from the buffer.
+ pub fn clear(&mut self) {
+ self.offsets.clear()
+ }
+
+ /// Returns the offset at `index`, if it exists.
+ fn offset_at(&self, index: usize) -> &wgpu::BufferAddress {
+ self.offsets.get(index).expect("No offset at index.")
+ }
+}
+
+fn next_copy_size<T>(amount: usize) -> u64 {
+ let align_mask = wgpu::COPY_BUFFER_ALIGNMENT - 1;
+
+ (((std::mem::size_of::<T>() * amount).next_power_of_two() as u64
+ + align_mask)
+ & !align_mask)
+ .max(wgpu::COPY_BUFFER_ALIGNMENT)
+}
diff --git a/wgpu/src/buffer/dynamic.rs b/wgpu/src/buffer/dynamic.rs
deleted file mode 100644
index 88289b98..00000000
--- a/wgpu/src/buffer/dynamic.rs
+++ /dev/null
@@ -1,219 +0,0 @@
-//! Utilities for uniform buffer operations.
-use encase::private::WriteInto;
-use encase::ShaderType;
-
-use std::fmt;
-use std::marker::PhantomData;
-
-/// A dynamic buffer is any type of buffer which does not have a static offset.
-#[derive(Debug)]
-pub struct Buffer<T: ShaderType> {
- offsets: Vec<wgpu::DynamicOffset>,
- cpu: Internal,
- gpu: wgpu::Buffer,
- label: &'static str,
- size: u64,
- _data: PhantomData<T>,
-}
-
-impl<T: ShaderType + WriteInto> Buffer<T> {
- /// Creates a new dynamic uniform buffer.
- pub fn uniform(device: &wgpu::Device, label: &'static str) -> Self {
- Buffer::new(
- device,
- Internal::Uniform(encase::DynamicUniformBuffer::new(Vec::new())),
- label,
- wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
- )
- }
-
- #[cfg(not(target_arch = "wasm32"))]
- /// Creates a new dynamic storage buffer.
- pub fn storage(device: &wgpu::Device, label: &'static str) -> Self {
- Buffer::new(
- device,
- Internal::Storage(encase::DynamicStorageBuffer::new(Vec::new())),
- label,
- wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
- )
- }
-
- fn new(
- device: &wgpu::Device,
- dynamic_buffer_type: Internal,
- label: &'static str,
- usage: wgpu::BufferUsages,
- ) -> Self {
- let initial_size = u64::from(T::min_size());
-
- Self {
- offsets: Vec::new(),
- cpu: dynamic_buffer_type,
- gpu: Buffer::<T>::create_gpu_buffer(
- device,
- label,
- usage,
- initial_size,
- ),
- label,
- size: initial_size,
- _data: Default::default(),
- }
- }
-
- fn create_gpu_buffer(
- device: &wgpu::Device,
- label: &'static str,
- usage: wgpu::BufferUsages,
- size: u64,
- ) -> wgpu::Buffer {
- device.create_buffer(&wgpu::BufferDescriptor {
- label: Some(label),
- size,
- usage,
- mapped_at_creation: false,
- })
- }
-
- /// Write a new value to the CPU buffer with proper alignment. Stores the returned offset value
- /// in the buffer for future use.
- pub fn push(&mut self, value: &T) {
- //this write operation on the cpu buffer will adjust for uniform alignment requirements
- let offset = self.cpu.write(value);
- self.offsets.push(offset);
- }
-
- /// Resize buffer contents if necessary. This will re-create the GPU buffer if current size is
- /// less than the newly computed size from the CPU buffer.
- ///
- /// If the gpu buffer is resized, its bind group will need to be recreated!
- pub fn resize(&mut self, device: &wgpu::Device) -> bool {
- let new_size = self.cpu.get_ref().len() as u64;
-
- if self.size < new_size {
- let usages = match self.cpu {
- Internal::Uniform(_) => {
- wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST
- }
- #[cfg(not(target_arch = "wasm32"))]
- Internal::Storage(_) => {
- wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST
- }
- };
-
- self.gpu = Buffer::<T>::create_gpu_buffer(
- device, self.label, usages, new_size,
- );
- self.size = new_size;
- true
- } else {
- false
- }
- }
-
- /// Write the contents of this dynamic buffer to the GPU via staging belt command.
- pub fn write(
- &mut self,
- device: &wgpu::Device,
- staging_belt: &mut wgpu::util::StagingBelt,
- encoder: &mut wgpu::CommandEncoder,
- ) {
- let size = self.cpu.get_ref().len();
-
- if let Some(buffer_size) = wgpu::BufferSize::new(size as u64) {
- let mut buffer = staging_belt.write_buffer(
- encoder,
- &self.gpu,
- 0,
- buffer_size,
- device,
- );
-
- buffer.copy_from_slice(self.cpu.get_ref());
- }
- }
-
- // Gets the aligned offset at the given index from the CPU buffer.
- pub fn offset_at_index(&self, index: usize) -> wgpu::DynamicOffset {
- let offset = self
- .offsets
- .get(index)
- .copied()
- .expect("Index not found in offsets.");
-
- offset
- }
-
- /// Returns a reference to the GPU buffer.
- pub fn raw(&self) -> &wgpu::Buffer {
- &self.gpu
- }
-
- /// Reset the buffer.
- pub fn clear(&mut self) {
- self.offsets.clear();
- self.cpu.clear();
- }
-}
-
-// Currently supported dynamic buffers.
-enum Internal {
- Uniform(encase::DynamicUniformBuffer<Vec<u8>>),
- #[cfg(not(target_arch = "wasm32"))]
- //storage buffers are not supported on wgpu wasm target (yet)
- Storage(encase::DynamicStorageBuffer<Vec<u8>>),
-}
-
-impl Internal {
- /// Writes the current value to its CPU buffer with proper alignment.
- pub(super) fn write<T: ShaderType + WriteInto>(
- &mut self,
- value: &T,
- ) -> wgpu::DynamicOffset {
- match self {
- Internal::Uniform(buf) => buf
- .write(value)
- .expect("Error when writing to dynamic uniform buffer.")
- as u32,
- #[cfg(not(target_arch = "wasm32"))]
- Internal::Storage(buf) => buf
- .write(value)
- .expect("Error when writing to dynamic storage buffer.")
- as u32,
- }
- }
-
- /// Returns bytearray of aligned CPU buffer.
- pub(super) fn get_ref(&self) -> &Vec<u8> {
- match self {
- Internal::Uniform(buf) => buf.as_ref(),
- #[cfg(not(target_arch = "wasm32"))]
- Internal::Storage(buf) => buf.as_ref(),
- }
- }
-
- /// Resets the CPU buffer.
- pub(super) fn clear(&mut self) {
- match self {
- Internal::Uniform(buf) => {
- buf.as_mut().clear();
- buf.set_offset(0);
- }
- #[cfg(not(target_arch = "wasm32"))]
- Internal::Storage(buf) => {
- buf.as_mut().clear();
- buf.set_offset(0);
- }
- }
- }
-}
-
-impl fmt::Debug for Internal {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::Uniform(_) => write!(f, "Internal::Uniform(_)"),
- #[cfg(not(target_arch = "wasm32"))]
- Self::Storage(_) => write!(f, "Internal::Storage(_)"),
- }
- }
-}
diff --git a/wgpu/src/buffer/static.rs b/wgpu/src/buffer/static.rs
deleted file mode 100644
index ef87422f..00000000
--- a/wgpu/src/buffer/static.rs
+++ /dev/null
@@ -1,117 +0,0 @@
-use bytemuck::{Pod, Zeroable};
-use std::marker::PhantomData;
-use std::mem;
-
-//128 triangles/indices
-const DEFAULT_STATIC_BUFFER_COUNT: wgpu::BufferAddress = 128;
-
-/// A generic buffer struct useful for items which have no alignment requirements
-/// (e.g. Vertex, Index buffers) & no dynamic offsets.
-#[derive(Debug)]
-pub struct Buffer<T> {
- //stored sequentially per mesh iteration; refers to the offset index in the GPU buffer
- offsets: Vec<wgpu::BufferAddress>,
- label: &'static str,
- usages: wgpu::BufferUsages,
- gpu: wgpu::Buffer,
- size: wgpu::BufferAddress,
- _data: PhantomData<T>,
-}
-
-impl<T: Pod + Zeroable> Buffer<T> {
- /// Initialize a new static buffer.
- pub fn new(
- device: &wgpu::Device,
- label: &'static str,
- usages: wgpu::BufferUsages,
- ) -> Self {
- let size = (mem::size_of::<T>() as u64) * DEFAULT_STATIC_BUFFER_COUNT;
-
- Self {
- offsets: Vec::new(),
- label,
- usages,
- gpu: Self::gpu_buffer(device, label, size, usages),
- size,
- _data: PhantomData,
- }
- }
-
- fn gpu_buffer(
- device: &wgpu::Device,
- label: &'static str,
- size: wgpu::BufferAddress,
- usage: wgpu::BufferUsages,
- ) -> wgpu::Buffer {
- device.create_buffer(&wgpu::BufferDescriptor {
- label: Some(label),
- size,
- usage,
- mapped_at_creation: false,
- })
- }
-
- /// Returns whether or not the buffer needs to be recreated. This can happen whenever mesh data
- /// changes & a redraw is requested.
- pub fn resize(&mut self, device: &wgpu::Device, new_count: usize) -> bool {
- let size = (mem::size_of::<T>() * new_count) as u64;
-
- if self.size < size {
- self.offsets.clear();
- self.size = size;
- self.gpu = Self::gpu_buffer(device, self.label, size, self.usages);
- true
- } else {
- false
- }
- }
-
- /// Writes the current vertex data to the gpu buffer with a memcpy & stores its offset.
- ///
- /// Returns the size of the written bytes.
- pub fn write(
- &mut self,
- device: &wgpu::Device,
- staging_belt: &mut wgpu::util::StagingBelt,
- encoder: &mut wgpu::CommandEncoder,
- offset: u64,
- content: &[T],
- ) -> u64 {
- let bytes = bytemuck::cast_slice(content);
- let bytes_size = bytes.len() as u64;
-
- if let Some(buffer_size) = wgpu::BufferSize::new(bytes_size) {
- let mut buffer = staging_belt.write_buffer(
- encoder,
- &self.gpu,
- offset,
- buffer_size,
- device,
- );
-
- buffer.copy_from_slice(bytes);
-
- self.offsets.push(offset);
- }
-
- bytes_size
- }
-
- fn offset_at(&self, index: usize) -> &wgpu::BufferAddress {
- self.offsets
- .get(index)
- .expect("Offset at index does not exist.")
- }
-
- /// Returns the slice calculated from the offset stored at the given index.
- /// e.g. to calculate the slice for the 2nd mesh in the layer, this would be the offset at index
- /// 1 that we stored earlier when writing.
- pub fn slice_from_index(&self, index: usize) -> wgpu::BufferSlice<'_> {
- self.gpu.slice(self.offset_at(index)..)
- }
-
- /// Clears any temporary data from the buffer.
- pub fn clear(&mut self) {
- self.offsets.clear()
- }
-}
diff --git a/wgpu/src/color.rs b/wgpu/src/color.rs
new file mode 100644
index 00000000..a1025601
--- /dev/null
+++ b/wgpu/src/color.rs
@@ -0,0 +1,165 @@
+use std::borrow::Cow;
+
+pub fn convert(
+ device: &wgpu::Device,
+ encoder: &mut wgpu::CommandEncoder,
+ source: wgpu::Texture,
+ format: wgpu::TextureFormat,
+) -> wgpu::Texture {
+ if source.format() == format {
+ return source;
+ }
+
+ let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
+ label: Some("iced_wgpu.offscreen.sampler"),
+ ..Default::default()
+ });
+
+ //sampler in 0
+ let sampler_layout =
+ device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("iced_wgpu.offscreen.blit.sampler_layout"),
+ entries: &[wgpu::BindGroupLayoutEntry {
+ binding: 0,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Sampler(
+ wgpu::SamplerBindingType::NonFiltering,
+ ),
+ count: None,
+ }],
+ });
+
+ let sampler_bind_group =
+ device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("iced_wgpu.offscreen.sampler.bind_group"),
+ layout: &sampler_layout,
+ entries: &[wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::Sampler(&sampler),
+ }],
+ });
+
+ let texture_layout =
+ device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("iced_wgpu.offscreen.blit.texture_layout"),
+ entries: &[wgpu::BindGroupLayoutEntry {
+ binding: 0,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Texture {
+ sample_type: wgpu::TextureSampleType::Float {
+ filterable: false,
+ },
+ view_dimension: wgpu::TextureViewDimension::D2,
+ multisampled: false,
+ },
+ count: None,
+ }],
+ });
+
+ let pipeline_layout =
+ device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("iced_wgpu.offscreen.blit.pipeline_layout"),
+ bind_group_layouts: &[&sampler_layout, &texture_layout],
+ push_constant_ranges: &[],
+ });
+
+ let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("iced_wgpu.offscreen.blit.shader"),
+ source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
+ "shader/blit.wgsl"
+ ))),
+ });
+
+ let pipeline =
+ device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: Some("iced_wgpu.offscreen.blit.pipeline"),
+ layout: Some(&pipeline_layout),
+ vertex: wgpu::VertexState {
+ module: &shader,
+ entry_point: "vs_main",
+ buffers: &[],
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: &shader,
+ entry_point: "fs_main",
+ targets: &[Some(wgpu::ColorTargetState {
+ format,
+ blend: Some(wgpu::BlendState {
+ color: wgpu::BlendComponent {
+ src_factor: wgpu::BlendFactor::SrcAlpha,
+ dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
+ operation: wgpu::BlendOperation::Add,
+ },
+ alpha: wgpu::BlendComponent {
+ src_factor: wgpu::BlendFactor::One,
+ dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
+ operation: wgpu::BlendOperation::Add,
+ },
+ }),
+ write_mask: wgpu::ColorWrites::ALL,
+ })],
+ }),
+ primitive: wgpu::PrimitiveState {
+ topology: wgpu::PrimitiveTopology::TriangleList,
+ front_face: wgpu::FrontFace::Cw,
+ ..Default::default()
+ },
+ depth_stencil: None,
+ multisample: Default::default(),
+ multiview: None,
+ });
+
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
+ label: Some("iced_wgpu.offscreen.conversion.source_texture"),
+ size: source.size(),
+ mip_level_count: 1,
+ sample_count: 1,
+ dimension: wgpu::TextureDimension::D2,
+ format,
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT
+ | wgpu::TextureUsages::COPY_SRC,
+ view_formats: &[],
+ });
+
+ let view = &texture.create_view(&wgpu::TextureViewDescriptor::default());
+
+ let texture_bind_group =
+ device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("iced_wgpu.offscreen.blit.texture_bind_group"),
+ layout: &texture_layout,
+ entries: &[wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::TextureView(
+ &source
+ .create_view(&wgpu::TextureViewDescriptor::default()),
+ ),
+ }],
+ });
+
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some("iced_wgpu.offscreen.blit.render_pass"),
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+ view,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Load,
+ store: true,
+ },
+ })],
+ depth_stencil_attachment: None,
+ });
+
+ pass.set_pipeline(&pipeline);
+ pass.set_bind_group(0, &sampler_bind_group, &[]);
+ pass.set_bind_group(1, &texture_bind_group, &[]);
+ pass.draw(0..6, 0..1);
+
+ texture
+}
+
+#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
+#[repr(C)]
+struct Vertex {
+ ndc: [f32; 2],
+ uv: [f32; 2],
+}
diff --git a/graphics/src/widget/canvas/frame.rs b/wgpu/src/geometry.rs
index d68548ae..e421e0b0 100644
--- a/graphics/src/widget/canvas/frame.rs
+++ b/wgpu/src/geometry.rs
@@ -1,17 +1,19 @@
-use crate::gradient::Gradient;
-use crate::triangle;
-use crate::widget::canvas::{path, Fill, Geometry, Path, Stroke, Style, Text};
-use crate::Primitive;
-
-use iced_native::{Point, Rectangle, Size, Vector};
+//! Build and draw geometry.
+use crate::core::{Point, Rectangle, Size, Vector};
+use crate::graphics::color;
+use crate::graphics::geometry::fill::{self, Fill};
+use crate::graphics::geometry::{
+ LineCap, LineDash, LineJoin, Path, Stroke, Style, Text,
+};
+use crate::graphics::gradient::{self, Gradient};
+use crate::graphics::mesh::{self, Mesh};
+use crate::primitive::{self, Primitive};
use lyon::geom::euclid;
use lyon::tessellation;
use std::borrow::Cow;
-/// The frame of a [`Canvas`].
-///
-/// [`Canvas`]: crate::widget::Canvas
+/// A frame for drawing some geometry.
#[allow(missing_debug_implementations)]
pub struct Frame {
size: Size,
@@ -23,11 +25,8 @@ pub struct Frame {
}
enum Buffer {
- Solid(tessellation::VertexBuffers<triangle::ColoredVertex2D, u32>),
- Gradient(
- tessellation::VertexBuffers<triangle::Vertex2D, u32>,
- Gradient,
- ),
+ Solid(tessellation::VertexBuffers<mesh::SolidVertex2D, u32>),
+ Gradient(tessellation::VertexBuffers<mesh::GradientVertex2D, u32>),
}
struct BufferStack {
@@ -49,12 +48,11 @@ impl BufferStack {
));
}
},
- Style::Gradient(gradient) => match self.stack.last() {
- Some(Buffer::Gradient(_, last)) if gradient == last => {}
+ Style::Gradient(_) => match self.stack.last() {
+ Some(Buffer::Gradient(_)) => {}
_ => {
self.stack.push(Buffer::Gradient(
tessellation::VertexBuffers::new(),
- gradient.clone(),
));
}
},
@@ -71,12 +69,17 @@ impl BufferStack {
(Style::Solid(color), Buffer::Solid(buffer)) => {
Box::new(tessellation::BuffersBuilder::new(
buffer,
- TriangleVertex2DBuilder(color.into_linear()),
+ TriangleVertex2DBuilder(color::pack(*color)),
+ ))
+ }
+ (Style::Gradient(gradient), Buffer::Gradient(buffer)) => {
+ Box::new(tessellation::BuffersBuilder::new(
+ buffer,
+ GradientVertex2DBuilder {
+ gradient: gradient.pack(),
+ },
))
}
- (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new(
- tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder),
- ),
_ => unreachable!(),
}
}
@@ -89,12 +92,17 @@ impl BufferStack {
(Style::Solid(color), Buffer::Solid(buffer)) => {
Box::new(tessellation::BuffersBuilder::new(
buffer,
- TriangleVertex2DBuilder(color.into_linear()),
+ TriangleVertex2DBuilder(color::pack(*color)),
+ ))
+ }
+ (Style::Gradient(gradient), Buffer::Gradient(buffer)) => {
+ Box::new(tessellation::BuffersBuilder::new(
+ buffer,
+ GradientVertex2DBuilder {
+ gradient: gradient.pack(),
+ },
))
}
- (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new(
- tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder),
- ),
_ => unreachable!(),
}
}
@@ -132,11 +140,13 @@ impl Transform {
}
fn transform_gradient(&self, mut gradient: Gradient) -> Gradient {
- let (start, end) = match &mut gradient {
- Gradient::Linear(linear) => (&mut linear.start, &mut linear.end),
- };
- self.transform_point(start);
- self.transform_point(end);
+ match &mut gradient {
+ Gradient::Linear(linear) => {
+ self.transform_point(&mut linear.start);
+ self.transform_point(&mut linear.end);
+ }
+ }
+
gradient
}
}
@@ -196,8 +206,8 @@ impl Frame {
.buffers
.get_fill(&self.transforms.current.transform_style(style));
- let options =
- tessellation::FillOptions::default().with_fill_rule(rule.into());
+ let options = tessellation::FillOptions::default()
+ .with_fill_rule(into_fill_rule(rule));
if self.transforms.current.is_identity {
self.fill_tessellator.tessellate_path(
@@ -206,7 +216,7 @@ impl Frame {
buffer.as_mut(),
)
} else {
- let path = path.transformed(&self.transforms.current.raw);
+ let path = path.transform(&self.transforms.current.raw);
self.fill_tessellator.tessellate_path(
path.raw(),
@@ -241,8 +251,8 @@ impl Frame {
lyon::math::Vector::new(size.width, size.height),
);
- let options =
- tessellation::FillOptions::default().with_fill_rule(rule.into());
+ let options = tessellation::FillOptions::default()
+ .with_fill_rule(into_fill_rule(rule));
self.fill_tessellator
.tessellate_rectangle(
@@ -264,14 +274,14 @@ impl Frame {
let mut options = tessellation::StrokeOptions::default();
options.line_width = stroke.width;
- options.start_cap = stroke.line_cap.into();
- options.end_cap = stroke.line_cap.into();
- options.line_join = stroke.line_join.into();
+ options.start_cap = into_line_cap(stroke.line_cap);
+ options.end_cap = into_line_cap(stroke.line_cap);
+ options.line_join = into_line_join(stroke.line_join);
let path = if stroke.line_dash.segments.is_empty() {
Cow::Borrowed(path)
} else {
- Cow::Owned(path::dashed(path, stroke.line_dash))
+ Cow::Owned(dashed(path, stroke.line_dash))
};
if self.transforms.current.is_identity {
@@ -281,7 +291,7 @@ impl Frame {
buffer.as_mut(),
)
} else {
- let path = path.transformed(&self.transforms.current.raw);
+ let path = path.transform(&self.transforms.current.raw);
self.stroke_tessellator.tessellate_path(
path.raw(),
@@ -331,9 +341,11 @@ impl Frame {
},
color: text.color,
size: text.size,
+ line_height: text.line_height,
font: text.font,
horizontal_alignment: text.horizontal_alignment,
vertical_alignment: text.vertical_alignment,
+ shaping: text.shaping,
});
}
@@ -344,10 +356,20 @@ impl Frame {
/// operations in different coordinate systems.
#[inline]
pub fn with_save(&mut self, f: impl FnOnce(&mut Frame)) {
- self.transforms.previous.push(self.transforms.current);
+ self.push_transform();
f(self);
+ self.pop_transform();
+ }
+
+ /// Pushes the current transform in the transform stack.
+ pub fn push_transform(&mut self) {
+ self.transforms.previous.push(self.transforms.current);
+ }
+
+ /// Pops a transform from the transform stack and sets it as the current transform.
+ pub fn pop_transform(&mut self) {
self.transforms.current = self.transforms.previous.pop().unwrap();
}
@@ -363,14 +385,21 @@ impl Frame {
f(&mut frame);
+ let origin = Point::new(region.x, region.y);
+
+ self.clip(frame, origin);
+ }
+
+ /// Draws the clipped contents of the given [`Frame`] with origin at the given [`Point`].
+ pub fn clip(&mut self, frame: Frame, at: Point) {
+ let size = frame.size();
let primitives = frame.into_primitives();
+ let translation = Vector::new(at.x, at.y);
let (text, meshes) = primitives
.into_iter()
.partition(|primitive| matches!(primitive, Primitive::Text { .. }));
- let translation = Vector::new(region.x, region.y);
-
self.primitives.push(Primitive::Group {
primitives: vec![
Primitive::Translate {
@@ -380,7 +409,7 @@ impl Frame {
Primitive::Translate {
translation,
content: Box::new(Primitive::Clip {
- bounds: Rectangle::with_size(region.size()),
+ bounds: Rectangle::with_size(size),
content: Box::new(Primitive::Group {
primitives: text,
}),
@@ -423,11 +452,11 @@ impl Frame {
self.transforms.current.is_identity = false;
}
- /// Produces the [`Geometry`] representing everything drawn on the [`Frame`].
- pub fn into_geometry(self) -> Geometry {
- Geometry::from_primitive(Primitive::Group {
+ /// Produces the [`Primitive`] representing everything drawn on the [`Frame`].
+ pub fn into_primitive(self) -> Primitive {
+ Primitive::Group {
primitives: self.into_primitives(),
- })
+ }
}
fn into_primitives(mut self) -> Vec<Primitive> {
@@ -435,25 +464,28 @@ impl Frame {
match buffer {
Buffer::Solid(buffer) => {
if !buffer.indices.is_empty() {
- self.primitives.push(Primitive::SolidMesh {
- buffers: triangle::Mesh2D {
- vertices: buffer.vertices,
- indices: buffer.indices,
- },
- size: self.size,
- })
+ self.primitives.push(Primitive::Custom(
+ primitive::Custom::Mesh(Mesh::Solid {
+ buffers: mesh::Indexed {
+ vertices: buffer.vertices,
+ indices: buffer.indices,
+ },
+ size: self.size,
+ }),
+ ))
}
}
- Buffer::Gradient(buffer, gradient) => {
+ Buffer::Gradient(buffer) => {
if !buffer.indices.is_empty() {
- self.primitives.push(Primitive::GradientMesh {
- buffers: triangle::Mesh2D {
- vertices: buffer.vertices,
- indices: buffer.indices,
- },
- size: self.size,
- gradient,
- })
+ self.primitives.push(Primitive::Custom(
+ primitive::Custom::Mesh(Mesh::Gradient {
+ buffers: mesh::Indexed {
+ vertices: buffer.vertices,
+ indices: buffer.indices,
+ },
+ size: self.size,
+ }),
+ ))
}
}
}
@@ -463,68 +495,137 @@ impl Frame {
}
}
-struct Vertex2DBuilder;
+struct GradientVertex2DBuilder {
+ gradient: gradient::Packed,
+}
-impl tessellation::FillVertexConstructor<triangle::Vertex2D>
- for Vertex2DBuilder
+impl tessellation::FillVertexConstructor<mesh::GradientVertex2D>
+ for GradientVertex2DBuilder
{
fn new_vertex(
&mut self,
vertex: tessellation::FillVertex<'_>,
- ) -> triangle::Vertex2D {
+ ) -> mesh::GradientVertex2D {
let position = vertex.position();
- triangle::Vertex2D {
+ mesh::GradientVertex2D {
position: [position.x, position.y],
+ gradient: self.gradient,
}
}
}
-impl tessellation::StrokeVertexConstructor<triangle::Vertex2D>
- for Vertex2DBuilder
+impl tessellation::StrokeVertexConstructor<mesh::GradientVertex2D>
+ for GradientVertex2DBuilder
{
fn new_vertex(
&mut self,
vertex: tessellation::StrokeVertex<'_, '_>,
- ) -> triangle::Vertex2D {
+ ) -> mesh::GradientVertex2D {
let position = vertex.position();
- triangle::Vertex2D {
+ mesh::GradientVertex2D {
position: [position.x, position.y],
+ gradient: self.gradient,
}
}
}
-struct TriangleVertex2DBuilder([f32; 4]);
+struct TriangleVertex2DBuilder(color::Packed);
-impl tessellation::FillVertexConstructor<triangle::ColoredVertex2D>
+impl tessellation::FillVertexConstructor<mesh::SolidVertex2D>
for TriangleVertex2DBuilder
{
fn new_vertex(
&mut self,
vertex: tessellation::FillVertex<'_>,
- ) -> triangle::ColoredVertex2D {
+ ) -> mesh::SolidVertex2D {
let position = vertex.position();
- triangle::ColoredVertex2D {
+ mesh::SolidVertex2D {
position: [position.x, position.y],
color: self.0,
}
}
}
-impl tessellation::StrokeVertexConstructor<triangle::ColoredVertex2D>
+impl tessellation::StrokeVertexConstructor<mesh::SolidVertex2D>
for TriangleVertex2DBuilder
{
fn new_vertex(
&mut self,
vertex: tessellation::StrokeVertex<'_, '_>,
- ) -> triangle::ColoredVertex2D {
+ ) -> mesh::SolidVertex2D {
let position = vertex.position();
- triangle::ColoredVertex2D {
+ mesh::SolidVertex2D {
position: [position.x, position.y],
color: self.0,
}
}
}
+
+fn into_line_join(line_join: LineJoin) -> lyon::tessellation::LineJoin {
+ match line_join {
+ LineJoin::Miter => lyon::tessellation::LineJoin::Miter,
+ LineJoin::Round => lyon::tessellation::LineJoin::Round,
+ LineJoin::Bevel => lyon::tessellation::LineJoin::Bevel,
+ }
+}
+
+fn into_line_cap(line_cap: LineCap) -> lyon::tessellation::LineCap {
+ match line_cap {
+ LineCap::Butt => lyon::tessellation::LineCap::Butt,
+ LineCap::Square => lyon::tessellation::LineCap::Square,
+ LineCap::Round => lyon::tessellation::LineCap::Round,
+ }
+}
+
+fn into_fill_rule(rule: fill::Rule) -> lyon::tessellation::FillRule {
+ match rule {
+ fill::Rule::NonZero => lyon::tessellation::FillRule::NonZero,
+ fill::Rule::EvenOdd => lyon::tessellation::FillRule::EvenOdd,
+ }
+}
+
+pub(super) fn dashed(path: &Path, line_dash: LineDash<'_>) -> Path {
+ use lyon::algorithms::walk::{
+ walk_along_path, RepeatedPattern, WalkerEvent,
+ };
+ use lyon::path::iterator::PathIterator;
+
+ Path::new(|builder| {
+ let segments_odd = (line_dash.segments.len() % 2 == 1)
+ .then(|| [line_dash.segments, line_dash.segments].concat());
+
+ let mut draw_line = false;
+
+ walk_along_path(
+ path.raw().iter().flattened(0.01),
+ 0.0,
+ lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE,
+ &mut RepeatedPattern {
+ callback: |event: WalkerEvent<'_>| {
+ let point = Point {
+ x: event.position.x,
+ y: event.position.y,
+ };
+
+ if draw_line {
+ builder.line_to(point);
+ } else {
+ builder.move_to(point);
+ }
+
+ draw_line = !draw_line;
+
+ true
+ },
+ index: line_dash.offset,
+ intervals: segments_odd
+ .as_deref()
+ .unwrap_or(line_dash.segments),
+ },
+ );
+ })
+}
diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs
index a5e63b17..553ba330 100644
--- a/wgpu/src/image.rs
+++ b/wgpu/src/image.rs
@@ -1,16 +1,17 @@
mod atlas;
#[cfg(feature = "image")]
-use iced_graphics::image::raster;
+mod raster;
#[cfg(feature = "svg")]
-use iced_graphics::image::vector;
+mod vector;
-use crate::Transformation;
use atlas::Atlas;
-use iced_graphics::layer;
-use iced_native::{Rectangle, Size};
+use crate::core::{Rectangle, Size};
+use crate::graphics::Transformation;
+use crate::layer;
+use crate::Buffer;
use std::cell::RefCell;
use std::mem;
@@ -18,10 +19,10 @@ use std::mem;
use bytemuck::{Pod, Zeroable};
#[cfg(feature = "image")]
-use iced_native::image;
+use crate::core::image;
#[cfg(feature = "svg")]
-use iced_native::svg;
+use crate::core::svg;
#[cfg(feature = "tracing")]
use tracing::info_span;
@@ -29,20 +30,112 @@ use tracing::info_span;
#[derive(Debug)]
pub struct Pipeline {
#[cfg(feature = "image")]
- raster_cache: RefCell<raster::Cache<Atlas>>,
+ raster_cache: RefCell<raster::Cache>,
#[cfg(feature = "svg")]
- vector_cache: RefCell<vector::Cache<Atlas>>,
+ vector_cache: RefCell<vector::Cache>,
pipeline: wgpu::RenderPipeline,
- uniforms: wgpu::Buffer,
vertices: wgpu::Buffer,
indices: wgpu::Buffer,
- instances: wgpu::Buffer,
- constants: wgpu::BindGroup,
+ sampler: wgpu::Sampler,
texture: wgpu::BindGroup,
texture_version: usize,
- texture_layout: wgpu::BindGroupLayout,
texture_atlas: Atlas,
+ texture_layout: wgpu::BindGroupLayout,
+ constant_layout: wgpu::BindGroupLayout,
+
+ layers: Vec<Layer>,
+ prepare_layer: usize,
+}
+
+#[derive(Debug)]
+struct Layer {
+ uniforms: wgpu::Buffer,
+ constants: wgpu::BindGroup,
+ instances: Buffer<Instance>,
+ instance_count: usize,
+}
+
+impl Layer {
+ fn new(
+ device: &wgpu::Device,
+ constant_layout: &wgpu::BindGroupLayout,
+ sampler: &wgpu::Sampler,
+ ) -> Self {
+ let uniforms = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("iced_wgpu::image uniforms buffer"),
+ size: mem::size_of::<Uniforms>() as u64,
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+
+ let constants = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("iced_wgpu::image constants bind group"),
+ layout: constant_layout,
+ entries: &[
+ wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::Buffer(
+ wgpu::BufferBinding {
+ buffer: &uniforms,
+ offset: 0,
+ size: None,
+ },
+ ),
+ },
+ wgpu::BindGroupEntry {
+ binding: 1,
+ resource: wgpu::BindingResource::Sampler(sampler),
+ },
+ ],
+ });
+
+ let instances = Buffer::new(
+ device,
+ "iced_wgpu::image instance buffer",
+ Instance::INITIAL,
+ wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+ );
+
+ Self {
+ uniforms,
+ constants,
+ instances,
+ instance_count: 0,
+ }
+ }
+
+ fn prepare(
+ &mut self,
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ instances: &[Instance],
+ transformation: Transformation,
+ ) {
+ queue.write_buffer(
+ &self.uniforms,
+ 0,
+ bytemuck::bytes_of(&Uniforms {
+ transform: transformation.into(),
+ }),
+ );
+
+ let _ = self.instances.resize(device, instances.len());
+ let _ = self.instances.write(queue, 0, instances);
+
+ self.instance_count = instances.len();
+ }
+
+ fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {
+ render_pass.set_bind_group(0, &self.constants, &[]);
+ render_pass.set_vertex_buffer(1, self.instances.slice(..));
+
+ render_pass.draw_indexed(
+ 0..QUAD_INDICES.len() as u32,
+ 0,
+ 0..self.instance_count as u32,
+ );
+ }
}
impl Pipeline {
@@ -86,35 +179,6 @@ impl Pipeline {
],
});
- let uniforms_buffer = device.create_buffer(&wgpu::BufferDescriptor {
- label: Some("iced_wgpu::image uniforms buffer"),
- size: mem::size_of::<Uniforms>() as u64,
- usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
-
- let constant_bind_group =
- device.create_bind_group(&wgpu::BindGroupDescriptor {
- label: Some("iced_wgpu::image constants bind group"),
- layout: &constant_layout,
- entries: &[
- wgpu::BindGroupEntry {
- binding: 0,
- resource: wgpu::BindingResource::Buffer(
- wgpu::BufferBinding {
- buffer: &uniforms_buffer,
- offset: 0,
- size: None,
- },
- ),
- },
- wgpu::BindGroupEntry {
- binding: 1,
- resource: wgpu::BindingResource::Sampler(&sampler),
- },
- ],
- });
-
let texture_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("iced_wgpu::image texture atlas layout"),
@@ -141,7 +205,7 @@ impl Pipeline {
let shader =
device.create_shader_module(wgpu::ShaderModuleDescriptor {
- label: Some("iced_wgpu::image::shader"),
+ label: Some("iced_wgpu image shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
include_str!("shader/image.wgsl"),
)),
@@ -214,7 +278,7 @@ impl Pipeline {
let vertices =
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("iced_wgpu::image vertex buffer"),
- contents: bytemuck::cast_slice(&QUAD_VERTS),
+ contents: bytemuck::cast_slice(&QUAD_VERTICES),
usage: wgpu::BufferUsages::VERTEX,
});
@@ -225,13 +289,6 @@ impl Pipeline {
usage: wgpu::BufferUsages::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::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
-
let texture_atlas = Atlas::new(device);
let texture = device.create_bind_group(&wgpu::BindGroupDescriptor {
@@ -253,15 +310,17 @@ impl Pipeline {
vector_cache: RefCell::new(vector::Cache::default()),
pipeline,
- uniforms: uniforms_buffer,
vertices,
indices,
- instances,
- constants: constant_bind_group,
+ sampler,
texture,
texture_version: texture_atlas.layer_count(),
- texture_layout,
texture_atlas,
+ texture_layout,
+ constant_layout,
+
+ layers: Vec::new(),
+ prepare_layer: 0,
}
}
@@ -281,18 +340,19 @@ impl Pipeline {
svg.viewport_dimensions()
}
- pub fn draw(
+ pub fn prepare(
&mut self,
device: &wgpu::Device,
- staging_belt: &mut wgpu::util::StagingBelt,
+ queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
images: &[layer::Image],
transformation: Transformation,
- bounds: Rectangle<u32>,
- target: &wgpu::TextureView,
_scale: f32,
) {
#[cfg(feature = "tracing")]
+ let _ = info_span!("Wgpu::Image", "PREPARE").entered();
+
+ #[cfg(feature = "tracing")]
let _ = info_span!("Wgpu::Image", "DRAW").entered();
let instances: &mut Vec<Instance> = &mut Vec::new();
@@ -308,8 +368,9 @@ impl Pipeline {
#[cfg(feature = "image")]
layer::Image::Raster { handle, bounds } => {
if let Some(atlas_entry) = raster_cache.upload(
+ device,
+ encoder,
handle,
- &mut (device, encoder),
&mut self.texture_atlas,
) {
add_instances(
@@ -332,11 +393,12 @@ impl Pipeline {
let size = [bounds.width, bounds.height];
if let Some(atlas_entry) = vector_cache.upload(
+ device,
+ encoder,
handle,
*color,
size,
_scale,
- &mut (device, encoder),
&mut self.texture_atlas,
) {
add_instances(
@@ -376,68 +438,28 @@ impl Pipeline {
self.texture_version = texture_version;
}
- {
- let mut uniforms_buffer = staging_belt.write_buffer(
- encoder,
- &self.uniforms,
- 0,
- wgpu::BufferSize::new(mem::size_of::<Uniforms>() as u64)
- .unwrap(),
+ if self.layers.len() <= self.prepare_layer {
+ self.layers.push(Layer::new(
device,
- );
-
- uniforms_buffer.copy_from_slice(bytemuck::bytes_of(&Uniforms {
- transform: transformation.into(),
- }));
+ &self.constant_layout,
+ &self.sampler,
+ ));
}
- let mut i = 0;
- let total = instances.len();
-
- while i < total {
- let end = (i + Instance::MAX).min(total);
- let amount = end - i;
-
- let mut instances_buffer = staging_belt.write_buffer(
- encoder,
- &self.instances,
- 0,
- wgpu::BufferSize::new(
- (amount * std::mem::size_of::<Instance>()) as u64,
- )
- .unwrap(),
- device,
- );
+ let layer = &mut self.layers[self.prepare_layer];
+ layer.prepare(device, queue, instances, transformation);
- instances_buffer.copy_from_slice(bytemuck::cast_slice(
- &instances[i..i + amount],
- ));
-
- let mut render_pass =
- encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
- label: Some("iced_wgpu::image render pass"),
- color_attachments: &[Some(
- wgpu::RenderPassColorAttachment {
- view: target,
- resolve_target: None,
- ops: wgpu::Operations {
- load: wgpu::LoadOp::Load,
- store: true,
- },
- },
- )],
- depth_stencil_attachment: None,
- });
+ self.prepare_layer += 1;
+ }
+ pub fn render<'a>(
+ &'a self,
+ layer: usize,
+ bounds: Rectangle<u32>,
+ render_pass: &mut wgpu::RenderPass<'a>,
+ ) {
+ if let Some(layer) = self.layers.get(layer) {
render_pass.set_pipeline(&self.pipeline);
- render_pass.set_bind_group(0, &self.constants, &[]);
- render_pass.set_bind_group(1, &self.texture, &[]);
- render_pass.set_index_buffer(
- self.indices.slice(..),
- wgpu::IndexFormat::Uint16,
- );
- render_pass.set_vertex_buffer(0, self.vertices.slice(..));
- render_pass.set_vertex_buffer(1, self.instances.slice(..));
render_pass.set_scissor_rect(
bounds.x,
@@ -446,30 +468,25 @@ impl Pipeline {
bounds.height,
);
- render_pass.draw_indexed(
- 0..QUAD_INDICES.len() as u32,
- 0,
- 0..amount as u32,
+ render_pass.set_bind_group(1, &self.texture, &[]);
+ render_pass.set_index_buffer(
+ self.indices.slice(..),
+ wgpu::IndexFormat::Uint16,
);
+ render_pass.set_vertex_buffer(0, self.vertices.slice(..));
- i += Instance::MAX;
+ layer.render(render_pass);
}
}
- pub fn trim_cache(
- &mut self,
- device: &wgpu::Device,
- encoder: &mut wgpu::CommandEncoder,
- ) {
+ pub fn end_frame(&mut self) {
#[cfg(feature = "image")]
- self.raster_cache
- .borrow_mut()
- .trim(&mut self.texture_atlas, &mut (device, encoder));
+ self.raster_cache.borrow_mut().trim(&mut self.texture_atlas);
#[cfg(feature = "svg")]
- self.vector_cache
- .borrow_mut()
- .trim(&mut self.texture_atlas, &mut (device, encoder));
+ self.vector_cache.borrow_mut().trim(&mut self.texture_atlas);
+
+ self.prepare_layer = 0;
}
}
@@ -481,7 +498,7 @@ pub struct Vertex {
const QUAD_INDICES: [u16; 6] = [0, 1, 2, 0, 2, 3];
-const QUAD_VERTS: [Vertex; 4] = [
+const QUAD_VERTICES: [Vertex; 4] = [
Vertex {
_position: [0.0, 0.0],
},
@@ -507,7 +524,7 @@ struct Instance {
}
impl Instance {
- pub const MAX: usize = 1_000;
+ pub const INITIAL: usize = 1_000;
}
#[repr(C)]
diff --git a/wgpu/src/image/atlas.rs b/wgpu/src/image/atlas.rs
index eafe2f96..e3de1290 100644
--- a/wgpu/src/image/atlas.rs
+++ b/wgpu/src/image/atlas.rs
@@ -12,10 +12,8 @@ use allocator::Allocator;
pub const SIZE: u32 = 2048;
-use iced_graphics::image;
-use iced_graphics::Size;
-
-use std::num::NonZeroU32;
+use crate::core::Size;
+use crate::graphics::color;
#[derive(Debug)]
pub struct Atlas {
@@ -38,10 +36,15 @@ impl Atlas {
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
- format: wgpu::TextureFormat::Rgba8UnormSrgb,
+ format: if color::GAMMA_CORRECTION {
+ wgpu::TextureFormat::Rgba8UnormSrgb
+ } else {
+ wgpu::TextureFormat::Rgba8Unorm
+ },
usage: wgpu::TextureUsages::COPY_DST
| wgpu::TextureUsages::COPY_SRC
| wgpu::TextureUsages::TEXTURE_BINDING,
+ view_formats: &[],
});
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor {
@@ -64,6 +67,98 @@ impl Atlas {
self.layers.len()
}
+ pub fn upload(
+ &mut self,
+ device: &wgpu::Device,
+ encoder: &mut wgpu::CommandEncoder,
+ width: u32,
+ height: u32,
+ data: &[u8],
+ ) -> Option<Entry> {
+ let entry = {
+ let current_size = self.layers.len();
+ let entry = self.allocate(width, height)?;
+
+ // We grow the internal texture after allocating if necessary
+ let new_layers = self.layers.len() - current_size;
+ self.grow(new_layers, device, encoder);
+
+ entry
+ };
+
+ log::info!("Allocated atlas entry: {:?}", entry);
+
+ // 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],
+ )
+ }
+
+ match &entry {
+ Entry::Contiguous(allocation) => {
+ self.upload_allocation(
+ &padded_data,
+ width,
+ height,
+ padding,
+ 0,
+ allocation,
+ device,
+ encoder,
+ );
+ }
+ Entry::Fragmented { fragments, .. } => {
+ for fragment in fragments {
+ let (x, y) = fragment.position;
+ let offset = (y * padded_width as u32 + 4 * x) as usize;
+
+ self.upload_allocation(
+ &padded_data,
+ width,
+ height,
+ padding,
+ offset,
+ &fragment.allocation,
+ device,
+ encoder,
+ );
+ }
+ }
+ }
+
+ log::info!("Current atlas: {:?}", self);
+
+ Some(entry)
+ }
+
+ pub fn remove(&mut self, entry: &Entry) {
+ log::info!("Removing atlas entry: {:?}", entry);
+
+ match entry {
+ Entry::Contiguous(allocation) => {
+ self.deallocate(allocation);
+ }
+ Entry::Fragmented { fragments, .. } => {
+ for fragment in fragments {
+ self.deallocate(&fragment.allocation);
+ }
+ }
+ }
+ }
+
fn allocate(&mut self, width: u32, height: u32) -> Option<Entry> {
// Allocate one layer if texture fits perfectly
if width == SIZE && height == SIZE {
@@ -185,14 +280,17 @@ impl Atlas {
fn upload_allocation(
&mut self,
- buffer: &wgpu::Buffer,
+ data: &[u8],
image_width: u32,
image_height: u32,
padding: u32,
offset: usize,
allocation: &Allocation,
+ device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
) {
+ use wgpu::util::DeviceExt;
+
let (x, y) = allocation.position();
let Size { width, height } = allocation.size();
let layer = allocation.layer();
@@ -203,13 +301,20 @@ impl Atlas {
depth_or_array_layers: 1,
};
+ let buffer =
+ device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+ label: Some("image upload buffer"),
+ contents: data,
+ usage: wgpu::BufferUsages::COPY_SRC,
+ });
+
encoder.copy_buffer_to_texture(
wgpu::ImageCopyBuffer {
- buffer,
+ buffer: &buffer,
layout: wgpu::ImageDataLayout {
offset: offset as u64,
- bytes_per_row: NonZeroU32::new(4 * image_width + padding),
- rows_per_image: NonZeroU32::new(image_height),
+ bytes_per_row: Some(4 * image_width + padding),
+ rows_per_image: Some(image_height),
},
},
wgpu::ImageCopyTexture {
@@ -246,10 +351,15 @@ impl Atlas {
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
- format: wgpu::TextureFormat::Rgba8UnormSrgb,
+ format: if color::GAMMA_CORRECTION {
+ wgpu::TextureFormat::Rgba8UnormSrgb
+ } else {
+ wgpu::TextureFormat::Rgba8Unorm
+ },
usage: wgpu::TextureUsages::COPY_DST
| wgpu::TextureUsages::COPY_SRC
| wgpu::TextureUsages::TEXTURE_BINDING,
+ view_formats: &[],
});
let amount_to_copy = self.layers.len() - amount;
@@ -298,100 +408,3 @@ impl Atlas {
});
}
}
-
-impl image::Storage for Atlas {
- type Entry = Entry;
- type State<'a> = (&'a wgpu::Device, &'a mut wgpu::CommandEncoder);
-
- fn upload(
- &mut self,
- width: u32,
- height: u32,
- data: &[u8],
- (device, encoder): &mut Self::State<'_>,
- ) -> Option<Self::Entry> {
- use wgpu::util::DeviceExt;
-
- let entry = {
- let current_size = self.layers.len();
- let entry = self.allocate(width, height)?;
-
- // We grow the internal texture after allocating if necessary
- let new_layers = self.layers.len() - current_size;
- self.grow(new_layers, device, encoder);
-
- entry
- };
-
- log::info!("Allocated atlas entry: {:?}", entry);
-
- // 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::BufferUsages::COPY_SRC,
- });
-
- match &entry {
- Entry::Contiguous(allocation) => {
- self.upload_allocation(
- &buffer, width, height, padding, 0, allocation, encoder,
- );
- }
- Entry::Fragmented { fragments, .. } => {
- for fragment in fragments {
- let (x, y) = fragment.position;
- let offset = (y * padded_width as u32 + 4 * x) as usize;
-
- self.upload_allocation(
- &buffer,
- width,
- height,
- padding,
- offset,
- &fragment.allocation,
- encoder,
- );
- }
- }
- }
-
- log::info!("Current atlas: {:?}", self);
-
- Some(entry)
- }
-
- fn remove(&mut self, entry: &Entry, _: &mut Self::State<'_>) {
- log::info!("Removing atlas entry: {:?}", entry);
-
- match entry {
- Entry::Contiguous(allocation) => {
- self.deallocate(allocation);
- }
- Entry::Fragmented { fragments, .. } => {
- for fragment in fragments {
- self.deallocate(&fragment.allocation);
- }
- }
- }
- }
-}
diff --git a/wgpu/src/image/atlas/allocation.rs b/wgpu/src/image/atlas/allocation.rs
index 43aba875..11289771 100644
--- a/wgpu/src/image/atlas/allocation.rs
+++ b/wgpu/src/image/atlas/allocation.rs
@@ -1,7 +1,6 @@
+use crate::core::Size;
use crate::image::atlas::{self, allocator};
-use iced_graphics::Size;
-
#[derive(Debug)]
pub enum Allocation {
Partial {
diff --git a/wgpu/src/image/atlas/allocator.rs b/wgpu/src/image/atlas/allocator.rs
index 03effdcb..204a5c26 100644
--- a/wgpu/src/image/atlas/allocator.rs
+++ b/wgpu/src/image/atlas/allocator.rs
@@ -46,10 +46,10 @@ impl Region {
(rectangle.min.x as u32, rectangle.min.y as u32)
}
- pub fn size(&self) -> iced_graphics::Size<u32> {
+ pub fn size(&self) -> crate::core::Size<u32> {
let size = self.allocation.rectangle.size();
- iced_graphics::Size::new(size.width as u32, size.height as u32)
+ crate::core::Size::new(size.width as u32, size.height as u32)
}
}
diff --git a/wgpu/src/image/atlas/entry.rs b/wgpu/src/image/atlas/entry.rs
index 69c05a50..7e4c92a2 100644
--- a/wgpu/src/image/atlas/entry.rs
+++ b/wgpu/src/image/atlas/entry.rs
@@ -1,8 +1,6 @@
+use crate::core::Size;
use crate::image::atlas;
-use iced_graphics::image;
-use iced_graphics::Size;
-
#[derive(Debug)]
pub enum Entry {
Contiguous(atlas::Allocation),
@@ -12,8 +10,9 @@ pub enum Entry {
},
}
-impl image::storage::Entry for Entry {
- fn size(&self) -> Size<u32> {
+impl Entry {
+ #[cfg(feature = "image")]
+ pub fn size(&self) -> Size<u32> {
match self {
Entry::Contiguous(allocation) => allocation.size(),
Entry::Fragmented { size, .. } => *size,
diff --git a/wgpu/src/image/raster.rs b/wgpu/src/image/raster.rs
new file mode 100644
index 00000000..a6cba76a
--- /dev/null
+++ b/wgpu/src/image/raster.rs
@@ -0,0 +1,119 @@
+use crate::core::image;
+use crate::core::Size;
+use crate::graphics;
+use crate::graphics::image::image_rs;
+use crate::image::atlas::{self, Atlas};
+
+use std::collections::{HashMap, HashSet};
+
+/// Entry in cache corresponding to an image handle
+#[derive(Debug)]
+pub enum Memory {
+ /// Image data on host
+ Host(image_rs::ImageBuffer<image_rs::Rgba<u8>, Vec<u8>>),
+ /// Storage entry
+ Device(atlas::Entry),
+ /// Image not found
+ NotFound,
+ /// Invalid image data
+ Invalid,
+}
+
+impl Memory {
+ /// Width and height of image
+ pub fn dimensions(&self) -> Size<u32> {
+ match self {
+ Memory::Host(image) => {
+ let (width, height) = image.dimensions();
+
+ Size::new(width, height)
+ }
+ Memory::Device(entry) => entry.size(),
+ Memory::NotFound => Size::new(1, 1),
+ Memory::Invalid => Size::new(1, 1),
+ }
+ }
+}
+
+/// Caches image raster data
+#[derive(Debug, Default)]
+pub struct Cache {
+ map: HashMap<u64, Memory>,
+ hits: HashSet<u64>,
+}
+
+impl Cache {
+ /// Load image
+ pub fn load(&mut self, handle: &image::Handle) -> &mut Memory {
+ if self.contains(handle) {
+ return self.get(handle).unwrap();
+ }
+
+ let memory = match graphics::image::load(handle) {
+ Ok(image) => Memory::Host(image.to_rgba8()),
+ Err(image_rs::error::ImageError::IoError(_)) => Memory::NotFound,
+ Err(_) => Memory::Invalid,
+ };
+
+ self.insert(handle, memory);
+ self.get(handle).unwrap()
+ }
+
+ /// Load image and upload raster data
+ pub fn upload(
+ &mut self,
+ device: &wgpu::Device,
+ encoder: &mut wgpu::CommandEncoder,
+ handle: &image::Handle,
+ atlas: &mut Atlas,
+ ) -> Option<&atlas::Entry> {
+ let memory = self.load(handle);
+
+ if let Memory::Host(image) = memory {
+ let (width, height) = image.dimensions();
+
+ let entry = atlas.upload(device, encoder, width, height, image)?;
+
+ *memory = Memory::Device(entry);
+ }
+
+ if let Memory::Device(allocation) = memory {
+ Some(allocation)
+ } else {
+ None
+ }
+ }
+
+ /// Trim cache misses from cache
+ pub fn trim(&mut self, atlas: &mut Atlas) {
+ let hits = &self.hits;
+
+ self.map.retain(|k, memory| {
+ let retain = hits.contains(k);
+
+ if !retain {
+ if let Memory::Device(entry) = memory {
+ atlas.remove(entry);
+ }
+ }
+
+ retain
+ });
+
+ self.hits.clear();
+ }
+
+ fn get(&mut self, handle: &image::Handle) -> Option<&mut Memory> {
+ let _ = self.hits.insert(handle.id());
+
+ self.map.get_mut(&handle.id())
+ }
+
+ fn insert(&mut self, handle: &image::Handle, memory: Memory) {
+ let _ = self.map.insert(handle.id(), memory);
+ }
+
+ fn contains(&self, handle: &image::Handle) -> bool {
+ self.map.contains_key(&handle.id())
+ }
+}
diff --git a/graphics/src/image/vector.rs b/wgpu/src/image/vector.rs
index c950ccd6..2c03d36b 100644
--- a/graphics/src/image/vector.rs
+++ b/wgpu/src/image/vector.rs
@@ -1,9 +1,6 @@
-//! Vector image loading and caching
-use crate::image::Storage;
-use crate::Color;
-
-use iced_native::svg;
-use iced_native::Size;
+use crate::core::svg;
+use crate::core::{Color, Size};
+use crate::image::atlas::{self, Atlas};
use resvg::tiny_skia;
use resvg::usvg;
@@ -33,19 +30,21 @@ impl Svg {
}
/// Caches svg vector and raster data
-#[derive(Debug)]
-pub struct Cache<T: Storage> {
+#[derive(Debug, Default)]
+pub struct Cache {
svgs: HashMap<u64, Svg>,
- rasterized: HashMap<(u64, u32, u32, ColorFilter), T::Entry>,
+ rasterized: HashMap<(u64, u32, u32, ColorFilter), atlas::Entry>,
svg_hits: HashSet<u64>,
rasterized_hits: HashSet<(u64, u32, u32, ColorFilter)>,
}
type ColorFilter = Option<[u8; 4]>;
-impl<T: Storage> Cache<T> {
+impl Cache {
/// Load svg
pub fn load(&mut self, handle: &svg::Handle) -> &Svg {
+ use usvg::TreeParsing;
+
if self.svgs.contains_key(&handle.id()) {
return self.svgs.get(&handle.id()).unwrap();
}
@@ -74,13 +73,14 @@ impl<T: Storage> Cache<T> {
/// Load svg and upload raster data
pub fn upload(
&mut self,
+ device: &wgpu::Device,
+ encoder: &mut wgpu::CommandEncoder,
handle: &svg::Handle,
color: Option<Color>,
[width, height]: [f32; 2],
scale: f32,
- state: &mut T::State<'_>,
- storage: &mut T,
- ) -> Option<&T::Entry> {
+ atlas: &mut Atlas,
+ ) -> Option<&atlas::Entry> {
let id = handle.id();
let (width, height) = (
@@ -114,16 +114,28 @@ impl<T: Storage> Cache<T> {
// It would be cool to be able to smooth resize the `svg` example.
let mut img = tiny_skia::Pixmap::new(width, height)?;
- resvg::render(
- tree,
- if width > height {
- usvg::FitTo::Width(width)
- } else {
- usvg::FitTo::Height(height)
- },
- tiny_skia::Transform::default(),
- img.as_mut(),
- )?;
+ let tree_size = tree.size.to_int_size();
+
+ let target_size = if width > height {
+ tree_size.scale_to_width(width)
+ } else {
+ tree_size.scale_to_height(height)
+ };
+
+ let transform = if let Some(target_size) = target_size {
+ let tree_size = tree_size.to_size();
+ let target_size = target_size.to_size();
+
+ tiny_skia::Transform::from_scale(
+ target_size.width() / tree_size.width(),
+ target_size.height() / tree_size.height(),
+ )
+ } else {
+ tiny_skia::Transform::default()
+ };
+
+ resvg::Tree::from_usvg(tree)
+ .render(transform, &mut img.as_mut());
let mut rgba = img.take();
@@ -137,7 +149,9 @@ impl<T: Storage> Cache<T> {
});
}
- let allocation = storage.upload(width, height, &rgba, state)?;
+ let allocation =
+ atlas.upload(device, encoder, width, height, &rgba)?;
+
log::debug!("allocating {} {}x{}", id, width, height);
let _ = self.svg_hits.insert(id);
@@ -151,7 +165,7 @@ impl<T: Storage> Cache<T> {
}
/// Load svg and upload raster data
- pub fn trim(&mut self, storage: &mut T, state: &mut T::State<'_>) {
+ pub fn trim(&mut self, atlas: &mut Atlas) {
let svg_hits = &self.svg_hits;
let rasterized_hits = &self.rasterized_hits;
@@ -160,7 +174,7 @@ impl<T: Storage> Cache<T> {
let retain = rasterized_hits.contains(k);
if !retain {
- storage.remove(entry, state);
+ atlas.remove(entry);
}
retain
@@ -170,17 +184,6 @@ impl<T: Storage> Cache<T> {
}
}
-impl<T: Storage> Default for Cache<T> {
- fn default() -> Self {
- Self {
- svgs: HashMap::new(),
- rasterized: HashMap::new(),
- svg_hits: HashSet::new(),
- rasterized_hits: HashSet::new(),
- }
- }
-}
-
impl std::fmt::Debug for Svg {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
diff --git a/graphics/src/layer.rs b/wgpu/src/layer.rs
index 1d453caa..b8f32db1 100644
--- a/graphics/src/layer.rs
+++ b/wgpu/src/layer.rs
@@ -1,19 +1,21 @@
//! Organize rendering primitives into a flattened list of layers.
mod image;
-mod quad;
mod text;
pub mod mesh;
pub use image::Image;
pub use mesh::Mesh;
-pub use quad::Quad;
pub use text::Text;
-use crate::alignment;
-use crate::{
- Background, Font, Point, Primitive, Rectangle, Size, Vector, Viewport,
-};
+use crate::core;
+use crate::core::alignment;
+use crate::core::{Color, Font, Point, Rectangle, Size, Vector};
+use crate::graphics;
+use crate::graphics::color;
+use crate::graphics::Viewport;
+use crate::primitive::{self, Primitive};
+use crate::quad::{self, Quad};
/// A group of primitives that should be clipped together.
#[derive(Debug)]
@@ -22,7 +24,7 @@ pub struct Layer<'a> {
pub bounds: Rectangle,
/// The quads of the [`Layer`].
- pub quads: Vec<Quad>,
+ pub quads: quad::Batch,
/// The triangle meshes of the [`Layer`].
pub meshes: Vec<Mesh<'a>>,
@@ -39,7 +41,7 @@ impl<'a> Layer<'a> {
pub fn new(bounds: Rectangle) -> Self {
Self {
bounds,
- quads: Vec::new(),
+ quads: quad::Batch::default(),
meshes: Vec::new(),
text: Vec::new(),
images: Vec::new(),
@@ -60,18 +62,20 @@ impl<'a> Layer<'a> {
Point::new(11.0, 11.0 + 25.0 * i as f32),
Size::INFINITY,
),
- color: [0.9, 0.9, 0.9, 1.0],
+ color: Color::new(0.9, 0.9, 0.9, 1.0),
size: 20.0,
- font: Font::Default,
+ line_height: core::text::LineHeight::default(),
+ font: Font::MONOSPACE,
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
+ shaping: core::text::Shaping::Basic,
};
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],
+ color: Color::BLACK,
..text
});
}
@@ -109,26 +113,16 @@ impl<'a> Layer<'a> {
current_layer: usize,
) {
match primitive {
- Primitive::None => {}
- Primitive::Group { primitives } => {
- // TODO: Inspect a bit and regroup (?)
- for primitive in primitives {
- Self::process_primitive(
- layers,
- translation,
- primitive,
- current_layer,
- )
- }
- }
Primitive::Text {
content,
bounds,
size,
+ line_height,
color,
font,
horizontal_alignment,
vertical_alignment,
+ shaping,
} => {
let layer = &mut layers[current_layer];
@@ -136,10 +130,12 @@ impl<'a> Layer<'a> {
content,
bounds: *bounds + translation,
size: *size,
- color: color.into_linear(),
+ line_height: *line_height,
+ color: *color,
font: *font,
horizontal_alignment: *horizontal_alignment,
vertical_alignment: *vertical_alignment,
+ shaping: *shaping,
});
}
Primitive::Quad {
@@ -151,58 +147,49 @@ impl<'a> Layer<'a> {
} => {
let layer = &mut layers[current_layer];
- // TODO: Move some of these computations to the GPU (?)
- layer.quads.push(Quad {
+ let quad = 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_color: color::pack(*border_color),
border_radius: *border_radius,
border_width: *border_width,
- border_color: border_color.into_linear(),
- });
+ };
+
+ layer.quads.add(quad, background);
}
- Primitive::SolidMesh { buffers, size } => {
+ Primitive::Image { handle, bounds } => {
let layer = &mut layers[current_layer];
- 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::Solid {
- origin: Point::new(translation.x, translation.y),
- buffers,
- clip_bounds,
- });
- }
+ layer.images.push(Image::Raster {
+ handle: handle.clone(),
+ bounds: *bounds + translation,
+ });
}
- Primitive::GradientMesh {
- buffers,
- size,
- gradient,
+ Primitive::Svg {
+ handle,
+ color,
+ bounds,
} => {
let layer = &mut layers[current_layer];
- 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::Gradient {
- origin: Point::new(translation.x, translation.y),
- buffers,
- clip_bounds,
- gradient,
- });
+ layer.images.push(Image::Vector {
+ handle: handle.clone(),
+ color: *color,
+ bounds: *bounds + translation,
+ });
+ }
+ Primitive::Group { primitives } => {
+ // TODO: Inspect a bit and regroup (?)
+ for primitive in primitives {
+ Self::process_primitive(
+ layers,
+ translation,
+ primitive,
+ current_layer,
+ )
}
}
Primitive::Clip { bounds, content } => {
@@ -235,35 +222,62 @@ impl<'a> Layer<'a> {
current_layer,
);
}
- Primitive::Cached { cache } => {
+ Primitive::Cache { content } => {
Self::process_primitive(
layers,
translation,
- cache,
+ content,
current_layer,
);
}
- Primitive::Image { handle, bounds } => {
- let layer = &mut layers[current_layer];
+ Primitive::Custom(custom) => match custom {
+ primitive::Custom::Mesh(mesh) => match mesh {
+ graphics::Mesh::Solid { buffers, size } => {
+ let layer = &mut layers[current_layer];
- layer.images.push(Image::Raster {
- handle: handle.clone(),
- bounds: *bounds + translation,
- });
- }
- Primitive::Svg {
- handle,
- color,
- bounds,
- } => {
- let layer = &mut layers[current_layer];
+ let bounds = Rectangle::new(
+ Point::new(translation.x, translation.y),
+ *size,
+ );
- layer.images.push(Image::Vector {
- handle: handle.clone(),
- color: *color,
- bounds: *bounds + translation,
- });
- }
+ // Only draw visible content
+ if let Some(clip_bounds) =
+ layer.bounds.intersection(&bounds)
+ {
+ layer.meshes.push(Mesh::Solid {
+ origin: Point::new(
+ translation.x,
+ translation.y,
+ ),
+ buffers,
+ clip_bounds,
+ });
+ }
+ }
+ graphics::Mesh::Gradient { buffers, size } => {
+ let layer = &mut layers[current_layer];
+
+ 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::Gradient {
+ origin: Point::new(
+ translation.x,
+ translation.y,
+ ),
+ buffers,
+ clip_bounds,
+ });
+ }
+ }
+ },
+ },
}
}
}
diff --git a/graphics/src/layer/image.rs b/wgpu/src/layer/image.rs
index 3eff2397..0de589f8 100644
--- a/graphics/src/layer/image.rs
+++ b/wgpu/src/layer/image.rs
@@ -1,6 +1,6 @@
-use crate::{Color, Rectangle};
-
-use iced_native::{image, svg};
+use crate::core::image;
+use crate::core::svg;
+use crate::core::{Color, Rectangle};
/// A raster or vector image.
#[derive(Debug, Clone)]
diff --git a/graphics/src/layer/mesh.rs b/wgpu/src/layer/mesh.rs
index 7661c5c9..7c6206cd 100644
--- a/graphics/src/layer/mesh.rs
+++ b/wgpu/src/layer/mesh.rs
@@ -1,6 +1,6 @@
//! A collection of triangle primitives.
-use crate::triangle;
-use crate::{Gradient, Point, Rectangle};
+use crate::core::{Point, Rectangle};
+use crate::graphics::mesh;
/// A mesh of triangles.
#[derive(Debug, Clone, Copy)]
@@ -11,7 +11,7 @@ pub enum Mesh<'a> {
origin: Point,
/// The vertex and index buffers of the [`Mesh`].
- buffers: &'a triangle::Mesh2D<triangle::ColoredVertex2D>,
+ buffers: &'a mesh::Indexed<mesh::SolidVertex2D>,
/// The clipping bounds of the [`Mesh`].
clip_bounds: Rectangle<f32>,
@@ -22,13 +22,10 @@ pub enum Mesh<'a> {
origin: Point,
/// The vertex and index buffers of the [`Mesh`].
- buffers: &'a triangle::Mesh2D<triangle::Vertex2D>,
+ buffers: &'a mesh::Indexed<mesh::GradientVertex2D>,
/// The clipping bounds of the [`Mesh`].
clip_bounds: Rectangle<f32>,
-
- /// The gradient to apply to the [`Mesh`].
- gradient: &'a Gradient,
},
}
@@ -65,9 +62,15 @@ pub struct AttributeCount {
/// The total amount of solid vertices.
pub solid_vertices: usize,
+ /// The total amount of solid meshes.
+ pub solids: usize,
+
/// The total amount of gradient vertices.
pub gradient_vertices: usize,
+ /// The total amount of gradient meshes.
+ pub gradients: usize,
+
/// The total amount of indices.
pub indices: usize,
}
@@ -79,10 +82,12 @@ pub fn attribute_count_of<'a>(meshes: &'a [Mesh<'a>]) -> AttributeCount {
.fold(AttributeCount::default(), |mut count, mesh| {
match mesh {
Mesh::Solid { buffers, .. } => {
+ count.solids += 1;
count.solid_vertices += buffers.vertices.len();
count.indices += buffers.indices.len();
}
Mesh::Gradient { buffers, .. } => {
+ count.gradients += 1;
count.gradient_vertices += buffers.vertices.len();
count.indices += buffers.indices.len();
}
diff --git a/graphics/src/layer/text.rs b/wgpu/src/layer/text.rs
index 74f7a676..ba1bdca8 100644
--- a/graphics/src/layer/text.rs
+++ b/wgpu/src/layer/text.rs
@@ -1,4 +1,6 @@
-use crate::{alignment, Font, Rectangle};
+use crate::core::alignment;
+use crate::core::text;
+use crate::core::{Color, Font, Rectangle};
/// A paragraph of text.
#[derive(Debug, Clone, Copy)]
@@ -10,11 +12,14 @@ pub struct Text<'a> {
pub bounds: Rectangle,
/// The color of the [`Text`], in __linear RGB_.
- pub color: [f32; 4],
+ pub color: Color,
- /// The size of the [`Text`].
+ /// The size of the [`Text`] in logical pixels.
pub size: f32,
+ /// The line height of the [`Text`].
+ pub line_height: text::LineHeight,
+
/// The font of the [`Text`].
pub font: Font,
@@ -23,4 +28,7 @@ pub struct Text<'a> {
/// The vertical alignment of the [`Text`].
pub vertical_alignment: alignment::Vertical,
+
+ /// The shaping strategy of the text.
+ pub shaping: text::Shaping,
}
diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs
index 1a293681..deb223ef 100644
--- a/wgpu/src/lib.rs
+++ b/wgpu/src/lib.rs
@@ -16,7 +16,7 @@
//! - Meshes of triangles, useful to draw geometry freely.
//!
//! [Iced]: https://github.com/iced-rs/iced
-//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.8/native
+//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.9/native
//! [`wgpu`]: https://github.com/gfx-rs/wgpu-rs
//! [WebGPU API]: https://gpuweb.github.io/gpuweb/
//! [`wgpu_glyph`]: https://github.com/hecrj/wgpu_glyph
@@ -36,26 +36,34 @@
)]
#![forbid(rust_2018_idioms)]
#![allow(clippy::inherent_to_string, clippy::type_complexity)]
-#![cfg_attr(docsrs, feature(doc_cfg))]
-
+#![cfg_attr(docsrs, feature(doc_auto_cfg))]
+pub mod layer;
+pub mod primitive;
pub mod settings;
pub mod window;
+#[cfg(feature = "geometry")]
+pub mod geometry;
+
mod backend;
mod buffer;
+mod color;
mod quad;
mod text;
mod triangle;
-pub use iced_graphics::{Antialiasing, Color, Error, Primitive, Viewport};
-pub use iced_native::Theme;
+use buffer::Buffer;
+
+pub use iced_graphics as graphics;
+pub use iced_graphics::core;
+
pub use wgpu;
pub use backend::Backend;
+pub use layer::Layer;
+pub use primitive::Primitive;
pub use settings::Settings;
-pub(crate) use iced_graphics::Transformation;
-
#[cfg(any(feature = "image", feature = "svg"))]
mod image;
@@ -63,5 +71,4 @@ mod image;
///
/// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs
/// [`iced`]: https://github.com/iced-rs/iced
-pub type Renderer<Theme = iced_native::Theme> =
- iced_graphics::Renderer<Backend, Theme>;
+pub type Renderer<Theme> = iced_graphics::Renderer<Backend, Theme>;
diff --git a/wgpu/src/primitive.rs b/wgpu/src/primitive.rs
new file mode 100644
index 00000000..8dbf3008
--- /dev/null
+++ b/wgpu/src/primitive.rs
@@ -0,0 +1,21 @@
+//! Draw using different graphical primitives.
+use crate::core::Rectangle;
+use crate::graphics::{Damage, Mesh};
+
+/// The graphical primitives supported by `iced_wgpu`.
+pub type Primitive = crate::graphics::Primitive<Custom>;
+
+/// The custom primitives supported by `iced_wgpu`.
+#[derive(Debug, Clone, PartialEq)]
+pub enum Custom {
+ /// A mesh primitive.
+ Mesh(Mesh),
+}
+
+impl Damage for Custom {
+ fn bounds(&self) -> Rectangle {
+ match self {
+ Self::Mesh(mesh) => mesh.bounds(),
+ }
+ }
+}
diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs
index 2f5fcc6b..37d0c623 100644
--- a/wgpu/src/quad.rs
+++ b/wgpu/src/quad.rs
@@ -1,22 +1,32 @@
-use crate::Transformation;
-use iced_graphics::layer;
-use iced_native::Rectangle;
+mod gradient;
+mod solid;
+
+use gradient::Gradient;
+use solid::Solid;
+
+use crate::core::{Background, Rectangle};
+use crate::graphics::color;
+use crate::graphics::{self, Transformation};
use bytemuck::{Pod, Zeroable};
-use std::mem;
use wgpu::util::DeviceExt;
+use std::mem;
+
#[cfg(feature = "tracing")]
use tracing::info_span;
+const INITIAL_INSTANCES: usize = 2_000;
+
#[derive(Debug)]
pub struct Pipeline {
- pipeline: wgpu::RenderPipeline,
- constants: wgpu::BindGroup,
- constants_buffer: wgpu::Buffer,
+ solid: solid::Pipeline,
+ gradient: gradient::Pipeline,
+ constant_layout: wgpu::BindGroupLayout,
vertices: wgpu::Buffer,
indices: wgpu::Buffer,
- instances: wgpu::Buffer,
+ layers: Vec<Layer>,
+ prepare_layer: usize,
}
impl Pipeline {
@@ -38,239 +48,292 @@ impl Pipeline {
}],
});
- let constants_buffer = device.create_buffer(&wgpu::BufferDescriptor {
- label: Some("iced_wgpu::quad uniforms buffer"),
- size: mem::size_of::<Uniforms>() as wgpu::BufferAddress,
- usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::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,
- entries: &[wgpu::BindGroupEntry {
- binding: 0,
- resource: constants_buffer.as_entire_binding(),
- }],
- });
-
- let layout =
- device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
- label: Some("iced_wgpu::quad pipeline layout"),
- push_constant_ranges: &[],
- bind_group_layouts: &[&constant_layout],
- });
-
- let shader =
- device.create_shader_module(wgpu::ShaderModuleDescriptor {
- label: Some("iced_wgpu::quad::shader"),
- source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
- include_str!("shader/quad.wgsl"),
- )),
- });
-
- let pipeline =
- device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
- label: Some("iced_wgpu::quad pipeline"),
- layout: Some(&layout),
- vertex: wgpu::VertexState {
- module: &shader,
- entry_point: "vs_main",
- buffers: &[
- wgpu::VertexBufferLayout {
- array_stride: mem::size_of::<Vertex>() as u64,
- step_mode: wgpu::VertexStepMode::Vertex,
- attributes: &[wgpu::VertexAttribute {
- shader_location: 0,
- format: wgpu::VertexFormat::Float32x2,
- offset: 0,
- }],
- },
- wgpu::VertexBufferLayout {
- array_stride: mem::size_of::<layer::Quad>() as u64,
- step_mode: wgpu::VertexStepMode::Instance,
- attributes: &wgpu::vertex_attr_array!(
- 1 => Float32x2,
- 2 => Float32x2,
- 3 => Float32x4,
- 4 => Float32x4,
- 5 => Float32x4,
- 6 => Float32,
- ),
- },
- ],
- },
- fragment: Some(wgpu::FragmentState {
- module: &shader,
- entry_point: "fs_main",
- targets: &[Some(wgpu::ColorTargetState {
- format,
- blend: Some(wgpu::BlendState {
- color: wgpu::BlendComponent {
- src_factor: wgpu::BlendFactor::SrcAlpha,
- dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
- operation: wgpu::BlendOperation::Add,
- },
- alpha: wgpu::BlendComponent {
- src_factor: wgpu::BlendFactor::One,
- dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
- operation: wgpu::BlendOperation::Add,
- },
- }),
- write_mask: wgpu::ColorWrites::ALL,
- })],
- }),
- primitive: wgpu::PrimitiveState {
- topology: wgpu::PrimitiveTopology::TriangleList,
- front_face: wgpu::FrontFace::Cw,
- ..Default::default()
- },
- depth_stencil: None,
- multisample: wgpu::MultisampleState {
- count: 1,
- mask: !0,
- alpha_to_coverage_enabled: false,
- },
- multiview: None,
- });
-
let vertices =
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("iced_wgpu::quad vertex buffer"),
- contents: bytemuck::cast_slice(&QUAD_VERTS),
+ contents: bytemuck::cast_slice(&VERTICES),
usage: wgpu::BufferUsages::VERTEX,
});
let indices =
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("iced_wgpu::quad index buffer"),
- contents: bytemuck::cast_slice(&QUAD_INDICES),
+ contents: bytemuck::cast_slice(&INDICES),
usage: wgpu::BufferUsages::INDEX,
});
- let instances = device.create_buffer(&wgpu::BufferDescriptor {
- label: Some("iced_wgpu::quad instance buffer"),
- size: mem::size_of::<layer::Quad>() as u64 * MAX_INSTANCES as u64,
- usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+ Self {
+ vertices,
+ indices,
+ solid: solid::Pipeline::new(device, format, &constant_layout),
+ gradient: gradient::Pipeline::new(device, format, &constant_layout),
+ layers: Vec::new(),
+ prepare_layer: 0,
+ constant_layout,
+ }
+ }
+
+ pub fn prepare(
+ &mut self,
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ quads: &Batch,
+ transformation: Transformation,
+ scale: f32,
+ ) {
+ if self.layers.len() <= self.prepare_layer {
+ self.layers.push(Layer::new(device, &self.constant_layout));
+ }
+
+ let layer = &mut self.layers[self.prepare_layer];
+ layer.prepare(device, queue, quads, transformation, scale);
+
+ self.prepare_layer += 1;
+ }
+
+ pub fn render<'a>(
+ &'a self,
+ layer: usize,
+ bounds: Rectangle<u32>,
+ quads: &Batch,
+ render_pass: &mut wgpu::RenderPass<'a>,
+ ) {
+ if let Some(layer) = self.layers.get(layer) {
+ render_pass.set_scissor_rect(
+ bounds.x,
+ bounds.y,
+ bounds.width,
+ bounds.height,
+ );
+ render_pass.set_index_buffer(
+ self.indices.slice(..),
+ wgpu::IndexFormat::Uint16,
+ );
+ render_pass.set_vertex_buffer(0, self.vertices.slice(..));
+
+ let mut solid_offset = 0;
+ let mut gradient_offset = 0;
+
+ for (kind, count) in &quads.order {
+ match kind {
+ Kind::Solid => {
+ self.solid.render(
+ render_pass,
+ &layer.constants,
+ &layer.solid,
+ solid_offset..(solid_offset + count),
+ );
+
+ solid_offset += count;
+ }
+ Kind::Gradient => {
+ self.gradient.render(
+ render_pass,
+ &layer.constants,
+ &layer.gradient,
+ gradient_offset..(gradient_offset + count),
+ );
+
+ gradient_offset += count;
+ }
+ }
+ }
+ }
+ }
+
+ pub fn end_frame(&mut self) {
+ self.prepare_layer = 0;
+ }
+}
+
+#[derive(Debug)]
+struct Layer {
+ constants: wgpu::BindGroup,
+ constants_buffer: wgpu::Buffer,
+ solid: solid::Layer,
+ gradient: gradient::Layer,
+}
+
+impl Layer {
+ pub fn new(
+ device: &wgpu::Device,
+ constant_layout: &wgpu::BindGroupLayout,
+ ) -> Self {
+ let constants_buffer = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("iced_wgpu::quad uniforms buffer"),
+ size: mem::size_of::<Uniforms>() as wgpu::BufferAddress,
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
- Pipeline {
- pipeline,
+ let constants = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("iced_wgpu::quad uniforms bind group"),
+ layout: constant_layout,
+ entries: &[wgpu::BindGroupEntry {
+ binding: 0,
+ resource: constants_buffer.as_entire_binding(),
+ }],
+ });
+
+ Self {
constants,
constants_buffer,
- vertices,
- indices,
- instances,
+ solid: solid::Layer::new(device),
+ gradient: gradient::Layer::new(device),
}
}
- pub fn draw(
+ pub fn prepare(
&mut self,
device: &wgpu::Device,
- staging_belt: &mut wgpu::util::StagingBelt,
- encoder: &mut wgpu::CommandEncoder,
- instances: &[layer::Quad],
+ queue: &wgpu::Queue,
+ quads: &Batch,
transformation: Transformation,
scale: f32,
- bounds: Rectangle<u32>,
- target: &wgpu::TextureView,
) {
#[cfg(feature = "tracing")]
- let _ = info_span!("Wgpu::Quad", "DRAW").entered();
+ let _ = info_span!("Wgpu::Quad", "PREPARE").entered();
let uniforms = Uniforms::new(transformation, scale);
- {
- let mut constants_buffer = staging_belt.write_buffer(
- encoder,
- &self.constants_buffer,
- 0,
- wgpu::BufferSize::new(mem::size_of::<Uniforms>() as u64)
- .unwrap(),
- device,
- );
+ queue.write_buffer(
+ &self.constants_buffer,
+ 0,
+ bytemuck::bytes_of(&uniforms),
+ );
- constants_buffer.copy_from_slice(bytemuck::bytes_of(&uniforms));
- }
+ self.solid.prepare(device, queue, &quads.solids);
+ self.gradient.prepare(device, queue, &quads.gradients);
+ }
+}
- let mut i = 0;
- let total = instances.len();
+/// The properties of a quad.
+#[derive(Clone, Copy, Debug, Pod, Zeroable)]
+#[repr(C)]
+pub struct Quad {
+ /// The position of the [`Quad`].
+ pub position: [f32; 2],
- while i < total {
- let end = (i + MAX_INSTANCES).min(total);
- let amount = end - i;
+ /// The size of the [`Quad`].
+ pub size: [f32; 2],
- let instance_bytes = bytemuck::cast_slice(&instances[i..end]);
+ /// The border color of the [`Quad`], in __linear RGB__.
+ pub border_color: color::Packed,
- let mut instance_buffer = staging_belt.write_buffer(
- encoder,
- &self.instances,
- 0,
- wgpu::BufferSize::new(instance_bytes.len() as u64).unwrap(),
- device,
- );
+ /// The border radii of the [`Quad`].
+ pub border_radius: [f32; 4],
+
+ /// The border width of the [`Quad`].
+ pub border_width: f32,
+}
+
+/// A group of [`Quad`]s rendered together.
+#[derive(Default, Debug)]
+pub struct Batch {
+ /// The solid quads of the [`Layer`].
+ solids: Vec<Solid>,
+
+ /// The gradient quads of the [`Layer`].
+ gradients: Vec<Gradient>,
+
+ /// The quad order of the [`Layer`]; stored as a tuple of the quad type & its count.
+ order: Vec<(Kind, usize)>,
+}
+
+impl Batch {
+ /// Returns true if there are no quads of any type in [`Quads`].
+ pub fn is_empty(&self) -> bool {
+ self.solids.is_empty() && self.gradients.is_empty()
+ }
+
+ /// Adds a [`Quad`] with the provided `Background` type to the quad [`Layer`].
+ pub fn add(&mut self, quad: Quad, background: &Background) {
+ let kind = match background {
+ Background::Color(color) => {
+ self.solids.push(Solid {
+ color: color::pack(*color),
+ quad,
+ });
- instance_buffer.copy_from_slice(instance_bytes);
-
- #[cfg(feature = "tracing")]
- let _ = info_span!("Wgpu::Quad", "BEGIN_RENDER_PASS").enter();
-
- {
- let mut render_pass =
- encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
- label: Some("iced_wgpu::quad render pass"),
- color_attachments: &[Some(
- wgpu::RenderPassColorAttachment {
- view: target,
- resolve_target: None,
- ops: wgpu::Operations {
- load: wgpu::LoadOp::Load,
- store: true,
- },
- },
- )],
- depth_stencil_attachment: None,
- });
-
- render_pass.set_pipeline(&self.pipeline);
- render_pass.set_bind_group(0, &self.constants, &[]);
- render_pass.set_index_buffer(
- self.indices.slice(..),
- wgpu::IndexFormat::Uint16,
- );
- render_pass.set_vertex_buffer(0, self.vertices.slice(..));
- render_pass.set_vertex_buffer(1, self.instances.slice(..));
-
- render_pass.set_scissor_rect(
- bounds.x,
- bounds.y,
- bounds.width,
- // TODO: Address anti-aliasing adjustments properly
- bounds.height,
- );
-
- render_pass.draw_indexed(
- 0..QUAD_INDICES.len() as u32,
- 0,
- 0..amount as u32,
- );
+ Kind::Solid
}
+ Background::Gradient(gradient) => {
+ self.gradients.push(Gradient {
+ gradient: graphics::gradient::pack(
+ gradient,
+ Rectangle::new(quad.position.into(), quad.size.into()),
+ ),
+ quad,
+ });
+
+ Kind::Gradient
+ }
+ };
- i += MAX_INSTANCES;
+ match self.order.last_mut() {
+ Some((last_kind, count)) if kind == *last_kind => {
+ *count += 1;
+ }
+ _ => {
+ self.order.push((kind, 1));
+ }
}
}
}
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+/// The kind of a quad.
+enum Kind {
+ /// A solid quad
+ Solid,
+ /// A gradient quad
+ Gradient,
+}
+
+fn color_target_state(
+ format: wgpu::TextureFormat,
+) -> [Option<wgpu::ColorTargetState>; 1] {
+ [Some(wgpu::ColorTargetState {
+ format,
+ blend: Some(wgpu::BlendState {
+ color: wgpu::BlendComponent {
+ src_factor: wgpu::BlendFactor::SrcAlpha,
+ dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
+ operation: wgpu::BlendOperation::Add,
+ },
+ alpha: wgpu::BlendComponent {
+ src_factor: wgpu::BlendFactor::One,
+ dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
+ operation: wgpu::BlendOperation::Add,
+ },
+ }),
+ write_mask: wgpu::ColorWrites::ALL,
+ })]
+}
+
#[repr(C)]
-#[derive(Clone, Copy, Zeroable, Pod)]
+#[derive(Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
pub struct Vertex {
_position: [f32; 2],
}
-const QUAD_INDICES: [u16; 6] = [0, 1, 2, 0, 2, 3];
+impl Vertex {
+ fn buffer_layout<'a>() -> wgpu::VertexBufferLayout<'a> {
+ wgpu::VertexBufferLayout {
+ array_stride: mem::size_of::<Self>() as u64,
+ step_mode: wgpu::VertexStepMode::Vertex,
+ attributes: &[wgpu::VertexAttribute {
+ shader_location: 0,
+ format: wgpu::VertexFormat::Float32x2,
+ offset: 0,
+ }],
+ }
+ }
+}
+
+const INDICES: [u16; 6] = [0, 1, 2, 0, 2, 3];
-const QUAD_VERTS: [Vertex; 4] = [
+const VERTICES: [Vertex; 4] = [
Vertex {
_position: [0.0, 0.0],
},
@@ -285,10 +348,8 @@ const QUAD_VERTS: [Vertex; 4] = [
},
];
-const MAX_INSTANCES: usize = 100_000;
-
#[repr(C)]
-#[derive(Debug, Clone, Copy, Zeroable, Pod)]
+#[derive(Debug, Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
struct Uniforms {
transform: [f32; 16],
scale: f32,
diff --git a/wgpu/src/quad/gradient.rs b/wgpu/src/quad/gradient.rs
new file mode 100644
index 00000000..6db37252
--- /dev/null
+++ b/wgpu/src/quad/gradient.rs
@@ -0,0 +1,165 @@
+use crate::graphics::gradient;
+use crate::quad::{self, Quad};
+use crate::Buffer;
+
+use bytemuck::{Pod, Zeroable};
+use std::ops::Range;
+
+/// A quad filled with interpolated colors.
+#[derive(Clone, Copy, Debug)]
+#[repr(C)]
+pub struct Gradient {
+ /// The background gradient data of the quad.
+ pub gradient: gradient::Packed,
+
+ /// The [`Quad`] data of the [`Gradient`].
+ pub quad: Quad,
+}
+
+#[allow(unsafe_code)]
+unsafe impl Pod for Gradient {}
+
+#[allow(unsafe_code)]
+unsafe impl Zeroable for Gradient {}
+
+#[derive(Debug)]
+pub struct Layer {
+ instances: Buffer<Gradient>,
+ instance_count: usize,
+}
+
+impl Layer {
+ pub fn new(device: &wgpu::Device) -> Self {
+ let instances = Buffer::new(
+ device,
+ "iced_wgpu.quad.gradient.buffer",
+ quad::INITIAL_INSTANCES,
+ wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+ );
+
+ Self {
+ instances,
+ instance_count: 0,
+ }
+ }
+
+ pub fn prepare(
+ &mut self,
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ instances: &[Gradient],
+ ) {
+ let _ = self.instances.resize(device, instances.len());
+ let _ = self.instances.write(queue, 0, instances);
+
+ self.instance_count = instances.len();
+ }
+}
+
+#[derive(Debug)]
+pub struct Pipeline {
+ pipeline: wgpu::RenderPipeline,
+}
+
+impl Pipeline {
+ pub fn new(
+ device: &wgpu::Device,
+ format: wgpu::TextureFormat,
+ constants_layout: &wgpu::BindGroupLayout,
+ ) -> Self {
+ let layout =
+ device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("iced_wgpu.quad.gradient.pipeline"),
+ push_constant_ranges: &[],
+ bind_group_layouts: &[constants_layout],
+ });
+
+ let shader =
+ device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("iced_wgpu.quad.gradient.shader"),
+ source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
+ include_str!("../shader/quad.wgsl"),
+ )),
+ });
+
+ let pipeline =
+ device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: Some("iced_wgpu.quad.gradient.pipeline"),
+ layout: Some(&layout),
+ vertex: wgpu::VertexState {
+ module: &shader,
+ entry_point: "gradient_vs_main",
+ buffers: &[
+ quad::Vertex::buffer_layout(),
+ wgpu::VertexBufferLayout {
+ array_stride: std::mem::size_of::<Gradient>()
+ as u64,
+ step_mode: wgpu::VertexStepMode::Instance,
+ attributes: &wgpu::vertex_attr_array!(
+ // Colors 1-2
+ 1 => Uint32x4,
+ // Colors 3-4
+ 2 => Uint32x4,
+ // Colors 5-6
+ 3 => Uint32x4,
+ // Colors 7-8
+ 4 => Uint32x4,
+ // Offsets 1-8
+ 5 => Uint32x4,
+ // Direction
+ 6 => Float32x4,
+ // Position & Scale
+ 7 => Float32x4,
+ // Border color
+ 8 => Float32x4,
+ // Border radius
+ 9 => Float32x4,
+ // Border width
+ 10 => Float32
+ ),
+ },
+ ],
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: &shader,
+ entry_point: "gradient_fs_main",
+ targets: &quad::color_target_state(format),
+ }),
+ primitive: wgpu::PrimitiveState {
+ topology: wgpu::PrimitiveTopology::TriangleList,
+ front_face: wgpu::FrontFace::Cw,
+ ..Default::default()
+ },
+ depth_stencil: None,
+ multisample: wgpu::MultisampleState {
+ count: 1,
+ mask: !0,
+ alpha_to_coverage_enabled: false,
+ },
+ multiview: None,
+ });
+
+ Self { pipeline }
+ }
+
+ pub fn render<'a>(
+ &'a self,
+ render_pass: &mut wgpu::RenderPass<'a>,
+ constants: &'a wgpu::BindGroup,
+ layer: &'a Layer,
+ range: Range<usize>,
+ ) {
+ #[cfg(feature = "tracing")]
+ let _ = tracing::info_span!("Wgpu::Quad::Gradient", "DRAW").entered();
+
+ render_pass.set_pipeline(&self.pipeline);
+ render_pass.set_bind_group(0, constants, &[]);
+ render_pass.set_vertex_buffer(1, layer.instances.slice(..));
+
+ render_pass.draw_indexed(
+ 0..quad::INDICES.len() as u32,
+ 0,
+ range.start as u32..range.end as u32,
+ );
+ }
+}
diff --git a/wgpu/src/quad/solid.rs b/wgpu/src/quad/solid.rs
new file mode 100644
index 00000000..f8f1e3a5
--- /dev/null
+++ b/wgpu/src/quad/solid.rs
@@ -0,0 +1,150 @@
+use crate::graphics::color;
+use crate::quad::{self, Quad};
+use crate::Buffer;
+
+use bytemuck::{Pod, Zeroable};
+use std::ops::Range;
+
+/// A quad filled with a solid color.
+#[derive(Clone, Copy, Debug, Pod, Zeroable)]
+#[repr(C)]
+pub struct Solid {
+ /// The background color data of the quad.
+ pub color: color::Packed,
+
+ /// The [`Quad`] data of the [`Solid`].
+ pub quad: Quad,
+}
+
+#[derive(Debug)]
+pub struct Layer {
+ instances: Buffer<Solid>,
+ instance_count: usize,
+}
+
+impl Layer {
+ pub fn new(device: &wgpu::Device) -> Self {
+ let instances = Buffer::new(
+ device,
+ "iced_wgpu.quad.solid.buffer",
+ quad::INITIAL_INSTANCES,
+ wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+ );
+
+ Self {
+ instances,
+ instance_count: 0,
+ }
+ }
+
+ pub fn prepare(
+ &mut self,
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ instances: &[Solid],
+ ) {
+ let _ = self.instances.resize(device, instances.len());
+ let _ = self.instances.write(queue, 0, instances);
+
+ self.instance_count = instances.len();
+ }
+}
+
+#[derive(Debug)]
+pub struct Pipeline {
+ pipeline: wgpu::RenderPipeline,
+}
+
+impl Pipeline {
+ pub fn new(
+ device: &wgpu::Device,
+ format: wgpu::TextureFormat,
+ constants_layout: &wgpu::BindGroupLayout,
+ ) -> Self {
+ let layout =
+ device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("iced_wgpu.quad.solid.pipeline"),
+ push_constant_ranges: &[],
+ bind_group_layouts: &[constants_layout],
+ });
+
+ let shader =
+ device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("iced_wgpu.quad.solid.shader"),
+ source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
+ include_str!("../shader/quad.wgsl"),
+ )),
+ });
+
+ let pipeline =
+ device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: Some("iced_wgpu.quad.solid.pipeline"),
+ layout: Some(&layout),
+ vertex: wgpu::VertexState {
+ module: &shader,
+ entry_point: "solid_vs_main",
+ buffers: &[
+ quad::Vertex::buffer_layout(),
+ wgpu::VertexBufferLayout {
+ array_stride: std::mem::size_of::<Solid>() as u64,
+ step_mode: wgpu::VertexStepMode::Instance,
+ attributes: &wgpu::vertex_attr_array!(
+ // Color
+ 1 => Float32x4,
+ // Position
+ 2 => Float32x2,
+ // Size
+ 3 => Float32x2,
+ // Border color
+ 4 => Float32x4,
+ // Border radius
+ 5 => Float32x4,
+ // Border width
+ 6 => Float32,
+ ),
+ },
+ ],
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: &shader,
+ entry_point: "solid_fs_main",
+ targets: &quad::color_target_state(format),
+ }),
+ primitive: wgpu::PrimitiveState {
+ topology: wgpu::PrimitiveTopology::TriangleList,
+ front_face: wgpu::FrontFace::Cw,
+ ..Default::default()
+ },
+ depth_stencil: None,
+ multisample: wgpu::MultisampleState {
+ count: 1,
+ mask: !0,
+ alpha_to_coverage_enabled: false,
+ },
+ multiview: None,
+ });
+
+ Self { pipeline }
+ }
+
+ pub fn render<'a>(
+ &'a self,
+ render_pass: &mut wgpu::RenderPass<'a>,
+ constants: &'a wgpu::BindGroup,
+ layer: &'a Layer,
+ range: Range<usize>,
+ ) {
+ #[cfg(feature = "tracing")]
+ let _ = tracing::info_span!("Wgpu::Quad::Solid", "DRAW").entered();
+
+ render_pass.set_pipeline(&self.pipeline);
+ render_pass.set_bind_group(0, constants, &[]);
+ render_pass.set_vertex_buffer(1, layer.instances.slice(..));
+
+ render_pass.draw_indexed(
+ 0..quad::INDICES.len() as u32,
+ 0,
+ range.start as u32..range.end as u32,
+ );
+ }
+}
diff --git a/wgpu/src/settings.rs b/wgpu/src/settings.rs
index 5ef79499..266a2c87 100644
--- a/wgpu/src/settings.rs
+++ b/wgpu/src/settings.rs
@@ -1,12 +1,11 @@
//! Configure a renderer.
-use std::fmt;
-
-pub use crate::Antialiasing;
+use crate::core::Font;
+use crate::graphics::Antialiasing;
/// The settings of a [`Backend`].
///
/// [`Backend`]: crate::Backend
-#[derive(Clone, Copy, PartialEq)]
+#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Settings {
/// The present mode of the [`Backend`].
///
@@ -16,42 +15,20 @@ pub struct Settings {
/// The internal graphics backend to use.
pub internal_backend: wgpu::Backends,
- /// 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 [`Font`] to use.
+ pub default_font: Font,
/// The default size of text.
///
/// By default, it will be set to `16.0`.
pub default_text_size: f32,
- /// If enabled, spread text workload in multiple threads when multiple cores
- /// are available.
- ///
- /// By default, it is disabled.
- pub text_multithreading: bool,
-
/// The antialiasing strategy that will be used for triangle primitives.
///
/// By default, it is `None`.
pub antialiasing: Option<Antialiasing>,
}
-impl fmt::Debug for Settings {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_struct("Settings")
- .field("present_mode", &self.present_mode)
- .field("internal_backend", &self.internal_backend)
- // Instead of printing the font bytes, we simply show a `bool` indicating if using a default font or not.
- .field("default_font", &self.default_font.is_some())
- .field("default_text_size", &self.default_text_size)
- .field("text_multithreading", &self.text_multithreading)
- .field("antialiasing", &self.antialiasing)
- .finish()
- }
-}
-
impl Settings {
/// Creates new [`Settings`] using environment configuration.
///
@@ -69,7 +46,7 @@ impl Settings {
/// - `primary`
pub fn from_env() -> Self {
Settings {
- internal_backend: backend_from_env()
+ internal_backend: wgpu::util::backend_bits_from_env()
.unwrap_or(wgpu::Backends::all()),
..Self::default()
}
@@ -81,25 +58,9 @@ impl Default for Settings {
Settings {
present_mode: wgpu::PresentMode::AutoVsync,
internal_backend: wgpu::Backends::all(),
- default_font: None,
- default_text_size: 20.0,
- text_multithreading: false,
+ default_font: Font::default(),
+ default_text_size: 16.0,
antialiasing: None,
}
}
}
-
-fn backend_from_env() -> Option<wgpu::Backends> {
- std::env::var("WGPU_BACKEND").ok().map(|backend| {
- match backend.to_lowercase().as_str() {
- "vulkan" => wgpu::Backends::VULKAN,
- "metal" => wgpu::Backends::METAL,
- "dx12" => wgpu::Backends::DX12,
- "dx11" => wgpu::Backends::DX11,
- "gl" => wgpu::Backends::GL,
- "webgpu" => wgpu::Backends::BROWSER_WEBGPU,
- "primary" => wgpu::Backends::PRIMARY,
- other => panic!("Unknown backend: {other}"),
- }
- })
-}
diff --git a/wgpu/src/shader/gradient.wgsl b/wgpu/src/shader/gradient.wgsl
deleted file mode 100644
index 63825aec..00000000
--- a/wgpu/src/shader/gradient.wgsl
+++ /dev/null
@@ -1,88 +0,0 @@
-struct Uniforms {
- transform: mat4x4<f32>,
- //xy = start, wz = end
- position: vec4<f32>,
- //x = start stop, y = end stop, zw = padding
- stop_range: vec4<i32>,
-}
-
-struct Stop {
- color: vec4<f32>,
- offset: f32,
-};
-
-@group(0) @binding(0)
-var<uniform> uniforms: Uniforms;
-
-@group(0) @binding(1)
-var<storage, read> color_stops: array<Stop>;
-
-struct VertexOutput {
- @builtin(position) position: vec4<f32>,
- @location(0) raw_position: vec2<f32>
-}
-
-@vertex
-fn vs_main(@location(0) input: vec2<f32>) -> VertexOutput {
- var output: VertexOutput;
- output.position = uniforms.transform * vec4<f32>(input.xy, 0.0, 1.0);
- output.raw_position = input;
-
- return output;
-}
-
-//TODO: rewrite without branching
-@fragment
-fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
- let start = uniforms.position.xy;
- let end = uniforms.position.zw;
- let start_stop = uniforms.stop_range.x;
- let end_stop = uniforms.stop_range.y;
-
- let v1 = end - start;
- let v2 = input.raw_position.xy - start;
- let unit = normalize(v1);
- let offset = dot(unit, v2) / length(v1);
-
- let min_stop = color_stops[start_stop];
- let max_stop = color_stops[end_stop];
-
- var color: vec4<f32>;
-
- if (offset <= min_stop.offset) {
- color = min_stop.color;
- } else if (offset >= max_stop.offset) {
- color = max_stop.color;
- } else {
- var min = min_stop;
- var max = max_stop;
- var min_index = start_stop;
- var max_index = end_stop;
-
- loop {
- if (min_index >= max_index - 1) {
- break;
- }
-
- let index = min_index + (max_index - min_index) / 2;
-
- let stop = color_stops[index];
-
- if (offset <= stop.offset) {
- max = stop;
- max_index = index;
- } else {
- min = stop;
- min_index = index;
- }
- }
-
- color = mix(min.color, max.color, smoothstep(
- min.offset,
- max.offset,
- offset
- ));
- }
-
- return color;
-}
diff --git a/wgpu/src/shader/quad.wgsl b/wgpu/src/shader/quad.wgsl
index cf4f7e4d..fb402158 100644
--- a/wgpu/src/shader/quad.wgsl
+++ b/wgpu/src/shader/quad.wgsl
@@ -5,17 +5,57 @@ struct Globals {
@group(0) @binding(0) var<uniform> globals: Globals;
-struct VertexInput {
+fn distance_alg(
+ frag_coord: vec2<f32>,
+ position: vec2<f32>,
+ size: vec2<f32>,
+ radius: f32
+) -> f32 {
+ var inner_size: vec2<f32> = size - vec2<f32>(radius, radius) * 2.0;
+ var top_left: vec2<f32> = position + vec2<f32>(radius, radius);
+ var bottom_right: vec2<f32> = top_left + inner_size;
+
+ var top_left_distance: vec2<f32> = top_left - frag_coord;
+ var bottom_right_distance: vec2<f32> = frag_coord - bottom_right;
+
+ var dist: vec2<f32> = vec2<f32>(
+ max(max(top_left_distance.x, bottom_right_distance.x), 0.0),
+ max(max(top_left_distance.y, bottom_right_distance.y), 0.0)
+ );
+
+ return sqrt(dist.x * dist.x + dist.y * dist.y);
+}
+
+// Based on the fragement position and the center of the quad, select one of the 4 radi.
+// Order matches CSS border radius attribute:
+// radi.x = top-left, radi.y = top-right, radi.z = bottom-right, radi.w = bottom-left
+fn select_border_radius(radi: vec4<f32>, position: vec2<f32>, center: vec2<f32>) -> f32 {
+ var rx = radi.x;
+ var ry = radi.y;
+ rx = select(radi.x, radi.y, position.x > center.x);
+ ry = select(radi.w, radi.z, position.x > center.x);
+ rx = select(rx, ry, position.y > center.y);
+ return rx;
+}
+
+fn unpack_u32(color: vec2<u32>) -> vec4<f32> {
+ let rg: vec2<f32> = unpack2x16float(color.x);
+ let ba: vec2<f32> = unpack2x16float(color.y);
+
+ return vec4<f32>(rg.y, rg.x, ba.y, ba.x);
+}
+
+struct SolidVertexInput {
@location(0) v_pos: vec2<f32>,
- @location(1) pos: vec2<f32>,
- @location(2) scale: vec2<f32>,
- @location(3) color: vec4<f32>,
+ @location(1) color: vec4<f32>,
+ @location(2) pos: vec2<f32>,
+ @location(3) scale: vec2<f32>,
@location(4) border_color: vec4<f32>,
@location(5) border_radius: vec4<f32>,
@location(6) border_width: f32,
}
-struct VertexOutput {
+struct SolidVertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) color: vec4<f32>,
@location(1) border_color: vec4<f32>,
@@ -26,8 +66,8 @@ struct VertexOutput {
}
@vertex
-fn vs_main(input: VertexInput) -> VertexOutput {
- var out: VertexOutput;
+fn solid_vs_main(input: SolidVertexInput) -> SolidVertexOutput {
+ var out: SolidVertexOutput;
var pos: vec2<f32> = input.pos * globals.scale;
var scale: vec2<f32> = input.scale * globals.scale;
@@ -47,54 +87,20 @@ fn vs_main(input: VertexInput) -> VertexOutput {
vec4<f32>(pos - vec2<f32>(0.5, 0.5), 0.0, 1.0)
);
+ out.position = globals.transform * transform * vec4<f32>(input.v_pos, 0.0, 1.0);
out.color = input.color;
out.border_color = input.border_color;
out.pos = pos;
out.scale = scale;
out.border_radius = border_radius * globals.scale;
out.border_width = input.border_width * globals.scale;
- out.position = globals.transform * transform * vec4<f32>(input.v_pos, 0.0, 1.0);
return out;
}
-fn distance_alg(
- frag_coord: vec2<f32>,
- position: vec2<f32>,
- size: vec2<f32>,
- radius: f32
-) -> f32 {
- var inner_size: vec2<f32> = size - vec2<f32>(radius, radius) * 2.0;
- var top_left: vec2<f32> = position + vec2<f32>(radius, radius);
- var bottom_right: vec2<f32> = top_left + inner_size;
-
- var top_left_distance: vec2<f32> = top_left - frag_coord;
- var bottom_right_distance: vec2<f32> = frag_coord - bottom_right;
-
- var dist: vec2<f32> = vec2<f32>(
- max(max(top_left_distance.x, bottom_right_distance.x), 0.0),
- max(max(top_left_distance.y, bottom_right_distance.y), 0.0)
- );
-
- return sqrt(dist.x * dist.x + dist.y * dist.y);
-}
-
-// Based on the fragement position and the center of the quad, select one of the 4 radi.
-// Order matches CSS border radius attribute:
-// radi.x = top-left, radi.y = top-right, radi.z = bottom-right, radi.w = bottom-left
-fn select_border_radius(radi: vec4<f32>, position: vec2<f32>, center: vec2<f32>) -> f32 {
- var rx = radi.x;
- var ry = radi.y;
- rx = select(radi.x, radi.y, position.x > center.x);
- ry = select(radi.w, radi.z, position.x > center.x);
- rx = select(rx, ry, position.y > center.y);
- return rx;
-}
-
-
@fragment
-fn fs_main(
- input: VertexOutput
+fn solid_fs_main(
+ input: SolidVertexOutput
) -> @location(0) vec4<f32> {
var mixed_color: vec4<f32> = input.color;
@@ -138,3 +144,202 @@ fn fs_main(
return vec4<f32>(mixed_color.x, mixed_color.y, mixed_color.z, mixed_color.w * radius_alpha);
}
+
+struct GradientVertexInput {
+ @location(0) v_pos: vec2<f32>,
+ @location(1) colors_1: vec4<u32>,
+ @location(2) colors_2: vec4<u32>,
+ @location(3) colors_3: vec4<u32>,
+ @location(4) colors_4: vec4<u32>,
+ @location(5) offsets: vec4<u32>,
+ @location(6) direction: vec4<f32>,
+ @location(7) position_and_scale: vec4<f32>,
+ @location(8) border_color: vec4<f32>,
+ @location(9) border_radius: vec4<f32>,
+ @location(10) border_width: f32,
+}
+
+struct GradientVertexOutput {
+ @builtin(position) position: vec4<f32>,
+ @location(1) colors_1: vec4<u32>,
+ @location(2) colors_2: vec4<u32>,
+ @location(3) colors_3: vec4<u32>,
+ @location(4) colors_4: vec4<u32>,
+ @location(5) offsets: vec4<u32>,
+ @location(6) direction: vec4<f32>,
+ @location(7) position_and_scale: vec4<f32>,
+ @location(8) border_color: vec4<f32>,
+ @location(9) border_radius: vec4<f32>,
+ @location(10) border_width: f32,
+}
+
+@vertex
+fn gradient_vs_main(input: GradientVertexInput) -> GradientVertexOutput {
+ var out: GradientVertexOutput;
+
+ var pos: vec2<f32> = input.position_and_scale.xy * globals.scale;
+ var scale: vec2<f32> = input.position_and_scale.zw * globals.scale;
+
+ var min_border_radius = min(input.position_and_scale.z, input.position_and_scale.w) * 0.5;
+ var border_radius: vec4<f32> = vec4<f32>(
+ min(input.border_radius.x, min_border_radius),
+ min(input.border_radius.y, min_border_radius),
+ min(input.border_radius.z, min_border_radius),
+ min(input.border_radius.w, min_border_radius)
+ );
+
+ var transform: mat4x4<f32> = mat4x4<f32>(
+ vec4<f32>(scale.x + 1.0, 0.0, 0.0, 0.0),
+ vec4<f32>(0.0, scale.y + 1.0, 0.0, 0.0),
+ vec4<f32>(0.0, 0.0, 1.0, 0.0),
+ vec4<f32>(pos - vec2<f32>(0.5, 0.5), 0.0, 1.0)
+ );
+
+ out.position = globals.transform * transform * vec4<f32>(input.v_pos, 0.0, 1.0);
+ out.colors_1 = input.colors_1;
+ out.colors_2 = input.colors_2;
+ out.colors_3 = input.colors_3;
+ out.colors_4 = input.colors_4;
+ out.offsets = input.offsets;
+ out.direction = input.direction * globals.scale;
+ out.position_and_scale = vec4<f32>(pos, scale);
+ out.border_color = input.border_color;
+ out.border_radius = border_radius * globals.scale;
+ out.border_width = input.border_width * globals.scale;
+
+ return out;
+}
+
+fn random(coords: vec2<f32>) -> f32 {
+ return fract(sin(dot(coords, vec2(12.9898,78.233))) * 43758.5453);
+}
+
+/// Returns the current interpolated color with a max 8-stop gradient
+fn gradient(
+ raw_position: vec2<f32>,
+ direction: vec4<f32>,
+ colors: array<vec4<f32>, 8>,
+ offsets: array<f32, 8>,
+ last_index: i32
+) -> vec4<f32> {
+ let start = direction.xy;
+ let end = direction.zw;
+
+ let v1 = end - start;
+ let v2 = raw_position - start;
+ let unit = normalize(v1);
+ let coord_offset = dot(unit, v2) / length(v1);
+
+ //need to store these as a var to use dynamic indexing in a loop
+ //this is already added to wgsl spec but not in wgpu yet
+ var colors_arr = colors;
+ var offsets_arr = offsets;
+
+ var color: vec4<f32>;
+
+ let noise_granularity: f32 = 0.3/255.0;
+
+ for (var i: i32 = 0; i < last_index; i++) {
+ let curr_offset = offsets_arr[i];
+ let next_offset = offsets_arr[i+1];
+
+ if (coord_offset <= offsets_arr[0]) {
+ color = colors_arr[0];
+ }
+
+ if (curr_offset <= coord_offset && coord_offset <= next_offset) {
+ color = mix(colors_arr[i], colors_arr[i+1], smoothstep(
+ curr_offset,
+ next_offset,
+ coord_offset,
+ ));
+ }
+
+ if (coord_offset >= offsets_arr[last_index]) {
+ color = colors_arr[last_index];
+ }
+ }
+
+ return color + mix(-noise_granularity, noise_granularity, random(raw_position));
+}
+
+@fragment
+fn gradient_fs_main(input: GradientVertexOutput) -> @location(0) vec4<f32> {
+ let colors = array<vec4<f32>, 8>(
+ unpack_u32(input.colors_1.xy),
+ unpack_u32(input.colors_1.zw),
+ unpack_u32(input.colors_2.xy),
+ unpack_u32(input.colors_2.zw),
+ unpack_u32(input.colors_3.xy),
+ unpack_u32(input.colors_3.zw),
+ unpack_u32(input.colors_4.xy),
+ unpack_u32(input.colors_4.zw),
+ );
+
+ let offsets_1: vec4<f32> = unpack_u32(input.offsets.xy);
+ let offsets_2: vec4<f32> = unpack_u32(input.offsets.zw);
+
+ var offsets = array<f32, 8>(
+ offsets_1.x,
+ offsets_1.y,
+ offsets_1.z,
+ offsets_1.w,
+ offsets_2.x,
+ offsets_2.y,
+ offsets_2.z,
+ offsets_2.w,
+ );
+
+ //TODO could just pass this in to the shader but is probably more performant to just check it here
+ var last_index = 7;
+ for (var i: i32 = 0; i <= 7; i++) {
+ if (offsets[i] > 1.0) {
+ last_index = i - 1;
+ break;
+ }
+ }
+
+ var mixed_color: vec4<f32> = gradient(input.position.xy, input.direction, colors, offsets, last_index);
+
+ let pos = input.position_and_scale.xy;
+ let scale = input.position_and_scale.zw;
+
+ var border_radius = select_border_radius(
+ input.border_radius,
+ input.position.xy,
+ (pos + scale * 0.5).xy
+ );
+
+ if (input.border_width > 0.0) {
+ var internal_border: f32 = max(border_radius - input.border_width, 0.0);
+
+ var internal_distance: f32 = distance_alg(
+ input.position.xy,
+ pos + vec2<f32>(input.border_width, input.border_width),
+ scale - vec2<f32>(input.border_width * 2.0, input.border_width * 2.0),
+ internal_border
+ );
+
+ var border_mix: f32 = smoothstep(
+ max(internal_border - 0.5, 0.0),
+ internal_border + 0.5,
+ internal_distance
+ );
+
+ mixed_color = mix(mixed_color, input.border_color, vec4<f32>(border_mix, border_mix, border_mix, border_mix));
+ }
+
+ var dist: f32 = distance_alg(
+ input.position.xy,
+ pos,
+ scale,
+ border_radius
+ );
+
+ var radius_alpha: f32 = 1.0 - smoothstep(
+ max(border_radius - 0.5, 0.0),
+ border_radius + 0.5,
+ dist);
+
+ return vec4<f32>(mixed_color.x, mixed_color.y, mixed_color.z, mixed_color.w * radius_alpha);
+}
diff --git a/wgpu/src/shader/solid.wgsl b/wgpu/src/shader/solid.wgsl
deleted file mode 100644
index b24402f8..00000000
--- a/wgpu/src/shader/solid.wgsl
+++ /dev/null
@@ -1,30 +0,0 @@
-struct Globals {
- transform: mat4x4<f32>,
-}
-
-@group(0) @binding(0) var<uniform> globals: Globals;
-
-struct VertexInput {
- @location(0) position: vec2<f32>,
- @location(1) color: vec4<f32>,
-}
-
-struct VertexOutput {
- @builtin(position) position: vec4<f32>,
- @location(0) color: vec4<f32>,
-}
-
-@vertex
-fn vs_main(input: VertexInput) -> VertexOutput {
- var out: VertexOutput;
-
- out.color = input.color;
- out.position = globals.transform * vec4<f32>(input.position, 0.0, 1.0);
-
- return out;
-}
-
-@fragment
-fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
- return input.color;
-}
diff --git a/wgpu/src/shader/triangle.wgsl b/wgpu/src/shader/triangle.wgsl
new file mode 100644
index 00000000..9f512d14
--- /dev/null
+++ b/wgpu/src/shader/triangle.wgsl
@@ -0,0 +1,165 @@
+struct Globals {
+ transform: mat4x4<f32>,
+}
+
+@group(0) @binding(0) var<uniform> globals: Globals;
+
+fn unpack_u32(color: vec2<u32>) -> vec4<f32> {
+ let rg: vec2<f32> = unpack2x16float(color.x);
+ let ba: vec2<f32> = unpack2x16float(color.y);
+
+ return vec4<f32>(rg.y, rg.x, ba.y, ba.x);
+}
+
+struct SolidVertexInput {
+ @location(0) position: vec2<f32>,
+ @location(1) color: vec4<f32>,
+}
+
+struct SolidVertexOutput {
+ @builtin(position) position: vec4<f32>,
+ @location(0) color: vec4<f32>,
+}
+
+@vertex
+fn solid_vs_main(input: SolidVertexInput) -> SolidVertexOutput {
+ var out: SolidVertexOutput;
+
+ out.color = input.color;
+ out.position = globals.transform * vec4<f32>(input.position, 0.0, 1.0);
+
+ return out;
+}
+
+@fragment
+fn solid_fs_main(input: SolidVertexOutput) -> @location(0) vec4<f32> {
+ return input.color;
+}
+
+struct GradientVertexInput {
+ @location(0) v_pos: vec2<f32>,
+ @location(1) colors_1: vec4<u32>,
+ @location(2) colors_2: vec4<u32>,
+ @location(3) colors_3: vec4<u32>,
+ @location(4) colors_4: vec4<u32>,
+ @location(5) offsets: vec4<u32>,
+ @location(6) direction: vec4<f32>,
+}
+
+struct GradientVertexOutput {
+ @builtin(position) position: vec4<f32>,
+ @location(0) raw_position: vec2<f32>,
+ @location(1) colors_1: vec4<u32>,
+ @location(2) colors_2: vec4<u32>,
+ @location(3) colors_3: vec4<u32>,
+ @location(4) colors_4: vec4<u32>,
+ @location(5) offsets: vec4<u32>,
+ @location(6) direction: vec4<f32>,
+}
+
+@vertex
+fn gradient_vs_main(input: GradientVertexInput) -> GradientVertexOutput {
+ var output: GradientVertexOutput;
+
+ output.position = globals.transform * vec4<f32>(input.v_pos, 0.0, 1.0);
+ output.raw_position = input.v_pos;
+ output.colors_1 = input.colors_1;
+ output.colors_2 = input.colors_2;
+ output.colors_3 = input.colors_3;
+ output.colors_4 = input.colors_4;
+ output.offsets = input.offsets;
+ output.direction = input.direction;
+
+ return output;
+}
+
+fn random(coords: vec2<f32>) -> f32 {
+ return fract(sin(dot(coords, vec2(12.9898,78.233))) * 43758.5453);
+}
+
+/// Returns the current interpolated color with a max 8-stop gradient
+fn gradient(
+ raw_position: vec2<f32>,
+ direction: vec4<f32>,
+ colors: array<vec4<f32>, 8>,
+ offsets: array<f32, 8>,
+ last_index: i32
+) -> vec4<f32> {
+ let start = direction.xy;
+ let end = direction.zw;
+
+ let v1 = end - start;
+ let v2 = raw_position - start;
+ let unit = normalize(v1);
+ let coord_offset = dot(unit, v2) / length(v1);
+
+ //need to store these as a var to use dynamic indexing in a loop
+ //this is already added to wgsl spec but not in wgpu yet
+ var colors_arr = colors;
+ var offsets_arr = offsets;
+
+ var color: vec4<f32>;
+
+ let noise_granularity: f32 = 0.3/255.0;
+
+ for (var i: i32 = 0; i < last_index; i++) {
+ let curr_offset = offsets_arr[i];
+ let next_offset = offsets_arr[i+1];
+
+ if (coord_offset <= offsets_arr[0]) {
+ color = colors_arr[0];
+ }
+
+ if (curr_offset <= coord_offset && coord_offset <= next_offset) {
+ color = mix(colors_arr[i], colors_arr[i+1], smoothstep(
+ curr_offset,
+ next_offset,
+ coord_offset,
+ ));
+ }
+
+ if (coord_offset >= offsets_arr[last_index]) {
+ color = colors_arr[last_index];
+ }
+ }
+
+ return color + mix(-noise_granularity, noise_granularity, random(raw_position));
+}
+
+@fragment
+fn gradient_fs_main(input: GradientVertexOutput) -> @location(0) vec4<f32> {
+ let colors = array<vec4<f32>, 8>(
+ unpack_u32(input.colors_1.xy),
+ unpack_u32(input.colors_1.zw),
+ unpack_u32(input.colors_2.xy),
+ unpack_u32(input.colors_2.zw),
+ unpack_u32(input.colors_3.xy),
+ unpack_u32(input.colors_3.zw),
+ unpack_u32(input.colors_4.xy),
+ unpack_u32(input.colors_4.zw),
+ );
+
+ let offsets_1: vec4<f32> = unpack_u32(input.offsets.xy);
+ let offsets_2: vec4<f32> = unpack_u32(input.offsets.zw);
+
+ var offsets = array<f32, 8>(
+ offsets_1.x,
+ offsets_1.y,
+ offsets_1.z,
+ offsets_1.w,
+ offsets_2.x,
+ offsets_2.y,
+ offsets_2.z,
+ offsets_2.w,
+ );
+
+ var last_index = 7;
+ for (var i: i32 = 0; i <= 7; i++) {
+ if (offsets[i] >= 1.0) {
+ last_index = i;
+ break;
+ }
+ }
+
+ return gradient(input.raw_position, input.direction, colors, offsets, last_index);
+}
diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs
index e17b84c1..65d3b818 100644
--- a/wgpu/src/text.rs
+++ b/wgpu/src/text.rs
@@ -1,265 +1,506 @@
-use crate::Transformation;
-
-use iced_graphics::font;
-
-use std::{cell::RefCell, collections::HashMap};
-use wgpu_glyph::ab_glyph;
-
-pub use iced_native::text::Hit;
-
-#[derive(Debug)]
+use crate::core::alignment;
+use crate::core::font::{self, Font};
+use crate::core::text::{Hit, LineHeight, Shaping};
+use crate::core::{Pixels, Point, Rectangle, Size};
+use crate::graphics::color;
+use crate::layer::Text;
+
+use rustc_hash::{FxHashMap, FxHashSet};
+use std::borrow::Cow;
+use std::cell::RefCell;
+use std::collections::hash_map;
+use std::hash::{BuildHasher, Hash, Hasher};
+use std::sync::Arc;
+
+#[allow(missing_debug_implementations)]
pub struct Pipeline {
- draw_brush: RefCell<wgpu_glyph::GlyphBrush<()>>,
- draw_font_map: RefCell<HashMap<String, wgpu_glyph::FontId>>,
- measure_brush: RefCell<glyph_brush::GlyphBrush<()>>,
+ font_system: RefCell<glyphon::FontSystem>,
+ renderers: Vec<glyphon::TextRenderer>,
+ atlas: glyphon::TextAtlas,
+ prepare_layer: usize,
+ cache: RefCell<Cache>,
}
impl Pipeline {
pub fn new(
device: &wgpu::Device,
+ queue: &wgpu::Queue,
format: wgpu::TextureFormat,
- default_font: Option<&[u8]>,
- multithreading: bool,
) -> Self {
- let default_font = default_font.map(|slice| slice.to_vec());
-
- // TODO: Font customization
- #[cfg(not(target_os = "ios"))]
- #[cfg(feature = "default_system_font")]
- let default_font = {
- default_font.or_else(|| {
- font::Source::new()
- .load(&[font::Family::SansSerif, font::Family::Serif])
- .ok()
- })
- };
+ Pipeline {
+ font_system: RefCell::new(glyphon::FontSystem::new_with_fonts(
+ [glyphon::fontdb::Source::Binary(Arc::new(
+ include_bytes!("../fonts/Iced-Icons.ttf").as_slice(),
+ ))]
+ .into_iter(),
+ )),
+ renderers: Vec::new(),
+ atlas: glyphon::TextAtlas::new(
+ device,
+ queue,
+ format,
+ if color::GAMMA_CORRECTION {
+ glyphon::ColorMode::Accurate
+ } else {
+ glyphon::ColorMode::Web
+ },
+ ),
+ prepare_layer: 0,
+ cache: RefCell::new(Cache::new()),
+ }
+ }
- let default_font =
- default_font.unwrap_or_else(|| font::FALLBACK.to_vec());
+ pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
+ let _ = self.font_system.get_mut().db_mut().load_font_source(
+ glyphon::fontdb::Source::Binary(Arc::new(bytes.into_owned())),
+ );
- 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..."
- );
+ self.cache = RefCell::new(Cache::new());
+ }
- ab_glyph::FontArc::try_from_slice(font::FALLBACK)
- .expect("Load fallback font")
- });
+ pub fn prepare(
+ &mut self,
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ sections: &[Text<'_>],
+ bounds: Rectangle,
+ scale_factor: f32,
+ target_size: Size<u32>,
+ ) -> bool {
+ if self.renderers.len() <= self.prepare_layer {
+ self.renderers.push(glyphon::TextRenderer::new(
+ &mut self.atlas,
+ device,
+ Default::default(),
+ None,
+ ));
+ }
- let draw_brush_builder =
- wgpu_glyph::GlyphBrushBuilder::using_font(font.clone())
- .initial_cache_size((2048, 2048))
- .draw_cache_multithread(multithreading);
+ let font_system = self.font_system.get_mut();
+ let renderer = &mut self.renderers[self.prepare_layer];
+ let cache = self.cache.get_mut();
- #[cfg(target_arch = "wasm32")]
- let draw_brush_builder = draw_brush_builder.draw_cache_align_4x4(true);
+ if self.prepare_layer == 0 {
+ cache.trim(Purpose::Drawing);
+ }
- let draw_brush = draw_brush_builder.build(device, format);
+ let keys: Vec<_> = sections
+ .iter()
+ .map(|section| {
+ let (key, _) = cache.allocate(
+ font_system,
+ Key {
+ content: section.content,
+ size: section.size,
+ line_height: f32::from(
+ section
+ .line_height
+ .to_absolute(Pixels(section.size)),
+ ),
+ font: section.font,
+ bounds: Size {
+ width: section.bounds.width,
+ height: section.bounds.height,
+ },
+ shaping: section.shaping,
+ },
+ Purpose::Drawing,
+ );
- let measure_brush =
- glyph_brush::GlyphBrushBuilder::using_font(font).build();
+ key
+ })
+ .collect();
+
+ let bounds = bounds * scale_factor;
+
+ let text_areas =
+ sections
+ .iter()
+ .zip(keys.iter())
+ .filter_map(|(section, key)| {
+ let entry = cache.get(key).expect("Get cached buffer");
+
+ let x = section.bounds.x * scale_factor;
+ let y = section.bounds.y * scale_factor;
+
+ let max_width = entry.bounds.width * scale_factor;
+ let total_height = entry.bounds.height * scale_factor;
+
+ let left = match section.horizontal_alignment {
+ alignment::Horizontal::Left => x,
+ alignment::Horizontal::Center => x - max_width / 2.0,
+ alignment::Horizontal::Right => x - max_width,
+ };
+
+ let top = match section.vertical_alignment {
+ alignment::Vertical::Top => y,
+ alignment::Vertical::Center => y - total_height / 2.0,
+ alignment::Vertical::Bottom => y - total_height,
+ };
+
+ let section_bounds = Rectangle {
+ x: left,
+ y: top,
+ width: section.bounds.width * scale_factor,
+ height: section.bounds.height * scale_factor,
+ };
+
+ let clip_bounds = bounds.intersection(&section_bounds)?;
+
+ Some(glyphon::TextArea {
+ buffer: &entry.buffer,
+ left,
+ top,
+ scale: scale_factor,
+ bounds: glyphon::TextBounds {
+ left: clip_bounds.x as i32,
+ top: clip_bounds.y as i32,
+ right: (clip_bounds.x + clip_bounds.width) as i32,
+ bottom: (clip_bounds.y + clip_bounds.height) as i32,
+ },
+ default_color: {
+ let [r, g, b, a] =
+ color::pack(section.color).components();
+
+ glyphon::Color::rgba(
+ (r * 255.0) as u8,
+ (g * 255.0) as u8,
+ (b * 255.0) as u8,
+ (a * 255.0) as u8,
+ )
+ },
+ })
+ });
+
+ let result = renderer.prepare(
+ device,
+ queue,
+ font_system,
+ &mut self.atlas,
+ glyphon::Resolution {
+ width: target_size.width,
+ height: target_size.height,
+ },
+ text_areas,
+ &mut glyphon::SwashCache::new(),
+ );
- Pipeline {
- draw_brush: RefCell::new(draw_brush),
- draw_font_map: RefCell::new(HashMap::new()),
- measure_brush: RefCell::new(measure_brush),
+ match result {
+ Ok(()) => {
+ self.prepare_layer += 1;
+
+ true
+ }
+ Err(glyphon::PrepareError::AtlasFull(content_type)) => {
+ self.prepare_layer = 0;
+
+ #[allow(clippy::needless_bool)]
+ if self.atlas.grow(device, content_type) {
+ false
+ } else {
+ // If the atlas cannot grow, then all bets are off.
+ // Instead of panicking, we will just pray that the result
+ // will be somewhat readable...
+ true
+ }
+ }
}
}
- pub fn queue(&mut self, section: wgpu_glyph::Section<'_>) {
- self.draw_brush.borrow_mut().queue(section);
+ pub fn render<'a>(
+ &'a self,
+ layer: usize,
+ bounds: Rectangle<u32>,
+ render_pass: &mut wgpu::RenderPass<'a>,
+ ) {
+ let renderer = &self.renderers[layer];
+
+ render_pass.set_scissor_rect(
+ bounds.x,
+ bounds.y,
+ bounds.width,
+ bounds.height,
+ );
+
+ renderer
+ .render(&self.atlas, render_pass)
+ .expect("Render text");
}
- pub fn draw_queued(
- &mut self,
- device: &wgpu::Device,
- staging_belt: &mut wgpu::util::StagingBelt,
- encoder: &mut wgpu::CommandEncoder,
- target: &wgpu::TextureView,
- transformation: Transformation,
- region: wgpu_glyph::Region,
- ) {
- self.draw_brush
- .borrow_mut()
- .draw_queued_with_transform_and_scissoring(
- device,
- staging_belt,
- encoder,
- target,
- transformation.into(),
- region,
- )
- .expect("Draw text");
+ pub fn end_frame(&mut self) {
+ self.atlas.trim();
+
+ self.prepare_layer = 0;
+ }
+
+ pub fn trim_measurements(&mut self) {
+ self.cache.get_mut().trim(Purpose::Measuring);
}
pub fn measure(
&self,
content: &str,
size: f32,
- font: iced_native::Font,
- bounds: iced_native::Size,
- ) -> (f32, f32) {
- use wgpu_glyph::GlyphCruncher;
-
- let wgpu_glyph::FontId(font_id) = self.find_font(font);
-
- let section = wgpu_glyph::Section {
- bounds: (bounds.width, bounds.height),
- text: vec![wgpu_glyph::Text {
- text: content,
- scale: size.into(),
- font_id: wgpu_glyph::FontId(font_id),
- extra: wgpu_glyph::Extra::default(),
- }],
- ..Default::default()
- };
+ line_height: LineHeight,
+ font: Font,
+ bounds: Size,
+ shaping: Shaping,
+ ) -> Size {
+ let mut cache = self.cache.borrow_mut();
+
+ let line_height = f32::from(line_height.to_absolute(Pixels(size)));
+
+ let (_, entry) = cache.allocate(
+ &mut self.font_system.borrow_mut(),
+ Key {
+ content,
+ size,
+ line_height,
+ font,
+ bounds,
+ shaping,
+ },
+ Purpose::Measuring,
+ );
- if let Some(bounds) =
- self.measure_brush.borrow_mut().glyph_bounds(section)
- {
- (bounds.width().ceil(), bounds.height().ceil())
- } else {
- (0.0, 0.0)
- }
+ entry.bounds
}
pub fn hit_test(
&self,
content: &str,
size: f32,
- font: iced_native::Font,
- bounds: iced_native::Size,
- point: iced_native::Point,
- nearest_only: bool,
+ line_height: LineHeight,
+ font: Font,
+ bounds: Size,
+ shaping: Shaping,
+ point: Point,
+ _nearest_only: bool,
) -> Option<Hit> {
- use wgpu_glyph::GlyphCruncher;
-
- let wgpu_glyph::FontId(font_id) = self.find_font(font);
-
- let section = wgpu_glyph::Section {
- bounds: (bounds.width, bounds.height),
- text: vec![wgpu_glyph::Text {
- text: content,
- scale: size.into(),
- font_id: wgpu_glyph::FontId(font_id),
- extra: wgpu_glyph::Extra::default(),
- }],
- ..Default::default()
- };
-
- let mut mb = self.measure_brush.borrow_mut();
-
- // The underlying type is FontArc, so clones are cheap.
- use wgpu_glyph::ab_glyph::{Font, ScaleFont};
- let font = mb.fonts()[font_id].clone().into_scaled(size);
-
- // Implements an iterator over the glyph bounding boxes.
- let bounds = mb.glyphs(section).map(
- |wgpu_glyph::SectionGlyph {
- byte_index, glyph, ..
- }| {
- (
- *byte_index,
- iced_native::Rectangle::new(
- iced_native::Point::new(
- glyph.position.x - font.h_side_bearing(glyph.id),
- glyph.position.y - font.ascent(),
- ),
- iced_native::Size::new(
- font.h_advance(glyph.id),
- font.ascent() - font.descent(),
- ),
- ),
- )
+ let mut cache = self.cache.borrow_mut();
+
+ let line_height = f32::from(line_height.to_absolute(Pixels(size)));
+
+ let (_, entry) = cache.allocate(
+ &mut self.font_system.borrow_mut(),
+ Key {
+ content,
+ size,
+ line_height,
+ font,
+ bounds,
+ shaping,
},
+ Purpose::Measuring,
);
- // Implements computation of the character index based on the byte index
- // within the input string.
- let char_index = |byte_index| {
- let mut b_count = 0;
- for (i, utf8_len) in
- content.chars().map(|c| c.len_utf8()).enumerate()
- {
- if byte_index < (b_count + utf8_len) {
- return i;
- }
- b_count += utf8_len;
- }
+ let cursor = entry.buffer.hit(point.x, point.y)?;
- byte_index
- };
+ Some(Hit::CharOffset(cursor.index))
+ }
+}
- if !nearest_only {
- for (idx, bounds) in bounds.clone() {
- if bounds.contains(point) {
- return Some(Hit::CharOffset(char_index(idx)));
- }
- }
- }
+fn measure(buffer: &glyphon::Buffer) -> Size {
+ let (width, total_lines) = buffer
+ .layout_runs()
+ .fold((0.0, 0usize), |(width, total_lines), run| {
+ (run.line_w.max(width), total_lines + 1)
+ });
+
+ Size::new(width, total_lines as f32 * buffer.metrics().line_height)
+}
- let nearest = bounds
- .map(|(index, bounds)| (index, bounds.center()))
- .min_by(|(_, center_a), (_, center_b)| {
- center_a
- .distance(point)
- .partial_cmp(&center_b.distance(point))
- .unwrap_or(std::cmp::Ordering::Greater)
- });
-
- nearest.map(|(idx, center)| {
- Hit::NearestCharOffset(char_index(idx), point - center)
- })
+fn to_family(family: font::Family) -> glyphon::Family<'static> {
+ match family {
+ font::Family::Name(name) => glyphon::Family::Name(name),
+ font::Family::SansSerif => glyphon::Family::SansSerif,
+ font::Family::Serif => glyphon::Family::Serif,
+ font::Family::Cursive => glyphon::Family::Cursive,
+ font::Family::Fantasy => glyphon::Family::Fantasy,
+ font::Family::Monospace => glyphon::Family::Monospace,
}
+}
- 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);
- }
- }
+fn to_weight(weight: font::Weight) -> glyphon::Weight {
+ match weight {
+ font::Weight::Thin => glyphon::Weight::THIN,
+ font::Weight::ExtraLight => glyphon::Weight::EXTRA_LIGHT,
+ font::Weight::Light => glyphon::Weight::LIGHT,
+ font::Weight::Normal => glyphon::Weight::NORMAL,
+ font::Weight::Medium => glyphon::Weight::MEDIUM,
+ font::Weight::Semibold => glyphon::Weight::SEMIBOLD,
+ font::Weight::Bold => glyphon::Weight::BOLD,
+ font::Weight::ExtraBold => glyphon::Weight::EXTRA_BOLD,
+ font::Weight::Black => glyphon::Weight::BLACK,
+ }
+}
+
+fn to_stretch(stretch: font::Stretch) -> glyphon::Stretch {
+ match stretch {
+ font::Stretch::UltraCondensed => glyphon::Stretch::UltraCondensed,
+ font::Stretch::ExtraCondensed => glyphon::Stretch::ExtraCondensed,
+ font::Stretch::Condensed => glyphon::Stretch::Condensed,
+ font::Stretch::SemiCondensed => glyphon::Stretch::SemiCondensed,
+ font::Stretch::Normal => glyphon::Stretch::Normal,
+ font::Stretch::SemiExpanded => glyphon::Stretch::SemiExpanded,
+ font::Stretch::Expanded => glyphon::Stretch::Expanded,
+ font::Stretch::ExtraExpanded => glyphon::Stretch::ExtraExpanded,
+ font::Stretch::UltraExpanded => glyphon::Stretch::UltraExpanded,
+ }
+}
+
+fn to_shaping(shaping: Shaping) -> glyphon::Shaping {
+ match shaping {
+ Shaping::Basic => glyphon::Shaping::Basic,
+ Shaping::Advanced => glyphon::Shaping::Advanced,
+ }
+}
+
+struct Cache {
+ entries: FxHashMap<KeyHash, Entry>,
+ aliases: FxHashMap<KeyHash, KeyHash>,
+ recently_measured: FxHashSet<KeyHash>,
+ recently_drawn: FxHashSet<KeyHash>,
+ hasher: HashBuilder,
+}
+
+struct Entry {
+ buffer: glyphon::Buffer,
+ bounds: Size,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum Purpose {
+ Measuring,
+ Drawing,
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+type HashBuilder = twox_hash::RandomXxHashBuilder64;
+
+#[cfg(target_arch = "wasm32")]
+type HashBuilder = std::hash::BuildHasherDefault<twox_hash::XxHash64>;
+
+impl Cache {
+ fn new() -> Self {
+ Self {
+ entries: FxHashMap::default(),
+ aliases: FxHashMap::default(),
+ recently_measured: FxHashSet::default(),
+ recently_drawn: FxHashSet::default(),
+ hasher: HashBuilder::default(),
}
}
- pub fn find_font(&self, font: iced_native::Font) -> wgpu_glyph::FontId {
- match font {
- iced_native::Font::Default => wgpu_glyph::FontId(0),
- iced_native::Font::External { name, bytes } => {
- if let Some(font_id) = self.draw_font_map.borrow().get(name) {
- return *font_id;
- }
+ fn get(&self, key: &KeyHash) -> Option<&Entry> {
+ self.entries.get(key)
+ }
- let font = ab_glyph::FontArc::try_from_slice(bytes)
- .expect("Load font");
+ fn allocate(
+ &mut self,
+ font_system: &mut glyphon::FontSystem,
+ key: Key<'_>,
+ purpose: Purpose,
+ ) -> (KeyHash, &mut Entry) {
+ let hash = key.hash(self.hasher.build_hasher());
+
+ let recently_used = match purpose {
+ Purpose::Measuring => &mut self.recently_measured,
+ Purpose::Drawing => &mut self.recently_drawn,
+ };
- let _ = self.measure_brush.borrow_mut().add_font(font.clone());
+ if let Some(hash) = self.aliases.get(&hash) {
+ let _ = recently_used.insert(*hash);
- let font_id = self.draw_brush.borrow_mut().add_font(font);
+ return (*hash, self.entries.get_mut(hash).unwrap());
+ }
- let _ = self
- .draw_font_map
- .borrow_mut()
- .insert(String::from(name), font_id);
+ if let hash_map::Entry::Vacant(entry) = self.entries.entry(hash) {
+ let metrics = glyphon::Metrics::new(key.size, key.line_height);
+ let mut buffer = glyphon::Buffer::new(font_system, metrics);
+
+ buffer.set_size(
+ font_system,
+ key.bounds.width,
+ key.bounds.height.max(key.line_height),
+ );
+ buffer.set_text(
+ font_system,
+ key.content,
+ glyphon::Attrs::new()
+ .family(to_family(key.font.family))
+ .weight(to_weight(key.font.weight))
+ .stretch(to_stretch(key.font.stretch)),
+ to_shaping(key.shaping),
+ );
+
+ let bounds = measure(&buffer);
+ let _ = entry.insert(Entry { buffer, bounds });
+
+ for bounds in [
+ bounds,
+ Size {
+ width: key.bounds.width,
+ ..bounds
+ },
+ ] {
+ if key.bounds != bounds {
+ let _ = self.aliases.insert(
+ Key { bounds, ..key }.hash(self.hasher.build_hasher()),
+ hash,
+ );
+ }
+ }
+ }
+
+ let _ = recently_used.insert(hash);
+
+ (hash, self.entries.get_mut(&hash).unwrap())
+ }
- font_id
+ fn trim(&mut self, purpose: Purpose) {
+ self.entries.retain(|key, _| {
+ self.recently_measured.contains(key)
+ || self.recently_drawn.contains(key)
+ });
+ self.aliases.retain(|_, value| {
+ self.recently_measured.contains(value)
+ || self.recently_drawn.contains(value)
+ });
+
+ match purpose {
+ Purpose::Measuring => {
+ self.recently_measured.clear();
+ }
+ Purpose::Drawing => {
+ self.recently_drawn.clear();
}
}
}
}
+
+#[derive(Debug, Clone, Copy)]
+struct Key<'a> {
+ content: &'a str,
+ size: f32,
+ line_height: f32,
+ font: Font,
+ bounds: Size,
+ shaping: Shaping,
+}
+
+impl Key<'_> {
+ fn hash<H: Hasher>(self, mut hasher: H) -> KeyHash {
+ self.content.hash(&mut hasher);
+ self.size.to_bits().hash(&mut hasher);
+ self.line_height.to_bits().hash(&mut hasher);
+ self.font.hash(&mut hasher);
+ self.bounds.width.to_bits().hash(&mut hasher);
+ self.bounds.height.to_bits().hash(&mut hasher);
+ self.shaping.hash(&mut hasher);
+
+ hasher.finish()
+ }
+}
+
+type KeyHash = u64;
diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs
index efdd214b..d8b23dfe 100644
--- a/wgpu/src/triangle.rs
+++ b/wgpu/src/triangle.rs
@@ -1,63 +1,59 @@
//! Draw meshes of triangles.
mod msaa;
-use crate::buffer::r#static::Buffer;
-use crate::settings;
-use crate::Transformation;
+use crate::core::Size;
+use crate::graphics::{Antialiasing, Transformation};
+use crate::layer::mesh::{self, Mesh};
+use crate::Buffer;
-use iced_graphics::layer::mesh::{self, Mesh};
-use iced_graphics::triangle::ColoredVertex2D;
-use iced_graphics::Size;
-#[cfg(feature = "tracing")]
-use tracing::info_span;
+const INITIAL_INDEX_COUNT: usize = 1_000;
+const INITIAL_VERTEX_COUNT: usize = 1_000;
#[derive(Debug)]
pub struct Pipeline {
blit: Option<msaa::Blit>,
- index_buffer: Buffer<u32>,
- index_strides: Vec<u32>,
solid: solid::Pipeline,
-
- /// Gradients are currently not supported on WASM targets due to their need of storage buffers.
- #[cfg(not(target_arch = "wasm32"))]
gradient: gradient::Pipeline,
+ layers: Vec<Layer>,
+ prepare_layer: usize,
}
-impl Pipeline {
- pub fn new(
+#[derive(Debug)]
+struct Layer {
+ index_buffer: Buffer<u32>,
+ index_strides: Vec<u32>,
+ solid: solid::Layer,
+ gradient: gradient::Layer,
+}
+
+impl Layer {
+ fn new(
device: &wgpu::Device,
- format: wgpu::TextureFormat,
- antialiasing: Option<settings::Antialiasing>,
- ) -> Pipeline {
- Pipeline {
- blit: antialiasing.map(|a| msaa::Blit::new(device, format, a)),
+ solid: &solid::Pipeline,
+ gradient: &gradient::Pipeline,
+ ) -> Self {
+ Self {
index_buffer: Buffer::new(
device,
- "iced_wgpu::triangle vertex buffer",
+ "iced_wgpu.triangle.index_buffer",
+ INITIAL_INDEX_COUNT,
wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
),
index_strides: Vec::new(),
- solid: solid::Pipeline::new(device, format, antialiasing),
-
- #[cfg(not(target_arch = "wasm32"))]
- gradient: gradient::Pipeline::new(device, format, antialiasing),
+ solid: solid::Layer::new(device, &solid.constants_layout),
+ gradient: gradient::Layer::new(device, &gradient.constants_layout),
}
}
- pub fn draw(
+ fn prepare(
&mut self,
device: &wgpu::Device,
- staging_belt: &mut wgpu::util::StagingBelt,
- encoder: &mut wgpu::CommandEncoder,
- target: &wgpu::TextureView,
- target_size: Size<u32>,
- transformation: Transformation,
- scale_factor: f32,
+ queue: &wgpu::Queue,
+ solid: &solid::Pipeline,
+ gradient: &gradient::Pipeline,
meshes: &[Mesh<'_>],
+ transformation: Transformation,
) {
- #[cfg(feature = "tracing")]
- let _ = info_span!("Wgpu::Triangle", "DRAW").entered();
-
// Count the total amount of vertices & indices we need to handle
let count = mesh::attribute_count_of(meshes);
@@ -67,175 +63,220 @@ impl Pipeline {
// the majority of use cases. Therefore we will write GPU data every frame (for now).
let _ = self.index_buffer.resize(device, count.indices);
let _ = self.solid.vertices.resize(device, count.solid_vertices);
-
- #[cfg(not(target_arch = "wasm32"))]
let _ = self
.gradient
.vertices
.resize(device, count.gradient_vertices);
- // Prepare dynamic buffers & data store for writing
+ if self.solid.uniforms.resize(device, count.solids) {
+ self.solid.constants = solid::Layer::bind_group(
+ device,
+ &self.solid.uniforms.raw,
+ &solid.constants_layout,
+ );
+ }
+
+ if self.gradient.uniforms.resize(device, count.gradients) {
+ self.gradient.constants = gradient::Layer::bind_group(
+ device,
+ &self.gradient.uniforms.raw,
+ &gradient.constants_layout,
+ );
+ }
+
self.index_strides.clear();
+ self.index_buffer.clear();
self.solid.vertices.clear();
self.solid.uniforms.clear();
-
- #[cfg(not(target_arch = "wasm32"))]
- {
- self.gradient.uniforms.clear();
- self.gradient.vertices.clear();
- self.gradient.storage.clear();
- }
+ self.gradient.vertices.clear();
+ self.gradient.uniforms.clear();
let mut solid_vertex_offset = 0;
- let mut index_offset = 0;
-
- #[cfg(not(target_arch = "wasm32"))]
+ let mut solid_uniform_offset = 0;
let mut gradient_vertex_offset = 0;
+ let mut gradient_uniform_offset = 0;
+ let mut index_offset = 0;
for mesh in meshes {
let origin = mesh.origin();
let indices = mesh.indices();
- let transform =
- transformation * Transformation::translate(origin.x, origin.y);
-
- let new_index_offset = self.index_buffer.write(
- device,
- staging_belt,
- encoder,
- index_offset,
- indices,
+ let uniforms = Uniforms::new(
+ transformation * Transformation::translate(origin.x, origin.y),
);
- index_offset += new_index_offset;
+ index_offset +=
+ self.index_buffer.write(queue, index_offset, indices);
self.index_strides.push(indices.len() as u32);
- //push uniform data to CPU buffers
match mesh {
Mesh::Solid { buffers, .. } => {
- self.solid.uniforms.push(&solid::Uniforms::new(transform));
-
- let written_bytes = self.solid.vertices.write(
- device,
- staging_belt,
- encoder,
+ solid_vertex_offset += self.solid.vertices.write(
+ queue,
solid_vertex_offset,
&buffers.vertices,
);
- solid_vertex_offset += written_bytes;
+ solid_uniform_offset += self.solid.uniforms.write(
+ queue,
+ solid_uniform_offset,
+ &[uniforms],
+ );
}
- #[cfg(not(target_arch = "wasm32"))]
- Mesh::Gradient {
- buffers, gradient, ..
- } => {
- let written_bytes = self.gradient.vertices.write(
- device,
- staging_belt,
- encoder,
+ Mesh::Gradient { buffers, .. } => {
+ gradient_vertex_offset += self.gradient.vertices.write(
+ queue,
gradient_vertex_offset,
&buffers.vertices,
);
- gradient_vertex_offset += written_bytes;
+ gradient_uniform_offset += self.gradient.uniforms.write(
+ queue,
+ gradient_uniform_offset,
+ &[uniforms],
+ );
+ }
+ }
+ }
+ }
- match gradient {
- iced_graphics::Gradient::Linear(linear) => {
- use glam::{IVec4, Vec4};
+ fn render<'a>(
+ &'a self,
+ solid: &'a solid::Pipeline,
+ gradient: &'a gradient::Pipeline,
+ meshes: &[Mesh<'_>],
+ scale_factor: f32,
+ render_pass: &mut wgpu::RenderPass<'a>,
+ ) {
+ let mut num_solids = 0;
+ let mut num_gradients = 0;
+ let mut last_is_solid = None;
- let start_offset = self.gradient.color_stop_offset;
- let end_offset = (linear.color_stops.len() as i32)
- + start_offset
- - 1;
+ for (index, mesh) in meshes.iter().enumerate() {
+ let clip_bounds = (mesh.clip_bounds() * scale_factor).snap();
- self.gradient.uniforms.push(&gradient::Uniforms {
- transform: transform.into(),
- direction: Vec4::new(
- linear.start.x,
- linear.start.y,
- linear.end.x,
- linear.end.y,
- ),
- stop_range: IVec4::new(
- start_offset,
- end_offset,
- 0,
- 0,
- ),
- });
-
- self.gradient.color_stop_offset = end_offset + 1;
-
- let stops: Vec<gradient::ColorStop> = linear
- .color_stops
- .iter()
- .map(|stop| {
- let [r, g, b, a] = stop.color.into_linear();
-
- gradient::ColorStop {
- offset: stop.offset,
- color: Vec4::new(r, g, b, a),
- }
- })
- .collect();
-
- self.gradient
- .color_stops_pending_write
- .color_stops
- .extend(stops);
- }
+ if clip_bounds.width < 1 || clip_bounds.height < 1 {
+ continue;
+ }
+
+ render_pass.set_scissor_rect(
+ clip_bounds.x,
+ clip_bounds.y,
+ clip_bounds.width,
+ clip_bounds.height,
+ );
+
+ match mesh {
+ Mesh::Solid { .. } => {
+ if !last_is_solid.unwrap_or(false) {
+ render_pass.set_pipeline(&solid.pipeline);
+
+ last_is_solid = Some(true);
}
+
+ render_pass.set_bind_group(
+ 0,
+ &self.solid.constants,
+ &[(num_solids * std::mem::size_of::<Uniforms>())
+ as u32],
+ );
+
+ render_pass.set_vertex_buffer(
+ 0,
+ self.solid.vertices.slice_from_index(num_solids),
+ );
+
+ num_solids += 1;
}
- #[cfg(target_arch = "wasm32")]
- Mesh::Gradient { .. } => {}
- }
- }
+ Mesh::Gradient { .. } => {
+ if last_is_solid.unwrap_or(true) {
+ render_pass.set_pipeline(&gradient.pipeline);
- // Write uniform data to GPU
- if count.solid_vertices > 0 {
- let uniforms_resized = self.solid.uniforms.resize(device);
+ last_is_solid = Some(false);
+ }
- if uniforms_resized {
- self.solid.bind_group = solid::Pipeline::bind_group(
- device,
- self.solid.uniforms.raw(),
- &self.solid.bind_group_layout,
- )
- }
+ render_pass.set_bind_group(
+ 0,
+ &self.gradient.constants,
+ &[(num_gradients * std::mem::size_of::<Uniforms>())
+ as u32],
+ );
+
+ render_pass.set_vertex_buffer(
+ 0,
+ self.gradient.vertices.slice_from_index(num_gradients),
+ );
+
+ num_gradients += 1;
+ }
+ };
- self.solid.uniforms.write(device, staging_belt, encoder);
+ render_pass.set_index_buffer(
+ self.index_buffer.slice_from_index(index),
+ wgpu::IndexFormat::Uint32,
+ );
+
+ render_pass.draw_indexed(0..self.index_strides[index], 0, 0..1);
}
+ }
+}
- #[cfg(not(target_arch = "wasm32"))]
- if count.gradient_vertices > 0 {
- // First write the pending color stops to the CPU buffer
- self.gradient
- .storage
- .push(&self.gradient.color_stops_pending_write);
-
- // Resize buffers if needed
- let uniforms_resized = self.gradient.uniforms.resize(device);
- let storage_resized = self.gradient.storage.resize(device);
-
- if uniforms_resized || storage_resized {
- self.gradient.bind_group = gradient::Pipeline::bind_group(
- device,
- self.gradient.uniforms.raw(),
- self.gradient.storage.raw(),
- &self.gradient.bind_group_layout,
- );
- }
+impl Pipeline {
+ pub fn new(
+ device: &wgpu::Device,
+ format: wgpu::TextureFormat,
+ antialiasing: Option<Antialiasing>,
+ ) -> Pipeline {
+ Pipeline {
+ blit: antialiasing.map(|a| msaa::Blit::new(device, format, a)),
+ solid: solid::Pipeline::new(device, format, antialiasing),
+ gradient: gradient::Pipeline::new(device, format, antialiasing),
+ layers: Vec::new(),
+ prepare_layer: 0,
+ }
+ }
- // Write to GPU
- self.gradient.uniforms.write(device, staging_belt, encoder);
- self.gradient.storage.write(device, staging_belt, encoder);
+ pub fn prepare(
+ &mut self,
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ meshes: &[Mesh<'_>],
+ transformation: Transformation,
+ ) {
+ #[cfg(feature = "tracing")]
+ let _ = tracing::info_span!("Wgpu::Triangle", "PREPARE").entered();
- // Cleanup
- self.gradient.color_stop_offset = 0;
- self.gradient.color_stops_pending_write.color_stops.clear();
+ if self.layers.len() <= self.prepare_layer {
+ self.layers
+ .push(Layer::new(device, &self.solid, &self.gradient));
}
- // Configure render pass
+ let layer = &mut self.layers[self.prepare_layer];
+ layer.prepare(
+ device,
+ queue,
+ &self.solid,
+ &self.gradient,
+ meshes,
+ transformation,
+ );
+
+ self.prepare_layer += 1;
+ }
+
+ pub fn render(
+ &mut self,
+ device: &wgpu::Device,
+ encoder: &mut wgpu::CommandEncoder,
+ target: &wgpu::TextureView,
+ layer: usize,
+ target_size: Size<u32>,
+ meshes: &[Mesh<'_>],
+ scale_factor: f32,
+ ) {
+ #[cfg(feature = "tracing")]
+ let _ = tracing::info_span!("Wgpu::Triangle", "DRAW").entered();
+
{
let (attachment, resolve_target, load) = if let Some(blit) =
&mut self.blit
@@ -252,12 +293,9 @@ impl Pipeline {
(target, None, wgpu::LoadOp::Load)
};
- #[cfg(feature = "tracing")]
- let _ = info_span!("Wgpu::Triangle", "BEGIN_RENDER_PASS").enter();
-
let mut render_pass =
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
- label: Some("iced_wgpu::triangle render pass"),
+ label: Some("iced_wgpu.triangle.render_pass"),
color_attachments: &[Some(
wgpu::RenderPassColorAttachment {
view: attachment,
@@ -268,87 +306,25 @@ impl Pipeline {
depth_stencil_attachment: None,
});
- let mut num_solids = 0;
- #[cfg(not(target_arch = "wasm32"))]
- let mut num_gradients = 0;
- let mut last_is_solid = None;
+ let layer = &mut self.layers[layer];
- for (index, mesh) in meshes.iter().enumerate() {
- let clip_bounds = (mesh.clip_bounds() * scale_factor).snap();
-
- render_pass.set_scissor_rect(
- clip_bounds.x,
- clip_bounds.y,
- clip_bounds.width,
- clip_bounds.height,
- );
-
- match mesh {
- Mesh::Solid { .. } => {
- if !last_is_solid.unwrap_or(false) {
- render_pass.set_pipeline(&self.solid.pipeline);
-
- last_is_solid = Some(true);
- }
-
- render_pass.set_bind_group(
- 0,
- &self.solid.bind_group,
- &[self.solid.uniforms.offset_at_index(num_solids)],
- );
-
- render_pass.set_vertex_buffer(
- 0,
- self.solid.vertices.slice_from_index(num_solids),
- );
-
- num_solids += 1;
- }
- #[cfg(not(target_arch = "wasm32"))]
- Mesh::Gradient { .. } => {
- if last_is_solid.unwrap_or(true) {
- render_pass.set_pipeline(&self.gradient.pipeline);
-
- last_is_solid = Some(false);
- }
-
- render_pass.set_bind_group(
- 0,
- &self.gradient.bind_group,
- &[self
- .gradient
- .uniforms
- .offset_at_index(num_gradients)],
- );
-
- render_pass.set_vertex_buffer(
- 0,
- self.gradient
- .vertices
- .slice_from_index(num_gradients),
- );
-
- num_gradients += 1;
- }
- #[cfg(target_arch = "wasm32")]
- Mesh::Gradient { .. } => {}
- };
-
- render_pass.set_index_buffer(
- self.index_buffer.slice_from_index(index),
- wgpu::IndexFormat::Uint32,
- );
-
- render_pass.draw_indexed(0..self.index_strides[index], 0, 0..1);
- }
+ layer.render(
+ &self.solid,
+ &self.gradient,
+ meshes,
+ scale_factor,
+ &mut render_pass,
+ );
}
- self.index_buffer.clear();
-
if let Some(blit) = &mut self.blit {
blit.draw(encoder, target);
}
}
+
+ pub fn end_frame(&mut self) {
+ self.prepare_layer = 0;
+ }
}
fn fragment_target(
@@ -370,7 +346,7 @@ fn primitive_state() -> wgpu::PrimitiveState {
}
fn multisample_state(
- antialiasing: Option<settings::Antialiasing>,
+ antialiasing: Option<Antialiasing>,
) -> wgpu::MultisampleState {
wgpu::MultisampleState {
count: antialiasing.map(|a| a.sample_count()).unwrap_or(1),
@@ -379,132 +355,88 @@ fn multisample_state(
}
}
+#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
+#[repr(C)]
+pub struct Uniforms {
+ transform: [f32; 16],
+ /// Uniform values must be 256-aligned;
+ /// see: [`wgpu::Limits`] `min_uniform_buffer_offset_alignment`.
+ _padding: [f32; 48],
+}
+
+impl Uniforms {
+ pub fn new(transform: Transformation) -> Self {
+ Self {
+ transform: transform.into(),
+ _padding: [0.0; 48],
+ }
+ }
+
+ pub fn entry() -> wgpu::BindGroupLayoutEntry {
+ wgpu::BindGroupLayoutEntry {
+ binding: 0,
+ visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
+ ty: wgpu::BindingType::Buffer {
+ ty: wgpu::BufferBindingType::Uniform,
+ has_dynamic_offset: true,
+ min_binding_size: wgpu::BufferSize::new(
+ std::mem::size_of::<Self>() as u64,
+ ),
+ },
+ count: None,
+ }
+ }
+
+ pub fn min_size() -> Option<wgpu::BufferSize> {
+ wgpu::BufferSize::new(std::mem::size_of::<Self>() as u64)
+ }
+}
+
mod solid {
- use crate::buffer::dynamic;
- use crate::buffer::r#static::Buffer;
- use crate::settings;
+ use crate::graphics::mesh;
+ use crate::graphics::Antialiasing;
use crate::triangle;
- use encase::ShaderType;
- use iced_graphics::Transformation;
+ use crate::Buffer;
#[derive(Debug)]
pub struct Pipeline {
pub pipeline: wgpu::RenderPipeline,
- pub vertices: Buffer<triangle::ColoredVertex2D>,
- pub uniforms: dynamic::Buffer<Uniforms>,
- pub bind_group_layout: wgpu::BindGroupLayout,
- pub bind_group: wgpu::BindGroup,
- }
-
- #[derive(Debug, Clone, Copy, ShaderType)]
- pub struct Uniforms {
- transform: glam::Mat4,
+ pub constants_layout: wgpu::BindGroupLayout,
}
- impl Uniforms {
- pub fn new(transform: Transformation) -> Self {
- Self {
- transform: transform.into(),
- }
- }
+ #[derive(Debug)]
+ pub struct Layer {
+ pub vertices: Buffer<mesh::SolidVertex2D>,
+ pub uniforms: Buffer<triangle::Uniforms>,
+ pub constants: wgpu::BindGroup,
}
- impl Pipeline {
- /// Creates a new [SolidPipeline] using `solid.wgsl` shader.
+ impl Layer {
pub fn new(
device: &wgpu::Device,
- format: wgpu::TextureFormat,
- antialiasing: Option<settings::Antialiasing>,
+ constants_layout: &wgpu::BindGroupLayout,
) -> Self {
let vertices = Buffer::new(
device,
- "iced_wgpu::triangle::solid vertex buffer",
+ "iced_wgpu.triangle.solid.vertex_buffer",
+ triangle::INITIAL_VERTEX_COUNT,
wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
);
- let uniforms = dynamic::Buffer::uniform(
+ let uniforms = Buffer::new(
device,
- "iced_wgpu::triangle::solid uniforms",
+ "iced_wgpu.triangle.solid.uniforms",
+ 1,
+ wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
);
- let bind_group_layout = device.create_bind_group_layout(
- &wgpu::BindGroupLayoutDescriptor {
- label: Some("iced_wgpu::triangle::solid bind group layout"),
- entries: &[wgpu::BindGroupLayoutEntry {
- binding: 0,
- visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
- ty: wgpu::BindingType::Buffer {
- ty: wgpu::BufferBindingType::Uniform,
- has_dynamic_offset: true,
- min_binding_size: Some(Uniforms::min_size()),
- },
- count: None,
- }],
- },
- );
-
- let bind_group =
- Self::bind_group(device, uniforms.raw(), &bind_group_layout);
-
- let layout = device.create_pipeline_layout(
- &wgpu::PipelineLayoutDescriptor {
- label: Some("iced_wgpu::triangle::solid pipeline layout"),
- bind_group_layouts: &[&bind_group_layout],
- push_constant_ranges: &[],
- },
- );
-
- let shader =
- device.create_shader_module(wgpu::ShaderModuleDescriptor {
- label: Some(
- "iced_wgpu::triangle::solid create shader module",
- ),
- source: wgpu::ShaderSource::Wgsl(
- std::borrow::Cow::Borrowed(include_str!(
- "shader/solid.wgsl"
- )),
- ),
- });
-
- let pipeline = device.create_render_pipeline(
- &wgpu::RenderPipelineDescriptor {
- label: Some("iced_wgpu::triangle::solid pipeline"),
- layout: Some(&layout),
- vertex: wgpu::VertexState {
- module: &shader,
- entry_point: "vs_main",
- buffers: &[wgpu::VertexBufferLayout {
- array_stride: std::mem::size_of::<
- triangle::ColoredVertex2D,
- >()
- as u64,
- step_mode: wgpu::VertexStepMode::Vertex,
- attributes: &wgpu::vertex_attr_array!(
- // Position
- 0 => Float32x2,
- // Color
- 1 => Float32x4,
- ),
- }],
- },
- fragment: Some(wgpu::FragmentState {
- module: &shader,
- entry_point: "fs_main",
- targets: &[triangle::fragment_target(format)],
- }),
- primitive: triangle::primitive_state(),
- depth_stencil: None,
- multisample: triangle::multisample_state(antialiasing),
- multiview: None,
- },
- );
+ let constants =
+ Self::bind_group(device, &uniforms.raw, constants_layout);
Self {
- pipeline,
vertices,
uniforms,
- bind_group_layout,
- bind_group,
+ constants,
}
}
@@ -514,7 +446,7 @@ mod solid {
layout: &wgpu::BindGroupLayout,
) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
- label: Some("iced_wgpu::triangle::solid bind group"),
+ label: Some("iced_wgpu.triangle.solid.bind_group"),
layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
@@ -522,167 +454,225 @@ mod solid {
wgpu::BufferBinding {
buffer,
offset: 0,
- size: Some(Uniforms::min_size()),
+ size: triangle::Uniforms::min_size(),
},
),
}],
})
}
}
+
+ impl Pipeline {
+ pub fn new(
+ device: &wgpu::Device,
+ format: wgpu::TextureFormat,
+ antialiasing: Option<Antialiasing>,
+ ) -> Self {
+ let constants_layout = device.create_bind_group_layout(
+ &wgpu::BindGroupLayoutDescriptor {
+ label: Some("iced_wgpu.triangle.solid.bind_group_layout"),
+ entries: &[triangle::Uniforms::entry()],
+ },
+ );
+
+ let layout = device.create_pipeline_layout(
+ &wgpu::PipelineLayoutDescriptor {
+ label: Some("iced_wgpu.triangle.solid.pipeline_layout"),
+ bind_group_layouts: &[&constants_layout],
+ push_constant_ranges: &[],
+ },
+ );
+
+ let shader =
+ device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("iced_wgpu.triangle.solid.shader"),
+ source: wgpu::ShaderSource::Wgsl(
+ std::borrow::Cow::Borrowed(include_str!(
+ "shader/triangle.wgsl"
+ )),
+ ),
+ });
+
+ let pipeline =
+ device.create_render_pipeline(
+ &wgpu::RenderPipelineDescriptor {
+ label: Some("iced_wgpu::triangle::solid pipeline"),
+ layout: Some(&layout),
+ vertex: wgpu::VertexState {
+ module: &shader,
+ entry_point: "solid_vs_main",
+ buffers: &[wgpu::VertexBufferLayout {
+ array_stride: std::mem::size_of::<
+ mesh::SolidVertex2D,
+ >(
+ )
+ as u64,
+ step_mode: wgpu::VertexStepMode::Vertex,
+ attributes: &wgpu::vertex_attr_array!(
+ // Position
+ 0 => Float32x2,
+ // Color
+ 1 => Float32x4,
+ ),
+ }],
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: &shader,
+ entry_point: "solid_fs_main",
+ targets: &[triangle::fragment_target(format)],
+ }),
+ primitive: triangle::primitive_state(),
+ depth_stencil: None,
+ multisample: triangle::multisample_state(antialiasing),
+ multiview: None,
+ },
+ );
+
+ Self {
+ pipeline,
+ constants_layout,
+ }
+ }
+ }
}
-#[cfg(not(target_arch = "wasm32"))]
mod gradient {
- use crate::buffer::dynamic;
- use crate::buffer::r#static::Buffer;
- use crate::settings;
+ use crate::graphics::mesh;
+ use crate::graphics::Antialiasing;
use crate::triangle;
-
- use encase::ShaderType;
- use glam::{IVec4, Vec4};
- use iced_graphics::triangle::Vertex2D;
+ use crate::Buffer;
#[derive(Debug)]
pub struct Pipeline {
pub pipeline: wgpu::RenderPipeline,
- pub vertices: Buffer<Vertex2D>,
- pub uniforms: dynamic::Buffer<Uniforms>,
- pub storage: dynamic::Buffer<Storage>,
- pub color_stop_offset: i32,
- //Need to store these and then write them all at once
- //or else they will be padded to 256 and cause gaps in the storage buffer
- pub color_stops_pending_write: Storage,
- pub bind_group_layout: wgpu::BindGroupLayout,
- pub bind_group: wgpu::BindGroup,
+ pub constants_layout: wgpu::BindGroupLayout,
}
- #[derive(Debug, ShaderType)]
- pub struct Uniforms {
- pub transform: glam::Mat4,
- //xy = start, zw = end
- pub direction: Vec4,
- //x = start stop, y = end stop, zw = padding
- pub stop_range: IVec4,
- }
-
- #[derive(Debug, ShaderType)]
- pub struct ColorStop {
- pub color: Vec4,
- pub offset: f32,
- }
-
- #[derive(Debug, ShaderType)]
- pub struct Storage {
- #[size(runtime)]
- pub color_stops: Vec<ColorStop>,
+ #[derive(Debug)]
+ pub struct Layer {
+ pub vertices: Buffer<mesh::GradientVertex2D>,
+ pub uniforms: Buffer<triangle::Uniforms>,
+ pub constants: wgpu::BindGroup,
}
- impl Pipeline {
- /// Creates a new [GradientPipeline] using `gradient.wgsl` shader.
- pub(super) fn new(
+ impl Layer {
+ pub fn new(
device: &wgpu::Device,
- format: wgpu::TextureFormat,
- antialiasing: Option<settings::Antialiasing>,
+ constants_layout: &wgpu::BindGroupLayout,
) -> Self {
let vertices = Buffer::new(
device,
- "iced_wgpu::triangle::gradient vertex buffer",
+ "iced_wgpu.triangle.gradient.vertex_buffer",
+ triangle::INITIAL_VERTEX_COUNT,
wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
);
- let uniforms = dynamic::Buffer::uniform(
+ let uniforms = Buffer::new(
device,
- "iced_wgpu::triangle::gradient uniforms",
+ "iced_wgpu.triangle.gradient.uniforms",
+ 1,
+ wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
);
- //Note: with a WASM target storage buffers are not supported. Will need to use UBOs & static
- // sized array (eg like the 32-sized array on OpenGL side right now) to make gradients work
- let storage = dynamic::Buffer::storage(
- device,
- "iced_wgpu::triangle::gradient storage",
- );
+ let constants =
+ Self::bind_group(device, &uniforms.raw, constants_layout);
- let bind_group_layout = device.create_bind_group_layout(
+ Self {
+ vertices,
+ uniforms,
+ constants,
+ }
+ }
+
+ pub fn bind_group(
+ device: &wgpu::Device,
+ uniform_buffer: &wgpu::Buffer,
+ layout: &wgpu::BindGroupLayout,
+ ) -> wgpu::BindGroup {
+ device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("iced_wgpu.triangle.gradient.bind_group"),
+ layout,
+ entries: &[wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::Buffer(
+ wgpu::BufferBinding {
+ buffer: uniform_buffer,
+ offset: 0,
+ size: triangle::Uniforms::min_size(),
+ },
+ ),
+ }],
+ })
+ }
+ }
+
+ impl Pipeline {
+ pub fn new(
+ device: &wgpu::Device,
+ format: wgpu::TextureFormat,
+ antialiasing: Option<Antialiasing>,
+ ) -> Self {
+ let constants_layout = device.create_bind_group_layout(
&wgpu::BindGroupLayoutDescriptor {
label: Some(
- "iced_wgpu::triangle::gradient bind group layout",
+ "iced_wgpu.triangle.gradient.bind_group_layout",
),
- entries: &[
- wgpu::BindGroupLayoutEntry {
- binding: 0,
- visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
- ty: wgpu::BindingType::Buffer {
- ty: wgpu::BufferBindingType::Uniform,
- has_dynamic_offset: true,
- min_binding_size: Some(Uniforms::min_size()),
- },
- count: None,
- },
- wgpu::BindGroupLayoutEntry {
- binding: 1,
- visibility: wgpu::ShaderStages::FRAGMENT,
- ty: wgpu::BindingType::Buffer {
- ty: wgpu::BufferBindingType::Storage {
- read_only: true,
- },
- has_dynamic_offset: false,
- min_binding_size: Some(Storage::min_size()),
- },
- count: None,
- },
- ],
+ entries: &[triangle::Uniforms::entry()],
},
);
- let bind_group = Pipeline::bind_group(
- device,
- uniforms.raw(),
- storage.raw(),
- &bind_group_layout,
- );
-
let layout = device.create_pipeline_layout(
&wgpu::PipelineLayoutDescriptor {
- label: Some(
- "iced_wgpu::triangle::gradient pipeline layout",
- ),
- bind_group_layouts: &[&bind_group_layout],
+ label: Some("iced_wgpu.triangle.gradient.pipeline_layout"),
+ bind_group_layouts: &[&constants_layout],
push_constant_ranges: &[],
},
);
let shader =
device.create_shader_module(wgpu::ShaderModuleDescriptor {
- label: Some(
- "iced_wgpu::triangle::gradient create shader module",
- ),
+ label: Some("iced_wgpu.triangle.gradient.shader"),
source: wgpu::ShaderSource::Wgsl(
std::borrow::Cow::Borrowed(include_str!(
- "shader/gradient.wgsl"
+ "shader/triangle.wgsl"
)),
),
});
let pipeline = device.create_render_pipeline(
&wgpu::RenderPipelineDescriptor {
- label: Some("iced_wgpu::triangle::gradient pipeline"),
+ label: Some("iced_wgpu.triangle.gradient.pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
- entry_point: "vs_main",
+ entry_point: "gradient_vs_main",
buffers: &[wgpu::VertexBufferLayout {
- array_stride: std::mem::size_of::<Vertex2D>()
+ array_stride: std::mem::size_of::<
+ mesh::GradientVertex2D,
+ >()
as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &wgpu::vertex_attr_array!(
// Position
0 => Float32x2,
+ // Colors 1-2
+ 1 => Uint32x4,
+ // Colors 3-4
+ 2 => Uint32x4,
+ // Colors 5-6
+ 3 => Uint32x4,
+ // Colors 7-8
+ 4 => Uint32x4,
+ // Offsets
+ 5 => Uint32x4,
+ // Direction
+ 6 => Float32x4
),
}],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
- entry_point: "fs_main",
+ entry_point: "gradient_fs_main",
targets: &[triangle::fragment_target(format)],
}),
primitive: triangle::primitive_state(),
@@ -694,44 +684,8 @@ mod gradient {
Self {
pipeline,
- vertices,
- uniforms,
- storage,
- color_stop_offset: 0,
- color_stops_pending_write: Storage {
- color_stops: vec![],
- },
- bind_group_layout,
- bind_group,
+ constants_layout,
}
}
-
- pub fn bind_group(
- device: &wgpu::Device,
- uniform_buffer: &wgpu::Buffer,
- storage_buffer: &wgpu::Buffer,
- layout: &wgpu::BindGroupLayout,
- ) -> wgpu::BindGroup {
- device.create_bind_group(&wgpu::BindGroupDescriptor {
- label: Some("iced_wgpu::triangle::gradient bind group"),
- layout,
- entries: &[
- wgpu::BindGroupEntry {
- binding: 0,
- resource: wgpu::BindingResource::Buffer(
- wgpu::BufferBinding {
- buffer: uniform_buffer,
- offset: 0,
- size: Some(Uniforms::min_size()),
- },
- ),
- },
- wgpu::BindGroupEntry {
- binding: 1,
- resource: storage_buffer.as_entire_binding(),
- },
- ],
- })
- }
}
}
diff --git a/wgpu/src/triangle/msaa.rs b/wgpu/src/triangle/msaa.rs
index a3016ff8..320b5b12 100644
--- a/wgpu/src/triangle/msaa.rs
+++ b/wgpu/src/triangle/msaa.rs
@@ -1,4 +1,4 @@
-use crate::settings;
+use crate::graphics;
#[derive(Debug)]
pub struct Blit {
@@ -14,17 +14,10 @@ impl Blit {
pub fn new(
device: &wgpu::Device,
format: wgpu::TextureFormat,
- antialiasing: settings::Antialiasing,
+ antialiasing: graphics::Antialiasing,
) -> Blit {
- let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
- address_mode_u: wgpu::AddressMode::ClampToEdge,
- address_mode_v: wgpu::AddressMode::ClampToEdge,
- address_mode_w: wgpu::AddressMode::ClampToEdge,
- mag_filter: wgpu::FilterMode::Nearest,
- min_filter: wgpu::FilterMode::Nearest,
- mipmap_filter: wgpu::FilterMode::Nearest,
- ..Default::default()
- });
+ let sampler =
+ device.create_sampler(&wgpu::SamplerDescriptor::default());
let constant_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
@@ -75,7 +68,7 @@ impl Blit {
let shader =
device.create_shader_module(wgpu::ShaderModuleDescriptor {
- label: Some("iced_wgpu::triangle::blit_shader"),
+ label: Some("iced_wgpu triangle blit_shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
include_str!("../shader/blit.wgsl"),
)),
@@ -223,6 +216,7 @@ impl Targets {
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
+ view_formats: &[],
});
let resolve = device.create_texture(&wgpu::TextureDescriptor {
@@ -234,6 +228,7 @@ impl Targets {
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING,
+ view_formats: &[],
});
let attachment =
diff --git a/wgpu/src/window.rs b/wgpu/src/window.rs
index aac5fb9e..9545a14e 100644
--- a/wgpu/src/window.rs
+++ b/wgpu/src/window.rs
@@ -1,4 +1,5 @@
//! Display rendering results on windows.
-mod compositor;
+pub mod compositor;
pub use compositor::Compositor;
+pub use wgpu::Surface;
diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs
index 6d0c36f6..cd5b20cc 100644
--- a/wgpu/src/window/compositor.rs
+++ b/wgpu/src/window/compositor.rs
@@ -1,9 +1,13 @@
-use crate::{Backend, Color, Error, Renderer, Settings, Viewport};
+//! Connect a window with a renderer.
+use crate::core::{Color, Size};
+use crate::graphics;
+use crate::graphics::color;
+use crate::graphics::compositor;
+use crate::graphics::{Error, Viewport};
+use crate::{Backend, Primitive, Renderer, Settings};
use futures::stream::{self, StreamExt};
-use iced_graphics::compositor;
-use iced_native::futures;
use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle};
use std::marker::PhantomData;
@@ -16,14 +20,11 @@ pub struct Compositor<Theme> {
adapter: wgpu::Adapter,
device: wgpu::Device,
queue: wgpu::Queue,
- staging_belt: wgpu::util::StagingBelt,
format: wgpu::TextureFormat,
theme: PhantomData<Theme>,
}
impl<Theme> Compositor<Theme> {
- const CHUNK_SIZE: u64 = 10 * 1024;
-
/// Requests a new [`Compositor`] with the given [`Settings`].
///
/// Returns `None` if no compatible graphics adapter could be found.
@@ -31,7 +32,10 @@ impl<Theme> Compositor<Theme> {
settings: Settings,
compatible_window: Option<&W>,
) -> Option<Self> {
- let instance = wgpu::Instance::new(settings.internal_backend);
+ let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
+ backends: settings.internal_backend,
+ ..Default::default()
+ });
log::info!("{:#?}", settings);
@@ -46,15 +50,16 @@ impl<Theme> Compositor<Theme> {
#[allow(unsafe_code)]
let compatible_surface = compatible_window
- .map(|window| unsafe { instance.create_surface(window) });
+ .and_then(|window| unsafe { instance.create_surface(window).ok() });
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
- power_preference: if settings.antialiasing.is_none() {
- wgpu::PowerPreference::LowPower
- } else {
- wgpu::PowerPreference::HighPerformance
- },
+ power_preference: wgpu::util::power_preference_from_env()
+ .unwrap_or(if settings.antialiasing.is_none() {
+ wgpu::PowerPreference::LowPower
+ } else {
+ wgpu::PowerPreference::HighPerformance
+ }),
compatible_surface: compatible_surface.as_ref(),
force_fallback_adapter: false,
})
@@ -63,7 +68,21 @@ impl<Theme> Compositor<Theme> {
log::info!("Selected: {:#?}", adapter.get_info());
let format = compatible_surface.as_ref().and_then(|surface| {
- surface.get_supported_formats(&adapter).first().copied()
+ let capabilities = surface.get_capabilities(&adapter);
+
+ let mut formats = capabilities.formats.iter().copied();
+
+ let format = if color::GAMMA_CORRECTION {
+ formats.find(wgpu::TextureFormat::is_srgb)
+ } else {
+ formats.find(|format| !wgpu::TextureFormat::is_srgb(format))
+ };
+
+ format.or_else(|| {
+ log::warn!("No format found!");
+
+ capabilities.formats.first().copied()
+ })
})?;
log::info!("Selected format: {:?}", format);
@@ -98,15 +117,12 @@ impl<Theme> Compositor<Theme> {
.next()
.await?;
- let staging_belt = wgpu::util::StagingBelt::new(Self::CHUNK_SIZE);
-
Some(Compositor {
instance,
settings,
adapter,
device,
queue,
- staging_belt,
format,
theme: PhantomData,
})
@@ -114,11 +130,82 @@ impl<Theme> Compositor<Theme> {
/// Creates a new rendering [`Backend`] for this [`Compositor`].
pub fn create_backend(&self) -> Backend {
- Backend::new(&self.device, self.settings, self.format)
+ Backend::new(&self.device, &self.queue, self.settings, self.format)
}
}
-impl<Theme> iced_graphics::window::Compositor for Compositor<Theme> {
+/// Creates a [`Compositor`] and its [`Backend`] for the given [`Settings`] and
+/// window.
+pub fn new<Theme, W: HasRawWindowHandle + HasRawDisplayHandle>(
+ settings: Settings,
+ compatible_window: Option<&W>,
+) -> Result<(Compositor<Theme>, Backend), Error> {
+ let compositor = futures::executor::block_on(Compositor::request(
+ settings,
+ compatible_window,
+ ))
+ .ok_or(Error::GraphicsAdapterNotFound)?;
+
+ let backend = compositor.create_backend();
+
+ Ok((compositor, backend))
+}
+
+/// Presents the given primitives with the given [`Compositor`] and [`Backend`].
+pub fn present<Theme, T: AsRef<str>>(
+ compositor: &mut Compositor<Theme>,
+ backend: &mut Backend,
+ surface: &mut wgpu::Surface,
+ primitives: &[Primitive],
+ viewport: &Viewport,
+ background_color: Color,
+ overlay: &[T],
+) -> Result<(), compositor::SurfaceError> {
+ match surface.get_current_texture() {
+ Ok(frame) => {
+ let mut encoder = compositor.device.create_command_encoder(
+ &wgpu::CommandEncoderDescriptor {
+ label: Some("iced_wgpu encoder"),
+ },
+ );
+
+ let view = &frame
+ .texture
+ .create_view(&wgpu::TextureViewDescriptor::default());
+
+ backend.present(
+ &compositor.device,
+ &compositor.queue,
+ &mut encoder,
+ Some(background_color),
+ view,
+ primitives,
+ viewport,
+ overlay,
+ );
+
+ // Submit work
+ let _submission = compositor.queue.submit(Some(encoder.finish()));
+ frame.present();
+
+ Ok(())
+ }
+ Err(error) => match error {
+ wgpu::SurfaceError::Timeout => {
+ Err(compositor::SurfaceError::Timeout)
+ }
+ wgpu::SurfaceError::Outdated => {
+ Err(compositor::SurfaceError::Outdated)
+ }
+ wgpu::SurfaceError::Lost => Err(compositor::SurfaceError::Lost),
+ wgpu::SurfaceError::OutOfMemory => {
+ Err(compositor::SurfaceError::OutOfMemory)
+ }
+ },
+ }
+}
+
+impl<Theme> graphics::Compositor for Compositor<Theme> {
type Settings = Settings;
type Renderer = Renderer<Theme>;
type Surface = wgpu::Surface;
@@ -127,13 +214,7 @@ impl<Theme> iced_graphics::window::Compositor for Compositor<Theme> {
settings: Self::Settings,
compatible_window: Option<&W>,
) -> Result<(Self, Self::Renderer), Error> {
- let compositor = futures::executor::block_on(Self::request(
- settings,
- compatible_window,
- ))
- .ok_or(Error::GraphicsAdapterNotFound)?;
-
- let backend = compositor.create_backend();
+ let (compositor, backend) = new(settings, compatible_window)?;
Ok((compositor, Renderer::new(backend)))
}
@@ -141,11 +222,16 @@ impl<Theme> iced_graphics::window::Compositor for Compositor<Theme> {
fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>(
&mut self,
window: &W,
+ width: u32,
+ height: u32,
) -> wgpu::Surface {
#[allow(unsafe_code)]
- unsafe {
- self.instance.create_surface(window)
- }
+ let mut surface = unsafe { self.instance.create_surface(window) }
+ .expect("Create surface");
+
+ self.configure_surface(&mut surface, width, height);
+
+ surface
}
fn configure_surface(
@@ -163,6 +249,7 @@ impl<Theme> iced_graphics::window::Compositor for Compositor<Theme> {
width,
height,
alpha_mode: wgpu::CompositeAlphaMode::Auto,
+ view_formats: vec![],
},
);
}
@@ -184,80 +271,166 @@ impl<Theme> iced_graphics::window::Compositor for Compositor<Theme> {
background_color: Color,
overlay: &[T],
) -> Result<(), compositor::SurfaceError> {
- match surface.get_current_texture() {
- Ok(frame) => {
- let mut encoder = self.device.create_command_encoder(
- &wgpu::CommandEncoderDescriptor {
- label: Some("iced_wgpu encoder"),
- },
- );
+ renderer.with_primitives(|backend, primitives| {
+ present(
+ self,
+ backend,
+ surface,
+ primitives,
+ viewport,
+ background_color,
+ overlay,
+ )
+ })
+ }
- let view = &frame
- .texture
- .create_view(&wgpu::TextureViewDescriptor::default());
+ fn screenshot<T: AsRef<str>>(
+ &mut self,
+ renderer: &mut Self::Renderer,
+ _surface: &mut Self::Surface,
+ viewport: &Viewport,
+ background_color: Color,
+ overlay: &[T],
+ ) -> Vec<u8> {
+ renderer.with_primitives(|backend, primitives| {
+ screenshot(
+ self,
+ backend,
+ primitives,
+ viewport,
+ background_color,
+ overlay,
+ )
+ })
+ }
+}
- let _ =
- encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
- label: Some(
- "iced_wgpu::window::Compositor render pass",
- ),
- color_attachments: &[Some(
- wgpu::RenderPassColorAttachment {
- 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,
- });
-
- renderer.with_primitives(|backend, primitives| {
- backend.present(
- &self.device,
- &mut self.staging_belt,
- &mut encoder,
- view,
- primitives,
- viewport,
- overlay,
- );
- });
-
- // Submit work
- self.staging_belt.finish();
- let _submission = self.queue.submit(Some(encoder.finish()));
- frame.present();
-
- // Recall staging buffers
- self.staging_belt.recall();
-
- Ok(())
- }
- Err(error) => match error {
- wgpu::SurfaceError::Timeout => {
- Err(compositor::SurfaceError::Timeout)
- }
- wgpu::SurfaceError::Outdated => {
- Err(compositor::SurfaceError::Outdated)
- }
- wgpu::SurfaceError::Lost => Err(compositor::SurfaceError::Lost),
- wgpu::SurfaceError::OutOfMemory => {
- Err(compositor::SurfaceError::OutOfMemory)
- }
+/// Renders the current surface to an offscreen buffer.
+///
+/// Returns RGBA bytes of the texture data.
+pub fn screenshot<Theme, T: AsRef<str>>(
+ compositor: &Compositor<Theme>,
+ backend: &mut Backend,
+ primitives: &[Primitive],
+ viewport: &Viewport,
+ background_color: Color,
+ overlay: &[T],
+) -> Vec<u8> {
+ let mut encoder = compositor.device.create_command_encoder(
+ &wgpu::CommandEncoderDescriptor {
+ label: Some("iced_wgpu.offscreen.encoder"),
+ },
+ );
+
+ let dimensions = BufferDimensions::new(viewport.physical_size());
+
+ let texture_extent = wgpu::Extent3d {
+ width: dimensions.width,
+ height: dimensions.height,
+ depth_or_array_layers: 1,
+ };
+
+ let texture = compositor.device.create_texture(&wgpu::TextureDescriptor {
+ label: Some("iced_wgpu.offscreen.source_texture"),
+ size: texture_extent,
+ mip_level_count: 1,
+ sample_count: 1,
+ dimension: wgpu::TextureDimension::D2,
+ format: compositor.format,
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT
+ | wgpu::TextureUsages::COPY_SRC
+ | wgpu::TextureUsages::TEXTURE_BINDING,
+ view_formats: &[],
+ });
+
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
+
+ backend.present(
+ &compositor.device,
+ &compositor.queue,
+ &mut encoder,
+ Some(background_color),
+ &view,
+ primitives,
+ viewport,
+ overlay,
+ );
+
+ let texture = crate::color::convert(
+ &compositor.device,
+ &mut encoder,
+ texture,
+ if color::GAMMA_CORRECTION {
+ wgpu::TextureFormat::Rgba8UnormSrgb
+ } else {
+ wgpu::TextureFormat::Rgba8Unorm
+ },
+ );
+
+ let output_buffer =
+ compositor.device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("iced_wgpu.offscreen.output_texture_buffer"),
+ size: (dimensions.padded_bytes_per_row * dimensions.height as usize)
+ as u64,
+ usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+
+ encoder.copy_texture_to_buffer(
+ texture.as_image_copy(),
+ wgpu::ImageCopyBuffer {
+ buffer: &output_buffer,
+ layout: wgpu::ImageDataLayout {
+ offset: 0,
+ bytes_per_row: Some(dimensions.padded_bytes_per_row as u32),
+ rows_per_image: None,
},
+ },
+ texture_extent,
+ );
+
+ let index = compositor.queue.submit(Some(encoder.finish()));
+
+ let slice = output_buffer.slice(..);
+ slice.map_async(wgpu::MapMode::Read, |_| {});
+
+ let _ = compositor
+ .device
+ .poll(wgpu::Maintain::WaitForSubmissionIndex(index));
+
+ let mapped_buffer = slice.get_mapped_range();
+
+ mapped_buffer.chunks(dimensions.padded_bytes_per_row).fold(
+ vec![],
+ |mut acc, row| {
+ acc.extend(&row[..dimensions.unpadded_bytes_per_row]);
+ acc
+ },
+ )
+}
+
+#[derive(Clone, Copy, Debug)]
+struct BufferDimensions {
+ width: u32,
+ height: u32,
+ unpadded_bytes_per_row: usize,
+ padded_bytes_per_row: usize,
+}
+
+impl BufferDimensions {
+ fn new(size: Size<u32>) -> Self {
+ let unpadded_bytes_per_row = size.width as usize * 4; //slice of buffer per row; always RGBA
+ let alignment = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize; //256
+ let padded_bytes_per_row_padding =
+ (alignment - unpadded_bytes_per_row % alignment) % alignment;
+ let padded_bytes_per_row =
+ unpadded_bytes_per_row + padded_bytes_per_row_padding;
+
+ Self {
+ width: size.width,
+ height: size.height,
+ unpadded_bytes_per_row,
+ padded_bytes_per_row,
}
}
}
diff --git a/widget/Cargo.toml b/widget/Cargo.toml
new file mode 100644
index 00000000..14aae72e
--- /dev/null
+++ b/widget/Cargo.toml
@@ -0,0 +1,37 @@
+[package]
+name = "iced_widget"
+version = "0.1.0"
+edition = "2021"
+
+[features]
+lazy = ["ouroboros"]
+image = ["iced_renderer/image"]
+svg = ["iced_renderer/svg"]
+canvas = ["iced_renderer/geometry"]
+qr_code = ["canvas", "qrcode"]
+
+[dependencies]
+unicode-segmentation = "1.6"
+num-traits = "0.2"
+thiserror = "1"
+
+[dependencies.iced_runtime]
+version = "0.1"
+path = "../runtime"
+
+[dependencies.iced_renderer]
+version = "0.1"
+path = "../renderer"
+
+[dependencies.iced_style]
+version = "0.8"
+path = "../style"
+
+[dependencies.ouroboros]
+version = "0.17"
+optional = true
+
+[dependencies.qrcode]
+version = "0.12"
+optional = true
+default-features = false
diff --git a/native/src/widget/button.rs b/widget/src/button.rs
index 39387173..8ebc9657 100644
--- a/native/src/widget/button.rs
+++ b/widget/src/button.rs
@@ -1,15 +1,15 @@
//! Allow your users to perform actions by pressing a button.
//!
//! A [`Button`] has some local [`State`].
-use crate::event::{self, Event};
-use crate::layout;
-use crate::mouse;
-use crate::overlay;
-use crate::renderer;
-use crate::touch;
-use crate::widget::tree::{self, Tree};
-use crate::widget::Operation;
-use crate::{
+use crate::core::event::{self, Event};
+use crate::core::layout;
+use crate::core::mouse;
+use crate::core::overlay;
+use crate::core::renderer;
+use crate::core::touch;
+use crate::core::widget::tree::{self, Tree};
+use crate::core::widget::Operation;
+use crate::core::{
Background, Clipboard, Color, Element, Layout, Length, Padding, Point,
Rectangle, Shell, Vector, Widget,
};
@@ -18,9 +18,9 @@ pub use iced_style::button::{Appearance, StyleSheet};
/// A generic widget that produces a message when pressed.
///
-/// ```
+/// ```no_run
/// # type Button<'a, Message> =
-/// # iced_native::widget::Button<'a, Message, iced_native::renderer::Null>;
+/// # iced_widget::Button<'a, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
/// #
/// #[derive(Clone)]
/// enum Message {
@@ -35,7 +35,7 @@ pub use iced_style::button::{Appearance, StyleSheet};
///
/// ```
/// # type Button<'a, Message> =
-/// # iced_native::widget::Button<'a, Message, iced_native::renderer::Null>;
+/// # iced_widget::Button<'a, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
/// #
/// #[derive(Clone)]
/// enum Message {
@@ -51,9 +51,9 @@ pub use iced_style::button::{Appearance, StyleSheet};
/// }
/// ```
#[allow(missing_debug_implementations)]
-pub struct Button<'a, Message, Renderer>
+pub struct Button<'a, Message, Renderer = crate::Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
content: Element<'a, Message, Renderer>,
@@ -66,7 +66,7 @@ where
impl<'a, Message, Renderer> Button<'a, Message, Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
/// Creates a new [`Button`] with the given content.
@@ -102,8 +102,17 @@ where
/// Sets the message that will be produced when the [`Button`] is pressed.
///
/// Unless `on_press` is called, the [`Button`] will be disabled.
- pub fn on_press(mut self, msg: Message) -> Self {
- self.on_press = Some(msg);
+ pub fn on_press(mut self, on_press: Message) -> Self {
+ self.on_press = Some(on_press);
+ self
+ }
+
+ /// Sets the message that will be produced when the [`Button`] is pressed,
+ /// if `Some`.
+ ///
+ /// If `None`, the [`Button`] will be disabled.
+ pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self {
+ self.on_press = on_press;
self
}
@@ -121,7 +130,7 @@ impl<'a, Message, Renderer> Widget<Message, Renderer>
for Button<'a, Message, Renderer>
where
Message: 'a + Clone,
- Renderer: 'a + crate::Renderer,
+ Renderer: 'a + crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
fn tag(&self) -> tree::Tag {
@@ -187,7 +196,7 @@ where
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
@@ -196,7 +205,7 @@ where
&mut tree.children[0],
event.clone(),
layout.children().next().unwrap(),
- cursor_position,
+ cursor,
renderer,
clipboard,
shell,
@@ -204,14 +213,9 @@ where
return event::Status::Captured;
}
- update(
- event,
- layout,
- cursor_position,
- shell,
- &self.on_press,
- || tree.state.downcast_mut::<State>(),
- )
+ update(event, layout, cursor, shell, &self.on_press, || {
+ tree.state.downcast_mut::<State>()
+ })
}
fn draw(
@@ -221,7 +225,7 @@ where
theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
let bounds = layout.bounds();
@@ -230,7 +234,7 @@ where
let styling = draw(
renderer,
bounds,
- cursor_position,
+ cursor,
self.on_press.is_some(),
theme,
&self.style,
@@ -245,7 +249,7 @@ where
text_color: styling.text_color,
},
content_layout,
- cursor_position,
+ cursor,
&bounds,
);
}
@@ -254,11 +258,11 @@ where
&self,
_tree: &Tree,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
- mouse_interaction(layout, cursor_position, self.on_press.is_some())
+ mouse_interaction(layout, cursor, self.on_press.is_some())
}
fn overlay<'b>(
@@ -279,7 +283,7 @@ impl<'a, Message, Renderer> From<Button<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Message: Clone + 'a,
- Renderer: crate::Renderer + 'a,
+ Renderer: crate::core::Renderer + 'a,
Renderer::Theme: StyleSheet,
{
fn from(button: Button<'a, Message, Renderer>) -> Self {
@@ -305,7 +309,7 @@ impl State {
pub fn update<'a, Message: Clone>(
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
shell: &mut Shell<'_, Message>,
on_press: &Option<Message>,
state: impl FnOnce() -> &'a mut State,
@@ -316,7 +320,7 @@ pub fn update<'a, Message: Clone>(
if on_press.is_some() {
let bounds = layout.bounds();
- if bounds.contains(cursor_position) {
+ if cursor.is_over(bounds) {
let state = state();
state.is_pressed = true;
@@ -335,7 +339,7 @@ pub fn update<'a, Message: Clone>(
let bounds = layout.bounds();
- if bounds.contains(cursor_position) {
+ if cursor.is_over(bounds) {
shell.publish(on_press);
}
@@ -355,10 +359,10 @@ pub fn update<'a, Message: Clone>(
}
/// Draws a [`Button`].
-pub fn draw<'a, Renderer: crate::Renderer>(
+pub fn draw<'a, Renderer: crate::core::Renderer>(
renderer: &mut Renderer,
bounds: Rectangle,
- cursor_position: Point,
+ cursor: mouse::Cursor,
is_enabled: bool,
style_sheet: &dyn StyleSheet<
Style = <Renderer::Theme as StyleSheet>::Style,
@@ -369,7 +373,7 @@ pub fn draw<'a, Renderer: crate::Renderer>(
where
Renderer::Theme: StyleSheet,
{
- let is_mouse_over = bounds.contains(cursor_position);
+ let is_mouse_over = cursor.is_over(bounds);
let styling = if !is_enabled {
style_sheet.disabled(style)
@@ -395,7 +399,7 @@ where
y: bounds.y + styling.shadow_offset.y,
..bounds
},
- border_radius: styling.border_radius.into(),
+ border_radius: styling.border_radius,
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
@@ -406,7 +410,7 @@ where
renderer.fill_quad(
renderer::Quad {
bounds,
- border_radius: styling.border_radius.into(),
+ border_radius: styling.border_radius,
border_width: styling.border_width,
border_color: styling.border_color,
},
@@ -442,10 +446,10 @@ pub fn layout<Renderer>(
/// Returns the [`mouse::Interaction`] of a [`Button`].
pub fn mouse_interaction(
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
is_enabled: bool,
) -> mouse::Interaction {
- let is_mouse_over = layout.bounds().contains(cursor_position);
+ let is_mouse_over = cursor.is_over(layout.bounds());
if is_mouse_over && is_enabled {
mouse::Interaction::Pointer
diff --git a/graphics/src/widget/canvas.rs b/widget/src/canvas.rs
index a8d050f5..96062038 100644
--- a/graphics/src/widget/canvas.rs
+++ b/widget/src/canvas.rs
@@ -1,43 +1,22 @@
//! 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 mod event;
-pub mod fill;
-pub mod path;
-pub mod stroke;
-mod cache;
-mod cursor;
-mod frame;
-mod geometry;
mod program;
-mod style;
-mod text;
-pub use crate::gradient::{self, Gradient};
-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, LineDash, LineJoin, Stroke};
-pub use style::Style;
-pub use text::Text;
-use crate::{Backend, Primitive, Renderer};
+pub use crate::graphics::geometry::*;
+pub use crate::renderer::geometry::*;
-use iced_native::layout::{self, Layout};
-use iced_native::mouse;
-use iced_native::renderer;
-use iced_native::widget::tree::{self, Tree};
-use iced_native::{
- Clipboard, Element, Length, Point, Rectangle, Shell, Size, Vector, Widget,
-};
+use crate::core;
+use crate::core::layout::{self, Layout};
+use crate::core::mouse;
+use crate::core::renderer;
+use crate::core::widget::tree::{self, Tree};
+use crate::core::{Clipboard, Element, Shell, Widget};
+use crate::core::{Length, Rectangle, Size, Vector};
+use crate::graphics::geometry;
use std::marker::PhantomData;
@@ -47,15 +26,12 @@ use std::marker::PhantomData;
/// If you want to get a quick overview, here's how we can draw a simple circle:
///
/// ```no_run
-/// # mod iced {
-/// # pub mod widget {
-/// # pub use iced_graphics::widget::canvas;
-/// # }
-/// # pub use iced_native::{Color, Rectangle, Theme};
-/// # }
-/// use iced::widget::canvas::{self, Canvas, Cursor, Fill, Frame, Geometry, Path, Program};
-/// use iced::{Color, Rectangle, Theme};
-///
+/// # use iced_widget::canvas::{self, Canvas, Fill, Frame, Geometry, Path, Program};
+/// # use iced_widget::core::{Color, Rectangle};
+/// # use iced_widget::core::mouse;
+/// # use iced_widget::style::Theme;
+/// #
+/// # pub type Renderer = iced_widget::renderer::Renderer<Theme>;
/// // First, we define the data we need for drawing
/// #[derive(Debug)]
/// struct Circle {
@@ -66,9 +42,9 @@ use std::marker::PhantomData;
/// impl Program<()> for Circle {
/// type State = ();
///
-/// fn draw(&self, _state: &(), _theme: &Theme, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry>{
+/// fn draw(&self, _state: &(), renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: mouse::Cursor) -> Vec<Geometry>{
/// // We prepare a new `Frame`
-/// let mut frame = Frame::new(bounds.size());
+/// let mut frame = Frame::new(renderer, bounds.size());
///
/// // We create a `Path` representing a simple circle
/// let circle = Path::circle(frame.center(), self.radius);
@@ -85,20 +61,22 @@ use std::marker::PhantomData;
/// let canvas = Canvas::new(Circle { radius: 50.0 });
/// ```
#[derive(Debug)]
-pub struct Canvas<Message, Theme, P>
+pub struct Canvas<P, Message, Renderer = crate::Renderer>
where
- P: Program<Message, Theme>,
+ Renderer: geometry::Renderer,
+ P: Program<Message, Renderer>,
{
width: Length,
height: Length,
program: P,
message_: PhantomData<Message>,
- theme_: PhantomData<Theme>,
+ theme_: PhantomData<Renderer>,
}
-impl<Message, Theme, P> Canvas<Message, Theme, P>
+impl<P, Message, Renderer> Canvas<P, Message, Renderer>
where
- P: Program<Message, Theme>,
+ Renderer: geometry::Renderer,
+ P: Program<Message, Renderer>,
{
const DEFAULT_SIZE: f32 = 100.0;
@@ -126,10 +104,11 @@ where
}
}
-impl<Message, P, B, T> Widget<Message, Renderer<B, T>> for Canvas<Message, T, P>
+impl<P, Message, Renderer> Widget<Message, Renderer>
+ for Canvas<P, Message, Renderer>
where
- P: Program<Message, T>,
- B: Backend,
+ Renderer: geometry::Renderer,
+ P: Program<Message, Renderer>,
{
fn tag(&self) -> tree::Tag {
struct Tag<T>(T);
@@ -150,7 +129,7 @@ where
fn layout(
&self,
- _renderer: &Renderer<B, T>,
+ _renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let limits = limits.width(self.width).height(self.height);
@@ -162,30 +141,24 @@ where
fn on_event(
&mut self,
tree: &mut Tree,
- event: iced_native::Event,
+ event: core::Event,
layout: Layout<'_>,
- cursor_position: Point,
- _renderer: &Renderer<B, T>,
+ cursor: mouse::Cursor,
+ _renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> 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::Touch(touch_event) => {
- Some(Event::Touch(touch_event))
- }
- iced_native::Event::Keyboard(keyboard_event) => {
+ core::Event::Mouse(mouse_event) => Some(Event::Mouse(mouse_event)),
+ core::Event::Touch(touch_event) => Some(Event::Touch(touch_event)),
+ core::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 state = tree.state.downcast_mut::<P::State>();
@@ -206,12 +179,11 @@ where
&self,
tree: &Tree,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_viewport: &Rectangle,
- _renderer: &Renderer<B, T>,
+ _renderer: &Renderer,
) -> mouse::Interaction {
let bounds = layout.bounds();
- let cursor = Cursor::from_window_position(cursor_position);
let state = tree.state.downcast_ref::<P::State>();
self.program.mouse_interaction(state, bounds, cursor)
@@ -220,49 +192,42 @@ where
fn draw(
&self,
tree: &Tree,
- renderer: &mut Renderer<B, T>,
- theme: &T,
+ renderer: &mut Renderer,
+ theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
- use iced_native::Renderer as _;
-
let bounds = layout.bounds();
if bounds.width < 1.0 || bounds.height < 1.0 {
return;
}
- let translation = Vector::new(bounds.x, bounds.y);
- let cursor = Cursor::from_window_position(cursor_position);
let state = tree.state.downcast_ref::<P::State>();
- renderer.with_translation(translation, |renderer| {
- renderer.draw_primitive(Primitive::Group {
- primitives: self
- .program
- .draw(state, theme, bounds, cursor)
- .into_iter()
- .map(Geometry::into_primitive)
- .collect(),
- });
- });
+ renderer.with_translation(
+ Vector::new(bounds.x, bounds.y),
+ |renderer| {
+ renderer.draw(
+ self.program.draw(state, renderer, theme, bounds, cursor),
+ );
+ },
+ );
}
}
-impl<'a, Message, P, B, T> From<Canvas<Message, T, P>>
- for Element<'a, Message, Renderer<B, T>>
+impl<'a, P, Message, Renderer> From<Canvas<P, Message, Renderer>>
+ for Element<'a, Message, Renderer>
where
Message: 'a,
- P: Program<Message, T> + 'a,
- B: Backend,
- T: 'a,
+ Renderer: 'a + geometry::Renderer,
+ P: Program<Message, Renderer> + 'a,
{
fn from(
- canvas: Canvas<Message, T, P>,
- ) -> Element<'a, Message, Renderer<B, T>> {
+ canvas: Canvas<P, Message, Renderer>,
+ ) -> Element<'a, Message, Renderer> {
Element::new(canvas)
}
}
diff --git a/graphics/src/widget/canvas/event.rs b/widget/src/canvas/event.rs
index 7c733a4d..4508c184 100644
--- a/graphics/src/widget/canvas/event.rs
+++ b/widget/src/canvas/event.rs
@@ -1,9 +1,9 @@
//! Handle events of a canvas.
-use iced_native::keyboard;
-use iced_native::mouse;
-use iced_native::touch;
+use crate::core::keyboard;
+use crate::core::mouse;
+use crate::core::touch;
-pub use iced_native::event::Status;
+pub use crate::core::event::Status;
/// A [`Canvas`] event.
///
diff --git a/graphics/src/widget/canvas/program.rs b/widget/src/canvas/program.rs
index 656dbfa6..b3f6175e 100644
--- a/graphics/src/widget/canvas/program.rs
+++ b/widget/src/canvas/program.rs
@@ -1,7 +1,7 @@
-use crate::widget::canvas::event::{self, Event};
-use crate::widget::canvas::mouse;
-use crate::widget::canvas::{Cursor, Geometry};
-use crate::Rectangle;
+use crate::canvas::event::{self, Event};
+use crate::canvas::mouse;
+use crate::core::Rectangle;
+use crate::graphics::geometry;
/// The state and logic of a [`Canvas`].
///
@@ -9,7 +9,10 @@ use crate::Rectangle;
/// application.
///
/// [`Canvas`]: crate::widget::Canvas
-pub trait Program<Message, Theme = iced_native::Theme> {
+pub trait Program<Message, Renderer = crate::Renderer>
+where
+ Renderer: geometry::Renderer,
+{
/// The internal state mutated by the [`Program`].
type State: Default + 'static;
@@ -29,7 +32,7 @@ pub trait Program<Message, Theme = iced_native::Theme> {
_state: &mut Self::State,
_event: Event,
_bounds: Rectangle,
- _cursor: Cursor,
+ _cursor: mouse::Cursor,
) -> (event::Status, Option<Message>) {
(event::Status::Ignored, None)
}
@@ -44,10 +47,11 @@ pub trait Program<Message, Theme = iced_native::Theme> {
fn draw(
&self,
state: &Self::State,
- theme: &Theme,
+ renderer: &Renderer,
+ theme: &Renderer::Theme,
bounds: Rectangle,
- cursor: Cursor,
- ) -> Vec<Geometry>;
+ cursor: mouse::Cursor,
+ ) -> Vec<Renderer::Geometry>;
/// Returns the current mouse interaction of the [`Program`].
///
@@ -59,15 +63,16 @@ pub trait Program<Message, Theme = iced_native::Theme> {
&self,
_state: &Self::State,
_bounds: Rectangle,
- _cursor: Cursor,
+ _cursor: mouse::Cursor,
) -> mouse::Interaction {
mouse::Interaction::default()
}
}
-impl<Message, Theme, T> Program<Message, Theme> for &T
+impl<Message, Renderer, T> Program<Message, Renderer> for &T
where
- T: Program<Message, Theme>,
+ Renderer: geometry::Renderer,
+ T: Program<Message, Renderer>,
{
type State = T::State;
@@ -76,7 +81,7 @@ where
state: &mut Self::State,
event: Event,
bounds: Rectangle,
- cursor: Cursor,
+ cursor: mouse::Cursor,
) -> (event::Status, Option<Message>) {
T::update(self, state, event, bounds, cursor)
}
@@ -84,18 +89,19 @@ where
fn draw(
&self,
state: &Self::State,
- theme: &Theme,
+ renderer: &Renderer,
+ theme: &Renderer::Theme,
bounds: Rectangle,
- cursor: Cursor,
- ) -> Vec<Geometry> {
- T::draw(self, state, theme, bounds, cursor)
+ cursor: mouse::Cursor,
+ ) -> Vec<Renderer::Geometry> {
+ T::draw(self, state, renderer, theme, bounds, cursor)
}
fn mouse_interaction(
&self,
state: &Self::State,
bounds: Rectangle,
- cursor: Cursor,
+ cursor: mouse::Cursor,
) -> mouse::Interaction {
T::mouse_interaction(self, state, bounds, cursor)
}
diff --git a/native/src/widget/checkbox.rs b/widget/src/checkbox.rs
index 9b69e574..aa0bff42 100644
--- a/native/src/widget/checkbox.rs
+++ b/widget/src/checkbox.rs
@@ -1,36 +1,27 @@
//! Show toggle controls using checkboxes.
-use crate::alignment;
-use crate::event::{self, Event};
-use crate::layout;
-use crate::mouse;
-use crate::renderer;
-use crate::text;
-use crate::touch;
-use crate::widget::{self, Row, Text, Tree};
-use crate::{
- Alignment, Clipboard, Element, Layout, Length, Pixels, Point, Rectangle,
- Shell, Widget,
+use crate::core::alignment;
+use crate::core::event::{self, Event};
+use crate::core::layout;
+use crate::core::mouse;
+use crate::core::renderer;
+use crate::core::text;
+use crate::core::touch;
+use crate::core::widget::Tree;
+use crate::core::{
+ Alignment, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell,
+ Widget,
};
+use crate::{Row, Text};
pub use iced_style::checkbox::{Appearance, StyleSheet};
-/// The icon in a [`Checkbox`].
-#[derive(Debug, Clone, PartialEq)]
-pub struct Icon<Font> {
- /// Font that will be used to display the `code_point`,
- pub font: Font,
- /// The unicode code point that will be used as the icon.
- pub code_point: char,
- /// Font size of the content.
- pub size: Option<f32>,
-}
-
/// A box that can be checked.
///
/// # Example
///
-/// ```
-/// # type Checkbox<'a, Message> = iced_native::widget::Checkbox<'a, Message, iced_native::renderer::Null>;
+/// ```no_run
+/// # type Checkbox<'a, Message> =
+/// # iced_widget::Checkbox<'a, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
/// #
/// pub enum Message {
/// CheckboxToggled(bool),
@@ -43,10 +34,10 @@ pub struct Icon<Font> {
///
/// ![Checkbox drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/checkbox.png?raw=true)
#[allow(missing_debug_implementations)]
-pub struct Checkbox<'a, Message, Renderer>
+pub struct Checkbox<'a, Message, Renderer = crate::Renderer>
where
Renderer: text::Renderer,
- Renderer::Theme: StyleSheet + widget::text::StyleSheet,
+ Renderer::Theme: StyleSheet + crate::text::StyleSheet,
{
is_checked: bool,
on_toggle: Box<dyn Fn(bool) -> Message + 'a>,
@@ -55,7 +46,9 @@ where
size: f32,
spacing: f32,
text_size: Option<f32>,
- font: Renderer::Font,
+ text_line_height: text::LineHeight,
+ text_shaping: text::Shaping,
+ font: Option<Renderer::Font>,
icon: Icon<Renderer::Font>,
style: <Renderer::Theme as StyleSheet>::Style,
}
@@ -63,7 +56,7 @@ where
impl<'a, Message, Renderer> Checkbox<'a, Message, Renderer>
where
Renderer: text::Renderer,
- Renderer::Theme: StyleSheet + widget::text::StyleSheet,
+ Renderer::Theme: StyleSheet + crate::text::StyleSheet,
{
/// The default size of a [`Checkbox`].
const DEFAULT_SIZE: f32 = 20.0;
@@ -91,11 +84,15 @@ where
size: Self::DEFAULT_SIZE,
spacing: Self::DEFAULT_SPACING,
text_size: None,
- font: Renderer::Font::default(),
+ text_line_height: text::LineHeight::default(),
+ text_shaping: text::Shaping::Basic,
+ font: None,
icon: Icon {
font: Renderer::ICON_FONT,
code_point: Renderer::CHECKMARK_ICON,
size: None,
+ line_height: text::LineHeight::default(),
+ shaping: text::Shaping::Basic,
},
style: Default::default(),
}
@@ -125,11 +122,26 @@ where
self
}
+ /// Sets the text [`LineHeight`] of the [`Checkbox`].
+ pub fn text_line_height(
+ mut self,
+ line_height: impl Into<text::LineHeight>,
+ ) -> Self {
+ self.text_line_height = line_height.into();
+ self
+ }
+
+ /// Sets the [`text::Shaping`] strategy of the [`Checkbox`].
+ pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
+ self.text_shaping = shaping;
+ self
+ }
+
/// Sets the [`Font`] of the text of the [`Checkbox`].
///
/// [`Font`]: crate::text::Renderer::Font
- pub fn font(mut self, font: Renderer::Font) -> Self {
- self.font = font;
+ pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
+ self.font = Some(font.into());
self
}
@@ -153,7 +165,7 @@ impl<'a, Message, Renderer> Widget<Message, Renderer>
for Checkbox<'a, Message, Renderer>
where
Renderer: text::Renderer,
- Renderer::Theme: StyleSheet + widget::text::StyleSheet,
+ Renderer::Theme: StyleSheet + crate::text::StyleSheet,
{
fn width(&self) -> Length {
self.width
@@ -175,12 +187,14 @@ where
.push(Row::new().width(self.size).height(self.size))
.push(
Text::new(&self.label)
- .font(self.font.clone())
+ .font(self.font.unwrap_or_else(|| renderer.default_font()))
.width(self.width)
.size(
self.text_size
.unwrap_or_else(|| renderer.default_size()),
- ),
+ )
+ .line_height(self.text_line_height)
+ .shaping(self.text_shaping),
)
.layout(renderer, limits)
}
@@ -190,7 +204,7 @@ where
_tree: &mut Tree,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
@@ -198,7 +212,7 @@ where
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
- let mouse_over = layout.bounds().contains(cursor_position);
+ let mouse_over = cursor.is_over(layout.bounds());
if mouse_over {
shell.publish((self.on_toggle)(!self.is_checked));
@@ -216,11 +230,11 @@ where
&self,
_tree: &Tree,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
- if layout.bounds().contains(cursor_position) {
+ if cursor.is_over(layout.bounds()) {
mouse::Interaction::Pointer
} else {
mouse::Interaction::default()
@@ -234,11 +248,10 @@ where
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
- let bounds = layout.bounds();
- let is_mouse_over = bounds.contains(cursor_position);
+ let is_mouse_over = cursor.is_over(layout.bounds());
let mut children = layout.children();
@@ -255,7 +268,7 @@ where
renderer.fill_quad(
renderer::Quad {
bounds,
- border_radius: custom_style.border_radius.into(),
+ border_radius: custom_style.border_radius,
border_width: custom_style.border_width,
border_color: custom_style.border_color,
},
@@ -266,14 +279,17 @@ where
font,
code_point,
size,
+ line_height,
+ shaping,
} = &self.icon;
- let size = size.map(f32::from).unwrap_or(bounds.height * 0.7);
+ let size = size.unwrap_or(bounds.height * 0.7);
if self.is_checked {
renderer.fill_text(text::Text {
content: &code_point.to_string(),
- font: font.clone(),
+ font: *font,
size,
+ line_height: *line_height,
bounds: Rectangle {
x: bounds.center_x(),
y: bounds.center_y(),
@@ -282,6 +298,7 @@ where
color: custom_style.icon_color,
horizontal_alignment: alignment::Horizontal::Center,
vertical_alignment: alignment::Vertical::Center,
+ shaping: *shaping,
});
}
}
@@ -289,18 +306,20 @@ where
{
let label_layout = children.next().unwrap();
- widget::text::draw(
+ crate::text::draw(
renderer,
style,
label_layout,
&self.label,
self.text_size,
- self.font.clone(),
- widget::text::Appearance {
+ self.text_line_height,
+ self.font,
+ crate::text::Appearance {
color: custom_style.text_color,
},
alignment::Horizontal::Left,
alignment::Vertical::Center,
+ self.text_shaping,
);
}
}
@@ -311,7 +330,7 @@ impl<'a, Message, Renderer> From<Checkbox<'a, Message, Renderer>>
where
Message: 'a,
Renderer: 'a + text::Renderer,
- Renderer::Theme: StyleSheet + widget::text::StyleSheet,
+ Renderer::Theme: StyleSheet + crate::text::StyleSheet,
{
fn from(
checkbox: Checkbox<'a, Message, Renderer>,
@@ -319,3 +338,18 @@ where
Element::new(checkbox)
}
}
+
+/// The icon in a [`Checkbox`].
+#[derive(Debug, Clone, PartialEq)]
+pub struct Icon<Font> {
+ /// Font that will be used to display the `code_point`,
+ pub font: Font,
+ /// The unicode code point that will be used as the icon.
+ pub code_point: char,
+ /// Font size of the content.
+ pub size: Option<f32>,
+ /// The line height of the icon.
+ pub line_height: text::LineHeight,
+ /// The shaping strategy of the icon.
+ pub shaping: text::Shaping,
+}
diff --git a/native/src/widget/column.rs b/widget/src/column.rs
index ebe579d5..d92d794b 100644
--- a/native/src/widget/column.rs
+++ b/widget/src/column.rs
@@ -1,18 +1,18 @@
//! Distribute content vertically.
-use crate::event::{self, Event};
-use crate::layout;
-use crate::mouse;
-use crate::overlay;
-use crate::renderer;
-use crate::widget::{Operation, Tree};
-use crate::{
- Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Point,
- Rectangle, Shell, Widget,
+use crate::core::event::{self, Event};
+use crate::core::layout;
+use crate::core::mouse;
+use crate::core::overlay;
+use crate::core::renderer;
+use crate::core::widget::{Operation, Tree};
+use crate::core::{
+ Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle,
+ Shell, Widget,
};
/// A container that distributes its contents vertically.
#[allow(missing_debug_implementations)]
-pub struct Column<'a, Message, Renderer> {
+pub struct Column<'a, Message, Renderer = crate::Renderer> {
spacing: f32,
padding: Padding,
width: Length,
@@ -102,7 +102,7 @@ impl<'a, Message, Renderer> Default for Column<'a, Message, Renderer> {
impl<'a, Message, Renderer> Widget<Message, Renderer>
for Column<'a, Message, Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
{
fn children(&self) -> Vec<Tree> {
self.children.iter().map(Tree::new).collect()
@@ -166,7 +166,7 @@ where
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
@@ -180,7 +180,7 @@ where
state,
event.clone(),
layout,
- cursor_position,
+ cursor,
renderer,
clipboard,
shell,
@@ -193,7 +193,7 @@ where
&self,
tree: &Tree,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
@@ -203,11 +203,7 @@ where
.zip(layout.children())
.map(|((child, state), layout)| {
child.as_widget().mouse_interaction(
- state,
- layout,
- cursor_position,
- viewport,
- renderer,
+ state, layout, cursor, viewport, renderer,
)
})
.max()
@@ -221,7 +217,7 @@ where
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
) {
for ((child, state), layout) in self
@@ -230,15 +226,9 @@ where
.zip(&tree.children)
.zip(layout.children())
{
- child.as_widget().draw(
- state,
- renderer,
- theme,
- style,
- layout,
- cursor_position,
- viewport,
- );
+ child
+ .as_widget()
+ .draw(state, renderer, theme, style, layout, cursor, viewport);
}
}
@@ -256,7 +246,7 @@ impl<'a, Message, Renderer> From<Column<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Message: 'a,
- Renderer: crate::Renderer + 'a,
+ Renderer: crate::core::Renderer + 'a,
{
fn from(column: Column<'a, Message, Renderer>) -> Self {
Self::new(column)
diff --git a/native/src/widget/container.rs b/widget/src/container.rs
index b77bf50d..da9a31d6 100644
--- a/native/src/widget/container.rs
+++ b/widget/src/container.rs
@@ -1,12 +1,12 @@
//! Decorate content and apply alignment.
-use crate::alignment::{self, Alignment};
-use crate::event::{self, Event};
-use crate::layout;
-use crate::mouse;
-use crate::overlay;
-use crate::renderer;
-use crate::widget::{self, Operation, Tree};
-use crate::{
+use crate::core::alignment::{self, Alignment};
+use crate::core::event::{self, Event};
+use crate::core::layout;
+use crate::core::mouse;
+use crate::core::overlay;
+use crate::core::renderer;
+use crate::core::widget::{self, Operation, Tree};
+use crate::core::{
Background, Clipboard, Color, Element, Layout, Length, Padding, Pixels,
Point, Rectangle, Shell, Widget,
};
@@ -17,9 +17,9 @@ pub use iced_style::container::{Appearance, StyleSheet};
///
/// It is normally used for alignment purposes.
#[allow(missing_debug_implementations)]
-pub struct Container<'a, Message, Renderer>
+pub struct Container<'a, Message, Renderer = crate::Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
id: Option<Id>,
@@ -36,7 +36,7 @@ where
impl<'a, Message, Renderer> Container<'a, Message, Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
/// Creates an empty [`Container`].
@@ -131,7 +131,7 @@ where
impl<'a, Message, Renderer> Widget<Message, Renderer>
for Container<'a, Message, Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
fn children(&self) -> Vec<Tree> {
@@ -196,7 +196,7 @@ where
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
@@ -205,7 +205,7 @@ where
&mut tree.children[0],
event,
layout.children().next().unwrap(),
- cursor_position,
+ cursor,
renderer,
clipboard,
shell,
@@ -216,14 +216,14 @@ where
&self,
tree: &Tree,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.content.as_widget().mouse_interaction(
&tree.children[0],
layout.children().next().unwrap(),
- cursor_position,
+ cursor,
viewport,
renderer,
)
@@ -236,7 +236,7 @@ where
theme: &Renderer::Theme,
renderer_style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
) {
let style = theme.appearance(&self.style);
@@ -253,7 +253,7 @@ where
.unwrap_or(renderer_style.text_color),
},
layout.children().next().unwrap(),
- cursor_position,
+ cursor,
viewport,
);
}
@@ -276,7 +276,7 @@ impl<'a, Message, Renderer> From<Container<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Message: 'a,
- Renderer: 'a + crate::Renderer,
+ Renderer: 'a + crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
fn from(
@@ -326,13 +326,13 @@ pub fn draw_background<Renderer>(
appearance: &Appearance,
bounds: Rectangle,
) where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
{
if appearance.background.is_some() || appearance.border_width > 0.0 {
renderer.fill_quad(
renderer::Quad {
bounds,
- border_radius: appearance.border_radius.into(),
+ border_radius: appearance.border_radius,
border_width: appearance.border_width,
border_color: appearance.border_color,
},
diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs
new file mode 100644
index 00000000..3f5136f8
--- /dev/null
+++ b/widget/src/helpers.rs
@@ -0,0 +1,368 @@
+//! Helper functions to create pure widgets.
+use crate::button::{self, Button};
+use crate::checkbox::{self, Checkbox};
+use crate::container::{self, Container};
+use crate::core;
+use crate::core::widget::operation;
+use crate::core::{Element, Length, Pixels};
+use crate::overlay;
+use crate::pick_list::{self, PickList};
+use crate::progress_bar::{self, ProgressBar};
+use crate::radio::{self, Radio};
+use crate::rule::{self, Rule};
+use crate::runtime::Command;
+use crate::scrollable::{self, Scrollable};
+use crate::slider::{self, Slider};
+use crate::text::{self, Text};
+use crate::text_input::{self, TextInput};
+use crate::toggler::{self, Toggler};
+use crate::tooltip::{self, Tooltip};
+use crate::{Column, MouseArea, Row, Space, VerticalSlider};
+
+use std::borrow::Cow;
+use std::ops::RangeInclusive;
+
+/// Creates a [`Column`] with the given children.
+///
+/// [`Column`]: widget::Column
+#[macro_export]
+macro_rules! column {
+ () => (
+ $crate::Column::new()
+ );
+ ($($x:expr),+ $(,)?) => (
+ $crate::Column::with_children(vec![$($crate::core::Element::from($x)),+])
+ );
+}
+
+/// Creates a [`Row`] with the given children.
+///
+/// [`Row`]: widget::Row
+#[macro_export]
+macro_rules! row {
+ () => (
+ $crate::Row::new()
+ );
+ ($($x:expr),+ $(,)?) => (
+ $crate::Row::with_children(vec![$($crate::core::Element::from($x)),+])
+ );
+}
+
+/// Creates a new [`Container`] with the provided content.
+///
+/// [`Container`]: widget::Container
+pub fn container<'a, Message, Renderer>(
+ content: impl Into<Element<'a, Message, Renderer>>,
+) -> Container<'a, Message, Renderer>
+where
+ Renderer: core::Renderer,
+ Renderer::Theme: container::StyleSheet,
+{
+ Container::new(content)
+}
+
+/// Creates a new [`Column`] with the given children.
+///
+/// [`Column`]: widget::Column
+pub fn column<Message, Renderer>(
+ children: Vec<Element<'_, Message, Renderer>>,
+) -> Column<'_, Message, Renderer> {
+ Column::with_children(children)
+}
+
+/// Creates a new [`Row`] with the given children.
+///
+/// [`Row`]: widget::Row
+pub fn row<Message, Renderer>(
+ children: Vec<Element<'_, Message, Renderer>>,
+) -> Row<'_, Message, Renderer> {
+ Row::with_children(children)
+}
+
+/// Creates a new [`Scrollable`] with the provided content.
+///
+/// [`Scrollable`]: widget::Scrollable
+pub fn scrollable<'a, Message, Renderer>(
+ content: impl Into<Element<'a, Message, Renderer>>,
+) -> Scrollable<'a, Message, Renderer>
+where
+ Renderer: core::Renderer,
+ Renderer::Theme: scrollable::StyleSheet,
+{
+ Scrollable::new(content)
+}
+
+/// Creates a new [`Button`] with the provided content.
+///
+/// [`Button`]: widget::Button
+pub fn button<'a, Message, Renderer>(
+ content: impl Into<Element<'a, Message, Renderer>>,
+) -> Button<'a, Message, Renderer>
+where
+ Renderer: core::Renderer,
+ Renderer::Theme: button::StyleSheet,
+ <Renderer::Theme as button::StyleSheet>::Style: Default,
+{
+ Button::new(content)
+}
+
+/// Creates a new [`Tooltip`] with the provided content, tooltip text, and [`tooltip::Position`].
+///
+/// [`Tooltip`]: widget::Tooltip
+/// [`tooltip::Position`]: widget::tooltip::Position
+pub fn tooltip<'a, Message, Renderer>(
+ content: impl Into<Element<'a, Message, Renderer>>,
+ tooltip: impl ToString,
+ position: tooltip::Position,
+) -> crate::Tooltip<'a, Message, Renderer>
+where
+ Renderer: core::text::Renderer,
+ Renderer::Theme: container::StyleSheet + text::StyleSheet,
+{
+ Tooltip::new(content, tooltip.to_string(), position)
+}
+
+/// Creates a new [`Text`] widget with the provided content.
+///
+/// [`Text`]: widget::Text
+pub fn text<'a, Renderer>(text: impl ToString) -> Text<'a, Renderer>
+where
+ Renderer: core::text::Renderer,
+ Renderer::Theme: text::StyleSheet,
+{
+ Text::new(text.to_string())
+}
+
+/// Creates a new [`Checkbox`].
+///
+/// [`Checkbox`]: widget::Checkbox
+pub fn checkbox<'a, Message, Renderer>(
+ label: impl Into<String>,
+ is_checked: bool,
+ f: impl Fn(bool) -> Message + 'a,
+) -> Checkbox<'a, Message, Renderer>
+where
+ Renderer: core::text::Renderer,
+ Renderer::Theme: checkbox::StyleSheet + text::StyleSheet,
+{
+ Checkbox::new(label, is_checked, f)
+}
+
+/// Creates a new [`Radio`].
+///
+/// [`Radio`]: widget::Radio
+pub fn radio<Message, Renderer, V>(
+ label: impl Into<String>,
+ value: V,
+ selected: Option<V>,
+ on_click: impl FnOnce(V) -> Message,
+) -> Radio<Message, Renderer>
+where
+ Message: Clone,
+ Renderer: core::text::Renderer,
+ Renderer::Theme: radio::StyleSheet,
+ V: Copy + Eq,
+{
+ Radio::new(label, value, selected, on_click)
+}
+
+/// Creates a new [`Toggler`].
+///
+/// [`Toggler`]: widget::Toggler
+pub fn toggler<'a, Message, Renderer>(
+ label: impl Into<Option<String>>,
+ is_checked: bool,
+ f: impl Fn(bool) -> Message + 'a,
+) -> Toggler<'a, Message, Renderer>
+where
+ Renderer: core::text::Renderer,
+ Renderer::Theme: toggler::StyleSheet,
+{
+ Toggler::new(label, is_checked, f)
+}
+
+/// Creates a new [`TextInput`].
+///
+/// [`TextInput`]: widget::TextInput
+pub fn text_input<'a, Message, Renderer>(
+ placeholder: &str,
+ value: &str,
+) -> TextInput<'a, Message, Renderer>
+where
+ Message: Clone,
+ Renderer: core::text::Renderer,
+ Renderer::Theme: text_input::StyleSheet,
+{
+ TextInput::new(placeholder, value)
+}
+
+/// Creates a new [`Slider`].
+///
+/// [`Slider`]: widget::Slider
+pub fn slider<'a, T, Message, Renderer>(
+ range: std::ops::RangeInclusive<T>,
+ value: T,
+ on_change: impl Fn(T) -> Message + 'a,
+) -> Slider<'a, T, Message, Renderer>
+where
+ T: Copy + From<u8> + std::cmp::PartialOrd,
+ Message: Clone,
+ Renderer: core::Renderer,
+ Renderer::Theme: slider::StyleSheet,
+{
+ Slider::new(range, value, on_change)
+}
+
+/// Creates a new [`VerticalSlider`].
+///
+/// [`VerticalSlider`]: widget::VerticalSlider
+pub fn vertical_slider<'a, T, Message, Renderer>(
+ range: std::ops::RangeInclusive<T>,
+ value: T,
+ on_change: impl Fn(T) -> Message + 'a,
+) -> VerticalSlider<'a, T, Message, Renderer>
+where
+ T: Copy + From<u8> + std::cmp::PartialOrd,
+ Message: Clone,
+ Renderer: core::Renderer,
+ Renderer::Theme: slider::StyleSheet,
+{
+ VerticalSlider::new(range, value, on_change)
+}
+
+/// Creates a new [`PickList`].
+///
+/// [`PickList`]: widget::PickList
+pub fn pick_list<'a, Message, Renderer, T>(
+ options: impl Into<Cow<'a, [T]>>,
+ selected: Option<T>,
+ on_selected: impl Fn(T) -> Message + 'a,
+) -> PickList<'a, T, Message, Renderer>
+where
+ T: ToString + Eq + 'static,
+ [T]: ToOwned<Owned = Vec<T>>,
+ Renderer: core::text::Renderer,
+ Renderer::Theme: pick_list::StyleSheet
+ + scrollable::StyleSheet
+ + overlay::menu::StyleSheet
+ + container::StyleSheet,
+ <Renderer::Theme as overlay::menu::StyleSheet>::Style:
+ From<<Renderer::Theme as pick_list::StyleSheet>::Style>,
+{
+ PickList::new(options, selected, on_selected)
+}
+
+/// Creates a new horizontal [`Space`] with the given [`Length`].
+///
+/// [`Space`]: widget::Space
+pub fn horizontal_space(width: impl Into<Length>) -> Space {
+ Space::with_width(width)
+}
+
+/// Creates a new vertical [`Space`] with the given [`Length`].
+///
+/// [`Space`]: widget::Space
+pub fn vertical_space(height: impl Into<Length>) -> Space {
+ Space::with_height(height)
+}
+
+/// Creates a horizontal [`Rule`] with the given height.
+///
+/// [`Rule`]: widget::Rule
+pub fn horizontal_rule<Renderer>(height: impl Into<Pixels>) -> Rule<Renderer>
+where
+ Renderer: core::Renderer,
+ Renderer::Theme: rule::StyleSheet,
+{
+ Rule::horizontal(height)
+}
+
+/// Creates a vertical [`Rule`] with the given width.
+///
+/// [`Rule`]: widget::Rule
+pub fn vertical_rule<Renderer>(width: impl Into<Pixels>) -> Rule<Renderer>
+where
+ Renderer: core::Renderer,
+ Renderer::Theme: rule::StyleSheet,
+{
+ Rule::vertical(width)
+}
+
+/// Creates a new [`ProgressBar`].
+///
+/// It expects:
+/// * an inclusive range of possible values, and
+/// * the current value of the [`ProgressBar`].
+///
+/// [`ProgressBar`]: widget::ProgressBar
+pub fn progress_bar<Renderer>(
+ range: RangeInclusive<f32>,
+ value: f32,
+) -> ProgressBar<Renderer>
+where
+ Renderer: core::Renderer,
+ Renderer::Theme: progress_bar::StyleSheet,
+{
+ ProgressBar::new(range, value)
+}
+
+/// Creates a new [`Image`].
+///
+/// [`Image`]: widget::Image
+#[cfg(feature = "image")]
+pub fn image<Handle>(handle: impl Into<Handle>) -> crate::Image<Handle> {
+ crate::Image::new(handle.into())
+}
+
+/// Creates a new [`Svg`] widget from the given [`Handle`].
+///
+/// [`Svg`]: widget::Svg
+/// [`Handle`]: widget::svg::Handle
+#[cfg(feature = "svg")]
+pub fn svg<Renderer>(
+ handle: impl Into<core::svg::Handle>,
+) -> crate::Svg<Renderer>
+where
+ Renderer: core::svg::Renderer,
+ Renderer::Theme: crate::svg::StyleSheet,
+{
+ crate::Svg::new(handle)
+}
+
+/// Creates a new [`Canvas`].
+#[cfg(feature = "canvas")]
+pub fn canvas<P, Message, Renderer>(
+ program: P,
+) -> crate::Canvas<P, Message, Renderer>
+where
+ Renderer: crate::graphics::geometry::Renderer,
+ P: crate::canvas::Program<Message, Renderer>,
+{
+ crate::Canvas::new(program)
+}
+
+/// Focuses the previous focusable widget.
+pub fn focus_previous<Message>() -> Command<Message>
+where
+ Message: 'static,
+{
+ Command::widget(operation::focusable::focus_previous())
+}
+
+/// Focuses the next focusable widget.
+pub fn focus_next<Message>() -> Command<Message>
+where
+ Message: 'static,
+{
+ Command::widget(operation::focusable::focus_next())
+}
+
+/// A container intercepting mouse events.
+pub fn mouse_area<'a, Message, Renderer>(
+ widget: impl Into<Element<'a, Message, Renderer>>,
+) -> MouseArea<'a, Message, Renderer>
+where
+ Renderer: core::Renderer,
+{
+ MouseArea::new(widget)
+}
diff --git a/native/src/widget/image.rs b/widget/src/image.rs
index 73257a74..66bf2156 100644
--- a/native/src/widget/image.rs
+++ b/widget/src/image.rs
@@ -2,16 +2,19 @@
pub mod viewer;
pub use viewer::Viewer;
-use crate::image;
-use crate::layout;
-use crate::renderer;
-use crate::widget::Tree;
-use crate::{
- ContentFit, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget,
+use crate::core::image;
+use crate::core::layout;
+use crate::core::mouse;
+use crate::core::renderer;
+use crate::core::widget::Tree;
+use crate::core::{
+ ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget,
};
use std::hash::Hash;
+pub use image::Handle;
+
/// Creates a new [`Viewer`] with the given image `Handle`.
pub fn viewer<Handle>(handle: Handle) -> Viewer<Handle> {
Viewer::new(handle)
@@ -21,9 +24,8 @@ pub fn viewer<Handle>(handle: Handle) -> Viewer<Handle> {
///
/// # Example
///
-/// ```
-/// # use iced_native::widget::Image;
-/// # use iced_native::image;
+/// ```no_run
+/// # use iced_widget::image::{self, Image};
/// #
/// let image = Image::<image::Handle>::new("resources/ferris.png");
/// ```
@@ -185,7 +187,7 @@ where
_theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
- _cursor_position: Point,
+ _cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
draw(renderer, layout, &self.handle, self.content_fit)
diff --git a/native/src/widget/image/viewer.rs b/widget/src/image/viewer.rs
index 1f8d5d7a..8040d6bd 100644
--- a/native/src/widget/image/viewer.rs
+++ b/widget/src/image/viewer.rs
@@ -1,11 +1,11 @@
//! Zoom and pan on an image.
-use crate::event::{self, Event};
-use crate::image;
-use crate::layout;
-use crate::mouse;
-use crate::renderer;
-use crate::widget::tree::{self, Tree};
-use crate::{
+use crate::core::event::{self, Event};
+use crate::core::image;
+use crate::core::layout;
+use crate::core::mouse;
+use crate::core::renderer;
+use crate::core::widget::tree::{self, Tree};
+use crate::core::{
Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size,
Vector, Widget,
};
@@ -144,18 +144,19 @@ where
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
_shell: &mut Shell<'_, Message>,
) -> event::Status {
let bounds = layout.bounds();
- let is_mouse_over = bounds.contains(cursor_position);
match event {
- Event::Mouse(mouse::Event::WheelScrolled { delta })
- if is_mouse_over =>
- {
+ Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
+ let Some(cursor_position) = cursor.position() else {
+ return event::Status::Ignored;
+ };
+
match delta {
mouse::ScrollDelta::Lines { y, .. }
| mouse::ScrollDelta::Pixels { y, .. } => {
@@ -205,9 +206,11 @@ where
event::Status::Captured
}
- Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
- if is_mouse_over =>
- {
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
+ let Some(cursor_position) = cursor.position() else {
+ return event::Status::Ignored;
+ };
+
let state = tree.state.downcast_mut::<State>();
state.cursor_grabbed_at = Some(cursor_position);
@@ -277,13 +280,13 @@ where
&self,
tree: &Tree,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
let state = tree.state.downcast_ref::<State>();
let bounds = layout.bounds();
- let is_mouse_over = bounds.contains(cursor_position);
+ let is_mouse_over = cursor.is_over(bounds);
if state.is_cursor_grabbed() {
mouse::Interaction::Grabbing
@@ -301,7 +304,7 @@ where
_theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
- _cursor_position: Point,
+ _cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
let state = tree.state.downcast_ref::<State>();
diff --git a/lazy/src/lazy.rs b/widget/src/lazy.rs
index 5e909a49..da287f06 100644
--- a/lazy/src/lazy.rs
+++ b/widget/src/lazy.rs
@@ -1,18 +1,33 @@
-use iced_native::event;
-use iced_native::layout::{self, Layout};
-use iced_native::mouse;
-use iced_native::overlay;
-use iced_native::renderer;
-use iced_native::widget::tree::{self, Tree};
-use iced_native::widget::{self, Widget};
-use iced_native::Element;
-use iced_native::{Clipboard, Hasher, Length, Point, Rectangle, Shell, Size};
+#![allow(clippy::await_holding_refcell_ref, clippy::type_complexity)]
+pub(crate) mod helpers;
+
+pub mod component;
+pub mod responsive;
+
+pub use component::Component;
+pub use responsive::Responsive;
+
+mod cache;
+
+use crate::core::event::{self, Event};
+use crate::core::layout::{self, Layout};
+use crate::core::mouse;
+use crate::core::overlay;
+use crate::core::renderer;
+use crate::core::widget::tree::{self, Tree};
+use crate::core::widget::{self, Widget};
+use crate::core::Element;
+use crate::core::{
+ self, Clipboard, Hasher, Length, Point, Rectangle, Shell, Size,
+};
+use crate::runtime::overlay::Nested;
use ouroboros::self_referencing;
use std::cell::RefCell;
use std::hash::{Hash, Hasher as H};
use std::rc::Rc;
+/// A widget that only rebuilds its contents when necessary.
#[allow(missing_debug_implementations)]
pub struct Lazy<'a, Message, Renderer, Dependency, View> {
dependency: Dependency,
@@ -28,6 +43,8 @@ where
Dependency: Hash + 'a,
View: Into<Element<'static, Message, Renderer>>,
{
+ /// Creates a new [`Lazy`] widget with the given data `Dependency` and a
+ /// closure that can turn this data into a widget tree.
pub fn new(
dependency: Dependency,
view: impl Fn(&Dependency) -> View + 'a,
@@ -41,7 +58,7 @@ where
fn with_element<T>(
&self,
- f: impl FnOnce(&Element<Message, Renderer>) -> T,
+ f: impl FnOnce(&Element<'_, Message, Renderer>) -> T,
) -> T {
f(self
.element
@@ -55,7 +72,7 @@ where
fn with_element_mut<T>(
&self,
- f: impl FnOnce(&mut Element<Message, Renderer>) -> T,
+ f: impl FnOnce(&mut Element<'_, Message, Renderer>) -> T,
) -> T {
f(self
.element
@@ -79,7 +96,7 @@ where
View: Into<Element<'static, Message, Renderer>> + 'static,
Dependency: Hash + 'a,
Message: 'static,
- Renderer: iced_native::Renderer + 'static,
+ Renderer: core::Renderer + 'static,
{
fn tag(&self) -> tree::Tag {
struct Tag<T>(T);
@@ -163,9 +180,9 @@ where
fn on_event(
&mut self,
tree: &mut Tree,
- event: iced_native::Event,
+ event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
@@ -175,7 +192,7 @@ where
&mut tree.children[0],
event,
layout,
- cursor_position,
+ cursor,
renderer,
clipboard,
shell,
@@ -187,7 +204,7 @@ where
&self,
tree: &Tree,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
@@ -195,7 +212,7 @@ where
element.as_widget().mouse_interaction(
&tree.children[0],
layout,
- cursor_position,
+ cursor,
viewport,
renderer,
)
@@ -209,7 +226,7 @@ where
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
) {
self.with_element(|element| {
@@ -219,7 +236,7 @@ where
theme,
style,
layout,
- cursor_position,
+ cursor,
viewport,
)
})
@@ -244,14 +261,17 @@ where
.unwrap(),
tree: &mut tree.children[0],
overlay_builder: |element, tree| {
- element.as_widget_mut().overlay(tree, layout, renderer)
+ element
+ .as_widget_mut()
+ .overlay(tree, layout, renderer)
+ .map(|overlay| RefCell::new(Nested::new(overlay)))
},
}
.build(),
));
- let has_overlay = overlay
- .with_overlay_maybe(|overlay| overlay::Element::position(overlay));
+ let has_overlay =
+ overlay.with_overlay_maybe(|overlay| overlay.position());
has_overlay
.map(|position| overlay::Element::new(position, Box::new(overlay)))
@@ -259,18 +279,14 @@ where
}
#[self_referencing]
-struct Inner<'a, Message, Renderer>
-where
- Message: 'a,
- Renderer: 'a,
-{
+struct Inner<'a, Message: 'a, Renderer: 'a> {
cell: Rc<RefCell<Option<Element<'static, Message, Renderer>>>>,
element: Element<'static, Message, Renderer>,
tree: &'a mut Tree,
#[borrows(mut element, mut tree)]
- #[covariant]
- overlay: Option<overlay::Element<'this, Message, Renderer>>,
+ #[not_covariant]
+ overlay: Option<RefCell<Nested<'this, Message, Renderer>>>,
}
struct Overlay<'a, Message, Renderer>(Option<Inner<'a, Message, Renderer>>);
@@ -285,26 +301,27 @@ impl<'a, Message, Renderer> Drop for Overlay<'a, Message, Renderer> {
impl<'a, Message, Renderer> Overlay<'a, Message, Renderer> {
fn with_overlay_maybe<T>(
&self,
- f: impl FnOnce(&overlay::Element<'_, Message, Renderer>) -> T,
+ f: impl FnOnce(&mut Nested<'_, Message, Renderer>) -> T,
) -> Option<T> {
- self.0.as_ref().unwrap().borrow_overlay().as_ref().map(f)
+ self.0.as_ref().unwrap().with_overlay(|overlay| {
+ overlay.as_ref().map(|nested| (f)(&mut nested.borrow_mut()))
+ })
}
fn with_overlay_mut_maybe<T>(
&mut self,
- f: impl FnOnce(&mut overlay::Element<'_, Message, Renderer>) -> T,
+ f: impl FnOnce(&mut Nested<'_, Message, Renderer>) -> T,
) -> Option<T> {
- self.0
- .as_mut()
- .unwrap()
- .with_overlay_mut(|overlay| overlay.as_mut().map(f))
+ self.0.as_mut().unwrap().with_overlay_mut(|overlay| {
+ overlay.as_mut().map(|nested| (f)(nested.get_mut()))
+ })
}
}
impl<'a, Message, Renderer> overlay::Overlay<Message, Renderer>
for Overlay<'a, Message, Renderer>
where
- Renderer: iced_native::Renderer,
+ Renderer: core::Renderer,
{
fn layout(
&self,
@@ -313,9 +330,7 @@ where
position: Point,
) -> layout::Node {
self.with_overlay_maybe(|overlay| {
- let translation = position - overlay.position();
-
- overlay.layout(renderer, bounds, translation)
+ overlay.layout(renderer, bounds, position)
})
.unwrap_or_default()
}
@@ -326,56 +341,49 @@ where
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
) {
let _ = self.with_overlay_maybe(|overlay| {
- overlay.draw(renderer, theme, style, layout, cursor_position);
+ overlay.draw(renderer, theme, style, layout, cursor);
});
}
fn mouse_interaction(
&self,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.with_overlay_maybe(|overlay| {
- overlay.mouse_interaction(
- layout,
- cursor_position,
- viewport,
- renderer,
- )
+ overlay.mouse_interaction(layout, cursor, viewport, renderer)
})
.unwrap_or_default()
}
fn on_event(
&mut self,
- event: iced_native::Event,
+ event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
self.with_overlay_mut_maybe(|overlay| {
- overlay.on_event(
- event,
- layout,
- cursor_position,
- renderer,
- clipboard,
- shell,
- )
+ overlay.on_event(event, layout, cursor, renderer, clipboard, shell)
})
- .unwrap_or(iced_native::event::Status::Ignored)
+ .unwrap_or(event::Status::Ignored)
}
- fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool {
+ fn is_over(
+ &self,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ cursor_position: Point,
+ ) -> bool {
self.with_overlay_maybe(|overlay| {
- overlay.is_over(layout, cursor_position)
+ overlay.is_over(layout, renderer, cursor_position)
})
.unwrap_or_default()
}
@@ -386,7 +394,7 @@ impl<'a, Message, Renderer, Dependency, View>
for Element<'a, Message, Renderer>
where
View: Into<Element<'static, Message, Renderer>> + 'static,
- Renderer: iced_native::Renderer + 'static,
+ Renderer: core::Renderer + 'static,
Message: 'static,
Dependency: Hash + 'a,
{
diff --git a/lazy/src/cache.rs b/widget/src/lazy/cache.rs
index 5b4a39f6..e7b87614 100644
--- a/lazy/src/cache.rs
+++ b/widget/src/lazy/cache.rs
@@ -1,5 +1,5 @@
-use iced_native::overlay;
-use iced_native::Element;
+use crate::core::overlay;
+use crate::core::Element;
use ouroboros::self_referencing;
diff --git a/lazy/src/component.rs b/widget/src/lazy/component.rs
index b23da9f7..c7814966 100644
--- a/lazy/src/component.rs
+++ b/widget/src/lazy/component.rs
@@ -1,18 +1,20 @@
//! Build and reuse custom widgets using The Elm Architecture.
-use iced_native::event;
-use iced_native::layout::{self, Layout};
-use iced_native::mouse;
-use iced_native::overlay;
-use iced_native::renderer;
-use iced_native::widget;
-use iced_native::widget::tree::{self, Tree};
-use iced_native::{
- Clipboard, Element, Length, Point, Rectangle, Shell, Size, Widget,
+use crate::core::event;
+use crate::core::layout::{self, Layout};
+use crate::core::mouse;
+use crate::core::overlay;
+use crate::core::renderer;
+use crate::core::widget;
+use crate::core::widget::tree::{self, Tree};
+use crate::core::{
+ self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Widget,
};
+use crate::runtime::overlay::Nested;
use ouroboros::self_referencing;
use std::cell::RefCell;
use std::marker::PhantomData;
+use std::rc::Rc;
/// A reusable, custom widget that uses The Elm Architecture.
///
@@ -58,6 +60,8 @@ pub trait Component<Message, Renderer> {
}
}
+struct Tag<T>(T);
+
/// Turns an implementor of [`Component`] into an [`Element`] that can be
/// embedded in any application.
pub fn view<'a, C, Message, Renderer>(
@@ -67,7 +71,7 @@ where
C: Component<Message, Renderer> + 'a,
C::State: 'static,
Message: 'a,
- Renderer: iced_native::Renderer + 'a,
+ Renderer: core::Renderer + 'a,
{
Element::new(Instance {
state: RefCell::new(Some(
@@ -79,11 +83,13 @@ where
}
.build(),
)),
+ tree: RefCell::new(Rc::new(RefCell::new(None))),
})
}
struct Instance<'a, Message, Renderer, Event, S> {
state: RefCell<Option<State<'a, Message, Renderer, Event, S>>>,
+ tree: RefCell<Rc<RefCell<Option<Tree>>>>,
}
#[self_referencing]
@@ -100,40 +106,91 @@ struct State<'a, Message: 'a, Renderer: 'a, Event: 'a, S: 'a> {
impl<'a, Message, Renderer, Event, S> Instance<'a, Message, Renderer, Event, S>
where
- S: Default,
+ S: Default + 'static,
+ Renderer: renderer::Renderer,
{
- fn rebuild_element(&self, state: &S) {
- let heads = self.state.borrow_mut().take().unwrap().into_heads();
+ fn diff_self(&self) {
+ self.with_element(|element| {
+ self.tree
+ .borrow_mut()
+ .borrow_mut()
+ .as_mut()
+ .unwrap()
+ .diff_children(std::slice::from_ref(&element));
+ });
+ }
- *self.state.borrow_mut() = Some(
- StateBuilder {
- component: heads.component,
- message: PhantomData,
- state: PhantomData,
- element_builder: |component| Some(component.view(state)),
- }
- .build(),
- );
+ fn rebuild_element_if_necessary(&self) {
+ let inner = self.state.borrow_mut().take().unwrap();
+ if inner.borrow_element().is_none() {
+ let heads = inner.into_heads();
+
+ *self.state.borrow_mut() = Some(
+ StateBuilder {
+ component: heads.component,
+ message: PhantomData,
+ state: PhantomData,
+ element_builder: |component| {
+ Some(
+ component.view(
+ self.tree
+ .borrow()
+ .borrow()
+ .as_ref()
+ .unwrap()
+ .state
+ .downcast_ref::<S>(),
+ ),
+ )
+ },
+ }
+ .build(),
+ );
+ self.diff_self();
+ } else {
+ *self.state.borrow_mut() = Some(inner);
+ }
}
fn rebuild_element_with_operation(
&self,
- state: &mut S,
operation: &mut dyn widget::Operation<Message>,
) {
let heads = self.state.borrow_mut().take().unwrap().into_heads();
- heads.component.operate(state, operation);
+ heads.component.operate(
+ self.tree
+ .borrow_mut()
+ .borrow_mut()
+ .as_mut()
+ .unwrap()
+ .state
+ .downcast_mut(),
+ operation,
+ );
*self.state.borrow_mut() = Some(
StateBuilder {
component: heads.component,
message: PhantomData,
state: PhantomData,
- element_builder: |component| Some(component.view(state)),
+ element_builder: |component| {
+ Some(
+ component.view(
+ self.tree
+ .borrow()
+ .borrow()
+ .as_ref()
+ .unwrap()
+ .state
+ .downcast_ref(),
+ ),
+ )
+ },
}
.build(),
);
+ self.diff_self();
}
fn with_element<T>(
@@ -147,6 +204,7 @@ where
&self,
f: impl FnOnce(&mut Element<'_, Event, Renderer>) -> T,
) -> T {
+ self.rebuild_element_if_necessary();
self.state
.borrow_mut()
.as_mut()
@@ -159,27 +217,30 @@ impl<'a, Message, Renderer, Event, S> Widget<Message, Renderer>
for Instance<'a, Message, Renderer, Event, S>
where
S: 'static + Default,
- Renderer: iced_native::Renderer,
+ Renderer: core::Renderer,
{
fn tag(&self) -> tree::Tag {
- struct Tag<T>(T);
tree::Tag::of::<Tag<S>>()
}
fn state(&self) -> tree::State {
- tree::State::new(S::default())
+ let state = Rc::new(RefCell::new(Some(Tree {
+ tag: tree::Tag::of::<Tag<S>>(),
+ state: tree::State::new(S::default()),
+ children: vec![Tree::empty()],
+ })));
+ *self.tree.borrow_mut() = state.clone();
+ tree::State::new(state)
}
fn children(&self) -> Vec<Tree> {
- self.rebuild_element(&S::default());
- self.with_element(|element| vec![Tree::new(element)])
+ vec![]
}
fn diff(&self, tree: &mut Tree) {
- self.rebuild_element(tree.state.downcast_ref());
- self.with_element(|element| {
- tree.diff_children(std::slice::from_ref(&element))
- })
+ let tree = tree.state.downcast_ref::<Rc<RefCell<Option<Tree>>>>();
+ *self.tree.borrow_mut() = tree.clone();
+ self.rebuild_element_if_necessary();
}
fn width(&self) -> Length {
@@ -203,9 +264,9 @@ where
fn on_event(
&mut self,
tree: &mut Tree,
- event: iced_native::Event,
+ event: core::Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
@@ -213,12 +274,13 @@ where
let mut local_messages = Vec::new();
let mut local_shell = Shell::new(&mut local_messages);
+ let t = tree.state.downcast_mut::<Rc<RefCell<Option<Tree>>>>();
let event_status = self.with_element_mut(|element| {
element.as_widget_mut().on_event(
- &mut tree.children[0],
+ &mut t.borrow_mut().as_mut().unwrap().children[0],
event,
layout,
- cursor_position,
+ cursor,
renderer,
clipboard,
&mut local_shell,
@@ -235,9 +297,10 @@ where
let mut heads = self.state.take().unwrap().into_heads();
for message in local_messages.into_iter().filter_map(|message| {
- heads
- .component
- .update(tree.state.downcast_mut::<S>(), message)
+ heads.component.update(
+ t.borrow_mut().as_mut().unwrap().state.downcast_mut(),
+ message,
+ )
}) {
shell.publish(message);
}
@@ -247,17 +310,11 @@ where
component: heads.component,
message: PhantomData,
state: PhantomData,
- element_builder: |state| {
- Some(state.view(tree.state.downcast_ref::<S>()))
- },
+ element_builder: |_| None,
}
.build(),
));
- self.with_element(|element| {
- tree.diff_children(std::slice::from_ref(&element))
- });
-
shell.invalidate_layout();
}
@@ -271,10 +328,7 @@ where
renderer: &Renderer,
operation: &mut dyn widget::Operation<Message>,
) {
- self.rebuild_element_with_operation(
- tree.state.downcast_mut(),
- operation,
- );
+ self.rebuild_element_with_operation(operation);
struct MapOperation<'a, B> {
operation: &'a mut dyn widget::Operation<B>,
@@ -308,13 +362,28 @@ where
) {
self.operation.text_input(state, id);
}
+
+ fn scrollable(
+ &mut self,
+ state: &mut dyn widget::operation::Scrollable,
+ id: Option<&widget::Id>,
+ ) {
+ self.operation.scrollable(state, id);
+ }
+
+ fn custom(
+ &mut self,
+ state: &mut dyn std::any::Any,
+ id: Option<&widget::Id>,
+ ) {
+ self.operation.custom(state, id);
+ }
}
+ let tree = tree.state.downcast_mut::<Rc<RefCell<Option<Tree>>>>();
self.with_element(|element| {
- tree.diff_children(std::slice::from_ref(&element));
-
element.as_widget().operate(
- &mut tree.children[0],
+ &mut tree.borrow_mut().as_mut().unwrap().children[0],
layout,
renderer,
&mut MapOperation { operation },
@@ -329,17 +398,18 @@ where
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
) {
+ let tree = tree.state.downcast_ref::<Rc<RefCell<Option<Tree>>>>();
self.with_element(|element| {
element.as_widget().draw(
- &tree.children[0],
+ &tree.borrow().as_ref().unwrap().children[0],
renderer,
theme,
style,
layout,
- cursor_position,
+ cursor,
viewport,
);
});
@@ -349,15 +419,16 @@ where
&self,
tree: &Tree,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
+ let tree = tree.state.downcast_ref::<Rc<RefCell<Option<Tree>>>>();
self.with_element(|element| {
element.as_widget().mouse_interaction(
- &tree.children[0],
+ &tree.borrow().as_ref().unwrap().children[0],
layout,
- cursor_position,
+ cursor,
viewport,
renderer,
)
@@ -370,26 +441,42 @@ where
layout: Layout<'_>,
renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> {
- let overlay = OverlayBuilder {
- instance: self,
- tree,
- types: PhantomData,
- overlay_builder: |instance, tree| {
- instance.state.get_mut().as_mut().unwrap().with_element_mut(
- move |element| {
- element.as_mut().unwrap().as_widget_mut().overlay(
- &mut tree.children[0],
- layout,
- renderer,
- )
- },
- )
- },
- }
- .build();
+ self.rebuild_element_if_necessary();
+ let tree = tree
+ .state
+ .downcast_mut::<Rc<RefCell<Option<Tree>>>>()
+ .borrow_mut()
+ .take()
+ .unwrap();
+ let overlay = Overlay(Some(
+ InnerBuilder {
+ instance: self,
+ tree,
+ types: PhantomData,
+ overlay_builder: |instance, tree| {
+ instance.state.get_mut().as_mut().unwrap().with_element_mut(
+ move |element| {
+ element
+ .as_mut()
+ .unwrap()
+ .as_widget_mut()
+ .overlay(
+ &mut tree.children[0],
+ layout,
+ renderer,
+ )
+ .map(|overlay| {
+ RefCell::new(Nested::new(overlay))
+ })
+ },
+ )
+ },
+ }
+ .build(),
+ ));
- let has_overlay = overlay.with_overlay(|overlay| {
- overlay.as_ref().map(overlay::Element::position)
+ let has_overlay = overlay.0.as_ref().unwrap().with_overlay(|overlay| {
+ overlay.as_ref().map(|nested| nested.borrow().position())
});
has_overlay.map(|position| {
@@ -403,15 +490,29 @@ where
}
}
+struct Overlay<'a, 'b, Message, Renderer, Event, S>(
+ Option<Inner<'a, 'b, Message, Renderer, Event, S>>,
+);
+
+impl<'a, 'b, Message, Renderer, Event, S> Drop
+ for Overlay<'a, 'b, Message, Renderer, Event, S>
+{
+ fn drop(&mut self) {
+ if let Some(heads) = self.0.take().map(|inner| inner.into_heads()) {
+ *heads.instance.tree.borrow_mut().borrow_mut() = Some(heads.tree);
+ }
+ }
+}
+
#[self_referencing]
-struct Overlay<'a, 'b, Message, Renderer, Event, S> {
+struct Inner<'a, 'b, Message, Renderer, Event, S> {
instance: &'a mut Instance<'b, Message, Renderer, Event, S>,
- tree: &'a mut Tree,
+ tree: Tree,
types: PhantomData<(Message, Event, S)>,
#[borrows(mut instance, mut tree)]
- #[covariant]
- overlay: Option<overlay::Element<'this, Event, Renderer>>,
+ #[not_covariant]
+ overlay: Option<RefCell<Nested<'this, Event, Renderer>>>,
}
struct OverlayInstance<'a, 'b, Message, Renderer, Event, S> {
@@ -423,31 +524,39 @@ impl<'a, 'b, Message, Renderer, Event, S>
{
fn with_overlay_maybe<T>(
&self,
- f: impl FnOnce(&overlay::Element<'_, Event, Renderer>) -> T,
+ f: impl FnOnce(&mut Nested<'_, Event, Renderer>) -> T,
) -> Option<T> {
self.overlay
.as_ref()
.unwrap()
- .borrow_overlay()
+ .0
.as_ref()
- .map(f)
+ .unwrap()
+ .with_overlay(|overlay| {
+ overlay.as_ref().map(|nested| (f)(&mut nested.borrow_mut()))
+ })
}
fn with_overlay_mut_maybe<T>(
&mut self,
- f: impl FnOnce(&mut overlay::Element<'_, Event, Renderer>) -> T,
+ f: impl FnOnce(&mut Nested<'_, Event, Renderer>) -> T,
) -> Option<T> {
self.overlay
.as_mut()
.unwrap()
- .with_overlay_mut(|overlay| overlay.as_mut().map(f))
+ .0
+ .as_mut()
+ .unwrap()
+ .with_overlay_mut(|overlay| {
+ overlay.as_mut().map(|nested| (f)(nested.get_mut()))
+ })
}
}
impl<'a, 'b, Message, Renderer, Event, S> overlay::Overlay<Message, Renderer>
for OverlayInstance<'a, 'b, Message, Renderer, Event, S>
where
- Renderer: iced_native::Renderer,
+ Renderer: core::Renderer,
S: 'static + Default,
{
fn layout(
@@ -457,9 +566,7 @@ where
position: Point,
) -> layout::Node {
self.with_overlay_maybe(|overlay| {
- let translation = position - overlay.position();
-
- overlay.layout(renderer, bounds, translation)
+ overlay.layout(renderer, bounds, position)
})
.unwrap_or_default()
}
@@ -470,40 +577,35 @@ where
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
) {
let _ = self.with_overlay_maybe(|overlay| {
- overlay.draw(renderer, theme, style, layout, cursor_position);
+ overlay.draw(renderer, theme, style, layout, cursor);
});
}
fn mouse_interaction(
&self,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.with_overlay_maybe(|overlay| {
- overlay.mouse_interaction(
- layout,
- cursor_position,
- viewport,
- renderer,
- )
+ overlay.mouse_interaction(layout, cursor, viewport, renderer)
})
.unwrap_or_default()
}
fn on_event(
&mut self,
- event: iced_native::Event,
+ event: core::Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
- ) -> iced_native::event::Status {
+ ) -> event::Status {
let mut local_messages = Vec::new();
let mut local_shell = Shell::new(&mut local_messages);
@@ -512,53 +614,52 @@ where
overlay.on_event(
event,
layout,
- cursor_position,
+ cursor,
renderer,
clipboard,
&mut local_shell,
)
})
- .unwrap_or(iced_native::event::Status::Ignored);
+ .unwrap_or(event::Status::Ignored);
local_shell.revalidate_layout(|| shell.invalidate_layout());
+ if let Some(redraw_request) = local_shell.redraw_request() {
+ shell.request_redraw(redraw_request);
+ }
+
if !local_messages.is_empty() {
- let overlay = self.overlay.take().unwrap().into_heads();
- let mut heads = overlay.instance.state.take().unwrap().into_heads();
+ let mut inner =
+ self.overlay.take().unwrap().0.take().unwrap().into_heads();
+ let mut heads = inner.instance.state.take().unwrap().into_heads();
for message in local_messages.into_iter().filter_map(|message| {
heads
.component
- .update(overlay.tree.state.downcast_mut::<S>(), message)
+ .update(inner.tree.state.downcast_mut(), message)
}) {
shell.publish(message);
}
- *overlay.instance.state.borrow_mut() = Some(
+ *inner.instance.state.borrow_mut() = Some(
StateBuilder {
component: heads.component,
message: PhantomData,
state: PhantomData,
- element_builder: |state| {
- Some(state.view(overlay.tree.state.downcast_ref::<S>()))
- },
+ element_builder: |_| None,
}
.build(),
);
- overlay.instance.with_element(|element| {
- overlay.tree.diff_children(std::slice::from_ref(&element))
- });
-
- self.overlay = Some(
- OverlayBuilder {
- instance: overlay.instance,
- tree: overlay.tree,
+ self.overlay = Some(Overlay(Some(
+ InnerBuilder {
+ instance: inner.instance,
+ tree: inner.tree,
types: PhantomData,
overlay_builder: |_, _| None,
}
.build(),
- );
+ )));
shell.invalidate_layout();
}
@@ -566,9 +667,14 @@ where
event_status
}
- fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool {
+ fn is_over(
+ &self,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ cursor_position: Point,
+ ) -> bool {
self.with_overlay_maybe(|overlay| {
- overlay.is_over(layout, cursor_position)
+ overlay.is_over(layout, renderer, cursor_position)
})
.unwrap_or_default()
}
diff --git a/lazy/src/lib.rs b/widget/src/lazy/helpers.rs
index 41a28773..8ca9cb86 100644
--- a/lazy/src/lib.rs
+++ b/widget/src/lazy/helpers.rs
@@ -1,36 +1,11 @@
-#![doc(
- html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"
-)]
-#![deny(
- missing_debug_implementations,
- unused_results,
- clippy::extra_unused_lifetimes,
- clippy::from_over_into,
- clippy::needless_borrow,
- clippy::new_without_default,
- clippy::useless_conversion
-)]
-#![forbid(unsafe_code)]
-#![allow(
- clippy::await_holding_refcell_ref,
- clippy::inherent_to_string,
- clippy::type_complexity
-)]
-#![cfg_attr(docsrs, feature(doc_cfg))]
-mod lazy;
+use crate::core::{self, Element, Size};
+use crate::lazy::component::{self, Component};
+use crate::lazy::{Lazy, Responsive};
-pub mod component;
-pub mod responsive;
-
-pub use component::Component;
-pub use lazy::Lazy;
-pub use responsive::Responsive;
-
-mod cache;
-
-use iced_native::{Element, Size};
use std::hash::Hash;
+/// Creates a new [`Lazy`] widget with the given data `Dependency` and a
+/// closure that can turn this data into a widget tree.
pub fn lazy<'a, Message, Renderer, Dependency, View>(
dependency: Dependency,
view: impl Fn(&Dependency) -> View + 'a,
@@ -51,16 +26,22 @@ where
C: Component<Message, Renderer> + 'a,
C::State: 'static,
Message: 'a,
- Renderer: iced_native::Renderer + 'a,
+ Renderer: core::Renderer + 'a,
{
component::view(component)
}
+/// Creates a new [`Responsive`] widget with a closure that produces its
+/// contents.
+///
+/// The `view` closure will be provided with the current [`Size`] of
+/// the [`Responsive`] widget and, therefore, can be used to build the
+/// contents of the widget in a responsive way.
pub fn responsive<'a, Message, Renderer>(
f: impl Fn(Size) -> Element<'a, Message, Renderer> + 'a,
) -> Responsive<'a, Message, Renderer>
where
- Renderer: iced_native::Renderer,
+ Renderer: core::Renderer,
{
Responsive::new(f)
}
diff --git a/lazy/src/responsive.rs b/widget/src/lazy/responsive.rs
index 57c07de1..07300857 100644
--- a/lazy/src/responsive.rs
+++ b/widget/src/lazy/responsive.rs
@@ -1,13 +1,15 @@
-use iced_native::event;
-use iced_native::layout::{self, Layout};
-use iced_native::mouse;
-use iced_native::overlay;
-use iced_native::renderer;
-use iced_native::widget::tree::{self, Tree};
-use iced_native::widget::{self, horizontal_space};
-use iced_native::{
- Clipboard, Element, Length, Point, Rectangle, Shell, Size, Widget,
+use crate::core::event::{self, Event};
+use crate::core::layout::{self, Layout};
+use crate::core::mouse;
+use crate::core::overlay;
+use crate::core::renderer;
+use crate::core::widget;
+use crate::core::widget::tree::{self, Tree};
+use crate::core::{
+ self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Widget,
};
+use crate::horizontal_space;
+use crate::runtime::overlay::Nested;
use ouroboros::self_referencing;
use std::cell::{RefCell, RefMut};
@@ -19,14 +21,14 @@ use std::ops::Deref;
/// A [`Responsive`] widget will always try to fill all the available space of
/// its parent.
#[allow(missing_debug_implementations)]
-pub struct Responsive<'a, Message, Renderer> {
+pub struct Responsive<'a, Message, Renderer = crate::Renderer> {
view: Box<dyn Fn(Size) -> Element<'a, Message, Renderer> + 'a>,
content: RefCell<Content<'a, Message, Renderer>>,
}
impl<'a, Message, Renderer> Responsive<'a, Message, Renderer>
where
- Renderer: iced_native::Renderer,
+ Renderer: core::Renderer,
{
/// Creates a new [`Responsive`] widget with a closure that produces its
/// contents.
@@ -41,7 +43,7 @@ where
view: Box::new(view),
content: RefCell::new(Content {
size: Size::ZERO,
- layout: layout::Node::new(Size::ZERO),
+ layout: None,
element: Element::new(horizontal_space(0)),
}),
}
@@ -50,18 +52,27 @@ where
struct Content<'a, Message, Renderer> {
size: Size,
- layout: layout::Node,
+ layout: Option<layout::Node>,
element: Element<'a, Message, Renderer>,
}
impl<'a, Message, Renderer> Content<'a, Message, Renderer>
where
- Renderer: iced_native::Renderer,
+ Renderer: core::Renderer,
{
+ fn layout(&mut self, renderer: &Renderer) {
+ if self.layout.is_none() {
+ self.layout =
+ Some(self.element.as_widget().layout(
+ renderer,
+ &layout::Limits::new(Size::ZERO, self.size),
+ ));
+ }
+ }
+
fn update(
&mut self,
tree: &mut Tree,
- renderer: &Renderer,
new_size: Size,
view: &dyn Fn(Size) -> Element<'a, Message, Renderer>,
) {
@@ -71,13 +82,9 @@ where
self.element = view(new_size);
self.size = new_size;
+ self.layout = None;
tree.diff(&self.element);
-
- self.layout = self
- .element
- .as_widget()
- .layout(renderer, &layout::Limits::new(Size::ZERO, self.size));
}
fn resolve<R, T>(
@@ -96,11 +103,12 @@ where
where
R: Deref<Target = Renderer>,
{
- self.update(tree, renderer.deref(), layout.bounds().size(), view);
+ self.update(tree, layout.bounds().size(), view);
+ self.layout(renderer.deref());
let content_layout = Layout::with_offset(
layout.position() - Point::ORIGIN,
- &self.layout,
+ self.layout.as_ref().unwrap(),
);
f(tree, renderer, content_layout, &mut self.element)
@@ -114,7 +122,7 @@ struct State {
impl<'a, Message, Renderer> Widget<Message, Renderer>
for Responsive<'a, Message, Renderer>
where
- Renderer: iced_native::Renderer,
+ Renderer: core::Renderer,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
@@ -168,9 +176,9 @@ where
fn on_event(
&mut self,
tree: &mut Tree,
- event: iced_native::Event,
+ event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
@@ -178,7 +186,10 @@ where
let state = tree.state.downcast_mut::<State>();
let mut content = self.content.borrow_mut();
- content.resolve(
+ let mut local_messages = vec![];
+ let mut local_shell = Shell::new(&mut local_messages);
+
+ let status = content.resolve(
&mut state.tree.borrow_mut(),
renderer,
layout,
@@ -188,13 +199,21 @@ where
tree,
event,
layout,
- cursor_position,
+ cursor,
renderer,
clipboard,
- shell,
+ &mut local_shell,
)
},
- )
+ );
+
+ if local_shell.is_layout_invalid() {
+ content.layout = None;
+ }
+
+ shell.merge(local_shell, std::convert::identity);
+
+ status
}
fn draw(
@@ -204,7 +223,7 @@ where
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
) {
let state = tree.state.downcast_ref::<State>();
@@ -217,13 +236,7 @@ where
&self.view,
|tree, renderer, layout, element| {
element.as_widget().draw(
- tree,
- renderer,
- theme,
- style,
- layout,
- cursor_position,
- viewport,
+ tree, renderer, theme, style, layout, cursor, viewport,
)
},
)
@@ -233,7 +246,7 @@ where
&self,
tree: &Tree,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
@@ -246,13 +259,9 @@ where
layout,
&self.view,
|tree, renderer, layout, element| {
- element.as_widget().mouse_interaction(
- tree,
- layout,
- cursor_position,
- viewport,
- renderer,
- )
+ element
+ .as_widget()
+ .mouse_interaction(tree, layout, cursor, viewport, renderer)
},
)
}
@@ -271,35 +280,32 @@ where
content: self.content.borrow_mut(),
tree: state.tree.borrow_mut(),
types: PhantomData,
- overlay_builder: |content: &mut RefMut<Content<_, _>>, tree| {
- content.update(
- tree,
- renderer,
- layout.bounds().size(),
- &self.view,
- );
+ overlay_builder: |content: &mut RefMut<'_, Content<'_, _, _>>,
+ tree| {
+ content.update(tree, layout.bounds().size(), &self.view);
+ content.layout(renderer);
let Content {
element,
- layout: content_layout,
+ layout: content_layout_node,
..
} = content.deref_mut();
let content_layout = Layout::with_offset(
layout.bounds().position() - Point::ORIGIN,
- content_layout,
+ content_layout_node.as_ref().unwrap(),
);
element
.as_widget_mut()
.overlay(tree, content_layout, renderer)
+ .map(|overlay| RefCell::new(Nested::new(overlay)))
},
}
.build();
- let has_overlay = overlay.with_overlay(|overlay| {
- overlay.as_ref().map(overlay::Element::position)
- });
+ let has_overlay =
+ overlay.with_overlay_maybe(|overlay| overlay.position());
has_overlay
.map(|position| overlay::Element::new(position, Box::new(overlay)))
@@ -309,7 +315,7 @@ where
impl<'a, Message, Renderer> From<Responsive<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
- Renderer: iced_native::Renderer + 'a,
+ Renderer: core::Renderer + 'a,
Message: 'a,
{
fn from(responsive: Responsive<'a, Message, Renderer>) -> Self {
@@ -324,30 +330,34 @@ struct Overlay<'a, 'b, Message, Renderer> {
types: PhantomData<Message>,
#[borrows(mut content, mut tree)]
- #[covariant]
- overlay: Option<overlay::Element<'this, Message, Renderer>>,
+ #[not_covariant]
+ overlay: Option<RefCell<Nested<'this, Message, Renderer>>>,
}
impl<'a, 'b, Message, Renderer> Overlay<'a, 'b, Message, Renderer> {
fn with_overlay_maybe<T>(
&self,
- f: impl FnOnce(&overlay::Element<'_, Message, Renderer>) -> T,
+ f: impl FnOnce(&mut Nested<'_, Message, Renderer>) -> T,
) -> Option<T> {
- self.borrow_overlay().as_ref().map(f)
+ self.with_overlay(|overlay| {
+ overlay.as_ref().map(|nested| (f)(&mut nested.borrow_mut()))
+ })
}
fn with_overlay_mut_maybe<T>(
&mut self,
- f: impl FnOnce(&mut overlay::Element<'_, Message, Renderer>) -> T,
+ f: impl FnOnce(&mut Nested<'_, Message, Renderer>) -> T,
) -> Option<T> {
- self.with_overlay_mut(|overlay| overlay.as_mut().map(f))
+ self.with_overlay_mut(|overlay| {
+ overlay.as_mut().map(|nested| (f)(nested.get_mut()))
+ })
}
}
impl<'a, 'b, Message, Renderer> overlay::Overlay<Message, Renderer>
for Overlay<'a, 'b, Message, Renderer>
where
- Renderer: iced_native::Renderer,
+ Renderer: core::Renderer,
{
fn layout(
&self,
@@ -356,9 +366,7 @@ where
position: Point,
) -> layout::Node {
self.with_overlay_maybe(|overlay| {
- let translation = position - overlay.position();
-
- overlay.layout(renderer, bounds, translation)
+ overlay.layout(renderer, bounds, position)
})
.unwrap_or_default()
}
@@ -369,56 +377,49 @@ where
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
) {
let _ = self.with_overlay_maybe(|overlay| {
- overlay.draw(renderer, theme, style, layout, cursor_position);
+ overlay.draw(renderer, theme, style, layout, cursor);
});
}
fn mouse_interaction(
&self,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.with_overlay_maybe(|overlay| {
- overlay.mouse_interaction(
- layout,
- cursor_position,
- viewport,
- renderer,
- )
+ overlay.mouse_interaction(layout, cursor, viewport, renderer)
})
.unwrap_or_default()
}
fn on_event(
&mut self,
- event: iced_native::Event,
+ event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
self.with_overlay_mut_maybe(|overlay| {
- overlay.on_event(
- event,
- layout,
- cursor_position,
- renderer,
- clipboard,
- shell,
- )
+ overlay.on_event(event, layout, cursor, renderer, clipboard, shell)
})
- .unwrap_or(iced_native::event::Status::Ignored)
+ .unwrap_or(event::Status::Ignored)
}
- fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool {
+ fn is_over(
+ &self,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ cursor_position: Point,
+ ) -> bool {
self.with_overlay_maybe(|overlay| {
- overlay.is_over(layout, cursor_position)
+ overlay.is_over(layout, renderer, cursor_position)
})
.unwrap_or_default()
}
diff --git a/widget/src/lib.rs b/widget/src/lib.rs
new file mode 100644
index 00000000..9da13f9b
--- /dev/null
+++ b/widget/src/lib.rs
@@ -0,0 +1,126 @@
+//! Use the built-in widgets or create your own.
+#![doc(
+ html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"
+)]
+#![deny(
+ missing_debug_implementations,
+ missing_docs,
+ unused_results,
+ clippy::extra_unused_lifetimes,
+ clippy::from_over_into,
+ clippy::needless_borrow,
+ clippy::new_without_default,
+ clippy::useless_conversion
+)]
+#![forbid(unsafe_code, rust_2018_idioms)]
+#![allow(clippy::inherent_to_string, clippy::type_complexity)]
+#![cfg_attr(docsrs, feature(doc_auto_cfg))]
+pub use iced_renderer as renderer;
+pub use iced_renderer::graphics;
+pub use iced_runtime as runtime;
+pub use iced_runtime::core;
+pub use iced_style as style;
+
+mod column;
+mod mouse_area;
+mod row;
+
+pub mod button;
+pub mod checkbox;
+pub mod container;
+pub mod overlay;
+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 space;
+pub mod text;
+pub mod text_input;
+pub mod toggler;
+pub mod tooltip;
+pub mod vertical_slider;
+
+mod helpers;
+
+pub use helpers::*;
+
+#[cfg(feature = "lazy")]
+mod lazy;
+
+#[cfg(feature = "lazy")]
+pub use crate::lazy::{Component, Lazy, Responsive};
+
+#[cfg(feature = "lazy")]
+pub use crate::lazy::helpers::*;
+
+#[doc(no_inline)]
+pub use button::Button;
+#[doc(no_inline)]
+pub use checkbox::Checkbox;
+#[doc(no_inline)]
+pub use column::Column;
+#[doc(no_inline)]
+pub use container::Container;
+#[doc(no_inline)]
+pub use mouse_area::MouseArea;
+#[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;
+#[doc(no_inline)]
+pub use space::Space;
+#[doc(no_inline)]
+pub use text::Text;
+#[doc(no_inline)]
+pub use text_input::TextInput;
+#[doc(no_inline)]
+pub use toggler::Toggler;
+#[doc(no_inline)]
+pub use tooltip::Tooltip;
+#[doc(no_inline)]
+pub use vertical_slider::VerticalSlider;
+
+#[cfg(feature = "svg")]
+pub mod svg;
+
+#[cfg(feature = "svg")]
+#[doc(no_inline)]
+pub use svg::Svg;
+
+#[cfg(feature = "image")]
+pub mod image;
+
+#[cfg(feature = "image")]
+#[doc(no_inline)]
+pub use image::Image;
+
+#[cfg(feature = "canvas")]
+pub mod canvas;
+
+#[cfg(feature = "canvas")]
+#[doc(no_inline)]
+pub use canvas::Canvas;
+
+#[cfg(feature = "qr_code")]
+pub mod qr_code;
+
+#[cfg(feature = "qr_code")]
+#[doc(no_inline)]
+pub use qr_code::QRCode;
+
+type Renderer<Theme = style::Theme> = renderer::Renderer<Theme>;
diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs
new file mode 100644
index 00000000..da7dc88f
--- /dev/null
+++ b/widget/src/mouse_area.rs
@@ -0,0 +1,311 @@
+//! A container for capturing mouse events.
+
+use crate::core::event::{self, Event};
+use crate::core::layout;
+use crate::core::mouse;
+use crate::core::overlay;
+use crate::core::renderer;
+use crate::core::touch;
+use crate::core::widget::{tree, Operation, Tree};
+use crate::core::{
+ Clipboard, Element, Layout, Length, Rectangle, Shell, Widget,
+};
+
+/// Emit messages on mouse events.
+#[allow(missing_debug_implementations)]
+pub struct MouseArea<'a, Message, Renderer> {
+ content: Element<'a, Message, Renderer>,
+ on_press: Option<Message>,
+ on_release: Option<Message>,
+ on_right_press: Option<Message>,
+ on_right_release: Option<Message>,
+ on_middle_press: Option<Message>,
+ on_middle_release: Option<Message>,
+}
+
+impl<'a, Message, Renderer> MouseArea<'a, Message, Renderer> {
+ /// The message to emit on a left button press.
+ #[must_use]
+ pub fn on_press(mut self, message: Message) -> Self {
+ self.on_press = Some(message);
+ self
+ }
+
+ /// The message to emit on a left button release.
+ #[must_use]
+ pub fn on_release(mut self, message: Message) -> Self {
+ self.on_release = Some(message);
+ self
+ }
+
+ /// The message to emit on a right button press.
+ #[must_use]
+ pub fn on_right_press(mut self, message: Message) -> Self {
+ self.on_right_press = Some(message);
+ self
+ }
+
+ /// The message to emit on a right button release.
+ #[must_use]
+ pub fn on_right_release(mut self, message: Message) -> Self {
+ self.on_right_release = Some(message);
+ self
+ }
+
+ /// The message to emit on a middle button press.
+ #[must_use]
+ pub fn on_middle_press(mut self, message: Message) -> Self {
+ self.on_middle_press = Some(message);
+ self
+ }
+
+ /// The message to emit on a middle button release.
+ #[must_use]
+ pub fn on_middle_release(mut self, message: Message) -> Self {
+ self.on_middle_release = Some(message);
+ self
+ }
+}
+
+/// Local state of the [`MouseArea`].
+#[derive(Default)]
+struct State {
+ // TODO: Support on_mouse_enter and on_mouse_exit
+}
+
+impl<'a, Message, Renderer> MouseArea<'a, Message, Renderer> {
+ /// Creates a [`MouseArea`] with the given content.
+ pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self {
+ MouseArea {
+ content: content.into(),
+ on_press: None,
+ on_release: None,
+ on_right_press: None,
+ on_right_release: None,
+ on_middle_press: None,
+ on_middle_release: None,
+ }
+ }
+}
+
+impl<'a, Message, Renderer> Widget<Message, Renderer>
+ for MouseArea<'a, Message, Renderer>
+where
+ Renderer: renderer::Renderer,
+ Message: Clone,
+{
+ fn tag(&self) -> tree::Tag {
+ tree::Tag::of::<State>()
+ }
+
+ fn state(&self) -> tree::State {
+ tree::State::new(State::default())
+ }
+
+ fn children(&self) -> Vec<Tree> {
+ vec![Tree::new(&self.content)]
+ }
+
+ fn diff(&self, tree: &mut Tree) {
+ tree.diff_children(std::slice::from_ref(&self.content));
+ }
+
+ fn width(&self) -> Length {
+ self.content.as_widget().width()
+ }
+
+ fn height(&self) -> Length {
+ self.content.as_widget().height()
+ }
+
+ fn layout(
+ &self,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ self.content.as_widget().layout(renderer, limits)
+ }
+
+ fn operate(
+ &self,
+ tree: &mut Tree,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ operation: &mut dyn Operation<Message>,
+ ) {
+ self.content.as_widget().operate(
+ &mut tree.children[0],
+ layout,
+ renderer,
+ operation,
+ );
+ }
+
+ fn on_event(
+ &mut self,
+ tree: &mut Tree,
+ event: Event,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ renderer: &Renderer,
+ clipboard: &mut dyn Clipboard,
+ shell: &mut Shell<'_, Message>,
+ ) -> event::Status {
+ if let event::Status::Captured = self.content.as_widget_mut().on_event(
+ &mut tree.children[0],
+ event.clone(),
+ layout,
+ cursor,
+ renderer,
+ clipboard,
+ shell,
+ ) {
+ return event::Status::Captured;
+ }
+
+ update(self, &event, layout, cursor, shell)
+ }
+
+ fn mouse_interaction(
+ &self,
+ tree: &Tree,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ viewport: &Rectangle,
+ renderer: &Renderer,
+ ) -> mouse::Interaction {
+ self.content.as_widget().mouse_interaction(
+ &tree.children[0],
+ layout,
+ cursor,
+ viewport,
+ renderer,
+ )
+ }
+
+ fn draw(
+ &self,
+ tree: &Tree,
+ renderer: &mut Renderer,
+ theme: &Renderer::Theme,
+ renderer_style: &renderer::Style,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ viewport: &Rectangle,
+ ) {
+ self.content.as_widget().draw(
+ &tree.children[0],
+ renderer,
+ theme,
+ renderer_style,
+ layout,
+ cursor,
+ viewport,
+ );
+ }
+
+ fn overlay<'b>(
+ &'b mut self,
+ tree: &'b mut Tree,
+ layout: Layout<'_>,
+ renderer: &Renderer,
+ ) -> Option<overlay::Element<'b, Message, Renderer>> {
+ self.content.as_widget_mut().overlay(
+ &mut tree.children[0],
+ layout,
+ renderer,
+ )
+ }
+}
+
+impl<'a, Message, Renderer> From<MouseArea<'a, Message, Renderer>>
+ for Element<'a, Message, Renderer>
+where
+ Message: 'a + Clone,
+ Renderer: 'a + renderer::Renderer,
+{
+ fn from(
+ area: MouseArea<'a, Message, Renderer>,
+ ) -> Element<'a, Message, Renderer> {
+ Element::new(area)
+ }
+}
+
+/// Processes the given [`Event`] and updates the [`State`] of an [`MouseArea`]
+/// accordingly.
+fn update<Message: Clone, Renderer>(
+ widget: &mut MouseArea<'_, Message, Renderer>,
+ event: &Event,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ shell: &mut Shell<'_, Message>,
+) -> event::Status {
+ if !cursor.is_over(layout.bounds()) {
+ return event::Status::Ignored;
+ }
+
+ if let Some(message) = widget.on_press.as_ref() {
+ if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
+ | Event::Touch(touch::Event::FingerPressed { .. }) = event
+ {
+ shell.publish(message.clone());
+
+ return event::Status::Captured;
+ }
+ }
+
+ if let Some(message) = widget.on_release.as_ref() {
+ if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
+ | Event::Touch(touch::Event::FingerLifted { .. }) = event
+ {
+ shell.publish(message.clone());
+
+ return event::Status::Captured;
+ }
+ }
+
+ if let Some(message) = widget.on_right_press.as_ref() {
+ if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) =
+ event
+ {
+ shell.publish(message.clone());
+
+ return event::Status::Captured;
+ }
+ }
+
+ if let Some(message) = widget.on_right_release.as_ref() {
+ if let Event::Mouse(mouse::Event::ButtonReleased(
+ mouse::Button::Right,
+ )) = event
+ {
+ shell.publish(message.clone());
+
+ return event::Status::Captured;
+ }
+ }
+
+ if let Some(message) = widget.on_middle_press.as_ref() {
+ if let Event::Mouse(mouse::Event::ButtonPressed(
+ mouse::Button::Middle,
+ )) = event
+ {
+ shell.publish(message.clone());
+
+ return event::Status::Captured;
+ }
+ }
+
+ if let Some(message) = widget.on_middle_release.as_ref() {
+ if let Event::Mouse(mouse::Event::ButtonReleased(
+ mouse::Button::Middle,
+ )) = event
+ {
+ shell.publish(message.clone());
+
+ return event::Status::Captured;
+ }
+ }
+
+ event::Status::Ignored
+}
diff --git a/graphics/src/overlay.rs b/widget/src/overlay.rs
index bc0ed744..bc0ed744 100644
--- a/graphics/src/overlay.rs
+++ b/widget/src/overlay.rs
diff --git a/native/src/overlay/menu.rs b/widget/src/overlay/menu.rs
index 50f741ef..ccf4dfb5 100644
--- a/native/src/overlay/menu.rs
+++ b/widget/src/overlay/menu.rs
@@ -1,25 +1,25 @@
//! Build and show dropdown menus.
-use crate::alignment;
-use crate::event::{self, Event};
-use crate::layout;
-use crate::mouse;
-use crate::overlay;
-use crate::renderer;
-use crate::text::{self, Text};
-use crate::touch;
-use crate::widget::container::{self, Container};
-use crate::widget::scrollable::{self, Scrollable};
-use crate::widget::Tree;
-use crate::{
- Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point,
- Rectangle, Shell, Size, Vector, Widget,
+use crate::container::{self, Container};
+use crate::core::alignment;
+use crate::core::event::{self, Event};
+use crate::core::layout::{self, Layout};
+use crate::core::mouse;
+use crate::core::overlay;
+use crate::core::renderer;
+use crate::core::text::{self, Text};
+use crate::core::touch;
+use crate::core::widget::Tree;
+use crate::core::{
+ Clipboard, Color, Length, Padding, Pixels, Point, Rectangle, Size, Vector,
};
+use crate::core::{Element, Shell, Widget};
+use crate::scrollable::{self, Scrollable};
pub use iced_style::menu::{Appearance, StyleSheet};
/// A list of selectable options.
#[allow(missing_debug_implementations)]
-pub struct Menu<'a, T, Renderer>
+pub struct Menu<'a, T, Message, Renderer = crate::Renderer>
where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
@@ -27,17 +27,20 @@ where
state: &'a mut State,
options: &'a [T],
hovered_option: &'a mut Option<usize>,
- last_selection: &'a mut Option<T>,
+ on_selected: Box<dyn FnMut(T) -> Message + 'a>,
width: f32,
padding: Padding,
text_size: Option<f32>,
- font: Renderer::Font,
+ text_line_height: text::LineHeight,
+ text_shaping: text::Shaping,
+ font: Option<Renderer::Font>,
style: <Renderer::Theme as StyleSheet>::Style,
}
-impl<'a, T, Renderer> Menu<'a, T, Renderer>
+impl<'a, T, Message, Renderer> Menu<'a, T, Message, Renderer>
where
T: ToString + Clone,
+ Message: 'a,
Renderer: text::Renderer + 'a,
Renderer::Theme:
StyleSheet + container::StyleSheet + scrollable::StyleSheet,
@@ -48,17 +51,19 @@ where
state: &'a mut State,
options: &'a [T],
hovered_option: &'a mut Option<usize>,
- last_selection: &'a mut Option<T>,
+ on_selected: impl FnMut(T) -> Message + 'a,
) -> Self {
Menu {
state,
options,
hovered_option,
- last_selection,
+ on_selected: Box::new(on_selected),
width: 0.0,
padding: Padding::ZERO,
text_size: None,
- font: Default::default(),
+ text_line_height: text::LineHeight::default(),
+ text_shaping: text::Shaping::Basic,
+ font: None,
style: Default::default(),
}
}
@@ -81,9 +86,24 @@ where
self
}
+ /// Sets the text [`LineHeight`] of the [`Menu`].
+ pub fn text_line_height(
+ mut self,
+ line_height: impl Into<text::LineHeight>,
+ ) -> Self {
+ self.text_line_height = line_height.into();
+ self
+ }
+
+ /// Sets the [`text::Shaping`] strategy of the [`Menu`].
+ pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
+ self.text_shaping = shaping;
+ self
+ }
+
/// Sets the font of the [`Menu`].
- pub fn font(mut self, font: Renderer::Font) -> Self {
- self.font = font;
+ pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
+ self.font = Some(font.into());
self
}
@@ -102,7 +122,7 @@ where
/// 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>(
+ pub fn overlay(
self,
position: Point,
target_height: f32,
@@ -137,7 +157,7 @@ impl Default for State {
struct Overlay<'a, Message, Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet + container::StyleSheet,
{
state: &'a mut Tree,
@@ -155,7 +175,10 @@ where
Renderer::Theme:
StyleSheet + container::StyleSheet + scrollable::StyleSheet,
{
- pub fn new<T>(menu: Menu<'a, T, Renderer>, target_height: f32) -> Self
+ pub fn new<T>(
+ menu: Menu<'a, T, Message, Renderer>,
+ target_height: f32,
+ ) -> Self
where
T: Clone + ToString,
{
@@ -163,20 +186,24 @@ where
state,
options,
hovered_option,
- last_selection,
+ on_selected,
width,
padding,
font,
text_size,
+ text_line_height,
+ text_shaping,
style,
} = menu;
let container = Container::new(Scrollable::new(List {
options,
hovered_option,
- last_selection,
+ on_selected,
font,
text_size,
+ text_line_height,
+ text_shaping,
padding,
style: style.clone(),
}));
@@ -193,7 +220,7 @@ where
}
}
-impl<'a, Message, Renderer> crate::Overlay<Message, Renderer>
+impl<'a, Message, Renderer> crate::core::Overlay<Message, Renderer>
for Overlay<'a, Message, Renderer>
where
Renderer: text::Renderer,
@@ -236,36 +263,25 @@ where
&mut self,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
self.container.on_event(
- self.state,
- event,
- layout,
- cursor_position,
- renderer,
- clipboard,
- shell,
+ self.state, event, layout, cursor, renderer, clipboard, shell,
)
}
fn mouse_interaction(
&self,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
- self.container.mouse_interaction(
- self.state,
- layout,
- cursor_position,
- viewport,
- renderer,
- )
+ self.container
+ .mouse_interaction(self.state, layout, cursor, viewport, renderer)
}
fn draw(
@@ -274,7 +290,7 @@ where
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
) {
let appearance = theme.appearance(&self.style);
let bounds = layout.bounds();
@@ -284,39 +300,34 @@ where
bounds,
border_color: appearance.border_color,
border_width: appearance.border_width,
- border_radius: appearance.border_radius.into(),
+ border_radius: appearance.border_radius,
},
appearance.background,
);
- self.container.draw(
- self.state,
- renderer,
- theme,
- style,
- layout,
- cursor_position,
- &bounds,
- );
+ self.container
+ .draw(self.state, renderer, theme, style, layout, cursor, &bounds);
}
}
-struct List<'a, T, Renderer>
+struct List<'a, T, Message, Renderer>
where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
{
options: &'a [T],
hovered_option: &'a mut Option<usize>,
- last_selection: &'a mut Option<T>,
+ on_selected: Box<dyn FnMut(T) -> Message + 'a>,
padding: Padding,
text_size: Option<f32>,
- font: Renderer::Font,
+ text_line_height: text::LineHeight,
+ text_shaping: text::Shaping,
+ font: Option<Renderer::Font>,
style: <Renderer::Theme as StyleSheet>::Style,
}
impl<'a, T, Message, Renderer> Widget<Message, Renderer>
- for List<'a, T, Renderer>
+ for List<'a, T, Message, Renderer>
where
T: Clone + ToString,
Renderer: text::Renderer,
@@ -341,10 +352,13 @@ where
let text_size =
self.text_size.unwrap_or_else(|| renderer.default_size());
+ let text_line_height =
+ self.text_line_height.to_absolute(Pixels(text_size));
+
let size = {
let intrinsic = Size::new(
0.0,
- (text_size + self.padding.vertical())
+ (f32::from(text_line_height) + self.padding.vertical())
* self.options.len() as f32,
);
@@ -359,55 +373,57 @@ where
_state: &mut Tree,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
- _shell: &mut Shell<'_, Message>,
+ shell: &mut Shell<'_, Message>,
) -> event::Status {
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
- let bounds = layout.bounds();
-
- if bounds.contains(cursor_position) {
+ if cursor.is_over(layout.bounds()) {
if let Some(index) = *self.hovered_option {
if let Some(option) = self.options.get(index) {
- *self.last_selection = Some(option.clone());
+ shell.publish((self.on_selected)(option.clone()));
+ return event::Status::Captured;
}
}
}
}
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
- let bounds = layout.bounds();
-
- if bounds.contains(cursor_position) {
+ if let Some(cursor_position) =
+ cursor.position_in(layout.bounds())
+ {
let text_size = self
.text_size
.unwrap_or_else(|| renderer.default_size());
- *self.hovered_option = Some(
- ((cursor_position.y - bounds.y)
- / (text_size + self.padding.vertical()))
- as usize,
- );
+ let option_height = f32::from(
+ self.text_line_height.to_absolute(Pixels(text_size)),
+ ) + self.padding.vertical();
+
+ *self.hovered_option =
+ Some((cursor_position.y / option_height) as usize);
}
}
Event::Touch(touch::Event::FingerPressed { .. }) => {
- let bounds = layout.bounds();
-
- if bounds.contains(cursor_position) {
+ if let Some(cursor_position) =
+ cursor.position_in(layout.bounds())
+ {
let text_size = self
.text_size
.unwrap_or_else(|| renderer.default_size());
- *self.hovered_option = Some(
- ((cursor_position.y - bounds.y)
- / (text_size + self.padding.vertical()))
- as usize,
- );
+ let option_height = f32::from(
+ self.text_line_height.to_absolute(Pixels(text_size)),
+ ) + self.padding.vertical();
+
+ *self.hovered_option =
+ Some((cursor_position.y / option_height) as usize);
if let Some(index) = *self.hovered_option {
if let Some(option) = self.options.get(index) {
- *self.last_selection = Some(option.clone());
+ shell.publish((self.on_selected)(option.clone()));
+ return event::Status::Captured;
}
}
}
@@ -422,11 +438,11 @@ where
&self,
_state: &Tree,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
- let is_mouse_over = layout.bounds().contains(cursor_position);
+ let is_mouse_over = cursor.is_over(layout.bounds());
if is_mouse_over {
mouse::Interaction::Pointer
@@ -442,7 +458,7 @@ where
theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
- _cursor_position: Point,
+ _cursor: mouse::Cursor,
viewport: &Rectangle,
) {
let appearance = theme.appearance(&self.style);
@@ -450,12 +466,13 @@ where
let text_size =
self.text_size.unwrap_or_else(|| renderer.default_size());
- let option_height = (text_size + self.padding.vertical()) as usize;
+ let option_height =
+ f32::from(self.text_line_height.to_absolute(Pixels(text_size)))
+ + self.padding.vertical();
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 start = (offset / option_height) as usize;
+ let end = ((offset + viewport.height) / option_height).ceil() as usize;
let visible_options = &self.options[start..end.min(self.options.len())];
@@ -465,18 +482,22 @@ where
let bounds = Rectangle {
x: bounds.x,
- y: bounds.y + (option_height * i) as f32,
+ y: bounds.y + (option_height * i as f32),
width: bounds.width,
- height: text_size + self.padding.vertical(),
+ height: option_height,
};
if is_selected {
renderer.fill_quad(
renderer::Quad {
- bounds,
+ bounds: Rectangle {
+ x: bounds.x + appearance.border_width,
+ width: bounds.width - appearance.border_width * 2.0,
+ ..bounds
+ },
border_color: Color::TRANSPARENT,
border_width: 0.0,
- border_radius: appearance.border_radius.into(),
+ border_radius: appearance.border_radius,
},
appearance.selected_background,
);
@@ -491,7 +512,8 @@ where
..bounds
},
size: text_size,
- font: self.font.clone(),
+ line_height: self.text_line_height,
+ font: self.font.unwrap_or_else(|| renderer.default_font()),
color: if is_selected {
appearance.selected_text_color
} else {
@@ -499,12 +521,13 @@ where
},
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
+ shaping: self.text_shaping,
});
}
}
}
-impl<'a, T, Message, Renderer> From<List<'a, T, Renderer>>
+impl<'a, T, Message, Renderer> From<List<'a, T, Message, Renderer>>
for Element<'a, Message, Renderer>
where
T: ToString + Clone,
@@ -512,7 +535,7 @@ where
Renderer: 'a + text::Renderer,
Renderer::Theme: StyleSheet,
{
- fn from(list: List<'a, T, Renderer>) -> Self {
+ fn from(list: List<'a, T, Message, Renderer>) -> Self {
Element::new(list)
}
}
diff --git a/native/src/widget/pane_grid.rs b/widget/src/pane_grid.rs
index bcb17ebd..31bb0e86 100644
--- a/native/src/widget/pane_grid.rs
+++ b/widget/src/pane_grid.rs
@@ -6,7 +6,7 @@
//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing,
//! drag and drop, and hotkey support.
//!
-//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.8/examples/pane_grid
+//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.9/examples/pane_grid
mod axis;
mod configuration;
mod content;
@@ -30,18 +30,18 @@ pub use split::Split;
pub use state::State;
pub use title_bar::TitleBar;
-pub use iced_style::pane_grid::{Line, StyleSheet};
-
-use crate::event::{self, Event};
-use crate::layout;
-use crate::mouse;
-use crate::overlay::{self, Group};
-use crate::renderer;
-use crate::touch;
-use crate::widget;
-use crate::widget::container;
-use crate::widget::tree::{self, Tree};
-use crate::{
+pub use crate::style::pane_grid::{Appearance, Line, StyleSheet};
+
+use crate::container;
+use crate::core::event::{self, Event};
+use crate::core::layout;
+use crate::core::mouse;
+use crate::core::overlay::{self, Group};
+use crate::core::renderer;
+use crate::core::touch;
+use crate::core::widget;
+use crate::core::widget::tree::{self, Tree};
+use crate::core::{
Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, Shell,
Size, Vector, Widget,
};
@@ -67,11 +67,11 @@ use crate::{
///
/// ## Example
///
-/// ```
-/// # use iced_native::widget::{pane_grid, text};
+/// ```no_run
+/// # use iced_widget::{pane_grid, text};
/// #
/// # type PaneGrid<'a, Message> =
-/// # iced_native::widget::PaneGrid<'a, Message, iced_native::renderer::Null>;
+/// # iced_widget::PaneGrid<'a, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
/// #
/// enum PaneState {
/// SomePane,
@@ -96,9 +96,9 @@ use crate::{
/// .on_resize(10, Message::PaneResized);
/// ```
#[allow(missing_debug_implementations)]
-pub struct PaneGrid<'a, Message, Renderer>
+pub struct PaneGrid<'a, Message, Renderer = crate::Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet + container::StyleSheet,
{
contents: Contents<'a, Content<'a, Message, Renderer>>,
@@ -113,7 +113,7 @@ where
impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet + container::StyleSheet,
{
/// Creates a [`PaneGrid`] with the given [`State`] and view function.
@@ -232,7 +232,7 @@ where
impl<'a, Message, Renderer> Widget<Message, Renderer>
for PaneGrid<'a, Message, Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet + container::StyleSheet,
{
fn tag(&self) -> tree::Tag {
@@ -313,7 +313,7 @@ where
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
@@ -331,7 +331,7 @@ where
self.contents.layout(),
&event,
layout,
- cursor_position,
+ cursor,
shell,
self.spacing,
self.contents.iter(),
@@ -353,7 +353,7 @@ where
tree,
event.clone(),
layout,
- cursor_position,
+ cursor,
renderer,
clipboard,
shell,
@@ -367,7 +367,7 @@ where
&self,
tree: &Tree,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
@@ -375,7 +375,7 @@ where
tree.state.downcast_ref(),
self.contents.layout(),
layout,
- cursor_position,
+ cursor,
self.spacing,
self.on_resize.as_ref().map(|(leeway, _)| *leeway),
)
@@ -388,7 +388,7 @@ where
content.mouse_interaction(
tree,
layout,
- cursor_position,
+ cursor,
viewport,
renderer,
self.drag_enabled(),
@@ -406,14 +406,14 @@ where
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
) {
draw(
tree.state.downcast_ref(),
self.contents.layout(),
layout,
- cursor_position,
+ cursor,
renderer,
theme,
style,
@@ -425,20 +425,9 @@ where
.iter()
.zip(&tree.children)
.map(|((pane, content), tree)| (pane, (content, tree))),
- |(content, tree),
- renderer,
- style,
- layout,
- cursor_position,
- rectangle| {
+ |(content, tree), renderer, style, layout, cursor, rectangle| {
content.draw(
- tree,
- renderer,
- theme,
- style,
- layout,
- cursor_position,
- rectangle,
+ tree, renderer, theme, style, layout, cursor, rectangle,
);
},
)
@@ -468,7 +457,7 @@ impl<'a, Message, Renderer> From<PaneGrid<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Message: 'a,
- Renderer: 'a + crate::Renderer,
+ Renderer: 'a + crate::core::Renderer,
Renderer::Theme: StyleSheet + container::StyleSheet,
{
fn from(
@@ -520,7 +509,7 @@ pub fn update<'a, Message, T: Draggable>(
node: &Node,
event: &Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
shell: &mut Shell<'_, Message>,
spacing: f32,
contents: impl Iterator<Item = (Pane, T)>,
@@ -535,7 +524,7 @@ pub fn update<'a, Message, T: Draggable>(
| Event::Touch(touch::Event::FingerPressed { .. }) => {
let bounds = layout.bounds();
- if bounds.contains(cursor_position) {
+ if let Some(cursor_position) = cursor.position_over(bounds) {
event_status = event::Status::Captured;
match on_resize {
@@ -592,30 +581,46 @@ pub fn update<'a, Message, T: Draggable>(
| Event::Touch(touch::Event::FingerLost { .. }) => {
if let Some((pane, _)) = action.picked_pane() {
if let Some(on_drag) = on_drag {
- let mut dropped_region = contents
- .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 }
- }
- _ => DragEvent::Canceled { pane },
- };
+ if let Some(cursor_position) = cursor.position() {
+ let event = if let Some(edge) =
+ in_edge(layout, cursor_position)
+ {
+ DragEvent::Dropped {
+ pane,
+ target: Target::Edge(edge),
+ }
+ } else {
+ let dropped_region = contents
+ .zip(layout.children())
+ .filter_map(|(target, layout)| {
+ layout_region(layout, cursor_position)
+ .map(|region| (target, region))
+ })
+ .next();
+
+ match dropped_region {
+ Some(((target, _), region))
+ if pane != target =>
+ {
+ DragEvent::Dropped {
+ pane,
+ target: Target::Pane(target, region),
+ }
+ }
+ _ => DragEvent::Canceled { pane },
+ }
+ };
- shell.publish(on_drag(event));
+ shell.publish(on_drag(event));
+ }
}
- *action = state::Action::Idle;
-
event_status = event::Status::Captured;
} else if action.picked_split().is_some() {
- *action = state::Action::Idle;
-
event_status = event::Status::Captured;
}
+
+ *action = state::Action::Idle;
}
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => {
@@ -629,24 +634,32 @@ pub fn update<'a, Message, T: Draggable>(
);
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).clamp(0.1, 0.9)
- }
- Axis::Vertical => {
- let position =
- cursor_position.x - bounds.x - rectangle.x;
-
- (position / rectangle.width).clamp(0.1, 0.9)
- }
- };
-
- shell.publish(on_resize(ResizeEvent { split, ratio }));
-
- event_status = event::Status::Captured;
+ if let Some(cursor_position) = cursor.position() {
+ let ratio = match axis {
+ Axis::Horizontal => {
+ let position = cursor_position.y
+ - bounds.y
+ - rectangle.y;
+
+ (position / rectangle.height)
+ .clamp(0.1, 0.9)
+ }
+ Axis::Vertical => {
+ let position = cursor_position.x
+ - bounds.x
+ - rectangle.x;
+
+ (position / rectangle.width).clamp(0.1, 0.9)
+ }
+ };
+
+ shell.publish(on_resize(ResizeEvent {
+ split,
+ ratio,
+ }));
+
+ event_status = event::Status::Captured;
+ }
}
}
}
@@ -657,6 +670,28 @@ pub fn update<'a, Message, T: Draggable>(
event_status
}
+fn layout_region(layout: Layout<'_>, cursor_position: Point) -> Option<Region> {
+ let bounds = layout.bounds();
+
+ if !bounds.contains(cursor_position) {
+ return None;
+ }
+
+ let region = if cursor_position.x < (bounds.x + bounds.width / 3.0) {
+ Region::Edge(Edge::Left)
+ } else if cursor_position.x > (bounds.x + 2.0 * bounds.width / 3.0) {
+ Region::Edge(Edge::Right)
+ } else if cursor_position.y < (bounds.y + bounds.height / 3.0) {
+ Region::Edge(Edge::Top)
+ } else if cursor_position.y > (bounds.y + 2.0 * bounds.height / 3.0) {
+ Region::Edge(Edge::Bottom)
+ } else {
+ Region::Center
+ };
+
+ Some(region)
+}
+
fn click_pane<'a, Message, T>(
action: &mut state::Action,
layout: Layout<'_>,
@@ -697,7 +732,7 @@ pub fn mouse_interaction(
action: &state::Action,
node: &Node,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
spacing: f32,
resize_leeway: Option<f32>,
) -> Option<mouse::Interaction> {
@@ -708,6 +743,7 @@ pub fn mouse_interaction(
let resize_axis =
action.picked_split().map(|(_, axis)| axis).or_else(|| {
resize_leeway.and_then(|leeway| {
+ let cursor_position = cursor.position()?;
let bounds = layout.bounds();
let splits = node.split_regions(spacing, bounds.size());
@@ -737,7 +773,7 @@ pub fn draw<Renderer, T>(
action: &state::Action,
node: &Node,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &mut Renderer,
theme: &Renderer::Theme,
default_style: &renderer::Style,
@@ -751,11 +787,11 @@ pub fn draw<Renderer, T>(
&mut Renderer,
&renderer::Style,
Layout<'_>,
- Point,
+ mouse::Cursor,
&Rectangle,
),
) where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
let picked_pane = action.picked_pane();
@@ -775,6 +811,7 @@ pub fn draw<Renderer, T>(
})
.or_else(|| match resize_leeway {
Some(leeway) => {
+ let cursor_position = cursor.position()?;
let bounds = layout.bounds();
let relative_cursor = Point::new(
@@ -795,93 +832,234 @@ pub fn draw<Renderer, T>(
None => None,
});
- let pane_cursor_position = if picked_pane.is_some() {
- // TODO: Remove once cursor availability is encoded in the type
- // system
- Point::new(-1.0, -1.0)
+ let pane_cursor = if picked_pane.is_some() {
+ mouse::Cursor::Unavailable
} else {
- cursor_position
+ cursor
};
let mut render_picked_pane = None;
- for ((id, pane), layout) in contents.zip(layout.children()) {
+ let pane_in_edge = if picked_pane.is_some() {
+ cursor
+ .position()
+ .and_then(|cursor_position| in_edge(layout, cursor_position))
+ } else {
+ None
+ };
+
+ for ((id, pane), pane_layout) in contents.zip(layout.children()) {
match picked_pane {
Some((dragging, origin)) if id == dragging => {
- render_picked_pane = Some((pane, origin, layout));
+ render_picked_pane = Some((pane, origin, pane_layout));
+ }
+ Some((dragging, _)) if id != dragging => {
+ draw_pane(
+ pane,
+ renderer,
+ default_style,
+ pane_layout,
+ pane_cursor,
+ viewport,
+ );
+
+ if picked_pane.is_some() && pane_in_edge.is_none() {
+ if let Some(region) =
+ cursor.position().and_then(|cursor_position| {
+ layout_region(pane_layout, cursor_position)
+ })
+ {
+ let bounds = layout_region_bounds(pane_layout, region);
+ let hovered_region_style = theme.hovered_region(style);
+
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds,
+ border_radius: hovered_region_style
+ .border_radius,
+ border_width: hovered_region_style.border_width,
+ border_color: hovered_region_style.border_color,
+ },
+ theme.hovered_region(style).background,
+ );
+ }
+ }
}
_ => {
draw_pane(
pane,
renderer,
default_style,
- layout,
- pane_cursor_position,
+ pane_layout,
+ pane_cursor,
viewport,
);
}
}
}
- // Render picked pane last
- if let Some((pane, origin, layout)) = render_picked_pane {
- let bounds = layout.bounds();
-
- renderer.with_translation(
- cursor_position
- - Point::new(bounds.x + origin.x, bounds.y + origin.y),
- |renderer| {
- renderer.with_layer(bounds, |renderer| {
- draw_pane(
- pane,
- renderer,
- default_style,
- layout,
- pane_cursor_position,
- viewport,
- );
- });
+ if let Some(edge) = pane_in_edge {
+ let hovered_region_style = theme.hovered_region(style);
+ let bounds = edge_bounds(layout, edge);
+
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds,
+ border_radius: hovered_region_style.border_radius,
+ border_width: hovered_region_style.border_width,
+ border_color: hovered_region_style.border_color,
},
+ theme.hovered_region(style).background,
);
- };
+ }
- if let Some((axis, split_region, is_picked)) = picked_split {
- let highlight = if is_picked {
- theme.picked_split(style)
- } else {
- theme.hovered_split(style)
- };
+ // Render picked pane last
+ if let Some((pane, origin, layout)) = render_picked_pane {
+ if let Some(cursor_position) = cursor.position() {
+ let bounds = layout.bounds();
- if let Some(highlight) = highlight {
- renderer.fill_quad(
- renderer::Quad {
- bounds: match axis {
- Axis::Horizontal => Rectangle {
- x: split_region.x,
- y: (split_region.y
- + (split_region.height - highlight.width)
- / 2.0)
- .round(),
- width: split_region.width,
- height: highlight.width,
- },
- Axis::Vertical => Rectangle {
- x: (split_region.x
- + (split_region.width - highlight.width) / 2.0)
- .round(),
- y: split_region.y,
- width: highlight.width,
- height: split_region.height,
- },
- },
- border_radius: 0.0.into(),
- border_width: 0.0,
- border_color: Color::TRANSPARENT,
+ renderer.with_translation(
+ cursor_position
+ - Point::new(bounds.x + origin.x, bounds.y + origin.y),
+ |renderer| {
+ renderer.with_layer(bounds, |renderer| {
+ draw_pane(
+ pane,
+ renderer,
+ default_style,
+ layout,
+ pane_cursor,
+ viewport,
+ );
+ });
},
- highlight.color,
);
}
}
+
+ if picked_pane.is_none() {
+ if let Some((axis, split_region, is_picked)) = picked_split {
+ let highlight = if is_picked {
+ theme.picked_split(style)
+ } else {
+ theme.hovered_split(style)
+ };
+
+ if let Some(highlight) = highlight {
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: match axis {
+ Axis::Horizontal => Rectangle {
+ x: split_region.x,
+ y: (split_region.y
+ + (split_region.height - highlight.width)
+ / 2.0)
+ .round(),
+ width: split_region.width,
+ height: highlight.width,
+ },
+ Axis::Vertical => Rectangle {
+ x: (split_region.x
+ + (split_region.width - highlight.width)
+ / 2.0)
+ .round(),
+ y: split_region.y,
+ width: highlight.width,
+ height: split_region.height,
+ },
+ },
+ border_radius: 0.0.into(),
+ border_width: 0.0,
+ border_color: Color::TRANSPARENT,
+ },
+ highlight.color,
+ );
+ }
+ }
+ }
+}
+
+const THICKNESS_RATIO: f32 = 25.0;
+
+fn in_edge(layout: Layout<'_>, cursor: Point) -> Option<Edge> {
+ let bounds = layout.bounds();
+
+ let height_thickness = bounds.height / THICKNESS_RATIO;
+ let width_thickness = bounds.width / THICKNESS_RATIO;
+ let thickness = height_thickness.min(width_thickness);
+
+ if cursor.x > bounds.x && cursor.x < bounds.x + thickness {
+ Some(Edge::Left)
+ } else if cursor.x > bounds.x + bounds.width - thickness
+ && cursor.x < bounds.x + bounds.width
+ {
+ Some(Edge::Right)
+ } else if cursor.y > bounds.y && cursor.y < bounds.y + thickness {
+ Some(Edge::Top)
+ } else if cursor.y > bounds.y + bounds.height - thickness
+ && cursor.y < bounds.y + bounds.height
+ {
+ Some(Edge::Bottom)
+ } else {
+ None
+ }
+}
+
+fn edge_bounds(layout: Layout<'_>, edge: Edge) -> Rectangle {
+ let bounds = layout.bounds();
+
+ let height_thickness = bounds.height / THICKNESS_RATIO;
+ let width_thickness = bounds.width / THICKNESS_RATIO;
+ let thickness = height_thickness.min(width_thickness);
+
+ match edge {
+ Edge::Top => Rectangle {
+ height: thickness,
+ ..bounds
+ },
+ Edge::Left => Rectangle {
+ width: thickness,
+ ..bounds
+ },
+ Edge::Right => Rectangle {
+ x: bounds.x + bounds.width - thickness,
+ width: thickness,
+ ..bounds
+ },
+ Edge::Bottom => Rectangle {
+ y: bounds.y + bounds.height - thickness,
+ height: thickness,
+ ..bounds
+ },
+ }
+}
+
+fn layout_region_bounds(layout: Layout<'_>, region: Region) -> Rectangle {
+ let bounds = layout.bounds();
+
+ match region {
+ Region::Center => bounds,
+ Region::Edge(edge) => match edge {
+ Edge::Top => Rectangle {
+ height: bounds.height / 2.0,
+ ..bounds
+ },
+ Edge::Left => Rectangle {
+ width: bounds.width / 2.0,
+ ..bounds
+ },
+ Edge::Right => Rectangle {
+ x: bounds.x + bounds.width / 2.0,
+ width: bounds.width / 2.0,
+ ..bounds
+ },
+ Edge::Bottom => Rectangle {
+ y: bounds.y + bounds.height / 2.0,
+ height: bounds.height / 2.0,
+ ..bounds
+ },
+ },
+ }
}
/// An event produced during a drag and drop interaction of a [`PaneGrid`].
@@ -898,8 +1076,8 @@ pub enum DragEvent {
/// The picked [`Pane`].
pane: Pane,
- /// The [`Pane`] where the picked one was dropped on.
- target: Pane,
+ /// The [`Target`] where the picked [`Pane`] was dropped on.
+ target: Target,
},
/// A [`Pane`] was picked and then dropped outside of other [`Pane`]
@@ -910,6 +1088,38 @@ pub enum DragEvent {
},
}
+/// The [`Target`] area a pane can be dropped on.
+#[derive(Debug, Clone, Copy)]
+pub enum Target {
+ /// An [`Edge`] of the full [`PaneGrid`].
+ Edge(Edge),
+ /// A single [`Pane`] of the [`PaneGrid`].
+ Pane(Pane, Region),
+}
+
+/// The region of a [`Pane`].
+#[derive(Debug, Clone, Copy, Default)]
+pub enum Region {
+ /// Center region.
+ #[default]
+ Center,
+ /// Edge region.
+ Edge(Edge),
+}
+
+/// The edges of an area.
+#[derive(Debug, Clone, Copy)]
+pub enum Edge {
+ /// Top edge.
+ Top,
+ /// Left edge.
+ Left,
+ /// Right edge.
+ Right,
+ /// Bottom edge.
+ Bottom,
+}
+
/// An event produced during a resize interaction of a [`PaneGrid`].
#[derive(Debug, Clone, Copy)]
pub struct ResizeEvent {
diff --git a/native/src/widget/pane_grid/axis.rs b/widget/src/pane_grid/axis.rs
index 02bde064..a3049230 100644
--- a/native/src/widget/pane_grid/axis.rs
+++ b/widget/src/pane_grid/axis.rs
@@ -1,4 +1,4 @@
-use crate::Rectangle;
+use crate::core::Rectangle;
/// A fixed reference line for the measurement of coordinates.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
diff --git a/native/src/widget/pane_grid/configuration.rs b/widget/src/pane_grid/configuration.rs
index 7d68fb46..ddbc3bc2 100644
--- a/native/src/widget/pane_grid/configuration.rs
+++ b/widget/src/pane_grid/configuration.rs
@@ -1,4 +1,4 @@
-use crate::widget::pane_grid::Axis;
+use crate::pane_grid::Axis;
/// The arrangement of a [`PaneGrid`].
///
diff --git a/native/src/widget/pane_grid/content.rs b/widget/src/pane_grid/content.rs
index c9b0df07..c28ae6e3 100644
--- a/native/src/widget/pane_grid/content.rs
+++ b/widget/src/pane_grid/content.rs
@@ -1,20 +1,20 @@
-use crate::event::{self, Event};
-use crate::layout;
-use crate::mouse;
-use crate::overlay;
-use crate::renderer;
-use crate::widget::container;
-use crate::widget::pane_grid::{Draggable, TitleBar};
-use crate::widget::{self, Tree};
-use crate::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size};
+use crate::container;
+use crate::core::event::{self, Event};
+use crate::core::layout;
+use crate::core::mouse;
+use crate::core::overlay;
+use crate::core::renderer;
+use crate::core::widget::{self, Tree};
+use crate::core::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size};
+use crate::pane_grid::{Draggable, TitleBar};
/// The content of a [`Pane`].
///
/// [`Pane`]: crate::widget::pane_grid::Pane
#[allow(missing_debug_implementations)]
-pub struct Content<'a, Message, Renderer>
+pub struct Content<'a, Message, Renderer = crate::Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: container::StyleSheet,
{
title_bar: Option<TitleBar<'a, Message, Renderer>>,
@@ -24,7 +24,7 @@ where
impl<'a, Message, Renderer> Content<'a, Message, Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: container::StyleSheet,
{
/// Creates a new [`Content`] with the provided body.
@@ -57,7 +57,7 @@ where
impl<'a, Message, Renderer> Content<'a, Message, Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: container::StyleSheet,
{
pub(super) fn state(&self) -> Tree {
@@ -95,7 +95,7 @@ where
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
) {
use container::StyleSheet;
@@ -113,7 +113,7 @@ where
let title_bar_layout = children.next().unwrap();
let body_layout = children.next().unwrap();
- let show_controls = bounds.contains(cursor_position);
+ let show_controls = cursor.is_over(bounds);
self.body.as_widget().draw(
&tree.children[0],
@@ -121,7 +121,7 @@ where
theme,
style,
body_layout,
- cursor_position,
+ cursor,
viewport,
);
@@ -131,7 +131,7 @@ where
theme,
style,
title_bar_layout,
- cursor_position,
+ cursor,
viewport,
show_controls,
);
@@ -142,7 +142,7 @@ where
theme,
style,
layout,
- cursor_position,
+ cursor,
viewport,
);
}
@@ -218,7 +218,7 @@ where
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
@@ -233,7 +233,7 @@ where
&mut tree.children[1],
event.clone(),
children.next().unwrap(),
- cursor_position,
+ cursor,
renderer,
clipboard,
shell,
@@ -251,7 +251,7 @@ where
&mut tree.children[0],
event,
body_layout,
- cursor_position,
+ cursor,
renderer,
clipboard,
shell,
@@ -265,42 +265,48 @@ where
&self,
tree: &Tree,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
drag_enabled: bool,
) -> mouse::Interaction {
- let (body_layout, title_bar_interaction) =
- if let Some(title_bar) = &self.title_bar {
- let mut children = layout.children();
- let title_bar_layout = children.next().unwrap();
-
- let is_over_pick_area = title_bar
- .is_over_pick_area(title_bar_layout, cursor_position);
-
- if is_over_pick_area && drag_enabled {
- return mouse::Interaction::Grab;
- }
-
- let mouse_interaction = title_bar.mouse_interaction(
- &tree.children[1],
- title_bar_layout,
- cursor_position,
- viewport,
- renderer,
- );
+ let (body_layout, title_bar_interaction) = if let Some(title_bar) =
+ &self.title_bar
+ {
+ let mut children = layout.children();
+ let title_bar_layout = children.next().unwrap();
+
+ let is_over_pick_area = cursor
+ .position()
+ .map(|cursor_position| {
+ title_bar
+ .is_over_pick_area(title_bar_layout, cursor_position)
+ })
+ .unwrap_or_default();
+
+ if is_over_pick_area && drag_enabled {
+ return mouse::Interaction::Grab;
+ }
- (children.next().unwrap(), mouse_interaction)
- } else {
- (layout, mouse::Interaction::default())
- };
+ let mouse_interaction = title_bar.mouse_interaction(
+ &tree.children[1],
+ title_bar_layout,
+ cursor,
+ viewport,
+ renderer,
+ );
+
+ (children.next().unwrap(), mouse_interaction)
+ } else {
+ (layout, mouse::Interaction::default())
+ };
self.body
.as_widget()
.mouse_interaction(
&tree.children[0],
body_layout,
- cursor_position,
+ cursor,
viewport,
renderer,
)
@@ -342,7 +348,7 @@ where
impl<'a, Message, Renderer> Draggable for &Content<'a, Message, Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: container::StyleSheet,
{
fn can_be_dragged_at(
@@ -364,7 +370,7 @@ where
impl<'a, T, Message, Renderer> From<T> for Content<'a, Message, Renderer>
where
T: Into<Element<'a, Message, Renderer>>,
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: container::StyleSheet,
{
fn from(element: T) -> Self {
diff --git a/native/src/widget/pane_grid/direction.rs b/widget/src/pane_grid/direction.rs
index b31a8737..b31a8737 100644
--- a/native/src/widget/pane_grid/direction.rs
+++ b/widget/src/pane_grid/direction.rs
diff --git a/native/src/widget/pane_grid/draggable.rs b/widget/src/pane_grid/draggable.rs
index 6044871d..9d31feb5 100644
--- a/native/src/widget/pane_grid/draggable.rs
+++ b/widget/src/pane_grid/draggable.rs
@@ -1,12 +1,8 @@
-use crate::{Layout, Point};
+use crate::core::{Layout, Point};
/// A pane that can be dragged.
pub trait Draggable {
/// Returns whether the [`Draggable`] with the given [`Layout`] can be picked
/// at the provided cursor position.
- fn can_be_dragged_at(
- &self,
- layout: Layout<'_>,
- cursor_position: Point,
- ) -> bool;
+ fn can_be_dragged_at(&self, layout: Layout<'_>, cursor: Point) -> bool;
}
diff --git a/native/src/widget/pane_grid/node.rs b/widget/src/pane_grid/node.rs
index cc304b96..6de5920f 100644
--- a/native/src/widget/pane_grid/node.rs
+++ b/widget/src/pane_grid/node.rs
@@ -1,5 +1,5 @@
-use crate::widget::pane_grid::{Axis, Pane, Split};
-use crate::{Rectangle, Size};
+use crate::core::{Rectangle, Size};
+use crate::pane_grid::{Axis, Pane, Split};
use std::collections::BTreeMap;
@@ -120,6 +120,16 @@ impl Node {
};
}
+ pub(crate) fn split_inverse(&mut self, id: Split, axis: Axis, pane: Pane) {
+ *self = Node::Split {
+ id,
+ axis,
+ ratio: 0.5,
+ a: Box::new(Node::Pane(pane)),
+ b: Box::new(self.clone()),
+ };
+ }
+
pub(crate) fn update(&mut self, f: &impl Fn(&mut Node)) {
if let Node::Split { a, b, .. } = self {
a.update(f);
diff --git a/native/src/widget/pane_grid/pane.rs b/widget/src/pane_grid/pane.rs
index d6fbab83..d6fbab83 100644
--- a/native/src/widget/pane_grid/pane.rs
+++ b/widget/src/pane_grid/pane.rs
diff --git a/native/src/widget/pane_grid/split.rs b/widget/src/pane_grid/split.rs
index 8132272a..8132272a 100644
--- a/native/src/widget/pane_grid/split.rs
+++ b/widget/src/pane_grid/split.rs
diff --git a/native/src/widget/pane_grid/state.rs b/widget/src/pane_grid/state.rs
index c4ae0a0e..6fd15890 100644
--- a/native/src/widget/pane_grid/state.rs
+++ b/widget/src/pane_grid/state.rs
@@ -1,10 +1,10 @@
//! The state of a [`PaneGrid`].
//!
//! [`PaneGrid`]: crate::widget::PaneGrid
-use crate::widget::pane_grid::{
- Axis, Configuration, Direction, Node, Pane, Split,
+use crate::core::{Point, Size};
+use crate::pane_grid::{
+ Axis, Configuration, Direction, Edge, Node, Pane, Region, Split, Target,
};
-use crate::{Point, Size};
use std::collections::HashMap;
@@ -145,7 +145,55 @@ impl<T> State<T> {
pane: &Pane,
state: T,
) -> Option<(Pane, Split)> {
- let node = self.internal.layout.find(pane)?;
+ self.split_node(axis, Some(pane), state, false)
+ }
+
+ /// Split a target [`Pane`] with a given [`Pane`] on a given [`Region`].
+ ///
+ /// Panes will be swapped by default for [`Region::Center`].
+ pub fn split_with(&mut self, target: &Pane, pane: &Pane, region: Region) {
+ match region {
+ Region::Center => self.swap(pane, target),
+ Region::Edge(edge) => match edge {
+ Edge::Top => {
+ self.split_and_swap(Axis::Horizontal, target, pane, true)
+ }
+ Edge::Bottom => {
+ self.split_and_swap(Axis::Horizontal, target, pane, false)
+ }
+ Edge::Left => {
+ self.split_and_swap(Axis::Vertical, target, pane, true)
+ }
+ Edge::Right => {
+ self.split_and_swap(Axis::Vertical, target, pane, false)
+ }
+ },
+ }
+ }
+
+ /// Drops the given [`Pane`] into the provided [`Target`].
+ pub fn drop(&mut self, pane: &Pane, target: Target) {
+ match target {
+ Target::Edge(edge) => self.move_to_edge(pane, edge),
+ Target::Pane(target, region) => {
+ self.split_with(&target, pane, region)
+ }
+ }
+ }
+
+ fn split_node(
+ &mut self,
+ axis: Axis,
+ pane: Option<&Pane>,
+ state: T,
+ inverse: bool,
+ ) -> Option<(Pane, Split)> {
+ let node = if let Some(pane) = pane {
+ self.internal.layout.find(pane)?
+ } else {
+ // Major node
+ &mut self.internal.layout
+ };
let new_pane = {
self.internal.last_id = self.internal.last_id.checked_add(1)?;
@@ -159,7 +207,11 @@ impl<T> State<T> {
Split(self.internal.last_id)
};
- node.split(new_split, axis, new_pane);
+ if inverse {
+ node.split_inverse(new_split, axis, new_pane);
+ } else {
+ node.split(new_split, axis, new_pane);
+ }
let _ = self.panes.insert(new_pane, state);
let _ = self.maximized.take();
@@ -167,6 +219,51 @@ impl<T> State<T> {
Some((new_pane, new_split))
}
+ fn split_and_swap(
+ &mut self,
+ axis: Axis,
+ target: &Pane,
+ pane: &Pane,
+ swap: bool,
+ ) {
+ if let Some((state, _)) = self.close(pane) {
+ if let Some((new_pane, _)) = self.split(axis, target, state) {
+ if swap {
+ self.swap(target, &new_pane);
+ }
+ }
+ }
+ }
+
+ /// Move [`Pane`] to an [`Edge`] of the [`PaneGrid`].
+ pub fn move_to_edge(&mut self, pane: &Pane, edge: Edge) {
+ match edge {
+ Edge::Top => {
+ self.split_major_node_and_swap(Axis::Horizontal, pane, true)
+ }
+ Edge::Bottom => {
+ self.split_major_node_and_swap(Axis::Horizontal, pane, false)
+ }
+ Edge::Left => {
+ self.split_major_node_and_swap(Axis::Vertical, pane, true)
+ }
+ Edge::Right => {
+ self.split_major_node_and_swap(Axis::Vertical, pane, false)
+ }
+ }
+ }
+
+ fn split_major_node_and_swap(
+ &mut self,
+ axis: Axis,
+ pane: &Pane,
+ swap: bool,
+ ) {
+ if let Some((state, _)) = self.close(pane) {
+ let _ = self.split_node(axis, None, state, swap);
+ }
+ }
+
/// Swaps the position of the provided panes in the [`State`].
///
/// If you want to swap panes on drag and drop in your [`PaneGrid`], you
diff --git a/native/src/widget/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs
index 107078ef..2fe79f80 100644
--- a/native/src/widget/pane_grid/title_bar.rs
+++ b/widget/src/pane_grid/title_bar.rs
@@ -1,11 +1,11 @@
-use crate::event::{self, Event};
-use crate::layout;
-use crate::mouse;
-use crate::overlay;
-use crate::renderer;
-use crate::widget::container;
-use crate::widget::{self, Tree};
-use crate::{
+use crate::container;
+use crate::core::event::{self, Event};
+use crate::core::layout;
+use crate::core::mouse;
+use crate::core::overlay;
+use crate::core::renderer;
+use crate::core::widget::{self, Tree};
+use crate::core::{
Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size,
};
@@ -13,9 +13,9 @@ use crate::{
///
/// [`Pane`]: crate::widget::pane_grid::Pane
#[allow(missing_debug_implementations)]
-pub struct TitleBar<'a, Message, Renderer>
+pub struct TitleBar<'a, Message, Renderer = crate::Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: container::StyleSheet,
{
content: Element<'a, Message, Renderer>,
@@ -27,7 +27,7 @@ where
impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: container::StyleSheet,
{
/// Creates a new [`TitleBar`] with the given content.
@@ -84,7 +84,7 @@ where
impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: container::StyleSheet,
{
pub(super) fn state(&self) -> Tree {
@@ -122,7 +122,7 @@ where
theme: &Renderer::Theme,
inherited_style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
show_controls: bool,
) {
@@ -158,7 +158,7 @@ where
theme,
&inherited_style,
controls_layout,
- cursor_position,
+ cursor,
viewport,
);
}
@@ -171,7 +171,7 @@ where
theme,
&inherited_style,
title_layout,
- cursor_position,
+ cursor,
viewport,
);
}
@@ -300,7 +300,7 @@ where
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
@@ -324,7 +324,7 @@ where
&mut tree.children[1],
event.clone(),
controls_layout,
- cursor_position,
+ cursor,
renderer,
clipboard,
shell,
@@ -338,7 +338,7 @@ where
&mut tree.children[0],
event,
title_layout,
- cursor_position,
+ cursor,
renderer,
clipboard,
shell,
@@ -354,7 +354,7 @@ where
&self,
tree: &Tree,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
@@ -367,7 +367,7 @@ where
let title_interaction = self.content.as_widget().mouse_interaction(
&tree.children[0],
title_layout,
- cursor_position,
+ cursor,
viewport,
renderer,
);
@@ -377,7 +377,7 @@ where
let controls_interaction = controls.as_widget().mouse_interaction(
&tree.children[1],
controls_layout,
- cursor_position,
+ cursor,
viewport,
renderer,
);
diff --git a/native/src/widget/pick_list.rs b/widget/src/pick_list.rs
index 17528db4..832aae6b 100644
--- a/native/src/widget/pick_list.rs
+++ b/widget/src/pick_list.rs
@@ -1,28 +1,29 @@
//! Display a dropdown list of selectable values.
-use crate::alignment;
-use crate::event::{self, Event};
-use crate::keyboard;
-use crate::layout;
-use crate::mouse;
-use crate::overlay;
-use crate::overlay::menu::{self, Menu};
-use crate::renderer;
-use crate::text::{self, Text};
-use crate::touch;
-use crate::widget::container;
-use crate::widget::scrollable;
-use crate::widget::tree::{self, Tree};
-use crate::{
- Clipboard, Element, Layout, Length, Padding, Pixels, Point, Rectangle,
- Shell, Size, Widget,
+use crate::container;
+use crate::core::alignment;
+use crate::core::event::{self, Event};
+use crate::core::keyboard;
+use crate::core::layout;
+use crate::core::mouse;
+use crate::core::overlay;
+use crate::core::renderer;
+use crate::core::text::{self, Text};
+use crate::core::touch;
+use crate::core::widget::tree::{self, Tree};
+use crate::core::{
+ Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, Shell,
+ Size, Widget,
};
+use crate::overlay::menu::{self, Menu};
+use crate::scrollable;
+
use std::borrow::Cow;
-pub use iced_style::pick_list::{Appearance, StyleSheet};
+pub use crate::style::pick_list::{Appearance, StyleSheet};
/// A widget for selecting a single value from a list of options.
#[allow(missing_debug_implementations)]
-pub struct PickList<'a, T, Message, Renderer>
+pub struct PickList<'a, T, Message, Renderer = crate::Renderer>
where
[T]: ToOwned<Owned = Vec<T>>,
Renderer: text::Renderer,
@@ -35,7 +36,9 @@ where
width: Length,
padding: Padding,
text_size: Option<f32>,
- font: Renderer::Font,
+ text_line_height: text::LineHeight,
+ text_shaping: text::Shaping,
+ font: Option<Renderer::Font>,
handle: Handle<Renderer::Font>,
style: <Renderer::Theme as StyleSheet>::Style,
}
@@ -70,7 +73,9 @@ where
width: Length::Shrink,
padding: Self::DEFAULT_PADDING,
text_size: None,
- font: Default::default(),
+ text_line_height: text::LineHeight::default(),
+ text_shaping: text::Shaping::Basic,
+ font: None,
handle: Default::default(),
style: Default::default(),
}
@@ -100,9 +105,24 @@ where
self
}
+ /// Sets the text [`LineHeight`] of the [`PickList`].
+ pub fn text_line_height(
+ mut self,
+ line_height: impl Into<text::LineHeight>,
+ ) -> Self {
+ self.text_line_height = line_height.into();
+ self
+ }
+
+ /// Sets the [`text::Shaping`] strategy of the [`PickList`].
+ pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
+ self.text_shaping = shaping;
+ self
+ }
+
/// Sets the font of the [`PickList`].
- pub fn font(mut self, font: Renderer::Font) -> Self {
- self.font = font;
+ pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
+ self.font = Some(font.into());
self
}
@@ -137,11 +157,11 @@ where
From<<Renderer::Theme as StyleSheet>::Style>,
{
fn tag(&self) -> tree::Tag {
- tree::Tag::of::<State<T>>()
+ tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
- tree::State::new(State::<T>::new())
+ tree::State::new(State::new())
}
fn width(&self) -> Length {
@@ -163,7 +183,9 @@ where
self.width,
self.padding,
self.text_size,
- &self.font,
+ self.text_line_height,
+ self.text_shaping,
+ self.font,
self.placeholder.as_deref(),
&self.options,
)
@@ -174,7 +196,7 @@ where
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
@@ -182,12 +204,12 @@ where
update(
event,
layout,
- cursor_position,
+ cursor,
shell,
self.on_selected.as_ref(),
self.selected.as_ref(),
&self.options,
- || tree.state.downcast_mut::<State<T>>(),
+ || tree.state.downcast_mut::<State>(),
)
}
@@ -195,11 +217,11 @@ where
&self,
_tree: &Tree,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
- mouse_interaction(layout, cursor_position)
+ mouse_interaction(layout, cursor)
}
fn draw(
@@ -209,22 +231,25 @@ where
theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
+ let font = self.font.unwrap_or_else(|| renderer.default_font());
draw(
renderer,
theme,
layout,
- cursor_position,
+ cursor,
self.padding,
self.text_size,
- &self.font,
+ self.text_line_height,
+ self.text_shaping,
+ font,
self.placeholder.as_deref(),
self.selected.as_ref(),
&self.handle,
&self.style,
- || tree.state.downcast_ref::<State<T>>(),
+ || tree.state.downcast_ref::<State>(),
)
}
@@ -232,17 +257,19 @@ where
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
- _renderer: &Renderer,
+ renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> {
- let state = tree.state.downcast_mut::<State<T>>();
+ let state = tree.state.downcast_mut::<State>();
overlay(
layout,
state,
self.padding,
self.text_size,
- self.font.clone(),
+ self.text_shaping,
+ self.font.unwrap_or_else(|| renderer.default_font()),
&self.options,
+ &self.on_selected,
self.style.clone(),
)
}
@@ -269,15 +296,14 @@ where
/// The local state of a [`PickList`].
#[derive(Debug)]
-pub struct State<T> {
+pub struct State {
menu: menu::State,
keyboard_modifiers: keyboard::Modifiers,
is_open: bool,
hovered_option: Option<usize>,
- last_selection: Option<T>,
}
-impl<T> State<T> {
+impl State {
/// Creates a new [`State`] for a [`PickList`].
pub fn new() -> Self {
Self {
@@ -285,12 +311,11 @@ impl<T> State<T> {
keyboard_modifiers: keyboard::Modifiers::default(),
is_open: bool::default(),
hovered_option: Option::default(),
- last_selection: Option::default(),
}
}
}
-impl<T> Default for State<T> {
+impl Default for State {
fn default() -> Self {
Self::new()
}
@@ -334,6 +359,10 @@ pub struct Icon<Font> {
pub code_point: char,
/// Font size of the content.
pub size: Option<f32>,
+ /// Line height of the content.
+ pub line_height: text::LineHeight,
+ /// The shaping strategy of the icon.
+ pub shaping: text::Shaping,
}
/// Computes the layout of a [`PickList`].
@@ -343,7 +372,9 @@ pub fn layout<Renderer, T>(
width: Length,
padding: Padding,
text_size: Option<f32>,
- font: &Renderer::Font,
+ text_line_height: text::LineHeight,
+ text_shaping: text::Shaping,
+ font: Option<Renderer::Font>,
placeholder: Option<&str>,
options: &[T],
) -> layout::Node
@@ -359,11 +390,11 @@ where
let max_width = match width {
Length::Shrink => {
let measure = |label: &str| -> f32 {
- let (width, _) = renderer.measure(
+ let width = renderer.measure_width(
label,
text_size,
- font.clone(),
- Size::new(f32::INFINITY, f32::INFINITY),
+ font.unwrap_or_else(|| renderer.default_font()),
+ text_shaping,
);
width.round()
@@ -383,8 +414,10 @@ where
};
let size = {
- let intrinsic =
- Size::new(max_width + text_size + padding.left, text_size);
+ let intrinsic = Size::new(
+ max_width + text_size + padding.left,
+ f32::from(text_line_height.to_absolute(Pixels(text_size))),
+ );
limits.resolve(intrinsic).pad(padding)
};
@@ -397,12 +430,12 @@ where
pub fn update<'a, T, Message>(
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
shell: &mut Shell<'_, Message>,
on_selected: &dyn Fn(T) -> Message,
selected: Option<&T>,
options: &[T],
- state: impl FnOnce() -> &'a mut State<T>,
+ state: impl FnOnce() -> &'a mut State,
) -> event::Status
where
T: PartialEq + Clone + 'a,
@@ -412,13 +445,13 @@ where
| Event::Touch(touch::Event::FingerPressed { .. }) => {
let state = state();
- let event_status = if state.is_open {
+ if state.is_open {
// Event wasn't processed by overlay, so cursor was clicked either outside it's
// bounds or on the drop-down, either way we close the overlay.
state.is_open = false;
event::Status::Captured
- } else if layout.bounds().contains(cursor_position) {
+ } else if cursor.is_over(layout.bounds()) {
state.is_open = true;
state.hovered_option =
options.iter().position(|option| Some(option) == selected);
@@ -426,16 +459,6 @@ where
event::Status::Captured
} else {
event::Status::Ignored
- };
-
- if let Some(last_selection) = state.last_selection.take() {
- shell.publish((on_selected)(last_selection));
-
- state.is_open = false;
-
- event::Status::Captured
- } else {
- event_status
}
}
Event::Mouse(mouse::Event::WheelScrolled {
@@ -444,7 +467,7 @@ where
let state = state();
if state.keyboard_modifiers.command()
- && layout.bounds().contains(cursor_position)
+ && cursor.is_over(layout.bounds())
&& !state.is_open
{
fn find_next<'a, T: PartialEq>(
@@ -495,10 +518,10 @@ where
/// Returns the current [`mouse::Interaction`] of a [`PickList`].
pub fn mouse_interaction(
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
) -> mouse::Interaction {
let bounds = layout.bounds();
- let is_mouse_over = bounds.contains(cursor_position);
+ let is_mouse_over = cursor.is_over(bounds);
if is_mouse_over {
mouse::Interaction::Pointer
@@ -510,11 +533,13 @@ pub fn mouse_interaction(
/// Returns the current overlay of a [`PickList`].
pub fn overlay<'a, T, Message, Renderer>(
layout: Layout<'_>,
- state: &'a mut State<T>,
+ state: &'a mut State,
padding: Padding,
text_size: Option<f32>,
+ text_shaping: text::Shaping,
font: Renderer::Font,
options: &'a [T],
+ on_selected: &'a dyn Fn(T) -> Message,
style: <Renderer::Theme as StyleSheet>::Style,
) -> Option<overlay::Element<'a, Message, Renderer>>
where
@@ -535,11 +560,16 @@ where
&mut state.menu,
options,
&mut state.hovered_option,
- &mut state.last_selection,
+ |option| {
+ state.is_open = false;
+
+ (on_selected)(option)
+ },
)
.width(bounds.width)
.padding(padding)
.font(font)
+ .text_shaping(text_shaping)
.style(style);
if let Some(text_size) = text_size {
@@ -557,22 +587,24 @@ pub fn draw<'a, T, Renderer>(
renderer: &mut Renderer,
theme: &Renderer::Theme,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
padding: Padding,
text_size: Option<f32>,
- font: &Renderer::Font,
+ text_line_height: text::LineHeight,
+ text_shaping: text::Shaping,
+ font: Renderer::Font,
placeholder: Option<&str>,
selected: Option<&T>,
handle: &Handle<Renderer::Font>,
style: &<Renderer::Theme as StyleSheet>::Style,
- state: impl FnOnce() -> &'a State<T>,
+ state: impl FnOnce() -> &'a State,
) where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
T: ToString + 'a,
{
let bounds = layout.bounds();
- let is_mouse_over = bounds.contains(cursor_position);
+ let is_mouse_over = cursor.is_over(bounds);
let is_selected = selected.is_some();
let style = if is_mouse_over {
@@ -586,46 +618,66 @@ pub fn draw<'a, T, Renderer>(
bounds,
border_color: style.border_color,
border_width: style.border_width,
- border_radius: style.border_radius.into(),
+ border_radius: style.border_radius,
},
style.background,
);
let handle = match handle {
- Handle::Arrow { size } => {
- Some((Renderer::ICON_FONT, Renderer::ARROW_DOWN_ICON, *size))
- }
+ Handle::Arrow { size } => Some((
+ Renderer::ICON_FONT,
+ Renderer::ARROW_DOWN_ICON,
+ *size,
+ text::LineHeight::default(),
+ text::Shaping::Basic,
+ )),
Handle::Static(Icon {
font,
code_point,
size,
- }) => Some((font.clone(), *code_point, *size)),
+ line_height,
+ shaping,
+ }) => Some((*font, *code_point, *size, *line_height, *shaping)),
Handle::Dynamic { open, closed } => {
if state().is_open {
- Some((open.font.clone(), open.code_point, open.size))
+ Some((
+ open.font,
+ open.code_point,
+ open.size,
+ open.line_height,
+ open.shaping,
+ ))
} else {
- Some((closed.font.clone(), closed.code_point, closed.size))
+ Some((
+ closed.font,
+ closed.code_point,
+ closed.size,
+ closed.line_height,
+ closed.shaping,
+ ))
}
}
Handle::None => None,
};
- if let Some((font, code_point, size)) = handle {
+ if let Some((font, code_point, size, line_height, shaping)) = handle {
let size = size.unwrap_or_else(|| renderer.default_size());
renderer.fill_text(Text {
content: &code_point.to_string(),
size,
+ line_height,
font,
color: style.handle_color,
bounds: Rectangle {
x: bounds.x + bounds.width - padding.horizontal(),
- y: bounds.center_y() - size / 2.0,
- height: size,
+ y: bounds.center_y(),
+ height: f32::from(line_height.to_absolute(Pixels(size))),
..bounds
},
horizontal_alignment: alignment::Horizontal::Right,
- vertical_alignment: alignment::Vertical::Top,
+ vertical_alignment: alignment::Vertical::Center,
+ shaping,
});
}
@@ -637,7 +689,8 @@ pub fn draw<'a, T, Renderer>(
renderer.fill_text(Text {
content: label,
size: text_size,
- font: font.clone(),
+ line_height: text_line_height,
+ font,
color: if is_selected {
style.text_color
} else {
@@ -645,12 +698,15 @@ pub fn draw<'a, T, Renderer>(
},
bounds: Rectangle {
x: bounds.x + padding.left,
- y: bounds.center_y() - text_size / 2.0,
+ y: bounds.center_y(),
width: bounds.width - padding.horizontal(),
- height: text_size,
+ height: f32::from(
+ text_line_height.to_absolute(Pixels(text_size)),
+ ),
},
horizontal_alignment: alignment::Horizontal::Left,
- vertical_alignment: alignment::Vertical::Top,
+ vertical_alignment: alignment::Vertical::Center,
+ shaping: text_shaping,
});
}
}
diff --git a/native/src/widget/progress_bar.rs b/widget/src/progress_bar.rs
index dd46fa76..37c6bc72 100644
--- a/native/src/widget/progress_bar.rs
+++ b/widget/src/progress_bar.rs
@@ -1,8 +1,9 @@
//! Provide progress feedback to your users.
-use crate::layout;
-use crate::renderer;
-use crate::widget::Tree;
-use crate::{Color, Element, Layout, Length, Point, Rectangle, Size, Widget};
+use crate::core::layout;
+use crate::core::mouse;
+use crate::core::renderer;
+use crate::core::widget::Tree;
+use crate::core::{Color, Element, Layout, Length, Rectangle, Size, Widget};
use std::ops::RangeInclusive;
@@ -11,8 +12,10 @@ pub use iced_style::progress_bar::{Appearance, StyleSheet};
/// A bar that displays progress.
///
/// # Example
-/// ```
-/// # type ProgressBar = iced_native::widget::ProgressBar<iced_native::renderer::Null>;
+/// ```no_run
+/// # type ProgressBar =
+/// # iced_widget::ProgressBar<iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
+/// #
/// let value = 50.0;
///
/// ProgressBar::new(0.0..=100.0, value);
@@ -20,9 +23,9 @@ pub use iced_style::progress_bar::{Appearance, StyleSheet};
///
/// ![Progress bar drawn with `iced_wgpu`](https://user-images.githubusercontent.com/18618951/71662391-a316c200-2d51-11ea-9cef-52758cab85e3.png)
#[allow(missing_debug_implementations)]
-pub struct ProgressBar<Renderer>
+pub struct ProgressBar<Renderer = crate::Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
range: RangeInclusive<f32>,
@@ -34,7 +37,7 @@ where
impl<Renderer> ProgressBar<Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
/// The default height of a [`ProgressBar`].
@@ -79,7 +82,7 @@ where
impl<Message, Renderer> Widget<Message, Renderer> for ProgressBar<Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
fn width(&self) -> Length {
@@ -111,7 +114,7 @@ where
theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
- _cursor_position: Point,
+ _cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
let bounds = layout.bounds();
@@ -129,7 +132,7 @@ where
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle { ..bounds },
- border_radius: style.border_radius.into(),
+ border_radius: style.border_radius,
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
@@ -143,7 +146,7 @@ where
width: active_progress_width,
..bounds
},
- border_radius: style.border_radius.into(),
+ border_radius: style.border_radius,
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
@@ -157,7 +160,7 @@ impl<'a, Message, Renderer> From<ProgressBar<Renderer>>
for Element<'a, Message, Renderer>
where
Message: 'a,
- Renderer: 'a + crate::Renderer,
+ Renderer: 'a + crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
fn from(
diff --git a/graphics/src/widget/qr_code.rs b/widget/src/qr_code.rs
index 12ce5b1f..51a541fd 100644
--- a/graphics/src/widget/qr_code.rs
+++ b/widget/src/qr_code.rs
@@ -1,13 +1,14 @@
//! Encode and display information in a QR code.
-use crate::renderer::{self, Renderer};
-use crate::widget::canvas;
-use crate::Backend;
-
-use iced_native::layout;
-use iced_native::widget::Tree;
-use iced_native::{
+use crate::canvas;
+use crate::core::layout;
+use crate::core::mouse;
+use crate::core::renderer::{self, Renderer as _};
+use crate::core::widget::Tree;
+use crate::core::{
Color, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget,
};
+use crate::graphics::geometry::Renderer as _;
+use crate::Renderer;
use thiserror::Error;
const DEFAULT_CELL_SIZE: u16 = 4;
@@ -48,10 +49,7 @@ impl<'a> QRCode<'a> {
}
}
-impl<'a, Message, B, T> Widget<Message, Renderer<B, T>> for QRCode<'a>
-where
- B: Backend,
-{
+impl<'a, Message, Theme> Widget<Message, Renderer<Theme>> for QRCode<'a> {
fn width(&self) -> Length {
Length::Shrink
}
@@ -62,7 +60,7 @@ where
fn layout(
&self,
- _renderer: &Renderer<B, T>,
+ _renderer: &Renderer<Theme>,
_limits: &layout::Limits,
) -> layout::Node {
let side_length = (self.state.width + 2 * QUIET_ZONE) as f32
@@ -74,63 +72,63 @@ where
fn draw(
&self,
_state: &Tree,
- renderer: &mut Renderer<B, T>,
- _theme: &T,
+ renderer: &mut Renderer<Theme>,
+ _theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
- _cursor_position: Point,
+ _cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
- use iced_native::Renderer as _;
-
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,
- );
- });
- });
+ let geometry =
+ self.state.cache.draw(renderer, 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,
+ );
+ });
+ });
let translation = Vector::new(bounds.x, bounds.y);
renderer.with_translation(translation, |renderer| {
- renderer.draw_primitive(geometry.into_primitive());
+ renderer.draw(vec![geometry]);
});
}
}
-impl<'a, Message, B, T> From<QRCode<'a>>
- for Element<'a, Message, Renderer<B, T>>
-where
- B: Backend,
+impl<'a, Message, Theme> From<QRCode<'a>>
+ for Element<'a, Message, Renderer<Theme>>
{
fn from(qr_code: QRCode<'a>) -> Self {
Self::new(qr_code)
diff --git a/native/src/widget/radio.rs b/widget/src/radio.rs
index 9daddfbc..5b883147 100644
--- a/native/src/widget/radio.rs
+++ b/widget/src/radio.rs
@@ -1,30 +1,34 @@
//! Create choices using radio buttons.
-use crate::alignment;
-use crate::event::{self, Event};
-use crate::layout;
-use crate::mouse;
-use crate::renderer;
-use crate::text;
-use crate::touch;
-use crate::widget::{self, Row, Text, Tree};
-use crate::{
- Alignment, Clipboard, Color, Element, Layout, Length, Pixels, Point,
- Rectangle, Shell, Widget,
+use crate::core::alignment;
+use crate::core::event::{self, Event};
+use crate::core::layout;
+use crate::core::mouse;
+use crate::core::renderer;
+use crate::core::text;
+use crate::core::touch;
+use crate::core::widget::Tree;
+use crate::core::{
+ Alignment, Clipboard, Color, Element, Layout, Length, Pixels, Rectangle,
+ Shell, Widget,
};
+use crate::{Row, Text};
pub use iced_style::radio::{Appearance, StyleSheet};
/// A circular button representing a choice.
///
/// # Example
-/// ```
+/// ```no_run
/// # type Radio<Message> =
-/// # iced_native::widget::Radio<Message, iced_native::renderer::Null>;
+/// # iced_widget::Radio<Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
/// #
+/// # use iced_widget::column;
/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// pub enum Choice {
/// A,
/// B,
+/// C,
+/// All,
/// }
///
/// #[derive(Debug, Clone, Copy)]
@@ -34,14 +38,38 @@ pub use iced_style::radio::{Appearance, StyleSheet};
///
/// let selected_choice = Some(Choice::A);
///
-/// Radio::new(Choice::A, "This is A", selected_choice, Message::RadioSelected);
+/// let a = Radio::new(
+/// "A",
+/// Choice::A,
+/// selected_choice,
+/// Message::RadioSelected,
+/// );
///
-/// Radio::new(Choice::B, "This is B", selected_choice, Message::RadioSelected);
-/// ```
+/// let b = Radio::new(
+/// "B",
+/// Choice::B,
+/// selected_choice,
+/// Message::RadioSelected,
+/// );
+///
+/// let c = Radio::new(
+/// "C",
+/// Choice::C,
+/// selected_choice,
+/// Message::RadioSelected,
+/// );
+///
+/// let all = Radio::new(
+/// "All of the above",
+/// Choice::All,
+/// selected_choice,
+/// Message::RadioSelected
+/// );
///
-/// ![Radio buttons drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/radio.png?raw=true)
+/// let content = column![a, b, c, all];
+/// ```
#[allow(missing_debug_implementations)]
-pub struct Radio<Message, Renderer>
+pub struct Radio<Message, Renderer = crate::Renderer>
where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
@@ -53,7 +81,9 @@ where
size: f32,
spacing: f32,
text_size: Option<f32>,
- font: Renderer::Font,
+ text_line_height: text::LineHeight,
+ text_shaping: text::Shaping,
+ font: Option<Renderer::Font>,
style: <Renderer::Theme as StyleSheet>::Style,
}
@@ -78,8 +108,8 @@ where
/// * a function that will be called when the [`Radio`] is selected. It
/// receives the value of the radio and must produce a `Message`.
pub fn new<F, V>(
- value: V,
label: impl Into<String>,
+ value: V,
selected: Option<V>,
f: F,
) -> Self
@@ -95,7 +125,9 @@ where
size: Self::DEFAULT_SIZE,
spacing: Self::DEFAULT_SPACING, //15
text_size: None,
- font: Default::default(),
+ text_line_height: text::LineHeight::default(),
+ text_shaping: text::Shaping::Basic,
+ font: None,
style: Default::default(),
}
}
@@ -124,9 +156,24 @@ where
self
}
+ /// Sets the text [`LineHeight`] of the [`Radio`] button.
+ pub fn text_line_height(
+ mut self,
+ line_height: impl Into<text::LineHeight>,
+ ) -> Self {
+ self.text_line_height = line_height.into();
+ self
+ }
+
+ /// Sets the [`text::Shaping`] strategy of the [`Radio`] button.
+ pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
+ self.text_shaping = shaping;
+ self
+ }
+
/// Sets the text font of the [`Radio`] button.
- pub fn font(mut self, font: Renderer::Font) -> Self {
- self.font = font;
+ pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
+ self.font = Some(font.into());
self
}
@@ -144,7 +191,7 @@ impl<Message, Renderer> Widget<Message, Renderer> for Radio<Message, Renderer>
where
Message: Clone,
Renderer: text::Renderer,
- Renderer::Theme: StyleSheet + widget::text::StyleSheet,
+ Renderer::Theme: StyleSheet + crate::text::StyleSheet,
{
fn width(&self) -> Length {
self.width
@@ -164,9 +211,16 @@ where
.spacing(self.spacing)
.align_items(Alignment::Center)
.push(Row::new().width(self.size).height(self.size))
- .push(Text::new(&self.label).width(self.width).size(
- self.text_size.unwrap_or_else(|| renderer.default_size()),
- ))
+ .push(
+ Text::new(&self.label)
+ .width(self.width)
+ .size(
+ self.text_size
+ .unwrap_or_else(|| renderer.default_size()),
+ )
+ .line_height(self.text_line_height)
+ .shaping(self.text_shaping),
+ )
.layout(renderer, limits)
}
@@ -175,7 +229,7 @@ where
_state: &mut Tree,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
@@ -183,7 +237,7 @@ where
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
- if layout.bounds().contains(cursor_position) {
+ if cursor.is_over(layout.bounds()) {
shell.publish(self.on_click.clone());
return event::Status::Captured;
@@ -199,11 +253,11 @@ where
&self,
_state: &Tree,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
- if layout.bounds().contains(cursor_position) {
+ if cursor.is_over(layout.bounds()) {
mouse::Interaction::Pointer
} else {
mouse::Interaction::default()
@@ -217,11 +271,10 @@ where
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
- let bounds = layout.bounds();
- let is_mouse_over = bounds.contains(cursor_position);
+ let is_mouse_over = cursor.is_over(layout.bounds());
let mut children = layout.children();
@@ -269,18 +322,20 @@ where
{
let label_layout = children.next().unwrap();
- widget::text::draw(
+ crate::text::draw(
renderer,
style,
label_layout,
&self.label,
self.text_size,
- self.font.clone(),
- widget::text::Appearance {
+ self.text_line_height,
+ self.font,
+ crate::text::Appearance {
color: custom_style.text_color,
},
alignment::Horizontal::Left,
alignment::Vertical::Center,
+ self.text_shaping,
);
}
}
@@ -291,7 +346,7 @@ impl<'a, Message, Renderer> From<Radio<Message, Renderer>>
where
Message: 'a + Clone,
Renderer: 'a + text::Renderer,
- Renderer::Theme: StyleSheet + widget::text::StyleSheet,
+ Renderer::Theme: StyleSheet + crate::text::StyleSheet,
{
fn from(radio: Radio<Message, Renderer>) -> Element<'a, Message, Renderer> {
Element::new(radio)
diff --git a/native/src/widget/row.rs b/widget/src/row.rs
index 286c1c2d..1db22416 100644
--- a/native/src/widget/row.rs
+++ b/widget/src/row.rs
@@ -1,18 +1,18 @@
//! Distribute content horizontally.
-use crate::event::{self, Event};
-use crate::layout::{self, Layout};
-use crate::mouse;
-use crate::overlay;
-use crate::renderer;
-use crate::widget::{Operation, Tree};
-use crate::{
- Alignment, Clipboard, Element, Length, Padding, Pixels, Point, Rectangle,
- Shell, Widget,
+use crate::core::event::{self, Event};
+use crate::core::layout::{self, Layout};
+use crate::core::mouse;
+use crate::core::overlay;
+use crate::core::renderer;
+use crate::core::widget::{Operation, Tree};
+use crate::core::{
+ Alignment, Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell,
+ Widget,
};
/// A container that distributes its contents horizontally.
#[allow(missing_debug_implementations)]
-pub struct Row<'a, Message, Renderer> {
+pub struct Row<'a, Message, Renderer = crate::Renderer> {
spacing: f32,
padding: Padding,
width: Length,
@@ -94,7 +94,7 @@ impl<'a, Message, Renderer> Default for Row<'a, Message, Renderer> {
impl<'a, Message, Renderer> Widget<Message, Renderer>
for Row<'a, Message, Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
{
fn children(&self) -> Vec<Tree> {
self.children.iter().map(Tree::new).collect()
@@ -155,7 +155,7 @@ where
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
@@ -169,7 +169,7 @@ where
state,
event.clone(),
layout,
- cursor_position,
+ cursor,
renderer,
clipboard,
shell,
@@ -182,7 +182,7 @@ where
&self,
tree: &Tree,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
@@ -192,11 +192,7 @@ where
.zip(layout.children())
.map(|((child, state), layout)| {
child.as_widget().mouse_interaction(
- state,
- layout,
- cursor_position,
- viewport,
- renderer,
+ state, layout, cursor, viewport, renderer,
)
})
.max()
@@ -210,7 +206,7 @@ where
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
) {
for ((child, state), layout) in self
@@ -219,15 +215,9 @@ where
.zip(&tree.children)
.zip(layout.children())
{
- child.as_widget().draw(
- state,
- renderer,
- theme,
- style,
- layout,
- cursor_position,
- viewport,
- );
+ child
+ .as_widget()
+ .draw(state, renderer, theme, style, layout, cursor, viewport);
}
}
@@ -245,7 +235,7 @@ impl<'a, Message, Renderer> From<Row<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Message: 'a,
- Renderer: crate::Renderer + 'a,
+ Renderer: crate::core::Renderer + 'a,
{
fn from(row: Row<'a, Message, Renderer>) -> Self {
Self::new(row)
diff --git a/native/src/widget/rule.rs b/widget/src/rule.rs
index 1ab6a0d3..d703e6ae 100644
--- a/native/src/widget/rule.rs
+++ b/widget/src/rule.rs
@@ -1,18 +1,19 @@
//! Display a horizontal or vertical rule for dividing content.
-use crate::layout;
-use crate::renderer;
-use crate::widget::Tree;
-use crate::{
- Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Widget,
+use crate::core::layout;
+use crate::core::mouse;
+use crate::core::renderer;
+use crate::core::widget::Tree;
+use crate::core::{
+ Color, Element, Layout, Length, Pixels, Rectangle, Size, Widget,
};
-pub use iced_style::rule::{Appearance, FillMode, StyleSheet};
+pub use crate::style::rule::{Appearance, FillMode, StyleSheet};
/// Display a horizontal or vertical rule for dividing content.
#[allow(missing_debug_implementations)]
-pub struct Rule<Renderer>
+pub struct Rule<Renderer = crate::Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
width: Length,
@@ -23,7 +24,7 @@ where
impl<Renderer> Rule<Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
/// Creates a horizontal [`Rule`] with the given height.
@@ -58,7 +59,7 @@ where
impl<Message, Renderer> Widget<Message, Renderer> for Rule<Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
fn width(&self) -> Length {
@@ -86,7 +87,7 @@ where
theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
- _cursor_position: Point,
+ _cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
let bounds = layout.bounds();
@@ -125,7 +126,7 @@ where
renderer.fill_quad(
renderer::Quad {
bounds,
- border_radius: style.radius.into(),
+ border_radius: style.radius,
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
@@ -138,7 +139,7 @@ impl<'a, Message, Renderer> From<Rule<Renderer>>
for Element<'a, Message, Renderer>
where
Message: 'a,
- Renderer: 'a + crate::Renderer,
+ Renderer: 'a + crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
fn from(rule: Rule<Renderer>) -> Element<'a, Message, Renderer> {
diff --git a/native/src/widget/scrollable.rs b/widget/src/scrollable.rs
index c1df8c39..88746ac4 100644
--- a/native/src/widget/scrollable.rs
+++ b/widget/src/scrollable.rs
@@ -1,58 +1,52 @@
//! Navigate an endless amount of content with a scrollbar.
-use crate::event::{self, Event};
-use crate::keyboard;
-use crate::layout;
-use crate::mouse;
-use crate::overlay;
-use crate::renderer;
-use crate::touch;
-use crate::widget;
-use crate::widget::operation::{self, Operation};
-use crate::widget::tree::{self, Tree};
-use crate::{
- Background, Clipboard, Color, Command, Element, Layout, Length, Pixels,
- Point, Rectangle, Shell, Size, Vector, Widget,
+use crate::core::event::{self, Event};
+use crate::core::keyboard;
+use crate::core::layout;
+use crate::core::mouse;
+use crate::core::overlay;
+use crate::core::renderer;
+use crate::core::touch;
+use crate::core::widget;
+use crate::core::widget::operation::{self, Operation};
+use crate::core::widget::tree::{self, Tree};
+use crate::core::{
+ Background, Clipboard, Color, Element, Layout, Length, Pixels, Point,
+ Rectangle, Shell, Size, Vector, Widget,
};
+use crate::runtime::Command;
-pub use iced_style::scrollable::StyleSheet;
-pub use operation::scrollable::RelativeOffset;
-
-pub mod style {
- //! The styles of a [`Scrollable`].
- //!
- //! [`Scrollable`]: crate::widget::Scrollable
- pub use iced_style::scrollable::{Scrollbar, Scroller};
-}
+pub use crate::style::scrollable::{Scrollbar, Scroller, StyleSheet};
+pub use operation::scrollable::{AbsoluteOffset, RelativeOffset};
/// A widget that can vertically display an infinite amount of content with a
/// scrollbar.
#[allow(missing_debug_implementations)]
-pub struct Scrollable<'a, Message, Renderer>
+pub struct Scrollable<'a, Message, Renderer = crate::Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
id: Option<Id>,
+ width: Length,
height: Length,
- vertical: Properties,
- horizontal: Option<Properties>,
+ direction: Direction,
content: Element<'a, Message, Renderer>,
- on_scroll: Option<Box<dyn Fn(RelativeOffset) -> Message + 'a>>,
+ on_scroll: Option<Box<dyn Fn(Viewport) -> Message + 'a>>,
style: <Renderer::Theme as StyleSheet>::Style,
}
impl<'a, Message, Renderer> Scrollable<'a, Message, Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
/// Creates a new [`Scrollable`].
pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self {
Scrollable {
id: None,
+ width: Length::Shrink,
height: Length::Shrink,
- vertical: Properties::default(),
- horizontal: None,
+ direction: Default::default(),
content: content.into(),
on_scroll: None,
style: Default::default(),
@@ -65,32 +59,28 @@ where
self
}
- /// Sets the height of the [`Scrollable`].
- pub fn height(mut self, height: impl Into<Length>) -> Self {
- self.height = height.into();
+ /// Sets the width of the [`Scrollable`].
+ pub fn width(mut self, width: impl Into<Length>) -> Self {
+ self.width = width.into();
self
}
- /// Configures the vertical scrollbar of the [`Scrollable`] .
- pub fn vertical_scroll(mut self, properties: Properties) -> Self {
- self.vertical = properties;
+ /// Sets the height of the [`Scrollable`].
+ pub fn height(mut self, height: impl Into<Length>) -> Self {
+ self.height = height.into();
self
}
- /// Configures the horizontal scrollbar of the [`Scrollable`] .
- pub fn horizontal_scroll(mut self, properties: Properties) -> Self {
- self.horizontal = Some(properties);
+ /// Sets the [`Direction`] of the [`Scrollable`] .
+ pub fn direction(mut self, direction: Direction) -> Self {
+ self.direction = direction;
self
}
/// Sets a function to call when the [`Scrollable`] is scrolled.
///
- /// The function takes the new relative x & y offset of the [`Scrollable`]
- /// (e.g. `0` means beginning, while `1` means end).
- pub fn on_scroll(
- mut self,
- f: impl Fn(RelativeOffset) -> Message + 'a,
- ) -> Self {
+ /// The function takes the [`Viewport`] of the [`Scrollable`]
+ pub fn on_scroll(mut self, f: impl Fn(Viewport) -> Message + 'a) -> Self {
self.on_scroll = Some(Box::new(f));
self
}
@@ -105,12 +95,55 @@ where
}
}
+/// The direction of [`Scrollable`].
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum Direction {
+ /// Vertical scrolling
+ Vertical(Properties),
+ /// Horizontal scrolling
+ Horizontal(Properties),
+ /// Both vertical and horizontal scrolling
+ Both {
+ /// The properties of the vertical scrollbar.
+ vertical: Properties,
+ /// The properties of the horizontal scrollbar.
+ horizontal: Properties,
+ },
+}
+
+impl Direction {
+ /// Returns the [`Properties`] of the horizontal scrollbar, if any.
+ pub fn horizontal(&self) -> Option<&Properties> {
+ match self {
+ Self::Horizontal(properties) => Some(properties),
+ Self::Both { horizontal, .. } => Some(horizontal),
+ _ => None,
+ }
+ }
+
+ /// Returns the [`Properties`] of the vertical scrollbar, if any.
+ pub fn vertical(&self) -> Option<&Properties> {
+ match self {
+ Self::Vertical(properties) => Some(properties),
+ Self::Both { vertical, .. } => Some(vertical),
+ _ => None,
+ }
+ }
+}
+
+impl Default for Direction {
+ fn default() -> Self {
+ Self::Vertical(Properties::default())
+ }
+}
+
/// Properties of a scrollbar within a [`Scrollable`].
-#[derive(Debug)]
+#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Properties {
width: f32,
margin: f32,
scroller_width: f32,
+ alignment: Alignment,
}
impl Default for Properties {
@@ -119,6 +152,7 @@ impl Default for Properties {
width: 10.0,
margin: 0.0,
scroller_width: 10.0,
+ alignment: Alignment::Start,
}
}
}
@@ -130,9 +164,8 @@ impl Properties {
}
/// Sets the scrollbar width of the [`Scrollable`] .
- /// Silently enforces a minimum width of 1.
pub fn width(mut self, width: impl Into<Pixels>) -> Self {
- self.width = width.into().0.max(1.0);
+ self.width = width.into().0.max(0.0);
self
}
@@ -143,17 +176,32 @@ impl Properties {
}
/// Sets the scroller width of the [`Scrollable`] .
- /// Silently enforces a minimum width of 1.
pub fn scroller_width(mut self, scroller_width: impl Into<Pixels>) -> Self {
- self.scroller_width = scroller_width.into().0.max(1.0);
+ self.scroller_width = scroller_width.into().0.max(0.0);
+ self
+ }
+
+ /// Sets the alignment of the [`Scrollable`] .
+ pub fn alignment(mut self, alignment: Alignment) -> Self {
+ self.alignment = alignment;
self
}
}
+/// Alignment of the scrollable's content relative to it's [`Viewport`] in one direction.
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
+pub enum Alignment {
+ /// Content is aligned to the start of the [`Viewport`].
+ #[default]
+ Start,
+ /// Content is aligned to the end of the [`Viewport`]
+ End,
+}
+
impl<'a, Message, Renderer> Widget<Message, Renderer>
for Scrollable<'a, Message, Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
fn tag(&self) -> tree::Tag {
@@ -173,7 +221,7 @@ where
}
fn width(&self) -> Length {
- self.content.as_widget().width()
+ self.width
}
fn height(&self) -> Length {
@@ -188,9 +236,9 @@ where
layout(
renderer,
limits,
- Widget::<Message, Renderer>::width(self),
+ self.width,
self.height,
- self.horizontal.is_some(),
+ &self.direction,
|renderer, limits| {
self.content.as_widget().layout(renderer, limits)
},
@@ -226,7 +274,7 @@ where
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
@@ -235,18 +283,17 @@ where
tree.state.downcast_mut::<State>(),
event,
layout,
- cursor_position,
+ cursor,
clipboard,
shell,
- &self.vertical,
- self.horizontal.as_ref(),
+ self.direction,
&self.on_scroll,
- |event, layout, cursor_position, clipboard, shell| {
+ |event, layout, cursor, clipboard, shell| {
self.content.as_widget_mut().on_event(
&mut tree.children[0],
event,
layout,
- cursor_position,
+ cursor,
renderer,
clipboard,
shell,
@@ -262,7 +309,7 @@ where
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
draw(
@@ -270,18 +317,17 @@ where
renderer,
theme,
layout,
- cursor_position,
- &self.vertical,
- self.horizontal.as_ref(),
+ cursor,
+ self.direction,
&self.style,
- |renderer, layout, cursor_position, viewport| {
+ |renderer, layout, cursor, viewport| {
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
style,
layout,
- cursor_position,
+ cursor,
viewport,
)
},
@@ -292,21 +338,20 @@ where
&self,
tree: &Tree,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
mouse_interaction(
tree.state.downcast_ref::<State>(),
layout,
- cursor_position,
- &self.vertical,
- self.horizontal.as_ref(),
- |layout, cursor_position, viewport| {
+ cursor,
+ self.direction,
+ |layout, cursor, viewport| {
self.content.as_widget().mouse_interaction(
&tree.children[0],
layout,
- cursor_position,
+ cursor,
viewport,
renderer,
)
@@ -331,12 +376,12 @@ where
let bounds = layout.bounds();
let content_layout = layout.children().next().unwrap();
let content_bounds = content_layout.bounds();
- let offset = tree
+ let translation = tree
.state
.downcast_ref::<State>()
- .offset(bounds, content_bounds);
+ .translation(self.direction, bounds, content_bounds);
- overlay.translate(Vector::new(-offset.x, -offset.y))
+ overlay.translate(Vector::new(-translation.x, -translation.y))
})
}
}
@@ -345,7 +390,7 @@ impl<'a, Message, Renderer> From<Scrollable<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Message: 'a,
- Renderer: 'a + crate::Renderer,
+ Renderer: 'a + crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
fn from(
@@ -388,34 +433,39 @@ pub fn snap_to<Message: 'static>(
Command::widget(operation::scrollable::snap_to(id.0, offset))
}
+/// Produces a [`Command`] that scrolls the [`Scrollable`] with the given [`Id`]
+/// to the provided [`AbsoluteOffset`] along the x & y axis.
+pub fn scroll_to<Message: 'static>(
+ id: Id,
+ offset: AbsoluteOffset,
+) -> Command<Message> {
+ Command::widget(operation::scrollable::scroll_to(id.0, offset))
+}
+
/// Computes the layout of a [`Scrollable`].
pub fn layout<Renderer>(
renderer: &Renderer,
limits: &layout::Limits,
width: Length,
height: Length,
- horizontal_enabled: bool,
+ direction: &Direction,
layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
) -> layout::Node {
- let limits = limits
- .max_height(f32::INFINITY)
- .max_width(if horizontal_enabled {
- f32::INFINITY
- } else {
- limits.max().width
- })
- .width(width)
- .height(height);
+ let limits = limits.width(width).height(height);
let child_limits = layout::Limits::new(
- Size::new(limits.min().width, 0.0),
+ Size::new(limits.min().width, limits.min().height),
Size::new(
- if horizontal_enabled {
+ if direction.horizontal().is_some() {
f32::INFINITY
} else {
limits.max().width
},
- f32::MAX,
+ if direction.vertical().is_some() {
+ f32::MAX
+ } else {
+ limits.max().height
+ },
),
);
@@ -431,52 +481,44 @@ pub fn update<Message>(
state: &mut State,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
- vertical: &Properties,
- horizontal: Option<&Properties>,
- on_scroll: &Option<Box<dyn Fn(RelativeOffset) -> Message + '_>>,
+ direction: Direction,
+ on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>,
update_content: impl FnOnce(
Event,
Layout<'_>,
- Point,
+ mouse::Cursor,
&mut dyn Clipboard,
&mut Shell<'_, Message>,
) -> event::Status,
) -> event::Status {
let bounds = layout.bounds();
- let mouse_over_scrollable = bounds.contains(cursor_position);
+ let cursor_over_scrollable = cursor.position_over(bounds);
let content = layout.children().next().unwrap();
let content_bounds = content.bounds();
- let scrollbars =
- Scrollbars::new(state, vertical, horizontal, bounds, content_bounds);
+ let scrollbars = Scrollbars::new(state, direction, bounds, content_bounds);
let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
- scrollbars.is_mouse_over(cursor_position);
+ scrollbars.is_mouse_over(cursor);
let event_status = {
- let cursor_position = if mouse_over_scrollable
- && !(mouse_over_y_scrollbar || mouse_over_x_scrollbar)
- {
- cursor_position + state.offset(bounds, content_bounds)
- } 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(-1.0, -1.0)
+ let cursor = match cursor_over_scrollable {
+ Some(cursor_position)
+ if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
+ {
+ mouse::Cursor::Available(
+ cursor_position
+ + state.translation(direction, bounds, content_bounds),
+ )
+ }
+ _ => mouse::Cursor::Unavailable,
};
- update_content(
- event.clone(),
- content,
- cursor_position,
- clipboard,
- shell,
- )
+ update_content(event.clone(), content, cursor, clipboard, shell)
};
if let event::Status::Captured = event_status {
@@ -490,76 +532,79 @@ pub fn update<Message>(
return event::Status::Ignored;
}
- if mouse_over_scrollable {
- match event {
- Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
- let delta = match delta {
- mouse::ScrollDelta::Lines { x, y } => {
- // TODO: Configurable speed/friction (?)
- let movement = if state.keyboard_modifiers.shift() {
- Vector::new(y, x)
- } else {
- Vector::new(x, y)
- };
+ match event {
+ Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
+ if cursor_over_scrollable.is_none() {
+ return event::Status::Ignored;
+ }
- movement * 60.0
- }
- mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y),
- };
+ let delta = match delta {
+ mouse::ScrollDelta::Lines { x, y } => {
+ // TODO: Configurable speed/friction (?)
+ let movement = if state.keyboard_modifiers.shift() {
+ Vector::new(y, x)
+ } else {
+ Vector::new(x, y)
+ };
- state.scroll(delta, bounds, content_bounds);
+ movement * 60.0
+ }
+ mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y),
+ };
- notify_on_scroll(
- state,
- on_scroll,
- bounds,
- content_bounds,
- shell,
- );
+ state.scroll(delta, direction, bounds, content_bounds);
+
+ notify_on_scroll(state, on_scroll, bounds, content_bounds, shell);
+
+ return event::Status::Captured;
+ }
+ Event::Touch(event)
+ if state.scroll_area_touched_at.is_some()
+ || !mouse_over_y_scrollbar && !mouse_over_x_scrollbar =>
+ {
+ match event {
+ touch::Event::FingerPressed { .. } => {
+ let Some(cursor_position) = cursor.position() else {
+ return event::Status::Ignored
+ };
+
+ state.scroll_area_touched_at = Some(cursor_position);
+ }
+ touch::Event::FingerMoved { .. } => {
+ if let Some(scroll_box_touched_at) =
+ state.scroll_area_touched_at
+ {
+ let Some(cursor_position) = cursor.position() else {
+ return event::Status::Ignored
+ };
+
+ let delta = Vector::new(
+ cursor_position.x - scroll_box_touched_at.x,
+ cursor_position.y - scroll_box_touched_at.y,
+ );
+
+ state.scroll(delta, direction, bounds, content_bounds);
- return event::Status::Captured;
- }
- Event::Touch(event)
- if state.scroll_area_touched_at.is_some()
- || !mouse_over_y_scrollbar && !mouse_over_x_scrollbar =>
- {
- match event {
- touch::Event::FingerPressed { .. } => {
state.scroll_area_touched_at = Some(cursor_position);
- }
- touch::Event::FingerMoved { .. } => {
- if let Some(scroll_box_touched_at) =
- state.scroll_area_touched_at
- {
- let delta = Vector::new(
- cursor_position.x - scroll_box_touched_at.x,
- cursor_position.y - scroll_box_touched_at.y,
- );
-
- state.scroll(delta, bounds, content_bounds);
-
- state.scroll_area_touched_at =
- Some(cursor_position);
-
- notify_on_scroll(
- state,
- on_scroll,
- bounds,
- content_bounds,
- shell,
- );
- }
- }
- touch::Event::FingerLifted { .. }
- | touch::Event::FingerLost { .. } => {
- state.scroll_area_touched_at = None;
+
+ notify_on_scroll(
+ state,
+ on_scroll,
+ bounds,
+ content_bounds,
+ shell,
+ );
}
}
-
- return event::Status::Captured;
+ touch::Event::FingerLifted { .. }
+ | touch::Event::FingerLost { .. } => {
+ state.scroll_area_touched_at = None;
+ }
}
- _ => {}
+
+ return event::Status::Captured;
}
+ _ => {}
}
if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at {
@@ -574,6 +619,10 @@ pub fn update<Message>(
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => {
if let Some(scrollbar) = scrollbars.y {
+ let Some(cursor_position) = cursor.position() else {
+ return event::Status::Ignored
+ };
+
state.scroll_y_to(
scrollbar.scroll_percentage_y(
scroller_grabbed_at,
@@ -600,6 +649,10 @@ pub fn update<Message>(
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
+ let Some(cursor_position) = cursor.position() else {
+ return event::Status::Ignored
+ };
+
if let (Some(scroller_grabbed_at), Some(scrollbar)) =
(scrollbars.grab_y_scroller(cursor_position), scrollbars.y)
{
@@ -640,6 +693,10 @@ pub fn update<Message>(
}
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => {
+ let Some(cursor_position) = cursor.position() else {
+ return event::Status::Ignored
+ };
+
if let Some(scrollbar) = scrollbars.x {
state.scroll_x_to(
scrollbar.scroll_percentage_x(
@@ -667,6 +724,10 @@ pub fn update<Message>(
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
+ let Some(cursor_position) = cursor.position() else {
+ return event::Status::Ignored
+ };
+
if let (Some(scroller_grabbed_at), Some(scrollbar)) =
(scrollbars.grab_x_scroller(cursor_position), scrollbars.x)
{
@@ -703,48 +764,47 @@ pub fn update<Message>(
pub fn mouse_interaction(
state: &State,
layout: Layout<'_>,
- cursor_position: Point,
- vertical: &Properties,
- horizontal: Option<&Properties>,
+ cursor: mouse::Cursor,
+ direction: Direction,
content_interaction: impl FnOnce(
Layout<'_>,
- Point,
+ mouse::Cursor,
&Rectangle,
) -> mouse::Interaction,
) -> mouse::Interaction {
let bounds = layout.bounds();
- let mouse_over_scrollable = bounds.contains(cursor_position);
+ let cursor_over_scrollable = cursor.position_over(bounds);
let content_layout = layout.children().next().unwrap();
let content_bounds = content_layout.bounds();
- let scrollbars =
- Scrollbars::new(state, vertical, horizontal, bounds, content_bounds);
+ let scrollbars = Scrollbars::new(state, direction, bounds, content_bounds);
let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
- scrollbars.is_mouse_over(cursor_position);
+ scrollbars.is_mouse_over(cursor);
if (mouse_over_x_scrollbar || mouse_over_y_scrollbar)
|| state.scrollers_grabbed()
{
mouse::Interaction::Idle
} else {
- let offset = state.offset(bounds, content_bounds);
+ let translation = state.translation(direction, bounds, content_bounds);
- let cursor_position = if mouse_over_scrollable
- && !(mouse_over_y_scrollbar || mouse_over_x_scrollbar)
- {
- cursor_position + offset
- } else {
- Point::new(-1.0, -1.0)
+ let cursor = match cursor_over_scrollable {
+ Some(cursor_position)
+ if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
+ {
+ mouse::Cursor::Available(cursor_position + translation)
+ }
+ _ => mouse::Cursor::Unavailable,
};
content_interaction(
content_layout,
- cursor_position,
+ cursor,
&Rectangle {
- y: bounds.y + offset.y,
- x: bounds.x + offset.x,
+ y: bounds.y + translation.y,
+ x: bounds.x + translation.x,
..bounds
},
)
@@ -757,49 +817,48 @@ pub fn draw<Renderer>(
renderer: &mut Renderer,
theme: &Renderer::Theme,
layout: Layout<'_>,
- cursor_position: Point,
- vertical: &Properties,
- horizontal: Option<&Properties>,
+ cursor: mouse::Cursor,
+ direction: Direction,
style: &<Renderer::Theme as StyleSheet>::Style,
- draw_content: impl FnOnce(&mut Renderer, Layout<'_>, Point, &Rectangle),
+ draw_content: impl FnOnce(&mut Renderer, Layout<'_>, mouse::Cursor, &Rectangle),
) where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
let bounds = layout.bounds();
let content_layout = layout.children().next().unwrap();
let content_bounds = content_layout.bounds();
- let scrollbars =
- Scrollbars::new(state, vertical, horizontal, bounds, content_bounds);
+ let scrollbars = Scrollbars::new(state, direction, bounds, content_bounds);
- let mouse_over_scrollable = bounds.contains(cursor_position);
+ let cursor_over_scrollable = cursor.position_over(bounds);
let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) =
- scrollbars.is_mouse_over(cursor_position);
+ scrollbars.is_mouse_over(cursor);
- let offset = state.offset(bounds, content_bounds);
+ let translation = state.translation(direction, bounds, content_bounds);
- let cursor_position = if mouse_over_scrollable
- && !(mouse_over_x_scrollbar || mouse_over_y_scrollbar)
- {
- cursor_position + offset
- } else {
- Point::new(-1.0, -1.0)
+ let cursor = match cursor_over_scrollable {
+ Some(cursor_position)
+ if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) =>
+ {
+ mouse::Cursor::Available(cursor_position + translation)
+ }
+ _ => mouse::Cursor::Unavailable,
};
// Draw inner content
if scrollbars.active() {
renderer.with_layer(bounds, |renderer| {
renderer.with_translation(
- Vector::new(-offset.x, -offset.y),
+ Vector::new(-translation.x, -translation.y),
|renderer| {
draw_content(
renderer,
content_layout,
- cursor_position,
+ cursor,
&Rectangle {
- y: bounds.y + offset.y,
- x: bounds.x + offset.x,
+ y: bounds.y + translation.y,
+ x: bounds.x + translation.x,
..bounds
},
);
@@ -809,17 +868,19 @@ pub fn draw<Renderer>(
let draw_scrollbar =
|renderer: &mut Renderer,
- style: style::Scrollbar,
- scrollbar: &Scrollbar| {
+ style: Scrollbar,
+ scrollbar: &internals::Scrollbar| {
//track
- if style.background.is_some()
- || (style.border_color != Color::TRANSPARENT
- && style.border_width > 0.0)
+ if scrollbar.bounds.width > 0.0
+ && scrollbar.bounds.height > 0.0
+ && (style.background.is_some()
+ || (style.border_color != Color::TRANSPARENT
+ && style.border_width > 0.0))
{
renderer.fill_quad(
renderer::Quad {
bounds: scrollbar.bounds,
- border_radius: style.border_radius.into(),
+ border_radius: style.border_radius,
border_width: style.border_width,
border_color: style.border_color,
},
@@ -830,14 +891,16 @@ pub fn draw<Renderer>(
}
//thumb
- if style.scroller.color != Color::TRANSPARENT
- || (style.scroller.border_color != Color::TRANSPARENT
- && style.scroller.border_width > 0.0)
+ if scrollbar.scroller.bounds.width > 0.0
+ && scrollbar.scroller.bounds.height > 0.0
+ && (style.scroller.color != Color::TRANSPARENT
+ || (style.scroller.border_color != Color::TRANSPARENT
+ && style.scroller.border_width > 0.0))
{
renderer.fill_quad(
renderer::Quad {
bounds: scrollbar.scroller.bounds,
- border_radius: style.scroller.border_radius.into(),
+ border_radius: style.scroller.border_radius,
border_width: style.scroller.border_width,
border_color: style.scroller.border_color,
},
@@ -857,8 +920,8 @@ pub fn draw<Renderer>(
if let Some(scrollbar) = scrollbars.y {
let style = if state.y_scroller_grabbed_at.is_some() {
theme.dragging(style)
- } else if mouse_over_y_scrollbar {
- theme.hovered(style)
+ } else if cursor_over_scrollable.is_some() {
+ theme.hovered(style, mouse_over_y_scrollbar)
} else {
theme.active(style)
};
@@ -870,8 +933,8 @@ pub fn draw<Renderer>(
if let Some(scrollbar) = scrollbars.x {
let style = if state.x_scroller_grabbed_at.is_some() {
theme.dragging_horizontal(style)
- } else if mouse_over_x_scrollbar {
- theme.hovered_horizontal(style)
+ } else if cursor_over_scrollable.is_some() {
+ theme.hovered_horizontal(style, mouse_over_x_scrollbar)
} else {
theme.active_horizontal(style)
};
@@ -884,10 +947,10 @@ pub fn draw<Renderer>(
draw_content(
renderer,
content_layout,
- cursor_position,
+ cursor,
&Rectangle {
- x: bounds.x + offset.x,
- y: bounds.y + offset.y,
+ x: bounds.x + translation.x,
+ y: bounds.y + translation.y,
..bounds
},
);
@@ -895,8 +958,8 @@ pub fn draw<Renderer>(
}
fn notify_on_scroll<Message>(
- state: &State,
- on_scroll: &Option<Box<dyn Fn(RelativeOffset) -> Message + '_>>,
+ state: &mut State,
+ on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>,
bounds: Rectangle,
content_bounds: Rectangle,
shell: &mut Shell<'_, Message>,
@@ -908,15 +971,36 @@ fn notify_on_scroll<Message>(
return;
}
- let x = state.offset_x.absolute(bounds.width, content_bounds.width)
- / (content_bounds.width - bounds.width);
+ let viewport = Viewport {
+ offset_x: state.offset_x,
+ offset_y: state.offset_y,
+ bounds,
+ content_bounds,
+ };
- let y = state
- .offset_y
- .absolute(bounds.height, content_bounds.height)
- / (content_bounds.height - bounds.height);
+ // Don't publish redundant viewports to shell
+ if let Some(last_notified) = state.last_notified {
+ let last_relative_offset = last_notified.relative_offset();
+ let current_relative_offset = viewport.relative_offset();
+
+ let last_absolute_offset = last_notified.absolute_offset();
+ let current_absolute_offset = viewport.absolute_offset();
- shell.publish(on_scroll(RelativeOffset { x, y }))
+ let unchanged = |a: f32, b: f32| {
+ (a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan())
+ };
+
+ if unchanged(last_relative_offset.x, current_relative_offset.x)
+ && unchanged(last_relative_offset.y, current_relative_offset.y)
+ && unchanged(last_absolute_offset.x, current_absolute_offset.x)
+ && unchanged(last_absolute_offset.y, current_absolute_offset.y)
+ {
+ return;
+ }
+ }
+
+ shell.publish(on_scroll(viewport));
+ state.last_notified = Some(viewport);
}
}
@@ -929,6 +1013,7 @@ pub struct State {
offset_x: Offset,
x_scroller_grabbed_at: Option<f32>,
keyboard_modifiers: keyboard::Modifiers,
+ last_notified: Option<Viewport>,
}
impl Default for State {
@@ -940,6 +1025,7 @@ impl Default for State {
offset_x: Offset::Absolute(0.0),
x_scroller_grabbed_at: None,
keyboard_modifiers: keyboard::Modifiers::default(),
+ last_notified: None,
}
}
}
@@ -948,6 +1034,10 @@ impl operation::Scrollable for State {
fn snap_to(&mut self, offset: RelativeOffset) {
State::snap_to(self, offset);
}
+
+ fn scroll_to(&mut self, offset: AbsoluteOffset) {
+ State::scroll_to(self, offset)
+ }
}
#[derive(Debug, Clone, Copy)]
@@ -957,16 +1047,63 @@ enum Offset {
}
impl Offset {
- fn absolute(self, window: f32, content: f32) -> f32 {
+ fn absolute(self, viewport: f32, content: f32) -> f32 {
match self {
Offset::Absolute(absolute) => {
- absolute.min((content - window).max(0.0))
+ absolute.min((content - viewport).max(0.0))
}
Offset::Relative(percentage) => {
- ((content - window) * percentage).max(0.0)
+ ((content - viewport) * percentage).max(0.0)
}
}
}
+
+ fn translation(
+ self,
+ viewport: f32,
+ content: f32,
+ alignment: Alignment,
+ ) -> f32 {
+ let offset = self.absolute(viewport, content);
+
+ match alignment {
+ Alignment::Start => offset,
+ Alignment::End => ((content - viewport).max(0.0) - offset).max(0.0),
+ }
+ }
+}
+
+/// The current [`Viewport`] of the [`Scrollable`].
+#[derive(Debug, Clone, Copy)]
+pub struct Viewport {
+ offset_x: Offset,
+ offset_y: Offset,
+ bounds: Rectangle,
+ content_bounds: Rectangle,
+}
+
+impl Viewport {
+ /// Returns the [`AbsoluteOffset`] of the current [`Viewport`].
+ pub fn absolute_offset(&self) -> AbsoluteOffset {
+ let x = self
+ .offset_x
+ .absolute(self.bounds.width, self.content_bounds.width);
+ let y = self
+ .offset_y
+ .absolute(self.bounds.height, self.content_bounds.height);
+
+ AbsoluteOffset { x, y }
+ }
+
+ /// Returns the [`RelativeOffset`] of the current [`Viewport`].
+ pub fn relative_offset(&self) -> RelativeOffset {
+ let AbsoluteOffset { x, y } = self.absolute_offset();
+
+ let x = x / (self.content_bounds.width - self.bounds.width);
+ let y = y / (self.content_bounds.height - self.bounds.height);
+
+ RelativeOffset { x, y }
+ }
}
impl State {
@@ -980,9 +1117,30 @@ impl State {
pub fn scroll(
&mut self,
delta: Vector<f32>,
+ direction: Direction,
bounds: Rectangle,
content_bounds: Rectangle,
) {
+ let horizontal_alignment = direction
+ .horizontal()
+ .map(|p| p.alignment)
+ .unwrap_or_default();
+
+ let vertical_alignment = direction
+ .vertical()
+ .map(|p| p.alignment)
+ .unwrap_or_default();
+
+ let align = |alignment: Alignment, delta: f32| match alignment {
+ Alignment::Start => delta,
+ Alignment::End => -delta,
+ };
+
+ let delta = Vector::new(
+ align(horizontal_alignment, delta.x),
+ align(vertical_alignment, delta.y),
+ );
+
if bounds.height < content_bounds.height {
self.offset_y = Offset::Absolute(
(self.offset_y.absolute(bounds.height, content_bounds.height)
@@ -1034,6 +1192,12 @@ impl State {
self.offset_y = Offset::Relative(offset.y.clamp(0.0, 1.0));
}
+ /// Scroll to the provided [`AbsoluteOffset`].
+ pub fn scroll_to(&mut self, offset: AbsoluteOffset) {
+ self.offset_x = Offset::Absolute(offset.x.max(0.0));
+ self.offset_y = Offset::Absolute(offset.y.max(0.0));
+ }
+
/// Unsnaps the current scroll position, if snapped, given the bounds of the
/// [`Scrollable`] and its contents.
pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) {
@@ -1045,16 +1209,33 @@ impl State {
);
}
- /// Returns the scrolling offset of the [`State`], given the bounds of the
- /// [`Scrollable`] and its contents.
- pub fn offset(
+ /// Returns the scrolling translation of the [`State`], given a [`Direction`],
+ /// the bounds of the [`Scrollable`] and its contents.
+ fn translation(
&self,
+ direction: Direction,
bounds: Rectangle,
content_bounds: Rectangle,
) -> Vector {
Vector::new(
- self.offset_x.absolute(bounds.width, content_bounds.width),
- self.offset_y.absolute(bounds.height, content_bounds.height),
+ if let Some(horizontal) = direction.horizontal() {
+ self.offset_x.translation(
+ bounds.width,
+ content_bounds.width,
+ horizontal.alignment,
+ )
+ } else {
+ 0.0
+ },
+ if let Some(vertical) = direction.vertical() {
+ self.offset_y.translation(
+ bounds.height,
+ content_bounds.height,
+ vertical.alignment,
+ )
+ } else {
+ 0.0
+ },
)
}
@@ -1068,34 +1249,34 @@ impl State {
#[derive(Debug)]
/// State of both [`Scrollbar`]s.
struct Scrollbars {
- y: Option<Scrollbar>,
- x: Option<Scrollbar>,
+ y: Option<internals::Scrollbar>,
+ x: Option<internals::Scrollbar>,
}
impl Scrollbars {
/// Create y and/or x scrollbar(s) if content is overflowing the [`Scrollable`] bounds.
fn new(
state: &State,
- vertical: &Properties,
- horizontal: Option<&Properties>,
+ direction: Direction,
bounds: Rectangle,
content_bounds: Rectangle,
) -> Self {
- let offset = state.offset(bounds, content_bounds);
+ let translation = state.translation(direction, bounds, content_bounds);
- let show_scrollbar_x = horizontal.and_then(|h| {
- if content_bounds.width > bounds.width {
- Some(h)
- } else {
- None
- }
- });
+ let show_scrollbar_x = direction
+ .horizontal()
+ .filter(|_| content_bounds.width > bounds.width);
+
+ let show_scrollbar_y = direction
+ .vertical()
+ .filter(|_| content_bounds.height > bounds.height);
- let y_scrollbar = if content_bounds.height > bounds.height {
+ let y_scrollbar = if let Some(vertical) = show_scrollbar_y {
let Properties {
width,
margin,
scroller_width,
+ ..
} = *vertical;
// Adjust the height of the vertical scrollbar if the horizontal scrollbar
@@ -1127,7 +1308,7 @@ impl Scrollbars {
let ratio = bounds.height / content_bounds.height;
// min height for easier grabbing with super tall content
let scroller_height = (bounds.height * ratio).max(2.0);
- let scroller_offset = offset.y * ratio;
+ let scroller_offset = translation.y * ratio;
let scroller_bounds = Rectangle {
x: bounds.x + bounds.width
@@ -1139,12 +1320,13 @@ impl Scrollbars {
height: scroller_height,
};
- Some(Scrollbar {
+ Some(internals::Scrollbar {
total_bounds: total_scrollbar_bounds,
bounds: scrollbar_bounds,
- scroller: Scroller {
+ scroller: internals::Scroller {
bounds: scroller_bounds,
},
+ alignment: vertical.alignment,
})
} else {
None
@@ -1155,13 +1337,13 @@ impl Scrollbars {
width,
margin,
scroller_width,
+ ..
} = *horizontal;
// Need to adjust the width of the horizontal scrollbar if the vertical scrollbar
// is present
- let scrollbar_y_width = y_scrollbar.map_or(0.0, |_| {
- vertical.width.max(vertical.scroller_width) + vertical.margin
- });
+ let scrollbar_y_width = show_scrollbar_y
+ .map_or(0.0, |v| v.width.max(v.scroller_width) + v.margin);
let total_scrollbar_height =
width.max(scroller_width) + 2.0 * margin;
@@ -1187,7 +1369,7 @@ impl Scrollbars {
let ratio = bounds.width / content_bounds.width;
// min width for easier grabbing with extra wide content
let scroller_length = (bounds.width * ratio).max(2.0);
- let scroller_offset = offset.x * ratio;
+ let scroller_offset = translation.x * ratio;
let scroller_bounds = Rectangle {
x: (scrollbar_bounds.x + scroller_offset - scrollbar_y_width)
@@ -1199,12 +1381,13 @@ impl Scrollbars {
height: scroller_width,
};
- Some(Scrollbar {
+ Some(internals::Scrollbar {
total_bounds: total_scrollbar_bounds,
bounds: scrollbar_bounds,
- scroller: Scroller {
+ scroller: internals::Scroller {
bounds: scroller_bounds,
},
+ alignment: horizontal.alignment,
})
} else {
None
@@ -1216,17 +1399,21 @@ impl Scrollbars {
}
}
- fn is_mouse_over(&self, cursor_position: Point) -> (bool, bool) {
- (
- self.y
- .as_ref()
- .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
- .unwrap_or(false),
- self.x
- .as_ref()
- .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
- .unwrap_or(false),
- )
+ fn is_mouse_over(&self, cursor: mouse::Cursor) -> (bool, bool) {
+ if let Some(cursor_position) = cursor.position() {
+ (
+ self.y
+ .as_ref()
+ .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
+ .unwrap_or(false),
+ self.x
+ .as_ref()
+ .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
+ .unwrap_or(false),
+ )
+ } else {
+ (false, false)
+ }
}
fn grab_y_scroller(&self, cursor_position: Point) -> Option<f32> {
@@ -1264,64 +1451,64 @@ impl Scrollbars {
}
}
-/// The scrollbar of a [`Scrollable`].
-#[derive(Debug, Copy, Clone)]
-struct Scrollbar {
- /// The total bounds of the [`Scrollbar`], including the scrollbar, the scroller,
- /// and the scrollbar margin.
- total_bounds: Rectangle,
-
- /// The bounds of just the [`Scrollbar`].
- bounds: Rectangle,
+pub(super) mod internals {
+ use crate::core::{Point, Rectangle};
- /// The state of this scrollbar's [`Scroller`].
- scroller: Scroller,
-}
+ use super::Alignment;
-impl Scrollbar {
- /// Returns whether the mouse is over the scrollbar or not.
- fn is_mouse_over(&self, cursor_position: Point) -> bool {
- self.total_bounds.contains(cursor_position)
+ #[derive(Debug, Copy, Clone)]
+ pub struct Scrollbar {
+ pub total_bounds: Rectangle,
+ pub bounds: Rectangle,
+ pub scroller: Scroller,
+ pub alignment: Alignment,
}
- /// Returns the y-axis scrolled percentage from the cursor position.
- fn scroll_percentage_y(
- &self,
- grabbed_at: f32,
- cursor_position: Point,
- ) -> f32 {
- if cursor_position.x < 0.0 && cursor_position.y < 0.0 {
- // cursor position is unavailable! Set to either end or beginning of scrollbar depending
- // on where the thumb currently is in the track
- (self.scroller.bounds.y / self.total_bounds.height).round()
- } else {
- (cursor_position.y
+ impl Scrollbar {
+ /// Returns whether the mouse is over the scrollbar or not.
+ pub fn is_mouse_over(&self, cursor_position: Point) -> bool {
+ self.total_bounds.contains(cursor_position)
+ }
+
+ /// Returns the y-axis scrolled percentage from the cursor position.
+ pub fn scroll_percentage_y(
+ &self,
+ grabbed_at: f32,
+ cursor_position: Point,
+ ) -> f32 {
+ let percentage = (cursor_position.y
- self.bounds.y
- self.scroller.bounds.height * grabbed_at)
- / (self.bounds.height - self.scroller.bounds.height)
+ / (self.bounds.height - self.scroller.bounds.height);
+
+ match self.alignment {
+ Alignment::Start => percentage,
+ Alignment::End => 1.0 - percentage,
+ }
}
- }
- /// Returns the x-axis scrolled percentage from the cursor position.
- fn scroll_percentage_x(
- &self,
- grabbed_at: f32,
- cursor_position: Point,
- ) -> f32 {
- if cursor_position.x < 0.0 && cursor_position.y < 0.0 {
- (self.scroller.bounds.x / self.total_bounds.width).round()
- } else {
- (cursor_position.x
+ /// Returns the x-axis scrolled percentage from the cursor position.
+ pub fn scroll_percentage_x(
+ &self,
+ grabbed_at: f32,
+ cursor_position: Point,
+ ) -> f32 {
+ let percentage = (cursor_position.x
- self.bounds.x
- self.scroller.bounds.width * grabbed_at)
- / (self.bounds.width - self.scroller.bounds.width)
+ / (self.bounds.width - self.scroller.bounds.width);
+
+ match self.alignment {
+ Alignment::Start => percentage,
+ Alignment::End => 1.0 - percentage,
+ }
}
}
-}
-/// The handle of a [`Scrollbar`].
-#[derive(Debug, Clone, Copy)]
-struct Scroller {
- /// The bounds of the [`Scroller`].
- bounds: Rectangle,
+ /// The handle of a [`Scrollbar`].
+ #[derive(Debug, Clone, Copy)]
+ pub struct Scroller {
+ /// The bounds of the [`Scroller`].
+ pub bounds: Rectangle,
+ }
}
diff --git a/native/src/widget/slider.rs b/widget/src/slider.rs
index d3715b1c..3ea4391b 100644
--- a/native/src/widget/slider.rs
+++ b/widget/src/slider.rs
@@ -1,20 +1,22 @@
//! Display an interactive selector of a single value from a range of values.
//!
//! A [`Slider`] has some local [`State`].
-use crate::event::{self, Event};
-use crate::layout;
-use crate::mouse;
-use crate::renderer;
-use crate::touch;
-use crate::widget::tree::{self, Tree};
-use crate::{
- Background, Clipboard, Color, Element, Layout, Length, Pixels, Point,
- Rectangle, Shell, Size, Widget,
+use crate::core::event::{self, Event};
+use crate::core::layout;
+use crate::core::mouse;
+use crate::core::renderer;
+use crate::core::touch;
+use crate::core::widget::tree::{self, Tree};
+use crate::core::{
+ Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, Shell,
+ Size, Widget,
};
use std::ops::RangeInclusive;
-pub use iced_style::slider::{Appearance, Handle, HandleShape, StyleSheet};
+pub use iced_style::slider::{
+ Appearance, Handle, HandleShape, Rail, StyleSheet,
+};
/// An horizontal bar and a handle that selects a single value from a range of
/// values.
@@ -25,11 +27,9 @@ pub use iced_style::slider::{Appearance, Handle, HandleShape, StyleSheet};
/// to 1 unit.
///
/// # Example
-/// ```
-/// # use iced_native::widget::slider;
-/// # use iced_native::renderer::Null;
-/// #
-/// # type Slider<'a, T, Message> = slider::Slider<'a, T, Message, Null>;
+/// ```no_run
+/// # type Slider<'a, T, Message> =
+/// # iced_widget::Slider<'a, Message, T, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
/// #
/// #[derive(Clone)]
/// pub enum Message {
@@ -43,9 +43,9 @@ pub use iced_style::slider::{Appearance, Handle, HandleShape, StyleSheet};
///
/// ![Slider drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/slider.png?raw=true)
#[allow(missing_debug_implementations)]
-pub struct Slider<'a, T, Message, Renderer>
+pub struct Slider<'a, T, Message, Renderer = crate::Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
range: RangeInclusive<T>,
@@ -62,7 +62,7 @@ impl<'a, T, Message, Renderer> Slider<'a, T, Message, Renderer>
where
T: Copy + From<u8> + std::cmp::PartialOrd,
Message: Clone,
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
/// The default height of a [`Slider`].
@@ -148,7 +148,7 @@ impl<'a, T, Message, Renderer> Widget<Message, Renderer>
where
T: Copy + Into<f64> + num_traits::FromPrimitive,
Message: Clone,
- Renderer: crate::Renderer,
+ Renderer: crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
fn tag(&self) -> tree::Tag {
@@ -183,7 +183,7 @@ where
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
@@ -191,7 +191,7 @@ where
update(
event,
layout,
- cursor_position,
+ cursor,
shell,
tree.state.downcast_mut::<State>(),
&mut self.value,
@@ -209,13 +209,13 @@ where
theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
draw(
renderer,
layout,
- cursor_position,
+ cursor,
tree.state.downcast_ref::<State>(),
self.value,
&self.range,
@@ -228,15 +228,11 @@ where
&self,
tree: &Tree,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
- mouse_interaction(
- layout,
- cursor_position,
- tree.state.downcast_ref::<State>(),
- )
+ mouse_interaction(layout, cursor, tree.state.downcast_ref::<State>())
}
}
@@ -245,7 +241,7 @@ impl<'a, T, Message, Renderer> From<Slider<'a, T, Message, Renderer>>
where
T: 'a + Copy + Into<f64> + num_traits::FromPrimitive,
Message: 'a + Clone,
- Renderer: 'a + crate::Renderer,
+ Renderer: 'a + crate::core::Renderer,
Renderer::Theme: StyleSheet,
{
fn from(
@@ -260,7 +256,7 @@ where
pub fn update<Message, T>(
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
shell: &mut Shell<'_, Message>,
state: &mut State,
value: &mut T,
@@ -275,7 +271,7 @@ where
{
let is_dragging = state.is_dragging;
- let mut change = || {
+ let mut change = |cursor_position: Point| {
let bounds = layout.bounds();
let new_value = if cursor_position.x <= bounds.x {
*range.start()
@@ -309,8 +305,9 @@ where
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
- if layout.bounds().contains(cursor_position) {
- change();
+ if let Some(cursor_position) = cursor.position_over(layout.bounds())
+ {
+ change(cursor_position);
state.is_dragging = true;
return event::Status::Captured;
@@ -331,7 +328,7 @@ where
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => {
if is_dragging {
- change();
+ let _ = cursor.position().map(change);
return event::Status::Captured;
}
@@ -346,7 +343,7 @@ where
pub fn draw<T, R>(
renderer: &mut R,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
state: &State,
value: T,
range: &RangeInclusive<T>,
@@ -354,11 +351,11 @@ pub fn draw<T, R>(
style: &<R::Theme as StyleSheet>::Style,
) where
T: Into<f64> + Copy,
- R: crate::Renderer,
+ R: crate::core::Renderer,
R::Theme: StyleSheet,
{
let bounds = layout.bounds();
- let is_mouse_over = bounds.contains(cursor_position);
+ let is_mouse_over = cursor.is_over(bounds);
let style = if state.is_dragging {
style_sheet.dragging(style)
@@ -368,72 +365,72 @@ pub fn draw<T, R>(
style_sheet.active(style)
};
- let rail_y = bounds.y + (bounds.height / 2.0).round();
+ let (handle_width, handle_height, handle_border_radius) =
+ match style.handle.shape {
+ HandleShape::Circle { radius } => {
+ (radius * 2.0, radius * 2.0, radius.into())
+ }
+ HandleShape::Rectangle {
+ width,
+ border_radius,
+ } => (f32::from(width), bounds.height, border_radius),
+ };
+
+ let value = value.into() as f32;
+ let (range_start, range_end) = {
+ let (start, end) = range.clone().into_inner();
+
+ (start.into() as f32, end.into() as f32)
+ };
+
+ let offset = if range_start >= range_end {
+ 0.0
+ } else {
+ (bounds.width - handle_width) * (value - range_start)
+ / (range_end - range_start)
+ };
+
+ let rail_y = bounds.y + bounds.height / 2.0;
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: bounds.x,
- y: rail_y - 1.0,
- width: bounds.width,
- height: 2.0,
+ y: rail_y - style.rail.width / 2.0,
+ width: offset + handle_width / 2.0,
+ height: style.rail.width,
},
- border_radius: 0.0.into(),
+ border_radius: style.rail.border_radius,
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
- style.rail_colors.0,
+ style.rail.colors.0,
);
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
- x: bounds.x,
- y: rail_y + 1.0,
- width: bounds.width,
- height: 2.0,
+ x: bounds.x + offset + handle_width / 2.0,
+ y: rail_y - style.rail.width / 2.0,
+ width: bounds.width - offset - handle_width / 2.0,
+ height: style.rail.width,
},
- border_radius: 0.0.into(),
+ border_radius: style.rail.border_radius,
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
- Background::Color(style.rail_colors.1),
+ style.rail.colors.1,
);
- 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), bounds.height, border_radius),
- };
-
- let value = value.into() as f32;
- let (range_start, range_end) = {
- let (start, end) = range.clone().into_inner();
-
- (start.into() as f32, end.into() as f32)
- };
-
- let handle_offset = if range_start >= range_end {
- 0.0
- } else {
- (bounds.width - handle_width) * (value - range_start)
- / (range_end - range_start)
- };
-
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
- x: bounds.x + handle_offset.round(),
+ x: bounds.x + offset,
y: rail_y - handle_height / 2.0,
width: handle_width,
height: handle_height,
},
- border_radius: handle_border_radius.into(),
+ border_radius: handle_border_radius,
border_width: style.handle.border_width,
border_color: style.handle.border_color,
},
@@ -444,11 +441,11 @@ pub fn draw<T, R>(
/// Computes the current [`mouse::Interaction`] of a [`Slider`].
pub fn mouse_interaction(
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
state: &State,
) -> mouse::Interaction {
let bounds = layout.bounds();
- let is_mouse_over = bounds.contains(cursor_position);
+ let is_mouse_over = cursor.is_over(bounds);
if state.is_dragging {
mouse::Interaction::Grabbing
diff --git a/native/src/widget/space.rs b/widget/src/space.rs
index a6fc977e..9a5385e8 100644
--- a/native/src/widget/space.rs
+++ b/widget/src/space.rs
@@ -1,8 +1,10 @@
//! Distribute content vertically.
-use crate::layout;
-use crate::renderer;
-use crate::widget::Tree;
-use crate::{Element, Layout, Length, Point, Rectangle, Size, Widget};
+use crate::core;
+use crate::core::layout;
+use crate::core::mouse;
+use crate::core::renderer;
+use crate::core::widget::Tree;
+use crate::core::{Element, Layout, Length, Rectangle, Size, Widget};
/// An amount of empty space.
///
@@ -41,7 +43,7 @@ impl Space {
impl<Message, Renderer> Widget<Message, Renderer> for Space
where
- Renderer: crate::Renderer,
+ Renderer: core::Renderer,
{
fn width(&self) -> Length {
self.width
@@ -68,7 +70,7 @@ where
_theme: &Renderer::Theme,
_style: &renderer::Style,
_layout: Layout<'_>,
- _cursor_position: Point,
+ _cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
}
@@ -76,7 +78,7 @@ where
impl<'a, Message, Renderer> From<Space> for Element<'a, Message, Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: core::Renderer,
Message: 'a,
{
fn from(space: Space) -> Element<'a, Message, Renderer> {
diff --git a/native/src/widget/svg.rs b/widget/src/svg.rs
index f5ed0a6c..1ccc5d62 100644
--- a/native/src/widget/svg.rs
+++ b/widget/src/svg.rs
@@ -1,15 +1,16 @@
//! Display vector graphics in your application.
-use crate::layout;
-use crate::renderer;
-use crate::svg;
-use crate::widget::Tree;
-use crate::{
- ContentFit, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget,
+use crate::core::layout;
+use crate::core::mouse;
+use crate::core::renderer;
+use crate::core::svg;
+use crate::core::widget::Tree;
+use crate::core::{
+ ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget,
};
use std::path::PathBuf;
-pub use iced_style::svg::{Appearance, StyleSheet};
+pub use crate::style::svg::{Appearance, StyleSheet};
pub use svg::Handle;
/// A vector graphics image.
@@ -19,7 +20,7 @@ pub use svg::Handle;
/// [`Svg`] images can have a considerable rendering cost when resized,
/// specially when they are complex.
#[allow(missing_debug_implementations)]
-pub struct Svg<Renderer>
+pub struct Svg<Renderer = crate::Renderer>
where
Renderer: svg::Renderer,
Renderer::Theme: StyleSheet,
@@ -143,7 +144,7 @@ where
theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
- _cursor_position: Point,
+ _cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
let Size { width, height } = renderer.dimensions(&self.handle);
diff --git a/widget/src/text.rs b/widget/src/text.rs
new file mode 100644
index 00000000..ce4f44bd
--- /dev/null
+++ b/widget/src/text.rs
@@ -0,0 +1,6 @@
+//! Draw and interact with text.
+pub use crate::core::widget::text::*;
+
+/// A paragraph.
+pub type Text<'a, Renderer = crate::Renderer> =
+ crate::core::widget::Text<'a, Renderer>;
diff --git a/native/src/widget/text_input.rs b/widget/src/text_input.rs
index 00e871e7..03bcb86a 100644
--- a/native/src/widget/text_input.rs
+++ b/widget/src/text_input.rs
@@ -11,31 +11,34 @@ pub use value::Value;
use editor::Editor;
-use crate::alignment;
-use crate::event::{self, Event};
-use crate::keyboard;
-use crate::layout;
-use crate::mouse::{self, click};
-use crate::renderer;
-use crate::text::{self, Text};
-use crate::time::{Duration, Instant};
-use crate::touch;
-use crate::widget;
-use crate::widget::operation::{self, Operation};
-use crate::widget::tree::{self, Tree};
-use crate::window;
-use crate::{
- Clipboard, Color, Command, Element, Layout, Length, Padding, Pixels, Point,
+use crate::core::alignment;
+use crate::core::event::{self, Event};
+use crate::core::keyboard;
+use crate::core::layout;
+use crate::core::mouse::{self, click};
+use crate::core::renderer;
+use crate::core::text::{self, Text};
+use crate::core::time::{Duration, Instant};
+use crate::core::touch;
+use crate::core::widget;
+use crate::core::widget::operation::{self, Operation};
+use crate::core::widget::tree::{self, Tree};
+use crate::core::window;
+use crate::core::{
+ Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point,
Rectangle, Shell, Size, Vector, Widget,
};
+use crate::runtime::Command;
pub use iced_style::text_input::{Appearance, StyleSheet};
/// A field that can be filled with text.
///
/// # Example
-/// ```
-/// # pub type TextInput<'a, Message> = iced_native::widget::TextInput<'a, Message, iced_native::renderer::Null>;
+/// ```no_run
+/// # pub type TextInput<'a, Message> =
+/// # iced_widget::TextInput<'a, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
+/// #
/// #[derive(Debug, Clone)]
/// enum Message {
/// TextInputChanged(String),
@@ -46,13 +49,13 @@ pub use iced_style::text_input::{Appearance, StyleSheet};
/// let input = TextInput::new(
/// "This is the placeholder...",
/// value,
-/// Message::TextInputChanged,
/// )
+/// .on_input(Message::TextInputChanged)
/// .padding(10);
/// ```
/// ![Text input drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/text_input.png?raw=true)
#[allow(missing_debug_implementations)]
-pub struct TextInput<'a, Message, Renderer>
+pub struct TextInput<'a, Message, Renderer = crate::Renderer>
where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
@@ -61,13 +64,15 @@ where
placeholder: String,
value: Value,
is_secure: bool,
- font: Renderer::Font,
+ font: Option<Renderer::Font>,
width: Length,
padding: Padding,
size: Option<f32>,
- on_change: Box<dyn Fn(String) -> Message + 'a>,
+ line_height: text::LineHeight,
+ on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,
on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>,
on_submit: Option<Message>,
+ icon: Option<Icon<Renderer::Font>>,
style: <Renderer::Theme as StyleSheet>::Style,
}
@@ -81,24 +86,22 @@ where
///
/// It expects:
/// - a placeholder,
- /// - the current value, and
- /// - a function that produces a message when the [`TextInput`] changes.
- pub fn new<F>(placeholder: &str, value: &str, on_change: F) -> Self
- where
- F: 'a + Fn(String) -> Message,
- {
+ /// - the current value
+ pub fn new(placeholder: &str, value: &str) -> Self {
TextInput {
id: None,
placeholder: String::from(placeholder),
value: Value::new(value),
is_secure: false,
- font: Default::default(),
+ font: None,
width: Length::Fill,
padding: Padding::new(5.0),
size: None,
- on_change: Box::new(on_change),
+ line_height: text::LineHeight::default(),
+ on_input: None,
on_paste: None,
on_submit: None,
+ icon: None,
style: Default::default(),
}
}
@@ -115,6 +118,25 @@ where
self
}
+ /// Sets the message that should be produced when some text is typed into
+ /// the [`TextInput`].
+ ///
+ /// If this method is not called, the [`TextInput`] will be disabled.
+ pub fn on_input<F>(mut self, callback: F) -> Self
+ where
+ F: 'a + Fn(String) -> Message,
+ {
+ self.on_input = Some(Box::new(callback));
+ self
+ }
+
+ /// Sets the message that should be produced when the [`TextInput`] is
+ /// focused and the enter key is pressed.
+ pub fn on_submit(mut self, message: Message) -> Self {
+ self.on_submit = Some(message);
+ self
+ }
+
/// Sets the message that should be produced when some text is pasted into
/// the [`TextInput`].
pub fn on_paste(
@@ -129,9 +151,16 @@ where
///
/// [`Font`]: text::Renderer::Font
pub fn font(mut self, font: Renderer::Font) -> Self {
- self.font = font;
+ self.font = Some(font);
self
}
+
+ /// Sets the [`Icon`] of the [`TextInput`].
+ pub fn icon(mut self, icon: Icon<Renderer::Font>) -> Self {
+ self.icon = Some(icon);
+ self
+ }
+
/// Sets the width of the [`TextInput`].
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
@@ -150,10 +179,12 @@ where
self
}
- /// Sets the message that should be produced when the [`TextInput`] is
- /// focused and the enter key is pressed.
- pub fn on_submit(mut self, message: Message) -> Self {
- self.on_submit = Some(message);
+ /// Sets the [`LineHeight`] of the [`TextInput`].
+ pub fn line_height(
+ mut self,
+ line_height: impl Into<text::LineHeight>,
+ ) -> Self {
+ self.line_height = line_height.into();
self
}
@@ -176,20 +207,23 @@ where
renderer: &mut Renderer,
theme: &Renderer::Theme,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
value: Option<&Value>,
) {
draw(
renderer,
theme,
layout,
- cursor_position,
+ cursor,
tree.state.downcast_ref::<State>(),
value.unwrap_or(&self.value),
&self.placeholder,
self.size,
- &self.font,
+ self.line_height,
+ self.font,
+ self.on_input.is_none(),
self.is_secure,
+ self.icon.as_ref(),
&self.style,
)
}
@@ -210,6 +244,18 @@ where
tree::State::new(State::new())
}
+ fn diff(&self, tree: &mut Tree) {
+ let state = tree.state.downcast_mut::<State>();
+
+ // Unfocus text input if it becomes disabled
+ if self.on_input.is_none() {
+ state.last_click = None;
+ state.is_focused = None;
+ state.is_pasting = None;
+ state.is_dragging = false;
+ }
+ }
+
fn width(&self) -> Length {
self.width
}
@@ -223,7 +269,15 @@ where
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
- layout(renderer, limits, self.width, self.padding, self.size)
+ layout(
+ renderer,
+ limits,
+ self.width,
+ self.padding,
+ self.size,
+ self.line_height,
+ self.icon.as_ref(),
+ )
}
fn operate(
@@ -244,7 +298,7 @@ where
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
@@ -252,15 +306,16 @@ where
update(
event,
layout,
- cursor_position,
+ cursor,
renderer,
clipboard,
shell,
&mut self.value,
self.size,
- &self.font,
+ self.line_height,
+ self.font,
self.is_secure,
- self.on_change.as_ref(),
+ self.on_input.as_deref(),
self.on_paste.as_deref(),
&self.on_submit,
|| tree.state.downcast_mut::<State>(),
@@ -274,20 +329,23 @@ where
theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
draw(
renderer,
theme,
layout,
- cursor_position,
+ cursor,
tree.state.downcast_ref::<State>(),
&self.value,
&self.placeholder,
self.size,
- &self.font,
+ self.line_height,
+ self.font,
+ self.on_input.is_none(),
self.is_secure,
+ self.icon.as_ref(),
&self.style,
)
}
@@ -296,11 +354,11 @@ where
&self,
_state: &Tree,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
- mouse_interaction(layout, cursor_position)
+ mouse_interaction(layout, cursor, self.on_input.is_none())
}
}
@@ -318,6 +376,30 @@ where
}
}
+/// The content of the [`Icon`].
+#[derive(Debug, Clone)]
+pub struct Icon<Font> {
+ /// The font that will be used to display the `code_point`.
+ pub font: Font,
+ /// The unicode code point that will be used as the icon.
+ pub code_point: char,
+ /// The font size of the content.
+ pub size: Option<f32>,
+ /// The spacing between the [`Icon`] and the text in a [`TextInput`].
+ pub spacing: f32,
+ /// The side of a [`TextInput`] where to display the [`Icon`].
+ pub side: Side,
+}
+
+/// The side of a [`TextInput`].
+#[derive(Debug, Clone)]
+pub enum Side {
+ /// The left side of a [`TextInput`].
+ Left,
+ /// The right side of a [`TextInput`].
+ Right,
+}
+
/// The identifier of a [`TextInput`].
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Id(widget::Id);
@@ -380,19 +462,65 @@ pub fn layout<Renderer>(
width: Length,
padding: Padding,
size: Option<f32>,
+ line_height: text::LineHeight,
+ icon: Option<&Icon<Renderer::Font>>,
) -> layout::Node
where
Renderer: text::Renderer,
{
let text_size = size.unwrap_or_else(|| renderer.default_size());
-
let padding = padding.fit(Size::ZERO, limits.max());
- let limits = limits.width(width).pad(padding).height(text_size);
+ let limits = limits
+ .width(width)
+ .pad(padding)
+ .height(line_height.to_absolute(Pixels(text_size)));
+
+ let text_bounds = limits.resolve(Size::ZERO);
+
+ if let Some(icon) = icon {
+ let icon_width = renderer.measure_width(
+ &icon.code_point.to_string(),
+ icon.size.unwrap_or_else(|| renderer.default_size()),
+ icon.font,
+ text::Shaping::Advanced,
+ );
+
+ let mut text_node = layout::Node::new(
+ text_bounds - Size::new(icon_width + icon.spacing, 0.0),
+ );
+
+ let mut icon_node =
+ layout::Node::new(Size::new(icon_width, text_bounds.height));
+
+ match icon.side {
+ Side::Left => {
+ text_node.move_to(Point::new(
+ padding.left + icon_width + icon.spacing,
+ padding.top,
+ ));
- let mut text = layout::Node::new(limits.resolve(Size::ZERO));
- text.move_to(Point::new(padding.left, padding.top));
+ icon_node.move_to(Point::new(padding.left, padding.top));
+ }
+ Side::Right => {
+ text_node.move_to(Point::new(padding.left, padding.top));
+
+ icon_node.move_to(Point::new(
+ padding.left + text_bounds.width - icon_width,
+ padding.top,
+ ));
+ }
+ };
- layout::Node::with_children(text.size().pad(padding), vec![text])
+ layout::Node::with_children(
+ text_bounds.pad(padding),
+ vec![text_node, icon_node],
+ )
+ } else {
+ let mut text = layout::Node::new(text_bounds);
+ text.move_to(Point::new(padding.left, padding.top));
+
+ layout::Node::with_children(text_bounds.pad(padding), vec![text])
+ }
}
/// Processes an [`Event`] and updates the [`State`] of a [`TextInput`]
@@ -400,15 +528,16 @@ where
pub fn update<'a, Message, Renderer>(
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
value: &mut Value,
size: Option<f32>,
- font: &Renderer::Font,
+ line_height: text::LineHeight,
+ font: Option<Renderer::Font>,
is_secure: bool,
- on_change: &dyn Fn(String) -> Message,
+ on_input: Option<&dyn Fn(String) -> Message>,
on_paste: Option<&dyn Fn(String) -> Message>,
on_submit: &Option<Message>,
state: impl FnOnce() -> &'a mut State,
@@ -421,9 +550,14 @@ where
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
let state = state();
- let is_clicked = layout.bounds().contains(cursor_position);
- state.is_focused = if is_clicked {
+ let click_position = if on_input.is_some() {
+ cursor.position_over(layout.bounds())
+ } else {
+ None
+ };
+
+ state.is_focused = if click_position.is_some() {
state.is_focused.or_else(|| {
let now = Instant::now();
@@ -436,7 +570,7 @@ where
None
};
- if is_clicked {
+ if let Some(cursor_position) = click_position {
let text_layout = layout.children().next().unwrap();
let target = cursor_position.x - text_layout.bounds().x;
@@ -455,8 +589,9 @@ where
find_cursor_position(
renderer,
text_layout.bounds(),
- font.clone(),
+ font,
size,
+ line_height,
&value,
state,
target,
@@ -483,8 +618,9 @@ where
let position = find_cursor_position(
renderer,
text_layout.bounds(),
- font.clone(),
+ font,
size,
+ line_height,
value,
state,
target,
@@ -532,8 +668,9 @@ where
let position = find_cursor_position(
renderer,
text_layout.bounds(),
- font.clone(),
+ font,
size,
+ line_height,
&value,
state,
target,
@@ -551,6 +688,8 @@ where
let state = state();
if let Some(focus) = &mut state.is_focused {
+ let Some(on_input) = on_input else { return event::Status::Ignored };
+
if state.is_pasting.is_none()
&& !state.keyboard_modifiers.command()
&& !c.is_control()
@@ -559,7 +698,7 @@ where
editor.insert(c);
- let message = (on_change)(editor.contents());
+ let message = (on_input)(editor.contents());
shell.publish(message);
focus.updated_at = Instant::now();
@@ -572,6 +711,8 @@ where
let state = state();
if let Some(focus) = &mut state.is_focused {
+ let Some(on_input) = on_input else { return event::Status::Ignored };
+
let modifiers = state.keyboard_modifiers;
focus.updated_at = Instant::now();
@@ -597,7 +738,7 @@ where
let mut editor = Editor::new(value, &mut state.cursor);
editor.backspace();
- let message = (on_change)(editor.contents());
+ let message = (on_input)(editor.contents());
shell.publish(message);
}
keyboard::KeyCode::Delete => {
@@ -617,7 +758,7 @@ where
let mut editor = Editor::new(value, &mut state.cursor);
editor.delete();
- let message = (on_change)(editor.contents());
+ let message = (on_input)(editor.contents());
shell.publish(message);
}
keyboard::KeyCode::Left => {
@@ -692,7 +833,7 @@ where
let mut editor = Editor::new(value, &mut state.cursor);
editor.delete();
- let message = (on_change)(editor.contents());
+ let message = (on_input)(editor.contents());
shell.publish(message);
}
keyboard::KeyCode::V => {
@@ -719,7 +860,7 @@ where
let message = if let Some(paste) = &on_paste {
(paste)(editor.contents())
} else {
- (on_change)(editor.contents())
+ (on_input)(editor.contents())
};
shell.publish(message);
@@ -807,13 +948,16 @@ pub fn draw<Renderer>(
renderer: &mut Renderer,
theme: &Renderer::Theme,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
state: &State,
value: &Value,
placeholder: &str,
size: Option<f32>,
- font: &Renderer::Font,
+ line_height: text::LineHeight,
+ font: Option<Renderer::Font>,
+ is_disabled: bool,
is_secure: bool,
+ icon: Option<&Icon<Renderer::Font>>,
style: &<Renderer::Theme as StyleSheet>::Style,
) where
Renderer: text::Renderer,
@@ -823,11 +967,15 @@ pub fn draw<Renderer>(
let value = secure_value.as_ref().unwrap_or(value);
let bounds = layout.bounds();
- let text_bounds = layout.children().next().unwrap().bounds();
- let is_mouse_over = bounds.contains(cursor_position);
+ let mut children_layout = layout.children();
+ let text_bounds = children_layout.next().unwrap().bounds();
- let appearance = if state.is_focused() {
+ let is_mouse_over = cursor.is_over(bounds);
+
+ let appearance = if is_disabled {
+ theme.disabled(style)
+ } else if state.is_focused() {
theme.focused(style)
} else if is_mouse_over {
theme.hovered(style)
@@ -838,14 +986,34 @@ pub fn draw<Renderer>(
renderer.fill_quad(
renderer::Quad {
bounds,
- border_radius: appearance.border_radius.into(),
+ border_radius: appearance.border_radius,
border_width: appearance.border_width,
border_color: appearance.border_color,
},
appearance.background,
);
+ if let Some(icon) = icon {
+ let icon_layout = children_layout.next().unwrap();
+
+ renderer.fill_text(Text {
+ content: &icon.code_point.to_string(),
+ size: icon.size.unwrap_or_else(|| renderer.default_size()),
+ line_height: text::LineHeight::default(),
+ font: icon.font,
+ color: appearance.icon_color,
+ bounds: Rectangle {
+ y: text_bounds.center_y(),
+ ..icon_layout.bounds()
+ },
+ horizontal_alignment: alignment::Horizontal::Left,
+ vertical_alignment: alignment::Vertical::Center,
+ shaping: text::Shaping::Advanced,
+ });
+ }
+
let text = value.to_string();
+ let font = font.unwrap_or_else(|| renderer.default_font());
let size = size.unwrap_or_else(|| renderer.default_size());
let (cursor, offset) = if let Some(focus) = &state.is_focused {
@@ -858,7 +1026,7 @@ pub fn draw<Renderer>(
value,
size,
position,
- font.clone(),
+ font,
);
let is_cursor_visible = ((focus.now - focus.updated_at)
@@ -899,7 +1067,7 @@ pub fn draw<Renderer>(
value,
size,
left,
- font.clone(),
+ font,
);
let (right_position, right_offset) =
@@ -909,7 +1077,7 @@ pub fn draw<Renderer>(
value,
size,
right,
- font.clone(),
+ font,
);
let width = right_position - left_position;
@@ -944,30 +1112,37 @@ pub fn draw<Renderer>(
let text_width = renderer.measure_width(
if text.is_empty() { placeholder } else { &text },
size,
- font.clone(),
+ font,
+ text::Shaping::Advanced,
);
let render = |renderer: &mut Renderer| {
if let Some((cursor, color)) = cursor {
renderer.fill_quad(cursor, color);
+ } else {
+ renderer.with_translation(Vector::ZERO, |_| {});
}
renderer.fill_text(Text {
content: if text.is_empty() { placeholder } else { &text },
color: if text.is_empty() {
theme.placeholder_color(style)
+ } else if is_disabled {
+ theme.disabled_color(style)
} else {
theme.value_color(style)
},
- font: font.clone(),
+ font,
bounds: Rectangle {
y: text_bounds.center_y(),
width: f32::INFINITY,
..text_bounds
},
size,
+ line_height,
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
+ shaping: text::Shaping::Advanced,
});
};
@@ -983,10 +1158,15 @@ pub fn draw<Renderer>(
/// Computes the current [`mouse::Interaction`] of the [`TextInput`].
pub fn mouse_interaction(
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
+ is_disabled: bool,
) -> mouse::Interaction {
- if layout.bounds().contains(cursor_position) {
- mouse::Interaction::Text
+ if cursor.is_over(layout.bounds()) {
+ if is_disabled {
+ mouse::Interaction::NotAllowed
+ } else {
+ mouse::Interaction::Text
+ }
} else {
mouse::Interaction::default()
}
@@ -1109,7 +1289,7 @@ impl operation::TextInput for State {
}
mod platform {
- use crate::keyboard;
+ use crate::core::keyboard;
pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool {
if cfg!(target_os = "macos") {
@@ -1167,8 +1347,12 @@ where
{
let text_before_cursor = value.until(cursor_index).to_string();
- let text_value_width =
- renderer.measure_width(&text_before_cursor, size, font);
+ let text_value_width = renderer.measure_width(
+ &text_before_cursor,
+ size,
+ font,
+ text::Shaping::Advanced,
+ );
let offset = ((text_value_width + 5.0) - text_bounds.width).max(0.0);
@@ -1180,8 +1364,9 @@ where
fn find_cursor_position<Renderer>(
renderer: &Renderer,
text_bounds: Rectangle,
- font: Renderer::Font,
+ font: Option<Renderer::Font>,
size: Option<f32>,
+ line_height: text::LineHeight,
value: &Value,
state: &State,
x: f32,
@@ -1189,21 +1374,32 @@ fn find_cursor_position<Renderer>(
where
Renderer: text::Renderer,
{
+ let font = font.unwrap_or_else(|| renderer.default_font());
let size = size.unwrap_or_else(|| renderer.default_size());
- let offset =
- offset(renderer, text_bounds, font.clone(), size, value, state);
+ let offset = offset(renderer, text_bounds, font, size, value, state);
+ let value = value.to_string();
- renderer
+ let char_offset = renderer
.hit_test(
- &value.to_string(),
+ &value,
size,
+ line_height,
font,
Size::INFINITY,
+ text::Shaping::Advanced,
Point::new(x + offset, text_bounds.height / 2.0),
true,
)
- .map(text::Hit::cursor)
+ .map(text::Hit::cursor)?;
+
+ Some(
+ unicode_segmentation::UnicodeSegmentation::graphemes(
+ &value[..char_offset],
+ true,
+ )
+ .count(),
+ )
}
const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500;
diff --git a/native/src/widget/text_input/cursor.rs b/widget/src/text_input/cursor.rs
index 4f3b159b..9680dfd7 100644
--- a/native/src/widget/text_input/cursor.rs
+++ b/widget/src/text_input/cursor.rs
@@ -1,5 +1,5 @@
//! Track the cursor of a text input.
-use crate::widget::text_input::Value;
+use crate::text_input::Value;
/// The cursor of a text input.
#[derive(Debug, Copy, Clone)]
diff --git a/native/src/widget/text_input/editor.rs b/widget/src/text_input/editor.rs
index d53fa8d9..f1fd641f 100644
--- a/native/src/widget/text_input/editor.rs
+++ b/widget/src/text_input/editor.rs
@@ -1,4 +1,4 @@
-use crate::widget::text_input::{Cursor, Value};
+use crate::text_input::{Cursor, Value};
pub struct Editor<'a> {
value: &'a mut Value,
diff --git a/native/src/widget/text_input/value.rs b/widget/src/text_input/value.rs
index cf4da562..cf4da562 100644
--- a/native/src/widget/text_input/value.rs
+++ b/widget/src/text_input/value.rs
diff --git a/native/src/widget/toggler.rs b/widget/src/toggler.rs
index a434af65..1b31765f 100644
--- a/native/src/widget/toggler.rs
+++ b/widget/src/toggler.rs
@@ -1,24 +1,27 @@
//! Show toggle controls using togglers.
-use crate::alignment;
-use crate::event;
-use crate::layout;
-use crate::mouse;
-use crate::renderer;
-use crate::text;
-use crate::widget::{self, Row, Text, Tree};
-use crate::{
- Alignment, Clipboard, Element, Event, Layout, Length, Pixels, Point,
- Rectangle, Shell, Widget,
+use crate::core::alignment;
+use crate::core::event;
+use crate::core::layout;
+use crate::core::mouse;
+use crate::core::renderer;
+use crate::core::text;
+use crate::core::touch;
+use crate::core::widget::Tree;
+use crate::core::{
+ Alignment, Clipboard, Element, Event, Layout, Length, Pixels, Rectangle,
+ Shell, Widget,
};
+use crate::{Row, Text};
-pub use iced_style::toggler::{Appearance, StyleSheet};
+pub use crate::style::toggler::{Appearance, StyleSheet};
/// A toggler widget.
///
/// # Example
///
-/// ```
-/// # type Toggler<'a, Message> = iced_native::widget::Toggler<'a, Message, iced_native::renderer::Null>;
+/// ```no_run
+/// # type Toggler<'a, Message> =
+/// # iced_widget::Toggler<'a, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
/// #
/// pub enum Message {
/// TogglerToggled(bool),
@@ -29,7 +32,7 @@ pub use iced_style::toggler::{Appearance, StyleSheet};
/// Toggler::new(String::from("Toggle me!"), is_toggled, |b| Message::TogglerToggled(b));
/// ```
#[allow(missing_debug_implementations)]
-pub struct Toggler<'a, Message, Renderer>
+pub struct Toggler<'a, Message, Renderer = crate::Renderer>
where
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
@@ -40,9 +43,11 @@ where
width: Length,
size: f32,
text_size: Option<f32>,
+ text_line_height: text::LineHeight,
text_alignment: alignment::Horizontal,
+ text_shaping: text::Shaping,
spacing: f32,
- font: Renderer::Font,
+ font: Option<Renderer::Font>,
style: <Renderer::Theme as StyleSheet>::Style,
}
@@ -77,9 +82,11 @@ where
width: Length::Fill,
size: Self::DEFAULT_SIZE,
text_size: None,
+ text_line_height: text::LineHeight::default(),
text_alignment: alignment::Horizontal::Left,
+ text_shaping: text::Shaping::Basic,
spacing: 0.0,
- font: Renderer::Font::default(),
+ font: None,
style: Default::default(),
}
}
@@ -102,12 +109,27 @@ where
self
}
+ /// Sets the text [`LineHeight`] of the [`Toggler`].
+ pub fn text_line_height(
+ mut self,
+ line_height: impl Into<text::LineHeight>,
+ ) -> Self {
+ self.text_line_height = line_height.into();
+ self
+ }
+
/// Sets the horizontal alignment of the text of the [`Toggler`]
pub fn text_alignment(mut self, alignment: alignment::Horizontal) -> Self {
self.text_alignment = alignment;
self
}
+ /// Sets the [`text::Shaping`] strategy of the [`Toggler`].
+ pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
+ self.text_shaping = shaping;
+ self
+ }
+
/// Sets the spacing between the [`Toggler`] and the text.
pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
self.spacing = spacing.into().0;
@@ -117,8 +139,8 @@ where
/// Sets the [`Font`] of the text of the [`Toggler`]
///
/// [`Font`]: crate::text::Renderer::Font
- pub fn font(mut self, font: Renderer::Font) -> Self {
- self.font = font;
+ pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
+ self.font = Some(font.into());
self
}
@@ -136,7 +158,7 @@ impl<'a, Message, Renderer> Widget<Message, Renderer>
for Toggler<'a, Message, Renderer>
where
Renderer: text::Renderer,
- Renderer::Theme: StyleSheet + widget::text::StyleSheet,
+ Renderer::Theme: StyleSheet + crate::text::StyleSheet,
{
fn width(&self) -> Length {
self.width
@@ -160,12 +182,14 @@ where
row = row.push(
Text::new(label)
.horizontal_alignment(self.text_alignment)
- .font(self.font.clone())
+ .font(self.font.unwrap_or_else(|| renderer.default_font()))
.width(self.width)
.size(
self.text_size
.unwrap_or_else(|| renderer.default_size()),
- ),
+ )
+ .line_height(self.text_line_height)
+ .shaping(self.text_shaping),
);
}
@@ -179,14 +203,15 @@ where
_state: &mut Tree,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
match event {
- Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
- let mouse_over = layout.bounds().contains(cursor_position);
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
+ | Event::Touch(touch::Event::FingerPressed { .. }) => {
+ let mouse_over = cursor.is_over(layout.bounds());
if mouse_over {
shell.publish((self.on_toggle)(!self.is_toggled));
@@ -204,11 +229,11 @@ where
&self,
_state: &Tree,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
- if layout.bounds().contains(cursor_position) {
+ if cursor.is_over(layout.bounds()) {
mouse::Interaction::Pointer
} else {
mouse::Interaction::default()
@@ -222,7 +247,7 @@ where
theme: &Renderer::Theme,
style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
/// Makes sure that the border radius of the toggler looks good at every size.
@@ -237,23 +262,25 @@ where
if let Some(label) = &self.label {
let label_layout = children.next().unwrap();
- crate::widget::text::draw(
+ crate::text::draw(
renderer,
style,
label_layout,
label,
self.text_size,
- self.font.clone(),
+ self.text_line_height,
+ self.font,
Default::default(),
self.text_alignment,
alignment::Vertical::Center,
+ self.text_shaping,
);
}
let toggler_layout = children.next().unwrap();
let bounds = toggler_layout.bounds();
- let is_mouse_over = bounds.contains(cursor_position);
+ let is_mouse_over = cursor.is_over(layout.bounds());
let style = if is_mouse_over {
theme.hovered(&self.style, self.is_toggled)
@@ -314,7 +341,7 @@ impl<'a, Message, Renderer> From<Toggler<'a, Message, Renderer>>
where
Message: 'a,
Renderer: 'a + text::Renderer,
- Renderer::Theme: StyleSheet + widget::text::StyleSheet,
+ Renderer::Theme: StyleSheet + crate::text::StyleSheet,
{
fn from(
toggler: Toggler<'a, Message, Renderer>,
diff --git a/native/src/widget/tooltip.rs b/widget/src/tooltip.rs
index 2a24c055..2dc3da01 100644
--- a/native/src/widget/tooltip.rs
+++ b/widget/src/tooltip.rs
@@ -1,26 +1,26 @@
//! Display a widget over another.
-use crate::event;
-use crate::layout;
-use crate::mouse;
-use crate::renderer;
-use crate::text;
-use crate::widget;
-use crate::widget::container;
-use crate::widget::overlay;
-use crate::widget::{Text, Tree};
-use crate::{
- Clipboard, Element, Event, Layout, Length, Padding, Pixels, Point,
- Rectangle, Shell, Size, Vector, Widget,
+use crate::container;
+use crate::core::event::{self, Event};
+use crate::core::layout::{self, Layout};
+use crate::core::mouse;
+use crate::core::overlay;
+use crate::core::renderer;
+use crate::core::text;
+use crate::core::widget::{self, Widget};
+use crate::core::{
+ Clipboard, Element, Length, Padding, Pixels, Point, Rectangle, Shell, Size,
+ Vector,
};
+use crate::Text;
use std::borrow::Cow;
/// An element to display a widget over another.
#[allow(missing_debug_implementations)]
-pub struct Tooltip<'a, Message, Renderer: text::Renderer>
+pub struct Tooltip<'a, Message, Renderer = crate::Renderer>
where
Renderer: text::Renderer,
- Renderer::Theme: container::StyleSheet + widget::text::StyleSheet,
+ Renderer::Theme: container::StyleSheet + crate::text::StyleSheet,
{
content: Element<'a, Message, Renderer>,
tooltip: Text<'a, Renderer>,
@@ -34,7 +34,7 @@ where
impl<'a, Message, Renderer> Tooltip<'a, Message, Renderer>
where
Renderer: text::Renderer,
- Renderer::Theme: container::StyleSheet + widget::text::StyleSheet,
+ Renderer::Theme: container::StyleSheet + crate::text::StyleSheet,
{
/// The default padding of a [`Tooltip`] drawn by this renderer.
const DEFAULT_PADDING: f32 = 5.0;
@@ -104,16 +104,24 @@ impl<'a, Message, Renderer> Widget<Message, Renderer>
for Tooltip<'a, Message, Renderer>
where
Renderer: text::Renderer,
- Renderer::Theme: container::StyleSheet + widget::text::StyleSheet,
+ Renderer::Theme: container::StyleSheet + crate::text::StyleSheet,
{
- fn children(&self) -> Vec<Tree> {
- vec![Tree::new(&self.content)]
+ fn children(&self) -> Vec<widget::Tree> {
+ vec![widget::Tree::new(&self.content)]
}
- fn diff(&self, tree: &mut Tree) {
+ fn diff(&self, tree: &mut widget::Tree) {
tree.diff_children(std::slice::from_ref(&self.content))
}
+ fn state(&self) -> widget::tree::State {
+ widget::tree::State::new(State::default())
+ }
+
+ fn tag(&self) -> widget::tree::Tag {
+ widget::tree::Tag::of::<State>()
+ }
+
fn width(&self) -> Length {
self.content.as_widget().width()
}
@@ -132,19 +140,26 @@ where
fn on_event(
&mut self,
- tree: &mut Tree,
+ tree: &mut widget::Tree,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
+ let state = tree.state.downcast_mut::<State>();
+
+ *state = cursor
+ .position_over(layout.bounds())
+ .map(|cursor_position| State::Hovered { cursor_position })
+ .unwrap_or_default();
+
self.content.as_widget_mut().on_event(
&mut tree.children[0],
event,
layout,
- cursor_position,
+ cursor,
renderer,
clipboard,
shell,
@@ -153,16 +168,16 @@ where
fn mouse_interaction(
&self,
- tree: &Tree,
+ tree: &widget::Tree,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.content.as_widget().mouse_interaction(
&tree.children[0],
layout,
- cursor_position,
+ cursor,
viewport,
renderer,
)
@@ -170,12 +185,12 @@ where
fn draw(
&self,
- tree: &Tree,
+ tree: &widget::Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
inherited_style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
viewport: &Rectangle,
) {
self.content.as_widget().draw(
@@ -184,53 +199,53 @@ where
theme,
inherited_style,
layout,
- cursor_position,
- viewport,
- );
-
- let tooltip = &self.tooltip;
-
- draw(
- renderer,
- theme,
- inherited_style,
- layout,
- cursor_position,
+ cursor,
viewport,
- self.position,
- self.gap,
- self.padding,
- self.snap_within_viewport,
- &self.style,
- |renderer, limits| {
- Widget::<(), Renderer>::layout(tooltip, renderer, limits)
- },
- |renderer, defaults, layout, cursor_position, viewport| {
- Widget::<(), Renderer>::draw(
- tooltip,
- &Tree::empty(),
- renderer,
- theme,
- defaults,
- layout,
- cursor_position,
- viewport,
- );
- },
);
}
fn overlay<'b>(
&'b mut self,
- tree: &'b mut Tree,
+ tree: &'b mut widget::Tree,
layout: Layout<'_>,
renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> {
- self.content.as_widget_mut().overlay(
+ let state = tree.state.downcast_ref::<State>();
+
+ let content = self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout,
renderer,
- )
+ );
+
+ let tooltip = if let State::Hovered { cursor_position } = *state {
+ Some(overlay::Element::new(
+ layout.position(),
+ Box::new(Overlay {
+ tooltip: &self.tooltip,
+ cursor_position,
+ content_bounds: layout.bounds(),
+ snap_within_viewport: self.snap_within_viewport,
+ position: self.position,
+ gap: self.gap,
+ padding: self.padding,
+ style: &self.style,
+ }),
+ ))
+ } else {
+ None
+ };
+
+ if content.is_some() || tooltip.is_some() {
+ Some(
+ overlay::Group::with_children(
+ content.into_iter().chain(tooltip).collect(),
+ )
+ .overlay(),
+ )
+ } else {
+ None
+ }
}
}
@@ -239,7 +254,7 @@ impl<'a, Message, Renderer> From<Tooltip<'a, Message, Renderer>>
where
Message: 'a,
Renderer: 'a + text::Renderer,
- Renderer::Theme: container::StyleSheet + widget::text::StyleSheet,
+ Renderer::Theme: container::StyleSheet + crate::text::StyleSheet,
{
fn from(
tooltip: Tooltip<'a, Message, Renderer>,
@@ -263,90 +278,107 @@ pub enum Position {
Right,
}
-/// Draws a [`Tooltip`].
-pub fn draw<Renderer>(
- renderer: &mut Renderer,
- theme: &Renderer::Theme,
- inherited_style: &renderer::Style,
- layout: Layout<'_>,
+#[derive(Debug, Clone, Copy, Default)]
+enum State {
+ #[default]
+ Idle,
+ Hovered {
+ cursor_position: Point,
+ },
+}
+
+struct Overlay<'a, 'b, Renderer>
+where
+ Renderer: text::Renderer,
+ Renderer::Theme: container::StyleSheet + widget::text::StyleSheet,
+{
+ tooltip: &'b Text<'a, Renderer>,
cursor_position: Point,
- viewport: &Rectangle,
+ content_bounds: Rectangle,
+ snap_within_viewport: bool,
position: Position,
gap: f32,
padding: f32,
- snap_within_viewport: bool,
- style: &<Renderer::Theme as container::StyleSheet>::Style,
- layout_text: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
- draw_text: impl FnOnce(
- &mut Renderer,
- &renderer::Style,
- Layout<'_>,
- Point,
- &Rectangle,
- ),
-) where
- Renderer: crate::Renderer,
- Renderer::Theme: container::StyleSheet,
-{
- use container::StyleSheet;
-
- let bounds = layout.bounds();
-
- if bounds.contains(cursor_position) {
- let style = theme.appearance(style);
+ style: &'b <Renderer::Theme as container::StyleSheet>::Style,
+}
- let defaults = renderer::Style {
- text_color: style.text_color.unwrap_or(inherited_style.text_color),
- };
+impl<'a, 'b, Message, Renderer> overlay::Overlay<Message, Renderer>
+ for Overlay<'a, 'b, Renderer>
+where
+ Renderer: text::Renderer,
+ Renderer::Theme: container::StyleSheet + widget::text::StyleSheet,
+{
+ fn layout(
+ &self,
+ renderer: &Renderer,
+ bounds: Size,
+ _position: Point,
+ ) -> layout::Node {
+ let viewport = Rectangle::with_size(bounds);
- let text_layout = layout_text(
+ let text_layout = Widget::<(), Renderer>::layout(
+ self.tooltip,
renderer,
&layout::Limits::new(
Size::ZERO,
- snap_within_viewport
+ self.snap_within_viewport
.then(|| viewport.size())
.unwrap_or(Size::INFINITY),
)
- .pad(Padding::new(padding)),
+ .pad(Padding::new(self.padding)),
);
let text_bounds = text_layout.bounds();
- let x_center = bounds.x + (bounds.width - text_bounds.width) / 2.0;
- let y_center = bounds.y + (bounds.height - text_bounds.height) / 2.0;
+ let x_center = self.content_bounds.x
+ + (self.content_bounds.width - text_bounds.width) / 2.0;
+ let y_center = self.content_bounds.y
+ + (self.content_bounds.height - text_bounds.height) / 2.0;
let mut tooltip_bounds = {
- let offset = match position {
+ let offset = match self.position {
Position::Top => Vector::new(
x_center,
- bounds.y - text_bounds.height - gap - padding,
+ self.content_bounds.y
+ - text_bounds.height
+ - self.gap
+ - self.padding,
),
Position::Bottom => Vector::new(
x_center,
- bounds.y + bounds.height + gap + padding,
+ self.content_bounds.y
+ + self.content_bounds.height
+ + self.gap
+ + self.padding,
),
Position::Left => Vector::new(
- bounds.x - text_bounds.width - gap - padding,
+ self.content_bounds.x
+ - text_bounds.width
+ - self.gap
+ - self.padding,
y_center,
),
Position::Right => Vector::new(
- bounds.x + bounds.width + gap + padding,
+ self.content_bounds.x
+ + self.content_bounds.width
+ + self.gap
+ + self.padding,
y_center,
),
Position::FollowCursor => Vector::new(
- cursor_position.x,
- cursor_position.y - text_bounds.height,
+ self.cursor_position.x,
+ self.cursor_position.y - text_bounds.height,
),
};
Rectangle {
- x: offset.x - padding,
- y: offset.y - padding,
- width: text_bounds.width + padding * 2.0,
- height: text_bounds.height + padding * 2.0,
+ x: offset.x - self.padding,
+ y: offset.y - self.padding,
+ width: text_bounds.width + self.padding * 2.0,
+ height: text_bounds.height + self.padding * 2.0,
}
};
- if snap_within_viewport {
+ if self.snap_within_viewport {
if tooltip_bounds.x < viewport.x {
tooltip_bounds.x = viewport.x;
} else if viewport.x + viewport.width
@@ -366,22 +398,49 @@ pub fn draw<Renderer>(
}
}
- renderer.with_layer(Rectangle::with_size(Size::INFINITY), |renderer| {
- container::draw_background(renderer, &style, tooltip_bounds);
-
- draw_text(
- renderer,
- &defaults,
- Layout::with_offset(
- Vector::new(
- tooltip_bounds.x + padding,
- tooltip_bounds.y + padding,
- ),
- &text_layout,
- ),
- cursor_position,
- viewport,
- )
- });
+ layout::Node::with_children(
+ tooltip_bounds.size(),
+ vec![text_layout.translate(Vector::new(self.padding, self.padding))],
+ )
+ .translate(Vector::new(tooltip_bounds.x, tooltip_bounds.y))
+ }
+
+ fn draw(
+ &self,
+ renderer: &mut Renderer,
+ theme: &<Renderer as renderer::Renderer>::Theme,
+ inherited_style: &renderer::Style,
+ layout: Layout<'_>,
+ cursor_position: mouse::Cursor,
+ ) {
+ let style = <Renderer::Theme as container::StyleSheet>::appearance(
+ theme, self.style,
+ );
+
+ container::draw_background(renderer, &style, layout.bounds());
+
+ let defaults = renderer::Style {
+ text_color: style.text_color.unwrap_or(inherited_style.text_color),
+ };
+
+ Widget::<(), Renderer>::draw(
+ self.tooltip,
+ &widget::Tree::empty(),
+ renderer,
+ theme,
+ &defaults,
+ layout.children().next().unwrap(),
+ cursor_position,
+ &Rectangle::with_size(Size::INFINITY),
+ );
+ }
+
+ fn is_over(
+ &self,
+ _layout: Layout<'_>,
+ _renderer: &Renderer,
+ _cursor_position: Point,
+ ) -> bool {
+ false
}
}
diff --git a/native/src/widget/vertical_slider.rs b/widget/src/vertical_slider.rs
index f1687e38..91f2b466 100644
--- a/native/src/widget/vertical_slider.rs
+++ b/widget/src/vertical_slider.rs
@@ -3,13 +3,18 @@
//! A [`VerticalSlider`] has some local [`State`].
use std::ops::RangeInclusive;
-pub use iced_style::slider::{Appearance, Handle, HandleShape, StyleSheet};
-
-use crate::event::{self, Event};
-use crate::widget::tree::{self, Tree};
-use crate::{
- layout, mouse, renderer, touch, Background, Clipboard, Color, Element,
- Layout, Length, Pixels, Point, Rectangle, Shell, Size, Widget,
+pub use crate::style::slider::{Appearance, Handle, HandleShape, StyleSheet};
+
+use crate::core;
+use crate::core::event::{self, Event};
+use crate::core::layout::{self, Layout};
+use crate::core::mouse;
+use crate::core::renderer;
+use crate::core::touch;
+use crate::core::widget::tree::{self, Tree};
+use crate::core::{
+ Clipboard, Color, Element, Length, Pixels, Point, Rectangle, Shell, Size,
+ Widget,
};
/// An vertical bar and a handle that selects a single value from a range of
@@ -21,11 +26,9 @@ use crate::{
/// to 1 unit.
///
/// # Example
-/// ```
-/// # use iced_native::widget::vertical_slider;
-/// # use iced_native::renderer::Null;
-/// #
-/// # type VerticalSlider<'a, T, Message> = vertical_slider::VerticalSlider<'a, T, Message, Null>;
+/// ```no_run
+/// # type VerticalSlider<'a, T, Message> =
+/// # iced_widget::VerticalSlider<'a, T, Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
/// #
/// #[derive(Clone)]
/// pub enum Message {
@@ -37,9 +40,9 @@ use crate::{
/// VerticalSlider::new(0.0..=100.0, value, Message::SliderChanged);
/// ```
#[allow(missing_debug_implementations)]
-pub struct VerticalSlider<'a, T, Message, Renderer>
+pub struct VerticalSlider<'a, T, Message, Renderer = crate::Renderer>
where
- Renderer: crate::Renderer,
+ Renderer: core::Renderer,
Renderer::Theme: StyleSheet,
{
range: RangeInclusive<T>,
@@ -56,7 +59,7 @@ impl<'a, T, Message, Renderer> VerticalSlider<'a, T, Message, Renderer>
where
T: Copy + From<u8> + std::cmp::PartialOrd,
Message: Clone,
- Renderer: crate::Renderer,
+ Renderer: core::Renderer,
Renderer::Theme: StyleSheet,
{
/// The default width of a [`VerticalSlider`].
@@ -142,7 +145,7 @@ impl<'a, T, Message, Renderer> Widget<Message, Renderer>
where
T: Copy + Into<f64> + num_traits::FromPrimitive,
Message: Clone,
- Renderer: crate::Renderer,
+ Renderer: core::Renderer,
Renderer::Theme: StyleSheet,
{
fn tag(&self) -> tree::Tag {
@@ -177,7 +180,7 @@ where
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
@@ -185,7 +188,7 @@ where
update(
event,
layout,
- cursor_position,
+ cursor,
shell,
tree.state.downcast_mut::<State>(),
&mut self.value,
@@ -203,13 +206,13 @@ where
theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
draw(
renderer,
layout,
- cursor_position,
+ cursor,
tree.state.downcast_ref::<State>(),
self.value,
&self.range,
@@ -222,15 +225,11 @@ where
&self,
tree: &Tree,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
- mouse_interaction(
- layout,
- cursor_position,
- tree.state.downcast_ref::<State>(),
- )
+ mouse_interaction(layout, cursor, tree.state.downcast_ref::<State>())
}
}
@@ -239,7 +238,7 @@ impl<'a, T, Message, Renderer> From<VerticalSlider<'a, T, Message, Renderer>>
where
T: 'a + Copy + Into<f64> + num_traits::FromPrimitive,
Message: 'a + Clone,
- Renderer: 'a + crate::Renderer,
+ Renderer: 'a + core::Renderer,
Renderer::Theme: StyleSheet,
{
fn from(
@@ -254,7 +253,7 @@ where
pub fn update<Message, T>(
event: Event,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
shell: &mut Shell<'_, Message>,
state: &mut State,
value: &mut T,
@@ -269,8 +268,9 @@ where
{
let is_dragging = state.is_dragging;
- let mut change = || {
+ let mut change = |cursor_position: Point| {
let bounds = layout.bounds();
+
let new_value = if cursor_position.y >= bounds.y + bounds.height {
*range.start()
} else if cursor_position.y <= bounds.y {
@@ -304,8 +304,9 @@ where
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
- if layout.bounds().contains(cursor_position) {
- change();
+ if let Some(cursor_position) = cursor.position_over(layout.bounds())
+ {
+ change(cursor_position);
state.is_dragging = true;
return event::Status::Captured;
@@ -326,7 +327,7 @@ where
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => {
if is_dragging {
- change();
+ let _ = cursor.position().map(change);
return event::Status::Captured;
}
@@ -341,7 +342,7 @@ where
pub fn draw<T, R>(
renderer: &mut R,
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
state: &State,
value: T,
range: &RangeInclusive<T>,
@@ -349,11 +350,11 @@ pub fn draw<T, R>(
style: &<R::Theme as StyleSheet>::Style,
) where
T: Into<f64> + Copy,
- R: crate::Renderer,
+ R: core::Renderer,
R::Theme: StyleSheet,
{
let bounds = layout.bounds();
- let is_mouse_over = bounds.contains(cursor_position);
+ let is_mouse_over = cursor.is_over(bounds);
let style = if state.is_dragging {
style_sheet.dragging(style)
@@ -363,72 +364,72 @@ pub fn draw<T, R>(
style_sheet.active(style)
};
- let rail_x = bounds.x + (bounds.width / 2.0).round();
+ let (handle_width, handle_height, handle_border_radius) =
+ match style.handle.shape {
+ HandleShape::Circle { radius } => {
+ (radius * 2.0, radius * 2.0, radius.into())
+ }
+ HandleShape::Rectangle {
+ width,
+ border_radius,
+ } => (f32::from(width), bounds.width, border_radius),
+ };
+
+ let value = value.into() as f32;
+ let (range_start, range_end) = {
+ let (start, end) = range.clone().into_inner();
+
+ (start.into() as f32, end.into() as f32)
+ };
+
+ let offset = if range_start >= range_end {
+ 0.0
+ } else {
+ (bounds.height - handle_width) * (value - range_end)
+ / (range_start - range_end)
+ };
+
+ let rail_x = bounds.x + bounds.width / 2.0;
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
- x: rail_x - 1.0,
+ x: rail_x - style.rail.width / 2.0,
y: bounds.y,
- width: 2.0,
- height: bounds.height,
+ width: style.rail.width,
+ height: offset + handle_width / 2.0,
},
- border_radius: 0.0.into(),
+ border_radius: style.rail.border_radius,
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
- style.rail_colors.0,
+ style.rail.colors.1,
);
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
- x: rail_x + 1.0,
- y: bounds.y,
- width: 2.0,
- height: bounds.height,
+ x: rail_x - style.rail.width / 2.0,
+ y: bounds.y + offset + handle_width / 2.0,
+ width: style.rail.width,
+ height: bounds.height - offset - handle_width / 2.0,
},
- border_radius: 0.0.into(),
+ border_radius: style.rail.border_radius,
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
- Background::Color(style.rail_colors.1),
+ style.rail.colors.0,
);
- 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), bounds.width, border_radius),
- };
-
- let value = value.into() as f32;
- let (range_start, range_end) = {
- let (start, end) = range.clone().into_inner();
-
- (start.into() as f32, end.into() as f32)
- };
-
- let handle_offset = if range_start >= range_end {
- 0.0
- } else {
- (bounds.height - handle_width) * (value - range_end)
- / (range_start - range_end)
- };
-
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
- x: rail_x - (handle_height / 2.0),
- y: bounds.y + handle_offset.round(),
+ x: rail_x - handle_height / 2.0,
+ y: bounds.y + offset,
width: handle_height,
height: handle_width,
},
- border_radius: handle_border_radius.into(),
+ border_radius: handle_border_radius,
border_width: style.handle.border_width,
border_color: style.handle.border_color,
},
@@ -439,11 +440,11 @@ pub fn draw<T, R>(
/// Computes the current [`mouse::Interaction`] of a [`VerticalSlider`].
pub fn mouse_interaction(
layout: Layout<'_>,
- cursor_position: Point,
+ cursor: mouse::Cursor,
state: &State,
) -> mouse::Interaction {
let bounds = layout.bounds();
- let is_mouse_over = bounds.contains(cursor_position);
+ let is_mouse_over = cursor.is_over(bounds);
if state.is_dragging {
mouse::Interaction::Grabbing
diff --git a/winit/Cargo.toml b/winit/Cargo.toml
index 9efd1890..a4c0a402 100644
--- a/winit/Cargo.toml
+++ b/winit/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "iced_winit"
-version = "0.8.0"
+version = "0.9.1"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
description = "A winit runtime for Iced"
@@ -11,34 +11,41 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"]
categories = ["gui"]
[features]
+default = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"]
trace = ["tracing", "tracing-core", "tracing-subscriber"]
chrome-trace = ["trace", "tracing-chrome"]
-debug = ["iced_native/debug"]
+debug = ["iced_runtime/debug"]
system = ["sysinfo"]
application = []
+x11 = ["winit/x11"]
+wayland = ["winit/wayland"]
+wayland-dlopen = ["winit/wayland-dlopen"]
+wayland-csd-adwaita = ["winit/wayland-csd-adwaita"]
multi-window = []
[dependencies]
-window_clipboard = "0.2"
+window_clipboard = "0.3"
log = "0.4"
thiserror = "1.0"
+raw-window-handle = "0.5"
[dependencies.winit]
-version = "0.27"
+version = "0.28"
git = "https://github.com/iced-rs/winit.git"
-rev = "940457522e9fb9f5dac228b0ecfafe0138b4048c"
+rev = "c52db2045d0a2f1b8d9923870de1d4ab1994146e"
+default-features = false
-[dependencies.iced_native]
-version = "0.9"
-path = "../native"
+[dependencies.iced_runtime]
+version = "0.1"
+path = "../runtime"
[dependencies.iced_graphics]
-version = "0.7"
+version = "0.8"
path = "../graphics"
-[dependencies.iced_futures]
-version = "0.6"
-path = "../futures"
+[dependencies.iced_style]
+version = "0.8"
+path = "../style"
[dependencies.tracing]
version = "0.1.37"
diff --git a/winit/README.md b/winit/README.md
index 83810473..91307970 100644
--- a/winit/README.md
+++ b/winit/README.md
@@ -20,7 +20,7 @@ It exposes a renderer-agnostic `Application` trait that can be implemented and t
Add `iced_winit` as a dependency in your `Cargo.toml`:
```toml
-iced_winit = "0.8"
+iced_winit = "0.9"
```
__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 fe97486f..ab7b2495 100644
--- a/winit/src/application.rs
+++ b/winit/src/application.rs
@@ -3,25 +3,25 @@ mod state;
pub use state::State;
-use crate::clipboard::{self, Clipboard};
use crate::conversion;
-use crate::mouse;
-use crate::renderer;
-use crate::widget::operation;
-use crate::{
- Command, Debug, Error, Event, Executor, Proxy, Runtime, Settings, Size,
- Subscription,
-};
-
-use iced_futures::futures;
-use iced_futures::futures::channel::mpsc;
-use iced_graphics::compositor;
-use iced_graphics::window;
-use iced_native::program::Program;
-use iced_native::time::Instant;
-use iced_native::user_interface::{self, UserInterface};
-
-pub use iced_native::application::{Appearance, StyleSheet};
+use crate::core;
+use crate::core::mouse;
+use crate::core::renderer;
+use crate::core::time::Instant;
+use crate::core::widget::operation;
+use crate::core::window;
+use crate::core::{Event, Size};
+use crate::futures::futures;
+use crate::futures::{Executor, Runtime, Subscription};
+use crate::graphics::compositor::{self, Compositor};
+use crate::runtime::clipboard;
+use crate::runtime::program::Program;
+use crate::runtime::user_interface::{self, UserInterface};
+use crate::runtime::{Command, Debug};
+use crate::style::application::{Appearance, StyleSheet};
+use crate::{Clipboard, Error, Proxy, Settings};
+
+use futures::channel::mpsc;
use std::mem::ManuallyDrop;
@@ -43,7 +43,7 @@ use tracing::{info_span, instrument::Instrument};
/// can be toggled by pressing `F12`.
pub trait Application: Program
where
- <Self::Renderer as crate::Renderer>::Theme: StyleSheet,
+ <Self::Renderer as core::Renderer>::Theme: StyleSheet,
{
/// The data needed to initialize your [`Application`].
type Flags;
@@ -65,12 +65,12 @@ where
fn title(&self) -> String;
/// Returns the current `Theme` of the [`Application`].
- fn theme(&self) -> <Self::Renderer as crate::Renderer>::Theme;
+ fn theme(&self) -> <Self::Renderer as core::Renderer>::Theme;
/// Returns the `Style` variation of the `Theme`.
fn style(
&self,
- ) -> <<Self::Renderer as crate::Renderer>::Theme as StyleSheet>::Style {
+ ) -> <<Self::Renderer as core::Renderer>::Theme as StyleSheet>::Style {
Default::default()
}
@@ -110,8 +110,8 @@ pub fn run<A, E, C>(
where
A: Application + 'static,
E: Executor + 'static,
- C: window::Compositor<Renderer = A::Renderer> + 'static,
- <A::Renderer as crate::Renderer>::Theme: StyleSheet,
+ C: Compositor<Renderer = A::Renderer> + 'static,
+ <A::Renderer as core::Renderer>::Theme: StyleSheet,
{
use futures::task;
use futures::Future;
@@ -177,13 +177,17 @@ where
.unwrap_or(None)
});
- let _ = match target {
- Some(node) => node
- .replace_child(&canvas, &node)
- .expect(&format!("Could not replace #{}", node.id())),
- None => body
- .append_child(&canvas)
- .expect("Append canvas to HTML body"),
+ match target {
+ Some(node) => {
+ let _ = node
+ .replace_with_with_node_1(&canvas)
+ .expect(&format!("Could not replace #{}", node.id()));
+ }
+ None => {
+ let _ = body
+ .append_child(&canvas)
+ .expect("Append canvas to HTML body");
+ }
};
}
@@ -276,28 +280,25 @@ async fn run_instance<A, E, C>(
) where
A: Application + 'static,
E: Executor + 'static,
- C: window::Compositor<Renderer = A::Renderer> + 'static,
- <A::Renderer as crate::Renderer>::Theme: StyleSheet,
+ C: Compositor<Renderer = A::Renderer> + 'static,
+ <A::Renderer as core::Renderer>::Theme: StyleSheet,
{
- use iced_futures::futures::stream::StreamExt;
+ use futures::stream::StreamExt;
use winit::event;
use winit::event_loop::ControlFlow;
- let mut clipboard = Clipboard::connect(&window);
- let mut cache = user_interface::Cache::default();
- let mut surface = compositor.create_surface(&window);
- let mut should_exit = false;
-
let mut state = State::new(&application, &window);
let mut viewport_version = state.viewport_version();
-
let physical_size = state.physical_size();
- compositor.configure_surface(
- &mut surface,
+ let mut clipboard = Clipboard::connect(&window);
+ let mut cache = user_interface::Cache::default();
+ let mut surface = compositor.create_surface(
+ &window,
physical_size.width,
physical_size.height,
);
+ let mut should_exit = false;
if should_be_visible {
window.set_visible(true);
@@ -305,6 +306,8 @@ async fn run_instance<A, E, C>(
run_command(
&application,
+ &mut compositor,
+ &mut surface,
&mut cache,
&state,
&mut renderer,
@@ -315,9 +318,8 @@ async fn run_instance<A, E, C>(
&mut proxy,
&mut debug,
&window,
- || compositor.fetch_information(),
);
- runtime.track(application.subscription());
+ runtime.track(application.subscription().into_recipes());
let mut user_interface = ManuallyDrop::new(build_user_interface(
&application,
@@ -353,7 +355,7 @@ async fn run_instance<A, E, C>(
let (interface_state, statuses) = user_interface.update(
&events,
- state.cursor_position(),
+ state.cursor(),
&mut renderer,
&mut clipboard,
&mut messages,
@@ -361,8 +363,10 @@ async fn run_instance<A, E, C>(
debug.event_processing_finished();
- for event in events.drain(..).zip(statuses.into_iter()) {
- runtime.broadcast(event);
+ for (event, status) in
+ events.drain(..).zip(statuses.into_iter())
+ {
+ runtime.broadcast(event, status);
}
if !messages.is_empty()
@@ -377,6 +381,8 @@ async fn run_instance<A, E, C>(
// Update application
update(
&mut application,
+ &mut compositor,
+ &mut surface,
&mut cache,
&state,
&mut renderer,
@@ -387,7 +393,6 @@ async fn run_instance<A, E, C>(
&mut debug,
&mut messages,
&window,
- || compositor.fetch_information(),
);
// Update window
@@ -412,13 +417,13 @@ async fn run_instance<A, E, C>(
// Then, we can use the `interface_state` here to decide if a redraw
// is needed right away, or simply wait until a specific time.
let redraw_event = Event::Window(
- crate::window::Id::MAIN,
- crate::window::Event::RedrawRequested(Instant::now()),
+ window::Id::MAIN,
+ window::Event::RedrawRequested(Instant::now()),
);
let (interface_state, _) = user_interface.update(
&[redraw_event.clone()],
- state.cursor_position(),
+ state.cursor(),
&mut renderer,
&mut clipboard,
&mut messages,
@@ -431,7 +436,7 @@ async fn run_instance<A, E, C>(
&renderer::Style {
text_color: state.text_color(),
},
- state.cursor_position(),
+ state.cursor(),
);
debug.draw_finished();
@@ -444,17 +449,14 @@ async fn run_instance<A, E, C>(
}
window.request_redraw();
- runtime
- .broadcast((redraw_event, crate::event::Status::Ignored));
+ runtime.broadcast(redraw_event, core::event::Status::Ignored);
let _ = control_sender.start_send(match interface_state {
user_interface::State::Updated {
redraw_request: Some(redraw_request),
} => match redraw_request {
- crate::window::RedrawRequest::NextFrame => {
- ControlFlow::Poll
- }
- crate::window::RedrawRequest::At(at) => {
+ window::RedrawRequest::NextFrame => ControlFlow::Poll,
+ window::RedrawRequest::At(at) => {
ControlFlow::WaitUntil(at)
}
},
@@ -466,9 +468,9 @@ async fn run_instance<A, E, C>(
event::Event::PlatformSpecific(event::PlatformSpecific::MacOS(
event::MacOS::ReceivedUrl(url),
)) => {
- use iced_native::event;
+ use crate::core::event;
- events.push(iced_native::Event::PlatformSpecific(
+ events.push(Event::PlatformSpecific(
event::PlatformSpecific::MacOS(event::MacOS::ReceivedUrl(
url,
)),
@@ -507,7 +509,7 @@ async fn run_instance<A, E, C>(
&renderer::Style {
text_color: state.text_color(),
},
- state.cursor_position(),
+ state.cursor(),
);
if new_mouse_interaction != mouse_interaction {
@@ -568,7 +570,7 @@ async fn run_instance<A, E, C>(
state.update(&window, &window_event, &mut debug);
if let Some(event) = conversion::window_event(
- crate::window::Id::MAIN,
+ window::Id::MAIN,
&window_event,
state.scale_factor(),
state.modifiers(),
@@ -618,7 +620,7 @@ pub fn build_user_interface<'a, A: Application>(
debug: &mut Debug,
) -> UserInterface<'a, A::Message, A::Renderer>
where
- <A::Renderer as crate::Renderer>::Theme: StyleSheet,
+ <A::Renderer as core::Renderer>::Theme: StyleSheet,
{
#[cfg(feature = "trace")]
let view_span = info_span!("Application", "VIEW").entered();
@@ -645,8 +647,10 @@ where
/// Updates an [`Application`] by feeding it the provided messages, spawning any
/// resulting [`Command`], and tracking its [`Subscription`].
-pub fn update<A: Application, E: Executor>(
+pub fn update<A: Application, C, E: Executor>(
application: &mut A,
+ compositor: &mut C,
+ surface: &mut C::Surface,
cache: &mut user_interface::Cache,
state: &State<A>,
renderer: &mut A::Renderer,
@@ -657,9 +661,9 @@ pub fn update<A: Application, E: Executor>(
debug: &mut Debug,
messages: &mut Vec<A::Message>,
window: &winit::window::Window,
- graphics_info: impl FnOnce() -> compositor::Information + Copy,
) where
- <A::Renderer as crate::Renderer>::Theme: StyleSheet,
+ C: Compositor<Renderer = A::Renderer> + 'static,
+ <A::Renderer as core::Renderer>::Theme: StyleSheet,
{
for message in messages.drain(..) {
#[cfg(feature = "trace")]
@@ -676,6 +680,8 @@ pub fn update<A: Application, E: Executor>(
run_command(
application,
+ compositor,
+ surface,
cache,
state,
renderer,
@@ -686,17 +692,18 @@ pub fn update<A: Application, E: Executor>(
proxy,
debug,
window,
- graphics_info,
);
}
let subscription = application.subscription();
- runtime.track(subscription);
+ runtime.track(subscription.into_recipes());
}
/// Runs the actions of a [`Command`].
-pub fn run_command<A, E>(
+pub fn run_command<A, C, E>(
application: &A,
+ compositor: &mut C,
+ surface: &mut C::Surface,
cache: &mut user_interface::Cache,
state: &State<A>,
renderer: &mut A::Renderer,
@@ -707,15 +714,15 @@ pub fn run_command<A, E>(
proxy: &mut winit::event_loop::EventLoopProxy<A::Message>,
debug: &mut Debug,
window: &winit::window::Window,
- _graphics_info: impl FnOnce() -> compositor::Information + Copy,
) where
A: Application,
E: Executor,
- <A::Renderer as crate::Renderer>::Theme: StyleSheet,
+ C: Compositor<Renderer = A::Renderer> + 'static,
+ <A::Renderer as core::Renderer>::Theme: StyleSheet,
{
- use iced_native::command;
- use iced_native::system;
- use iced_native::window;
+ use crate::runtime::command;
+ use crate::runtime::system;
+ use crate::runtime::window;
for action in command.actions() {
match action {
@@ -746,12 +753,22 @@ pub fn run_command<A, E>(
"Spawning a window is only available with `multi_window::Application`s."
)
}
- window::Action::Resize { width, height } => {
+ window::Action::Resize(size) => {
window.set_inner_size(winit::dpi::LogicalSize {
- width,
- height,
+ width: size.width,
+ height: size.height,
});
}
+ window::Action::FetchSize(callback) => {
+ let size = window.inner_size();
+
+ proxy
+ .send_event(callback(Size::new(
+ size.width,
+ size.height,
+ )))
+ .expect("Send message to event loop")
+ }
window::Action::Maximize(maximized) => {
window.set_maximized(maximized);
}
@@ -771,11 +788,14 @@ pub fn run_command<A, E>(
mode,
));
}
+ window::Action::ChangeIcon(icon) => {
+ window.set_window_icon(conversion::icon(icon))
+ }
window::Action::FetchMode(tag) => {
let mode = if window.is_visible().unwrap_or(true) {
conversion::mode(window.fullscreen())
} else {
- window::Mode::Hidden
+ core::window::Mode::Hidden
};
proxy
@@ -796,20 +816,36 @@ pub fn run_command<A, E>(
window::Action::GainFocus => {
window.focus_window();
}
- window::Action::ChangeAlwaysOnTop(on_top) => {
- window.set_always_on_top(on_top);
+ window::Action::ChangeLevel(level) => {
+ window.set_window_level(conversion::window_level(level));
}
window::Action::FetchId(tag) => {
proxy
.send_event(tag(window.id().into()))
.expect("Send message to event loop");
}
+ window::Action::Screenshot(tag) => {
+ let bytes = compositor.screenshot(
+ renderer,
+ surface,
+ state.viewport(),
+ state.background_color(),
+ &debug.overlay(),
+ );
+
+ proxy
+ .send_event(tag(window::Screenshot::new(
+ bytes,
+ state.physical_size(),
+ )))
+ .expect("Send message to event loop.")
+ }
},
command::Action::System(action) => match action {
system::Action::QueryInformation(_tag) => {
#[cfg(feature = "system")]
{
- let graphics_info = _graphics_info();
+ let graphics_info = compositor.fetch_information();
let proxy = proxy.clone();
let _ = std::thread::spawn(move || {
@@ -827,7 +863,7 @@ pub fn run_command<A, E>(
},
command::Action::Widget(action) => {
let mut current_cache = std::mem::take(cache);
- let mut current_operation = Some(action.into_operation());
+ let mut current_operation = Some(action);
let mut user_interface = build_user_interface(
application,
@@ -856,6 +892,16 @@ pub fn run_command<A, E>(
current_cache = user_interface.into_cache();
*cache = current_cache;
}
+ command::Action::LoadFont { bytes, tagger } => {
+ use crate::core::text::Renderer;
+
+ // TODO: Error handling (?)
+ renderer.load_font(bytes);
+
+ proxy
+ .send_event(tagger(Ok(())))
+ .expect("Send message to event loop");
+ }
}
}
}
diff --git a/winit/src/application/state.rs b/winit/src/application/state.rs
index 8d6a1df1..967f43f2 100644
--- a/winit/src/application/state.rs
+++ b/winit/src/application/state.rs
@@ -1,6 +1,11 @@
use crate::application::{self, StyleSheet as _};
use crate::conversion;
-use crate::{Application, Color, Debug, Point, Size, Viewport};
+use crate::core;
+use crate::core::mouse;
+use crate::core::{Color, Size};
+use crate::graphics::Viewport;
+use crate::runtime::Debug;
+use crate::Application;
use std::marker::PhantomData;
use winit::event::{Touch, WindowEvent};
@@ -10,22 +15,22 @@ use winit::window::Window;
#[allow(missing_debug_implementations)]
pub struct State<A: Application>
where
- <A::Renderer as crate::Renderer>::Theme: application::StyleSheet,
+ <A::Renderer as core::Renderer>::Theme: application::StyleSheet,
{
title: String,
scale_factor: f64,
viewport: Viewport,
viewport_version: usize,
- cursor_position: winit::dpi::PhysicalPosition<f64>,
+ cursor_position: Option<winit::dpi::PhysicalPosition<f64>>,
modifiers: winit::event::ModifiersState,
- theme: <A::Renderer as crate::Renderer>::Theme,
+ theme: <A::Renderer as core::Renderer>::Theme,
appearance: application::Appearance,
application: PhantomData<A>,
}
impl<A: Application> State<A>
where
- <A::Renderer as crate::Renderer>::Theme: application::StyleSheet,
+ <A::Renderer as core::Renderer>::Theme: application::StyleSheet,
{
/// Creates a new [`State`] for the provided [`Application`] and window.
pub fn new(application: &A, window: &Window) -> Self {
@@ -48,8 +53,7 @@ where
scale_factor,
viewport,
viewport_version: 0,
- // TODO: Encode cursor availability in the type-system
- cursor_position: winit::dpi::PhysicalPosition::new(-1.0, -1.0),
+ cursor_position: None,
modifiers: winit::event::ModifiersState::default(),
theme,
appearance,
@@ -85,11 +89,16 @@ where
}
/// Returns the current cursor position of the [`State`].
- pub fn cursor_position(&self) -> Point {
- conversion::cursor_position(
- self.cursor_position,
- self.viewport.scale_factor(),
- )
+ pub fn cursor(&self) -> mouse::Cursor {
+ self.cursor_position
+ .map(|cursor_position| {
+ conversion::cursor_position(
+ cursor_position,
+ self.viewport.scale_factor(),
+ )
+ })
+ .map(mouse::Cursor::Available)
+ .unwrap_or(mouse::Cursor::Unavailable)
}
/// Returns the current keyboard modifiers of the [`State`].
@@ -98,7 +107,7 @@ where
}
/// Returns the current theme of the [`State`].
- pub fn theme(&self) -> &<A::Renderer as crate::Renderer>::Theme {
+ pub fn theme(&self) -> &<A::Renderer as core::Renderer>::Theme {
&self.theme
}
@@ -149,12 +158,10 @@ where
| WindowEvent::Touch(Touch {
location: position, ..
}) => {
- self.cursor_position = *position;
+ self.cursor_position = Some(*position);
}
WindowEvent::CursorLeft { .. } => {
- // TODO: Encode cursor availability in the type-system
- self.cursor_position =
- winit::dpi::PhysicalPosition::new(-1.0, -1.0);
+ self.cursor_position = None;
}
WindowEvent::ModifiersChanged(new_modifiers) => {
self.modifiers = *new_modifiers;
diff --git a/winit/src/clipboard.rs b/winit/src/clipboard.rs
index c1fd8813..7271441d 100644
--- a/winit/src/clipboard.rs
+++ b/winit/src/clipboard.rs
@@ -1,7 +1,4 @@
//! Access the clipboard.
-pub use iced_native::clipboard::Action;
-
-use crate::command::{self, Command};
/// A buffer for short-term storage and transfer within and between
/// applications.
@@ -56,7 +53,7 @@ impl Clipboard {
}
}
-impl iced_native::Clipboard for Clipboard {
+impl crate::core::Clipboard for Clipboard {
fn read(&self) -> Option<String> {
self.read()
}
@@ -65,15 +62,3 @@ impl iced_native::Clipboard for Clipboard {
self.write(contents)
}
}
-
-/// Read the current contents of the clipboard.
-pub fn read<Message>(
- f: impl Fn(Option<String>) -> Message + 'static,
-) -> Command<Message> {
- Command::single(command::Action::Clipboard(Action::Read(Box::new(f))))
-}
-
-/// Write the given contents to the clipboard.
-pub fn write<Message>(contents: String) -> Command<Message> {
- Command::single(command::Action::Clipboard(Action::Write(contents)))
-}
diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs
index 5c86b30a..fe0fce19 100644
--- a/winit/src/conversion.rs
+++ b/winit/src/conversion.rs
@@ -1,12 +1,13 @@
//! Convert [`winit`] types into [`iced_native`] types, and viceversa.
//!
//! [`winit`]: https://github.com/rust-windowing/winit
-//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.8/native
-use crate::keyboard;
-use crate::mouse;
-use crate::touch;
-use crate::window;
-use crate::{Event, Point, Position};
+//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.9/native
+use crate::core::keyboard;
+use crate::core::mouse;
+use crate::core::touch;
+use crate::core::window;
+use crate::core::{Event, Point};
+use crate::Position;
/// Converts a winit window event into an iced event.
pub fn window_event(
@@ -149,6 +150,19 @@ pub fn window_event(
}
}
+/// Converts a [`window::Level`] to a [`winit`] window level.
+///
+/// [`winit`]: https://github.com/rust-windowing/winit
+pub fn window_level(level: window::Level) -> winit::window::WindowLevel {
+ match level {
+ window::Level::Normal => winit::window::WindowLevel::Normal,
+ window::Level::AlwaysOnBottom => {
+ winit::window::WindowLevel::AlwaysOnBottom
+ }
+ window::Level::AlwaysOnTop => winit::window::WindowLevel::AlwaysOnTop,
+ }
+}
+
/// Converts a [`Position`] to a [`winit`] logical position for a given monitor.
///
/// [`winit`]: https://github.com/rust-windowing/winit
@@ -228,7 +242,7 @@ pub fn mode(mode: Option<winit::window::Fullscreen>) -> window::Mode {
/// Converts a `MouseCursor` from [`iced_native`] to a [`winit`] cursor icon.
///
/// [`winit`]: https://github.com/rust-windowing/winit
-/// [`iced_native`]: https://github.com/iced-rs/iced/tree/0.8/native
+/// [`iced_native`]: https://github.com/iced-rs/iced/tree/0.9/native
pub fn mouse_interaction(
interaction: mouse::Interaction,
) -> winit::window::CursorIcon {
@@ -246,21 +260,20 @@ pub fn mouse_interaction(
winit::window::CursorIcon::EwResize
}
Interaction::ResizingVertically => winit::window::CursorIcon::NsResize,
+ Interaction::NotAllowed => winit::window::CursorIcon::NotAllowed,
}
}
/// Converts a `MouseButton` from [`winit`] to an [`iced_native`] mouse button.
///
/// [`winit`]: https://github.com/rust-windowing/winit
-/// [`iced_native`]: https://github.com/iced-rs/iced/tree/0.8/native
+/// [`iced_native`]: https://github.com/iced-rs/iced/tree/0.9/native
pub fn mouse_button(mouse_button: winit::event::MouseButton) -> mouse::Button {
match 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 as u8)
- }
+ winit::event::MouseButton::Other(other) => mouse::Button::Other(other),
}
}
@@ -268,7 +281,7 @@ pub fn mouse_button(mouse_button: winit::event::MouseButton) -> mouse::Button {
/// modifiers state.
///
/// [`winit`]: https://github.com/rust-windowing/winit
-/// [`iced_native`]: https://github.com/iced-rs/iced/tree/0.8/native
+/// [`iced_native`]: https://github.com/iced-rs/iced/tree/0.9/native
pub fn modifiers(
modifiers: winit::event::ModifiersState,
) -> keyboard::Modifiers {
@@ -295,7 +308,7 @@ pub fn cursor_position(
/// Converts a `Touch` from [`winit`] to an [`iced_native`] touch event.
///
/// [`winit`]: https://github.com/rust-windowing/winit
-/// [`iced_native`]: https://github.com/iced-rs/iced/tree/0.8/native
+/// [`iced_native`]: https://github.com/iced-rs/iced/tree/0.9/native
pub fn touch_event(
touch: winit::event::Touch,
scale_factor: f64,
@@ -326,7 +339,7 @@ pub fn touch_event(
/// Converts a `VirtualKeyCode` from [`winit`] to an [`iced_native`] key code.
///
/// [`winit`]: https://github.com/rust-windowing/winit
-/// [`iced_native`]: https://github.com/iced-rs/iced/tree/0.8/native
+/// [`iced_native`]: https://github.com/iced-rs/iced/tree/0.9/native
pub fn key_code(
virtual_keycode: winit::event::VirtualKeyCode,
) -> keyboard::KeyCode {
@@ -519,6 +532,15 @@ pub fn user_attention(
}
}
+/// Converts some [`Icon`] into it's `winit` counterpart.
+///
+/// Returns `None` if there is an error during the conversion.
+pub fn icon(icon: window::Icon) -> Option<winit::window::Icon> {
+ let (pixels, size) = icon.into_raw();
+
+ winit::window::Icon::from_rgba(pixels, size.width, size.height).ok()
+}
+
// As defined in: http://www.unicode.org/faq/private_use.html
pub(crate) fn is_private_use_character(c: char) -> bool {
matches!(
diff --git a/winit/src/error.rs b/winit/src/error.rs
index eaeafd51..7687fb17 100644
--- a/winit/src/error.rs
+++ b/winit/src/error.rs
@@ -1,4 +1,5 @@
-use iced_futures::futures;
+use crate::futures::futures;
+use crate::graphics;
/// An error that occurred while running an application.
#[derive(Debug, thiserror::Error)]
@@ -13,10 +14,10 @@ pub enum Error {
/// The application graphics context could not be created.
#[error("the application graphics context could not be created")]
- GraphicsCreationFailed(iced_graphics::Error),
+ GraphicsCreationFailed(graphics::Error),
}
-impl From<iced_graphics::Error> for Error {
+impl From<graphics::Error> for Error {
fn from(error: iced_graphics::Error) -> Error {
Error::GraphicsCreationFailed(error)
}
diff --git a/winit/src/icon.rs b/winit/src/icon.rs
index 4113d9d0..0fe010ca 100644
--- a/winit/src/icon.rs
+++ b/winit/src/icon.rs
@@ -1,182 +1,63 @@
//! Attach an icon to the window of your application.
-use std::fmt;
-use std::io;
-
-#[cfg(feature = "image_rs")]
-use std::path::Path;
-
-/// The icon of a window.
-#[derive(Clone)]
-pub struct Icon(winit::window::Icon);
+pub use crate::core::window::icon::*;
-impl fmt::Debug for Icon {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_tuple("Icon").field(&format_args!("_")).finish()
- }
-}
-
-impl Icon {
- /// Creates an icon from 32bpp RGBA data.
- pub fn from_rgba(
- rgba: Vec<u8>,
- width: u32,
- height: u32,
- ) -> Result<Self, Error> {
- let raw = winit::window::Icon::from_rgba(rgba, width, height)?;
+use crate::core::window::icon;
- Ok(Icon(raw))
- }
-
- /// Creates an icon from an image file.
- ///
- /// This will return an error in case the file is missing at run-time. You may prefer [`Self::from_file_data`] instead.
- #[cfg(feature = "image_rs")]
- pub fn from_file<P: AsRef<Path>>(icon_path: P) -> Result<Self, Error> {
- let icon = image_rs::io::Reader::open(icon_path)?.decode()?.to_rgba8();
+use std::io;
- Self::from_rgba(icon.to_vec(), icon.width(), icon.height())
- }
+#[cfg(feature = "image")]
+use std::path::Path;
- /// Creates an icon from the content of an image file.
- ///
- /// This content can be included in your application at compile-time, e.g. using the `include_bytes!` macro. \
- /// You can pass an explicit file format. Otherwise, the file format will be guessed at runtime.
- #[cfg(feature = "image_rs")]
- pub fn from_file_data(
- data: &[u8],
- explicit_format: Option<image_rs::ImageFormat>,
- ) -> Result<Self, Error> {
- let mut icon = image_rs::io::Reader::new(std::io::Cursor::new(data));
- let icon_with_format = match explicit_format {
- Some(format) => {
- icon.set_format(format);
- icon
- }
- None => icon.with_guessed_format()?,
- };
+/// Creates an icon from an image file.
+///
+/// This will return an error in case the file is missing at run-time. You may prefer [`Self::from_file_data`] instead.
+#[cfg(feature = "image")]
+pub fn from_file<P: AsRef<Path>>(icon_path: P) -> Result<Icon, Error> {
+ let icon = image_rs::io::Reader::open(icon_path)?.decode()?.to_rgba8();
+
+ Ok(icon::from_rgba(icon.to_vec(), icon.width(), icon.height())?)
+}
+
+/// Creates an icon from the content of an image file.
+///
+/// This content can be included in your application at compile-time, e.g. using the `include_bytes!` macro.
+/// You can pass an explicit file format. Otherwise, the file format will be guessed at runtime.
+#[cfg(feature = "image")]
+pub fn from_file_data(
+ data: &[u8],
+ explicit_format: Option<image_rs::ImageFormat>,
+) -> Result<Icon, Error> {
+ let mut icon = image_rs::io::Reader::new(std::io::Cursor::new(data));
+ let icon_with_format = match explicit_format {
+ Some(format) => {
+ icon.set_format(format);
+ icon
+ }
+ None => icon.with_guessed_format()?,
+ };
- let pixels = icon_with_format.decode()?.to_rgba8();
+ let pixels = icon_with_format.decode()?.to_rgba8();
- Self::from_rgba(pixels.to_vec(), pixels.width(), pixels.height())
- }
+ Ok(icon::from_rgba(
+ pixels.to_vec(),
+ pixels.width(),
+ pixels.height(),
+ )?)
}
-/// An error produced when using `Icon::from_rgba` with invalid arguments.
-#[derive(Debug)]
+/// An error produced when creating an [`Icon`].
+#[derive(Debug, thiserror::Error)]
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 [`Icon`] is not valid.
+ #[error("The icon is invalid: {0}")]
+ InvalidError(#[from] icon::Error),
/// The underlying OS failed to create the icon.
- OsError(io::Error),
-
- /// The `image` crate reported an error
- #[cfg(feature = "image_rs")]
- ImageError(image_rs::error::ImageError),
-}
-
-impl From<std::io::Error> for Error {
- fn from(os_error: std::io::Error) -> Self {
- Error::OsError(os_error)
- }
-}
-
-impl From<winit::window::BadIcon> for Error {
- fn from(error: winit::window::BadIcon) -> Self {
- use 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),
- }
- }
-}
-
-impl From<Icon> for winit::window::Icon {
- fn from(icon: Icon) -> Self {
- icon.0
- }
-}
-
-#[cfg(feature = "image_rs")]
-impl From<image_rs::error::ImageError> for Error {
- fn from(image_error: image_rs::error::ImageError) -> Self {
- Self::ImageError(image_error)
- }
-}
-
-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 {byte_count:?}) isn't divisble by \
- 4. Therefore, it cannot be safely interpreted as 32bpp RGBA \
- pixels."
- )
- }
- Error::DimensionsMismatch {
- width,
- height,
- pixel_count,
- } => {
- write!(
- f,
- "The number of RGBA pixels ({pixel_count:?}) does not match the provided \
- dimensions ({width:?}x{height:?})."
- )
- }
- Error::OsError(e) => write!(
- f,
- "The underlying OS failed to create the window \
- icon: {e:?}"
- ),
- #[cfg(feature = "image_rs")]
- Error::ImageError(e) => {
- write!(f, "Unable to create icon from a file: {e:?}")
- }
- }
- }
-}
-
-impl std::error::Error for Error {
- fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
- Some(self)
- }
-}
-
-impl TryFrom<iced_native::window::Icon> for Icon {
- type Error = Error;
+ #[error("The underlying OS failted to create the window icon: {0}")]
+ OsError(#[from] io::Error),
- fn try_from(icon: iced_native::window::Icon) -> Result<Self, Self::Error> {
- Icon::from_rgba(icon.rgba, icon.width, icon.height)
- }
+ /// The `image` crate reported an error.
+ #[cfg(feature = "image")]
+ #[error("Unable to create icon from a file: {0}")]
+ ImageError(#[from] image_rs::error::ImageError),
}
diff --git a/winit/src/lib.rs b/winit/src/lib.rs
index 54b9c31f..dc163430 100644
--- a/winit/src/lib.rs
+++ b/winit/src/lib.rs
@@ -11,7 +11,7 @@
//! Additionally, a [`conversion`] module is available for users that decide to
//! implement a custom event loop.
//!
-//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.8/native
+//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.9/native
//! [`winit`]: https://github.com/rust-windowing/winit
//! [`conversion`]: crate::conversion
#![doc(
@@ -25,14 +25,17 @@
clippy::from_over_into,
clippy::needless_borrow,
clippy::new_without_default,
- clippy::useless_conversion
+ clippy::useless_conversion,
+ unsafe_code
)]
-#![forbid(rust_2018_idioms, unsafe_code)]
+#![forbid(rust_2018_idioms)]
#![allow(clippy::inherent_to_string, clippy::type_complexity)]
-#![cfg_attr(docsrs, feature(doc_cfg))]
-
-#[doc(no_inline)]
-pub use iced_native::*;
+#![cfg_attr(docsrs, feature(doc_auto_cfg))]
+pub use iced_graphics as graphics;
+pub use iced_runtime as runtime;
+pub use iced_runtime::core;
+pub use iced_runtime::futures;
+pub use iced_style as style;
pub use winit;
#[cfg(feature = "multi-window")]
@@ -43,7 +46,6 @@ pub mod application;
pub mod clipboard;
pub mod conversion;
pub mod settings;
-pub mod window;
#[cfg(feature = "system")]
pub mod system;
diff --git a/winit/src/proxy.rs b/winit/src/proxy.rs
index 7b9074d7..1d6c48bb 100644
--- a/winit/src/proxy.rs
+++ b/winit/src/proxy.rs
@@ -1,4 +1,4 @@
-use iced_native::futures::{
+use crate::futures::futures::{
channel::mpsc,
task::{Context, Poll},
Sink,
diff --git a/winit/src/settings.rs b/winit/src/settings.rs
index 88d7c1de..40b3d487 100644
--- a/winit/src/settings.rs
+++ b/winit/src/settings.rs
@@ -22,7 +22,7 @@ mod platform;
pub use platform::PlatformSpecific;
use crate::conversion;
-use crate::Icon;
+use crate::core::window::{Icon, Level};
use crate::Position;
use winit::monitor::MonitorHandle;
@@ -50,20 +50,8 @@ pub struct Settings<Flags> {
/// Whether the [`Application`] should exit when the user requests the
/// window to close (e.g. the user presses the close button).
///
- /// NOTE: This is not used for `multi-window`, instead check [`Application::close_requested`].
- ///
- /// [`close_requested`]: crate::multi_window::Application::close_requested
- ///
/// [`Application`]: crate::Application
pub exit_on_close_request: bool,
-
- /// Whether the [`Application`] should try to build the context
- /// using OpenGL ES first then OpenGL.
- ///
- /// NOTE: Only works for the `glow` backend.
- ///
- /// [`Application`]: crate::Application
- pub try_opengles_first: bool,
}
/// The window settings of an application.
@@ -93,11 +81,11 @@ pub struct Window {
/// 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 [`Level`].
+ pub level: Level,
/// The window icon, which is also usually used in the taskbar
- pub icon: Option<winit::window::Icon>,
+ pub icon: Option<Icon>,
/// Platform specific settings.
pub platform_specific: platform::PlatformSpecific,
@@ -114,7 +102,7 @@ impl fmt::Debug for Window {
.field("resizable", &self.resizable)
.field("decorations", &self.decorations)
.field("transparent", &self.transparent)
- .field("always_on_top", &self.always_on_top)
+ .field("level", &self.level)
.field("icon", &self.icon.is_some())
.field("platform_specific", &self.platform_specific)
.finish()
@@ -139,8 +127,9 @@ impl Window {
.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_window_icon(self.icon.and_then(conversion::icon))
+ .with_window_level(conversion::window_level(self.level))
+ .with_visible(self.visible);
if let Some(position) = conversion::position(
primary_monitor.as_ref(),
@@ -168,7 +157,9 @@ impl Window {
target_os = "openbsd"
))]
{
- use ::winit::platform::unix::WindowBuilderExtUnix;
+ // `with_name` is available on both `WindowBuilderExtWayland` and `WindowBuilderExtX11` and they do
+ // exactly the same thing. We arbitrarily choose `WindowBuilderExtWayland` here.
+ use ::winit::platform::wayland::WindowBuilderExtWayland;
if let Some(id) = _id {
window_builder = window_builder.with_name(id.clone(), id);
@@ -178,11 +169,11 @@ impl Window {
#[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);
+ #[allow(unsafe_code)]
+ unsafe {
+ window_builder = window_builder
+ .with_parent_window(self.platform_specific.parent);
}
-
window_builder = window_builder
.with_drag_and_drop(self.platform_specific.drag_and_drop);
}
@@ -216,29 +207,9 @@ impl Default for Window {
resizable: true,
decorations: true,
transparent: false,
- always_on_top: false,
+ level: Level::default(),
icon: None,
platform_specific: Default::default(),
}
}
}
-
-impl From<iced_native::window::Settings> for Window {
- fn from(settings: iced_native::window::Settings) -> Self {
- Self {
- size: settings.size,
- position: settings.position,
- min_size: settings.min_size,
- max_size: settings.max_size,
- visible: settings.visible,
- resizable: settings.resizable,
- decorations: settings.decorations,
- transparent: settings.transparent,
- always_on_top: settings.always_on_top,
- icon: settings.icon.and_then(|icon| {
- Icon::try_from(icon).map(winit::window::Icon::from).ok()
- }),
- platform_specific: Default::default(),
- }
- }
-}
diff --git a/winit/src/settings/windows.rs b/winit/src/settings/windows.rs
index ff03a9c5..45d753bd 100644
--- a/winit/src/settings/windows.rs
+++ b/winit/src/settings/windows.rs
@@ -1,10 +1,11 @@
//! Platform specific settings for Windows.
+use raw_window_handle::RawWindowHandle;
/// The platform specific window settings of an application.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PlatformSpecific {
/// Parent window
- pub parent: Option<winit::platform::windows::HWND>,
+ pub parent: Option<RawWindowHandle>,
/// Drag and drop support
pub drag_and_drop: bool,
diff --git a/winit/src/system.rs b/winit/src/system.rs
index 8d8b018c..145a4d92 100644
--- a/winit/src/system.rs
+++ b/winit/src/system.rs
@@ -1,8 +1,7 @@
//! Access the native system.
-use crate::command::{self, Command};
-pub use iced_native::system::*;
-
-use iced_graphics::compositor;
+use crate::graphics::compositor;
+use crate::runtime::command::{self, Command};
+use crate::runtime::system::{Action, Information};
/// Query for available system information.
pub fn fetch_information<Message>(
diff --git a/winit/src/window.rs b/winit/src/window.rs
index 8fd415ef..e69de29b 100644
--- a/winit/src/window.rs
+++ b/winit/src/window.rs
@@ -1,128 +0,0 @@
-//! Interact with the window of your application.
-use crate::command::{self, Command};
-use iced_native::window;
-
-pub use window::{Event, Id, Mode, RedrawRequest, frames, UserAttention};
-
-/// Closes the window.
-pub fn close<Message>(id: window::Id) -> Command<Message> {
- Command::single(command::Action::Window(id, window::Action::Close))
-}
-
-/// Begins dragging the window while the left mouse button is held.
-pub fn drag<Message>(id: window::Id) -> Command<Message> {
- Command::single(command::Action::Window(id, window::Action::Drag))
-}
-
-/// Spawns a new window.
-pub fn spawn<Message>(
- id: window::Id,
- settings: window::Settings,
-) -> Command<Message> {
- Command::single(command::Action::Window(
- id,
- window::Action::Spawn { settings },
- ))
-}
-
-/// Resizes the window to the given logical dimensions.
-pub fn resize<Message>(
- id: window::Id,
- width: u32,
- height: u32,
-) -> Command<Message> {
- Command::single(command::Action::Window(
- id,
- window::Action::Resize { width, height },
- ))
-}
-
-/// Maximizes the window.
-pub fn maximize<Message>(id: window::Id, maximized: bool) -> Command<Message> {
- Command::single(command::Action::Window(
- id,
- window::Action::Maximize(maximized),
- ))
-}
-
-/// Minimes the window.
-pub fn minimize<Message>(id: window::Id, minimized: bool) -> Command<Message> {
- Command::single(command::Action::Window(
- id,
- window::Action::Minimize(minimized),
- ))
-}
-
-/// Moves a window to the given logical coordinates.
-pub fn move_to<Message>(id: window::Id, x: i32, y: i32) -> Command<Message> {
- Command::single(command::Action::Window(id, window::Action::Move { x, y }))
-}
-
-/// Changes the [`Mode`] of the window.
-pub fn change_mode<Message>(id: window::Id, mode: Mode) -> Command<Message> {
- Command::single(command::Action::Window(id, window::Action::ChangeMode(mode)))
-}
-
-/// Fetches the current [`Mode`] of the window.
-pub fn fetch_mode<Message>(
- id: window::Id,
- f: impl FnOnce(Mode) -> Message + 'static,
-) -> Command<Message> {
- Command::single(command::Action::Window(
- id,
- window::Action::FetchMode(Box::new(f)),
- ))
-}
-
-/// Toggles the window to maximized or back.
-pub fn toggle_maximize<Message>(id: window::Id) -> Command<Message> {
- Command::single(command::Action::Window(id, window::Action::ToggleMaximize))
-}
-
-/// Toggles the window decorations.
-pub fn toggle_decorations<Message>(id: window::Id) -> Command<Message> {
- Command::single(command::Action::Window(id, window::Action::ToggleDecorations))
-}
-
-/// Request user attention to the window, this has no effect if the application
-/// is already focused. How requesting for user attention manifests is platform dependent,
-/// see [`UserAttention`] for details.
-///
-/// Providing `None` will unset the request for user attention. Unsetting the request for
-/// user attention might not be done automatically by the WM when the window receives input.
-pub fn request_user_attention<Message>(
- id: window::Id,
- user_attention: Option<UserAttention>,
-) -> Command<Message> {
- Command::single(command::Action::Window(
- id,
- window::Action::RequestUserAttention(user_attention),
- ))
-}
-
-/// Brings the window to the front and sets input focus. Has no effect if the window is
-/// already in focus, minimized, or not visible.
-///
-/// This [`Command`] steals input focus from other applications. Do not use this method unless
-/// you are certain that's what the user wants. Focus stealing can cause an extremely disruptive
-/// user experience.
-pub fn gain_focus<Message>(id: window::Id) -> Command<Message> {
- Command::single(command::Action::Window(id, window::Action::GainFocus))
-}
-
-/// Changes whether or not the window will always be on top of other windows.
-pub fn change_always_on_top<Message>(id: window::Id, on_top: bool) -> Command<Message> {
- Command::single(command::Action::Window(id, window::Action::ChangeAlwaysOnTop(
- on_top,
- )))
-}
-
-/// Fetches an identifier unique to the window.
-pub fn fetch_id<Message>(
- id: window::Id,
- f: impl FnOnce(u64) -> Message + 'static,
-) -> Command<Message> {
- Command::single(command::Action::Window(id, window::Action::FetchId(Box::new(
- f,
- ))))
-}