diff options
author | 2023-07-12 12:23:18 -0700 | |
---|---|---|
committer | 2023-07-12 12:23:18 -0700 | |
commit | 633f405f3f78bc7f82d2b2061491b0e011137451 (patch) | |
tree | 5ebfc1f45d216a5c14a90492563599e6969eab4d | |
parent | 41836dd80d0534608e7aedfbf2319c540a23de1a (diff) | |
parent | 21bd51426d900e271206f314e0c915dd41065521 (diff) | |
download | iced-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.yml | 6 | ||||
-rw-r--r-- | .github/workflows/document.yml | 8 | ||||
-rw-r--r-- | .github/workflows/test.yml | 4 | ||||
-rw-r--r-- | CHANGELOG.md | 56 | ||||
-rw-r--r-- | Cargo.toml | 57 | ||||
-rw-r--r-- | DEPENDENCIES.md | 33 | ||||
-rw-r--r-- | README.md | 38 | ||||
-rw-r--r-- | ROADMAP.md | 14 | ||||
-rw-r--r-- | core/Cargo.toml | 10 | ||||
-rw-r--r-- | core/README.md | 2 | ||||
-rw-r--r-- | core/src/angle.rs | 33 | ||||
-rw-r--r-- | core/src/background.rs | 19 | ||||
-rw-r--r-- | core/src/border_radius.rs | 22 | ||||
-rw-r--r-- | core/src/clipboard.rs | 23 | ||||
-rw-r--r-- | core/src/color.rs | 40 | ||||
-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.rs | 116 | ||||
-rw-r--r-- | core/src/gradient.rs | 105 | ||||
-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.rs | 8 | ||||
-rw-r--r-- | core/src/lib.rs | 34 | ||||
-rw-r--r-- | core/src/mouse.rs | 5 | ||||
-rw-r--r-- | core/src/mouse/button.rs | 2 | ||||
-rw-r--r-- | core/src/mouse/click.rs (renamed from native/src/mouse/click.rs) | 0 | ||||
-rw-r--r-- | core/src/mouse/cursor.rs | 52 | ||||
-rw-r--r-- | core/src/mouse/interaction.rs | 10 | ||||
-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.rs | 6 | ||||
-rw-r--r-- | core/src/rectangle.rs | 48 | ||||
-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.rs | 2 | ||||
-rw-r--r-- | core/src/svg.rs (renamed from native/src/svg.rs) | 4 | ||||
-rw-r--r-- | core/src/text.rs | 212 | ||||
-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.rs | 226 | ||||
-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.rs | 15 | ||||
-rw-r--r-- | core/src/window/event.rs (renamed from native/src/window/event.rs) | 0 | ||||
-rw-r--r-- | core/src/window/icon.rs | 80 | ||||
-rw-r--r-- | core/src/window/id.rs (renamed from native/src/window/id.rs) | 0 | ||||
-rw-r--r-- | core/src/window/level.rs | 19 | ||||
-rw-r--r-- | core/src/window/mode.rs (renamed from native/src/window/mode.rs) | 0 | ||||
-rw-r--r-- | core/src/window/position.rs | 0 | ||||
-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.py | 52 | ||||
-rw-r--r-- | examples/README.md | 3 | ||||
-rw-r--r-- | examples/arc/src/main.rs | 12 | ||||
-rw-r--r-- | examples/bezier_tool/src/main.rs | 39 | ||||
-rw-r--r-- | examples/checkbox/fonts/icons.ttf | bin | 1272 -> 1784 bytes | |||
-rw-r--r-- | examples/checkbox/src/main.rs | 32 | ||||
-rw-r--r-- | examples/clock/src/main.rs | 16 | ||||
-rw-r--r-- | examples/color_palette/Cargo.toml | 2 | ||||
-rw-r--r-- | examples/color_palette/src/main.rs | 63 | ||||
-rw-r--r-- | examples/component/Cargo.toml | 4 | ||||
-rw-r--r-- | examples/component/src/main.rs | 24 | ||||
-rw-r--r-- | examples/custom_quad/Cargo.toml | 3 | ||||
-rw-r--r-- | examples/custom_quad/src/main.rs | 11 | ||||
-rw-r--r-- | examples/custom_widget/Cargo.toml | 3 | ||||
-rw-r--r-- | examples/custom_widget/src/main.rs | 11 | ||||
-rw-r--r-- | examples/download_progress/Cargo.toml | 2 | ||||
-rw-r--r-- | examples/download_progress/src/download.rs | 19 | ||||
-rw-r--r-- | examples/events/Cargo.toml | 1 | ||||
-rw-r--r-- | examples/events/src/main.rs | 9 | ||||
-rw-r--r-- | examples/game_of_life/src/main.rs | 95 | ||||
-rw-r--r-- | examples/geometry/Cargo.toml | 4 | ||||
-rw-r--r-- | examples/geometry/src/main.rs | 121 | ||||
-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.toml | 12 | ||||
-rw-r--r-- | examples/integration_opengl/README.md | 16 | ||||
-rw-r--r-- | examples/integration_opengl/src/controls.rs | 101 | ||||
-rw-r--r-- | examples/integration_opengl/src/main.rs | 188 | ||||
-rw-r--r-- | examples/integration_opengl/src/scene.rs | 102 | ||||
-rw-r--r-- | examples/lazy/Cargo.toml | 3 | ||||
-rw-r--r-- | examples/lazy/src/main.rs | 12 | ||||
-rw-r--r-- | examples/loading_spinners/Cargo.toml | 11 | ||||
-rw-r--r-- | examples/loading_spinners/README.md | 14 | ||||
-rw-r--r-- | examples/loading_spinners/src/circular.rs | 421 | ||||
-rw-r--r-- | examples/loading_spinners/src/easing.rs | 133 | ||||
-rw-r--r-- | examples/loading_spinners/src/linear.rs | 326 | ||||
-rw-r--r-- | examples/loading_spinners/src/main.rs | 118 | ||||
-rw-r--r-- | examples/modal/Cargo.toml | 3 | ||||
-rw-r--r-- | examples/modal/src/main.rs | 152 | ||||
-rw-r--r-- | examples/modern_art/Cargo.toml | 11 | ||||
-rw-r--r-- | examples/modern_art/src/main.rs | 142 | ||||
-rw-r--r-- | examples/multitouch/src/main.rs | 14 | ||||
-rw-r--r-- | examples/pane_grid/Cargo.toml | 4 | ||||
-rw-r--r-- | examples/pane_grid/src/main.rs | 13 | ||||
-rw-r--r-- | examples/pick_list/src/main.rs | 9 | ||||
-rw-r--r-- | examples/qr_code/src/main.rs | 12 | ||||
-rw-r--r-- | examples/screenshot/Cargo.toml | 11 | ||||
-rw-r--r-- | examples/screenshot/src/main.rs | 320 | ||||
-rw-r--r-- | examples/scrollable/src/main.rs | 149 | ||||
-rw-r--r-- | examples/sierpinski_triangle/src/main.rs | 24 | ||||
-rw-r--r-- | examples/solar_system/Cargo.toml | 1 | ||||
-rw-r--r-- | examples/solar_system/src/main.rs | 50 | ||||
-rw-r--r-- | examples/styling/src/main.rs | 15 | ||||
-rw-r--r-- | examples/toast/Cargo.toml | 3 | ||||
-rw-r--r-- | examples/toast/src/main.rs | 81 | ||||
-rw-r--r-- | examples/todos/fonts/icons.ttf | bin | 5596 -> 5732 bytes | |||
-rw-r--r-- | examples/todos/src/main.rs | 76 | ||||
-rw-r--r-- | examples/tour/Cargo.toml | 2 | ||||
-rw-r--r-- | examples/tour/src/main.rs | 91 | ||||
-rw-r--r-- | examples/url_handler/Cargo.toml | 1 | ||||
-rw-r--r-- | examples/url_handler/src/main.rs | 10 | ||||
-rw-r--r-- | examples/websocket/Cargo.toml | 2 | ||||
-rw-r--r-- | examples/websocket/src/echo.rs | 104 | ||||
-rw-r--r-- | examples/websocket/src/echo/server.rs | 2 | ||||
-rw-r--r-- | examples/websocket/src/main.rs | 9 | ||||
-rw-r--r-- | futures/Cargo.toml | 4 | ||||
-rw-r--r-- | futures/src/backend/native.rs | 4 | ||||
-rw-r--r-- | futures/src/backend/native/async_std.rs | 14 | ||||
-rw-r--r-- | futures/src/backend/native/smol.rs | 16 | ||||
-rw-r--r-- | futures/src/backend/native/thread_pool.rs | 1 | ||||
-rw-r--r-- | futures/src/backend/native/tokio.rs | 14 | ||||
-rw-r--r-- | futures/src/backend/wasm/wasm_bindgen.rs | 14 | ||||
-rw-r--r-- | futures/src/command.rs | 70 | ||||
-rw-r--r-- | futures/src/lib.rs | 5 | ||||
-rw-r--r-- | futures/src/runtime.rs | 25 | ||||
-rw-r--r-- | futures/src/subscription.rs | 355 | ||||
-rw-r--r-- | futures/src/subscription/tracker.rs | 51 | ||||
-rw-r--r-- | glow/Cargo.toml | 51 | ||||
-rw-r--r-- | glow/README.md | 51 | ||||
-rw-r--r-- | glow/src/backend.rs | 280 | ||||
-rw-r--r-- | glow/src/image.rs | 254 | ||||
-rw-r--r-- | glow/src/image/storage.rs | 78 | ||||
-rw-r--r-- | glow/src/lib.rs | 53 | ||||
-rw-r--r-- | glow/src/program.rs | 133 | ||||
-rw-r--r-- | glow/src/quad.rs | 74 | ||||
-rw-r--r-- | glow/src/quad/compatibility.rs | 349 | ||||
-rw-r--r-- | glow/src/quad/core.rs | 244 | ||||
-rw-r--r-- | glow/src/settings.rs | 61 | ||||
-rw-r--r-- | glow/src/shader/common/gradient.frag | 59 | ||||
-rw-r--r-- | glow/src/shader/common/gradient.vert | 9 | ||||
-rw-r--r-- | glow/src/shader/common/image.frag | 22 | ||||
-rw-r--r-- | glow/src/shader/common/image.vert | 9 | ||||
-rw-r--r-- | glow/src/shader/common/solid.frag | 18 | ||||
-rw-r--r-- | glow/src/shader/common/solid.vert | 11 | ||||
-rw-r--r-- | glow/src/shader/compatibility/quad.frag | 83 | ||||
-rw-r--r-- | glow/src/shader/compatibility/quad.vert | 46 | ||||
-rw-r--r-- | glow/src/shader/core/quad.frag | 95 | ||||
-rw-r--r-- | glow/src/shader/core/quad.vert | 52 | ||||
-rw-r--r-- | glow/src/text.rs | 257 | ||||
-rw-r--r-- | glow/src/triangle.rs | 595 | ||||
-rw-r--r-- | glow/src/window.rs | 4 | ||||
-rw-r--r-- | glow/src/window/compositor.rs | 111 | ||||
-rw-r--r-- | glutin/Cargo.toml | 42 | ||||
-rw-r--r-- | glutin/README.md | 29 | ||||
-rw-r--r-- | glutin/src/application.rs | 510 | ||||
-rw-r--r-- | glutin/src/lib.rs | 33 | ||||
-rw-r--r-- | graphics/Cargo.toml | 60 | ||||
-rw-r--r-- | graphics/fonts/Lato-Regular.ttf | bin | 75136 -> 0 bytes | |||
-rw-r--r-- | graphics/fonts/OFL.txt | 93 | ||||
-rw-r--r-- | graphics/src/backend.rs | 25 | ||||
-rw-r--r-- | graphics/src/color.rs | 46 | ||||
-rw-r--r-- | graphics/src/compositor.rs (renamed from graphics/src/window/compositor.rs) | 23 | ||||
-rw-r--r-- | graphics/src/damage.rs | 205 | ||||
-rw-r--r-- | graphics/src/font.rs | 35 | ||||
-rw-r--r-- | graphics/src/font/source.rs | 45 | ||||
-rw-r--r-- | graphics/src/geometry.rs | 24 | ||||
-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.rs | 257 | ||||
-rw-r--r-- | graphics/src/gradient/linear.rs | 112 | ||||
-rw-r--r-- | graphics/src/image.rs | 99 | ||||
-rw-r--r-- | graphics/src/image/raster.rs | 242 | ||||
-rw-r--r-- | graphics/src/image/storage.rs | 2 | ||||
-rw-r--r-- | graphics/src/layer/quad.rs | 30 | ||||
-rw-r--r-- | graphics/src/lib.rs | 29 | ||||
-rw-r--r-- | graphics/src/mesh.rs | 76 | ||||
-rw-r--r-- | graphics/src/overlay/menu.rs | 3 | ||||
-rw-r--r-- | graphics/src/primitive.rs | 97 | ||||
-rw-r--r-- | graphics/src/renderer.rs | 132 | ||||
-rw-r--r-- | graphics/src/triangle.rs | 33 | ||||
-rw-r--r-- | graphics/src/viewport.rs | 4 | ||||
-rw-r--r-- | graphics/src/widget/canvas/cache.rs | 100 | ||||
-rw-r--r-- | graphics/src/widget/canvas/cursor.rs | 64 | ||||
-rw-r--r-- | graphics/src/widget/canvas/geometry.rs | 24 | ||||
-rw-r--r-- | graphics/src/window.rs | 10 | ||||
-rw-r--r-- | graphics/src/window/gl_compositor.rs | 71 | ||||
-rw-r--r-- | lazy/Cargo.toml | 18 | ||||
-rw-r--r-- | native/README.md | 37 | ||||
-rw-r--r-- | native/src/mouse.rs | 6 | ||||
-rw-r--r-- | native/src/runtime.rs | 18 | ||||
-rw-r--r-- | native/src/subscription.rs | 248 | ||||
-rw-r--r-- | native/src/text.rs | 114 | ||||
-rw-r--r-- | native/src/widget/action.rs | 154 | ||||
-rw-r--r-- | native/src/widget/helpers.rs | 317 | ||||
-rw-r--r-- | native/src/widget/operation.rs | 112 | ||||
-rw-r--r-- | native/src/window.rs | 38 | ||||
-rw-r--r-- | native/src/window/icon.rs | 12 | ||||
-rw-r--r-- | native/src/window/position.rs | 22 | ||||
-rw-r--r-- | renderer/Cargo.toml | 30 | ||||
-rw-r--r-- | renderer/src/backend.rs | 100 | ||||
-rw-r--r-- | renderer/src/compositor.rs | 262 | ||||
-rw-r--r-- | renderer/src/geometry.rs | 184 | ||||
-rw-r--r-- | renderer/src/geometry/cache.rs | 125 | ||||
-rw-r--r-- | renderer/src/lib.rs | 269 | ||||
-rw-r--r-- | renderer/src/settings.rs | 31 | ||||
-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.md | 18 | ||||
-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.rs | 19 | ||||
-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.rs | 4 | ||||
-rw-r--r-- | runtime/src/overlay/nested.rs | 353 | ||||
-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.rs | 135 | ||||
-rw-r--r-- | runtime/src/window/action.rs (renamed from native/src/window/action.rs) | 73 | ||||
-rw-r--r-- | runtime/src/window/screenshot.rs | 92 | ||||
-rw-r--r-- | src/advanced.rs | 16 | ||||
-rw-r--r-- | src/application.rs | 33 | ||||
-rw-r--r-- | src/clipboard.rs | 3 | ||||
-rw-r--r-- | src/element.rs | 5 | ||||
-rw-r--r-- | src/error.rs | 16 | ||||
-rw-r--r-- | src/executor.rs | 14 | ||||
-rw-r--r-- | src/keyboard.rs | 2 | ||||
-rw-r--r-- | src/lib.rs | 159 | ||||
-rw-r--r-- | src/mouse.rs | 2 | ||||
-rw-r--r-- | src/overlay.rs | 18 | ||||
-rw-r--r-- | src/result.rs | 6 | ||||
-rw-r--r-- | src/sandbox.rs | 22 | ||||
-rw-r--r-- | src/settings.rs | 32 | ||||
-rw-r--r-- | src/touch.rs | 2 | ||||
-rw-r--r-- | src/widget.rs | 238 | ||||
-rw-r--r-- | src/window.rs | 12 | ||||
-rw-r--r-- | style/Cargo.toml | 6 | ||||
-rw-r--r-- | style/src/button.rs | 9 | ||||
-rw-r--r-- | style/src/checkbox.rs | 4 | ||||
-rw-r--r-- | style/src/container.rs | 6 | ||||
-rw-r--r-- | style/src/lib.rs | 3 | ||||
-rw-r--r-- | style/src/menu.rs | 4 | ||||
-rw-r--r-- | style/src/pane_grid.rs | 38 | ||||
-rw-r--r-- | style/src/pick_list.rs | 4 | ||||
-rw-r--r-- | style/src/progress_bar.rs | 4 | ||||
-rw-r--r-- | style/src/rule.rs | 4 | ||||
-rw-r--r-- | style/src/scrollable.rs | 28 | ||||
-rw-r--r-- | style/src/slider.rs | 17 | ||||
-rw-r--r-- | style/src/text.rs | 20 | ||||
-rw-r--r-- | style/src/text_input.rs | 12 | ||||
-rw-r--r-- | style/src/theme.rs | 182 | ||||
-rw-r--r-- | style/src/theme/palette.rs | 22 | ||||
-rw-r--r-- | tiny_skia/Cargo.toml | 38 | ||||
-rw-r--r-- | tiny_skia/fonts/Iced-Icons.ttf (renamed from graphics/fonts/Icons.ttf) | bin | 5032 -> 5108 bytes | |||
-rw-r--r-- | tiny_skia/src/backend.rs | 856 | ||||
-rw-r--r-- | tiny_skia/src/geometry.rs | 315 | ||||
-rw-r--r-- | tiny_skia/src/lib.rs | 28 | ||||
-rw-r--r-- | tiny_skia/src/primitive.rs | 48 | ||||
-rw-r--r-- | tiny_skia/src/raster.rs | 116 | ||||
-rw-r--r-- | tiny_skia/src/settings.rs | 24 | ||||
-rw-r--r-- | tiny_skia/src/text.rs | 483 | ||||
-rw-r--r-- | tiny_skia/src/vector.rs | 195 | ||||
-rw-r--r-- | tiny_skia/src/window.rs | 3 | ||||
-rw-r--r-- | tiny_skia/src/window/compositor.rs | 228 | ||||
-rw-r--r-- | wgpu/Cargo.toml | 69 | ||||
-rw-r--r-- | wgpu/README.md | 2 | ||||
-rw-r--r-- | wgpu/fonts/Iced-Icons.ttf | bin | 0 -> 5108 bytes | |||
-rw-r--r-- | wgpu/src/backend.rs | 429 | ||||
-rw-r--r-- | wgpu/src/buffer.rs | 109 | ||||
-rw-r--r-- | wgpu/src/buffer/dynamic.rs | 219 | ||||
-rw-r--r-- | wgpu/src/buffer/static.rs | 117 | ||||
-rw-r--r-- | wgpu/src/color.rs | 165 | ||||
-rw-r--r-- | wgpu/src/geometry.rs (renamed from graphics/src/widget/canvas/frame.rs) | 261 | ||||
-rw-r--r-- | wgpu/src/image.rs | 287 | ||||
-rw-r--r-- | wgpu/src/image/atlas.rs | 227 | ||||
-rw-r--r-- | wgpu/src/image/atlas/allocation.rs | 3 | ||||
-rw-r--r-- | wgpu/src/image/atlas/allocator.rs | 4 | ||||
-rw-r--r-- | wgpu/src/image/atlas/entry.rs | 9 | ||||
-rw-r--r-- | wgpu/src/image/raster.rs | 119 | ||||
-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.rs | 25 | ||||
-rw-r--r-- | wgpu/src/primitive.rs | 21 | ||||
-rw-r--r-- | wgpu/src/quad.rs | 455 | ||||
-rw-r--r-- | wgpu/src/quad/gradient.rs | 165 | ||||
-rw-r--r-- | wgpu/src/quad/solid.rs | 150 | ||||
-rw-r--r-- | wgpu/src/settings.rs | 55 | ||||
-rw-r--r-- | wgpu/src/shader/gradient.wgsl | 88 | ||||
-rw-r--r-- | wgpu/src/shader/quad.wgsl | 293 | ||||
-rw-r--r-- | wgpu/src/shader/solid.wgsl | 30 | ||||
-rw-r--r-- | wgpu/src/shader/triangle.wgsl | 165 | ||||
-rw-r--r-- | wgpu/src/text.rs | 661 | ||||
-rw-r--r-- | wgpu/src/triangle.rs | 900 | ||||
-rw-r--r-- | wgpu/src/triangle/msaa.rs | 19 | ||||
-rw-r--r-- | wgpu/src/window.rs | 3 | ||||
-rw-r--r-- | wgpu/src/window/compositor.rs | 373 | ||||
-rw-r--r-- | widget/Cargo.toml | 37 | ||||
-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.rs | 368 | ||||
-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.rs | 126 | ||||
-rw-r--r-- | widget/src/mouse_area.rs | 311 | ||||
-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.rs | 6 | ||||
-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.toml | 31 | ||||
-rw-r--r-- | winit/README.md | 2 | ||||
-rw-r--r-- | winit/src/application.rs | 208 | ||||
-rw-r--r-- | winit/src/application/state.rs | 41 | ||||
-rw-r--r-- | winit/src/clipboard.rs | 17 | ||||
-rw-r--r-- | winit/src/conversion.rs | 50 | ||||
-rw-r--r-- | winit/src/error.rs | 7 | ||||
-rw-r--r-- | winit/src/icon.rs | 217 | ||||
-rw-r--r-- | winit/src/lib.rs | 18 | ||||
-rw-r--r-- | winit/src/proxy.rs | 2 | ||||
-rw-r--r-- | winit/src/settings.rs | 61 | ||||
-rw-r--r-- | winit/src/settings/windows.rs | 3 | ||||
-rw-r--r-- | winit/src/system.rs | 7 | ||||
-rw-r--r-- | winit/src/window.rs | 128 |
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 @@ -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; +} +``` @@ -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 @@ -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 @@ //!  //! //! [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])); -/// ``` -/// -///  #[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 Binary files differindex a2046844..82f28481 100644 --- a/examples/checkbox/fonts/icons.ttf +++ b/examples/checkbox/fonts/icons.ttf 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 Binary files differindex 4498299d..7b65fd36 100644 --- a/examples/todos/fonts/icons.ttf +++ b/examples/todos/fonts/icons.ttf 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://crates.io/crates/iced_glow) -[](https://github.com/iced-rs/iced/blob/master/LICENSE) -[](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`]. -//! -//!  -//! -//! [`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(¢er_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://crates.io/crates/iced_glutin) -[](https://github.com/iced-rs/iced/blob/master/LICENSE) -[](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`]. -//! -//!  -//! -//! [`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 Binary files differdeleted file mode 100644 index 33eba8b1..00000000 --- a/graphics/fonts/Lato-Regular.ttf +++ /dev/null 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(®ion); + + 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://crates.io/crates/iced_native) -[](https://github.com/iced-rs/iced/blob/master/LICENSE) -[](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://crates.io/crates/iced_native) +[](https://github.com/iced-rs/iced/blob/master/LICENSE) +[](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}; @@ -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. - //! - //! [](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. - /// - /// [](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 Binary files differindex 5e455b69..e3273141 100644 --- a/graphics/fonts/Icons.ttf +++ b/tiny_skia/fonts/Iced-Icons.ttf 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 ®ion 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 Binary files differnew file mode 100644 index 00000000..e3273141 --- /dev/null +++ b/wgpu/fonts/Iced-Icons.ttf 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(§ion_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(¢er_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> { /// ///  #[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}; /// ///  #[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 +/// ); /// -///  +/// 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}; /// ///  #[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); /// ``` ///  #[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, - )))) -} |