From 7b50c0a017a1cf3c98f8f95f0fdc82a84402e4d6 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 2 May 2023 15:48:20 -0700 Subject: [PATCH] feat: pop-os megasquash x11: Workaround nvidia driver lacking DRI feat(mouse area): add double click mouse area: add double click compositor: Add code to extract adapter from x11 refactor: Extract ids_from_dev from wayland specific code wayland: Don't crash if libwayland isn't available feat(sctk): support for overflow widget sctk: Fixes for cursor icon * With multiple windows, `SetCursor` is only sent for the focused window. Fixing a flicker between icons when two windows are using different cursors. * If there is a drag surface, let that surface set the cursor. And not any other. * Set cursor on `enter`, and when switching between CSDs and app area. Fixes https://github.com/pop-os/libcosmic/issues/533. improv(sctk): per-surface cursor position tracking feat(sctk): support ShowWindowMenu Make text wrap configurable fix(core): state order and handling of new trees fix: settings.decorations enables SSD refactor(sctk): convert window actions fix: enable the tokio feature for accesskit_unix fix: only try to connect to clipboard if on linux iced_wgpu: don't query Wayland on macos Update `window_clipboard` sctk: Unmap subsurfaces instead of immediately destroying them Destroying a surface is immediate, rather than synchronized with commits. This fixes a flickering behavior with drag and drop in cosmic-workspaces. sctk: Add alpha setting to `Subsurface` widget sctk: Update `sctk`, `wayland-protocols` Update for cosmic-text undefined buffer size Adapt to cosmic-text undefined width change fix: unset VK_LOADER_DRIVERS_DISABLE after enumeration Allows applications to be launched on the NVIDIA GPU with Vulkan support Adapt to new cosmic-text wgpu: Fix wayland device id conversion wgpu: Fix querying adapter, even if we already have one wgpu: fix nvidia gpu powering up in hybrid setups cargo fmt fix: refactor dnd impl to support responsive widget fix: update read and write methods so they don't recurse fix(core): replace debug_assert in diff fix: avoid with_borrow_mut fix: better handling of state tree This persists widget state associated with widgets assigned custom IDs even when the tree structure changes, but resets state if the custom ID is not found. fix: emit Event::Resized to fix nav bar in cosmic-settings fix(image): guess the image format before decoding iced_wgpu: Query wayland for the device to use, if possible sctk: Support `start_drag` with drags started from touch events sctk: Add touch support fix: update widnow-clipboard tag fix: clean up dnd surfaces when a window is removed Adjust to line ending needing to be specified as part of cosmic_text::BufferLine sctk: Add support for drag-and-drop surface offsets This adds an offset `Vector` as an argument to `on_drag`, and allows passing an offset to `start_drag`. Some applications using drag and drop want the top left corner of the drag surface (as happens without an offset). But others want the drag surface to be offset based on where the cursor is on the widget when starting the drag. This can just be `-1 * offset`, but may be scaled if the drag surface is a different size from the original widget. fix(sctk): nested popup parent feat(mouseare): mouse enter and exit fix(tiny_skia): damage fix(scrollable): filter scroll events in the wrong direction sctk: Use empty input region for subsurfaces This seems to work, and is a better way to deal with subsurface input if there aren't any problems. This way, input events simply go to the parent surface, so we don't have to deal with various edge cases related to that. (Though for compositor-side issues, we still need to fix those for other clients.) This helps with an issue with drag-and-drop and subsurfaces on Smithay, and a different issue on Kwin (in KDE 5.27, at least). Send `DataSource` events to all surfaces Previously these events are directed to the first surface, then removed from `sctk_events`. Which is definitely not right. slider & toggler roundness Update window_clipboard to pop-dnd-4 fix(tiny-skia): non-simple border scaling the issue can be seen with sharp corners when using the screenshot portal with scaling Add read_primary/write_primary chore: update tag fix: translate offer positions in scrollable fix(winit multi-window): handle exit_on_close request fix(scrollable): pass child layout when calculating drag destinations fix(container): id and set_id should use content Clean up after lock surfaces are destroyed Call unlock on session lock chore: update tag fixes for dnd sctk: Fix handling of DnD with subsurfaces (#122) Map subsurface to parent and add offset. refactor: remove Sync bound for Message fix: pass correct state and layout for container widgets fix: docs feat: update advertised drag destinations after rebuilding an interface fix: color format & multi-window fix: doc feat: winit dnd fix: ambiguous import chore: reexport mime from window_clipboard chore: use tag clippy feat: add actions and commands for new clipboard methods cleanup docs feat: custom mime types for Clipboard sctk: Fix handling of layer surface `pointer_interactivity` (#115) A null `region` represents an infinite region (the default). To set an empty region, we need to create a `wl_region`. fix(tiny_skia): disable shadows due to rendering glitch fix(winit): add static lifetimes to multi-window application update fix(winit): add static lifetimes to application update Use `TypeId` to identify `subscription::Map` (cherry picked from commit f39a5fd8953494fd8e41c05bc053519740d09612) fix(sctk): destroy drag icon and send event after cancel action fix: clipboard cleanup fix(sctk): clipboard dummy impl typo refactor(sctk): optional clipboard fix(sctk): broadcast events after update when broadcasting events for no specific surface, it should be done after update so that the runtime subscription is current fix(multi_window): enable drag resize sctk: Map subsurface pointer events to parent surface, with offset sctk_subsurface: Use two surfaces, handle button presses Useful for testing pointer input to subsurfaces. sctk: Add `subsurface_ids` mapping subsurface to parent and offset sctk_subsurface_gst: NV12 surface suppport; disabled Whether or not this works seems to depend on driver, or gstreamer version... Handle frame callbacks for subsurfaces, and `commit` parent surface If the main surface is occluded completely by opaque subsurfaces, it may not receive `frame` events. So we need to request frame events for all subsurfaces as well. Additionally, with "synchronized" subsurfaces, we need to `commit` the parent surface for subsurface changes to take effect. Fixes issues with subsurfaces updating slowly, or only when mouse moved under some circumstances. examples/sctk_subsurface_gst: Cache `BufferSource` in `BufferRef` qdata Similar to `waylandsink`. Allows us to avoid creating a buffer source (and ultimately `wl_buffer`) for every buffer swap. sctk/subsurface: Cache `wl_buffer`s Creating a new `wl_buffer` each frame seems to perform poorly. We can instead keep a cache of `wl_buffer`s we have created from a `BufferSource`. sctk/subsurface: Avoid unnecessary subsurface commits if unchanged feat(slider): add breakpoints fix: autosize surface layout Autosized surfaces perform the layout step to get the size and then again when building the interface, but sometimes the calculated size is not enough space when used as a bound, so we need to add a tiny amount to the calculated size. This also makes the event loop timeout duration configurable. Viewport physical size is calculated directly from the logical size now as well in iced-sctk to avoid inconsistencies that resulted from recalculating the logical size after using it to calculate the physical size. fix(sctk): send close event instead of close requested when a window is closed sctk: add command to set maximize state Add `show_window_menu` action Winit currently supports this only on Windows and Wayland. This requests that a context menu is shown at the cursor position, like the menu normally triggered by right clicking the title bar. This is important for implementing client side decorations with Iced widgets. Remove unnecessary redraw request This was particularly visible on Redox where there is no vsync, but also causes unnecessary redraws on Linux chore: update accesskit Disable broken rustdoc links sctk: Add `Subsurface` widget (#79) This adds a widget that attaches an shm or dma buffer to a subsurface, scaled with `wp_viewporter`. By exposing this as a widget, rather than as a type of window, it can be positioned and scaled like any other iced widget. It provides an API that's similar to an iced image. The initial version of this just took a `wl_buffer`. But this makes buffer re-use problematic. In particular, the docs for `wl_surface::attach` note that `wl_buffer::release` events become unreliable if a buffer is attached to multiple surfaces. And indicates that a client should create multiple `wl_buffer` instances, or use `wp_linux_buffer_release`. So we store information about the buffer, and create `wl_buffer`s as needed. `SubsurfaceBuffer::new` also returns a future that's signaled when all references are destroyed, both `wl_buffer`s and any instance of the `SubsurfaceBuffer` that might still be used in the `view`. So this seems like the best solution for now, within the model-view-update architecture. This has two examples: `sctk_subsurface`, showing a single-color shm buffer, and `sctk_subsurface_gst`, which plays an h264 video to a subsurface with vaapi decoding. chore: use pop-os fork of winit chore: unpin cosmic-text Update wgpu to a commit that fixes use on Nvidia drivers This can be tested with something like `VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/nvidia_icd.json cargo run -p tour --features iced/wgpu`. On Nvidia I'm seeing a flood of `Suboptimal present of frame` warnings. So some improvement may still be needed here. But if it doesn't regress behavior on other hardware, that seems like an improvement over freezing. fix(winit): pass text with modifiers in event chore: update cosmic-text and glyphon fix: distinguish between the key character and the utf8 of a key event feat(wgpu): use alpha modes for compositing if available chore: use updated softbuffer fix: typo fix: downgrade resvg fix: core/serde chore: remove default features typo: add rev to glyphon Update to cosmic-text refactor Fix docs error Add function to fill a Raw Fixes for last commit fix: broadcast surface events dnd_listener: Fix behavior when there are multiple listeners (#87) A `dnd_listener` widget shouldn't handle a DnD event when the dnd drag isn't within the widget's bounds. So add a few more checks for this. Enter/leave events generated by `DndOfferEvent::Motion` also don't behave as one might expect, since the enter may occur before the leave depending on the order it calls `on_event` on the widget. Not sure how to address that, but cosmic-workspaces can just ignore the leave events for now. Otherwise, this seems to be working fine, after these changes. chore: fix sctk multi-window dependency cleanup: formatting and clippy fix(example): sctk_drag id fix: translate the wayland event position for content inside a scrollable fix: set web-sys to =0.3.64 fix: clip mask checks chore: use advanced text shaping for pick list fix: dnd widget layout fix: ambiguous palette import chore: remove artifacts job fix: CI tests fix: add back the window id to the frames subscription fix: tooltip children and diff refactor: udpate gradient angles for slider reexport limits fix: editor and sctk_todos examples cleanup: clippy cleanup git workflows chore: cleanup iced_widget refactor Update mod.rs chore: update softbuffer Hack to remove image blur iced_core: feature for serde serialization of KeyCode fix(wgpu): handle border_radius property with image raster feat: add border radius to image rendering feat: Add side mouse button events cleanup: clippy fixes and formatting Part of this is a refactor of the ID cleanup: clippy and fmt fix: test workflow fix: add note in CHANGELOG fix: clippy refactor: restore default style of slider feat: allow setting the width and height of a rule fix: slider gradient angle feat: gradient backgground for the slider rail feat(mouse-area): added on_drag method fix(widget): container inherited wrong icon color from renderer fix(button): inherit icon color if set to none feat(renderer): define default icon color By default, this is the same as the text color for best visibility. feat(winit): client-side resize drag support feat(winit): client-side resize drag support Make vertical scroll properties optional fix: quad rendering including border only inside of the bounds Move `Screenshot` inside `window` module Added offscreen rendering support for wgpu & tiny-skia exposed with the window::screenshot command. Provide access to font from each crate Use nested for lazy widgets Use layout with children for nesting Introduce internal `overlay::Nested` for `UserInterface` fix: reset button state if the cursor leaves runtime: Handle widget operations in `program::State` helper (#46) chore: default line height, text size, and shaping for cosmic feat: sctk shell fix: quad rendering including border only inside of the bounds fix: better slider drawing (it allows just the border part of the handle quad outside of the layout bouds, which isn't great, but is ok for our purposes due to being transparent) cleanup: fix & format fix: use iced_core::Font cleanup fix: allow leaving out winit & iced-sctk fix: settings fix: slider draw improvements fix: websocket example fix: modal example fix: scrollable example fix: toast example fix: avoid panicking in iced_sctk with lazy widgets in auto-size surfaces fix: todos panic fix: only diff auto-sized surfaces in iced_sctk build_user_interface & improve sctk examples wip (iced-sctk): window resize with icons feat (iced-sctk): support for setting cursor refactor: default decorations to client fix: set window geometry after receiving configure fix: size limits with no max bound must be cut off fix: send size update when autosized surface resizes fix: use ceil size for positioner cleanup: remove dbg statement fix: remove a destroyed surface from compositor surfaces fix errors after rebase and wip scaling support fix: handling of scale factor in set_logical_size fix (sctk_drag example): add .into for border radius fix: fractional scaling sctk: Fire RedrawRequests wip: animations via frame event fix / refactor iced-sctk redraw & frame event handling cleanup: note about frame request in iced-sctk fix: send resize when necessary for layer surface and popups too fix: always request redraw for a new surface fix: scaling and autosize surface improvements refactor: sctk_lazy keyboard interactivity feat(sctk): configurable natural_scroll property feat: send state and capabilities events when there are changes fix: redraw when an update is needed and clean up the logic Update sctk to latest commit Fix compilation of sctk drag example fix(sctk): update interface before checking if it has a redraw request refactor: after autosize surface resize wait to redraw until the resize has been applied refactor: better handling of autosize surfaces chore: update sctk chore: update sctk fixes sctk_drag example fix: default to ControlFlow::Wait for applications with no surface this seems to help CPU usage for app library and launcher default to 250ms timeout in the event loop Update sctk sctk: Implement xdg-activation support fix: don't require Flags to be clone for settings on wayland chore: error if neither winit or wayland feature is set chore: Allow compiling without windowing system (#65) fix(iced-sctk): handle exit_on_close_request fix: make sure that each widget operation operates on every interface This should be ok even for widget actions like focus next because there can only ever be a single focused widget cargo fmt cleanup: dbg statement fix(iced-sctk): replace panic with handling for remaining enum variants refactor: use iced clipboard for interacting with the selection refactor: allow passing an activation token when creating a window sctk: Add support for `ext-session-lock` protocol fix(sctk): build and use tree for layout of autosize surfaces Update winit to latest commit used by upstream iced fix(sctk): send key characters fix(sctk): check if key is a named key first refactor(sctk): keep compositor surface in state feat: accessibility with some widget impls feat: stable ids a11y: Don't unconditionally pull winit (#43) Update conversion.rs integration fixes integration integration integration integration s integration some integration work more integration Update Cargo.toml Update mod.rs Update multi_window.rs s more integration more integration (ryanabx wip #100000) more integration!! integration 2 integration more integration (rbx) s integration integration work integration Update Cargo.toml integration simple integration things int integration to 175 integration(170) Co-Authored-By: Ashley Wulber <48420062+wash2@users.noreply.github.com> Co-Authored-By: Victoria Brekenfeld <4404502+Drakulix@users.noreply.github.com> Co-Authored-By: Eduardo Flores Co-Authored-By: Michael Murphy Co-Authored-By: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Co-Authored-By: Jeremy Soller Co-Authored-By: Ryan Brue <56272643+ryanabx@users.noreply.github.com> fix(column): handle keys len change fix: iced-sctk a11y wip: winit single window updates tokio feature hangs even without a11y feature fix: multiwindow a11y fixes fix: component update winit wip: sctk integration to winit shell refactor: remove accesskit_unix fix: svg fix: remove wayland default feature feat: derive Hash for image Handle fix: cleanup 0.13 rebase errors fix: remove path dependencies conversion for Radius conversion for Padding setter for Svg border radius re-export Limits fix: connect clipboard if disconnected on layer surface or popup creation fix: connect clipboard if disconnected on session lock surface creation fix: update size of layer surface after configure fix: insert user interfaces for popup and lock surfaces on creation fix: svg scaling feat: popups on winit windows fix: default text shaping to advanced fix: fallback to renderer icon style if svg is symbolic fixes fix: sctk frame handling feat: autosize handling fix: better autosize handling fix: avoid duplicate window events from sctk fix: better handling of popups fix: refactor redraw handling for sctk fix: include id in frames fix: image fix: scrollable delta direction sctk: unregister clipboard when surface is done set min / max size when size is requested fix: popups filter pointer events feat: add Hide variant to mouse Interaction dnd fixes fix: use physical width for DnD surface fix: tiny-skia svg quality refactor: peek_dnd try to parse data cleanup text conversion cleanup svg scaling fixes background color fix Introduce consecutive_click_distance like other toolkits do such as gtk,qt,imgui. --- .github/workflows/audit.yml | 12 - .github/workflows/lint.yml | 7 +- .github/workflows/test.yml | 36 +- CHANGELOG.md | 6 + Cargo.toml | 91 +- accessibility/Cargo.toml | 24 + accessibility/src/a11y_tree.rs | 80 + accessibility/src/id.rs | 215 +++ accessibility/src/lib.rs | 17 + accessibility/src/node.rs | 41 + accessibility/src/traits.rs | 19 + core/Cargo.toml | 25 + core/src/border.rs | 51 + core/src/clipboard.rs | 172 ++ core/src/element.rs | 93 +- core/src/event.rs | 28 +- core/src/event/wayland/layer.rs | 10 + core/src/event/wayland/mod.rs | 39 + core/src/event/wayland/output.rs | 34 + core/src/event/wayland/popup.rs | 21 + core/src/event/wayland/seat.rs | 9 + core/src/event/wayland/session_lock.rs | 19 + core/src/event/wayland/window.rs | 8 + core/src/id.rs | 155 ++ core/src/image.rs | 19 +- core/src/keyboard/key.rs | 2 + core/src/lib.rs | 6 + core/src/mouse/click.rs | 1 - core/src/mouse/interaction.rs | 1 + core/src/overlay.rs | 6 +- core/src/overlay/group.rs | 1 + core/src/padding.rs | 23 + core/src/rectangle.rs | 20 + core/src/renderer.rs | 6 + core/src/renderer/null.rs | 16 +- core/src/svg.rs | 10 + core/src/text.rs | 12 +- core/src/theme/palette.rs | 10 +- core/src/widget.rs | 39 +- core/src/widget/id.rs | 43 - core/src/widget/operation.rs | 5 +- core/src/widget/operation/focusable.rs | 3 +- core/src/widget/operation/search_id.rs | 42 + core/src/widget/text.rs | 75 +- core/src/widget/tree.rs | 298 +++- core/src/window/id.rs | 10 +- core/src/window/settings.rs | 8 +- examples/editor/Cargo.toml | 2 +- examples/game_of_life/Cargo.toml | 2 +- examples/gradient/Cargo.toml | 2 +- examples/integration/Cargo.toml | 2 +- examples/integration/src/controls.rs | 1 + examples/integration/src/main.rs | 7 +- examples/lazy/Cargo.toml | 2 +- examples/loading_spinners/Cargo.toml | 2 +- examples/multi_window/Cargo.toml | 9 +- examples/multi_window/src/main.rs | 7 +- examples/pane_grid/Cargo.toml | 2 +- examples/pokedex/Cargo.toml | 2 +- examples/screenshot/Cargo.toml | 2 +- examples/scrollable/Cargo.toml | 4 +- examples/scrollable/src/main.rs | 4 +- examples/sctk_drag/Cargo.toml | 19 + examples/sctk_drag/src/dnd_destination.rs | 728 ++++++++ examples/sctk_drag/src/dnd_source.rs | 382 +++++ examples/sctk_drag/src/main.rs | 202 +++ examples/sctk_lazy/Cargo.toml | 17 + examples/sctk_lazy/src/main.rs | 255 +++ examples/sctk_session_lock/Cargo.toml | 18 + examples/sctk_session_lock/src/main.rs | 96 ++ examples/sctk_subsurface/Cargo.toml | 19 + examples/sctk_subsurface/src/main.rs | 123 ++ examples/sctk_subsurface/src/wayland.rs | 128 ++ examples/sctk_subsurface_gst/Cargo.toml | 21 + examples/sctk_subsurface_gst/src/main.rs | 84 + examples/sctk_subsurface_gst/src/pipewire.rs | 185 ++ examples/sctk_todos/Cargo.toml | 32 + examples/sctk_todos/README.md | 20 + examples/sctk_todos/fonts/icons.ttf | Bin 0 -> 5596 bytes examples/sctk_todos/iced-todos.desktop | 4 + examples/sctk_todos/index.html | 12 + examples/sctk_todos/src/main.rs | 648 +++++++ examples/svg/Cargo.toml | 2 +- examples/system_information/Cargo.toml | 12 - examples/system_information/src/main.rs | 142 -- examples/toast/src/main.rs | 10 +- examples/todos/Cargo.toml | 26 +- examples/todos/src/main.rs | 62 +- examples/tooltip/Cargo.toml | 2 +- examples/tour/Cargo.toml | 2 +- examples/websocket/src/main.rs | 1 + futures/Cargo.toml | 1 + futures/src/subscription.rs | 9 +- graphics/src/compositor.rs | 12 + graphics/src/geometry/text.rs | 8 +- graphics/src/image.rs | 27 +- graphics/src/settings.rs | 4 +- graphics/src/text.rs | 9 +- graphics/src/text/editor.rs | 16 +- graphics/src/text/paragraph.rs | 11 +- graphics/src/viewport.rs | 17 + renderer/Cargo.toml | 1 + renderer/src/compositor.rs | 1 - renderer/src/fallback.rs | 29 +- runtime/Cargo.toml | 9 +- runtime/src/clipboard.rs | 83 +- runtime/src/dnd.rs | 129 ++ runtime/src/lib.rs | 50 + runtime/src/multi_window/state.rs | 1 + runtime/src/platform_specific/mod.rs | 26 + .../platform_specific/wayland/activation.rs | 42 + .../wayland/layer_surface.rs | 185 ++ runtime/src/platform_specific/wayland/mod.rs | 42 + .../src/platform_specific/wayland/popup.rs | 141 ++ .../platform_specific/wayland/session_lock.rs | 45 + runtime/src/program/state.rs | 11 + runtime/src/user_interface.rs | 52 +- runtime/src/window.rs | 26 +- runtime/src/window/screenshot.rs | 2 +- src/application.rs | 8 +- src/daemon.rs | 9 +- src/error.rs | 7 + src/lib.rs | 69 +- src/program.rs | 9 +- src/settings.rs | 12 +- src/wayland/application.rs | 211 +++ src/wayland/mod.rs | 4 + src/wayland/program.rs | 833 +++++++++ src/window.rs | 5 + src/window/icon.rs | 5 +- tiny_skia/fonts/Iced-Icons.ttf | Bin 0 -> 5108 bytes tiny_skia/src/engine.rs | 99 +- tiny_skia/src/geometry.rs | 10 +- tiny_skia/src/layer.rs | 27 +- tiny_skia/src/lib.rs | 31 +- tiny_skia/src/raster.rs | 149 +- tiny_skia/src/settings.rs | 2 +- tiny_skia/src/text.rs | 6 +- tiny_skia/src/vector.rs | 1 + tiny_skia/src/window/compositor.rs | 17 +- wgpu/Cargo.toml | 17 + wgpu/src/geometry.rs | 10 +- wgpu/src/image/mod.rs | 30 +- wgpu/src/layer.rs | 19 +- wgpu/src/lib.rs | 39 +- wgpu/src/offscreen.rs | 102 ++ wgpu/src/settings.rs | 4 +- wgpu/src/shader/offscreen_blit.wgsl | 22 + wgpu/src/window.rs | 36 + wgpu/src/window/compositor.rs | 105 +- wgpu/src/window/wayland.rs | 115 ++ wgpu/src/window/x11.rs | 171 ++ widget/Cargo.toml | 9 +- widget/src/button.rs | 249 ++- widget/src/canvas.rs | 1 + widget/src/checkbox.rs | 137 +- widget/src/column.rs | 46 +- widget/src/container.rs | 92 +- widget/src/helpers.rs | 14 +- widget/src/image.rs | 158 +- widget/src/image/viewer.rs | 12 +- widget/src/keyed/column.rs | 10 +- widget/src/lazy.rs | 47 +- widget/src/lazy/component.rs | 51 +- widget/src/lazy/responsive.rs | 65 +- widget/src/mouse_area.rs | 137 +- widget/src/overlay/menu.rs | 17 +- widget/src/pane_grid.rs | 25 +- widget/src/pane_grid/content.rs | 8 +- widget/src/pane_grid/title_bar.rs | 18 +- widget/src/pick_list.rs | 32 +- widget/src/radio.rs | 2 +- widget/src/row.rs | 48 +- widget/src/rule.rs | 18 + widget/src/scrollable.rs | 348 +++- widget/src/shader.rs | 4 + widget/src/slider.rs | 271 ++- widget/src/stack.rs | 5 +- widget/src/svg.rs | 134 +- widget/src/text_input.rs | 2 +- widget/src/themer.rs | 7 +- widget/src/toggler.rs | 211 ++- widget/src/tooltip.rs | 16 +- widget/src/vertical_slider.rs | 6 +- winit/Cargo.toml | 32 +- winit/src/a11y.rs | 64 + winit/src/application/drag_resize.rs | 138 ++ winit/src/application/state.rs | 231 +++ winit/src/clipboard.rs | 258 ++- winit/src/conversion.rs | 82 +- winit/src/error.rs | 2 +- winit/src/lib.rs | 5 + winit/src/platform_specific/mod.rs | 174 ++ .../wayland/commands/activation.rs | 31 + .../wayland/commands/layer_surface.rs | 116 ++ .../platform_specific/wayland/commands/mod.rs | 6 + .../wayland/commands/popup.rs | 42 + .../wayland/commands/session_lock.rs | 42 + .../platform_specific/wayland/conversion.rs | 120 ++ .../wayland/event_loop/control_flow.rs | 56 + .../wayland/event_loop/mod.rs | 387 +++++ .../wayland/event_loop/proxy.rs | 66 + .../wayland/event_loop/state.rs | 1133 ++++++++++++ .../wayland/handlers/activation.rs | 47 + .../wayland/handlers/compositor.rs | 67 + .../platform_specific/wayland/handlers/mod.rs | 37 + .../wayland/handlers/output.rs | 49 + .../wayland/handlers/seat/keyboard.rs | 255 +++ .../wayland/handlers/seat/mod.rs | 5 + .../wayland/handlers/seat/pointer.rs | 184 ++ .../wayland/handlers/seat/seat.rs | 214 +++ .../wayland/handlers/seat/touch.rs | 157 ++ .../wayland/handlers/session_lock.rs | 58 + .../wayland/handlers/shell/layer.rs | 118 ++ .../wayland/handlers/shell/mod.rs | 3 + .../wayland/handlers/shell/xdg_popup.rs | 92 + .../wayland/handlers/shell/xdg_window.rs | 27 + .../wayland/handlers/subcompositor.rs | 4 + .../wayland/handlers/wp_fractional_scaling.rs | 91 + .../wayland/handlers/wp_viewporter.rs | 71 + winit/src/platform_specific/wayland/keymap.rs | 895 ++++++++++ winit/src/platform_specific/wayland/mod.rs | 211 +++ .../platform_specific/wayland/sctk_event.rs | 1210 +++++++++++++ .../wayland/subsurface_widget.rs | 702 ++++++++ .../platform_specific/wayland/winit_window.rs | 437 +++++ winit/src/program.rs | 1517 +++++++++++++---- winit/src/program/state.rs | 54 +- winit/src/program/window_manager.rs | 43 +- winit/src/proxy.rs | 20 +- 229 files changed, 18927 insertions(+), 1169 deletions(-) create mode 100644 accessibility/Cargo.toml create mode 100644 accessibility/src/a11y_tree.rs create mode 100644 accessibility/src/id.rs create mode 100644 accessibility/src/lib.rs create mode 100644 accessibility/src/node.rs create mode 100644 accessibility/src/traits.rs create mode 100644 core/src/event/wayland/layer.rs create mode 100644 core/src/event/wayland/mod.rs create mode 100644 core/src/event/wayland/output.rs create mode 100644 core/src/event/wayland/popup.rs create mode 100644 core/src/event/wayland/seat.rs create mode 100644 core/src/event/wayland/session_lock.rs create mode 100644 core/src/event/wayland/window.rs create mode 100644 core/src/id.rs delete mode 100644 core/src/widget/id.rs create mode 100644 core/src/widget/operation/search_id.rs create mode 100644 examples/sctk_drag/Cargo.toml create mode 100644 examples/sctk_drag/src/dnd_destination.rs create mode 100644 examples/sctk_drag/src/dnd_source.rs create mode 100644 examples/sctk_drag/src/main.rs create mode 100644 examples/sctk_lazy/Cargo.toml create mode 100644 examples/sctk_lazy/src/main.rs create mode 100644 examples/sctk_session_lock/Cargo.toml create mode 100644 examples/sctk_session_lock/src/main.rs create mode 100644 examples/sctk_subsurface/Cargo.toml create mode 100644 examples/sctk_subsurface/src/main.rs create mode 100644 examples/sctk_subsurface/src/wayland.rs create mode 100644 examples/sctk_subsurface_gst/Cargo.toml create mode 100644 examples/sctk_subsurface_gst/src/main.rs create mode 100644 examples/sctk_subsurface_gst/src/pipewire.rs create mode 100644 examples/sctk_todos/Cargo.toml create mode 100644 examples/sctk_todos/README.md create mode 100644 examples/sctk_todos/fonts/icons.ttf create mode 100644 examples/sctk_todos/iced-todos.desktop create mode 100644 examples/sctk_todos/index.html create mode 100644 examples/sctk_todos/src/main.rs delete mode 100644 examples/system_information/Cargo.toml delete mode 100644 examples/system_information/src/main.rs delete mode 100644 renderer/src/compositor.rs create mode 100644 runtime/src/dnd.rs create mode 100644 runtime/src/platform_specific/mod.rs create mode 100644 runtime/src/platform_specific/wayland/activation.rs create mode 100644 runtime/src/platform_specific/wayland/layer_surface.rs create mode 100644 runtime/src/platform_specific/wayland/mod.rs create mode 100644 runtime/src/platform_specific/wayland/popup.rs create mode 100644 runtime/src/platform_specific/wayland/session_lock.rs create mode 100644 src/wayland/application.rs create mode 100644 src/wayland/mod.rs create mode 100644 src/wayland/program.rs create mode 100644 tiny_skia/fonts/Iced-Icons.ttf create mode 100644 wgpu/src/offscreen.rs create mode 100644 wgpu/src/shader/offscreen_blit.wgsl create mode 100644 wgpu/src/window/wayland.rs create mode 100644 wgpu/src/window/x11.rs create mode 100644 winit/src/a11y.rs create mode 100644 winit/src/application/drag_resize.rs create mode 100644 winit/src/application/state.rs create mode 100644 winit/src/platform_specific/mod.rs create mode 100644 winit/src/platform_specific/wayland/commands/activation.rs create mode 100644 winit/src/platform_specific/wayland/commands/layer_surface.rs create mode 100644 winit/src/platform_specific/wayland/commands/mod.rs create mode 100644 winit/src/platform_specific/wayland/commands/popup.rs create mode 100644 winit/src/platform_specific/wayland/commands/session_lock.rs create mode 100644 winit/src/platform_specific/wayland/conversion.rs create mode 100644 winit/src/platform_specific/wayland/event_loop/control_flow.rs create mode 100644 winit/src/platform_specific/wayland/event_loop/mod.rs create mode 100644 winit/src/platform_specific/wayland/event_loop/proxy.rs create mode 100644 winit/src/platform_specific/wayland/event_loop/state.rs create mode 100644 winit/src/platform_specific/wayland/handlers/activation.rs create mode 100644 winit/src/platform_specific/wayland/handlers/compositor.rs create mode 100644 winit/src/platform_specific/wayland/handlers/mod.rs create mode 100644 winit/src/platform_specific/wayland/handlers/output.rs create mode 100644 winit/src/platform_specific/wayland/handlers/seat/keyboard.rs create mode 100644 winit/src/platform_specific/wayland/handlers/seat/mod.rs create mode 100644 winit/src/platform_specific/wayland/handlers/seat/pointer.rs create mode 100644 winit/src/platform_specific/wayland/handlers/seat/seat.rs create mode 100644 winit/src/platform_specific/wayland/handlers/seat/touch.rs create mode 100644 winit/src/platform_specific/wayland/handlers/session_lock.rs create mode 100644 winit/src/platform_specific/wayland/handlers/shell/layer.rs create mode 100644 winit/src/platform_specific/wayland/handlers/shell/mod.rs create mode 100644 winit/src/platform_specific/wayland/handlers/shell/xdg_popup.rs create mode 100644 winit/src/platform_specific/wayland/handlers/shell/xdg_window.rs create mode 100644 winit/src/platform_specific/wayland/handlers/subcompositor.rs create mode 100644 winit/src/platform_specific/wayland/handlers/wp_fractional_scaling.rs create mode 100644 winit/src/platform_specific/wayland/handlers/wp_viewporter.rs create mode 100644 winit/src/platform_specific/wayland/keymap.rs create mode 100644 winit/src/platform_specific/wayland/mod.rs create mode 100755 winit/src/platform_specific/wayland/sctk_event.rs create mode 100644 winit/src/platform_specific/wayland/subsurface_widget.rs create mode 100644 winit/src/platform_specific/wayland/winit_window.rs diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 40e9235a63..6e81ee0720 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -16,15 +16,3 @@ jobs: run: cargo update - name: Audit vulnerabilities run: cargo audit - - # artifacts: - # runs-on: ubuntu-latest - # steps: - # - uses: hecrj/setup-rust-action@v2 - # - name: Install cargo-outdated - # run: cargo install cargo-outdated - # - uses: actions/checkout@master - # - name: Delete `web-sys` dependency from `integration` example - # run: sed -i '$d' examples/integration/Cargo.toml - # - name: Find outdated dependencies - # run: cargo outdated --workspace --exit-code 1 --ignore raw-window-handle diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 16ee8bf9b9..36d43c0a3b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,11 +7,14 @@ jobs: - uses: hecrj/setup-rust-action@v2 with: components: clippy + - uses: actions/checkout@master - name: Install dependencies run: | export DEBIAN_FRONTED=noninteractive sudo apt-get -qq update - sudo apt-get install -y libxkbcommon-dev libgtk-3-dev + sudo apt-get install -y libxkbcommon-dev libgtk-3-dev libwayland-dev - name: Check lints - run: cargo lint + run: | + cargo clippy --no-default-features --features "winit" --all-targets + cargo clippy --no-default-features --features "wayland wgpu svg canvas qr_code lazy debug tokio palette web-colors a11y" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ea941509b1..2bff7b6ab1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,8 +19,38 @@ jobs: run: | export DEBIAN_FRONTED=noninteractive sudo apt-get -qq update - sudo apt-get install -y libxkbcommon-dev libgtk-3-dev + sudo apt-get install -y libxkbcommon-dev libwayland-dev - name: Run tests run: | - cargo test --verbose --workspace - cargo test --verbose --workspace --all-features + cargo test --verbose --features "winit wgpu svg canvas qr_code lazy debug tokio palette web-colors a11y" + cargo test -p iced_accessibility + cargo test -p iced_core + cargo test -p iced_futures + cargo test -p iced_graphics + cargo test -p iced_renderer + cargo test -p iced_runtime + cargo test -p iced_tiny_skia + cargo test -p iced_widget + cargo test -p iced_wgpu + - name: test wayland + if: matrix.os == 'ubuntu-latest' + run: | + cargo test --verbose --features "wayland wgpu svg canvas qr_code lazy debug tokio palette web-colors a11y" + cargo test -p iced_sctk + + web: + runs-on: ubuntu-latest + steps: + - uses: hecrj/setup-rust-action@v1 + with: + rust-version: stable + targets: wasm32-unknown-unknown + - uses: actions/checkout@master + - name: Run checks + run: cargo check --package iced --target wasm32-unknown-unknown --no-default-features --features "winit" + - name: Check compilation of `tour` example + run: cargo build --package tour --target wasm32-unknown-unknown + - name: Check compilation of `todos` example + run: cargo build --package todos --target wasm32-unknown-unknown + - name: Check compilation of `integration` example + run: cargo build --package integration --target wasm32-unknown-unknown diff --git a/CHANGELOG.md b/CHANGELOG.md index fff7d3417f..83fbc378e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -227,6 +227,8 @@ Many thanks to... - @wiiznokes - @woelfman - @Zaubentrucker +- @ryanabx +- @edfloreshz ## [0.12.1] - 2024-02-22 ### Added @@ -413,6 +415,10 @@ Many thanks to... - @william-shere - @wyatt-herkamp +Many thanks to... +- @jackpot51 +- @wash2 + ## [0.10.0] - 2023-07-28 ### Added - Text shaping, font fallback, and `iced_wgpu` overhaul. [#1697](https://github.com/iced-rs/iced/pull/1697) diff --git a/Cargo.toml b/Cargo.toml index ea78457a1b..f05272365a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ all-features = true maintenance = { status = "actively-developed" } [features] -default = ["wgpu", "tiny-skia", "fira-sans", "auto-detect-theme"] +default = ["tiny-skia"] # Enable the `wgpu` GPU-accelerated renderer backend wgpu = ["iced_renderer/wgpu", "iced_widget/wgpu"] # Enable the `tiny-skia` software renderer backend @@ -42,11 +42,11 @@ markdown = ["iced_widget/markdown"] # Enables lazy widgets lazy = ["iced_widget/lazy"] # Enables a debug view in native platforms (press F12) -debug = ["iced_winit/debug"] +debug = ["iced_winit?/debug"] # Enables `tokio` as the `executor::Default` on native platforms -tokio = ["iced_futures/tokio"] +tokio = ["iced_futures/tokio", "iced_accessibility?/tokio"] # Enables `async-std` as the `executor::Default` on native platforms -async-std = ["iced_futures/async-std"] +async-std = ["iced_futures/async-std", "iced_accessibility?/async-io"] # Enables `smol` as the `executor::Default` on native platforms smol = ["iced_futures/smol"] # Enables querying system information @@ -58,16 +58,29 @@ webgl = ["iced_renderer/webgl"] # Enables the syntax `highlighter` module highlighter = ["iced_highlighter", "iced_widget/highlighter"] # Enables experimental multi-window support. -multi-window = ["iced_winit/multi-window"] +multi-window = ["iced_winit?/multi-window"] # Enables the advanced module advanced = ["iced_core/advanced", "iced_widget/advanced"] # Enables embedding Fira Sans as the default font on Wasm builds fira-sans = ["iced_renderer/fira-sans"] + + # Enables auto-detecting light/dark mode for the built-in theme auto-detect-theme = ["iced_core/auto-detect-theme"] -# Enables strict assertions for debugging purposes at the expense of performance strict-assertions = ["iced_renderer/strict-assertions"] +# Enables the `accesskit` accessibility library +a11y = [ + "iced_accessibility", + "iced_core/a11y", + "iced_widget/a11y", + "iced_winit?/a11y", +] +# Enables the winit shell. Conflicts with `wayland` and `glutin`. +winit = ["iced_winit", "iced_accessibility?/accesskit_winit"] + +# Enables the sctk shell. +wayland = ["iced_widget/wayland", "iced_core/wayland", "iced_winit/wayland"] [dependencies] iced_core.workspace = true iced_futures.workspace = true @@ -75,11 +88,15 @@ iced_renderer.workspace = true iced_widget.workspace = true iced_winit.features = ["program"] iced_winit.workspace = true - +iced_winit.optional = true iced_highlighter.workspace = true iced_highlighter.optional = true - +iced_accessibility.workspace = true +iced_accessibility.optional = true thiserror.workspace = true +window_clipboard.workspace = true +mime.workspace = true +dnd.workspace = true image.workspace = true image.optional = true @@ -116,7 +133,9 @@ members = [ "widget", "winit", "examples/*", + "accessibility", ] +exclude = ["examples/integration"] [workspace.package] version = "0.14.0-dev" @@ -141,16 +160,27 @@ iced_tiny_skia = { version = "0.14.0-dev", path = "tiny_skia" } iced_wgpu = { version = "0.14.0-dev", path = "wgpu" } iced_widget = { version = "0.14.0-dev", path = "widget" } iced_winit = { version = "0.14.0-dev", path = "winit" } +iced_accessibility = { version = "0.1", path = "accessibility" } async-std = "1.0" -bitflags = "2.0" +# bitflags = "2.0" +bitflags = "2.5" bytemuck = { version = "1.0", features = ["derive"] } bytes = "1.6" -cosmic-text = "0.12" + +cosmic-text = { git = "https://github.com/pop-os/cosmic-text.git" } +# cosmic-text = "0.10" + + dark-light = "1.0" futures = "0.3" glam = "0.25" -glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "0d7ba1bba4dd71eb88d2cface5ce649db2413cb7" } +glyphon = { package = "iced_glyphon", git = "https://github.com/pop-os/glyphon.git", tag = "iced-0.14-dev" } +# glyphon = { package = "iced_glyphon", path = "../../../glyphon" } +resvg = "0.42" + + +web-sys = "0.3.69" guillotiere = "0.6" half = "2.2" image = { version = "0.24", default-features = false } @@ -166,12 +196,12 @@ palette = "0.7" pulldown-cmark = "0.11" qrcode = { version = "0.13", default-features = false } raw-window-handle = "0.6" -resvg = "0.42" rustc-hash = "2.0" +sctk = { package = "smithay-client-toolkit", version = "0.19.1" } smol = "1.0" smol_str = "0.2" -softbuffer = "0.4" -syntect = "5.1" +softbuffer = { git = "https://github.com/pop-os/softbuffer", tag = "cosmic-4.0" } +syntect = "5.2" sysinfo = "0.30" thiserror = "1.0" tiny-skia = "0.11" @@ -181,18 +211,33 @@ unicode-segmentation = "1.0" url = "2.5" wasm-bindgen-futures = "0.4" wasm-timer = "0.2" -web-sys = "0.3.69" web-time = "1.1" wgpu = "22.0" +wayland-protocols = { version = "0.32.1", features = ["staging"] } +# web-time = "1.1" + + +# wgpu = "0.19" +# Newer wgpu commit that fixes Vulkan backend on Nvidia winapi = "0.3" -window_clipboard = "0.4.1" -winit = "0.30" +# window_clipboard = "0.4.1" + +window_clipboard = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-0.13" } +dnd = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-0.13" } +mime = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-0.13" } +winit = { git = "https://github.com/pop-os/winit.git", tag = "iced-xdg-surface-0.13" } +# winit = { path = "../../winit" } +# winit = { git = "https://github.com/iced-rs/winit.git", rev = "254d6b3420ce4e674f516f7a2bd440665e05484d" } +# winit = { git = "https://github.com/rust-windowing/winit.git", rev = "241b7a80bba96c91fa3901729cd5dec66abb9be4" } +# winit = { path = "../../../winit" } + [workspace.lints.rust] -rust_2018_idioms = { level = "forbid", priority = -1 } -missing_debug_implementations = "deny" -missing_docs = "deny" -unsafe_code = "deny" +# rust_2018_idioms = { level = "forbid", priority = -1 } +# missing_debug_implementations = "deny" +# missing_docs = "deny" +# unsafe_code = "deny" +# TODO(POP): We have some unsafe code that needs to be fixed unused_results = "deny" [workspace.lints.clippy] @@ -212,3 +257,7 @@ useless_conversion = "deny" [workspace.lints.rustdoc] broken_intra_doc_links = "forbid" + +# [patch."https://github.com/rust-windowing/winit.git"] +# winit = { git = "https://github.com/rust-windowing/winit.git", rev = "241b7a80bba96c91fa3901729cd5dec66abb9be4" } +# winit = { path = "../../../winit" } diff --git a/accessibility/Cargo.toml b/accessibility/Cargo.toml new file mode 100644 index 0000000000..78af375be6 --- /dev/null +++ b/accessibility/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "iced_accessibility" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +# TODO Ashley re-export more platform adapters +[features] +async-io = ["accesskit_winit?/async-io"] +tokio = ["accesskit_winit?/tokio"] + +[dependencies] +accesskit = { git = "https://github.com/wash2/accesskit", tag = "iced-xdg-surface-0.13" } +accesskit_windows = { git = "https://github.com/wash2/accesskit", tag = "iced-xdg-surface-0.13", optional = true } +accesskit_macos = { git = "https://github.com/wash2/accesskit", tag = "iced-xdg-surface-0.13", optional = true } +accesskit_winit = { git = "https://github.com/wash2/accesskit", tag = "iced-xdg-surface-0.13", optional = true, default-features = false, features = [ + "rwh_06", +] } +# accesskit = { path = "../../../../accesskit/common" } +# accesskit_windows = { path = "../../../../accesskit/platforms/windows", optional = true } +# accesskit_macos = { path = "../../../../accesskit/platforms/macos", optional = true } +# accesskit_winit = { path = "../../../../accesskit/platforms/winit", optional = true, default-features = false, features = [ +# "rwh_06", +# ] } diff --git a/accessibility/src/a11y_tree.rs b/accessibility/src/a11y_tree.rs new file mode 100644 index 0000000000..bb61981571 --- /dev/null +++ b/accessibility/src/a11y_tree.rs @@ -0,0 +1,80 @@ +use crate::{A11yId, A11yNode, IdEq}; + +#[derive(Debug, Clone, Default)] +/// Accessible tree of nodes +pub struct A11yTree { + /// The root of the current widget, children of the parent widget or the Window if there is no parent widget + root: Vec, + /// The children of a widget and its children + children: Vec, +} + +impl A11yTree { + /// Create a new A11yTree + /// XXX if you use this method, you will need to manually add the children of the root nodes + pub fn new(root: Vec, children: Vec) -> Self { + Self { root, children } + } + + pub fn leaf>(node: accesskit::NodeBuilder, id: T) -> Self { + Self { + root: vec![A11yNode::new(node, id)], + children: vec![], + } + } + + /// Helper for creating an A11y tree with a single root node and some children + pub fn node_with_child_tree(mut root: A11yNode, child_tree: Self) -> Self { + root.add_children( + child_tree.root.iter().map(|n| n.id()).cloned().collect(), + ); + Self { + root: vec![root], + children: child_tree + .children + .into_iter() + .chain(child_tree.root) + .collect(), + } + } + + /// Joins multiple trees into a single tree + pub fn join>(trees: T) -> Self { + trees.fold(Self::default(), |mut acc, A11yTree { root, children }| { + acc.root.extend(root); + acc.children.extend(children); + acc + }) + } + + pub fn root(&self) -> &Vec { + &self.root + } + + pub fn children(&self) -> &Vec { + &self.children + } + + pub fn root_mut(&mut self) -> &mut Vec { + &mut self.root + } + + pub fn children_mut(&mut self) -> &mut Vec { + &mut self.children + } + + pub fn contains(&self, id: &A11yId) -> bool { + self.root.iter().any(|n| IdEq::eq(n.id(), id)) + || self.children.iter().any(|n| IdEq::eq(n.id(), id)) + } +} + +impl From for Vec<(accesskit::NodeId, accesskit::Node)> { + fn from(tree: A11yTree) -> Vec<(accesskit::NodeId, accesskit::Node)> { + tree.root + .into_iter() + .map(|node| node.into()) + .chain(tree.children.into_iter().map(|node| node.into())) + .collect() + } +} diff --git a/accessibility/src/id.rs b/accessibility/src/id.rs new file mode 100644 index 0000000000..752e51c192 --- /dev/null +++ b/accessibility/src/id.rs @@ -0,0 +1,215 @@ +//! Widget and Window IDs. + +use std::borrow; +use std::hash::Hash; +use std::sync::atomic::{self, AtomicU64}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum A11yId { + Window(u64), + Widget(Id), +} + +// impl A11yId { +// pub fn new_widget() -> Self { +// Self::Widget(Id::unique()) +// } + +// pub fn new_window() -> Self { +// Self::Window(window_node_id()) +// } +// } + +impl From for A11yId { + fn from(id: u64) -> Self { + Self::Window(id) + } +} + +impl From for A11yId { + fn from(id: Id) -> Self { + assert!(!matches!(id.0, Internal::Set(_))); + Self::Widget(id) + } +} + +impl IdEq for A11yId { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (A11yId::Widget(self_), A11yId::Widget(other)) => { + IdEq::eq(self_, other) + } + _ => self == other, + } + } +} + +impl From for A11yId { + fn from(value: accesskit::NodeId) -> Self { + let val = u64::from(value.0); + if val > u32::MAX as u64 { + Self::Window(value.0) + } else { + Self::Widget(Id::from(val as u64)) + } + } +} + +impl From for accesskit::NodeId { + fn from(value: A11yId) -> Self { + let node_id = match value { + A11yId::Window(id) => id, + A11yId::Widget(id) => id.into(), + }; + accesskit::NodeId(node_id) + } +} + +static NEXT_ID: AtomicU64 = AtomicU64::new(1); +static NEXT_WINDOW_ID: AtomicU64 = AtomicU64::new(1); + +/// The identifier of a generic widget. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Id(pub Internal); + +impl Id { + /// Creates a custom [`Id`]. + pub fn new(id: impl Into>) -> Self { + Self(Internal::Custom(Self::next(), id.into())) + } + + /// resets the id counter + pub fn reset() { + NEXT_ID.store(1, atomic::Ordering::Relaxed); + } + + fn next() -> u64 { + NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed) + } + + /// Creates a unique [`Id`]. + /// + /// This function produces a different [`Id`] every time it is called. + pub fn unique() -> Self { + let id = Self::next(); + + Self(Internal::Unique(id)) + } +} + +impl IdEq for Id { + fn eq(&self, other: &Self) -> bool { + IdEq::eq(&self.0, &other.0) + } +} +// Not meant to be used directly +impl From for Id { + fn from(value: u64) -> Self { + Self(Internal::Unique(value)) + } +} + +// Not meant to be used directly +impl From for u64 { + fn from(val: Id) -> u64 { + match &val.0 { + Internal::Unique(id) => *id, + Internal::Custom(id, _) => *id, + // this is a set id, which is not a valid id and will not ever be converted to a NonZeroU128 + // so we panic + Internal::Set(_) => { + panic!("Cannot convert a set id to a NonZeroU128") + } + } + } +} + +impl ToString for Id { + fn to_string(&self) -> String { + match &self.0 { + Internal::Unique(_) => "Undefined".to_string(), + Internal::Custom(_, id) => id.to_string(), + Internal::Set(_) => "Set".to_string(), + } + } +} + +// XXX WIndow IDs are made unique by adding u32::MAX to them +/// get window node id that won't conflict with other node ids for the duration of the program +pub fn window_node_id() -> u64 { + u32::MAX as u64 + + NEXT_WINDOW_ID.fetch_add(1, atomic::Ordering::Relaxed) as u64 +} + +// TODO refactor to make panic impossible? +#[derive(Debug, Clone, Eq)] +/// Internal representation of an [`Id`]. +pub enum Internal { + /// a unique id + Unique(u64), + /// a custom id, which is equal to any [`Id`] with a matching number or string + Custom(u64, borrow::Cow<'static, str>), + /// XXX Do not use this as an id for an accessibility node, it will panic! + /// XXX Only meant to be used for widgets that have multiple accessibility nodes, each with a + /// unique or custom id + /// an Id Set, which is equal to any [`Id`] with a matching number or string + Set(Vec), +} + +impl PartialEq for Internal { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Unique(l0), Self::Unique(r0)) => l0 == r0, + (Self::Custom(_, l1), Self::Custom(_, r1)) => l1 == r1, + (Self::Set(l0), Self::Set(r0)) => l0 == r0, + _ => false, + } + } +} + +/// Similar to PartialEq, but only intended for use when comparing Ids +pub trait IdEq { + fn eq(&self, other: &Self) -> bool; +} + +impl IdEq for Internal { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Unique(l0), Self::Unique(r0)) => l0 == r0, + (Self::Custom(l0, l1), Self::Custom(r0, r1)) => { + l0 == r0 || l1 == r1 + } + // allow custom ids to be equal to unique ids + (Self::Unique(l0), Self::Custom(r0, _)) + | (Self::Custom(l0, _), Self::Unique(r0)) => l0 == r0, + (Self::Set(l0), Self::Set(r0)) => l0 == r0, + // allow set ids to just be equal to any of their members + (Self::Set(l0), r) | (r, Self::Set(l0)) => { + l0.iter().any(|l| l == r) + } + } + } +} + +impl Hash for Internal { + fn hash(&self, state: &mut H) { + match self { + Self::Unique(id) => id.hash(state), + Self::Custom(name, _) => name.hash(state), + Self::Set(ids) => ids.hash(state), + } + } +} + +#[cfg(test)] +mod tests { + use super::Id; + + #[test] + fn unique_generates_different_ids() { + let a = Id::unique(); + let b = Id::unique(); + + assert_ne!(a, b); + } +} diff --git a/accessibility/src/lib.rs b/accessibility/src/lib.rs new file mode 100644 index 0000000000..273a4737c6 --- /dev/null +++ b/accessibility/src/lib.rs @@ -0,0 +1,17 @@ +mod a11y_tree; +pub mod id; +mod node; +mod traits; + +pub use a11y_tree::*; +pub use accesskit; +pub use id::*; +pub use node::*; +pub use traits::*; + +#[cfg(feature = "accesskit_macos")] +pub use accesskit_macos; +#[cfg(feature = "accesskit_windows")] +pub use accesskit_windows; +#[cfg(feature = "accesskit_winit")] +pub use accesskit_winit; diff --git a/accessibility/src/node.rs b/accessibility/src/node.rs new file mode 100644 index 0000000000..bfe89c20f0 --- /dev/null +++ b/accessibility/src/node.rs @@ -0,0 +1,41 @@ +use crate::A11yId; + +#[derive(Debug, Clone, PartialEq)] +pub struct A11yNode { + node: accesskit::NodeBuilder, + id: A11yId, +} + +impl A11yNode { + pub fn new>(node: accesskit::NodeBuilder, id: T) -> Self { + Self { + node, + id: id.into(), + } + } + + pub fn id(&self) -> &A11yId { + &self.id + } + + pub fn node_mut(&mut self) -> &mut accesskit::NodeBuilder { + &mut self.node + } + + pub fn node(&self) -> &accesskit::NodeBuilder { + &self.node + } + + pub fn add_children(&mut self, children: Vec) { + let mut children = + children.into_iter().map(|id| id.into()).collect::>(); + children.extend_from_slice(self.node.children()); + self.node.set_children(children); + } +} + +impl From for (accesskit::NodeId, accesskit::Node) { + fn from(node: A11yNode) -> Self { + (node.id.into(), node.node.build()) + } +} diff --git a/accessibility/src/traits.rs b/accessibility/src/traits.rs new file mode 100644 index 0000000000..88e1d9208e --- /dev/null +++ b/accessibility/src/traits.rs @@ -0,0 +1,19 @@ +use std::borrow::Cow; + +use crate::A11yId; + +#[derive(Debug, Clone, PartialEq)] +pub enum Description<'a> { + Text(Cow<'a, str>), + Id(Vec), +} + +// Describes a widget +pub trait Describes { + fn description(&self) -> Vec; +} + +// Labels a widget +pub trait Labels { + fn label(&self) -> Vec; +} diff --git a/core/Cargo.toml b/core/Cargo.toml index a12289099c..ce5b4d1dd2 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -16,6 +16,8 @@ workspace = true [features] auto-detect-theme = ["dep:dark-light"] advanced = [] +a11y = ["iced_accessibility"] +wayland = ["sctk"] [dependencies] bitflags.workspace = true @@ -30,8 +32,31 @@ smol_str.workspace = true thiserror.workspace = true web-time.workspace = true +# TODO(POP): I think some of these dependencies were removed. Check on that +# xxhash-rust.workspace = true +window_clipboard.workspace = true +dnd.workspace = true +mime.workspace = true + +sctk.workspace = true +sctk.optional = true +# /TODO(POP) + dark-light.workspace = true dark-light.optional = true +[dependencies.serde] +version = "1" +optional = true +features = ["serde_derive"] + + +[target.'cfg(windows)'.dependencies] +raw-window-handle.workspace = true + [dev-dependencies] approx = "0.5" +[dependencies.iced_accessibility] +version = "0.1.0" +path = "../accessibility" +optional = true diff --git a/core/src/border.rs b/core/src/border.rs index da0aaa2808..8d9c6a21be 100644 --- a/core/src/border.rs +++ b/core/src/border.rs @@ -263,3 +263,54 @@ impl From for [f32; 4] { ] } } + +impl From<[f32; 4]> for Radius { + /// [ + /// radi.top_left, + /// radi.top_right, + /// radi.bottom_right, + /// radi.bottom_left, + /// ] + fn from(value: [f32; 4]) -> Self { + Self { + top_left: value[0], + top_right: value[1], + bottom_right: value[2], + bottom_left: value[3], + } + } +} + +impl From<[u8; 4]> for Radius { + /// [ + /// radi.top_left, + /// radi.top_right, + /// radi.bottom_right, + /// radi.bottom_left, + /// ] + fn from(value: [u8; 4]) -> Self { + Self { + top_left: f32::from(value[0]), + top_right: f32::from(value[1]), + bottom_right: f32::from(value[2]), + bottom_left: f32::from(value[3]), + } + } +} + +impl From<[u16; 4]> for Radius { + /// [ + /// radi.top_left, + /// radi.top_right, + /// radi.bottom_right, + /// radi.bottom_left, + /// ] + fn from(value: [u16; 4]) -> Self { + Self { + top_left: f32::from(value[0]), + top_right: f32::from(value[1]), + bottom_right: f32::from(value[2]), + bottom_left: f32::from(value[3]), + } + } +} diff --git a/core/src/clipboard.rs b/core/src/clipboard.rs index 5df3e26758..3e7458c024 100644 --- a/core/src/clipboard.rs +++ b/core/src/clipboard.rs @@ -1,5 +1,12 @@ //! Access the clipboard. +use std::{any::Any, sync::Arc}; + +use dnd::{DndAction, DndDestinationRectangle, DndSurface}; +use mime::{self, AllowedMimeTypes, AsMimeTypes, ClipboardStoreData}; + +use crate::{widget::tree::State, window, Element}; + /// A buffer for short-term storage and transfer within and between /// applications. pub trait Clipboard { @@ -8,6 +15,62 @@ pub trait Clipboard { /// Writes the given text contents to the [`Clipboard`]. fn write(&mut self, kind: Kind, contents: String); + + /// Consider using [`read_data`] instead + /// Reads the current content of the [`Clipboard`] as text. + fn read_data( + &self, + kind: Kind, + _mimes: Vec, + ) -> Option<(Vec, String)> { + None + } + + /// Writes the given contents to the [`Clipboard`]. + fn write_data( + &mut self, + kind: Kind, + _contents: ClipboardStoreData< + Box, + >, + ) { + } + + /// Starts a DnD operation. + fn register_dnd_destination( + &self, + _surface: DndSurface, + _rectangles: Vec, + ) { + } + + /// Set the final action for the DnD operation. + /// Only should be done if it is requested. + fn set_action(&self, _action: DndAction) {} + + /// Registers Dnd destinations + fn start_dnd( + &mut self, + _internal: bool, + _source_surface: Option, + _icon_surface: Option>, + _content: Box, + _actions: DndAction, + ) { + } + + /// Ends a DnD operation. + fn end_dnd(&self) {} + + /// Consider using [`peek_dnd`] instead + /// Peeks the data on the DnD with a specific mime type. + /// Will return an error if there is no ongoing DnD operation. + fn peek_dnd(&self, _mime: String) -> Option<(Vec, String)> { + None + } + + /// Request window size + fn request_logical_window_size(&self, width: f32, height: f32) {} } /// The kind of [`Clipboard`]. @@ -21,6 +84,28 @@ pub enum Kind { Primary, } +/// Starts a DnD operation. +/// icon surface is a tuple of the icon element and optionally the icon element state. +pub fn start_dnd( + clipboard: &mut dyn Clipboard, + internal: bool, + source_surface: Option, + icon_surface: Option<(Element<'static, M, T, R>, State)>, + content: Box, + actions: DndAction, +) { + clipboard.start_dnd( + internal, + source_surface, + icon_surface.map(|i| { + let i: Box = Box::new(Arc::new(i)); + i + }), + content, + actions, + ); +} + /// A null implementation of the [`Clipboard`] trait. #[derive(Debug, Clone, Copy)] pub struct Null; @@ -32,3 +117,90 @@ impl Clipboard for Null { fn write(&mut self, _kind: Kind, _contents: String) {} } + +/// Reads the current content of the [`Clipboard`]. +pub fn read_data( + clipboard: &mut dyn Clipboard, +) -> Option { + clipboard + .read_data(Kind::Standard, T::allowed().into()) + .and_then(|data| T::try_from(data).ok()) +} + +/// Reads the current content of the primary [`Clipboard`]. +pub fn read_primary_data( + clipboard: &mut dyn Clipboard, +) -> Option { + clipboard + .read_data(Kind::Primary, T::allowed().into()) + .and_then(|data| T::try_from(data).ok()) +} + +/// Reads the current content of the primary [`Clipboard`]. +pub fn peek_dnd( + clipboard: &mut dyn Clipboard, + mime: Option, +) -> Option { + let Some(mime) = mime.or_else(|| T::allowed().first().cloned().into()) + else { + return None; + }; + clipboard + .peek_dnd(mime) + .and_then(|data| T::try_from(data).ok()) +} + +/// Source of a DnD operation. +#[derive(Debug, Clone)] +pub enum DndSource { + /// A widget is the source of the DnD operation. + Widget(crate::id::Id), + /// A surface is the source of the DnD operation. + Surface(window::Id), +} + +/// A list of DnD destination rectangles. +#[derive(Debug, Clone)] +pub struct DndDestinationRectangles { + /// The rectangle of the DnD destination. + rectangles: Vec, +} + +impl DndDestinationRectangles { + /// Creates a new [`DndDestinationRectangles`]. + pub fn new() -> Self { + Self { + rectangles: Vec::new(), + } + } + + /// Creates a new [`DndDestinationRectangles`] with the given capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self { + rectangles: Vec::with_capacity(capacity), + } + } + + /// Pushes a new rectangle to the list of DnD destination rectangles. + pub fn push(&mut self, rectangle: DndDestinationRectangle) { + self.rectangles.push(rectangle); + } + + /// Appends the list of DnD destination rectangles to the current list. + pub fn append(&mut self, other: &mut Vec) { + self.rectangles.append(other); + } + + /// Returns the list of DnD destination rectangles. + /// This consumes the [`DndDestinationRectangles`]. + pub fn into_rectangles(mut self) -> Vec { + self.rectangles.reverse(); + self.rectangles + } +} + +impl AsRef<[DndDestinationRectangle]> for DndDestinationRectangles { + fn as_ref(&self) -> &[DndDestinationRectangle] { + &self.rectangles + } +} diff --git a/core/src/element.rs b/core/src/element.rs index 6ebb8a1576..3cb0f17a81 100644 --- a/core/src/element.rs +++ b/core/src/element.rs @@ -1,4 +1,5 @@ use crate::event::{self, Event}; +use crate::id::Id; use crate::layout; use crate::mouse; use crate::overlay; @@ -10,7 +11,7 @@ use crate::{ Widget, }; -use std::borrow::Borrow; +use std::borrow::{Borrow, BorrowMut}; /// A generic [`Widget`]. /// @@ -239,6 +240,37 @@ impl<'a, Message, Theme, Renderer> } } +impl<'a, Message, Theme, Renderer> + Borrow + 'a> + for &mut Element<'a, Message, Theme, Renderer> +{ + fn borrow(&self) -> &(dyn Widget + 'a) { + self.widget.borrow() + } +} + +impl<'a, Message, Theme, Renderer> + BorrowMut + 'a> + for &mut Element<'a, Message, Theme, Renderer> +{ + fn borrow_mut( + &mut self, + ) -> &mut (dyn Widget + 'a) { + self.widget.borrow_mut() + } +} + +impl<'a, Message, Theme, Renderer> + BorrowMut + 'a> + for Element<'a, Message, Theme, Renderer> +{ + fn borrow_mut( + &mut self, + ) -> &mut (dyn Widget + 'a) { + self.widget.borrow_mut() + } +} + struct Map<'a, A, B, Theme, Renderer> { widget: Box + 'a>, mapper: Box B + 'a>, @@ -278,8 +310,8 @@ where self.widget.children() } - fn diff(&self, tree: &mut Tree) { - self.widget.diff(tree); + fn diff(&mut self, tree: &mut Tree) { + self.widget.diff(tree) } fn size(&self) -> Size { @@ -378,6 +410,35 @@ where .overlay(tree, layout, renderer, translation) .map(move |overlay| overlay.map(mapper)) } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + _layout: Layout<'_>, + _state: &Tree, + _cursor_position: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + self.widget.a11y_nodes(_layout, _state, _cursor_position) + } + + fn id(&self) -> Option { + self.widget.id() + } + + fn set_id(&mut self, id: Id) { + self.widget.set_id(id); + } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut crate::clipboard::DndDestinationRectangles, + ) { + self.widget + .drag_destinations(state, layout, renderer, dnd_rectangles); + } } struct Explain<'a, Message, Theme, Renderer: crate::Renderer> { @@ -422,7 +483,7 @@ where self.element.widget.children() } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { self.element.widget.diff(tree); } @@ -527,4 +588,28 @@ where .widget .overlay(state, layout, renderer, translation) } + + fn id(&self) -> Option { + self.element.widget.id() + } + + fn set_id(&mut self, id: Id) { + self.element.widget.set_id(id); + } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut crate::clipboard::DndDestinationRectangles, + ) { + self.element.widget.drag_destinations( + state, + layout, + renderer, + dnd_rectangles, + ); + } + // TODO maybe a11y_nodes } diff --git a/core/src/event.rs b/core/src/event.rs index b6cf321ec3..791a790fc1 100644 --- a/core/src/event.rs +++ b/core/src/event.rs @@ -1,9 +1,14 @@ //! Handle events of a user interface. +use dnd::DndEvent; +use dnd::DndSurface; + use crate::keyboard; use crate::mouse; use crate::touch; use crate::window; - +#[cfg(feature = "wayland")] +/// A platform specific event for wayland +pub mod wayland; /// A user interface event. /// /// _**Note:** This type is largely incomplete! If you need to track @@ -23,6 +28,27 @@ pub enum Event { /// A touch event Touch(touch::Event), + + #[cfg(feature = "a11y")] + /// An Accesskit event for a specific Accesskit Node in an accessible widget + A11y( + crate::widget::Id, + iced_accessibility::accesskit::ActionRequest, + ), + + /// A DnD event. + Dnd(DndEvent), + + /// Platform specific events + PlatformSpecific(PlatformSpecific), +} + +/// A platform specific event +#[derive(Debug, Clone, PartialEq)] +pub enum PlatformSpecific { + #[cfg(feature = "wayland")] + /// A Wayland specific event + Wayland(wayland::Event), } /// The status of an [`Event`] after being processed. diff --git a/core/src/event/wayland/layer.rs b/core/src/event/wayland/layer.rs new file mode 100644 index 0000000000..c1928ad36e --- /dev/null +++ b/core/src/event/wayland/layer.rs @@ -0,0 +1,10 @@ +/// layer surface events +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LayerEvent { + /// layer surface Done + Done, + /// layer surface focused + Focused, + /// layer_surface unfocused + Unfocused, +} diff --git a/core/src/event/wayland/mod.rs b/core/src/event/wayland/mod.rs new file mode 100644 index 0000000000..0e908d6708 --- /dev/null +++ b/core/src/event/wayland/mod.rs @@ -0,0 +1,39 @@ +mod layer; +mod output; +mod popup; +mod seat; +mod session_lock; +mod window; + +use crate::{time::Instant, window::Id}; +use sctk::reexports::client::protocol::{ + wl_output::WlOutput, wl_seat::WlSeat, wl_surface::WlSurface, +}; + +pub use layer::*; +pub use output::*; +pub use popup::*; +pub use seat::*; +pub use session_lock::*; +pub use window::*; + +/// wayland events +#[derive(Debug, Clone, PartialEq)] +pub enum Event { + /// layer surface event + Layer(LayerEvent, WlSurface, Id), + /// popup event + Popup(PopupEvent, WlSurface, Id), + /// output event + Output(OutputEvent, WlOutput), + /// window event + Window(WindowEvent), + /// Seat Event + Seat(SeatEvent, WlSeat), + /// Session lock events + SessionLock(SessionLockEvent), + /// Frame events + Frame(Instant, WlSurface, Id), + /// Request Resize + RequestResize, +} diff --git a/core/src/event/wayland/output.rs b/core/src/event/wayland/output.rs new file mode 100644 index 0000000000..c5024e85b7 --- /dev/null +++ b/core/src/event/wayland/output.rs @@ -0,0 +1,34 @@ +use sctk::output::OutputInfo; + +/// output events +#[derive(Debug, Clone)] +pub enum OutputEvent { + /// created output + Created(Option), + /// removed output + Removed, + /// Output Info + InfoUpdate(OutputInfo), +} + +impl Eq for OutputEvent {} + +impl PartialEq for OutputEvent { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Created(l0), Self::Created(r0)) => { + if let Some((l0, r0)) = l0.as_ref().zip(r0.as_ref()) { + l0.id == r0.id && l0.make == r0.make && l0.model == r0.model + } else { + l0.is_none() && r0.is_none() + } + } + (Self::InfoUpdate(l0), Self::InfoUpdate(r0)) => { + l0.id == r0.id && l0.make == r0.make && l0.model == r0.model + } + _ => { + core::mem::discriminant(self) == core::mem::discriminant(other) + } + } + } +} diff --git a/core/src/event/wayland/popup.rs b/core/src/event/wayland/popup.rs new file mode 100644 index 0000000000..ff925870b2 --- /dev/null +++ b/core/src/event/wayland/popup.rs @@ -0,0 +1,21 @@ +/// popup events +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PopupEvent { + /// Done + Done, + /// repositioned, + Configured { + /// x position + x: i32, + /// y position + y: i32, + /// width + width: u32, + /// height + height: u32, + }, + /// popup focused + Focused, + /// popup unfocused + Unfocused, +} diff --git a/core/src/event/wayland/seat.rs b/core/src/event/wayland/seat.rs new file mode 100644 index 0000000000..3da4374e71 --- /dev/null +++ b/core/src/event/wayland/seat.rs @@ -0,0 +1,9 @@ +/// seat events +/// Only one seat can interact with an iced_sctk application at a time, but many may interact with the application over the lifetime of the application +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SeatEvent { + /// A new seat is interacting with the application + Enter, + /// A seat is not interacting with the application anymore + Leave, +} diff --git a/core/src/event/wayland/session_lock.rs b/core/src/event/wayland/session_lock.rs new file mode 100644 index 0000000000..db99566d95 --- /dev/null +++ b/core/src/event/wayland/session_lock.rs @@ -0,0 +1,19 @@ +use crate::window::Id; +use sctk::reexports::client::protocol::wl_surface::WlSurface; + +/// session lock events +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SessionLockEvent { + /// Compositor has activated lock + Locked, + /// Lock rejected / canceled by compositor + Finished, + /// Session lock protocol not supported + NotSupported, + /// Session lock surface focused + Focused(WlSurface, Id), + /// Session lock surface unfocused + Unfocused(WlSurface, Id), + /// Session unlock has been processed by server + Unlocked, +} diff --git a/core/src/event/wayland/window.rs b/core/src/event/wayland/window.rs new file mode 100644 index 0000000000..70085dac5b --- /dev/null +++ b/core/src/event/wayland/window.rs @@ -0,0 +1,8 @@ +#![allow(missing_docs)] + +/// window events +#[derive(Debug, PartialEq, Clone)] +pub enum WindowEvent { + /// Window suggested bounds. + SuggestedBounds(Option), +} diff --git a/core/src/id.rs b/core/src/id.rs new file mode 100644 index 0000000000..3fc74ce925 --- /dev/null +++ b/core/src/id.rs @@ -0,0 +1,155 @@ +//! Widget and Window IDs. + +use std::borrow; +use std::num::NonZeroU128; +use std::sync::atomic::{self, AtomicU64}; + +static NEXT_ID: AtomicU64 = AtomicU64::new(1); +static NEXT_WINDOW_ID: AtomicU64 = AtomicU64::new(1); + +/// The identifier of a generic widget. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Id(pub Internal); + +impl Id { + /// Creates a custom [`Id`]. + pub fn new(id: impl Into>) -> Self { + Self(Internal::Custom(Self::next(), id.into())) + } + + /// resets the id counter + pub fn reset() { + NEXT_ID.store(1, atomic::Ordering::Relaxed); + } + + fn next() -> u64 { + NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed) + } + + /// Creates a unique [`Id`]. + /// + /// This function produces a different [`Id`] every time it is called. + pub fn unique() -> Self { + let id = Self::next(); + + Self(Internal::Unique(id)) + } +} + +// Not meant to be used directly +impl From for Id { + fn from(value: u64) -> Self { + Self(Internal::Unique(value)) + } +} + +// Not meant to be used directly +impl From for NonZeroU128 { + fn from(id: Id) -> NonZeroU128 { + match &id.0 { + Internal::Unique(id) => NonZeroU128::try_from(*id as u128).unwrap(), + Internal::Custom(id, _) => { + NonZeroU128::try_from(*id as u128).unwrap() + } + // this is a set id, which is not a valid id and will not ever be converted to a NonZeroU128 + // so we panic + Internal::Set(_) => { + panic!("Cannot convert a set id to a NonZeroU128") + } + } + } +} + +impl ToString for Id { + fn to_string(&self) -> String { + match &self.0 { + Internal::Unique(_) => "Undefined".to_string(), + Internal::Custom(_, id) => id.to_string(), + Internal::Set(_) => "Set".to_string(), + } + } +} + +// XXX WIndow IDs are made unique by adding u64::MAX to them +/// get window node id that won't conflict with other node ids for the duration of the program +pub fn window_node_id() -> NonZeroU128 { + std::num::NonZeroU128::try_from( + u64::MAX as u128 + + NEXT_WINDOW_ID.fetch_add(1, atomic::Ordering::Relaxed) as u128, + ) + .unwrap() +} + +// TODO refactor to make panic impossible? +#[derive(Debug, Clone, Eq)] +/// Internal representation of an [`Id`]. +pub enum Internal { + /// a unique id + Unique(u64), + /// a custom id, which is equal to any [`Id`] with a matching number or string + Custom(u64, borrow::Cow<'static, str>), + /// XXX Do not use this as an id for an accessibility node, it will panic! + /// XXX Only meant to be used for widgets that have multiple accessibility nodes, each with a + /// unique or custom id + /// an Id Set, which is equal to any [`Id`] with a matching number or string + Set(Vec), +} + +impl PartialEq for Internal { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Unique(l0), Self::Unique(r0)) => l0 == r0, + (Self::Custom(_, l1), Self::Custom(_, r1)) => l1 == r1, + (Self::Set(l0), Self::Set(r0)) => l0 == r0, + _ => false, + } + } +} + +/// Similar to PartialEq, but only intended for use when comparing Ids +pub trait IdEq { + /// Compare two Ids for equality based on their number or name + fn eq(&self, other: &Self) -> bool; +} + +impl IdEq for Internal { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Unique(l0), Self::Unique(r0)) => l0 == r0, + (Self::Custom(l0, l1), Self::Custom(r0, r1)) => { + l0 == r0 || l1 == r1 + } + // allow custom ids to be equal to unique ids + (Self::Unique(l0), Self::Custom(r0, _)) + | (Self::Custom(l0, _), Self::Unique(r0)) => l0 == r0, + (Self::Set(l0), Self::Set(r0)) => l0 == r0, + // allow set ids to just be equal to any of their members + (Self::Set(l0), r) | (r, Self::Set(l0)) => { + l0.iter().any(|l| l == r) + } + } + } +} + +impl std::hash::Hash for Internal { + fn hash(&self, state: &mut H) { + match self { + Self::Unique(id) => id.hash(state), + Self::Custom(name, _) => name.hash(state), + Self::Set(ids) => ids.hash(state), + } + } +} + +#[cfg(test)] +mod tests { + use super::Id; + + #[test] + fn unique_generates_different_ids() { + let a = Id::unique(); + let b = Id::unique(); + + assert_ne!(a, b); + } +} diff --git a/core/src/image.rs b/core/src/image.rs index f985636a39..0dd1d60efc 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -29,6 +29,9 @@ pub struct Image { /// This can avoid graphical glitches, specially when using /// [`FilterMethod::Nearest`]. pub snap: bool, + + /// The border radii of the image + pub border_radius: [f32; 4], } impl Image { @@ -40,6 +43,7 @@ impl Image { rotation: Radians(0.0), opacity: 1.0, snap: false, + border_radius: [0.0; 4], } } @@ -75,7 +79,7 @@ impl From<&Handle> for Image { } /// A handle of some image data. -#[derive(Clone, PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq, Hash)] pub enum Handle { /// A file handle. The image data will be read /// from the file path. @@ -239,6 +243,15 @@ pub trait Renderer: crate::Renderer { /// Returns the dimensions of an image for the given [`Handle`]. fn measure_image(&self, handle: &Self::Handle) -> Size; - /// Draws an [`Image`] inside the provided `bounds`. - fn draw_image(&mut self, image: Image, bounds: Rectangle); + /// Draws an image with the given [`Handle`] and inside the provided + /// `bounds`. + fn draw_image( + &mut self, + handle: Self::Handle, + filter_method: FilterMethod, + bounds: Rectangle, + rotation: Radians, + opacity: f32, + border_radius: [f32; 4], + ); } diff --git a/core/src/keyboard/key.rs b/core/src/keyboard/key.rs index 69a91902fc..00f6fd42ae 100644 --- a/core/src/keyboard/key.rs +++ b/core/src/keyboard/key.rs @@ -7,6 +7,7 @@ use crate::SmolStr; /// /// [`winit`]: https://docs.rs/winit/0.29.10/winit/keyboard/enum.Key.html #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Key { /// A key with an established name. Named(Named), @@ -38,6 +39,7 @@ impl Key { /// /// [`winit`]: https://docs.rs/winit/0.29.10/winit/keyboard/enum.Key.html #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[allow(missing_docs)] pub enum Named { /// The `Alt` (Alternative) key. diff --git a/core/src/lib.rs b/core/src/lib.rs index df599f45df..12c1e42a15 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -35,6 +35,9 @@ mod background; mod color; mod content_fit; mod element; +mod hasher; +#[cfg(not(feature = "a11y"))] +pub mod id; mod length; mod pixels; mod point; @@ -57,6 +60,9 @@ pub use element::Element; pub use event::Event; pub use font::Font; pub use gradient::Gradient; +pub use hasher::Hasher; +#[cfg(feature = "a11y")] +pub use iced_accessibility::id; pub use image::Image; pub use layout::Layout; pub use length::Length; diff --git a/core/src/mouse/click.rs b/core/src/mouse/click.rs index 0a373878c0..944347d883 100644 --- a/core/src/mouse/click.rs +++ b/core/src/mouse/click.rs @@ -81,7 +81,6 @@ impl Click { } else { None }; - self.position.distance(new_position) < 6.0 && duration .map(|duration| duration.as_millis() <= 300) diff --git a/core/src/mouse/interaction.rs b/core/src/mouse/interaction.rs index aad6a3ea93..36cf293a1d 100644 --- a/core/src/mouse/interaction.rs +++ b/core/src/mouse/interaction.rs @@ -22,4 +22,5 @@ pub enum Interaction { Move, Copy, Help, + Hide, } diff --git a/core/src/overlay.rs b/core/src/overlay.rs index f09de8312b..1cb98f8982 100644 --- a/core/src/overlay.rs +++ b/core/src/overlay.rs @@ -9,7 +9,7 @@ use crate::event::{self, Event}; use crate::layout; use crate::mouse; use crate::renderer; -use crate::widget; +use crate::widget::Operation; use crate::widget::Tree; use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size, Vector}; @@ -36,12 +36,12 @@ where cursor: mouse::Cursor, ); - /// Applies a [`widget::Operation`] to the [`Overlay`]. + /// Applies an [`Operation`] to the [`Overlay`]. fn operate( &mut self, _layout: Layout<'_>, _renderer: &Renderer, - _operation: &mut dyn widget::Operation, + _operation: &mut dyn crate::widget::Operation, ) { } diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs index 6541d311a0..d31f2234a6 100644 --- a/core/src/overlay/group.rs +++ b/core/src/overlay/group.rs @@ -4,6 +4,7 @@ use crate::mouse; use crate::overlay; use crate::renderer; use crate::widget; +use crate::widget::Operation; use crate::{Clipboard, Event, Layout, Overlay, Point, Rectangle, Shell, Size}; /// An [`Overlay`] container that displays multiple overlay [`overlay::Element`] diff --git a/core/src/padding.rs b/core/src/padding.rs index e26cdd9bde..97bc56565c 100644 --- a/core/src/padding.rs +++ b/core/src/padding.rs @@ -175,6 +175,17 @@ impl From<[u16; 2]> for Padding { } } +impl From<[u16; 4]> for Padding { + fn from(p: [u16; 4]) -> Self { + Padding { + top: f32::from(p[0]), + right: f32::from(p[1]), + bottom: f32::from(p[2]), + left: f32::from(p[3]), + } + } +} + impl From for Padding { fn from(p: f32) -> Self { Padding { @@ -197,6 +208,18 @@ impl From<[f32; 2]> for Padding { } } +impl From<[f32; 4]> for Padding { + /// [top, rght, bottom, left] + fn from(p: [f32; 4]) -> Self { + Padding { + top: p[0], + right: p[1], + bottom: p[2], + left: p[3], + } + } +} + impl From for Size { fn from(padding: Padding) -> Self { Self::new(padding.horizontal(), padding.vertical()) diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index cff33991c0..a6058185bb 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -143,6 +143,16 @@ impl Rectangle { && point.y < self.y + self.height } + /// Returns true if the given [`Point`] is contained in the [`Rectangle`]. + /// The [`Point`] must be strictly contained, i.e. it must not be on the + /// border. + pub fn contains_strict(&self, point: Point) -> bool { + self.x < point.x + && point.x < self.x + self.width + && self.y < point.y + && 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 { @@ -152,6 +162,16 @@ impl Rectangle { ) } + /// Returns true if the current [`Rectangle`] is completely within the given + /// `container`. The [`Rectangle`] must be strictly contained, i.e. it must + /// not be on the border. + pub fn is_within_strict(&self, container: &Rectangle) -> bool { + container.contains_strict(self.position()) + && container.contains_strict( + self.position() + Vector::new(self.width, self.height), + ) + } + /// Computes the intersection with the given [`Rectangle`]. pub fn intersection( &self, diff --git a/core/src/renderer.rs b/core/src/renderer.rs index 6684517f0f..e617a6c25c 100644 --- a/core/src/renderer.rs +++ b/core/src/renderer.rs @@ -89,14 +89,20 @@ impl Default for Quad { /// The styling attributes of a [`Renderer`]. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { + /// The color to apply to symbolic icons. + pub icon_color: Color, /// The text color pub text_color: Color, + /// The scale factor + pub scale_factor: f64, } impl Default for Style { fn default() -> Self { Style { + icon_color: Color::BLACK, text_color: Color::BLACK, + scale_factor: 1.0, } } } diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index bbcdd8ffd3..36d73e2b54 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -30,6 +30,7 @@ impl text::Renderer for () { type Font = Font; type Paragraph = (); type Editor = (); + type Raw = (); const ICON_FONT: Font = Font::DEFAULT; const CHECKMARK_ICON: char = '0'; @@ -40,7 +41,7 @@ impl text::Renderer for () { } fn default_size(&self) -> Pixels { - Pixels(16.0) + Pixels(14.0) } fn fill_paragraph( @@ -61,6 +62,8 @@ impl text::Renderer for () { ) { } + fn fill_raw(&mut self, _raw: Self::Raw) {} + fn fill_text( &mut self, _paragraph: Text, @@ -184,7 +187,16 @@ impl image::Renderer for () { Size::default() } - fn draw_image(&mut self, _image: Image, _bounds: Rectangle) {} + fn draw_image( + &mut self, + _handle: Self::Handle, + _filter_method: image::FilterMethod, + _bounds: Rectangle, + _rotation: crate::Radians, + _opacity: f32, + _border_radius: [f32; 4], + ) { + } } impl svg::Renderer for () { diff --git a/core/src/svg.rs b/core/src/svg.rs index ac19b22337..fddaf90cdd 100644 --- a/core/src/svg.rs +++ b/core/src/svg.rs @@ -29,6 +29,9 @@ pub struct Svg { /// /// 0 means transparent. 1 means opaque. pub opacity: f32, + + /// The border radius for the svg + pub border_radius: [f32; 4], } impl Svg { @@ -39,6 +42,7 @@ impl Svg { color: None, rotation: Radians(0.0), opacity: 1.0, + border_radius: [0.0; 4], } } @@ -59,6 +63,12 @@ impl Svg { self.opacity = opacity.into(); self } + + /// Sets the border radius of the [`Svg`] + pub fn border_radius(mut self, border_radius: impl Into<[f32; 4]>) -> Self { + self.border_radius = border_radius.into(); + self + } } impl From<&Handle> for Svg { diff --git a/core/src/text.rs b/core/src/text.rs index a9e3dce5db..a9a80e9f88 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -57,8 +57,6 @@ pub enum Shaping { /// 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. /// @@ -67,6 +65,8 @@ pub enum Shaping { /// may be needed to display all of the glyphs. /// /// Advanced shaping is expensive! You should only enable it when necessary. + /// This is the default. + #[default] Advanced, } @@ -108,7 +108,7 @@ impl LineHeight { impl Default for LineHeight { fn default() -> Self { - Self::Relative(1.3) + Self::Relative(1.4) } } @@ -193,6 +193,9 @@ pub trait Renderer: crate::Renderer { /// The [`Editor`] of this [`Renderer`]. type Editor: Editor + 'static; + /// The Raw of this [`Renderer`]. + type Raw; + /// The icon font of the backend. const ICON_FONT: Self::Font; @@ -232,6 +235,9 @@ pub trait Renderer: crate::Renderer { clip_bounds: Rectangle, ); + /// Draws the given Raw + fn fill_raw(&mut self, raw: Self::Raw); + /// Draws the given [`Text`] at the given position and with the given /// [`Color`]. fn fill_text( diff --git a/core/src/theme/palette.rs b/core/src/theme/palette.rs index e0ff397ab0..1e5e748f5f 100644 --- a/core/src/theme/palette.rs +++ b/core/src/theme/palette.rs @@ -420,12 +420,15 @@ impl Extended { } } -/// A pair of background and text colors. +/// Recommended background, icon, and text [`Color`]. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Pair { /// The background color. pub color: Color, + /// The icon color, which defaults to the text color. + pub icon: Color, + /// The text color. /// /// It's guaranteed to be readable on top of the background [`color`]. @@ -437,9 +440,12 @@ pub struct Pair { impl Pair { /// Creates a new [`Pair`] from a background [`Color`] and some text [`Color`]. pub fn new(color: Color, text: Color) -> Self { + let text = readable(color, text); + Self { color, - text: readable(color, text), + icon: text, + text, } } } diff --git a/core/src/widget.rs b/core/src/widget.rs index 9cfff83d46..d758f1c362 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -3,9 +3,7 @@ pub mod operation; pub mod text; pub mod tree; -mod id; - -pub use id::Id; +pub use crate::id::Id; pub use operation::Operation; pub use text::Text; pub use tree::Tree; @@ -96,8 +94,8 @@ where Vec::new() } - /// Reconciles the [`Widget`] with the provided [`Tree`]. - fn diff(&self, _tree: &mut Tree) {} + /// Reconciliates the [`Widget`] with the provided [`Tree`]. + fn diff(&mut self, _tree: &mut Tree) {} /// Applies an [`Operation`] to the [`Widget`]. fn operate( @@ -150,4 +148,35 @@ where ) -> Option> { None } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget and its children + fn a11y_nodes( + &self, + _layout: Layout<'_>, + _state: &Tree, + _cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + iced_accessibility::A11yTree::default() + } + + /// Returns the id of the widget + fn id(&self) -> Option { + None + } + + /// Sets the id of the widget + /// This may be called while diffing the widget tree + fn set_id(&mut self, _id: Id) {} + + /// Adds the drag destination rectangles of the widget. + /// Runs after the layout phase for each widget in the tree. + fn drag_destinations( + &self, + _state: &Tree, + _layout: Layout<'_>, + _renderer: &Renderer, + _dnd_rectangles: &mut crate::clipboard::DndDestinationRectangles, + ) { + } } diff --git a/core/src/widget/id.rs b/core/src/widget/id.rs deleted file mode 100644 index ae739bb73d..0000000000 --- a/core/src/widget/id.rs +++ /dev/null @@ -1,43 +0,0 @@ -use std::borrow; -use std::sync::atomic::{self, AtomicUsize}; - -static NEXT_ID: AtomicUsize = AtomicUsize::new(0); - -/// The identifier of a generic widget. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Id(Internal); - -impl Id { - /// Creates a custom [`Id`]. - pub fn new(id: impl Into>) -> Self { - Self(Internal::Custom(id.into())) - } - - /// Creates a unique [`Id`]. - /// - /// This function produces a different [`Id`] every time it is called. - pub fn unique() -> Self { - let id = NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed); - - Self(Internal::Unique(id)) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -enum Internal { - Unique(usize), - Custom(borrow::Cow<'static, str>), -} - -#[cfg(test)] -mod tests { - use super::Id; - - #[test] - fn unique_generates_different_ids() { - let a = Id::unique(); - let b = Id::unique(); - - assert_ne!(a, b); - } -} diff --git a/core/src/widget/operation.rs b/core/src/widget/operation.rs index 097c360133..a912eb1bf7 100644 --- a/core/src/widget/operation.rs +++ b/core/src/widget/operation.rs @@ -1,6 +1,7 @@ //! Query or update internal widget state. pub mod focusable; pub mod scrollable; +pub mod search_id; pub mod text_input; pub use focusable::Focusable; @@ -46,7 +47,7 @@ pub trait Operation: Send { /// 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. + /// Operates on a custom widget. fn custom(&mut self, _state: &mut dyn Any, _id: Option<&Id>) {} /// Finishes the [`Operation`] and returns its [`Outcome`]. @@ -412,7 +413,7 @@ where /// Produces an [`Operation`] that applies the given [`Operation`] to the /// children of a container with the given [`Id`]. -pub fn scope( +pub fn scoped( target: Id, operation: impl Operation + 'static, ) -> impl Operation { diff --git a/core/src/widget/operation/focusable.rs b/core/src/widget/operation/focusable.rs index 867c682e52..6b5be19d53 100644 --- a/core/src/widget/operation/focusable.rs +++ b/core/src/widget/operation/focusable.rs @@ -1,4 +1,5 @@ //! Operate on widgets that can be focused. +use crate::id::IdEq; use crate::widget::operation::{self, Operation, Outcome}; use crate::widget::Id; use crate::Rectangle; @@ -34,7 +35,7 @@ pub fn focus(target: Id) -> impl Operation { impl Operation for Focus { fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { match id { - Some(id) if id == &self.target => { + Some(id) if IdEq::eq(&id.0, &self.target.0) => { state.focus(); } _ => { diff --git a/core/src/widget/operation/search_id.rs b/core/src/widget/operation/search_id.rs new file mode 100644 index 0000000000..b6e330f779 --- /dev/null +++ b/core/src/widget/operation/search_id.rs @@ -0,0 +1,42 @@ +//! Search for widgets with the target Id. + +use super::Operation; +use crate::{id::Id, widget::operation::Outcome, Rectangle}; + +/// Produces an [`Operation`] that searches for the Id +pub fn search_id(target: Id) -> impl Operation { + struct Find { + found: bool, + target: Id, + } + + impl Operation for Find { + fn custom(&mut self, _state: &mut dyn std::any::Any, id: Option<&Id>) { + if Some(&self.target) == id { + self.found = true; + } + } + + fn container( + &mut self, + _id: Option<&Id>, + _bounds: Rectangle, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self); + } + + fn finish(&self) -> Outcome { + if self.found { + Outcome::Some(self.target.clone()) + } else { + Outcome::None + } + } + } + + Find { + found: false, + target, + } +} diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index b34c5632d9..d95095ba5d 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -24,14 +24,16 @@ use crate::alignment; use crate::layout; use crate::mouse; use crate::renderer; -use crate::text; use crate::text::paragraph::{self, Paragraph}; +use crate::text::{self, Fragment}; use crate::widget::tree::{self, Tree}; use crate::{ Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Theme, Widget, }; +use std::borrow::Cow; + pub use text::{LineHeight, Shaping, Wrapping}; /// A bunch of text. @@ -62,7 +64,8 @@ where Theme: Catalog, Renderer: text::Renderer, { - fragment: text::Fragment<'a>, + fragment: Fragment<'a>, + id: crate::widget::Id, size: Option, line_height: LineHeight, width: Length, @@ -84,6 +87,7 @@ where pub fn new(fragment: impl text::IntoFragment<'a>) -> Self { Text { fragment: fragment.into_fragment(), + id: crate::widget::Id::unique(), size: None, line_height: LineHeight::default(), font: None, @@ -267,6 +271,50 @@ where draw(renderer, defaults, layout, state.0.raw(), style, viewport); } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + _state: &Tree, + _: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::{ + accesskit::{Live, NodeBuilder, Rect, Role}, + A11yTree, + }; + + let Rectangle { + x, + y, + width, + height, + } = layout.bounds(); + let bounds = Rect::new( + x as f64, + y as f64, + (x + width) as f64, + (y + height) as f64, + ); + + let mut node = NodeBuilder::new(Role::Paragraph); + + // TODO is the name likely different from the content? + node.set_name(self.fragment.to_string().into_boxed_str()); + node.set_bounds(bounds); + + // TODO make this configurable + node.set_live(Live::Polite); + A11yTree::leaf(node, self.id.clone()) + } + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn set_id(&mut self, id: crate::widget::Id) { + self.id = id + } } /// Produces the [`layout::Node`] of a [`Text`] widget. @@ -367,6 +415,29 @@ where } } +// impl<'a, Theme, Renderer> Clone for Text<'a, Theme, Renderer> +// where +// Renderer: text::Renderer, +// { +// fn clone(&self) -> Self { +// Self { +// id: self.id.clone(), +// 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, +// style: self.style, +// shaping: self.shaping, +// wrap: self.wrap, +// } +// } +// } +// TODO(POP): Clone no longer can be implemented because of style being a Box(style) + impl<'a, Theme, Renderer> From<&'a str> for Text<'a, Theme, Renderer> where Theme: Catalog + 'a, diff --git a/core/src/widget/tree.rs b/core/src/widget/tree.rs index 2600cfc6dd..af8ba86361 100644 --- a/core/src/widget/tree.rs +++ b/core/src/widget/tree.rs @@ -1,9 +1,16 @@ //! Store internal widget state in a state tree to ensure continuity. +use crate::id::{Id, Internal}; use crate::Widget; - use std::any::{self, Any}; -use std::borrow::Borrow; -use std::fmt; +use std::borrow::{Borrow, BorrowMut, Cow}; +use std::collections::HashMap; +use std::hash::Hash; +use std::{fmt, mem}; + +thread_local! { + /// A map of named widget states. +pub static NAMED: std::cell::RefCell, (State, Vec<(usize, Tree)>)>> = std::cell::RefCell::new(HashMap::new()); +} /// A persistent state widget tree. /// @@ -13,6 +20,9 @@ pub struct Tree { /// The tag of the [`Tree`]. pub tag: Tag, + /// the Id of the [`Tree`] + pub id: Option, + /// The [`State`] of the [`Tree`]. pub state: State, @@ -24,6 +34,7 @@ impl Tree { /// Creates an empty, stateless [`Tree`] with no children. pub fn empty() -> Self { Self { + id: None, tag: Tag::stateless(), state: State::None, children: Vec::new(), @@ -40,13 +51,104 @@ impl Tree { let widget = widget.borrow(); Self { + id: widget.id(), tag: widget.tag(), state: widget.state(), children: widget.children(), } } - /// Reconciles the current tree with the provided [`Widget`]. + /// Takes all named widgets from the tree. + pub fn take_all_named( + &mut self, + ) -> HashMap, (State, Vec<(usize, Tree)>)> { + let mut named = HashMap::new(); + struct Visit { + parent: Cow<'static, str>, + index: usize, + visited: bool, + } + // tree traversal to find all named widgets + // and keep their state and children + let mut stack = vec![(self, None)]; + while let Some((tree, visit)) = stack.pop() { + if let Some(Id(Internal::Custom(_, n))) = tree.id.clone() { + let state = mem::replace(&mut tree.state, State::None); + let children_count = tree.children.len(); + let children = + tree.children.iter_mut().enumerate().rev().map(|(i, c)| { + if matches!(c.id, Some(Id(Internal::Custom(_, _)))) { + (c, None) + } else { + ( + c, + Some(Visit { + index: i, + parent: n.clone(), + visited: false, + }), + ) + } + }); + _ = named.insert( + n.clone(), + (state, Vec::with_capacity(children_count)), + ); + stack.extend(children); + } else if let Some(visit) = visit { + if visit.visited { + named.get_mut(&visit.parent).unwrap().1.push(( + visit.index, + mem::replace( + tree, + Tree { + id: tree.id.clone(), + tag: tree.tag, + ..Tree::empty() + }, + ), + )); + } else { + let ptr = tree as *mut Tree; + + stack.push(( + // TODO remove this unsafe block + #[allow(unsafe_code)] + // SAFETY: when the reference is finally accessed, all the children references will have been processed first. + unsafe { + ptr.as_mut().unwrap() + }, + Some(Visit { + visited: true, + ..visit + }), + )); + stack.extend(tree.children.iter_mut().map(|c| (c, None))); + } + } else { + stack.extend(tree.children.iter_mut().map(|s| (s, None))); + } + } + + named + } + + /// Finds a widget state in the tree by its id. + pub fn find<'a>(&'a self, id: &Id) -> Option<&'a Tree> { + if self.id == Some(id.clone()) { + return Some(self); + } + + for child in self.children.iter() { + if let Some(tree) = child.find(id) { + return Some(tree); + } + } + + None + } + + /// Reconciliates the current tree with the provided [`Widget`]. /// /// If the tag of the [`Widget`] matches the tag of the [`Tree`], then the /// [`Widget`] proceeds with the reconciliation (i.e. [`Widget::diff`] is called). @@ -56,53 +158,203 @@ impl Tree { /// [`Widget::diff`]: crate::Widget::diff pub fn diff<'a, Message, Theme, Renderer>( &mut self, - new: impl Borrow + 'a>, + mut new: impl BorrowMut + 'a>, ) where Renderer: crate::Renderer, { - if self.tag == new.borrow().tag() { - new.borrow().diff(self); + let borrowed: &mut dyn Widget = + new.borrow_mut(); + let mut needs_reset = false; + let tag_match = self.tag == borrowed.tag(); + if let Some(Id(Internal::Custom(_, n))) = borrowed.id() { + if let Some((mut state, children)) = NAMED + .with(|named| named.borrow_mut().remove(&n)) + .or_else(|| { + //check self.id + if let Some(Id(Internal::Custom(_, ref name))) = self.id { + if name == &n { + Some(( + mem::replace(&mut self.state, State::None), + self.children + .iter_mut() + .map(|s| { + // take the data + mem::replace( + s, + Tree { + id: s.id.clone(), + tag: s.tag, + ..Tree::empty() + }, + ) + }) + .enumerate() + .collect(), + )) + } else { + None + } + } else { + None + } + }) + { + std::mem::swap(&mut self.state, &mut state); + let widget_children = borrowed.children(); + if !tag_match || self.children.len() != widget_children.len() { + self.children = borrowed.children(); + } else { + for (old_i, mut old) in children { + let Some(my_state) = self.children.get_mut(old_i) + else { + continue; + }; + if my_state.tag != old.tag || { + !match (&old.id, &my_state.id) { + ( + Some(Id(Internal::Custom(_, ref old_name))), + Some(Id(Internal::Custom(_, ref my_name))), + ) => old_name == my_name, + ( + Some(Id(Internal::Set(a))), + Some(Id(Internal::Set(b))), + ) => a.len() == b.len(), + ( + Some(Id(Internal::Unique(_))), + Some(Id(Internal::Unique(_))), + ) => true, + (None, None) => true, + _ => false, + } + } { + continue; + } + + mem::swap(my_state, &mut old); + } + } + } else { + needs_reset = true; + } + } else if tag_match { + if let Some(id) = self.id.clone() { + borrowed.set_id(id); + } + if self.children.len() != borrowed.children().len() { + self.children = borrowed.children(); + } + } else { + needs_reset = true; + } + if needs_reset { + *self = Self::new(borrowed); + let borrowed = new.borrow_mut(); + borrowed.diff(self); } else { - *self = Self::new(new); + borrowed.diff(self); } } /// Reconciles the children of the tree with the provided list of widgets. pub fn diff_children<'a, Message, Theme, Renderer>( &mut self, - new_children: &[impl Borrow + 'a>], + new_children: &mut [impl BorrowMut< + dyn Widget + 'a, + >], ) where Renderer: crate::Renderer, { self.diff_children_custom( new_children, - |tree, widget| tree.diff(widget.borrow()), - |widget| Self::new(widget.borrow()), - ); + new_children.iter().map(|c| c.borrow().id()).collect(), + |tree, widget| { + let borrowed: &mut dyn Widget<_, _, _> = widget.borrow_mut(); + tree.diff(borrowed) + }, + |widget| { + let borrowed: &dyn Widget<_, _, _> = widget.borrow(); + Self::new(borrowed) + }, + ) } /// Reconciles the children of the tree with the provided list of widgets using custom /// logic both for diffing and creating new widget state. pub fn diff_children_custom( &mut self, - new_children: &[T], - diff: impl Fn(&mut Tree, &T), + new_children: &mut [T], + new_ids: Vec>, + diff: impl Fn(&mut Tree, &mut T), new_state: impl Fn(&T) -> Self, ) { if self.children.len() > new_children.len() { self.children.truncate(new_children.len()); } - for (child_state, new) in - self.children.iter_mut().zip(new_children.iter()) + let len_changed = self.children.len() != new_children.len(); + + let children_len = self.children.len(); + let (mut id_map, mut id_list): ( + HashMap, + Vec<&mut Tree>, + ) = self.children.iter_mut().fold( + (HashMap::new(), Vec::with_capacity(children_len)), + |(mut id_map, mut id_list), c| { + if let Some(id) = c.id.as_ref() { + if let Internal::Custom(_, ref name) = id.0 { + let _ = id_map.insert(name.to_string(), c); + } else { + id_list.push(c); + } + } else { + id_list.push(c); + } + (id_map, id_list) + }, + ); + + let mut child_state_i = 0; + let mut new_trees: Vec<(Tree, usize)> = + Vec::with_capacity(new_children.len()); + for (i, (new, new_id)) in + new_children.iter_mut().zip(new_ids.iter()).enumerate() { + let child_state = if let Some(c) = new_id.as_ref().and_then(|id| { + if let Internal::Custom(_, ref name) = id.0 { + id_map.remove(name.as_ref()) + } else { + None + } + }) { + c + } else if child_state_i < id_list.len() + && !matches!( + id_list[child_state_i].id, + Some(Id(Internal::Custom(_, _))) + ) + { + let c = &mut id_list[child_state_i]; + if len_changed { + c.id.clone_from(new_id); + } + child_state_i += 1; + c + } else { + let mut my_new_state = new_state(new); + diff(&mut my_new_state, new); + new_trees.push((my_new_state, i)); + continue; + }; + diff(child_state, new); } - if self.children.len() < new_children.len() { - self.children.extend( - new_children[self.children.len()..].iter().map(new_state), - ); + for (new_tree, i) in new_trees { + if self.children.len() > i { + self.children[i] = new_tree; + } else { + self.children.push(new_tree); + } } } } @@ -114,8 +366,8 @@ impl Tree { /// `maybe_changed` closure. pub fn diff_children_custom_with_search( current_children: &mut Vec, - new_children: &[T], - diff: impl Fn(&mut Tree, &T), + new_children: &mut [T], + diff: impl Fn(&mut Tree, &mut T), maybe_changed: impl Fn(usize) -> bool, new_state: impl Fn(&T) -> Tree, ) { @@ -183,7 +435,7 @@ pub fn diff_children_custom_with_search( // TODO: Merge loop with extend logic (?) for (child_state, new) in - current_children.iter_mut().zip(new_children.iter()) + current_children.iter_mut().zip(new_children.iter_mut()) { diff(child_state, new); } diff --git a/core/src/window/id.rs b/core/src/window/id.rs index 5d5a817e48..c006a08259 100644 --- a/core/src/window/id.rs +++ b/core/src/window/id.rs @@ -9,9 +9,17 @@ pub struct Id(u64); static COUNT: AtomicU64 = AtomicU64::new(1); impl Id { + /// No window will match this Id + pub const NONE: Id = Id(0); + /// Creates a new unique window [`Id`]. pub fn unique() -> Id { - Id(COUNT.fetch_add(1, atomic::Ordering::Relaxed)) + let id = Id(COUNT.fetch_add(1, atomic::Ordering::Relaxed)); + if id.0 == 0 { + Id(COUNT.fetch_add(1, atomic::Ordering::Relaxed)) + } else { + id + } } } diff --git a/core/src/window/settings.rs b/core/src/window/settings.rs index fbbf86abd8..c5a16d743a 100644 --- a/core/src/window/settings.rs +++ b/core/src/window/settings.rs @@ -34,6 +34,9 @@ pub struct Settings { /// The initial logical dimensions of the window. pub size: Size, + /// The border area for the drag resize handle. + pub resize_border: u32, + /// The initial position of the window. pub position: Position, @@ -76,9 +79,10 @@ pub struct Settings { } impl Default for Settings { - fn default() -> Self { - Self { + fn default() -> Settings { + Settings { size: Size::new(1024.0, 768.0), + resize_border: 8, position: Position::default(), min_size: None, max_size: None, diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml index dc885728ca..683bb55c5b 100644 --- a/examples/editor/Cargo.toml +++ b/examples/editor/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["highlighter", "tokio", "debug"] +iced.features = ["highlighter", "tokio", "debug", "winit", "tiny-skia"] tokio.workspace = true tokio.features = ["fs"] diff --git a/examples/game_of_life/Cargo.toml b/examples/game_of_life/Cargo.toml index 7596844cd6..c328029458 100644 --- a/examples/game_of_life/Cargo.toml +++ b/examples/game_of_life/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["debug", "canvas", "tokio"] +iced.features = ["debug", "canvas", "tokio", "winit", "tiny-skia"] itertools = "0.12" rustc-hash.workspace = true diff --git a/examples/gradient/Cargo.toml b/examples/gradient/Cargo.toml index 8102b8665f..9f9347cc23 100644 --- a/examples/gradient/Cargo.toml +++ b/examples/gradient/Cargo.toml @@ -6,6 +6,6 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["debug"] +iced.features = ["debug", "winit", "wgpu"] tracing-subscriber = "0.3" diff --git a/examples/integration/Cargo.toml b/examples/integration/Cargo.toml index 7f8feb3f52..9d3cd0864b 100644 --- a/examples/integration/Cargo.toml +++ b/examples/integration/Cargo.toml @@ -22,4 +22,4 @@ iced_wgpu.features = ["webgl"] console_error_panic_hook = "0.1" console_log = "1.0" wasm-bindgen = "0.2" -web-sys = { version = "0.3", features = ["Element", "HtmlCanvasElement", "Window", "Document"] } +web-sys = { version = "=0.3", features = ["Element", "HtmlCanvasElement", "Window", "Document"] } diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs index 0b11a323d4..72293ff95c 100644 --- a/examples/integration/src/controls.rs +++ b/examples/integration/src/controls.rs @@ -1,3 +1,4 @@ +use iced_wgpu::core::window::Id; use iced_wgpu::Renderer; use iced_widget::{column, container, row, slider, text, text_input}; use iced_winit::core::{Color, Element, Length::*, Theme}; diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index 87a5b22b46..4a1e80e5f5 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -4,6 +4,7 @@ mod scene; use controls::Controls; use scene::Scene; +use iced_wgpu::core::window::Id; use iced_wgpu::graphics::Viewport; use iced_wgpu::{wgpu, Engine, Renderer}; use iced_winit::conversion; @@ -34,7 +35,7 @@ pub fn main() -> Result<(), winit::error::EventLoopError> { enum Runner { Loading, Ready { - window: Arc, + window: Arc, device: wgpu::Device, queue: wgpu::Queue, surface: wgpu::Surface<'static>, @@ -53,7 +54,7 @@ pub fn main() -> Result<(), winit::error::EventLoopError> { } impl winit::application::ApplicationHandler for Runner { - fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { + fn resumed(&mut self, event_loop: &dyn winit::event_loop::ActiveEventLoop) { if let Self::Loading = self { let window = Arc::new( event_loop @@ -187,7 +188,7 @@ pub fn main() -> Result<(), winit::error::EventLoopError> { fn window_event( &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, + event_loop: &dyn winit::event_loop::ActiveEventLoop, _window_id: winit::window::WindowId, event: WindowEvent, ) { diff --git a/examples/lazy/Cargo.toml b/examples/lazy/Cargo.toml index 4ccb958422..779e659ed9 100644 --- a/examples/lazy/Cargo.toml +++ b/examples/lazy/Cargo.toml @@ -7,4 +7,4 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["debug", "lazy"] +iced.features = ["debug", "lazy", "async-std", "winit", "tiny-skia"] diff --git a/examples/loading_spinners/Cargo.toml b/examples/loading_spinners/Cargo.toml index a32da3864c..0eaacfdb7b 100644 --- a/examples/loading_spinners/Cargo.toml +++ b/examples/loading_spinners/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["advanced", "canvas"] +iced.features = ["advanced", "canvas", "winit"] lyon_algorithms = "1.0" once_cell.workspace = true \ No newline at end of file diff --git a/examples/multi_window/Cargo.toml b/examples/multi_window/Cargo.toml index 2e222dfbb1..abe762c9b5 100644 --- a/examples/multi_window/Cargo.toml +++ b/examples/multi_window/Cargo.toml @@ -6,4 +6,11 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../..", features = ["debug", "multi-window"] } +iced = { path = "../..", default-features = false, features = [ + "a11y", + "tokio", + "debug", + "winit", + "multi-window", + "tiny-skia", +] } diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index b43a627a0d..33fadea9db 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -3,7 +3,11 @@ use iced::widget::{ text_input, }; use iced::window; -use iced::{Center, Element, Fill, Subscription, Task, Theme, Vector}; +use iced::Length::Fill; +use iced::{ + id::Id, Alignment, Center, Element, Length, Point, Settings, Subscription, + Task, Theme, Vector, +}; use std::collections::BTreeMap; @@ -25,6 +29,7 @@ struct Window { scale_input: String, current_scale: f64, theme: Theme, + input_id: text_input::Id, } #[derive(Debug, Clone)] diff --git a/examples/pane_grid/Cargo.toml b/examples/pane_grid/Cargo.toml index 095ecd1088..fff22c8df1 100644 --- a/examples/pane_grid/Cargo.toml +++ b/examples/pane_grid/Cargo.toml @@ -7,4 +7,4 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["debug", "lazy"] +iced.features = ["debug", "lazy", "winit"] diff --git a/examples/pokedex/Cargo.toml b/examples/pokedex/Cargo.toml index 1a6d54453e..21d2f8f85c 100644 --- a/examples/pokedex/Cargo.toml +++ b/examples/pokedex/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["image", "debug", "tokio"] +iced.features = ["image", "debug", "tokio", "winit", "tiny-skia"] serde_json = "1.0" diff --git a/examples/screenshot/Cargo.toml b/examples/screenshot/Cargo.toml index 77b108bd51..479772afc0 100644 --- a/examples/screenshot/Cargo.toml +++ b/examples/screenshot/Cargo.toml @@ -14,4 +14,4 @@ image.features = ["png"] tokio.workspace = true -tracing-subscriber = "0.3" \ No newline at end of file +tracing-subscriber = "0.3" diff --git a/examples/scrollable/Cargo.toml b/examples/scrollable/Cargo.toml index f8c735c014..ab3892eb68 100644 --- a/examples/scrollable/Cargo.toml +++ b/examples/scrollable/Cargo.toml @@ -7,6 +7,6 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["debug"] - +iced.features = ["debug", "winit", "tiny-skia"] +iced_core.workspace = true once_cell.workspace = true diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index de4f2f9ada..785ef624f2 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -1,12 +1,14 @@ +use iced::widget::scrollable::{self, Properties, Scrollbar, Scroller}; use iced::widget::{ button, column, container, horizontal_space, progress_bar, radio, row, scrollable, slider, text, vertical_space, }; use iced::{Border, Center, Color, Element, Fill, Task, Theme}; +use iced_core::id::Id; use once_cell::sync::Lazy; -static SCROLLABLE_ID: Lazy = Lazy::new(scrollable::Id::unique); +static SCROLLABLE_ID: Lazy = Lazy::new(|| Id::new("scrollable")); pub fn main() -> iced::Result { iced::application( diff --git a/examples/sctk_drag/Cargo.toml b/examples/sctk_drag/Cargo.toml new file mode 100644 index 0000000000..e118b277f2 --- /dev/null +++ b/examples/sctk_drag/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "sctk_drag" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +iced_core = { path = "../../core" } +iced = { path = "../..", default-features = false, features = [ + "tiny-skia", + "tokio", + "winit", + "wayland", + "debug", +] } +env_logger = "0.10" +# sctk = { package = "smithay-client-toolkit", path = "../../../fork/client-toolkit/" } +sctk.workspace = true diff --git a/examples/sctk_drag/src/dnd_destination.rs b/examples/sctk_drag/src/dnd_destination.rs new file mode 100644 index 0000000000..8c4a624560 --- /dev/null +++ b/examples/sctk_drag/src/dnd_destination.rs @@ -0,0 +1,728 @@ +use std::{ + borrow::Cow, + sync::atomic::{AtomicU64, Ordering}, +}; + +use iced::{ + clipboard::{ + dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent}, + mime::AllowedMimeTypes, + }, + event, + id::Internal, + mouse, overlay, Event, Length, Rectangle, +}; +use iced::{id::Id, Element}; +use iced_core::{ + self, layout, + widget::{tree, Tree}, + Clipboard, Layout, Shell, Widget, +}; + +pub fn dnd_destination<'a, Message: 'static>( + child: impl Into>, + mimes: Vec>, +) -> DndDestination<'a, Message> { + DndDestination::new(child, mimes) +} + +pub fn dnd_destination_for_data( + child: impl Into>, + on_finish: impl Fn(Option, DndAction) -> Message + 'static, +) -> DndDestination<'static, Message> { + DndDestination::for_data(child, on_finish) +} + +static DRAG_ID_COUNTER: AtomicU64 = AtomicU64::new(0); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct DragId(pub u128); + +impl DragId { + pub fn new() -> Self { + DragId( + u128::from(u64::MAX) + + u128::from(DRAG_ID_COUNTER.fetch_add(1, Ordering::Relaxed)), + ) + } +} + +#[allow(clippy::new_without_default)] +impl Default for DragId { + fn default() -> Self { + DragId::new() + } +} + +pub struct DndDestination<'a, Message> { + id: Id, + drag_id: Option, + preferred_action: DndAction, + action: DndAction, + container: Element<'a, Message>, + mime_types: Vec>, + forward_drag_as_cursor: bool, + on_hold: Option Message>>, + on_drop: Option Message>>, + on_enter: Option) -> Message>>, + on_leave: Option Message>>, + on_motion: Option Message>>, + on_action_selected: Option Message>>, + on_data_received: Option) -> Message>>, + on_finish: + Option, DndAction, f64, f64) -> Message>>, +} + +impl<'a, Message: 'static> DndDestination<'a, Message> { + pub fn new( + child: impl Into>, + mimes: Vec>, + ) -> Self { + Self { + id: Id::unique(), + drag_id: None, + mime_types: mimes, + preferred_action: DndAction::Move, + action: DndAction::Copy | DndAction::Move, + container: child.into(), + forward_drag_as_cursor: false, + on_hold: None, + on_drop: None, + on_enter: None, + on_leave: None, + on_motion: None, + on_action_selected: None, + on_data_received: None, + on_finish: None, + } + } + + pub fn for_data( + child: impl Into>, + on_finish: impl Fn(Option, DndAction) -> Message + 'static, + ) -> Self { + Self { + id: Id::unique(), + drag_id: None, + mime_types: T::allowed().iter().cloned().map(Cow::Owned).collect(), + preferred_action: DndAction::Move, + action: DndAction::Copy | DndAction::Move, + container: child.into(), + forward_drag_as_cursor: false, + on_hold: None, + on_drop: None, + on_enter: None, + on_leave: None, + on_motion: None, + on_action_selected: None, + on_data_received: None, + on_finish: Some(Box::new(move |mime, data, action, _, _| { + on_finish(T::try_from((data, mime)).ok(), action) + })), + } + } + + #[must_use] + pub fn data_received_for( + mut self, + f: impl Fn(Option) -> Message + 'static, + ) -> Self { + self.on_data_received = + Some(Box::new( + move |mime, data| f(T::try_from((data, mime)).ok()), + )); + self + } + + pub fn with_id( + child: impl Into>, + id: Id, + mimes: Vec>, + ) -> Self { + Self { + id, + drag_id: None, + mime_types: mimes, + preferred_action: DndAction::Move, + action: DndAction::Copy | DndAction::Move, + container: child.into(), + forward_drag_as_cursor: false, + on_hold: None, + on_drop: None, + on_enter: None, + on_leave: None, + on_motion: None, + on_action_selected: None, + on_data_received: None, + on_finish: None, + } + } + + #[must_use] + pub fn drag_id(mut self, id: u64) -> Self { + self.drag_id = Some(id); + self + } + + #[must_use] + pub fn action(mut self, action: DndAction) -> Self { + self.action = action; + self + } + + #[must_use] + pub fn preferred_action(mut self, action: DndAction) -> Self { + self.preferred_action = action; + self + } + + #[must_use] + pub fn forward_drag_as_cursor(mut self, forward: bool) -> Self { + self.forward_drag_as_cursor = forward; + self + } + + #[must_use] + pub fn on_hold( + mut self, + f: impl Fn(f64, f64) -> Message + 'static, + ) -> Self { + self.on_hold = Some(Box::new(f)); + self + } + + #[must_use] + pub fn on_drop( + mut self, + f: impl Fn(f64, f64) -> Message + 'static, + ) -> Self { + self.on_drop = Some(Box::new(f)); + self + } + + #[must_use] + pub fn on_enter( + mut self, + f: impl Fn(f64, f64, Vec) -> Message + 'static, + ) -> Self { + self.on_enter = Some(Box::new(f)); + self + } + + #[must_use] + pub fn on_leave(mut self, m: impl Fn() -> Message + 'static) -> Self { + self.on_leave = Some(Box::new(m)); + self + } + + #[must_use] + pub fn on_finish( + mut self, + f: impl Fn(String, Vec, DndAction, f64, f64) -> Message + 'static, + ) -> Self { + self.on_finish = Some(Box::new(f)); + self + } + + #[must_use] + pub fn on_motion( + mut self, + f: impl Fn(f64, f64) -> Message + 'static, + ) -> Self { + self.on_motion = Some(Box::new(f)); + self + } + + #[must_use] + pub fn on_action_selected( + mut self, + f: impl Fn(DndAction) -> Message + 'static, + ) -> Self { + self.on_action_selected = Some(Box::new(f)); + self + } + + #[must_use] + pub fn on_data_received( + mut self, + f: impl Fn(String, Vec) -> Message + 'static, + ) -> Self { + self.on_data_received = Some(Box::new(f)); + self + } + + /// Returns the drag id of the destination. + /// + /// # Panics + /// Panics if the destination has been assigned a Set id, which is invalid. + #[must_use] + pub fn get_drag_id(&self) -> u128 { + u128::from(self.drag_id.unwrap_or_else(|| match &self.id.0 { + Internal::Unique(id) | Internal::Custom(id, _) => *id, + Internal::Set(_) => { + panic!("Invalid Id assigned to dnd destination.") + } + })) + } +} + +impl<'a, Message: 'static> Widget + for DndDestination<'a, Message> +{ + fn children(&self) -> Vec { + vec![Tree::new(&self.container)] + } + + fn tag(&self) -> iced_core::widget::tree::Tag { + tree::Tag::of::>() + } + + fn diff(&mut self, tree: &mut Tree) { + tree.children[0].diff(self.container.as_widget_mut()); + } + + fn state(&self) -> iced_core::widget::tree::State { + tree::State::new(State::<()>::new()) + } + + fn size(&self) -> iced_core::Size { + self.container.as_widget().size() + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &iced::Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.container.as_widget().layout( + &mut tree.children[0], + renderer, + limits, + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: layout::Layout<'_>, + renderer: &iced::Renderer, + operation: &mut dyn iced_core::widget::Operation<()>, + ) { + self.container.as_widget().operate( + &mut tree.children[0], + layout, + renderer, + operation, + ); + } + + #[allow(clippy::too_many_lines)] + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: layout::Layout<'_>, + cursor: mouse::Cursor, + renderer: &iced::Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + let s = self.container.as_widget_mut().on_event( + &mut tree.children[0], + event.clone(), + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ); + if matches!(s, event::Status::Captured) { + return event::Status::Captured; + } + + let state = tree.state.downcast_mut::>(); + + let my_id = self.get_drag_id(); + + match event { + Event::Dnd(DndEvent::Offer( + id, + OfferEvent::Enter { + x, y, mime_types, .. + }, + )) if id == Some(my_id) => { + if let Some(msg) = state.on_enter( + x, + y, + mime_types, + self.on_enter.as_ref().map(std::convert::AsRef::as_ref), + (), + ) { + shell.publish(msg); + } + if self.forward_drag_as_cursor { + #[allow(clippy::cast_possible_truncation)] + let drag_cursor = + mouse::Cursor::Available((x as f32, y as f32).into()); + let event = Event::Mouse(mouse::Event::CursorMoved { + position: drag_cursor.position().unwrap(), + }); + self.container.as_widget_mut().on_event( + &mut tree.children[0], + event, + layout, + drag_cursor, + renderer, + clipboard, + shell, + viewport, + ); + } + return event::Status::Captured; + } + Event::Dnd(DndEvent::Offer(id, OfferEvent::Leave)) + if id == Some(my_id) => + { + state.on_leave( + self.on_leave.as_ref().map(std::convert::AsRef::as_ref), + ); + + if self.forward_drag_as_cursor { + let drag_cursor = mouse::Cursor::Unavailable; + let event = Event::Mouse(mouse::Event::CursorLeft); + self.container.as_widget_mut().on_event( + &mut tree.children[0], + event, + layout, + drag_cursor, + renderer, + clipboard, + shell, + viewport, + ); + } + return event::Status::Captured; + } + Event::Dnd(DndEvent::Offer(id, OfferEvent::Motion { x, y })) + if id == Some(my_id) => + { + if let Some(msg) = state.on_motion( + x, + y, + self.on_motion.as_ref().map(std::convert::AsRef::as_ref), + self.on_enter.as_ref().map(std::convert::AsRef::as_ref), + (), + ) { + shell.publish(msg); + } + + if self.forward_drag_as_cursor { + #[allow(clippy::cast_possible_truncation)] + let drag_cursor = + mouse::Cursor::Available((x as f32, y as f32).into()); + let event = Event::Mouse(mouse::Event::CursorMoved { + position: drag_cursor.position().unwrap(), + }); + self.container.as_widget_mut().on_event( + &mut tree.children[0], + event, + layout, + drag_cursor, + renderer, + clipboard, + shell, + viewport, + ); + } + return event::Status::Captured; + } + Event::Dnd(DndEvent::Offer(id, OfferEvent::LeaveDestination)) + if id == Some(my_id) => + { + if let Some(msg) = state.on_leave( + self.on_leave.as_ref().map(std::convert::AsRef::as_ref), + ) { + shell.publish(msg); + } + return event::Status::Captured; + } + Event::Dnd(DndEvent::Offer(id, OfferEvent::Drop)) + if id == Some(my_id) => + { + if let Some(msg) = state.on_drop( + self.on_drop.as_ref().map(std::convert::AsRef::as_ref), + ) { + shell.publish(msg); + } + return event::Status::Captured; + } + Event::Dnd(DndEvent::Offer( + id, + OfferEvent::SelectedAction(action), + )) if id == Some(my_id) => { + if let Some(msg) = state.on_action_selected( + action, + self.on_action_selected + .as_ref() + .map(std::convert::AsRef::as_ref), + ) { + shell.publish(msg); + } + return event::Status::Captured; + } + Event::Dnd(DndEvent::Offer( + id, + OfferEvent::Data { data, mime_type }, + )) if id == Some(my_id) => { + dbg!("got data"); + if let (Some(msg), ret) = state.on_data_received( + mime_type, + data, + self.on_data_received + .as_ref() + .map(std::convert::AsRef::as_ref), + self.on_finish.as_ref().map(std::convert::AsRef::as_ref), + ) { + shell.publish(msg); + return ret; + } + return event::Status::Captured; + } + _ => {} + } + event::Status::Ignored + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: layout::Layout<'_>, + cursor_position: mouse::Cursor, + viewport: &Rectangle, + renderer: &iced::Renderer, + ) -> mouse::Interaction { + self.container.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor_position, + viewport, + renderer, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut iced::Renderer, + theme: &iced::Theme, + renderer_style: &iced_core::renderer::Style, + layout: layout::Layout<'_>, + cursor_position: mouse::Cursor, + viewport: &Rectangle, + ) { + self.container.as_widget().draw( + &tree.children[0], + renderer, + theme, + renderer_style, + layout, + cursor_position, + viewport, + ); + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &iced::Renderer, + translation: iced::Vector, + ) -> Option> + { + None + } + + fn drag_destinations( + &self, + state: &Tree, + layout: layout::Layout<'_>, + renderer: &iced::Renderer, + dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, + ) { + let bounds = layout.bounds(); + let my_id = self.get_drag_id(); + let my_dest = DndDestinationRectangle { + id: my_id, + rectangle: dnd::Rectangle { + x: f64::from(bounds.x), + y: f64::from(bounds.y), + width: f64::from(bounds.width), + height: f64::from(bounds.height), + }, + mime_types: self.mime_types.clone(), + actions: self.action, + preferred: self.preferred_action, + }; + dnd_rectangles.push(my_dest); + + self.container.as_widget().drag_destinations( + &state.children[0], + layout, + renderer, + dnd_rectangles, + ); + } + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn set_id(&mut self, id: Id) { + self.id = id; + } +} + +#[derive(Default)] +pub struct State { + pub drag_offer: Option>, +} + +pub struct DragOffer { + pub x: f64, + pub y: f64, + pub dropped: bool, + pub selected_action: DndAction, + pub data: T, +} + +impl State { + #[must_use] + pub fn new() -> Self { + Self { drag_offer: None } + } + + pub fn on_enter( + &mut self, + x: f64, + y: f64, + mime_types: Vec, + on_enter: Option) -> Message>, + data: T, + ) -> Option { + self.drag_offer = Some(DragOffer { + x, + y, + dropped: false, + selected_action: DndAction::empty(), + data, + }); + on_enter.map(|f| f(x, y, mime_types)) + } + + pub fn on_leave( + &mut self, + on_leave: Option<&dyn Fn() -> Message>, + ) -> Option { + if self.drag_offer.as_ref().is_some_and(|d| !d.dropped) { + self.drag_offer = None; + on_leave.map(|f| f()) + } else { + None + } + } + + pub fn on_motion( + &mut self, + x: f64, + y: f64, + on_motion: Option Message>, + on_enter: Option) -> Message>, + data: T, + ) -> Option { + if let Some(s) = self.drag_offer.as_mut() { + s.x = x; + s.y = y; + } else { + self.drag_offer = Some(DragOffer { + x, + y, + dropped: false, + selected_action: DndAction::empty(), + data, + }); + if let Some(f) = on_enter { + return Some(f(x, y, vec![])); + } + } + on_motion.map(|f| f(x, y)) + } + + pub fn on_drop( + &mut self, + on_drop: Option Message>, + ) -> Option { + if let Some(offer) = self.drag_offer.as_mut() { + offer.dropped = true; + if let Some(f) = on_drop { + return Some(f(offer.x, offer.y)); + } + } + None + } + + pub fn on_action_selected( + &mut self, + action: DndAction, + on_action_selected: Option Message>, + ) -> Option { + if let Some(s) = self.drag_offer.as_mut() { + s.selected_action = action; + } + if let Some(f) = on_action_selected { + f(action).into() + } else { + None + } + } + + pub fn on_data_received( + &mut self, + mime: String, + data: Vec, + on_data_received: Option) -> Message>, + on_finish: Option< + impl Fn(String, Vec, DndAction, f64, f64) -> Message, + >, + ) -> (Option, event::Status) { + dbg!("data received"); + let Some(dnd) = self.drag_offer.as_ref() else { + self.drag_offer = None; + return (None, event::Status::Ignored); + }; + + if dnd.dropped { + let ret = ( + on_finish + .map(|f| f(mime, data, dnd.selected_action, dnd.x, dnd.y)), + event::Status::Captured, + ); + self.drag_offer = None; + ret + } else if let Some(f) = on_data_received { + (Some(f(mime, data)), event::Status::Captured) + } else { + (None, event::Status::Ignored) + } + } +} + +impl<'a, Message: 'static> From> + for Element<'a, Message> +{ + fn from(wrapper: DndDestination<'a, Message>) -> Self { + Element::new(wrapper) + } +} diff --git a/examples/sctk_drag/src/dnd_source.rs b/examples/sctk_drag/src/dnd_source.rs new file mode 100644 index 0000000000..ffa0dad638 --- /dev/null +++ b/examples/sctk_drag/src/dnd_source.rs @@ -0,0 +1,382 @@ +use std::any::Any; + +use iced::id::Id; +use iced::widget::container; +use iced::Element; +use iced::{ + clipboard::dnd::{DndAction, DndEvent, SourceEvent}, + event, mouse, overlay, Event, Length, Point, Rectangle, +}; +use iced_core::{ + layout, renderer, + widget::{tree, Tree}, + Clipboard, Shell, +}; +use iced_core::{Layout, Widget}; + +pub fn dnd_source< + 'a, + Message: 'static, + AppMessage: 'static, + D: iced::clipboard::mime::AsMimeTypes + Send + 'static, +>( + child: impl Into>, +) -> DndSource<'a, Message, AppMessage, D> { + DndSource::new(child) +} + +pub struct DndSource<'a, Message, AppMessage, D> { + id: Id, + action: DndAction, + container: Element<'a, Message>, + drag_content: Option D>>, + drag_icon: + Option (Element<'static, AppMessage>, tree::State)>>, + drag_threshold: f32, + _phantom: std::marker::PhantomData, +} + +impl< + 'a, + Message: 'static, + AppMessage: 'static, + D: iced::clipboard::mime::AsMimeTypes + std::marker::Send + 'static, + > DndSource<'a, Message, AppMessage, D> +{ + pub fn new(child: impl Into>) -> Self { + Self { + id: Id::unique(), + action: DndAction::Copy | DndAction::Move, + container: container(child).into(), + drag_content: None, + drag_icon: None, + drag_threshold: 8.0, + _phantom: std::marker::PhantomData, + } + } + + pub fn with_id(child: impl Into>, id: Id) -> Self { + Self { + id, + action: DndAction::Copy | DndAction::Move, + container: container(child).into(), + drag_content: None, + drag_icon: None, + drag_threshold: 8.0, + _phantom: std::marker::PhantomData, + } + } + + #[must_use] + pub fn action(mut self, action: DndAction) -> Self { + self.action = action; + self + } + + #[must_use] + pub fn drag_content(mut self, f: impl Fn() -> D + 'static) -> Self { + self.drag_content = Some(Box::new(f)); + self + } + + #[must_use] + pub fn drag_icon( + mut self, + f: impl Fn() -> (Element<'static, AppMessage>, tree::State) + 'static, + ) -> Self { + self.drag_icon = Some(Box::new(f)); + self + } + + #[must_use] + pub fn drag_threshold(mut self, threshold: f32) -> Self { + self.drag_threshold = threshold; + self + } + + pub fn start_dnd(&self, clipboard: &mut dyn Clipboard, bounds: Rectangle) { + let Some(content) = self.drag_content.as_ref().map(|f| f()) else { + return; + }; + iced_core::clipboard::start_dnd( + clipboard, + false, + Some(iced_core::clipboard::DndSource::Widget(self.id.clone())), + self.drag_icon.as_ref().map(|f| { + let (icon, state) = f(); + ( + container(icon) + .width(Length::Fixed(bounds.width)) + .height(Length::Fixed(bounds.height)) + .into(), + state, + ) + }), + Box::new(content), + self.action, + ); + } +} + +impl< + 'a, + Message: 'static, + AppMessage: 'static, + D: iced::clipboard::mime::AsMimeTypes + std::marker::Send + 'static, + > Widget + for DndSource<'a, Message, AppMessage, D> +{ + fn children(&self) -> Vec { + vec![Tree::new(&self.container)] + } + + fn tag(&self) -> iced_core::widget::tree::Tag { + tree::Tag::of::() + } + + fn diff(&mut self, tree: &mut Tree) { + tree.children[0].diff(self.container.as_widget_mut()); + } + + fn state(&self) -> iced_core::widget::tree::State { + tree::State::new(State::new()) + } + + fn size(&self) -> iced_core::Size { + self.container.as_widget().size() + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &iced::Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let state = tree.state.downcast_mut::(); + let node = self.container.as_widget().layout( + &mut tree.children[0], + renderer, + limits, + ); + state.cached_bounds = node.bounds(); + node + } + + fn operate( + &self, + tree: &mut Tree, + layout: layout::Layout<'_>, + renderer: &iced::Renderer, + operation: &mut dyn iced_core::widget::Operation<()>, + ) { + operation.custom((&mut tree.state) as &mut dyn Any, Some(&self.id)); + operation.container( + Some(&self.id), + layout.bounds(), + &mut |operation| { + self.container.as_widget().operate( + &mut tree.children[0], + layout, + renderer, + operation, + ) + }, + ); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: layout::Layout<'_>, + cursor: mouse::Cursor, + renderer: &iced::Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + let ret = self.container.as_widget_mut().on_event( + &mut tree.children[0], + event.clone(), + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ); + + let state = tree.state.downcast_mut::(); + + match event { + Event::Mouse(mouse_event) => match mouse_event { + mouse::Event::ButtonPressed(mouse::Button::Left) => { + if let Some(position) = cursor.position() { + if !state.hovered { + return ret; + } + + state.left_pressed_position = Some(position); + // dbg!(&state, &self.id); + return event::Status::Captured; + } + } + mouse::Event::ButtonReleased(mouse::Button::Left) + if state.left_pressed_position.is_some() => + { + state.left_pressed_position = None; + return event::Status::Captured; + } + mouse::Event::CursorMoved { .. } => { + if let Some(position) = cursor.position() { + if state.hovered { + // We ignore motion if we do not possess drag content by now. + if self.drag_content.is_none() { + state.left_pressed_position = None; + return ret; + } + if let Some(left_pressed_position) = + state.left_pressed_position + { + // dbg!(&state); + if position.distance(left_pressed_position) + > self.drag_threshold + { + self.start_dnd( + clipboard, + state.cached_bounds, + ); + state.is_dragging = true; + state.left_pressed_position = None; + } + } + if !cursor.is_over(layout.bounds()) { + state.hovered = false; + + return ret; + } + } else if cursor.is_over(layout.bounds()) { + state.hovered = true; + } + return event::Status::Captured; + } + } + _ => return ret, + }, + Event::Dnd(DndEvent::Source( + SourceEvent::Cancelled | SourceEvent::Finished, + )) => { + if state.is_dragging { + state.is_dragging = false; + return event::Status::Captured; + } + return ret; + } + _ => return ret, + } + ret + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: layout::Layout<'_>, + cursor_position: mouse::Cursor, + viewport: &Rectangle, + renderer: &iced::Renderer, + ) -> mouse::Interaction { + let state = tree.state.downcast_ref::(); + if state.is_dragging { + return mouse::Interaction::Grabbing; + } + self.container.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor_position, + viewport, + renderer, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut iced::Renderer, + theme: &iced::Theme, + renderer_style: &renderer::Style, + layout: layout::Layout<'_>, + cursor_position: mouse::Cursor, + viewport: &Rectangle, + ) { + self.container.as_widget().draw( + &tree.children[0], + renderer, + theme, + renderer_style, + layout, + cursor_position, + viewport, + ); + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &iced::Renderer, + translation: iced::Vector, + ) -> Option> + { + None + } + + fn drag_destinations( + &self, + state: &Tree, + layout: layout::Layout<'_>, + renderer: &iced::Renderer, + dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, + ) { + self.container.as_widget().drag_destinations( + &state.children[0], + layout, + renderer, + dnd_rectangles, + ); + } + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn set_id(&mut self, id: Id) { + self.id = id; + } +} + +impl< + 'a, + Message: 'static, + AppMessage: 'static, + D: iced::clipboard::mime::AsMimeTypes + std::marker::Send + 'static, + > From> for Element<'a, Message> +{ + fn from(e: DndSource<'a, Message, AppMessage, D>) -> Element<'a, Message> { + Element::new(e) + } +} + +/// Local state of the [`MouseListener`]. +#[derive(Debug, Default)] +struct State { + hovered: bool, + left_pressed_position: Option, + is_dragging: bool, + cached_bounds: Rectangle, +} + +impl State { + fn new() -> Self { + Self::default() + } +} diff --git a/examples/sctk_drag/src/main.rs b/examples/sctk_drag/src/main.rs new file mode 100644 index 0000000000..f2d1a5e527 --- /dev/null +++ b/examples/sctk_drag/src/main.rs @@ -0,0 +1,202 @@ +mod dnd_destination; +mod dnd_source; + +use std::{borrow::Cow, convert::Infallible}; + +use dnd_destination::dnd_destination; +use iced::{ + clipboard::mime::{AllowedMimeTypes, AsMimeTypes}, + platform_specific::{ + runtime::wayland::layer_surface::SctkLayerSurfaceSettings, + shell::commands::layer_surface::get_layer_surface, + }, + widget::{column, container, text}, + window, Element, Length, Task, +}; +use iced_core::{ + widget::{tree, Text}, + Widget, +}; + +fn main() -> iced::Result { + iced::daemon(DndTest::title, DndTest::update, DndTest::view) + .run_with(DndTest::new) + // iced::application(Todos::title, Todos::update, Todos::view) + // .subscription(Todos::subscription) + // .font(include_bytes!("../fonts/icons.ttf").as_slice()) + // .window_size((500.0, 800.0)) + // .run_with(Todos::new) +} + +const SUPPORTED_MIME_TYPES: &'static [&'static str; 6] = &[ + "text/plain;charset=utf-8", + "text/plain;charset=UTF-8", + "UTF8_STRING", + "STRING", + "text/plain", + "TEXT", +]; + +#[derive(Debug, Default, Clone)] +pub struct MyDndString(String); + +impl AllowedMimeTypes for MyDndString { + fn allowed() -> std::borrow::Cow<'static, [String]> { + std::borrow::Cow::Owned(vec![ + "text/plain;charset=utf-8".to_string(), + "text/plain;charset=UTF-8".to_string(), + "UTF8_STRING".to_string(), + "STRING".to_string(), + "text/plain".to_string(), + "TEXT".to_string(), + ]) + } +} + +impl TryFrom<(Vec, String)> for MyDndString { + type Error = Infallible; + + fn try_from(value: (Vec, String)) -> Result { + Ok(MyDndString( + String::from_utf8_lossy(value.0.as_slice()).to_string(), + )) + } +} + +impl AsMimeTypes for MyDndString { + fn available(&self) -> std::borrow::Cow<'static, [String]> { + std::borrow::Cow::Owned(vec![ + "text/plain;charset=utf-8".to_string(), + "text/plain;charset=UTF-8".to_string(), + "UTF8_STRING".to_string(), + "STRING".to_string(), + "text/plain".to_string(), + "TEXT".to_string(), + ]) + } + + fn as_bytes( + &self, + _mime_type: &str, + ) -> Option> { + Some(Cow::Owned(self.0.clone().into_bytes())) + } +} + +#[derive(Debug, Clone)] +pub struct DndTest { + /// option with the dragged text + source: Option, + /// is the dnd over the target + current_text: String, + /// main id + id: iced_core::window::Id, +} + +#[derive(Debug, Clone)] +pub enum Message { + Drag, + DndData(MyDndString), +} + +impl DndTest { + fn new() -> (DndTest, Task) { + let current_text = String::from("Hello, world!"); + let mut s = SctkLayerSurfaceSettings::default(); + s.size_limits = s.size_limits.min_width(100.0).max_width(400.0); + s.size = Some((Some(500), Some(600))); + // s.anchor = Anchor::TOP.union(Anchor::BOTTOM); + ( + DndTest { + current_text, + source: None, + id: iced_core::window::Id::unique(), + }, + get_layer_surface(s), + ) + } + + fn title(&self, _id: window::Id) -> String { + String::from("DndTest") + } + + fn update(&mut self, message: Message) -> Task { + match message { + Message::DndData(s) => { + dbg!(&s); + self.current_text = s.0; + } + _ => {} + } + Task::none() + } + + fn view(&self, _id: window::Id) -> Element { + let s = self.current_text.chars().rev().collect::(); + let s2 = s.clone(); + column![ + dnd_destination::dnd_destination_for_data::( + container(text(format!( + "Drag text here: {}", + &self.current_text + ))) + .width(Length::Fill) + .height(Length::FillPortion(1)) + .padding(20), + |data, _| { + dbg!("got data"); + Message::DndData(data.unwrap_or_default()) + } + ) + .drag_id(1) + .on_enter(|_, _, m| { + dbg!(m); + Message::Drag + }) + .on_action_selected(|a| { + dbg!(a); + Message::Drag + }) + .on_drop(|_, _| { + dbg!("drop"); + Message::Drag + }) + .on_motion(|x, y| { + dbg!(x, y); + Message::Drag + }), + dnd_source::dnd_source( + container(text(format!( + "Drag me: {}", + &self.current_text.chars().rev().collect::() + ))) + .width(Length::Fill) + .height(Length::FillPortion(1)) + .padding(20) + ) + .drag_threshold(5.0) + .drag_icon(move || { + let t: Text<'static, iced::Theme, iced::Renderer> = + text(s.clone()); + let state = as iced_core::Widget<(), iced::Theme, iced::Renderer>>::state( + &t, + ); + ( + Element::<'static, (), iced::Theme, iced::Renderer>::from( + t, + ), + state, + ) + }) + .drag_content(move || { MyDndString(s2.clone()) }) + ] + .width(Length::Fill) + .into() + } +} + +pub struct CustomTheme; diff --git a/examples/sctk_lazy/Cargo.toml b/examples/sctk_lazy/Cargo.toml new file mode 100644 index 0000000000..512c00a0e2 --- /dev/null +++ b/examples/sctk_lazy/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "sctk_lazy" +version = "0.1.0" +authors = ["Nick Senger "] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../..", features = [ + "debug", + "lazy", + "wayland", + "winit", + "tokio", + "tiny-skia", + "advanced", +], default-features = false } diff --git a/examples/sctk_lazy/src/main.rs b/examples/sctk_lazy/src/main.rs new file mode 100644 index 0000000000..47560ee39d --- /dev/null +++ b/examples/sctk_lazy/src/main.rs @@ -0,0 +1,255 @@ +use iced::advanced::layout::Limits; +use iced::platform_specific::runtime::wayland::layer_surface::SctkLayerSurfaceSettings; +use iced::platform_specific::shell::commands::layer_surface::{ + get_layer_surface, KeyboardInteractivity, +}; + +use iced::widget::{ + button, column, horizontal_space, lazy, pick_list, row, scrollable, text, + text_input, +}; +use iced::window::Id; +use iced::Task; +use iced::{Element, Length}; + +use std::collections::HashSet; +use std::hash::Hash; + +pub fn main() -> iced::Result { + iced::daemon(App::title, App::update, App::view).run_with(App::new) +} + +struct App { + version: u8, + items: HashSet, + input: String, + order: Order, +} + +impl Default for App { + fn default() -> Self { + Self { + version: 0, + items: ["Foo", "Bar", "Baz", "Qux", "Corge", "Waldo", "Fred"] + .into_iter() + .map(From::from) + .collect(), + input: Default::default(), + order: Order::Ascending, + } + } +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +enum Color { + #[default] + Black, + Red, + Orange, + Yellow, + Green, + Blue, + Purple, +} + +impl Color { + const ALL: &'static [Color] = &[ + Color::Black, + Color::Red, + Color::Orange, + Color::Yellow, + Color::Green, + Color::Blue, + Color::Purple, + ]; +} + +impl std::fmt::Display for Color { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Black => "Black", + Self::Red => "Red", + Self::Orange => "Orange", + Self::Yellow => "Yellow", + Self::Green => "Green", + Self::Blue => "Blue", + Self::Purple => "Purple", + }) + } +} + +impl From for iced::Color { + fn from(value: Color) -> Self { + match value { + Color::Black => iced::Color::from_rgb8(0, 0, 0), + Color::Red => iced::Color::from_rgb8(220, 50, 47), + Color::Orange => iced::Color::from_rgb8(203, 75, 22), + Color::Yellow => iced::Color::from_rgb8(181, 137, 0), + Color::Green => iced::Color::from_rgb8(133, 153, 0), + Color::Blue => iced::Color::from_rgb8(38, 139, 210), + Color::Purple => iced::Color::from_rgb8(108, 113, 196), + } + } +} + +#[derive(Clone, Debug, Eq)] +struct Item { + name: String, + color: Color, +} + +impl Hash for Item { + fn hash(&self, state: &mut H) { + self.name.hash(state); + } +} + +impl PartialEq for Item { + fn eq(&self, other: &Self) -> bool { + self.name.eq(&other.name) + } +} + +impl From<&str> for Item { + fn from(s: &str) -> Self { + Self { + name: s.to_owned(), + color: Default::default(), + } + } +} + +#[derive(Debug, Clone)] +enum Message { + InputChanged(String), + ToggleOrder, + DeleteItem(Item), + AddItem(String), + ItemColorChanged(Item, Color), +} + +impl App { + fn new() -> (App, Task) { + let mut initial_surface = SctkLayerSurfaceSettings::default(); + initial_surface.keyboard_interactivity = + KeyboardInteractivity::OnDemand; + initial_surface.size_limits = Limits::NONE + .min_width(1.0) + .min_height(1.0) + .max_height(500.0) + .max_width(900.0); + initial_surface.size = Some((Some(500), Some(500))); + (Self::default(), get_layer_surface(initial_surface)) + } + + fn title(&self, _id: Id) -> String { + String::from("Lazy - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::InputChanged(input) => { + self.input = input; + } + Message::ToggleOrder => { + self.version = self.version.wrapping_add(1); + self.order = match self.order { + Order::Ascending => Order::Descending, + Order::Descending => Order::Ascending, + } + } + Message::AddItem(name) => { + self.version = self.version.wrapping_add(1); + self.items.insert(name.as_str().into()); + self.input.clear(); + } + Message::DeleteItem(item) => { + self.version = self.version.wrapping_add(1); + self.items.remove(&item); + } + Message::ItemColorChanged(item, color) => { + self.version = self.version.wrapping_add(1); + if self.items.remove(&item) { + self.items.insert(Item { + name: item.name, + color, + }); + } + } + } + } + + fn view(&self, _: Id) -> Element { + let options = lazy(self.version, |_| { + let mut items: Vec<_> = self.items.iter().cloned().collect(); + + items.sort_by(|a, b| match self.order { + Order::Ascending => { + a.name.to_lowercase().cmp(&b.name.to_lowercase()) + } + Order::Descending => { + b.name.to_lowercase().cmp(&a.name.to_lowercase()) + } + }); + + column(items.into_iter().map(|item| { + let button = button("Delete") + .on_press(Message::DeleteItem(item.clone())); + + row![ + text(item.name.clone()), + horizontal_space(), + pick_list(Color::ALL, Some(item.color), move |color| { + Message::ItemColorChanged(item.clone(), color) + }), + button + ] + .spacing(20) + .into() + })) + .spacing(10) + }); + + column![ + scrollable(options).height(Length::Fill), + row![ + 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) + ] + .spacing(10) + ] + .spacing(20) + .padding(20) + .into() + } + + fn theme(&self) -> iced::Theme { + iced::Theme::default() + } + + fn scale_factor(&self) -> f64 { + 1.0 + } +} + +#[derive(Debug, Hash)] +enum Order { + Ascending, + Descending, +} + +impl std::fmt::Display for Order { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Ascending => "Ascending", + Self::Descending => "Descending", + } + ) + } +} diff --git a/examples/sctk_session_lock/Cargo.toml b/examples/sctk_session_lock/Cargo.toml new file mode 100644 index 0000000000..c383ef3a2c --- /dev/null +++ b/examples/sctk_session_lock/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "sctk_session_lock" +version = "0.1.0" +edition = "2021" + +[dependencies] +sctk = { package = "smithay-client-toolkit", git = "https://github.com/smithay/client-toolkit", rev = "828b1eb" } +iced = { path = "../..", default-features = false, features = [ + "async-std", + "winit", + "wayland", + "debug", + "tiny-skia", + # "a11y", +] } +iced_runtime = { path = "../../runtime" } +env_logger = "0.10" +async-std = "1" diff --git a/examples/sctk_session_lock/src/main.rs b/examples/sctk_session_lock/src/main.rs new file mode 100644 index 0000000000..8d42973f0e --- /dev/null +++ b/examples/sctk_session_lock/src/main.rs @@ -0,0 +1,96 @@ +use iced::event::listen_raw; +use iced::Task; +use iced::{ + event::wayland::{Event as WaylandEvent, OutputEvent, SessionLockEvent}, + platform_specific::shell::commands::session_lock, + widget::text, + window, Element, Subscription, +}; + +fn main() -> iced::Result { + iced::daemon(Locker::title, Locker::update, Locker::view) + .subscription(Locker::subscription) + .run_with(Locker::new) +} + +#[derive(Debug, Clone, Default)] +struct Locker { + _exit: bool, +} + +#[derive(Debug, Clone)] +pub enum Message { + WaylandEvent(WaylandEvent), + TimeUp, + Ignore, +} + +impl Locker { + fn new() -> (Locker, Task) { + ( + Locker { + ..Locker::default() + }, + session_lock::lock(), + ) + } + + fn title(&self, _id: window::Id) -> String { + String::from("Locker") + } + + fn update(&mut self, message: Message) -> Task { + match message { + Message::WaylandEvent(evt) => match evt { + WaylandEvent::Output(evt, output) => match evt { + OutputEvent::Created(_) => { + return session_lock::get_lock_surface( + window::Id::unique(), + output, + ); + } + OutputEvent::Removed => {} + _ => {} + }, + WaylandEvent::SessionLock(evt) => match evt { + SessionLockEvent::Locked => { + return iced::Task::perform( + async_std::task::sleep( + std::time::Duration::from_secs(5), + ), + |_| Message::TimeUp, + ); + } + SessionLockEvent::Unlocked => { + // Server has processed unlock, so it's safe to exit + std::process::exit(0); + } + _ => {} + }, + _ => {} + }, + Message::TimeUp => { + return session_lock::unlock(); + } + Message::Ignore => {} + } + Task::none() + } + + fn view(&self, id: window::Id) -> Element { + text(format!("Lock Surface {:?}", id)).into() + } + + fn subscription(&self) -> Subscription { + listen_raw(|evt, _, _| { + if let iced::Event::PlatformSpecific( + iced::event::PlatformSpecific::Wayland(evt), + ) = evt + { + Some(Message::WaylandEvent(evt)) + } else { + None + } + }) + } +} diff --git a/examples/sctk_subsurface/Cargo.toml b/examples/sctk_subsurface/Cargo.toml new file mode 100644 index 0000000000..30949510b2 --- /dev/null +++ b/examples/sctk_subsurface/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "sctk_subsurface" +version = "0.1.0" +edition = "2021" + +[dependencies] +sctk = { package = "smithay-client-toolkit", git = "https://github.com/smithay/client-toolkit", rev = "828b1eb" } +iced = { path = "../..", default-features = false, features = [ + "tokio", + "wayland", + "winit", + "debug", + "tiny-skia", +] } +iced_runtime = { path = "../../runtime" } +env_logger = "0.10" +futures-channel = "0.3.29" +calloop = "0.12.3" +rustix = { version = "0.38.30", features = ["fs", "shm"] } diff --git a/examples/sctk_subsurface/src/main.rs b/examples/sctk_subsurface/src/main.rs new file mode 100644 index 0000000000..21a9a75fed --- /dev/null +++ b/examples/sctk_subsurface/src/main.rs @@ -0,0 +1,123 @@ +// Shows a subsurface with a 1x1 px red buffer, stretch to window size + +use iced::{ + event::wayland::Event as WaylandEvent, + platform_specific::shell::subsurface_widget::{self, SubsurfaceBuffer}, + widget::text, + window::{self, Id, Settings}, + Element, Length, Subscription, Task, +}; +use sctk::reexports::client::{Connection, Proxy}; + +mod wayland; + +fn main() -> iced::Result { + iced::daemon( + SubsurfaceApp::title, + SubsurfaceApp::update, + SubsurfaceApp::view, + ) + .subscription(SubsurfaceApp::subscription) + .run_with(SubsurfaceApp::new) +} + +#[derive(Debug, Clone, Default)] +struct SubsurfaceApp { + connection: Option, + red_buffer: Option, +} + +#[derive(Debug, Clone)] +pub enum Message { + WaylandEvent(WaylandEvent), + Wayland(wayland::Event), + Pressed(&'static str), + Id(Id), +} + +impl SubsurfaceApp { + fn new() -> (SubsurfaceApp, Task) { + ( + SubsurfaceApp { + ..SubsurfaceApp::default() + }, + iced::window::open(Settings { + ..Default::default() + }) + .1 + .map(Message::Id), + ) + } + + fn title(&self, _id: window::Id) -> String { + String::from("SubsurfaceApp") + } + + fn update(&mut self, message: Message) -> Task { + match message { + Message::WaylandEvent(evt) => match evt { + WaylandEvent::Output(_evt, output) => { + if self.connection.is_none() { + if let Some(backend) = output.backend().upgrade() { + self.connection = + Some(Connection::from_backend(backend)); + } + } + } + _ => {} + }, + Message::Wayland(evt) => match evt { + wayland::Event::RedBuffer(buffer) => { + self.red_buffer = Some(buffer); + } + }, + Message::Pressed(side) => println!("{side} surface pressed"), + Message::Id(_) => {} + } + Task::none() + } + + fn view(&self, _id: window::Id) -> Element { + if let Some(buffer) = &self.red_buffer { + iced::widget::row![ + iced::widget::button( + subsurface_widget::Subsurface::new(1, 1, buffer) + .width(Length::Fill) + .height(Length::Fill) + ) + .width(Length::Fill) + .height(Length::Fill) + .on_press(Message::Pressed("left")), + iced::widget::button( + subsurface_widget::Subsurface::new(1, 1, buffer) + .width(Length::Fill) + .height(Length::Fill) + ) + .width(Length::Fill) + .height(Length::Fill) + .on_press(Message::Pressed("right")) + ] + .into() + } else { + text("No subsurface").into() + } + } + + fn subscription(&self) -> Subscription { + let mut subscriptions = vec![iced::event::listen_with(|evt, _, _| { + if let iced::Event::PlatformSpecific( + iced::event::PlatformSpecific::Wayland(evt), + ) = evt + { + Some(Message::WaylandEvent(evt)) + } else { + None + } + })]; + if let Some(connection) = &self.connection { + subscriptions + .push(wayland::subscription(connection).map(Message::Wayland)); + } + Subscription::batch(subscriptions) + } +} diff --git a/examples/sctk_subsurface/src/wayland.rs b/examples/sctk_subsurface/src/wayland.rs new file mode 100644 index 0000000000..315f168199 --- /dev/null +++ b/examples/sctk_subsurface/src/wayland.rs @@ -0,0 +1,128 @@ +use futures_channel::mpsc; +use iced::{ + futures::{FutureExt, SinkExt}, + platform_specific::shell::subsurface_widget::{Shmbuf, SubsurfaceBuffer}, +}; +use iced_runtime::futures::subscription; +use rustix::{io::Errno, shm::ShmOFlags}; +use sctk::{ + reexports::{ + calloop_wayland_source::WaylandSource, + client::{ + delegate_noop, + globals::registry_queue_init, + protocol::{wl_buffer::WlBuffer, wl_shm}, + Connection, + }, + }, + registry::{ProvidesRegistryState, RegistryState}, + shm::{Shm, ShmHandler}, +}; +use std::{ + os::fd::OwnedFd, + sync::Arc, + thread, + time::{SystemTime, UNIX_EPOCH}, +}; + +#[derive(Debug, Clone)] +pub enum Event { + RedBuffer(SubsurfaceBuffer), +} + +struct AppData { + registry_state: RegistryState, + shm_state: Shm, +} + +impl ProvidesRegistryState for AppData { + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } + + sctk::registry_handlers!(); +} + +impl ShmHandler for AppData { + fn shm_state(&mut self) -> &mut Shm { + &mut self.shm_state + } +} + +pub fn subscription(connection: &Connection) -> iced::Subscription { + let connection = connection.clone(); + subscription::Subscription::run_with_id( + "wayland-sub", + async { start(connection).await }.flatten_stream(), + ) +} + +async fn start(conn: Connection) -> mpsc::Receiver { + let (mut sender, receiver) = mpsc::channel(20); + + let (globals, event_queue) = registry_queue_init(&conn).unwrap(); + let qh = event_queue.handle(); + + let mut app_data = AppData { + registry_state: RegistryState::new(&globals), + shm_state: Shm::bind(&globals, &qh).unwrap(), + }; + + let fd = create_memfile().unwrap(); + rustix::io::write(&fd, &[0, 255, 0, 255]).unwrap(); + + let shmbuf = Shmbuf { + fd, + offset: 0, + width: 1, + height: 1, + stride: 4, + format: wl_shm::Format::Xrgb8888, + }; + + let buffer = SubsurfaceBuffer::new(Arc::new(shmbuf.into())).0; + let _ = sender.send(Event::RedBuffer(buffer)).await; + + thread::spawn(move || { + let mut event_loop = calloop::EventLoop::try_new().unwrap(); + WaylandSource::new(conn, event_queue) + .insert(event_loop.handle()) + .unwrap(); + loop { + event_loop.dispatch(None, &mut app_data).unwrap(); + } + }); + + receiver +} + +fn create_memfile() -> rustix::io::Result { + loop { + let flags = ShmOFlags::CREATE | ShmOFlags::EXCL | ShmOFlags::RDWR; + + let time = SystemTime::now(); + let name = format!( + "/iced-sctk-{}", + time.duration_since(UNIX_EPOCH).unwrap().subsec_nanos() + ); + + match rustix::io::retry_on_intr(|| { + rustix::shm::shm_open(&name, flags, 0600.into()) + }) { + Ok(fd) => match rustix::shm::shm_unlink(&name) { + Ok(_) => return Ok(fd), + Err(errno) => { + return Err(errno.into()); + } + }, + Err(Errno::EXIST) => { + continue; + } + Err(err) => return Err(err.into()), + } + } +} + +delegate_noop!(AppData: ignore WlBuffer); +sctk::delegate_registry!(AppData); +sctk::delegate_shm!(AppData); diff --git a/examples/sctk_subsurface_gst/Cargo.toml b/examples/sctk_subsurface_gst/Cargo.toml new file mode 100644 index 0000000000..4b8095ced7 --- /dev/null +++ b/examples/sctk_subsurface_gst/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "sctk_subsurface_gst" +version = "0.1.0" +edition = "2021" + +[dependencies] +sctk = { package = "smithay-client-toolkit", git = "https://github.com/smithay/client-toolkit", rev = "828b1eb" } +iced = { path = "../..", default-features = false, features = [ + "wayland", + "debug", + "a11y", +] } +iced_runtime = { path = "../../runtime" } +env_logger = "0.10" +futures-channel = "0.3.29" +calloop = "0.12.3" +gst = { package = "gstreamer", version = "0.21.3" } +gst-app = { package = "gstreamer-app", version = "0.21.2" } +gst-video = { package = "gstreamer-video", version = "0.21.2" } +gst-allocators = { package = "gstreamer-allocators", version = "0.21.2" } +drm-fourcc = "2.2.0" diff --git a/examples/sctk_subsurface_gst/src/main.rs b/examples/sctk_subsurface_gst/src/main.rs new file mode 100644 index 0000000000..09119117bb --- /dev/null +++ b/examples/sctk_subsurface_gst/src/main.rs @@ -0,0 +1,84 @@ +// Shows a subsurface with a 1x1 px red buffer, stretch to window size + +use iced::{ + wayland::InitialSurface, widget::text, window, Application, Command, + Element, Length, Subscription, Theme, +}; +use iced_sctk::subsurface_widget::SubsurfaceBuffer; +use std::{env, path::Path}; + +mod pipewire; + +fn main() { + let args = env::args(); + if args.len() != 2 { + eprintln!("usage: sctk_subsurface_gst [h264 mp4 path]"); + return; + } + let path = args.skip(1).next().unwrap(); + if !Path::new(&path).exists() { + eprintln!("File `{path}` not found."); + return; + } + let mut settings = iced::Settings::with_flags(path); + settings.initial_surface = InitialSurface::XdgWindow(Default::default()); + SubsurfaceApp::run(settings).unwrap(); +} + +#[derive(Debug, Clone, Default)] +struct SubsurfaceApp { + path: String, + buffer: Option, +} + +#[derive(Debug, Clone)] +pub enum Message { + Pipewire(pipewire::Event), +} + +impl Application for SubsurfaceApp { + type Executor = iced::executor::Default; + type Message = Message; + type Flags = String; + type Theme = Theme; + + fn new(flags: String) -> (SubsurfaceApp, Command) { + ( + SubsurfaceApp { + path: flags, + ..SubsurfaceApp::default() + }, + Command::none(), + ) + } + + fn title(&self, _id: window::Id) -> String { + String::from("SubsurfaceApp") + } + + fn update(&mut self, message: Self::Message) -> Command { + match message { + Message::Pipewire(evt) => match evt { + pipewire::Event::Frame(subsurface_buffer) => { + self.buffer = Some(subsurface_buffer); + } + }, + } + Command::none() + } + + fn view(&self, _id: window::Id) -> Element { + if let Some(buffer) = &self.buffer { + iced_sctk::subsurface_widget::Subsurface::new(1, 1, buffer) + .width(Length::Fill) + .height(Length::Fill) + .into() + } else { + text("No subsurface").into() + } + } + + fn subscription(&self) -> Subscription { + pipewire::subscription(&self.path).map(Message::Pipewire) + } +} diff --git a/examples/sctk_subsurface_gst/src/pipewire.rs b/examples/sctk_subsurface_gst/src/pipewire.rs new file mode 100644 index 0000000000..9933ee099d --- /dev/null +++ b/examples/sctk_subsurface_gst/src/pipewire.rs @@ -0,0 +1,185 @@ +use drm_fourcc::{DrmFourcc, DrmModifier}; +use gst::glib::{self, translate::IntoGlib}; +use gst::prelude::*; +use iced::futures::{executor::block_on, SinkExt}; +use iced_sctk::subsurface_widget::{ + BufferSource, Dmabuf, Plane, SubsurfaceBuffer, +}; +use std::{ffi::c_void, os::unix::io::BorrowedFd, sync::Arc, thread}; + +const USE_NV12: bool = false; + +// Store a reference to the `BufferSource` in the data assocaited with the `BufferRef`. +// So the `BufferSource` can be re-used, instead of dupping fds and creating a new +// `wl_buffer` each buffer swap. +// +// See https://gitlab.freedesktop.org/gstreamer/gstreamer/-/blob/main/subprojects/gst-plugins-bad/gst-libs/gst/wayland/gstwlbuffer.c +// for information about how `waylandsink` does this. +fn get_buffer_source(buffer: &gst::BufferRef) -> Option> { + let buffer_source_quark = glib::Quark::from_str("SctkBufferSource"); + unsafe { + let data = gst::ffi::gst_mini_object_get_qdata( + buffer.upcast_ref().as_mut_ptr(), + buffer_source_quark.into_glib(), + ); + if data.is_null() { + None + } else { + Arc::increment_strong_count(data as *const BufferSource); + Some(Arc::from_raw(data as *const BufferSource)) + } + } +} + +fn set_buffer_source(buffer: &gst::BufferRef, source: Arc) { + let buffer_source_quark = glib::Quark::from_str("SctkBufferSource"); + unsafe extern "C" fn destroy_buffer_source(data: *mut c_void) { + Arc::from_raw(data); + } + unsafe { + gst::ffi::gst_mini_object_set_qdata( + buffer.upcast_ref().as_mut_ptr(), + buffer_source_quark.into_glib(), + Arc::into_raw(source) as *mut c_void, + Some(destroy_buffer_source), + ); + } +} + +#[derive(Debug, Clone)] +pub enum Event { + Frame(SubsurfaceBuffer), +} + +pub fn subscription(path: &str) -> iced::Subscription { + let path = path.to_string(); + iced::subscription::channel("pw", 16, |sender| async { + thread::spawn(move || pipewire_thread(&path, sender)); + std::future::pending().await + }) +} + +fn pipewire_thread( + path: &str, + mut sender: futures_channel::mpsc::Sender, +) { + gst::init().unwrap(); + + let pipeline = gst::parse_launch(&format!( + "filesrc name=filesrc ! + qtdemux ! + h264parse ! + vah264dec ! + vapostproc name=postproc ! + capsfilter name=capfilter ! + appsink name=sink", + )) + .unwrap() + .dynamic_cast::() + .unwrap(); + pipeline + .by_name("filesrc") + .unwrap() + .set_property("location", path); + + let format = if USE_NV12 { + /* + pipeline + .remove(&pipeline.by_name("postproc").unwrap()) + .unwrap(); + */ + gst_video::VideoFormat::Nv12 + } else { + gst_video::VideoFormat::Bgra + }; + pipeline.by_name("capfilter").unwrap().set_property( + "caps", + gst_video::VideoCapsBuilder::new() + .features(["memory:DMABuf"]) + .format(format) + .build(), + ); + + let appsink = pipeline + .by_name("sink") + .unwrap() + .dynamic_cast::() + .unwrap(); + + let mut subsurface_release = None; + + appsink.set_callbacks( + gst_app::AppSinkCallbacks::builder() + .new_sample(move |appsink| { + let sample = + appsink.pull_sample().map_err(|_| gst::FlowError::Eos)?; + + let buffer = sample.buffer().unwrap(); + let meta = buffer.meta::().unwrap(); + + let buffer_source = if let Some(buffer_source) = get_buffer_source(buffer) { + buffer_source + } else { + let planes = (0..meta.n_planes()) + .map(|plane_idx| { + let memory = buffer + .memory(plane_idx) + .unwrap() + .downcast_memory::() + .unwrap(); + + // TODO avoid dup? + let fd = unsafe { BorrowedFd::borrow_raw(memory.fd()) } + .try_clone_to_owned() + .unwrap(); + + Plane { + fd, + plane_idx, + offset: meta.offset()[plane_idx as usize] as u32, + stride: meta.stride()[plane_idx as usize] as u32, + } + }) + .collect(); + + let format = if USE_NV12 { + DrmFourcc::Nv12 + } else { + DrmFourcc::Argb8888 + }; + let dmabuf = Dmabuf { + width: meta.width() as i32, + height: meta.height() as i32, + planes, + // TODO should use dmabuf protocol to get supported formats, + // convert if needed. + format: format as u32, + // TODO modifier negotiation + modifier: DrmModifier::Linear.into(), + }; + + let buffer_source = Arc::new(BufferSource::from(dmabuf)); + set_buffer_source(buffer, buffer_source.clone()); + buffer_source + }; + + let (buffer, new_subsurface_release) = + SubsurfaceBuffer::new(buffer_source); + block_on(sender.send(Event::Frame(buffer))).unwrap(); + + // Wait for server to release other buffer + // TODO is gstreamer using triple buffering? + if let Some(release) = subsurface_release.take() { + block_on(release); + } + subsurface_release = Some(new_subsurface_release); + + Ok(gst::FlowSuccess::Ok) + }) + .build(), + ); + + pipeline.set_state(gst::State::Playing).unwrap(); + let bus = pipeline.bus().unwrap(); + for _msg in bus.iter_timed(gst::ClockTime::NONE) {} +} diff --git a/examples/sctk_todos/Cargo.toml b/examples/sctk_todos/Cargo.toml new file mode 100644 index 0000000000..1638e2e1e7 --- /dev/null +++ b/examples/sctk_todos/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "sctk_todos" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2021" +publish = false + +[dependencies] +iced.workspace = true +iced.features = [ + "tokio", + "wayland", + "winit", + "debug", + "tiny-skia", + "a11y", + "wgpu", +] +# iced.features = ["async-std", "wayland", "debug", "wayland-clipboard", "a11y", "tiny-skia"] +# TODO(POP): Fix a11y not working with new winit +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +iced_core.workspace = true +once_cell = "1.15" +sctk.workspace = true +log = "0.4.17" +env_logger = "0.10.0" +async-std = "1.0" +directories-next = "2.0.0" + +[profile.release-opt] +debug = true diff --git a/examples/sctk_todos/README.md b/examples/sctk_todos/README.md new file mode 100644 index 0000000000..9c2598b95e --- /dev/null +++ b/examples/sctk_todos/README.md @@ -0,0 +1,20 @@ +## Todos + +A todos tracker inspired by [TodoMVC]. It showcases dynamic layout, text input, checkboxes, scrollables, icons, and async actions! It automatically saves your tasks in the background, even if you did not finish typing them. + +All the example code is located in the __[`main`]__ file. + + + +You can run the native version with `cargo run`: +``` +cargo run --package todos +``` +We have not yet implemented a `LocalStorage` version of the auto-save feature. Therefore, it does not work on web _yet_! + +[`main`]: src/main.rs +[TodoMVC]: http://todomvc.com/ diff --git a/examples/sctk_todos/fonts/icons.ttf b/examples/sctk_todos/fonts/icons.ttf new file mode 100644 index 0000000000000000000000000000000000000000..4498299db26811ff8490001ea6f681529a4118f4 GIT binary patch literal 5596 zcmd^CO>7&-6@Ih3q$ui#vPDs`8QVjxer$0`$&xI|w&c*HBs!7Y$g=E^c3g8sQ9q6( zDiV`AMGpat2C14RX%9hBwCJS&3oU>ym|BH?TQ2u`RGN`X*GHNLR@~m?aR>o8~5~+=ev5wtmzPu zm_++xcG+J2m73zgx)Jvw;tO{uYtj4}1Rt6jj6eMYJc-Ze3U|T(3U?L~duC zrF_;F-zSXiei2IVvVDD3Xf>ar{R-N0#a_<+=6i?Wulr4m|J&94dg1KPCZ;g|S70Ar zUCXWh?Q|HvG)<%Z67kx-)J>;I8yTCJrurqjutNLEfSxb5-;Kr6;=E0svPHngRsoG5 zcSWneCSE5O=Kr$BtA3><#b4>D(4Zxk4($W3$+^*4ihWr7v93>TU!zO<6uki&`%t6(!CLa0IgB(OS@0VE->+IH z0Da{!ASxe1!#BtHqfbgV$Ms{__%27E^qbnZsfiB6_WJ}0kt9uMd2|waaOV8Ye%;j^ z7XB*XZs`#1eUFL$ojY@(m3W zKF+S~qP|zJ!O1;DT{J+KDHXdgeq8gokA(K^sTOW*X_9KG%3WKP@d^*QJ=1kHn%mGc z-sX%;*F<%-m}V)eQ&cUgC(@}4Q%{~vjwNF4EsgDbnf0y%;kG?}?P1a4ZrbAyoD@C% z1E0|ry&dfxrn}133cRwX}EaFp!@^!5Y}2|UC>ucy`Hbsn$X zfvr4E6 z2-bQ|yM%C^$I$=zXLKYU)f$~CuQWX>4*IX)=-^Z#u4lDvoMu1mqgHw;)_hQCt^6%Wu5IFhCakY0c73(0E=E{?%W2<4pR>PQe3t<>y3PKo9ks*xnV61&Nlk&TX z>DXSPkbI=M!B>r4LuEuD!_5O7RZYE3qR(tW{xtb}dj>>*N3$@G3BONt3~(w1e%*7U z_l&q&>oT_9GwNK1=+Y0~-s--spY>n4eZ_w=aKm5Kd!u(Kskv*7t=}7Xim37I?X9j4 z#CHxfUHRbYrluPKPJFH=xZj|c8m{_={zWzVZC3yJqf<_WC`m->HZqXw3{Hb{p^sC$ zi22*wc=AYhUj|#8dk-Yu6wWnB?~8dLW*hW0=Ql2m9z{)C2U@J*`p?&1`peFk$Ivc~ z&lUJs8EaHU!)2^PKT^g9@I)EAsD`G?*bV*FGWJrK=F7N-8tGenwvB4cbB%76v7iRw zD`So7#i26Rp^ucY0X$sBE((aVW$cDNTgG0xDAvokhT6ri68Y5^))wa%3i5E`i0tV; zdR)%DAoEZyuGmY`ey*^PUt5@G%&p|s>_TpqM_+$_zNb)_lXGkNWjR(JSFWz*ujR6Z=t7~edZMeV(v#<-1m$U! zUZ6EvM5q?1K#~qadjzrvuhOG*9B2j%31*44NGoL15;QhFhaL-#WgYDp?m4tppv{4? z1RSL-p3A%RQ((-a{}M)7+hx6fl#5`mA$b;^(Ixzf!n^xfNw8KNrtNqz3x7(!uha9G ztq0lyda;*lj#rY#oDuK%D-jR2UBft8u%k{?3ecWFY3|xJXJviJs>-=R>3QH~2u0Aux2x|X7WwfblOxjnaZWp5v5ylR4Sv*hC{BzWJd7ge4)qFk1$N`yDDeNJfHXqs^oAvWW-(q`tA$YOlu>Wru=OR|$SiR{}3&42Aj2G!+V(p>$^`qUx-orj4pudnBUjEi6Dv zRhxK%*9Bn4)2fbJQ)tzp6;VD6)8K?eA_7^st?CmQxsj2o9zlz!25WpeRWxQt(ygj4 zXI_t}J=XZS)cE<5G8lrs(b4a*Ou;_;14aj!e9*22LSgvpP!HHIUq$tnt# z0mPYQvsKhtK4KLOmiw%ti{*Z+=w=zTid8HhwTd2=2drW>%YzoFy71?4;^}S?mn)l0uOYH^%VG#Q-eoNnS;eH(S(&8#k%3>1G{99wf0~{^;ps7p@{1JEGjZA z3wj^6f&y(aDwBLN5yHneHj-u%l^}(hjhct!+ABnpAM+nW2?-$k@#j!fbt0VGh?-Ik zZD6eaJ7yUzjiC&T36@kDKFqOmsau-VW$>2PuJ2FBxxjf)Dls2sG{&_Zk(o7>p0H<8W3+@F1kR*!Fz@eU!zEN*bIcwLnwVh>>w<7*!FUgt1debeG;q2R zdlwQ3b^AU~FrtmlZH^Oo;x)o0?9N=sk^zo^#O$v2atzENgl5oDD-TYulw)R+C*$2Z z?u3jNP>v`~r=oHQFFy9Tti)hv5QNUah5#+MQe(v%E9#F``bCJxElxCd2RE z`fs%=!>)9_hjYqO$HEoMJ%c`Gss8W= za)^^<1IKaK#MqXo3S<756E04`N_087Oq_}+4oS(!(L||Q=tJ~l zsI|i1sCvLjTB;A?3`cDgag}3uXI0|#xW(zH&LFH$Serzr0mcCYg9&R>IGVEnj^+!@ ziNo|Ha~MoAhrv1KFqmS_DS-3LVKB`c1{ava;39Kk08cT8L5evH(#&CSi8%>?%gkZ$ zG;R;?y#JKq-DUsc98 J@S+$Y`Y%9~)?EMq literal 0 HcmV?d00001 diff --git a/examples/sctk_todos/iced-todos.desktop b/examples/sctk_todos/iced-todos.desktop new file mode 100644 index 0000000000..dd7ce53dad --- /dev/null +++ b/examples/sctk_todos/iced-todos.desktop @@ -0,0 +1,4 @@ +[Desktop Entry] +Name=Todos - Iced +Exec=iced-todos +Type=Application diff --git a/examples/sctk_todos/index.html b/examples/sctk_todos/index.html new file mode 100644 index 0000000000..ee5570fb9e --- /dev/null +++ b/examples/sctk_todos/index.html @@ -0,0 +1,12 @@ + + + + + + Todos - Iced + + + + + + diff --git a/examples/sctk_todos/src/main.rs b/examples/sctk_todos/src/main.rs new file mode 100644 index 0000000000..ce8e2c3f9e --- /dev/null +++ b/examples/sctk_todos/src/main.rs @@ -0,0 +1,648 @@ +use env_logger::Env; +use iced::alignment::{self, Alignment}; +use iced::event::{self, listen_raw, Event}; +use iced::platform_specific::shell::commands::layer_surface::{ + get_layer_surface, Anchor, +}; +use iced::theme::{self, Theme}; +use iced::widget::{ + self, button, checkbox, column, container, row, scrollable, text, + text_input, Text, +}; +use iced::window::Settings; +use iced::{window, Application, Element, Program, Task}; +use iced::{Color, Font, Length, Subscription}; +use iced_core::id::Id; +use iced_core::keyboard::key::Named; +use iced_core::layout::Limits; +use iced_core::{id, keyboard}; + +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; + +static INPUT_ID: Lazy = Lazy::new(|| text_input::Id::unique()); + +pub fn main() -> iced::Result { + let env = Env::default() + .filter_or("MY_LOG_LEVEL", "info") + .write_style_or("MY_LOG_STYLE", "always"); + + env_logger::init_from_env(env); + iced::daemon(Todos::title, Todos::update, Todos::view) + .subscription(Todos::subscription) + .font(include_bytes!("../fonts/icons.ttf").as_slice()) + .run_with(Todos::new) +} + +#[derive(Debug)] +enum Todos { + Loading, + Loaded(State), +} + +#[derive(Debug, Default)] +struct State { + window_id_ctr: u128, + input_value: String, + filter: Filter, + tasks: Vec, + dirty: bool, + saving: bool, +} + +#[derive(Clone)] +enum Message { + Loaded(Result), + Saved(Result<(), SaveError>), + InputChanged(String), + CreateTask, + FilterChanged(Filter), + TaskMessage(usize, TaskMessage), + TabPressed { shift: bool }, + CloseRequested(window::Id), + Ignore, +} + +impl Debug for Message { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Message::Loaded(_) => write!(f, "Message::Loaded(_)"), + Message::Saved(_) => write!(f, "Message::Saved(_)"), + Message::InputChanged(_) => write!(f, "Message::InputChanged(_)"), + Message::CreateTask => write!(f, "Message::CreateTask"), + Message::FilterChanged(_) => write!(f, "Message::FilterChanged(_)"), + Message::TaskMessage(_, _) => { + write!(f, "Message::TaskMessage(_, _)") + } + Message::TabPressed { shift: _ } => { + write!(f, "Message::TabPressed {{ shift: _ }}") + } + Message::CloseRequested(_) => { + write!(f, "Message::CloseRequested(_)") + } + + Message::Ignore => write!(f, "Message::Ignore"), + } + } +} + +impl Todos { + fn new() -> (Todos, Task) { + ( + Todos::Loading, + Task::batch(vec![ + Task::perform(SavedState::load(), Message::Loaded), + get_layer_surface(iced::platform_specific::runtime::wayland::layer_surface::SctkLayerSurfaceSettings { + size: Some((None, Some(500))), + pointer_interactivity: true, + keyboard_interactivity: sctk::shell::wlr_layer::KeyboardInteractivity::OnDemand, + anchor: Anchor::LEFT.union(Anchor::RIGHT).union(Anchor::TOP), + ..Default::default() + }), + ]), + ) + } + + fn title(&self, _id: window::Id) -> String { + let dirty = match self { + Todos::Loading => false, + Todos::Loaded(state) => state.dirty, + }; + + format!("Todos{} - Iced", if dirty { "*" } else { "" }) + } + + fn update(&mut self, message: Message) -> Task { + match self { + Todos::Loading => { + match message { + Message::Loaded(Ok(state)) => { + *self = Todos::Loaded(State { + input_value: state.input_value, + filter: state.filter, + tasks: state.tasks, + window_id_ctr: 1, + ..State::default() + }); + } + Message::Loaded(Err(_)) => { + *self = Todos::Loaded(State::default()); + } + _ => {} + } + + text_input::focus(INPUT_ID.clone()) + } + Todos::Loaded(state) => { + let mut saved = false; + + let command = match message { + Message::InputChanged(value) => { + state.input_value = value; + + Task::none() + } + Message::CreateTask => { + if !state.input_value.is_empty() { + state + .tasks + .push(MyTask::new(state.input_value.clone())); + state.input_value.clear(); + } + Task::none() + } + Message::FilterChanged(filter) => { + state.filter = filter; + + Task::none() + } + Message::TaskMessage(i, TaskMessage::Delete) => { + state.tasks.remove(i); + + Task::none() + } + Message::TaskMessage(i, task_message) => { + if let Some(task) = state.tasks.get_mut(i) { + let should_focus = + matches!(task_message, TaskMessage::Edit); + + task.update(task_message); + + if should_focus { + let id = MyTask::text_input_id(i); + Task::batch(vec![ + text_input::focus(INPUT_ID.clone()), + text_input::select_all(INPUT_ID.clone()), + ]) + } else { + Task::none() + } + } else { + Task::none() + } + } + Message::Saved(_) => { + state.saving = false; + saved = true; + + Task::none() + } + Message::TabPressed { shift } => { + if shift { + widget::focus_previous() + } else { + widget::focus_next() + } + } + Message::CloseRequested(_) => { + std::process::exit(0); + } + _ => Task::none(), + }; + + if !saved { + state.dirty = true; + } + + let save = if state.dirty && !state.saving { + state.dirty = false; + state.saving = true; + + Task::perform( + SavedState { + input_value: state.input_value.clone(), + filter: state.filter, + tasks: state.tasks.clone(), + } + .save(), + Message::Saved, + ) + } else { + Task::none() + }; + + Task::batch(vec![command, save]) + } + } + } + + fn view(&self, id: window::Id) -> Element { + match self { + Todos::Loading => loading_message(), + Todos::Loaded(State { + input_value, + filter, + tasks, + window_id_ctr, + .. + }) => { + let title = text("todos") + .width(Length::Fill) + .size(100) + .color([0.5, 0.5, 0.5]); + + let input = text_input("What needs to be done?", input_value) + .id(INPUT_ID.clone()) + .padding(15) + .size(30) + .on_submit(Message::CreateTask) + .on_input(Message::InputChanged) + .on_paste(Message::InputChanged); + + let controls = view_controls(tasks, *filter); + let filtered_tasks = + tasks.iter().filter(|task| filter.matches(task)); + + let tasks: Element<_> = if filtered_tasks.count() > 0 { + column( + tasks + .iter() + .enumerate() + .filter(|(_, task)| filter.matches(task)) + .map(|(i, task)| { + task.view(i).map(move |message| { + Message::TaskMessage(i, message) + }) + }) + .collect::>(), + ) + .spacing(10) + .into() + } else { + empty_message(match filter { + Filter::All => "You have not created a task yet...", + Filter::Active => "All your tasks are done! :D", + Filter::Completed => { + "You have not completed a task yet..." + } + }) + }; + + let content = column![title, input, controls, tasks] + .spacing(20) + .max_width(800); + + scrollable( + container(content) + .width(Length::Fill) + .padding(40) + .center_x(Length::Fill), + ) + .into() + } + } + } + + fn subscription(&self) -> Subscription { + listen_raw(|event, status, window| { + // dbg!(&event); + match (event, status, window) { + ( + Event::Keyboard(keyboard::Event::KeyPressed { + key: keyboard::Key::Named(Named::Tab), + modifiers, + .. + }), + event::Status::Ignored, + _, + ) => Some(Message::TabPressed { + shift: modifiers.shift(), + }), + ( + Event::PlatformSpecific(event::PlatformSpecific::Wayland( + event::wayland::Event::Window(e), + )), + _, + _, + ) => { + dbg!(&e); + None + } + _ => None, + } + }) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct MyTask { + description: String, + completed: bool, + + #[serde(skip)] + state: TaskState, +} + +#[derive(Debug, Clone)] +pub enum TaskState { + Idle, + Editing, +} + +impl Default for TaskState { + fn default() -> Self { + Self::Idle + } +} + +#[derive(Debug, Clone)] +pub enum TaskMessage { + Completed(bool), + Edit, + DescriptionEdited(String), + FinishEdition, + Delete, +} + +impl MyTask { + fn text_input_id(i: usize) -> text_input::Id { + text_input::Id::new(format!("task-{}", i)) + } + + fn new(description: String) -> Self { + MyTask { + description, + completed: false, + state: TaskState::Idle, + } + } + + fn update(&mut self, message: TaskMessage) { + match message { + TaskMessage::Completed(completed) => { + self.completed = completed; + } + TaskMessage::Edit => { + self.state = TaskState::Editing; + } + TaskMessage::DescriptionEdited(new_description) => { + self.description = new_description; + } + TaskMessage::FinishEdition => { + if !self.description.is_empty() { + self.state = TaskState::Idle; + } + } + TaskMessage::Delete => {} + } + } + + fn view(&self, i: usize) -> Element { + match &self.state { + TaskState::Idle => { + let checkbox = checkbox(&self.description, self.completed) + .width(Length::Fill) + .on_toggle(TaskMessage::Completed); + + row![ + checkbox, + button(edit_icon()) + .on_press(TaskMessage::Edit) + .padding(10) + .style(button::text), + ] + .spacing(20) + .align_y(Alignment::Center) + .into() + } + TaskState::Editing => { + let text_input = + text_input("Describe your task...", &self.description) + .id(Self::text_input_id(i)) + .on_submit(TaskMessage::FinishEdition) + .on_input(TaskMessage::DescriptionEdited) + .on_paste(TaskMessage::DescriptionEdited) + .padding(10); + + row![ + text_input, + button(row![delete_icon(), "Delete"].spacing(10)) + .on_press(TaskMessage::Delete) + .padding(10) + .style(button::danger) + ] + .spacing(20) + .align_y(Alignment::Center) + .into() + } + } + } +} + +fn view_controls(tasks: &[MyTask], current_filter: Filter) -> Element { + let tasks_left = tasks.iter().filter(|task| !task.completed).count(); + + let filter_button = |label, filter, current_filter| { + let label = text(label).size(16); + + let button = button(label).style(if filter == current_filter { + button::primary + } else { + button::text + }); + + button.on_press(Message::FilterChanged(filter)).padding(8) + }; + + row![ + text(format!( + "{} {} left", + tasks_left, + if tasks_left == 1 { "task" } else { "tasks" } + )) + .width(Length::Fill) + .size(16), + row![ + filter_button("All", Filter::All, current_filter), + filter_button("Active", Filter::Active, current_filter), + filter_button("Completed", Filter::Completed, current_filter,), + ] + .width(Length::Shrink) + .spacing(10) + ] + .spacing(20) + .align_y(Alignment::Center) + .into() +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum Filter { + All, + Active, + Completed, +} + +impl Default for Filter { + fn default() -> Self { + Filter::All + } +} + +impl Filter { + fn matches(&self, task: &MyTask) -> bool { + match self { + Filter::All => true, + Filter::Active => !task.completed, + Filter::Completed => task.completed, + } + } +} + +fn loading_message<'a>() -> Element<'a, Message> { + container(text("Loading...").size(50)) + .width(Length::Fill) + .height(Length::Fill) + .center_y(Length::Fill) + .into() +} + +fn empty_message(message: &str) -> Element<'_, Message> { + container( + text(message) + .width(Length::Fill) + .size(25) + .color([0.7, 0.7, 0.7]), + ) + .width(Length::Fill) + .height(Length::Fixed(200.0)) + .center_y(Length::Fill) + .into() +} + +// Fonts +const ICONS: Font = Font::with_name("Iced-Todos-Icons"); + +fn icon(unicode: char) -> Text<'static> { + text(unicode.to_string()) + .font(ICONS) + .width(Length::Fixed(20.0)) + .size(20) +} + +fn edit_icon() -> Text<'static> { + icon('\u{F303}') +} + +fn delete_icon() -> Text<'static> { + icon('\u{F1F8}') +} + +// Persistence +#[derive(Debug, Clone, Serialize, Deserialize)] +struct SavedState { + input_value: String, + filter: Filter, + tasks: Vec, +} + +#[derive(Debug, Clone)] +enum LoadError { + File, + Format, +} + +#[derive(Debug, Clone)] +enum SaveError { + File, + Write, + Format, +} + +#[cfg(not(target_arch = "wasm32"))] +impl SavedState { + fn path() -> std::path::PathBuf { + let mut path = if let Some(project_dirs) = + directories_next::ProjectDirs::from("rs", "Iced", "Todos") + { + project_dirs.data_dir().into() + } else { + std::env::current_dir().unwrap_or_default() + }; + + path.push("todos.json"); + + path + } + + async fn load() -> Result { + use async_std::prelude::*; + + let mut contents = String::new(); + + let mut file = async_std::fs::File::open(Self::path()) + .await + .map_err(|_| LoadError::File)?; + + file.read_to_string(&mut contents) + .await + .map_err(|_| LoadError::File)?; + + serde_json::from_str(&contents).map_err(|_| LoadError::Format) + } + + async fn save(self) -> Result<(), SaveError> { + use async_std::prelude::*; + + let json = serde_json::to_string_pretty(&self) + .map_err(|_| SaveError::Format)?; + + let path = Self::path(); + + if let Some(dir) = path.parent() { + async_std::fs::create_dir_all(dir) + .await + .map_err(|_| SaveError::File)?; + } + + { + let mut file = async_std::fs::File::create(path) + .await + .map_err(|_| SaveError::File)?; + + file.write_all(json.as_bytes()) + .await + .map_err(|_| SaveError::Write)?; + } + + // This is a simple way to save at most once every couple seconds + async_std::task::sleep(std::time::Duration::from_secs(2)).await; + + Ok(()) + } +} + +#[cfg(target_arch = "wasm32")] +impl SavedState { + fn storage() -> Option { + let window = web_sys::window()?; + + window.local_storage().ok()? + } + + async fn load() -> Result { + let storage = Self::storage().ok_or(LoadError::File)?; + + let contents = storage + .get_item("state") + .map_err(|_| LoadError::File)? + .ok_or(LoadError::File)?; + + serde_json::from_str(&contents).map_err(|_| LoadError::Format) + } + + async fn save(self) -> Result<(), SaveError> { + let storage = Self::storage().ok_or(SaveError::File)?; + + let json = serde_json::to_string_pretty(&self) + .map_err(|_| SaveError::Format)?; + + storage + .set_item("state", &json) + .map_err(|_| SaveError::Write)?; + + let _ = wasm_timer::Delay::new(std::time::Duration::from_secs(2)).await; + + Ok(()) + } +} diff --git a/examples/svg/Cargo.toml b/examples/svg/Cargo.toml index 78208fb0b0..4be61237af 100644 --- a/examples/svg/Cargo.toml +++ b/examples/svg/Cargo.toml @@ -7,4 +7,4 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["svg"] +iced.features = ["svg", "winit", "tokio"] diff --git a/examples/system_information/Cargo.toml b/examples/system_information/Cargo.toml deleted file mode 100644 index 419031227e..0000000000 --- a/examples/system_information/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "system_information" -version = "0.1.0" -authors = ["Richard "] -edition = "2021" -publish = false - -[dependencies] -iced.workspace = true -iced.features = ["system"] - -bytesize = "1.1" diff --git a/examples/system_information/src/main.rs b/examples/system_information/src/main.rs deleted file mode 100644 index 363df59000..0000000000 --- a/examples/system_information/src/main.rs +++ /dev/null @@ -1,142 +0,0 @@ -use iced::widget::{button, center, column, text}; -use iced::{system, Element, Task}; - -pub fn main() -> iced::Result { - iced::application( - "System Information - Iced", - Example::update, - Example::view, - ) - .run() -} - -#[derive(Default)] -#[allow(clippy::large_enum_variant)] -enum Example { - #[default] - Loading, - Loaded { - information: system::Information, - }, -} - -#[derive(Clone, Debug)] -#[allow(clippy::large_enum_variant)] -enum Message { - InformationReceived(system::Information), - Refresh, -} - -impl Example { - fn update(&mut self, message: Message) -> Task { - match message { - Message::Refresh => { - *self = Self::Loading; - - return system::fetch_information() - .map(Message::InformationReceived); - } - Message::InformationReceived(information) => { - *self = Self::Loaded { information }; - } - } - - Task::none() - } - - fn view(&self) -> Element { - use bytesize::ByteSize; - - let content: Element<_> = match self { - Example::Loading => text("Loading...").size(40).into(), - Example::Loaded { information } => { - let system_name = text!( - "System name: {}", - information - .system_name - .as_ref() - .unwrap_or(&"unknown".to_string()) - ); - - let system_kernel = text!( - "System kernel: {}", - information - .system_kernel - .as_ref() - .unwrap_or(&"unknown".to_string()) - ); - - let system_version = text!( - "System version: {}", - information - .system_version - .as_ref() - .unwrap_or(&"unknown".to_string()) - ); - - let system_short_version = text!( - "System short version: {}", - information - .system_short_version - .as_ref() - .unwrap_or(&"unknown".to_string()) - ); - - let cpu_brand = - text!("Processor brand: {}", information.cpu_brand); - - let cpu_cores = text!( - "Processor cores: {}", - information - .cpu_cores - .map_or("unknown".to_string(), |cores| cores - .to_string()) - ); - - let memory_readable = - ByteSize::b(information.memory_total).to_string(); - - let memory_total = text!( - "Memory (total): {} bytes ({memory_readable})", - information.memory_total, - ); - - let memory_text = if let Some(memory_used) = - information.memory_used - { - let memory_readable = ByteSize::b(memory_used).to_string(); - - format!("{memory_used} bytes ({memory_readable})") - } else { - String::from("None") - }; - - let memory_used = text!("Memory (used): {memory_text}"); - - let graphics_adapter = - text!("Graphics adapter: {}", information.graphics_adapter); - - let graphics_backend = - text!("Graphics backend: {}", information.graphics_backend); - - column![ - system_name.size(30), - system_kernel.size(30), - system_version.size(30), - system_short_version.size(30), - cpu_brand.size(30), - cpu_cores.size(30), - memory_total.size(30), - memory_used.size(30), - graphics_adapter.size(30), - graphics_backend.size(30), - button("Refresh").on_press(Message::Refresh) - ] - .spacing(10) - .into() - } - }; - - center(content).into() - } -} diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 8f6a836e50..7b19aee0bd 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -167,7 +167,9 @@ mod toast { use iced::advanced::layout::{self, Layout}; use iced::advanced::overlay; use iced::advanced::renderer; - use iced::advanced::widget::{self, Operation, Tree}; + use iced::advanced::widget::{ + self, Operation, OperationOutputWrapper, Tree, + }; use iced::advanced::{Clipboard, Shell, Widget}; use iced::event::{self, Event}; use iced::mouse; @@ -315,7 +317,7 @@ mod toast { .collect() } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { let instants = tree.state.downcast_mut::>>(); // Invalidating removed instants to None allows us to remove @@ -336,8 +338,8 @@ mod toast { } tree.diff_children( - &std::iter::once(&self.content) - .chain(self.toasts.iter()) + &mut std::iter::once(&mut self.content) + .chain(self.toasts.iter_mut()) .collect::>(), ); } diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml index 0d72be8695..b67dfb41c7 100644 --- a/examples/todos/Cargo.toml +++ b/examples/todos/Cargo.toml @@ -7,7 +7,19 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["async-std", "debug"] +iced_core.workspace = true +# iced.features = ["async-std", "debug", "winit", "a11y", "tiny-skia"] +# TODO(POP): Fix a11y not working with new winit +iced.features = [ + "a11y", + "tokio", + "debug", + "winit", + "tiny-skia", + "wgpu", + "svg", + "web-colors", +] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" @@ -28,6 +40,14 @@ wasm-timer.workspace = true [package.metadata.deb] assets = [ - ["target/release-opt/todos", "usr/bin/iced-todos", "755"], - ["iced-todos.desktop", "usr/share/applications/", "644"], + [ + "target/release-opt/todos", + "usr/bin/iced-todos", + "755", + ], + [ + "iced-todos.desktop", + "usr/share/applications/", + "644", + ], ] diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 25e3ead2a6..7ad83ec83e 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -1,10 +1,18 @@ -use iced::keyboard; +use std::{borrow::Cow, convert::Infallible}; + +use iced::alignment::{self, Alignment}; +use iced::clipboard; +use iced::clipboard::mime::{AllowedMimeTypes, AsMimeTypes}; +use iced::font::{self, Font}; +use iced::futures::task; +use iced::keyboard::{self, Modifiers}; use iced::widget::{ self, button, center, checkbox, column, container, keyed_column, row, scrollable, text, text_input, Text, }; use iced::window; -use iced::{Center, Element, Fill, Font, Subscription, Task as Command}; +use iced::{Center, Element, Fill, Length, Subscription, Task as Command}; +use iced_core::widget::Id; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -45,6 +53,37 @@ enum Message { TaskMessage(usize, TaskMessage), TabPressed { shift: bool }, ToggleFullscreen(window::Mode), + Dbg(Option), +} + +pub struct MyText(pub String); +impl AllowedMimeTypes for MyText { + fn allowed() -> std::borrow::Cow<'static, [String]> { + Cow::Owned(vec!["text/plain".to_string()].into()) + } +} + +impl TryFrom<(Vec, String)> for MyText { + type Error = Infallible; + + fn try_from(value: (Vec, String)) -> Result { + Ok(MyText( + String::from_utf8_lossy(value.0.as_ref()).to_string(), + )) + } +} + +impl AsMimeTypes for MyText { + fn available(&self) -> std::borrow::Cow<'static, [String]> { + Cow::Owned(vec!["text/plain".to_string()]) + } + + fn as_bytes( + &self, + mime_type: &str, + ) -> Option> { + (mime_type == "text/plain").then(|| self.0.clone().into_bytes().into()) + } } impl Todos { @@ -89,9 +128,12 @@ impl Todos { let command = match message { Message::InputChanged(value) => { - state.input_value = value; - - Command::none() + state.input_value = value.clone(); + Command::batch(vec![ + clipboard::write_data(MyText(value)), + clipboard::read_data() + .map(|s| Message::Dbg(s.map(|s: MyText| s.0))), + ]) } Message::CreateTask => { if !state.input_value.is_empty() { @@ -103,6 +145,10 @@ impl Todos { Command::none() } + Message::Dbg(s) => { + dbg!(s); + Command::none() + } Message::FilterChanged(filter) => { state.filter = filter; @@ -180,6 +226,9 @@ impl Todos { } fn view(&self) -> Element { + // row![ + // button("Press me").on_press(Message::ToggleFullscreen(window::Mode::Fullscreen)) + // ].into() match self { Todos::Loading => loading_message(), Todos::Loaded(State { @@ -232,8 +281,9 @@ impl Todos { } }) }; + let test = row![container(text("0000 0000 00000 000000000000 000000000000000 00000 0000 00000000 000000 000000000 l00000")).width(Length::Fill), container(text("a")).width(Length::Fixed(100.0))]; - let content = column![title, input, controls, tasks] + let content = column![title, input, controls, tasks, test] .spacing(20) .max_width(800); diff --git a/examples/tooltip/Cargo.toml b/examples/tooltip/Cargo.toml index 57bb0dcb44..a587cdd200 100644 --- a/examples/tooltip/Cargo.toml +++ b/examples/tooltip/Cargo.toml @@ -7,4 +7,4 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["debug"] +iced.features = ["debug", "winit", "tiny-skia"] diff --git a/examples/tour/Cargo.toml b/examples/tour/Cargo.toml index 9e984ad124..266b4fcd9c 100644 --- a/examples/tour/Cargo.toml +++ b/examples/tour/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["image", "debug"] +iced.features = ["image", "debug", "winit"] [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tracing-subscriber = "0.3" diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index 8b1efb4125..1a19bd8ea1 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -11,6 +11,7 @@ pub fn main() -> iced::Result { .subscription(WebSocket::subscription) .run_with(WebSocket::new) } +use iced::id::Id; struct WebSocket { messages: Vec, diff --git a/futures/Cargo.toml b/futures/Cargo.toml index a6fcfde13b..1d09a42533 100644 --- a/futures/Cargo.toml +++ b/futures/Cargo.toml @@ -19,6 +19,7 @@ all-features = true [features] thread-pool = ["futures/thread-pool"] +a11y = ["iced_core/a11y"] [dependencies] iced_core.workspace = true diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs index de0a312ecf..2cd7a7971b 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -323,6 +323,7 @@ struct Map where F: Fn(A) -> B + 'static, { + id: TypeId, recipe: Box>, mapper: F, } @@ -332,7 +333,11 @@ where F: Fn(A) -> B + 'static, { fn new(recipe: Box>, mapper: F) -> Self { - Map { recipe, mapper } + Map { + id: TypeId::of::(), + recipe, + mapper, + } } } @@ -345,7 +350,7 @@ where type Output = B; fn hash(&self, state: &mut Hasher) { - TypeId::of::().hash(state); + self.id.hash(state); self.recipe.hash(state); } diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs index 3026bead46..d4db344377 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -136,6 +136,18 @@ pub enum SurfaceError { /// There is no more memory left to allocate a new frame. #[error("There is no more memory left to allocate a new frame")] OutOfMemory, + /// Resize Error + #[error("Resize Error")] + Resize, + /// Invalid dimensions + #[error("Invalid dimensions")] + InvalidDimensions, + /// Present Error + #[error("Present Error")] + Present(String), + /// Present Error + #[error("No damage to present")] + NoDamage, } /// Contains information about the graphics (e.g. graphics adapter, graphics backend). diff --git a/graphics/src/geometry/text.rs b/graphics/src/geometry/text.rs index 90147f8796..9cadf71763 100644 --- a/graphics/src/geometry/text.rs +++ b/graphics/src/geometry/text.rs @@ -54,7 +54,7 @@ impl Text { None, cosmic_text::Wrap::None, None, - 4, + 8, ); let translation_x = match self.horizontal_alignment { @@ -174,12 +174,12 @@ impl Default for Text { content: String::new(), position: Point::ORIGIN, color: Color::BLACK, - size: Pixels(16.0), - line_height: LineHeight::Relative(1.2), + size: Pixels(14.0), + line_height: LineHeight::default(), font: Font::default(), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, - shaping: Shaping::Basic, + shaping: Shaping::Advanced, } } } diff --git a/graphics/src/image.rs b/graphics/src/image.rs index 67a5e0cf35..fcafb27aa0 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -4,24 +4,39 @@ pub use ::image as image_rs; use crate::core::image; use crate::core::svg; +use crate::core::Color; +use crate::core::Radians; use crate::core::Rectangle; /// A raster or vector image. #[derive(Debug, Clone, PartialEq)] pub enum Image { /// A raster image. - Raster(image::Image, Rectangle), + Raster { + /// The image handle + handle: image::Image, + /// The bounds of the image. + bounds: Rectangle, + }, /// A vector image. - Vector(svg::Svg, Rectangle), + Vector { + /// The handle of a vector image. + handle: svg::Svg, + + /// The bounds of the image. + bounds: Rectangle, + }, } impl Image { /// Returns the bounds of the [`Image`]. pub fn bounds(&self) -> Rectangle { match self { - Image::Raster(image, bounds) => bounds.rotate(image.rotation), - Image::Vector(svg, bounds) => bounds.rotate(svg.rotation), + Image::Raster { handle, bounds } => bounds.rotate(handle.rotation), + Image::Vector { handle, bounds, .. } => { + bounds.rotate(handle.rotation) + } } } } @@ -85,7 +100,9 @@ pub fn load( let (width, height, pixels) = match handle { image::Handle::Path(_, path) => { - let image = ::image::open(path)?; + let image = ::image::io::Reader::open(&path)? + .with_guessed_format()? + .decode()?; let operation = std::fs::File::open(path) .ok() diff --git a/graphics/src/settings.rs b/graphics/src/settings.rs index 2e8275c699..02a81947ac 100644 --- a/graphics/src/settings.rs +++ b/graphics/src/settings.rs @@ -9,7 +9,7 @@ pub struct Settings { /// The default size of text. /// - /// By default, it will be set to `16.0`. + /// By default, it will be set to `14.0`. pub default_text_size: Pixels, /// The antialiasing strategy that will be used for triangle primitives. @@ -22,7 +22,7 @@ impl Default for Settings { fn default() -> Settings { Settings { default_font: Font::default(), - default_text_size: Pixels(16.0), + default_text_size: Pixels(14.0), antialiasing: None, } } diff --git a/graphics/src/text.rs b/graphics/src/text.rs index feb9932a81..55c2385b6f 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -238,8 +238,15 @@ pub fn measure(buffer: &cosmic_text::Buffer) -> Size { .fold((0.0, 0.0), |(width, height), run| { (run.line_w.max(width), height + run.line_height) }); - Size::new(width, height) + + // let (max_width_opt, max_height_opt) = buffer.size(); + + // Size::new( + // width.min(max_width_opt.unwrap_or(f32::MAX)), + // (buffer.total_lines as f32 * buffer.metrics().line_height) + // .min(max_height_opt.unwrap_or(f32::MAX)), + // ) } /// Returns the attributes of the given [`Font`]. diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 1f1d0050c8..42bb8dce90 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -166,10 +166,8 @@ impl editor::Editor for Editor { .get(cursor.line) .expect("Cursor line should be present"); - let layout = line - .layout_opt() - .as_ref() - .expect("Line layout should be cached"); + let layout = + line.layout_opt().expect("Line layout should be cached"); let mut lines = layout.iter().enumerate(); @@ -674,11 +672,7 @@ fn highlight_line( from: usize, to: usize, ) -> impl Iterator + '_ { - let layout = line - .layout_opt() - .as_ref() - .map(Vec::as_slice) - .unwrap_or_default(); + let layout = line.layout_opt().map(Vec::as_slice).unwrap_or_default(); layout.iter().map(move |visual_line| { let start = visual_line @@ -729,9 +723,7 @@ fn visual_lines_offset(line: usize, buffer: &cosmic_text::Buffer) -> i32 { let visual_lines_offset: usize = buffer.lines[start..] .iter() .take(end - start) - .map(|line| { - line.layout_opt().as_ref().map(Vec::len).unwrap_or_default() - }) + .map(|line| line.layout_opt().map(Vec::len).unwrap_or_default()) .sum(); visual_lines_offset as i32 * if scroll.line < line { 1 } else { -1 } diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index 07ddbb821a..f90282c637 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -1,8 +1,8 @@ //! Draw paragraphs. use crate::core; use crate::core::alignment; -use crate::core::text::{Hit, Shaping, Span, Text, Wrapping}; -use crate::core::{Font, Point, Rectangle, Size}; +use crate::core::text::{Hit, LineHeight, Shaping, Span, Text, Wrapping}; +use crate::core::{Font, Pixels, Point, Rectangle, Size}; use crate::text; use std::fmt; @@ -80,6 +80,8 @@ impl core::text::Paragraph for Paragraph { Some(text.bounds.height), ); + buffer.set_wrap(font_system.raw(), text::to_wrap(text.wrapping)); + buffer.set_text( font_system.raw(), text.content, @@ -152,6 +154,11 @@ impl core::text::Paragraph for Paragraph { }), text::to_attributes(text.font), text::to_shaping(text.shaping), + Some(match text.horizontal_alignment { + alignment::Horizontal::Left => cosmic_text::Align::Left, + alignment::Horizontal::Center => cosmic_text::Align::Center, + alignment::Horizontal::Right => cosmic_text::Align::Right, + }), ); let min_bounds = text::measure(&buffer); diff --git a/graphics/src/viewport.rs b/graphics/src/viewport.rs index dc8e21d327..c30d8b05fb 100644 --- a/graphics/src/viewport.rs +++ b/graphics/src/viewport.rs @@ -10,6 +10,23 @@ pub struct Viewport { } impl Viewport { + /// Creates a new [`Viewport`] with the given logical dimensions and scale factor + pub fn with_logical_size(size: Size, scale_factor: f64) -> Viewport { + let physical_size = Size::new( + (size.width as f64 * scale_factor).ceil() as u32, + (size.height as f64 * scale_factor).ceil() as u32, + ); + Viewport { + physical_size, + logical_size: size, + scale_factor, + projection: Transformation::orthographic( + physical_size.width, + physical_size.height, + ), + } + } + /// Creates a new [`Viewport`] with the given physical dimensions and scale /// factor. pub fn with_physical_size(size: Size, scale_factor: f64) -> Viewport { diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml index ac223f16e2..11f7be1d1c 100644 --- a/renderer/Cargo.toml +++ b/renderer/Cargo.toml @@ -14,6 +14,7 @@ keywords.workspace = true workspace = true [features] +default = [] wgpu = ["iced_wgpu"] tiny-skia = ["iced_tiny_skia"] image = ["iced_tiny_skia?/image", "iced_wgpu?/image"] diff --git a/renderer/src/compositor.rs b/renderer/src/compositor.rs deleted file mode 100644 index 8b13789179..0000000000 --- a/renderer/src/compositor.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index 8cb18bdea0..f940f15833 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -77,11 +77,13 @@ where Font = A::Font, Paragraph = A::Paragraph, Editor = A::Editor, + Raw = A::Raw, >, { type Font = A::Font; type Paragraph = A::Paragraph; type Editor = A::Editor; + type Raw = A::Raw; const ICON_FONT: Self::Font = A::ICON_FONT; const CHECKMARK_ICON: char = A::CHECKMARK_ICON; @@ -123,6 +125,10 @@ where ); } + fn fill_raw(&mut self, raw: Self::Raw) { + delegate!(self, renderer, renderer.fill_raw(raw)); + } + fn fill_text( &mut self, text: core::Text, @@ -149,8 +155,27 @@ where delegate!(self, renderer, renderer.measure_image(handle)) } - fn draw_image(&mut self, image: Image, bounds: Rectangle) { - delegate!(self, renderer, renderer.draw_image(image, bounds)); + fn draw_image( + &mut self, + handle: Self::Handle, + filter_method: image::FilterMethod, + bounds: Rectangle, + rotation: crate::core::Radians, + opacity: f32, + border_radius: [f32; 4], + ) { + delegate!( + self, + renderer, + renderer.draw_image( + handle, + filter_method, + bounds, + rotation, + opacity, + border_radius, + ) + ); } } diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 703c3ed955..c4c60c254f 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -16,12 +16,19 @@ workspace = true [features] debug = [] multi-window = [] +a11y = ["iced_accessibility", "iced_core/a11y"] +wayland = ["iced_core/wayland", "sctk"] [dependencies] bytes.workspace = true iced_core.workspace = true iced_futures.workspace = true iced_futures.features = ["thread-pool"] - +sctk.workspace = true +sctk.optional = true thiserror.workspace = true raw-window-handle.workspace = true +iced_accessibility.workspace = true +iced_accessibility.optional = true +window_clipboard.workspace = true +dnd.workspace = true diff --git a/runtime/src/clipboard.rs b/runtime/src/clipboard.rs index a02cc011cf..6c0c92db88 100644 --- a/runtime/src/clipboard.rs +++ b/runtime/src/clipboard.rs @@ -1,4 +1,6 @@ //! Access the clipboard. +use window_clipboard::mime::{AllowedMimeTypes, AsMimeTypes}; + use crate::core::clipboard::Kind; use crate::futures::futures::channel::oneshot; use crate::task::{self, Task}; @@ -6,9 +8,8 @@ use crate::task::{self, Task}; /// A clipboard action to be performed by some [`Task`]. /// /// [`Task`]: crate::Task -#[derive(Debug)] pub enum Action { - /// Read the clipboard and produce `T` with the result. + /// Read the clipboard and produce `String` with the result. Read { /// The clipboard target. target: Kind, @@ -23,6 +24,40 @@ pub enum Action { /// The contents to be written. contents: String, }, + + /// Write the given contents to the clipboard. + WriteData(Box, Kind), + + #[allow(clippy::type_complexity)] + /// Read the clipboard and produce `T` with the result. + ReadData( + Vec, + oneshot::Sender, String)>>, + // Box, String)>) -> T + Send + 'static>, + Kind, + ), +} + +impl std::fmt::Debug for Action { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Read { channel: _, target } => { + write!(f, "Action::Read{target:?}") + } + Self::Write { + contents: _, + target, + } => { + write!(f, "Action::Write({target:?})") + } + Self::WriteData(_, target) => { + write!(f, "Action::WriteData({target:?})") + } + Self::ReadData(_, _, target) => { + write!(f, "Action::ReadData({target:?})") + } + } + } } /// Read the current contents of the clipboard. @@ -60,3 +95,47 @@ pub fn write_primary(contents: String) -> Task { contents, })) } +/// Read the current contents of the clipboard. +pub fn read_data() -> Task> { + task::oneshot(|tx| { + crate::Action::Clipboard(Action::ReadData( + T::allowed().into(), + tx, + Kind::Standard, + )) + }) + .map(|d| d.and_then(|d| T::try_from(d).ok())) +} + +/// Write the given contents to the clipboard. +pub fn write_data( + contents: impl AsMimeTypes + std::marker::Sync + std::marker::Send + 'static, +) -> Task { + task::effect(crate::Action::Clipboard(Action::WriteData( + Box::new(contents), + Kind::Standard, + ))) +} + +/// Read from the primary clipboard +pub fn read_primary_data() -> Task> { + task::oneshot(|tx| { + crate::Action::Clipboard(Action::ReadData( + T::allowed().into(), + tx, + // Box::new(move |d| f(d.and_then(|d| T::try_from(d).ok()))), + Kind::Primary, + )) + }) + .map(|d| d.and_then(|d| T::try_from(d).ok())) +} + +/// Write the given contents to the clipboard. +pub fn write_primary_data( + contents: impl AsMimeTypes + std::marker::Sync + std::marker::Send + 'static, +) -> Task { + task::effect(crate::Action::Clipboard(Action::WriteData( + Box::new(contents), + Kind::Primary, + ))) +} diff --git a/runtime/src/dnd.rs b/runtime/src/dnd.rs new file mode 100644 index 0000000000..2fd3924499 --- /dev/null +++ b/runtime/src/dnd.rs @@ -0,0 +1,129 @@ +//! Access the clipboard. + +use std::any::Any; + +use bytes::buf::Take; +use dnd::{DndDestinationRectangle, DndSurface}; +use iced_core::{clipboard::DndSource, Vector}; +use iced_futures::MaybeSend; +use window_clipboard::mime::{AllowedMimeTypes, AsMimeTypes}; + +use crate::{oneshot, task, Action, Task}; + +/// An action to be performed on the system. +pub enum DndAction { + /// Register a Dnd destination. + RegisterDndDestination { + /// The surface to register. + surface: DndSurface, + /// The rectangles to register. + rectangles: Vec, + }, + /// Start a Dnd operation. + StartDnd { + /// Whether the Dnd operation is internal. + internal: bool, + /// The source surface of the Dnd operation. + source_surface: Option, + /// The icon surface of the Dnd operation. + icon_surface: Option>, + /// The content of the Dnd operation. + content: Box, + /// The actions of the Dnd operation. + actions: dnd::DndAction, + }, + /// End a Dnd operation. + EndDnd, + /// Peek the current Dnd operation. + PeekDnd( + String, + oneshot::Sender, String)>>, + // Box, String)>) -> T + Send + 'static>, + ), + /// Set the action of the Dnd operation. + SetAction(dnd::DndAction), +} + +impl std::fmt::Debug for DndAction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::RegisterDndDestination { + surface, + rectangles, + } => f + .debug_struct("RegisterDndDestination") + .field("surface", surface) + .field("rectangles", rectangles) + .finish(), + Self::StartDnd { + internal, + source_surface, + icon_surface, + content: _, + actions, + } => f + .debug_struct("StartDnd") + .field("internal", internal) + .field("source_surface", source_surface) + .field("icon_surface", icon_surface) + .field("actions", actions) + .finish(), + Self::EndDnd => f.write_str("EndDnd"), + Self::PeekDnd(mime, _) => { + f.debug_struct("PeekDnd").field("mime", mime).finish() + } + Self::SetAction(a) => f.debug_tuple("SetAction").field(a).finish(), + } + } +} + +/// Read the current contents of the Dnd operation. +pub fn peek_dnd() -> Task> { + task::oneshot(|tx| { + Action::Dnd(DndAction::PeekDnd( + T::allowed() + .get(0) + .map_or_else(|| String::new(), |s| s.to_string()), + tx, + )) + }) + .map(|data| data.and_then(|data| T::try_from(data).ok())) +} + +/// Register a Dnd destination. +pub fn register_dnd_destination( + surface: DndSurface, + rectangles: Vec, +) -> Task { + task::effect(Action::Dnd(DndAction::RegisterDndDestination { + surface, + rectangles, + })) +} + +/// Start a Dnd operation. +pub fn start_dnd( + internal: bool, + source_surface: Option, + icon_surface: Option>, + content: Box, + actions: dnd::DndAction, +) -> Task { + task::effect(Action::Dnd(DndAction::StartDnd { + internal, + source_surface, + icon_surface, + content, + actions, + })) +} + +/// End a Dnd operation. +pub fn end_dnd() -> Task { + task::effect(Action::Dnd(DndAction::EndDnd)) +} + +/// Set the action of the Dnd operation. +pub fn set_action(a: dnd::DndAction) -> Task { + task::effect(Action::Dnd(DndAction::SetAction(a))) +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index ae6d1dce82..8516561f90 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -10,9 +10,11 @@ )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] pub mod clipboard; +pub mod dnd; pub mod font; pub mod keyboard; pub mod overlay; +pub mod platform_specific; pub mod program; pub mod system; pub mod task; @@ -41,6 +43,7 @@ pub use user_interface::UserInterface; use crate::core::widget; use crate::futures::futures::channel::oneshot; +use dnd::DndAction; use std::borrow::Cow; use std::fmt; @@ -75,6 +78,11 @@ pub enum Action { /// This will normally close any application windows and /// terminate the runtime loop. Exit, + /// Run a Dnd action. + Dnd(crate::dnd::DndAction), + + /// Run a platform specific action + PlatformSpecific(crate::platform_specific::Action), } impl Action { @@ -94,6 +102,8 @@ impl Action { Action::Window(action) => Err(Action::Window(action)), Action::System(action) => Err(Action::System(action)), Action::Exit => Err(Action::Exit), + Action::Dnd(a) => Err(Action::Dnd(a)), + Action::PlatformSpecific(a) => Err(Action::PlatformSpecific(a)), } } } @@ -117,6 +127,10 @@ where Action::Window(_) => write!(f, "Action::Window"), Action::System(action) => write!(f, "Action::System({action:?})"), Action::Exit => write!(f, "Action::Exit"), + Action::PlatformSpecific(action) => { + write!(f, "Action::PlatformSpecific({:?})", action) + } + Action::Dnd(action) => write!(f, "Action::Dnd"), } } } @@ -128,3 +142,39 @@ where pub fn exit() -> Task { task::effect(Action::Exit) } + +/// The appearance of a program. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Appearance { + /// The background [`Color`] of the application. + pub background_color: iced_core::Color, + + /// The default text [`Color`] of the application. + pub text_color: iced_core::Color, + + /// The default icon [`Color`] of the application. + pub icon_color: iced_core::Color, +} + +/// The default style of a [`Program`]. +pub trait DefaultStyle { + /// Returns the default style of a [`Program`]. + fn default_style(&self) -> Appearance; +} + +impl DefaultStyle for iced_core::Theme { + fn default_style(&self) -> Appearance { + default(self) + } +} + +/// The default [`Appearance`] of a [`Program`] with the built-in [`Theme`]. +pub fn default(theme: &iced_core::Theme) -> Appearance { + let palette = theme.extended_palette(); + + Appearance { + background_color: palette.background.base.color, + text_color: palette.background.base.text, + icon_color: palette.background.base.text, + } +} diff --git a/runtime/src/multi_window/state.rs b/runtime/src/multi_window/state.rs index 0bec555f0e..4cb8d7cca3 100644 --- a/runtime/src/multi_window/state.rs +++ b/runtime/src/multi_window/state.rs @@ -231,6 +231,7 @@ where operation::Outcome::Chain(next) => { current_operation = Some(next); } + _ => {} }; } } diff --git a/runtime/src/platform_specific/mod.rs b/runtime/src/platform_specific/mod.rs new file mode 100644 index 0000000000..a8f761b75a --- /dev/null +++ b/runtime/src/platform_specific/mod.rs @@ -0,0 +1,26 @@ +//! Platform specific actions defined for wayland + +use std::{fmt, marker::PhantomData}; + +use iced_futures::MaybeSend; + +#[cfg(feature = "wayland")] +/// Platform specific actions defined for wayland +pub mod wayland; + +/// Platform specific actions defined for wayland +pub enum Action { + /// Wayland Specific Actions + #[cfg(feature = "wayland")] + Wayland(wayland::Action), +} + +impl fmt::Debug for Action { + fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + #[cfg(feature = "wayland")] + Action::Wayland(action) => action.fmt(_f), + _ => Ok(()), + } + } +} diff --git a/runtime/src/platform_specific/wayland/activation.rs b/runtime/src/platform_specific/wayland/activation.rs new file mode 100644 index 0000000000..64d4dc4dc4 --- /dev/null +++ b/runtime/src/platform_specific/wayland/activation.rs @@ -0,0 +1,42 @@ +use iced_core::window::Id; + +use std::fmt; + +use crate::oneshot; + +/// xdg-activation Actions +pub enum Action { + /// request an activation token + RequestToken { + /// application id + app_id: Option, + /// window, if provided + window: Option, + /// message generation + channel: oneshot::Sender>, + }, + /// request a window to be activated + Activate { + /// window to activate + window: Id, + /// activation token + token: String, + }, +} + +impl fmt::Debug for Action { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Action::RequestToken { app_id, window, .. } => write!( + f, + "Action::ActivationAction::RequestToken {{ app_id: {:?}, window: {:?} }}", + app_id, window, + ), + Action::Activate { window, token } => write!( + f, + "Action::ActivationAction::Activate {{ window: {:?}, token: {:?} }}", + window, token, + ) + } + } +} diff --git a/runtime/src/platform_specific/wayland/layer_surface.rs b/runtime/src/platform_specific/wayland/layer_surface.rs new file mode 100644 index 0000000000..1629a463bd --- /dev/null +++ b/runtime/src/platform_specific/wayland/layer_surface.rs @@ -0,0 +1,185 @@ +use std::fmt; + +use iced_core::layout::Limits; +use sctk::{ + reexports::client::protocol::wl_output::WlOutput, + shell::wlr_layer::{Anchor, KeyboardInteractivity, Layer}, +}; + +use iced_core::window::Id; + +/// output for layer surface +#[derive(Debug, Clone)] +pub enum IcedOutput { + /// show on all outputs + All, + /// show on active output + Active, + /// show on a specific output + Output(WlOutput), +} + +impl Default for IcedOutput { + fn default() -> Self { + Self::Active + } +} + +/// margins of the layer surface +#[derive(Debug, Clone, Copy, Default)] +pub struct IcedMargin { + /// top + pub top: i32, + /// right + pub right: i32, + /// bottom + pub bottom: i32, + /// left + pub left: i32, +} + +/// layer surface +#[derive(Debug, Clone)] +pub struct SctkLayerSurfaceSettings { + /// XXX id must be unique for every surface, window, and popup + pub id: Id, + /// layer + pub layer: Layer, + /// keyboard interactivity + pub keyboard_interactivity: KeyboardInteractivity, + /// pointer interactivity + pub pointer_interactivity: bool, + /// anchor, if a surface is anchored to two opposite edges, it will be stretched to fit between those edges, regardless of the specified size in that dimension. + pub anchor: Anchor, + /// output + pub output: IcedOutput, + /// namespace + pub namespace: String, + /// margin + pub margin: IcedMargin, + /// XXX size, providing None will autosize the layer surface to its contents + /// If Some size is provided, None in a given dimension lets the compositor decide for that dimension, usually this would be done with a layer surface that is anchored to left & right or top & bottom + pub size: Option<(Option, Option)>, + /// exclusive zone + pub exclusive_zone: i32, + /// Limits of the popup size + pub size_limits: Limits, +} + +impl Default for SctkLayerSurfaceSettings { + fn default() -> Self { + Self { + id: Id::unique(), + layer: Layer::Top, + keyboard_interactivity: Default::default(), + pointer_interactivity: true, + anchor: Anchor::empty(), + output: Default::default(), + namespace: Default::default(), + margin: Default::default(), + size: Default::default(), + exclusive_zone: Default::default(), + size_limits: Limits::NONE + .min_height(1.0) + .min_width(1.0) + .max_width(1920.0) + .max_height(1080.023), + } + } +} + +#[derive(Clone)] +/// LayerSurface Action +pub enum Action { + /// create a layer surface and receive a message with its Id + LayerSurface { + /// surface builder + builder: SctkLayerSurfaceSettings, + }, + /// Set size of the layer surface. + Size { + /// id of the layer surface + id: Id, + /// The new logical width of the window + width: Option, + /// The new logical height of the window + height: Option, + }, + /// Destroy the layer surface + Destroy(Id), + /// The edges which the layer surface is anchored to + Anchor { + /// id of the layer surface + id: Id, + /// anchor of the layer surface + anchor: Anchor, + }, + /// exclusive zone of the layer surface + ExclusiveZone { + /// id of the layer surface + id: Id, + /// exclusive zone of the layer surface + exclusive_zone: i32, + }, + /// margin of the layer surface, ignored for un-anchored edges + Margin { + /// id of the layer surface + id: Id, + /// margins of the layer surface + margin: IcedMargin, + }, + /// keyboard interactivity of the layer surface + KeyboardInteractivity { + /// id of the layer surface + id: Id, + /// keyboard interactivity of the layer surface + keyboard_interactivity: KeyboardInteractivity, + }, + /// layer of the layer surface + Layer { + /// id of the layer surface + id: Id, + /// layer of the layer surface + layer: Layer, + }, +} + +impl fmt::Debug for Action { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Action::LayerSurface { builder, .. } => write!( + f, + "Action::LayerSurfaceAction::LayerSurface {{ builder: {:?} }}", + builder + ), + Action::Size { id, width, height } => write!( + f, + "Action::LayerSurfaceAction::Size {{ id: {:#?}, width: {:?}, height: {:?} }}", id, width, height + ), + Action::Destroy(id) => write!( + f, + "Action::LayerSurfaceAction::Destroy {{ id: {:#?} }}", id + ), + Action::Anchor { id, anchor } => write!( + f, + "Action::LayerSurfaceAction::Anchor {{ id: {:#?}, anchor: {:?} }}", id, anchor + ), + Action::ExclusiveZone { id, exclusive_zone } => write!( + f, + "Action::LayerSurfaceAction::ExclusiveZone {{ id: {:#?}, exclusive_zone: {exclusive_zone} }}", id + ), + Action::Margin { id, margin } => write!( + f, + "Action::LayerSurfaceAction::Margin {{ id: {:#?}, margin: {:?} }}", id, margin + ), + Action::KeyboardInteractivity { id, keyboard_interactivity } => write!( + f, + "Action::LayerSurfaceAction::Margin {{ id: {:#?}, keyboard_interactivity: {:?} }}", id, keyboard_interactivity + ), + Action::Layer { id, layer } => write!( + f, + "Action::LayerSurfaceAction::Margin {{ id: {:#?}, layer: {:?} }}", id, layer + ), + } + } +} diff --git a/runtime/src/platform_specific/wayland/mod.rs b/runtime/src/platform_specific/wayland/mod.rs new file mode 100644 index 0000000000..c370d38592 --- /dev/null +++ b/runtime/src/platform_specific/wayland/mod.rs @@ -0,0 +1,42 @@ +//! Wayland specific actions + +use std::fmt::Debug; + +/// activation Actions +pub mod activation; + +/// layer surface actions +pub mod layer_surface; +/// popup actions +pub mod popup; +/// session locks +pub mod session_lock; + +/// Platform specific actions defined for wayland +pub enum Action { + /// LayerSurface Actions + LayerSurface(layer_surface::Action), + /// popup + Popup(popup::Action), + /// activation + Activation(activation::Action), + /// session lock + SessionLock(session_lock::Action), +} + +impl Debug for Action { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::LayerSurface(arg0) => { + f.debug_tuple("LayerSurface").field(arg0).finish() + } + Self::Popup(arg0) => f.debug_tuple("Popup").field(arg0).finish(), + Self::Activation(arg0) => { + f.debug_tuple("Activation").field(arg0).finish() + } + Self::SessionLock(arg0) => { + f.debug_tuple("SessionLock").field(arg0).finish() + } + } + } +} diff --git a/runtime/src/platform_specific/wayland/popup.rs b/runtime/src/platform_specific/wayland/popup.rs new file mode 100644 index 0000000000..615a6d49d7 --- /dev/null +++ b/runtime/src/platform_specific/wayland/popup.rs @@ -0,0 +1,141 @@ +use std::fmt; +use std::hash::{Hash, Hasher}; + +use iced_core::layout::Limits; +use iced_core::window::Id; +use iced_core::Rectangle; +use sctk::reexports::protocols::xdg::shell::client::xdg_positioner::{ + Anchor, Gravity, +}; +/// Popup creation details +#[derive(Debug, Clone)] +pub struct SctkPopupSettings { + /// XXX must be unique, id of the parent + pub parent: Id, + /// XXX must be unique, id of the popup + pub id: Id, + /// positioner of the popup + pub positioner: SctkPositioner, + /// optional parent size, must be correct if specified or the behavior is undefined + pub parent_size: Option<(u32, u32)>, + /// whether a grab should be requested for the popup after creation + pub grab: bool, +} + +impl Hash for SctkPopupSettings { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +/// Positioner of a popup +#[derive(Debug, Clone)] +pub struct SctkPositioner { + /// size of the popup (if it is None, the popup will be autosized) + pub size: Option<(u32, u32)>, + /// Limits of the popup size + pub size_limits: Limits, + /// the rectangle which the popup will be anchored to + pub anchor_rect: Rectangle, + /// the anchor location on the popup + pub anchor: Anchor, + /// the gravity of the popup + pub gravity: Gravity, + /// the constraint adjustment, + /// Specify how the window should be positioned if the originally intended position caused the surface to be constrained, meaning at least partially outside positioning boundaries set by the compositor. The adjustment is set by constructing a bitmask describing the adjustment to be made when the surface is constrained on that axis. + /// If no bit for one axis is set, the compositor will assume that the child surface should not change its position on that axis when constrained. + /// + /// If more than one bit for one axis is set, the order of how adjustments are applied is specified in the corresponding adjustment descriptions. + /// + /// The default adjustment is none. + pub constraint_adjustment: u32, + /// offset of the popup + pub offset: (i32, i32), + /// whether the popup is reactive + pub reactive: bool, +} + +impl Hash for SctkPositioner { + fn hash(&self, state: &mut H) { + self.size.hash(state); + self.anchor_rect.x.hash(state); + self.anchor_rect.y.hash(state); + self.anchor_rect.width.hash(state); + self.anchor_rect.height.hash(state); + self.anchor.hash(state); + self.gravity.hash(state); + self.constraint_adjustment.hash(state); + self.offset.hash(state); + self.reactive.hash(state); + } +} + +impl Default for SctkPositioner { + fn default() -> Self { + Self { + size: None, + size_limits: Limits::NONE + .min_height(1.0) + .min_width(1.0) + .max_width(300.0) + .max_height(1080.0), + anchor_rect: Rectangle { + x: 0, + y: 0, + width: 1, + height: 1, + }, + anchor: Anchor::None, + gravity: Gravity::None, + constraint_adjustment: 15, + offset: Default::default(), + reactive: true, + } + } +} + +#[derive(Clone)] +/// Window Action +pub enum Action { + /// create a window and receive a message with its Id + Popup { + /// popup + popup: SctkPopupSettings, + }, + /// destroy the popup + Destroy { + /// id of the popup + id: Id, + }, + /// set the size of the popup + Size { + /// id of the popup + id: Id, + /// width + width: u32, + /// height + height: u32, + }, +} + +impl fmt::Debug for Action { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Action::Popup { popup, .. } => write!( + f, + "Action::PopupAction::Popup {{ popup: {:?} }}", + popup + ), + Action::Destroy { id } => write!( + f, + "Action::PopupAction::Destroy {{ id: {:?} }}", + id + ), + Action::Size { id, width, height } => write!( + f, + "Action::PopupAction::Size {{ id: {:?}, width: {:?}, height: {:?} }}", + id, width, height + ), + } + } +} diff --git a/runtime/src/platform_specific/wayland/session_lock.rs b/runtime/src/platform_specific/wayland/session_lock.rs new file mode 100644 index 0000000000..adbc2390e4 --- /dev/null +++ b/runtime/src/platform_specific/wayland/session_lock.rs @@ -0,0 +1,45 @@ +use std::fmt; + +use iced_core::window::Id; + +use sctk::reexports::client::protocol::wl_output::WlOutput; + +/// Session lock action +#[derive(Clone)] +pub enum Action { + /// Request a session lock + Lock, + /// Destroy lock + Unlock, + /// Create lock surface for output + LockSurface { + /// unique id for surface + id: Id, + /// output + output: WlOutput, + }, + /// Destroy lock surface + DestroyLockSurface { + /// unique id for surface + id: Id, + }, +} + +impl fmt::Debug for Action { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Action::Lock => write!(f, "Action::SessionLock::Lock"), + Action::Unlock => write!(f, "Action::SessionLock::Unlock"), + Action::LockSurface { id, output } => write!( + f, + "Action::SessionLock::LockSurface {{ id: {:?}, output: {:?} }}", + id, output + ), + Action::DestroyLockSurface { id } => write!( + f, + "Action::SessionLock::DestroyLockSurface {{ id: {:?} }}", + id + ), + } + } +} diff --git a/runtime/src/program/state.rs b/runtime/src/program/state.rs index c377814a74..c0befaa2b0 100644 --- a/runtime/src/program/state.rs +++ b/runtime/src/program/state.rs @@ -1,3 +1,5 @@ +use iced_core::widget::operation::Outcome; + use crate::core::event::{self, Event}; use crate::core::mouse; use crate::core::renderer; @@ -27,12 +29,14 @@ where /// Creates a new [`State`] with the provided [`Program`], initializing its /// primitive with the given logical bounds and renderer. pub fn new( + id: iced_core::id::Id, mut program: P, bounds: Size, renderer: &mut P::Renderer, debug: &mut Debug, ) -> Self { let user_interface = build_user_interface( + id, &mut program, user_interface::Cache::default(), renderer, @@ -88,6 +92,7 @@ where /// after updating it, only if an update was necessary. pub fn update( &mut self, + id: iced_core::id::Id, bounds: Size, cursor: mouse::Cursor, renderer: &mut P::Renderer, @@ -97,6 +102,7 @@ where debug: &mut Debug, ) -> (Vec, Option>) { let mut user_interface = build_user_interface( + id.clone(), &mut self.program, self.cache.take().unwrap(), renderer, @@ -154,6 +160,7 @@ where })); let mut user_interface = build_user_interface( + id, &mut self.program, temp_cache, renderer, @@ -177,12 +184,14 @@ where /// Applies [`Operation`]s to the [`State`] pub fn operate( &mut self, + id: iced_core::id::Id, renderer: &mut P::Renderer, operations: impl Iterator>, bounds: Size, debug: &mut Debug, ) { let mut user_interface = build_user_interface( + id, &mut self.program, self.cache.take().unwrap(), renderer, @@ -202,6 +211,7 @@ where operation::Outcome::Chain(next) => { current_operation = Some(next); } + _ => {} }; } } @@ -211,6 +221,7 @@ where } fn build_user_interface<'a, P: Program>( + _id: iced_core::id::Id, program: &'a mut P, cache: user_interface::Cache, renderer: &mut P::Renderer, diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index 8dfc97a728..f6184d144c 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -1,4 +1,9 @@ //! Implement your own event loop to drive a user interface. + +use iced_core::clipboard::DndDestinationRectangles; +use iced_core::widget::tree::NAMED; +use iced_core::widget::Operation; + use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -90,10 +95,15 @@ where cache: Cache, renderer: &mut Renderer, ) -> Self { - let root = root.into(); + let mut root = root.into(); let Cache { mut state } = cache; - state.diff(root.as_widget()); + NAMED.with(|named| { + let mut guard = named.borrow_mut(); + *guard = state.take_all_named(); + }); + + state.diff(root.as_widget_mut()); let base = root.as_widget().layout( &mut state, @@ -101,6 +111,10 @@ where &layout::Limits::new(Size::ZERO, bounds), ); + NAMED.with(|named| { + named.borrow_mut().clear(); + }); + UserInterface { root, base, @@ -609,6 +623,40 @@ where pub fn into_cache(self) -> Cache { Cache { state: self.state } } + + /// get a11y nodes + #[cfg(feature = "a11y")] + pub fn a11y_nodes( + &self, + cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + self.root.as_widget().a11y_nodes( + Layout::new(&self.base), + &self.state, + cursor, + ) + } + + /// Find widget with given id + pub fn find(&self, id: &widget::Id) -> Option<&widget::Tree> { + self.state.find(id) + } + + /// Get the destination rectangles for the user interface. + pub fn dnd_rectangles( + &self, + prev_capacity: usize, + renderer: &Renderer, + ) -> DndDestinationRectangles { + let mut ret = DndDestinationRectangles::with_capacity(prev_capacity); + self.root.as_widget().drag_destinations( + &self.state, + Layout::new(&self.base), + renderer, + &mut ret, + ); + ret + } } /// Reusable data of a specific [`UserInterface`]. diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 382f4518d4..48c200ec99 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -169,9 +169,31 @@ pub enum Action { /// /// In any case, this [`Subscription`] is useful to smoothly draw application-driven /// animations without missing any frames. -pub fn frames() -> Subscription { +pub fn frames() -> Subscription<(Id, Instant)> { + event::listen_raw(|event, _status, window| match event { + crate::core::Event::Window(Event::RedrawRequested(at)) => { + Some((window, at)) + } + _ => None, + }) +} +#[cfg(feature = "wayland")] +/// 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 wayland_frames() -> Subscription { event::listen_raw(|event, _status, _window| match event { - crate::core::Event::Window(Event::RedrawRequested(at)) => Some(at), + iced_core::Event::Window(Event::RedrawRequested(at)) + | iced_core::Event::PlatformSpecific( + iced_core::event::PlatformSpecific::Wayland( + iced_core::event::wayland::Event::Frame(at, _, _), + ), + ) => Some(at), _ => None, }) } diff --git a/runtime/src/window/screenshot.rs b/runtime/src/window/screenshot.rs index d9adbc010a..06eadf92e1 100644 --- a/runtime/src/window/screenshot.rs +++ b/runtime/src/window/screenshot.rs @@ -6,7 +6,7 @@ use std::fmt::{Debug, Formatter}; /// Data of a screenshot, captured with `window::screenshot()`. /// -/// The `bytes` of this screenshot will always be ordered as `RGBA` in the `sRGB` color space. +/// 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`]. diff --git a/src/application.rs b/src/application.rs index 2ba764bed6..acd38062b3 100644 --- a/src/application.rs +++ b/src/application.rs @@ -31,6 +31,10 @@ //! } //! ``` use crate::program::{self, Program}; +#[cfg(not(feature = "winit"))] +use crate::runtime::{Appearance, DefaultStyle}; +#[cfg(feature = "winit")] +pub use crate::shell::program::{Appearance, DefaultStyle}; use crate::window; use crate::{ Element, Executor, Font, Result, Settings, Size, Subscription, Task, @@ -38,8 +42,6 @@ use crate::{ use std::borrow::Cow; -pub use crate::shell::program::{Appearance, DefaultStyle}; - /// Creates an iced [`Application`] given its title, update, and view logic. /// /// # Example @@ -152,6 +154,7 @@ pub struct Application { } impl Application

{ + #[cfg(any(feature = "winit", feature = "wayland"))] /// Runs the [`Application`]. /// /// The state of the [`Application`] must implement [`Default`]. @@ -167,6 +170,7 @@ impl Application

{ self.raw.run(self.settings, Some(self.window)) } + #[cfg(any(feature = "winit", feature = "wayland"))] /// Runs the [`Application`] with a closure that creates the initial state. pub fn run_with(self, initialize: I) -> Result where diff --git a/src/daemon.rs b/src/daemon.rs index 81254bf947..689b5222a4 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,12 +1,15 @@ //! Create and run daemons that run in the background. use crate::application; use crate::program::{self, Program}; +#[cfg(any(feature = "winit", feature = "wayland"))] +pub use crate::shell::program::{Appearance, DefaultStyle}; use crate::window; use crate::{Element, Executor, Font, Result, Settings, Subscription, Task}; -use std::borrow::Cow; +#[cfg(not(any(feature = "winit", feature = "wayland")))] +use crate::runtime::{Appearance, DefaultStyle}; -pub use crate::shell::program::{Appearance, DefaultStyle}; +use std::borrow::Cow; /// Creates an iced [`Daemon`] given its title, update, and view logic. /// @@ -100,6 +103,7 @@ pub struct Daemon { } impl Daemon

{ + #[cfg(any(feature = "winit", feature = "wayland"))] /// Runs the [`Daemon`]. /// /// The state of the [`Daemon`] must implement [`Default`]. @@ -115,6 +119,7 @@ impl Daemon

{ self.raw.run(self.settings, None) } + #[cfg(any(feature = "winit", feature = "wayland"))] /// Runs the [`Daemon`] with a closure that creates the initial state. pub fn run_with(self, initialize: I) -> Result where diff --git a/src/error.rs b/src/error.rs index 111bedf245..3285592f85 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,6 @@ use crate::futures; use crate::graphics; +#[cfg(any(feature = "winit", feature = "wayland"))] use crate::shell; /// An error that occurred while running an application. @@ -18,12 +19,18 @@ pub enum Error { GraphicsCreationFailed(graphics::Error), } +#[cfg(any(feature = "winit", feature = "wayland"))] impl From for Error { fn from(error: shell::Error) -> Error { match error { shell::Error::ExecutorCreationFailed(error) => { Error::ExecutorCreationFailed(error) } + #[cfg(feature = "winit")] + shell::Error::WindowCreationFailed(error) => { + Error::WindowCreationFailed(Box::new(error)) + } + #[cfg(feature = "wayland")] shell::Error::WindowCreationFailed(error) => { Error::WindowCreationFailed(Box::new(error)) } diff --git a/src/lib.rs b/src/lib.rs index 588c7313c5..bdd4154265 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -471,14 +471,19 @@ )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))] -use iced_widget::graphics; -use iced_widget::renderer; -use iced_winit as shell; -use iced_winit::core; -use iced_winit::runtime; + +// #[cfg(all(feature = "wayland", feature = "winit"))] +// compile_error!("cannot use `wayland` feature with `winit"); pub use iced_futures::futures; pub use iced_futures::stream; +pub use iced_widget::core; +use iced_widget::graphics; +use iced_widget::renderer; +pub use iced_widget::runtime; + +#[cfg(feature = "winit")] +use iced_winit as shell; #[cfg(feature = "highlighter")] pub use iced_highlighter as highlighter; @@ -487,7 +492,7 @@ pub use iced_highlighter as highlighter; pub use iced_renderer::wgpu::wgpu; mod error; -mod program; +pub mod program; pub mod application; pub mod daemon; @@ -495,19 +500,44 @@ pub mod settings; pub mod time; pub mod window; +#[cfg(feature = "winit")] +pub mod platform_specific { + pub use iced_winit::{ + platform_specific as shell, runtime::platform_specific as runtime, + }; +} + +#[cfg(feature = "winit")] +pub use application::Application; +#[cfg(feature = "winit")] +pub use program::Program; + +// wayland application +// #[cfg(feature = "wayland")] +// pub mod wayland; +// #[cfg(feature = "wayland")] +// pub use wayland::application; +// #[cfg(feature = "wayland")] +// pub use wayland::application::Application; +// #[cfg(feature = "wayland")] +// pub use wayland::program; +// #[doc(inline)] +// #[cfg(feature = "wayland")] +// pub use wayland::program::Program; + #[cfg(feature = "advanced")] pub mod advanced; pub use crate::core::alignment; -pub use crate::core::border; +pub use crate::core::border::{self, Radius}; pub use crate::core::color; pub use crate::core::gradient; pub use crate::core::padding; pub use crate::core::theme; pub use crate::core::{ - Alignment, Background, Border, Color, ContentFit, Degrees, Gradient, - Length, Padding, Pixels, Point, Radians, Rectangle, Rotation, Shadow, Size, - Theme, Transformation, Vector, + id, layout::Limits, Alignment, Background, Border, Color, ContentFit, + Degrees, Gradient, Length, Padding, Pixels, Point, Radians, Rectangle, + Rotation, Shadow, Size, Theme, Transformation, Vector, }; pub use crate::runtime::exit; pub use iced_futures::Subscription; @@ -525,8 +555,11 @@ pub mod task { pub mod clipboard { //! Access the clipboard. pub use crate::runtime::clipboard::{ - read, read_primary, write, write_primary, + read, read_data, read_primary, read_primary_data, write, write_data, + write_primary, write_primary_data, }; + pub use dnd; + pub use mime; } pub mod executor { @@ -554,6 +587,9 @@ pub mod font { pub mod event { //! Handle events of a user interface. + #[cfg(feature = "wayland")] + pub use crate::core::event::wayland; + pub use crate::core::event::PlatformSpecific; pub use crate::core::event::{Event, Status}; pub use iced_futures::event::{listen, listen_raw, listen_with}; } @@ -576,6 +612,7 @@ pub mod mouse { pub mod system { //! Retrieve system information. pub use crate::runtime::system::Information; + #[cfg(any(feature = "winit", feature = "wayland"))] pub use crate::shell::system::*; } @@ -616,8 +653,8 @@ pub mod widget { mod runtime {} } -pub use application::Application; -pub use daemon::Daemon; +pub use application::application; +pub use daemon::{daemon, Daemon}; pub use error::Error; pub use event::Event; pub use executor::Executor; @@ -626,11 +663,6 @@ pub use renderer::Renderer; pub use settings::Settings; pub use task::Task; -#[doc(inline)] -pub use application::application; -#[doc(inline)] -pub use daemon::daemon; - /// A generic widget. /// /// This is an alias of an `iced_native` element with a default `Renderer`. @@ -644,6 +676,7 @@ pub type Element< /// The result of running an iced program. pub type Result = std::result::Result<(), Error>; +#[cfg(any(feature = "winit"))] /// Runs a basic iced application with default [`Settings`] given its title, /// update, and view logic. /// diff --git a/src/program.rs b/src/program.rs index 94cb9a7d33..bac6ddfc39 100644 --- a/src/program.rs +++ b/src/program.rs @@ -1,10 +1,14 @@ use crate::core::text; use crate::graphics::compositor; +#[cfg(any(feature = "winit", feature = "wayland"))] use crate::shell; +#[cfg(any(feature = "winit", feature = "wayland"))] +pub use crate::shell::program::{Appearance, DefaultStyle}; use crate::window; use crate::{Element, Executor, Result, Settings, Subscription, Task}; -pub use crate::shell::program::{Appearance, DefaultStyle}; +#[cfg(not(any(feature = "winit", feature = "wayland")))] +pub use crate::runtime::{Appearance, DefaultStyle}; /// The internal definition of a [`Program`]. /// @@ -62,6 +66,7 @@ pub trait Program: Sized { 1.0 } + #[cfg(any(feature = "winit", feature = "wayland"))] /// Runs the [`Program`]. /// /// The state of the [`Program`] must implement [`Default`]. @@ -83,6 +88,7 @@ pub trait Program: Sized { }) } + #[cfg(any(feature = "winit", feature = "wayland"))] /// Runs the [`Program`] with the given [`Settings`] and a closure that creates the initial state. fn run_with( self, @@ -184,6 +190,7 @@ pub trait Program: Sized { default_font: settings.default_font, default_text_size: settings.default_text_size, antialiasing: settings.antialiasing, + exit_on_close_request: settings.exit_on_close_request, } .into(), renderer_settings, diff --git a/src/settings.rs b/src/settings.rs index ebac7a86de..3b48ca1e31 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -28,13 +28,13 @@ pub struct Settings { /// If set to true, the renderer will try to perform antialiasing for some /// primitives. /// - /// Enabling it can produce a smoother result in some widgets, like the - /// [`Canvas`], at a performance cost. + /// Enabling it can produce a smoother result in some widgets /// /// By default, it is disabled. - /// - /// [`Canvas`]: crate::widget::Canvas pub antialiasing: bool, + + /// If set to true the application will exit when the main window is closed. + pub exit_on_close_request: bool, } impl Default for Settings { @@ -43,12 +43,14 @@ impl Default for Settings { id: None, fonts: Vec::new(), default_font: Font::default(), - default_text_size: Pixels(16.0), + default_text_size: Pixels(14.0), antialiasing: false, + exit_on_close_request: false, } } } +#[cfg(feature = "winit")] impl From for iced_winit::Settings { fn from(settings: Settings) -> iced_winit::Settings { iced_winit::Settings { diff --git a/src/wayland/application.rs b/src/wayland/application.rs new file mode 100644 index 0000000000..97bd0313ac --- /dev/null +++ b/src/wayland/application.rs @@ -0,0 +1,211 @@ +use crate::{Element, Executor, Settings as Settings_, Subscription}; +use iced_core::window::Id; + +use crate::core::text; +pub use crate::{platform_specific::wayland as actions, Task}; +use iced_renderer::graphics::{compositor, Antialiasing}; +pub use iced_sctk::application::Appearance; +pub use iced_sctk::{ + application::{DefaultStyle, SurfaceIdWrapper}, + commands::*, + settings::*, +}; + +/// A pure version of [`Application`]. +/// +/// Unlike the impure version, the `view` method of this trait takes an +/// immutable reference to `self` and returns a pure [`Element`]. +pub trait Application: Sized { + /// The [`Executor`] that will run commands and subscriptions. + /// + /// The [default executor] can be a good starting point! + /// + /// [`Executor`]: Self::Executor + /// [default executor]: crate::executor::Default + type Executor: Executor; + + /// The type of __messages__ your [`Application`] will produce. + type Message: std::fmt::Debug + Send; + + /// The theme of your [`Application`]. + type Theme: Default + DefaultStyle; + + /// The renderer of your [`Application`]. + type Renderer: text::Renderer + compositor::Default; + + /// The data needed to initialize your [`Application`]. + type Flags; + + /// Initializes the [`Application`] with the flags provided to + /// [`run`] as part of the [`Settings`]. + /// + /// Here is where you should return the initial state of your app. + /// + /// Additionally, you can return a [`Task`] if you need to perform some + /// async action in the background on startup. This is useful if you want to + /// load state from a file, perform an initial HTTP request, etc. + /// + /// [`run`]: Self::run + fn new(flags: Self::Flags) -> (Self, Task); + + /// Returns the current title of the [`Application`]. + /// + /// This title can be dynamic! The runtime will automatically update the + /// title of your application when necessary. + fn title(&self, id: Id) -> String; + + /// Handles a __message__ and updates the state of the [`Application`]. + /// + /// This is where you define your __update logic__. All the __messages__, + /// produced by either user interactions or commands, will be handled by + /// this method. + /// + /// Any [`Task`] returned will be executed immediately in the background. + fn update(&mut self, message: Self::Message) -> Task; + + /// Returns the current [`Theme`] of the [`Application`]. + /// + /// [`Theme`]: Self::Theme + fn theme(&self, _id: Id) -> Self::Theme { + Self::Theme::default() + } + + /// Returns the current Style of the Theme. + fn style(&self, theme: &Self::Theme) -> Appearance { + theme.default_style() + } + + /// Returns the event [`Subscription`] for the current state of the + /// application. + /// + /// A [`Subscription`] will be kept alive as long as you keep returning it, + /// and the __messages__ produced will be handled by + /// [`update`](#tymethod.update). + /// + /// By default, this method returns an empty [`Subscription`]. + fn subscription(&self) -> Subscription { + Subscription::none() + } + + /// Returns the widgets to display in the [`Application`]. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view( + &self, + id: Id, + ) -> Element<'_, Self::Message, Self::Theme, Self::Renderer>; + + /// Returns the scale factor of the [`Application`]. + /// + /// It can be used to dynamically control the size of the UI at runtime + /// (i.e. zooming). + /// + /// For instance, a scale factor of `2.0` will make widgets twice as big, + /// while a scale factor of `0.5` will shrink them to half their size. + /// + /// By default, it returns `1.0`. + fn scale_factor(&self, _id: Id) -> f64 { + 1.0 + } + + /// Runs the [`Application`]. + /// + /// On native platforms, this method will take control of the current thread + /// until the [`Application`] exits. + /// + /// On the web platform, this method __will NOT return__ unless there is an + /// [`Error`] during startup. + /// + /// [`Error`]: crate::Error + fn run(settings: Settings_) -> crate::Result + where + Self: 'static, + { + #[allow(clippy::needless_update)] + let renderer_settings = crate::graphics::Settings { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + antialiasing: if settings.antialiasing { + Some(Antialiasing::MSAAx4) + } else { + None + }, + ..crate::graphics::Settings::default() + }; + + let run = crate::shell::application::run::< + Instance, + Self::Executor, + ::Compositor, + >(settings.into(), renderer_settings); + #[cfg(target_arch = "wasm32")] + { + use crate::futures::FutureExt; + use iced_futures::backend::wasm::wasm_bindgen::Executor; + + Executor::new() + .expect("Create Wasm executor") + .spawn(run.map(|_| ())); + + Ok(()) + } + + #[cfg(not(target_arch = "wasm32"))] + Ok(crate::futures::executor::block_on(run)?) + } +} + +struct Instance(A); + +impl crate::runtime::multi_window::Program for Instance +where + A: Application, +{ + type Theme = A::Theme; + type Renderer = A::Renderer; + type Message = A::Message; + + fn update(&mut self, message: Self::Message) -> Task { + self.0.update(message) + } + + fn view( + &self, + id: Id, + ) -> Element<'_, Self::Message, Self::Theme, Self::Renderer> { + self.0.view(id) + } +} + +impl crate::shell::Application for Instance +where + A: Application, +{ + type Flags = A::Flags; + + fn new(flags: Self::Flags) -> (Self, Task) { + let (app, command) = A::new(flags); + + (Instance(app), command) + } + + fn title(&self, window: Id) -> String { + self.0.title(window) + } + + fn theme(&self, id: Id) -> A::Theme { + self.0.theme(id) + } + + fn style(&self, theme: &A::Theme) -> Appearance { + self.0.style(theme) + } + + fn subscription(&self) -> Subscription { + self.0.subscription() + } + + fn scale_factor(&self, window: Id) -> f64 { + self.0.scale_factor(window) + } +} diff --git a/src/wayland/mod.rs b/src/wayland/mod.rs new file mode 100644 index 0000000000..1e0107a02d --- /dev/null +++ b/src/wayland/mod.rs @@ -0,0 +1,4 @@ +/// wayland application +pub mod application; +/// wayland program +pub mod program; diff --git a/src/wayland/program.rs b/src/wayland/program.rs new file mode 100644 index 0000000000..c701a6c2c0 --- /dev/null +++ b/src/wayland/program.rs @@ -0,0 +1,833 @@ +//! Create and run iced applications step by step. +//! +//! # Example +//! ```no_run +//! use iced::widget::{button, column, text, Column}; +//! use iced::Theme; +//! +//! pub fn main() -> iced::Result { +//! iced::program("A counter", update, view) +//! .theme(|_| Theme::Dark) +//! .centered() +//! .run() +//! } +//! +//! #[derive(Debug, Clone)] +//! enum Message { +//! Increment, +//! } +//! +//! fn update(value: &mut u64, message: Message) { +//! match message { +//! Message::Increment => *value += 1, +//! } +//! } +//! +//! fn view(value: &u64) -> Column { +//! column![ +//! text(value), +//! button("+").on_press(Message::Increment), +//! ] +//! } +//! ``` +use crate::executor::{self, Executor}; +use crate::window; +use crate::Result; +use crate::Task; + +use iced_core::window::Id; +pub use iced_sctk::application::{Appearance, DefaultStyle}; + +use crate::theme::{self, Theme}; +use crate::{ + wayland::application::Application, Element, Error, Font, Settings, + Subscription, +}; + +use crate::core::text; +use crate::graphics::compositor; + +use std::borrow::Cow; + +/// Creates an iced [`Program`] given its title, update, and view logic. +/// +/// # Example +/// ```no_run +/// use iced::widget::{button, column, text, Column}; +/// +/// pub fn main() -> iced::Result { +/// iced::program("A counter", update, view).run() +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// Increment, +/// } +/// +/// fn update(value: &mut u64, message: Message) { +/// match message { +/// Message::Increment => *value += 1, +/// } +/// } +/// +/// fn view(value: &u64) -> Column { +/// column![ +/// text(value), +/// button("+").on_press(Message::Increment), +/// ] +/// } +/// ``` +pub fn program( + title: impl Title, + update: impl Update, + view: impl for<'a> self::View<'a, State, Message, Theme, Renderer>, +) -> Program> +where + State: 'static, + Message: Send + std::fmt::Debug + 'static, + Theme: Default + DefaultStyle, + Renderer: self::Renderer, +{ + use std::marker::PhantomData; + + struct Application { + update: Update, + view: View, + _state: PhantomData, + _message: PhantomData, + _theme: PhantomData, + _renderer: PhantomData, + } + + impl Definition + for Application + where + Message: Send + std::fmt::Debug + 'static, + Theme: Default + DefaultStyle, + Renderer: self::Renderer, + Update: self::Update, + View: for<'a> self::View<'a, State, Message, Theme, Renderer>, + { + type State = State; + type Message = Message; + type Theme = Theme; + type Renderer = Renderer; + type Executor = executor::Default; + + fn load(&self) -> Task { + Task::none() + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Task { + self.update.update(state, message).into() + } + + fn view<'a>( + &self, + state: &'a Self::State, + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + self.view.view(state).into() + } + } + + Program { + raw: Application { + update, + view, + _state: PhantomData, + _message: PhantomData, + _theme: PhantomData, + _renderer: PhantomData, + }, + settings: Settings::default(), + } + .title(title) +} + +/// The underlying definition and configuration of an iced application. +/// +/// You can use this API to create and run iced applications +/// step by step—without coupling your logic to a trait +/// or a specific type. +/// +/// You can create a [`Program`] with the [`program`] helper. +/// +/// [`run`]: Program::run +#[derive(Debug)] +pub struct Program { + raw: P, + settings: Settings, +} + +impl Program

{ + /// Runs the underlying [`Application`] of the [`Program`]. + /// + /// The state of the [`Program`] must implement [`Default`]. + /// If your state does not implement [`Default`], use [`run_with`] + /// instead. + /// + /// [`run_with`]: Self::run_with + pub fn run(self) -> Result + where + Self: 'static, + P::State: Default, + { + self.run_with(P::State::default) + } + + /// Runs the underlying [`Application`] of the [`Program`] with a + /// closure that creates the initial state. + pub fn run_with( + self, + initialize: impl Fn() -> P::State + Clone + 'static, + ) -> Result + where + Self: 'static, + { + use std::marker::PhantomData; + + struct Instance { + program: P, + state: P::State, + _initialize: PhantomData, + } + + impl P::State> Application for Instance { + type Message = P::Message; + type Theme = P::Theme; + type Renderer = P::Renderer; + type Flags = (P, I); + type Executor = P::Executor; + + fn new( + (program, initialize): Self::Flags, + ) -> (Self, Task) { + let state = initialize(); + let command = program.load(); + + ( + Self { + program, + state, + _initialize: PhantomData, + }, + command, + ) + } + + fn title(&self, _id: Id) -> String { + self.program.title(&self.state) + } + + fn update( + &mut self, + message: Self::Message, + ) -> Task { + self.program.update(&mut self.state, message) + } + + fn view( + &self, + _id: Id, + ) -> crate::Element<'_, Self::Message, Self::Theme, Self::Renderer> + { + self.program.view(&self.state) + } + + fn subscription(&self) -> Subscription { + self.program.subscription(&self.state) + } + + fn theme(&self, _id: Id) -> Self::Theme { + self.program.theme(&self.state) + } + + fn style(&self, theme: &Self::Theme) -> Appearance { + self.program.style(&self.state, theme) + } + } + + let Self { raw, settings } = self; + + Instance::run(Settings { + flags: (raw, initialize), + id: settings.id, + initial_surface: settings.initial_surface, + fonts: settings.fonts, + default_font: settings.default_font, + default_text_size: settings.default_text_size, + antialiasing: settings.antialiasing, + exit_on_close_request: settings.exit_on_close_request, + }) + } + + /// Sets the [`Settings`] that will be used to run the [`Program`]. + pub fn settings(self, settings: Settings) -> Self { + Self { settings, ..self } + } + + /// Sets the [`Settings::antialiasing`] of the [`Program`]. + pub fn antialiasing(self, antialiasing: bool) -> Self { + Self { + settings: Settings { + antialiasing, + ..self.settings + }, + ..self + } + } + + /// Sets the default [`Font`] of the [`Program`]. + pub fn default_font(self, default_font: Font) -> Self { + Self { + settings: Settings { + default_font, + ..self.settings + }, + ..self + } + } + + /// Adds a font to the list of fonts that will be loaded at the start of the [`Program`]. + pub fn font(mut self, font: impl Into>) -> Self { + self.settings.fonts.push(font.into()); + self + } + + /// Sets the [`Title`] of the [`Program`]. + pub(crate) fn title( + self, + title: impl Title, + ) -> Program< + impl Definition, + > { + Program { + raw: with_title(self.raw, title), + settings: self.settings, + } + } + + /// Runs the [`Task`] produced by the closure at startup. + pub fn load( + self, + f: impl Fn() -> Task, + ) -> Program< + impl Definition, + > { + Program { + raw: with_load(self.raw, f), + settings: self.settings, + } + } + + /// Sets the subscription logic of the [`Program`]. + pub fn subscription( + self, + f: impl Fn(&P::State) -> Subscription, + ) -> Program< + impl Definition, + > { + Program { + raw: with_subscription(self.raw, f), + settings: self.settings, + } + } + + /// Sets the theme logic of the [`Program`]. + pub fn theme( + self, + f: impl Fn(&P::State) -> P::Theme, + ) -> Program< + impl Definition, + > { + Program { + raw: with_theme(self.raw, f), + settings: self.settings, + } + } + + /// Sets the style logic of the [`Program`]. + pub fn style( + self, + f: impl Fn(&P::State, &P::Theme) -> Appearance, + ) -> Program< + impl Definition, + > { + Program { + raw: with_style(self.raw, f), + settings: self.settings, + } + } +} + +/// The internal definition of a [`Program`]. +/// +/// You should not need to implement this trait directly. Instead, use the +/// methods available in the [`Program`] struct. +#[allow(missing_docs)] +pub trait Definition: Sized { + /// The state of the program. + type State; + + /// The message of the program. + type Message: Send + std::fmt::Debug + 'static; + + /// The theme of the program. + type Theme: Default + DefaultStyle; + + /// The renderer of the program. + type Renderer: Renderer; + + /// The executor of the program. + type Executor: Executor; + + fn load(&self) -> Task; + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Task; + + fn view<'a>( + &self, + state: &'a Self::State, + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer>; + + fn title(&self, _state: &Self::State) -> String { + String::from("A cool iced application!") + } + + fn subscription( + &self, + _state: &Self::State, + ) -> Subscription { + Subscription::none() + } + + fn theme(&self, _state: &Self::State) -> Self::Theme { + Self::Theme::default() + } + + fn style(&self, _state: &Self::State, theme: &Self::Theme) -> Appearance { + DefaultStyle::default_style(theme) + } +} + +fn with_title( + program: P, + title: impl Title, +) -> impl Definition { + struct WithTitle { + program: P, + title: Title, + } + + impl Definition for WithTitle + where + P: Definition, + Title: self::Title, + { + type State = P::State; + type Message = P::Message; + type Theme = P::Theme; + type Renderer = P::Renderer; + type Executor = P::Executor; + + fn load(&self) -> Task { + self.program.load() + } + + fn title(&self, state: &Self::State) -> String { + self.title.title(state) + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Task { + self.program.update(state, message) + } + + fn view<'a>( + &self, + state: &'a Self::State, + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + self.program.view(state) + } + + fn theme(&self, state: &Self::State) -> Self::Theme { + self.program.theme(state) + } + + fn subscription( + &self, + state: &Self::State, + ) -> Subscription { + self.program.subscription(state) + } + + fn style( + &self, + state: &Self::State, + theme: &Self::Theme, + ) -> Appearance { + self.program.style(state, theme) + } + } + + WithTitle { program, title } +} + +fn with_load( + program: P, + f: impl Fn() -> Task, +) -> impl Definition { + struct WithLoad { + program: P, + load: F, + } + + impl Definition for WithLoad + where + F: Fn() -> Task, + { + type State = P::State; + type Message = P::Message; + type Theme = P::Theme; + type Renderer = P::Renderer; + type Executor = executor::Default; + + fn load(&self) -> Task { + Task::batch([self.program.load(), (self.load)()]) + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Task { + self.program.update(state, message) + } + + fn view<'a>( + &self, + state: &'a Self::State, + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + self.program.view(state) + } + + fn title(&self, state: &Self::State) -> String { + self.program.title(state) + } + + fn subscription( + &self, + state: &Self::State, + ) -> Subscription { + self.program.subscription(state) + } + + fn theme(&self, state: &Self::State) -> Self::Theme { + self.program.theme(state) + } + + fn style( + &self, + state: &Self::State, + theme: &Self::Theme, + ) -> Appearance { + self.program.style(state, theme) + } + } + + WithLoad { program, load: f } +} + +fn with_subscription( + program: P, + f: impl Fn(&P::State) -> Subscription, +) -> impl Definition { + struct WithSubscription { + program: P, + subscription: F, + } + + impl Definition for WithSubscription + where + F: Fn(&P::State) -> Subscription, + { + type State = P::State; + type Message = P::Message; + type Theme = P::Theme; + type Renderer = P::Renderer; + type Executor = executor::Default; + + fn subscription( + &self, + state: &Self::State, + ) -> Subscription { + (self.subscription)(state) + } + + fn load(&self) -> Task { + self.program.load() + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Task { + self.program.update(state, message) + } + + fn view<'a>( + &self, + state: &'a Self::State, + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + self.program.view(state) + } + + fn title(&self, state: &Self::State) -> String { + self.program.title(state) + } + + fn theme(&self, state: &Self::State) -> Self::Theme { + self.program.theme(state) + } + + fn style( + &self, + state: &Self::State, + theme: &Self::Theme, + ) -> Appearance { + self.program.style(state, theme) + } + } + + WithSubscription { + program, + subscription: f, + } +} + +fn with_theme( + program: P, + f: impl Fn(&P::State) -> P::Theme, +) -> impl Definition { + struct WithTheme { + program: P, + theme: F, + } + + impl Definition for WithTheme + where + F: Fn(&P::State) -> P::Theme, + { + type State = P::State; + type Message = P::Message; + type Theme = P::Theme; + type Renderer = P::Renderer; + type Executor = P::Executor; + + fn theme(&self, state: &Self::State) -> Self::Theme { + (self.theme)(state) + } + + fn load(&self) -> Task { + self.program.load() + } + + fn title(&self, state: &Self::State) -> String { + self.program.title(state) + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Task { + self.program.update(state, message) + } + + fn view<'a>( + &self, + state: &'a Self::State, + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + self.program.view(state) + } + + fn subscription( + &self, + state: &Self::State, + ) -> Subscription { + self.program.subscription(state) + } + + fn style( + &self, + state: &Self::State, + theme: &Self::Theme, + ) -> Appearance { + self.program.style(state, theme) + } + } + + WithTheme { program, theme: f } +} + +fn with_style( + program: P, + f: impl Fn(&P::State, &P::Theme) -> Appearance, +) -> impl Definition { + struct WithStyle { + program: P, + style: F, + } + + impl Definition for WithStyle + where + F: Fn(&P::State, &P::Theme) -> Appearance, + { + type State = P::State; + type Message = P::Message; + type Theme = P::Theme; + type Renderer = P::Renderer; + type Executor = P::Executor; + + fn style( + &self, + state: &Self::State, + theme: &Self::Theme, + ) -> Appearance { + (self.style)(state, theme) + } + + fn load(&self) -> Task { + self.program.load() + } + + fn title(&self, state: &Self::State) -> String { + self.program.title(state) + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Task { + self.program.update(state, message) + } + + fn view<'a>( + &self, + state: &'a Self::State, + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + self.program.view(state) + } + + fn subscription( + &self, + state: &Self::State, + ) -> Subscription { + self.program.subscription(state) + } + + fn theme(&self, state: &Self::State) -> Self::Theme { + self.program.theme(state) + } + } + + WithStyle { program, style: f } +} + +/// The title logic of some [`Program`]. +/// +/// This trait is implemented both for `&static str` and +/// any closure `Fn(&State) -> String`. +/// +/// This trait allows the [`program`] builder to take any of them. +pub trait Title { + /// Produces the title of the [`Program`]. + fn title(&self, state: &State) -> String; +} + +impl Title for &'static str { + fn title(&self, _state: &State) -> String { + self.to_string() + } +} + +impl Title for T +where + T: Fn(&State) -> String, +{ + fn title(&self, state: &State) -> String { + self(state) + } +} + +/// The update logic of some [`Program`]. +/// +/// This trait allows the [`program`] builder to take any closure that +/// returns any `Into>`. +pub trait Update { + /// Processes the message and updates the state of the [`Program`]. + fn update( + &self, + state: &mut State, + message: Message, + ) -> impl Into>; +} + +impl Update for T +where + T: Fn(&mut State, Message) -> C, + C: Into>, +{ + fn update( + &self, + state: &mut State, + message: Message, + ) -> impl Into> { + self(state, message) + } +} + +/// The view logic of some [`Program`]. +/// +/// This trait allows the [`program`] builder to take any closure that +/// returns any `Into>`. +pub trait View<'a, State, Message, Theme, Renderer> { + /// Produces the widget of the [`Program`]. + fn view( + &self, + state: &'a State, + ) -> impl Into>; +} + +impl<'a, T, State, Message, Theme, Renderer, Widget> + View<'a, State, Message, Theme, Renderer> for T +where + T: Fn(&'a State) -> Widget, + State: 'static, + Widget: Into>, +{ + fn view( + &self, + state: &'a State, + ) -> impl Into> { + self(state) + } +} + +/// The renderer of some [`Program`]. +pub trait Renderer: text::Renderer + compositor::Default {} + +impl Renderer for T where T: text::Renderer + compositor::Default {} diff --git a/src/window.rs b/src/window.rs index 9f96da5245..1ceb3f9094 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,8 +1,13 @@ //! Configure the window of your application in native platforms. +#[cfg(feature = "winit")] pub mod icon; +#[cfg(feature = "winit")] pub use icon::Icon; +#[cfg(feature = "winit")] +pub use settings::{PlatformSpecific, Settings}; + pub use crate::core::window::*; pub use crate::runtime::window::*; diff --git a/src/window/icon.rs b/src/window/icon.rs index 7fe4ca7bd1..0856dcb969 100644 --- a/src/window/icon.rs +++ b/src/window/icon.rs @@ -13,7 +13,10 @@ use std::path::Path; /// This will return an error in case the file is missing at run-time. You may prefer [`from_file_data`] instead. #[cfg(feature = "image")] pub fn from_file>(icon_path: P) -> Result { - let icon = image::io::Reader::open(icon_path)?.decode()?.to_rgba8(); + let icon = ::image::io::Reader::open(icon_path)? + .with_guessed_format()? + .decode()? + .to_rgba8(); Ok(icon::from_rgba(icon.to_vec(), icon.width(), icon.height())?) } diff --git a/tiny_skia/fonts/Iced-Icons.ttf b/tiny_skia/fonts/Iced-Icons.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e3273141c45fff98c2f411b421e2e3fd225668aa GIT binary patch literal 5108 zcmds5Uu;`f8UN1ppQLdbdrRt;xn;+@Nt(vjPU^O4)@GgOKk?dh%t`;?+X3^9?fmJI z*f`#35f2Cisw_Z*2LwFufT%PHgpQLTm5zr>yub@D2#J4UlMvE235kaxjY+KhzI(6T zrAwK#N1WXI{l4$~&Ue1^o$s9MYZ62h!p%!GJA3h&AKd-wYea*8fc0QLo13M>bO8D- z=bKS{>-+tL7@}!6Y z^<`_b4togx6lCx6+V!RUTfhAh_!a1PR;pIz(0}$C@YN4}Y6S-Wzdc3hS?I$n>&~yQJ74+B_c7!!VwkVjHk)s~k}Y8T zYiOUYH>&l&KNb5W(E!Kub>dv$I{CxbUVT0|^DlbT^Cm3xr#o+7X8A_tbsk4}Zff6y z>?Mt@_@Iwx;qF!3ZNBHG?a66#?mpY%qp;hekso*yjR`trQ-tRs@iO^{v@dFih`!-S z%=Df%G;Nx)?U1aB8iUEzq?+XRNoL+D z6eWUaSyJ^CiX~VixAifaVl-8XluXl%C=oMFT~VP}HO;v47&0f7cf>+KzD%L0e0o~> z^)$jal__p7f1iTJyzHA!03Ov7s5uTJga$NZg)su;Bp_uH{_@U})9Vyn+meLlNlqkc$@{Pw;k0Ck8K&E`lON*c& z%v8x_N-1jx*=ua|1SpeBk4L-7-ec@cexKt-#1KLTGcL(oOSgC->`YOFv#DeRL3W~4 zo)O*3Izt2=&|M9q7dm&R_Q6j5Tr+xlE70f36{8V7YK}+aYOm4Ow4AC~*|-`oFaar5 zf94#|3qVhsYA9Smaqth5ToxJXgD>2^Qg_Tr{Q7%Sl!DHPjW zxky&Jh2`a8$D=yBb#TOK=PsyyZZ$EJJmSxnu!uXB>H!Pb#v!dE#Ti9swuBINH4GZm zCM|cdsDgT0=2QSh*@uBkX<5SXKM&zSHbCigtJHqjA5)E3}`H0c>vy2$+9+n4EM zR53%1$J98si-Ahbqoj7-FS<3Y^I7#j8K{@Iig(vQgq1O_#J)*8%ZU$WWoOfbAy3$G zPJ$zHAKtDw1$GUK?G%31esW~b=+o^9u@5sjig_c@UAvS}wbpc8C5^=XnYcRkQB+h| z8T8{=3JQlc0z*8)qK2>oFnr4PpEfI6}gxe*D`MwI>s+wi3FO zGcEXO(k&$g})bog$<^!%5MHJQ=HkGe*l;5#W-Ejd_0`3nAgu)n~2gh>j6vF)s*v6!_a^5Cp;}p%=Rw zx%Z)vaBklh2|QZpJYoldJ02r^gxwnpRdSMZAThdc)XjlDgVaphX4Q+`E9I=7=fQZj z9=B7*6L2$17mEowg9jf+>>AchoV(%qFojB$0!jUdxmni4;I z@1J64DNh@^OmNm_aq7;PiFQm3VdKqo%~;KQ<|n#lo89TUgLB4ECt{tJ9ZsJ#)Ksj6 z8scJUVLN_kU@S{Q#V~%#P8SzoC%ar+I(9_@o5YFnDI69!pu3gYneRuJVt%*6_&C+a zdE;|sMxTyEKd|0W(~U9B$>=0C!}E`F<~c-n9^ENtG39;E=ES2M`*1L!j$$w79$=rv zSu2JHRT7-Mp^ig5%~{JKuW}a8tWKWJ8eER*X{7a>u|@P0z#M>JaNgJww&fWB+wv*q zq|x*&a~Kqu!(g5{3>KJk2H*m77+hoygGPJkJ~kCFU@&n8RSvP$#=-F0-Oe!(6e^3{cgsCzzjrvSg^~E_<03 zo4sNqX0O_asaFm4c$fMDD>ik_Moe9|5mPq|b*f9Pv0_u}He%}MZN$`uvAxHub-o#; zW2(2RJi~=+{HTc&zGVM;0bfAGw{R#sU#shF@})6*8fUQGr-@z%4Umoe>D$^JH1C16 zCw-ez4)##CJuGg_<6Gm6V%t;6wntgJ-TP${-qejUzB++|FURI~zb5*}Z;88KQ{Hdl zLBVfO_kRxHJsE%e4(7ZIx&(eddL-!h(}Y&*Ivv_S z|!tViL#4*6cEl^Hosx^iC0{EAH}qQ zivu*MJ-5FDw%Bm7ANKFKxQ{N2 zZ5Ib`DJO*xi1B9oKJNx67k)?ixQu-2sYVsoX|*pw-` zRNH7)*Vbx@at+rsToU0-LY~6aaqJ++vD0!KgC*^Y6L1W!5YcR{v0RnO#H2hWd3f9~Je1?{WE`^r zZ8wj0s16su8X<@V;$O8xlXTP$hb~teo2#`Ac{DM3&&dPln}?GP+mU6bxq`Vk3$#fu zVV+4?n^*>3iiVwf+08{~Q6v_f+pfY-8SDg6zPeFuSWQd}tFrmha}lcYt<=7- r5wEN^wAu@>3H%}xw08^tN*2PY!@j(@SPJ)Y5WW7#+sxYW8`ggTroxeh literal 0 HcmV?d00001 diff --git a/tiny_skia/src/engine.rs b/tiny_skia/src/engine.rs index 196c36cf16..7e5b517a6c 100644 --- a/tiny_skia/src/engine.rs +++ b/tiny_skia/src/engine.rs @@ -1,3 +1,5 @@ +use tiny_skia::Transform; + use crate::core::renderer::Quad; use crate::core::{ Background, Color, Gradient, Rectangle, Size, Transformation, Vector, @@ -51,7 +53,7 @@ impl Engine { return; } - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + let clip_mask = (!physical_bounds.is_within_strict(&clip_bounds)) .then_some(clip_mask as &_); let transform = into_transform(transformation); @@ -64,18 +66,27 @@ impl Engine { .min(quad.bounds.height / 2.0); let mut fill_border_radius = <[f32; 4]>::from(quad.border.radius); - + // Offset the fill by the border width + let path_bounds = Rectangle { + x: quad.bounds.x + border_width, + y: quad.bounds.y + border_width, + width: quad.bounds.width - 2.0 * border_width, + height: quad.bounds.height - 2.0 * border_width, + }; + // fill border radius is the border radius minus the border width for radius in &mut fill_border_radius { - *radius = (*radius) - .min(quad.bounds.width / 2.0) - .min(quad.bounds.height / 2.0); + *radius = (*radius - border_width / 2.0) + .min(path_bounds.width / 2.0) + .min(path_bounds.height / 2.0); } - let path = rounded_rectangle(quad.bounds, fill_border_radius); + let path = rounded_rectangle(path_bounds, fill_border_radius); let shadow = quad.shadow; - - if shadow.color.a > 0.0 { + // TODO: Disabled due to graphical glitches + // TODO(POP): This TODO existed in the pop fork, and if false was used. Evaluate if still needed + // if shadow.color.a > 0.0 { + if false { let shadow_bounds = Rectangle { x: quad.bounds.x + shadow.offset.x - shadow.blur_radius, y: quad.bounds.y + shadow.offset.y - shadow.blur_radius, @@ -262,22 +273,22 @@ impl Engine { // 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( - quad.bounds.width as u32, - quad.bounds.height as u32, + path_bounds.width as u32, + path_bounds.height as u32, ) .unwrap(); let mut quad_mask = tiny_skia::Mask::new( - quad.bounds.width as u32, - quad.bounds.height as u32, + path_bounds.width as u32, + path_bounds.height as u32, ) .unwrap(); let zero_bounds = Rectangle { x: 0.0, y: 0.0, - width: quad.bounds.width, - height: quad.bounds.height, + width: path_bounds.width, + height: path_bounds.height, }; let path = rounded_rectangle(zero_bounds, fill_border_radius); @@ -288,10 +299,10 @@ impl Engine { transform, ); let path_bounds = Rectangle { - x: border_width / 2.0, - y: border_width / 2.0, - width: quad.bounds.width - border_width, - height: quad.bounds.height - border_width, + x: (border_width / 2.0), + y: (border_width / 2.0), + width: path_bounds.width - border_width, + height: path_bounds.height - border_width, }; let border_radius_path = @@ -315,8 +326,8 @@ impl Engine { ); pixels.draw_pixmap( - quad.bounds.x as i32, - quad.bounds.y as i32, + (quad.bounds.x) as i32, + (quad.bounds.y) as i32, temp_pixmap.as_ref(), &tiny_skia::PixmapPaint::default(), transform, @@ -352,8 +363,9 @@ impl Engine { return; } - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); + let clip_mask = (!physical_bounds + .is_within_strict(&clip_bounds)) + .then_some(clip_mask as &_); self.text_pipeline.draw_paragraph( paragraph, @@ -380,8 +392,9 @@ impl Engine { return; } - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); + let clip_mask = (!physical_bounds + .is_within_strict(&clip_bounds)) + .then_some(clip_mask as &_); self.text_pipeline.draw_editor( editor, @@ -410,8 +423,9 @@ impl Engine { return; } - let clip_mask = (!physical_bounds.is_within(&clip_bounds)) - .then_some(clip_mask as &_); + let clip_mask = (!physical_bounds + .is_within_strict(&clip_bounds)) + .then_some(clip_mask as &_); self.text_pipeline.draw_cached( content, @@ -437,13 +451,13 @@ impl Engine { }; let transformation = transformation * *local_transformation; - let (width, height) = buffer.size(); + let (width_opt, height_opt) = buffer.size(); let physical_bounds = Rectangle::new( raw.position, Size::new( - width.unwrap_or(clip_bounds.width), - height.unwrap_or(clip_bounds.height), + width_opt.unwrap_or(clip_bounds.width), + height_opt.unwrap_or(clip_bounds.height), ), ) * transformation; @@ -550,7 +564,7 @@ impl Engine { ) { match image { #[cfg(feature = "image")] - Image::Raster(raster, bounds) => { + Image::Raster { handle, bounds } => { let physical_bounds = *bounds * _transformation; if !_clip_bounds.intersects(&physical_bounds) { @@ -561,26 +575,27 @@ impl Engine { .then_some(_clip_mask as &_); let center = physical_bounds.center(); - let radians = f32::from(raster.rotation); + let radians = f32::from(handle.rotation); - let transform = into_transform(_transformation).post_rotate_at( + let transform = Transform::default().post_rotate_at( radians.to_degrees(), center.x, center.y, ); self.raster_pipeline.draw( - &raster.handle, - raster.filter_method, - *bounds, - raster.opacity, + &handle.handle, + handle.filter_method, + physical_bounds, + handle.opacity, _pixels, transform, clip_mask, + handle.border_radius, ); } #[cfg(feature = "svg")] - Image::Vector(svg, bounds) => { + Image::Vector { handle, bounds } => { let physical_bounds = *bounds * _transformation; if !_clip_bounds.intersects(&physical_bounds) { @@ -591,19 +606,19 @@ impl Engine { .then_some(_clip_mask as &_); let center = physical_bounds.center(); - let radians = f32::from(svg.rotation); + let radians = f32::from(handle.rotation); - let transform = into_transform(_transformation).post_rotate_at( + let transform = Transform::default().post_rotate_at( radians.to_degrees(), center.x, center.y, ); self.vector_pipeline.draw( - &svg.handle, - svg.color, + &handle.handle, + handle.color, physical_bounds, - svg.opacity, + handle.opacity, _pixels, transform, clip_mask, diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index 681bf25d3c..d1f1c7816e 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -299,7 +299,10 @@ impl geometry::frame::Backend for Frame { image.rotation += external_rotation; - self.images.push(graphics::Image::Raster(image, bounds)); + self.images.push(graphics::Image::Raster { + handle: image, + bounds, + }); } fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into) { @@ -310,7 +313,10 @@ impl geometry::frame::Backend for Frame { svg.rotation += external_rotation; - self.images.push(Image::Vector(svg, bounds)); + self.images.push(Image::Vector { + handle: svg, + bounds, + }); } } diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs index bdfd4d388a..30ee93bcad 100644 --- a/tiny_skia/src/layer.rs +++ b/tiny_skia/src/layer.rs @@ -1,6 +1,6 @@ use crate::core::renderer::Quad; use crate::core::{ - self, Background, Color, Point, Rectangle, Svg, Transformation, + self, Background, Color, Point, Radians, Rectangle, Svg, Transformation, }; use crate::graphics::damage; use crate::graphics::layer; @@ -117,11 +117,11 @@ impl Layer { pub fn draw_image(&mut self, image: Image, transformation: Transformation) { match image { - Image::Raster(raster, bounds) => { - self.draw_raster(raster, bounds, transformation); + Image::Raster { handle, bounds } => { + self.draw_raster(handle, bounds, transformation) } - Image::Vector(svg, bounds) => { - self.draw_svg(svg, bounds, transformation); + Image::Vector { handle, bounds } => { + self.draw_svg(handle, bounds, transformation) } } } @@ -132,7 +132,17 @@ impl Layer { bounds: Rectangle, transformation: Transformation, ) { - let image = Image::Raster(image, bounds * transformation); + let image = Image::Raster { + handle: crate::core::Image { + handle: image.handle, + filter_method: image.filter_method, + rotation: image.rotation, + opacity: image.opacity, + snap: image.snap, + border_radius: image.border_radius, + }, + bounds: bounds * transformation, + }; self.images.push(image); } @@ -143,7 +153,10 @@ impl Layer { bounds: Rectangle, transformation: Transformation, ) { - let svg = Image::Vector(svg, bounds * transformation); + let svg = Image::Vector { + handle: svg, + bounds: bounds * transformation, + }; self.images.push(svg); } diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index 758921d44f..9085045790 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -33,7 +33,7 @@ use crate::core::{ }; use crate::engine::Engine; use crate::graphics::compositor; -use crate::graphics::text::{Editor, Paragraph}; +use crate::graphics::text::{Editor, Paragraph, Raw}; use crate::graphics::Viewport; /// A [`tiny-skia`] graphics renderer for [`iced`]. @@ -261,6 +261,7 @@ impl core::text::Renderer for Renderer { type Font = Font; type Paragraph = Paragraph; type Editor = Editor; + type Raw = Raw; const ICON_FONT: Font = Font::with_name("Iced-Icons"); const CHECKMARK_ICON: char = '\u{f00c}'; @@ -282,7 +283,6 @@ impl core::text::Renderer for Renderer { clip_bounds: Rectangle, ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_paragraph( text, position, @@ -313,6 +313,8 @@ impl core::text::Renderer for Renderer { let (layer, transformation) = self.layers.current_mut(); layer.draw_text(text, position, color, clip_bounds, transformation); } + + fn fill_raw(&mut self, _raw: Self::Raw) {} } #[cfg(feature = "geometry")] @@ -381,9 +383,28 @@ impl core::image::Renderer for Renderer { self.engine.raster_pipeline.dimensions(handle) } - fn draw_image(&mut self, image: core::Image, bounds: Rectangle) { + fn draw_image( + &mut self, + handle: Self::Handle, + filter_method: core::image::FilterMethod, + bounds: Rectangle, + rotation: core::Radians, + opacity: f32, + border_radius: [f32; 4], + ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_raster(image, bounds, transformation); + layer.draw_raster( + crate::core::Image { + handle, + filter_method, + rotation, + opacity, + snap: true, + border_radius, + }, + bounds, + transformation, + ); } } @@ -396,7 +417,7 @@ impl core::svg::Renderer for Renderer { self.engine.vector_pipeline.viewport_dimensions(handle) } - fn draw_svg(&mut self, svg: core::Svg, bounds: Rectangle) { + fn draw_svg(&mut self, svg: core::svg::Svg, bounds: Rectangle) { let (layer, transformation) = self.layers.current_mut(); layer.draw_svg(svg, bounds, transformation); } diff --git a/tiny_skia/src/raster.rs b/tiny_skia/src/raster.rs index c40f55b2ff..96d8c98ea6 100644 --- a/tiny_skia/src/raster.rs +++ b/tiny_skia/src/raster.rs @@ -35,8 +35,9 @@ impl Pipeline { pixels: &mut tiny_skia::PixmapMut<'_>, transform: tiny_skia::Transform, clip_mask: Option<&tiny_skia::Mask>, + border_radius: [f32; 4], ) { - if let Some(image) = self.cache.borrow_mut().allocate(handle) { + if let Some(mut 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; @@ -50,6 +51,24 @@ impl Pipeline { tiny_skia::FilterQuality::Nearest } }; + let mut scratch; + + // Round the borders if a border radius is defined + if border_radius.iter().any(|&corner| corner != 0.0) { + scratch = image.to_owned(); + round(&mut scratch.as_mut(), { + let [a, b, c, d] = border_radius; + let scale_by = width_scale.min(height_scale); + let max_radius = image.width().min(image.height()) / 2; + [ + ((a / scale_by) as u32).max(1).min(max_radius), + ((b / scale_by) as u32).max(1).min(max_radius), + ((c / scale_by) as u32).max(1).min(max_radius), + ((d / scale_by) as u32).max(1).min(max_radius), + ] + }); + image = scratch.as_ref(); + } pixels.draw_pixmap( (bounds.x / width_scale) as i32, @@ -128,3 +147,131 @@ struct Entry { height: u32, pixels: Vec, } + +// https://users.rust-lang.org/t/how-to-trim-image-to-circle-image-without-jaggy/70374/2 +fn round(img: &mut tiny_skia::PixmapMut<'_>, radius: [u32; 4]) { + let (width, height) = (img.width(), img.height()); + assert!(radius[0] + radius[1] <= width); + assert!(radius[3] + radius[2] <= width); + assert!(radius[0] + radius[3] <= height); + assert!(radius[1] + radius[2] <= height); + + // top left + border_radius(img, radius[0], |x, y| (x - 1, y - 1)); + // top right + border_radius(img, radius[1], |x, y| (width - x, y - 1)); + // bottom right + border_radius(img, radius[2], |x, y| (width - x, height - y)); + // bottom left + border_radius(img, radius[3], |x, y| (x - 1, height - y)); +} + +fn border_radius( + img: &mut tiny_skia::PixmapMut<'_>, + r: u32, + coordinates: impl Fn(u32, u32) -> (u32, u32), +) { + if r == 0 { + return; + } + let r0 = r; + + // 16x antialiasing: 16x16 grid creates 256 possible shades, great for u8! + let r = 16 * r; + + let mut x = 0; + let mut y = r - 1; + let mut p: i32 = 2 - r as i32; + + // ... + + let mut alpha: u16 = 0; + let mut skip_draw = true; + + fn pixel_id(width: u32, (x, y): (u32, u32)) -> usize { + ((width as usize * y as usize) + x as usize) * 4 + } + + let clear_pixel = |img: &mut tiny_skia::PixmapMut<'_>, + (x, y): (u32, u32)| { + let pixel = pixel_id(img.width(), (x, y)); + img.data_mut()[pixel..pixel + 4].copy_from_slice(&[0; 4]); + }; + + let draw = |img: &mut tiny_skia::PixmapMut<'_>, alpha, x, y| { + debug_assert!((1..=256).contains(&alpha)); + let pixel = pixel_id(img.width(), coordinates(r0 - x, r0 - y)); + let pixel_alpha = &mut img.data_mut()[pixel + 3]; + *pixel_alpha = ((alpha * *pixel_alpha as u16 + 128) / 256) as u8; + }; + + 'l: loop { + // (comments for bottom_right case:) + // remove contents below current position + { + let i = x / 16; + for j in y / 16 + 1..r0 { + clear_pixel(img, coordinates(r0 - i, r0 - j)); + } + } + // remove contents right of current position mirrored + { + let j = x / 16; + for i in y / 16 + 1..r0 { + clear_pixel(img, coordinates(r0 - i, r0 - j)); + } + } + + // draw when moving to next pixel in x-direction + if !skip_draw { + draw(img, alpha, x / 16 - 1, y / 16); + draw(img, alpha, y / 16, x / 16 - 1); + alpha = 0; + } + + for _ in 0..16 { + skip_draw = false; + + if x >= y { + break 'l; + } + + alpha += y as u16 % 16 + 1; + if p < 0 { + x += 1; + p += (2 * x + 2) as i32; + } else { + // draw when moving to next pixel in y-direction + if y % 16 == 0 { + draw(img, alpha, x / 16, y / 16); + draw(img, alpha, y / 16, x / 16); + skip_draw = true; + alpha = (x + 1) as u16 % 16 * 16; + } + + x += 1; + p -= (2 * (y - x) + 2) as i32; + y -= 1; + } + } + } + + // one corner pixel left + if x / 16 == y / 16 { + // column under current position possibly not yet accounted + if x == y { + alpha += y as u16 % 16 + 1; + } + let s = y as u16 % 16 + 1; + let alpha = 2 * alpha - s * s; + draw(img, alpha, x / 16, y / 16); + } + + // remove remaining square of content in the corner + let range = y / 16 + 1..r0; + for i in range.clone() { + for j in range.clone() { + clear_pixel(img, coordinates(r0 - i, r0 - j)); + } + } +} diff --git a/tiny_skia/src/settings.rs b/tiny_skia/src/settings.rs index 672c49f32d..03361d21ce 100644 --- a/tiny_skia/src/settings.rs +++ b/tiny_skia/src/settings.rs @@ -19,7 +19,7 @@ impl Default for Settings { fn default() -> Settings { Settings { default_font: Font::default(), - default_text_size: Pixels(16.0), + default_text_size: Pixels(14.0), } } } diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index 0fc3d1f71b..4924d1f3bc 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -163,7 +163,7 @@ impl Pipeline { ) { let mut font_system = font_system().write().expect("Write font system"); - let (width, height) = buffer.size(); + let (width_opt, height_opt) = buffer.size(); draw( font_system.raw(), @@ -172,8 +172,8 @@ impl Pipeline { Rectangle::new( position, Size::new( - width.unwrap_or(pixels.width() as f32), - height.unwrap_or(pixels.height() as f32), + width_opt.unwrap_or(pixels.width() as f32), + height_opt.unwrap_or(pixels.height() as f32), ), ), color, diff --git a/tiny_skia/src/vector.rs b/tiny_skia/src/vector.rs index ea7de2150b..4eb9add55a 100644 --- a/tiny_skia/src/vector.rs +++ b/tiny_skia/src/vector.rs @@ -50,6 +50,7 @@ impl Pipeline { image, &tiny_skia::PixmapPaint { opacity, + quality: tiny_skia::FilterQuality::Bicubic, ..tiny_skia::PixmapPaint::default() }, transform, diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 153af6d562..cc887d04bb 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -72,7 +72,7 @@ impl crate::graphics::Compositor for Compositor { clip_mask: tiny_skia::Mask::new(width, height) .expect("Create clip mask"), layer_stack: VecDeque::new(), - background_color: Color::BLACK, + background_color: Color::TRANSPARENT, max_age: 0, }; @@ -181,11 +181,15 @@ pub fn present>( }) .unwrap_or_else(|| vec![Rectangle::with_size(viewport.logical_size())]); - if damage.is_empty() { - return Ok(()); - } - + // TODO(POP): I tried to adapt this to what I saw in the diff, which was essentially making sure this is called + // before the damage.is_empty() check surface.layer_stack.push_front(renderer.layers().to_vec()); + + // TODO better handling of no damage. As it is, the winit shell does not handle a skipped present well. + // if damage.is_empty() { + // return Err(compositor::SurfaceError::NoDamage); + // } + surface.background_color = background_color; let damage = @@ -198,6 +202,9 @@ pub fn present>( ) .expect("Create pixel map"); + // let damage = damage::group(damage, scale_factor, physical_size); + // TODO(POP): Is this something that needs to be adapted? + renderer.draw( &mut pixels, &mut surface.clip_mask, diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index a8ebf3aa85..35af5870be 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -45,3 +45,20 @@ lyon.optional = true resvg.workspace = true resvg.optional = true + +[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] +rustix = { version = "0.38" } +raw-window-handle.workspace = true +sctk.workspace = true +wayland-protocols.workspace = true +wayland-backend = { version = "0.3.3", features = ["client_system"] } +wayland-client = { version = "0.31.2" } +wayland-sys = { version = "0.31.1", features = ["dlopen"] } +as-raw-xcb-connection = "1.0.1" +tiny-xlib = "0.2.3" +x11rb = { version = "0.13.1", features = [ + "allow-unsafe-code", + "dl-libxcb", + "dri3", + "randr", +] } diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index d2ffda53e1..8ad4c758e2 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -422,7 +422,10 @@ impl geometry::frame::Backend for Frame { image.rotation += external_rotation; - self.images.push(Image::Raster(image, bounds)); + self.images.push(Image::Raster { + handle: image, + bounds, + }); } fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into) { @@ -433,7 +436,10 @@ impl geometry::frame::Backend for Frame { svg.rotation += external_rotation; - self.images.push(Image::Vector(svg, bounds)); + self.images.push(Image::Vector { + handle: svg, + bounds, + }); } } diff --git a/wgpu/src/image/mod.rs b/wgpu/src/image/mod.rs index cf83c3f203..1735960f8b 100644 --- a/wgpu/src/image/mod.rs +++ b/wgpu/src/image/mod.rs @@ -225,18 +225,18 @@ impl Pipeline { for image in images { match &image { #[cfg(feature = "image")] - Image::Raster(image, bounds) => { + Image::Raster { handle, bounds } => { if let Some(atlas_entry) = - cache.upload_raster(device, encoder, &image.handle) + cache.upload_raster(device, encoder, &handle.handle) { add_instances( [bounds.x, bounds.y], [bounds.width, bounds.height], - f32::from(image.rotation), - image.opacity, - image.snap, + f32::from(handle.rotation), + handle.opacity, + handle.snap, atlas_entry, - match image.filter_method { + match handle.filter_method { crate::core::image::FilterMethod::Nearest => { nearest_instances } @@ -251,22 +251,22 @@ impl Pipeline { Image::Raster { .. } => {} #[cfg(feature = "svg")] - Image::Vector(svg, bounds) => { + Image::Vector { handle, bounds } => { let size = [bounds.width, bounds.height]; if let Some(atlas_entry) = cache.upload_vector( device, encoder, - &svg.handle, - svg.color, + &handle.handle, + handle.color, size, scale, ) { add_instances( [bounds.x, bounds.y], size, - f32::from(svg.rotation), - svg.opacity, + f32::from(handle.rotation), + handle.opacity, true, atlas_entry, nearest_instances, @@ -595,12 +595,12 @@ fn add_instance( _rotation: rotation, _opacity: opacity, _position_in_atlas: [ - (x as f32 + 0.5) / atlas::SIZE as f32, - (y as f32 + 0.5) / atlas::SIZE as f32, + x as f32 / atlas::SIZE as f32, + y as f32 / atlas::SIZE as f32, ], _size_in_atlas: [ - (width as f32 - 1.0) / atlas::SIZE as f32, - (height as f32 - 1.0) / atlas::SIZE as f32, + width as f32 / atlas::SIZE as f32, + height as f32 / atlas::SIZE as f32, ], _layer: layer as u32, _snap: snap as u32, diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 68d5a015bf..5340a66b90 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -1,3 +1,4 @@ +use crate::core::Radians; use crate::core::{ self, renderer, Background, Color, Point, Rectangle, Svg, Transformation, }; @@ -114,11 +115,11 @@ impl Layer { pub fn draw_image(&mut self, image: Image, transformation: Transformation) { match image { - Image::Raster(image, bounds) => { - self.draw_raster(image, bounds, transformation); + Image::Raster { handle, bounds } => { + self.draw_raster(handle, bounds, transformation); } - Image::Vector(svg, bounds) => { - self.draw_svg(svg, bounds, transformation); + Image::Vector { handle, bounds } => { + self.draw_svg(handle, bounds, transformation); } } } @@ -129,7 +130,10 @@ impl Layer { bounds: Rectangle, transformation: Transformation, ) { - let image = Image::Raster(image, bounds * transformation); + let image = Image::Raster { + handle: image, + bounds: bounds * transformation, + }; self.images.push(image); } @@ -140,7 +144,10 @@ impl Layer { bounds: Rectangle, transformation: Transformation, ) { - let svg = Image::Vector(svg, bounds * transformation); + let svg = Image::Vector { + handle: svg, + bounds: (bounds * transformation), + }; self.images.push(svg); } diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index d79f0dc85f..c0703996c1 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -49,6 +49,7 @@ use buffer::Buffer; pub use iced_graphics as graphics; pub use iced_graphics::core; +use iced_graphics::text::Raw; pub use wgpu; @@ -61,8 +62,8 @@ pub use settings::Settings; pub use geometry::Geometry; use crate::core::{ - Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, - Vector, + image::FilterMethod, Background, Color, Font, Pixels, Point, Radians, + Rectangle, Size, Transformation, Vector, }; use crate::graphics::text::{Editor, Paragraph}; use crate::graphics::Viewport; @@ -407,7 +408,7 @@ impl Renderer { font: Font::MONOSPACE, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, - shaping: core::text::Shaping::Basic, + shaping: core::text::Shaping::Advanced, wrapping: core::text::Wrapping::Word, }; @@ -466,6 +467,7 @@ impl core::text::Renderer for Renderer { type Font = Font; type Paragraph = Paragraph; type Editor = Editor; + type Raw = Raw; const ICON_FONT: Font = Font::with_name("Iced-Icons"); const CHECKMARK_ICON: char = '\u{f00c}'; @@ -518,6 +520,10 @@ impl core::text::Renderer for Renderer { let (layer, transformation) = self.layers.current_mut(); layer.draw_text(text, position, color, clip_bounds, transformation); } + + fn fill_raw(&mut self, raw: Self::Raw) { + // TODO + } } #[cfg(feature = "image")] @@ -528,9 +534,28 @@ impl core::image::Renderer for Renderer { self.image_cache.borrow_mut().measure_image(handle) } - fn draw_image(&mut self, image: core::Image, bounds: Rectangle) { + fn draw_image( + &mut self, + handle: Self::Handle, + filter_method: FilterMethod, + bounds: Rectangle, + rotation: Radians, + opacity: f32, + border_radius: [f32; 4], + ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_raster(image, bounds, transformation); + layer.draw_raster( + crate::core::Image { + handle, + filter_method, + rotation, + opacity, + snap: true, + border_radius, + }, + bounds, + transformation, + ); } } @@ -540,9 +565,9 @@ impl core::svg::Renderer for Renderer { self.image_cache.borrow_mut().measure_svg(handle) } - fn draw_svg(&mut self, svg: core::Svg, bounds: Rectangle) { + fn draw_svg(&mut self, handle: core::svg::Svg, bounds: Rectangle) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_svg(svg, bounds, transformation); + layer.draw_svg(handle, bounds, transformation); } } diff --git a/wgpu/src/offscreen.rs b/wgpu/src/offscreen.rs new file mode 100644 index 0000000000..29913d0244 --- /dev/null +++ b/wgpu/src/offscreen.rs @@ -0,0 +1,102 @@ +use std::borrow::Cow; + +/// A simple compute pipeline to convert any texture to Rgba8UnormSrgb. +#[derive(Debug)] +pub struct Pipeline { + pipeline: wgpu::ComputePipeline, + layout: wgpu::BindGroupLayout, +} + +impl Pipeline { + pub fn new(device: &wgpu::Device) -> Self { + let shader = + device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("iced_wgpu.offscreen.blit.shader"), + source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!( + "shader/offscreen_blit.wgsl" + ))), + }); + + let bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("iced_wgpu.offscreen.blit.bind_group_layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::COMPUTE, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { + filterable: false, + }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::COMPUTE, + ty: wgpu::BindingType::StorageTexture { + access: wgpu::StorageTextureAccess::WriteOnly, + format: wgpu::TextureFormat::Rgba8Unorm, + view_dimension: wgpu::TextureViewDimension::D2, + }, + count: None, + }, + ], + }); + + let pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("iced_wgpu.offscreen.blit.pipeline_layout"), + bind_group_layouts: &[&bind_group_layout], + push_constant_ranges: &[], + }); + + let pipeline = + device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor { + label: Some("iced_wgpu.offscreen.blit.pipeline"), + layout: Some(&pipeline_layout), + module: &shader, + entry_point: "main", + }); + + Self { + pipeline, + layout: bind_group_layout, + } + } + + pub fn convert( + &self, + device: &wgpu::Device, + extent: wgpu::Extent3d, + frame: &wgpu::TextureView, + view: &wgpu::TextureView, + encoder: &mut wgpu::CommandEncoder, + ) { + let bind = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu.offscreen.blit.bind_group"), + layout: &self.layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(frame), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(view), + }, + ], + }); + + let mut compute_pass = + encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { + label: Some("iced_wgpu.offscreen.blit.compute_pass"), + }); + + compute_pass.set_pipeline(&self.pipeline); + compute_pass.set_bind_group(0, &bind, &[]); + compute_pass.dispatch_workgroups(extent.width, extent.height, 1); + } +} diff --git a/wgpu/src/settings.rs b/wgpu/src/settings.rs index b3c3cf6ad6..b4f97a7d60 100644 --- a/wgpu/src/settings.rs +++ b/wgpu/src/settings.rs @@ -20,7 +20,7 @@ pub struct Settings { /// The default size of text. /// - /// By default, it will be set to `16.0`. + /// By default, it will be set to `14.0`. pub default_text_size: Pixels, /// The antialiasing strategy that will be used for triangle primitives. @@ -35,7 +35,7 @@ impl Default for Settings { present_mode: wgpu::PresentMode::AutoVsync, backends: wgpu::Backends::all(), default_font: Font::default(), - default_text_size: Pixels(16.0), + default_text_size: Pixels(14.0), antialiasing: None, } } diff --git a/wgpu/src/shader/offscreen_blit.wgsl b/wgpu/src/shader/offscreen_blit.wgsl new file mode 100644 index 0000000000..9c764c36dc --- /dev/null +++ b/wgpu/src/shader/offscreen_blit.wgsl @@ -0,0 +1,22 @@ +@group(0) @binding(0) var u_texture: texture_2d; +@group(0) @binding(1) var out_texture: texture_storage_2d; + +fn srgb(color: f32) -> f32 { + if (color <= 0.0031308) { + return 12.92 * color; + } else { + return (1.055 * (pow(color, (1.0/2.4)))) - 0.055; + } +} + +@compute @workgroup_size(1) +fn main(@builtin(global_invocation_id) id: vec3) { + // texture coord must be i32 due to a naga bug: + // https://github.com/gfx-rs/naga/issues/1997 + let coords = vec2(i32(id.x), i32(id.y)); + + let src: vec4 = textureLoad(u_texture, coords, 0); + let srgb_color: vec4 = vec4(srgb(src.x), srgb(src.y), srgb(src.z), src.w); + + textureStore(out_texture, coords, srgb_color); +} diff --git a/wgpu/src/window.rs b/wgpu/src/window.rs index 9545a14e5b..92f1687372 100644 --- a/wgpu/src/window.rs +++ b/wgpu/src/window.rs @@ -1,5 +1,41 @@ //! Display rendering results on windows. pub mod compositor; +#[cfg(all(unix, not(target_os = "macos")))] +mod wayland; +#[cfg(all(unix, not(target_os = "macos")))] +mod x11; pub use compositor::Compositor; pub use wgpu::Surface; + +#[cfg(all(unix, not(target_os = "macos")))] +use rustix::fs::{major, minor}; +#[cfg(all(unix, not(target_os = "macos")))] +use std::{fs::File, io::Read, path::PathBuf}; + +#[cfg(all(unix, not(target_os = "macos")))] +fn ids_from_dev(dev: u64) -> Option<(u16, u16)> { + let path = PathBuf::from(format!( + "/sys/dev/char/{}:{}/device", + major(dev), + minor(dev) + )); + let vendor = { + let path = path.join("vendor"); + let mut file = File::open(&path).ok()?; + let mut contents = String::new(); + let _ = file.read_to_string(&mut contents).ok()?; + u16::from_str_radix(contents.trim().trim_start_matches("0x"), 16) + .ok()? + }; + let device = { + let path = path.join("device"); + let mut file = File::open(&path).ok()?; + let mut contents = String::new(); + let _ = file.read_to_string(&mut contents).ok()?; + u16::from_str_radix(contents.trim().trim_start_matches("0x"), 16) + .ok()? + }; + + Some((vendor, device)) +} diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 56f33b5013..260f0171ae 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -7,6 +7,12 @@ use crate::graphics::{self, Viewport}; use crate::settings::{self, Settings}; use crate::{Engine, Renderer}; +#[cfg(all(unix, not(target_os = "macos")))] +use super::wayland::get_wayland_device_ids; +#[cfg(all(unix, not(target_os = "macos")))] +use super::x11::get_x11_device_ids; +use std::future::Future; + /// A window graphics backend for iced powered by `wgpu`. #[allow(missing_debug_implementations)] pub struct Compositor { @@ -54,6 +60,26 @@ impl Compositor { settings: Settings, compatible_window: Option, ) -> Result { + #[cfg(all(unix, not(target_os = "macos")))] + let ids = compatible_window.as_ref().and_then(|window| { + get_wayland_device_ids(window) + .or_else(|| get_x11_device_ids(window)) + }); + + // HACK: + // 1. If we specifically didn't select an nvidia gpu + // 2. and nobody set an adapter name, + // 3. and the user didn't request the high power pref + // => don't load the nvidia icd, as it might power on the gpu in hybrid setups causing severe delays + #[cfg(all(unix, not(target_os = "macos")))] + if !matches!(ids, Some((0x10de, _))) + && std::env::var_os("WGPU_ADAPTER_NAME").is_none() + && std::env::var("WGPU_POWER_PREF").as_deref() != Ok("high") + { + std::env::set_var("VK_LOADER_DRIVERS_DISABLE", "nvidia*"); + } + + // only load the instance after setting environment variables, this initializes the vulkan loader let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { backends: settings.backends, flags: if cfg!(feature = "strict-assertions") { @@ -66,6 +92,10 @@ impl Compositor { log::info!("{settings:#?}"); + let available_adapters = instance.enumerate_adapters(settings.backends); + + std::env::remove_var("VK_LOADER_DRIVERS_DISABLE"); + #[cfg(not(target_arch = "wasm32"))] if log::max_level() >= log::LevelFilter::Info { let available_adapters: Vec<_> = instance @@ -95,7 +125,63 @@ impl Compositor { .request_adapter(&adapter_options) .await .ok_or(Error::NoAdapterFound(format!("{:?}", adapter_options)))?; - + // start pop + // let mut adapter = None; + // #[cfg_attr(not(unix), allow(dead_code))] + // if std::env::var_os("WGPU_ADAPTER_NAME").is_none() { + // #[cfg(all(unix, not(target_os = "macos")))] + // if let Some((vendor_id, device_id)) = ids { + // adapter = available_adapters + // .into_iter() + // .filter(|adapter| { + // let info = adapter.get_info(); + // info.device == device_id as u32 + // && info.vendor == vendor_id as u32 + // }) + // .find(|adapter| { + // if let Some(surface) = compatible_surface.as_ref() { + // adapter.is_surface_supported(surface) + // } else { + // true + // } + // }); + // } + // } else if let Ok(name) = std::env::var("WGPU_ADAPTER_NAME") { + // adapter = available_adapters + // .into_iter() + // .filter(|adapter| { + // let info = adapter.get_info(); + // info.name == name + // }) + // .find(|adapter| { + // if let Some(surface) = compatible_surface.as_ref() { + // adapter.is_surface_supported(surface) + // } else { + // true + // } + // }); + // } + + // let adapter = + // match adapter { + // Some(adapter) => adapter, + // None => instance + // .request_adapter(&wgpu::RequestAdapterOptions { + // 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, + // }) + // .await?, + // }; + // end pop + // TODO(POP): Merge conflict ensued with above stuff, is your code still needed? log::info!("Selected: {:#?}", adapter.get_info()); let (format, alpha_mode) = compatible_surface @@ -332,6 +418,21 @@ impl graphics::Compositor for Compositor { width: u32, height: u32, ) { + let caps = surface.get_capabilities(&self.adapter); + let alpha_mode = if caps + .alpha_modes + .contains(&wgpu::CompositeAlphaMode::PostMultiplied) + { + wgpu::CompositeAlphaMode::PostMultiplied + } else if caps + .alpha_modes + .contains(&wgpu::CompositeAlphaMode::PreMultiplied) + { + wgpu::CompositeAlphaMode::PreMultiplied + } else { + wgpu::CompositeAlphaMode::Auto + }; + surface.configure( &self.device, &wgpu::SurfaceConfiguration { @@ -340,7 +441,7 @@ impl graphics::Compositor for Compositor { present_mode: self.settings.present_mode, width, height, - alpha_mode: self.alpha_mode, + alpha_mode, view_formats: vec![], desired_maximum_frame_latency: 1, }, diff --git a/wgpu/src/window/wayland.rs b/wgpu/src/window/wayland.rs new file mode 100644 index 0000000000..b50f5d6909 --- /dev/null +++ b/wgpu/src/window/wayland.rs @@ -0,0 +1,115 @@ +use crate::graphics::compositor::Window; +use raw_window_handle::{RawDisplayHandle, WaylandDisplayHandle}; +use sctk::{ + dmabuf::{DmabufFeedback, DmabufHandler, DmabufState}, + registry::{ProvidesRegistryState, RegistryState}, + registry_handlers, +}; +use wayland_client::{ + backend::Backend, globals::registry_queue_init, protocol::wl_buffer, + Connection, QueueHandle, +}; +use wayland_protocols::wp::linux_dmabuf::zv1::client::{ + zwp_linux_buffer_params_v1, zwp_linux_dmabuf_feedback_v1, +}; + +struct AppData { + registry_state: RegistryState, + dmabuf_state: DmabufState, + feedback: Option, +} + +impl DmabufHandler for AppData { + fn dmabuf_state(&mut self) -> &mut DmabufState { + &mut self.dmabuf_state + } + + fn dmabuf_feedback( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _proxy: &zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1, + feedback: DmabufFeedback, + ) { + self.feedback = Some(feedback); + } + + fn created( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _params: &zwp_linux_buffer_params_v1::ZwpLinuxBufferParamsV1, + _buffer: wl_buffer::WlBuffer, + ) { + } + + fn failed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _params: &zwp_linux_buffer_params_v1::ZwpLinuxBufferParamsV1, + ) { + } + + fn released( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _buffer: &wl_buffer::WlBuffer, + ) { + } +} + +impl ProvidesRegistryState for AppData { + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } + registry_handlers![,]; +} + +pub fn get_wayland_device_ids(window: &W) -> Option<(u16, u16)> { + if !wayland_sys::client::is_lib_available() { + return None; + } + + let conn = match window.display_handle().map(|handle| handle.as_raw()) { + #[allow(unsafe_code)] + Ok(RawDisplayHandle::Wayland(WaylandDisplayHandle { + display, .. + })) => Connection::from_backend(unsafe { + Backend::from_foreign_display(display.as_ptr() as *mut _) + }), + _ => { + return None; + } + }; + + let (globals, mut event_queue) = registry_queue_init(&conn).unwrap(); + let qh = event_queue.handle(); + + let mut app_data = AppData { + registry_state: RegistryState::new(&globals), + dmabuf_state: DmabufState::new(&globals, &qh), + feedback: None, + }; + + match app_data.dmabuf_state.version() { + Some(4..) => { + let _ = app_data.dmabuf_state.get_default_feedback(&qh).unwrap(); + + let feedback = loop { + let _ = event_queue.blocking_dispatch(&mut app_data).ok()?; + if let Some(feedback) = app_data.feedback.as_ref() { + break feedback; + } + }; + + let dev = feedback.main_device(); + super::ids_from_dev(dev) + } + _ => None, + } +} + +sctk::delegate_dmabuf!(AppData); +sctk::delegate_registry!(AppData); diff --git a/wgpu/src/window/x11.rs b/wgpu/src/window/x11.rs new file mode 100644 index 0000000000..58da401a2e --- /dev/null +++ b/wgpu/src/window/x11.rs @@ -0,0 +1,171 @@ +use std::{ + fs, + io::{BufRead, BufReader}, + path::Path, +}; + +use crate::graphics::compositor::Window; + +use as_raw_xcb_connection::AsRawXcbConnection; +use raw_window_handle::{ + RawDisplayHandle, XcbDisplayHandle, XlibDisplayHandle, +}; +use rustix::fs::{fstat, stat}; +use tiny_xlib::Display; +use x11rb::{ + connection::{Connection, RequestConnection}, + protocol::{ + dri3::{ConnectionExt as _, X11_EXTENSION_NAME as DRI3_NAME}, + randr::{ + ConnectionExt as _, ProviderCapability, + X11_EXTENSION_NAME as RANDR_NAME, + }, + }, + xcb_ffi::XCBConnection, +}; + +pub fn get_x11_device_ids(window: &W) -> Option<(u16, u16)> { + x11rb::xcb_ffi::load_libxcb().ok()?; + + #[allow(unsafe_code)] + let (conn, screen) = match window + .display_handle() + .map(|handle| handle.as_raw()) + { + #[allow(unsafe_code)] + Ok(RawDisplayHandle::Xlib(XlibDisplayHandle { + display, + screen, + .. + })) => match display { + Some(ptr) => unsafe { + let xlib_display = Display::from_ptr(ptr.as_ptr()); + let conn = XCBConnection::from_raw_xcb_connection( + xlib_display.as_raw_xcb_connection() as *mut _, + false, + ) + .ok(); + // intentially leak the display, we don't want to close the connection + + (conn?, screen) + }, + None => (XCBConnection::connect(None).ok()?.0, screen), + }, + Ok(RawDisplayHandle::Xcb(XcbDisplayHandle { + connection, + screen, + .. + })) => match connection { + Some(ptr) => ( + unsafe { + XCBConnection::from_raw_xcb_connection(ptr.as_ptr(), false) + .ok()? + }, + screen, + ), + None => (XCBConnection::connect(None).ok()?.0, screen), + }, + _ => { + return None; + } + }; + let root = conn.setup().roots[screen as usize].root; + + // The nvidia xorg driver advertises DRI2 and DRI3, + // but doesn't really return any useful data for either of them. + // We also can't query EGL, as a display created from an X11 display + // running on the properietary driver won't return an EGLDevice. + // + // So we have to resort to hacks. + + // check for randr + let _ = conn.extension_information(RANDR_NAME).ok()??; + // check version, because we need providers to exist + let version = conn.randr_query_version(1, 4).ok()?.reply().ok()?; + if version.major_version < 1 + || (version.major_version == 1 && version.minor_version < 4) + { + return None; + } + + // get the name of the first Source Output provider, that will be our main device + let randr = conn.randr_get_providers(root).ok()?.reply().ok()?; + let mut name = None; + for provider in randr.providers { + let info = conn + .randr_get_provider_info(provider, randr.timestamp) + .ok()? + .reply() + .ok()?; + if info + .capabilities + .contains(ProviderCapability::SOURCE_OUTPUT) + || name.is_none() + { + name = std::str::from_utf8(&info.name) + .ok() + .map(ToString::to_string); + } + } + + // if that name is formatted `NVIDIA-x`, then x represents the /dev/nvidiaX number, which we can relate to /dev/dri + if let Some(number) = name.and_then(|name| { + name.trim().strip_prefix("NVIDIA-")?.parse::().ok() + }) { + // let it be known, that I hate this "interface"... + for busid in fs::read_dir("/proc/driver/nvidia/gpus") + .ok()? + .map(Result::ok) + .flatten() + { + for line in BufReader::new( + fs::File::open(busid.path().join("information")).ok()?, + ) + .lines() + { + if let Ok(line) = line { + if line.starts_with("Device Minor") { + if let Some((_, num)) = line.split_once(":") { + let minor = num.trim().parse::().ok()?; + if minor == number { + // we found the device + for device in fs::read_dir( + Path::new("/sys/module/nvidia/drivers/pci:nvidia/") + .join(busid.file_name()) + .join("drm"), + ) + .ok()? + .map(Result::ok) + .flatten() + { + let device = device.file_name(); + if device.to_string_lossy().starts_with("card") + || device.to_string_lossy().starts_with("render") + { + let stat = + stat(Path::new("/dev/dri").join(device)).ok()?; + let dev = stat.st_rdev; + return super::ids_from_dev(dev); + } + } + } + } + } + } + } + } + + None + } else { + // check via DRI3 + let _ = conn.extension_information(DRI3_NAME).ok()??; + // we have dri3, dri3_open exists on any version, so skip version checks. + + // provider being NONE tells the X server to use the RandR provider. + let dri3 = conn.dri3_open(root, x11rb::NONE).ok()?.reply().ok()?; + let device_fd = dri3.device_fd; + let stat = fstat(device_fd).ok()?; + let dev = stat.st_rdev; + super::ids_from_dev(dev) + } +} diff --git a/widget/Cargo.toml b/widget/Cargo.toml index 98a8114582..1aec15e56d 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -27,16 +27,23 @@ wgpu = ["iced_renderer/wgpu"] markdown = ["dep:pulldown-cmark", "dep:url"] highlighter = ["dep:iced_highlighter"] advanced = [] +a11y = ["iced_accessibility"] +wayland = ["sctk", "iced_runtime/wayland"] [dependencies] iced_renderer.workspace = true iced_runtime.workspace = true - +iced_accessibility.workspace = true +iced_accessibility.optional = true +sctk.workspace = true +sctk.optional = true num-traits.workspace = true once_cell.workspace = true rustc-hash.workspace = true thiserror.workspace = true unicode-segmentation.workspace = true +window_clipboard.workspace = true +dnd.workspace = true ouroboros.workspace = true ouroboros.optional = true diff --git a/widget/src/button.rs b/widget/src/button.rs index a3394a013a..5d4e580a4e 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -16,6 +16,13 @@ //! button("Press me!").on_press(Message::ButtonPressed).into() //! } //! ``` +//! Allow your users to perform actions by pressing a button. +use iced_runtime::core::border::Radius; +use iced_runtime::core::widget::Id; +use iced_runtime::{keyboard, task, Task}; +#[cfg(feature = "a11y")] +use std::borrow::Cow; + use crate::core::border::{self, Border}; use crate::core::event::{self, Event}; use crate::core::layout; @@ -31,6 +38,8 @@ use crate::core::{ Shadow, Shell, Size, Theme, Vector, Widget, }; +use iced_renderer::core::widget::operation; + /// A generic widget that produces a message when pressed. /// /// # Example @@ -76,6 +85,13 @@ where { content: Element<'a, Message, Theme, Renderer>, on_press: Option>, + id: Id, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, + #[cfg(feature = "a11y")] + label: Option>, width: Length, height: Length, padding: Padding, @@ -111,6 +127,13 @@ where Button { content, + id: Id::unique(), + #[cfg(feature = "a11y")] + name: None, + #[cfg(feature = "a11y")] + description: None, + #[cfg(feature = "a11y")] + label: None, on_press: None, width: size.width.fluid(), height: size.height.fluid(), @@ -195,11 +218,54 @@ where self.class = class.into(); self } + + /// Sets the [`Id`] of the [`Button`]. + pub fn id(mut self, id: Id) -> Self { + self.id = id; + self + } + + #[cfg(feature = "a11y")] + /// Sets the name of the [`Button`]. + pub fn name(mut self, name: impl Into>) -> Self { + self.name = Some(name.into()); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description_widget( + mut self, + description: &T, + ) -> Self { + self.description = Some(iced_accessibility::Description::Id( + description.description(), + )); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description(mut self, description: impl Into>) -> Self { + self.description = + Some(iced_accessibility::Description::Text(description.into())); + self + } + + #[cfg(feature = "a11y")] + /// Sets the label of the [`Button`]. + pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self { + self.label = + Some(label.label().into_iter().map(|l| l.into()).collect()); + self + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] struct State { + is_hovered: bool, is_pressed: bool, + is_focused: bool, } impl<'a, Message, Theme, Renderer> Widget @@ -221,8 +287,8 @@ where vec![Tree::new(&self.content)] } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(std::slice::from_ref(&self.content)); + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(std::slice::from_mut(&mut self.content)) } fn size(&self) -> Size { @@ -328,9 +394,43 @@ where } } } - Event::Touch(touch::Event::FingerLost { .. }) => { + #[cfg(feature = "a11y")] + Event::A11y( + event_id, + iced_accessibility::accesskit::ActionRequest { action, .. }, + ) => { let state = tree.state.downcast_mut::(); - + if let Some(Some(on_press)) = (self.id == event_id + && matches!( + action, + iced_accessibility::accesskit::Action::Default + )) + .then(|| self.on_press.as_ref()) + { + state.is_pressed = false; + shell.publish(on_press.get()); + } + return event::Status::Captured; + } + Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { + if let Some(on_press) = self.on_press.as_ref() { + let state = tree.state.downcast_mut::(); + if state.is_focused + && matches!( + key, + keyboard::Key::Named(keyboard::key::Named::Enter) + ) + { + state.is_pressed = true; + shell.publish(on_press.get()); + return event::Status::Captured; + } + } + } + Event::Touch(touch::Event::FingerLost { .. }) + | Event::Mouse(mouse::Event::CursorLeft) => { + let state = tree.state.downcast_mut::(); + state.is_hovered = false; state.is_pressed = false; } _ => {} @@ -344,7 +444,7 @@ where tree: &Tree, renderer: &mut Renderer, theme: &Theme, - _style: &renderer::Style, + renderer_style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, @@ -397,6 +497,10 @@ where theme, &renderer::Style { text_color: style.text_color, + icon_color: style + .icon_color + .unwrap_or(renderer_style.icon_color), + scale_factor: renderer_style.scale_factor, }, content_layout, cursor, @@ -435,6 +539,90 @@ where translation, ) } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + p: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::{ + accesskit::{ + Action, DefaultActionVerb, NodeBuilder, NodeId, Rect, Role, + }, + A11yNode, A11yTree, + }; + + let child_layout = layout.children().next().unwrap(); + let child_tree = &state.children[0]; + let child_tree = + self.content + .as_widget() + .a11y_nodes(child_layout, child_tree, p); + + let Rectangle { + x, + y, + width, + height, + } = layout.bounds(); + let bounds = Rect::new( + x as f64, + y as f64, + (x + width) as f64, + (y + height) as f64, + ); + let is_hovered = state.state.downcast_ref::().is_hovered; + + let mut node = NodeBuilder::new(Role::Button); + node.add_action(Action::Focus); + node.add_action(Action::Default); + node.set_bounds(bounds); + if let Some(name) = self.name.as_ref() { + node.set_name(name.clone()); + } + match self.description.as_ref() { + Some(iced_accessibility::Description::Id(id)) => { + node.set_described_by( + id.iter() + .cloned() + .map(|id| NodeId::from(id)) + .collect::>(), + ); + } + Some(iced_accessibility::Description::Text(text)) => { + node.set_description(text.clone()); + } + None => {} + } + + if let Some(label) = self.label.as_ref() { + node.set_labelled_by(label.clone()); + } + + if self.on_press.is_none() { + node.set_disabled() + } + if is_hovered { + node.set_hovered() + } + node.set_default_action_verb(DefaultActionVerb::Click); + + A11yTree::node_with_child_tree( + A11yNode::new(node, self.id.clone()), + child_tree, + ) + } + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn set_id(&mut self, id: Id) { + self.id = id; + } } impl<'a, Message, Theme, Renderer> From> @@ -478,6 +666,14 @@ pub enum Status { pub struct Style { /// The [`Background`] of the button. pub background: Option, + /// The border radius of the button. + pub border_radius: Radius, + /// The border width of the button. + pub border_width: f32, + /// The border [`Color`] of the button. + pub border_color: Color, + /// The icon [`Color`] of the button. + pub icon_color: Option, /// The text [`Color`] of the button. pub text_color: Color, /// The [`Border`] of the button. @@ -494,12 +690,36 @@ impl Style { ..self } } + + // /// Returns whether the [`Button`] is currently focused or not. + // pub fn is_focused(&self) -> bool { + // self.is_focused + // } + + // /// Returns whether the [`Button`] is currently hovered or not. + // pub fn is_hovered(&self) -> bool { + // self.is_hovered + // } + + // /// Focuses the [`Button`]. + // pub fn focus(&mut self) { + // self.is_focused = true; + // } + + // /// Unfocuses the [`Button`]. + // pub fn unfocus(&mut self) { + // self.is_focused = false; + // } } impl Default for Style { fn default() -> Self { Self { background: None, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + icon_color: None, text_color: Color::BLACK, border: Border::default(), shadow: Shadow::default(), @@ -679,3 +899,22 @@ fn disabled(style: Style) -> Style { ..style } } + +/// Produces a [`Command`] that focuses the [`Button`] with the given [`Id`]. +pub fn focus(id: Id) -> Task { + task::widget(operation::focusable::focus(id)) +} + +impl operation::Focusable for State { + fn is_focused(&self) -> bool { + self.is_focused + } + + fn focus(&mut self) { + self.is_focused = true; + } + + fn unfocus(&mut self) { + self.is_focused = false; + } +} diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index 9fbccf824e..6bffccb730 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -233,6 +233,7 @@ where Some(Event::Keyboard(keyboard_event)) } core::Event::Window(_) => None, + _ => None, }; if let Some(canvas_event) = canvas_event { diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 819f0d9da1..3956561cec 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -30,6 +30,11 @@ //! } //! ``` //! ![Checkbox drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/checkbox.png?raw=true) +//! Show toggle controls using checkboxes. +use iced_runtime::core::widget::Id; +#[cfg(feature = "a11y")] +use std::borrow::Cow; + use crate::core::alignment; use crate::core::event::{self, Event}; use crate::core::layout; @@ -41,8 +46,8 @@ use crate::core::touch; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Background, Border, Clipboard, Color, Element, Layout, Length, Pixels, - Rectangle, Shell, Size, Theme, Widget, + id::Internal, Background, Border, Clipboard, Color, Element, Layout, + Length, Pixels, Rectangle, Shell, Size, Theme, Widget, }; /// A box that can be checked. @@ -87,6 +92,12 @@ pub struct Checkbox< Renderer: text::Renderer, Theme: Catalog, { + id: Id, + label_id: Id, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, is_checked: bool, on_toggle: Option Message + 'a>>, label: String, @@ -120,6 +131,12 @@ where /// * a boolean describing whether the [`Checkbox`] is checked or not pub fn new(label: impl Into, is_checked: bool) -> Self { Checkbox { + id: Id::unique(), + label_id: Id::unique(), + #[cfg(feature = "a11y")] + name: None, + #[cfg(feature = "a11y")] + description: None, is_checked, on_toggle: None, label: label.into(), @@ -136,7 +153,8 @@ where code_point: Renderer::CHECKMARK_ICON, size: None, line_height: text::LineHeight::default(), - shaping: text::Shaping::Basic, + shaping: text::Shaping::Advanced, + wrap: text::Wrapping::default(), }, class: Theme::default(), } @@ -243,6 +261,33 @@ where self.class = class.into(); self } + + #[cfg(feature = "a11y")] + /// Sets the name of the [`Button`]. + pub fn name(mut self, name: impl Into>) -> Self { + self.name = Some(name.into()); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description_widget( + mut self, + description: &T, + ) -> Self { + self.description = Some(iced_accessibility::Description::Id( + description.description(), + )); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description(mut self, description: impl Into>) -> Self { + self.description = + Some(iced_accessibility::Description::Text(description.into())); + self + } } impl<'a, Message, Theme, Renderer> Widget @@ -275,7 +320,7 @@ where layout::next_to_each_other( &limits.width(self.width), self.spacing, - |_| layout::Node::new(Size::new(self.size, self.size)), + |_| layout::Node::new(crate::core::Size::new(self.size, self.size)), |limits| { let state = tree .state @@ -389,6 +434,7 @@ where size, line_height, shaping, + wrap, } = &self.icon; let size = size.unwrap_or(Pixels(bounds.height * 0.7)); @@ -429,6 +475,87 @@ where ); } } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + _state: &Tree, + cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::{ + accesskit::{Action, NodeBuilder, NodeId, Rect, Role}, + A11yNode, A11yTree, + }; + + let bounds = layout.bounds(); + let is_hovered = cursor.is_over(bounds); + let Rectangle { + x, + y, + width, + height, + } = bounds; + + let bounds = Rect::new( + x as f64, + y as f64, + (x + width) as f64, + (y + height) as f64, + ); + + let mut node = NodeBuilder::new(Role::CheckBox); + node.add_action(Action::Focus); + node.add_action(Action::Default); + node.set_bounds(bounds); + if let Some(name) = self.name.as_ref() { + node.set_name(name.clone()); + } + match self.description.as_ref() { + Some(iced_accessibility::Description::Id(id)) => { + node.set_described_by( + id.iter() + .cloned() + .map(|id| NodeId::from(id)) + .collect::>(), + ); + } + Some(iced_accessibility::Description::Text(text)) => { + node.set_description(text.clone()); + } + None => {} + } + node.set_selected(self.is_checked); + if is_hovered { + node.set_hovered(); + } + node.add_action(Action::Default); + let mut label_node = NodeBuilder::new(Role::Label); + label_node.set_name(self.label.clone()); + // TODO proper label bounds + label_node.set_bounds(bounds); + + A11yTree::node_with_child_tree( + A11yNode::new(node, self.id.clone()), + A11yTree::leaf(label_node, self.label_id.clone()), + ) + } + fn id(&self) -> Option { + Some(Id(Internal::Set(vec![ + self.id.0.clone(), + self.label_id.0.clone(), + ]))) + } + + fn set_id(&mut self, id: Id) { + if let Id(Internal::Set(list)) = id { + if list.len() == 2 { + self.id.0 = list[0].clone(); + self.label_id.0 = list[1].clone(); + } + } + } } impl<'a, Message, Theme, Renderer> From> @@ -458,6 +585,8 @@ pub struct Icon { pub line_height: text::LineHeight, /// The shaping strategy of the icon. pub shaping: text::Shaping, + /// The wrap mode of the icon. + pub wrap: text::Wrapping, } /// The possible status of a [`Checkbox`]. diff --git a/widget/src/column.rs b/widget/src/column.rs index 213f68fc1a..ec0c059c28 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -205,8 +205,8 @@ where self.children.iter().map(Tree::new).collect() } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(&self.children); + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(self.children.as_mut_slice()); } fn size(&self) -> Size { @@ -355,6 +355,48 @@ where translation, ) } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::A11yTree; + A11yTree::join( + self.children + .iter() + .zip(layout.children()) + .zip(state.children.iter()) + .map(|((c, c_layout), state)| { + c.as_widget().a11y_nodes(c_layout, state, cursor) + }), + ) + } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut crate::core::clipboard::DndDestinationRectangles, + ) { + for ((e, layout), state) in self + .children + .iter() + .zip(layout.children()) + .zip(state.children.iter()) + { + e.as_widget().drag_destinations( + state, + layout, + renderer, + dnd_rectangles, + ); + } + } } impl<'a, Message, Theme, Renderer> From> diff --git a/widget/src/container.rs b/widget/src/container.rs index f4993ac935..9fe70d7c73 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -28,7 +28,7 @@ use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; -use crate::core::widget::{self, Operation}; +use crate::core::widget::{self, Id, Operation}; use crate::core::{ self, color, Background, Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shadow, Shell, Size, Theme, Vector, @@ -67,7 +67,6 @@ pub struct Container< Theme: Catalog, Renderer: core::Renderer, { - id: Option, padding: Padding, width: Length, height: Length, @@ -93,7 +92,6 @@ where let size = content.as_widget().size_hint(); Container { - id: None, padding: Padding::ZERO, width: size.width.fluid(), height: size.height.fluid(), @@ -107,12 +105,6 @@ where } } - /// Sets the [`Id`] of the [`Container`]. - pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); - self - } - /// Sets the [`Padding`] of the [`Container`]. pub fn padding>(mut self, padding: P) -> Self { self.padding = padding.into(); @@ -247,8 +239,8 @@ where self.content.as_widget().children() } - fn diff(&self, tree: &mut Tree) { - self.content.as_widget().diff(tree); + fn diff(&mut self, tree: &mut Tree) { + self.content.as_widget_mut().diff(tree); } fn size(&self) -> Size { @@ -285,7 +277,7 @@ where operation: &mut dyn Operation, ) { operation.container( - self.id.as_ref().map(|id| &id.0), + self.content.as_widget().id().as_ref(), layout.bounds(), &mut |operation| { self.content.as_widget().operate( @@ -359,9 +351,13 @@ where renderer, theme, &renderer::Style { + icon_color: style + .icon_color + .unwrap_or(renderer_style.icon_color), text_color: style .text_color .unwrap_or(renderer_style.text_color), + scale_factor: renderer_style.scale_factor, }, layout.children().next().unwrap(), cursor, @@ -388,6 +384,49 @@ where translation, ) } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + let c_layout = layout.children().next().unwrap(); + let c_state = state.children.get(0); + + self.content.as_widget().a11y_nodes( + c_layout, + c_state.unwrap_or(&Tree::empty()), + cursor, + ) + } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut crate::core::clipboard::DndDestinationRectangles, + ) { + if let Some(l) = layout.children().next() { + self.content.as_widget().drag_destinations( + state, + l, + renderer, + dnd_rectangles, + ); + } + } + + fn id(&self) -> Option { + self.content.as_widget().id().clone() + } + + fn set_id(&mut self, id: Id) { + self.content.as_widget_mut().set_id(id); + } } impl<'a, Message, Theme, Renderer> From> @@ -457,30 +496,6 @@ pub fn draw_background( } } -/// The identifier of a [`Container`]. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Id(widget::Id); - -impl Id { - /// Creates a custom [`Id`]. - pub fn new(id: impl Into>) -> Self { - Self(widget::Id::new(id)) - } - - /// Creates a unique [`Id`]. - /// - /// This function produces a different [`Id`] every time it is called. - pub fn unique() -> Self { - Self(widget::Id::unique()) - } -} - -impl From for widget::Id { - fn from(id: Id) -> Self { - id.0 - } -} - /// Produces a [`Task`] that queries the visible screen bounds of the /// [`Container`] with the given [`Id`]. pub fn visible_bounds(id: Id) -> Task> { @@ -574,6 +589,8 @@ pub fn visible_bounds(id: Id) -> Task> { /// The appearance of a container. #[derive(Debug, Clone, Copy, PartialEq, Default)] pub struct Style { + /// The icon [`Color`] of the container. + pub icon_color: Option, /// The text [`Color`] of the container. pub text_color: Option, /// The [`Background`] of the container. @@ -605,6 +622,8 @@ impl Style { pub fn background(self, background: impl Into) -> Self { Self { background: Some(background.into()), + icon_color: None, + text_color: None, ..self } } @@ -684,6 +703,7 @@ pub fn rounded_box(theme: &Theme) -> Style { let palette = theme.extended_palette(); Style { + icon_color: None, background: Some(palette.background.weak.color.into()), border: border::rounded(2), ..Style::default() diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 52290a54d5..e1f41ec585 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -27,6 +27,7 @@ use crate::vertical_slider::{self, VerticalSlider}; use crate::{Column, MouseArea, Row, Space, Stack, Themer}; use std::borrow::Borrow; + use std::ops::RangeInclusive; /// Creates a [`Column`] with the given children. @@ -391,8 +392,8 @@ where self.content.as_widget().children() } - fn diff(&self, tree: &mut Tree) { - self.content.as_widget().diff(tree); + fn diff(&mut self, tree: &mut Tree) { + self.content.as_widget_mut().diff(tree); } fn size(&self) -> Size { @@ -558,8 +559,8 @@ where vec![Tree::new(&self.base), Tree::new(&self.top)] } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(&[&self.base, &self.top]); + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(&mut [&mut self.base, &mut self.top]); } fn size(&self) -> Size { @@ -1624,7 +1625,10 @@ where /// ``` /// #[cfg(feature = "image")] -pub fn image(handle: impl Into) -> crate::Image { +#[cfg_attr(docsrs, doc(cfg(feature = "image")))] +pub fn image<'a, Handle>( + handle: impl Into, +) -> crate::Image<'a, Handle> { crate::Image::new(handle.into()) } diff --git a/widget/src/image.rs b/widget/src/image.rs index c8f2a620f6..2e80094078 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -17,6 +17,7 @@ //! ``` //! pub mod viewer; +use iced_runtime::core::widget::Id; pub use viewer::Viewer; use crate::core::image; @@ -31,6 +32,9 @@ use crate::core::{ pub use image::{FilterMethod, Handle}; +#[cfg(feature = "a11y")] +use std::borrow::Cow; + /// Creates a new [`Viewer`] with the given image `Handle`. pub fn viewer(handle: Handle) -> Viewer { Viewer::new(handle) @@ -55,7 +59,14 @@ pub fn viewer(handle: Handle) -> Viewer { /// ``` /// #[derive(Debug)] -pub struct Image { +pub struct Image<'a, Handle = image::Handle> { + id: Id, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, + #[cfg(feature = "a11y")] + label: Option>, handle: Handle, width: Length, height: Length, @@ -63,12 +74,21 @@ pub struct Image { filter_method: FilterMethod, rotation: Rotation, opacity: f32, + border_radius: [f32; 4], + phantom_data: std::marker::PhantomData<&'a ()>, } -impl Image { +impl<'a, Handle> Image<'a, Handle> { /// Creates a new [`Image`] with the given path. pub fn new(handle: impl Into) -> Self { Image { + id: Id::unique(), + #[cfg(feature = "a11y")] + name: None, + #[cfg(feature = "a11y")] + description: None, + #[cfg(feature = "a11y")] + label: None, handle: handle.into(), width: Length::Shrink, height: Length::Shrink, @@ -76,9 +96,17 @@ impl Image { filter_method: FilterMethod::default(), rotation: Rotation::default(), opacity: 1.0, + border_radius: [0.0; 4], + phantom_data: std::marker::PhantomData, } } + /// Sets the border radius of the image. + pub fn border_radius(mut self, border_radius: [f32; 4]) -> Self { + self.border_radius = border_radius; + self + } + /// Sets the width of the [`Image`] boundaries. pub fn width(mut self, width: impl Into) -> Self { self.width = width.into(); @@ -119,6 +147,41 @@ impl Image { self.opacity = opacity.into(); self } + + #[cfg(feature = "a11y")] + /// Sets the name of the [`Button`]. + pub fn name(mut self, name: impl Into>) -> Self { + self.name = Some(name.into()); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description_widget( + mut self, + description: &T, + ) -> Self { + self.description = Some(iced_accessibility::Description::Id( + description.description(), + )); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description(mut self, description: impl Into>) -> Self { + self.description = + Some(iced_accessibility::Description::Text(description.into())); + self + } + + #[cfg(feature = "a11y")] + /// Sets the label of the [`Button`]. + pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self { + self.label = + Some(label.label().into_iter().map(|l| l.into()).collect()); + self + } } /// Computes the layout of an [`Image`]. @@ -130,6 +193,7 @@ pub fn layout( height: Length, content_fit: ContentFit, rotation: Rotation, + _border_radius: [f32; 4], ) -> layout::Node where Renderer: image::Renderer, @@ -172,6 +236,7 @@ pub fn draw( filter_method: FilterMethod, rotation: Rotation, opacity: f32, + border_radius: [f32; 4], ) where Renderer: image::Renderer, Handle: Clone, @@ -203,16 +268,19 @@ pub fn draw( let drawing_bounds = Rectangle::new(position, final_size); + let offset = Vector::new( + (bounds.width - adjusted_fit.width).max(0.0) / 2.0, + (bounds.height - adjusted_fit.height).max(0.0) / 2.0, + ); + let render = |renderer: &mut Renderer| { renderer.draw_image( - image::Image { - handle: handle.clone(), - filter_method, - rotation: rotation.radians(), - opacity, - snap: true, - }, - drawing_bounds, + handle.clone(), + filter_method, + drawing_bounds + offset, + rotation.radians(), + opacity, + border_radius, ); }; @@ -224,8 +292,8 @@ pub fn draw( } } -impl Widget - for Image +impl<'a, Message, Theme, Renderer, Handle> Widget + for Image<'a, Handle> where Renderer: image::Renderer, Handle: Clone, @@ -251,6 +319,7 @@ where self.height, self.content_fit, self.rotation, + self.border_radius, ) } @@ -272,17 +341,78 @@ where self.filter_method, self.rotation, self.opacity, + self.border_radius, ); } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + _state: &Tree, + _cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::{ + accesskit::{NodeBuilder, NodeId, Rect, Role}, + A11yTree, + }; + + let bounds = layout.bounds(); + let Rectangle { + x, + y, + width, + height, + } = bounds; + let bounds = Rect::new( + x as f64, + y as f64, + (x + width) as f64, + (y + height) as f64, + ); + let mut node = NodeBuilder::new(Role::Image); + node.set_bounds(bounds); + if let Some(name) = self.name.as_ref() { + node.set_name(name.clone()); + } + match self.description.as_ref() { + Some(iced_accessibility::Description::Id(id)) => { + node.set_described_by( + id.iter() + .cloned() + .map(|id| NodeId::from(id)) + .collect::>(), + ); + } + Some(iced_accessibility::Description::Text(text)) => { + node.set_description(text.clone()); + } + None => {} + } + + if let Some(label) = self.label.as_ref() { + node.set_labelled_by(label.clone()); + } + + A11yTree::leaf(node, self.id.clone()) + } + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn set_id(&mut self, id: Id) { + self.id = id; + } } -impl<'a, Message, Theme, Renderer, Handle> From> +impl<'a, Message, Theme, Renderer, Handle> From> for Element<'a, Message, Theme, Renderer> where Renderer: image::Renderer, Handle: Clone + 'a, { - fn from(image: Image) -> Element<'a, Message, Theme, Renderer> { + fn from(image: Image<'a, Handle>) -> Element<'a, Message, Theme, Renderer> { Element::new(image) } } diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index b1aad22c79..d378315822 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -349,14 +349,12 @@ where let render = |renderer: &mut Renderer| { renderer.with_translation(translation, |renderer| { renderer.draw_image( - Image { - handle: self.handle.clone(), - filter_method: self.filter_method, - rotation: Radians(0.0), - opacity: 1.0, - snap: true, - }, + self.handle.clone(), + self.filter_method, drawing_bounds, + Radians(0.0), + 1.0, + [0.0; 4], ); }); }; diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs index 5852ede19d..6ec5ca4cf3 100644 --- a/widget/src/keyed/column.rs +++ b/widget/src/keyed/column.rs @@ -1,4 +1,6 @@ //! Keyed columns distribute content vertically while keeping continuity. +//! Distribute content vertically. + use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -223,7 +225,7 @@ where self.children.iter().map(Tree::new).collect() } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { let Tree { state, children, .. } = tree; @@ -232,11 +234,11 @@ where tree::diff_children_custom_with_search( children, - &self.children, - |tree, child| child.as_widget().diff(tree), + &mut self.children, + |tree, child| child.as_widget_mut().diff(tree), |index| { self.keys.get(index).or_else(|| self.keys.last()).copied() - != Some(state.keys[index]) + != state.keys.get(index).copied() }, |child| Tree::new(child.as_widget()), ); diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 232f254c2c..430a99a765 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -6,6 +6,7 @@ pub mod responsive; #[allow(deprecated)] pub use component::Component; +use iced_renderer::core::widget::Operation; pub use responsive::Responsive; mod cache; @@ -16,7 +17,7 @@ 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::widget::Widget; use crate::core::Element; use crate::core::{ self, Clipboard, Length, Point, Rectangle, Shell, Size, Vector, @@ -128,7 +129,7 @@ where self.with_element(|element| vec![Tree::new(element.as_widget())]) } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { let current = tree .state .downcast_mut::>(); @@ -147,8 +148,10 @@ where current.element = Rc::new(RefCell::new(Some(element))); (*self.element.borrow_mut()) = Some(current.element.clone()); - self.with_element(|element| { - tree.diff_children(std::slice::from_ref(&element.as_widget())); + self.with_element_mut(|element| { + tree.diff_children(std::slice::from_mut( + &mut element.as_widget_mut(), + )) }); } else { (*self.element.borrow_mut()) = Some(current.element.clone()); @@ -184,7 +187,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn crate::core::widget::Operation, ) { self.with_element(|element| { element.as_widget().operate( @@ -305,6 +308,40 @@ where None } } + + fn set_id(&mut self, _id: iced_runtime::core::id::Id) { + if let Some(e) = self.element.borrow_mut().as_mut() { + if let Some(e) = e.borrow_mut().as_mut() { + e.as_widget_mut().set_id(_id); + } + } + } + + fn id(&self) -> Option { + if let Some(e) = self.element.borrow().as_ref() { + if let Some(e) = e.borrow().as_ref() { + return e.as_widget().id(); + } + } + None + } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut core::clipboard::DndDestinationRectangles, + ) { + self.with_element(|element| { + element.as_widget().drag_destinations( + &state.children[0], + layout, + renderer, + dnd_rectangles, + ); + }); + } } #[self_referencing] diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index c7bc1264c7..cf366a3015 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -13,6 +13,7 @@ use crate::core::{ }; use crate::runtime::overlay::Nested; +use iced_renderer::core::widget::Operation; use ouroboros::self_referencing; use std::cell::RefCell; use std::marker::PhantomData; @@ -148,13 +149,13 @@ where Renderer: renderer::Renderer, { fn diff_self(&self) { - self.with_element(|element| { + self.with_element_mut(|element| { self.tree .borrow_mut() .borrow_mut() .as_mut() .unwrap() - .diff_children(std::slice::from_ref(&element)); + .diff_children(std::slice::from_mut(element)); }); } @@ -263,6 +264,7 @@ where fn state(&self) -> tree::State { let state = Rc::new(RefCell::new(Some(Tree { + id: None, tag: tree::Tag::of::>(), state: tree::State::new(S::default()), children: vec![Tree::empty()], @@ -275,7 +277,7 @@ where vec![] } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { let tree = tree.state.downcast_ref::>>>(); *self.tree.borrow_mut() = tree.clone(); self.rebuild_element_if_necessary(); @@ -489,6 +491,49 @@ where None } } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + tree: &Tree, + cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + let tree = tree.state.downcast_ref::>>>(); + self.with_element(|element| { + if let Some(tree) = tree.borrow().as_ref() { + element.as_widget().a11y_nodes( + layout, + &tree.children[0], + cursor, + ) + } else { + iced_accessibility::A11yTree::default() + } + }) + } + + fn drag_destinations( + &self, + tree: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut core::clipboard::DndDestinationRectangles, + ) { + let mut tree = tree + .state + .downcast_ref::>>>() + .borrow_mut(); + let mut tree = tree.as_ref().unwrap(); + self.with_element(|element| { + element.as_widget().drag_destinations( + &tree.children[0], + layout, + renderer, + dnd_rectangles, + ) + }); + } } struct Overlay<'a, 'b, Message, Theme, Renderer, Event, S>( diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index a7a99f56d6..e9ddf8c41a 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -3,7 +3,6 @@ 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, Vector, @@ -12,6 +11,7 @@ use crate::core::{ use crate::horizontal_space; use crate::runtime::overlay::Nested; +use iced_renderer::core::widget::Operation; use ouroboros::self_referencing; use std::cell::{RefCell, RefMut}; use std::marker::PhantomData; @@ -91,7 +91,7 @@ where self.size = new_size; self.layout = None; - tree.diff(&self.element); + tree.diff(&mut self.element); } fn resolve( @@ -162,7 +162,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn crate::core::widget::Operation, ) { let state = tree.state.downcast_mut::(); let mut content = self.content.borrow_mut(); @@ -326,6 +326,63 @@ where None } } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + tree: &Tree, + cursor_position: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use std::rc::Rc; + + let tree = tree.state.downcast_ref::>>>(); + if let Some(tree) = tree.borrow().as_ref() { + self.content.borrow().element.as_widget().a11y_nodes( + layout, + &tree.children[0], + cursor_position, + ) + } else { + iced_accessibility::A11yTree::default() + } + } + + fn id(&self) -> Option { + self.content.borrow().element.as_widget().id() + } + + fn set_id(&mut self, _id: iced_runtime::core::id::Id) { + self.content + .borrow_mut() + .element + .as_widget_mut() + .set_id(_id); + } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut core::clipboard::DndDestinationRectangles, + ) { + let ret = self.content.borrow_mut().resolve( + &mut state.state.downcast_ref::().tree.borrow_mut(), + renderer, + layout, + &self.view, + |tree, r, layout, element| { + element.as_widget().drag_destinations( + tree, + layout, + r, + dnd_rectangles, + ); + }, + ); + ret + } } impl<'a, Message, Theme, Renderer> @@ -462,7 +519,7 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn Operation, ) { let _ = self.with_overlay_mut_maybe(|overlay| { overlay.operate(layout, renderer, operation); diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index c5a37ae3f1..0eaddf5781 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -1,4 +1,7 @@ //! A container for capturing mouse events. + +use iced_renderer::core::mouse::Click; + use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -20,7 +23,9 @@ pub struct MouseArea< Renderer = crate::Renderer, > { content: Element<'a, Message, Theme, Renderer>, + on_drag: Option, on_press: Option, + on_double_press: Option, on_release: Option, on_double_click: Option, on_right_press: Option, @@ -35,12 +40,25 @@ pub struct MouseArea< } impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { + /// The message to emit when a drag is initiated. + #[must_use] + pub fn on_drag(mut self, message: Message) -> Self { + self.on_drag = Some(message); + self + } + /// 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 double button press. + #[must_use] + pub fn on_double_press(mut self, message: Message) -> Self { + self.on_double_press = Some(message); + self + } /// The message to emit on a left button release. #[must_use] @@ -133,12 +151,28 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { } /// Local state of the [`MouseArea`]. -#[derive(Default)] struct State { is_hovered: bool, bounds: Rectangle, cursor_position: Option, previous_click: Option, + // TODO: Support on_enter and on_exit + drag_initiated: Option, + is_out_of_bounds: bool, + last_click: Option, +} +impl Default for State { + fn default() -> Self { + Self { + is_hovered: Default::default(), + drag_initiated: Default::default(), + is_out_of_bounds: true, + last_click: Default::default(), + cursor_position: None, + bounds: Default::default(), + previous_click: None, + } + } } impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { @@ -148,7 +182,9 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { ) -> Self { MouseArea { content: content.into(), + on_drag: None, on_press: None, + on_double_press: None, on_release: None, on_double_click: None, on_right_press: None, @@ -182,8 +218,8 @@ where vec![Tree::new(&self.content)] } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(std::slice::from_ref(&self.content)); + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(std::slice::from_mut(&mut self.content)); } fn size(&self) -> Size { @@ -289,7 +325,6 @@ where viewport, ); } - fn overlay<'b>( &'b mut self, tree: &'b mut Tree, @@ -304,6 +339,22 @@ where translation, ) } + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut crate::core::clipboard::DndDestinationRectangles, + ) { + if let Some(state) = state.children.iter().next() { + self.content.as_widget().drag_destinations( + state, + layout, + renderer, + dnd_rectangles, + ); + } + } } impl<'a, Message, Theme, Renderer> From> @@ -331,14 +382,15 @@ fn update( shell: &mut Shell<'_, Message>, ) -> event::Status { let state: &mut State = tree.state.downcast_mut(); - let cursor_position = cursor.position(); - let bounds = layout.bounds(); - if state.cursor_position != cursor_position || state.bounds != bounds { + if let Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) = event + { let was_hovered = state.is_hovered; + let bounds = layout.bounds(); - state.is_hovered = cursor.is_over(layout.bounds()); + state.is_hovered = cursor.is_over(bounds); state.cursor_position = cursor_position; state.bounds = bounds; @@ -363,9 +415,45 @@ fn update( } if !cursor.is_over(layout.bounds()) { + if !state.is_out_of_bounds { + if widget + .on_enter + .as_ref() + .or(widget.on_exit.as_ref()) + .is_some() + { + if let Event::Mouse(mouse::Event::CursorMoved { .. }) = event { + state.is_out_of_bounds = true; + if let Some(message) = widget.on_exit.as_ref() { + shell.publish(message.clone()); + } + return event::Status::Captured; + } + } + } + return event::Status::Ignored; } + if let Some(message) = widget.on_double_press.as_ref() { + if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) = + event + { + if let Some(cursor_position) = cursor.position() { + let click = mouse::Click::new( + cursor_position, + mouse::Button::Left, + state.last_click, + ); + state.last_click = Some(click); + if let mouse::click::Kind::Double = click.kind() { + shell.publish(message.clone()); + return event::Status::Captured; + } + } + } + } + if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) = event { @@ -405,6 +493,7 @@ fn update( if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(touch::Event::FingerLifted { .. }) = event { + state.drag_initiated = None; shell.publish(message.clone()); return event::Status::Captured; @@ -462,5 +551,37 @@ fn update( } } + if let Some(message) = widget.on_enter.as_ref().or(widget.on_exit.as_ref()) + { + if let Event::Mouse(mouse::Event::CursorMoved { .. }) = event { + if state.is_out_of_bounds { + state.is_out_of_bounds = false; + if widget.on_enter.is_some() { + shell.publish(message.clone()); + } + return event::Status::Captured; + } + } + } + + if state.drag_initiated.is_none() && widget.on_drag.is_some() { + if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) = event + { + state.drag_initiated = cursor.position(); + } + } else if let Some((message, drag_source)) = + widget.on_drag.as_ref().zip(state.drag_initiated) + { + if let Some(position) = cursor.position() { + if position.distance(drag_source) > 1.0 { + state.drag_initiated = None; + shell.publish(message.clone()); + + return event::Status::Captured; + } + } + } + event::Status::Ignored } diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index b641e8f516..1c86791ecc 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -40,6 +40,7 @@ pub struct Menu< text_size: Option, text_line_height: text::LineHeight, text_shaping: text::Shaping, + text_wrap: text::Wrapping, font: Option, class: &'a ::Class<'b>, } @@ -73,7 +74,8 @@ where padding: Padding::ZERO, text_size: None, text_line_height: text::LineHeight::default(), - text_shaping: text::Shaping::Basic, + text_shaping: text::Shaping::Advanced, + text_wrap: text::Wrapping::default(), font: None, class, } @@ -112,6 +114,12 @@ where self } + /// Sets the [`text::Wrap`] mode of the [`Menu`]. + pub fn text_wrap(mut self, wrap: text::Wrapping) -> Self { + self.text_wrap = wrap; + self + } + /// Sets the font of the [`Menu`]. pub fn font(mut self, font: impl Into) -> Self { self.font = Some(font.into()); @@ -198,10 +206,11 @@ where text_size, text_line_height, text_shaping, + text_wrap, class, } = menu; - let list = Scrollable::new(List { + let mut list = Scrollable::new(List { options, hovered_option, on_selected, @@ -209,12 +218,13 @@ where font, text_size, text_line_height, + text_wrap, text_shaping, padding, class, }); - state.tree.diff(&list as &dyn Widget<_, _, _>); + state.tree.diff(&mut list as &mut dyn Widget<_, _, _>); Self { position, @@ -330,6 +340,7 @@ where text_size: Option, text_line_height: text::LineHeight, text_shaping: text::Shaping, + text_wrap: text::Wrapping, font: Option, class: &'a ::Class<'b>, } diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index e6fda66009..8ae4b3cd90 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -85,7 +85,6 @@ 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::{ self, Background, Border, Clipboard, Color, Element, Layout, Length, @@ -163,6 +162,7 @@ pub struct PaneGrid< spacing: f32, on_click: Option Message + 'a>>, on_drag: Option Message + 'a>>, + #[allow(clippy::type_complexity)] on_resize: Option<(f32, Box Message + 'a>)>, class: ::Class<'a>, } @@ -318,15 +318,20 @@ where .collect() } - fn diff(&self, tree: &mut Tree) { - match &self.contents { - Contents::All(contents, _) => tree.diff_children_custom( - contents, - |state, (_, content)| content.diff(state), - |(_, content)| content.state(), - ), + fn diff(&mut self, tree: &mut Tree) { + match &mut self.contents { + Contents::All(contents, _) => { + let ids = contents.iter().map(|_| None).collect(); // TODO + tree.diff_children_custom( + contents, + ids, + |state, (_, content)| content.diff(state), + |(_, content)| content.state(), + ) + } Contents::Maximized(_, content, _) => tree.diff_children_custom( - &[content], + &mut [content], + vec![None], // TODO |state, content| content.diff(state), |content| content.state(), ), @@ -376,7 +381,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn crate::core::widget::Operation, ) { operation.container(None, layout.bounds(), &mut |operation| { self.contents diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs index ec0676b188..ff6ce9ea34 100644 --- a/widget/src/pane_grid/content.rs +++ b/widget/src/pane_grid/content.rs @@ -1,3 +1,5 @@ +use iced_renderer::core::widget::Operation; + use crate::container; use crate::core::event::{self, Event}; use crate::core::layout; @@ -91,13 +93,13 @@ where } } - pub(super) fn diff(&self, tree: &mut Tree) { + pub(super) fn diff(&mut self, tree: &mut Tree) { if tree.children.len() == 2 { - if let Some(title_bar) = self.title_bar.as_ref() { + if let Some(title_bar) = self.title_bar.as_mut() { title_bar.diff(&mut tree.children[1]); } - tree.children[0].diff(&self.body); + tree.children[0].diff(&mut self.body); } else { *tree = self.state(); } diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs index 5002b4f73c..1495a575f2 100644 --- a/widget/src/pane_grid/title_bar.rs +++ b/widget/src/pane_grid/title_bar.rs @@ -1,10 +1,12 @@ +use iced_renderer::core::widget::Operation; + 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::widget::Tree; use crate::core::{ self, Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size, Vector, @@ -129,17 +131,17 @@ where } } - pub(super) fn diff(&self, tree: &mut Tree) { + pub(super) fn diff(&mut self, tree: &mut Tree) { if tree.children.len() == 3 { - if let Some(controls) = self.controls.as_ref() { - if let Some(compact) = controls.compact.as_ref() { + if let Some(controls) = self.controls.as_mut() { + if let Some(compact) = controls.compact.as_mut() { tree.children[2].diff(compact); } - tree.children[1].diff(&controls.full); + tree.children[1].diff(&mut controls.full); } - tree.children[0].diff(&self.content); + tree.children[0].diff(&mut self.content); } else { *tree = self.state(); } @@ -163,7 +165,9 @@ where let style = theme.style(&self.class); let inherited_style = renderer::Style { + icon_color: style.icon_color.unwrap_or(inherited_style.icon_color), text_color: style.text_color.unwrap_or(inherited_style.text_color), + scale_factor: inherited_style.scale_factor, }; container::draw_background(renderer, &style, bounds); @@ -374,7 +378,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn crate::core::widget::Operation, ) { let mut children = layout.children(); let padded = children.next().unwrap(); diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 4f1e9da9cd..042d32f1f0 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -60,6 +60,8 @@ //! } //! } //! ``` +//! Display a dropdown list of selectable values. + use crate::core::alignment; use crate::core::event::{self, Event}; use crate::core::keyboard; @@ -169,6 +171,7 @@ pub struct PickList< text_size: Option, text_line_height: text::LineHeight, text_shaping: text::Shaping, + text_wrap: text::Wrapping, font: Option, handle: Handle, class: ::Class<'a>, @@ -203,7 +206,8 @@ where padding: crate::button::DEFAULT_PADDING, text_size: None, text_line_height: text::LineHeight::default(), - text_shaping: text::Shaping::default(), + text_shaping: text::Shaping::Advanced, + text_wrap: text::Wrapping::default(), font: None, handle: Handle::default(), class: ::default(), @@ -250,6 +254,12 @@ where self } + /// Sets the [`text::Wrap`] mode of the [`PickList`]. + pub fn text_wrap(mut self, wrap: text::Wrapping) -> Self { + self.text_wrap = wrap; + self + } + /// Sets the font of the [`PickList`]. pub fn font(mut self, font: impl Into) -> Self { self.font = Some(font.into()); @@ -372,7 +382,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: self.text_shaping, - wrapping: text::Wrapping::default(), + wrapping: self.text_wrap, }; for (option, paragraph) in options.iter().zip(state.options.iter_mut()) @@ -592,6 +602,7 @@ where *size, text::LineHeight::default(), text::Shaping::Basic, + text::Wrapping::default(), )), Handle::Static(Icon { font, @@ -599,7 +610,10 @@ where size, line_height, shaping, - }) => Some((*font, *code_point, *size, *line_height, *shaping)), + wrap, + }) => { + Some((*font, *code_point, *size, *line_height, *shaping, *wrap)) + } Handle::Dynamic { open, closed } => { if state.is_open { Some(( @@ -608,6 +622,7 @@ where open.size, open.line_height, open.shaping, + open.wrap, )) } else { Some(( @@ -616,13 +631,16 @@ where closed.size, closed.line_height, closed.shaping, + closed.wrap, )) } } Handle::None => None, }; - if let Some((font, code_point, size, line_height, shaping)) = handle { + if let Some((font, code_point, size, line_height, shaping, wrap)) = + handle + { let size = size.unwrap_or_else(|| renderer.default_size()); renderer.fill_text( @@ -638,7 +656,7 @@ where horizontal_alignment: alignment::Horizontal::Right, vertical_alignment: alignment::Vertical::Center, shaping, - wrapping: text::Wrapping::default(), + wrapping: wrap, }, Point::new( bounds.x + bounds.width - self.padding.right, @@ -668,7 +686,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: self.text_shaping, - wrapping: text::Wrapping::default(), + wrapping: self.text_wrap, }, Point::new(bounds.x + self.padding.left, bounds.center_y()), if is_selected { @@ -814,6 +832,8 @@ pub struct Icon { pub line_height: text::LineHeight, /// The shaping strategy of the icon. pub shaping: text::Shaping, + /// The wrap mode of the icon. + pub wrap: text::Wrapping, } /// The possible status of a [`PickList`]. diff --git a/widget/src/radio.rs b/widget/src/radio.rs index d2a3bd6a37..66cad3150c 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -188,7 +188,7 @@ where spacing: Self::DEFAULT_SPACING, text_size: None, text_line_height: text::LineHeight::default(), - text_shaping: text::Shaping::default(), + text_shaping: text::Shaping::Advanced, text_wrapping: text::Wrapping::default(), font: None, class: Theme::default(), diff --git a/widget/src/row.rs b/widget/src/row.rs index 9c0fa97e11..575dd24cd9 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -203,8 +203,8 @@ where self.children.iter().map(Tree::new).collect() } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(&self.children); + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(&mut self.children) } fn size(&self) -> Size { @@ -351,6 +351,48 @@ where translation, ) } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::A11yTree; + A11yTree::join( + self.children + .iter() + .zip(layout.children()) + .zip(state.children.iter()) + .map(|((c, c_layout), state)| { + c.as_widget().a11y_nodes(c_layout, state, cursor) + }), + ) + } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut crate::core::clipboard::DndDestinationRectangles, + ) { + for ((e, layout), state) in self + .children + .iter() + .zip(layout.children()) + .zip(state.children.iter()) + { + e.as_widget().drag_destinations( + state, + layout, + renderer, + dnd_rectangles, + ); + } + } } impl<'a, Message, Theme, Renderer> From> @@ -390,7 +432,7 @@ where self.row.children() } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { self.row.diff(tree); } diff --git a/widget/src/rule.rs b/widget/src/rule.rs index 2457768367..d4b68c807e 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -79,6 +79,24 @@ where } } + /// Set the width of the rule + /// Will not be applied if it is vertical + pub fn width(mut self, width: impl Into) -> Self { + if self.is_horizontal { + self.width = width.into(); + } + self + } + + /// Set the height of the rule + /// Will not be applied if it is horizontal + pub fn height(mut self, height: impl Into) -> Self { + if !self.is_horizontal { + self.height = height.into(); + } + self + } + /// Sets the style of the [`Rule`]. #[must_use] pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 528d63c1da..6841d19c80 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -21,6 +21,11 @@ //! ``` use crate::container; use crate::core::border::{self, Border}; +use crate::core::clipboard::DndDestinationRectangles; +use iced_runtime::core::widget::Id; +#[cfg(feature = "a11y")] +use std::borrow::Cow; + use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::layout; @@ -29,13 +34,12 @@ use crate::core::overlay; use crate::core::renderer; 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::{ - self, Background, Clipboard, Color, Element, Layout, Length, Padding, - Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, + self, id::Internal, Background, Clipboard, Color, Element, Layout, Length, + Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; use crate::runtime::task::{self, Task}; use crate::runtime::Action; @@ -74,7 +78,14 @@ pub struct Scrollable< Theme: Catalog, Renderer: core::Renderer, { - id: Option, + id: Id, + scrollbar_id: Id, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, + #[cfg(feature = "a11y")] + label: Option>, width: Length, height: Length, direction: Direction, @@ -101,7 +112,14 @@ where direction: impl Into, ) -> Self { Scrollable { - id: None, + id: Id::unique(), + scrollbar_id: Id::unique(), + #[cfg(feature = "a11y")] + name: None, + #[cfg(feature = "a11y")] + description: None, + #[cfg(feature = "a11y")] + label: None, width: Length::Shrink, height: Length::Shrink, direction: direction.into(), @@ -144,7 +162,7 @@ where /// Sets the [`Id`] of the [`Scrollable`]. pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); + self.id = id; self } @@ -248,6 +266,41 @@ where self.class = class.into(); self } + + #[cfg(feature = "a11y")] + /// Sets the name of the [`Button`]. + pub fn name(mut self, name: impl Into>) -> Self { + self.name = Some(name.into()); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description_widget( + mut self, + description: &impl iced_accessibility::Describes, + ) -> Self { + self.description = Some(iced_accessibility::Description::Id( + description.description(), + )); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description(mut self, description: impl Into>) -> Self { + self.description = + Some(iced_accessibility::Description::Text(description.into())); + self + } + + #[cfg(feature = "a11y")] + /// Sets the label of the [`Button`]. + pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self { + self.label = + Some(label.label().into_iter().map(|l| l.into()).collect()); + self + } } /// The direction of [`Scrollable`]. @@ -402,8 +455,8 @@ where vec![Tree::new(&self.content)] } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(std::slice::from_ref(&self.content)); + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(std::slice::from_mut(&mut self.content)) } fn size(&self) -> Size { @@ -487,24 +540,20 @@ where operation.scrollable( state, - self.id.as_ref().map(|id| &id.0), + Some(&self.id), bounds, content_bounds, translation, ); - operation.container( - self.id.as_ref().map(|id| &id.0), - bounds, - &mut |operation| { - self.content.as_widget().operate( - &mut tree.children[0], - layout.children().next().unwrap(), - renderer, - operation, - ); - }, - ); + operation.container(Some(&self.id), bounds, &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + operation, + ); + }); } fn on_event( @@ -764,36 +813,36 @@ where return event::Status::Ignored; } - let delta = match delta { + let Vector { x, y } = match delta { mouse::ScrollDelta::Lines { x, y } => { - let is_shift_pressed = state.keyboard_modifiers.shift(); - - // macOS automatically inverts the axes when Shift is pressed - let (x, y) = - if cfg!(target_os = "macos") && is_shift_pressed { - (y, x) - } else { - (x, y) - }; - - let is_vertical = match self.direction { - Direction::Vertical(_) => true, - Direction::Horizontal(_) => false, - Direction::Both { .. } => !is_shift_pressed, - }; - - let movement = if is_vertical { - Vector::new(x, y) - } else { - Vector::new(y, x) - }; - // TODO: Configurable speed/friction (?) - -movement * 60.0 + Vector::new(x, y) * 60. } mouse::ScrollDelta::Pixels { x, y } => -Vector::new(x, y), }; + let is_shift_pressed = state.keyboard_modifiers.shift(); + + // macOS automatically inverts the axes when Shift is pressed + let (x, y) = if cfg!(target_os = "macos") && is_shift_pressed { + (y, x) + } else { + (x, y) + }; + + let is_vertical = match self.direction { + Direction::Vertical(_) => true, + Direction::Horizontal(_) => false, + Direction::Both { .. } => !is_shift_pressed, + }; + + let movement = if is_vertical { + Vector::new(x, y) + } else { + Vector::new(y, x) + }; + let delta = movement * -1.; + state.scroll( self.direction.align(delta), bounds, @@ -1146,6 +1195,181 @@ where translation - offset, ) } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::{ + accesskit::{NodeBuilder, NodeId, Rect, Role}, + A11yId, A11yNode, A11yTree, + }; + + let child_layout = layout.children().next().unwrap(); + let child_tree = &state.children[0]; + let child_tree = self.content.as_widget().a11y_nodes( + child_layout, + &child_tree, + cursor, + ); + + let window = layout.bounds(); + let is_hovered = cursor.is_over(window); + let Rectangle { + x, + y, + width, + height, + } = window; + let bounds = Rect::new( + x as f64, + y as f64, + (x + width) as f64, + (y + height) as f64, + ); + let mut node = NodeBuilder::new(Role::ScrollView); + node.set_bounds(bounds); + if let Some(name) = self.name.as_ref() { + node.set_name(name.clone()); + } + match self.description.as_ref() { + Some(iced_accessibility::Description::Id(id)) => { + node.set_described_by( + id.iter() + .cloned() + .map(|id| NodeId::from(id)) + .collect::>(), + ); + } + Some(iced_accessibility::Description::Text(text)) => { + node.set_description(text.clone()); + } + None => {} + } + + if is_hovered { + node.set_hovered(); + } + + if let Some(label) = self.label.as_ref() { + node.set_labelled_by(label.clone()); + } + + let content = layout.children().next().unwrap(); + let content_bounds = content.bounds(); + + let mut scrollbar_node = NodeBuilder::new(Role::ScrollBar); + if matches!(state.state, tree::State::Some(_)) { + let state = state.state.downcast_ref::(); + let scrollbars = Scrollbars::new( + state, + self.direction, + content_bounds, + content_bounds, + ); + for (window, content, offset, scrollbar) in scrollbars + .x + .iter() + .map(|s| { + (window.width, content_bounds.width, state.offset_x, s) + }) + .chain(scrollbars.y.iter().map(|s| { + (window.height, content_bounds.height, state.offset_y, s) + })) + { + let scrollbar_bounds = scrollbar.total_bounds; + let is_hovered = cursor.is_over(scrollbar_bounds); + let Rectangle { + x, + y, + width, + height, + } = scrollbar_bounds; + let bounds = Rect::new( + x as f64, + y as f64, + (x + width) as f64, + (y + height) as f64, + ); + scrollbar_node.set_bounds(bounds); + if is_hovered { + scrollbar_node.set_hovered(); + } + scrollbar_node + .set_controls(vec![A11yId::Widget(self.id.clone()).into()]); + scrollbar_node.set_numeric_value( + 100.0 * offset.absolute(window, content) as f64 + / scrollbar_bounds.height as f64, + ); + } + } + + let child_tree = A11yTree::join( + [ + child_tree, + A11yTree::leaf(scrollbar_node, self.scrollbar_id.clone()), + ] + .into_iter(), + ); + A11yTree::node_with_child_tree( + A11yNode::new(node, self.id.clone()), + child_tree, + ) + } + + fn id(&self) -> Option { + Some(Id(Internal::Set(vec![ + self.id.0.clone(), + self.scrollbar_id.0.clone(), + ]))) + } + + fn set_id(&mut self, id: Id) { + if let Id(Internal::Set(list)) = id { + if list.len() == 2 { + self.id.0 = list[0].clone(); + self.scrollbar_id.0 = list[1].clone(); + } + } + } + + fn drag_destinations( + &self, + tree: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut crate::core::clipboard::DndDestinationRectangles, + ) { + let my_state = tree.state.downcast_ref::(); + if let Some((c_layout, c_state)) = + layout.children().zip(tree.children.iter()).next() + { + let mut my_dnd_rectangles = DndDestinationRectangles::new(); + self.content.as_widget().drag_destinations( + c_state, + c_layout, + renderer, + &mut my_dnd_rectangles, + ); + let mut my_dnd_rectangles = my_dnd_rectangles.into_rectangles(); + + let bounds = layout.bounds(); + let content_bounds = c_layout.bounds(); + for r in &mut my_dnd_rectangles { + let translation = my_state.translation( + self.direction, + bounds, + content_bounds, + ); + r.rectangle.x -= translation.x as f64; + r.rectangle.y -= translation.y as f64; + } + dnd_rectangles.append(&mut my_dnd_rectangles); + } + } } impl<'a, Message, Theme, Renderer> @@ -1163,50 +1387,22 @@ where } } -/// The identifier of a [`Scrollable`]. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Id(widget::Id); - -impl Id { - /// Creates a custom [`Id`]. - pub fn new(id: impl Into>) -> Self { - Self(widget::Id::new(id)) - } - - /// Creates a unique [`Id`]. - /// - /// This function produces a different [`Id`] every time it is called. - pub fn unique() -> Self { - Self(widget::Id::unique()) - } -} - -impl From for widget::Id { - fn from(id: Id) -> Self { - id.0 - } -} - /// Produces a [`Task`] that snaps the [`Scrollable`] with the given [`Id`] /// to the provided [`RelativeOffset`]. pub fn snap_to(id: Id, offset: RelativeOffset) -> Task { - task::effect(Action::widget(operation::scrollable::snap_to(id.0, offset))) + task::effect(Action::widget(operation::scrollable::snap_to(id, offset))) } /// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`Id`] /// to the provided [`AbsoluteOffset`]. pub fn scroll_to(id: Id, offset: AbsoluteOffset) -> Task { - task::effect(Action::widget(operation::scrollable::scroll_to( - id.0, offset, - ))) + task::effect(Action::widget(operation::scrollable::scroll_to(id, offset))) } /// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`Id`] /// by the provided [`AbsoluteOffset`]. pub fn scroll_by(id: Id, offset: AbsoluteOffset) -> Task { - task::effect(Action::widget(operation::scrollable::scroll_by( - id.0, offset, - ))) + task::effect(Action::widget(operation::scrollable::scroll_by(id, offset))) } fn notify_scroll( diff --git a/widget/src/shader.rs b/widget/src/shader.rs index fa6923360b..80fa62d549 100644 --- a/widget/src/shader.rs +++ b/widget/src/shader.rs @@ -110,6 +110,10 @@ where Some(Event::RedrawRequested(instant)) } core::Event::Window(_) => None, + #[cfg(feature = "a11y")] + core::Event::A11y(_, _) => None, + core::Event::Dnd(_) => None, + core::Event::PlatformSpecific(_) => None, }; if let Some(custom_shader_event) = custom_shader_event { diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 31aa0e0cba..f770dfadd4 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -37,6 +37,7 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; +use crate::core::widget::Id; use crate::core::{ self, Background, Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, Theme, Widget, @@ -44,6 +45,12 @@ use crate::core::{ use std::ops::RangeInclusive; +use iced_renderer::core::border::Radius; +use iced_runtime::core::gradient::Linear; + +#[cfg(feature = "a11y")] +use std::borrow::Cow; + /// An horizontal bar and a handle that selects a single value from a range of /// values. /// @@ -85,11 +92,19 @@ pub struct Slider<'a, T, Message, Theme = crate::Theme> where Theme: Catalog, { + id: Id, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, + #[cfg(feature = "a11y")] + label: Option>, range: RangeInclusive, step: T, shift_step: Option, value: T, default: Option, + breakpoints: &'a [T], on_change: Box Message + 'a>, on_release: Option, width: Length, @@ -131,11 +146,19 @@ where }; Slider { + id: Id::unique(), + #[cfg(feature = "a11y")] + name: None, + #[cfg(feature = "a11y")] + description: None, + #[cfg(feature = "a11y")] + label: None, value, default: None, range, step: T::from(1), shift_step: None, + breakpoints: &[], on_change: Box::new(on_change), on_release: None, width: Length::Fill, @@ -152,12 +175,20 @@ where self } + /// Defines breakpoints to visibly mark on the slider. + /// + /// The slider will gravitate towards a breakpoint when near it. + pub fn breakpoints(mut self, breakpoints: &'a [T]) -> Self { + self.breakpoints = breakpoints; + self + } + /// Sets the release message of the [`Slider`]. /// This is called when the mouse is released from the slider. /// /// Typically, the user's interaction with the slider is finished when this message is produced. /// This is useful if you need to spawn a long-running task from the slider's result, where - /// the default on_change message could create too many events. + /// the default `on_change` message could create too many events. pub fn on_release(mut self, on_release: Message) -> Self { self.on_release = Some(on_release); self @@ -206,6 +237,41 @@ where self.class = class.into(); self } + + #[cfg(feature = "a11y")] + /// Sets the name of the [`Button`]. + pub fn name(mut self, name: impl Into>) -> Self { + self.name = Some(name.into()); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description_widget( + mut self, + description: &impl iced_accessibility::Describes, + ) -> Self { + self.description = Some(iced_accessibility::Description::Id( + description.description(), + )); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description(mut self, description: impl Into>) -> Self { + self.description = + Some(iced_accessibility::Description::Text(description.into())); + self + } + + #[cfg(feature = "a11y")] + /// Sets the label of the [`Button`]. + pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self { + self.label = + Some(label.label().into_iter().map(|l| l.into()).collect()); + self + } } impl<'a, T, Message, Theme, Renderer> Widget @@ -433,15 +499,51 @@ where }, ); + let border_width = style + .handle + .border_width + .min(bounds.height / 2.0) + .min(bounds.width / 2.0); + let (handle_width, handle_height, handle_border_radius) = match style.handle.shape { HandleShape::Circle { radius } => { - (radius * 2.0, radius * 2.0, radius.into()) + let radius = (radius) + .max(2.0 * border_width) + .min(bounds.height / 2.0) + .min(bounds.width / 2.0); + (radius * 2.0, radius * 2.0, Radius::from(radius)) } HandleShape::Rectangle { + height, width, border_radius, - } => (f32::from(width), bounds.height, border_radius), + } => { + let width = (f32::from(width)) + .max(2.0 * border_width) + .min(bounds.width); + let height = (f32::from(height)) + .max(2.0 * border_width) + .min(bounds.height); + let mut border_radius: [f32; 4] = border_radius.into(); + for r in &mut border_radius { + *r = (*r) + .min(height / 2.0) + .min(width / 2.0) + .max(*r * (width + border_width * 2.0) / width); + } + + ( + width, + height, + Radius { + top_left: border_radius[0], + top_right: border_radius[1], + bottom_right: border_radius[2], + bottom_left: border_radius[3], + }, + ) + } }; let value = self.value.into() as f32; @@ -460,6 +562,52 @@ where let rail_y = bounds.y + bounds.height / 2.0; + // Draw the breakpoint indicators beneath the slider. + const BREAKPOINT_WIDTH: f32 = 2.0; + for &value in self.breakpoints { + let value: f64 = value.into(); + let offset = if range_start >= range_end { + 0.0 + } else { + (bounds.width - BREAKPOINT_WIDTH) * (value as f32 - range_start) + / (range_end - range_start) + }; + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + offset, + y: rail_y + 6.0, + width: BREAKPOINT_WIDTH, + height: 8.0, + }, + border: Border { + radius: 0.0.into(), + width: 0.0, + color: Color::TRANSPARENT, + }, + ..renderer::Quad::default() + }, + crate::core::Background::Color(style.breakpoint.color), + ); + } + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: rail_y - style.rail.width / 2.0, + width: offset + handle_width / 2.0, + height: style.rail.width, + }, + border: style.rail.border, + ..renderer::Quad::default() + }, + style.rail.backgrounds.0, + ); + + // TODO align the angle of the gradient for the slider? + renderer.fill_quad( renderer::Quad { bounds: Rectangle { @@ -477,9 +625,9 @@ where renderer.fill_quad( renderer::Quad { bounds: Rectangle { - x: bounds.x + offset + handle_width / 2.0, + x: bounds.x, y: rail_y - style.rail.width / 2.0, - width: bounds.width - offset - handle_width / 2.0, + width: offset + handle_width / 2.0, height: style.rail.width, }, border: style.rail.border, @@ -488,11 +636,12 @@ where style.rail.backgrounds.1, ); + // handle renderer.fill_quad( renderer::Quad { bounds: Rectangle { x: bounds.x + offset, - y: rail_y - handle_height / 2.0, + y: rail_y - (handle_height / 2.0), width: handle_width, height: handle_height, }, @@ -527,6 +676,87 @@ where mouse::Interaction::default() } } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + _state: &Tree, + cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::{ + accesskit::{NodeBuilder, NodeId, Rect, Role}, + A11yTree, + }; + + let bounds = layout.bounds(); + let is_hovered = cursor.is_over(bounds); + let Rectangle { + x, + y, + width, + height, + } = bounds; + let bounds = Rect::new( + x as f64, + y as f64, + (x + width) as f64, + (y + height) as f64, + ); + let mut node = NodeBuilder::new(Role::Slider); + node.set_bounds(bounds); + if let Some(name) = self.name.as_ref() { + node.set_name(name.clone()); + } + match self.description.as_ref() { + Some(iced_accessibility::Description::Id(id)) => { + node.set_described_by( + id.iter() + .cloned() + .map(|id| NodeId::from(id)) + .collect::>(), + ); + } + Some(iced_accessibility::Description::Text(text)) => { + node.set_description(text.clone()); + } + None => {} + } + + if is_hovered { + node.set_hovered(); + } + + if let Some(label) = self.label.as_ref() { + node.set_labelled_by(label.clone()); + } + + if let Ok(min) = self.range.start().clone().try_into() { + node.set_min_numeric_value(min); + } + if let Ok(max) = self.range.end().clone().try_into() { + node.set_max_numeric_value(max); + } + if let Ok(value) = self.value.clone().try_into() { + node.set_numeric_value(value); + } + if let Ok(step) = self.step.clone().try_into() { + node.set_numeric_value_step(step); + } + + // TODO: This could be a setting on the slider + node.set_live(iced_accessibility::accesskit::Live::Polite); + + A11yTree::leaf(node, self.id.clone()) + } + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn set_id(&mut self, id: Id) { + self.id = id; + } } impl<'a, T, Message, Theme, Renderer> From> @@ -568,6 +798,15 @@ pub struct Style { pub rail: Rail, /// The appearance of the [`Handle`] of the slider. pub handle: Handle, + /// The appearance of breakpoints. + pub breakpoint: Breakpoint, +} + +/// The appearance of slider breakpoints. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Breakpoint { + /// The color of the slider breakpoint. + pub color: Color, } impl Style { @@ -592,6 +831,21 @@ pub struct Rail { pub border: Border, } +/// The background color of the rail +#[derive(Debug, Clone, Copy)] +pub enum RailBackground { + /// Start and end colors of the rail + Pair(Color, Color), + /// Linear gradient for the background of the rail + /// includes an option for auto-selecting the angle + Gradient { + /// the linear gradient of the slider + gradient: Linear, + /// Let the widget determin the angle of the gradient + auto_angle: bool, + }, +} + /// The appearance of the handle of a slider. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Handle { @@ -617,6 +871,8 @@ pub enum HandleShape { Rectangle { /// The width of the rectangle. width: u16, + /// The height of the rectangle. + height: u16, /// The border radius of the corners of the rectangle. border_radius: border::Radius, }, @@ -675,5 +931,8 @@ pub fn default(theme: &Theme, status: Status) -> Style { border_color: Color::TRANSPARENT, border_width: 0.0, }, + breakpoint: Breakpoint { + color: palette.background.weak.text, + }, } } diff --git a/widget/src/stack.rs b/widget/src/stack.rs index 6a44c32830..32d1617522 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -1,4 +1,5 @@ //! Display content on top of other content. + use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -134,8 +135,8 @@ where self.children.iter().map(Tree::new).collect() } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(&self.children); + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(&mut self.children); } fn size(&self) -> Size { diff --git a/widget/src/svg.rs b/widget/src/svg.rs index 8d57265a7a..7cb1afb9d3 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -15,6 +15,9 @@ //! svg("tiger.svg").into() //! } //! ``` +//! Display vector graphics in your application. +use iced_runtime::core::widget::Id; + use crate::core::layout; use crate::core::mouse; use crate::core::renderer; @@ -25,6 +28,9 @@ use crate::core::{ Size, Theme, Vector, Widget, }; +#[cfg(feature = "a11y")] +use std::borrow::Cow; +use std::marker::PhantomData; use std::path::PathBuf; pub use crate::core::svg::Handle; @@ -56,6 +62,13 @@ pub struct Svg<'a, Theme = crate::Theme> where Theme: Catalog, { + id: Id, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, + #[cfg(feature = "a11y")] + label: Option>, handle: Handle, width: Length, height: Length, @@ -63,6 +76,8 @@ where class: Theme::Class<'a>, rotation: Rotation, opacity: f32, + symbolic: bool, + _phantom_data: PhantomData<&'a ()>, } impl<'a, Theme> Svg<'a, Theme> @@ -72,6 +87,13 @@ where /// Creates a new [`Svg`] from the given [`Handle`]. pub fn new(handle: impl Into) -> Self { Svg { + id: Id::unique(), + #[cfg(feature = "a11y")] + name: None, + #[cfg(feature = "a11y")] + description: None, + #[cfg(feature = "a11y")] + label: None, handle: handle.into(), width: Length::Fill, height: Length::Shrink, @@ -79,6 +101,8 @@ where class: Theme::default(), rotation: Rotation::default(), opacity: 1.0, + symbolic: false, + _phantom_data: PhantomData::default(), } } @@ -114,6 +138,13 @@ where } } + /// Symbolic icons inherit their color from the renderer if a color is not defined. + #[must_use] + pub fn symbolic(mut self, symbolic: bool) -> Self { + self.symbolic = symbolic; + self + } + /// Sets the style of the [`Svg`]. #[must_use] pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self @@ -146,6 +177,41 @@ where self.opacity = opacity.into(); self } + + #[cfg(feature = "a11y")] + /// Sets the name of the [`Button`]. + pub fn name(mut self, name: impl Into>) -> Self { + self.name = Some(name.into()); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description_widget( + mut self, + description: &T, + ) -> Self { + self.description = Some(iced_accessibility::Description::Id( + description.description(), + )); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description(mut self, description: impl Into>) -> Self { + self.description = + Some(iced_accessibility::Description::Text(description.into())); + self + } + + #[cfg(feature = "a11y")] + /// Sets the label of the [`Button`]. + pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self { + self.label = + Some(label.label().into_iter().map(|l| l.into()).collect()); + self + } } impl<'a, Message, Theme, Renderer> Widget @@ -200,7 +266,7 @@ where _state: &Tree, renderer: &mut Renderer, theme: &Theme, - _style: &renderer::Style, + renderer_style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, _viewport: &Rectangle, @@ -239,7 +305,10 @@ where Status::Idle }; - let style = theme.style(&self.class, status); + let mut style = theme.style(&self.class, status); + if self.symbolic && style.color.is_none() { + style.color = Some(renderer_style.icon_color); + } let render = |renderer: &mut Renderer| { renderer.draw_svg( @@ -248,6 +317,7 @@ where color: style.color, rotation: self.rotation.radians(), opacity: self.opacity, + border_radius: [0.0; 4], }, drawing_bounds, ); @@ -261,6 +331,66 @@ where render(renderer); } } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + _state: &Tree, + _cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::{ + accesskit::{NodeBuilder, NodeId, Rect, Role}, + A11yTree, + }; + + let bounds = layout.bounds(); + let Rectangle { + x, + y, + width, + height, + } = bounds; + let bounds = Rect::new( + x as f64, + y as f64, + (x + width) as f64, + (y + height) as f64, + ); + let mut node = NodeBuilder::new(Role::Image); + node.set_bounds(bounds); + if let Some(name) = self.name.as_ref() { + node.set_name(name.clone()); + } + match self.description.as_ref() { + Some(iced_accessibility::Description::Id(id)) => { + node.set_described_by( + id.iter() + .cloned() + .map(|id| NodeId::from(id)) + .collect::>(), + ); + } + Some(iced_accessibility::Description::Text(text)) => { + node.set_description(text.clone()); + } + None => {} + } + + if let Some(label) = self.label.as_ref() { + node.set_labelled_by(label.clone()); + } + + A11yTree::leaf(node, self.id.clone()) + } + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn set_id(&mut self, id: Id) { + self.id = id; + } } impl<'a, Message, Theme, Renderer> From> diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index ff41377946..3c0842e720 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -599,7 +599,7 @@ where tree::State::new(State::::new()) } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { let state = tree.state.downcast_mut::>(); // Stop pasting if input becomes disabled diff --git a/widget/src/themer.rs b/widget/src/themer.rs index 499a9fe8e8..384c04cfa3 100644 --- a/widget/src/themer.rs +++ b/widget/src/themer.rs @@ -1,4 +1,5 @@ use crate::container; + use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -82,8 +83,8 @@ where self.content.as_widget().children() } - fn diff(&self, tree: &mut Tree) { - self.content.as_widget().diff(tree); + fn diff(&mut self, tree: &mut Tree) { + self.content.as_widget_mut().diff(tree); } fn size(&self) -> Size { @@ -166,6 +167,8 @@ where let style = if let Some(text_color) = self.text_color { renderer::Style { text_color: text_color(&theme), + icon_color: style.icon_color, // TODO(POP): Is this correct? + scale_factor: style.scale_factor, // TODO(POP): Is this correct? } } else { *style diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index fdd2e68c31..08aa7fe8ca 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -30,6 +30,12 @@ //! } //! } //! ``` +//! Show toggle controls using togglers. +#[cfg(feature = "a11y")] +use std::borrow::Cow; + +use iced_runtime::core::border::Radius; + use crate::core::alignment; use crate::core::event; use crate::core::layout; @@ -37,10 +43,10 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::text; use crate::core::touch; -use crate::core::widget; use crate::core::widget::tree::{self, Tree}; +use crate::core::widget::{self, Id}; use crate::core::{ - Border, Clipboard, Color, Element, Event, Layout, Length, Pixels, + id, Border, Clipboard, Color, Element, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Theme, Widget, }; @@ -86,6 +92,14 @@ pub struct Toggler< Theme: Catalog, Renderer: text::Renderer, { + id: Id, + label_id: Option, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, + #[cfg(feature = "a11y")] + labeled_by_widget: Option>, is_toggled: bool, on_toggle: Option Message + 'a>>, label: Option>, @@ -114,11 +128,16 @@ where /// It expects: /// * a boolean describing whether the [`Toggler`] is checked or not /// * An optional label for the [`Toggler`] - /// * a function that will be called when the [`Toggler`] is toggled. It - /// will receive the new state of the [`Toggler`] and must produce a - /// `Message`. pub fn new(is_toggled: bool) -> Self { Toggler { + id: Id::unique(), + label_id: None, + #[cfg(feature = "a11y")] + name: None, + #[cfg(feature = "a11y")] + description: None, + #[cfg(feature = "a11y")] + labeled_by_widget: None, is_toggled, on_toggle: None, label: None, @@ -127,9 +146,9 @@ where text_size: None, text_line_height: text::LineHeight::default(), text_alignment: alignment::Horizontal::Left, - text_shaping: text::Shaping::default(), text_wrapping: text::Wrapping::default(), spacing: Self::DEFAULT_SIZE / 2.0, + text_shaping: text::Shaping::Advanced, font: None, class: Theme::default(), } @@ -138,6 +157,7 @@ where /// Sets the label of the [`Toggler`]. pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self { self.label = Some(label.into_fragment()); + self.label_id = Some(Id::unique()); self } @@ -241,6 +261,44 @@ where self.class = class.into(); self } + + #[cfg(feature = "a11y")] + /// Sets the name of the [`Button`]. + pub fn name(mut self, name: impl Into>) -> Self { + self.name = Some(name.into()); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description_widget( + mut self, + description: &T, + ) -> Self { + self.description = Some(iced_accessibility::Description::Id( + description.description(), + )); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Button`]. + pub fn description(mut self, description: impl Into>) -> Self { + self.description = + Some(iced_accessibility::Description::Text(description.into())); + self + } + + #[cfg(feature = "a11y")] + /// Sets the label of the [`Button`] using another widget. + pub fn labeled_by_widget( + mut self, + label: &dyn iced_accessibility::Labels, + ) -> Self { + self.labeled_by_widget = + Some(label.label().into_iter().map(|l| l.into()).collect()); + self + } } impl<'a, Message, Theme, Renderer> Widget @@ -275,7 +333,7 @@ where layout::next_to_each_other( &limits, self.spacing, - |_| layout::Node::new(Size::new(2.0 * self.size, self.size)), + |_| layout::Node::new(crate::core::Size::new(48., 24.)), |limits| { if let Some(label) = self.label.as_deref() { let state = tree @@ -298,7 +356,7 @@ where self.text_wrapping, ) } else { - layout::Node::new(Size::ZERO) + layout::Node::new(crate::core::Size::ZERO) } }, ) @@ -365,13 +423,6 @@ where cursor: mouse::Cursor, viewport: &Rectangle, ) { - /// Makes sure that the border radius of the toggler looks good at every size. - const BORDER_RADIUS_RATIO: f32 = 32.0 / 13.0; - - /// The space ratio between the background Quad and the Toggler bounds, and - /// between the background Quad and foreground Quad. - const SPACE_RATIO: f32 = 0.05; - let mut children = layout.children(); let toggler_layout = children.next().unwrap(); @@ -407,21 +458,20 @@ where let style = theme.style(&self.class, status); - let border_radius = bounds.height / BORDER_RADIUS_RATIO; - let space = SPACE_RATIO * bounds.height; + let space = style.handle_margin; let toggler_background_bounds = Rectangle { - x: bounds.x + space, - y: bounds.y + space, - width: bounds.width - (2.0 * space), - height: bounds.height - (2.0 * space), + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, }; renderer.fill_quad( renderer::Quad { bounds: toggler_background_bounds, border: Border { - radius: border_radius.into(), + radius: style.border_radius, width: style.background_border_width, color: style.background_border_color, }, @@ -433,20 +483,20 @@ where let toggler_foreground_bounds = Rectangle { x: bounds.x + if self.is_toggled { - bounds.width - 2.0 * space - (bounds.height - (4.0 * space)) + bounds.width - space - (bounds.height - (2.0 * space)) } else { - 2.0 * space + space }, - y: bounds.y + (2.0 * space), - width: bounds.height - (4.0 * space), - height: bounds.height - (4.0 * space), + y: bounds.y + space, + width: bounds.height - (2.0 * space), + height: bounds.height - (2.0 * space), }; renderer.fill_quad( renderer::Quad { bounds: toggler_foreground_bounds, border: Border { - radius: border_radius.into(), + radius: style.handle_radius, width: style.foreground_border_width, color: style.foreground_border_color, }, @@ -455,6 +505,102 @@ where style.foreground, ); } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + _state: &Tree, + cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::{ + accesskit::{Action, NodeBuilder, NodeId, Rect, Role}, + A11yNode, A11yTree, + }; + + let bounds = layout.bounds(); + let is_hovered = cursor.is_over(bounds); + let Rectangle { + x, + y, + width, + height, + } = bounds; + + let bounds = Rect::new( + x as f64, + y as f64, + (x + width) as f64, + (y + height) as f64, + ); + + let mut node = NodeBuilder::new(Role::Switch); + node.add_action(Action::Focus); + node.add_action(Action::Default); + node.set_bounds(bounds); + if let Some(name) = self.name.as_ref() { + node.set_name(name.clone()); + } + match self.description.as_ref() { + Some(iced_accessibility::Description::Id(id)) => { + node.set_described_by( + id.iter() + .cloned() + .map(|id| NodeId::from(id)) + .collect::>(), + ); + } + Some(iced_accessibility::Description::Text(text)) => { + node.set_description(text.clone()); + } + None => {} + } + node.set_selected(self.is_toggled); + if is_hovered { + node.set_hovered(); + } + node.add_action(Action::Default); + if let Some(label) = self.label.as_ref() { + let mut label_node = NodeBuilder::new(Role::Label); + + label_node.set_name(label.clone()); + // TODO proper label bounds for the label + label_node.set_bounds(bounds); + + A11yTree::node_with_child_tree( + A11yNode::new(node, self.id.clone()), + A11yTree::leaf(label_node, self.label_id.clone().unwrap()), + ) + } else { + if let Some(labeled_by_widget) = self.labeled_by_widget.as_ref() { + node.set_labelled_by(labeled_by_widget.clone()); + } + A11yTree::leaf(node, self.id.clone()) + } + } + + fn id(&self) -> Option { + if self.label.is_some() { + Some(Id(iced_runtime::core::id::Internal::Set(vec![ + self.id.0.clone(), + self.label_id.clone().unwrap().0, + ]))) + } else { + Some(self.id.clone()) + } + } + + fn set_id(&mut self, id: Id) { + if let Id(id::Internal::Set(list)) = id { + if list.len() == 2 && self.label.is_some() { + self.id.0 = list[0].clone(); + self.label_id = Some(Id(list[1].clone())); + } + } else if self.label.is_none() { + self.id = id; + } + } } impl<'a, Message, Theme, Renderer> From> @@ -503,6 +649,12 @@ pub struct Style { pub foreground_border_width: f32, /// The [`Color`] of the foreground border of the toggler. pub foreground_border_color: Color, + /// The border radius of the toggler. + pub border_radius: Radius, + /// the radius of the handle of the toggler + pub handle_radius: Radius, + /// the space between the handle and the border of the toggler + pub handle_margin: f32, } /// The theme catalog of a [`Toggler`]. @@ -577,5 +729,8 @@ pub fn default(theme: &Theme, status: Status) -> Style { foreground_border_color: Color::TRANSPARENT, background_border_width: 0.0, background_border_color: Color::TRANSPARENT, + border_radius: Radius::from(8.0), + handle_radius: Radius::from(8.0), + handle_margin: 2.0, } } diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index e98f4da7ea..54eae1e08d 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -156,13 +156,6 @@ where ] } - fn diff(&self, tree: &mut widget::Tree) { - tree.diff_children(&[ - self.content.as_widget(), - self.tooltip.as_widget(), - ]); - } - fn state(&self) -> widget::tree::State { widget::tree::State::new(State::default()) } @@ -179,6 +172,13 @@ where self.content.as_widget().size_hint() } + fn diff(&mut self, tree: &mut widget::Tree) { + tree.diff_children(&mut [ + self.content.as_widget_mut(), + self.tooltip.as_widget_mut(), + ]) + } + fn layout( &self, tree: &mut widget::Tree, @@ -488,7 +488,9 @@ where container::draw_background(renderer, &style, layout.bounds()); let defaults = renderer::Style { + icon_color: inherited_style.icon_color, text_color: style.text_color.unwrap_or(inherited_style.text_color), + scale_factor: inherited_style.scale_factor, }; self.tooltip.as_widget().draw( diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index 18633474e2..ee110519af 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -31,7 +31,8 @@ use std::ops::RangeInclusive; pub use crate::slider::{ - default, Catalog, Handle, HandleShape, Status, Style, StyleFn, + default, Catalog, Handle, HandleShape, RailBackground, Status, Style, + StyleFn, }; use crate::core::border::Border; @@ -448,7 +449,8 @@ where HandleShape::Rectangle { width, border_radius, - } => (f32::from(width), bounds.width, border_radius), + height, + } => (f32::from(width), f32::from(height), border_radius), }; let value = self.value.into() as f32; diff --git a/winit/Cargo.toml b/winit/Cargo.toml index bd6feb008a..436e7f75f4 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -19,26 +19,54 @@ debug = ["iced_runtime/debug"] system = ["sysinfo"] program = [] x11 = ["winit/x11"] -wayland = ["winit/wayland"] +wayland = [ + "winit/wayland", + "sctk", + "wayland-protocols", + "raw-window-handle", + "iced_runtime/wayland", + "wayland-backend", + "xkbcommon", + "xkbcommon-dl", + "xkeysym", + "iced_runtime/wayland", +] wayland-dlopen = ["winit/wayland-dlopen"] wayland-csd-adwaita = ["winit/wayland-csd-adwaita"] multi-window = ["iced_runtime/multi-window"] +a11y = ["iced_accessibility", "iced_runtime/a11y"] [dependencies] iced_futures.workspace = true iced_graphics.workspace = true iced_runtime.workspace = true - +iced_accessibility.workspace = true +iced_accessibility.optional = true +iced_accessibility.features = ["accesskit_winit"] log.workspace = true rustc-hash.workspace = true thiserror.workspace = true tracing.workspace = true window_clipboard.workspace = true +dnd.workspace = true winit.workspace = true sysinfo.workspace = true sysinfo.optional = true +[target.'cfg(target_os = "linux")'.dependencies] +raw-window-handle = { version = "0.6", optional = true } +sctk.workspace = true +sctk.optional = true +wayland-protocols.workspace = true +wayland-protocols.optional = true +wayland-backend = { version = "0.3.1", features = [ + "client_system", +], optional = true } +xkbcommon = { version = "0.7", features = ["wayland"], optional = true } +xkbcommon-dl = { version = "0.4.1", optional = true } +xkeysym = { version = "0.2.0", optional = true } + [target.'cfg(target_os = "windows")'.dependencies] winapi.workspace = true diff --git a/winit/src/a11y.rs b/winit/src/a11y.rs new file mode 100644 index 0000000000..8f59871754 --- /dev/null +++ b/winit/src/a11y.rs @@ -0,0 +1,64 @@ +use crate::futures::futures::channel::mpsc; +use crate::program::Control; + +use iced_accessibility::accesskit::{ + ActivationHandler, NodeBuilder, NodeId, Role, Tree, TreeUpdate, +}; +use iced_accessibility::accesskit_winit::Adapter; +use iced_runtime::core; + +pub struct WinitActivationHandler { + pub proxy: mpsc::UnboundedSender, + pub title: String, +} + +impl ActivationHandler for WinitActivationHandler { + fn request_initial_tree( + &mut self, + ) -> Option { + let node_id = core::id::window_node_id(); + + let _ = self + .proxy + .unbounded_send(Control::AccessibilityEnabled(true)); + let mut node = NodeBuilder::new(Role::Window); + node.set_name(self.title.clone()); + let node = node.build(); + let root = NodeId(node_id); + Some(TreeUpdate { + nodes: vec![(root, node)], + tree: Some(Tree::new(root)), + focus: root, + }) + } +} + +pub struct WinitActionHandler { + pub id: core::window::Id, + pub proxy: mpsc::UnboundedSender, +} + +impl iced_accessibility::accesskit::ActionHandler for WinitActionHandler { + fn do_action( + &mut self, + request: iced_accessibility::accesskit::ActionRequest, + ) { + let _ = self + .proxy + .unbounded_send(Control::Accessibility(self.id, request)); + } +} + +pub struct WinitDeactivationHandler { + pub proxy: mpsc::UnboundedSender, +} + +impl iced_accessibility::accesskit::DeactivationHandler + for WinitDeactivationHandler +{ + fn deactivate_accessibility(&mut self) { + let _ = self + .proxy + .unbounded_send(Control::AccessibilityEnabled(false)); + } +} diff --git a/winit/src/application/drag_resize.rs b/winit/src/application/drag_resize.rs new file mode 100644 index 0000000000..9f109d3907 --- /dev/null +++ b/winit/src/application/drag_resize.rs @@ -0,0 +1,138 @@ +use winit::window::{CursorIcon, ResizeDirection}; + +/// If supported by winit, returns a closure that implements cursor resize support. +pub fn event_func( + window: &dyn winit::window::Window, + border_size: f64, +) -> Option< + Box< + dyn FnMut( + &dyn winit::window::Window, + &winit::event::WindowEvent, + ) -> bool, + >, +> { + if window.drag_resize_window(ResizeDirection::East).is_ok() { + // Keep track of cursor when it is within a resizeable border. + let mut cursor_prev_resize_direction = None; + + Some(Box::new( + move |window: &dyn winit::window::Window, + window_event: &winit::event::WindowEvent| + -> bool { + // Keep track of border resize state and set cursor icon when in range + match window_event { + winit::event::WindowEvent::CursorMoved { + position, .. + } => { + if !window.is_decorated() { + let location = cursor_resize_direction( + window.surface_size(), + *position, + border_size, + ); + if location != cursor_prev_resize_direction { + window.set_cursor( + resize_direction_cursor_icon(location) + .into(), + ); + cursor_prev_resize_direction = location; + return true; + } + } + } + winit::event::WindowEvent::MouseInput { + state: winit::event::ElementState::Pressed, + button: winit::event::MouseButton::Left, + .. + } => { + if let Some(direction) = cursor_prev_resize_direction { + let _res = window.drag_resize_window(direction); + return true; + } + } + _ => (), + } + + false + }, + )) + } else { + None + } +} + +/// Get the cursor icon that corresponds to the resize direction. +fn resize_direction_cursor_icon( + resize_direction: Option, +) -> CursorIcon { + match resize_direction { + Some(resize_direction) => match resize_direction { + ResizeDirection::East => CursorIcon::EResize, + ResizeDirection::North => CursorIcon::NResize, + ResizeDirection::NorthEast => CursorIcon::NeResize, + ResizeDirection::NorthWest => CursorIcon::NwResize, + ResizeDirection::South => CursorIcon::SResize, + ResizeDirection::SouthEast => CursorIcon::SeResize, + ResizeDirection::SouthWest => CursorIcon::SwResize, + ResizeDirection::West => CursorIcon::WResize, + }, + None => CursorIcon::Default, + } +} + +/// Identifies resize direction based on cursor position and window dimensions. +#[allow(clippy::similar_names)] +fn cursor_resize_direction( + win_size: winit::dpi::PhysicalSize, + position: winit::dpi::PhysicalPosition, + border_size: f64, +) -> Option { + enum XDirection { + West, + East, + Default, + } + + enum YDirection { + North, + South, + Default, + } + + let xdir = if position.x < border_size { + XDirection::West + } else if position.x > (win_size.width as f64 - border_size) { + XDirection::East + } else { + XDirection::Default + }; + + let ydir = if position.y < border_size { + YDirection::North + } else if position.y > (win_size.height as f64 - border_size) { + YDirection::South + } else { + YDirection::Default + }; + + Some(match xdir { + XDirection::West => match ydir { + YDirection::North => ResizeDirection::NorthWest, + YDirection::South => ResizeDirection::SouthWest, + YDirection::Default => ResizeDirection::West, + }, + + XDirection::East => match ydir { + YDirection::North => ResizeDirection::NorthEast, + YDirection::South => ResizeDirection::SouthEast, + YDirection::Default => ResizeDirection::East, + }, + + XDirection::Default => match ydir { + YDirection::North => ResizeDirection::North, + YDirection::South => ResizeDirection::South, + YDirection::Default => return None, + }, + }) +} diff --git a/winit/src/application/state.rs b/winit/src/application/state.rs new file mode 100644 index 0000000000..9989a88fcb --- /dev/null +++ b/winit/src/application/state.rs @@ -0,0 +1,231 @@ +use crate::application; +use crate::conversion; +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}; +use winit::window::Window; + +/// The state of a windowed [`Application`]. +#[allow(missing_debug_implementations)] +pub struct State +where + A::Theme: application::DefaultStyle, +{ + title: String, + scale_factor: f64, + viewport: Viewport, + viewport_version: usize, + cursor_position: Option>, + modifiers: winit::keyboard::ModifiersState, + theme: A::Theme, + appearance: application::Appearance, + application: PhantomData, +} + +impl State +where + A::Theme: application::DefaultStyle, +{ + /// Creates a new [`State`] for the provided [`Application`] and window. + pub fn new(application: &A, window: &Window) -> Self { + let title = application.title(); + let scale_factor = application.scale_factor(); + let theme = application.theme(); + let appearance = application.style(&theme); + + let viewport = { + let physical_size = window.inner_size(); + + Viewport::with_physical_size( + Size::new(physical_size.width, physical_size.height), + window.scale_factor() * scale_factor, + ) + }; + + Self { + title, + scale_factor, + viewport, + viewport_version: 0, + cursor_position: None, + modifiers: winit::keyboard::ModifiersState::default(), + theme, + appearance, + application: PhantomData, + } + } + + /// Returns the current [`Viewport`] of the [`State`]. + pub fn viewport(&self) -> &Viewport { + &self.viewport + } + + /// Returns the version of the [`Viewport`] of the [`State`]. + /// + /// The version is incremented every time the [`Viewport`] changes. + pub fn viewport_version(&self) -> usize { + self.viewport_version + } + + /// Returns the physical [`Size`] of the [`Viewport`] of the [`State`]. + pub fn physical_size(&self) -> Size { + self.viewport.physical_size() + } + + /// Returns the logical [`Size`] of the [`Viewport`] of the [`State`]. + pub fn logical_size(&self) -> Size { + self.viewport.logical_size() + } + + /// Returns the current scale factor of the [`Viewport`] of the [`State`]. + pub fn scale_factor(&self) -> f64 { + self.viewport.scale_factor() + } + + /// Returns the current cursor position of the [`State`]. + pub fn cursor(&self) -> mouse::Cursor { + self.cursor_position + .map(|cursor_position| { + conversion::cursor_position( + cursor_position, + self.viewport.scale_factor(), + ) + }) + .map(mouse::Cursor::Available) + .unwrap_or(mouse::Cursor::Unavailable) + } + + /// Returns the current keyboard modifiers of the [`State`]. + pub fn modifiers(&self) -> winit::keyboard::ModifiersState { + self.modifiers + } + + /// Returns the current theme of the [`State`]. + pub fn theme(&self) -> &A::Theme { + &self.theme + } + + /// Returns the current title of the [`State`]. + pub fn title(&self) -> &str { + &self.title + } + + /// Returns the current background [`Color`] of the [`State`]. + pub fn background_color(&self) -> Color { + self.appearance.background_color + } + + /// Returns the current icon [`Color`] of the [`State`]. + pub fn icon_color(&self) -> Color { + self.appearance.icon_color + } + + /// Returns the current text [`Color`] of the [`State`]. + pub fn text_color(&self) -> Color { + self.appearance.text_color + } + + /// Processes the provided window event and updates the [`State`] + /// accordingly. + pub fn update( + &mut self, + window: &Window, + event: &WindowEvent, + _debug: &mut Debug, + ) { + match event { + WindowEvent::Resized(new_size) => { + let size = Size::new(new_size.width, new_size.height); + + self.viewport = Viewport::with_physical_size( + size, + window.scale_factor() * self.scale_factor, + ); + + self.viewport_version = self.viewport_version.wrapping_add(1); + } + WindowEvent::ScaleFactorChanged { + scale_factor: new_scale_factor, + .. + } => { + let size = self.viewport.physical_size(); + + self.viewport = Viewport::with_physical_size( + size, + new_scale_factor * self.scale_factor, + ); + + self.viewport_version = self.viewport_version.wrapping_add(1); + } + WindowEvent::CursorMoved { position, .. } + | WindowEvent::Touch(Touch { + location: position, .. + }) => { + self.cursor_position = Some(*position); + } + WindowEvent::CursorLeft { .. } => { + self.cursor_position = None; + } + WindowEvent::ModifiersChanged(new_modifiers) => { + self.modifiers = new_modifiers.state(); + } + #[cfg(feature = "debug")] + WindowEvent::KeyboardInput { + event: + winit::event::KeyEvent { + logical_key: + winit::keyboard::Key::Named( + winit::keyboard::NamedKey::F12, + ), + state: winit::event::ElementState::Pressed, + .. + }, + .. + } => _debug.toggle(), + _ => {} + } + } + + /// Synchronizes the [`State`] with its [`Application`] and its respective + /// window. + /// + /// Normally an [`Application`] should be synchronized with its [`State`] + /// and window after calling [`crate::application::update`]. + pub fn synchronize(&mut self, application: &A, window: &Window) { + // Update window title + let new_title = application.title(); + + if self.title != new_title { + window.set_title(&new_title); + + self.title = new_title; + } + + // Update scale factor and size + let new_scale_factor = application.scale_factor(); + let new_size = window.inner_size(); + let current_size = self.viewport.physical_size(); + + if self.scale_factor != new_scale_factor + || (current_size.width, current_size.height) + != (new_size.width, new_size.height) + { + self.viewport = Viewport::with_physical_size( + Size::new(new_size.width, new_size.height), + window.scale_factor() * new_scale_factor, + ); + self.viewport_version = self.viewport_version.wrapping_add(1); + + self.scale_factor = new_scale_factor; + } + + // Update theme and appearance + self.theme = application.theme(); + self.appearance = application.style(&self.theme); + } +} diff --git a/winit/src/clipboard.rs b/winit/src/clipboard.rs index d54a1fe08b..26381d2554 100644 --- a/winit/src/clipboard.rs +++ b/winit/src/clipboard.rs @@ -1,46 +1,117 @@ //! Access the clipboard. +use std::sync::Mutex; +use std::{any::Any, borrow::Cow}; + +use crate::core::clipboard::DndSource; use crate::core::clipboard::Kind; use std::sync::Arc; +use winit::dpi::LogicalSize; use winit::window::{Window, WindowId}; +use dnd::{DndAction, DndDestinationRectangle, DndSurface, Icon}; +use window_clipboard::{ + dnd::DndProvider, + mime::{self, ClipboardData, ClipboardStoreData}, +}; + /// A buffer for short-term storage and transfer within and between /// applications. #[allow(missing_debug_implementations)] pub struct Clipboard { state: State, + pub(crate) requested_logical_size: Arc>>>, +} + +pub(crate) struct StartDnd { + pub(crate) internal: bool, + pub(crate) source_surface: Option, + pub(crate) icon_surface: Option>, + pub(crate) content: Box, + pub(crate) actions: DndAction, } enum State { Connected { clipboard: window_clipboard::Clipboard, + sender: ControlSender, // Held until drop to satisfy the safety invariants of // `window_clipboard::Clipboard`. // // Note that the field ordering is load-bearing. #[allow(dead_code)] - window: Arc, + window: Arc, + queued_events: Vec, }, Unavailable, } +#[derive(Debug, Clone)] +pub(crate) struct ControlSender { + pub(crate) sender: iced_futures::futures::channel::mpsc::UnboundedSender< + crate::program::Control, + >, + pub(crate) proxy: winit::event_loop::EventLoopProxy, +} + +impl dnd::Sender for ControlSender { + fn send( + &self, + event: dnd::DndEvent, + ) -> Result<(), std::sync::mpsc::SendError>> { + let res = self + .sender + .unbounded_send(crate::program::Control::Dnd(event)) + .map_err(|_err| { + std::sync::mpsc::SendError(dnd::DndEvent::Offer( + None, + dnd::OfferEvent::Leave, + )) + }); + self.proxy.wake_up(); + res + } +} + impl Clipboard { /// Creates a new [`Clipboard`] for the given window. - pub fn connect(window: Arc) -> Clipboard { - // SAFETY: The window handle will stay alive throughout the entire - // lifetime of the `window_clipboard::Clipboard` because we hold - // the `Arc` together with `State`, and enum variant fields - // get dropped in declaration order. + pub(crate) fn connect( + window: Arc, + sender: ControlSender, + ) -> Clipboard { #[allow(unsafe_code)] - let clipboard = - unsafe { window_clipboard::Clipboard::connect(&window) }; + let state = + unsafe { window_clipboard::Clipboard::connect(window.as_ref()) } + .ok() + .map(|c| State::Connected { + clipboard: c, + sender: sender.clone(), + window, + queued_events: Vec::new(), + }) + .unwrap_or(State::Unavailable); - let state = match clipboard { - Ok(clipboard) => State::Connected { clipboard, window }, - Err(_) => State::Unavailable, - }; + #[cfg(target_os = "linux")] + if let State::Connected { clipboard, .. } = &state { + clipboard.init_dnd(Box::new(sender)); + } - Clipboard { state } + Clipboard { + state, + requested_logical_size: Arc::new(Mutex::new(None)), + } + } + + pub(crate) fn proxy(&self) -> Option { + if let State::Connected { + sender: ControlSender { proxy, .. }, + .. + } = &self.state + { + Some(proxy.clone()) + } else { + None + } } /// Creates a new [`Clipboard`] that isn't associated with a window. @@ -48,6 +119,19 @@ impl Clipboard { pub fn unconnected() -> Clipboard { Clipboard { state: State::Unavailable, + requested_logical_size: Arc::new(Mutex::new(None)), + } + } + + pub(crate) fn get_queued(&mut self) -> Vec { + match &mut self.state { + State::Connected { queued_events, .. } => { + std::mem::take(queued_events) + } + State::Unavailable => { + log::error!("Invalid request for queued dnd events"); + Vec::::new() + } } } @@ -91,14 +175,158 @@ impl Clipboard { State::Unavailable => None, } } + + pub(crate) fn start_dnd_winit( + &self, + internal: bool, + source_surface: DndSurface, + icon_surface: Option, + content: Box, + actions: DndAction, + ) { + match &self.state { + State::Connected { clipboard, .. } => { + _ = clipboard.start_dnd( + internal, + source_surface, + icon_surface, + content, + actions, + ) + } + State::Unavailable => {} + } + } } impl crate::core::Clipboard for Clipboard { fn read(&self, kind: Kind) -> Option { - self.read(kind) + match (&self.state, kind) { + (State::Connected { clipboard, .. }, Kind::Standard) => { + clipboard.read().ok() + } + (State::Connected { clipboard, .. }, Kind::Primary) => { + clipboard.read_primary().and_then(|res| res.ok()) + } + (State::Unavailable, _) => None, + } } fn write(&mut self, kind: Kind, contents: String) { - self.write(kind, contents); + match (&mut self.state, kind) { + (State::Connected { clipboard, .. }, Kind::Standard) => { + _ = clipboard.write(contents) + } + (State::Connected { clipboard, .. }, Kind::Primary) => { + _ = clipboard.write_primary(contents) + } + (State::Unavailable, _) => {} + } + } + fn read_data( + &self, + kind: Kind, + mimes: Vec, + ) -> Option<(Vec, String)> { + match (&self.state, kind) { + (State::Connected { clipboard, .. }, Kind::Standard) => { + clipboard.read_raw(mimes).and_then(|res| res.ok()) + } + (State::Connected { clipboard, .. }, Kind::Primary) => { + clipboard.read_primary_raw(mimes).and_then(|res| res.ok()) + } + (State::Unavailable, _) => None, + } + } + + fn write_data( + &mut self, + kind: Kind, + contents: ClipboardStoreData< + Box, + >, + ) { + match (&mut self.state, kind) { + (State::Connected { clipboard, .. }, Kind::Standard) => { + _ = clipboard.write_data(contents) + } + (State::Connected { clipboard, .. }, Kind::Primary) => { + _ = clipboard.write_primary_data(contents) + } + (State::Unavailable, _) => {} + } + } + + fn start_dnd( + &mut self, + internal: bool, + source_surface: Option, + icon_surface: Option>, + content: Box, + actions: DndAction, + ) { + match &mut self.state { + State::Connected { + queued_events, + sender, + .. + } => { + _ = sender + .sender + .unbounded_send(crate::program::Control::StartDnd); + queued_events.push(StartDnd { + internal, + source_surface, + icon_surface, + content, + actions, + }); + } + State::Unavailable => {} + } + } + + fn register_dnd_destination( + &self, + surface: DndSurface, + rectangles: Vec, + ) { + match &self.state { + State::Connected { clipboard, .. } => { + _ = clipboard.register_dnd_destination(surface, rectangles) + } + State::Unavailable => {} + } + } + + fn end_dnd(&self) { + match &self.state { + State::Connected { clipboard, .. } => _ = clipboard.end_dnd(), + State::Unavailable => {} + } + } + + fn peek_dnd(&self, mime: String) -> Option<(Vec, String)> { + match &self.state { + State::Connected { clipboard, .. } => clipboard + .peek_offer::(Some(Cow::Owned(mime))) + .ok() + .map(|res| (res.0, res.1)), + State::Unavailable => None, + } + } + + fn set_action(&self, action: DndAction) { + match &self.state { + State::Connected { clipboard, .. } => { + _ = clipboard.set_action(action) + } + State::Unavailable => {} + } + } + + fn request_logical_window_size(&self, width: f32, height: f32) { + let mut logical_size = self.requested_logical_size.lock().unwrap(); + *logical_size = Some(LogicalSize::new(width, height)); } } diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 5d0f834817..d3b0252596 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -2,11 +2,16 @@ //! //! [`winit`]: https://github.com/rust-windowing/winit //! [`iced_runtime`]: https://github.com/iced-rs/iced/tree/0.13/runtime +use std::hash::DefaultHasher; +use std::hash::Hash; +use std::hash::Hasher; + use crate::core::keyboard; use crate::core::mouse; use crate::core::touch; use crate::core::window; use crate::core::{Event, Point, Size}; +use iced_futures::core::event::PlatformSpecific; /// Converts some [`window::Settings`] into some `WindowAttributes` from `winit`. pub fn window_attributes( @@ -19,7 +24,7 @@ pub fn window_attributes( attributes = attributes .with_title(title) - .with_inner_size(winit::dpi::LogicalSize { + .with_surface_size(winit::dpi::LogicalSize { width: settings.size.width, height: settings.size.height, }) @@ -43,17 +48,19 @@ pub fn window_attributes( } if let Some(min_size) = settings.min_size { - attributes = attributes.with_min_inner_size(winit::dpi::LogicalSize { - width: min_size.width, - height: min_size.height, - }); + attributes = + attributes.with_min_surface_size(winit::dpi::LogicalSize { + width: min_size.width, + height: min_size.height, + }); } if let Some(max_size) = settings.max_size { - attributes = attributes.with_max_inner_size(winit::dpi::LogicalSize { - width: max_size.width, - height: max_size.height, - }); + attributes = + attributes.with_max_surface_size(winit::dpi::LogicalSize { + width: max_size.width, + height: max_size.height, + }); } #[cfg(any( @@ -137,7 +144,7 @@ pub fn window_event( use winit::event::WindowEvent; match event { - WindowEvent::Resized(new_size) => { + WindowEvent::SurfaceResized(new_size) => { let logical_size = new_size.to_logical(scale_factor); Some(Event::Window(window::Event::Resized(Size { @@ -298,6 +305,20 @@ pub fn window_event( Some(Event::Window(window::Event::Moved(Point::new(x, y)))) } + #[cfg(feature = "wayland")] + WindowEvent::SuggestedBounds(bounds) => { + let size = bounds.map(|bounds| { + let size = bounds.to_logical(scale_factor); + Size::new(size.width, size.height) + }); + + Some(Event::PlatformSpecific(PlatformSpecific::Wayland( + iced_runtime::core::event::wayland::Event::Window( + iced_runtime::core::event::wayland::WindowEvent::SuggestedBounds(size), + ), + ))) + } + _ => None, } } @@ -333,10 +354,12 @@ pub fn position( } window::Position::SpecificWith(to_position) => { if let Some(monitor) = monitor { - let start = monitor.position(); + let start = monitor.position().unwrap_or_default(); - let resolution: winit::dpi::LogicalSize = - monitor.size().to_logical(monitor.scale_factor()); + let resolution: winit::dpi::LogicalSize = monitor + .current_video_mode() + .map(|m| m.size().to_logical(monitor.scale_factor())) + .unwrap_or_default(); let position = to_position( size, @@ -362,10 +385,12 @@ pub fn position( } window::Position::Centered => { if let Some(monitor) = monitor { - let start = monitor.position(); + let start = monitor.position().unwrap_or_default(); - let resolution: winit::dpi::LogicalSize = - monitor.size().to_logical(monitor.scale_factor()); + let resolution: winit::dpi::LogicalSize = monitor + .current_video_mode() + .map(|m| m.size().to_logical(monitor.scale_factor())) + .unwrap_or_default(); let centered: winit::dpi::PhysicalPosition = winit::dpi::LogicalPosition { @@ -425,10 +450,10 @@ pub fn mode(mode: Option) -> window::Mode { /// [`winit`]: https://github.com/rust-windowing/winit pub fn mouse_interaction( interaction: mouse::Interaction, -) -> winit::window::CursorIcon { +) -> Option { use mouse::Interaction; - match interaction { + Some(match interaction { Interaction::None | Interaction::Idle => { winit::window::CursorIcon::Default } @@ -455,7 +480,10 @@ pub fn mouse_interaction( Interaction::Move => winit::window::CursorIcon::Move, Interaction::Copy => winit::window::CursorIcon::Copy, Interaction::Help => winit::window::CursorIcon::Help, - } + Interaction::Hide => { + return None; + } + }) } /// Converts a `MouseButton` from [`winit`] to an [`iced`] mouse button. @@ -509,7 +537,11 @@ pub fn touch_event( touch: winit::event::Touch, scale_factor: f64, ) -> touch::Event { - let id = touch::Finger(touch.id); + // TODO we probably should get the actual internal id in some way instead + let mut s = DefaultHasher::new(); + touch.finger_id.hash(&mut s); + + let id = touch::Finger(s.finish()); let position = { let location = touch.location.to_logical::(scale_factor); @@ -1141,3 +1173,13 @@ pub fn icon(icon: window::Icon) -> Option { fn is_private_use(c: char) -> bool { ('\u{E000}'..='\u{F8FF}').contains(&c) } + +#[cfg(feature = "a11y")] +pub(crate) fn a11y( + event: iced_accessibility::accesskit::ActionRequest, +) -> Event { + // XXX + let id = + iced_runtime::core::id::Id::from(u128::from(event.target.0) as u64); + Event::A11y(id, event) +} diff --git a/winit/src/error.rs b/winit/src/error.rs index 7687fb1750..48c79a360c 100644 --- a/winit/src/error.rs +++ b/winit/src/error.rs @@ -10,7 +10,7 @@ pub enum Error { /// The application window could not be created. #[error("the application window could not be created")] - WindowCreationFailed(winit::error::OsError), + WindowCreationFailed(winit::error::RequestError), /// The application graphics context could not be created. #[error("the application graphics context could not be created")] diff --git a/winit/src/lib.rs b/winit/src/lib.rs index ed2910d64a..c75036b3e9 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -24,8 +24,11 @@ pub use iced_runtime::core; pub use iced_runtime::futures; pub use winit; +#[cfg(feature = "a11y")] +pub mod a11y; pub mod clipboard; pub mod conversion; +pub mod platform_specific; pub mod settings; #[cfg(feature = "program")] @@ -44,3 +47,5 @@ pub use settings::Settings; #[cfg(feature = "program")] pub use program::Program; + +pub use platform_specific::*; diff --git a/winit/src/platform_specific/mod.rs b/winit/src/platform_specific/mod.rs new file mode 100644 index 0000000000..4ef88dd7d9 --- /dev/null +++ b/winit/src/platform_specific/mod.rs @@ -0,0 +1,174 @@ +//! Wayland specific shell +//! + +use iced_graphics::Compositor; +use iced_runtime::{core::window, platform_specific, Debug}; +use sctk::reexports::client::Connection; +use wayland::sctk_event::UserInterfaces; + +#[cfg(all(feature = "wayland", target_os = "linux"))] +pub mod wayland; + +#[cfg(all(feature = "wayland", target_os = "linux"))] +pub use wayland::*; +use wayland_backend::client::Backend; + +use crate::{program::WindowManager, Program}; + +#[derive(Debug)] +pub enum Event { + #[cfg(all(feature = "wayland", target_os = "linux"))] + Wayland(sctk_event::SctkEvent), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SurfaceIdWrapper { + LayerSurface(window::Id), + Window(window::Id), + Popup(window::Id), + SessionLock(window::Id), +} +impl SurfaceIdWrapper { + pub fn inner(&self) -> window::Id { + match self { + SurfaceIdWrapper::LayerSurface(id) => *id, + SurfaceIdWrapper::Window(id) => *id, + SurfaceIdWrapper::Popup(id) => *id, + SurfaceIdWrapper::SessionLock(id) => *id, + } + } +} + +#[derive(Debug, Default)] +pub struct PlatformSpecific { + #[cfg(all(feature = "wayland", target_os = "linux"))] + wayland: WaylandSpecific, +} + +impl PlatformSpecific { + pub(crate) fn send_action( + &mut self, + action: iced_runtime::platform_specific::Action, + ) { + match action { + #[cfg(all(feature = "wayland", target_os = "linux"))] + platform_specific::Action::Wayland(a) => { + self.send_wayland(wayland::Action::Action(a)); + } + } + } + + pub(crate) fn send_ready(&mut self) { + #[cfg(all(feature = "wayland", target_os = "linux"))] + { + self.send_wayland(wayland::Action::Ready); + } + } + + pub(crate) fn update_subsurfaces( + &mut self, + id: window::Id, + window: &dyn winit::window::Window, + ) { + #[cfg(all(feature = "wayland", target_os = "linux"))] + { + use sctk::reexports::client::{ + protocol::wl_surface::WlSurface, Proxy, + }; + use wayland_backend::client::ObjectId; + + let Ok(backend) = window.rwh_06_display_handle().display_handle() + else { + log::error!("No display handle"); + return; + }; + + let conn = match backend.as_raw() { + raw_window_handle::RawDisplayHandle::Wayland( + wayland_display_handle, + ) => { + let backend = unsafe { + Backend::from_foreign_display( + wayland_display_handle.display.as_ptr().cast(), + ) + }; + Connection::from_backend(backend) + } + _ => { + return; + } + }; + + let Ok(raw) = window.rwh_06_window_handle().window_handle() else { + log::error!("Invalid window handle {id:?}"); + return; + }; + let wl_surface = match raw.as_raw() { + raw_window_handle::RawWindowHandle::Wayland( + wayland_window_handle, + ) => { + let res = unsafe { + ObjectId::from_ptr( + WlSurface::interface(), + wayland_window_handle.surface.as_ptr().cast(), + ) + }; + let Ok(id) = res else { + log::error!( + "Could not create WlSurface Id from window" + ); + return; + }; + let Ok(surface) = WlSurface::from_id(&conn, id) else { + log::error!("Could not create WlSurface from Id"); + return; + }; + surface + } + + _ => { + log::error!("Unexpected window handle type"); + return; + } + }; + self.wayland.update_subsurfaces(id, &wl_surface); + } + } +} + +pub(crate) fn handle_event<'a, P, C>( + e: Event, + events: &mut Vec<(Option, iced_runtime::core::Event)>, + platform_specific: &mut PlatformSpecific, + program: &'a P, + compositor: &mut C, + window_manager: &mut WindowManager, + debug: &mut Debug, + user_interfaces: &mut UserInterfaces<'a, P>, + clipboard: &mut crate::Clipboard, + #[cfg(feature = "a11y")] adapters: &mut std::collections::HashMap< + window::Id, + (u64, iced_accessibility::accesskit_winit::Adapter), + >, +) where + P: Program, + C: Compositor, +{ + match e { + #[cfg(all(feature = "wayland", target_os = "linux"))] + Event::Wayland(e) => { + platform_specific.wayland.handle_event( + e, + events, + program, + compositor, + window_manager, + debug, + user_interfaces, + clipboard, + #[cfg(feature = "a11y")] + adapters, + ); + } + } +} diff --git a/winit/src/platform_specific/wayland/commands/activation.rs b/winit/src/platform_specific/wayland/commands/activation.rs new file mode 100644 index 0000000000..fa0510317e --- /dev/null +++ b/winit/src/platform_specific/wayland/commands/activation.rs @@ -0,0 +1,31 @@ +use crate::core::window::Id as SurfaceId; +use iced_runtime::{ + self, + platform_specific::{self, wayland}, + task, Action, Task, +}; + +pub fn request_token( + app_id: Option, + window: Option, +) -> Task> { + task::oneshot(|channel| { + Action::PlatformSpecific(platform_specific::Action::Wayland( + wayland::Action::Activation( + wayland::activation::Action::RequestToken { + app_id, + window, + channel, + }, + ), + )) + }) +} + +pub fn activate(window: SurfaceId, token: String) -> Task { + task::effect(Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::Activation( + wayland::activation::Action::Activate { window, token }, + )), + )) +} diff --git a/winit/src/platform_specific/wayland/commands/layer_surface.rs b/winit/src/platform_specific/wayland/commands/layer_surface.rs new file mode 100644 index 0000000000..84c580b4e3 --- /dev/null +++ b/winit/src/platform_specific/wayland/commands/layer_surface.rs @@ -0,0 +1,116 @@ +//! Interact with the window of your application. + +use crate::core::window::Id as SurfaceId; +use iced_runtime::{ + self, + platform_specific::{ + self, + wayland::{ + self, + layer_surface::{IcedMargin, SctkLayerSurfaceSettings}, + }, + }, + task, Action, Task, +}; + +pub use sctk::shell::wlr_layer::{Anchor, KeyboardInteractivity, Layer}; + +// TODO ASHLEY: maybe implement as builder that outputs a batched commands +/// +pub fn get_layer_surface( + builder: SctkLayerSurfaceSettings, +) -> Task { + task::effect(Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::LayerSurface( + wayland::layer_surface::Action::LayerSurface { builder }, + )), + )) +} + +/// +pub fn destroy_layer_surface(id: SurfaceId) -> Task { + task::effect(Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::LayerSurface( + wayland::layer_surface::Action::Destroy(id), + )), + )) +} + +/// +pub fn set_size( + id: SurfaceId, + width: Option, + height: Option, +) -> Task { + task::effect(Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::LayerSurface( + wayland::layer_surface::Action::Size { id, width, height }, + )), + )) +} +/// +pub fn set_anchor(id: SurfaceId, anchor: Anchor) -> Task { + task::effect(Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::LayerSurface( + wayland::layer_surface::Action::Anchor { id, anchor }, + )), + )) +} +/// +pub fn set_exclusive_zone(id: SurfaceId, zone: i32) -> Task { + task::effect(Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::LayerSurface( + wayland::layer_surface::Action::ExclusiveZone { + id, + exclusive_zone: zone, + }, + )), + )) +} + +/// +pub fn set_margin( + id: SurfaceId, + top: i32, + right: i32, + bottom: i32, + left: i32, +) -> Task { + task::effect(Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::LayerSurface( + wayland::layer_surface::Action::Margin { + id, + margin: IcedMargin { + top, + right, + bottom, + left, + }, + }, + )), + )) +} + +/// +pub fn set_keyboard_interactivity( + id: SurfaceId, + keyboard_interactivity: KeyboardInteractivity, +) -> Task { + task::effect(Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::LayerSurface( + wayland::layer_surface::Action::KeyboardInteractivity { + id, + keyboard_interactivity, + }, + )), + )) +} + +/// +pub fn set_layer(id: SurfaceId, layer: Layer) -> Task { + task::effect(Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::LayerSurface( + wayland::layer_surface::Action::Layer { id, layer }, + )), + )) +} diff --git a/winit/src/platform_specific/wayland/commands/mod.rs b/winit/src/platform_specific/wayland/commands/mod.rs new file mode 100644 index 0000000000..d097333d09 --- /dev/null +++ b/winit/src/platform_specific/wayland/commands/mod.rs @@ -0,0 +1,6 @@ +//! Interact with the wayland objects of your application. + +pub mod activation; +pub mod layer_surface; +pub mod popup; +pub mod session_lock; diff --git a/winit/src/platform_specific/wayland/commands/popup.rs b/winit/src/platform_specific/wayland/commands/popup.rs new file mode 100644 index 0000000000..f70d97f45d --- /dev/null +++ b/winit/src/platform_specific/wayland/commands/popup.rs @@ -0,0 +1,42 @@ +//! Interact with the popups of your application. +use crate::core::window::Id as SurfaceId; +use iced_runtime::{ + self, + platform_specific::{ + self, + wayland::{self, popup::SctkPopupSettings}, + }, + task, Action, Task, +}; + +/// +/// +pub fn get_popup(popup: SctkPopupSettings) -> Task { + task::effect(Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::Popup( + wayland::popup::Action::Popup { popup }, + )), + )) +} + +/// +pub fn set_size( + id: SurfaceId, + width: u32, + height: u32, +) -> Task { + task::effect(Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::Popup( + wayland::popup::Action::Size { id, width, height }, + )), + )) +} + +/// +pub fn destroy_popup(id: SurfaceId) -> Task { + task::effect(Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::Popup( + wayland::popup::Action::Destroy { id }, + )), + )) +} diff --git a/winit/src/platform_specific/wayland/commands/session_lock.rs b/winit/src/platform_specific/wayland/commands/session_lock.rs new file mode 100644 index 0000000000..5a327552ad --- /dev/null +++ b/winit/src/platform_specific/wayland/commands/session_lock.rs @@ -0,0 +1,42 @@ +use crate::core::window::Id as SurfaceId; +use iced_runtime::{ + self, + platform_specific::{self, wayland}, + task, Action, Task, +}; +use sctk::reexports::client::protocol::wl_output::WlOutput; + +pub fn lock() -> Task { + task::effect(Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::SessionLock( + wayland::session_lock::Action::Lock, + )), + )) +} + +pub fn unlock() -> Task { + task::effect(Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::SessionLock( + wayland::session_lock::Action::Unlock, + )), + )) +} + +pub fn get_lock_surface( + id: SurfaceId, + output: WlOutput, +) -> Task { + task::effect(Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::SessionLock( + wayland::session_lock::Action::LockSurface { id, output }, + )), + )) +} + +pub fn destroy_lock_surface(id: SurfaceId) -> Task { + task::effect(Action::PlatformSpecific( + platform_specific::Action::Wayland(wayland::Action::SessionLock( + wayland::session_lock::Action::DestroyLockSurface { id }, + )), + )) +} diff --git a/winit/src/platform_specific/wayland/conversion.rs b/winit/src/platform_specific/wayland/conversion.rs new file mode 100644 index 0000000000..e980157fdc --- /dev/null +++ b/winit/src/platform_specific/wayland/conversion.rs @@ -0,0 +1,120 @@ +use iced_runtime::core::{ + keyboard, + mouse::{self, ScrollDelta}, +}; +use sctk::{ + reexports::client::protocol::wl_pointer::AxisSource, + seat::{ + keyboard::Modifiers, + pointer::{ + AxisScroll, BTN_EXTRA, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT, + BTN_SIDE, + }, + }, +}; + +/// An error that occurred while running an application. +#[derive(Debug, thiserror::Error)] +#[error("the futures executor could not be created")] +pub struct KeyCodeError(u32); + +pub fn pointer_button_to_native(button: u32) -> Option { + if button == BTN_LEFT { + Some(mouse::Button::Left) + } else if button == BTN_RIGHT { + Some(mouse::Button::Right) + } else if button == BTN_MIDDLE { + Some(mouse::Button::Middle) + } else if button == BTN_SIDE { + Some(mouse::Button::Back) + } else if button == BTN_EXTRA { + Some(mouse::Button::Forward) + } else { + button.try_into().ok().map(mouse::Button::Other) + } +} + +pub fn pointer_axis_to_native( + source: Option, + horizontal: AxisScroll, + vertical: AxisScroll, +) -> Option { + source.map(|source| match source { + AxisSource::Wheel | AxisSource::WheelTilt => ScrollDelta::Lines { + x: -1. * horizontal.discrete as f32, + y: -1. * vertical.discrete as f32, + }, + _ => ScrollDelta::Pixels { + x: -1. * horizontal.absolute as f32, + y: -1. * vertical.absolute as f32, + }, + }) +} + +pub fn modifiers_to_native(mods: Modifiers) -> keyboard::Modifiers { + let mut native_mods = keyboard::Modifiers::empty(); + if mods.alt { + native_mods = native_mods.union(keyboard::Modifiers::ALT); + } + if mods.ctrl { + native_mods = native_mods.union(keyboard::Modifiers::CTRL); + } + if mods.logo { + native_mods = native_mods.union(keyboard::Modifiers::LOGO); + } + if mods.shift { + native_mods = native_mods.union(keyboard::Modifiers::SHIFT); + } + // TODO Ashley: missing modifiers as platform specific additions? + // if mods.caps_lock { + // native_mods = native_mods.union(keyboard::Modifier); + // } + // if mods.num_lock { + // native_mods = native_mods.union(keyboard::Modifiers::); + // } + native_mods +} + +// pub fn keysym_to_vkey(keysym: RawKeysym) -> Option { +// key_conversion.get(&keysym).cloned() +// } + +// pub(crate) fn cursor_icon(cursor: winit::window::CursorIcon) -> CursorIcon { +// match cursor { +// CursorIcon::Default => todo!(), +// CursorIcon::ContextMenu => todo!(), +// CursorIcon::Help => todo!(), +// CursorIcon::Pointer => todo!(), +// CursorIcon::Progress => todo!(), +// CursorIcon::Wait => todo!(), +// CursorIcon::Cell => todo!(), +// CursorIcon::Crosshair => todo!(), +// CursorIcon::Text => todo!(), +// CursorIcon::VerticalText => todo!(), +// CursorIcon::Alias => todo!(), +// CursorIcon::Copy => todo!(), +// CursorIcon::Move => todo!(), +// CursorIcon::NoDrop => todo!(), +// CursorIcon::NotAllowed => todo!(), +// CursorIcon::Grab => todo!(), +// CursorIcon::Grabbing => todo!(), +// CursorIcon::EResize => todo!(), +// CursorIcon::NResize => todo!(), +// CursorIcon::NeResize => todo!(), +// CursorIcon::NwResize => todo!(), +// CursorIcon::SResize => todo!(), +// CursorIcon::SeResize => todo!(), +// CursorIcon::SwResize => todo!(), +// CursorIcon::WResize => todo!(), +// CursorIcon::EwResize => todo!(), +// CursorIcon::NsResize => todo!(), +// CursorIcon::NeswResize => todo!(), +// CursorIcon::NwseResize => todo!(), +// CursorIcon::ColResize => todo!(), +// CursorIcon::RowResize => todo!(), +// CursorIcon::AllScroll => todo!(), +// CursorIcon::ZoomIn => todo!(), +// CursorIcon::ZoomOut => todo!(), +// _ => todo!(), +// } +// } diff --git a/winit/src/platform_specific/wayland/event_loop/control_flow.rs b/winit/src/platform_specific/wayland/event_loop/control_flow.rs new file mode 100644 index 0000000000..bc920ed478 --- /dev/null +++ b/winit/src/platform_specific/wayland/event_loop/control_flow.rs @@ -0,0 +1,56 @@ +/// Set by the user callback given to the [`EventLoop::run`] method. +/// +/// Indicates the desired behavior of the event loop after [`Event::RedrawEventsCleared`] is emitted. +/// +/// Defaults to [`Poll`]. +/// +/// ## Persistency +/// +/// Almost every change is persistent between multiple calls to the event loop closure within a +/// given run loop. The only exception to this is [`ExitWithCode`] which, once set, cannot be unset. +/// Changes are **not** persistent between multiple calls to `run_return` - issuing a new call will +/// reset the control flow to [`Poll`]. +/// +/// [`ExitWithCode`]: Self::ExitWithCode +/// [`Poll`]: Self::Poll +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ControlFlow { + /// When the current loop iteration finishes, immediately begin a new iteration regardless of + /// whether or not new events are available to process. + /// + /// ## Platform-specific + /// + /// - **Web:** Events are queued and usually sent when `requestAnimationFrame` fires but sometimes + /// the events in the queue may be sent before the next `requestAnimationFrame` callback, for + /// example when the scaling of the page has changed. This should be treated as an implementation + /// detail which should not be relied on. + Poll, + /// When the current loop iteration finishes, suspend the thread until another event arrives. + Wait, + /// When the current loop iteration finishes, suspend the thread until either another event + /// arrives or the given time is reached. + /// + /// Useful for implementing efficient timers. Applications which want to render at the display's + /// native refresh rate should instead use [`Poll`] and the VSync functionality of a graphics API + /// to reduce odds of missed frames. + /// + /// [`Poll`]: Self::Poll + WaitUntil(std::time::Instant), + /// Send a [`LoopDestroyed`] event and stop the event loop. This variant is *sticky* - once set, + /// `control_flow` cannot be changed from `ExitWithCode`, and any future attempts to do so will + /// result in the `control_flow` parameter being reset to `ExitWithCode`. + /// + /// The contained number will be used as exit code. The [`Exit`] constant is a shortcut for this + /// with exit code 0. + /// + /// ## Platform-specific + /// + /// - **Android / iOS / WASM:** The supplied exit code is unused. + /// - **Unix:** On most Unix-like platforms, only the 8 least significant bits will be used, + /// which can cause surprises with negative exit values (`-42` would end up as `214`). See + /// [`std::process::exit`]. + /// + /// [`LoopDestroyed`]: Event::LoopDestroyed + /// [`Exit`]: ControlFlow::Exit + ExitWithCode(i32), +} diff --git a/winit/src/platform_specific/wayland/event_loop/mod.rs b/winit/src/platform_specific/wayland/event_loop/mod.rs new file mode 100644 index 0000000000..74b3cb0c62 --- /dev/null +++ b/winit/src/platform_specific/wayland/event_loop/mod.rs @@ -0,0 +1,387 @@ +pub mod control_flow; +pub mod proxy; +pub mod state; + +#[cfg(feature = "a11y")] +use crate::platform_specific::SurfaceIdWrapper; +use crate::{ + futures::futures::channel::mpsc, + platform_specific::wayland::{ + handlers::{ + wp_fractional_scaling::FractionalScalingManager, + wp_viewporter::ViewporterState, + }, + sctk_event::SctkEvent, + }, + program::Control, + subsurface_widget::SubsurfaceState, +}; + +use raw_window_handle::HasDisplayHandle; +use sctk::reexports::{ + calloop_wayland_source::WaylandSource, client::protocol::wl_subcompositor, +}; +use sctk::{ + activation::ActivationState, + compositor::CompositorState, + globals::GlobalData, + output::OutputState, + reexports::{ + calloop::{self, EventLoop}, + client::{ + globals::registry_queue_init, ConnectError, Connection, Proxy, + }, + }, + registry::RegistryState, + seat::SeatState, + session_lock::SessionLockState, + shell::{wlr_layer::LayerShell, xdg::XdgShell, WaylandSurface}, + shm::Shm, +}; +use state::{FrameStatus, SctkWindow}; +#[cfg(feature = "a11y")] +use std::sync::{Arc, Mutex}; +use std::{ + collections::{HashMap, HashSet}, + fmt::Debug, +}; +use tracing::error; +use wayland_backend::client::Backend; +use winit::event_loop::OwnedDisplayHandle; + +use self::state::SctkState; + +#[derive(Debug, Default, Clone, Copy)] +pub struct Features { + // TODO +} + +pub struct SctkEventLoop { + pub(crate) event_loop: EventLoop<'static, SctkState>, + pub(crate) _features: Features, + pub(crate) state: SctkState, +} + +impl SctkEventLoop { + pub(crate) fn new( + winit_event_sender: mpsc::UnboundedSender, + proxy: winit::event_loop::EventLoopProxy, + display: OwnedDisplayHandle, + ) -> Result< + calloop::channel::Sender, + Box, + > { + let (action_tx, action_rx) = calloop::channel::channel(); + let res = std::thread::spawn(move || { + let Ok(dh) = display.display_handle() else { + log::error!("Failed to get display handle"); + return Ok(()); + }; + let raw_window_handle::RawDisplayHandle::Wayland(wayland_dh) = + dh.as_raw() + else { + panic!("Invalid wayland display handle."); + }; + + let backend = unsafe { + Backend::from_foreign_display( + wayland_dh.display.as_ptr().cast(), + ) + }; + let connection = Connection::from_backend(backend); + + let _display = connection.display(); + let (globals, event_queue) = + registry_queue_init(&connection).unwrap(); + let event_loop = + calloop::EventLoop::::try_new().unwrap(); + let loop_handle = event_loop.handle(); + + let qh = event_queue.handle(); + let registry_state = RegistryState::new(&globals); + + _ = loop_handle + .insert_source(action_rx, |event, _, state| { + match event { + calloop::channel::Event::Msg(e) => match e { + crate::platform_specific::Action::Action(a) => { + if let Err(err) = state.handle_action(a) { + log::warn!("{err:?}"); + } + } + crate::platform_specific::Action::TrackWindow( + window, + id, + ) => { + state.windows.push(SctkWindow { window, id }); + } + crate::Action::RemoveWindow(id) => { + // TODO clean up popups matching the window. + state.windows.retain(|window| id != window.id); + } + crate::platform_specific::Action::SetCursor(icon) => { + if let Some(seat) = state.seats.get_mut(0) { + seat.icon = Some(icon); + seat.set_cursor(&state.connection, icon); + } + } + crate::platform_specific::Action::RequestRedraw(id) => { + let e = state.frame_status.entry(id).or_insert(FrameStatus::RequestedRedraw); + if matches!(e, FrameStatus::Received) { + *e = FrameStatus::Ready; + } + } + crate::platform_specific::Action::PrePresentNotify( + _, + ) => { + // TODO + } + crate::platform_specific::Action::Ready => { + state.ready = true; + } + crate::Action::Dropped(id) => { + _ = state.destroyed.remove(&id.inner()); + } + }, + calloop::channel::Event::Closed => { + log::info!("Calloop channel closed."); + } + } + }) + .unwrap(); + let wayland_source = + WaylandSource::new(connection.clone(), event_queue); + + let wayland_dispatcher = calloop::Dispatcher::new( + wayland_source, + |_, queue, winit_state| queue.dispatch_pending(winit_state), + ); + + let _wayland_source_dispatcher = event_loop + .handle() + .register_dispatcher(wayland_dispatcher.clone()) + .unwrap(); + + let (viewporter_state, fractional_scaling_manager) = + match FractionalScalingManager::new(&globals, &qh) { + Ok(m) => { + let viewporter_state = + match ViewporterState::new(&globals, &qh) { + Ok(s) => Some(s), + Err(e) => { + error!( + "Failed to initialize viewporter: {}", + e + ); + None + } + }; + (viewporter_state, Some(m)) + } + Err(e) => { + error!( + "Failed to initialize fractional scaling manager: {}", + e + ); + (None, None) + } + }; + + let mut state = Self { + event_loop, + state: SctkState { + connection, + registry_state, + seat_state: SeatState::new(&globals, &qh), + output_state: OutputState::new(&globals, &qh), + compositor_state: CompositorState::bind(&globals, &qh) + .expect("wl_compositor is not available"), + shm_state: Shm::bind(&globals, &qh) + .expect("wl_shm is not available"), + xdg_shell_state: XdgShell::bind(&globals, &qh) + .expect("xdg shell is not available"), + layer_shell: LayerShell::bind(&globals, &qh).ok(), + activation_state: ActivationState::bind(&globals, &qh).ok(), + session_lock_state: SessionLockState::new(&globals, &qh), + session_lock: None, + + queue_handle: qh, + loop_handle, + + _cursor_surface: None, + _multipool: None, + outputs: Vec::new(), + seats: Vec::new(), + windows: Vec::new(), + layer_surfaces: Vec::new(), + popups: Vec::new(), + lock_surfaces: Vec::new(), + _kbd_focus: None, + touch_points: HashMap::new(), + sctk_events: Vec::new(), + frame_status: HashMap::new(), + fractional_scaling_manager, + viewporter_state, + compositor_updates: Default::default(), + events_sender: winit_event_sender, + proxy, + id_map: Default::default(), + to_commit: HashMap::new(), + ready: true, + destroyed: HashSet::new(), + pending_popup: Default::default(), + activation_token_ctr: 0, + token_senders: HashMap::new(), + }, + _features: Default::default(), + }; + let wl_compositor = state + .state + .registry_state + .bind_one(&state.state.queue_handle, 1..=6, GlobalData) + .unwrap(); + let wl_subcompositor = state.state.registry_state.bind_one( + &state.state.queue_handle, + 1..=1, + GlobalData, + ); + let wp_viewporter = state.state.registry_state.bind_one( + &state.state.queue_handle, + 1..=1, + GlobalData, + ); + let wl_shm = state + .state + .registry_state + .bind_one(&state.state.queue_handle, 1..=1, GlobalData) + .unwrap(); + let wp_dmabuf = state + .state + .registry_state + .bind_one(&state.state.queue_handle, 2..=4, GlobalData) + .ok(); + let wp_alpha_modifier = state + .state + .registry_state + .bind_one(&state.state.queue_handle, 1..=1, ()) + .ok(); + + if let (Ok(wl_subcompositor), Ok(wp_viewporter)) = + (wl_subcompositor, wp_viewporter) + { + state::send_event( + &state.state.events_sender, + &state.state.proxy, + SctkEvent::Subcompositor(SubsurfaceState { + wl_compositor, + wl_subcompositor, + wp_viewporter, + wl_shm, + wp_dmabuf, + wp_alpha_modifier, + qh: state.state.queue_handle.clone(), + buffers: HashMap::new(), + unmapped_subsurfaces: Vec::new(), + }), + ); + } else { + log::warn!("Subsurfaces not supported.") + } + + log::info!("SCTK setup complete."); + loop { + match state + .state + .events_sender + .unbounded_send(Control::AboutToWait) + { + Ok(_) => {} + Err(err) => { + log::error!( + "SCTK failed to send Control::AboutToWait. {err:?}" + ); + if state.state.events_sender.is_closed() { + return Ok(()); + } + } + } + if !state.state.ready { + continue; + } + + if let Err(err) = + state.event_loop.dispatch(None, &mut state.state) + { + log::error!("SCTK dispatch error: {err}"); + } + let had_events = !state.state.sctk_events.is_empty(); + let mut wake_up = had_events; + + for s in state + .state + .layer_surfaces + .iter() + .map(|s| s.surface.wl_surface()) + .chain( + state.state.popups.iter().map(|s| s.popup.wl_surface()), + ) + .chain( + state + .state + .lock_surfaces + .iter() + .map(|s| s.session_lock_surface.wl_surface()), + ) + { + let id = s.id(); + if state + .state + .frame_status + .get(&id) + .map(|v| !matches!(v, state::FrameStatus::Ready)) + .unwrap_or(true) + || !state.state.id_map.contains_key(&id) + { + continue; + } + wake_up = true; + + _ = s.frame(&state.state.queue_handle, s.clone()); + _ = state.state.frame_status.remove(&id); + _ = state.state.events_sender.unbounded_send( + Control::Winit( + winit::window::WindowId::from(id.as_ptr() as u64), + winit::event::WindowEvent::RedrawRequested, + ), + ); + } + + for e in state.state.sctk_events.drain(..) { + if let SctkEvent::Winit(id, e) = e { + _ = state + .state + .events_sender + .unbounded_send(Control::Winit(id, e)); + } else { + _ = state.state.events_sender.unbounded_send( + Control::PlatformSpecific( + crate::platform_specific::Event::Wayland(e), + ), + ); + } + } + + if wake_up { + state.state.proxy.wake_up(); + } + } + }); + + if res.is_finished() { + log::warn!("SCTK thread finished."); + res.join().map(|_: Result<(), ConnectError>| action_tx) + } else { + Ok(action_tx) + } + } +} diff --git a/winit/src/platform_specific/wayland/event_loop/proxy.rs b/winit/src/platform_specific/wayland/event_loop/proxy.rs new file mode 100644 index 0000000000..7140cef708 --- /dev/null +++ b/winit/src/platform_specific/wayland/event_loop/proxy.rs @@ -0,0 +1,66 @@ +use iced_futures::futures::{ + channel::mpsc, + task::{Context, Poll}, + Sink, +}; +use sctk::reexports::calloop; +use std::pin::Pin; + +/// An event loop proxy that implements `Sink`. +#[derive(Debug)] +pub struct Proxy { + raw: calloop::channel::Sender, +} + +impl Clone for Proxy { + fn clone(&self) -> Self { + Self { + raw: self.raw.clone(), + } + } +} + +impl Proxy { + /// Creates a new [`Proxy`] from an `EventLoopProxy`. + pub fn new(raw: calloop::channel::Sender) -> Self { + Self { raw } + } + /// send an event + pub fn send_event(&self, message: Message) { + let _ = self.raw.send(message); + } +} + +impl Sink for Proxy { + type Error = mpsc::SendError; + + fn poll_ready( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + + fn start_send( + self: Pin<&mut Self>, + message: Message, + ) -> Result<(), Self::Error> { + let _ = self.raw.send(message); + + Ok(()) + } + + fn poll_flush( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_close( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } +} diff --git a/winit/src/platform_specific/wayland/event_loop/state.rs b/winit/src/platform_specific/wayland/event_loop/state.rs new file mode 100644 index 0000000000..99e7f47c71 --- /dev/null +++ b/winit/src/platform_specific/wayland/event_loop/state.rs @@ -0,0 +1,1133 @@ +use crate::{ + handlers::activation::IcedRequestData, platform_specific::{ + wayland::{ + handlers::{ + wp_fractional_scaling::FractionalScalingManager, + wp_viewporter::ViewporterState, + }, + sctk_event::{ + LayerSurfaceEventVariant, SctkEvent, + }, + }, + Event, + }, program::Control +}; +use iced_futures::futures::channel::{mpsc, oneshot}; +use raw_window_handle::HasWindowHandle; +use std::{ + collections::{HashMap, HashSet}, + convert::Infallible, + fmt::Debug, sync::{atomic::AtomicU32, Arc, Mutex}, time::Duration, +}; +use wayland_backend::client::ObjectId; +use winit::{dpi::{LogicalPosition, LogicalSize}, platform::wayland::WindowExtWayland}; + +use iced_runtime::{ + core::{self, touch, Point}, + keyboard::Modifiers, + platform_specific::{ + self, + wayland::{ + layer_surface::{IcedMargin, IcedOutput, SctkLayerSurfaceSettings}, + popup::SctkPopupSettings, + Action, + }, + }, +}; +use sctk::{ + activation::{ActivationState, RequestData}, + compositor::CompositorState, + error::GlobalError, + output::OutputState, + reexports::{ + calloop::{timer::TimeoutAction, LoopHandle}, + client::{ + delegate_noop, + protocol::{ + wl_keyboard::WlKeyboard, + wl_output::WlOutput, + wl_region::WlRegion, + wl_seat::WlSeat, + wl_subsurface::WlSubsurface, + wl_surface::{self, WlSurface}, + wl_touch::WlTouch, + }, + Connection, Proxy, QueueHandle, + }, + }, + registry::RegistryState, + seat::{ + keyboard::KeyEvent, pointer::{CursorIcon, PointerData, ThemedPointer}, touch::TouchData, SeatState + }, + session_lock::{ + SessionLock, SessionLockState, SessionLockSurface, + SessionLockSurfaceConfigure, + }, + shell::{ + wlr_layer::{ + Anchor, KeyboardInteractivity, Layer, LayerShell, LayerSurface, + LayerSurfaceConfigure, + }, + xdg::{ + popup::{Popup, PopupConfigure}, + XdgPositioner, XdgShell, + }, + WaylandSurface, + }, + shm::{multi::MultiPool, Shm}, +}; +use wayland_protocols::{wp::{ + fractional_scale::v1::client::wp_fractional_scale_v1::WpFractionalScaleV1, + viewporter::client::wp_viewport::WpViewport, +}, xdg::shell::client::xdg_surface::XdgSurface}; + +pub static TOKEN_CTR: AtomicU32 = AtomicU32::new(0); + +#[derive(Debug)] +pub(crate) struct SctkSeat { + pub(crate) seat: WlSeat, + pub(crate) kbd: Option, + pub(crate) kbd_focus: Option, + pub(crate) last_kbd_press: Option<(KeyEvent, u32)>, + pub(crate) ptr: Option, + pub(crate) ptr_focus: Option, + pub(crate) last_ptr_press: Option<(u32, u32, u32)>, // (time, button, serial) + pub(crate) touch: Option, + pub(crate) last_touch_down: Option<(u32, i32, u32)>, // (time, point, serial) + pub(crate) _modifiers: Modifiers, + // Cursor icon currently set (by CSDs, or application) + pub(crate) active_icon: Option, + // Cursor icon set by application + pub(crate) icon: Option, +} + +impl SctkSeat { + pub(crate) fn set_cursor(&mut self, conn: &Connection, icon: CursorIcon) { + if let Some(ptr) = self.ptr.as_ref() { + _ = ptr.set_cursor(conn, icon); + self.active_icon = Some(icon); + } + } +} + +#[derive(Debug, Clone)] +pub struct SctkLayerSurface { + pub(crate) id: core::window::Id, + pub(crate) surface: LayerSurface, + pub(crate) current_size: Option>, + pub(crate) layer: Layer, + pub(crate) anchor: Anchor, + pub(crate) keyboard_interactivity: KeyboardInteractivity, + pub(crate) margin: IcedMargin, + pub(crate) exclusive_zone: i32, + pub(crate) last_configure: Option, + pub(crate) _pending_requests: + Vec, + pub(crate) wp_fractional_scale: Option, + pub(crate) common: Arc>, +} + +impl SctkLayerSurface { + pub(crate) fn set_size(&mut self, w: Option, h: Option) { + let mut common = self.common.lock().unwrap(); + common.requested_size = (w, h); + + let (w, h) = (w.unwrap_or_default(), h.unwrap_or_default()); + self.surface.set_size(w, h); + } + + pub(crate) fn update_viewport(&mut self, w: u32, h: u32) { + let common = self.common.lock().unwrap(); + self.current_size = Some(LogicalSize::new(w, h)); + if let Some(viewport) = common.wp_viewport.as_ref() { + // Set inner size without the borders. + viewport.set_destination(w as i32, h as i32); + } + } +} + +#[derive(Debug, Clone)] +pub enum PopupParent { + LayerSurface(WlSurface), + Window(WlSurface), + Popup(WlSurface), +} + +impl PopupParent { + pub fn wl_surface(&self) -> &WlSurface { + match self { + PopupParent::LayerSurface(s) + | PopupParent::Window(s) + | PopupParent::Popup(s) => s, + } + } +} + +#[derive(Debug, Clone)] +pub enum CommonSurface { + Popup(Popup, Arc), + Layer(LayerSurface), + Lock(SessionLockSurface) +} + +impl CommonSurface { + pub fn wl_surface(&self) -> &WlSurface { + let wl_surface = + match self { + CommonSurface::Popup(popup, _) => popup.wl_surface(), + CommonSurface::Layer(layer_surface) => layer_surface.wl_surface(), + CommonSurface::Lock(session_lock_surface) => session_lock_surface.wl_surface(), + }; + wl_surface + } +} + +#[derive(Debug, Clone)] +pub struct Common { + pub(crate) fractional_scale: Option, + pub(crate) has_focus: bool, + pub(crate) ime_pos: LogicalPosition, + pub(crate) ime_size: LogicalSize, + pub(crate) size: LogicalSize, + pub(crate) requested_size: (Option, Option), + pub(crate) wp_viewport: Option, +} + +impl Default for Common { + fn default() -> Self { + Self { fractional_scale: Default::default(), has_focus: Default::default(), ime_pos: Default::default(), ime_size: Default::default(), size: LogicalSize::new(1, 1), requested_size: (None, None), wp_viewport: None} + } +} + +impl From> for Common { + fn from(value: LogicalSize) -> Self { + Common { + size: value, + ..Default::default() + } + } +} + + + +#[derive(Debug)] +pub struct SctkPopup { + pub(crate) popup: Popup, + pub(crate) last_configure: Option, + pub(crate) _pending_requests: + Vec, + pub(crate) data: SctkPopupData, + pub(crate) common: Arc>, + pub(crate) wp_fractional_scale: Option, +} + +impl SctkPopup { + pub(crate) fn set_size(&mut self, w: u32, h: u32, token: u32) { + // update geometry + self.popup + .xdg_surface() + .set_window_geometry(0, 0, w as i32, h as i32); + // update positioner + self.data.positioner.set_size(w as i32, h as i32); + self.popup.reposition(&self.data.positioner, token); + } +} + +#[derive(Debug)] +pub struct SctkLockSurface { + pub(crate) id: core::window::Id, + pub(crate) session_lock_surface: SessionLockSurface, + pub(crate) last_configure: Option, + pub(crate) wp_fractional_scale: Option, + pub(crate) wp_viewport: Option, + pub(crate) common: Arc>, +} + +#[derive(Debug)] +pub struct SctkPopupData { + pub(crate) id: core::window::Id, + pub(crate) parent: PopupParent, + pub(crate) toplevel: WlSurface, + pub(crate) positioner: Arc, +} + +pub struct SctkWindow { + pub(crate) window: Arc, + pub(crate) id: core::window::Id +} + +impl SctkWindow { + pub fn wl_surface(&self, conn: &Connection) -> WlSurface { + let window_handle = self.window.window_handle().unwrap(); + let ptr = { + let raw_window_handle::RawWindowHandle::Wayland(h) = window_handle.as_raw() else { + panic!("Invalid window handle"); + }; + h.surface + + }; + let id = unsafe { ObjectId::from_ptr(WlSurface::interface(), ptr.as_ptr().cast()) }.unwrap(); + WlSurface::from_id(conn, id).unwrap() + } + + pub fn xdg_surface(&self, conn: &Connection) -> XdgSurface { + let window_handle = self.window.xdg_surface_handle().unwrap(); + let ptr = { + let h = window_handle.xdg_surface_handle().expect("Invalid window handle"); + h.as_raw() + + }; + let id = unsafe { ObjectId::from_ptr(XdgSurface::interface(), ptr.as_ptr().cast()) }.unwrap(); + XdgSurface::from_id(conn, id).unwrap() + } +} + +pub(crate) enum FrameStatus { + /// Received frame, but redraw wasn't requested + Received, + /// Requested redraw, but frame wasn't received + RequestedRedraw, + /// Ready for requested redraw + Ready +} + +/// Wrapper to carry sctk state. +pub struct SctkState { + pub(crate) connection: Connection, + + /// the cursor wl_surface + pub(crate) _cursor_surface: Option, + /// a memory pool + pub(crate) _multipool: Option>, + + // all present outputs + pub(crate) outputs: Vec, + // though (for now) only one seat will be active in an iced application at a time, all ought to be tracked + // Active seat is the first seat in the list + pub(crate) seats: Vec, + // Windows / Surfaces + /// Window list containing all SCTK windows. Since those windows aren't allowed + /// to be sent to other threads, they live on the event loop's thread + /// and requests from winit's windows are being forwarded to them either via + /// `WindowUpdate` or buffer on the associated with it `WindowHandle`. + pub(crate) windows: Vec, + pub(crate) layer_surfaces: Vec, + pub(crate) popups: Vec, + pub(crate) lock_surfaces: Vec, + pub(crate) _kbd_focus: Option, + pub(crate) touch_points: HashMap, + + /// Window updates, which are coming from SCTK or the compositor, which require + /// calling back to the sctk's downstream. They are handled right in the event loop, + /// unlike the ones coming from buffers on the `WindowHandle`'s. + pub compositor_updates: Vec, + + /// A sink for window and device events that is being filled during dispatching + /// event loop and forwarded downstream afterwards. + pub(crate) sctk_events: Vec, + pub(crate) frame_status: HashMap, + + /// Send events to winit + pub(crate) events_sender: mpsc::UnboundedSender, + pub(crate) proxy: winit::event_loop::EventLoopProxy, + + // handles + pub(crate) queue_handle: QueueHandle, + pub(crate) loop_handle: LoopHandle<'static, Self>, + + // sctk state objects + /// Viewporter state on the given window. + pub viewporter_state: Option, + pub(crate) fractional_scaling_manager: Option, + pub(crate) registry_state: RegistryState, + pub(crate) seat_state: SeatState, + pub(crate) output_state: OutputState, + pub(crate) compositor_state: CompositorState, + pub(crate) shm_state: Shm, + pub(crate) xdg_shell_state: XdgShell, + pub(crate) layer_shell: Option, + pub(crate) activation_state: Option, + pub(crate) session_lock_state: SessionLockState, + pub(crate) session_lock: Option, + pub(crate) id_map: HashMap, + pub(crate) to_commit: HashMap, + pub(crate) destroyed: HashSet, + pub(crate) ready: bool, + pub(crate) pending_popup: Option<(SctkPopupSettings, usize)>, + + pub(crate) activation_token_ctr: u32, + pub(crate) token_senders: HashMap>>, +} + +/// An error that occurred while running an application. +#[derive(Debug, thiserror::Error)] +pub enum PopupCreationError { + /// Positioner creation failed + #[error("Positioner creation failed")] + PositionerCreationFailed(GlobalError), + + /// The specified parent is missing + #[error("The specified parent is missing")] + ParentMissing, + + /// The specified size is missing + #[error("The specified size is missing")] + SizeMissing, + + /// Popup creation failed + #[error("Popup creation failed")] + PopupCreationFailed(GlobalError), +} + +/// An error that occurred while running an application. +#[derive(Debug, thiserror::Error)] +pub enum LayerSurfaceCreationError { + /// Layer shell is not supported by the compositor + #[error("Layer shell is not supported by the compositor")] + LayerShellNotSupported, + + /// WlSurface creation failed + #[error("WlSurface creation failed")] + WlSurfaceCreationFailed(GlobalError), + + /// LayerSurface creation failed + #[error("Layer Surface creation failed")] + LayerSurfaceCreationFailed(GlobalError), +} + +pub(crate) fn receive_frame(frame_status: &mut HashMap, s: &WlSurface) { + let e = frame_status.entry(s.id()).or_insert(FrameStatus::Received); + if matches!(e, FrameStatus::RequestedRedraw) { + *e = FrameStatus::Ready; + } +} + +impl SctkState { + pub fn request_redraw(&mut self, surface: &WlSurface) { + let e = self.frame_status.entry(surface.id()).or_insert(FrameStatus::RequestedRedraw); + if matches!(e, FrameStatus::Received) { + *e = FrameStatus::Ready; + } + } + + pub fn scale_factor_changed( + &mut self, + surface: &WlSurface, + scale_factor: f64, + legacy: bool, + ) { + let mut id = None; + + if let Some(popup) = self + .popups + .iter_mut() + .find(|p| p.popup.wl_surface() == surface) + { + id = Some(popup.data.id); + if legacy && popup.wp_fractional_scale.is_some() { + return; + } + let mut common = popup.common.lock().unwrap(); + common.fractional_scale = Some(scale_factor); + if legacy { + popup.popup.wl_surface().set_buffer_scale(scale_factor as _); + } + } + + if let Some(layer_surface) = self + .layer_surfaces + .iter_mut() + .find(|l| l.surface.wl_surface() == surface) + { + id = Some(layer_surface.id); + if legacy && layer_surface.wp_fractional_scale.is_some() { + return; + } + let mut common = layer_surface.common.lock().unwrap(); + common.fractional_scale = Some(scale_factor); + if legacy { + let _ = + layer_surface.surface.wl_surface().set_buffer_scale(scale_factor as i32); + } + + } + + if let Some(lock_surface) = self + .lock_surfaces + .iter_mut() + .find(|l| l.session_lock_surface.wl_surface() == surface) + { + id = Some(lock_surface.id); + if legacy && lock_surface.wp_fractional_scale.is_some() { + return; + } + let mut common = lock_surface.common.lock().unwrap(); + common.fractional_scale = Some(scale_factor); + if legacy { + let _ = + lock_surface.session_lock_surface.wl_surface().set_buffer_scale(scale_factor as i32); + } + } + + if let Some(id) = id { + self.sctk_events.push(SctkEvent::SurfaceScaleFactorChanged(scale_factor, surface.clone(), id)); + } + + // TODO winit sets cursor size after handling the change for the window, so maybe that should be done as well. + } +} + +impl SctkState { + pub fn get_popup( + &mut self, + settings: SctkPopupSettings, + ) -> Result< + (core::window::Id, WlSurface, WlSurface, CommonSurface, Arc>), + PopupCreationError, + > { + let (parent, toplevel) = if let Some(parent) = + self.layer_surfaces.iter().find(|l| l.id == settings.parent) + { + ( + PopupParent::LayerSurface(parent.surface.wl_surface().clone()), + parent.surface.wl_surface().clone(), + ) + } else if let Some(parent) = + self.windows.iter().find(|w| w.id == settings.parent) + { + ( + PopupParent::Window(parent.wl_surface(&self.connection)), + parent.wl_surface(&self.connection), + ) + } else if let Some(i) = self + .popups + .iter() + .position(|p| p.data.id == settings.parent) + { + let parent = &self.popups[i]; + ( + PopupParent::Popup(parent.popup.wl_surface().clone()), + parent.data.toplevel.clone(), + ) + } else { + return Err(PopupCreationError::ParentMissing); + }; + + let size = if settings.positioner.size.is_none() { + log::info!("No configured popup size"); + (1, 1) + } else { + settings.positioner.size.unwrap() + }; + + let positioner = XdgPositioner::new(&self.xdg_shell_state) + .map_err(PopupCreationError::PositionerCreationFailed)?; + positioner.set_anchor(settings.positioner.anchor); + positioner.set_anchor_rect( + settings.positioner.anchor_rect.x, + settings.positioner.anchor_rect.y, + settings.positioner.anchor_rect.width, + settings.positioner.anchor_rect.height, + ); + if let Ok(constraint_adjustment) = + settings.positioner.constraint_adjustment.try_into() + { + positioner.set_constraint_adjustment(constraint_adjustment); + } + positioner.set_gravity(settings.positioner.gravity); + positioner.set_offset( + settings.positioner.offset.0, + settings.positioner.offset.1, + ); + if settings.positioner.reactive { + positioner.set_reactive(); + } + positioner.set_size(size.0 as i32, size.1 as i32); + + let grab = settings.grab; + + let wl_surface = + self.compositor_state.create_surface(&self.queue_handle); + _ = self.id_map.insert(wl_surface.id(), settings.id.clone()); + + let (toplevel, popup) = match &parent { + PopupParent::LayerSurface(parent) => { + let Some(parent_layer_surface) = self + .layer_surfaces + .iter() + .find(|w| w.surface.wl_surface() == parent) + else { + return Err(PopupCreationError::ParentMissing); + }; + let popup = Popup::from_surface( + None, + &positioner, + &self.queue_handle, + wl_surface.clone(), + &self.xdg_shell_state, + ) + .map_err(PopupCreationError::PopupCreationFailed)?; + parent_layer_surface.surface.get_popup(popup.xdg_popup()); + (parent_layer_surface.surface.wl_surface(), popup) + } + PopupParent::Window(parent) => { + let Some(parent_window) = self + .windows + .iter() + .find(|w| &w.wl_surface(&self.connection) == parent) + else { + return Err(PopupCreationError::ParentMissing); + }; + ( + &parent_window.wl_surface(&self.connection), + Popup::from_surface( + Some(&parent_window.xdg_surface(&self.connection)), + &positioner, + &self.queue_handle, + wl_surface.clone(), + &self.xdg_shell_state, + ) + .map_err(PopupCreationError::PopupCreationFailed)?, + ) + } + PopupParent::Popup(parent) => { + let Some(parent_xdg) = self.popups.iter().find_map(|p| { + (p.popup.wl_surface() == parent) + .then(|| p.popup.xdg_surface()) + }) else { + return Err(PopupCreationError::ParentMissing); + }; + + ( + &toplevel, + Popup::from_surface( + Some(parent_xdg), + &positioner, + &self.queue_handle, + wl_surface.clone(), + &self.xdg_shell_state, + ) + .map_err(PopupCreationError::PopupCreationFailed)?, + ) + } + }; + if grab { + if let Some(s) = self.seats.first() { + let ptr_data = s.ptr.as_ref().and_then(|p| p.pointer().data::()).and_then(|data| data.latest_button_serial()); + if let Some(serial) = ptr_data.or_else(|| s.touch.as_ref().and_then(|t| t.data::()).and_then(|t| t.latest_down_serial())).or_else(|| s.last_kbd_press + .as_ref() + .map(|p| p.1)) { + popup.xdg_popup().grab( + &s.seat, + serial + ); + } + } else { + log::error!("Can't take grab on popup. Missing serial."); + } + } + popup.xdg_surface().set_window_geometry(0, 0, size.0 as i32, size.1 as i32); + _ = wl_surface.frame(&self.queue_handle, wl_surface.clone()); + wl_surface.commit(); + + let wp_viewport = self.viewporter_state.as_ref().map(|state| { + let viewport = + state.get_viewport(popup.wl_surface(), &self.queue_handle); + viewport.set_destination(size.0 as i32, size.1 as i32); + viewport + }); + let wp_fractional_scale = + self.fractional_scaling_manager.as_ref().map(|fsm| { + fsm.fractional_scaling(popup.wl_surface(), &self.queue_handle) + }); + let mut common: Common = LogicalSize::new(size.0, size.1).into(); + common.wp_viewport = wp_viewport; + let common = Arc::new(Mutex::new(common)); + let positioner = Arc::new(positioner); + + self.popups.push(SctkPopup { + popup: popup.clone(), + data: SctkPopupData { + id: settings.id, + parent: parent.clone(), + toplevel: toplevel.clone(), + positioner: positioner.clone(), + }, + last_configure: None, + _pending_requests: Default::default(), + wp_fractional_scale, + common: common.clone() + }); + + Ok(( + settings.id, + parent.wl_surface().clone(), + toplevel.clone(), + CommonSurface::Popup(popup.clone(), positioner.clone()), + common + )) + } + + pub fn get_layer_surface( + &mut self, + SctkLayerSurfaceSettings { + id, + layer, + keyboard_interactivity, + pointer_interactivity, + anchor, + output, + namespace, + margin, + size, + exclusive_zone, + .. + }: SctkLayerSurfaceSettings, + ) -> Result<(core::window::Id, CommonSurface, Arc>), LayerSurfaceCreationError> { + let wl_output = match output { + IcedOutput::All => None, // TODO + IcedOutput::Active => None, + IcedOutput::Output(output) => Some(output), + }; + + let layer_shell = self + .layer_shell + .as_ref() + .ok_or(LayerSurfaceCreationError::LayerShellNotSupported)?; + let wl_surface = + self.compositor_state.create_surface(&self.queue_handle); + _ = self.id_map.insert(wl_surface.id(), id.clone()); + let mut size = size.unwrap_or((None, None)); + if anchor.contains(Anchor::BOTTOM.union(Anchor::TOP)) { + size.1 = None; + } else { + size.1 = Some(size.1.unwrap_or(1).max(1)); + } + if anchor.contains(Anchor::LEFT.union(Anchor::RIGHT)) { + size.0 = None; + } else { + size.0 = Some(size.0.unwrap_or(1).max(1)); + } + let layer_surface = layer_shell.create_layer_surface( + &self.queue_handle, + wl_surface.clone(), + layer, + Some(namespace), + wl_output.as_ref(), + ); + layer_surface.set_anchor(anchor); + layer_surface.set_keyboard_interactivity(keyboard_interactivity); + layer_surface.set_margin( + margin.top, + margin.right, + margin.bottom, + margin.left, + ); + layer_surface + .set_size(size.0.unwrap_or_default(), size.1.unwrap_or_default()); + layer_surface.set_exclusive_zone(exclusive_zone); + if !pointer_interactivity { + let region = self + .compositor_state + .wl_compositor() + .create_region(&self.queue_handle, ()); + layer_surface.set_input_region(Some(®ion)); + region.destroy(); + } + layer_surface.commit(); + + let wp_viewport = self.viewporter_state.as_ref().map(|state| { + state.get_viewport(layer_surface.wl_surface(), &self.queue_handle) + }); + let wp_fractional_scale = + self.fractional_scaling_manager.as_ref().map(|fsm| { + fsm.fractional_scaling( + layer_surface.wl_surface(), + &self.queue_handle, + ) + }); + let mut common = Common::from(LogicalSize::new(size.0.unwrap_or(1), size.1.unwrap_or(1))); + common.requested_size = size; + common.wp_viewport = wp_viewport; + let common = Arc::new(Mutex::new(common)); + self.layer_surfaces.push(SctkLayerSurface { + id, + surface: layer_surface.clone(), + current_size: None, + layer, + // builder needs to be refactored such that these fields are accessible + anchor, + keyboard_interactivity, + margin, + exclusive_zone, + last_configure: None, + _pending_requests: Vec::new(), + wp_fractional_scale, + common: common.clone() + }); + Ok((id, CommonSurface::Layer(layer_surface), common)) + } + pub fn get_lock_surface( + &mut self, + id: core::window::Id, + output: &WlOutput, + ) -> Option<(CommonSurface, Arc>)> { + if let Some(lock) = self.session_lock.as_ref() { + let wl_surface = + self.compositor_state.create_surface(&self.queue_handle); + _ = self.id_map.insert(wl_surface.id(), id.clone()); + let session_lock_surface = lock.create_lock_surface( + wl_surface.clone(), + output, + &self.queue_handle, + ); + let wp_viewport = self.viewporter_state.as_ref().map(|state| { + let viewport = + state.get_viewport(&wl_surface, &self.queue_handle); + viewport + }); + let wp_fractional_scale = + self.fractional_scaling_manager.as_ref().map(|fsm| { + fsm.fractional_scaling(&wl_surface, &self.queue_handle) + }); + let common = Arc::new(Mutex::new(Common::from( + LogicalSize::new(1, 1) + ))); + self.lock_surfaces.push(SctkLockSurface { + id, + session_lock_surface: session_lock_surface.clone(), + last_configure: None, + wp_fractional_scale, + wp_viewport, + common: common.clone() + }); + Some((CommonSurface::Lock(session_lock_surface), common)) + } else { + None + } + } + + pub(crate) fn handle_action( + &mut self, + action: iced_runtime::platform_specific::wayland::Action, + ) -> Result<(), Infallible> { + match action { + Action::LayerSurface(action) => match action { + platform_specific::wayland::layer_surface::Action::LayerSurface { + builder, + } => { + let title = builder.namespace.clone(); + if let Ok((id, surface, common)) = self.get_layer_surface(builder) { + // TODO Ashley: all surfaces should probably have an optional title for a11y if nothing else + let wl_surface = surface.wl_surface().clone(); + receive_frame(&mut self.frame_status, &wl_surface); + send_event(&self.events_sender, &self.proxy, + SctkEvent::LayerSurfaceEvent { + variant: LayerSurfaceEventVariant::Created(self.queue_handle.clone(), surface, id, common, self.connection.display(), title), + id: wl_surface.clone(), + } + ); + } + } + platform_specific::wayland::layer_surface::Action::Size { + id, + width, + height, + } => { + if let Some(layer_surface) = self.layer_surfaces.iter_mut().find(|l| l.id == id) { + layer_surface.set_size(width, height); + let wl_surface = layer_surface.surface.wl_surface(); + receive_frame(&mut self.frame_status, &wl_surface); + if let Some(mut prev_configure) = layer_surface.last_configure.clone() { + prev_configure.new_size = (width.unwrap_or(prev_configure.new_size.0), width.unwrap_or(prev_configure.new_size.1)); + _ = send_event(&self.events_sender, &self.proxy, + SctkEvent::LayerSurfaceEvent { variant: LayerSurfaceEventVariant::Configure(prev_configure, wl_surface.clone(), false), id: wl_surface.clone()}); + + } + } + }, + platform_specific::wayland::layer_surface::Action::Destroy(id) => { + if let Some(i) = self.layer_surfaces.iter().position(|l| l.id == id) { + let l = self.layer_surfaces.remove(i); + if let Some(destroyed) = self.id_map.remove(&l.surface.wl_surface().id()) { + _ = self.destroyed.insert(destroyed); + } + send_event(&self.events_sender, &self.proxy, SctkEvent::LayerSurfaceEvent { + variant: LayerSurfaceEventVariant::Done, + id: l.surface.wl_surface().clone(), + } + ); + + } + }, + platform_specific::wayland::layer_surface::Action::Anchor { id, anchor } => { + if let Some(layer_surface) = self.layer_surfaces.iter_mut().find(|l| l.id == id) { + layer_surface.anchor = anchor; + layer_surface.surface.set_anchor(anchor); + _ = self.to_commit.insert(id, layer_surface.surface.wl_surface().clone()); + + } + } + platform_specific::wayland::layer_surface::Action::ExclusiveZone { + id, + exclusive_zone, + } => { + if let Some(layer_surface) = self.layer_surfaces.iter_mut().find(|l| l.id == id) { + layer_surface.exclusive_zone = exclusive_zone; + layer_surface.surface.set_exclusive_zone(exclusive_zone); + _ = self.to_commit.insert(id, layer_surface.surface.wl_surface().clone()); + } + }, + platform_specific::wayland::layer_surface::Action::Margin { + id, + margin, + } => { + if let Some(layer_surface) = self.layer_surfaces.iter_mut().find(|l| l.id == id) { + layer_surface.margin = margin; + layer_surface.surface.set_margin(margin.top, margin.right, margin.bottom, margin.left); + _ = self.to_commit.insert(id, layer_surface.surface.wl_surface().clone()); + } + }, + platform_specific::wayland::layer_surface::Action::KeyboardInteractivity { id, keyboard_interactivity } => { + if let Some(layer_surface) = self.layer_surfaces.iter_mut().find(|l| l.id == id) { + layer_surface.keyboard_interactivity = keyboard_interactivity; + layer_surface.surface.set_keyboard_interactivity(keyboard_interactivity); + _ = self.to_commit.insert(id, layer_surface.surface.wl_surface().clone()); + + } + }, + platform_specific::wayland::layer_surface::Action::Layer { id, layer } => { + if let Some(layer_surface) = self.layer_surfaces.iter_mut().find(|l| l.id == id) { + layer_surface.layer = layer; + layer_surface.surface.set_layer(layer); + _ = self.to_commit.insert(id, layer_surface.surface.wl_surface().clone()); + + } + }, + }, + Action::Popup(action) => match action { + platform_specific::wayland::popup::Action::Popup { popup, .. } => { + let parent_mismatch = self.popups.last().is_some_and(|p| { + self.id_map.get(&p.popup.wl_surface().id()).map_or(true, |p| *p != popup.parent) + }); + if !self.destroyed.is_empty() || parent_mismatch { + if parent_mismatch { + for i in 0..self.popups.len() { + let id = self.id_map.get(&self.popups[i].popup.wl_surface().id()); + if let Some(id) = id { + if *id != popup.parent { + _ = self.handle_action(Action::Popup(platform_specific::wayland::popup::Action::Destroy{id: *id})); + } + } + } + } + if self.pending_popup.replace((popup, 0)).is_none() { + let timer = sctk::reexports::calloop::timer::Timer::from_duration(Duration::from_millis(30)); + let queue_handle = self.queue_handle.clone(); + _ = self.loop_handle.insert_source(timer, move |_, _, state| { + let Some((popup, attempt)) = state.pending_popup.take() else { + return TimeoutAction::Drop; + }; + if !state.destroyed.is_empty() || state.popups.last().is_some_and(|p| { + state.id_map.get(&p.popup.wl_surface().id()).map_or(true, |p| *p != popup.parent) + }) { + if attempt < 5 { + state.pending_popup = Some((popup, attempt+1)); + TimeoutAction::ToDuration(Duration::from_millis(30)) + } + else { + TimeoutAction::Drop + } + } else { + match state.get_popup(popup) { + Ok((id, parent_id, toplevel_id, surface, common)) => { + let wl_surface = surface.wl_surface().clone(); + receive_frame(&mut state.frame_status, &wl_surface); + send_event(&state.events_sender, &state.proxy, + SctkEvent::PopupEvent { + variant: crate::platform_specific::wayland::sctk_event::PopupEventVariant::Created(queue_handle.clone(), surface, id, common, state.connection.display()), + toplevel_id, parent_id, id: wl_surface }); + } + Err(err) => { + log::error!("Failed to create popup. {err:?}"); + } + }; + TimeoutAction::Drop + } + }); + } + // log::error!("Invalid popup Id {:?}", popup.id); + } else { + self.pending_popup = None; + match self.get_popup(popup) { + Ok((id, parent_id, toplevel_id, surface, common)) => { + let wl_surface = surface.wl_surface().clone(); + + receive_frame(&mut self.frame_status, &wl_surface); + send_event(&self.events_sender, &self.proxy, + SctkEvent::PopupEvent { + variant: crate::platform_specific::wayland::sctk_event::PopupEventVariant::Created(self.queue_handle.clone(), surface, id, common, self.connection.display()), + toplevel_id, parent_id, id: wl_surface }); + } + Err(err) => { + log::error!("Failed to create popup. {err:?}"); + } + } + } + }, + // XXX popup destruction must be done carefully + // first destroy the uppermost popup, then work down to the requested popup + platform_specific::wayland::popup::Action::Destroy { id } => { + let sctk_popup = match self + .popups + .iter() + .position(|s| s.data.id == id) + { + Some(p) => self.popups.remove(p), + None => { + log::warn!("No popup to destroy"); + return Ok(()); + }, + }; + let mut to_destroy = vec![sctk_popup]; + while let Some(popup_to_destroy) = to_destroy.last() { + match popup_to_destroy.data.parent.clone() { + PopupParent::LayerSurface(_) | PopupParent::Window(_) => { + break; + } + PopupParent::Popup(popup_to_destroy_first) => { + let popup_to_destroy_first = self + .popups + .iter() + .position(|p| p.popup.wl_surface() == &popup_to_destroy_first) + .unwrap(); + let popup_to_destroy_first = self.popups.remove(popup_to_destroy_first); + to_destroy.push(popup_to_destroy_first); + } + } + } + for popup in to_destroy.into_iter().rev() { + if let Some(id) = self.id_map.remove(&popup.popup.wl_surface().id()) { + _ = self.destroyed.insert(id); + } + _ = send_event(&self.events_sender, &self.proxy, + SctkEvent::PopupEvent { variant: crate::sctk_event::PopupEventVariant::Done, toplevel_id: popup.data.toplevel.clone(), parent_id: popup.data.parent.wl_surface().clone(), id: popup.popup.wl_surface().clone() }); + } + }, + platform_specific::wayland::popup::Action::Size { id, width, height } => { + if let Some(sctk_popup) = self + .popups + .iter_mut() + .find(|s| s.data.id == id) + { + // update geometry + // update positioner + sctk_popup.set_size(width, height, TOKEN_CTR.fetch_add(1, std::sync::atomic::Ordering::Relaxed)); + let surface = sctk_popup.popup.wl_surface().clone(); + _ = send_event(&self.events_sender, &self.proxy, + SctkEvent::PopupEvent { variant: crate::sctk_event::PopupEventVariant::Size(width, height), toplevel_id: sctk_popup.data.parent.wl_surface().clone(), parent_id: sctk_popup.data.parent.wl_surface().clone(), id: surface }); + } + }, + }, + Action::Activation(activation_event) => match activation_event { + platform_specific::wayland::activation::Action::RequestToken { app_id, window, channel } => { + if let Some(activation_state) = self.activation_state.as_ref() { + let (seat_and_serial, surface) = if let Some(id) = window { + let surface = self.windows.iter().find(|w| w.id == id) + .map(|w| w.wl_surface(&self.connection).clone()) + .or_else(|| self.layer_surfaces.iter().find(|l| l.id == id) + .map(|l| l.surface.wl_surface().clone()) + ); + let seat_and_serial = surface.as_ref().and_then(|surface| { + self.seats.first().and_then(|seat| if seat.kbd_focus.as_ref().map(|focus| focus == surface).unwrap_or(false) { + seat.last_kbd_press.as_ref().map(|(_, serial)| (seat.seat.clone(), *serial)) + } else if seat.ptr_focus.as_ref().map(|focus| focus == surface).unwrap_or(false) { + seat.last_ptr_press.as_ref().map(|(_, _, serial)| (seat.seat.clone(), *serial)) + } else { + None + }) + }); + + (seat_and_serial, surface) + } else { + (None, None) + }; + + + activation_state.request_token_with_data(&self.queue_handle, + IcedRequestData::new(RequestData { + app_id, + seat_and_serial, + surface, + }, + self.activation_token_ctr + ) + ); + _ = self.token_senders.insert(self.activation_token_ctr, channel); + self.activation_token_ctr = self.activation_token_ctr.wrapping_add(1); + } else { + // if we don't have the global, we don't want to stall the app + _ = channel.send(None); + + } + }, + platform_specific::wayland::activation::Action::Activate { window, token } => { + if let Some(activation_state) = self.activation_state.as_ref() { + if let Some(surface) = self.windows.iter().find(|w| w.id == window).map(|w| w.wl_surface(&self.connection)) { + activation_state.activate::(&surface, token) + } + } + }, + }, + Action::SessionLock(action) => match action { + platform_specific::wayland::session_lock::Action::Lock => { + if self.session_lock.is_none() { + // TODO send message on error? When protocol doesn't exist. + self.session_lock = self.session_lock_state.lock(&self.queue_handle).ok(); + send_event(&self.events_sender, &self.proxy, SctkEvent::SessionLocked); + } + } + platform_specific::wayland::session_lock::Action::Unlock => { + if let Some(session_lock) = self.session_lock.take() { + session_lock.unlock(); + } + // Make sure server processes unlock before client exits + let _ = self.connection.roundtrip(); + + send_event(&self.events_sender, &self.proxy, SctkEvent::SessionUnlocked); + } + platform_specific::wayland::session_lock::Action::LockSurface { id, output } => { + // TODO how to handle this when there's no lock? + if let Some((surface, common)) = self.get_lock_surface(id, &output) { + let wl_surface = surface.wl_surface(); + receive_frame(&mut self.frame_status, &wl_surface); + send_event(&self.events_sender, &self.proxy, SctkEvent::SessionLockSurfaceCreated { queue_handle: self.queue_handle.clone(), surface, native_id: id, common, display: self.connection.display() }); + } + } + platform_specific::wayland::session_lock::Action::DestroyLockSurface { id } => { + if let Some(i) = + self.lock_surfaces.iter().position(|s| { + s.id == id + }) + { + let surface = self.lock_surfaces.remove(i); + if let Some(id) = self.id_map.remove(&surface.session_lock_surface.wl_surface().id()) { + _ = self.destroyed.insert(id); + } + + send_event(&self.events_sender, &self.proxy, SctkEvent::SessionLockSurfaceDone { surface: surface.session_lock_surface.wl_surface().clone() }); + } + } + } + }; + Ok(()) + } +} + +pub(crate) fn send_event(sender: &mpsc::UnboundedSender, proxy: &winit::event_loop::EventLoopProxy, sctk_event: SctkEvent) { + _ = sender.unbounded_send(Control::PlatformSpecific(Event::Wayland(sctk_event))); + proxy.wake_up(); +} + +delegate_noop!(SctkState: ignore WlSubsurface); +delegate_noop!(SctkState: ignore WlRegion); diff --git a/winit/src/platform_specific/wayland/handlers/activation.rs b/winit/src/platform_specific/wayland/handlers/activation.rs new file mode 100644 index 0000000000..5b8b0f5a82 --- /dev/null +++ b/winit/src/platform_specific/wayland/handlers/activation.rs @@ -0,0 +1,47 @@ +use iced_futures::futures::channel::oneshot::Sender; +use sctk::{ + activation::{ActivationHandler, RequestData, RequestDataExt}, + delegate_activation, + reexports::client::protocol::{wl_seat::WlSeat, wl_surface::WlSurface}, +}; + +use crate::platform_specific::wayland::event_loop::state::SctkState; + +pub struct IcedRequestData { + id: u32, + data: RequestData, +} + +impl IcedRequestData { + pub fn new(data: RequestData, id: u32) -> IcedRequestData { + IcedRequestData { data, id } + } +} + +impl RequestDataExt for IcedRequestData { + fn app_id(&self) -> Option<&str> { + self.data.app_id() + } + + fn seat_and_serial(&self) -> Option<(&WlSeat, u32)> { + self.data.seat_and_serial() + } + + fn surface(&self) -> Option<&WlSurface> { + self.data.surface() + } +} + +impl ActivationHandler for SctkState { + type RequestData = IcedRequestData; + + fn new_token(&mut self, token: String, data: &Self::RequestData) { + if let Some(tx) = self.token_senders.remove(&data.id) { + _ = tx.send(Some(token)); + } else { + log::error!("Missing activation request Id."); + } + } +} + +delegate_activation!(SctkState, IcedRequestData); diff --git a/winit/src/platform_specific/wayland/handlers/compositor.rs b/winit/src/platform_specific/wayland/handlers/compositor.rs new file mode 100644 index 0000000000..a1b636839f --- /dev/null +++ b/winit/src/platform_specific/wayland/handlers/compositor.rs @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MPL-2.0-only +use sctk::{ + compositor::CompositorHandler, + delegate_compositor, + reexports::client::{ + protocol::{wl_output, wl_surface}, + Connection, Proxy, QueueHandle, + }, +}; + +use crate::{ + event_loop::state::receive_frame, + platform_specific::wayland::event_loop::state::SctkState, +}; + +impl CompositorHandler for SctkState { + fn scale_factor_changed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + surface: &wl_surface::WlSurface, + new_factor: i32, + ) { + self.scale_factor_changed(surface, new_factor as f64, true); + } + + fn frame( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + surface: &wl_surface::WlSurface, + _time: u32, + ) { + _ = receive_frame(&mut self.frame_status, surface); + } + + fn transform_changed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _surface: &wl_surface::WlSurface, + _new_transform: wl_output::Transform, + ) { + // TODO + // this is not required + } + + fn surface_enter( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_surface::WlSurface, + _: &wl_output::WlOutput, + ) { + } + + fn surface_leave( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_surface::WlSurface, + _: &wl_output::WlOutput, + ) { + } +} + +delegate_compositor!(SctkState); diff --git a/winit/src/platform_specific/wayland/handlers/mod.rs b/winit/src/platform_specific/wayland/handlers/mod.rs new file mode 100644 index 0000000000..e584f44db8 --- /dev/null +++ b/winit/src/platform_specific/wayland/handlers/mod.rs @@ -0,0 +1,37 @@ +// handlers +pub mod activation; +pub mod compositor; +pub mod output; +pub mod seat; +pub mod session_lock; +pub mod shell; +pub mod subcompositor; +pub mod wp_fractional_scaling; +pub mod wp_viewporter; + +use sctk::{ + delegate_registry, delegate_shm, + output::OutputState, + registry::{ProvidesRegistryState, RegistryState}, + registry_handlers, + seat::SeatState, + shm::{Shm, ShmHandler}, +}; + +use crate::platform_specific::wayland::event_loop::state::SctkState; + +impl ShmHandler for SctkState { + fn shm_state(&mut self) -> &mut Shm { + &mut self.shm_state + } +} + +impl ProvidesRegistryState for SctkState { + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } + registry_handlers![OutputState, SeatState,]; +} + +delegate_shm!(SctkState); +delegate_registry!(SctkState); diff --git a/winit/src/platform_specific/wayland/handlers/output.rs b/winit/src/platform_specific/wayland/handlers/output.rs new file mode 100644 index 0000000000..8bc9291752 --- /dev/null +++ b/winit/src/platform_specific/wayland/handlers/output.rs @@ -0,0 +1,49 @@ +use crate::platform_specific::wayland::{ + event_loop::state::SctkState, sctk_event::SctkEvent, +}; +use sctk::{delegate_output, output::OutputHandler}; + +impl OutputHandler for SctkState { + fn output_state(&mut self) -> &mut sctk::output::OutputState { + &mut self.output_state + } + + fn new_output( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + output: sctk::reexports::client::protocol::wl_output::WlOutput, + ) { + self.sctk_events.push(SctkEvent::NewOutput { + id: output.clone(), + info: self.output_state.info(&output), + }); + self.outputs.push(output); + } + + fn update_output( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + output: sctk::reexports::client::protocol::wl_output::WlOutput, + ) { + if let Some(info) = self.output_state.info(&output) { + self.sctk_events.push(SctkEvent::UpdateOutput { + id: output.clone(), + info, + }); + } + } + + fn output_destroyed( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + output: sctk::reexports::client::protocol::wl_output::WlOutput, + ) { + self.sctk_events.push(SctkEvent::RemovedOutput(output)); + // TODO clean up any layer surfaces on this output? + } +} + +delegate_output!(SctkState); diff --git a/winit/src/platform_specific/wayland/handlers/seat/keyboard.rs b/winit/src/platform_specific/wayland/handlers/seat/keyboard.rs new file mode 100644 index 0000000000..faba2fe2f1 --- /dev/null +++ b/winit/src/platform_specific/wayland/handlers/seat/keyboard.rs @@ -0,0 +1,255 @@ +use crate::platform_specific::wayland::{ + event_loop::state::SctkState, + sctk_event::{KeyboardEventVariant, SctkEvent}, +}; +use sctk::reexports::client::Proxy; +use sctk::{ + delegate_keyboard, + seat::keyboard::{KeyboardHandler, Keysym}, +}; + +impl KeyboardHandler for SctkState { + fn enter( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + keyboard: &sctk::reexports::client::protocol::wl_keyboard::WlKeyboard, + surface: &sctk::reexports::client::protocol::wl_surface::WlSurface, + _serial: u32, + _raw: &[u32], + _keysyms: &[Keysym], + ) { + self.request_redraw(surface); + let (i, mut is_active, _seat) = { + let (i, is_active, my_seat) = + match self.seats.iter_mut().enumerate().find_map(|(i, s)| { + if s.kbd.as_ref() == Some(keyboard) { + Some((i, s)) + } else { + None + } + }) { + Some((i, s)) => (i, i == 0, s), + None => return, + }; + _ = my_seat.kbd_focus.replace(surface.clone()); + + let seat = my_seat.seat.clone(); + (i, is_active, seat) + }; + + // TODO Ashley: thoroughly test this + // swap the active seat to be the current seat if the current "active" seat is not focused on the application anyway + if !is_active && self.seats[0].kbd_focus.is_none() { + is_active = true; + self.seats.swap(0, i); + } + + if is_active { + let id = + winit::window::WindowId::from(surface.id().as_ptr() as u64); + if self.windows.iter().any(|w| w.window.id() == id) { + return; + } + self.sctk_events.push(SctkEvent::Winit( + id, + winit::event::WindowEvent::Focused(true), + )); + // self.sctk_events.push(SctkEvent::KeyboardEvent { + // variant: KeyboardEventVariant::Enter(surface.clone()), + // kbd_id: keyboard.clone(), + // seat_id: seat, + // }) + } + } + + fn leave( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + keyboard: &sctk::reexports::client::protocol::wl_keyboard::WlKeyboard, + surface: &sctk::reexports::client::protocol::wl_surface::WlSurface, + _serial: u32, + ) { + self.request_redraw(surface); + let (is_active, seat, kbd) = { + let (is_active, my_seat) = + match self.seats.iter_mut().enumerate().find_map(|(i, s)| { + if s.kbd.as_ref() == Some(keyboard) { + Some((i, s)) + } else { + None + } + }) { + Some((i, s)) => (i == 0, s), + None => return, + }; + let seat = my_seat.seat.clone(); + let kbd = keyboard.clone(); + _ = my_seat.kbd_focus.take(); + (is_active, seat, kbd) + }; + + if is_active { + self.sctk_events.push(SctkEvent::KeyboardEvent { + variant: KeyboardEventVariant::Leave(surface.clone()), + kbd_id: kbd, + seat_id: seat, + surface: surface.clone(), + }); + // if there is another seat with a keyboard focused on a surface make that the new active seat + if let Some(i) = + self.seats.iter().position(|s| s.kbd_focus.is_some()) + { + self.seats.swap(0, i); + let s = &self.seats[0]; + let id = + winit::window::WindowId::from(surface.id().as_ptr() as u64); + if self.windows.iter().any(|w| w.window.id() == id) { + return; + } + self.sctk_events.push(SctkEvent::Winit( + id, + winit::event::WindowEvent::Focused(true), + )); + self.sctk_events.push(SctkEvent::KeyboardEvent { + variant: KeyboardEventVariant::Enter( + s.kbd_focus.clone().unwrap(), + ), + kbd_id: s.kbd.clone().unwrap(), + seat_id: s.seat.clone(), + surface: surface.clone(), + }) + } + } + } + + fn press_key( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + keyboard: &sctk::reexports::client::protocol::wl_keyboard::WlKeyboard, + serial: u32, + event: sctk::seat::keyboard::KeyEvent, + ) { + let (is_active, my_seat) = + match self.seats.iter_mut().enumerate().find_map(|(i, s)| { + if s.kbd.as_ref() == Some(keyboard) { + Some((i, s)) + } else { + None + } + }) { + Some((i, s)) => (i == 0, s), + None => return, + }; + let seat_id = my_seat.seat.clone(); + let kbd_id = keyboard.clone(); + _ = my_seat.last_kbd_press.replace((event.clone(), serial)); + if is_active { + // FIXME can't create winit key events because of private field + // if let Some(id) = id { + // let physical_key = raw_keycode_to_physicalkey(event.raw_code); + // let (logical_key, location) = + // keysym_to_vkey_location(event.keysym); + // self.sctk_events.push(SctkEvent::Winit( + // id, + // winit::event::WindowEvent::KeyboardInput { + // device_id: Default::default(), + // event: winit::event::KeyEvent { + // physical_key, + // logical_key, + // text: event.utf8.map(|s| s.into()), + // location, + // state: winit::event::ElementState::Pressed, + // repeat: false, // TODO we don't have this info... + // }, + // is_synthetic: false, + // }, + // )) + // } + if let Some(surface) = my_seat.kbd_focus.clone() { + self.request_redraw(&surface); + self.sctk_events.push(SctkEvent::KeyboardEvent { + variant: KeyboardEventVariant::Press(event), + kbd_id, + seat_id, + surface, + }); + } + } + } + + fn release_key( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + keyboard: &sctk::reexports::client::protocol::wl_keyboard::WlKeyboard, + _serial: u32, + event: sctk::seat::keyboard::KeyEvent, + ) { + let (is_active, my_seat) = + match self.seats.iter_mut().enumerate().find_map(|(i, s)| { + if s.kbd.as_ref() == Some(keyboard) { + Some((i, s)) + } else { + None + } + }) { + Some((i, s)) => (i == 0, s), + None => return, + }; + let seat_id = my_seat.seat.clone(); + let kbd_id = keyboard.clone(); + + if is_active { + if let Some(surface) = my_seat.kbd_focus.clone() { + self.request_redraw(&surface); + self.sctk_events.push(SctkEvent::KeyboardEvent { + variant: KeyboardEventVariant::Release(event), + kbd_id, + seat_id, + surface, + }); + } + } + } + + fn update_modifiers( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + keyboard: &sctk::reexports::client::protocol::wl_keyboard::WlKeyboard, + _serial: u32, + modifiers: sctk::seat::keyboard::Modifiers, + layout: u32, + ) { + let (is_active, my_seat) = + match self.seats.iter_mut().enumerate().find_map(|(i, s)| { + if s.kbd.as_ref() == Some(keyboard) { + Some((i, s)) + } else { + None + } + }) { + Some((i, s)) => (i == 0, s), + None => return, + }; + let seat_id = my_seat.seat.clone(); + let kbd_id = keyboard.clone(); + + if is_active { + if let Some(surface) = my_seat.kbd_focus.clone() { + self.request_redraw(&surface); + self.sctk_events.push(SctkEvent::KeyboardEvent { + variant: KeyboardEventVariant::Modifiers(modifiers), + kbd_id, + seat_id, + surface, + }); + } + } + } +} + +delegate_keyboard!(SctkState); diff --git a/winit/src/platform_specific/wayland/handlers/seat/mod.rs b/winit/src/platform_specific/wayland/handlers/seat/mod.rs new file mode 100644 index 0000000000..38369b437b --- /dev/null +++ b/winit/src/platform_specific/wayland/handlers/seat/mod.rs @@ -0,0 +1,5 @@ +// TODO support multi-seat handling +pub mod keyboard; +pub mod pointer; +pub mod seat; +pub mod touch; diff --git a/winit/src/platform_specific/wayland/handlers/seat/pointer.rs b/winit/src/platform_specific/wayland/handlers/seat/pointer.rs new file mode 100644 index 0000000000..798ecbe74b --- /dev/null +++ b/winit/src/platform_specific/wayland/handlers/seat/pointer.rs @@ -0,0 +1,184 @@ +use crate::{ + event_loop::state::FrameStatus, + platform_specific::wayland::{ + event_loop::state::SctkState, sctk_event::SctkEvent, + }, +}; +use sctk::{ + delegate_pointer, + reexports::client::Proxy, + seat::pointer::{ + CursorIcon, PointerEvent, PointerEventKind, PointerHandler, + }, +}; +use winit::{ + dpi::PhysicalPosition, + event::{MouseButton, MouseScrollDelta, TouchPhase, WindowEvent}, +}; + +impl PointerHandler for SctkState { + fn pointer_frame( + &mut self, + conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + pointer: &sctk::reexports::client::protocol::wl_pointer::WlPointer, + events: &[sctk::seat::pointer::PointerEvent], + ) { + let (is_active, my_seat) = + match self.seats.iter_mut().enumerate().find_map(|(i, s)| { + if s.ptr.as_ref().map(|p| p.pointer()) == Some(pointer) { + Some((i, s)) + } else { + None + } + }) { + Some((i, s)) => (i == 0, s), + None => return, + }; + + // track events, but only forward for the active seat + for e in events { + if my_seat.active_icon != my_seat.icon { + // Restore cursor that was set by appliction, or default + my_seat.set_cursor( + conn, + my_seat.icon.unwrap_or(CursorIcon::Default), + ); + } + + if is_active { + let id = winit::window::WindowId::from( + e.surface.id().as_ptr() as u64 + ); + if self.windows.iter().any(|w| w.window.id() == id) { + continue; + } + let entry = self + .frame_status + .entry(e.surface.id()) + .or_insert(FrameStatus::RequestedRedraw); + if matches!(entry, FrameStatus::Received) { + *entry = FrameStatus::Ready; + } + if let PointerEventKind::Motion { time } = &e.kind { + self.sctk_events.push(SctkEvent::PointerEvent { + variant: PointerEvent { + surface: e.surface.clone(), + position: e.position, + kind: PointerEventKind::Motion { time: *time }, + }, + ptr_id: pointer.clone(), + seat_id: my_seat.seat.clone(), + }); + } else { + self.sctk_events.push(SctkEvent::Winit( + id, + match e.kind { + PointerEventKind::Enter { serial } => { + WindowEvent::CursorEntered { + device_id: Default::default(), + } + } + PointerEventKind::Leave { serial } => { + WindowEvent::CursorLeft { + device_id: Default::default(), + } + } + PointerEventKind::Motion { time } => { + WindowEvent::CursorMoved { + device_id: Default::default(), + position: e.position.into(), + } + } + PointerEventKind::Press { + time, + button, + serial, + } => WindowEvent::MouseInput { + device_id: Default::default(), + state: winit::event::ElementState::Pressed, + button: wayland_button_to_winit(button), + }, + PointerEventKind::Release { + time, + button, + serial, + } => WindowEvent::MouseInput { + device_id: Default::default(), + state: winit::event::ElementState::Released, + button: wayland_button_to_winit(button), + }, + PointerEventKind::Axis { + time, + horizontal, + vertical, + source, + } => WindowEvent::MouseWheel { + device_id: Default::default(), + delta: if horizontal.discrete > 0 { + MouseScrollDelta::LineDelta( + horizontal.discrete as f32, + vertical.discrete as f32, + ) + } else { + MouseScrollDelta::PixelDelta( + PhysicalPosition::new( + horizontal.absolute, + vertical.absolute, + ), + ) + }, + phase: if horizontal.stop { + TouchPhase::Ended + } else { + TouchPhase::Moved + }, + }, + }, + )); + } + } + match e.kind { + PointerEventKind::Enter { .. } => { + _ = my_seat.ptr_focus.replace(e.surface.clone()); + } + PointerEventKind::Leave { .. } => { + _ = my_seat.ptr_focus.take(); + _ = my_seat.active_icon = None; + } + PointerEventKind::Press { + time, + button, + serial, + } => { + _ = my_seat.last_ptr_press.replace((time, button, serial)); + } + // TODO revisit events that ought to be handled and change internal state + _ => {} + } + } + } +} + +/// Convert the Wayland button into winit. +fn wayland_button_to_winit(button: u32) -> MouseButton { + // These values are coming from . + const BTN_LEFT: u32 = 0x110; + const BTN_RIGHT: u32 = 0x111; + const BTN_MIDDLE: u32 = 0x112; + const BTN_SIDE: u32 = 0x113; + const BTN_EXTRA: u32 = 0x114; + const BTN_FORWARD: u32 = 0x115; + const BTN_BACK: u32 = 0x116; + + match button { + BTN_LEFT => MouseButton::Left, + BTN_RIGHT => MouseButton::Right, + BTN_MIDDLE => MouseButton::Middle, + BTN_BACK | BTN_SIDE => MouseButton::Back, + BTN_FORWARD | BTN_EXTRA => MouseButton::Forward, + button => MouseButton::Other(button as u16), + } +} + +delegate_pointer!(SctkState); diff --git a/winit/src/platform_specific/wayland/handlers/seat/seat.rs b/winit/src/platform_specific/wayland/handlers/seat/seat.rs new file mode 100644 index 0000000000..ee7d83e56e --- /dev/null +++ b/winit/src/platform_specific/wayland/handlers/seat/seat.rs @@ -0,0 +1,214 @@ +use crate::platform_specific::wayland::{ + event_loop::{state::SctkSeat, state::SctkState}, + sctk_event::{KeyboardEventVariant, SctkEvent, SeatEventVariant}, +}; +use iced_runtime::keyboard::Modifiers; +use sctk::{ + delegate_seat, + reexports::client::{protocol::wl_keyboard::WlKeyboard, Proxy}, + seat::{pointer::ThemeSpec, SeatHandler}, +}; + +impl SeatHandler for SctkState { + fn seat_state(&mut self) -> &mut sctk::seat::SeatState { + &mut self.seat_state + } + + fn new_seat( + &mut self, + _conn: &sctk::reexports::client::Connection, + qh: &sctk::reexports::client::QueueHandle, + seat: sctk::reexports::client::protocol::wl_seat::WlSeat, + ) { + self.sctk_events.push(SctkEvent::SeatEvent { + variant: SeatEventVariant::New, + id: seat.clone(), + }); + + self.seats.push(SctkSeat { + seat, + kbd: None, + ptr: None, + touch: None, + _modifiers: Modifiers::default(), + kbd_focus: None, + ptr_focus: None, + last_ptr_press: None, + last_kbd_press: None, + last_touch_down: None, + icon: None, + active_icon: None, + }); + } + + fn new_capability( + &mut self, + _conn: &sctk::reexports::client::Connection, + qh: &sctk::reexports::client::QueueHandle, + seat: sctk::reexports::client::protocol::wl_seat::WlSeat, + capability: sctk::seat::Capability, + ) { + let my_seat = match self.seats.iter_mut().find(|s| s.seat == seat) { + Some(s) => s, + None => { + self.seats.push(SctkSeat { + seat: seat.clone(), + kbd: None, + ptr: None, + touch: None, + + _modifiers: Modifiers::default(), + kbd_focus: None, + ptr_focus: None, + last_ptr_press: None, + last_kbd_press: None, + last_touch_down: None, + icon: None, + active_icon: None, + }); + self.seats.last_mut().unwrap() + } + }; + // TODO data device + match capability { + sctk::seat::Capability::Keyboard => { + let seat_clone = seat.clone(); + let seat_clone_2 = seat.clone(); + if let Ok(kbd) = self.seat_state.get_keyboard_with_repeat( + qh, + &seat, + None, + self.loop_handle.clone(), + Box::new(move |state, kbd: &WlKeyboard, e| { + let Some(my_seat) = state + .seats + .iter_mut() + .find(|s| s.seat == seat_clone_2) + else { + return; + }; + if let Some(surface) = my_seat.kbd_focus.clone() { + state.sctk_events.push(SctkEvent::KeyboardEvent { + variant: KeyboardEventVariant::Repeat(e), + kbd_id: kbd.clone(), + seat_id: seat_clone.clone(), + surface, + }); + } + }), + ) { + self.sctk_events.push(SctkEvent::SeatEvent { + variant: SeatEventVariant::NewCapability( + capability, + kbd.id(), + ), + id: seat.clone(), + }); + _ = my_seat.kbd.replace(kbd); + } + } + sctk::seat::Capability::Pointer => { + let surface = self.compositor_state.create_surface(qh); + + if let Ok(ptr) = self.seat_state.get_pointer_with_theme( + qh, + &seat, + self.shm_state.wl_shm(), + surface, + ThemeSpec::default(), + ) { + self.sctk_events.push(SctkEvent::SeatEvent { + variant: SeatEventVariant::NewCapability( + capability, + ptr.pointer().id(), + ), + id: seat.clone(), + }); + _ = my_seat.ptr.replace(ptr); + } + } + sctk::seat::Capability::Touch => { + if let Some(touch) = self.seat_state.get_touch(qh, &seat).ok() { + self.sctk_events.push(SctkEvent::SeatEvent { + variant: SeatEventVariant::NewCapability( + capability, + touch.id(), + ), + id: seat.clone(), + }); + _ = my_seat.touch.replace(touch); + } + } + _ => unimplemented!(), + } + } + + fn remove_capability( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + seat: sctk::reexports::client::protocol::wl_seat::WlSeat, + capability: sctk::seat::Capability, + ) { + let my_seat = match self.seats.iter_mut().find(|s| s.seat == seat) { + Some(s) => s, + None => return, + }; + + // TODO data device + match capability { + // TODO use repeating kbd? + sctk::seat::Capability::Keyboard => { + if let Some(kbd) = my_seat.kbd.take() { + self.sctk_events.push(SctkEvent::SeatEvent { + variant: SeatEventVariant::RemoveCapability( + capability, + kbd.id(), + ), + id: seat.clone(), + }); + } + } + sctk::seat::Capability::Pointer => { + if let Some(ptr) = my_seat.ptr.take() { + self.sctk_events.push(SctkEvent::SeatEvent { + variant: SeatEventVariant::RemoveCapability( + capability, + ptr.pointer().id(), + ), + id: seat.clone(), + }); + } + } + sctk::seat::Capability::Touch => { + if let Some(touch) = my_seat.touch.take() { + self.sctk_events.push(SctkEvent::SeatEvent { + variant: SeatEventVariant::RemoveCapability( + capability, + touch.id(), + ), + id: seat.clone(), + }); + } + } + _ => unimplemented!(), + } + } + + fn remove_seat( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + seat: sctk::reexports::client::protocol::wl_seat::WlSeat, + ) { + self.sctk_events.push(SctkEvent::SeatEvent { + variant: SeatEventVariant::Remove, + id: seat.clone(), + }); + if let Some(i) = self.seats.iter().position(|s| s.seat == seat) { + _ = self.seats.remove(i); + } + } +} + +delegate_seat!(SctkState); diff --git a/winit/src/platform_specific/wayland/handlers/seat/touch.rs b/winit/src/platform_specific/wayland/handlers/seat/touch.rs new file mode 100644 index 0000000000..99e6cacad5 --- /dev/null +++ b/winit/src/platform_specific/wayland/handlers/seat/touch.rs @@ -0,0 +1,157 @@ +// TODO handle multiple seats? + +use crate::{ + event_loop::state::FrameStatus, + platform_specific::wayland::{ + event_loop::state::SctkState, sctk_event::SctkEvent, + }, +}; +use iced_runtime::core::{touch, Point}; +use sctk::{ + delegate_touch, + reexports::client::{ + protocol::{wl_surface::WlSurface, wl_touch::WlTouch}, + Connection, Proxy, QueueHandle, + }, + seat::touch::TouchHandler, +}; + +impl TouchHandler for SctkState { + fn down( + &mut self, + _: &Connection, + _: &QueueHandle, + touch: &WlTouch, + serial: u32, + time: u32, + surface: WlSurface, + id: i32, + position: (f64, f64), + ) { + self.request_redraw(&surface); + let Some(my_seat) = self + .seats + .iter_mut() + .find(|s| s.touch.as_ref() == Some(touch)) + else { + return; + }; + + _ = my_seat.last_touch_down.replace((time, id, serial)); + + let id = touch::Finger(id as u64); + let position = Point::new(position.0 as f32, position.1 as f32); + _ = self.touch_points.insert(id, (surface.clone(), position)); + self.sctk_events.push(SctkEvent::TouchEvent { + variant: touch::Event::FingerPressed { id, position }, + touch_id: touch.clone(), + seat_id: my_seat.seat.clone(), + surface, + }); + } + + fn up( + &mut self, + _: &Connection, + _: &QueueHandle, + touch: &WlTouch, + _serial: u32, + _time: u32, + id: i32, + ) { + let Some(my_seat) = + self.seats.iter().find(|s| s.touch.as_ref() == Some(touch)) + else { + return; + }; + + let id = touch::Finger(id as u64); + if let Some((surface, position)) = self.touch_points.get(&id).cloned() { + self.sctk_events.push(SctkEvent::TouchEvent { + variant: touch::Event::FingerLifted { id, position }, + touch_id: touch.clone(), + seat_id: my_seat.seat.clone(), + surface, + }); + } + } + fn motion( + &mut self, + _: &Connection, + _: &QueueHandle, + touch: &WlTouch, + _time: u32, + id: i32, + position: (f64, f64), + ) { + let Some(my_seat) = + self.seats.iter().find(|s| s.touch.as_ref() == Some(touch)) + else { + return; + }; + + let id = touch::Finger(id as u64); + let position = Point::new(position.0 as f32, position.1 as f32); + if let Some((surface, position_ref)) = self.touch_points.get_mut(&id) { + let entry = self + .frame_status + .entry(surface.id()) + .or_insert(FrameStatus::RequestedRedraw); + if matches!(entry, FrameStatus::Received) { + *entry = FrameStatus::Ready; + } + *position_ref = position; + self.sctk_events.push(SctkEvent::TouchEvent { + variant: touch::Event::FingerMoved { id, position }, + touch_id: touch.clone(), + seat_id: my_seat.seat.clone(), + surface: surface.clone(), + }); + } + } + + fn shape( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlTouch, + _: i32, + _: f64, + _: f64, + ) { + } + + fn orientation( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlTouch, + _: i32, + _: f64, + ) { + } + + fn cancel( + &mut self, + _: &Connection, + _: &QueueHandle, + touch: &WlTouch, + ) { + let Some(my_seat) = + self.seats.iter().find(|s| s.touch.as_ref() == Some(touch)) + else { + return; + }; + + for (id, (surface, position)) in self.touch_points.drain() { + self.sctk_events.push(SctkEvent::TouchEvent { + variant: touch::Event::FingerLost { id, position }, + touch_id: touch.clone(), + seat_id: my_seat.seat.clone(), + surface, + }); + } + } +} + +delegate_touch!(SctkState); diff --git a/winit/src/platform_specific/wayland/handlers/session_lock.rs b/winit/src/platform_specific/wayland/handlers/session_lock.rs new file mode 100644 index 0000000000..27ae70b720 --- /dev/null +++ b/winit/src/platform_specific/wayland/handlers/session_lock.rs @@ -0,0 +1,58 @@ +use crate::platform_specific::wayland::{ + handlers::SctkState, sctk_event::SctkEvent, +}; +use sctk::{ + delegate_session_lock, + reexports::client::{Connection, QueueHandle}, + session_lock::{ + SessionLock, SessionLockHandler, SessionLockSurface, + SessionLockSurfaceConfigure, + }, +}; + +impl SessionLockHandler for SctkState { + fn locked( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _session_lock: SessionLock, + ) { + self.sctk_events.push(SctkEvent::SessionLocked); + } + + fn finished( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _session_lock: SessionLock, + ) { + self.sctk_events.push(SctkEvent::SessionLockFinished); + } + + fn configure( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + session_lock_surface: SessionLockSurface, + configure: SessionLockSurfaceConfigure, + _serial: u32, + ) { + let lock_surface = match self.lock_surfaces.iter_mut().find(|s| { + s.session_lock_surface.wl_surface() + == session_lock_surface.wl_surface() + }) { + Some(l) => l, + None => return, + }; + let first = lock_surface.last_configure.is_none(); + _ = lock_surface.last_configure.replace(configure.clone()); + self.sctk_events + .push(SctkEvent::SessionLockSurfaceConfigure { + surface: session_lock_surface.wl_surface().clone(), + configure, + first, + }); + } +} + +delegate_session_lock!(SctkState); diff --git a/winit/src/platform_specific/wayland/handlers/shell/layer.rs b/winit/src/platform_specific/wayland/handlers/shell/layer.rs new file mode 100644 index 0000000000..d7d8b3e85e --- /dev/null +++ b/winit/src/platform_specific/wayland/handlers/shell/layer.rs @@ -0,0 +1,118 @@ +use crate::platform_specific::wayland::{ + event_loop::state::SctkState, + sctk_event::{LayerSurfaceEventVariant, SctkEvent}, +}; +use sctk::{ + delegate_layer, + reexports::client::Proxy, + shell::{ + wlr_layer::{Anchor, KeyboardInteractivity, LayerShellHandler}, + WaylandSurface, + }, +}; +use std::fmt::Debug; +use winit::dpi::LogicalSize; + +impl LayerShellHandler for SctkState { + fn closed( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + layer: &sctk::shell::wlr_layer::LayerSurface, + ) { + let layer = match self.layer_surfaces.iter().position(|s| { + s.surface.wl_surface().id() == layer.wl_surface().id() + }) { + Some(w) => self.layer_surfaces.remove(w), + None => return, + }; + + self.sctk_events.push(SctkEvent::LayerSurfaceEvent { + variant: LayerSurfaceEventVariant::Done, + id: layer.surface.wl_surface().clone(), + }) + // TODO popup cleanup + } + + fn configure( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + layer: &sctk::shell::wlr_layer::LayerSurface, + mut configure: sctk::shell::wlr_layer::LayerSurfaceConfigure, + _serial: u32, + ) { + self.request_redraw(layer.wl_surface()); + let layer = + match self.layer_surfaces.iter_mut().find(|s| { + s.surface.wl_surface().id() == layer.wl_surface().id() + }) { + Some(l) => l, + None => return, + }; + let common = layer.common.lock().unwrap(); + let requested_size = common.requested_size; + drop(common); + configure.new_size.0 = if let Some(w) = requested_size.0 { + w + } else { + configure.new_size.0.max(1) + }; + configure.new_size.1 = if let Some(h) = requested_size.1 { + h + } else { + configure.new_size.1.max(1) + }; + + layer.update_viewport(configure.new_size.0, configure.new_size.1); + let first = layer.last_configure.is_none(); + _ = layer.last_configure.replace(configure.clone()); + let mut common = layer.common.lock().unwrap(); + common.size = + LogicalSize::new(configure.new_size.0, configure.new_size.1); + self.sctk_events.push(SctkEvent::LayerSurfaceEvent { + variant: LayerSurfaceEventVariant::Configure( + configure, + layer.surface.wl_surface().clone(), + first, + ), + id: layer.surface.wl_surface().clone(), + }); + } +} + +delegate_layer!(SctkState); + +#[allow(dead_code)] +/// A request to SCTK window from Winit window. +#[derive(Debug, Clone)] +pub enum LayerSurfaceRequest { + /// Set fullscreen. + /// + /// Passing `None` will set it on the current monitor. + Size(LogicalSize), + + /// Unset fullscreen. + UnsetFullscreen, + + /// Show cursor for the certain window or not. + ShowCursor(bool), + + /// Set anchor + Anchor(Anchor), + + /// Set margin + ExclusiveZone(i32), + + /// Set margin + Margin(u32), + + /// Passthrough mouse input to underlying windows. + KeyboardInteractivity(KeyboardInteractivity), + + /// Redraw was requested. + Redraw, + + /// Window should be closed. + Close, +} diff --git a/winit/src/platform_specific/wayland/handlers/shell/mod.rs b/winit/src/platform_specific/wayland/handlers/shell/mod.rs new file mode 100644 index 0000000000..5556c08d3e --- /dev/null +++ b/winit/src/platform_specific/wayland/handlers/shell/mod.rs @@ -0,0 +1,3 @@ +pub mod layer; +pub mod xdg_popup; +pub mod xdg_window; diff --git a/winit/src/platform_specific/wayland/handlers/shell/xdg_popup.rs b/winit/src/platform_specific/wayland/handlers/shell/xdg_popup.rs new file mode 100644 index 0000000000..0975dc0b9e --- /dev/null +++ b/winit/src/platform_specific/wayland/handlers/shell/xdg_popup.rs @@ -0,0 +1,92 @@ +use crate::platform_specific::wayland::{ + event_loop::state::{self, PopupParent, SctkState}, + sctk_event::{PopupEventVariant, SctkEvent}, +}; +use sctk::{ + delegate_xdg_popup, reexports::client::Proxy, + shell::xdg::popup::PopupHandler, +}; + +impl PopupHandler for SctkState { + fn configure( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + popup: &sctk::shell::xdg::popup::Popup, + configure: sctk::shell::xdg::popup::PopupConfigure, + ) { + self.request_redraw(popup.wl_surface()); + let sctk_popup = match self.popups.iter_mut().find(|s| { + s.popup.wl_surface().clone() == popup.wl_surface().clone() + }) { + Some(p) => p, + None => return, + }; + let first = sctk_popup.last_configure.is_none(); + _ = sctk_popup.last_configure.replace(configure.clone()); + + self.sctk_events.push(SctkEvent::PopupEvent { + variant: PopupEventVariant::Configure( + configure, + popup.wl_surface().clone(), + first, + ), + id: popup.wl_surface().clone(), + toplevel_id: sctk_popup.data.toplevel.clone(), + parent_id: match &sctk_popup.data.parent { + PopupParent::LayerSurface(s) => s.clone(), + PopupParent::Window(s) => s.clone(), + PopupParent::Popup(s) => s.clone(), + }, + }); + } + + fn done( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + popup: &sctk::shell::xdg::popup::Popup, + ) { + let sctk_popup = match self.popups.iter().position(|s| { + s.popup.wl_surface().clone() == popup.wl_surface().clone() + }) { + Some(p) => self.popups.remove(p), + None => return, + }; + let mut to_destroy = vec![sctk_popup]; + while let Some(popup_to_destroy) = to_destroy.last() { + match popup_to_destroy.data.parent.clone() { + state::PopupParent::LayerSurface(_) + | state::PopupParent::Window(_) => { + break; + } + state::PopupParent::Popup(popup_to_destroy_first) => { + let popup_to_destroy_first = self + .popups + .iter() + .position(|p| { + p.popup.wl_surface() == &popup_to_destroy_first + }) + .unwrap(); + let popup_to_destroy_first = + self.popups.remove(popup_to_destroy_first); + to_destroy.push(popup_to_destroy_first); + } + } + } + for popup in to_destroy.into_iter().rev() { + if let Some(id) = self.id_map.remove(&popup.popup.wl_surface().id()) + { + _ = self.destroyed.insert(id); + } + + self.sctk_events.push(SctkEvent::PopupEvent { + variant: PopupEventVariant::Done, + toplevel_id: popup.data.toplevel.clone(), + parent_id: popup.data.parent.wl_surface().clone(), + id: popup.popup.wl_surface().clone(), + }); + } + } +} +delegate_xdg_popup!(SctkState); diff --git a/winit/src/platform_specific/wayland/handlers/shell/xdg_window.rs b/winit/src/platform_specific/wayland/handlers/shell/xdg_window.rs new file mode 100644 index 0000000000..24da3cfa23 --- /dev/null +++ b/winit/src/platform_specific/wayland/handlers/shell/xdg_window.rs @@ -0,0 +1,27 @@ +use crate::platform_specific::wayland::event_loop::state::SctkState; +use sctk::{ + delegate_xdg_shell, delegate_xdg_window, shell::xdg::window::WindowHandler, +}; + +impl WindowHandler for SctkState { + fn request_close( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + _window: &sctk::shell::xdg::window::Window, + ) { + } + + fn configure( + &mut self, + _conn: &sctk::reexports::client::Connection, + _qh: &sctk::reexports::client::QueueHandle, + _window: &sctk::shell::xdg::window::Window, + _configure: sctk::shell::xdg::window::WindowConfigure, + _serial: u32, + ) { + } +} + +delegate_xdg_window!(SctkState); +delegate_xdg_shell!(SctkState); diff --git a/winit/src/platform_specific/wayland/handlers/subcompositor.rs b/winit/src/platform_specific/wayland/handlers/subcompositor.rs new file mode 100644 index 0000000000..40f9a0db8f --- /dev/null +++ b/winit/src/platform_specific/wayland/handlers/subcompositor.rs @@ -0,0 +1,4 @@ +use crate::platform_specific::wayland::handlers::SctkState; +use sctk::delegate_subcompositor; + +delegate_subcompositor!(SctkState); diff --git a/winit/src/platform_specific/wayland/handlers/wp_fractional_scaling.rs b/winit/src/platform_specific/wayland/handlers/wp_fractional_scaling.rs new file mode 100644 index 0000000000..5ad326afd7 --- /dev/null +++ b/winit/src/platform_specific/wayland/handlers/wp_fractional_scaling.rs @@ -0,0 +1,91 @@ +// From: https://github.com/rust-windowing/winit/blob/master/src/platform_impl/linux/wayland/types/wp_fractional_scaling.rs +//! Handling of the fractional scaling. + + +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::Dispatch; +use sctk::reexports::client::{delegate_dispatch, Connection, Proxy, QueueHandle}; +use sctk::reexports::protocols::wp::fractional_scale::v1::client::wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1; +use sctk::reexports::protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1::Event as FractionalScalingEvent; +use sctk::reexports::protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1::WpFractionalScaleV1; + +use sctk::globals::GlobalData; + +use crate::platform_specific::wayland::event_loop::state::SctkState; + +/// The scaling factor denominator. +const SCALE_DENOMINATOR: f64 = 120.; + +/// Fractional scaling manager. +#[derive(Debug)] +pub struct FractionalScalingManager { + manager: WpFractionalScaleManagerV1, +} + +pub struct FractionalScaling { + /// The surface used for scaling. + surface: WlSurface, +} + +impl FractionalScalingManager { + /// Create new viewporter. + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle, + ) -> Result { + let manager = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { manager }) + } + + pub fn fractional_scaling( + &self, + surface: &WlSurface, + queue_handle: &QueueHandle, + ) -> WpFractionalScaleV1 { + let data = FractionalScaling { + surface: surface.clone(), + }; + self.manager + .get_fractional_scale(surface, queue_handle, data) + } +} + +impl Dispatch + for FractionalScalingManager +{ + fn event( + _: &mut SctkState, + _: &WpFractionalScaleManagerV1, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + // No events. + } +} + +impl Dispatch + for FractionalScalingManager +{ + fn event( + state: &mut SctkState, + _: &WpFractionalScaleV1, + event: ::Event, + data: &FractionalScaling, + _: &Connection, + _: &QueueHandle, + ) { + if let FractionalScalingEvent::PreferredScale { scale } = event { + state.scale_factor_changed( + &data.surface, + scale as f64 / SCALE_DENOMINATOR, + false, + ); + } + } +} + +delegate_dispatch!(SctkState: [WpFractionalScaleManagerV1: GlobalData] => FractionalScalingManager); +delegate_dispatch!(SctkState: [WpFractionalScaleV1: FractionalScaling] => FractionalScalingManager); diff --git a/winit/src/platform_specific/wayland/handlers/wp_viewporter.rs b/winit/src/platform_specific/wayland/handlers/wp_viewporter.rs new file mode 100644 index 0000000000..c0ca1025c8 --- /dev/null +++ b/winit/src/platform_specific/wayland/handlers/wp_viewporter.rs @@ -0,0 +1,71 @@ +//! Handling of the wp-viewporter. + + +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::Dispatch; +use sctk::reexports::client::{ + delegate_dispatch, Connection, Proxy, QueueHandle, +}; +use sctk::reexports::protocols::wp::viewporter::client::wp_viewport::WpViewport; +use sctk::reexports::protocols::wp::viewporter::client::wp_viewporter::WpViewporter; + +use sctk::globals::GlobalData; + +use crate::platform_specific::wayland::event_loop::state::SctkState; + +/// Viewporter. +#[derive(Debug)] +pub struct ViewporterState { + viewporter: WpViewporter, +} + +impl ViewporterState { + /// Create new viewporter. + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle, + ) -> Result { + let viewporter = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { viewporter }) + } + + /// Get the viewport for the given object. + pub fn get_viewport( + &self, + surface: &WlSurface, + queue_handle: &QueueHandle, + ) -> WpViewport { + self.viewporter + .get_viewport(surface, queue_handle, GlobalData) + } +} + +impl Dispatch for ViewporterState { + fn event( + _: &mut SctkState, + _: &WpViewporter, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + // No events. + } +} + +impl Dispatch for ViewporterState { + fn event( + _: &mut SctkState, + _: &WpViewport, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + // No events. + } +} + +delegate_dispatch!(SctkState: [WpViewporter: GlobalData] => ViewporterState); +delegate_dispatch!(SctkState: [WpViewport: GlobalData] => ViewporterState); diff --git a/winit/src/platform_specific/wayland/keymap.rs b/winit/src/platform_specific/wayland/keymap.rs new file mode 100644 index 0000000000..0d96cfcaff --- /dev/null +++ b/winit/src/platform_specific/wayland/keymap.rs @@ -0,0 +1,895 @@ +// Borrowed from winit +use winit::keyboard::{KeyCode, NativeKeyCode, PhysicalKey}; +/// Map the raw X11-style keycode to the `KeyCode` enum. +/// +/// X11-style keycodes are offset by 8 from the keycodes the Linux kernel uses. +pub fn raw_keycode_to_physicalkey(keycode: u32) -> PhysicalKey { + scancode_to_physicalkey(keycode.saturating_sub(8)) +} + +/// Map the linux scancode to Keycode. +/// +/// Both X11 and Wayland use keys with `+ 8` offset to linux scancode. +pub fn scancode_to_physicalkey(scancode: u32) -> PhysicalKey { + // The keycode values are taken from linux/include/uapi/linux/input-event-codes.h, as + // libxkbcommon's documentation seems to suggest that the keycode values we're interested in + // are defined by the Linux kernel. If Winit programs end up being run on other Unix-likes, + // I can only hope they agree on what the keycodes mean. + // + // Some of the keycodes are likely superfluous for our purposes, and some are ones which are + // difficult to test the correctness of, or discover the purpose of. Because of this, they've + // either been commented out here, or not included at all. + PhysicalKey::Code(match scancode { + 0 => return PhysicalKey::Unidentified(NativeKeyCode::Xkb(0)), + 1 => KeyCode::Escape, + 2 => KeyCode::Digit1, + 3 => KeyCode::Digit2, + 4 => KeyCode::Digit3, + 5 => KeyCode::Digit4, + 6 => KeyCode::Digit5, + 7 => KeyCode::Digit6, + 8 => KeyCode::Digit7, + 9 => KeyCode::Digit8, + 10 => KeyCode::Digit9, + 11 => KeyCode::Digit0, + 12 => KeyCode::Minus, + 13 => KeyCode::Equal, + 14 => KeyCode::Backspace, + 15 => KeyCode::Tab, + 16 => KeyCode::KeyQ, + 17 => KeyCode::KeyW, + 18 => KeyCode::KeyE, + 19 => KeyCode::KeyR, + 20 => KeyCode::KeyT, + 21 => KeyCode::KeyY, + 22 => KeyCode::KeyU, + 23 => KeyCode::KeyI, + 24 => KeyCode::KeyO, + 25 => KeyCode::KeyP, + 26 => KeyCode::BracketLeft, + 27 => KeyCode::BracketRight, + 28 => KeyCode::Enter, + 29 => KeyCode::ControlLeft, + 30 => KeyCode::KeyA, + 31 => KeyCode::KeyS, + 32 => KeyCode::KeyD, + 33 => KeyCode::KeyF, + 34 => KeyCode::KeyG, + 35 => KeyCode::KeyH, + 36 => KeyCode::KeyJ, + 37 => KeyCode::KeyK, + 38 => KeyCode::KeyL, + 39 => KeyCode::Semicolon, + 40 => KeyCode::Quote, + 41 => KeyCode::Backquote, + 42 => KeyCode::ShiftLeft, + 43 => KeyCode::Backslash, + 44 => KeyCode::KeyZ, + 45 => KeyCode::KeyX, + 46 => KeyCode::KeyC, + 47 => KeyCode::KeyV, + 48 => KeyCode::KeyB, + 49 => KeyCode::KeyN, + 50 => KeyCode::KeyM, + 51 => KeyCode::Comma, + 52 => KeyCode::Period, + 53 => KeyCode::Slash, + 54 => KeyCode::ShiftRight, + 55 => KeyCode::NumpadMultiply, + 56 => KeyCode::AltLeft, + 57 => KeyCode::Space, + 58 => KeyCode::CapsLock, + 59 => KeyCode::F1, + 60 => KeyCode::F2, + 61 => KeyCode::F3, + 62 => KeyCode::F4, + 63 => KeyCode::F5, + 64 => KeyCode::F6, + 65 => KeyCode::F7, + 66 => KeyCode::F8, + 67 => KeyCode::F9, + 68 => KeyCode::F10, + 69 => KeyCode::NumLock, + 70 => KeyCode::ScrollLock, + 71 => KeyCode::Numpad7, + 72 => KeyCode::Numpad8, + 73 => KeyCode::Numpad9, + 74 => KeyCode::NumpadSubtract, + 75 => KeyCode::Numpad4, + 76 => KeyCode::Numpad5, + 77 => KeyCode::Numpad6, + 78 => KeyCode::NumpadAdd, + 79 => KeyCode::Numpad1, + 80 => KeyCode::Numpad2, + 81 => KeyCode::Numpad3, + 82 => KeyCode::Numpad0, + 83 => KeyCode::NumpadDecimal, + 85 => KeyCode::Lang5, + 86 => KeyCode::IntlBackslash, + 87 => KeyCode::F11, + 88 => KeyCode::F12, + 89 => KeyCode::IntlRo, + 90 => KeyCode::Lang3, + 91 => KeyCode::Lang4, + 92 => KeyCode::Convert, + 93 => KeyCode::KanaMode, + 94 => KeyCode::NonConvert, + // 95 => KeyCode::KPJPCOMMA, + 96 => KeyCode::NumpadEnter, + 97 => KeyCode::ControlRight, + 98 => KeyCode::NumpadDivide, + 99 => KeyCode::PrintScreen, + 100 => KeyCode::AltRight, + // 101 => KeyCode::LINEFEED, + 102 => KeyCode::Home, + 103 => KeyCode::ArrowUp, + 104 => KeyCode::PageUp, + 105 => KeyCode::ArrowLeft, + 106 => KeyCode::ArrowRight, + 107 => KeyCode::End, + 108 => KeyCode::ArrowDown, + 109 => KeyCode::PageDown, + 110 => KeyCode::Insert, + 111 => KeyCode::Delete, + // 112 => KeyCode::MACRO, + 113 => KeyCode::AudioVolumeMute, + 114 => KeyCode::AudioVolumeDown, + 115 => KeyCode::AudioVolumeUp, + // 116 => KeyCode::POWER, + 117 => KeyCode::NumpadEqual, + // 118 => KeyCode::KPPLUSMINUS, + 119 => KeyCode::Pause, + // 120 => KeyCode::SCALE, + 121 => KeyCode::NumpadComma, + 122 => KeyCode::Lang1, + 123 => KeyCode::Lang2, + 124 => KeyCode::IntlYen, + 125 => KeyCode::SuperLeft, + 126 => KeyCode::SuperRight, + 127 => KeyCode::ContextMenu, + // 128 => KeyCode::STOP, + // 129 => KeyCode::AGAIN, + // 130 => KeyCode::PROPS, + // 131 => KeyCode::UNDO, + // 132 => KeyCode::FRONT, + // 133 => KeyCode::COPY, + // 134 => KeyCode::OPEN, + // 135 => KeyCode::PASTE, + // 136 => KeyCode::FIND, + // 137 => KeyCode::CUT, + // 138 => KeyCode::HELP, + // 139 => KeyCode::MENU, + // 140 => KeyCode::CALC, + // 141 => KeyCode::SETUP, + // 142 => KeyCode::SLEEP, + // 143 => KeyCode::WAKEUP, + // 144 => KeyCode::FILE, + // 145 => KeyCode::SENDFILE, + // 146 => KeyCode::DELETEFILE, + // 147 => KeyCode::XFER, + // 148 => KeyCode::PROG1, + // 149 => KeyCode::PROG2, + // 150 => KeyCode::WWW, + // 151 => KeyCode::MSDOS, + // 152 => KeyCode::COFFEE, + // 153 => KeyCode::ROTATE_DISPLAY, + // 154 => KeyCode::CYCLEWINDOWS, + // 155 => KeyCode::MAIL, + // 156 => KeyCode::BOOKMARKS, + // 157 => KeyCode::COMPUTER, + // 158 => KeyCode::BACK, + // 159 => KeyCode::FORWARD, + // 160 => KeyCode::CLOSECD, + // 161 => KeyCode::EJECTCD, + // 162 => KeyCode::EJECTCLOSECD, + 163 => KeyCode::MediaTrackNext, + 164 => KeyCode::MediaPlayPause, + 165 => KeyCode::MediaTrackPrevious, + 166 => KeyCode::MediaStop, + // 167 => KeyCode::RECORD, + // 168 => KeyCode::REWIND, + // 169 => KeyCode::PHONE, + // 170 => KeyCode::ISO, + // 171 => KeyCode::CONFIG, + // 172 => KeyCode::HOMEPAGE, + // 173 => KeyCode::REFRESH, + // 174 => KeyCode::EXIT, + // 175 => KeyCode::MOVE, + // 176 => KeyCode::EDIT, + // 177 => KeyCode::SCROLLUP, + // 178 => KeyCode::SCROLLDOWN, + // 179 => KeyCode::KPLEFTPAREN, + // 180 => KeyCode::KPRIGHTPAREN, + // 181 => KeyCode::NEW, + // 182 => KeyCode::REDO, + 183 => KeyCode::F13, + 184 => KeyCode::F14, + 185 => KeyCode::F15, + 186 => KeyCode::F16, + 187 => KeyCode::F17, + 188 => KeyCode::F18, + 189 => KeyCode::F19, + 190 => KeyCode::F20, + 191 => KeyCode::F21, + 192 => KeyCode::F22, + 193 => KeyCode::F23, + 194 => KeyCode::F24, + // 200 => KeyCode::PLAYCD, + // 201 => KeyCode::PAUSECD, + // 202 => KeyCode::PROG3, + // 203 => KeyCode::PROG4, + // 204 => KeyCode::DASHBOARD, + // 205 => KeyCode::SUSPEND, + // 206 => KeyCode::CLOSE, + // 207 => KeyCode::PLAY, + // 208 => KeyCode::FASTFORWARD, + // 209 => KeyCode::BASSBOOST, + // 210 => KeyCode::PRINT, + // 211 => KeyCode::HP, + // 212 => KeyCode::CAMERA, + // 213 => KeyCode::SOUND, + // 214 => KeyCode::QUESTION, + // 215 => KeyCode::EMAIL, + // 216 => KeyCode::CHAT, + // 217 => KeyCode::SEARCH, + // 218 => KeyCode::CONNECT, + // 219 => KeyCode::FINANCE, + // 220 => KeyCode::SPORT, + // 221 => KeyCode::SHOP, + // 222 => KeyCode::ALTERASE, + // 223 => KeyCode::CANCEL, + // 224 => KeyCode::BRIGHTNESSDOW, + // 225 => KeyCode::BRIGHTNESSU, + // 226 => KeyCode::MEDIA, + // 227 => KeyCode::SWITCHVIDEOMODE, + // 228 => KeyCode::KBDILLUMTOGGLE, + // 229 => KeyCode::KBDILLUMDOWN, + // 230 => KeyCode::KBDILLUMUP, + // 231 => KeyCode::SEND, + // 232 => KeyCode::REPLY, + // 233 => KeyCode::FORWARDMAIL, + // 234 => KeyCode::SAVE, + // 235 => KeyCode::DOCUMENTS, + // 236 => KeyCode::BATTERY, + // 237 => KeyCode::BLUETOOTH, + // 238 => KeyCode::WLAN, + // 239 => KeyCode::UWB, + 240 => return PhysicalKey::Unidentified(NativeKeyCode::Unidentified), + // 241 => KeyCode::VIDEO_NEXT, + // 242 => KeyCode::VIDEO_PREV, + // 243 => KeyCode::BRIGHTNESS_CYCLE, + // 244 => KeyCode::BRIGHTNESS_AUTO, + // 245 => KeyCode::DISPLAY_OFF, + // 246 => KeyCode::WWAN, + // 247 => KeyCode::RFKILL, + // 248 => KeyCode::KEY_MICMUTE, + _ => return PhysicalKey::Unidentified(NativeKeyCode::Xkb(scancode)), + }) +} + +pub fn physicalkey_to_scancode(key: PhysicalKey) -> Option { + let code = match key { + PhysicalKey::Code(code) => code, + PhysicalKey::Unidentified(code) => { + return match code { + NativeKeyCode::Unidentified => Some(240), + NativeKeyCode::Xkb(raw) => Some(raw), + _ => None, + }; + } + }; + + match code { + KeyCode::Escape => Some(1), + KeyCode::Digit1 => Some(2), + KeyCode::Digit2 => Some(3), + KeyCode::Digit3 => Some(4), + KeyCode::Digit4 => Some(5), + KeyCode::Digit5 => Some(6), + KeyCode::Digit6 => Some(7), + KeyCode::Digit7 => Some(8), + KeyCode::Digit8 => Some(9), + KeyCode::Digit9 => Some(10), + KeyCode::Digit0 => Some(11), + KeyCode::Minus => Some(12), + KeyCode::Equal => Some(13), + KeyCode::Backspace => Some(14), + KeyCode::Tab => Some(15), + KeyCode::KeyQ => Some(16), + KeyCode::KeyW => Some(17), + KeyCode::KeyE => Some(18), + KeyCode::KeyR => Some(19), + KeyCode::KeyT => Some(20), + KeyCode::KeyY => Some(21), + KeyCode::KeyU => Some(22), + KeyCode::KeyI => Some(23), + KeyCode::KeyO => Some(24), + KeyCode::KeyP => Some(25), + KeyCode::BracketLeft => Some(26), + KeyCode::BracketRight => Some(27), + KeyCode::Enter => Some(28), + KeyCode::ControlLeft => Some(29), + KeyCode::KeyA => Some(30), + KeyCode::KeyS => Some(31), + KeyCode::KeyD => Some(32), + KeyCode::KeyF => Some(33), + KeyCode::KeyG => Some(34), + KeyCode::KeyH => Some(35), + KeyCode::KeyJ => Some(36), + KeyCode::KeyK => Some(37), + KeyCode::KeyL => Some(38), + KeyCode::Semicolon => Some(39), + KeyCode::Quote => Some(40), + KeyCode::Backquote => Some(41), + KeyCode::ShiftLeft => Some(42), + KeyCode::Backslash => Some(43), + KeyCode::KeyZ => Some(44), + KeyCode::KeyX => Some(45), + KeyCode::KeyC => Some(46), + KeyCode::KeyV => Some(47), + KeyCode::KeyB => Some(48), + KeyCode::KeyN => Some(49), + KeyCode::KeyM => Some(50), + KeyCode::Comma => Some(51), + KeyCode::Period => Some(52), + KeyCode::Slash => Some(53), + KeyCode::ShiftRight => Some(54), + KeyCode::NumpadMultiply => Some(55), + KeyCode::AltLeft => Some(56), + KeyCode::Space => Some(57), + KeyCode::CapsLock => Some(58), + KeyCode::F1 => Some(59), + KeyCode::F2 => Some(60), + KeyCode::F3 => Some(61), + KeyCode::F4 => Some(62), + KeyCode::F5 => Some(63), + KeyCode::F6 => Some(64), + KeyCode::F7 => Some(65), + KeyCode::F8 => Some(66), + KeyCode::F9 => Some(67), + KeyCode::F10 => Some(68), + KeyCode::NumLock => Some(69), + KeyCode::ScrollLock => Some(70), + KeyCode::Numpad7 => Some(71), + KeyCode::Numpad8 => Some(72), + KeyCode::Numpad9 => Some(73), + KeyCode::NumpadSubtract => Some(74), + KeyCode::Numpad4 => Some(75), + KeyCode::Numpad5 => Some(76), + KeyCode::Numpad6 => Some(77), + KeyCode::NumpadAdd => Some(78), + KeyCode::Numpad1 => Some(79), + KeyCode::Numpad2 => Some(80), + KeyCode::Numpad3 => Some(81), + KeyCode::Numpad0 => Some(82), + KeyCode::NumpadDecimal => Some(83), + KeyCode::Lang5 => Some(85), + KeyCode::IntlBackslash => Some(86), + KeyCode::F11 => Some(87), + KeyCode::F12 => Some(88), + KeyCode::IntlRo => Some(89), + KeyCode::Lang3 => Some(90), + KeyCode::Lang4 => Some(91), + KeyCode::Convert => Some(92), + KeyCode::KanaMode => Some(93), + KeyCode::NonConvert => Some(94), + KeyCode::NumpadEnter => Some(96), + KeyCode::ControlRight => Some(97), + KeyCode::NumpadDivide => Some(98), + KeyCode::PrintScreen => Some(99), + KeyCode::AltRight => Some(100), + KeyCode::Home => Some(102), + KeyCode::ArrowUp => Some(103), + KeyCode::PageUp => Some(104), + KeyCode::ArrowLeft => Some(105), + KeyCode::ArrowRight => Some(106), + KeyCode::End => Some(107), + KeyCode::ArrowDown => Some(108), + KeyCode::PageDown => Some(109), + KeyCode::Insert => Some(110), + KeyCode::Delete => Some(111), + KeyCode::AudioVolumeMute => Some(113), + KeyCode::AudioVolumeDown => Some(114), + KeyCode::AudioVolumeUp => Some(115), + KeyCode::NumpadEqual => Some(117), + KeyCode::Pause => Some(119), + KeyCode::NumpadComma => Some(121), + KeyCode::Lang1 => Some(122), + KeyCode::Lang2 => Some(123), + KeyCode::IntlYen => Some(124), + KeyCode::SuperLeft => Some(125), + KeyCode::SuperRight => Some(126), + KeyCode::ContextMenu => Some(127), + KeyCode::MediaTrackNext => Some(163), + KeyCode::MediaPlayPause => Some(164), + KeyCode::MediaTrackPrevious => Some(165), + KeyCode::MediaStop => Some(166), + KeyCode::F13 => Some(183), + KeyCode::F14 => Some(184), + KeyCode::F15 => Some(185), + KeyCode::F16 => Some(186), + KeyCode::F17 => Some(187), + KeyCode::F18 => Some(188), + KeyCode::F19 => Some(189), + KeyCode::F20 => Some(190), + KeyCode::F21 => Some(191), + KeyCode::F22 => Some(192), + KeyCode::F23 => Some(193), + KeyCode::F24 => Some(194), + _ => None, + } +} + +pub fn keysym_to_key(keysym: u32) -> Key { + use xkbcommon_dl::keysyms; + Key::Named(match keysym { + // TTY function keys + keysyms::BackSpace => Named::Backspace, + keysyms::Tab => Named::Tab, + // keysyms::Linefeed => Named::Linefeed, + keysyms::Clear => Named::Clear, + keysyms::Return => Named::Enter, + keysyms::Pause => Named::Pause, + keysyms::Scroll_Lock => Named::ScrollLock, + keysyms::Sys_Req => Named::PrintScreen, + keysyms::Escape => Named::Escape, + keysyms::Delete => Named::Delete, + + // IME keys + keysyms::Multi_key => Named::Compose, + keysyms::Codeinput => Named::CodeInput, + keysyms::SingleCandidate => Named::SingleCandidate, + keysyms::MultipleCandidate => Named::AllCandidates, + keysyms::PreviousCandidate => Named::PreviousCandidate, + + // Japanese key + keysyms::Kanji => Named::KanjiMode, + keysyms::Muhenkan => Named::NonConvert, + keysyms::Henkan_Mode => Named::Convert, + keysyms::Romaji => Named::Romaji, + keysyms::Hiragana => Named::Hiragana, + keysyms::Hiragana_Katakana => Named::HiraganaKatakana, + keysyms::Zenkaku => Named::Zenkaku, + keysyms::Hankaku => Named::Hankaku, + keysyms::Zenkaku_Hankaku => Named::ZenkakuHankaku, + // keysyms::Touroku => Named::Touroku, + // keysyms::Massyo => Named::Massyo, + keysyms::Kana_Lock => Named::KanaMode, + keysyms::Kana_Shift => Named::KanaMode, + keysyms::Eisu_Shift => Named::Alphanumeric, + keysyms::Eisu_toggle => Named::Alphanumeric, + // NOTE: The next three items are aliases for values we've already mapped. + // keysyms::Kanji_Bangou => Named::CodeInput, + // keysyms::Zen_Koho => Named::AllCandidates, + // keysyms::Mae_Koho => Named::PreviousCandidate, + + // Cursor control & motion + keysyms::Home => Named::Home, + keysyms::Left => Named::ArrowLeft, + keysyms::Up => Named::ArrowUp, + keysyms::Right => Named::ArrowRight, + keysyms::Down => Named::ArrowDown, + // keysyms::Prior => Named::PageUp, + keysyms::Page_Up => Named::PageUp, + // keysyms::Next => Named::PageDown, + keysyms::Page_Down => Named::PageDown, + keysyms::End => Named::End, + // keysyms::Begin => Named::Begin, + + // Misc. functions + keysyms::Select => Named::Select, + keysyms::Print => Named::PrintScreen, + keysyms::Execute => Named::Execute, + keysyms::Insert => Named::Insert, + keysyms::Undo => Named::Undo, + keysyms::Redo => Named::Redo, + keysyms::Menu => Named::ContextMenu, + keysyms::Find => Named::Find, + keysyms::Cancel => Named::Cancel, + keysyms::Help => Named::Help, + keysyms::Break => Named::Pause, + keysyms::Mode_switch => Named::ModeChange, + // keysyms::script_switch => Named::ModeChange, + keysyms::Num_Lock => Named::NumLock, + + // Keypad keys + // keysyms::KP_Space => return Key::Character(" "), + keysyms::KP_Tab => Named::Tab, + keysyms::KP_Enter => Named::Enter, + keysyms::KP_F1 => Named::F1, + keysyms::KP_F2 => Named::F2, + keysyms::KP_F3 => Named::F3, + keysyms::KP_F4 => Named::F4, + keysyms::KP_Home => Named::Home, + keysyms::KP_Left => Named::ArrowLeft, + keysyms::KP_Up => Named::ArrowUp, + keysyms::KP_Right => Named::ArrowRight, + keysyms::KP_Down => Named::ArrowDown, + // keysyms::KP_Prior => Named::PageUp, + keysyms::KP_Page_Up => Named::PageUp, + // keysyms::KP_Next => Named::PageDown, + keysyms::KP_Page_Down => Named::PageDown, + keysyms::KP_End => Named::End, + // This is the key labeled "5" on the numpad when NumLock is off. + // keysyms::KP_Begin => Named::Begin, + keysyms::KP_Insert => Named::Insert, + keysyms::KP_Delete => Named::Delete, + // keysyms::KP_Equal => Named::Equal, + // keysyms::KP_Multiply => Named::Multiply, + // keysyms::KP_Add => Named::Add, + // keysyms::KP_Separator => Named::Separator, + // keysyms::KP_Subtract => Named::Subtract, + // keysyms::KP_Decimal => Named::Decimal, + // keysyms::KP_Divide => Named::Divide, + + // keysyms::KP_0 => return Key::Character("0"), + // keysyms::KP_1 => return Key::Character("1"), + // keysyms::KP_2 => return Key::Character("2"), + // keysyms::KP_3 => return Key::Character("3"), + // keysyms::KP_4 => return Key::Character("4"), + // keysyms::KP_5 => return Key::Character("5"), + // keysyms::KP_6 => return Key::Character("6"), + // keysyms::KP_7 => return Key::Character("7"), + // keysyms::KP_8 => return Key::Character("8"), + // keysyms::KP_9 => return Key::Character("9"), + + // Function keys + keysyms::F1 => Named::F1, + keysyms::F2 => Named::F2, + keysyms::F3 => Named::F3, + keysyms::F4 => Named::F4, + keysyms::F5 => Named::F5, + keysyms::F6 => Named::F6, + keysyms::F7 => Named::F7, + keysyms::F8 => Named::F8, + keysyms::F9 => Named::F9, + keysyms::F10 => Named::F10, + keysyms::F11 => Named::F11, + keysyms::F12 => Named::F12, + keysyms::F13 => Named::F13, + keysyms::F14 => Named::F14, + keysyms::F15 => Named::F15, + keysyms::F16 => Named::F16, + keysyms::F17 => Named::F17, + keysyms::F18 => Named::F18, + keysyms::F19 => Named::F19, + keysyms::F20 => Named::F20, + keysyms::F21 => Named::F21, + keysyms::F22 => Named::F22, + keysyms::F23 => Named::F23, + keysyms::F24 => Named::F24, + keysyms::F25 => Named::F25, + keysyms::F26 => Named::F26, + keysyms::F27 => Named::F27, + keysyms::F28 => Named::F28, + keysyms::F29 => Named::F29, + keysyms::F30 => Named::F30, + keysyms::F31 => Named::F31, + keysyms::F32 => Named::F32, + keysyms::F33 => Named::F33, + keysyms::F34 => Named::F34, + keysyms::F35 => Named::F35, + + // Modifiers + keysyms::Shift_L => Named::Shift, + keysyms::Shift_R => Named::Shift, + keysyms::Control_L => Named::Control, + keysyms::Control_R => Named::Control, + keysyms::Caps_Lock => Named::CapsLock, + // keysyms::Shift_Lock => Named::ShiftLock, + + // keysyms::Meta_L => Named::Meta, + // keysyms::Meta_R => Named::Meta, + keysyms::Alt_L => Named::Alt, + keysyms::Alt_R => Named::Alt, + keysyms::Super_L => Named::Super, + keysyms::Super_R => Named::Super, + keysyms::Hyper_L => Named::Hyper, + keysyms::Hyper_R => Named::Hyper, + + // XKB function and modifier keys + // keysyms::ISO_Lock => Named::IsoLock, + // keysyms::ISO_Level2_Latch => Named::IsoLevel2Latch, + keysyms::ISO_Level3_Shift => Named::AltGraph, + keysyms::ISO_Level3_Latch => Named::AltGraph, + keysyms::ISO_Level3_Lock => Named::AltGraph, + // keysyms::ISO_Level5_Shift => Named::IsoLevel5Shift, + // keysyms::ISO_Level5_Latch => Named::IsoLevel5Latch, + // keysyms::ISO_Level5_Lock => Named::IsoLevel5Lock, + // keysyms::ISO_Group_Shift => Named::IsoGroupShift, + // keysyms::ISO_Group_Latch => Named::IsoGroupLatch, + // keysyms::ISO_Group_Lock => Named::IsoGroupLock, + keysyms::ISO_Next_Group => Named::GroupNext, + // keysyms::ISO_Next_Group_Lock => Named::GroupNextLock, + keysyms::ISO_Prev_Group => Named::GroupPrevious, + // keysyms::ISO_Prev_Group_Lock => Named::GroupPreviousLock, + keysyms::ISO_First_Group => Named::GroupFirst, + // keysyms::ISO_First_Group_Lock => Named::GroupFirstLock, + keysyms::ISO_Last_Group => Named::GroupLast, + // keysyms::ISO_Last_Group_Lock => Named::GroupLastLock, + // + keysyms::ISO_Left_Tab => Named::Tab, + // keysyms::ISO_Move_Line_Up => Named::IsoMoveLineUp, + // keysyms::ISO_Move_Line_Down => Named::IsoMoveLineDown, + // keysyms::ISO_Partial_Line_Up => Named::IsoPartialLineUp, + // keysyms::ISO_Partial_Line_Down => Named::IsoPartialLineDown, + // keysyms::ISO_Partial_Space_Left => Named::IsoPartialSpaceLeft, + // keysyms::ISO_Partial_Space_Right => Named::IsoPartialSpaceRight, + // keysyms::ISO_Set_Margin_Left => Named::IsoSetMarginLeft, + // keysyms::ISO_Set_Margin_Right => Named::IsoSetMarginRight, + // keysyms::ISO_Release_Margin_Left => Named::IsoReleaseMarginLeft, + // keysyms::ISO_Release_Margin_Right => Named::IsoReleaseMarginRight, + // keysyms::ISO_Release_Both_Margins => Named::IsoReleaseBothMargins, + // keysyms::ISO_Fast_Cursor_Left => Named::IsoFastCursorLeft, + // keysyms::ISO_Fast_Cursor_Right => Named::IsoFastCursorRight, + // keysyms::ISO_Fast_Cursor_Up => Named::IsoFastCursorUp, + // keysyms::ISO_Fast_Cursor_Down => Named::IsoFastCursorDown, + // keysyms::ISO_Continuous_Underline => Named::IsoContinuousUnderline, + // keysyms::ISO_Discontinuous_Underline => Named::IsoDiscontinuousUnderline, + // keysyms::ISO_Emphasize => Named::IsoEmphasize, + // keysyms::ISO_Center_Object => Named::IsoCenterObject, + keysyms::ISO_Enter => Named::Enter, + + // dead_grave..dead_currency + + // dead_lowline..dead_longsolidusoverlay + + // dead_a..dead_capital_schwa + + // dead_greek + + // First_Virtual_Screen..Terminate_Server + + // AccessX_Enable..AudibleBell_Enable + + // Pointer_Left..Pointer_Drag5 + + // Pointer_EnableKeys..Pointer_DfltBtnPrev + + // ch..C_H + + // 3270 terminal keys + // keysyms::3270_Duplicate => Named::Duplicate, + // keysyms::3270_FieldMark => Named::FieldMark, + // keysyms::3270_Right2 => Named::Right2, + // keysyms::3270_Left2 => Named::Left2, + // keysyms::3270_BackTab => Named::BackTab, + keysyms::_3270_EraseEOF => Named::EraseEof, + // keysyms::3270_EraseInput => Named::EraseInput, + // keysyms::3270_Reset => Named::Reset, + // keysyms::3270_Quit => Named::Quit, + // keysyms::3270_PA1 => Named::Pa1, + // keysyms::3270_PA2 => Named::Pa2, + // keysyms::3270_PA3 => Named::Pa3, + // keysyms::3270_Test => Named::Test, + keysyms::_3270_Attn => Named::Attn, + // keysyms::3270_CursorBlink => Named::CursorBlink, + // keysyms::3270_AltCursor => Named::AltCursor, + // keysyms::3270_KeyClick => Named::KeyClick, + // keysyms::3270_Jump => Named::Jump, + // keysyms::3270_Ident => Named::Ident, + // keysyms::3270_Rule => Named::Rule, + // keysyms::3270_Copy => Named::Copy, + keysyms::_3270_Play => Named::Play, + // keysyms::3270_Setup => Named::Setup, + // keysyms::3270_Record => Named::Record, + // keysyms::3270_ChangeScreen => Named::ChangeScreen, + // keysyms::3270_DeleteWord => Named::DeleteWord, + keysyms::_3270_ExSelect => Named::ExSel, + keysyms::_3270_CursorSelect => Named::CrSel, + keysyms::_3270_PrintScreen => Named::PrintScreen, + keysyms::_3270_Enter => Named::Enter, + + keysyms::space => Named::Space, + // exclam..Sinh_kunddaliya + + // XFree86 + // keysyms::XF86_ModeLock => Named::ModeLock, + + // XFree86 - Backlight controls + keysyms::XF86_MonBrightnessUp => Named::BrightnessUp, + keysyms::XF86_MonBrightnessDown => Named::BrightnessDown, + // keysyms::XF86_KbdLightOnOff => Named::LightOnOff, + // keysyms::XF86_KbdBrightnessUp => Named::KeyboardBrightnessUp, + // keysyms::XF86_KbdBrightnessDown => Named::KeyboardBrightnessDown, + + // XFree86 - "Internet" + keysyms::XF86_Standby => Named::Standby, + keysyms::XF86_AudioLowerVolume => Named::AudioVolumeDown, + keysyms::XF86_AudioRaiseVolume => Named::AudioVolumeUp, + keysyms::XF86_AudioPlay => Named::MediaPlay, + keysyms::XF86_AudioStop => Named::MediaStop, + keysyms::XF86_AudioPrev => Named::MediaTrackPrevious, + keysyms::XF86_AudioNext => Named::MediaTrackNext, + keysyms::XF86_HomePage => Named::BrowserHome, + keysyms::XF86_Mail => Named::LaunchMail, + // keysyms::XF86_Start => Named::Start, + keysyms::XF86_Search => Named::BrowserSearch, + keysyms::XF86_AudioRecord => Named::MediaRecord, + + // XFree86 - PDA + keysyms::XF86_Calculator => Named::LaunchApplication2, + // keysyms::XF86_Memo => Named::Memo, + // keysyms::XF86_ToDoList => Named::ToDoList, + keysyms::XF86_Calendar => Named::LaunchCalendar, + keysyms::XF86_PowerDown => Named::Power, + // keysyms::XF86_ContrastAdjust => Named::AdjustContrast, + // keysyms::XF86_RockerUp => Named::RockerUp, + // keysyms::XF86_RockerDown => Named::RockerDown, + // keysyms::XF86_RockerEnter => Named::RockerEnter, + + // XFree86 - More "Internet" + keysyms::XF86_Back => Named::BrowserBack, + keysyms::XF86_Forward => Named::BrowserForward, + // keysyms::XF86_Stop => Named::Stop, + keysyms::XF86_Refresh => Named::BrowserRefresh, + keysyms::XF86_PowerOff => Named::Power, + keysyms::XF86_WakeUp => Named::WakeUp, + keysyms::XF86_Eject => Named::Eject, + keysyms::XF86_ScreenSaver => Named::LaunchScreenSaver, + keysyms::XF86_WWW => Named::LaunchWebBrowser, + keysyms::XF86_Sleep => Named::Standby, + keysyms::XF86_Favorites => Named::BrowserFavorites, + keysyms::XF86_AudioPause => Named::MediaPause, + // keysyms::XF86_AudioMedia => Named::AudioMedia, + keysyms::XF86_MyComputer => Named::LaunchApplication1, + // keysyms::XF86_VendorHome => Named::VendorHome, + // keysyms::XF86_LightBulb => Named::LightBulb, + // keysyms::XF86_Shop => Named::BrowserShop, + // keysyms::XF86_History => Named::BrowserHistory, + // keysyms::XF86_OpenURL => Named::OpenUrl, + // keysyms::XF86_AddFavorite => Named::AddFavorite, + // keysyms::XF86_HotLinks => Named::HotLinks, + // keysyms::XF86_BrightnessAdjust => Named::BrightnessAdjust, + // keysyms::XF86_Finance => Named::BrowserFinance, + // keysyms::XF86_Community => Named::BrowserCommunity, + keysyms::XF86_AudioRewind => Named::MediaRewind, + // keysyms::XF86_BackForward => Key::???, + // XF86_Launch0..XF86_LaunchF + + // XF86_ApplicationLeft..XF86_CD + keysyms::XF86_Calculater => Named::LaunchApplication2, // Nice typo, libxkbcommon :) + // XF86_Clear + keysyms::XF86_Close => Named::Close, + keysyms::XF86_Copy => Named::Copy, + keysyms::XF86_Cut => Named::Cut, + // XF86_Display..XF86_Documents + keysyms::XF86_Excel => Named::LaunchSpreadsheet, + // XF86_Explorer..XF86iTouch + keysyms::XF86_LogOff => Named::LogOff, + // XF86_Market..XF86_MenuPB + keysyms::XF86_MySites => Named::BrowserFavorites, + keysyms::XF86_New => Named::New, + // XF86_News..XF86_OfficeHome + keysyms::XF86_Open => Named::Open, + // XF86_Option + keysyms::XF86_Paste => Named::Paste, + keysyms::XF86_Phone => Named::LaunchPhone, + // XF86_Q + keysyms::XF86_Reply => Named::MailReply, + keysyms::XF86_Reload => Named::BrowserRefresh, + // XF86_RotateWindows..XF86_RotationKB + keysyms::XF86_Save => Named::Save, + // XF86_ScrollUp..XF86_ScrollClick + keysyms::XF86_Send => Named::MailSend, + keysyms::XF86_Spell => Named::SpellCheck, + keysyms::XF86_SplitScreen => Named::SplitScreenToggle, + // XF86_Support..XF86_User2KB + keysyms::XF86_Video => Named::LaunchMediaPlayer, + // XF86_WheelButton + keysyms::XF86_Word => Named::LaunchWordProcessor, + // XF86_Xfer + keysyms::XF86_ZoomIn => Named::ZoomIn, + keysyms::XF86_ZoomOut => Named::ZoomOut, + + // XF86_Away..XF86_Messenger + keysyms::XF86_WebCam => Named::LaunchWebCam, + keysyms::XF86_MailForward => Named::MailForward, + // XF86_Pictures + keysyms::XF86_Music => Named::LaunchMusicPlayer, + + // XF86_Battery..XF86_UWB + // + keysyms::XF86_AudioForward => Named::MediaFastForward, + // XF86_AudioRepeat + keysyms::XF86_AudioRandomPlay => Named::RandomToggle, + keysyms::XF86_Subtitle => Named::Subtitle, + keysyms::XF86_AudioCycleTrack => Named::MediaAudioTrack, + // XF86_CycleAngle..XF86_Blue + // + keysyms::XF86_Suspend => Named::Standby, + keysyms::XF86_Hibernate => Named::Hibernate, + // XF86_TouchpadToggle..XF86_TouchpadOff + // + keysyms::XF86_AudioMute => Named::AudioVolumeMute, + + // XF86_Switch_VT_1..XF86_Switch_VT_12 + + // XF86_Ungrab..XF86_ClearGrab + keysyms::XF86_Next_VMode => Named::VideoModeNext, + // keysyms::XF86_Prev_VMode => Named::VideoModePrevious, + // XF86_LogWindowTree..XF86_LogGrabInfo + + // SunFA_Grave..SunFA_Cedilla + + // keysyms::SunF36 => Named::F36 | Named::F11, + // keysyms::SunF37 => Named::F37 | Named::F12, + + // keysyms::SunSys_Req => Named::PrintScreen, + // The next couple of xkb (until SunStop) are already handled. + // SunPrint_Screen..SunPageDown + + // SunUndo..SunFront + keysyms::SUN_Copy => Named::Copy, + keysyms::SUN_Open => Named::Open, + keysyms::SUN_Paste => Named::Paste, + keysyms::SUN_Cut => Named::Cut, + + // SunPowerSwitch + keysyms::SUN_AudioLowerVolume => Named::AudioVolumeDown, + keysyms::SUN_AudioMute => Named::AudioVolumeMute, + keysyms::SUN_AudioRaiseVolume => Named::AudioVolumeUp, + // SUN_VideoDegauss + keysyms::SUN_VideoLowerBrightness => Named::BrightnessDown, + keysyms::SUN_VideoRaiseBrightness => Named::BrightnessUp, + // SunPowerSwitchShift + // + _ => return Key::Unidentified, + }) +} + +use iced_runtime::keyboard::{key::Named, Key, Location}; + +pub fn keysym_location(keysym: u32) -> Location { + use xkbcommon_dl::keysyms; + match keysym { + xkeysym::key::Shift_L + | keysyms::Control_L + | keysyms::Meta_L + | keysyms::Alt_L + | keysyms::Super_L + | keysyms::Hyper_L => Location::Left, + keysyms::Shift_R + | keysyms::Control_R + | keysyms::Meta_R + | keysyms::Alt_R + | keysyms::Super_R + | keysyms::Hyper_R => Location::Right, + keysyms::KP_0 + | keysyms::KP_1 + | keysyms::KP_2 + | keysyms::KP_3 + | keysyms::KP_4 + | keysyms::KP_5 + | keysyms::KP_6 + | keysyms::KP_7 + | keysyms::KP_8 + | keysyms::KP_9 + | keysyms::KP_Space + | keysyms::KP_Tab + | keysyms::KP_Enter + | keysyms::KP_F1 + | keysyms::KP_F2 + | keysyms::KP_F3 + | keysyms::KP_F4 + | keysyms::KP_Home + | keysyms::KP_Left + | keysyms::KP_Up + | keysyms::KP_Right + | keysyms::KP_Down + | keysyms::KP_Page_Up + | keysyms::KP_Page_Down + | keysyms::KP_End + | keysyms::KP_Begin + | keysyms::KP_Insert + | keysyms::KP_Delete + | keysyms::KP_Equal + | keysyms::KP_Multiply + | keysyms::KP_Add + | keysyms::KP_Separator + | keysyms::KP_Subtract + | keysyms::KP_Decimal + | keysyms::KP_Divide => Location::Numpad, + _ => Location::Standard, + } +} diff --git a/winit/src/platform_specific/wayland/mod.rs b/winit/src/platform_specific/wayland/mod.rs new file mode 100644 index 0000000000..7fbd07b4d8 --- /dev/null +++ b/winit/src/platform_specific/wayland/mod.rs @@ -0,0 +1,211 @@ +pub mod commands; +pub mod conversion; +pub(crate) mod event_loop; +pub(crate) mod handlers; +pub mod keymap; +pub mod sctk_event; +pub mod subsurface_widget; +pub mod winit_window; + +use super::{PlatformSpecific, SurfaceIdWrapper}; +use crate::program::{Control, Program, WindowManager}; + +use iced_futures::futures::channel::mpsc; +use iced_graphics::Compositor; +use iced_runtime::core::window; +use iced_runtime::Debug; +use sctk::reexports::calloop; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::seat::keyboard::Modifiers; +use sctk_event::SctkEvent; +use sctk_event::UserInterfaces; +use std::{collections::HashMap, sync::Arc}; +use subsurface_widget::{SubsurfaceInstance, SubsurfaceState}; +use wayland_backend::client::ObjectId; +use winit::event_loop::OwnedDisplayHandle; +use winit::window::CursorIcon; + +pub(crate) enum Action { + Action(iced_runtime::platform_specific::wayland::Action), + SetCursor(CursorIcon), + RequestRedraw(ObjectId), + PrePresentNotify(ObjectId), + TrackWindow(Arc, window::Id), + RemoveWindow(window::Id), + Dropped(SurfaceIdWrapper), + Ready, +} + +impl std::fmt::Debug for Action { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Action(arg0) => f.debug_tuple("Action").field(arg0).finish(), + Self::SetCursor(arg0) => { + f.debug_tuple("SetCursor").field(arg0).finish() + } + Self::RequestRedraw(arg0) => { + f.debug_tuple("RequestRedraw").field(arg0).finish() + } + Self::PrePresentNotify(arg0) => { + f.debug_tuple("PrePresentNotify").field(arg0).finish() + } + Self::TrackWindow(_arg0, arg1) => { + f.debug_tuple("TrackWindow").field(arg1).finish() + } + Self::RemoveWindow(arg0) => { + f.debug_tuple("RemoveWindow").field(arg0).finish() + } + Self::Ready => write!(f, "Ready"), + Self::Dropped(_surface_id_wrapper) => write!(f, "Dropped"), + } + } +} + +#[derive(Debug, Default)] +pub(crate) struct WaylandSpecific { + winit_event_sender: Option>, + proxy: Option, + sender: Option>, + display_handle: Option, + modifiers: Modifiers, + surface_ids: HashMap, + destroyed_surface_ids: HashMap, + subsurface_ids: HashMap, + subsurface_state: Option, + surface_subsurfaces: HashMap>, +} + +impl PlatformSpecific { + pub(crate) fn with_wayland( + mut self, + tx: mpsc::UnboundedSender, + raw: winit::event_loop::EventLoopProxy, + display: OwnedDisplayHandle, + ) -> Self { + self.wayland.winit_event_sender = Some(tx); + self.wayland.display_handle = Some(display); + self.wayland.proxy = Some(raw); + // TODO remove this + self.wayland.sender = + crate::platform_specific::event_loop::SctkEventLoop::new( + self.wayland.winit_event_sender.clone().unwrap(), + self.wayland.proxy.clone().unwrap(), + self.wayland.display_handle.clone().unwrap(), + ) + .ok(); + self + } + + pub(crate) fn send_wayland(&mut self, action: Action) { + if self.wayland.sender.is_none() + && self.wayland.winit_event_sender.is_some() + && self.wayland.display_handle.is_some() + && self.wayland.proxy.is_some() + { + self.wayland.sender = + crate::platform_specific::event_loop::SctkEventLoop::new( + self.wayland.winit_event_sender.clone().unwrap(), + self.wayland.proxy.clone().unwrap(), + self.wayland.display_handle.clone().unwrap(), + ) + .ok(); + } + + if let Some(tx) = self.wayland.sender.as_ref() { + _ = tx.send(action); + } else { + log::error!("Failed to process wayland Action."); + } + } +} + +impl WaylandSpecific { + pub(crate) fn handle_event<'a, P, C>( + &mut self, + e: SctkEvent, + events: &mut Vec<(Option, iced_runtime::core::Event)>, + program: &'a P, + compositor: &mut C, + window_manager: &mut WindowManager, + debug: &mut Debug, + user_interfaces: &mut UserInterfaces<'a, P>, + clipboard: &mut crate::Clipboard, + #[cfg(feature = "a11y")] adapters: &mut HashMap< + window::Id, + (u64, iced_accessibility::accesskit_winit::Adapter), + >, + ) where + P: Program, + C: Compositor, + { + let Self { + winit_event_sender, + proxy, + sender, + display_handle, + surface_ids, + destroyed_surface_ids, + subsurface_ids, + modifiers, + subsurface_state, + surface_subsurfaces, + } = self; + + match e { + sctk_event => { + let Some(sender) = sender.as_ref() else { + log::error!("Missing calloop sender"); + return Default::default(); + }; + let Some(event_sender) = winit_event_sender.as_ref() else { + log::error!("Missing control sender"); + return Default::default(); + }; + let Some(proxy) = proxy.as_ref() else { + log::error!("Missing event loop proxy"); + return Default::default(); + }; + + sctk_event.process( + modifiers, + program, + compositor, + window_manager, + surface_ids, + subsurface_ids, + sender, + event_sender, + proxy, + debug, + user_interfaces, + events, + clipboard, + subsurface_state, + #[cfg(feature = "a11y")] + adapters, + ); + } + }; + } + + pub(crate) fn update_subsurfaces( + &mut self, + id: window::Id, + wl_surface: &WlSurface, + ) { + let subsurfaces = crate::subsurface_widget::take_subsurfaces(); + let mut entry = self.surface_subsurfaces.entry(id); + let surface_subsurfaces = entry.or_default(); + let Some(subsurface_state) = self.subsurface_state.as_mut() else { + return; + }; + + subsurface_state.update_subsurfaces( + id, + &mut self.subsurface_ids, + wl_surface, + surface_subsurfaces, + &subsurfaces, + ); + } +} diff --git a/winit/src/platform_specific/wayland/sctk_event.rs b/winit/src/platform_specific/wayland/sctk_event.rs new file mode 100755 index 0000000000..a7939f48c9 --- /dev/null +++ b/winit/src/platform_specific/wayland/sctk_event.rs @@ -0,0 +1,1210 @@ +use crate::{ + platform_specific::{ + wayland::{ + conversion::{ + modifiers_to_native, pointer_axis_to_native, + pointer_button_to_native, + }, + keymap::{self, keysym_to_key}, + subsurface_widget::SubsurfaceState, + }, + SurfaceIdWrapper, + }, + program::{Control, Program}, + Clipboard, +}; + +use dnd::DndSurface; +use iced_futures::{ + core::{ + event::{ + wayland::{LayerEvent, PopupEvent, SessionLockEvent}, + PlatformSpecific, + }, + Clipboard as _, + }, + futures::channel::mpsc, +}; +use iced_graphics::Compositor; +use iced_runtime::{ + core::{ + event::wayland, + keyboard, mouse, touch, + window::{self, Id as SurfaceId}, + Point, + }, + keyboard::{key, Key, Location}, + user_interface, Debug, +}; + +use sctk::{ + output::OutputInfo, + reexports::{ + calloop::channel, + client::{ + backend::ObjectId, + protocol::{ + wl_display::WlDisplay, wl_keyboard::WlKeyboard, + wl_output::WlOutput, wl_pointer::WlPointer, wl_seat::WlSeat, + wl_surface::WlSurface, wl_touch::WlTouch, + }, + Proxy, QueueHandle, + }, + csd_frame::WindowManagerCapabilities, + }, + seat::{ + keyboard::{KeyEvent, Modifiers}, + pointer::{PointerEvent, PointerEventKind}, + Capability, + }, + session_lock::SessionLockSurfaceConfigure, + shell::{ + wlr_layer::LayerSurfaceConfigure, + xdg::{popup::PopupConfigure, window::WindowConfigure}, + }, +}; +use std::{ + collections::HashMap, + num::NonZeroU32, + sync::{Arc, Mutex}, + time::Instant, +}; +use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport; +use winit::{ + dpi::PhysicalSize, event::WindowEvent, event_loop::EventLoopProxy, + window::WindowId, +}; +use xkeysym::Keysym; + +use super::{ + event_loop::state::{Common, CommonSurface, SctkState}, + keymap::raw_keycode_to_physicalkey, + winit_window::SctkWinitWindow, +}; + +pub enum IcedSctkEvent { + /// Emitted when new events arrive from the OS to be processed. + /// + /// This event type is useful as a place to put code that should be done before you start + /// processing events, such as updating frame timing information for benchmarking or checking + /// the [`StartCause`][crate::event::StartCause] to see if a timer set by + /// [`ControlFlow::WaitUntil`](crate::platform_specific::wayland::event_loop::ControlFlow::WaitUntil) has elapsed. + NewEvents(StartCause), + + /// An event produced by sctk + SctkEvent(SctkEvent), + + /// Emitted when all of the event loop's input events have been processed and redraw processing + /// is about to begin. + /// + /// This event is useful as a place to put your code that should be run after all + /// state-changing events have been handled and you want to do stuff (updating state, performing + /// calculations, etc) that happens as the "main body" of your event loop. If your program only draws + /// graphics when something changes, it's usually better to do it in response to + /// [`Event::RedrawRequested`](crate::event::Event::RedrawRequested), which gets emitted + /// immediately after this event. Programs that draw graphics continuously, like most games, + /// can render here unconditionally for simplicity. + MainEventsCleared, + + /// Emitted after [`MainEventsCleared`] when a window should be redrawn. + /// + /// This gets triggered in two scenarios: + /// - The OS has performed an operation that's invalidated the window's contents (such as + /// resizing the window). + /// - The application has explicitly requested a redraw via [`Window::request_redraw`]. + /// + /// During each iteration of the event loop, Winit will aggregate duplicate redraw requests + /// into a single event, to help avoid duplicating rendering work. + /// + /// Mainly of interest to applications with mostly-static graphics that avoid redrawing unless + /// something changes, like most non-game GUIs. + /// + /// [`MainEventsCleared`]: Self::MainEventsCleared + RedrawRequested(ObjectId), + + /// Emitted after all [`RedrawRequested`] events have been processed and control flow is about to + /// be taken away from the program. If there are no `RedrawRequested` events, it is emitted + /// immediately after `MainEventsCleared`. + /// + /// This event is useful for doing any cleanup or bookkeeping work after all the rendering + /// tasks have been completed. + /// + /// [`RedrawRequested`]: Self::RedrawRequested + RedrawEventsCleared, + + /// Emitted when the event loop is being shut down. + /// + /// This is irreversible - if this event is emitted, it is guaranteed to be the last event that + /// gets emitted. You generally want to treat this as an "do on quit" event. + LoopDestroyed, + + /// Frame callback event + Frame(WlSurface, u32), +} + +#[derive(Debug, Clone)] +pub enum SctkEvent { + // + // Input events + // + SeatEvent { + variant: SeatEventVariant, + id: WlSeat, + }, + PointerEvent { + variant: PointerEvent, + ptr_id: WlPointer, + seat_id: WlSeat, + }, + KeyboardEvent { + variant: KeyboardEventVariant, + kbd_id: WlKeyboard, + seat_id: WlSeat, + surface: WlSurface, + }, + TouchEvent { + variant: touch::Event, + touch_id: WlTouch, + seat_id: WlSeat, + surface: WlSurface, + }, + // TODO data device & touch + + // + // Surface Events + // + WindowEvent { + variant: WindowEventVariant, + id: WlSurface, + }, + LayerSurfaceEvent { + variant: LayerSurfaceEventVariant, + id: WlSurface, + }, + PopupEvent { + variant: PopupEventVariant, + /// this may be the Id of a window or layer surface + toplevel_id: WlSurface, + /// this may be any SurfaceId + parent_id: WlSurface, + /// the id of this popup + id: WlSurface, + }, + + // + // output events + // + NewOutput { + id: WlOutput, + info: Option, + }, + UpdateOutput { + id: WlOutput, + info: OutputInfo, + }, + RemovedOutput(WlOutput), + // + // compositor events + // + ScaleFactorChanged { + factor: f64, + id: WlOutput, + inner_size: winit::dpi::PhysicalSize, + }, + + /// session lock events + SessionLocked, + SessionLockFinished, + SessionLockSurfaceCreated { + queue_handle: QueueHandle, + surface: CommonSurface, + native_id: SurfaceId, + common: Arc>, + display: WlDisplay, + }, + SessionLockSurfaceConfigure { + surface: WlSurface, + configure: SessionLockSurfaceConfigure, + first: bool, + }, + SessionLockSurfaceDone { + surface: WlSurface, + }, + SessionUnlocked, + SurfaceScaleFactorChanged(f64, WlSurface, window::Id), + Winit(WindowId, WindowEvent), + Subcompositor(SubsurfaceState), +} + +#[cfg(feature = "a11y")] +#[derive(Debug, Clone)] +pub struct ActionRequestEvent { + pub surface_id: ObjectId, + pub request: iced_accessibility::accesskit::ActionRequest, +} + +#[derive(Debug, Clone)] +pub enum SeatEventVariant { + New, + Remove, + NewCapability(Capability, ObjectId), + RemoveCapability(Capability, ObjectId), +} + +#[derive(Debug, Clone)] +pub enum KeyboardEventVariant { + Leave(WlSurface), + Enter(WlSurface), + Press(KeyEvent), + Repeat(KeyEvent), + Release(KeyEvent), + Modifiers(Modifiers), +} + +#[derive(Debug, Clone)] +pub enum WindowEventVariant { + Created(WlSurface, SurfaceId), + /// + Close, + /// + WmCapabilities(WindowManagerCapabilities), + /// + ConfigureBounds { + width: u32, + height: u32, + }, + /// + Configure((NonZeroU32, NonZeroU32), WindowConfigure, WlSurface, bool), + Size((NonZeroU32, NonZeroU32), WlSurface, bool), + /// window state changed + StateChanged(sctk::reexports::csd_frame::WindowState), + /// Scale Factor + ScaleFactorChanged(f64, Option), +} + +#[derive(Debug, Clone)] +pub enum PopupEventVariant { + /// Popup Created + Created( + QueueHandle, + CommonSurface, + SurfaceId, + Arc>, + WlDisplay, + ), + /// + Done, + /// + Configure(PopupConfigure, WlSurface, bool), + /// + RepositionionedPopup { token: u32 }, + /// size + Size(u32, u32), + /// Scale Factor + ScaleFactorChanged(f64, Option), +} + +#[derive(Debug, Clone)] +pub enum LayerSurfaceEventVariant { + /// sent after creation of the layer surface + Created( + QueueHandle, + CommonSurface, + SurfaceId, + Arc>, + WlDisplay, + String, + ), + /// + Done, + /// + Configure(LayerSurfaceConfigure, WlSurface, bool), + /// Scale Factor + ScaleFactorChanged(f64, Option), +} + +/// Describes the reason the event loop is resuming. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StartCause { + /// Sent if the time specified by [`ControlFlow::WaitUntil`] has been reached. Contains the + /// moment the timeout was requested and the requested resume time. The actual resume time is + /// guaranteed to be equal to or after the requested resume time. + /// + /// [`ControlFlow::WaitUntil`]: crate::platform_specific::wayland::event_loop::ControlFlow::WaitUntil + ResumeTimeReached { + start: Instant, + requested_resume: Instant, + }, + + /// Sent if the OS has new events to send to the window, after a wait was requested. Contains + /// the moment the wait was requested and the resume time, if requested. + WaitCancelled { + start: Instant, + requested_resume: Option, + }, + + /// Sent if the event loop is being resumed after the loop's control flow was set to + /// [`ControlFlow::Poll`]. + /// + /// [`ControlFlow::Poll`]: crate::platform_specific::wayland::event_loop::ControlFlow::Poll + Poll, + + /// Sent once, immediately after `run` is called. Indicates that the loop was just initialized. + Init, +} + +/// Pending update to a window requested by the user. +#[derive(Default, Debug, Clone, Copy)] +pub struct SurfaceUserRequest { + /// Whether `redraw` was requested. + pub redraw_requested: bool, + + /// Wether the frame should be refreshed. + pub refresh_frame: bool, +} + +// The window update coming from the compositor. +#[derive(Default, Debug, Clone)] +pub struct SurfaceCompositorUpdate { + /// New window configure. + pub configure: Option, + + /// New scale factor. + pub scale_factor: Option, +} +pub type UserInterfaces<'a, P> = HashMap< + SurfaceId, + user_interface::UserInterface< + 'a, +

::Message, +

::Theme, +

::Renderer, + >, + rustc_hash::FxBuildHasher, +>; + +impl SctkEvent { + pub(crate) fn process<'a, P, C>( + self, + modifiers: &mut Modifiers, + program: &'a P, + compositor: &mut C, + window_manager: &mut crate::program::WindowManager, + surface_ids: &mut HashMap, + subsurface_ids: &mut HashMap, + sctk_tx: &channel::Sender, + control_sender: &mpsc::UnboundedSender, + proxy: &EventLoopProxy, + debug: &mut Debug, + user_interfaces: &mut UserInterfaces<'a, P>, + events: &mut Vec<(Option, iced_runtime::core::Event)>, + clipboard: &mut Clipboard, + subsurface_state: &mut Option, + #[cfg(feature = "a11y")] adapters: &mut HashMap< + window::Id, + (u64, iced_accessibility::accesskit_winit::Adapter), + >, + ) where + P: Program, + C: Compositor, + { + match self { + // TODO Ashley: Platform specific multi-seat events? + SctkEvent::SeatEvent { .. } => Default::default(), + SctkEvent::PointerEvent { variant, .. } => match variant.kind { + PointerEventKind::Enter { .. } => { + events.push(( + surface_ids + .get(&variant.surface.id()) + .map(|id| id.inner()), + iced_runtime::core::Event::Mouse( + mouse::Event::CursorEntered, + ), + )); + } + PointerEventKind::Leave { .. } => events.push(( + surface_ids.get(&variant.surface.id()).map(|id| id.inner()), + iced_runtime::core::Event::Mouse(mouse::Event::CursorLeft), + )), + PointerEventKind::Motion { .. } => { + let offset = if let Some((x_offset, y_offset, _)) = + subsurface_ids.get(&variant.surface.id()) + { + (*x_offset, *y_offset) + } else { + (0, 0) + }; + let id = surface_ids + .get(&variant.surface.id()) + .map(|id| id.inner()); + if let Some(w) = + id.clone().and_then(|id| window_manager.get_mut(id)) + { + w.state.set_logical_cursor_pos( + ( + variant.position.0 + offset.0 as f64, + variant.position.1 + offset.1 as f64, + ) + .into(), + ) + } + events.push(( + id, + iced_runtime::core::Event::Mouse( + mouse::Event::CursorMoved { + position: Point::new( + variant.position.0 as f32 + offset.0 as f32, + variant.position.1 as f32 + offset.1 as f32, + ), + }, + ), + )); + } + PointerEventKind::Press { + time: _, + button, + serial: _, + } => { + if let Some(e) = pointer_button_to_native(button).map(|b| { + iced_runtime::core::Event::Mouse( + mouse::Event::ButtonPressed(b), + ) + }) { + events.push(( + surface_ids + .get(&variant.surface.id()) + .map(|id| id.inner()), + e, + )); + } + } // TODO Ashley: conversion + PointerEventKind::Release { + time: _, + button, + serial: _, + } => { + if let Some(e) = pointer_button_to_native(button).map(|b| { + iced_runtime::core::Event::Mouse( + mouse::Event::ButtonReleased(b), + ) + }) { + events.push(( + surface_ids + .get(&variant.surface.id()) + .map(|id| id.inner()), + e, + )); + } + } // TODO Ashley: conversion + PointerEventKind::Axis { + time: _, + horizontal, + vertical, + source, + } => { + if let Some(e) = + pointer_axis_to_native(source, horizontal, vertical) + .map(|a| { + iced_runtime::core::Event::Mouse( + mouse::Event::WheelScrolled { delta: a }, + ) + }) + { + events.push(( + surface_ids + .get(&variant.surface.id()) + .map(|id| id.inner()), + e, + )); + } + } // TODO Ashley: conversion + }, + SctkEvent::KeyboardEvent { + variant, + kbd_id: _, + seat_id, + surface, + } => match variant { + KeyboardEventVariant::Leave(surface) => { + if let Some(e) = + surface_ids.get(&surface.id()).and_then(|id| match id { + SurfaceIdWrapper::LayerSurface(_id) => Some( + iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::Layer( + LayerEvent::Unfocused, + surface.clone(), + id.inner(), + ), + ), + ), + ), + SurfaceIdWrapper::Window(id) => { + Some(iced_runtime::core::Event::Window( + window::Event::Unfocused, + )) + } + SurfaceIdWrapper::Popup(_id) => Some( + iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::Popup( + PopupEvent::Unfocused, + surface.clone(), + id.inner(), + ), + ), + ), + ), + SurfaceIdWrapper::SessionLock(_) => Some( + iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::SessionLock( + SessionLockEvent::Unfocused( + surface.clone(), + id.inner(), + ), + ), + ), + ), + ), + }) + { + events.push(( + surface_ids.get(&surface.id()).map(|id| id.inner()), + e, + )); + } + + events.push(( + surface_ids.get(&surface.id()).map(|id| id.inner()), + iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::Seat( + wayland::SeatEvent::Leave, + seat_id, + )), + ), + )) + } + KeyboardEventVariant::Enter(surface) => { + if let Some(e) = + surface_ids.get(&surface.id()).and_then(|id| { + match id { + SurfaceIdWrapper::LayerSurface(_id) => Some( + iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::Layer( + LayerEvent::Focused, + surface.clone(), + id.inner(), + ), + ), + ), + ), + SurfaceIdWrapper::Window(id) => { + Some(iced_runtime::core::Event::Window( + window::Event::Focused, + )) + } + SurfaceIdWrapper::Popup(_id) => Some( + iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::Popup( + PopupEvent::Focused, + surface.clone(), + id.inner(), + ), + ), + ), + ), + SurfaceIdWrapper::SessionLock(_) => Some( + iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::SessionLock( + SessionLockEvent::Focused( + surface.clone(), + id.inner(), + ), + ), + ), + ), + ), + } + .map(|e| (Some(id.inner()), e)) + }) + { + events.push(e); + } + + events.push(( + surface_ids.get(&surface.id()).map(|id| id.inner()), + iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::Seat( + wayland::SeatEvent::Enter, + seat_id, + )), + ), + )); + } + KeyboardEventVariant::Press(ke) => { + let (key, location) = keysym_to_vkey_location(ke.keysym); + let physical_key = raw_keycode_to_physicalkey(ke.raw_code); + let physical_key = + crate::conversion::physical_key(physical_key); + + events.push(( + surface_ids.get(&surface.id()).map(|id| id.inner()), + iced_runtime::core::Event::Keyboard( + keyboard::Event::KeyPressed { + key: key.clone(), + location: location, + text: ke.utf8.map(|s| s.into()), + modifiers: modifiers_to_native(*modifiers), + physical_key, + modified_key: key, // TODO calculate without Ctrl? + }, + ), + )) + } + KeyboardEventVariant::Repeat(KeyEvent { + keysym, + utf8, + raw_code, + .. + }) => { + let (key, location) = keysym_to_vkey_location(keysym); + let physical_key = raw_keycode_to_physicalkey(raw_code); + let physical_key = + crate::conversion::physical_key(physical_key); + + events.push(( + surface_ids.get(&surface.id()).map(|id| id.inner()), + iced_runtime::core::Event::Keyboard( + keyboard::Event::KeyPressed { + key: key.clone(), + location: location, + text: utf8.map(|s| s.into()), + modifiers: modifiers_to_native(*modifiers), + physical_key, + modified_key: key, // TODO calculate without Ctrl? + }, + ), + )) + } + KeyboardEventVariant::Release(ke) => { + let (k, location) = keysym_to_vkey_location(ke.keysym); + let physical_key = raw_keycode_to_physicalkey(ke.raw_code); + let physical_key = + crate::conversion::physical_key(physical_key); + events.push(( + surface_ids.get(&surface.id()).map(|id| id.inner()), + iced_runtime::core::Event::Keyboard( + keyboard::Event::KeyReleased { + key: k.clone(), + location, + modifiers: modifiers_to_native(*modifiers), + modified_key: k, + physical_key: physical_key, + }, + ), + )) + } + KeyboardEventVariant::Modifiers(new_mods) => { + *modifiers = new_mods; + events.push(( + surface_ids.get(&surface.id()).map(|id| id.inner()), + iced_runtime::core::Event::Keyboard( + keyboard::Event::ModifiersChanged( + modifiers_to_native(new_mods), + ), + ), + )) + } + }, + SctkEvent::TouchEvent { + variant, + touch_id: _, + seat_id: _, + surface, + } => events.push(( + surface_ids.get(&surface.id()).map(|id| id.inner()), + iced_runtime::core::Event::Touch(variant), + )), + SctkEvent::WindowEvent { .. } => {} + SctkEvent::LayerSurfaceEvent { + variant, + id: surface, + } => match variant { + LayerSurfaceEventVariant::Done => { + if let Some(id) = surface_ids.remove(&surface.id()) { + if let Some(w) = window_manager.remove(id.inner()) { + if clipboard + .window_id() + .is_some_and(|id| w.raw.id() == id) + { + clipboard.register_dnd_destination( + DndSurface(Arc::new(Box::new( + w.raw.clone(), + ))), + Vec::new(), + ); + *clipboard = Clipboard::unconnected(); + } + } + events.push(( + Some(id.inner()), + iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::Layer( + LayerEvent::Done, + surface, + id.inner(), + ), + ), + ), + )); + } + } + LayerSurfaceEventVariant::Created( + queue_handle, + surface, + surface_id, + common, + display, + .., + ) => { + let wl_surface = surface.wl_surface(); + let object_id = wl_surface.id(); + let wrapper = + SurfaceIdWrapper::LayerSurface(surface_id.clone()); + _ = surface_ids.insert(object_id.clone(), wrapper.clone()); + let sctk_winit = SctkWinitWindow::new( + sctk_tx.clone(), + common, + wrapper, + surface, + display, + queue_handle, + ); + + #[cfg(feature = "a11y")] + { + use crate::a11y::*; + use iced_accessibility::accesskit::{ + ActivationHandler, NodeBuilder, NodeId, Role, Tree, + TreeUpdate, + }; + use iced_accessibility::accesskit_winit::Adapter; + + let node_id = iced_runtime::core::id::window_node_id(); + + let activation_handler = WinitActivationHandler { + proxy: control_sender.clone(), + title: String::new(), + }; + + let action_handler = WinitActionHandler { + id: surface_id, + proxy: control_sender.clone(), + }; + + let deactivation_handler = WinitDeactivationHandler { + proxy: control_sender.clone(), + }; + _ = adapters.insert( + surface_id, + ( + node_id, + Adapter::with_direct_handlers( + sctk_winit.as_ref(), + activation_handler, + action_handler, + deactivation_handler, + ), + ), + ); + } + + let window = window_manager.insert( + surface_id, sctk_winit, program, compositor, + false, // TODO do we want to get this value here? + 0, + ); + _ = surface_ids.insert(object_id, wrapper.clone()); + let logical_size = window.size(); + + if clipboard.window_id().is_none() { + *clipboard = Clipboard::connect( + window.raw.clone(), + crate::clipboard::ControlSender { + sender: control_sender.clone(), + proxy: proxy.clone(), + }, + ); + } + + let _ = user_interfaces.insert( + surface_id, + crate::program::build_user_interface( + program, + user_interface::Cache::default(), + &mut window.renderer, + logical_size, + debug, + surface_id, + window.raw.clone(), + window.prev_dnd_destination_rectangles_count, + clipboard, + ), + ); + } + LayerSurfaceEventVariant::ScaleFactorChanged(..) => {} + LayerSurfaceEventVariant::Configure( + configure, + surface, + first, + ) => { + if let Some(w) = surface_ids + .get(&surface.id()) + .and_then(|id| window_manager.get_mut(id.inner())) + { + let scale = w.state.scale_factor(); + let p_w = (configure.new_size.0.max(1) as f64 * scale) + .ceil() as u32; + let p_h = (configure.new_size.1.max(1) as f64 * scale) + .ceil() as u32; + w.state.update( + w.raw.as_ref(), + &WindowEvent::SurfaceResized(PhysicalSize::new( + p_w, p_h, + )), + debug, + ); + } + } + }, + SctkEvent::PopupEvent { + variant, + id: surface, + .. + } => { + match variant { + PopupEventVariant::Done => { + if let Some(e) = + surface_ids.remove(&surface.id()).map(|id| { + if let Some(w) = + window_manager.remove(id.inner()) + { + clipboard.register_dnd_destination( + DndSurface(Arc::new(Box::new( + w.raw.clone(), + ))), + Vec::new(), + ); + if clipboard + .window_id() + .is_some_and(|id| w.raw.id() == id) + { + *clipboard = Clipboard::unconnected(); + } + } + _ = user_interfaces.remove(&id.inner()); + + ( + Some(id.inner()), + iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland( + wayland::Event::Popup( + PopupEvent::Done, + surface, + id.inner(), + ), + ), + ), + ) + }) + { + events.push(e) + } + } + PopupEventVariant::Created( + queue_handle, + surface, + surface_id, + common, + display, + ) => { + let wl_surface = surface.wl_surface(); + let wrapper = SurfaceIdWrapper::Popup(surface_id); + _ = surface_ids + .insert(wl_surface.id(), wrapper.clone()); + let sctk_winit = SctkWinitWindow::new( + sctk_tx.clone(), + common, + wrapper, + surface, + display, + queue_handle, + ); + #[cfg(feature = "a11y")] + { + use crate::a11y::*; + use iced_accessibility::accesskit::{ + ActivationHandler, NodeBuilder, NodeId, Role, + Tree, TreeUpdate, + }; + use iced_accessibility::accesskit_winit::Adapter; + + let node_id = + iced_runtime::core::id::window_node_id(); + + let activation_handler = WinitActivationHandler { + proxy: control_sender.clone(), + title: String::new(), + }; + + let action_handler = WinitActionHandler { + id: surface_id, + proxy: control_sender.clone(), + }; + + let deactivation_handler = + WinitDeactivationHandler { + proxy: control_sender.clone(), + }; + _ = adapters.insert( + surface_id, + ( + node_id, + Adapter::with_direct_handlers( + sctk_winit.as_ref(), + activation_handler, + action_handler, + deactivation_handler, + ), + ), + ); + } + + if clipboard.window_id().is_none() { + *clipboard = Clipboard::connect( + sctk_winit.clone(), + crate::clipboard::ControlSender { + sender: control_sender.clone(), + proxy: proxy.clone(), + }, + ); + } + + let window = window_manager.insert( + surface_id, sctk_winit, program, compositor, + false, // TODO do we want to get this value here? + 0, + ); + let logical_size = window.size(); + + let _ = user_interfaces.insert( + surface_id, + crate::program::build_user_interface( + program, + user_interface::Cache::default(), + &mut window.renderer, + logical_size, + debug, + surface_id, + window.raw.clone(), + window.prev_dnd_destination_rectangles_count, + clipboard, + ), + ); + } + PopupEventVariant::Configure(_, _, _) => {} // TODO + PopupEventVariant::RepositionionedPopup { token: _ } => {} + PopupEventVariant::Size(_, _) => {} + PopupEventVariant::ScaleFactorChanged(..) => {} + } + } + SctkEvent::NewOutput { id, info } => events.push(( + None, + iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::Output( + wayland::OutputEvent::Created(info), + id, + )), + ), + )), + SctkEvent::UpdateOutput { id, info } => events.push(( + None, + iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::Output( + wayland::OutputEvent::InfoUpdate(info), + id, + )), + ), + )), + SctkEvent::RemovedOutput(id) => events.push(( + None, + iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::Output( + wayland::OutputEvent::Removed, + id, + )), + ), + )), + SctkEvent::ScaleFactorChanged { + factor: _, + id: _, + inner_size: _, + } => Default::default(), + SctkEvent::SessionLocked => events.push(( + None, + iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::SessionLock( + wayland::SessionLockEvent::Locked, + )), + ), + )), + SctkEvent::SessionLockFinished => events.push(( + None, + iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::SessionLock( + wayland::SessionLockEvent::Finished, + )), + ), + )), + SctkEvent::SessionLockSurfaceCreated { + queue_handle, + surface, + native_id: surface_id, + common, + display, + } => { + let wl_surface = surface.wl_surface(); + let object_id = wl_surface.id().clone(); + let wrapper = SurfaceIdWrapper::SessionLock(surface_id.clone()); + _ = surface_ids.insert(object_id.clone(), wrapper.clone()); + let sctk_winit = SctkWinitWindow::new( + sctk_tx.clone(), + common, + wrapper, + surface, + display, + queue_handle, + ); + + #[cfg(feature = "a11y")] + { + use crate::a11y::*; + use iced_accessibility::accesskit::{ + ActivationHandler, NodeBuilder, NodeId, Role, Tree, + TreeUpdate, + }; + use iced_accessibility::accesskit_winit::Adapter; + + let node_id = iced_runtime::core::id::window_node_id(); + + let activation_handler = WinitActivationHandler { + proxy: control_sender.clone(), + // TODO lock screen title + title: String::new(), + }; + + let action_handler = WinitActionHandler { + id: surface_id, + proxy: control_sender.clone(), + }; + + let deactivation_handler = WinitDeactivationHandler { + proxy: control_sender.clone(), + }; + _ = adapters.insert( + surface_id, + ( + node_id, + Adapter::with_direct_handlers( + sctk_winit.as_ref(), + activation_handler, + action_handler, + deactivation_handler, + ), + ), + ); + } + + if clipboard.window_id().is_none() { + *clipboard = Clipboard::connect( + sctk_winit.clone(), + crate::clipboard::ControlSender { + sender: control_sender.clone(), + proxy: proxy.clone(), + }, + ); + } + + let window = window_manager.insert( + surface_id, sctk_winit, program, compositor, + false, // TODO do we want to get this value here? + 0, + ); + _ = surface_ids.insert(object_id, wrapper.clone()); + let logical_size = window.size(); + + let _ = user_interfaces.insert( + surface_id, + crate::program::build_user_interface( + program, + user_interface::Cache::default(), + &mut window.renderer, + logical_size, + debug, + surface_id, + window.raw.clone(), + window.prev_dnd_destination_rectangles_count, + clipboard, + ), + ); + } + SctkEvent::SessionLockSurfaceConfigure { .. } => {} + SctkEvent::SessionLockSurfaceDone { surface } => { + if let Some(id) = surface_ids.remove(&surface.id()) { + _ = window_manager.remove(id.inner()); + } + } + SctkEvent::SessionUnlocked => events.push(( + None, + iced_runtime::core::Event::PlatformSpecific( + PlatformSpecific::Wayland(wayland::Event::SessionLock( + wayland::SessionLockEvent::Unlocked, + )), + ), + )), + SctkEvent::Winit(_, _) => {} + SctkEvent::SurfaceScaleFactorChanged(scale, _, id) => { + if let Some(w) = window_manager.get_mut(id) { + w.state.update_scale_factor(scale); + } + } + SctkEvent::Subcompositor(s) => { + *subsurface_state = Some(s); + } + } + } +} + +fn keysym_to_vkey_location(keysym: Keysym) -> (Key, Location) { + let raw = keysym.raw(); + let mut key = keysym_to_key(raw); + if matches!(key, key::Key::Unidentified) { + // XXX is there a better way to do this? + // we need to be able to determine the actual character for the key + // not the combination, so this seems to be correct + let mut utf8 = xkbcommon::xkb::keysym_to_utf8(keysym); + // remove null terminator + _ = utf8.pop(); + if utf8.len() > 0 { + key = Key::Character(utf8.into()); + } + } + + let location = keymap::keysym_location(raw); + (key, location) +} diff --git a/winit/src/platform_specific/wayland/subsurface_widget.rs b/winit/src/platform_specific/wayland/subsurface_widget.rs new file mode 100644 index 0000000000..1f657300df --- /dev/null +++ b/winit/src/platform_specific/wayland/subsurface_widget.rs @@ -0,0 +1,702 @@ +// TODO z-order option? + +use crate::core::{ + layout::{self, Layout}, + mouse, renderer, + widget::{self, Widget}, + ContentFit, Element, Length, Rectangle, Size, +}; +use std::{ + cell::RefCell, + collections::HashMap, + fmt::Debug, + future::Future, + hash::{Hash, Hasher}, + mem, + os::unix::io::{AsFd, OwnedFd}, + pin::Pin, + ptr, + sync::{Arc, Mutex, Weak}, + task, +}; + +use crate::futures::futures::channel::oneshot; +use iced_futures::core::window; +use sctk::{ + compositor::SurfaceData, + globals::GlobalData, + reexports::client::{ + delegate_noop, + protocol::{ + wl_buffer::{self, WlBuffer}, + wl_compositor::WlCompositor, + wl_shm::{self, WlShm}, + wl_shm_pool::{self, WlShmPool}, + wl_subcompositor::WlSubcompositor, + wl_subsurface::WlSubsurface, + wl_surface::WlSurface, + }, + Connection, Dispatch, Proxy, QueueHandle, + }, +}; +use wayland_backend::client::ObjectId; +use wayland_protocols::wp::{ + alpha_modifier::v1::client::{ + wp_alpha_modifier_surface_v1::WpAlphaModifierSurfaceV1, + wp_alpha_modifier_v1::WpAlphaModifierV1, + }, + linux_dmabuf::zv1::client::{ + zwp_linux_buffer_params_v1::{self, ZwpLinuxBufferParamsV1}, + zwp_linux_dmabuf_v1::{self, ZwpLinuxDmabufV1}, + }, + viewporter::client::{ + wp_viewport::WpViewport, wp_viewporter::WpViewporter, + }, +}; + +use crate::platform_specific::{ + event_loop::state::SctkState, SurfaceIdWrapper, +}; + +#[derive(Debug)] +pub struct Plane { + pub fd: OwnedFd, + pub plane_idx: u32, + pub offset: u32, + pub stride: u32, +} + +#[derive(Debug)] +pub struct Dmabuf { + pub width: i32, + pub height: i32, + pub planes: Vec, + pub format: u32, + pub modifier: u64, +} + +#[derive(Debug)] +pub struct Shmbuf { + pub fd: OwnedFd, + pub offset: i32, + pub width: i32, + pub height: i32, + pub stride: i32, + pub format: wl_shm::Format, +} + +#[derive(Debug)] +pub enum BufferSource { + Shm(Shmbuf), + Dma(Dmabuf), +} + +impl From for BufferSource { + fn from(buf: Shmbuf) -> Self { + Self::Shm(buf) + } +} + +impl From for BufferSource { + fn from(buf: Dmabuf) -> Self { + Self::Dma(buf) + } +} + +#[derive(Debug)] +struct SubsurfaceBufferInner { + source: Arc, + _sender: oneshot::Sender<()>, +} + +/// Refcounted type containing a `BufferSource` with a sender that is signaled +/// all references are dropped and `wl_buffer`s created from the source are +/// released. +#[derive(Clone, Debug)] +pub struct SubsurfaceBuffer(Arc); + +pub struct BufferData { + source: WeakBufferSource, + // This reference is held until the surface `release`s the buffer + subsurface_buffer: Mutex>, +} + +impl BufferData { + fn for_buffer(buffer: &WlBuffer) -> Option<&Self> { + buffer.data::() + } +} + +/// Future signalled when subsurface buffer is released +pub struct SubsurfaceBufferRelease(oneshot::Receiver<()>); + +impl SubsurfaceBufferRelease { + /// Non-blocking check if buffer is released yet, without awaiting + pub fn released(&mut self) -> bool { + self.0.try_recv() == Ok(None) + } +} + +impl Future for SubsurfaceBufferRelease { + type Output = (); + + fn poll( + mut self: Pin<&mut Self>, + cx: &mut task::Context<'_>, + ) -> task::Poll<()> { + Pin::new(&mut self.0).poll(cx).map(|_| ()) + } +} + +impl SubsurfaceBuffer { + pub fn new(source: Arc) -> (Self, SubsurfaceBufferRelease) { + let (_sender, receiver) = oneshot::channel(); + let subsurface_buffer = + SubsurfaceBuffer(Arc::new(SubsurfaceBufferInner { + source, + _sender, + })); + (subsurface_buffer, SubsurfaceBufferRelease(receiver)) + } + + // Behavior of `wl_buffer::released` is undefined if attached to multiple surfaces. To allow + // things like that, create a new `wl_buffer` each time. + fn create_buffer( + &self, + shm: &WlShm, + dmabuf: Option<&ZwpLinuxDmabufV1>, + qh: &QueueHandle, + ) -> Option { + // create reference to source, that is dropped on release + match self.0.source.as_ref() { + BufferSource::Shm(buf) => { + let pool = shm.create_pool( + buf.fd.as_fd(), + buf.offset + buf.height * buf.stride, + qh, + GlobalData, + ); + let buffer = pool.create_buffer( + buf.offset, + buf.width, + buf.height, + buf.stride, + buf.format, + qh, + BufferData { + source: WeakBufferSource(Arc::downgrade( + &self.0.source, + )), + subsurface_buffer: Mutex::new(Some(self.clone())), + }, + ); + pool.destroy(); + Some(buffer) + } + BufferSource::Dma(buf) => { + if let Some(dmabuf) = dmabuf { + let params = dmabuf.create_params(qh, GlobalData); + for plane in &buf.planes { + let modifier_hi = (buf.modifier >> 32) as u32; + let modifier_lo = (buf.modifier & 0xffffffff) as u32; + params.add( + plane.fd.as_fd(), + plane.plane_idx, + plane.offset, + plane.stride, + modifier_hi, + modifier_lo, + ); + } + // Will cause protocol error if format is not supported + Some(params.create_immed( + buf.width, + buf.height, + buf.format, + zwp_linux_buffer_params_v1::Flags::empty(), + qh, + BufferData { + source: WeakBufferSource(Arc::downgrade( + &self.0.source, + )), + subsurface_buffer: Mutex::new(Some(self.clone())), + }, + )) + } else { + None + } + } + } + } +} + +impl PartialEq for SubsurfaceBuffer { + fn eq(&self, rhs: &Self) -> bool { + Arc::ptr_eq(&self.0, &rhs.0) + } +} + +impl Dispatch for SctkState { + fn event( + _: &mut SctkState, + _: &WlShmPool, + _: wl_shm_pool::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + unreachable!() + } +} + +impl Dispatch for SctkState { + fn event( + _: &mut SctkState, + _: &ZwpLinuxDmabufV1, + _: zwp_linux_dmabuf_v1::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + } +} + +impl Dispatch for SctkState { + fn event( + _: &mut SctkState, + _: &ZwpLinuxBufferParamsV1, + _: zwp_linux_buffer_params_v1::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + } +} + +impl Dispatch for SctkState { + fn event( + _: &mut SctkState, + _: &WlBuffer, + event: wl_buffer::Event, + data: &BufferData, + _: &Connection, + _: &QueueHandle, + ) { + match event { + wl_buffer::Event::Release => { + // Release reference to `SubsurfaceBuffer` + _ = data.subsurface_buffer.lock().unwrap().take(); + } + _ => unreachable!(), + } + } +} + +#[doc(hidden)] +#[derive(Clone, Debug)] +pub(crate) struct WeakBufferSource(Weak); + +impl PartialEq for WeakBufferSource { + fn eq(&self, rhs: &Self) -> bool { + Weak::ptr_eq(&self.0, &rhs.0) + } +} + +impl Eq for WeakBufferSource {} + +impl Hash for WeakBufferSource { + fn hash(&self, state: &mut H) { + ptr::hash::(self.0.as_ptr(), state) + } +} + +// create wl_buffer from BufferSource (avoid create_immed?) +// release +#[derive(Debug, Clone)] +#[doc(hidden)] +pub struct SubsurfaceState { + pub wl_compositor: WlCompositor, + pub wl_subcompositor: WlSubcompositor, + pub wp_viewporter: WpViewporter, + pub wl_shm: WlShm, + pub wp_dmabuf: Option, + pub wp_alpha_modifier: Option, + pub qh: QueueHandle, + pub(crate) buffers: HashMap>, + pub unmapped_subsurfaces: Vec, +} + +impl SubsurfaceState { + fn create_subsurface(&self, parent: &WlSurface) -> SubsurfaceInstance { + let wl_surface = self + .wl_compositor + .create_surface(&self.qh, SurfaceData::new(None, 1)); + + // Use empty input region so parent surface gets pointer events + let region = self.wl_compositor.create_region(&self.qh, ()); + wl_surface.set_input_region(Some(®ion)); + region.destroy(); + + let wl_subsurface = self.wl_subcompositor.get_subsurface( + &wl_surface, + parent, + &self.qh, + (), + ); + + let wp_viewport = self.wp_viewporter.get_viewport( + &wl_surface, + &self.qh, + sctk::globals::GlobalData, + ); + + let wp_alpha_modifier_surface = + self.wp_alpha_modifier.as_ref().map(|wp_alpha_modifier| { + wp_alpha_modifier.get_surface(&wl_surface, &self.qh, ()) + }); + + SubsurfaceInstance { + wl_surface, + wl_subsurface, + wp_viewport, + wp_alpha_modifier_surface, + wl_buffer: None, + bounds: None, + } + } + + // Update `subsurfaces` from `view_subsurfaces` + pub(crate) fn update_subsurfaces( + &mut self, + parent_id: window::Id, + subsurface_ids: &mut HashMap, + parent: &WlSurface, + subsurfaces: &mut Vec, + view_subsurfaces: &[SubsurfaceInfo], + ) { + // Subsurfaces aren't destroyed immediately to sync removal with parent + // surface commit. Since `destroy` is immediate. + // + // They should be safe to destroy by the next time `update_subsurfaces` + // is run. + self.unmapped_subsurfaces.clear(); + + // Remove cached `wl_buffers` for any `BufferSource`s that no longer exist. + self.buffers.retain(|k, v| { + let retain = k.0.strong_count() > 0; + if !retain { + v.iter().for_each(|b| b.destroy()); + } + retain + }); + + // If view requested fewer subsurfaces than there currently are, + // unmap excess. + while view_subsurfaces.len() < subsurfaces.len() { + let subsurface = subsurfaces.pop().unwrap(); + subsurface.unmap(); + self.unmapped_subsurfaces.push(subsurface); + } + // Create new subsurfaces if there aren't enough. + while subsurfaces.len() < view_subsurfaces.len() { + subsurfaces.push(self.create_subsurface(parent)); + } + // Attach buffers to subsurfaces, set viewports, and commit. + for (subsurface_data, subsurface) in + view_subsurfaces.iter().zip(subsurfaces.iter_mut()) + { + subsurface.attach_and_commit( + parent_id, + subsurface_ids, + subsurface_data, + self, + ); + } + + if let Some(backend) = parent.backend().upgrade() { + subsurface_ids.retain(|k, _| backend.info(k.clone()).is_ok()); + } + } + + // Cache `wl_buffer` for use when `BufferSource` is used in future + // (Avoid creating wl_buffer each buffer swap) + fn insert_cached_wl_buffer(&mut self, buffer: WlBuffer) { + let source = BufferData::for_buffer(&buffer).unwrap().source.clone(); + self.buffers.entry(source).or_default().push(buffer); + } + + // Gets a cached `wl_buffer` for the `SubsurfaceBuffer`, if any. And stores `SubsurfaceBuffer` + // reference to be releated on `wl_buffer` release. + // + // If `wl_buffer` isn't released, it is destroyed instead. + fn get_cached_wl_buffer( + &mut self, + subsurface_buffer: &SubsurfaceBuffer, + ) -> Option { + let buffers = self.buffers.get_mut(&WeakBufferSource( + Arc::downgrade(&subsurface_buffer.0.source), + ))?; + while let Some(buffer) = buffers.pop() { + let mut subsurface_buffer_ref = buffer + .data::() + .unwrap() + .subsurface_buffer + .lock() + .unwrap(); + if subsurface_buffer_ref.is_none() { + *subsurface_buffer_ref = Some(subsurface_buffer.clone()); + drop(subsurface_buffer_ref); + return Some(buffer); + } else { + buffer.destroy(); + } + } + None + } +} + +impl Drop for SubsurfaceState { + fn drop(&mut self) { + self.buffers + .values() + .flatten() + .for_each(|buffer| buffer.destroy()); + } +} + +#[derive(Clone, Debug)] +pub(crate) struct SubsurfaceInstance { + pub(crate) wl_surface: WlSurface, + wl_subsurface: WlSubsurface, + wp_viewport: WpViewport, + wp_alpha_modifier_surface: Option, + wl_buffer: Option, + bounds: Option>, +} + +impl SubsurfaceInstance { + // TODO correct damage? no damage/commit if unchanged? + fn attach_and_commit( + &mut self, + parent_id: window::Id, + subsurface_ids: &mut HashMap, + info: &SubsurfaceInfo, + state: &mut SubsurfaceState, + ) { + let buffer_changed; + + let old_buffer = self.wl_buffer.take(); + let old_buffer_data = + old_buffer.as_ref().and_then(|b| BufferData::for_buffer(&b)); + let buffer = if old_buffer_data.is_some_and(|b| { + b.subsurface_buffer.lock().unwrap().as_ref() == Some(&info.buffer) + }) { + // Same "BufferSource" is already attached to this subsurface. Don't create new `wl_buffer`. + buffer_changed = false; + old_buffer.unwrap() + } else { + if let Some(old_buffer) = old_buffer { + state.insert_cached_wl_buffer(old_buffer); + } + + buffer_changed = true; + + if let Some(buffer) = state.get_cached_wl_buffer(&info.buffer) { + buffer + } else if let Some(buffer) = info.buffer.create_buffer( + &state.wl_shm, + state.wp_dmabuf.as_ref(), + &state.qh, + ) { + buffer + } else { + // TODO log error + self.wl_surface.attach(None, 0, 0); + return; + } + }; + + // XXX scale factor? + let bounds_changed = self.bounds != Some(info.bounds); + // wlroots seems to have issues changing buffer without running this + if bounds_changed || buffer_changed { + self.wl_subsurface + .set_position(info.bounds.x as i32, info.bounds.y as i32); + self.wp_viewport.set_destination( + info.bounds.width as i32, + info.bounds.height as i32, + ); + } + if buffer_changed { + self.wl_surface.attach(Some(&buffer), 0, 0); + self.wl_surface.damage(0, 0, i32::MAX, i32::MAX); + } + if buffer_changed || bounds_changed { + _ = self.wl_surface.frame(&state.qh, self.wl_surface.clone()); + self.wl_surface.commit(); + } + + if let Some(wp_alpha_modifier_surface) = &self.wp_alpha_modifier_surface + { + let alpha = (info.alpha.clamp(0.0, 1.0) * u32::MAX as f32) as u32; + wp_alpha_modifier_surface.set_multiplier(alpha); + } + + _ = subsurface_ids.insert( + self.wl_surface.id(), + (info.bounds.x as i32, info.bounds.y as i32, parent_id), + ); + + self.wl_buffer = Some(buffer); + self.bounds = Some(info.bounds); + } + + pub fn unmap(&self) { + self.wl_surface.attach(None, 0, 0); + self.wl_surface.commit(); + } +} + +impl Drop for SubsurfaceInstance { + fn drop(&mut self) { + self.wp_viewport.destroy(); + self.wl_subsurface.destroy(); + self.wl_surface.destroy(); + if let Some(wl_buffer) = self.wl_buffer.as_ref() { + wl_buffer.destroy(); + } + } +} + +pub(crate) struct SubsurfaceInfo { + pub buffer: SubsurfaceBuffer, + pub bounds: Rectangle, + pub alpha: f32, +} + +thread_local! { + static SUBSURFACES: RefCell> = RefCell::new(Vec::new()); +} + +pub(crate) fn take_subsurfaces() -> Vec { + SUBSURFACES.with(|subsurfaces| mem::take(&mut *subsurfaces.borrow_mut())) +} + +#[must_use] +pub struct Subsurface<'a> { + buffer_size: Size, + buffer: &'a SubsurfaceBuffer, + width: Length, + height: Length, + content_fit: ContentFit, + alpha: f32, +} + +impl<'a, Message, Theme, Renderer> Widget + for Subsurface<'a> +where + Renderer: renderer::Renderer, +{ + fn size(&self) -> Size { + Size::new(self.width, self.height) + } + + // Based on image widget + fn layout( + &self, + _tree: &mut widget::Tree, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let raw_size = + limits.resolve(self.width, self.height, self.buffer_size); + + let full_size = self.content_fit.fit(self.buffer_size, raw_size); + + let final_size = Size { + width: match self.width { + Length::Shrink => f32::min(raw_size.width, full_size.width), + _ => raw_size.width, + }, + height: match self.height { + Length::Shrink => f32::min(raw_size.height, full_size.height), + _ => raw_size.height, + }, + }; + + layout::Node::new(final_size) + } + + fn draw( + &self, + _state: &widget::Tree, + _renderer: &mut Renderer, + _theme: &Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + // Instead of using renderer, we need to add surface to a list that is + // read by the iced-sctk shell. + SUBSURFACES.with(|subsurfaces| { + subsurfaces.borrow_mut().push(SubsurfaceInfo { + buffer: self.buffer.clone(), + bounds: layout.bounds(), + alpha: self.alpha, + }) + }); + } +} + +impl<'a> Subsurface<'a> { + pub fn new( + buffer_width: u32, + buffer_height: u32, + buffer: &'a SubsurfaceBuffer, + ) -> Self { + Self { + buffer_size: Size::new(buffer_width as f32, buffer_height as f32), + buffer, + // Matches defaults of image widget + width: Length::Shrink, + height: Length::Shrink, + content_fit: ContentFit::Contain, + alpha: 1., + } + } + + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + pub fn content_fit(mut self, content_fit: ContentFit) -> Self { + self.content_fit = content_fit; + self + } + + pub fn alpha(mut self, alpha: f32) -> Self { + self.alpha = alpha; + self + } +} + +impl<'a, Message, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Message: Clone + 'a, + Renderer: renderer::Renderer, +{ + fn from(subsurface: Subsurface<'a>) -> Self { + Self::new(subsurface) + } +} + +delegate_noop!(SctkState: ignore WpAlphaModifierV1); +delegate_noop!(SctkState: ignore WpAlphaModifierSurfaceV1); diff --git a/winit/src/platform_specific/wayland/winit_window.rs b/winit/src/platform_specific/wayland/winit_window.rs new file mode 100644 index 0000000000..eb7fc9dc77 --- /dev/null +++ b/winit/src/platform_specific/wayland/winit_window.rs @@ -0,0 +1,437 @@ +use crate::platform_specific::wayland::Action; +use raw_window_handle::HandleError; +use sctk::reexports::{ + calloop::channel, + client::{ + protocol::{wl_display::WlDisplay, wl_surface::WlSurface}, + Proxy, QueueHandle, + }, +}; +use std::sync::{Arc, Mutex}; +use winit::{ + dpi::LogicalSize, + error::{NotSupportedError, RequestError}, + window::WindowButtons, +}; + +use crate::platform_specific::SurfaceIdWrapper; + +use super::event_loop::state::{ + Common, CommonSurface, SctkLayerSurface, SctkLockSurface, SctkPopup, + SctkState, TOKEN_CTR, +}; + +#[derive(Debug)] +pub(crate) enum Surface { + Popup(SctkPopup), + Layer(SctkLayerSurface), + Lock(SctkLockSurface), +} + +impl Surface {} + +pub struct SctkWinitWindow { + tx: channel::Sender, + id: SurfaceIdWrapper, + surface: CommonSurface, + common: Arc>, + display: WlDisplay, + pub(crate) queue_handle: QueueHandle, + wait_redraw: bool, +} + +impl Drop for SctkWinitWindow { + fn drop(&mut self) { + self.tx.send(Action::Dropped(self.id)).unwrap(); + } +} + +impl SctkWinitWindow { + pub(crate) fn new( + tx: channel::Sender, + common: Arc>, + id: SurfaceIdWrapper, + surface: CommonSurface, + display: WlDisplay, + queue_handle: QueueHandle, + ) -> Arc { + Arc::new(Self { + tx, + common, + id, + surface, + display, + queue_handle, + wait_redraw: false, + }) + } +} + +impl winit::window::Window for SctkWinitWindow { + fn id(&self) -> winit::window::WindowId { + winit::window::WindowId::from( + self.surface.wl_surface().id().as_ptr() as u64 + ) + } + + fn scale_factor(&self) -> f64 { + let guard = self.common.lock().unwrap(); + guard.fractional_scale.unwrap_or(1.) + } + + fn request_redraw(&self) { + let surface = self.surface.wl_surface(); + _ = self.tx.send(Action::RequestRedraw(surface.id())); + } + + fn pre_present_notify(&self) { + let surface = self.surface.wl_surface(); + _ = surface.frame(&self.queue_handle, surface.clone()); + _ = self + .tx + .send(Action::PrePresentNotify(self.surface.wl_surface().id())); + } + + fn set_cursor(&self, cursor: winit::window::Cursor) { + match cursor { + winit::window::Cursor::Icon(icon) => { + _ = self.tx.send(Action::SetCursor(icon)); + } + winit::window::Cursor::Custom(_) => { + // TODO + } + } + } + + fn set_cursor_visible(&self, visible: bool) { + // TODO + } + + fn surface_size(&self) -> winit::dpi::PhysicalSize { + let guard = self.common.lock().unwrap(); + let size = guard.size; + size.to_physical(guard.fractional_scale.unwrap_or(1.)) + } + + fn request_surface_size( + &self, + size: winit::dpi::Size, + ) -> Option> { + let mut guard = self.common.lock().unwrap(); + self.request_redraw(); + let size: LogicalSize = + size.to_logical(guard.fractional_scale.unwrap_or(1.)); + match &self.surface { + CommonSurface::Popup(popup, positioner) => { + if size.width == 0 || size.height == 0 { + return None; + } + guard.size = size; + guard.requested_size.0 = Some(guard.size.width); + guard.requested_size.1 = Some(guard.size.height); + positioner.set_size( + guard.size.width as i32, + guard.size.height as i32, + ); + popup.xdg_surface().set_window_geometry( + 0, + 0, + guard.size.width as i32, + guard.size.height as i32, + ); + popup.xdg_popup().reposition( + positioner, + TOKEN_CTR + .fetch_add(1, std::sync::atomic::Ordering::Relaxed), + ); + if let Some(viewport) = guard.wp_viewport.as_ref() { + // Set inner size without the borders. + viewport.set_destination( + guard.size.width as i32, + guard.size.height as i32, + ); + } + } + CommonSurface::Layer(layer_surface) => { + guard.requested_size = ( + (size.width > 0).then_some(size.width), + (size.height > 0).then_some(size.height), + ); + if size.width > 0 { + guard.size.width = size.width; + } + if size.height > 0 { + guard.size.height = size.height; + } + layer_surface.set_size(size.width, size.height); + if let Some(viewport) = guard.wp_viewport.as_ref() { + // Set inner size without the borders. + viewport.set_destination( + guard.size.width as i32, + guard.size.height as i32, + ); + } + } + CommonSurface::Lock(_) => {} + } + None + } + + fn reset_dead_keys(&self) { + // TODO refer to winit for implementation + } + + fn set_outer_position(&self, position: winit::dpi::Position) {} + + fn outer_size(&self) -> winit::dpi::PhysicalSize { + // XXX not applicable to wrapped surfaces + Default::default() + } + + fn set_min_surface_size(&self, min_size: Option) { + // XXX not applicable to wrapped surfaces + } + + fn set_max_surface_size(&self, max_size: Option) { + // XXX not applicable to wrapped surfaces + } + + fn set_surface_resize_increments( + &self, + increments: Option, + ) { + log::warn!( + "`set_surface_resize_increments` is not implemented for Wayland" + ) + } + + fn set_title(&self, title: &str) { + // XXX not applicable to wrapped surfaces + } + + fn set_transparent(&self, transparent: bool) { + todo!() + } + + fn rwh_06_display_handle( + &self, + ) -> &dyn raw_window_handle::HasDisplayHandle { + self + } + + fn rwh_06_window_handle(&self) -> &dyn raw_window_handle::HasWindowHandle { + self + } + + fn current_monitor(&self) -> Option { + todo!() + } + + fn available_monitors( + &self, + ) -> Box> { + todo!() + } + + fn has_focus(&self) -> bool { + todo!() + } + + fn set_ime_cursor_area( + &self, + position: winit::dpi::Position, + size: winit::dpi::Size, + ) { + todo!() + } + + fn set_ime_allowed(&self, allowed: bool) { + todo!() + } + + fn set_ime_purpose(&self, purpose: winit::window::ImePurpose) { + todo!() + } + + fn set_blur(&self, blur: bool) { + // TODO + } + + fn set_visible(&self, visible: bool) {} + + fn is_visible(&self) -> Option { + None + } + + fn set_resizable(&self, resizable: bool) {} + + fn is_resizable(&self) -> bool { + false + } + + fn set_enabled_buttons(&self, buttons: winit::window::WindowButtons) { + // TODO v5 of xdg_shell. + } + + fn enabled_buttons(&self) -> winit::window::WindowButtons { + WindowButtons::all() + } + + fn set_minimized(&self, minimized: bool) { + // XXX not applicable to the wrapped surfaces + } + + fn is_minimized(&self) -> Option { + // XXX clients don't know whether they are minimized or not. + None + } + + fn set_maximized(&self, maximized: bool) { + // XXX can't minimize the wrapped surfaces + } + + fn is_maximized(&self) -> bool { + // XXX can't maximize the wrapped surfaces + false + } + + fn set_fullscreen(&self, fullscreen: Option) { + // XXX can't fullscreen the wrapped surfaces + } + + fn fullscreen(&self) -> Option { + // XXX can't fullscreen the wrapped surfaces + None + } + + fn set_decorations(&self, decorations: bool) { + // XXX no decorations supported for the wrapped surfaces + } + + fn is_decorated(&self) -> bool { + false + } + + fn set_window_level(&self, level: winit::window::WindowLevel) {} + + fn set_window_icon(&self, window_icon: Option) {} + + fn focus_window(&self) {} + + fn request_user_attention( + &self, + request_type: Option, + ) { + // XXX can't request attention on wrapped surfaces + } + + fn set_theme(&self, theme: Option) {} + + fn theme(&self) -> Option { + None + } + + fn set_content_protected(&self, protected: bool) {} + + fn title(&self) -> String { + String::new() + } + + fn show_window_menu(&self, _position: winit::dpi::Position) { + // XXX can't show window menu on wrapped surfaces + } + + fn primary_monitor(&self) -> Option { + None + } + + fn surface_resize_increments( + &self, + ) -> Option> { + None + } + + fn drag_window(&self) -> Result<(), winit::error::RequestError> { + Ok(()) + } + + fn drag_resize_window( + &self, + _direction: winit::window::ResizeDirection, + ) -> Result<(), winit::error::RequestError> { + Ok(()) + } + + fn set_cursor_hittest( + &self, + _hittest: bool, + ) -> Result<(), winit::error::RequestError> { + todo!() + } + + fn inner_position( + &self, + ) -> Result, winit::error::RequestError> + { + Err(RequestError::NotSupported(NotSupportedError::new( + "Not supported on wayland.", + ))) + } + + fn outer_position( + &self, + ) -> Result, winit::error::RequestError> + { + Err(RequestError::NotSupported(NotSupportedError::new( + "Not supported on wayland.", + ))) + } + + fn set_cursor_position( + &self, + position: winit::dpi::Position, + ) -> Result<(), winit::error::RequestError> { + todo!() + } + + fn set_cursor_grab( + &self, + mode: winit::window::CursorGrabMode, + ) -> Result<(), winit::error::RequestError> { + todo!() + } +} + +impl raw_window_handle::HasWindowHandle for SctkWinitWindow { + fn window_handle( + &self, + ) -> Result + { + let raw = raw_window_handle::WaylandWindowHandle::new({ + let ptr = self.surface.wl_surface().id().as_ptr(); + let Some(ptr) = std::ptr::NonNull::new(ptr as *mut _) else { + return Err(HandleError::Unavailable); + }; + ptr + }); + + unsafe { Ok(raw_window_handle::WindowHandle::borrow_raw(raw.into())) } + } +} + +impl raw_window_handle::HasDisplayHandle for SctkWinitWindow { + fn display_handle( + &self, + ) -> Result< + raw_window_handle::DisplayHandle<'_>, + raw_window_handle::HandleError, + > { + let raw = raw_window_handle::WaylandDisplayHandle::new({ + let ptr = self.display.id().as_ptr(); + std::ptr::NonNull::new(ptr as *mut _) + .expect("wl_proxy should never be null") + }); + + unsafe { Ok(raw_window_handle::DisplayHandle::borrow_raw(raw.into())) } + } +} diff --git a/winit/src/program.rs b/winit/src/program.rs index 93b88c9f62..6a86252907 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -1,8 +1,11 @@ //! Create interactive, native cross-platform applications for WGPU. +#[path = "application/drag_resize.rs"] +mod drag_resize; mod state; mod window_manager; -pub use state::State; +pub use runtime::{default, Appearance, DefaultStyle}; +use winit::event_loop::OwnedDisplayHandle; use crate::conversion; use crate::core; @@ -10,8 +13,11 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::time::Instant; use crate::core::widget::operation; +use crate::core::widget::Operation; use crate::core::window; -use crate::core::{Color, Element, Point, Size, Theme}; +use crate::core::Clipboard as CoreClipboard; +use crate::core::Length; +use crate::core::{Element, Point, Size}; use crate::futures::futures::channel::mpsc; use crate::futures::futures::channel::oneshot; use crate::futures::futures::task; @@ -20,17 +26,28 @@ use crate::futures::subscription::{self, Subscription}; use crate::futures::{Executor, Runtime}; use crate::graphics; use crate::graphics::{compositor, Compositor}; +use crate::platform_specific; use crate::runtime::user_interface::{self, UserInterface}; use crate::runtime::Debug; use crate::runtime::{self, Action, Task}; use crate::{Clipboard, Error, Proxy, Settings}; +use dnd::DndSurface; +use dnd::Icon; +use iced_futures::core::widget::operation::search_id; +use iced_graphics::Viewport; +pub use state::State; +use window_clipboard::mime::ClipboardStoreData; +use winit::raw_window_handle::HasWindowHandle; -use window_manager::WindowManager; +pub(crate) use window_manager::WindowManager; use rustc_hash::FxHashMap; +use std::any::Any; use std::borrow::Cow; +use std::collections::HashMap; use std::mem::ManuallyDrop; use std::sync::Arc; +use std::time::Duration; /// An interactive, native, cross-platform, multi-windowed application. /// @@ -138,37 +155,6 @@ where } } -/// The appearance of a program. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Appearance { - /// The background [`Color`] of the application. - pub background_color: Color, - - /// The default text [`Color`] of the application. - pub text_color: Color, -} - -/// The default style of a [`Program`]. -pub trait DefaultStyle { - /// Returns the default style of a [`Program`]. - fn default_style(&self) -> Appearance; -} - -impl DefaultStyle for Theme { - fn default_style(&self) -> Appearance { - default(self) - } -} - -/// The default [`Appearance`] of a [`Program`] with the built-in [`Theme`]. -pub fn default(theme: &Theme) -> Appearance { - let palette = theme.extended_palette(); - - Appearance { - background_color: palette.background.base.color, - text_color: palette.background.base.text, - } -} /// Runs a [`Program`] with an executor, compositor, and the provided /// settings. pub fn run( @@ -187,11 +173,16 @@ where let mut debug = Debug::new(); debug.startup_started(); - let event_loop = EventLoop::with_user_event() - .build() - .expect("Create event loop"); + let event_loop = EventLoop::new().expect("Create event loop"); + #[cfg(feature = "wayland")] + let is_wayland = + winit::platform::wayland::EventLoopExtWayland::is_wayland(&event_loop); + #[cfg(not(feature = "wayland"))] + let is_wayland = false; - let (proxy, worker) = Proxy::new(event_loop.create_proxy()); + let (event_sender, event_receiver) = mpsc::unbounded(); + let (proxy, worker): (Proxy<

::Message>, _) = + Proxy::new(event_loop.create_proxy(), event_sender.clone()); let mut runtime = { let executor = @@ -223,7 +214,6 @@ where )); let (boot_sender, boot_receiver) = oneshot::channel(); - let (event_sender, event_receiver) = mpsc::unbounded(); let (control_sender, control_receiver) = mpsc::unbounded(); let instance = Box::pin(run_instance::( @@ -233,7 +223,8 @@ where debug, boot_receiver, event_receiver, - control_sender, + control_sender.clone(), + event_loop.owned_display_handle(), is_daemon, )); @@ -244,7 +235,7 @@ where context: task::Context<'static>, id: Option, boot: Option>, - sender: mpsc::UnboundedSender>>, + sender: mpsc::UnboundedSender>, receiver: mpsc::UnboundedReceiver, error: Option, @@ -258,6 +249,8 @@ where sender: oneshot::Sender>, fonts: Vec>, graphics_settings: graphics::Settings, + control_sender: mpsc::UnboundedSender, + is_wayland: bool, } let runner = Runner { @@ -268,6 +261,8 @@ where sender: boot_sender, fonts: settings.fonts, graphics_settings, + control_sender, + is_wayland, }), sender: event_sender, receiver: control_receiver, @@ -279,39 +274,102 @@ where canvas: None, }; - impl winit::application::ApplicationHandler> + impl winit::application::ApplicationHandler for Runner where Message: std::fmt::Debug, F: Future, C: Compositor + 'static, { - fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { + fn proxy_wake_up( + &mut self, + event_loop: &dyn winit::event_loop::ActiveEventLoop, + ) { + self.process_event(event_loop, None); + } + + fn new_events( + &mut self, + event_loop: &dyn winit::event_loop::ActiveEventLoop, + cause: winit::event::StartCause, + ) { + if self.boot.is_some() { + return; + } + self.process_event(event_loop, Some(Event::NewEvents(cause))); + } + + fn window_event( + &mut self, + event_loop: &dyn winit::event_loop::ActiveEventLoop, + window_id: winit::window::WindowId, + event: winit::event::WindowEvent, + ) { + #[cfg(target_os = "windows")] + let is_move_or_resize = matches!( + event, + winit::event::WindowEvent::Resized(_) + | winit::event::WindowEvent::Moved(_) + ); + + self.process_event( + event_loop, + Some(Event::Winit(window_id, event)), + ); + + // TODO: Remove when unnecessary + // On Windows, we emulate an `AboutToWait` event after every `Resized` event + // since the event loop does not resume during resize interaction. + // More details: https://github.com/rust-windowing/winit/issues/3272 + #[cfg(target_os = "windows")] + { + if is_move_or_resize { + self.process_event( + event_loop, + Event::EventLoopAwakened( + winit::event::Event::AboutToWait, + ), + ); + } + } + } + + fn about_to_wait( + &mut self, + event_loop: &dyn winit::event_loop::ActiveEventLoop, + ) { + self.process_event(event_loop, Some(Event::AboutToWait)); + } + + fn can_create_surfaces( + &mut self, + event_loop: &dyn winit::event_loop::ActiveEventLoop, + ) { + // create initial window let Some(BootConfig { sender, fonts, graphics_settings, + control_sender, + is_wayland, }) = self.boot.take() else { return; }; - let window = { - let attributes = winit::window::WindowAttributes::default(); - - #[cfg(target_os = "windows")] - let attributes = { - use winit::platform::windows::WindowAttributesExtWindows; - attributes.with_drag_and_drop(false) - }; - - match event_loop.create_window(attributes.with_visible(false)) { - Ok(window) => Arc::new(window), - Err(error) => { - self.error = Some(Error::WindowCreationFailed(error)); - event_loop.exit(); - return; - } + // XXX what to do if the program is a daemon? + // Can we avoid creating a useless window that hangs around forever? + + let window: Arc = match event_loop + .create_window( + winit::window::WindowAttributes::default() + .with_visible(false), + ) { + Ok(window) => Arc::from(window), + Err(error) => { + self.error = Some(Error::WindowCreationFailed(error)); + event_loop.exit(); + return; } }; @@ -330,7 +388,10 @@ where } sender - .send(Boot { compositor }) + .send(Boot { + compositor, + is_wayland, + }) .ok() .expect("Send boot event"); @@ -359,92 +420,6 @@ where .set_control_flow(winit::event_loop::ControlFlow::Poll); } } - - fn new_events( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - cause: winit::event::StartCause, - ) { - if self.boot.is_some() { - return; - } - - #[cfg(target_arch = "wasm32")] - if !*self.is_booted.borrow() { - return; - } - - self.process_event( - event_loop, - Event::EventLoopAwakened(winit::event::Event::NewEvents(cause)), - ); - } - - fn window_event( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - window_id: winit::window::WindowId, - event: winit::event::WindowEvent, - ) { - #[cfg(target_os = "windows")] - let is_move_or_resize = matches!( - event, - winit::event::WindowEvent::Resized(_) - | winit::event::WindowEvent::Moved(_) - ); - - self.process_event( - event_loop, - Event::EventLoopAwakened(winit::event::Event::WindowEvent { - window_id, - event, - }), - ); - - // TODO: Remove when unnecessary - // On Windows, we emulate an `AboutToWait` event after every `Resized` event - // since the event loop does not resume during resize interaction. - // More details: https://github.com/rust-windowing/winit/issues/3272 - #[cfg(target_os = "windows")] - { - if is_move_or_resize { - self.process_event( - event_loop, - Event::EventLoopAwakened( - winit::event::Event::AboutToWait, - ), - ); - } - } - } - - fn user_event( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - action: Action, - ) { - self.process_event( - event_loop, - Event::EventLoopAwakened(winit::event::Event::UserEvent( - action, - )), - ); - } - - fn about_to_wait( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - ) { - #[cfg(target_arch = "wasm32")] - if !*self.is_booted.borrow() { - return; - } - - self.process_event( - event_loop, - Event::EventLoopAwakened(winit::event::Event::AboutToWait), - ); - } } impl Runner @@ -454,14 +429,16 @@ where { fn process_event( &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - event: Event>, + event_loop: &dyn winit::event_loop::ActiveEventLoop, + event: Option>, ) { if event_loop.exiting() { return; } - self.sender.start_send(event).expect("Send event"); + if let Some(event) = event { + self.sender.start_send(event).expect("Send event"); + } loop { let poll = self.instance.as_mut().poll(&mut self.context); @@ -495,6 +472,7 @@ where } => { let exit_on_close_request = settings.exit_on_close_request; + let resize_border = settings.resize_border; let visible = settings.visible; @@ -521,9 +499,11 @@ where log::info!("Window attributes for id `{id:#?}`: {window_attributes:#?}"); - let window = event_loop - .create_window(window_attributes) - .expect("Create window"); + let window = Arc::from( + event_loop + .create_window(window_attributes) + .expect("Create window"), + ); #[cfg(target_arch = "wasm32")] { @@ -573,18 +553,56 @@ where self.process_event( event_loop, - Event::WindowCreated { + Some(Event::WindowCreated { id, window, exit_on_close_request, make_visible: visible, on_open, - }, + resize_border, + }), ); } Control::Exit => { event_loop.exit(); } + Control::Dnd(e) => { + self.sender.start_send(Event::Dnd(e)).unwrap(); + } + #[cfg(feature = "a11y")] + Control::Accessibility(id, event) => { + self.process_event( + event_loop, + Some(Event::Accessibility(id, event)), + ); + } + #[cfg(feature = "a11y")] + Control::AccessibilityEnabled(event) => { + self.process_event( + event_loop, + Some(Event::AccessibilityEnabled(event)), + ); + } + Control::PlatformSpecific(e) => { + self.sender + .start_send(Event::PlatformSpecific(e)) + .unwrap(); + } + Control::AboutToWait => { + self.sender + .start_send(Event::AboutToWait) + .expect("Send event"); + } + Control::Winit(id, e) => { + self.sender + .start_send(Event::Winit(id, e)) + .expect("Send event"); + } + Control::StartDnd => { + self.sender + .start_send(Event::StartDnd) + .expect("Send event"); + } }, _ => { break; @@ -618,22 +636,32 @@ where struct Boot { compositor: C, + is_wayland: bool, } -#[derive(Debug)] -enum Event { +pub(crate) enum Event { WindowCreated { id: window::Id, - window: winit::window::Window, + window: Arc, exit_on_close_request: bool, make_visible: bool, on_open: oneshot::Sender, + resize_border: u32, }, - EventLoopAwakened(winit::event::Event), + Dnd(dnd::DndEvent), + #[cfg(feature = "a11y")] + Accessibility(window::Id, iced_accessibility::accesskit::ActionRequest), + #[cfg(feature = "a11y")] + AccessibilityEnabled(bool), + Winit(winit::window::WindowId, winit::event::WindowEvent), + AboutToWait, + UserEvent(Action), + NewEvents(winit::event::StartCause), + PlatformSpecific(crate::platform_specific::Event), + StartDnd, } -#[derive(Debug)] -enum Control { +pub(crate) enum Control { ChangeFlow(winit::event_loop::ControlFlow), Exit, CreateWindow { @@ -643,16 +671,26 @@ enum Control { monitor: Option, on_open: oneshot::Sender, }, + Dnd(dnd::DndEvent), + #[cfg(feature = "a11y")] + Accessibility(window::Id, iced_accessibility::accesskit::ActionRequest), + #[cfg(feature = "a11y")] + AccessibilityEnabled(bool), + PlatformSpecific(crate::platform_specific::Event), + AboutToWait, + Winit(winit::window::WindowId, winit::event::WindowEvent), + StartDnd, } -async fn run_instance( +async fn run_instance<'a, P, C>( mut program: P, mut runtime: Runtime, Action>, mut proxy: Proxy, mut debug: Debug, boot: oneshot::Receiver>, - mut event_receiver: mpsc::UnboundedReceiver>>, + mut event_receiver: mpsc::UnboundedReceiver>, mut control_sender: mpsc::UnboundedSender, + display_handle: OwnedDisplayHandle, is_daemon: bool, ) where P: Program + 'static, @@ -662,7 +700,21 @@ async fn run_instance( use winit::event; use winit::event_loop::ControlFlow; - let Boot { mut compositor } = boot.await.expect("Receive boot"); + let Boot { + mut compositor, + is_wayland, + } = boot.await.expect("Receive boot"); + + let mut platform_specific_handler = + crate::platform_specific::PlatformSpecific::default(); + #[cfg(all(feature = "wayland", target_os = "linux"))] + if is_wayland { + platform_specific_handler = platform_specific_handler.with_wayland( + control_sender.clone(), + proxy.raw.clone(), + display_handle, + ); + } let mut window_manager = WindowManager::new(); let mut is_window_opening = !is_daemon; @@ -671,17 +723,76 @@ async fn run_instance( let mut messages = Vec::new(); let mut actions = 0; + #[cfg(feature = "a11y")] + let (mut adapters, mut a11y_enabled) = if let Some((main_id, title, raw)) = + window_manager.ids().next().and_then(|id| { + window_manager + .get(id) + .map(|w| (id, w.state.title.clone(), w.raw.clone())) + }) { + let node_id = core::id::window_node_id(); + use crate::a11y::*; + use iced_accessibility::accesskit::{ + ActivationHandler, NodeBuilder, NodeId, Role, Tree, TreeUpdate, + }; + use iced_accessibility::accesskit_winit::Adapter; + + let activation_handler = WinitActivationHandler { + proxy: control_sender.clone(), + title: title.clone(), + }; + + let action_handler = WinitActionHandler { + id: main_id, + proxy: control_sender.clone(), + }; + + let deactivation_handler = WinitDeactivationHandler { + proxy: control_sender.clone(), + }; + ( + HashMap::from([( + main_id, + ( + node_id, + Adapter::with_direct_handlers( + raw.as_ref(), + activation_handler, + action_handler, + deactivation_handler, + ), + ), + )]), + false, + ) + } else { + (Default::default(), false) + }; + let mut ui_caches = FxHashMap::default(); - let mut user_interfaces = ManuallyDrop::new(FxHashMap::default()); + let mut user_interfaces: ManuallyDrop< + HashMap< + window::Id, + UserInterface< + '_, +

::Message, +

::Theme, +

::Renderer, + >, + rustc_hash::FxBuildHasher, + >, + > = ManuallyDrop::new(FxHashMap::default()); let mut clipboard = Clipboard::unconnected(); - debug.startup_finished(); + let mut cur_dnd_surface: Option = None; + debug.startup_finished(); loop { // Empty the queue if possible let event = if let Ok(event) = event_receiver.try_next() { event } else { + platform_specific_handler.send_ready(); event_receiver.next().await }; @@ -690,20 +801,254 @@ async fn run_instance( }; match event { + Event::StartDnd => { + let queued = clipboard.get_queued(); + for crate::clipboard::StartDnd { + internal, + source_surface, + icon_surface, + content, + actions, + } in queued + { + let Some(window_id) = source_surface.and_then(|source| { + match source { + core::clipboard::DndSource::Surface(s) => Some(s), + core::clipboard::DndSource::Widget(w) => { + // search windows for widget with operation + user_interfaces.iter_mut().find_map( + |(ui_id, ui)| { + let Some(ui_renderer) = window_manager + .get_mut(ui_id.clone()) + .map(|w| &w.renderer) + else { + return None; + }; + + let operation: Box> = + Box::new(operation::map( + Box::new(search_id::search_id( + w.clone(), + )), + |_| {}, + )); + let mut current_operation = + Some(operation); + + while let Some(mut operation) = + current_operation.take() + { + ui.operate( + ui_renderer, + operation.as_mut(), + ); + + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some( + (), + ) => { + return Some(ui_id.clone()); + } + operation::Outcome::Chain( + next, + ) => { + current_operation = + Some(next); + } + } + } + None + }, + ) + } + } + }) else { + eprintln!("No source surface"); + continue; + }; + + let Some(window) = window_manager.get_mut(window_id) else { + eprintln!("No window"); + continue; + }; + + let state = &window.state; + let icon_surface = icon_surface + .map(|i| { + let i: Box = i; + i + }) + .map(|i| { + i.downcast::, + core::widget::tree::State, + )>>() + .unwrap() + }) + .map( + |e: Box< + Arc<( + core::Element< + 'static, + (), + P::Theme, + P::Renderer, + >, + core::widget::tree::State, + )>, + >| { + let mut renderer = compositor.create_renderer(); + + let e = Arc::into_inner(*e).unwrap(); + let (mut e, widget_state) = e; + let lim = core::layout::Limits::new( + Size::new(1., 1.), + Size::new( + state.viewport().physical_width() + as f32, + state.viewport().physical_height() + as f32, + ), + ); + + let mut tree = core::widget::Tree { + id: e.as_widget().id(), + tag: e.as_widget().tag(), + state: widget_state, + children: e.as_widget().children(), + }; + + let size = e + .as_widget() + .layout(&mut tree, &renderer, &lim); + e.as_widget_mut().diff(&mut tree); + + let size = lim.resolve( + Length::Shrink, + Length::Shrink, + size.size(), + ); + let viewport = Viewport::with_logical_size( + size, + state.viewport().scale_factor(), + ); + let mut surface = compositor.create_surface( + window.raw.clone(), + viewport.physical_width(), + viewport.physical_height(), + ); + + let mut ui = UserInterface::build( + e, + size, + user_interface::Cache::default(), + &mut renderer, + ); + _ = ui.draw( + &mut renderer, + state.theme(), + &renderer::Style { + icon_color: state.icon_color(), + text_color: state.text_color(), + scale_factor: state.scale_factor(), + }, + Default::default(), + ); + let mut bytes = compositor.screenshot( + &mut renderer, + &mut surface, + &viewport, + core::Color::TRANSPARENT, + &debug.overlay(), + ); + for pix in bytes.chunks_exact_mut(4) { + // rgba -> argb little endian + pix.swap(0, 2); + } + Icon::Buffer { + data: Arc::new(bytes), + width: viewport.physical_width(), + height: viewport.physical_height(), + transparent: true, + } + }, + ); + + clipboard.start_dnd_winit( + internal, + DndSurface(Arc::new(Box::new(window.raw.clone()))), + icon_surface, + content, + actions, + ); + } + } Event::WindowCreated { id, window, exit_on_close_request, make_visible, on_open, + resize_border, } => { let window = window_manager.insert( id, - Arc::new(window), + window, &program, &mut compositor, exit_on_close_request, + resize_border, ); + #[cfg(feature = "wayland")] + platform_specific_handler.send_wayland( + platform_specific::Action::TrackWindow( + window.raw.clone(), + id, + ), + ); + #[cfg(feature = "a11y")] + { + use crate::a11y::*; + use iced_accessibility::accesskit::{ + ActivationHandler, NodeBuilder, NodeId, Role, Tree, + TreeUpdate, + }; + use iced_accessibility::accesskit_winit::Adapter; + + let node_id = core::id::window_node_id(); + + let activation_handler = WinitActivationHandler { + proxy: control_sender.clone(), + title: window.state.title.clone(), + }; + + let action_handler = WinitActionHandler { + id, + proxy: control_sender.clone(), + }; + + let deactivation_handler = WinitDeactivationHandler { + proxy: control_sender.clone(), + }; + _ = adapters.insert( + id, + ( + node_id, + Adapter::with_direct_handlers( + window.raw.as_ref(), + activation_handler, + action_handler, + deactivation_handler, + ), + ), + ); + } let logical_size = window.state.logical_size(); @@ -716,6 +1061,9 @@ async fn run_instance( logical_size, &mut debug, id, + window.raw.clone(), + window.prev_dnd_destination_rectangles_count, + &mut clipboard, ), ); let _ = ui_caches.insert(id, user_interface::Cache::default()); @@ -725,7 +1073,7 @@ async fn run_instance( } events.push(( - id, + Some(id), core::Event::Window(window::Event::Opened { position: window.position(), size: window.size(), @@ -733,46 +1081,53 @@ async fn run_instance( )); if clipboard.window_id().is_none() { - clipboard = Clipboard::connect(window.raw.clone()); + clipboard = Clipboard::connect( + window.raw.clone(), + crate::clipboard::ControlSender { + sender: control_sender.clone(), + proxy: proxy.raw.clone(), + }, + ); } let _ = on_open.send(id); is_window_opening = false; } - Event::EventLoopAwakened(event) => { + Event::UserEvent(action) => { + run_action( + action, + &program, + &mut compositor, + &mut events, + &mut messages, + &mut clipboard, + &mut control_sender, + &mut debug, + &mut user_interfaces, + &mut window_manager, + &mut ui_caches, + &mut is_window_opening, + &mut platform_specific_handler, + ); + actions += 1; + } + Event::NewEvents( + event::StartCause::Init + | event::StartCause::ResumeTimeReached { .. }, + ) => { + if window_manager.ids().next().is_none() { + _ = control_sender + .start_send(Control::ChangeFlow(ControlFlow::Wait)); + } + for (_id, window) in window_manager.iter_mut() { + window.request_redraw(); + } + } + Event::Winit(window_id, event) => { match event { - event::Event::NewEvents( - event::StartCause::Init - | event::StartCause::ResumeTimeReached { .. }, - ) => { - for (_id, window) in window_manager.iter_mut() { - window.raw.request_redraw(); - } - } - event::Event::UserEvent(action) => { - run_action( - action, - &program, - &mut compositor, - &mut events, - &mut messages, - &mut clipboard, - &mut control_sender, - &mut debug, - &mut user_interfaces, - &mut window_manager, - &mut ui_caches, - &mut is_window_opening, - ); - actions += 1; - } - event::Event::WindowEvent { - window_id: id, - event: event::WindowEvent::RedrawRequested, - .. - } => { + event::WindowEvent::RedrawRequested => { let Some((id, window)) = - window_manager.get_mut_alias(id) + window_manager.get_mut_alias(window_id) else { continue; }; @@ -805,18 +1160,32 @@ async fn run_instance( &mut window.renderer, window.state.theme(), &renderer::Style { + icon_color: window.state.icon_color(), text_color: window.state.text_color(), + scale_factor: window.state.scale_factor(), }, cursor, ); + platform_specific_handler + .update_subsurfaces(id, window.raw.as_ref()); debug.draw_finished(); if new_mouse_interaction != window.mouse_interaction { - window.raw.set_cursor( + if let Some(interaction) = conversion::mouse_interaction( new_mouse_interaction, - ), - ); + ) + { + if matches!( + window.mouse_interaction, + mouse::Interaction::Hide + ) { + window.raw.set_cursor_visible(true); + } + window.raw.set_cursor(interaction.into()) + } else { + window.raw.set_cursor_visible(false); + } window.mouse_interaction = new_mouse_interaction; } @@ -827,13 +1196,13 @@ async fn run_instance( status: core::event::Status::Ignored, }); - let _ = control_sender.start_send(Control::ChangeFlow( - match ui_state { + if control_sender + .start_send(Control::ChangeFlow(match ui_state { user_interface::State::Updated { redraw_request: Some(redraw_request), } => match redraw_request { window::RedrawRequest::NextFrame => { - window.raw.request_redraw(); + window.request_redraw(); ControlFlow::Wait } @@ -842,30 +1211,121 @@ async fn run_instance( } }, _ => ControlFlow::Wait, - }, - )); + })) + .is_err() + { + panic!("send error"); + } let physical_size = window.state.physical_size(); - if physical_size.width == 0 || physical_size.height == 0 { continue; } - if window.viewport_version != window.state.viewport_version() { let logical_size = window.state.logical_size(); - debug.layout_started(); - let ui = user_interfaces + let mut ui = user_interfaces .remove(&id) - .expect("Remove user interface"); + .expect("Remove user interface") + .relayout(logical_size, &mut window.renderer); - let _ = user_interfaces.insert( - id, - ui.relayout(logical_size, &mut window.renderer), - ); + #[cfg(feature = "a11y")] + { + use iced_accessibility::{ + accesskit::{ + NodeBuilder, NodeId, Role, Tree, + TreeUpdate, + }, + A11yId, A11yNode, A11yTree, + }; + if let Some(Some((a11y_id, adapter))) = + a11y_enabled.then(|| adapters.get_mut(&id)) + { + // TODO cleanup duplication + let child_tree = + ui.a11y_nodes(window.state.cursor()); + let mut root = + NodeBuilder::new(Role::Window); + root.set_name( + window.state.title.to_string(), + ); + let window_tree = + A11yTree::node_with_child_tree( + A11yNode::new(root, *a11y_id), + child_tree, + ); + let tree = Tree::new(NodeId(*a11y_id)); + + let focus = + Arc::new(std::sync::Mutex::new(None)); + let focus_clone = focus.clone(); + let operation: Box> = + Box::new(operation::map( + Box::new( + operation::focusable::find_focused( + ), + ), + move |id| { + let mut guard = focus.lock().unwrap(); + _ = guard.replace(id); + }, + )); + let mut current_operation = Some(operation); + + while let Some(mut operation) = + current_operation.take() + { + ui.operate( + &window.renderer, + operation.as_mut(), + ); + + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(()) => { + break; + } + operation::Outcome::Chain(next) => { + current_operation = Some(next); + } + } + } + let mut guard = focus_clone.lock().unwrap(); + let focus = guard + .take() + .map(|id| A11yId::Widget(id)); + tracing::debug!( + "focus: {:?}\ntree root: {:?}\n children: {:?}", + &focus, + window_tree + .root() + .iter() + .map(|n| (n.node().role(), n.id())) + .collect::>(), + window_tree + .children() + .iter() + .map(|n| (n.node().role(), n.id())) + .collect::>() + ); + let focus = focus + .filter(|f_id| { + window_tree.contains(f_id) + }) + .map(|id| id.into()) + .unwrap_or_else(|| tree.root); + adapter.update_if_active(|| TreeUpdate { + nodes: window_tree.into(), + tree: Some(tree), + focus, + }); + } + } + + let _ = user_interfaces.insert(id, ui); debug.layout_finished(); debug.draw_started(); @@ -876,24 +1336,39 @@ async fn run_instance( &mut window.renderer, window.state.theme(), &renderer::Style { + icon_color: window.state.icon_color(), text_color: window.state.text_color(), + scale_factor: window + .state + .scale_factor(), }, window.state.cursor(), ); + platform_specific_handler + .update_subsurfaces(id, window.raw.as_ref()); debug.draw_finished(); if new_mouse_interaction != window.mouse_interaction { - window.raw.set_cursor( + if let Some(interaction) = conversion::mouse_interaction( new_mouse_interaction, - ), - ); + ) + { + if matches!( + window.mouse_interaction, + mouse::Interaction::Hide + ) { + window.raw.set_cursor_visible(true); + } + window.raw.set_cursor(interaction.into()) + } else { + window.raw.set_cursor_visible(false); + } window.mouse_interaction = new_mouse_interaction; } - compositor.configure_surface( &mut window.surface, physical_size.width, @@ -904,6 +1379,7 @@ async fn run_instance( window.state.viewport_version(); } + window.raw.pre_present_notify(); debug.render_started(); match compositor.present( &mut window.renderer, @@ -915,33 +1391,45 @@ async fn run_instance( Ok(()) => { debug.render_finished(); } - Err(error) => match error { - // This is an unrecoverable error. - compositor::SurfaceError::OutOfMemory => { - panic!("{:?}", error); - } - _ => { - debug.render_finished(); - - log::error!( - "Error {error:?} when \ + Err(error) => { + match error { + // This is an unrecoverable error. + compositor::SurfaceError::OutOfMemory => { + panic!("{:?}", error); + } + compositor::SurfaceError::NoDamage => { + debug.render_finished(); + + // TODO Ideally there would be a way to know if some widget wants to animate? + let _ = control_sender.start_send( + Control::ChangeFlow( + ControlFlow::WaitUntil( + Instant::now().checked_add( + Duration::from_millis(100), + ).unwrap_or(Instant::now()), + ), + ), + ); + } + _ => { + debug.render_finished(); + log::error!( + "Error {error:?} when \ presenting surface." - ); + ); - // Try rendering all windows again next frame. - for (_id, window) in - window_manager.iter_mut() - { - window.raw.request_redraw(); + // Try rendering all windows again next frame. + for (_id, window) in + window_manager.iter_mut() + { + window.request_redraw(); + } } } - }, + } } } - event::Event::WindowEvent { - event: window_event, - window_id, - } => { + window_event => { if !is_daemon && matches!( window_event, @@ -963,6 +1451,15 @@ async fn run_instance( continue; }; + // Initiates a drag resize window state when found. + if let Some(func) = + window.drag_resize_window_func.as_mut() + { + if func(window.raw.as_ref(), &window_event) { + continue; + } + } + if matches!( window_event, winit::event::WindowEvent::CloseRequested @@ -983,135 +1480,346 @@ async fn run_instance( &mut window_manager, &mut ui_caches, &mut is_window_opening, + &mut platform_specific_handler, ); } else { window.state.update( - &window.raw, + window.raw.as_ref(), &window_event, &mut debug, ); - if let Some(event) = conversion::window_event( window_event, window.state.scale_factor(), window.state.modifiers(), ) { - events.push((id, event)); + events.push((Some(id), event)); } } } - event::Event::AboutToWait => { - if events.is_empty() && messages.is_empty() { - continue; + _ => {} + } + } + Event::AboutToWait => { + let skip = events.is_empty() && messages.is_empty(); + if skip + && window_manager.iter_mut().all(|(_, w)| !w.resize_enabled) + { + continue; + } + + debug.event_processing_started(); + let mut uis_stale = false; + let mut resized = false; + for (id, window) in window_manager.iter_mut() { + if skip && !window.resize_enabled { + continue; + } + let mut window_events = vec![]; + + events.retain(|(window_id, event)| { + if *window_id == Some(id) { + window_events.push(event.clone()); + false + } else { + true } + }); + let no_window_events = window_events.is_empty(); + #[cfg(feature = "wayland")] + window_events.push(core::Event::PlatformSpecific( + core::event::PlatformSpecific::Wayland( + core::event::wayland::Event::RequestResize, + ), + )); + let (ui_state, statuses) = user_interfaces + .get_mut(&id) + .expect("Get user interface") + .update( + &window_events, + window.state.cursor(), + &mut window.renderer, + &mut clipboard, + &mut messages, + ); - debug.event_processing_started(); - let mut uis_stale = false; + let mut needs_redraw = + !no_window_events || !messages.is_empty(); - for (id, window) in window_manager.iter_mut() { - let mut window_events = vec![]; + if let Some(requested_size) = + clipboard.requested_logical_size.lock().unwrap().take() + { + let requested_physical_size = + winit::dpi::PhysicalSize::new( + (requested_size.width as f64 + * window.state.scale_factor()) + .ceil() as u32, + (requested_size.height as f64 + * window.state.scale_factor()) + .ceil() as u32, + ); + let physical_size = window.state.physical_size(); + if requested_physical_size.width != physical_size.width + || requested_physical_size.height + != physical_size.height + { + // FIXME what to do when we are stuck in a configure event/resize request loop + // We don't have control over how winit handles this. + window.resize_enabled = true; + resized = true; + needs_redraw = true; + let s = winit::dpi::Size::Physical( + requested_physical_size, + ); + _ = window.raw.request_surface_size(s); + window.raw.set_min_surface_size(Some(s)); + window.raw.set_max_surface_size(Some(s)); + window.state.synchronize( + &program, + id, + window.raw.as_ref(), + ); + } + } + if needs_redraw { + window.request_redraw(); + } else { + continue; + } - events.retain(|(window_id, event)| { - if *window_id == id { - window_events.push(event.clone()); - false - } else { - true - } - }); + if !uis_stale { + uis_stale = + matches!(ui_state, user_interface::State::Outdated); + } - if window_events.is_empty() && messages.is_empty() { - continue; - } + for (event, status) in + window_events.into_iter().zip(statuses.into_iter()) + { + runtime.broadcast(subscription::Event::Interaction { + window: id, + event, + status, + }); + } + } - let (ui_state, statuses) = user_interfaces - .get_mut(&id) - .expect("Get user interface") - .update( - &window_events, - window.state.cursor(), - &mut window.renderer, - &mut clipboard, - &mut messages, - ); + if !resized && skip { + continue; + } - window.raw.request_redraw(); + for (id, event) in events.drain(..) { + runtime.broadcast(subscription::Event::Interaction { + window: id.unwrap_or(window::Id::NONE), + event, + status: core::event::Status::Ignored, + }); + } - if !uis_stale { - uis_stale = matches!( - ui_state, - user_interface::State::Outdated - ); - } + debug.event_processing_finished(); - for (event, status) in window_events - .into_iter() - .zip(statuses.into_iter()) - { - runtime.broadcast( - subscription::Event::Interaction { - window: id, - event, - status, - }, - ); - } - } + if !messages.is_empty() || uis_stale { + let cached_interfaces: FxHashMap< + window::Id, + user_interface::Cache, + > = ManuallyDrop::into_inner(user_interfaces) + .drain() + .map(|(id, ui)| (id, ui.into_cache())) + .collect(); - for (id, event) in events.drain(..) { - runtime.broadcast( - subscription::Event::Interaction { - window: id, - event, - status: core::event::Status::Ignored, - }, - ); - } + update( + &mut program, + &mut runtime, + &mut debug, + &mut messages, + ); - debug.event_processing_finished(); + for (id, window) in window_manager.iter_mut() { + window.state.synchronize( + &program, + id, + window.raw.as_ref(), + ); - if !messages.is_empty() || uis_stale { - let cached_interfaces: FxHashMap< - window::Id, - user_interface::Cache, - > = ManuallyDrop::into_inner(user_interfaces) - .drain() - .map(|(id, ui)| (id, ui.into_cache())) - .collect(); + window.request_redraw(); + } - update( - &mut program, - &mut runtime, - &mut debug, - &mut messages, - ); + user_interfaces = ManuallyDrop::new(build_user_interfaces( + &program, + &mut debug, + &mut window_manager, + cached_interfaces, + &mut clipboard, + )); - for (id, window) in window_manager.iter_mut() { - window.state.synchronize( - &program, - id, - &window.raw, - ); + if actions > 0 { + proxy.free_slots(actions); + actions = 0; + } + } - window.raw.request_redraw(); - } + debug.draw_started(); + + for (id, window) in window_manager.iter_mut() { + // TODO: Avoid redrawing all the time by forcing widgets to + // request redraws on state changes + // + // Then, we can use the `interface_state` here to decide if a redraw + // is needed right away, or simply wait until a specific time. + let redraw_event = core::Event::Window( + window::Event::RedrawRequested(Instant::now()), + ); + + let cursor = window.state.cursor(); - user_interfaces = - ManuallyDrop::new(build_user_interfaces( - &program, - &mut debug, - &mut window_manager, - cached_interfaces, - )); - - if actions > 0 { - proxy.free_slots(actions); - actions = 0; + let ui = user_interfaces + .get_mut(&id) + .expect("Get user interface"); + + let (ui_state, _) = ui.update( + &[redraw_event.clone()], + cursor, + &mut window.renderer, + &mut clipboard, + &mut messages, + ); + + let new_mouse_interaction = { + let state = &window.state; + + ui.draw( + &mut window.renderer, + state.theme(), + &renderer::Style { + icon_color: state.icon_color(), + text_color: state.text_color(), + scale_factor: state.scale_factor(), + }, + cursor, + ) + }; + + if new_mouse_interaction != window.mouse_interaction { + if let Some(interaction) = + conversion::mouse_interaction(new_mouse_interaction) + { + if matches!( + window.mouse_interaction, + mouse::Interaction::Hide + ) { + window.raw.set_cursor_visible(true); } + window.raw.set_cursor(interaction.into()) + } else { + window.raw.set_cursor_visible(false); + } + + window.mouse_interaction = new_mouse_interaction; + } + + // TODO once widgets can request to be redrawn, we can avoid always requesting a + // redraw + window.request_redraw(); + runtime.broadcast(subscription::Event::Interaction { + window: id, + event: redraw_event, + status: core::event::Status::Ignored, + }); + + let _ = control_sender.start_send(Control::ChangeFlow( + match ui_state { + user_interface::State::Updated { + redraw_request: Some(redraw_request), + } => match redraw_request { + window::RedrawRequest::NextFrame => { + window.request_redraw(); + + ControlFlow::Wait + } + window::RedrawRequest::At(at) => { + ControlFlow::WaitUntil(at) + } + }, + _ => ControlFlow::Wait, + }, + )); + } + + debug.draw_finished(); + } + + Event::Dnd(e) => { + match &e { + dnd::DndEvent::Offer(_, dnd::OfferEvent::Leave) => { + events.push((cur_dnd_surface, core::Event::Dnd(e))); + // XXX can't clear the dnd surface on leave because + // the data event comes after + // cur_dnd_surface = None; + } + dnd::DndEvent::Offer( + _, + dnd::OfferEvent::Enter { surface, .. }, + ) => { + let window_handle = surface.0.window_handle().ok(); + let window_id = window_manager.iter_mut().find_map( + |(id, window)| { + if window + .raw + .window_handle() + .ok() + .zip(window_handle) + .map(|(a, b)| a == b) + .unwrap_or_default() + { + Some(id) + } else { + None + } + }, + ); + cur_dnd_surface = window_id; + events.push((cur_dnd_surface, core::Event::Dnd(e))); + } + dnd::DndEvent::Offer(..) => { + events.push((cur_dnd_surface, core::Event::Dnd(e))); + } + dnd::DndEvent::Source(_) => { + for w in window_manager.ids() { + events.push((Some(w), core::Event::Dnd(e.clone()))); } } + }; + } + #[cfg(feature = "a11y")] + Event::Accessibility(id, e) => { + match e.action { + iced_accessibility::accesskit::Action::Focus => { + // TODO send a command for this + } _ => {} } + events.push((Some(id), conversion::a11y(e))); + } + #[cfg(feature = "a11y")] + Event::AccessibilityEnabled(enabled) => { + a11y_enabled = enabled; + } + Event::PlatformSpecific(e) => { + crate::platform_specific::handle_event( + e, + &mut events, + &mut platform_specific_handler, + &program, + &mut compositor, + &mut window_manager, + &mut debug, + &mut user_interfaces, + &mut clipboard, + #[cfg(feature = "a11y")] + &mut adapters, + ); + } + _ => { + // log ignored events? } } } @@ -1120,13 +1828,16 @@ async fn run_instance( } /// Builds a window's [`UserInterface`] for the [`Program`]. -fn build_user_interface<'a, P: Program>( +pub(crate) fn build_user_interface<'a, P: Program>( program: &'a P, cache: user_interface::Cache, renderer: &mut P::Renderer, size: Size, debug: &mut Debug, id: window::Id, + raw: Arc, + prev_dnd_destination_rectangles_count: usize, + clipboard: &mut Clipboard, ) -> UserInterface<'a, P::Message, P::Theme, P::Renderer> where P::Theme: DefaultStyle, @@ -1139,6 +1850,17 @@ where let user_interface = UserInterface::build(view, size, cache, renderer); debug.layout_finished(); + let dnd_rectangles = user_interface + .dnd_rectangles(prev_dnd_destination_rectangles_count, renderer); + let new_dnd_rectangles_count = dnd_rectangles.as_ref().len(); + if new_dnd_rectangles_count > 0 || prev_dnd_destination_rectangles_count > 0 + { + clipboard.register_dnd_destination( + DndSurface(Arc::new(Box::new(raw.clone()))), + dnd_rectangles.into_rectangles(), + ); + } + user_interface } @@ -1170,7 +1892,7 @@ fn run_action( action: Action, program: &P, compositor: &mut C, - events: &mut Vec<(window::Id, core::Event)>, + events: &mut Vec<(Option, core::Event)>, messages: &mut Vec, clipboard: &mut Clipboard, control_sender: &mut mpsc::UnboundedSender, @@ -1182,6 +1904,7 @@ fn run_action( window_manager: &mut WindowManager, ui_caches: &mut FxHashMap, is_window_opening: &mut bool, + platform_specific: &mut crate::platform_specific::PlatformSpecific, ) where P: Program, C: Compositor + 'static, @@ -1202,6 +1925,13 @@ fn run_action( clipboard::Action::Write { target, contents } => { clipboard.write(target, contents); } + clipboard::Action::WriteData(contents, kind) => { + clipboard.write_data(kind, ClipboardStoreData(contents)) + } + clipboard::Action::ReadData(allowed, tx, kind) => { + let contents = clipboard.read_data(kind, allowed); + _ = tx.send(contents); + } }, Action::Window(action) => match action { window::Action::Open(id, settings, channel) => { @@ -1222,18 +1952,35 @@ fn run_action( window::Action::Close(id) => { let _ = ui_caches.remove(&id); let _ = interfaces.remove(&id); + #[cfg(feature = "wayland")] + platform_specific + .send_wayland(platform_specific::Action::RemoveWindow(id)); if let Some(window) = window_manager.remove(id) { + clipboard.register_dnd_destination( + DndSurface(Arc::new(Box::new(window.raw.clone()))), + Vec::new(), + ); + let proxy = clipboard.proxy(); if clipboard.window_id() == Some(window.raw.id()) { *clipboard = window_manager .first() .map(|window| window.raw.clone()) - .map(Clipboard::connect) + .zip(proxy) + .map(|(w, proxy)| { + Clipboard::connect( + w, + crate::clipboard::ControlSender { + sender: control_sender.clone(), + proxy, + }, + ) + }) .unwrap_or_else(Clipboard::unconnected); } events.push(( - id, + Some(id), core::Event::Window(core::window::Event::Closed), )); } @@ -1257,11 +2004,12 @@ fn run_action( } window::Action::Resize(id, size) => { if let Some(window) = window_manager.get_mut(id) { - let _ = window.raw.request_inner_size( + let _ = window.raw.request_surface_size( winit::dpi::LogicalSize { width: size.width, height: size.height, - }, + } + .into(), ); } } @@ -1269,7 +2017,7 @@ fn run_action( if let Some(window) = window_manager.get_mut(id) { let size = window .raw - .inner_size() + .surface_size() .to_logical(window.raw.scale_factor()); let _ = channel.send(Size::new(size.width, size.height)); @@ -1324,7 +2072,8 @@ fn run_action( winit::dpi::LogicalPosition { x: position.x, y: position.y, - }, + } + .into(), ); } } @@ -1391,7 +2140,8 @@ fn run_action( winit::dpi::LogicalPosition { x: point.x, y: point.y, - }, + } + .into(), ); } } @@ -1484,6 +2234,42 @@ fn run_action( .start_send(Control::Exit) .expect("Send control action"); } + Action::Dnd(a) => match a { + iced_runtime::dnd::DndAction::RegisterDndDestination { + surface, + rectangles, + } => { + clipboard.register_dnd_destination(surface, rectangles); + } + iced_runtime::dnd::DndAction::StartDnd { + internal, + source_surface, + icon_surface, + content, + actions, + } => { + clipboard.start_dnd( + internal, + source_surface, + icon_surface.map(|d| d as Box), + content, + actions, + ); + } + iced_runtime::dnd::DndAction::EndDnd => { + clipboard.end_dnd(); + } + iced_runtime::dnd::DndAction::PeekDnd(m, channel) => { + let data = clipboard.peek_dnd(m); + _ = channel.send(data); + } + iced_runtime::dnd::DndAction::SetAction(a) => { + clipboard.set_action(a); + } + }, + Action::PlatformSpecific(a) => { + platform_specific.send_action(a); + } } } @@ -1493,6 +2279,7 @@ pub fn build_user_interfaces<'a, P: Program, C>( debug: &mut Debug, window_manager: &mut WindowManager, mut cached_user_interfaces: FxHashMap, + clipboard: &mut Clipboard, ) -> FxHashMap> where C: Compositor, @@ -1502,18 +2289,36 @@ where .drain() .filter_map(|(id, cache)| { let window = window_manager.get_mut(id)?; - - Some(( + let interface = build_user_interface( + program, + cache, + &mut window.renderer, + window.state.logical_size(), + debug, id, - build_user_interface( - program, - cache, - &mut window.renderer, - window.state.logical_size(), - debug, - id, - ), - )) + window.raw.clone(), + window.prev_dnd_destination_rectangles_count, + clipboard, + ); + + let dnd_rectangles = interface.dnd_rectangles( + window.prev_dnd_destination_rectangles_count, + &window.renderer, + ); + let new_dnd_rectangles_count = dnd_rectangles.as_ref().len(); + if new_dnd_rectangles_count > 0 + || window.prev_dnd_destination_rectangles_count > 0 + { + clipboard.register_dnd_destination( + DndSurface(Arc::new(Box::new(window.raw.clone()))), + dnd_rectangles.into_rectangles(), + ); + } + + window.prev_dnd_destination_rectangles_count = + new_dnd_rectangles_count; + + Some((id, interface)) }) .collect() } diff --git a/winit/src/program/state.rs b/winit/src/program/state.rs index a7fa2788d9..5c7838c2e4 100644 --- a/winit/src/program/state.rs +++ b/winit/src/program/state.rs @@ -5,6 +5,7 @@ use crate::graphics::Viewport; use crate::program::{self, Program}; use std::fmt::{Debug, Formatter}; +use winit::dpi::LogicalPosition; use winit::event::{Touch, WindowEvent}; use winit::window::Window; @@ -13,7 +14,7 @@ pub struct State where P::Theme: program::DefaultStyle, { - title: String, + pub(crate) title: String, scale_factor: f64, viewport: Viewport, viewport_version: u64, @@ -47,7 +48,7 @@ where pub fn new( application: &P, window_id: window::Id, - window: &Window, + window: &dyn Window, ) -> Self { let title = application.title(window_id); let scale_factor = application.scale_factor(window_id); @@ -55,7 +56,7 @@ where let appearance = application.style(&theme); let viewport = { - let physical_size = window.inner_size(); + let physical_size = window.surface_size(); Viewport::with_physical_size( Size::new(physical_size.width, physical_size.height), @@ -102,6 +103,11 @@ where self.viewport.scale_factor() } + pub fn set_logical_cursor_pos(&mut self, pos: LogicalPosition) { + let physical = pos.to_physical(self.scale_factor()); + self.cursor_position = Some(physical); + } + /// Returns the current cursor position of the [`State`]. pub fn cursor(&self) -> mouse::Cursor { self.cursor_position @@ -135,15 +141,32 @@ where self.appearance.text_color } + /// Returns the current icon [`Color`] of the [`State`]. + pub fn icon_color(&self) -> Color { + self.appearance.icon_color + } + + /// Update the scale factor + pub(crate) fn update_scale_factor(&mut self, new_scale_factor: f64) { + let size = self.viewport.physical_size(); + + self.viewport = Viewport::with_physical_size( + size, + new_scale_factor * self.scale_factor, + ); + + self.viewport_version = self.viewport_version.wrapping_add(1); + } + /// Processes the provided window event and updates the [`State`] accordingly. pub fn update( &mut self, - window: &Window, + window: &dyn Window, event: &WindowEvent, _debug: &mut crate::runtime::Debug, ) { match event { - WindowEvent::Resized(new_size) => { + WindowEvent::SurfaceResized(new_size) => { let size = Size::new(new_size.width, new_size.height); self.viewport = Viewport::with_physical_size( @@ -157,14 +180,7 @@ where scale_factor: new_scale_factor, .. } => { - let size = self.viewport.physical_size(); - - self.viewport = Viewport::with_physical_size( - size, - new_scale_factor * self.scale_factor, - ); - - self.viewport_version = self.viewport_version.wrapping_add(1); + self.update_scale_factor(*new_scale_factor); } WindowEvent::CursorMoved { position, .. } | WindowEvent::Touch(Touch { @@ -204,7 +220,7 @@ where &mut self, application: &P, window_id: window::Id, - window: &Window, + window: &dyn Window, ) { // Update window title let new_title = application.title(window_id); @@ -216,13 +232,19 @@ where // Update scale factor and size let new_scale_factor = application.scale_factor(window_id); - let new_size = window.inner_size(); + let mut new_size = window.surface_size(); let current_size = self.viewport.physical_size(); - if self.scale_factor != new_scale_factor || (current_size.width, current_size.height) != (new_size.width, new_size.height) + && !(new_size.width == 0 && new_size.height == 0) { + if new_size.width == 0 { + new_size.width = current_size.width; + } + if new_size.height == 0 { + new_size.height = current_size.height; + } self.viewport = Viewport::with_physical_size( Size::new(new_size.width, new_size.height), window.scale_factor() * new_scale_factor, diff --git a/winit/src/program/window_manager.rs b/winit/src/program/window_manager.rs index 3d22e15509..5ffc5ea5c1 100644 --- a/winit/src/program/window_manager.rs +++ b/winit/src/program/window_manager.rs @@ -35,12 +35,13 @@ where pub fn insert( &mut self, id: Id, - window: Arc, + window: Arc, application: &P, compositor: &mut C, exit_on_close_request: bool, + resize_border: u32, ) -> &mut Window { - let state = State::new(application, id, &window); + let state = State::new(application, id, window.as_ref()); let viewport_version = state.viewport_version(); let physical_size = state.physical_size(); let surface = compositor.create_surface( @@ -52,6 +53,11 @@ where let _ = self.aliases.insert(window.id(), id); + let drag_resize_window_func = super::drag_resize::event_func( + window.as_ref(), + resize_border as f64 * window.scale_factor(), + ); + let _ = self.entries.insert( id, Window { @@ -59,9 +65,12 @@ where state, viewport_version, exit_on_close_request, + drag_resize_window_func, surface, renderer, mouse_interaction: mouse::Interaction::None, + prev_dnd_destination_rectangles_count: 0, + resize_enabled: false, }, ); @@ -92,6 +101,10 @@ where self.entries.get_mut(&id) } + pub fn ids(&self) -> impl Iterator + '_ { + self.entries.keys().cloned() + } + pub fn get_mut_alias( &mut self, id: winit::window::WindowId, @@ -124,6 +137,12 @@ where } } +pub(crate) enum Frame { + None, + Waiting, + Ready, +} + #[allow(missing_debug_implementations)] pub struct Window where @@ -131,13 +150,23 @@ where C: Compositor, P::Theme: DefaultStyle, { - pub raw: Arc, - pub state: State

, + pub raw: Arc, + pub(crate) state: State

, pub viewport_version: u64, pub exit_on_close_request: bool, + pub drag_resize_window_func: Option< + Box< + dyn FnMut( + &dyn winit::window::Window, + &winit::event::WindowEvent, + ) -> bool, + >, + >, + pub prev_dnd_destination_rectangles_count: usize, pub mouse_interaction: mouse::Interaction, pub surface: C::Surface, pub renderer: P::Renderer, + pub resize_enabled: bool, } impl Window @@ -158,8 +187,12 @@ where } pub fn size(&self) -> Size { - let size = self.raw.inner_size().to_logical(self.raw.scale_factor()); + let size = self.raw.surface_size().to_logical(self.raw.scale_factor()); Size::new(size.width, size.height) } + + pub fn request_redraw(&mut self) { + self.raw.request_redraw(); + } } diff --git a/winit/src/proxy.rs b/winit/src/proxy.rs index d8ad8b3f01..a23275c344 100644 --- a/winit/src/proxy.rs +++ b/winit/src/proxy.rs @@ -4,14 +4,16 @@ use crate::futures::futures::{ task::{Context, Poll}, Future, Sink, StreamExt, }; +use crate::program::Event; use crate::runtime::Action; use std::pin::Pin; /// An event loop proxy with backpressure that implements `Sink`. #[derive(Debug)] pub struct Proxy { - raw: winit::event_loop::EventLoopProxy>, + pub(crate) raw: winit::event_loop::EventLoopProxy, sender: mpsc::Sender>, + event_sender: mpsc::UnboundedSender>, notifier: mpsc::Sender, } @@ -21,6 +23,7 @@ impl Clone for Proxy { raw: self.raw.clone(), sender: self.sender.clone(), notifier: self.notifier.clone(), + event_sender: self.event_sender.clone(), } } } @@ -30,11 +33,14 @@ impl Proxy { /// Creates a new [`Proxy`] from an `EventLoopProxy`. pub fn new( - raw: winit::event_loop::EventLoopProxy>, + raw: winit::event_loop::EventLoopProxy, + event_sender: mpsc::UnboundedSender>, ) -> (Self, impl Future) { let (notifier, mut processed) = mpsc::channel(Self::MAX_SIZE); - let (sender, mut receiver) = mpsc::channel(Self::MAX_SIZE); + let (sender, mut receiver): (mpsc::Sender>, _) = + mpsc::channel(Self::MAX_SIZE); let proxy = raw.clone(); + let event_sender_clone = event_sender.clone(); let worker = async move { let mut count = 0; @@ -43,7 +49,8 @@ impl Proxy { if count < Self::MAX_SIZE { select! { message = receiver.select_next_some() => { - let _ = proxy.send_event(message); + let _ = event_sender_clone.unbounded_send(Event::UserEvent(message)); + let _ = proxy.wake_up(); count += 1; } @@ -68,6 +75,7 @@ impl Proxy { raw, sender, notifier, + event_sender, }, worker, ) @@ -92,8 +100,8 @@ impl Proxy { where T: std::fmt::Debug, { - self.raw - .send_event(action) + self.event_sender + .unbounded_send(Event::UserEvent(action)) .expect("Send message to event loop"); }