From 08fe1f3aa56bd8dc45201b96aec859067806cf8c 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 | 30 +- CHANGELOG.md | 6 + Cargo.lock | 1826 ++++++++++--- Cargo.toml | 156 +- accessibility/Cargo.toml | 24 + accessibility/src/a11y_tree.rs | 80 + accessibility/src/id.rs | 228 ++ accessibility/src/lib.rs | 17 + accessibility/src/node.rs | 41 + accessibility/src/traits.rs | 19 + core/Cargo.toml | 21 + core/src/border.rs | 51 + core/src/clipboard.rs | 218 ++ core/src/element.rs | 99 +- core/src/event.rs | 27 +- 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 | 170 ++ core/src/image.rs | 3 +- core/src/keyboard/key.rs | 2 + core/src/lib.rs | 5 + core/src/mouse/click.rs | 1 - core/src/overlay.rs | 6 +- core/src/overlay/group.rs | 3 +- core/src/padding.rs | 23 + core/src/rectangle.rs | 22 + core/src/renderer.rs | 6 + core/src/renderer/null.rs | 5 +- core/src/settings.rs | 20 +- core/src/svg.rs | 10 + core/src/text.rs | 8 +- core/src/theme.rs | 4 + core/src/theme/palette.rs | 10 +- core/src/widget.rs | 37 +- core/src/widget/operation.rs | 3 +- core/src/widget/operation/focusable.rs | 3 +- core/src/widget/operation/search_id.rs | 49 + core/src/widget/text.rs | 72 +- core/src/widget/tree.rs | 298 ++- core/src/window/event.rs | 4 +- core/src/window/id.rs | 13 +- core/src/window/screenshot.rs | 2 +- core/src/window/settings.rs | 4 +- devtools/src/lib.rs | 4 +- examples/counter/Cargo.toml | 7 +- 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 | 9 +- examples/lazy/Cargo.toml | 2 +- examples/loading_spinners/Cargo.toml | 2 +- examples/loupe/src/main.rs | 2 +- examples/multi_window/Cargo.toml | 9 +- examples/multi_window/src/main.rs | 1 + examples/pokedex/Cargo.toml | 2 +- examples/screenshot/Cargo.toml | 2 +- examples/scrollable/src/main.rs | 2 + 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 | 146 - examples/toast/src/main.rs | 10 +- examples/todos/Cargo.toml | 14 +- examples/todos/src/main.rs | 4 +- examples/tooltip/Cargo.toml | 2 +- examples/tour/Cargo.toml | 2 +- examples/websocket/src/main.rs | 1 + futures/Cargo.toml | 1 + futures/src/runtime.rs | 2 +- graphics/src/compositor.rs | 17 +- graphics/src/geometry/text.rs | 41 +- graphics/src/image.rs | 11 +- graphics/src/settings.rs | 4 +- graphics/src/text.rs | 11 +- graphics/src/text/paragraph.rs | 11 +- graphics/src/viewport.rs | 27 +- program/src/lib.rs | 164 +- renderer/Cargo.toml | 6 +- renderer/src/fallback.rs | 6 + runtime/Cargo.toml | 17 +- runtime/src/clipboard.rs | 83 +- runtime/src/dnd.rs | 129 + runtime/src/lib.rs | 50 + runtime/src/multi_window/state.rs | 279 ++ 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 | 240 ++ runtime/src/user_interface.rs | 52 +- runtime/src/window.rs | 26 +- src/application.rs | 77 +- src/application/timed.rs | 22 +- src/daemon.rs | 83 +- src/error.rs | 12 + src/lib.rs | 74 +- src/time.rs | 2 +- src/wayland/application.rs | 211 ++ src/wayland/mod.rs | 4 + src/wayland/program.rs | 833 ++++++ src/window.rs | 5 + src/window/icon.rs | 5 +- tester/src/recorder.rs | 2 +- tiny_skia/fonts/Iced-Icons.ttf | Bin 0 -> 5108 bytes tiny_skia/src/engine.rs | 107 +- tiny_skia/src/layer.rs | 2 +- tiny_skia/src/lib.rs | 10 +- tiny_skia/src/raster.rs | 153 +- tiny_skia/src/settings.rs | 2 +- tiny_skia/src/vector.rs | 1 + tiny_skia/src/window/compositor.rs | 3 +- wgpu/Cargo.toml | 17 + wgpu/src/image/mod.rs | 2 +- wgpu/src/layer.rs | 1 + wgpu/src/lib.rs | 73 +- 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 | 107 +- wgpu/src/window/wayland.rs | 115 + wgpu/src/window/x11.rs | 171 ++ widget/Cargo.toml | 10 +- widget/src/button.rs | 250 +- widget/src/checkbox.rs | 135 +- widget/src/column.rs | 48 +- widget/src/combo_box.rs | 2 +- widget/src/container.rs | 155 +- widget/src/float.rs | 4 +- widget/src/grid.rs | 4 +- widget/src/helpers.rs | 14 +- widget/src/image.rs | 134 +- widget/src/keyed/column.rs | 11 +- widget/src/lazy.rs | 45 +- widget/src/lazy/component.rs | 51 +- widget/src/lazy/responsive.rs | 528 ++++ widget/src/mouse_area.rs | 249 +- widget/src/overlay/menu.rs | 17 +- widget/src/pane_grid.rs | 20 +- widget/src/pane_grid/content.rs | 8 +- widget/src/pane_grid/title_bar.rs | 18 +- widget/src/pick_list.rs | 32 +- widget/src/pin.rs | 4 +- widget/src/radio.rs | 2 +- widget/src/responsive.rs | 4 +- widget/src/row.rs | 48 +- widget/src/rule.rs | 60 +- widget/src/scrollable.rs | 269 +- widget/src/sensor.rs | 4 +- widget/src/slider.rs | 267 +- widget/src/stack.rs | 6 +- widget/src/svg.rs | 171 +- widget/src/table.rs | 4 +- widget/src/text_input.rs | 2 +- widget/src/themer.rs | 10 +- widget/src/toggler.rs | 244 +- widget/src/tooltip.rs | 16 +- widget/src/vertical_slider.rs | 6 +- winit/Cargo.toml | 63 +- winit/src/a11y.rs | 63 + winit/src/application/drag_resize.rs | 138 + winit/src/application/state.rs | 231 ++ winit/src/clipboard.rs | 388 ++- winit/src/conversion.rs | 410 +-- winit/src/error.rs | 6 +- winit/src/lib.rs | 1566 ++++++----- winit/src/platform_specific/mod.rs | 188 ++ .../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 | 257 ++ .../wayland/handlers/seat/mod.rs | 5 + .../wayland/handlers/seat/pointer.rs | 204 ++ .../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 | 210 ++ .../platform_specific/wayland/sctk_event.rs | 1212 +++++++++ .../wayland/subsurface_widget.rs | 701 +++++ .../platform_specific/wayland/winit_window.rs | 453 ++++ winit/src/program.rs | 2345 +++++++++++++++++ winit/src/proxy.rs | 22 +- winit/src/window.rs | 40 +- winit/src/window/state.rs | 81 +- 233 files changed, 24391 insertions(+), 1911 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 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 create mode 100644 runtime/src/dnd.rs create mode 100644 runtime/src/multi_window/state.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 runtime/src/program/state.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 widget/src/lazy/responsive.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 create mode 100644 winit/src/program.rs diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 40e9235a..6e81ee07 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 16ee8bf9..36d43c0a 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 e1280769..ed4bff21 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,18 +11,18 @@ jobs: os: [ubuntu-latest, windows-latest, macOS-latest] rust: [stable, beta, "1.88"] steps: - - uses: hecrj/setup-rust-action@v2 - with: - rust-version: ${{ matrix.rust }} - - uses: actions/checkout@master - - name: Install dependencies - if: matrix.os == 'ubuntu-latest' - run: | - export DEBIAN_FRONTED=noninteractive - sudo apt-get -qq update - sudo apt-get install -y libxkbcommon-dev libgtk-3-dev - - name: Run tests - run: | - cargo test --verbose --workspace - cargo test --verbose --workspace -- --ignored - cargo test --verbose --workspace --all-features + - uses: hecrj/setup-rust-action@v2 + with: + rust-version: ${{ matrix.rust }} + - uses: actions/checkout@master + - name: Install dependencies + if: matrix.os == 'ubuntu-latest' + run: | + export DEBIAN_FRONTED=noninteractive + sudo apt-get -qq update + sudo apt-get install -y libxkbcommon-dev libwayland-dev + - name: Run tests + run: | + cargo test --verbose --workspace + cargo test --verbose --workspace -- --ignored + cargo test --verbose --workspace --all-features diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c1c451b..d3db5512 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -601,6 +601,8 @@ Many thanks to... - @wiiznokes - @woelfman - @Zaubentrucker +- @ryanabx +- @edfloreshz ## [0.12.1] - 2024-02-22 ### Added @@ -787,6 +789,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.lock b/Cargo.lock index 8b65d213..46930e5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,86 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" +[[package]] +name = "accesskit" +version = "0.22.0" +dependencies = [ + "uuid", +] + +[[package]] +name = "accesskit_atspi_common" +version = "0.15.0" +dependencies = [ + "accesskit", + "accesskit_consumer", + "atspi-common", + "serde", + "zvariant", +] + +[[package]] +name = "accesskit_consumer" +version = "0.32.0" +dependencies = [ + "accesskit", + "hashbrown 0.16.1", +] + +[[package]] +name = "accesskit_macos" +version = "0.23.0" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.16.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "accesskit_unix" +version = "0.18.0" +dependencies = [ + "accesskit", + "accesskit_atspi_common", + "async-channel 2.5.0", + "async-executor", + "async-task", + "atspi", + "futures-lite", + "futures-util", + "serde", + "tokio", + "tokio-stream", + "zbus", +] + +[[package]] +name = "accesskit_windows" +version = "0.30.0" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.16.1", + "static_assertions", + "windows 0.61.3", + "windows-core 0.61.2", +] + +[[package]] +name = "accesskit_winit" +version = "0.30.0" +dependencies = [ + "accesskit", + "accesskit_macos", + "accesskit_unix", + "accesskit_windows", + "raw-window-handle", + "winit", +] + [[package]] name = "adler2" version = "2.0.1" @@ -163,7 +243,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -220,7 +300,7 @@ dependencies = [ "url", "wayland-backend", "wayland-client", - "wayland-protocols", + "wayland-protocols 0.32.9", "zbus", ] @@ -230,12 +310,23 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ - "event-listener", + "event-listener 5.4.1", "event-listener-strategy", "futures-core", "pin-project-lite", ] +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + [[package]] name = "async-channel" version = "2.5.0" @@ -273,6 +364,21 @@ dependencies = [ "futures-lite", ] +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.5.0", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + [[package]] name = "async-io" version = "2.6.0" @@ -297,7 +403,7 @@ version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" dependencies = [ - "event-listener", + "event-listener 5.4.1", "event-listener-strategy", "pin-project-lite", ] @@ -319,14 +425,14 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" dependencies = [ - "async-channel", + "async-channel 2.5.0", "async-io", "async-lock", "async-signal", "async-task", "blocking", "cfg-if", - "event-listener", + "event-listener 5.4.1", "futures-lite", "rustix 1.1.2", ] @@ -339,7 +445,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -360,6 +466,32 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "async-std" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" +dependencies = [ + "async-channel 1.9.0", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + [[package]] name = "async-task" version = "4.7.1" @@ -374,7 +506,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -400,6 +532,49 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atomic_refcell" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" + +[[package]] +name = "atspi" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d" +dependencies = [ + "atspi-common", + "atspi-proxies", +] + +[[package]] +name = "atspi-common" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d" +dependencies = [ + "enumflags2", + "serde", + "static_assertions", + "zbus", + "zbus-lockstep", + "zbus-lockstep-macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "atspi-proxies" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc" +dependencies = [ + "atspi-common", + "serde", + "zbus", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -578,7 +753,7 @@ version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ - "async-channel", + "async-channel 2.5.0", "async-task", "futures-io", "futures-lite", @@ -591,6 +766,15 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e79769241dcd44edf79a732545e8b5cec84c247ac060f5252cd51885d093a8fc" +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "cfg_aliases", +] + [[package]] name = "built" version = "0.8.0" @@ -626,7 +810,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -648,10 +832,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] -name = "bytesize" -version = "1.3.3" +name = "calloop" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e93abca9e28e0a1b9877922aacb20576e05d4679ffa78c3d6dc22a26a216659" +checksum = "fba7adb4dd5aa98e5553510223000e7148f621165ec5f9acd7113f6ca4995298" +dependencies = [ + "bitflags 2.10.0", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] [[package]] name = "calloop" @@ -680,6 +872,18 @@ dependencies = [ "tracing", ] +[[package]] +name = "calloop-wayland-source" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02" +dependencies = [ + "calloop 0.12.4", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + [[package]] name = "calloop-wayland-source" version = "0.3.0" @@ -740,6 +944,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -784,7 +998,7 @@ dependencies = [ "js-sys", "num-traits", "wasm-bindgen", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -850,29 +1064,28 @@ dependencies = [ [[package]] name = "clipboard_macos" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7f4aaa047ba3c3630b080bb9860894732ff23e2aee290a418909aa6d5df38f" +version = "0.1.0" +source = "git+https://github.com/pop-os/window_clipboard.git?tag=pop-0.13-2#6b9faab87bea9cebec6ae036906fd67fed254f5f" dependencies = [ - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", + "objc", + "objc-foundation", + "objc_id", ] [[package]] name = "clipboard_wayland" version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "003f886bc4e2987729d10c1db3424e7f80809f3fc22dbc16c685738887cb37b8" +source = "git+https://github.com/pop-os/window_clipboard.git?tag=pop-0.13-2#6b9faab87bea9cebec6ae036906fd67fed254f5f" dependencies = [ + "dnd", + "mime 0.1.0", "smithay-clipboard", ] [[package]] name = "clipboard_x11" version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4274ea815e013e0f9f04a2633423e14194e408a0576c943ce3d14ca56c50031c" +source = "git+https://github.com/pop-os/window_clipboard.git?tag=pop-0.13-2#6b9faab87bea9cebec6ae036906fd67fed254f5f" dependencies = [ "thiserror 1.0.69", "x11rb", @@ -887,6 +1100,36 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation 0.9.4", + "core-graphics", + "foreign-types 0.5.0", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "libc", + "objc", +] + [[package]] name = "codespan-reporting" version = "0.12.0" @@ -997,19 +1240,6 @@ dependencies = [ "libc", ] -[[package]] -name = "core-graphics" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" -dependencies = [ - "bitflags 2.10.0", - "core-foundation 0.10.1", - "core-graphics-types 0.2.0", - "foreign-types 0.5.0", - "libc", -] - [[package]] name = "core-graphics-types" version = "0.1.3" @@ -1052,9 +1282,8 @@ dependencies = [ [[package]] name = "cosmic-text" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "173852283a9a57a3cbe365d86e74dc428a09c50421477d5ad6fe9d9509e37737" +version = "0.16.0" +source = "git+https://github.com/pop-os/cosmic-text.git#0d9af4f7de087878100b296c81d1baca2e05433d" dependencies = [ "bitflags 2.10.0", "fontdb", @@ -1064,8 +1293,8 @@ dependencies = [ "rangemap", "rustc-hash 1.1.0", "self_cell", - "skrifa", - "smol_str", + "skrifa 0.39.0", + "smol_str 0.2.2", "swash", "sys-locale", "unicode-bidi", @@ -1079,7 +1308,6 @@ name = "counter" version = "0.1.0" dependencies = [ "iced", - "iced_test", ] [[package]] @@ -1170,8 +1398,7 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "cryoglyph" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc795bdbccdbd461736fb163930a009da6597b226d6f6fce33e7a8eb6ec519" +source = "git+https://github.com/pop-os/glyphon.git?branch=iced-0.14#c49de15bce4d8254ac136d1be9911960cc85ce12" dependencies = [ "cosmic-text", "etagere", @@ -1274,6 +1501,16 @@ dependencies = [ "dirs-sys", ] +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + [[package]] name = "dirs-sys" version = "0.5.0" @@ -1282,10 +1519,21 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.5.2", "windows-sys 0.61.2", ] +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users 0.4.6", + "winapi", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -1312,7 +1560,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -1324,6 +1572,18 @@ dependencies = [ "libloading", ] +[[package]] +name = "dnd" +version = "0.1.0" +source = "git+https://github.com/pop-os/window_clipboard.git?tag=pop-0.13-2#6b9faab87bea9cebec6ae036906fd67fed254f5f" +dependencies = [ + "bitflags 2.10.0", + "mime 0.1.0", + "raw-window-handle", + "smithay-client-toolkit 0.19.2", + "smithay-clipboard", +] + [[package]] name = "document-features" version = "0.2.12" @@ -1350,8 +1610,45 @@ dependencies = [ [[package]] name = "dpi" version = "0.1.2" + +[[package]] +name = "drm" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +checksum = "a0f8a69e60d75ae7dab4ef26a59ca99f2a89d4c142089b537775ae0c198bdcde" +dependencies = [ + "bitflags 2.10.0", + "bytemuck", + "drm-ffi", + "drm-fourcc", + "rustix 0.38.44", +] + +[[package]] +name = "drm-ffi" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41334f8405792483e32ad05fbb9c5680ff4e84491883d2947a4757dc54cb2ac6" +dependencies = [ + "drm-sys", + "rustix 0.38.44", +] + +[[package]] +name = "drm-fourcc" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" + +[[package]] +name = "drm-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d09ff881f92f118b11105ba5e34ff8f4adf27b30dae8f12e28c193af1c83176" +dependencies = [ + "libc", + "linux-raw-sys 0.6.5", +] [[package]] name = "editor" @@ -1401,7 +1698,20 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", ] [[package]] @@ -1421,7 +1731,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -1465,6 +1775,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "event-listener" version = "5.4.1" @@ -1482,7 +1798,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "event-listener", + "event-listener 5.4.1", "pin-project-lite", ] @@ -1555,7 +1871,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -1646,7 +1962,7 @@ checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" dependencies = [ "fontconfig-parser", "log", - "memmap2", + "memmap2 0.9.9", "slotmap", "tinyvec", "ttf-parser", @@ -1679,7 +1995,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -1773,7 +2089,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -1855,7 +2171,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ "rustix 1.1.2", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -1912,6 +2228,19 @@ dependencies = [ "weezl", ] +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + [[package]] name = "gl_generator" version = "0.14.0" @@ -1932,6 +2261,65 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.10.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "glow" version = "0.16.0" @@ -1953,6 +2341,17 @@ dependencies = [ "gl_generator", ] +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + [[package]] name = "gpu-alloc" version = "0.6.0" @@ -1981,7 +2380,7 @@ dependencies = [ "log", "presser", "thiserror 1.0.69", - "windows 0.58.0", + "windows 0.57.0", ] [[package]] @@ -2012,6 +2411,153 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "gstreamer" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de95703f4c8e79f4f4e42279cf1ab0e5a46b7ece4a9dfcd16424164af7be9055" +dependencies = [ + "cfg-if", + "futures-channel", + "futures-core", + "futures-util", + "glib", + "gstreamer-sys", + "itertools 0.12.1", + "libc", + "muldiv", + "num-integer", + "num-rational", + "option-operations", + "paste", + "pin-project-lite", + "pretty-hex", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gstreamer-allocators" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c9fb20b4b33d4e5dfdddf817d47937a1a1dcc3cfa72bd6f68f6b73231214293" +dependencies = [ + "glib", + "gstreamer", + "gstreamer-allocators-sys", + "libc", +] + +[[package]] +name = "gstreamer-allocators-sys" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1158478db5aacee46d876c7012861345a6486d1d064ce5cdceac57f3bd137af" +dependencies = [ + "glib-sys", + "gobject-sys", + "gstreamer-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gstreamer-app" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16bc8090a8806193237e7b6531ee429ff6e39686425f5c3eb06dfa75875390fb" +dependencies = [ + "futures-core", + "futures-sink", + "glib", + "gstreamer", + "gstreamer-app-sys", + "gstreamer-base", + "libc", +] + +[[package]] +name = "gstreamer-app-sys" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aea07f07a3f17278e6998390ecaea127e476f0af0360c2d83d96e6d3a97fb75e" +dependencies = [ + "glib-sys", + "gstreamer-base-sys", + "gstreamer-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gstreamer-base" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb150b6904a49052237fede7cc2e6479df6ced5043d95e6af8134bc141a3167f" +dependencies = [ + "atomic_refcell", + "cfg-if", + "glib", + "gstreamer", + "gstreamer-base-sys", + "libc", +] + +[[package]] +name = "gstreamer-base-sys" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ca701f9078fe115b29b24c80910b577f9cb5b039182f050dbadf5933594b64" +dependencies = [ + "glib-sys", + "gobject-sys", + "gstreamer-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gstreamer-sys" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "564cda782b3e6eed1b81cb4798a06794db56440fb05b422505be689f34ce3bc4" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gstreamer-video" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e85b2a4d1d3b7a98ae03806c3ed5c2db89d6b37a5f138780b48de015d68715e5" +dependencies = [ + "cfg-if", + "futures-channel", + "glib", + "gstreamer", + "gstreamer-base", + "gstreamer-video-sys", + "libc", + "thiserror 1.0.69", +] + +[[package]] +name = "gstreamer-video-sys" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0302318d98e6b054501e485b6bb4ee20225823218f4a8660c182f115a33b16ee" +dependencies = [ + "glib-sys", + "gobject-sys", + "gstreamer-base-sys", + "gstreamer-sys", + "libc", + "system-deps", +] + [[package]] name = "guillotiere" version = "0.6.2" @@ -2074,14 +2620,14 @@ dependencies = [ [[package]] name = "harfrust" -version = "0.3.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92c020db12c71d8a12a3fe7607873cade3a01a6287e29d540c8723276221b9d8" +checksum = "e0caaee032384c10dd597af4579c67dee16650d862a9ccbe1233ff1a379abc07" dependencies = [ "bitflags 2.10.0", "bytemuck", "core_maths", - "read-fonts", + "read-fonts 0.36.0", "smallvec", ] @@ -2114,7 +2660,7 @@ dependencies = [ "headers-core", "http 0.2.12", "httpdate", - "mime", + "mime 0.3.17", "sha1", ] @@ -2133,6 +2679,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.5.2" @@ -2218,6 +2770,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + [[package]] name = "hyper" version = "0.14.32" @@ -2351,11 +2909,14 @@ name = "iced" version = "0.14.0" dependencies = [ "criterion", + "dnd", + "iced_accessibility", "iced_core", "iced_debug", "iced_devtools", "iced_futures", "iced_highlighter", + "iced_program", "iced_renderer", "iced_runtime", "iced_tester", @@ -2363,7 +2924,19 @@ dependencies = [ "iced_widget", "iced_winit", "image", + "mime 0.1.0", "thiserror 2.0.17", + "window_clipboard", +] + +[[package]] +name = "iced_accessibility" +version = "0.1.0" +dependencies = [ + "accesskit", + "accesskit_macos", + "accesskit_windows", + "accesskit_winit", ] [[package]] @@ -2384,17 +2957,24 @@ dependencies = [ name = "iced_core" version = "0.14.0" dependencies = [ + "approx", "bitflags 2.10.0", "bytes", + "dnd", "glam", + "iced_accessibility", "lilt", "log", + "mime 0.1.0", "num-traits", + "raw-window-handle", "rustc-hash 2.1.1", "serde", - "smol_str", + "smithay-client-toolkit 0.19.2", + "smol_str 0.3.2", "thiserror 2.0.17", "web-time", + "window_clipboard", ] [[package]] @@ -2484,12 +3064,16 @@ name = "iced_runtime" version = "0.14.0" dependencies = [ "bytes", + "dnd", + "iced_accessibility", "iced_core", "iced_futures", "iced_selector", "raw-window-handle", "sipper", + "smithay-client-toolkit 0.19.2", "thiserror 2.0.17", + "window_clipboard", ] [[package]] @@ -2544,6 +3128,7 @@ dependencies = [ name = "iced_wgpu" version = "0.14.0" dependencies = [ + "as-raw-xcb-connection", "bitflags 2.10.0", "bytemuck", "cryoglyph", @@ -2554,44 +3139,73 @@ dependencies = [ "iced_graphics", "log", "lyon", + "raw-window-handle", "resvg", "rustc-hash 2.1.1", + "rustix 0.38.44", + "smithay-client-toolkit 0.19.2", "thiserror 2.0.17", + "tiny-xlib", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.32.9", + "wayland-sys", "wgpu", + "x11rb", ] [[package]] name = "iced_widget" version = "0.14.2" dependencies = [ + "dnd", + "iced_accessibility", "iced_highlighter", "iced_renderer", + "iced_runtime", "log", "num-traits", "ouroboros", "pulldown-cmark", "qrcode", "rustc-hash 2.1.1", + "smithay-client-toolkit 0.19.2", "thiserror 2.0.17", "unicode-segmentation", + "window_clipboard", ] [[package]] name = "iced_winit" version = "0.14.0" dependencies = [ + "cursor-icon", + "dnd", + "iced_accessibility", "iced_debug", + "iced_futures", + "iced_graphics", "iced_program", + "iced_runtime", "log", "mundy", + "raw-window-handle", "rustc-hash 2.1.1", + "smithay-client-toolkit 0.19.2", "sysinfo", "thiserror 2.0.17", "tracing", "wasm-bindgen-futures", + "wayland-backend", + "wayland-protocols 0.32.9", "web-sys", + "winapi", "window_clipboard", "winit", + "winit-core", + "xkbcommon", + "xkbcommon-dl", + "xkeysym", ] [[package]] @@ -2752,21 +3366,6 @@ dependencies = [ "hashbrown 0.16.1", ] -[[package]] -name = "integration" -version = "0.1.0" -dependencies = [ - "console_error_panic_hook", - "console_log", - "futures", - "iced_wgpu", - "iced_widget", - "iced_winit", - "tracing-subscriber", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "interpolate_name" version = "0.2.4" @@ -2775,7 +3374,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -2880,7 +3479,7 @@ checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -2949,6 +3548,16 @@ dependencies = [ "mutate_once", ] +[[package]] +name = "keyboard-types" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d" +dependencies = [ + "bitflags 2.10.0", + "serde", +] + [[package]] name = "khronos-egl" version = "6.0.0" @@ -2987,6 +3596,15 @@ dependencies = [ "smallvec", ] +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + [[package]] name = "layout" version = "0.1.0" @@ -3036,7 +3654,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -3053,7 +3671,7 @@ checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags 2.10.0", "libc", - "redox_syscall 0.5.18", + "redox_syscall", ] [[package]] @@ -3083,6 +3701,12 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "linux-raw-sys" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a385b1be4e5c3e362ad2ffa73c392e53f031eaa5b7d648e64cd87f27f6063d7" + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -3123,6 +3747,9 @@ name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +dependencies = [ + "value-bag", +] [[package]] name = "loop9" @@ -3142,9 +3769,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.16.2" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96051b46fc183dc9cd4a223960ef37b9af631b55191852a8274bfef064cda20f" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" [[package]] name = "lyon" @@ -3253,6 +3880,15 @@ dependencies = [ "rustix 1.1.2", ] +[[package]] +name = "memmap2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed" +dependencies = [ + "libc", +] + [[package]] name = "memmap2" version = "0.9.9" @@ -3286,6 +3922,14 @@ dependencies = [ "paste", ] +[[package]] +name = "mime" +version = "0.1.0" +source = "git+https://github.com/pop-os/window_clipboard.git?tag=pop-0.13-2#6b9faab87bea9cebec6ae036906fd67fed254f5f" +dependencies = [ + "smithay-clipboard", +] + [[package]] name = "mime" version = "0.3.17" @@ -3298,7 +3942,7 @@ version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" dependencies = [ - "mime", + "mime 0.3.17", "unicase", ] @@ -3340,6 +3984,12 @@ dependencies = [ "pxfm", ] +[[package]] +name = "muldiv" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0" + [[package]] name = "multer" version = "2.1.0" @@ -3353,7 +4003,7 @@ dependencies = [ "httparse", "log", "memchr", - "mime", + "mime 0.3.17", "spin", "version_check", ] @@ -3554,7 +4204,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -3613,10 +4263,10 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -3628,6 +4278,17 @@ dependencies = [ "malloc_buf", ] +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + [[package]] name = "objc-sys" version = "0.3.5" @@ -3679,7 +4340,7 @@ dependencies = [ "block2 0.6.2", "libc", "objc2 0.6.3", - "objc2-cloud-kit 0.3.2", + "objc2-cloud-kit", "objc2-core-data 0.3.2", "objc2-core-foundation", "objc2-core-graphics", @@ -3690,19 +4351,6 @@ dependencies = [ "objc2-quartz-core 0.3.2", ] -[[package]] -name = "objc2-cloud-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" -dependencies = [ - "bitflags 2.10.0", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-core-location", - "objc2-foundation 0.2.2", -] - [[package]] name = "objc2-cloud-kit" version = "0.3.2" @@ -3714,17 +4362,6 @@ dependencies = [ "objc2-foundation 0.3.2", ] -[[package]] -name = "objc2-contacts" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" -dependencies = [ - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - [[package]] name = "objc2-core-data" version = "0.2.2" @@ -3755,6 +4392,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.10.0", + "block2 0.6.2", "dispatch2", "objc2 0.6.3", ] @@ -3767,6 +4405,7 @@ checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ "bitflags 2.10.0", "dispatch2", + "libc", "objc2 0.6.3", "objc2-core-foundation", "objc2-io-surface", @@ -3794,18 +4433,6 @@ dependencies = [ "objc2-foundation 0.3.2", ] -[[package]] -name = "objc2-core-location" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" -dependencies = [ - "block2 0.5.1", - "objc2 0.5.2", - "objc2-contacts", - "objc2-foundation 0.2.2", -] - [[package]] name = "objc2-core-text" version = "0.3.2" @@ -3845,7 +4472,6 @@ checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ "bitflags 2.10.0", "block2 0.5.1", - "dispatch", "libc", "objc2 0.5.2", ] @@ -3874,18 +4500,6 @@ dependencies = [ "objc2-core-foundation", ] -[[package]] -name = "objc2-link-presentation" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" -dependencies = [ - "block2 0.5.1", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", -] - [[package]] name = "objc2-metal" version = "0.2.2" @@ -3922,59 +4536,25 @@ dependencies = [ "objc2-foundation 0.3.2", ] -[[package]] -name = "objc2-symbols" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" -dependencies = [ - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - [[package]] name = "objc2-ui-kit" -version = "0.2.2" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ "bitflags 2.10.0", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-cloud-kit 0.2.2", - "objc2-core-data 0.2.2", - "objc2-core-image 0.2.2", - "objc2-core-location", - "objc2-foundation 0.2.2", - "objc2-link-presentation", - "objc2-quartz-core 0.2.2", - "objc2-symbols", - "objc2-uniform-type-identifiers", - "objc2-user-notifications", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-foundation 0.3.2", ] [[package]] -name = "objc2-uniform-type-identifiers" -version = "0.2.2" +name = "objc_id" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" dependencies = [ - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-user-notifications" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" -dependencies = [ - "bitflags 2.10.0", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-core-location", - "objc2-foundation 0.2.2", + "objc", ] [[package]] @@ -4023,7 +4603,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -4050,6 +4630,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "option-operations" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c26d27bb1aeab65138e4bf7666045169d1717febcc9ff870166be8348b223d0" +dependencies = [ + "paste", +] + [[package]] name = "orbclient" version = "0.3.49" @@ -4095,11 +4684,11 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -4132,7 +4721,7 @@ dependencies = [ "by_address", "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -4166,9 +4755,9 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.18", + "redox_syscall", "smallvec", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -4225,7 +4814,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -4267,7 +4856,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -4443,13 +5032,53 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" +[[package]] +name = "pretty-hex" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc83ee4a840062f368f9096d80077a9841ec117e17e7f700df81958f1451254" + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit", + "toml_edit 0.23.9", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", ] [[package]] @@ -4469,7 +5098,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", "version_check", "yansi", ] @@ -4490,7 +5119,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -4572,6 +5201,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" dependencies = [ "memchr", + "serde", ] [[package]] @@ -4743,17 +5373,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358" dependencies = [ "bytemuck", - "core_maths", "font-types", ] [[package]] -name = "redox_syscall" -version = "0.4.1" +name = "read-fonts" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "5eaa2941a4c05443ee3a7b26ab076a553c343ad5995230cc2b1d3e993bdc6345" dependencies = [ - "bitflags 1.3.2", + "bytemuck", + "core_maths", + "font-types", ] [[package]] @@ -4765,6 +5396,17 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -4832,7 +5474,7 @@ dependencies = [ "hyper-util", "js-sys", "log", - "mime", + "mime 0.3.17", "native-tls", "percent-encoding", "pin-project-lite", @@ -5099,17 +5741,91 @@ dependencies = [ [[package]] name = "sctk-adwaita" -version = "0.10.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +checksum = "1dd3accc0f3f4bbaf2c9e1957a030dc582028130c67660d44c0a0345a22ca69b" dependencies = [ "ab_glyph", "log", - "memmap2", - "smithay-client-toolkit 0.19.2", + "memmap2 0.9.9", + "smithay-client-toolkit 0.20.0", "tiny-skia", ] +[[package]] +name = "sctk_drag" +version = "0.1.0" +dependencies = [ + "env_logger", + "iced", + "iced_core", + "smithay-client-toolkit 0.19.2", +] + +[[package]] +name = "sctk_lazy" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "sctk_session_lock" +version = "0.1.0" +dependencies = [ + "async-std", + "env_logger", + "iced", + "iced_runtime", + "smithay-client-toolkit 0.18.0", +] + +[[package]] +name = "sctk_subsurface" +version = "0.1.0" +dependencies = [ + "calloop 0.12.4", + "env_logger", + "futures-channel", + "iced", + "iced_runtime", + "rustix 0.38.44", + "smithay-client-toolkit 0.18.0", +] + +[[package]] +name = "sctk_subsurface_gst" +version = "0.1.0" +dependencies = [ + "calloop 0.12.4", + "drm-fourcc", + "env_logger", + "futures-channel", + "gstreamer", + "gstreamer-allocators", + "gstreamer-app", + "gstreamer-video", + "iced", + "iced_runtime", + "smithay-client-toolkit 0.18.0", +] + +[[package]] +name = "sctk_todos" +version = "0.1.0" +dependencies = [ + "async-std", + "directories-next", + "env_logger", + "iced", + "iced_core", + "log", + "once_cell", + "serde", + "serde_json", + "smithay-client-toolkit 0.19.2", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -5176,7 +5892,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -5200,7 +5916,16 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", ] [[package]] @@ -5316,7 +6041,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841" dependencies = [ "bytemuck", - "read-fonts", + "read-fonts 0.35.0", +] + +[[package]] +name = "skrifa" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9eb0b904a04d09bd68c65d946617b8ff733009999050f3b851c32fb3cfb60e" +dependencies = [ + "bytemuck", + "read-fonts 0.36.0", ] [[package]] @@ -5349,26 +6084,56 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "smithay-client-toolkit" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +version = "0.18.0" +source = "git+https://github.com/smithay/client-toolkit?rev=828b1eb#828b1eb469ad022eb3ac30be02bd5645b879922f" dependencies = [ "bitflags 2.10.0", - "calloop 0.13.0", - "calloop-wayland-source 0.3.0", + "bytemuck", + "calloop 0.12.4", + "calloop-wayland-source 0.2.0", "cursor-icon", "libc", "log", - "memmap2", + "memmap2 0.9.9", + "pkg-config", "rustix 0.38.44", "thiserror 1.0.69", "wayland-backend", "wayland-client", "wayland-csd-frame", "wayland-cursor", - "wayland-protocols", - "wayland-protocols-wlr", + "wayland-protocols 0.31.2", + "wayland-protocols-wlr 0.2.0", "wayland-scanner", + "xkbcommon", + "xkeysym", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.10.0", + "bytemuck", + "calloop 0.13.0", + "calloop-wayland-source 0.3.0", + "cursor-icon", + "libc", + "log", + "memmap2 0.9.9", + "pkg-config", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols 0.32.9", + "wayland-protocols-wlr 0.3.9", + "wayland-scanner", + "xkbcommon", "xkeysym", ] @@ -5384,29 +6149,29 @@ dependencies = [ "cursor-icon", "libc", "log", - "memmap2", + "memmap2 0.9.9", "rustix 1.1.2", "thiserror 2.0.17", "wayland-backend", "wayland-client", "wayland-csd-frame", "wayland-cursor", - "wayland-protocols", + "wayland-protocols 0.32.9", "wayland-protocols-experimental", "wayland-protocols-misc", - "wayland-protocols-wlr", + "wayland-protocols-wlr 0.3.9", "wayland-scanner", "xkeysym", ] [[package]] name = "smithay-clipboard" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" +version = "0.8.0" +source = "git+https://github.com/pop-os/smithay-clipboard?tag=pop-dnd-5#5a3007def49eb678d1144850c9ee04b80707c56a" dependencies = [ "libc", - "smithay-client-toolkit 0.20.0", + "raw-window-handle", + "smithay-client-toolkit 0.19.2", "wayland-backend", ] @@ -5416,7 +6181,7 @@ version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" dependencies = [ - "async-channel", + "async-channel 2.5.0", "async-executor", "async-fs", "async-io", @@ -5432,7 +6197,14 @@ name = "smol_str" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" + +[[package]] +name = "smol_str" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9676b89cd56310a87b93dec47b11af744f34d5fc9f367b829474eec0a891350d" dependencies = [ + "borsh", "serde", ] @@ -5458,24 +6230,23 @@ dependencies = [ [[package]] name = "softbuffer" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" +version = "0.4.1" +source = "git+https://github.com/pop-os/softbuffer?tag=cosmic-4.0#a3f77e251e7422803f693df6e3fc313c010c4dcb" dependencies = [ "as-raw-xcb-connection", "bytemuck", "cfg_aliases", - "core-graphics 0.24.0", + "cocoa", + "core-graphics", + "drm", "fastrand", "foreign-types 0.5.0", "js-sys", "log", - "memmap2", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-quartz-core 0.2.2", + "memmap2 0.9.9", + "objc", "raw-window-handle", - "redox_syscall 0.5.18", + "redox_syscall", "rustix 0.38.44", "tiny-xlib", "wasm-bindgen", @@ -5483,7 +6254,7 @@ dependencies = [ "wayland-client", "wayland-sys", "web-sys", - "windows-sys 0.59.0", + "windows-sys 0.52.0", "x11rb", ] @@ -5558,7 +6329,7 @@ dependencies = [ "libc", "libloading", "memfd", - "memmap2", + "memmap2 0.9.9", "serde", "subsecond-types", "thiserror 2.0.17", @@ -5611,11 +6382,21 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47846491253e976bdd07d0f9cc24b7daf24720d11309302ccbbc6e6b6e53550a" dependencies = [ - "skrifa", + "skrifa 0.37.0", "yazi", "zeno", ] +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.111" @@ -5644,7 +6425,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -5713,11 +6494,16 @@ dependencies = [ ] [[package]] -name = "system_information" -version = "0.1.0" +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" dependencies = [ - "bytesize", - "iced", + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml", + "version-compare", ] [[package]] @@ -5727,6 +6513,12 @@ dependencies = [ "iced", ] +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "tempfile" version = "3.23.0" @@ -5784,7 +6576,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -5795,7 +6587,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -5973,7 +6765,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -6007,6 +6799,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-tungstenite" version = "0.21.0" @@ -6032,6 +6835,27 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "0.7.3" @@ -6041,6 +6865,19 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + [[package]] name = "toml_edit" version = "0.23.9" @@ -6048,9 +6885,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832" dependencies = [ "indexmap", - "toml_datetime", + "toml_datetime 0.7.3", "toml_parser", - "winnow", + "winnow 0.7.14", ] [[package]] @@ -6059,7 +6896,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ - "winnow", + "winnow 0.7.14", ] [[package]] @@ -6145,7 +6982,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -6412,6 +7249,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" + [[package]] name = "vcpkg" version = "0.2.15" @@ -6425,6 +7268,12 @@ dependencies = [ "iced", ] +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + [[package]] name = "version_check" version = "0.9.5" @@ -6478,7 +7327,7 @@ dependencies = [ "http 0.2.12", "hyper 0.14.32", "log", - "mime", + "mime 0.3.17", "mime_guess", "multer", "percent-encoding", @@ -6554,7 +7403,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.111", "wasm-bindgen-shared", ] @@ -6642,6 +7491,18 @@ dependencies = [ "xcursor", ] +[[package]] +name = "wayland-protocols" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + [[package]] name = "wayland-protocols" version = "0.32.9" @@ -6663,7 +7524,7 @@ dependencies = [ "bitflags 2.10.0", "wayland-backend", "wayland-client", - "wayland-protocols", + "wayland-protocols 0.32.9", "wayland-scanner", ] @@ -6676,7 +7537,7 @@ dependencies = [ "bitflags 2.10.0", "wayland-backend", "wayland-client", - "wayland-protocols", + "wayland-protocols 0.32.9", "wayland-scanner", ] @@ -6689,7 +7550,20 @@ dependencies = [ "bitflags 2.10.0", "wayland-backend", "wayland-client", - "wayland-protocols", + "wayland-protocols 0.32.9", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.31.2", "wayland-scanner", ] @@ -6702,7 +7576,7 @@ dependencies = [ "bitflags 2.10.0", "wayland-backend", "wayland-client", - "wayland-protocols", + "wayland-protocols 0.32.9", "wayland-scanner", ] @@ -6993,14 +7867,15 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "window_clipboard" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5793d0b08c9e6a1240fe9ab2bd8db277487bf92436fd1a6321861a90a1b0cb7e" +version = "0.4.1" +source = "git+https://github.com/pop-os/window_clipboard.git?tag=pop-0.13-2#6b9faab87bea9cebec6ae036906fd67fed254f5f" dependencies = [ "clipboard-win", "clipboard_macos", "clipboard_wayland", "clipboard_x11", + "dnd", + "mime 0.1.0", "raw-window-handle", "thiserror 1.0.69", ] @@ -7025,16 +7900,38 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", +] + [[package]] name = "windows" version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ - "windows-collections", + "windows-collections 0.3.2", "windows-core 0.62.2", - "windows-future", - "windows-numerics", + "windows-future 0.3.2", + "windows-numerics 0.3.1", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", ] [[package]] @@ -7071,6 +7968,19 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -7079,11 +7989,22 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement 0.60.2", "windows-interface 0.59.3", - "windows-link", + "windows-link 0.2.1", "windows-result 0.4.1", "windows-strings 0.5.1", ] +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading 0.1.0", +] + [[package]] name = "windows-future" version = "0.3.2" @@ -7091,8 +8012,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ "windows-core 0.62.2", - "windows-link", - "windows-threading", + "windows-link 0.2.1", + "windows-threading 0.2.1", ] [[package]] @@ -7103,7 +8024,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -7114,7 +8035,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -7125,7 +8046,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -7136,7 +8057,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -7147,7 +8068,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -7158,15 +8079,31 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + [[package]] name = "windows-numerics" version = "0.3.1" @@ -7174,7 +8111,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ "windows-core 0.62.2", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -7183,7 +8120,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-link", + "windows-link 0.2.1", "windows-result 0.4.1", "windows-strings 0.5.1", ] @@ -7206,13 +8143,22 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows-result" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -7225,13 +8171,22 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows-strings" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -7276,7 +8231,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -7316,7 +8271,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link", + "windows-link 0.2.1", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", @@ -7327,13 +8282,22 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows-threading" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -7476,56 +8440,216 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winit" -version = "0.30.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732" +version = "0.31.0-beta.2" +dependencies = [ + "bitflags 2.10.0", + "cfg_aliases", + "cursor-icon", + "dpi", + "libc", + "raw-window-handle", + "rustix 1.1.2", + "smol_str 0.3.2", + "tracing", + "winit-android", + "winit-appkit", + "winit-common", + "winit-core", + "winit-orbital", + "winit-uikit", + "winit-wayland", + "winit-web", + "winit-win32", + "winit-x11", +] + +[[package]] +name = "winit-android" +version = "0.31.0-beta.2" +dependencies = [ + "android-activity", + "bitflags 2.10.0", + "dpi", + "ndk", + "raw-window-handle", + "smol_str 0.3.2", + "tracing", + "winit-core", +] + +[[package]] +name = "winit-appkit" +version = "0.31.0-beta.2" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "dispatch2", + "dpi", + "objc2 0.6.3", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-video", + "objc2-foundation 0.3.2", + "raw-window-handle", + "smol_str 0.3.2", + "tracing", + "winit-common", + "winit-core", +] + +[[package]] +name = "winit-common" +version = "0.31.0-beta.2" +dependencies = [ + "memmap2 0.9.9", + "objc2 0.6.3", + "objc2-core-foundation", + "smol_str 0.3.2", + "tracing", + "winit-core", + "x11-dl", + "xkbcommon-dl", +] + +[[package]] +name = "winit-core" +version = "0.31.0-beta.2" +dependencies = [ + "bitflags 2.10.0", + "cursor-icon", + "dpi", + "keyboard-types", + "raw-window-handle", + "smol_str 0.3.2", + "web-time", +] + +[[package]] +name = "winit-orbital" +version = "0.31.0-beta.2" +dependencies = [ + "bitflags 2.10.0", + "dpi", + "orbclient", + "raw-window-handle", + "redox_syscall", + "smol_str 0.3.2", + "tracing", + "winit-core", +] + +[[package]] +name = "winit-uikit" +version = "0.31.0-beta.2" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "dispatch2", + "dpi", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-ui-kit", + "raw-window-handle", + "smol_str 0.3.2", + "tracing", + "winit-common", + "winit-core", +] + +[[package]] +name = "winit-wayland" +version = "0.31.0-beta.2" dependencies = [ "ahash", - "android-activity", + "bitflags 2.10.0", + "calloop 0.14.3", + "cursor-icon", + "dpi", + "libc", + "memmap2 0.9.9", + "raw-window-handle", + "rustix 1.1.2", + "sctk-adwaita", + "smithay-client-toolkit 0.20.0", + "smol_str 0.3.2", + "tracing", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.32.9", + "wayland-protocols-plasma", + "winit-common", + "winit-core", +] + +[[package]] +name = "winit-web" +version = "0.31.0-beta.2" +dependencies = [ "atomic-waker", "bitflags 2.10.0", - "block2 0.5.1", - "bytemuck", - "calloop 0.13.0", - "cfg_aliases", "concurrent-queue", - "core-foundation 0.9.4", - "core-graphics 0.23.2", "cursor-icon", "dpi", "js-sys", - "libc", - "memmap2", - "ndk", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", - "objc2-ui-kit", - "orbclient", - "percent-encoding", "pin-project", "raw-window-handle", - "redox_syscall 0.4.1", - "rustix 0.38.44", - "sctk-adwaita", - "smithay-client-toolkit 0.19.2", - "smol_str", + "smol_str 0.3.2", "tracing", - "unicode-segmentation", "wasm-bindgen", "wasm-bindgen-futures", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-protocols-plasma", "web-sys", "web-time", - "windows-sys 0.52.0", + "winit-core", +] + +[[package]] +name = "winit-win32" +version = "0.31.0-beta.2" +dependencies = [ + "bitflags 2.10.0", + "cursor-icon", + "dpi", + "raw-window-handle", + "smol_str 0.3.2", + "tracing", + "unicode-segmentation", + "windows-sys 0.59.0", + "winit-core", +] + +[[package]] +name = "winit-x11" +version = "0.31.0-beta.2" +dependencies = [ + "bitflags 2.10.0", + "bytemuck", + "calloop 0.14.3", + "cursor-icon", + "dpi", + "libc", + "percent-encoding", + "raw-window-handle", + "rustix 1.1.2", + "smol_str 0.3.2", + "tracing", + "winit-common", + "winit-core", "x11-dl", "x11rb", "xkbcommon-dl", ] +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "winnow" version = "0.7.14" @@ -7571,6 +8695,7 @@ dependencies = [ "once_cell", "rustix 1.1.2", "x11rb-protocol", + "xcursor", ] [[package]] @@ -7585,6 +8710,17 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" +[[package]] +name = "xkbcommon" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13867d259930edc7091a6c41b4ce6eee464328c6ff9659b7e4c668ca20d4c91e" +dependencies = [ + "libc", + "memmap2 0.8.0", + "xkeysym", +] + [[package]] name = "xkbcommon-dl" version = "0.4.2" @@ -7603,6 +8739,9 @@ name = "xkeysym" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" +dependencies = [ + "bytemuck", +] [[package]] name = "xml-rs" @@ -7662,7 +8801,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", "synstructure", ] @@ -7682,7 +8821,7 @@ dependencies = [ "async-trait", "blocking", "enumflags2", - "event-listener", + "event-listener 5.4.1", "futures-core", "futures-lite", "hex", @@ -7694,22 +8833,46 @@ dependencies = [ "uds_windows", "uuid", "windows-sys 0.61.2", - "winnow", + "winnow 0.7.14", "zbus_macros", "zbus_names", "zvariant", ] +[[package]] +name = "zbus-lockstep" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863" +dependencies = [ + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus-lockstep-macros" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", + "zbus-lockstep", + "zbus_xml", + "zvariant", +] + [[package]] name = "zbus_macros" version = "5.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn", + "syn 2.0.111", "zbus_names", "zvariant", "zvariant_utils", @@ -7717,13 +8880,24 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.2.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" dependencies = [ "serde", - "static_assertions", - "winnow", + "winnow 0.7.14", + "zvariant", +] + +[[package]] +name = "zbus_xml" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "441a0064125265655bccc3a6af6bef56814d9277ac83fce48b1cd7e160b80eac" +dependencies = [ + "quick-xml 0.38.4", + "serde", + "zbus_names", "zvariant", ] @@ -7750,7 +8924,7 @@ checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -7770,7 +8944,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", "synstructure", ] @@ -7810,7 +8984,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -7854,41 +9028,41 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.8.0" +version = "5.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c" +checksum = "326aaed414f04fe839777b4c443d4e94c74e7b3621093bd9c5e649ac8aa96543" dependencies = [ "endi", "enumflags2", "serde", "url", - "winnow", + "winnow 0.7.14", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "5.8.0" +version = "5.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006" +checksum = "ba44e1f8f4da9e6e2d25d2a60b116ef8b9d0be174a7685e55bb12a99866279a7" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn", + "syn 2.0.111", "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "3.2.1" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" dependencies = [ "proc-macro2", "quote", "serde", - "syn", - "winnow", + "syn 2.0.111", + "winnow 0.7.14", ] diff --git a/Cargo.toml b/Cargo.toml index bfaf6f44..e042c89f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,12 +22,14 @@ all-features = true maintenance = { status = "actively-developed" } [features] -default = ["wgpu", "tiny-skia", "crisp", "web-colors", "thread-pool", "linux-theme-detection", "x11", "wayland"] -# Enables the `wgpu` GPU-accelerated renderer with all its default features (Vulkan, Metal, DX12, OpenGL, and WebGPU) wgpu = ["wgpu-bare", "iced_renderer/wgpu"] # Enables the `wgpu` GPU-accelerated renderer with the minimum required features (no backends!) wgpu-bare = ["iced_renderer/wgpu-bare", "iced_widget/wgpu"] # Enables the `tiny-skia` software renderer + + +default = ["tiny-skia", "x11", "wayland", "tokio"] +# Enable the `tiny-skia` software renderer backend tiny-skia = ["iced_renderer/tiny-skia"] # Enables the `image` widget image = ["image-without-codecs", "image/default"] @@ -39,11 +41,11 @@ svg = ["iced_widget/svg"] canvas = ["iced_widget/canvas"] # Enables the `qr_code` widget qr_code = ["iced_widget/qr_code"] -# Enables the `markdown` widget -markdown = ["iced_widget/markdown"] -# Enables lazy widgets -lazy = ["iced_widget/lazy"] + + # Enables debug metrics in native platforms (press F12) +lazy = ["iced_widget/lazy"] # Enables lazy widgets +markdown = ["iced_widget/markdown"] # Enables the `markdown` widget debug = ["iced_winit/debug", "dep:iced_devtools"] # Enables time-travel debugging (very experimental!) time-travel = ["debug", "iced_devtools/time-travel"] @@ -51,10 +53,15 @@ time-travel = ["debug", "iced_devtools/time-travel"] hot = ["debug", "iced_debug/hot"] # Enables the tester developer tool for recording and playing tests (press F12) tester = ["dep:iced_tester"] + + # Enables the `thread-pool` futures executor as the `executor::Default` on native platforms thread-pool = ["iced_futures/thread-pool"] + # 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_accessibility?/async-io"] # Enables `smol` as the `executor::Default` on native platforms smol = ["iced_futures/smol"] # Enables querying system information @@ -62,18 +69,30 @@ sysinfo = ["iced_winit/sysinfo"] # Enables broken "sRGB linear" blending to reproduce color management of the Web web-colors = ["iced_renderer/web-colors"] # Enables pixel snapping for crisp edges by default (can cause jitter!) + + +# Enables the `widget::selector` module crisp = ["iced_core/crisp", "iced_widget/crisp"] # Enables the WebGL backend + + webgl = ["iced_renderer/webgl"] # Enables syntax highlighting highlighter = ["iced_highlighter", "iced_widget/highlighter"] -# Enables the `widget::selector` module selector = ["iced_runtime/selector"] -# Enables the advanced module -advanced = ["iced_core/advanced", "iced_widget/advanced"] -# Embeds Fira Sans into the final application; useful for testing and Wasm builds -fira-sans = ["iced_renderer/fira-sans"] + + # Enables basic text shaping by default +fira-sans = [ + "iced_renderer/fira-sans", +] # Embeds Fira Sans into the final application; useful for testing and Wasm builds +advanced = [ + "iced_core/advanced", + "iced_widget/advanced", +] # Enables the advanced module +multi-window = [ + "iced_winit?/multi-window", +] # Enables experimental multi-window support. basic-shaping = ["iced_core/basic-shaping"] # Enables advanced text shaping by default advanced-shaping = ["iced_core/advanced-shaping"] @@ -83,32 +102,48 @@ strict-assertions = ["iced_renderer/strict-assertions"] unconditional-rendering = ["iced_winit/unconditional-rendering"] # Enables support for the `sipper` library sipper = ["iced_runtime/sipper"] + + # Enables Linux system theme detection linux-theme-detection = ["iced_winit/linux-theme-detection"] # Enables the Unix X11 backend x11 = ["iced_renderer/x11", "iced_winit/x11"] -# Enables the Unix Wayland backend -wayland = ["iced_renderer/wayland", "iced_winit/wayland"] - +# 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_devtools.workspace = true iced_debug.workspace = true + +iced_program.workspace = true iced_core.workspace = true iced_futures.workspace = true iced_renderer.workspace = true iced_runtime.workspace = true iced_widget.workspace = true iced_winit.workspace = true - -iced_devtools.workspace = true iced_devtools.optional = true - iced_tester.workspace = true iced_tester.optional = 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 @@ -152,7 +187,9 @@ members = [ "widget", "winit", "examples/*", + "accessibility", ] +exclude = ["examples/integration"] [workspace.package] version = "0.14.0" @@ -184,16 +221,28 @@ iced_tiny_skia = { version = "0.14.0", path = "tiny_skia", default-features = fa iced_wgpu = { version = "0.14.0", path = "wgpu", default-features = false } iced_widget = { version = "0.14.0", path = "widget" } iced_winit = { version = "0.14.0", path = "winit", default-features = false } - bincode = "1.3" -bitflags = "2.0" +cargo-hot = { version = "0.1", package = "cargo-hot-protocol" } +futures = { version = "0.3", default-features = false, features = [ + "std", + "async-await", +] } +glam = "0.25" +iced_accessibility = { version = "0.1", path = "accessibility" } + +async-std = "1.0" + + +bitflags = "2.5" bytemuck = { version = "1.0", features = ["derive"] } bytes = "1.6" -cargo-hot = { version = "0.1", package = "cargo-hot-protocol" } -cosmic-text = "0.15" -cryoglyph = "0.1" -futures = { version = "0.3", default-features = false, features = ["std", "async-await"] } -glam = "0.25" +cosmic-text = { git = "https://github.com/pop-os/cosmic-text.git" } +# cosmic-text = "0.10" +dark-light = "1.0" +cryoglyph = { package = "cryoglyph", git = "https://github.com/pop-os/glyphon.git", branch = "iced-0.14" } +resvg = "0.45" +# glyphon = { package = "iced_glyphon", path = "../../../glyphon" } +web-sys = "0.3.69" guillotiere = "0.6" half = "2.2" image = { version = "0.25", default-features = false } @@ -211,7 +260,6 @@ png = "0.18" pulldown-cmark = "0.12" qrcode = { version = "0.13", default-features = false } raw-window-handle = "0.6" -resvg = "0.45" rfd = "0.16" rustc-hash = "2.0" semver = "1.0" @@ -219,28 +267,56 @@ serde = "1.0" sha2 = "0.10" sipper = "0.1" smol = "2" -smol_str = "0.2" -softbuffer = { version = "0.4", default-features = false } +smol_str = "0.3" sysinfo = "0.33" thiserror = "2" -tiny-skia = { version = "0.11", default-features = false, features = ["std", "simd"] } +tiny-skia = { version = "0.11", default-features = false, features = [ + "std", + "simd", +] } +sctk = { package = "smithay-client-toolkit", version = "0.19.1" } +softbuffer = { git = "https://github.com/pop-os/softbuffer", tag = "cosmic-4.0" } +syntect = "5.2" tokio = "1.0" tracing = "0.1" -two-face = { version = "0.4", default-features = false, features = ["syntect-default-fancy"] } +two-face = { version = "0.4", default-features = false, features = [ + "syntect-default-fancy", +] } unicode-segmentation = "1.0" url = "2.5" wasm-bindgen-futures = "0.4" wasmtimer = "0.4.2" -web-sys = "0.3.69" web-time = "1.1" -wgpu = { version = "27.0", default-features = false, features = ["std", "wgsl"] } -window_clipboard = { version = "0.5", default-features = false } -winit = { version = "0.30", default-features = false, features = ["rwh_06"] } +wgpu = { version = "27.0", default-features = false, features = [ + "std", + "wgsl", +] } +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" + +window_clipboard = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-0.13-2" } +dnd = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-0.13-2" } +mime = { git = "https://github.com/pop-os/window_clipboard.git", tag = "pop-0.13-2" } +# window_clipboard = { path = "../../window_clipboard", tag = "pop-0.13-2" } +# dnd = { path = "../../window_clipboard/dnd", tag = "pop-0.13" } +# mime = { path = "../../window_clipboard/mime", tag = "pop-0.13" } +# winit = { git = "https://github.com/pop-os/winit.git", tag = "iced-xdg-surface-0.13" } +winit = { path = "../../winit/winit" } +winit-core = { path = "../../winit/winit-core" } +cursor-icon = "1.1.0" +# 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 = "deny", priority = -1 } -missing_docs = "deny" -unsafe_code = "deny" unused_results = "deny" [workspace.lints.clippy] @@ -263,3 +339,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 00000000..c549d01a --- /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 00000000..031c1053 --- /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::Node, 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 00000000..e4042e34 --- /dev/null +++ b/accessibility/src/id.rs @@ -0,0 +1,228 @@ +//! Widget and Window IDs. + +use std::borrow::{self, Cow}; +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) + } +} + +impl From<&'static str> for Id { + fn from(value: &'static str) -> Self { + Id(Internal::Custom(Id::next(), Cow::Borrowed(value))) + } +} + +impl<'a> From for Id { + fn from(value: String) -> Self { + Id(Internal::Custom(Id::next(), Cow::Owned(value.to_string()))) + } +} + +// 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 00000000..273a4737 --- /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 00000000..ceb2fb3f --- /dev/null +++ b/accessibility/src/node.rs @@ -0,0 +1,41 @@ +use crate::A11yId; + +#[derive(Debug, Clone, PartialEq)] +pub struct A11yNode { + node: accesskit::Node, + id: A11yId, +} + +impl A11yNode { + pub fn new>(node: accesskit::Node, id: T) -> Self { + Self { + node, + id: id.into(), + } + } + + pub fn id(&self) -> &A11yId { + &self.id + } + + pub fn node_mut(&mut self) -> &mut accesskit::Node { + &mut self.node + } + + pub fn node(&self) -> &accesskit::Node { + &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) + } +} diff --git a/accessibility/src/traits.rs b/accessibility/src/traits.rs new file mode 100644 index 00000000..88e1d920 --- /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 31296446..0164ae39 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -18,6 +18,8 @@ advanced = [] crisp = [] basic-shaping = [] advanced-shaping = [] +a11y = ["iced_accessibility"] +wayland = ["sctk"] [dependencies] bitflags.workspace = true @@ -34,3 +36,22 @@ web-time.workspace = true serde.workspace = true serde.optional = true serde.features = ["derive"] +# 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) +[dependencies.iced_accessibility] +version = "0.1.0" +path = "../accessibility" +optional = true + +[target.'cfg(windows)'.dependencies] +raw-window-handle.workspace = true + +[dev-dependencies] +approx = "0.5" diff --git a/core/src/border.rs b/core/src/border.rs index 232211db..0cccd124 100644 --- a/core/src/border.rs +++ b/core/src/border.rs @@ -276,3 +276,54 @@ impl std::ops::Mul for Radius { } } } + +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 5df3e267..caf0b938 100644 --- a/core/src/clipboard.rs +++ b/core/src/clipboard.rs @@ -1,5 +1,61 @@ //! Access the clipboard. +use std::{any::Any, sync::Arc}; + +use dnd::{DndAction, DndDestinationRectangle, DndSurface}; +use mime::{self, AllowedMimeTypes, AsMimeTypes, ClipboardStoreData}; + +use crate::{Element, Vector, widget::tree::State, window}; + +#[derive(Debug)] +pub struct IconSurface { + pub element: E, + pub state: State, + pub offset: Vector, +} + +pub type DynIconSurface = IconSurface>; + +impl IconSurface> { + pub fn new( + element: Element<'static, (), T, R>, + state: State, + offset: Vector, + ) -> Self { + Self { + element, + state, + offset, + } + } + + fn upcast(self) -> DynIconSurface { + IconSurface { + element: Box::new(self.element), + state: self.state, + offset: self.offset, + } + } +} + +impl DynIconSurface { + /// Downcast `element` to concrete type `Element<(), T, R>` + /// + /// Panics if type doesn't match + pub fn downcast( + self, + ) -> IconSurface> { + IconSurface { + element: *self + .element + .downcast() + .expect("drag-and-drop icon surface has invalid element type"), + state: self.state, + offset: self.offset, + } + } +} + /// A buffer for short-term storage and transfer within and between /// applications. pub trait Clipboard { @@ -8,6 +64,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 +133,25 @@ 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>>, + content: Box, + actions: DndAction, +) { + clipboard.start_dnd( + internal, + source_surface, + icon_surface.map(IconSurface::upcast), + content, + actions, + ); +} + /// A null implementation of the [`Clipboard`] trait. #[derive(Debug, Clone, Copy)] pub struct Null; @@ -32,3 +163,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 52cd6f61..6a45aa83 100644 --- a/core/src/element.rs +++ b/core/src/element.rs @@ -1,3 +1,5 @@ +use crate::event::{self, Event}; +use crate::id::Id; use crate::layout; use crate::mouse; use crate::overlay; @@ -5,11 +7,11 @@ use crate::renderer; use crate::widget; use crate::widget::tree::{self, Tree}; use crate::{ - Border, Clipboard, Color, Event, Layout, Length, Rectangle, Shell, Size, - Vector, Widget, + Border, Clipboard, Color, Layout, Length, Rectangle, Shell, Size, Vector, + Widget, }; -use std::borrow::Borrow; +use std::borrow::{Borrow, BorrowMut}; /// A generic [`Widget`]. /// @@ -238,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>, @@ -277,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 { @@ -376,6 +409,35 @@ where .overlay(tree, layout, renderer, viewport, 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> { @@ -420,7 +482,7 @@ where self.element.widget.children() } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { self.element.widget.diff(tree); } @@ -532,8 +594,31 @@ where 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 +} impl<'a, T, Message, Theme, Renderer> From> for Element<'a, Message, Theme, Renderer> where diff --git a/core/src/event.rs b/core/src/event.rs index 7f0ab914..e5ed162a 100644 --- a/core/src/event.rs +++ b/core/src/event.rs @@ -1,10 +1,15 @@ //! Handle events of a user interface. use crate::input_method; +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 @@ -27,6 +32,26 @@ pub enum Event { /// An input method event InputMethod(input_method::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 00000000..c1928ad3 --- /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 00000000..0e908d67 --- /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 00000000..c5024e85 --- /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 00000000..ff925870 --- /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 00000000..3da4374e --- /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 00000000..db99566d --- /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 00000000..70085dac --- /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 00000000..60862670 --- /dev/null +++ b/core/src/id.rs @@ -0,0 +1,170 @@ +//! 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)) + } +} + +impl From<&'static str> for Id { + fn from(value: &'static str) -> Self { + Id(Internal::Custom(Id::next(), borrow::Cow::Borrowed(value))) + } +} + +impl<'a> From for Id { + fn from(value: String) -> Self { + Id(Internal::Custom( + Id::next(), + borrow::Cow::Owned(value.to_string()), + )) + } +} + +// 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 f49d41be..fb0cdc5c 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -48,6 +48,7 @@ impl Image { border_radius: border::Radius::default(), opacity: 1.0, snap: false, + // border_radius: [0.0; 4], } } @@ -83,7 +84,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. diff --git a/core/src/keyboard/key.rs b/core/src/keyboard/key.rs index 117b7e6a..37a91597 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.30/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), @@ -129,6 +130,7 @@ impl From for Key { /// /// [`winit`]: https://docs.rs/winit/0.30/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 04ba7e44..9ff985fe 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -37,6 +37,8 @@ mod background; mod color; mod content_fit; mod element; +#[cfg(not(feature = "a11y"))] +pub mod id; mod length; mod pixels; mod point; @@ -61,6 +63,9 @@ pub use element::Element; pub use event::Event; pub use font::Font; pub use gradient::Gradient; + +#[cfg(feature = "a11y")] +pub use iced_accessibility::id; pub use image::Image; pub use input_method::InputMethod; pub use layout::Layout; diff --git a/core/src/mouse/click.rs b/core/src/mouse/click.rs index 12039d79..5424d25f 100644 --- a/core/src/mouse/click.rs +++ b/core/src/mouse/click.rs @@ -83,7 +83,6 @@ impl Click { } else { None }; - self.position.distance(new_position) < 6.0 && duration .map(|duration| duration.as_millis() <= 300) diff --git a/core/src/overlay.rs b/core/src/overlay.rs index 76fe321a..b29934ec 100644 --- a/core/src/overlay.rs +++ b/core/src/overlay.rs @@ -10,7 +10,7 @@ pub use nested::Nested; use crate::layout; use crate::mouse; use crate::renderer; -use crate::widget; +use crate::widget::Operation; use crate::widget::Tree; use crate::{Clipboard, Event, Layout, Rectangle, Shell, Size, Vector}; @@ -37,12 +37,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 1a4ce672..f80af3b2 100644 --- a/core/src/overlay/group.rs +++ b/core/src/overlay/group.rs @@ -3,7 +3,8 @@ use crate::mouse; use crate::overlay; use crate::renderer; use crate::widget; -use crate::{Clipboard, Event, Layout, Overlay, Shell, Size}; +use crate::widget::Operation; +use crate::{Clipboard, Event, Layout, Overlay, Point, Rectangle, Shell, Size}; /// An [`Overlay`] container that displays multiple overlay [`overlay::Element`] /// children. diff --git a/core/src/padding.rs b/core/src/padding.rs index 91f7a3b7..68e30df8 100644 --- a/core/src/padding.rs +++ b/core/src/padding.rs @@ -213,6 +213,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 { @@ -235,6 +246,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.x(), padding.y()) diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index e417d08d..0fa013d6 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -188,6 +188,18 @@ impl Rectangle { /// Returns true if the current [`Rectangle`] is within the given /// `container`. Includes the right and bottom edges. + /// 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 { self.x >= container.x && self.y >= container.y @@ -195,6 +207,16 @@ impl Rectangle { && self.y + self.height <= container.y + container.height } + /// 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 89157c56..eac7b069 100644 --- a/core/src/renderer.rs +++ b/core/src/renderer.rs @@ -104,14 +104,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 289fbe79..22b89ada 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -41,6 +41,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'; @@ -56,7 +57,7 @@ impl text::Renderer for () { } fn default_size(&self) -> Pixels { - Pixels(16.0) + Pixels(14.0) } fn fill_paragraph( @@ -77,6 +78,8 @@ impl text::Renderer for () { ) { } + fn fill_raw(&mut self, _raw: Self::Raw) {} + fn fill_text( &mut self, _paragraph: Text, diff --git a/core/src/settings.rs b/core/src/settings.rs index 1b86c50e..e3eb433a 100644 --- a/core/src/settings.rs +++ b/core/src/settings.rs @@ -40,6 +40,9 @@ pub struct Settings { /// /// By default, it is enabled. pub vsync: 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 { @@ -48,9 +51,20 @@ impl Default for Settings { id: None, fonts: Vec::new(), default_font: Font::default(), - default_text_size: Pixels(16.0), - antialiasing: true, - vsync: true, + vsync: false, + 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 { + id: settings.id, + fonts: settings.fonts, } } } diff --git a/core/src/svg.rs b/core/src/svg.rs index 4a25b7e7..fe8701e6 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 dfe7cfdb..4ac4fb2a 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -214,7 +214,7 @@ impl LineHeight { impl Default for LineHeight { fn default() -> Self { - Self::Relative(1.3) + Self::Relative(1.4) } } @@ -299,6 +299,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; @@ -363,6 +366,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.rs b/core/src/theme.rs index 22cf632a..ead3015a 100644 --- a/core/src/theme.rs +++ b/core/src/theme.rs @@ -227,6 +227,9 @@ pub struct Style { /// The default text [`Color`] of the application. pub text_color: Color, + + /// The default icon [`iced_core::Color`] of the application. + pub icon_color: Color, } /// The default blank style of a theme. @@ -332,5 +335,6 @@ pub fn default(theme: &Theme) -> Style { Style { background_color: palette.background.base.color, text_color: palette.background.base.text, + icon_color: palette.background.base.text, } } diff --git a/core/src/theme/palette.rs b/core/src/theme/palette.rs index 4cd66c9e..57f4ff82 100644 --- a/core/src/theme/palette.rs +++ b/core/src/theme/palette.rs @@ -421,12 +421,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`]. @@ -438,9 +441,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 31755306..b1e027ac 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; @@ -92,7 +90,7 @@ where } /// Reconciles the [`Widget`] with the provided [`Tree`]. - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { tree.children.clear(); } @@ -147,4 +145,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/operation.rs b/core/src/widget/operation.rs index 9e7b0d34..40f01064 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; @@ -525,7 +526,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 5b066f2d..25a67f68 100644 --- a/core/src/widget/operation/focusable.rs +++ b/core/src/widget/operation/focusable.rs @@ -1,5 +1,6 @@ //! Operate on widgets that can be focused. use crate::Rectangle; +use crate::id::IdEq; use crate::widget::Id; use crate::widget::operation::{self, Operation, Outcome}; @@ -39,7 +40,7 @@ pub fn focus(target: Id) -> impl Operation { state: &mut dyn Focusable, ) { 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 00000000..1286abaf --- /dev/null +++ b/core/src/widget/operation/search_id.rs @@ -0,0 +1,49 @@ +//! Search for widgets with the target Id. + +use super::Operation; +use crate::{ + Rectangle, + id::Id, + widget::operation::{Outcome, focusable::Count}, +}; + +/// 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, + id: Option<&Id>, + _bounds: Rectangle, + _state: &mut dyn std::any::Any, + ) { + if Some(&self.target) == id { + self.found = true; + } + } + + fn finish(&self) -> Outcome { + if self.found { + Outcome::Some(self.target.clone()) + } else { + Outcome::None + } + } + + fn traverse( + &mut self, + operate: &mut dyn FnMut(&mut dyn Operation), + ) { + operate(self); + } + } + + Find { + found: false, + target, + } +} diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 59f26b0d..c16f5d5d 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -24,13 +24,14 @@ 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, Rectangle, Size, Theme, Widget, }; +use std::borrow::Cow; pub use text::{Alignment, LineHeight, Shaping, Wrapping}; /// A bunch of text. @@ -60,6 +61,7 @@ where Theme: Catalog, Renderer: text::Renderer, { + id: crate::widget::Id, fragment: text::Fragment<'a>, format: Format, class: Theme::Class<'a>, @@ -73,6 +75,7 @@ where /// Create a new fragment of [`Text`] with the given contents. pub fn new(fragment: impl text::IntoFragment<'a>) -> Self { Text { + id: crate::widget::Id::unique(), fragment: fragment.into_fragment(), format: Format::default(), class: Theme::default(), @@ -263,6 +266,50 @@ where ) { operation.text(None, layout.bounds(), &self.fragment); } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + _state: &Tree, + _: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::{ + A11yTree, + accesskit::{Live, Node, Rect, Role}, + }; + + 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 = Node::new(Role::Paragraph); + + // TODO is the name likely different from the content? + node.set_label(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; + } } /// The format of some [`Text`]. @@ -370,6 +417,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 2600cfc6..00b0be1e 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::Widget; - +use crate::id::{Id, Internal}; 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(_, old_name))), + Some(Id(Internal::Custom(_, 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 { - *self = Self::new(new); + needs_reset = true; + } + if needs_reset { + *self = Self::new(borrowed); + let borrowed = new.borrow_mut(); + borrowed.diff(self); + } else { + 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/event.rs b/core/src/window/event.rs index b5815935..dae9306c 100644 --- a/core/src/window/event.rs +++ b/core/src/window/event.rs @@ -53,7 +53,7 @@ pub enum Event { /// ## Platform-specific /// /// - **Wayland:** Not implemented. - FileHovered(PathBuf), + FileHovered(Vec), /// A file has been dropped into the window. /// @@ -63,7 +63,7 @@ pub enum Event { /// ## Platform-specific /// /// - **Wayland:** Not implemented. - FileDropped(PathBuf), + FileDropped(Vec), /// A file was hovered, but has exited the window. /// diff --git a/core/src/window/id.rs b/core/src/window/id.rs index ee0a4c59..03e13ccc 100644 --- a/core/src/window/id.rs +++ b/core/src/window/id.rs @@ -10,9 +10,20 @@ pub struct Id(u64); static COUNT: AtomicU64 = AtomicU64::new(1); impl Id { + /// No window will match this Id + pub const NONE: Id = Id(0); + pub const RESERVED: Id = Id(1); + /// 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(2, atomic::Ordering::Relaxed)) + } else if id.0 == 1 { + Id(COUNT.fetch_add(1, atomic::Ordering::Relaxed)) + } else { + id + } } } diff --git a/core/src/window/screenshot.rs b/core/src/window/screenshot.rs index 0bd03dc4..cc4fe00e 100644 --- a/core/src/window/screenshot.rs +++ b/core/src/window/screenshot.rs @@ -5,7 +5,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 RGBA bytes of the [`Screenshot`]. diff --git a/core/src/window/settings.rs b/core/src/window/settings.rs index 2c21b888..115becbc 100644 --- a/core/src/window/settings.rs +++ b/core/src/window/settings.rs @@ -101,8 +101,8 @@ pub struct Settings { } impl Default for Settings { - fn default() -> Self { - Self { + fn default() -> Settings { + Settings { size: Size::new(1024.0, 768.0), maximized: false, fullscreen: false, diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index 61b7cb98..5a4d873b 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -110,7 +110,7 @@ where state.style(&self.program, theme) } - fn scale_factor(&self, state: &Self::State, window: window::Id) -> f32 { + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { state.scale_factor(&self.program, window) } } @@ -405,7 +405,7 @@ where program.style(self.state(), theme) } - pub fn scale_factor(&self, program: &P, window: window::Id) -> f32 { + pub fn scale_factor(&self, program: &P, window: window::Id) -> f64 { program.scale_factor(self.state(), window) } diff --git a/examples/counter/Cargo.toml b/examples/counter/Cargo.toml index 7d88a745..22f86064 100644 --- a/examples/counter/Cargo.toml +++ b/examples/counter/Cargo.toml @@ -2,7 +2,7 @@ name = "counter" version = "0.1.0" authors = ["Héctor Ramón Jiménez "] -edition = "2024" +edition = "2021" publish = false [dependencies] @@ -10,7 +10,4 @@ iced.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] iced.workspace = true -iced.features = ["webgl", "fira-sans"] - -[dev-dependencies] -iced_test.workspace = true +iced.features = ["webgl"] diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml index d3f408d6..0b1995c2 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 3e5f8161..ca349e4e 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 7c368659..2ce36da3 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 246d726f..e33d8606 100644 --- a/examples/integration/Cargo.toml +++ b/examples/integration/Cargo.toml @@ -27,4 +27,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 1f2f3805..0e966547 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::{bottom, column, row, slider, text, text_input}; use iced_winit::core::{Color, Element, Theme}; diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index 7fbf85f2..baf3e644 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -4,6 +4,8 @@ mod scene; use controls::Controls; use scene::Scene; +use iced_wgpu::core::window::Id; +use iced_wgpu::graphics::Viewport; use iced_wgpu::graphics::{Shell, Viewport}; use iced_wgpu::{Engine, Renderer, wgpu}; use iced_winit::Clipboard; @@ -54,7 +56,10 @@ 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 @@ -183,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 f9264aa5..f7a70dee 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 a4566134..1e8065eb 100644 --- a/examples/loading_spinners/Cargo.toml +++ b/examples/loading_spinners/Cargo.toml @@ -7,6 +7,6 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["advanced", "canvas"] +iced.features = ["advanced", "canvas", "winit"] lyon_algorithms = "1.0" diff --git a/examples/loupe/src/main.rs b/examples/loupe/src/main.rs index b140d721..6d32a62a 100644 --- a/examples/loupe/src/main.rs +++ b/examples/loupe/src/main.rs @@ -87,7 +87,7 @@ mod loupe { self.content.as_widget().children() } - fn diff(&self, tree: &mut widget::Tree) { + fn diff(&mut self, tree: &mut widget::Tree) { self.content.as_widget().diff(tree); } diff --git a/examples/multi_window/Cargo.toml b/examples/multi_window/Cargo.toml index 738f4ba1..cf2c5394 100644 --- a/examples/multi_window/Cargo.toml +++ b/examples/multi_window/Cargo.toml @@ -6,4 +6,11 @@ edition = "2024" publish = false [dependencies] -iced = { path = "../..", features = ["debug"] } +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 a834a617..b2ed47fa 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -28,6 +28,7 @@ struct Window { scale_input: String, current_scale: f32, theme: Theme, + input_id: text_input::Id, } #[derive(Debug, Clone)] diff --git a/examples/pokedex/Cargo.toml b/examples/pokedex/Cargo.toml index 4913e7a0..22fcfe13 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 cbea961f..7685d462 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/src/main.rs b/examples/scrollable/src/main.rs index 0b183024..a67489ed 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -1,8 +1,10 @@ +use iced::widget::scrollable::{self, Properties, Scrollbar, Scroller}; use iced::widget::{ button, column, container, operation, progress_bar, radio, row, scrollable, slider, space, text, }; use iced::{Border, Center, Color, Element, Fill, Task, Theme}; +use iced_core::id::Id; 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 00000000..e118b277 --- /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 00000000..8c4a6245 --- /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 00000000..ffa0dad6 --- /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 00000000..f2d1a5e5 --- /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 00000000..512c00a0 --- /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 00000000..47560ee3 --- /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 00000000..c383ef3a --- /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 00000000..8d42973f --- /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 00000000..30949510 --- /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 00000000..21a9a75f --- /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 00000000..315f1681 --- /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 00000000..4b8095ce --- /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 00000000..09119117 --- /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 00000000..9933ee09 --- /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 00000000..1638e2e1 --- /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 00000000..9c2598b9 --- /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 00000000..dd7ce53d --- /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 00000000..ee5570fb --- /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 00000000..ce8e2c3f --- /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 b8a54817..10d63a53 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 6c99e006..00000000 --- a/examples/system_information/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "system_information" -version = "0.1.0" -authors = ["Richard "] -edition = "2024" -publish = false - -[dependencies] -iced.workspace = true -iced.features = ["sysinfo"] - -bytesize = "1.1" diff --git a/examples/system_information/src/main.rs b/examples/system_information/src/main.rs deleted file mode 100644 index 4de4ab23..00000000 --- a/examples/system_information/src/main.rs +++ /dev/null @@ -1,146 +0,0 @@ -use iced::system; -use iced::widget::{button, center, column, text}; -use iced::{Element, Task}; - -pub fn main() -> iced::Result { - iced::application(Example::new, 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 new() -> (Self, Task) { - ( - Self::Loading, - system::information().map(Message::InformationReceived), - ) - } - - fn update(&mut self, message: Message) -> Task { - match message { - Message::Refresh => { - let (state, refresh) = Self::new(); - - *self = state; - - refresh - } - Message::InformationReceived(information) => { - *self = Self::Loaded { information }; - - Task::none() - } - } - } - - fn view(&self) -> Element<'_, Message> { - 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_as(true); - - 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_as(true); - - 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 5faaea60..3b06346b 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -166,7 +166,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::mouse; use iced::time::{self, Duration, Instant}; @@ -319,7 +321,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 @@ -341,8 +343,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 f47cd861..60e6a6f7 100644 --- a/examples/todos/Cargo.toml +++ b/examples/todos/Cargo.toml @@ -10,7 +10,7 @@ tester = ["iced/tester"] [dependencies] iced.workspace = true -iced.features = ["tokio", "debug", "time-travel"] +iced.features = ["tokio", "debug", "time-travel", "winit"] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" @@ -35,6 +35,14 @@ iced_test.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 39f90602..eddc4728 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -5,8 +5,8 @@ use iced::widget::{ }; use iced::window; use iced::{ - Application, Center, Element, Fill, Font, Function, Preset, Program, - Subscription, Task as Command, Theme, + Center, Element, Fill, Font, Function, Preset, Program, Subscription, + Task as Command, Theme, application::Application, }; use serde::{Deserialize, Serialize}; diff --git a/examples/tooltip/Cargo.toml b/examples/tooltip/Cargo.toml index 95b88e0d..7b9d96aa 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 de9cc724..51eed88f 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 1968843a..5292d0e8 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -10,6 +10,7 @@ pub fn main() -> iced::Result { .subscription(WebSocket::subscription) .run() } +use iced::id::Id; struct WebSocket { messages: Vec, diff --git a/futures/Cargo.toml b/futures/Cargo.toml index 4990fbec..275d45c2 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/runtime.rs b/futures/src/runtime.rs index 927b35db..e5e8ab77 100644 --- a/futures/src/runtime.rs +++ b/futures/src/runtime.rs @@ -68,7 +68,7 @@ where let sender = self.sender.clone(); let future = stream.map(Ok).forward(sender).map(|result| match result { - Ok(()) => (), + Ok(()) => {} Err(error) => { log::warn!( "Stream could not run until completion: {error}" diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs index 90c7b6fe..ba5698a1 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -116,10 +116,9 @@ impl Window for T where /// /// This is just a convenient super trait of the `raw-window-handle` /// trait. -pub trait Display: HasDisplayHandle + MaybeSend + MaybeSync + 'static {} +pub trait Display: HasDisplayHandle + 'static {} -impl Display for T where T: HasDisplayHandle + MaybeSend + MaybeSync + 'static -{} +impl Display for T where T: HasDisplayHandle + 'static {} /// Defines the default compositor of a renderer. pub trait Default { @@ -147,6 +146,18 @@ pub enum SurfaceError { /// Acquiring a texture failed with a generic error. #[error("Acquiring a texture failed with a generic error")] Other, + /// 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 867ae4fc..37b25fc9 100644 --- a/graphics/src/geometry/text.rs +++ b/graphics/src/geometry/text.rs @@ -58,12 +58,41 @@ impl Text { wrapping: Wrapping::default(), }); + let mut buffer = cosmic_text::BufferLine::new( + &self.content, + cosmic_text::LineEnding::default(), + cosmic_text::AttrsList::new(&text::to_attributes(self.font)), + text::to_shaping(self.shaping, &self.content), + ); + + let mut font_system = text::font_system().write().unwrap(); + let layout = buffer.layout( + font_system.raw(), + self.size.0, + None, + cosmic_text::Wrap::None, + None, + 8, + cosmic_text::Hinting::Disabled, + ); + let translation_x = match self.align_x { - Alignment::Default | Alignment::Left | Alignment::Justified => { + Alignment::Left | Alignment::Default | Alignment::Justified => { self.position.x } - Alignment::Center => self.position.x - paragraph.min_width() / 2.0, - Alignment::Right => self.position.x - paragraph.min_width(), + Alignment::Center | Alignment::Right => { + let mut line_width = 0.0f32; + + for line in layout.iter() { + line_width = line_width.max(line.w); + } + + if self.align_x == Alignment::Center { + self.position.x - line_width / 2.0 + } else { + self.position.x - line_width + } + } }; let translation_y = { @@ -171,12 +200,12 @@ impl Default for Text { position: Point::ORIGIN, max_width: f32::INFINITY, color: Color::BLACK, - size: Pixels(16.0), - line_height: LineHeight::Relative(1.2), + size: Pixels(14.0), + line_height: LineHeight::default(), font: Font::default(), align_x: Alignment::Default, align_y: alignment::Vertical::Top, - shaping: Shaping::default(), + shaping: Shaping::Advanced, } } } diff --git a/graphics/src/image.rs b/graphics/src/image.rs index c05d7499..e5fe9281 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -2,6 +2,8 @@ #[cfg(feature = "image")] use crate::core::Bytes; +use crate::core::Color; +use crate::core::Radians; use crate::core::Rectangle; use crate::core::image; use crate::core::svg; @@ -117,7 +119,14 @@ pub fn load(handle: &image::Handle) -> Result { let (width, height, pixels) = match handle { image::Handle::Path(_, path) => { - let image = ::image::open(path).map_err(to_error)?; + use std::sync::Arc; + + let image = ::image::ImageReader::open(&path) + .map_err(|e| image::Error::Inaccessible(Arc::new(e)))? + .with_guessed_format() + .map_err(|e| image::Error::Invalid(Arc::new(e)))? + .decode() + .map_err(|e| image::Error::Invalid(Arc::new(e)))?; let operation = std::fs::File::open(path) .ok() diff --git a/graphics/src/settings.rs b/graphics/src/settings.rs index 7432a78b..121953bb 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. @@ -27,7 +27,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, vsync: true, } diff --git a/graphics/src/text.rs b/graphics/src/text.rs index 35ff24fb..8a5ed946 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -323,9 +323,18 @@ fn to_align(alignment: Alignment) -> Option { /// Converts some [`Shaping`] strategy to a [`cosmic_text::Shaping`] strategy. pub fn to_shaping(shaping: Shaping, text: &str) -> cosmic_text::Shaping { + to_shaping_ascii(shaping, text.is_ascii()) +} + +#[inline] +/// Converts some [`Shaping`] strategy to a [`cosmic_text::Shaping`] strategy. +pub fn to_shaping_ascii( + shaping: Shaping, + is_ascii: bool, +) -> cosmic_text::Shaping { match shaping { Shaping::Auto => { - if text.is_ascii() { + if is_ascii { cosmic_text::Shaping::Basic } else { cosmic_text::Shaping::Advanced diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index 99c592e7..f6750922 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -155,12 +155,17 @@ impl core::text::Paragraph for Paragraph { } else { attrs }; - (span.text.as_ref(), attrs.metadata(i)) }), &text::to_attributes(text.font), - cosmic_text::Shaping::Advanced, - None, + text::to_shaping_ascii(text.shaping, false), // TODO iterate over spans and check if is ascii, if default shaping is chosen + match text.align_x { + Alignment::Default => None, + Alignment::Left => Some(cosmic_text::Align::Left), + Alignment::Center => Some(cosmic_text::Align::Center), + Alignment::Right => Some(cosmic_text::Align::Right), + Alignment::Justified => Some(cosmic_text::Align::Justified), + }, ); let min_bounds = diff --git a/graphics/src/viewport.rs b/graphics/src/viewport.rs index ea4cd8b2..b27276d5 100644 --- a/graphics/src/viewport.rs +++ b/graphics/src/viewport.rs @@ -5,19 +5,36 @@ use crate::core::{Size, Transformation}; pub struct Viewport { physical_size: Size, logical_size: Size, - scale_factor: f32, + scale_factor: f64, projection: Transformation, } 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 as f64).ceil() as u32, + (size.height as f64 * scale_factor as f64).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: f32) -> Viewport { + pub fn with_physical_size(size: Size, scale_factor: f64) -> Viewport { Viewport { physical_size: size, logical_size: Size::new( - size.width as f32 / scale_factor, - size.height as f32 / scale_factor, + (size.width as f64 / scale_factor) as f32, + (size.height as f64 / scale_factor) as f32, ), scale_factor, projection: Transformation::orthographic(size.width, size.height), @@ -45,7 +62,7 @@ impl Viewport { } /// Returns the scale factor of the [`Viewport`]. - pub fn scale_factor(&self) -> f32 { + pub fn scale_factor(&self) -> f64 { self.scale_factor } diff --git a/program/src/lib.rs b/program/src/lib.rs index 800a1422..0a88c375 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -17,9 +17,16 @@ use crate::core::window; use crate::core::{Element, Font, Settings}; use crate::futures::{Executor, Subscription}; use crate::graphics::compositor; -use crate::runtime::Task; +use crate::runtime::task::Task; +#[cfg(any(feature = "winit", feature = "wayland"))] +use crate::shell; +#[cfg(any(feature = "winit", feature = "wayland"))] +pub use crate::shell::program::{Appearance, DefaultStyle}; -/// An interactive, native, cross-platform, multi-windowed application. +#[cfg(not(any(feature = "winit", feature = "wayland")))] +pub use crate::runtime::{Appearance, DefaultStyle}; + +/// The internal definition of a [`Program`]. /// /// A [`Program`] can execute asynchronous actions by returning a /// [`Task`] in some of its methods. @@ -109,13 +116,146 @@ pub trait Program: Sized { theme::Base::base(theme) } - fn scale_factor(&self, _state: &Self::State, _window: window::Id) -> f32 { + fn scale_factor(&self, _state: &Self::State, _window: window::Id) -> f64 { 1.0 } fn presets(&self) -> &[Preset] { &[] } + + #[cfg(any(feature = "winit", feature = "wayland"))] + /// Runs 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 + fn run( + self, + settings: Settings, + window_settings: Option, + ) -> Result + where + Self: 'static, + Self::State: Default, + { + self.run_with(settings, window_settings, || { + (Self::State::default(), Task::none()) + }) + } + + #[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, + settings: Settings, + window_settings: Option, + initialize: I, + ) -> Result + where + Self: 'static, + I: FnOnce() -> (Self::State, Task) + 'static, + { + use std::marker::PhantomData; + + struct Instance { + program: P, + state: P::State, + _initialize: PhantomData, + } + + impl (P::State, Task)> + shell::Program 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, task) = initialize(); + + ( + Self { + program, + state, + _initialize: PhantomData, + }, + task, + ) + } + + fn title(&self, window: window::Id) -> String { + self.program.title(&self.state, window) + } + + fn update( + &mut self, + message: Self::Message, + ) -> Task { + self.program.update(&mut self.state, message) + } + + fn view( + &self, + window: window::Id, + ) -> crate::Element<'_, Self::Message, Self::Theme, Self::Renderer> + { + self.program.view(&self.state, window) + } + + fn subscription(&self) -> Subscription { + self.program.subscription(&self.state) + } + + fn theme(&self, window: window::Id) -> Self::Theme { + self.program.theme(&self.state, window) + } + + fn style(&self, theme: &Self::Theme) -> Appearance { + self.program.style(&self.state, theme) + } + + fn scale_factor(&self, window: window::Id) -> f64 { + self.program.scale_factor(&self.state, window) + } + } + + #[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(crate::graphics::Antialiasing::MSAAx4) + } else { + None + }, + ..crate::graphics::Settings::default() + }; + + Ok(shell::program::run::< + Instance, + ::Compositor, + >( + Settings { + id: settings.id, + 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, + } + .into(), + renderer_settings, + window_settings, + (self, initialize), + )?) + } } /// Decorates a [`Program`] with the given title function. @@ -198,7 +338,7 @@ pub fn with_title( self.program.style(state, theme) } - fn scale_factor(&self, state: &Self::State, window: window::Id) -> f32 { + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { self.program.scale_factor(state, window) } } @@ -285,7 +425,7 @@ pub fn with_subscription( self.program.style(state, theme) } - fn scale_factor(&self, state: &Self::State, window: window::Id) -> f32 { + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { self.program.scale_factor(state, window) } } @@ -375,7 +515,7 @@ pub fn with_theme( self.program.style(state, theme) } - fn scale_factor(&self, state: &Self::State, window: window::Id) -> f32 { + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { self.program.scale_factor(state, window) } } @@ -462,7 +602,7 @@ pub fn with_style( self.program.theme(state, window) } - fn scale_factor(&self, state: &Self::State, window: window::Id) -> f32 { + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { self.program.scale_factor(state, window) } } @@ -473,7 +613,7 @@ pub fn with_style( /// Decorates a [`Program`] with the given scale factor function. pub fn with_scale_factor( program: P, - f: impl Fn(&P::State, window::Id) -> f32, + f: impl Fn(&P::State, window::Id) -> f64, ) -> impl Program { struct WithScaleFactor { program: P, @@ -482,7 +622,7 @@ pub fn with_scale_factor( impl Program for WithScaleFactor where - F: Fn(&P::State, window::Id) -> f32, + F: Fn(&P::State, window::Id) -> f64, { type State = P::State; type Message = P::Message; @@ -549,7 +689,7 @@ pub fn with_scale_factor( self.program.style(state, theme) } - fn scale_factor(&self, state: &Self::State, window: window::Id) -> f32 { + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { (self.scale_factor)(state, window) } } @@ -640,7 +780,7 @@ pub fn with_executor( self.program.style(state, theme) } - fn scale_factor(&self, state: &Self::State, window: window::Id) -> f32 { + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { self.program.scale_factor(state, window) } } @@ -710,7 +850,7 @@ impl Instance

{ } /// Returns the current scale factor of the [`Instance`]. - pub fn scale_factor(&self, window: window::Id) -> f32 { + pub fn scale_factor(&self, window: window::Id) -> f64 { self.program.scale_factor(&self.state, window) } } diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml index f025927a..2c43c896 100644 --- a/renderer/Cargo.toml +++ b/renderer/Cargo.toml @@ -19,7 +19,11 @@ wgpu-bare = ["iced_wgpu"] tiny-skia = ["iced_tiny_skia"] image = ["iced_tiny_skia?/image", "iced_wgpu?/image"] svg = ["iced_tiny_skia?/svg", "iced_wgpu?/svg"] -geometry = ["iced_graphics/geometry", "iced_tiny_skia?/geometry", "iced_wgpu?/geometry"] +geometry = [ + "iced_graphics/geometry", + "iced_tiny_skia?/geometry", + "iced_wgpu?/geometry", +] web-colors = ["iced_wgpu?/web-colors"] webgl = ["iced_wgpu?/webgl"] fira-sans = ["iced_graphics/fira-sans"] diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index 8bd0013a..2a0bdea7 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -89,11 +89,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; @@ -140,6 +142,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, diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 26b094d4..d37b3936 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -10,12 +10,16 @@ homepage.workspace = true categories.workspace = true keywords.workspace = true -[features] -selector = ["dep:iced_selector"] - [lints] workspace = true +[features] +debug = [] +selector = ["dep:iced_selector"] +multi-window = [] +a11y = ["iced_accessibility", "iced_core/a11y"] +wayland = ["iced_core/wayland", "sctk"] + [dependencies] bytes.workspace = true iced_core.workspace = true @@ -29,3 +33,10 @@ sipper.optional = true iced_selector.workspace = true iced_selector.optional = true +iced_futures.features = ["thread-pool"] +sctk.workspace = true +sctk.optional = 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 a02cc011..6c0c92db 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 00000000..2fd39244 --- /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 53bb9958..11328920 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -10,9 +10,11 @@ )] #![cfg_attr(docsrs, feature(doc_cfg))] pub mod clipboard; +pub mod dnd; pub mod font; pub mod image; pub mod keyboard; +pub mod platform_specific; pub mod system; pub mod task; pub mod user_interface; @@ -27,6 +29,7 @@ pub use user_interface::UserInterface; pub use window::Window; use crate::futures::futures::channel::oneshot; +use dnd::DndAction; use std::borrow::Cow; use std::fmt; @@ -67,6 +70,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 { @@ -88,6 +96,8 @@ impl Action { Action::Image(action) => Err(Action::Image(action)), Action::Reload => Err(Action::Reload), Action::Exit => Err(Action::Exit), + Action::Dnd(a) => Err(Action::Dnd(a)), + Action::PlatformSpecific(a) => Err(Action::PlatformSpecific(a)), } } } @@ -113,6 +123,10 @@ where Action::Image(_) => write!(f, "Action::Image"), Action::Reload => write!(f, "Action::Reload"), Action::Exit => write!(f, "Action::Exit"), + Action::PlatformSpecific(action) => { + write!(f, "Action::PlatformSpecific({:?})", action) + } + Action::Dnd(action) => write!(f, "Action::Dnd({:?})", action), } } } @@ -124,3 +138,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 new file mode 100644 index 00000000..4cb8d7cc --- /dev/null +++ b/runtime/src/multi_window/state.rs @@ -0,0 +1,279 @@ +//! The internal state of a multi-window [`Program`]. +use crate::core::event::{self, Event}; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::widget::operation::{self, Operation}; +use crate::core::{Clipboard, Size}; +use crate::user_interface::{self, UserInterface}; +use crate::{Debug, Program, Task}; + +/// The execution state of a multi-window [`Program`]. It leverages caching, event +/// processing, and rendering primitive storage. +#[allow(missing_debug_implementations)] +pub struct State

+where + P: Program + 'static, +{ + program: P, + caches: Option>, + queued_events: Vec, + queued_messages: Vec, + mouse_interaction: mouse::Interaction, +} + +impl

State

+where + P: Program + 'static, +{ + /// Creates a new [`State`] with the provided [`Program`], initializing its + /// primitive with the given logical bounds and renderer. + pub fn new( + program: P, + bounds: Size, + renderer: &mut P::Renderer, + debug: &mut Debug, + ) -> Self { + let user_interface = build_user_interface( + &program, + user_interface::Cache::default(), + renderer, + bounds, + debug, + ); + + let caches = Some(vec![user_interface.into_cache()]); + + State { + program, + caches, + queued_events: Vec::new(), + queued_messages: Vec::new(), + mouse_interaction: mouse::Interaction::None, + } + } + + /// Returns a reference to the [`Program`] of the [`State`]. + pub fn program(&self) -> &P { + &self.program + } + + /// Queues an event in the [`State`] for processing during an [`update`]. + /// + /// [`update`]: Self::update + pub fn queue_event(&mut self, event: Event) { + self.queued_events.push(event); + } + + /// Queues a message in the [`State`] for processing during an [`update`]. + /// + /// [`update`]: Self::update + pub fn queue_message(&mut self, message: P::Message) { + self.queued_messages.push(message); + } + + /// Returns whether the event queue of the [`State`] is empty or not. + pub fn is_queue_empty(&self) -> bool { + self.queued_events.is_empty() && self.queued_messages.is_empty() + } + + /// Returns the current [`mouse::Interaction`] of the [`State`]. + pub fn mouse_interaction(&self) -> mouse::Interaction { + self.mouse_interaction + } + + /// Processes all the queued events and messages, rebuilding and redrawing + /// the widgets of the linked [`Program`] if necessary. + /// + /// Returns a list containing the instances of [`Event`] that were not + /// captured by any widget, and the [`Task`] obtained from [`Program`] + /// after updating it, only if an update was necessary. + pub fn update( + &mut self, + bounds: Size, + cursor: mouse::Cursor, + renderer: &mut P::Renderer, + theme: &P::Theme, + style: &renderer::Style, + clipboard: &mut dyn Clipboard, + debug: &mut Debug, + ) -> (Vec, Option>) { + let mut user_interfaces = build_user_interfaces( + &self.program, + self.caches.take().unwrap(), + renderer, + bounds, + debug, + ); + + debug.event_processing_started(); + let mut messages = Vec::new(); + + let uncaptured_events = user_interfaces.iter_mut().fold( + vec![], + |mut uncaptured_events, ui| { + let (_, event_statuses) = ui.update( + &self.queued_events, + cursor, + renderer, + clipboard, + &mut messages, + ); + + uncaptured_events.extend( + self.queued_events + .iter() + .zip(event_statuses) + .filter_map(|(event, status)| { + matches!(status, event::Status::Ignored) + .then_some(event) + }) + .cloned(), + ); + uncaptured_events + }, + ); + + self.queued_events.clear(); + messages.append(&mut self.queued_messages); + debug.event_processing_finished(); + + let commands = if messages.is_empty() { + debug.draw_started(); + + for ui in &mut user_interfaces { + self.mouse_interaction = + ui.draw(renderer, theme, style, cursor); + } + + debug.draw_finished(); + + self.caches = Some( + user_interfaces + .drain(..) + .map(UserInterface::into_cache) + .collect(), + ); + + None + } else { + let temp_caches = user_interfaces + .drain(..) + .map(UserInterface::into_cache) + .collect(); + + drop(user_interfaces); + + let commands = Task::batch(messages.into_iter().map(|msg| { + debug.log_message(&msg); + + debug.update_started(); + let task = self.program.update(msg); + debug.update_finished(); + + task + })); + + let mut user_interfaces = build_user_interfaces( + &self.program, + temp_caches, + renderer, + bounds, + debug, + ); + + debug.draw_started(); + for ui in &mut user_interfaces { + self.mouse_interaction = + ui.draw(renderer, theme, style, cursor); + } + debug.draw_finished(); + + self.caches = Some( + user_interfaces + .drain(..) + .map(UserInterface::into_cache) + .collect(), + ); + + Some(commands) + }; + + (uncaptured_events, commands) + } + + /// Applies widget [`Operation`]s to the [`State`]. + pub fn operate( + &mut self, + renderer: &mut P::Renderer, + operations: impl Iterator>, + bounds: Size, + debug: &mut Debug, + ) { + let mut user_interfaces = build_user_interfaces( + &self.program, + self.caches.take().unwrap(), + renderer, + bounds, + debug, + ); + + for operation in operations { + let mut current_operation = Some(operation); + + while let Some(mut operation) = current_operation.take() { + for ui in &mut user_interfaces { + ui.operate(renderer, operation.as_mut()); + } + + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(()) => {} + operation::Outcome::Chain(next) => { + current_operation = Some(next); + } + _ => {} + }; + } + } + + self.caches = Some( + user_interfaces + .drain(..) + .map(UserInterface::into_cache) + .collect(), + ); + } +} + +fn build_user_interfaces<'a, P: Program>( + program: &'a P, + mut caches: Vec, + renderer: &mut P::Renderer, + size: Size, + debug: &mut Debug, +) -> Vec> { + caches + .drain(..) + .map(|cache| { + build_user_interface(program, cache, renderer, size, debug) + }) + .collect() +} + +fn build_user_interface<'a, P: Program>( + program: &'a P, + cache: user_interface::Cache, + renderer: &mut P::Renderer, + size: Size, + debug: &mut Debug, +) -> UserInterface<'a, P::Message, P::Theme, P::Renderer> { + debug.view_started(); + let view = program.view(); + debug.view_finished(); + + debug.layout_started(); + let user_interface = UserInterface::build(view, size, cache, renderer); + debug.layout_finished(); + + user_interface +} diff --git a/runtime/src/platform_specific/mod.rs b/runtime/src/platform_specific/mod.rs new file mode 100644 index 00000000..a8f761b7 --- /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 00000000..64d4dc4d --- /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 00000000..1629a463 --- /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 00000000..c370d385 --- /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 00000000..615a6d49 --- /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 00000000..adbc2390 --- /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 new file mode 100644 index 00000000..c0befaa2 --- /dev/null +++ b/runtime/src/program/state.rs @@ -0,0 +1,240 @@ +use iced_core::widget::operation::Outcome; + +use crate::core::event::{self, Event}; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::widget::operation::{self, Operation}; +use crate::core::{Clipboard, Size}; +use crate::user_interface::{self, UserInterface}; +use crate::{Debug, Program, Task}; + +/// The execution state of a [`Program`]. It leverages caching, event +/// processing, and rendering primitive storage. +#[allow(missing_debug_implementations)] +pub struct State

+where + P: Program + 'static, +{ + program: P, + cache: Option, + queued_events: Vec, + queued_messages: Vec, + mouse_interaction: mouse::Interaction, +} + +impl

State

+where + P: Program + 'static, +{ + /// Creates a new [`State`] with the provided [`Program`], initializing its + /// primitive with the given logical bounds and renderer. + pub fn new( + 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, + bounds, + debug, + ); + + let cache = Some(user_interface.into_cache()); + + State { + program, + cache, + queued_events: Vec::new(), + queued_messages: Vec::new(), + mouse_interaction: mouse::Interaction::None, + } + } + + /// Returns a reference to the [`Program`] of the [`State`]. + pub fn program(&self) -> &P { + &self.program + } + + /// Queues an event in the [`State`] for processing during an [`update`]. + /// + /// [`update`]: Self::update + pub fn queue_event(&mut self, event: Event) { + self.queued_events.push(event); + } + + /// Queues a message in the [`State`] for processing during an [`update`]. + /// + /// [`update`]: Self::update + pub fn queue_message(&mut self, message: P::Message) { + self.queued_messages.push(message); + } + + /// Returns whether the event queue of the [`State`] is empty or not. + pub fn is_queue_empty(&self) -> bool { + self.queued_events.is_empty() && self.queued_messages.is_empty() + } + + /// Returns the current [`mouse::Interaction`] of the [`State`]. + pub fn mouse_interaction(&self) -> mouse::Interaction { + self.mouse_interaction + } + + /// Processes all the queued events and messages, rebuilding and redrawing + /// the widgets of the linked [`Program`] if necessary. + /// + /// Returns a list containing the instances of [`Event`] that were not + /// captured by any widget, and the [`Task`] obtained from [`Program`] + /// 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, + theme: &P::Theme, + style: &renderer::Style, + clipboard: &mut dyn Clipboard, + debug: &mut Debug, + ) -> (Vec, Option>) { + let mut user_interface = build_user_interface( + id.clone(), + &mut self.program, + self.cache.take().unwrap(), + renderer, + bounds, + debug, + ); + + debug.event_processing_started(); + let mut messages = Vec::new(); + + let (_, event_statuses) = user_interface.update( + &self.queued_events, + cursor, + renderer, + clipboard, + &mut messages, + ); + + let uncaptured_events = self + .queued_events + .iter() + .zip(event_statuses) + .filter_map(|(event, status)| { + matches!(status, event::Status::Ignored).then_some(event) + }) + .cloned() + .collect(); + + self.queued_events.clear(); + messages.append(&mut self.queued_messages); + debug.event_processing_finished(); + + let task = if messages.is_empty() { + debug.draw_started(); + self.mouse_interaction = + user_interface.draw(renderer, theme, style, cursor); + debug.draw_finished(); + + self.cache = Some(user_interface.into_cache()); + + None + } else { + // When there are messages, we are forced to rebuild twice + // for now :^) + let temp_cache = user_interface.into_cache(); + + let tasks = Task::batch(messages.into_iter().map(|message| { + debug.log_message(&message); + + debug.update_started(); + let task = self.program.update(message); + debug.update_finished(); + + task + })); + + let mut user_interface = build_user_interface( + id, + &mut self.program, + temp_cache, + renderer, + bounds, + debug, + ); + + debug.draw_started(); + self.mouse_interaction = + user_interface.draw(renderer, theme, style, cursor); + debug.draw_finished(); + + self.cache = Some(user_interface.into_cache()); + + Some(tasks) + }; + + (uncaptured_events, task) + } + + /// 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, + bounds, + debug, + ); + + for operation in operations { + let mut current_operation = Some(operation); + + while let Some(mut operation) = current_operation.take() { + user_interface.operate(renderer, operation.as_mut()); + + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(()) => {} + operation::Outcome::Chain(next) => { + current_operation = Some(next); + } + _ => {} + }; + } + } + + self.cache = Some(user_interface.into_cache()); + } +} + +fn build_user_interface<'a, P: Program>( + _id: iced_core::id::Id, + program: &'a mut P, + cache: user_interface::Cache, + renderer: &mut P::Renderer, + size: Size, + debug: &mut Debug, +) -> UserInterface<'a, P::Message, P::Theme, P::Renderer> { + debug.view_started(); + let view = program.view(); + debug.view_finished(); + + debug.layout_started(); + let user_interface = UserInterface::build(view, size, cache, renderer); + debug.layout_finished(); + + user_interface +} diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index 7061d3e3..3c705f13 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::Operation; +use iced_core::widget::tree::NAMED; + use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -99,7 +104,12 @@ where 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_mut().layout( &mut state, @@ -107,6 +117,10 @@ where &layout::Limits::new(Size::ZERO, bounds), ); + NAMED.with(|named| { + named.borrow_mut().clear(); + }); + UserInterface { root, base, @@ -588,6 +602,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`]. @@ -615,7 +663,7 @@ impl Default for Cache { } /// The resulting state after updating a [`UserInterface`]. -#[derive(Debug, Clone)] +#[derive(Clone)] pub enum State { /// The [`UserInterface`] is outdated and needs to be rebuilt. Outdated, diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 2eb05098..8552a7b1 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -205,9 +205,31 @@ impl Window for T where T: HasWindowHandle + HasDisplayHandle {} /// /// 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/src/application.rs b/src/application.rs index da5a7d2e..4ec326d7 100644 --- a/src/application.rs +++ b/src/application.rs @@ -31,13 +31,16 @@ //! } //! ``` use crate::message; -use crate::program::{self, Program}; -use crate::shell; +use crate::program::{self, Appearance, DefaultStyle, Program}; + use crate::theme; + +#[cfg(feature = "winit")] +use crate::shell; use crate::window; use crate::{ Element, Executor, Font, Never, Preset, Result, Settings, Size, - Subscription, Task, Theme, + Subscription, Theme, task::Task, }; use iced_debug as debug; @@ -127,17 +130,22 @@ where fn update( &self, - state: &mut Self::State, - message: Self::Message, - ) -> Task { + state: &mut ::State, + message: ::Message, + ) -> Task<::Message> { self.update.update(state, message) } fn view<'a>( &self, - state: &'a Self::State, + state: &'a ::State, _window: window::Id, - ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + ) -> Element< + 'a, + ::Message, + ::Theme, + ::Renderer, + > { self.view.view(state) } @@ -182,6 +190,7 @@ pub struct Application { } impl Application

{ + #[cfg(feature = "winit")] /// Runs the [`Application`]. pub fn run(self) -> Result where @@ -415,7 +424,7 @@ impl Application

{ > { Application { raw: program::with_scale_factor(self.raw, move |state, _window| { - f(state) + f(state) as f64 }), settings: self.settings, window: self.window, @@ -475,51 +484,75 @@ impl Program for Application

{ Some(self.window.clone()) } - fn boot(&self) -> (Self::State, Task) { + fn boot( + &self, + ) -> (::State, Task<::Message>) { self.raw.boot() } fn update( &self, - state: &mut Self::State, - message: Self::Message, - ) -> Task { + state: &mut ::State, + message: ::Message, + ) -> Task<::Message> { debug::hot(|| self.raw.update(state, message)) } fn view<'a>( &self, - state: &'a Self::State, + state: &'a ::State, window: window::Id, - ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + ) -> Element< + 'a, + ::Message, + ::Theme, + ::Renderer, + > { debug::hot(|| self.raw.view(state, window)) } - fn title(&self, state: &Self::State, window: window::Id) -> String { + fn title( + &self, + state: &::State, + window: window::Id, + ) -> String { debug::hot(|| self.raw.title(state, window)) } - fn subscription(&self, state: &Self::State) -> Subscription { + fn subscription( + &self, + state: &::State, + ) -> Subscription<::Message> { debug::hot(|| self.raw.subscription(state)) } fn theme( &self, - state: &Self::State, + state: &::State, window: iced_core::window::Id, - ) -> Option { + ) -> Option<::Theme> { debug::hot(|| self.raw.theme(state, window)) } - fn style(&self, state: &Self::State, theme: &Self::Theme) -> theme::Style { + fn style( + &self, + state: &::State, + theme: &::Theme, + ) -> theme::Style { debug::hot(|| self.raw.style(state, theme)) } - fn scale_factor(&self, state: &Self::State, window: window::Id) -> f32 { + fn scale_factor( + &self, + state: &::State, + window: window::Id, + ) -> f64 { debug::hot(|| self.raw.scale_factor(state, window)) } - fn presets(&self) -> &[Preset] { + fn presets( + &self, + ) -> &[Preset<::State, ::Message>] { &self.presets } } diff --git a/src/application/timed.rs b/src/application/timed.rs index 099b3dc4..0e2ac597 100644 --- a/src/application/timed.rs +++ b/src/application/timed.rs @@ -4,9 +4,10 @@ use crate::program; use crate::theme; use crate::time::Instant; use crate::window; -use crate::{Element, Program, Settings, Subscription, Task}; +use crate::{Element, Settings, Subscription, task::Task}; use iced_debug as debug; +use iced_program::Program; /// Creates an [`Application`] with an `update` function that also /// takes the [`Instant`] of each `Message`. @@ -104,9 +105,9 @@ where fn update( &self, - state: &mut Self::State, - (message, now): Self::Message, - ) -> Task { + state: &mut ::State, + (message, now): ::Message, + ) -> Task<::Message> { debug::hot(move || { self.update .update(state, message, now) @@ -117,9 +118,14 @@ where fn view<'a>( &self, - state: &'a Self::State, + state: &'a ::State, _window: window::Id, - ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + ) -> Element< + 'a, + ::Message, + ::Theme, + ::Renderer, + > { debug::hot(|| { self.view .view(state) @@ -129,8 +135,8 @@ where fn subscription( &self, - state: &Self::State, - ) -> self::Subscription { + state: &::State, + ) -> self::Subscription<::Message> { debug::hot(|| { (self.subscription)(state) .map(|message| (message, Instant::now())) diff --git a/src/daemon.rs b/src/daemon.rs index f6573313..35213273 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,20 +1,24 @@ //! Create and run daemons that run in the background. use crate::application; use crate::message; -use crate::program::{self, Program}; +pub use crate::program::{self, Program}; +#[cfg(feature = "winit")] use crate::shell; use crate::theme; use crate::window; use crate::{ - Element, Executor, Font, Preset, Result, Settings, Subscription, Task, - Theme, + Element, Executor, Font, Preset, Result, Settings, Subscription, Theme, + task::Task, }; use iced_debug as debug; +#[cfg(not(any(feature = "winit", feature = "wayland")))] +use crate::runtime::{Appearance, DefaultStyle}; + use std::borrow::Cow; -/// Creates an iced [`Daemon`] given its boot, update, and view logic. +/// Creates an iced [`Daemon`] given its title, update, and view logic. /// /// A [`Daemon`] will not open a window by default, but will run silently /// instead until a [`Task`] from [`window::open`] is returned by its update logic. @@ -77,23 +81,31 @@ where None } - fn boot(&self) -> (Self::State, Task) { + fn boot( + &self, + ) -> (::State, Task<::Message>) + { self.boot.boot() } fn update( &self, - state: &mut Self::State, - message: Self::Message, - ) -> Task { + state: &mut ::State, + message: ::Message, + ) -> Task<::Message> { self.update.update(state, message) } fn view<'a>( &self, - state: &'a Self::State, + state: &'a ::State, window: window::Id, - ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + ) -> Element< + 'a, + ::Message, + ::Theme, + ::Renderer, + > { self.view.view(state, window) } } @@ -128,6 +140,7 @@ pub struct Daemon { } impl Daemon

{ + #[cfg(feature = "winit")] /// Runs the [`Daemon`]. pub fn run(self) -> Result where @@ -249,7 +262,7 @@ impl Daemon

{ /// Sets the scale factor of the [`Daemon`]. pub fn scale_factor( self, - f: impl Fn(&P::State, window::Id) -> f32, + f: impl Fn(&P::State, window::Id) -> f64, ) -> Daemon< impl Program, > { @@ -311,51 +324,73 @@ impl Program for Daemon

{ None } - fn boot(&self) -> (Self::State, Task) { + fn boot( + &self, + ) -> (::State, Task<::Message>) { self.raw.boot() } fn update( &self, - state: &mut Self::State, - message: Self::Message, - ) -> Task { + state: &mut ::State, + message: ::Message, + ) -> Task<::Message> { debug::hot(|| self.raw.update(state, message)) } fn view<'a>( &self, - state: &'a Self::State, + state: &'a ::State, window: window::Id, - ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + ) -> Element< + 'a, + ::Message, + ::Theme, + ::Renderer, + > { debug::hot(|| self.raw.view(state, window)) } - fn title(&self, state: &Self::State, window: window::Id) -> String { + fn title( + &self, + state: &::State, + window: window::Id, + ) -> String { debug::hot(|| self.raw.title(state, window)) } - fn subscription(&self, state: &Self::State) -> Subscription { + fn subscription( + &self, + state: &::State, + ) -> Subscription<::Message> { debug::hot(|| self.raw.subscription(state)) } fn theme( &self, - state: &Self::State, + state: &::State, window: iced_core::window::Id, - ) -> Option { + ) -> Option<::Theme> { debug::hot(|| self.raw.theme(state, window)) } - fn style(&self, state: &Self::State, theme: &Self::Theme) -> theme::Style { + fn style( + &self, + state: &::State, + theme: &::Theme, + ) -> theme::Style { debug::hot(|| self.raw.style(state, theme)) } - fn scale_factor(&self, state: &Self::State, window: window::Id) -> f32 { + fn scale_factor( + &self, + state: &::State, + window: window::Id, + ) -> f64 { debug::hot(|| self.raw.scale_factor(state, window)) } - fn presets(&self) -> &[Preset] { + fn presets(&self) -> &[Preset<::State, Self::Message>] { &self.presets } } diff --git a/src/error.rs b/src/error.rs index 111bedf2..507cd3ab 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,6 @@ use crate::futures; use crate::graphics; +#[cfg(feature = "winit")] use crate::shell; /// An error that occurred while running an application. @@ -16,20 +17,31 @@ pub enum Error { /// The application graphics context could not be created. #[error("the application graphics context could not be created")] GraphicsCreationFailed(graphics::Error), + + /// There was an event loop error. + #[error("there was an event loop error")] + EventLoop(Box), } +#[cfg(feature = "winit")] 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)) } shell::Error::GraphicsCreationFailed(error) => { Error::GraphicsCreationFailed(error) } + shell::Error::EventLoop(error) => Error::EventLoop(Box::new(error)), } } } diff --git a/src/lib.rs b/src/lib.rs index e4ee6efb..bcadf12f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -476,13 +476,21 @@ #![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::program; -use iced_winit::runtime; +// use iced_winit as shell; +// use iced_winit::core; +// use iced_winit::program; +// 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_runtime as runtime; +pub use iced_widget::core; + +#[cfg(feature = "winit")] +use iced_winit as shell; #[cfg(not(any( target_arch = "wasm32", @@ -514,19 +522,47 @@ pub use iced_highlighter as highlighter; #[cfg(feature = "wgpu")] pub use iced_renderer::wgpu::wgpu; +pub use iced_program as program; + mod error; +// pub mod program; pub mod application; pub mod daemon; 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::animation; -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; @@ -535,10 +571,9 @@ pub use crate::core::{ Alignment, Animation, Background, Border, Color, ContentFit, Degrees, Function, Gradient, Length, Never, Padding, Pixels, Point, Radians, Rectangle, Rotation, Settings, Shadow, Size, Theme, Transformation, Vector, - never, + id, layout::Limits, never, }; -pub use crate::program::Preset; -pub use crate::program::message; +pub use crate::program::{Preset, message}; pub use crate::runtime::exit; pub use iced_futures::Subscription; @@ -563,8 +598,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 { @@ -581,6 +619,9 @@ pub mod font { pub mod event { //! Handle events of a user interface. + pub use crate::core::event::PlatformSpecific; + #[cfg(feature = "wayland")] + pub use crate::core::event::wayland; pub use crate::core::event::{Event, Status}; pub use iced_futures::event::{listen, listen_raw, listen_with}; } @@ -649,22 +690,16 @@ pub mod widget { mod renderer {} } -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; pub use font::Font; -pub use program::Program; pub use renderer::Renderer; pub use task::Task; pub use window::Window; -#[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`. @@ -678,8 +713,9 @@ pub type Element< /// The result of running an iced program. pub type Result = std::result::Result<(), Error>; -/// Runs a basic iced application with default [`Settings`] given its update -/// and view logic. +#[cfg(any(feature = "winit"))] +/// Runs a basic iced application with default [`Settings`] given its title, +/// update, and view logic. /// /// This is equivalent to chaining [`application()`] with [`Application::run`]. /// diff --git a/src/time.rs b/src/time.rs index b43ced96..fdd86763 100644 --- a/src/time.rs +++ b/src/time.rs @@ -12,7 +12,7 @@ pub use crate::core::time::*; )] pub use iced_futures::backend::default::time::*; -use crate::Task; +use crate::task::Task; /// Returns a [`Task`] that produces the current [`Instant`] /// by calling [`Instant::now`]. diff --git a/src/wayland/application.rs b/src/wayland/application.rs new file mode 100644 index 00000000..97bd0313 --- /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 00000000..1e0107a0 --- /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 00000000..c701a6c2 --- /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 9f96da52..1ceb3f90 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 f453d580..e0317cb4 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::ImageReader::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/tester/src/recorder.rs b/tester/src/recorder.rs index b108ca86..890acd81 100644 --- a/tester/src/recorder.rs +++ b/tester/src/recorder.rs @@ -72,7 +72,7 @@ where vec![widget::Tree::new(&self.content)] } - fn diff(&self, tree: &mut tree::Tree) { + fn diff(&mut self, tree: &mut tree::Tree) { tree.diff_children(std::slice::from_ref(&self.content)); } 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 d23a40c4..e5d3b65d 100644 --- a/tiny_skia/src/engine.rs +++ b/tiny_skia/src/engine.rs @@ -1,4 +1,6 @@ use crate::Primitive; +use tiny_skia::Transform; + use crate::core::renderer::Quad; use crate::core::{ Background, Color, Gradient, Rectangle, Size, Transformation, Vector, @@ -42,7 +44,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); @@ -55,18 +57,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, @@ -253,22 +264,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); @@ -279,10 +290,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 = @@ -306,8 +317,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, @@ -348,13 +359,14 @@ impl Engine { return; } - let clip_mask = match physical_bounds.is_within(&clip_bounds) { - true => None, - false => { - adjust_clip_mask(clip_mask, clip_bounds); - Some(clip_mask as &_) - } - }; + let clip_mask = + match physical_bounds.is_within_strict(&clip_bounds) { + true => None, + false => { + adjust_clip_mask(clip_mask, clip_bounds); + Some(clip_mask as &_) + } + }; self.text_pipeline.draw_paragraph( paragraph, @@ -386,13 +398,14 @@ impl Engine { return; } - let clip_mask = match physical_bounds.is_within(&clip_bounds) { - true => None, - false => { - adjust_clip_mask(clip_mask, clip_bounds); - Some(clip_mask as &_) - } - }; + let clip_mask = + match physical_bounds.is_within_strict(&clip_bounds) { + true => None, + false => { + adjust_clip_mask(clip_mask, clip_bounds); + Some(clip_mask as &_) + } + }; self.text_pipeline.draw_editor( editor, @@ -421,13 +434,14 @@ impl Engine { return; } - let clip_mask = match physical_bounds.is_within(&clip_bounds) { - true => None, - false => { - adjust_clip_mask(clip_mask, clip_bounds); - Some(clip_mask as &_) - } - }; + let clip_mask = + match physical_bounds.is_within_strict(&clip_bounds) { + true => None, + false => { + adjust_clip_mask(clip_mask, clip_bounds); + Some(clip_mask as &_) + } + }; self.text_pipeline.draw_cached( content, @@ -453,13 +467,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; @@ -575,7 +589,7 @@ impl Engine { let center = physical_bounds.center(); let radians = f32::from(image.rotation); - let transform = into_transform(_transformation).post_rotate_at( + let transform = Transform::default().post_rotate_at( radians.to_degrees(), center.x, center.y, @@ -589,6 +603,7 @@ impl Engine { _pixels, transform, clip_mask, + image.border_radius.into(), ); } #[cfg(feature = "svg")] @@ -605,7 +620,7 @@ impl Engine { let center = physical_bounds.center(); let radians = f32::from(svg.rotation); - let transform = into_transform(_transformation).post_rotate_at( + let transform = Transform::default().post_rotate_at( radians.to_degrees(), center.x, center.y, diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs index 56b70b2c..3774762f 100644 --- a/tiny_skia/src/layer.rs +++ b/tiny_skia/src/layer.rs @@ -1,7 +1,7 @@ use crate::Primitive; 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; diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index 249e8260..6dd763ec 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -35,7 +35,7 @@ use crate::core::{ use crate::engine::Engine; use crate::graphics::Viewport; use crate::graphics::compositor; -use crate::graphics::text::{Editor, Paragraph}; +use crate::graphics::text::{Editor, Paragraph, Raw}; /// A [`tiny-skia`] graphics renderer for [`iced`]. /// @@ -72,7 +72,7 @@ impl Renderer { damage: &[Rectangle], background_color: Color, ) { - let scale_factor = viewport.scale_factor(); + let scale_factor = viewport.scale_factor() as f32; self.layers.flush(); @@ -251,6 +251,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}'; @@ -277,7 +278,6 @@ impl core::text::Renderer for Renderer { clip_bounds: Rectangle, ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_paragraph( text, position, @@ -308,6 +308,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) {} } impl graphics::text::Renderer for Renderer { @@ -457,7 +459,7 @@ impl renderer::Headless for Renderer { scale_factor: f32, background_color: Color, ) -> Vec { - let viewport = Viewport::with_physical_size(size, scale_factor); + let viewport = Viewport::with_physical_size(size, scale_factor as f64); window::compositor::screenshot(self, &viewport, background_color) } diff --git a/tiny_skia/src/raster.rs b/tiny_skia/src/raster.rs index e5804192..e373767f 100644 --- a/tiny_skia/src/raster.rs +++ b/tiny_skia/src/raster.rs @@ -47,15 +47,38 @@ impl Pipeline { pixels: &mut tiny_skia::PixmapMut<'_>, transform: tiny_skia::Transform, clip_mask: Option<&tiny_skia::Mask>, + border_radius: [f32; 4], ) { let mut cache = self.cache.borrow_mut(); - let Ok(image) = cache.allocate(handle) else { + let Ok(mut image) = cache.allocate(handle) else { return; }; let width_scale = bounds.width / image.width() as f32; let height_scale = bounds.height / image.height() as f32; + let quality = match filter_method { + raster::FilterMethod::Linear => tiny_skia::FilterQuality::Bilinear, + raster::FilterMethod::Nearest => 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(); + } let transform = transform.pre_scale(width_scale, height_scale); @@ -158,3 +181,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 672c49f3..03361d21 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/vector.rs b/tiny_skia/src/vector.rs index 5f157f1a..854319a2 100644 --- a/tiny_skia/src/vector.rs +++ b/tiny_skia/src/vector.rs @@ -54,6 +54,7 @@ impl Pipeline { image, &tiny_skia::PixmapPaint { opacity, + quality: tiny_skia::FilterQuality::Bicubic, ..tiny_skia::PixmapPaint::default() }, Transform::default(), diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 404948fd..e5303567 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -71,7 +71,7 @@ impl crate::graphics::Compositor for Compositor { window, clip_mask: tiny_skia::Mask::new(1, 1).expect("Create clip mask"), layer_stack: VecDeque::new(), - background_color: Color::BLACK, + background_color: Color::TRANSPARENT, max_age: 0, }; @@ -186,6 +186,7 @@ pub fn present( }) .unwrap_or_else(|| vec![Rectangle::with_size(viewport.logical_size())]); + /// XXX winit shell does not handle skipped present well. if damage.is_empty() { if let Some(last_layers) = last_layers { surface.layer_stack.push_front(last_layers.clone()); diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 9995823b..7118fb2e 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -46,3 +46,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/image/mod.rs b/wgpu/src/image/mod.rs index ba98f443..0564792f 100644 --- a/wgpu/src/image/mod.rs +++ b/wgpu/src/image/mod.rs @@ -293,7 +293,7 @@ impl State { image.opacity, image.snap, atlas_entry, - match image.filter_method { + match handle.filter_method { crate::core::image::FilterMethod::Nearest => { &mut self.nearest_instances } diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index c61730b6..d4a96ef8 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -1,3 +1,4 @@ +use crate::core::Radians; use crate::core::{ self, Background, Color, Point, Rectangle, Svg, Transformation, renderer, }; diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index ec103cf3..13794f8a 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -50,6 +50,7 @@ use buffer::Buffer; use iced_debug as debug; pub use iced_graphics as graphics; pub use iced_graphics::core; +use iced_graphics::text::Raw; pub use wgpu; @@ -63,7 +64,8 @@ pub use geometry::Geometry; use crate::core::renderer; use crate::core::{ - Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, + Background, Color, Font, Pixels, Point, Radians, Rectangle, Size, + Transformation, Vector, image::FilterMethod, }; use crate::graphics::mesh; use crate::graphics::text::{Editor, Paragraph}; @@ -312,7 +314,7 @@ impl Renderer { self.layers.merge(); for layer in self.layers.iter() { - let clip_bounds = layer.bounds * scale_factor; + let clip_bounds = layer.bounds * scale_factor as f32; if physical_bounds .intersection(&clip_bounds) @@ -332,7 +334,7 @@ impl Renderer { encoder, &layer.quads, viewport.projection(), - scale_factor, + scale_factor as f32, ); prepare_span.finish(); @@ -347,7 +349,7 @@ impl Renderer { &mut self.staging_belt, encoder, &layer.triangles, - Transformation::scale(scale_factor), + Transformation::scale(scale_factor as f32), viewport.physical_size(), ); @@ -389,7 +391,7 @@ impl Renderer { &mut self.image_cache.borrow_mut(), &layer.images, viewport.projection(), - scale_factor, + scale_factor as f32, ); prepare_span.finish(); @@ -406,7 +408,7 @@ impl Renderer { encoder, &layer.text, layer.bounds, - Transformation::scale(scale_factor), + Transformation::scale(scale_factor as f32), ); prepare_span.finish(); @@ -467,11 +469,11 @@ impl Renderer { viewport.physical_size(), )); - let scale = Transformation::scale(scale_factor); + let scale = Transformation::scale(scale_factor as f32); for layer in self.layers.iter() { - let Some(physical_bounds) = - physical_bounds.intersection(&(layer.bounds * scale_factor)) + let Some(physical_bounds) = physical_bounds + .intersection(&(layer.bounds * scale_factor as f32)) else { continue; }; @@ -662,12 +664,56 @@ impl Renderer { .filter(|layer| { !layer.is_empty() && physical_bounds - .intersection(&(layer.bounds * scale_factor)) + .intersection(&(layer.bounds * scale_factor as f32)) .is_some_and(|viewport| viewport.snap().is_some()) }) .count() }); } + + fn draw_overlay( + &mut self, + overlay: &[impl AsRef], + viewport: &Viewport, + ) { + use crate::core::Renderer as _; + use crate::core::alignment; + use crate::core::text::Renderer as _; + + self.with_layer( + Rectangle::with_size(viewport.logical_size()), + |renderer| { + for (i, line) in overlay.iter().enumerate() { + let text = crate::core::Text { + content: line.as_ref().to_owned(), + bounds: viewport.logical_size(), + size: Pixels(20.0), + line_height: core::text::LineHeight::default(), + font: Font::MONOSPACE, + align_x: core::text::Alignment::Left, + align_y: alignment::Vertical::Top, + shaping: core::text::Shaping::Advanced, + wrapping: core::text::Wrapping::Word, + }; + + renderer.fill_text( + text.clone(), + Point::new(11.0, 11.0 + 25.0 * i as f32), + Color::from_rgba(0.9, 0.9, 0.9, 1.0), + Rectangle::with_size(Size::INFINITE), + ); + + renderer.fill_text( + text, + Point::new(11.0, 11.0 + 25.0 * i as f32) + + Vector::new(-1.0, -1.0), + Color::BLACK, + Rectangle::with_size(Size::INFINITE), + ); + } + }, + ); + } } impl core::Renderer for Renderer { @@ -718,6 +764,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}'; @@ -775,6 +822,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 + } } impl graphics::text::Renderer for Renderer { @@ -977,7 +1028,7 @@ impl renderer::Headless for Renderer { background_color: Color, ) -> Vec { self.screenshot( - &Viewport::with_physical_size(size, scale_factor), + &Viewport::with_physical_size(size, scale_factor as f64), background_color, ) } diff --git a/wgpu/src/offscreen.rs b/wgpu/src/offscreen.rs new file mode 100644 index 00000000..29913d02 --- /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 646263b7..1b46afbd 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 00000000..9c764c36 --- /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 9545a14e..92f16873 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 f612c80f..7ec93e18 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -7,6 +7,12 @@ use crate::graphics::{self, Shell, 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`. pub struct Compositor { instance: wgpu::Instance, @@ -52,6 +58,28 @@ impl Compositor { compatible_window: Option, shell: Shell, ) -> 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") + { + unsafe { + 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::util::new_instance_with_webgpu_detection( &wgpu::InstanceDescriptor { backends: settings.backends, @@ -67,6 +95,12 @@ impl Compositor { log::info!("{settings:#?}"); + let available_adapters = instance.enumerate_adapters(settings.backends); + + unsafe { + 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 @@ -92,7 +126,63 @@ impl Compositor { instance.request_adapter(&adapter_options).await.map_err( |_error| 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 @@ -323,6 +413,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.engine.device, &wgpu::SurfaceConfiguration { @@ -331,7 +436,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 00000000..b50f5d69 --- /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 00000000..58da401a --- /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 aa987c2c..cfe7c23c 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -28,15 +28,23 @@ markdown = ["dep:pulldown-cmark"] highlighter = ["dep:iced_highlighter"] advanced = [] crisp = [] +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 log.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 3dffb49f..6fb43be5 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::{Task, keyboard, task}; +#[cfg(feature = "a11y")] +use std::borrow::Cow; + use crate::core::border::{self, Border}; use crate::core::layout; use crate::core::mouse; @@ -31,6 +38,8 @@ use crate::core::{ Rectangle, Shadow, Shell, Size, Theme, Vector, Widget, }; +use iced_renderer::core::widget::operation; + /// A generic widget that produces a message when pressed. /// /// # Example @@ -75,6 +84,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(), @@ -196,11 +219,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 @@ -222,8 +288,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 { @@ -331,9 +397,44 @@ 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::Click + )) + .then(|| self.on_press.as_ref()) + { + state.is_pressed = false; + shell.publish(on_press.get()); + } + return; + } + 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()); + shell.capture_event(); + return; + } + } + } + Event::Touch(touch::Event::FingerLost { .. }) + | Event::Mouse(mouse::Event::CursorLeft) => { + let state = tree.state.downcast_mut::(); + state.is_hovered = false; state.is_pressed = false; } _ => {} @@ -365,7 +466,7 @@ where tree: &Tree, renderer: &mut Renderer, theme: &Theme, - _style: &renderer::Style, + renderer_style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, @@ -404,6 +505,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, @@ -444,6 +549,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::{ + A11yNode, A11yTree, + accesskit::{Action, Node, NodeId, Rect, Role}, + }; + + 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 = Node::new(Role::Button); + node.add_action(Action::Focus); + node.add_action(Action::Click); + node.set_bounds(bounds); + if let Some(name) = self.name.as_ref() + && self.label.as_ref().is_none_or(|l| l.is_empty()) + { + node.set_label(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() + } + // TODO hover + // if is_hovered { + // node.set_busy() + // } + + 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> @@ -487,6 +676,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. @@ -505,12 +702,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(), @@ -750,3 +971,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/checkbox.rs b/widget/src/checkbox.rs index 6edab836..e3c26c58 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -31,6 +31,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::layout; use crate::core::mouse; @@ -43,7 +48,7 @@ use crate::core::widget::tree::{self, Tree}; use crate::core::window; use crate::core::{ Background, Border, Clipboard, Color, Element, Event, Layout, Length, - Pixels, Rectangle, Shell, Size, Theme, Widget, + Pixels, Rectangle, Shell, Size, Theme, Widget, id::Internal, }; /// A box that can be checked. @@ -88,6 +93,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: Option>, @@ -118,6 +129,12 @@ where /// * a boolean describing whether the [`Checkbox`] is checked or not pub fn new(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: None, @@ -134,7 +151,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(), last_status: None, @@ -248,6 +266,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 Widget @@ -416,6 +461,7 @@ where size, line_height, shaping, + wrap, } = &self.icon; let size = size.unwrap_or(Pixels(bounds.height * 0.7)); @@ -472,6 +518,89 @@ where operation.text(None, layout.bounds(), label); } } + #[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::{ + A11yNode, A11yTree, + accesskit::{Action, Node, NodeId, Rect, Role}, + }; + + 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 = Node::new(Role::CheckBox); + node.add_action(Action::Focus); + node.add_action(Action::Click); + node.set_bounds(bounds); + if let Some(name) = self.name.as_ref() { + node.set_label(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); + // TODO hover + // if is_hovered { + // node.set_hovered(); + // } + node.add_action(Action::Click); + let mut label_node = Node::new(Role::Label); + if let Some(label) = self.label.as_ref() { + label_node.set_label(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> @@ -501,6 +630,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 0db0c74c..bb55954d 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -204,8 +204,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 { @@ -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> @@ -406,7 +448,7 @@ where self.column.children() } - fn diff(&self, tree: &mut Tree) { + fn diff(&mut self, tree: &mut Tree) { self.column.diff(tree); } diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 775702cd..785185c7 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -546,7 +546,7 @@ where vec![widget::Tree::new(&self.text_input as &dyn Widget<_, _, _>)] } - fn diff(&self, _tree: &mut widget::Tree) { + fn diff(&mut self, _tree: &mut widget::Tree) { // do nothing so the children don't get cleared } diff --git a/widget/src/container.rs b/widget/src/container.rs index f83784d1..44886f09 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -28,13 +28,15 @@ use crate::core::overlay; use crate::core::renderer; use crate::core::theme; use crate::core::widget::tree::{self, Tree}; -use crate::core::widget::{self, Operation}; +use crate::core::widget::{self, Id, Operation}; use crate::core::{ self, Background, Clipboard, Color, Element, Event, Layout, Length, Padding, Pixels, Rectangle, Shadow, Shell, Size, Theme, Vector, Widget, color, }; +use iced_runtime::{Action, Task, task}; + /// A widget that aligns its contents inside of its boundaries. /// /// # Example @@ -245,8 +247,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 { @@ -356,9 +358,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, @@ -387,6 +393,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,9 +506,104 @@ pub fn draw_background( } } +/// Produces a [`Task`] that queries the visible screen bounds of the +/// [`Container`] with the given [`Id`]. +pub fn visible_bounds(id: Id) -> Task> { + struct VisibleBounds { + target: widget::Id, + depth: usize, + scrollables: Vec<(Vector, Rectangle, usize)>, + bounds: Option, + } + + impl Operation> for VisibleBounds { + fn scrollable( + &mut self, + _id: Option<&widget::Id>, + bounds: Rectangle, + _content_bounds: Rectangle, + translation: Vector, + _state: &mut dyn widget::operation::Scrollable, + ) { + match self.scrollables.last() { + Some((last_translation, last_viewport, _depth)) => { + let viewport = last_viewport + .intersection(&(bounds - *last_translation)) + .unwrap_or(Rectangle::new( + crate::core::Point::ORIGIN, + Size::ZERO, + )); + + self.scrollables.push(( + translation + *last_translation, + viewport, + self.depth, + )); + } + None => { + self.scrollables.push((translation, bounds, self.depth)); + } + } + } + + fn container(&mut self, id: Option<&Id>, bounds: Rectangle) { + if self.bounds.is_some() { + return; + } + + if id == Some(&self.target) { + match self.scrollables.last() { + Some((translation, viewport, _)) => { + self.bounds = + viewport.intersection(&(bounds - *translation)); + } + None => { + self.bounds = Some(bounds); + } + } + + return; + } + + self.depth += 1; + // TODO do we need to traverse here?? + self.depth -= 1; + + match self.scrollables.last() { + Some((_, _, depth)) if self.depth == *depth => { + let _ = self.scrollables.pop(); + } + _ => {} + } + } + + fn traverse( + &mut self, + operate: &mut dyn FnMut(&mut dyn Operation>), + ) { + self.depth += 1; + self.traverse(operate); + self.depth -= 1; + } + + fn finish(&self) -> widget::operation::Outcome> { + widget::operation::Outcome::Some(self.bounds) + } + } + + task::widget(VisibleBounds { + target: id.into(), + depth: 0, + scrollables: Vec::new(), + bounds: None, + }) +} + /// The appearance of a container. #[derive(Debug, Clone, Copy, PartialEq)] 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. @@ -475,6 +619,7 @@ pub struct Style { impl Default for Style { fn default() -> Self { Self { + icon_color: None, text_color: None, background: None, border: Border::default(), @@ -505,6 +650,8 @@ impl Style { pub fn background(self, background: impl Into) -> Self { Self { background: Some(background.into()), + icon_color: None, + text_color: None, ..self } } @@ -584,6 +731,7 @@ pub fn rounded_box(theme: &Theme) -> Style { let palette = theme.extended_palette(); Style { + icon_color: None, background: Some(palette.background.weak.color.into()), text_color: Some(palette.background.weak.text), border: border::rounded(2), @@ -612,6 +760,7 @@ pub fn dark(_theme: &Theme) -> Style { style(theme::palette::Pair { color: color!(0x111111), text: Color::WHITE, + icon: Color::WHITE, }) } diff --git a/widget/src/float.rs b/widget/src/float.rs index cfd4bed0..0364b7b4 100644 --- a/widget/src/float.rs +++ b/widget/src/float.rs @@ -102,8 +102,8 @@ where self.content.as_widget().children() } - fn diff(&self, tree: &mut widget::Tree) { - self.content.as_widget().diff(tree); + fn diff(&mut self, tree: &mut widget::Tree) { + self.content.as_widget_mut().diff(tree); } fn size(&self) -> Size { diff --git a/widget/src/grid.rs b/widget/src/grid.rs index d76b351f..a083b6c2 100644 --- a/widget/src/grid.rs +++ b/widget/src/grid.rs @@ -157,8 +157,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/helpers.rs b/widget/src/helpers.rs index 0d4c78f4..a4f81b6d 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -28,6 +28,7 @@ use crate::{ }; use std::borrow::Borrow; + use std::ops::RangeInclusive; pub use crate::table::table; @@ -609,8 +610,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 { @@ -772,8 +773,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 { @@ -1809,7 +1810,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 b1dafb62..d472e913 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::border; @@ -32,6 +33,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,15 @@ pub fn viewer(handle: Handle) -> Viewer { /// } /// ``` /// -pub struct Image { +#[derive(Debug)] +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, @@ -69,21 +81,28 @@ pub struct Image { expand: bool, } -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, crop: None, - border_radius: border::Radius::default(), content_fit: ContentFit::default(), filter_method: FilterMethod::default(), rotation: Rotation::default(), opacity: 1.0, scale: 1.0, expand: false, + border_radius: [0.0; 4].into(), } } @@ -179,6 +198,41 @@ impl Image { self.border_radius = border_radius.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`]. @@ -192,6 +246,7 @@ pub fn layout( content_fit: ContentFit, rotation: Rotation, expand: bool, + _border_radius: [f32; 4], ) -> layout::Node where Renderer: image::Renderer, @@ -236,6 +291,8 @@ fn drawing_bounds( content_fit: ContentFit, rotation: Rotation, scale: f32, + opacity: f32, + border_radius: [f32; 4], ) -> Rectangle where Renderer: image::Renderer, @@ -333,6 +390,8 @@ pub fn draw( content_fit, rotation, scale, + opacity, + border_radius.into(), ); renderer.draw_image( @@ -349,8 +408,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, @@ -378,6 +437,7 @@ where self.content_fit, self.rotation, self.expand, + self.border_radius.into(), ) } @@ -404,15 +464,75 @@ where self.scale, ); } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + _state: &Tree, + _cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::{ + A11yTree, + accesskit::{Node, NodeId, Rect, Role}, + }; + + 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 = Node::new(Role::Image); + node.set_bounds(bounds); + if let Some(name) = self.name.as_ref() { + node.set_label(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/keyed/column.rs b/widget/src/keyed/column.rs index f98c58b2..99087e68 100644 --- a/widget/src/keyed/column.rs +++ b/widget/src/keyed/column.rs @@ -1,4 +1,7 @@ //! Keyed columns distribute content vertically while keeping continuity. +//! Distribute content vertically. + +use crate::core::event; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; @@ -221,7 +224,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; @@ -230,11 +233,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 85d87f10..7a1f8506 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -5,6 +5,7 @@ pub mod component; #[allow(deprecated)] pub use component::Component; +use iced_renderer::core::widget::Operation; mod cache; @@ -123,7 +124,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::>(); @@ -142,8 +143,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()); @@ -181,7 +184,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn crate::core::widget::Operation, ) { self.with_element_mut(|element| { element.as_widget_mut().operate( @@ -304,6 +307,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 f783e7e2..ca259407 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -10,6 +10,7 @@ use crate::core::{ self, Clipboard, Element, Length, Rectangle, Shell, Size, Vector, Widget, }; +use iced_renderer::core::widget::Operation; use ouroboros::self_referencing; use std::cell::RefCell; use std::marker::PhantomData; @@ -146,13 +147,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()], @@ -278,7 +280,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(); @@ -497,6 +499,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 new file mode 100644 index 00000000..e9ddf8c4 --- /dev/null +++ b/widget/src/lazy/responsive.rs @@ -0,0 +1,528 @@ +use crate::core::event::{self, Event}; +use crate::core::layout::{self, Layout}; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::widget::tree::{self, Tree}; +use crate::core::{ + self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Vector, + Widget, +}; +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; +use std::ops::Deref; + +/// A widget that is aware of its dimensions. +/// +/// A [`Responsive`] widget will always try to fill all the available space of +/// its parent. +#[cfg(feature = "lazy")] +#[allow(missing_debug_implementations)] +pub struct Responsive< + 'a, + Message, + Theme = crate::Theme, + Renderer = crate::Renderer, +> { + view: Box Element<'a, Message, Theme, Renderer> + 'a>, + content: RefCell>, +} + +impl<'a, Message, Theme, Renderer> Responsive<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer, +{ + /// Creates a new [`Responsive`] widget with a closure that produces its + /// contents. + /// + /// The `view` closure will be provided with the current [`Size`] of + /// the [`Responsive`] widget and, therefore, can be used to build the + /// contents of the widget in a responsive way. + pub fn new( + view: impl Fn(Size) -> Element<'a, Message, Theme, Renderer> + 'a, + ) -> Self { + Self { + view: Box::new(view), + content: RefCell::new(Content { + size: Size::ZERO, + layout: None, + element: Element::new(horizontal_space().width(0)), + }), + } + } +} + +struct Content<'a, Message, Theme, Renderer> { + size: Size, + layout: Option, + element: Element<'a, Message, Theme, Renderer>, +} + +impl<'a, Message, Theme, Renderer> Content<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer, +{ + fn layout(&mut self, tree: &mut Tree, renderer: &Renderer) { + if self.layout.is_none() { + self.layout = Some(self.element.as_widget().layout( + tree, + renderer, + &layout::Limits::new(Size::ZERO, self.size), + )); + } + } + + fn update( + &mut self, + tree: &mut Tree, + new_size: Size, + view: &dyn Fn(Size) -> Element<'a, Message, Theme, Renderer>, + ) { + if self.size == new_size { + return; + } + + self.element = view(new_size); + self.size = new_size; + self.layout = None; + + tree.diff(&mut self.element); + } + + fn resolve( + &mut self, + tree: &mut Tree, + renderer: R, + layout: Layout<'_>, + view: &dyn Fn(Size) -> Element<'a, Message, Theme, Renderer>, + f: impl FnOnce( + &mut Tree, + R, + Layout<'_>, + &mut Element<'a, Message, Theme, Renderer>, + ) -> T, + ) -> T + where + R: Deref, + { + self.update(tree, layout.bounds().size(), view); + self.layout(tree, renderer.deref()); + + let content_layout = Layout::with_offset( + layout.position() - Point::ORIGIN, + self.layout.as_ref().unwrap(), + ); + + f(tree, renderer, content_layout, &mut self.element) + } +} + +struct State { + tree: RefCell, +} + +impl<'a, Message, Theme, Renderer> Widget + for Responsive<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State { + tree: RefCell::new(Tree::empty()), + }) + } + + fn size(&self) -> Size { + Size { + width: Length::Fill, + height: Length::Fill, + } + } + + fn layout( + &self, + _tree: &mut Tree, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout::Node::new(limits.max()) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn crate::core::widget::Operation, + ) { + let state = tree.state.downcast_mut::(); + let mut content = self.content.borrow_mut(); + + content.resolve( + &mut state.tree.borrow_mut(), + renderer, + layout, + &self.view, + |tree, renderer, layout, element| { + element + .as_widget() + .operate(tree, layout, renderer, operation); + }, + ); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + let state = tree.state.downcast_mut::(); + let mut content = self.content.borrow_mut(); + + let mut local_messages = vec![]; + let mut local_shell = Shell::new(&mut local_messages); + + let status = content.resolve( + &mut state.tree.borrow_mut(), + renderer, + layout, + &self.view, + |tree, renderer, layout, element| { + element.as_widget_mut().on_event( + tree, + event, + layout, + cursor, + renderer, + clipboard, + &mut local_shell, + viewport, + ) + }, + ); + + if local_shell.is_layout_invalid() { + content.layout = None; + } + + shell.merge(local_shell, std::convert::identity); + + status + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + let state = tree.state.downcast_ref::(); + let mut content = self.content.borrow_mut(); + + content.resolve( + &mut state.tree.borrow_mut(), + renderer, + layout, + &self.view, + |tree, renderer, layout, element| { + element.as_widget().draw( + tree, renderer, theme, style, layout, cursor, viewport, + ); + }, + ); + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + let state = tree.state.downcast_ref::(); + let mut content = self.content.borrow_mut(); + + content.resolve( + &mut state.tree.borrow_mut(), + renderer, + layout, + &self.view, + |tree, renderer, layout, element| { + element + .as_widget() + .mouse_interaction(tree, layout, cursor, viewport, renderer) + }, + ) + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + translation: Vector, + ) -> Option> { + use std::ops::DerefMut; + + let state = tree.state.downcast_ref::(); + + let overlay = OverlayBuilder { + content: self.content.borrow_mut(), + tree: state.tree.borrow_mut(), + types: PhantomData, + overlay_builder: |content: &mut RefMut< + '_, + Content<'_, _, _, _>, + >, + tree| { + content.update(tree, layout.bounds().size(), &self.view); + content.layout(tree, renderer); + + let Content { + element, + layout: content_layout_node, + .. + } = content.deref_mut(); + + let content_layout = Layout::with_offset( + layout.bounds().position() - Point::ORIGIN, + content_layout_node.as_ref().unwrap(), + ); + + ( + element + .as_widget_mut() + .overlay(tree, content_layout, renderer, translation) + .map(|overlay| RefCell::new(Nested::new(overlay))), + content_layout_node, + ) + }, + } + .build(); + + if overlay.with_overlay(|(overlay, _layout)| overlay.is_some()) { + Some(overlay::Element::new(Box::new(overlay))) + } else { + 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> + From> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: core::Renderer + 'a, +{ + fn from(responsive: Responsive<'a, Message, Theme, Renderer>) -> Self { + Self::new(responsive) + } +} + +#[self_referencing] +struct Overlay<'a, 'b, Message, Theme, Renderer> { + content: RefMut<'a, Content<'b, Message, Theme, Renderer>>, + tree: RefMut<'a, Tree>, + types: PhantomData, + + #[borrows(mut content, mut tree)] + #[not_covariant] + overlay: ( + Option>>, + &'this mut Option, + ), +} + +impl<'a, 'b, Message, Theme, Renderer> + Overlay<'a, 'b, Message, Theme, Renderer> +{ + fn with_overlay_maybe( + &self, + f: impl FnOnce(&mut Nested<'_, Message, Theme, Renderer>) -> T, + ) -> Option { + self.with_overlay(|(overlay, _layout)| { + overlay.as_ref().map(|nested| (f)(&mut nested.borrow_mut())) + }) + } + + fn with_overlay_mut_maybe( + &mut self, + f: impl FnOnce(&mut Nested<'_, Message, Theme, Renderer>) -> T, + ) -> Option { + self.with_overlay_mut(|(overlay, _layout)| { + overlay.as_mut().map(|nested| (f)(nested.get_mut())) + }) + } +} + +impl<'a, 'b, Message, Theme, Renderer> + overlay::Overlay + for Overlay<'a, 'b, Message, Theme, Renderer> +where + Renderer: core::Renderer, +{ + fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node { + self.with_overlay_maybe(|overlay| overlay.layout(renderer, bounds)) + .unwrap_or_default() + } + + fn draw( + &self, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + ) { + let _ = self.with_overlay_maybe(|overlay| { + overlay.draw(renderer, theme, style, layout, cursor); + }); + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.with_overlay_maybe(|overlay| { + overlay.mouse_interaction(layout, cursor, viewport, renderer) + }) + .unwrap_or_default() + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let mut is_layout_invalid = false; + + let event_status = self + .with_overlay_mut_maybe(|overlay| { + let event_status = overlay.on_event( + event, layout, cursor, renderer, clipboard, shell, + ); + + is_layout_invalid = shell.is_layout_invalid(); + + event_status + }) + .unwrap_or(event::Status::Ignored); + + if is_layout_invalid { + self.with_overlay_mut(|(_overlay, layout)| { + **layout = None; + }); + } + + event_status + } + + fn is_over( + &self, + layout: Layout<'_>, + renderer: &Renderer, + cursor_position: Point, + ) -> bool { + self.with_overlay_maybe(|overlay| { + overlay.is_over(layout, renderer, cursor_position) + }) + .unwrap_or_default() + } + + fn operate( + &mut self, + layout: Layout<'_>, + renderer: &Renderer, + 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 8ca6f882..c0e3cc61 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -1,4 +1,8 @@ //! A container for capturing mouse events. + +use iced_renderer::core::mouse::Click; + +use crate::core::event; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; @@ -18,7 +22,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, @@ -33,12 +39,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] @@ -131,12 +150,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> { @@ -146,7 +181,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, @@ -180,8 +217,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 { @@ -291,7 +328,6 @@ where viewport, ); } - fn overlay<'b>( &'b mut self, tree: &'b mut Tree, @@ -308,6 +344,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> @@ -335,14 +387,15 @@ fn update( shell: &mut Shell<'_, Message>, ) { 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; @@ -367,71 +420,153 @@ 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; + } + } + } + return; } - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if let Some(message) = widget.on_press.as_ref() { + 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()); + shell.capture_event(); + return; + } + } + } + } + + if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) = event + { + let mut captured = false; + + if let Some(position) = cursor_position + && let Some(message) = widget.on_double_click.as_ref() + { + let new_click = mouse::Click::new( + position, + mouse::Button::Left, + state.previous_click, + ); + + if new_click.kind() == mouse::click::Kind::Double { shell.publish(message.clone()); - shell.capture_event(); } - if let Some(position) = cursor_position - && let Some(message) = widget.on_double_click.as_ref() - { - let new_click = mouse::Click::new( - position, - mouse::Button::Left, - state.previous_click, - ); + state.previous_click = Some(new_click); - if new_click.kind() == mouse::click::Kind::Double { + // Even if this is not a double click, but the press is nevertheless + // processed by us and should not be popup to parent widgets. + shell.capture_event(); + } + } + + if let Some(message) = widget.on_release.as_ref() { + match event { + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) => { + state.drag_initiated = None; + shell.publish(message.clone()); + shell.capture_event(); + return; + } + Event::Mouse(mouse::Event::ButtonReleased( + mouse::Button::Right, + )) => { + if let Some(message) = widget.on_right_release.as_ref() { shell.publish(message.clone()); } + } + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Middle, + )) => { + if let Some(message) = widget.on_middle_press.as_ref() { + shell.publish(message.clone()); + shell.capture_event(); + } + } + Event::Mouse(mouse::Event::ButtonReleased( + mouse::Button::Middle, + )) => { + if let Some(message) = widget.on_middle_release.as_ref() { + shell.publish(message.clone()); + } + } + Event::Mouse(mouse::Event::WheelScrolled { delta }) => { + if let Some(on_scroll) = widget.on_scroll.as_ref() { + shell.publish(on_scroll(*delta)); + shell.capture_event(); + } + } + _ => {} + }; + } - state.previous_click = Some(new_click); + if let Some(on_scroll) = widget.on_scroll.as_ref() { + if let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event { + shell.publish(on_scroll(*delta)); + shell.capture_event(); - // Even if this is not a double click, but the press is nevertheless - // processed by us and should not be popup to parent widgets. + return; + } + } + + 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()); + } shell.capture_event(); + + return; } } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) => { - if let Some(message) = widget.on_release.as_ref() { + } + + 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::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => { - if let Some(message) = widget.on_right_press.as_ref() { - shell.publish(message.clone()); - shell.capture_event(); - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right)) => { - if let Some(message) = widget.on_right_release.as_ref() { - shell.publish(message.clone()); - } - } - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) => { - if let Some(message) = widget.on_middle_press.as_ref() { - shell.publish(message.clone()); - shell.capture_event(); - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Middle)) => { - if let Some(message) = widget.on_middle_release.as_ref() { - shell.publish(message.clone()); - } - } - Event::Mouse(mouse::Event::WheelScrolled { delta }) => { - if let Some(on_scroll) = widget.on_scroll.as_ref() { - shell.publish(on_scroll(*delta)); - shell.capture_event(); - } - } - _ => {} } } diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index 7eb67c0c..b5845e23 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -39,6 +39,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>, } @@ -72,7 +73,8 @@ where padding: Padding::ZERO, 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, class, } @@ -111,6 +113,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()); @@ -204,10 +212,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, @@ -215,13 +224,14 @@ where font, text_size, text_line_height, + text_wrap, text_shaping, padding, class, }) .height(menu_height); - state.tree.diff(&list as &dyn Widget<_, _, _>); + state.tree.diff(&mut list as &mut dyn Widget<_, _, _>); Self { position, @@ -342,6 +352,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 8004055f..85a335b5 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -84,7 +84,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::window; use crate::core::{ @@ -165,6 +164,7 @@ pub struct PaneGrid< min_size: 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>, last_mouse_interaction: Option, @@ -377,8 +377,8 @@ where self.contents.iter().map(Content::state).collect() } - fn diff(&self, tree: &mut Tree) { - let Memory { order, .. } = tree.state.downcast_ref(); + fn diff(&mut self, tree: &mut Tree) { + let Memory { order, .. } = tree.state.downcast_mut(); // `Pane` always increments and is iterated by Ord so new // states are always added at the end. We can simply remove @@ -399,11 +399,13 @@ where retain }); - tree.diff_children_custom( - &self.contents, - |state, content| content.diff(state), - Content::state, - ); + // let ids = self.contents.iter().map(|_| None).collect(); // TODO + // tree.diff_children_custom( + // &mut self.contents, + // ids, + // |state, (_, content): (_, _)| content.diff(state), + // |(_, content): (_, _)| content.state(), + // ); let Memory { order, .. } = tree.state.downcast_mut(); order.clone_from(&self.panes); @@ -464,7 +466,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()); operation.traverse(&mut |operation| { diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs index b6f27203..3400bd3e 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::layout; use crate::core::mouse; @@ -90,13 +92,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 14a15c5f..c451b6b1 100644 --- a/widget/src/pane_grid/title_bar.rs +++ b/widget/src/pane_grid/title_bar.rs @@ -1,9 +1,11 @@ +use iced_renderer::core::widget::Operation; + use crate::container; 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, Event, Layout, Padding, Point, Rectangle, Shell, Size, Vector, @@ -127,17 +129,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(); } @@ -161,7 +163,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); @@ -372,7 +376,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 ba39143f..4e36db99 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::keyboard; use crate::core::layout; @@ -168,6 +170,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>, @@ -204,7 +207,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(), @@ -259,6 +263,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()); @@ -381,7 +391,7 @@ where align_x: text::Alignment::Default, align_y: 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()) @@ -603,6 +613,7 @@ where *size, text::LineHeight::default(), text::Shaping::Basic, + text::Wrapping::default(), )), Handle::Static(Icon { font, @@ -610,7 +621,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(( @@ -619,6 +633,7 @@ where open.size, open.line_height, open.shaping, + open.wrap, )) } else { Some(( @@ -627,13 +642,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( @@ -649,7 +667,7 @@ where align_x: text::Alignment::Right, align_y: alignment::Vertical::Center, shaping, - wrapping: text::Wrapping::default(), + wrapping: wrap, }, Point::new( bounds.x + bounds.width - self.padding.right, @@ -679,7 +697,7 @@ where align_x: text::Alignment::Default, align_y: 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 selected.is_some() { @@ -831,6 +849,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/pin.rs b/widget/src/pin.rs index 77df188e..8eb5906e 100644 --- a/widget/src/pin.rs +++ b/widget/src/pin.rs @@ -126,8 +126,8 @@ where self.content.as_widget().children() } - fn diff(&self, tree: &mut widget::Tree) { - self.content.as_widget().diff(tree); + fn diff(&mut self, tree: &mut widget::Tree) { + self.content.as_widget_mut().diff(tree); } fn size(&self) -> Size { diff --git a/widget/src/radio.rs b/widget/src/radio.rs index 5d0e4a66..d637abe4 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/responsive.rs b/widget/src/responsive.rs index 76e9c62c..9c305977 100644 --- a/widget/src/responsive.rs +++ b/widget/src/responsive.rs @@ -65,7 +65,7 @@ impl Widget where Renderer: core::Renderer, { - fn diff(&self, _tree: &mut Tree) { + fn diff(&mut self, _tree: &mut Tree) { // Diff is deferred to layout } @@ -86,7 +86,7 @@ where let size = limits.max(); self.content = (self.view)(size); - tree.diff_children(std::slice::from_ref(&self.content)); + tree.diff_children(std::slice::from_mut(&mut self.content)); let node = self.content.as_widget_mut().layout( &mut tree.children[0], diff --git a/widget/src/row.rs b/widget/src/row.rs index 448f40fe..9d41b5ae 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -195,8 +195,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 { @@ -340,6 +340,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> @@ -397,7 +439,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 badd4b7f..ad812d7f 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -32,7 +32,8 @@ where Theme: Catalog, { Rule { - thickness: Length::Fixed(height.into().0), + width: Length::Fill, + height: Length::Fixed(height.into().0), is_vertical: false, class: Theme::default(), } @@ -44,7 +45,8 @@ where Theme: Catalog, { Rule { - thickness: Length::Fixed(width.into().0), + width: Length::Fixed(width.into().0), + height: Length::Fill, is_vertical: true, class: Theme::default(), } @@ -72,7 +74,8 @@ pub struct Rule<'a, Theme = crate::Theme> where Theme: Catalog, { - thickness: Length, + width: Length, + height: Length, is_vertical: bool, class: Theme::Class<'a>, } @@ -81,6 +84,44 @@ impl<'a, Theme> Rule<'a, Theme> where Theme: Catalog, { + /// Creates a horizontal [`Rule`] with the given height. + pub fn horizontal(height: impl Into) -> Self { + Rule { + width: Length::Fill, + height: Length::Fixed(height.into().0), + is_vertical: false, + class: Theme::default(), + } + } + + /// Creates a vertical [`Rule`] with the given width. + pub fn vertical(width: impl Into) -> Self { + Rule { + width: Length::Fixed(width.into().0), + height: Length::Fill, + is_vertical: true, + class: Theme::default(), + } + } + + /// 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_vertical { + 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_vertical { + 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 @@ -107,16 +148,9 @@ where Theme: Catalog, { fn size(&self) -> Size { - if self.is_vertical { - Size { - width: self.thickness, - height: Length::Fill, - } - } else { - Size { - width: Length::Fill, - height: self.thickness, - } + Size { + width: self.width, + height: self.height, } } diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 91d6c02b..f26750be 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -22,6 +22,12 @@ use crate::container; use crate::core::alignment; 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; use crate::core::keyboard; use crate::core::layout; use crate::core::mouse; @@ -30,18 +36,17 @@ use crate::core::renderer; use crate::core::text; use crate::core::time::{Duration, Instant}; use crate::core::touch; -use crate::core::widget; use crate::core::widget::operation::{self, Operation}; use crate::core::widget::tree::{self, Tree}; use crate::core::window; use crate::core::{ self, Background, Clipboard, Color, Element, Event, InputMethod, Layout, Length, Padding, Pixels, Point, Rectangle, Shadow, Shell, Size, Theme, - Vector, Widget, + Vector, Widget, id::Internal, }; +use iced_runtime::{Action, Task, task}; pub use operation::scrollable::{AbsoluteOffset, RelativeOffset}; - /// A widget that can vertically display an infinite amount of content with a /// scrollbar. /// @@ -73,7 +78,14 @@ pub struct Scrollable< Theme: Catalog, Renderer: text::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, @@ -102,7 +114,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(), @@ -141,8 +160,8 @@ where } /// Sets the [`widget::Id`] of the [`Scrollable`]. - pub fn id(mut self, id: impl Into) -> Self { - self.id = Some(id.into()); + pub fn id(mut self, id: impl Into) -> Self { + self.id = id.into(); self } @@ -255,6 +274,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`]. @@ -415,8 +469,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 { @@ -552,7 +606,7 @@ where state.translation(self.direction, bounds, content_bounds); operation.scrollable( - self.id.as_ref(), + Some(&self.id), bounds, content_bounds, translation, @@ -1406,6 +1460,183 @@ where )), } } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::{ + A11yId, A11yNode, A11yTree, + accesskit::{Node, NodeId, Rect, Role}, + }; + + 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 = Node::new(Role::ScrollView); + node.set_bounds(bounds); + if let Some(name) = self.name.as_ref() { + node.set_label(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 => {} + } + + // TODO hover + // 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 = Node::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); + // TODO: hover + // 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); + } + } } struct AutoScrollIcon<'a, Class> { @@ -1567,6 +1798,24 @@ where } } +/// 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, 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, 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, offset))) +} + fn notify_scroll( state: &mut State, on_scroll: &Option Message + '_>>, diff --git a/widget/src/sensor.rs b/widget/src/sensor.rs index ff8a8f12..c7d097ab 100644 --- a/widget/src/sensor.rs +++ b/widget/src/sensor.rs @@ -180,8 +180,8 @@ where vec![Tree::new(&self.content)] } - fn diff(&self, tree: &mut Tree) { - tree.diff_children(&[&self.content]); + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(&mut [&mut self.content]); } fn update( diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 8adff568..eced713f 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -35,6 +35,7 @@ use crate::core::layout; use crate::core::mouse; use crate::core::renderer; use crate::core::touch; +use crate::core::widget::Id; use crate::core::widget::tree::{self, Tree}; use crate::core::window; use crate::core::{ @@ -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. /// @@ -84,11 +91,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, @@ -153,12 +176,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 @@ -207,6 +238,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 Widget @@ -448,15 +514,51 @@ where let style = theme.style(&self.class, self.status.unwrap_or(Status::Active)); + 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; @@ -475,6 +577,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 { @@ -492,9 +640,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, @@ -503,11 +651,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, }, @@ -550,6 +699,83 @@ where mouse::Interaction::default() } } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + _state: &Tree, + cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::{ + A11yTree, + accesskit::{Node, NodeId, Rect, Role}, + }; + + 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 = Node::new(Role::Slider); + node.set_bounds(bounds); + if let Some(name) = self.name.as_ref() { + node.set_label(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 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> @@ -591,6 +817,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 { @@ -615,6 +850,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 { @@ -640,6 +890,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, }, @@ -698,5 +950,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 80741d6b..28da011e 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -1,4 +1,6 @@ //! Display content on top of other content. + +use crate::core::event; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; @@ -150,8 +152,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 c602e2e3..bb4751d6 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; @@ -26,6 +29,9 @@ use crate::core::{ Rectangle, Rotation, Shell, 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, @@ -64,6 +77,7 @@ where rotation: Rotation, opacity: f32, status: Option, + symbolic: bool, } impl<'a, Theme> Svg<'a, Theme> @@ -73,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, @@ -81,6 +102,7 @@ where rotation: Rotation::default(), opacity: 1.0, status: None, + symbolic: false, } } @@ -116,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 @@ -148,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 Widget @@ -226,9 +290,9 @@ where _state: &Tree, renderer: &mut Renderer, theme: &Theme, - _style: &renderer::Style, + renderer_style: &renderer::Style, layout: Layout<'_>, - _cursor: mouse::Cursor, + cursor: mouse::Cursor, _viewport: &Rectangle, ) { let Size { width, height } = renderer.measure_svg(&self.handle); @@ -257,19 +321,100 @@ where let drawing_bounds = Rectangle::new(position, final_size); - let style = - theme.style(&self.class, self.status.unwrap_or(Status::Idle)); + let is_mouse_over = cursor.is_over(bounds); + let status = if is_mouse_over { + Status::Hovered + } else { + Status::Idle + }; - renderer.draw_svg( - svg::Svg { - handle: self.handle.clone(), - color: style.color, - rotation: self.rotation.radians(), - opacity: self.opacity, - }, - drawing_bounds, - bounds, + let mut style = theme.style(&self.class, self.status.unwrap_or(status)); + + if self.symbolic && style.color.is_none() { + style.color = Some(renderer_style.icon_color); + } + + let render = |renderer: &mut Renderer| { + renderer.draw_svg( + svg::Svg { + handle: self.handle.clone(), + color: style.color, + rotation: self.rotation.radians(), + opacity: self.opacity, + border_radius: [0., 0., 0., 0.], + }, + drawing_bounds, + bounds, + ); + }; + + if adjusted_fit.width > bounds.width + || adjusted_fit.height > bounds.height + { + renderer.with_layer(bounds, render); + } else { + render(renderer); + } + } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + _state: &Tree, + _cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::{ + A11yTree, + accesskit::{Node, NodeId, Rect, Role}, + }; + + 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 = Node::new(Role::Image); + node.set_bounds(bounds); + if let Some(name) = self.name.as_ref() { + node.set_label(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; } } diff --git a/widget/src/table.rs b/widget/src/table.rs index cdb6f17e..8087f367 100644 --- a/widget/src/table.rs +++ b/widget/src/table.rs @@ -226,8 +226,8 @@ where .collect() } - fn diff(&self, tree: &mut widget::Tree) { - tree.diff_children(&self.cells); + fn diff(&mut self, tree: &mut widget::Tree) { + tree.diff_children(&mut self.cells); } fn layout( diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 9c80dfd0..d5d3e2ce 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -653,7 +653,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 ccc03484..2f5a9ae7 100644 --- a/widget/src/themer.rs +++ b/widget/src/themer.rs @@ -1,4 +1,6 @@ use crate::container; + +use crate::core::event; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; @@ -75,8 +77,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 { @@ -159,7 +161,9 @@ where let style = if let Some(text_color) = self.text_color { renderer::Style { - text_color: text_color(theme), + 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 29ce9184..2739ab22 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::border; use crate::core::layout; @@ -37,12 +43,14 @@ 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::window; use crate::core::{ Background, Border, Clipboard, Color, Element, Event, Layout, Length, - Pixels, Rectangle, Shell, Size, Theme, Widget, + Pixels, Rectangle, Shell, Size, Theme, Widget, id, +}; +use crate::core::{ + widget::{self, Id}, + window, }; /// A toggler widget. @@ -86,6 +94,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>, @@ -115,11 +131,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, @@ -128,9 +149,9 @@ where text_size: None, text_line_height: text::LineHeight::default(), text_alignment: text::Alignment::Default, - 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(), last_status: None, @@ -140,6 +161,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 } @@ -246,6 +268,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 Widget @@ -279,12 +339,8 @@ where layout::next_to_each_other( &limits, - if self.label.is_some() { - self.spacing - } else { - 0.0 - }, - |_| layout::Node::new(Size::new(2.0 * self.size, self.size)), + self.spacing, + |_| layout::Node::new(crate::core::Size::new(48., 24.)), |limits| { if let Some(label) = self.label.as_deref() { let state = tree @@ -309,7 +365,7 @@ where }, ) } else { - layout::Node::new(Size::ZERO) + layout::Node::new(crate::core::Size::ZERO) } }, ) @@ -393,7 +449,7 @@ where theme: &Theme, defaults: &renderer::Style, layout: Layout<'_>, - _cursor: mouse::Cursor, + cursor: mouse::Cursor, viewport: &Rectangle, ) { let mut children = layout.children(); @@ -424,15 +480,38 @@ where } let bounds = toggler_layout.bounds(); - let border_radius = style - .border_radius - .unwrap_or_else(|| border::Radius::new(bounds.height / 2.0)); + let is_mouse_over = cursor.is_over(layout.bounds()); + + let status = if self.on_toggle.is_none() { + Status::Disabled { + is_toggled: self.is_toggled, + } + } else if is_mouse_over { + Status::Hovered { + is_toggled: self.is_toggled, + } + } else { + Status::Active { + is_toggled: self.is_toggled, + } + }; + + let style = theme.style(&self.class, status); + + let space = style.handle_margin; + + let toggler_background_bounds = Rectangle { + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, + }; renderer.fill_quad( renderer::Quad { bounds, border: Border { - radius: border_radius, + radius: style.border_radius, width: style.background_border_width, color: style.background_border_color, }, @@ -445,20 +524,20 @@ where let toggler_foreground_bounds = Rectangle { x: bounds.x + if self.is_toggled { - bounds.width - bounds.height + padding + bounds.width - space - (bounds.height - (2.0 * space)) } else { - padding + space }, - y: bounds.y + padding, - width: bounds.height - (2.0 * padding), - height: bounds.height - (2.0 * padding), + 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, + radius: style.handle_radius, width: style.foreground_border_width, color: style.foreground_border_color, }, @@ -467,6 +546,103 @@ 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::{ + A11yNode, A11yTree, + accesskit::{Action, Node, NodeId, Rect, Role}, + }; + + 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 = Node::new(Role::Switch); + node.add_action(Action::Focus); + node.add_action(Action::Click); + node.set_bounds(bounds); + if let Some(name) = self.name.as_ref() { + node.set_label(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); + // TODO hover + // if is_hovered { + // node.set_hovered(); + // } + node.add_action(Action::Click); + if let Some(label) = self.label.as_ref() { + let mut label_node = Node::new(Role::Label); + + label_node.set_label(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> @@ -518,14 +694,16 @@ pub struct Style { pub foreground_border_width: f32, /// The [`Color`] of the foreground border of the toggler. pub foreground_border_color: Color, - /// The text [`Color`] of the toggler. - pub text_color: Option, /// The border radius of the toggler. - /// - /// If `None`, the toggler will be perfectly round. - pub border_radius: Option, + 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 ratio of separation between the background and the toggle in relative height. pub padding_ratio: f32, + /// The text [`Color`] of the toggler. + pub text_color: Option, } /// The theme catalog of a [`Toggler`]. @@ -606,8 +784,10 @@ pub fn default(theme: &Theme, status: Status) -> Style { foreground_border_color: Color::TRANSPARENT, background_border_width: 0.0, background_border_color: Color::TRANSPARENT, - text_color: None, - border_radius: None, + border_radius: Radius::from(8.0), + handle_radius: Radius::from(8.0), + handle_margin: 2.0, padding_ratio: 0.1, + text_color: None, } } diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index 19ec546b..7df6b307 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -169,13 +169,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()) } @@ -192,6 +185,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( &mut self, tree: &mut widget::Tree, @@ -568,7 +568,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 e8544b0b..a5260374 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::{ - Catalog, Handle, HandleShape, Status, Style, StyleFn, default, + Catalog, Handle, HandleShape, RailBackground, Status, Style, StyleFn, + default, }; 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 cdce9c10..a96e2665 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -14,33 +14,76 @@ keywords.workspace = true workspace = true [features] -default = ["x11", "wayland"] +default = ["x11", "wayland", "wayland-dlopen"] debug = ["iced_debug/enable"] sysinfo = ["dep:sysinfo"] unconditional-rendering = [] linux-theme-detection = ["dep:mundy", "mundy/async-io", "mundy/color-scheme"] -x11 = ["winit/x11", "window_clipboard/x11"] -wayland = ["winit/wayland", "winit/wayland-dlopen", "winit/wayland-csd-adwaita", "window_clipboard/wayland"] +x11 = ["winit/x11"] +system = ["sysinfo"] +program = [] +wayland = [ + "winit/wayland", + "sctk", + "wayland-protocols", + "raw-window-handle", + "iced_runtime/wayland", + "wayland-backend", + "xkbcommon", + "xkbcommon-dl", + "xkeysym", + "wayland-csd-adwaita", +] +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_debug.workspace = true -iced_program.workspace = true - +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 +winit-core.workspace = true +cursor-icon.workspace = true sysinfo.workspace = true sysinfo.optional = true +iced_debug.workspace = true +iced_program.workspace = true + + +[target.'cfg(target_os = "linux")'.dependencies] +mundy.workspace = true +mundy.optional = true + +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 + [target.'cfg(target_arch = "wasm32")'.dependencies] web-sys.workspace = true web-sys.features = ["Document", "Window", "HtmlCanvasElement"] wasm-bindgen-futures.workspace = true - -[target.'cfg(target_os = "linux")'.dependencies] -mundy.workspace = true -mundy.optional = true diff --git a/winit/src/a11y.rs b/winit/src/a11y.rs new file mode 100644 index 00000000..a901b480 --- /dev/null +++ b/winit/src/a11y.rs @@ -0,0 +1,63 @@ +use crate::Control; +use crate::futures::futures::channel::mpsc; + +use iced_accessibility::accesskit::{ + ActivationHandler, Node, NodeId, Role, Tree, TreeId, 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 = Node::new(Role::Window); + node.set_label(self.title.clone()); + let root = NodeId(node_id); + Some(TreeUpdate { + tree_id: TreeId::ROOT, + 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 00000000..9f109d39 --- /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 00000000..9989a88f --- /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 b1463fdc..30403c70 100644 --- a/winit/src/clipboard.rs +++ b/winit/src/clipboard.rs @@ -1,45 +1,118 @@ //! Access the clipboard. +use std::sync::Mutex; +use std::{any::Any, borrow::Cow}; + +use crate::Control; + use crate::core::clipboard::Kind; +use crate::core::clipboard::{DndSource, DynIconSurface}; +use log::trace; 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. 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, + 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(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. @@ -47,6 +120,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() + } } } @@ -90,14 +176,286 @@ impl Clipboard { State::Unavailable => None, } } + + pub 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, + } + } + + pub 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, _) => {} + } + } + + pub 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, + .. + } => { + log::debug!( + "clipboard::start_dnd queued internal={} source={:?}", + internal, + source_surface + ); + _ = sender.sender.unbounded_send(crate::Control::StartDnd); + queued_events.push(StartDnd { + internal, + source_surface, + icon_surface, + content, + actions, + }); + } + State::Unavailable => {} + } + } + + pub fn register_dnd_destination( + &self, + surface: DndSurface, + rectangles: Vec, + ) { + match &self.state { + State::Connected { clipboard, .. } => { + trace!( + target: "iced::winit::clipboard", + "register destination surface={:?} count={}", + surface, + rectangles.len() + ); + for rect in &rectangles { + trace!( + target: "iced::winit::clipboard", + "rect id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) actions={:?} preferred={:?}", + rect.id, + rect.rectangle.x, + rect.rectangle.y, + rect.rectangle.width, + rect.rectangle.height, + rect.actions, + rect.preferred + ); + } + _ = clipboard.register_dnd_destination(surface, rectangles) + } + State::Unavailable => {} + } + } + + pub fn end_dnd(&self) { + match &self.state { + State::Connected { clipboard, .. } => _ = clipboard.end_dnd(), + State::Unavailable => {} + } + } + + pub 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, + } + } + + pub fn set_action(&self, action: DndAction) { + match &self.state { + State::Connected { clipboard, .. } => { + _ = clipboard.set_action(action) + } + State::Unavailable => {} + } + } + + pub 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)); + } + + 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(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 54260e11..1966aeb2 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -3,18 +3,31 @@ //! [`winit`]: https://github.com/rust-windowing/winit //! [`iced_runtime`]: https://github.com/iced-rs/iced/tree/0.14/runtime use crate::core::input_method; +use std::hash::DefaultHasher; +use std::hash::Hash; +use std::hash::Hasher; +use std::sync::Arc; + use crate::core::keyboard; use crate::core::mouse; use crate::core::theme; use crate::core::touch; use crate::core::window; use crate::core::{Event, Point, Size}; +use iced_futures::core::event::PlatformSpecific; +use winit::dpi::PhysicalPosition; +use winit::event::ButtonSource; +use winit::event::ElementState; +use winit::event::FingerId; +use winit::event::Force; +use winit::icon::IconProvider; +use winit::keyboard::SmolStr; /// Converts some [`window::Settings`] into some `WindowAttributes` from `winit`. pub fn window_attributes( settings: window::Settings, title: &str, - scale_factor: f32, + scale_factor: f64, primary_monitor: Option, _id: Option, ) -> winit::window::WindowAttributes { @@ -36,15 +49,15 @@ pub fn window_attributes( attributes = attributes .with_title(title) - .with_inner_size(winit::dpi::LogicalSize { - width: settings.size.width * scale_factor, - height: settings.size.height * scale_factor, + .with_surface_size(winit::dpi::LogicalSize { + width: settings.size.width, + height: settings.size.height, }) .with_maximized(settings.maximized) .with_fullscreen( settings .fullscreen - .then_some(winit::window::Fullscreen::Borderless(None)), + .then_some(winit_core::monitor::Fullscreen::Borderless(None)), ) .with_resizable(settings.resizable) .with_enabled_buttons(buttons) @@ -62,17 +75,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( @@ -85,7 +100,12 @@ pub fn window_attributes( use ::winit::platform::wayland::WindowAttributesExtWayland; if let Some(id) = _id { - attributes = attributes.with_name(id.clone(), id); + attributes = attributes.with_platform_attributes(Box::new( + WindowAttributesWayland::default().with_name( + &settings.platform_specific.application_id, + &settings.platform_specific.application_id, + ), + )); } } @@ -93,20 +113,21 @@ pub fn window_attributes( { use window::settings::platform; use winit::platform::windows::{ - CornerPreference, WindowAttributesExtWindows, + CornerPreference, WindowAttributesWindows, }; + let mut w_attributes = WindowAttributesWindows::default(); - attributes = attributes + w_attributes = w_attributes .with_drag_and_drop(settings.platform_specific.drag_and_drop); - attributes = attributes + w_attributes = w_attributes .with_skip_taskbar(settings.platform_specific.skip_taskbar); - attributes = attributes.with_undecorated_shadow( + w_attributes = w_attributes.with_undecorated_shadow( settings.platform_specific.undecorated_shadow, ); - attributes = attributes.with_corner_preference( + w_attributes = w_attributes.with_corner_preference( match settings.platform_specific.corner_preference { platform::CornerPreference::Default => { CornerPreference::Default @@ -120,45 +141,53 @@ pub fn window_attributes( } }, ); + attributes = + attributes.with_platform_attributes(Box::new(w_attributes)); } #[cfg(target_os = "macos")] { - use winit::platform::macos::WindowAttributesExtMacOS; + use winit::platform::macos::WindowAttributesMacOS; - attributes = attributes - .with_title_hidden(settings.platform_specific.title_hidden) - .with_titlebar_transparent( - settings.platform_specific.titlebar_transparent, - ) - .with_fullsize_content_view( - settings.platform_specific.fullsize_content_view, - ); + attributes = attributes.with_platform_attributes(Box::new( + WindowAttributesMacOS::default() + .with_title_hidden(settings.platform_specific.title_hidden) + .with_titlebar_transparent( + settings.platform_specific.titlebar_transparent, + ) + .with_fullsize_content_view( + settings.platform_specific.fullsize_content_view, + ), + )); } #[cfg(target_os = "linux")] { #[cfg(feature = "x11")] { - use winit::platform::x11::WindowAttributesExtX11; + use winit::platform::x11::WindowAttributesX11; - attributes = attributes - .with_override_redirect( - settings.platform_specific.override_redirect, - ) - .with_name( - &settings.platform_specific.application_id, - &settings.platform_specific.application_id, - ); + attributes = attributes.with_platform_attributes(Box::new( + WindowAttributesX11::default() + .with_override_redirect( + settings.platform_specific.override_redirect, + ) + .with_name( + &settings.platform_specific.application_id, + &settings.platform_specific.application_id, + ), + )); } #[cfg(feature = "wayland")] { - use winit::platform::wayland::WindowAttributesExtWayland; + use winit::platform::wayland::WindowAttributesWayland; - attributes = attributes.with_name( - &settings.platform_specific.application_id, - &settings.platform_specific.application_id, - ); + attributes = attributes.with_platform_attributes(Box::new( + WindowAttributesWayland::default().with_name( + &settings.platform_specific.application_id, + &settings.platform_specific.application_id, + ), + )); } } @@ -168,15 +197,15 @@ pub fn window_attributes( /// Converts a winit window event into an iced event. pub fn window_event( event: winit::event::WindowEvent, - scale_factor: f32, + scale_factor: f64, modifiers: winit::keyboard::ModifiersState, ) -> Option { use winit::event::Ime; use winit::event::WindowEvent; match event { - WindowEvent::Resized(new_size) => { - let logical_size = new_size.to_logical(f64::from(scale_factor)); + WindowEvent::SurfaceResized(new_size) => { + let logical_size = new_size.to_logical(scale_factor); Some(Event::Window(window::Event::Resized(Size { width: logical_size.width, @@ -186,20 +215,51 @@ pub fn window_event( WindowEvent::CloseRequested => { Some(Event::Window(window::Event::CloseRequested)) } - WindowEvent::CursorMoved { position, .. } => { - let position = position.to_logical::(f64::from(scale_factor)); + WindowEvent::PointerMoved { + position, source, .. + } => match source { + winit::event::PointerSource::Mouse => { + let position = position.to_logical::(scale_factor); - Some(Event::Mouse(mouse::Event::CursorMoved { - position: Point::new(position.x as f32, position.y as f32), - })) - } - WindowEvent::CursorEntered { .. } => { + Some(Event::Mouse(mouse::Event::CursorMoved { + position: Point::new(position.x as f32, position.y as f32), + })) + } + winit::event::PointerSource::Touch { finger_id, force } => { + Some(Event::Touch(touch_event( + finger_id, + TouchInternal::Moved, + force, + Some(position), + scale_factor, + ))) + } + _ => None, + }, + // TODO how to handle this for Touch? Is it different than a press? Need to double check winit impl + WindowEvent::PointerEntered { .. } => { Some(Event::Mouse(mouse::Event::CursorEntered)) } - WindowEvent::CursorLeft { .. } => { - Some(Event::Mouse(mouse::Event::CursorLeft)) - } - WindowEvent::MouseInput { button, state, .. } => { + WindowEvent::PointerLeft { kind, position, .. } => match kind { + winit::event::PointerKind::Mouse => { + Some(Event::Mouse(mouse::Event::CursorLeft)) + } + winit::event::PointerKind::Touch(finger_id) => { + Some(Event::Touch(touch_event( + finger_id, + TouchInternal::Left, + None, + position, + scale_factor, + ))) + } + _ => None, + }, + WindowEvent::PointerButton { + button: ButtonSource::Mouse(button), + state, + .. + } => { let button = mouse_button(button); Some(Event::Mouse(match state { @@ -211,6 +271,18 @@ pub fn window_event( } })) } + WindowEvent::PointerButton { + button: ButtonSource::Touch { finger_id, force }, + state, + position, + .. + } => Some(Event::Touch(touch_event( + finger_id, + TouchInternal::ElementState(state), + force, + Some(position), + scale_factor, + ))), WindowEvent::MouseWheel { delta, .. } => match delta { winit::event::MouseScrollDelta::LineDelta(delta_x, delta_y) => { Some(Event::Mouse(mouse::Event::WheelScrolled { @@ -235,8 +307,7 @@ pub fn window_event( let key = { #[cfg(not(target_arch = "wasm32"))] { - use winit::platform::modifier_supplement::KeyEventExtModifierSupplement; - event.key_without_modifiers() + event.key_without_modifiers } #[cfg(target_arch = "wasm32")] @@ -246,13 +317,10 @@ pub fn window_event( } }; - let text = { + let text: Option = { #[cfg(not(target_arch = "wasm32"))] { - use crate::core::SmolStr; - use winit::platform::modifier_supplement::KeyEventExtModifierSupplement; - - event.text_with_all_modifiers().map(SmolStr::new) + event.text_with_all_modifiers.map(SmolStr::new) } #[cfg(target_arch = "wasm32")] @@ -260,7 +328,8 @@ pub fn window_event( // TODO: Fix inconsistent API on Wasm event.text } - }.filter(|text| !text.as_str().chars().any(is_private_use)); + } + .filter(|text| !text.as_str().chars().any(is_private_use)); let winit::event::KeyEvent { state, @@ -325,24 +394,26 @@ pub fn window_event( ), Ime::Commit(content) => input_method::Event::Commit(content), Ime::Disabled => input_method::Event::Closed, + Ime::DeleteSurrounding { + before_bytes, + after_bytes, + } => todo!(), })), WindowEvent::Focused(focused) => Some(Event::Window(if focused { window::Event::Focused } else { window::Event::Unfocused })), - WindowEvent::HoveredFile(path) => { - Some(Event::Window(window::Event::FileHovered(path.clone()))) + WindowEvent::DragEntered { paths, .. } => { + Some(Event::Window(window::Event::FileHovered(paths.clone()))) } - WindowEvent::DroppedFile(path) => { - Some(Event::Window(window::Event::FileDropped(path.clone()))) + WindowEvent::DragDropped { paths, .. } => { + Some(Event::Window(window::Event::FileDropped(paths.clone()))) } - WindowEvent::HoveredFileCancelled => { + WindowEvent::DragLeft { position } => { Some(Event::Window(window::Event::FilesHoveredLeft)) } - WindowEvent::Touch(touch) => { - Some(Event::Touch(touch_event(touch, scale_factor))) - } + WindowEvent::Moved(position) => { let winit::dpi::LogicalPosition { x, y } = position.to_logical(f64::from(scale_factor)); @@ -352,6 +423,20 @@ pub fn window_event( WindowEvent::ScaleFactorChanged { scale_factor, .. } => { Some(Event::Window(window::Event::Rescaled(scale_factor as f32))) } + #[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, } } @@ -387,10 +472,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, @@ -416,10 +503,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 { @@ -447,11 +536,11 @@ pub fn position( pub fn fullscreen( monitor: Option, mode: window::Mode, -) -> Option { +) -> Option { match mode { window::Mode::Windowed | window::Mode::Hidden => None, window::Mode::Fullscreen => { - Some(winit::window::Fullscreen::Borderless(monitor)) + Some(winit_core::monitor::Fullscreen::Borderless(monitor)) } } } @@ -467,7 +556,7 @@ pub fn visible(mode: window::Mode) -> bool { /// Converts a [`winit`] fullscreen mode into a [`window::Mode`]. /// /// [`winit`]: https://github.com/rust-windowing/winit -pub fn mode(mode: Option) -> window::Mode { +pub fn mode(mode: Option) -> window::Mode { match mode { None => window::Mode::Windowed, Some(_) => window::Mode::Fullscreen, @@ -500,49 +589,55 @@ pub fn window_theme(mode: theme::Mode) -> Option { /// [`winit`]: https://github.com/rust-windowing/winit pub fn mouse_interaction( interaction: mouse::Interaction, -) -> Option { +) -> Option { use mouse::Interaction; - let icon = match interaction { + Some(match interaction { + Interaction::None | Interaction::Idle => { + winit_core::cursor::CursorIcon::Default + } + Interaction::ContextMenu => winit_core::cursor::CursorIcon::ContextMenu, + Interaction::Help => winit_core::cursor::CursorIcon::Help, + Interaction::Pointer => winit_core::cursor::CursorIcon::Pointer, + Interaction::Progress => winit_core::cursor::CursorIcon::Progress, + Interaction::Wait => winit_core::cursor::CursorIcon::Wait, + Interaction::Cell => winit_core::cursor::CursorIcon::Cell, + Interaction::Crosshair => winit_core::cursor::CursorIcon::Crosshair, + Interaction::Text => winit_core::cursor::CursorIcon::Text, + Interaction::Alias => winit_core::cursor::CursorIcon::Alias, + Interaction::Copy => winit_core::cursor::CursorIcon::Copy, + Interaction::Move => winit_core::cursor::CursorIcon::Move, + Interaction::NoDrop => winit_core::cursor::CursorIcon::NoDrop, + Interaction::NotAllowed => winit_core::cursor::CursorIcon::NotAllowed, + Interaction::Grab => winit_core::cursor::CursorIcon::Grab, + Interaction::Grabbing => winit_core::cursor::CursorIcon::Grabbing, + Interaction::ResizingHorizontally => { + winit_core::cursor::CursorIcon::EwResize + } + Interaction::ResizingVertically => { + winit_core::cursor::CursorIcon::NsResize + } + Interaction::ResizingDiagonallyUp => { + winit_core::cursor::CursorIcon::NeswResize + } + Interaction::ResizingDiagonallyDown => { + winit_core::cursor::CursorIcon::NwseResize + } + Interaction::ResizingColumn => { + winit_core::cursor::CursorIcon::ColResize + } + Interaction::ResizingRow => winit_core::cursor::CursorIcon::RowResize, + Interaction::AllScroll => winit_core::cursor::CursorIcon::AllScroll, + Interaction::ZoomIn => winit_core::cursor::CursorIcon::ZoomIn, + Interaction::ZoomOut => winit_core::cursor::CursorIcon::ZoomOut, + Interaction::Cell => winit_core::cursor::CursorIcon::Cell, + Interaction::Move => winit_core::cursor::CursorIcon::Move, + Interaction::Copy => winit_core::cursor::CursorIcon::Copy, + Interaction::Help => winit_core::cursor::CursorIcon::Help, Interaction::Hidden => { return None; } - Interaction::None | Interaction::Idle => { - winit::window::CursorIcon::Default - } - Interaction::ContextMenu => winit::window::CursorIcon::ContextMenu, - Interaction::Help => winit::window::CursorIcon::Help, - Interaction::Pointer => winit::window::CursorIcon::Pointer, - Interaction::Progress => winit::window::CursorIcon::Progress, - Interaction::Wait => winit::window::CursorIcon::Wait, - Interaction::Cell => winit::window::CursorIcon::Cell, - Interaction::Crosshair => winit::window::CursorIcon::Crosshair, - Interaction::Text => winit::window::CursorIcon::Text, - Interaction::Alias => winit::window::CursorIcon::Alias, - Interaction::Copy => winit::window::CursorIcon::Copy, - Interaction::Move => winit::window::CursorIcon::Move, - Interaction::NoDrop => winit::window::CursorIcon::NoDrop, - Interaction::NotAllowed => winit::window::CursorIcon::NotAllowed, - Interaction::Grab => winit::window::CursorIcon::Grab, - Interaction::Grabbing => winit::window::CursorIcon::Grabbing, - Interaction::ResizingHorizontally => { - winit::window::CursorIcon::EwResize - } - Interaction::ResizingVertically => winit::window::CursorIcon::NsResize, - Interaction::ResizingDiagonallyUp => { - winit::window::CursorIcon::NeswResize - } - Interaction::ResizingDiagonallyDown => { - winit::window::CursorIcon::NwseResize - } - Interaction::ResizingColumn => winit::window::CursorIcon::ColResize, - Interaction::ResizingRow => winit::window::CursorIcon::RowResize, - Interaction::AllScroll => winit::window::CursorIcon::AllScroll, - Interaction::ZoomIn => winit::window::CursorIcon::ZoomIn, - Interaction::ZoomOut => winit::window::CursorIcon::ZoomOut, - }; - - Some(icon) + }) } /// Converts a `MouseButton` from [`winit`] to an [`iced`] mouse button. @@ -556,7 +651,7 @@ pub fn mouse_button(mouse_button: winit::event::MouseButton) -> mouse::Button { winit::event::MouseButton::Middle => mouse::Button::Middle, winit::event::MouseButton::Back => mouse::Button::Back, winit::event::MouseButton::Forward => mouse::Button::Forward, - winit::event::MouseButton::Other(other) => mouse::Button::Other(other), + other => mouse::Button::Other(other as u16), } } @@ -573,7 +668,7 @@ pub fn modifiers( result.set(keyboard::Modifiers::SHIFT, modifiers.shift_key()); result.set(keyboard::Modifiers::CTRL, modifiers.control_key()); result.set(keyboard::Modifiers::ALT, modifiers.alt_key()); - result.set(keyboard::Modifiers::LOGO, modifiers.super_key()); + result.set(keyboard::Modifiers::LOGO, modifiers.meta_key()); result } @@ -581,42 +676,47 @@ pub fn modifiers( /// Converts a physical cursor position into a logical `Point`. pub fn cursor_position( position: winit::dpi::PhysicalPosition, - scale_factor: f32, + scale_factor: f64, ) -> Point { let logical_position = position.to_logical(f64::from(scale_factor)); Point::new(logical_position.x, logical_position.y) } +enum TouchInternal { + Moved, + ElementState(ElementState), + Left, +} /// Converts a `Touch` from [`winit`] to an [`iced`] touch event. /// /// [`winit`]: https://github.com/rust-windowing/winit /// [`iced`]: https://github.com/iced-rs/iced/tree/0.12 pub fn touch_event( - touch: winit::event::Touch, - scale_factor: f32, + finger: FingerId, + state: TouchInternal, + force: Option, + position: Option>, + scale_factor: f64, ) -> touch::Event { - let id = touch::Finger(touch.id); + let s = finger.into_raw() as u64; + let id = touch::Finger(s); + let position = { let location = - touch.location.to_logical::(f64::from(scale_factor)); + position.unwrap_or_default().to_logical::(scale_factor); Point::new(location.x as f32, location.y as f32) }; - - match touch.phase { - winit::event::TouchPhase::Started => { + match state { + TouchInternal::ElementState(ElementState::Pressed) => { touch::Event::FingerPressed { id, position } } - winit::event::TouchPhase::Moved => { - touch::Event::FingerMoved { id, position } - } - winit::event::TouchPhase::Ended => { + TouchInternal::ElementState(ElementState::Released) => { touch::Event::FingerLifted { id, position } } - winit::event::TouchPhase::Cancelled => { - touch::Event::FingerLost { id, position } - } + TouchInternal::Left => touch::Event::FingerLost { id, position }, + TouchInternal::Moved => touch::Event::FingerMoved { id, position }, } } @@ -643,12 +743,9 @@ pub fn key(key: winit::keyboard::Key) -> keyboard::Key { NamedKey::Shift => Named::Shift, NamedKey::Symbol => Named::Symbol, NamedKey::SymbolLock => Named::SymbolLock, - NamedKey::Meta => Named::Meta, - NamedKey::Hyper => Named::Hyper, - NamedKey::Super => Named::Super, + NamedKey::Meta => Named::Super, NamedKey::Enter => Named::Enter, NamedKey::Tab => Named::Tab, - NamedKey::Space => Named::Space, NamedKey::ArrowDown => Named::ArrowDown, NamedKey::ArrowLeft => Named::ArrowLeft, NamedKey::ArrowRight => Named::ArrowRight, @@ -1036,8 +1133,8 @@ pub fn key_code( KeyCode::ControlLeft => keyboard::key::Code::ControlLeft, KeyCode::ControlRight => keyboard::key::Code::ControlRight, KeyCode::Enter => keyboard::key::Code::Enter, - KeyCode::SuperLeft => keyboard::key::Code::SuperLeft, - KeyCode::SuperRight => keyboard::key::Code::SuperRight, + KeyCode::MetaLeft => keyboard::key::Code::SuperLeft, + KeyCode::MetaRight => keyboard::key::Code::SuperRight, KeyCode::ShiftLeft => keyboard::key::Code::ShiftLeft, KeyCode::ShiftRight => keyboard::key::Code::ShiftRight, KeyCode::Space => keyboard::key::Code::Space, @@ -1122,9 +1219,7 @@ pub fn key_code( KeyCode::AudioVolumeMute => keyboard::key::Code::AudioVolumeMute, KeyCode::AudioVolumeUp => keyboard::key::Code::AudioVolumeUp, KeyCode::WakeUp => keyboard::key::Code::WakeUp, - KeyCode::Meta => keyboard::key::Code::Meta, - KeyCode::Hyper => keyboard::key::Code::Hyper, - KeyCode::Turbo => keyboard::key::Code::Turbo, + KeyCode::Super => keyboard::key::Code::Meta, KeyCode::Abort => keyboard::key::Code::Abort, KeyCode::Resume => keyboard::key::Code::Resume, KeyCode::Suspend => keyboard::key::Code::Suspend, @@ -1240,13 +1335,21 @@ pub fn resize_direction( } } +#[derive(Debug)] +pub struct RawImage(Vec, Size); +impl IconProvider for RawImage {} +impl From for winit_core::icon::Icon { + fn from(value: RawImage) -> Self { + winit_core::icon::Icon(Arc::new(value)) + } +} + /// Converts some [`window::Icon`] into its `winit` counterpart. /// /// Returns `None` if there is an error during the conversion. -pub fn icon(icon: window::Icon) -> Option { +pub fn icon(icon: window::Icon) -> Option { let (pixels, size) = icon.into_raw(); - - winit::window::Icon::from_rgba(pixels, size.width, size.height).ok() + Some(winit_core::icon::Icon::from(RawImage(pixels, size))) } /// Converts some [`input_method::Purpose`] into its `winit` counterpart. @@ -1264,3 +1367,14 @@ pub fn ime_purpose( 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 { + // TODO include newly added target tree + let id = iced_runtime::core::id::Id::from( + u128::from(event.target_node.0) as u64 + ); + Event::A11y(id, event) +} diff --git a/winit/src/error.rs b/winit/src/error.rs index 2b7f4344..4e942678 100644 --- a/winit/src/error.rs +++ b/winit/src/error.rs @@ -10,11 +10,15 @@ 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")] GraphicsCreationFailed(graphics::Error), + + /// There was an event loop error. + #[error("there was an event loop error")] + EventLoop(winit::error::EventLoopError), } impl From for Error { diff --git a/winit/src/lib.rs b/winit/src/lib.rs index 9f48aa8c..383395ea 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -18,16 +18,27 @@ html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] #![cfg_attr(docsrs, feature(doc_cfg))] +use dnd::DndSurface; pub use iced_debug as debug; pub use iced_program as program; pub use program::core; pub use program::graphics; pub use program::runtime; pub use runtime::futures; +use window_clipboard::mime::ClipboardStoreData; pub use winit; +#[cfg(feature = "a11y")] +pub mod a11y; pub mod clipboard; pub mod conversion; +pub mod platform_specific; + +#[cfg(feature = "program")] +pub mod program; + +#[cfg(feature = "system")] +pub mod system; mod error; mod proxy; @@ -36,6 +47,8 @@ mod window; pub use clipboard::Clipboard; pub use error::Error; pub use proxy::Proxy; +use winit::dpi::LogicalSize; +use winit::dpi::PhysicalPosition; use crate::core::mouse; use crate::core::renderer; @@ -76,14 +89,21 @@ where let settings = program.settings(); let window_settings = program.window(); - 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; + + // TODO this is new.. let graphics_settings = settings.clone().into(); let display_handle = event_loop.owned_display_handle(); - 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()); #[cfg(feature = "debug")] { @@ -123,7 +143,7 @@ where runtime.enter(|| program.subscription().map(Action::Output)), )); - let (event_sender, event_receiver) = mpsc::unbounded(); + let (boot_sender, boot_receiver) = oneshot::channel(); let (control_sender, control_receiver) = mpsc::unbounded(); let (system_theme_sender, system_theme_receiver) = oneshot::channel(); @@ -131,26 +151,38 @@ where program, runtime, proxy.clone(), + boot_receiver, event_receiver, - control_sender, + control_sender.clone(), display_handle, is_daemon, + is_wayland, graphics_settings, - settings.fonts, + settings.fonts.clone(), system_theme_receiver, )); let context = task::Context::from_waker(task::noop_waker_ref()); + struct BootConfig { + sender: oneshot::Sender<()>, + fonts: Vec>, + graphics_settings: graphics::Settings, + control_sender: mpsc::UnboundedSender, + is_wayland: bool, + } struct Runner { instance: std::pin::Pin>, context: task::Context<'static>, id: Option, - sender: mpsc::UnboundedSender>>, + boot: Option, + sender: mpsc::UnboundedSender>, receiver: mpsc::UnboundedReceiver, error: Option, system_theme: Option>, + #[cfg(target_arch = "wasm32")] + is_booted: std::rc::Rc>, #[cfg(target_arch = "wasm32")] canvas: Option, } @@ -158,6 +190,13 @@ where let runner = Runner { instance, context, + boot: Some(BootConfig { + sender: boot_sender, + fonts: settings.fonts, + graphics_settings, + control_sender, + is_wayland, + }), id: settings.id, sender: event_sender, receiver: control_receiver, @@ -170,36 +209,31 @@ where boot_span.finish(); - impl winit::application::ApplicationHandler> - for Runner + impl winit::application::ApplicationHandler for Runner where F: Future, { - fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { - if let Some(sender) = self.system_theme.take() { - let _ = sender.send( - event_loop - .system_theme() - .map(conversion::theme_mode) - .unwrap_or_default(), - ); - } + 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: &winit::event_loop::ActiveEventLoop, + event_loop: &dyn winit::event_loop::ActiveEventLoop, cause: winit::event::StartCause, ) { - self.process_event( - event_loop, - Event::EventLoopAwakened(winit::event::Event::NewEvents(cause)), - ); + if self.boot.is_some() { + return; + } + self.process_event(event_loop, Some(Event::NewEvents(cause))); } fn window_event( &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, + event_loop: &dyn winit::event_loop::ActiveEventLoop, window_id: winit::window::WindowId, event: winit::event::WindowEvent, ) { @@ -212,10 +246,7 @@ where self.process_event( event_loop, - Event::EventLoopAwakened(winit::event::Event::WindowEvent { - window_id, - event, - }), + Some(Event::Winit(window_id, event)), ); // TODO: Remove when unnecessary @@ -227,35 +258,61 @@ where if is_move_or_resize { self.process_event( event_loop, - Event::EventLoopAwakened( - winit::event::Event::AboutToWait, - ), + Event::EventLoopAwakened(winit::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, + event_loop: &dyn winit::event_loop::ActiveEventLoop, ) { - self.process_event( - event_loop, - Event::EventLoopAwakened(winit::event::Event::AboutToWait), - ); + 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 finish_boot = async move { + sender.send(()).ok().expect("Send boot event"); + Ok::<_, graphics::Error>(()) + }; + + #[cfg(not(target_arch = "wasm32"))] + if let Err(error) = + crate::futures::futures::executor::block_on(finish_boot) + { + self.error = Some(Error::GraphicsCreationFailed(error)); + event_loop.exit(); + } + + #[cfg(target_arch = "wasm32")] + { + let is_booted = self.is_booted.clone(); + + wasm_bindgen_futures::spawn_local(async move { + finish_boot.await.expect("Finish boot!"); + + *is_booted.borrow_mut() = true; + }); + + event_loop + .set_control_flow(winit::event_loop::ControlFlow::Poll); + } } } @@ -265,15 +322,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); @@ -403,17 +461,20 @@ where self.process_event( event_loop, - Event::WindowCreated { + Some(Event::WindowCreated { id, - window: Arc::new(window), + window: Arc::from(window), exit_on_close_request, make_visible: visible, on_open, - }, + }), ); } Control::Exit => { - self.process_event(event_loop, Event::Exit); + self.process_event( + event_loop, + Some(Event::Exit), + ); event_loop.exit(); break; } @@ -431,6 +492,43 @@ where ); } } + 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; @@ -447,10 +545,7 @@ where #[cfg(not(target_arch = "wasm32"))] { - let mut runner = runner; - let _ = event_loop.run_app(&mut runner); - - runner.error.map(Err).unwrap_or(Ok(())) + event_loop.run_app(runner).map_err(error::Error::EventLoop) } #[cfg(target_arch = "wasm32")] @@ -466,13 +561,23 @@ where enum Event { WindowCreated { id: window::Id, - window: Arc, + window: Arc, exit_on_close_request: bool, make_visible: bool, on_open: oneshot::Sender, }, - EventLoopAwakened(winit::event::Event), Exit, + 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)] @@ -486,19 +591,30 @@ enum Control { title: String, monitor: Option, on_open: oneshot::Sender, - scale_factor: f32, + scale_factor: f64, }, SetAutomaticWindowTabbing(bool), + 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

( mut program: program::Instance

, mut runtime: Runtime, Action>, mut proxy: Proxy, - mut event_receiver: mpsc::UnboundedReceiver>>, + boot: oneshot::Receiver<()>, + mut event_receiver: mpsc::UnboundedReceiver>, mut control_sender: mpsc::UnboundedSender, display_handle: winit::event_loop::OwnedDisplayHandle, is_daemon: bool, + is_wayland: bool, graphics_settings: graphics::Settings, default_fonts: Vec>, mut _system_theme: oneshot::Receiver, @@ -509,7 +625,23 @@ async fn run_instance

( use winit::event; use winit::event_loop::ControlFlow; - let mut window_manager = WindowManager::new(); + _ = 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.clone(), + ); + } + + let mut window_manager: WindowManager< + P, + <

::Renderer as compositor::Default>::Compositor, + > = WindowManager::new(); let mut is_window_opening = !is_daemon; let mut compositor = None; @@ -520,6 +652,54 @@ async fn run_instance

( let mut ui_caches = FxHashMap::default(); let mut user_interfaces = ManuallyDrop::new(FxHashMap::default()); let mut clipboard = Clipboard::unconnected(); + let mut cur_dnd_surface: Option = None; + + #[cfg(feature = "a11y")] + let (mut adapters, mut a11y_enabled) = (Default::default(), false); + // 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, Node, 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) + // }; #[cfg(all(feature = "linux-theme-detection", target_os = "linux"))] let mut system_theme = { @@ -656,13 +836,14 @@ async fn run_instance

( } let is_first = window_manager.is_empty(); + let compositor: &mut <

::Renderer as compositor::Default >::Compositor = compositor + .as_mut() + .expect("Compositor must be initialized"); let window = window_manager.insert( id, window, &program, - compositor - .as_mut() - .expect("Compositor must be initialized"), + compositor, exit_on_close_request, system_theme, ); @@ -689,6 +870,9 @@ async fn run_instance

( &mut window.renderer, logical_size, id, + window.raw.clone(), + window.prev_dnd_destination_rectangles_count, + &mut clipboard, ), ); let _ = ui_caches.insert(id, user_interface::Cache::default()); @@ -698,7 +882,7 @@ async fn run_instance

( } events.push(( - id, + Some(id), core::Event::Window(window::Event::Opened { position: window.position(), size: window.logical_size(), @@ -706,45 +890,488 @@ 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) => { - match event { - event::Event::NewEvents(event::StartCause::Init) => { - for (_id, window) in window_manager.iter_mut() { + Event::NewEvents(event::StartCause::Init) => { + for (_id, window) in window_manager.iter_mut() { + window.raw.request_redraw(); + } + } + Event::NewEvents(event::StartCause::ResumeTimeReached { + .. + }) => { + let now = Instant::now(); + + for (_id, window) in window_manager.iter_mut() { + if let Some(redraw_at) = window.redraw_at + && redraw_at <= now + { + window.raw.request_redraw(); + window.redraw_at = None; + } + } + + if let Some(redraw_at) = window_manager.redraw_at() { + let _ = control_sender.start_send(Control::ChangeFlow( + ControlFlow::WaitUntil(redraw_at), + )); + } else { + let _ = control_sender + .start_send(Control::ChangeFlow(ControlFlow::Wait)); + } + } + Event::UserEvent(action) => { + run_action( + action, + &program, + &mut runtime, + &mut compositor, + &mut events, + &mut messages, + &mut clipboard, + &mut control_sender, + &mut user_interfaces, + &mut window_manager, + &mut ui_caches, + &mut is_window_opening, + &mut system_theme, + &mut platform_specific_handler, + ); + actions += 1; + } + Event::Winit(window_id, event) => { + let Some(mut current_compositor) = compositor.as_mut() else { + continue; + }; + + let Some((id, mut window)) = + window_manager.get_mut_alias(window_id) + else { + continue; + }; + + let physical_size = window.state.physical_size(); + let mut logical_size = window.state.logical_size(); + + if physical_size.width == 0 || physical_size.height == 0 { + continue; + } + + // Window was resized between redraws + if window.surface_version != window.state.surface_version() { + let ui = user_interfaces + .remove(&id) + .expect("Remove user interface"); + + let layout_span = debug::layout(id); + let _ = user_interfaces.insert( + id, + ui.relayout(logical_size, &mut window.renderer), + ); + layout_span.finish(); + + current_compositor.configure_surface( + &mut window.surface, + physical_size.width, + physical_size.height, + ); + + window.surface_version = window.state.surface_version(); + } + + let redraw_event = core::Event::Window( + window::Event::RedrawRequested(Instant::now()), + ); + + let cursor = window.state.cursor(); + + let mut interface = + user_interfaces.get_mut(&id).expect("Get user interface"); + + let interact_span = debug::interact(id); + let mut redraw_count = 0; + + let state = loop { + let message_count = messages.len(); + let (state, _) = interface.update( + slice::from_ref(&redraw_event), + cursor, + &mut window.renderer, + &mut clipboard, + &mut messages, + ); + + if message_count == messages.len() + && !state.has_layout_changed() + { + break state; + } + + if redraw_count >= 2 { + log::warn!( + "More than 3 consecutive RedrawRequested events \ + produced layout invalidation" + ); + + break state; + } + + redraw_count += 1; + + if !messages.is_empty() { + let caches: FxHashMap<_, _> = + ManuallyDrop::into_inner(user_interfaces) + .into_iter() + .map(|(id, interface)| { + (id, interface.into_cache()) + }) + .collect(); + + let actions = + update(&mut program, &mut runtime, &mut messages); + + user_interfaces = + ManuallyDrop::new(build_user_interfaces( + &program, + &mut window_manager, + caches, + &mut clipboard, + )); + + for action in actions { + // Defer all window actions to avoid compositor + // race conditions while redrawing + if let Action::Window(_) = action { + proxy.send_action(action); + continue; + } + + run_action( + action, + &program, + &mut runtime, + &mut compositor, + &mut events, + &mut messages, + &mut clipboard, + &mut control_sender, + &mut user_interfaces, + &mut window_manager, + &mut ui_caches, + &mut is_window_opening, + &mut system_theme, + &mut platform_specific_handler, + ); + } + + for (window_id, window) in window_manager.iter_mut() { + // We are already redrawing this window + if window_id == id { + continue; + } + window.raw.request_redraw(); } - } - event::Event::NewEvents( - event::StartCause::ResumeTimeReached { .. }, - ) => { - let now = Instant::now(); - for (_id, window) in window_manager.iter_mut() { - if let Some(redraw_at) = window.redraw_at - && redraw_at <= now - { - window.raw.request_redraw(); - window.redraw_at = None; - } + let Some(next_compositor) = compositor.as_mut() else { + continue 'next_event; + }; + + current_compositor = next_compositor; + window = window_manager.get_mut(id).unwrap(); + + // Window scale factor changed during a redraw request + if logical_size != window.state.logical_size() { + logical_size = window.state.logical_size(); + + log::debug!( + "Window scale factor changed during a redraw request" + ); + + let ui = user_interfaces + .remove(&id) + .expect("Remove user interface"); + + let layout_span = debug::layout(id); + let _ = user_interfaces.insert( + id, + ui.relayout(logical_size, &mut window.renderer), + ); + layout_span.finish(); } - if let Some(redraw_at) = window_manager.redraw_at() { - let _ = - control_sender.start_send(Control::ChangeFlow( - ControlFlow::WaitUntil(redraw_at), - )); - } else { - let _ = control_sender.start_send( - Control::ChangeFlow(ControlFlow::Wait), + interface = user_interfaces.get_mut(&id).unwrap(); + } + }; + interact_span.finish(); + + let draw_span = debug::draw(id); + interface.draw( + &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, + ); + draw_span.finish(); + + if let user_interface::State::Updated { + redraw_request, + input_method, + mouse_interaction, + .. + } = state + { + window.request_redraw(redraw_request); + window.request_input_method(input_method); + window.update_mouse(mouse_interaction); + } + + runtime.broadcast(subscription::Event::Interaction { + window: id, + event: redraw_event, + status: core::event::Status::Ignored, + }); + + window.draw_preedit(); + + let present_span = debug::present(id); + match current_compositor.present( + &mut window.renderer, + &mut window.surface, + window.state.viewport(), + window.state.background_color(), + || window.raw.pre_present_notify(), + ) { + Ok(()) => { + present_span.finish(); + } + Err(error) => match error { + compositor::SurfaceError::OutOfMemory => { + // This is an unrecoverable error. + panic!("{error:?}"); + } + compositor::SurfaceError::Outdated + | compositor::SurfaceError::Lost => { + present_span.finish(); + + // Reconfigure surface and try redrawing + let physical_size = window.state.physical_size(); + + if error == compositor::SurfaceError::Lost { + window.surface = current_compositor + .create_surface( + window.raw.clone(), + physical_size.width, + physical_size.height, + ); + } else { + current_compositor.configure_surface( + &mut window.surface, + physical_size.width, + physical_size.height, + ); + } + + window.raw.request_redraw(); + } + _ => { + present_span.finish(); + + 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(); + } + } + }, + } + + if !is_daemon + && matches!(event, winit::event::WindowEvent::Destroyed) + && !is_window_opening + && window_manager.is_empty() + { + control_sender + .start_send(Control::Exit) + .expect("Send control action"); + + continue; + } + + let Some((id, window)) = + window_manager.get_mut_alias(window_id) + else { + continue; + }; + + match event { + winit::event::WindowEvent::SurfaceResized(_) => { + window.raw.request_redraw(); + } + winit::event::WindowEvent::ThemeChanged(theme) => { + let mode = conversion::theme_mode(theme); + + if mode != system_theme { + system_theme = mode; + + runtime.broadcast( + subscription::Event::SystemThemeChanged(mode), ); } } - event::Event::UserEvent(action) => { + _ => {} + } + + if matches!(event, winit::event::WindowEvent::CloseRequested) + && window.exit_on_close_request + { + run_action( + Action::Window(runtime::window::Action::Close(id)), + &program, + &mut runtime, + &mut compositor, + &mut events, + &mut messages, + &mut clipboard, + &mut control_sender, + &mut user_interfaces, + &mut window_manager, + &mut ui_caches, + &mut is_window_opening, + &mut system_theme, + &mut platform_specific_handler, + ); + } else { + window.state.update(&program, window.raw.as_ref(), &event); + + if let Some(event) = conversion::window_event( + event, + window.state.scale_factor(), + window.state.modifiers(), + ) { + events.push((Some(id), event)); + } + } + } + Event::AboutToWait => { + if actions > 0 { + proxy.free_slots(actions); + actions = 0; + } + + if events.is_empty() + && messages.is_empty() + && window_manager.is_idle() + { + continue; + } + + let mut uis_stale = false; + + for (id, window) in window_manager.iter_mut() { + let interact_span = debug::interact(id); + let mut window_events = vec![]; + + events.retain(|(window_id, event)| { + if *window_id == Some(id) { + window_events.push(event.clone()); + false + } else { + true + } + }); + + if window_events.is_empty() && messages.is_empty() { + continue; + } + + 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, + ); + + #[cfg(feature = "unconditional-rendering")] + window.request_redraw(window::RedrawRequest::NextFrame); + + match ui_state { + user_interface::State::Updated { + redraw_request: _redraw_request, + mouse_interaction, + .. + } => { + window.update_mouse(mouse_interaction); + + #[cfg(not(feature = "unconditional-rendering"))] + window.request_redraw(_redraw_request); + } + user_interface::State::Outdated => { + uis_stale = true; + } + } + + for (event, status) in + window_events.into_iter().zip(statuses.into_iter()) + { + runtime.broadcast(subscription::Event::Interaction { + window: id, + event, + status, + }); + } + + interact_span.finish(); + } + + 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 !messages.is_empty() || uis_stale { + let cached_interfaces: FxHashMap<_, _> = + ManuallyDrop::into_inner(user_interfaces) + .into_iter() + .map(|(id, ui)| (id, ui.into_cache())) + .collect(); + + let actions = + update(&mut program, &mut runtime, &mut messages); + + user_interfaces = ManuallyDrop::new(build_user_interfaces( + &program, + &mut window_manager, + cached_interfaces, + &mut clipboard, + )); + + for action in actions { run_action( action, &program, @@ -759,516 +1386,103 @@ async fn run_instance

( &mut ui_caches, &mut is_window_opening, &mut system_theme, + &mut platform_specific_handler, ); - actions += 1; } - event::Event::WindowEvent { - window_id: id, - event: event::WindowEvent::RedrawRequested, - .. - } => { - let Some(mut current_compositor) = compositor.as_mut() - else { - continue; - }; - let Some((id, mut window)) = - window_manager.get_mut_alias(id) - else { - continue; - }; - - let physical_size = window.state.physical_size(); - let mut logical_size = window.state.logical_size(); - - if physical_size.width == 0 || physical_size.height == 0 - { - continue; - } - - // Window was resized between redraws - if window.surface_version - != window.state.surface_version() - { - let ui = user_interfaces - .remove(&id) - .expect("Remove user interface"); - - let layout_span = debug::layout(id); - let _ = user_interfaces.insert( - id, - ui.relayout(logical_size, &mut window.renderer), - ); - layout_span.finish(); - - current_compositor.configure_surface( - &mut window.surface, - physical_size.width, - physical_size.height, - ); - - window.surface_version = - window.state.surface_version(); - } - - let redraw_event = core::Event::Window( - window::Event::RedrawRequested(Instant::now()), - ); - - let cursor = window.state.cursor(); - - let mut interface = user_interfaces - .get_mut(&id) - .expect("Get user interface"); - - let interact_span = debug::interact(id); - let mut redraw_count = 0; - - let state = loop { - let message_count = messages.len(); - let (state, _) = interface.update( - slice::from_ref(&redraw_event), - cursor, - &mut window.renderer, - &mut clipboard, - &mut messages, - ); - - if message_count == messages.len() - && !state.has_layout_changed() - { - break state; - } - - if redraw_count >= 2 { - log::warn!( - "More than 3 consecutive RedrawRequested events \ - produced layout invalidation" - ); - - break state; - } - - redraw_count += 1; - - if !messages.is_empty() { - let caches: FxHashMap<_, _> = - ManuallyDrop::into_inner(user_interfaces) - .into_iter() - .map(|(id, interface)| { - (id, interface.into_cache()) - }) - .collect(); - - let actions = update( - &mut program, - &mut runtime, - &mut messages, - ); - - user_interfaces = - ManuallyDrop::new(build_user_interfaces( - &program, - &mut window_manager, - caches, - )); - - for action in actions { - // Defer all window actions to avoid compositor - // race conditions while redrawing - if let Action::Window(_) = action { - proxy.send_action(action); - continue; - } - - run_action( - action, - &program, - &mut runtime, - &mut compositor, - &mut events, - &mut messages, - &mut clipboard, - &mut control_sender, - &mut user_interfaces, - &mut window_manager, - &mut ui_caches, - &mut is_window_opening, - &mut system_theme, - ); - } - - for (window_id, window) in - window_manager.iter_mut() - { - // We are already redrawing this window - if window_id == id { - continue; - } - - window.raw.request_redraw(); - } - - let Some(next_compositor) = compositor.as_mut() - else { - continue 'next_event; - }; - - current_compositor = next_compositor; - window = window_manager.get_mut(id).unwrap(); - - // Window scale factor changed during a redraw request - if logical_size != window.state.logical_size() { - logical_size = window.state.logical_size(); - - log::debug!( - "Window scale factor changed during a redraw request" - ); - - let ui = user_interfaces - .remove(&id) - .expect("Remove user interface"); - - let layout_span = debug::layout(id); - let _ = user_interfaces.insert( - id, - ui.relayout( - logical_size, - &mut window.renderer, - ), - ); - layout_span.finish(); - } - - interface = - user_interfaces.get_mut(&id).unwrap(); - } - }; - interact_span.finish(); - - let draw_span = debug::draw(id); - interface.draw( - &mut window.renderer, - window.state.theme(), - &renderer::Style { - text_color: window.state.text_color(), - }, - cursor, - ); - draw_span.finish(); - - if let user_interface::State::Updated { - redraw_request, - input_method, - mouse_interaction, - .. - } = state - { - window.request_redraw(redraw_request); - window.request_input_method(input_method); - window.update_mouse(mouse_interaction); - } - - runtime.broadcast(subscription::Event::Interaction { - window: id, - event: redraw_event, - status: core::event::Status::Ignored, - }); - - window.draw_preedit(); - - let present_span = debug::present(id); - match current_compositor.present( - &mut window.renderer, - &mut window.surface, - window.state.viewport(), - window.state.background_color(), - || window.raw.pre_present_notify(), - ) { - Ok(()) => { - present_span.finish(); - } - Err(error) => match error { - compositor::SurfaceError::OutOfMemory => { - // This is an unrecoverable error. - panic!("{error:?}"); - } - compositor::SurfaceError::Outdated - | compositor::SurfaceError::Lost => { - present_span.finish(); - - // Reconfigure surface and try redrawing - let physical_size = - window.state.physical_size(); - - if error == compositor::SurfaceError::Lost { - window.surface = current_compositor - .create_surface( - window.raw.clone(), - physical_size.width, - physical_size.height, - ); - } else { - current_compositor.configure_surface( - &mut window.surface, - physical_size.width, - physical_size.height, - ); - } - - window.raw.request_redraw(); - } - _ => { - present_span.finish(); - - 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(); - } - } - }, - } + for (_id, window) in window_manager.iter_mut() { + window.raw.request_redraw(); } - event::Event::WindowEvent { - event: window_event, - window_id, - } => { - if !is_daemon - && matches!( - window_event, - winit::event::WindowEvent::Destroyed - ) - && !is_window_opening - && window_manager.is_empty() - { - control_sender - .start_send(Control::Exit) - .expect("Send control action"); + } - continue; - } - - let Some((id, window)) = - window_manager.get_mut_alias(window_id) - else { - continue; - }; - - match window_event { - winit::event::WindowEvent::Resized(_) => { - window.raw.request_redraw(); - } - winit::event::WindowEvent::ThemeChanged(theme) => { - let mode = conversion::theme_mode(theme); - - if mode != system_theme { - system_theme = mode; - - runtime.broadcast( - subscription::Event::SystemThemeChanged( - mode, - ), - ); - } - } - _ => {} - } - - if matches!( - window_event, - winit::event::WindowEvent::CloseRequested - ) && window.exit_on_close_request - { - run_action( - Action::Window(runtime::window::Action::Close( - id, - )), - &program, - &mut runtime, - &mut compositor, - &mut events, - &mut messages, - &mut clipboard, - &mut control_sender, - &mut user_interfaces, - &mut window_manager, - &mut ui_caches, - &mut is_window_opening, - &mut system_theme, - ); - } else { - window.state.update( - &program, - &window.raw, - &window_event, - ); - - if let Some(event) = conversion::window_event( - window_event, - window.state.scale_factor(), - window.state.modifiers(), - ) { - events.push((id, event)); - } - } - } - event::Event::AboutToWait => { - if actions > 0 { - proxy.free_slots(actions); - actions = 0; - } - - if events.is_empty() - && messages.is_empty() - && window_manager.is_idle() - { - continue; - } - - let mut uis_stale = false; - - for (id, window) in window_manager.iter_mut() { - let interact_span = debug::interact(id); - let mut window_events = vec![]; - - events.retain(|(window_id, event)| { - if *window_id == id { - window_events.push(event.clone()); - false - } else { - true - } - }); - - if window_events.is_empty() && messages.is_empty() { - continue; - } - - 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, - ); - - #[cfg(feature = "unconditional-rendering")] - window.request_redraw( - window::RedrawRequest::NextFrame, - ); - - match ui_state { - user_interface::State::Updated { - redraw_request: _redraw_request, - mouse_interaction, - .. - } => { - window.update_mouse(mouse_interaction); - - #[cfg(not( - feature = "unconditional-rendering" - ))] - window.request_redraw(_redraw_request); - } - user_interface::State::Outdated => { - uis_stale = true; - } - } - - for (event, status) in window_events - .into_iter() - .zip(statuses.into_iter()) - { - runtime.broadcast( - subscription::Event::Interaction { - window: id, - event, - status, - }, - ); - } - - interact_span.finish(); - } - - for (id, event) in events.drain(..) { - runtime.broadcast( - subscription::Event::Interaction { - window: id, - event, - status: core::event::Status::Ignored, - }, - ); - } - - if !messages.is_empty() || uis_stale { - let cached_interfaces: FxHashMap<_, _> = - ManuallyDrop::into_inner(user_interfaces) - .into_iter() - .map(|(id, ui)| (id, ui.into_cache())) - .collect(); - - let actions = update( - &mut program, - &mut runtime, - &mut messages, - ); - - user_interfaces = - ManuallyDrop::new(build_user_interfaces( - &program, - &mut window_manager, - cached_interfaces, - )); - - for action in actions { - run_action( - action, - &program, - &mut runtime, - &mut compositor, - &mut events, - &mut messages, - &mut clipboard, - &mut control_sender, - &mut user_interfaces, - &mut window_manager, - &mut ui_caches, - &mut is_window_opening, - &mut system_theme, - ); - } - - for (_id, window) in window_manager.iter_mut() { - window.raw.request_redraw(); - } - } - - if let Some(redraw_at) = window_manager.redraw_at() { - let _ = - control_sender.start_send(Control::ChangeFlow( - ControlFlow::WaitUntil(redraw_at), - )); - } else { - let _ = control_sender.start_send( - Control::ChangeFlow(ControlFlow::Wait), - ); - } - } - _ => {} + if let Some(redraw_at) = window_manager.redraw_at() { + let _ = control_sender.start_send(Control::ChangeFlow( + ControlFlow::WaitUntil(redraw_at), + )); + } else { + let _ = control_sender + .start_send(Control::ChangeFlow(ControlFlow::Wait)); } } Event::Exit => break, + Event::Dnd(e) => { + use raw_window_handle::HasWindowHandle; + 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, action_request) => { + match action_request.action { + iced_accessibility::accesskit::Action::Focus => { + // TODO send a command for this + } + _ => {} + } + events.push((Some(id), conversion::a11y(action_request))); + } + #[cfg(feature = "a11y")] + Event::AccessibilityEnabled(enabled) => { + a11y_enabled = enabled; + } + Event::PlatformSpecific(e) => { + if let Some(c) = compositor.as_mut() { + crate::platform_specific::handle_event( + e, + &mut events, + &mut platform_specific_handler, + &program, + c, + &mut window_manager, + &mut user_interfaces, + &mut clipboard, + #[cfg(feature = "a11y")] + &mut adapters, + ); + } else { + // TODO should we break? + break; + } + } + Event::StartDnd => {} + _ => {} } } @@ -1282,6 +1496,9 @@ fn build_user_interface<'a, P: Program>( renderer: &mut P::Renderer, size: Size, 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: theme::Base, @@ -1294,6 +1511,17 @@ where let user_interface = UserInterface::build(view, size, cache, renderer); layout_span.finish(); + 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 } @@ -1347,7 +1575,7 @@ fn run_action<'a, P, C>( program: &'a program::Instance

, runtime: &mut Runtime, Action>, compositor: &mut Option, - events: &mut Vec<(window::Id, core::Event)>, + events: &mut Vec<(Option, core::Event)>, messages: &mut Vec, clipboard: &mut Clipboard, control_sender: &mut mpsc::UnboundedSender, @@ -1359,6 +1587,7 @@ fn run_action<'a, P, C>( ui_caches: &mut FxHashMap, is_window_opening: &mut bool, system_theme: &mut theme::Mode, + platform_specific: &mut crate::platform_specific::PlatformSpecific, ) where P: Program, C: Compositor + 'static, @@ -1378,6 +1607,13 @@ fn run_action<'a, P, C>( 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) => { @@ -1399,18 +1635,28 @@ fn run_action<'a, P, C>( window::Action::Close(id) => { let _ = ui_caches.remove(&id); let _ = interfaces.remove(&id); + let proxy = clipboard.proxy(); if let Some(window) = window_manager.remove(id) { if clipboard.window_id() == Some(window.raw.id()) { *clipboard = window_manager .first() .map(|window| window.raw.clone()) - .map(Clipboard::connect) + .zip(proxy.clone()) + .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), )); } @@ -1445,54 +1691,48 @@ fn run_action<'a, P, C>( } 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, } - .to_physical::(f64::from( - window.state.scale_factor(), - )), + .into(), ); } } window::Action::SetMinSize(id, size) => { if let Some(window) = window_manager.get_mut(id) { - window.raw.set_min_inner_size(size.map(|size| { + window.raw.set_min_surface_size(size.map(|size| { winit::dpi::LogicalSize { width: size.width, height: size.height, } - .to_physical::(f64::from( - window.state.scale_factor(), - )) + .into() })); } } window::Action::SetMaxSize(id, size) => { if let Some(window) = window_manager.get_mut(id) { - window.raw.set_max_inner_size(size.map(|size| { + window.raw.set_max_surface_size(size.map(|size| { winit::dpi::LogicalSize { width: size.width, height: size.height, } - .to_physical::(f64::from( - window.state.scale_factor(), - )) + .into() })); } } window::Action::SetResizeIncrements(id, increments) => { if let Some(window) = window_manager.get_mut(id) { - window.raw.set_resize_increments(increments.map(|size| { - winit::dpi::LogicalSize { - width: size.width, - height: size.height, - } - .to_physical::(f64::from( - window.state.scale_factor(), - )) - })); + window.raw.set_surface_resize_increments(increments.map( + |size| { + winit::dpi::LogicalSize { + width: size.width, + height: size.height, + } + .into() + }, + )); } } window::Action::SetResizable(id, resizable) => { @@ -1531,7 +1771,7 @@ fn run_action<'a, P, C>( let position = window .raw .outer_position() - .map(|position| { + .map(|position: PhysicalPosition| { let position = position .to_logical::(window.raw.scale_factor()); @@ -1555,7 +1795,8 @@ fn run_action<'a, P, C>( winit::dpi::LogicalPosition { x: position.x, y: position.y, - }, + } + .into(), ); } } @@ -1618,15 +1859,18 @@ fn run_action<'a, P, C>( && let mouse::Cursor::Available(point) = window.state.cursor() { - window.raw.show_window_menu(winit::dpi::LogicalPosition { - x: point.x, - y: point.y, - }); + window.raw.show_window_menu( + winit::dpi::LogicalPosition { + x: point.x, + y: point.y, + } + .into(), + ); } } window::Action::GetRawId(id, channel) => { if let Some(window) = window_manager.get_mut(id) { - let _ = channel.send(window.raw.id().into()); + let _ = channel.send(window.raw.id().into_raw() as u64); // TODO make these types match? } } window::Action::Run(id, f) => { @@ -1647,7 +1891,7 @@ fn run_action<'a, P, C>( let _ = channel.send(core::window::Screenshot::new( bytes, window.state.physical_size(), - window.state.scale_factor(), + window.state.scale_factor() as f32, )); } } @@ -1663,12 +1907,17 @@ fn run_action<'a, P, C>( } window::Action::GetMonitorSize(id, channel) => { if let Some(window) = window_manager.get(id) { - let size = window.raw.current_monitor().map(|monitor| { - let scale = window.state.scale_factor(); - let size = monitor.size().to_logical(f64::from(scale)); + let size = window + .raw + .current_monitor() + .and_then(|m| m.current_video_mode()) + .map(|monitor| { + let scale = window.state.scale_factor(); + let size = + monitor.size().to_logical(f64::from(scale)); - Size::new(size.width, size.height) - }); + Size::new(size.width, size.height) + }); let _ = channel.send(size); } @@ -1733,7 +1982,7 @@ fn run_action<'a, P, C>( for (_id, window) in window_manager.iter_mut() { window.state.update( program, - &window.raw, + window.raw.as_ref(), &winit::event::WindowEvent::ThemeChanged(theme), ); } @@ -1798,6 +2047,9 @@ fn run_action<'a, P, C>( &mut window.renderer, size, id, + window.raw.clone(), + window.prev_dnd_destination_rectangles_count, + clipboard, ), ); @@ -1809,6 +2061,36 @@ fn run_action<'a, P, C>( .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::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); + } + iced_runtime::dnd::DndAction::StartDnd { + internal, + source_surface, + icon_surface, + content, + actions, + } => { + // removed + } + }, + Action::PlatformSpecific(a) => { + platform_specific.send_action(a); + } } } @@ -1817,13 +2099,14 @@ pub fn build_user_interfaces<'a, P: Program, C>( program: &'a program::Instance

, window_manager: &mut WindowManager, mut cached_user_interfaces: FxHashMap, + clipboard: &mut Clipboard, ) -> FxHashMap> where C: Compositor, P::Theme: theme::Base, { for (id, window) in window_manager.iter_mut() { - window.state.synchronize(program, id, &window.raw); + window.state.synchronize(program, id, window.raw.as_ref()); } debug::theme_changed(|| { @@ -1845,6 +2128,9 @@ where &mut window.renderer, window.state.logical_size(), id, + window.raw.clone(), + window.prev_dnd_destination_rectangles_count, + clipboard, ), )) }) @@ -1905,3 +2191,7 @@ fn system_information( graphics_backend: graphics.backend, } } +#[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 00000000..9f6dc302 --- /dev/null +++ b/winit/src/platform_specific/mod.rs @@ -0,0 +1,188 @@ +//! Wayland specific shell +//! + +use std::collections::HashMap; + +use iced_graphics::{Compositor, compositor}; +use iced_runtime::{core::window, platform_specific, user_interface}; +#[cfg(all(feature = "wayland", target_os = "linux"))] +use sctk::reexports::client::Connection; + +#[cfg(all(feature = "wayland", target_os = "linux"))] +pub mod wayland; + +#[cfg(all(feature = "wayland", target_os = "linux"))] +pub use wayland::*; +#[cfg(all(feature = "wayland", target_os = "linux"))] +use wayland_backend::client::Backend; + +use crate::{Program, WindowManager}; + +pub type UserInterfaces<'a, P> = HashMap< + window::Id, + user_interface::UserInterface< + 'a, +

::Message, +

::Theme, +

::Renderer, + >, + rustc_hash::FxBuildHasher, +>; + +#[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::{ + Proxy, protocol::wl_surface::WlSurface, + }; + 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>( + e: Event, + events: &mut Vec<(Option, iced_runtime::core::Event)>, + platform_specific: &mut PlatformSpecific, + program: &'a crate::program::Instance

, + compositor: &mut <

::Renderer as compositor::Default>::Compositor, + window_manager: &mut WindowManager< + P, + <

::Renderer as compositor::Default>::Compositor, + >, + 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, +{ + match e { + #[cfg(all(feature = "wayland", target_os = "linux"))] + Event::Wayland(e) => { + platform_specific.wayland.handle_event( + e, + events, + program, + compositor, + window_manager, + 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 00000000..fa051031 --- /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 00000000..84c580b4 --- /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 00000000..d097333d --- /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 00000000..f70d97f4 --- /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 00000000..5a327552 --- /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 00000000..e980157f --- /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 00000000..bc920ed4 --- /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 00000000..9b8891d1 --- /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::{ + Control, + futures::futures::channel::mpsc, + platform_specific::wayland::{ + handlers::{ + wp_fractional_scaling::FractionalScalingManager, + wp_viewporter::ViewporterState, + }, + sctk_event::SctkEvent, + }, + 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::{ + ConnectError, Connection, Proxy, globals::registry_queue_init, + }, + }, + registry::RegistryState, + seat::SeatState, + session_lock::SessionLockState, + shell::{WaylandSurface, wlr_layer::LayerShell, xdg::XdgShell}, + 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 Ok(dh) = display.display_handle() else { + log::error!("Failed to get display handle"); + return Err(Box::new(())); + }; + 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 (action_tx, action_rx) = calloop::channel::channel(); + let res = std::thread::spawn(move || { + 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::platform_specific::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::platform_specific::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_raw( + id.as_ptr() as usize + ), + 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 00000000..7140cef7 --- /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 00000000..ad53e4bc --- /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, + }, 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 00000000..5b8b0f5a --- /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 00000000..a1b63683 --- /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 00000000..e584f44d --- /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 00000000..8bc92917 --- /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 00000000..eafaef9a --- /dev/null +++ b/winit/src/platform_specific/wayland/handlers/seat/keyboard.rs @@ -0,0 +1,257 @@ +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_raw( + surface.id().as_ptr() as usize + ); + 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_raw( + surface.id().as_ptr() as usize, + ); + 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 00000000..38369b43 --- /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 00000000..d2a0beac --- /dev/null +++ b/winit/src/platform_specific/wayland/handlers/seat/pointer.rs @@ -0,0 +1,204 @@ +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::{ + ButtonSource, MouseButton, MouseScrollDelta, PointerKind, + PointerSource, 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_raw( + e.surface.id().as_ptr() as usize, + ); + 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::PointerEntered { + device_id: Default::default(), + position: e.position.into(), + primary: is_active, + kind: PointerKind::Mouse, + } + } + PointerEventKind::Leave { serial } => { + WindowEvent::PointerLeft { + device_id: Default::default(), + position: Some(e.position.into()), + primary: is_active, + kind: PointerKind::Mouse, + } + } + PointerEventKind::Motion { time } => { + WindowEvent::PointerMoved { + device_id: Default::default(), + position: e.position.into(), + primary: is_active, + source: PointerSource::Mouse, + } + } + PointerEventKind::Press { + time, + button, + serial, + } => WindowEvent::PointerButton { + device_id: Default::default(), + state: winit::event::ElementState::Pressed, + button: ButtonSource::Mouse( + wayland_button_to_winit(button), + ), + position: e.position.into(), + primary: is_active, + }, + PointerEventKind::Release { + time, + button, + serial, + } => WindowEvent::PointerButton { + device_id: Default::default(), + state: winit::event::ElementState::Released, + button: ButtonSource::Mouse( + wayland_button_to_winit(button), + ), + position: e.position.into(), + primary: is_active, + }, + 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::try_from_u8(button as u8) + .unwrap_or(MouseButton::Button32), // TODO why was Other variant removed? + } +} + +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 00000000..ee7d83e5 --- /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 00000000..99e6caca --- /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 00000000..27ae70b7 --- /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 00000000..d7d8b3e8 --- /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 00000000..5556c08d --- /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 00000000..0975dc0b --- /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 00000000..24da3cfa --- /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 00000000..40f9a0db --- /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 00000000..5ad326af --- /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 00000000..c0ca1025 --- /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 00000000..116625a0 --- /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::MetaLeft, + 126 => KeyCode::MetaRight, + 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::MetaLeft => Some(125), + KeyCode::MetaRight => 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, Location, key::Named}; + +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 00000000..16daf829 --- /dev/null +++ b/winit/src/platform_specific/wayland/mod.rs @@ -0,0 +1,210 @@ +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::{Control, Program, WindowManager}; + +use crate::platform_specific::UserInterfaces; +use cursor_icon::CursorIcon; +use iced_futures::futures::channel::mpsc; +use iced_graphics::{Compositor, compositor}; +use iced_runtime::core::window; +use sctk::reexports::calloop; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::seat::keyboard::Modifiers; +use sctk_event::SctkEvent; +use std::{collections::HashMap, sync::Arc}; +use subsurface_widget::{SubsurfaceInstance, SubsurfaceState}; +use wayland_backend::client::ObjectId; +use winit::event_loop::OwnedDisplayHandle; + +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>( + &mut self, + e: SctkEvent, + events: &mut Vec<(Option, iced_runtime::core::Event)>, + program: &'a crate::program::Instance

, + compositor: &mut <

::Renderer as compositor::Default>::Compositor, + window_manager: &mut WindowManager< + P, + <

::Renderer as compositor::Default>::Compositor, + >, + 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, + { + 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, + 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 00000000..9d6f36fd --- /dev/null +++ b/winit/src/platform_specific/wayland/sctk_event.rs @@ -0,0 +1,1212 @@ +use crate::{ + Clipboard, Control, Program, + platform_specific::{ + SurfaceIdWrapper, UserInterfaces, + wayland::{ + conversion::{ + modifiers_to_native, pointer_axis_to_native, + pointer_button_to_native, + }, + keymap::{self, keysym_to_key}, + subsurface_widget::SubsurfaceState, + }, + }, +}; + +use dnd::DndSurface; +use iced_debug::core::theme; +use iced_futures::{ + core::{ + Clipboard as _, + event::{ + PlatformSpecific, + wayland::{LayerEvent, PopupEvent, SessionLockEvent}, + }, + }, + futures::channel::mpsc, +}; +use iced_graphics::{Compositor, compositor}; +use iced_runtime::{ + core::{ + Point, + event::wayland, + keyboard, mouse, touch, + window::{self, Id as SurfaceId}, + }, + keyboard::{Key, Location, key}, + user_interface, +}; + +use sctk::{ + output::OutputInfo, + reexports::{ + calloop::channel, + client::{ + Proxy, QueueHandle, + backend::ObjectId, + protocol::{ + wl_display::WlDisplay, wl_keyboard::WlKeyboard, + wl_output::WlOutput, wl_pointer::WlPointer, wl_seat::WlSeat, + wl_surface::WlSurface, wl_touch::WlTouch, + }, + }, + csd_frame::WindowManagerCapabilities, + }, + seat::{ + Capability, + keyboard::{KeyEvent, Modifiers}, + pointer::{PointerEvent, PointerEventKind}, + }, + 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, +} + +impl SctkEvent { + pub(crate) fn process<'a, P>( + self, + modifiers: &mut Modifiers, + program: &'a crate::program::Instance

, + compositor: &mut <

::Renderer as compositor::Default>::Compositor, + window_manager: &mut crate::WindowManager< + P, + <

::Renderer as compositor::Default>::Compositor, + >, + surface_ids: &mut HashMap, + subsurface_ids: &mut HashMap, + sctk_tx: &channel::Sender, + control_sender: &mpsc::UnboundedSender, + proxy: &EventLoopProxy, + 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, + { + 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, + repeat: false, + 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, + repeat: true, + 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, + ); + // TODO must move this to the winit handler, where we have access to the event loop + // #[cfg(feature = "a11y")] + // { + // use crate::a11y::*; + // use iced_accessibility::accesskit::{ + // ActivationHandler, Node, 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( + // event_loop, + // 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? + theme::Mode::None, // TODO do we really need to track the system theme here? + ); + _ = surface_ids.insert(object_id, wrapper.clone()); + let logical_size = window.logical_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::build_user_interface( + program, + user_interface::Cache::default(), + &mut window.renderer, + logical_size, + 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( + program, + w.raw.as_ref(), + &WindowEvent::SurfaceResized(PhysicalSize::new( + p_w, p_h, + )), + ); + } + } + }, + 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, + ); + // TODO must move this to the winit handler, where we have access to the event loop + // #[cfg(feature = "a11y")] + // { + // use crate::a11y::*; + // use iced_accessibility::accesskit::{ + // ActivationHandler, Node, 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( + // event_loop, + // 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? + theme::Mode::None, // TODO do we really need to track the system theme here? + ); + let logical_size = window.logical_size(); + todo!() + // let _ = user_interfaces.insert( + // surface_id, + // crate::build_user_interface( + // program, + // user_interface::Cache::default(), + // &mut window.renderer, + // logical_size, + // 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, + ); + // TODO must move this to the winit handler, where we have access to the event loop + // #[cfg(feature = "a11y")] + // { + // use crate::a11y::*; + // use iced_accessibility::accesskit::{ + // ActivationHandler, Node, 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( + // event_loop, + // 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? + theme::Mode::None, // TODO do we really need to track the system theme here? + ); + _ = surface_ids.insert(object_id, wrapper.clone()); + let logical_size = window.logical_size(); + todo!() + // let _ = user_interfaces.insert( + // surface_id, + // crate::build_user_interface( + // program, + // user_interface::Cache::default(), + // &mut window.renderer, + // logical_size, + // 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 00000000..cb410efe --- /dev/null +++ b/winit/src/platform_specific/wayland/subsurface_widget.rs @@ -0,0 +1,701 @@ +// TODO z-order option? + +use crate::core::{ + ContentFit, Element, Length, Rectangle, Size, + layout::{self, Layout}, + mouse, renderer, + widget::{self, Widget}, +}; +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::{ + Connection, Dispatch, Proxy, QueueHandle, 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, + }, + }, +}; +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::{ + SurfaceIdWrapper, event_loop::state::SctkState, +}; + +#[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( + &mut 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 00000000..91e556a3 --- /dev/null +++ b/winit/src/platform_specific/wayland/winit_window.rs @@ -0,0 +1,453 @@ +use crate::platform_specific::wayland::Action; +use raw_window_handle::HandleError; +use sctk::reexports::{ + calloop::channel, + client::{ + Proxy, QueueHandle, + protocol::{wl_display::WlDisplay, wl_surface::WlSurface}, + }, +}; +use std::sync::{Arc, Mutex}; +use winit::{ + dpi::{LogicalSize, PhysicalPosition}, + 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 {} + +#[derive(Debug)] +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_raw( + self.surface.wl_surface().id().as_ptr() as usize, + ) + } + + 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_core::cursor::Cursor) { + match cursor { + winit_core::cursor::Cursor::Icon(icon) => { + _ = self.tx.send(Action::SetCursor(icon)); + } + winit_core::cursor::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 surface_position(&self) -> winit::dpi::PhysicalPosition { + PhysicalPosition::new(0, 0) + } + + 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!() + } + + fn safe_area(&self) -> winit::dpi::PhysicalInsets { + todo!() + } + + fn request_ime_update( + &self, + request: winit::window::ImeRequest, + ) -> Result<(), winit::window::ImeRequestError> { + todo!() + } + + fn ime_capabilities(&self) -> Option { + todo!() + } +} + +impl raw_window_handle::HasWindowHandle for SctkWinitWindow { + fn window_handle( + &self, + ) -> Result< + raw_window_handle::WindowHandle<'_>, + raw_window_handle::HandleError, + > { + 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 new file mode 100644 index 00000000..76806bd6 --- /dev/null +++ b/winit/src/program.rs @@ -0,0 +1,2345 @@ +//! Create interactive, native cross-platform applications for WGPU. +#[path = "application/drag_resize.rs"] +mod drag_resize; +mod state; +mod window_manager; + +pub use runtime::{Appearance, DefaultStyle, default}; +use winit::event_loop::OwnedDisplayHandle; + +use crate::conversion; +use crate::core; +use crate::core::Clipboard as CoreClipboard; +use crate::core::Length; +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::{Element, Point, Size}; +use crate::futures::futures::channel::mpsc; +use crate::futures::futures::channel::oneshot; +use crate::futures::futures::task; +use crate::futures::futures::{Future, StreamExt}; +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::Debug; +use crate::runtime::user_interface::{self, UserInterface}; +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; + +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. +/// +/// This trait is the main entrypoint of multi-window Iced. Once implemented, you can run +/// your GUI application by simply calling [`run`]. It will run in +/// its own window. +/// +/// A [`Program`] can execute asynchronous actions by returning a +/// [`Task`] in some of its methods. +/// +/// When using a [`Program`] with the `debug` feature enabled, a debug view +/// can be toggled by pressing `F12`. +pub trait Program +where + Self: Sized, + Self::Theme: DefaultStyle, +{ + /// The type of __messages__ your [`Program`] will produce. + type Message: std::fmt::Debug + Send; + + /// The theme used to draw the [`Program`]. + type Theme; + + /// The [`Executor`] that will run commands and subscriptions. + /// + /// The [default executor] can be a good starting point! + /// + /// [`Executor`]: Self::Executor + /// [default executor]: crate::futures::backend::default::Executor + type Executor: Executor; + + /// The graphics backend to use to draw the [`Program`]. + type Renderer: core::Renderer + core::text::Renderer; + + /// The data needed to initialize your [`Program`]. + type Flags; + + /// Initializes the [`Program`] 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. + fn new(flags: Self::Flags) -> (Self, Task); + + /// Returns the current title of the [`Program`]. + /// + /// This title can be dynamic! The runtime will automatically update the + /// title of your application when necessary. + fn title(&self, window: window::Id) -> String; + + /// Handles a __message__ and updates the state of the [`Program`]. + /// + /// This is where you define your __update logic__. All the __messages__, + /// produced by either user interactions or commands, will be handled by + /// this method. + /// + /// Any [`Task`] returned will be executed immediately in the background by the + /// runtime. + fn update(&mut self, message: Self::Message) -> Task; + + /// Returns the widgets to display in the [`Program`] for the `window`. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view( + &self, + window: window::Id, + ) -> Element<'_, Self::Message, Self::Theme, Self::Renderer>; + + /// Returns the current `Theme` of the [`Program`]. + fn theme(&self, window: window::Id) -> Self::Theme; + + /// Returns the `Style` variation of the `Theme`. + fn style(&self, theme: &Self::Theme) -> Appearance { + theme.default_style() + } + + /// Returns the event `Subscription` for the current state of the + /// application. + /// + /// The messages produced by the `Subscription` will be handled by + /// [`update`](#tymethod.update). + /// + /// A `Subscription` will be kept alive as long as you keep returning it! + /// + /// By default, it returns an empty subscription. + fn subscription(&self) -> Subscription { + Subscription::none() + } + + /// Returns the scale factor of the window of the [`Program`]. + /// + /// 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`. + #[allow(unused_variables)] + fn scale_factor(&self, window: window::Id) -> f64 { + 1.0 + } +} + +/// Runs a [`Program`] with an executor, compositor, and the provided +/// settings. +pub fn run( + settings: Settings, + graphics_settings: graphics::Settings, + window_settings: Option, + flags: P::Flags, +) -> Result<(), Error> +where + P: Program + 'static, + C: Compositor + 'static, + P::Theme: DefaultStyle, +{ + use winit::event_loop::EventLoop; + + let mut debug = Debug::new(); + debug.startup_started(); + + 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 (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 = + P::Executor::new().map_err(Error::ExecutorCreationFailed)?; + executor.spawn(worker); + + Runtime::new(executor, proxy.clone()) + }; + + let (program, task) = runtime.enter(|| P::new(flags)); + let is_daemon = window_settings.is_none(); + + let task = if let Some(window_settings) = window_settings { + let mut task = Some(task); + + let (_id, open) = runtime::window::open(window_settings); + + open.then(move |_| task.take().unwrap_or(Task::none())) + } else { + task + }; + + if let Some(stream) = runtime::task::into_stream(task) { + runtime.run(stream); + } + + runtime.track(subscription::into_recipes( + runtime.enter(|| program.subscription().map(Action::Output)), + )); + + let (boot_sender, boot_receiver) = oneshot::channel(); + let (control_sender, control_receiver) = mpsc::unbounded(); + + let instance = Box::pin(run_instance::( + program, + runtime, + proxy.clone(), + debug, + boot_receiver, + event_receiver, + control_sender.clone(), + event_loop.owned_display_handle(), + is_daemon, + )); + + let context = task::Context::from_waker(task::noop_waker_ref()); + + struct Runner { + instance: std::pin::Pin>, + context: task::Context<'static>, + id: Option, + boot: Option>, + sender: mpsc::UnboundedSender>, + receiver: mpsc::UnboundedReceiver, + error: Option, + + #[cfg(target_arch = "wasm32")] + is_booted: std::rc::Rc>, + #[cfg(target_arch = "wasm32")] + canvas: Option, + } + + struct BootConfig { + sender: oneshot::Sender>, + fonts: Vec>, + graphics_settings: graphics::Settings, + control_sender: mpsc::UnboundedSender, + is_wayland: bool, + } + + let runner = Runner { + instance, + context, + id: settings.id, + boot: Some(BootConfig { + sender: boot_sender, + fonts: settings.fonts, + graphics_settings, + control_sender, + is_wayland, + }), + sender: event_sender, + receiver: control_receiver, + error: None, + + #[cfg(target_arch = "wasm32")] + is_booted: std::rc::Rc::new(std::cell::RefCell::new(false)), + #[cfg(target_arch = "wasm32")] + canvas: None, + }; + + impl winit::application::ApplicationHandler + for Runner + where + Message: std::fmt::Debug, + F: Future, + C: Compositor + 'static, + { + 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; + }; + + // 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; + } + }; + + #[cfg(target_arch = "wasm32")] + { + use winit::platform::web::WindowExtWebSys; + self.canvas = window.canvas(); + } + + let finish_boot = async move { + let mut compositor = + C::new(graphics_settings, window.clone()).await?; + + for font in fonts { + compositor.load_font(font); + } + + sender + .send(Boot { + compositor, + is_wayland, + }) + .ok() + .expect("Send boot event"); + + Ok::<_, graphics::Error>(()) + }; + + #[cfg(not(target_arch = "wasm32"))] + if let Err(error) = + crate::futures::futures::executor::block_on(finish_boot) + { + self.error = Some(Error::GraphicsCreationFailed(error)); + event_loop.exit(); + } + + #[cfg(target_arch = "wasm32")] + { + let is_booted = self.is_booted.clone(); + + wasm_bindgen_futures::spawn_local(async move { + finish_boot.await.expect("Finish boot!"); + + *is_booted.borrow_mut() = true; + }); + + event_loop + .set_control_flow(winit::event_loop::ControlFlow::Poll); + } + } + } + + impl Runner + where + F: Future, + C: Compositor, + { + fn process_event( + &mut self, + event_loop: &dyn winit::event_loop::ActiveEventLoop, + event: Option>, + ) { + if event_loop.exiting() { + return; + } + + if let Some(event) = event { + self.sender.start_send(event).expect("Send event"); + } + + loop { + let poll = self.instance.as_mut().poll(&mut self.context); + + match poll { + task::Poll::Pending => match self.receiver.try_next() { + Ok(Some(control)) => match control { + Control::ChangeFlow(flow) => { + use winit::event_loop::ControlFlow; + + match (event_loop.control_flow(), flow) { + ( + ControlFlow::WaitUntil(current), + ControlFlow::WaitUntil(new), + ) if new < current => {} + ( + ControlFlow::WaitUntil(target), + ControlFlow::Wait, + ) if target > Instant::now() => {} + _ => { + event_loop.set_control_flow(flow); + } + } + } + Control::CreateWindow { + id, + settings, + title, + monitor, + on_open, + } => { + let exit_on_close_request = + settings.exit_on_close_request; + let resize_border = settings.resize_border; + + let visible = settings.visible; + + #[cfg(target_arch = "wasm32")] + let target = + settings.platform_specific.target.clone(); + + let window_attributes = + conversion::window_attributes( + settings, + &title, + monitor + .or(event_loop.primary_monitor()), + self.id.clone(), + ) + .with_visible(false); + + #[cfg(target_arch = "wasm32")] + let window_attributes = { + use winit::platform::web::WindowAttributesExtWebSys; + window_attributes + .with_canvas(self.canvas.take()) + }; + + log::info!( + "Window attributes for id `{id:#?}`: {window_attributes:#?}" + ); + + let window = Arc::from( + event_loop + .create_window(window_attributes) + .expect("Create window"), + ); + + #[cfg(target_arch = "wasm32")] + { + use winit::platform::web::WindowExtWebSys; + + let canvas = window + .canvas() + .expect("Get window canvas"); + + let _ = canvas.set_attribute( + "style", + "display: block; width: 100%; height: 100%", + ); + + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let body = document.body().unwrap(); + + let target = target.and_then(|target| { + body.query_selector(&format!( + "#{target}" + )) + .ok() + .unwrap_or(None) + }); + + match target { + Some(node) => { + let _ = node + .replace_with_with_node_1( + &canvas, + ) + .expect(&format!( + "Could not replace #{}", + node.id() + )); + } + None => { + let _ = body + .append_child(&canvas) + .expect( + "Append canvas to HTML body", + ); + } + }; + } + + self.process_event( + event_loop, + 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; + } + }, + task::Poll::Ready(_) => { + event_loop.exit(); + break; + } + }; + } + } + } + + #[cfg(not(target_arch = "wasm32"))] + { + let mut runner = runner; + let _ = event_loop.run_app(&mut runner); + + runner.error.map(Err).unwrap_or(Ok(())) + } + + #[cfg(target_arch = "wasm32")] + { + use winit::platform::web::EventLoopExtWebSys; + let _ = event_loop.spawn_app(runner); + + Ok(()) + } +} + +struct Boot { + compositor: C, + is_wayland: bool, +} + +pub(crate) enum Event { + WindowCreated { + id: window::Id, + window: Arc, + exit_on_close_request: bool, + make_visible: bool, + on_open: oneshot::Sender, + resize_border: u32, + }, + 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, +} + +pub(crate) enum Control { + ChangeFlow(winit::event_loop::ControlFlow), + Exit, + CreateWindow { + id: window::Id, + settings: window::Settings, + title: String, + 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<'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 control_sender: mpsc::UnboundedSender, + display_handle: OwnedDisplayHandle, + is_daemon: bool, + is_wayland: bool, +) where + P: Program + 'static, + C: Compositor + 'static, + P::Theme: DefaultStyle, +{ + use winit::event; + use winit::event_loop::ControlFlow; + + 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; + + let mut events = Vec::new(); + 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, Node, 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< + HashMap< + window::Id, + UserInterface< + '_, +

::Message, +

::Theme, +

::Renderer, + >, + rustc_hash::FxBuildHasher, + >, + > = ManuallyDrop::new(FxHashMap::default()); + let mut clipboard = Clipboard::unconnected(); + + 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 + }; + + let Some(event) = event else { + break; + }; + + 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, + 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, Node, 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(); + + let _ = user_interfaces.insert( + id, + build_user_interface( + &program, + user_interface::Cache::default(), + &mut window.renderer, + 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()); + + if make_visible { + window.raw.set_visible(true); + } + + events.push(( + Some(id), + core::Event::Window(window::Event::Opened { + position: window.position(), + size: window.size(), + }), + )); + + if clipboard.window_id().is_none() { + 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::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::WindowEvent::RedrawRequested => { + let Some((id, window)) = + window_manager.get_mut_alias(window_id) + else { + continue; + }; + + // 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(); + + 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, + ); + + debug.draw_started(); + let new_mouse_interaction = ui.draw( + &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 { + 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; + } + + runtime.broadcast(subscription::Event::Interaction { + window: id, + event: redraw_event, + status: core::event::Status::Ignored, + }); + + 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.request_redraw(); + + ControlFlow::Wait + } + window::RedrawRequest::At(at) => { + ControlFlow::WaitUntil(at) + } + }, + _ => 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 mut ui = user_interfaces + .remove(&id) + .expect("Remove user interface") + .relayout(logical_size, &mut window.renderer); + + #[cfg(feature = "a11y")] + { + use iced_accessibility::{ + A11yId, A11yNode, A11yTree, + accesskit::{ + Node, NodeId, Role, Tree, TreeUpdate, + }, + }; + 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 = Node::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(); + let new_mouse_interaction = user_interfaces + .get_mut(&id) + .expect("Get user interface") + .draw( + &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 + { + 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, + physical_size.height, + ); + + window.viewport_version = + window.state.viewport_version(); + } + + window.raw.pre_present_notify(); + debug.render_started(); + match compositor.present( + &mut window.renderer, + &mut window.surface, + window.state.viewport(), + window.state.background_color(), + &debug.overlay(), + ) { + Ok(()) => { + debug.render_finished(); + } + 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.request_redraw(); + } + } + } + } + } + } + window_event => { + if !is_daemon + && matches!( + window_event, + winit::event::WindowEvent::Destroyed + ) + && !is_window_opening + && window_manager.is_empty() + { + control_sender + .start_send(Control::Exit) + .expect("Send control action"); + + continue; + } + + let Some((id, window)) = + window_manager.get_mut_alias(window_id) + else { + 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 + ) && window.exit_on_close_request + { + run_action( + Action::Window(runtime::window::Action::Close( + id, + )), + &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, + ); + } else { + window.state.update( + 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((Some(id), event)); + } + } + } + _ => {} + } + } + 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, + ); + + let mut needs_redraw = + !no_window_events || !messages.is_empty(); + + 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; + } + + if !uis_stale { + uis_stale = + matches!(ui_state, user_interface::State::Outdated); + } + + for (event, status) in + window_events.into_iter().zip(statuses.into_iter()) + { + runtime.broadcast(subscription::Event::Interaction { + window: id, + event, + status, + }); + } + } + + if !resized && skip { + continue; + } + + for (id, event) in events.drain(..) { + runtime.broadcast(subscription::Event::Interaction { + window: id.unwrap_or(window::Id::NONE), + event, + status: core::event::Status::Ignored, + }); + } + + debug.event_processing_finished(); + + 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(); + + update( + &mut program, + &mut runtime, + &mut debug, + &mut messages, + ); + + for (id, window) in window_manager.iter_mut() { + window.state.synchronize( + &program, + id, + window.raw.as_ref(), + ); + + window.request_redraw(); + } + + user_interfaces = ManuallyDrop::new(build_user_interfaces( + &program, + &mut debug, + &mut window_manager, + cached_interfaces, + &mut clipboard, + )); + + if actions > 0 { + proxy.free_slots(actions); + actions = 0; + } + } + + 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(); + + 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? + } + } + } + + let _ = ManuallyDrop::into_inner(user_interfaces); +} + +/// Builds a window's [`UserInterface`] for the [`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, +{ + debug.view_started(); + let view = program.view(id); + debug.view_finished(); + + debug.layout_started(); + 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 +} + +fn update( + program: &mut P, + runtime: &mut Runtime, Action>, + debug: &mut Debug, + messages: &mut Vec, +) where + P::Theme: DefaultStyle, +{ + for message in messages.drain(..) { + debug.log_message(&message); + debug.update_started(); + + let task = runtime.enter(|| program.update(message)); + debug.update_finished(); + + if let Some(stream) = runtime::task::into_stream(task) { + runtime.run(stream); + } + } + + let subscription = runtime.enter(|| program.subscription()); + runtime.track(subscription::into_recipes(subscription.map(Action::Output))); +} + +fn run_action( + action: Action, + program: &P, + compositor: &mut C, + events: &mut Vec<(Option, core::Event)>, + messages: &mut Vec, + clipboard: &mut Clipboard, + control_sender: &mut mpsc::UnboundedSender, + debug: &mut Debug, + interfaces: &mut FxHashMap< + window::Id, + UserInterface<'_, P::Message, P::Theme, P::Renderer>, + >, + 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, + P::Theme: DefaultStyle, +{ + use crate::runtime::clipboard; + use crate::runtime::system; + use crate::runtime::window; + + match action { + Action::Output(message) => { + messages.push(message); + } + Action::Clipboard(action) => match action { + clipboard::Action::Read { target, channel } => { + let _ = channel.send(clipboard.read(target)); + } + 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) => { + let monitor = window_manager.last_monitor(); + + control_sender + .start_send(Control::CreateWindow { + id, + settings, + title: program.title(id), + monitor, + on_open: channel, + }) + .expect("Send control action"); + + *is_window_opening = true; + } + 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()) + .zip(proxy) + .map(|(w, proxy)| { + Clipboard::connect( + w, + crate::clipboard::ControlSender { + sender: control_sender.clone(), + proxy, + }, + ) + }) + .unwrap_or_else(Clipboard::unconnected); + } + + events.push(( + Some(id), + core::Event::Window(core::window::Event::Closed), + )); + } + } + window::Action::GetOldest(channel) => { + let id = + window_manager.iter_mut().next().map(|(id, _window)| id); + + let _ = channel.send(id); + } + window::Action::GetLatest(channel) => { + let id = + window_manager.iter_mut().last().map(|(id, _window)| id); + + let _ = channel.send(id); + } + window::Action::Drag(id) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = window.raw.drag_window(); + } + } + window::Action::Resize(id, size) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = window.raw.request_surface_size( + winit::dpi::LogicalSize { + width: size.width, + height: size.height, + } + .into(), + ); + } + } + window::Action::GetSize(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let size = window + .raw + .surface_size() + .to_logical(window.raw.scale_factor()); + + let _ = channel.send(Size::new(size.width, size.height)); + } + } + window::Action::GetMaximized(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = channel.send(window.raw.is_maximized()); + } + } + window::Action::Maximize(id, maximized) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_maximized(maximized); + } + } + window::Action::GetMinimized(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = channel.send(window.raw.is_minimized()); + } + } + window::Action::Minimize(id, minimized) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_minimized(minimized); + } + } + window::Action::GetPosition(id, channel) => { + if let Some(window) = window_manager.get(id) { + let position = window + .raw + .inner_position() + .map(|position| { + let position = position + .to_logical::(window.raw.scale_factor()); + + Point::new(position.x, position.y) + }) + .ok(); + + let _ = channel.send(position); + } + } + window::Action::GetScaleFactor(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let scale_factor = window.raw.scale_factor(); + + let _ = channel.send(scale_factor as f32); + } + } + window::Action::Move(id, position) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_outer_position( + winit::dpi::LogicalPosition { + x: position.x, + y: position.y, + } + .into(), + ); + } + } + window::Action::ChangeMode(id, mode) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_visible(conversion::visible(mode)); + window.raw.set_fullscreen(conversion::fullscreen( + window.raw.current_monitor(), + mode, + )); + } + } + window::Action::ChangeIcon(id, icon) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_window_icon(conversion::icon(icon)); + } + } + window::Action::GetMode(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let mode = if window.raw.is_visible().unwrap_or(true) { + conversion::mode(window.raw.fullscreen()) + } else { + core::window::Mode::Hidden + }; + + let _ = channel.send(mode); + } + } + window::Action::ToggleMaximize(id) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_maximized(!window.raw.is_maximized()); + } + } + window::Action::ToggleDecorations(id) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_decorations(!window.raw.is_decorated()); + } + } + window::Action::RequestUserAttention(id, attention_type) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.request_user_attention( + attention_type.map(conversion::user_attention), + ); + } + } + window::Action::GainFocus(id) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.focus_window(); + } + } + window::Action::ChangeLevel(id, level) => { + if let Some(window) = window_manager.get_mut(id) { + window + .raw + .set_window_level(conversion::window_level(level)); + } + } + window::Action::ShowSystemMenu(id) => { + if let Some(window) = window_manager.get_mut(id) { + if let mouse::Cursor::Available(point) = + window.state.cursor() + { + window.raw.show_window_menu( + winit::dpi::LogicalPosition { + x: point.x, + y: point.y, + } + .into(), + ); + } + } + } + window::Action::GetRawId(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = channel.send(window.raw.id().into()); + } + } + window::Action::RunWithHandle(id, f) => { + use window::raw_window_handle::HasWindowHandle; + + if let Some(handle) = window_manager + .get_mut(id) + .and_then(|window| window.raw.window_handle().ok()) + { + f(handle); + } + } + window::Action::Screenshot(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let bytes = compositor.screenshot( + &mut window.renderer, + &mut window.surface, + window.state.viewport(), + window.state.background_color(), + &debug.overlay(), + ); + + let _ = channel.send(window::Screenshot::new( + bytes, + window.state.physical_size(), + window.state.viewport().scale_factor(), + )); + } + } + window::Action::EnableMousePassthrough(id) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = window.raw.set_cursor_hittest(false); + } + } + window::Action::DisableMousePassthrough(id) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = window.raw.set_cursor_hittest(true); + } + } + }, + Action::System(action) => match action { + system::Action::QueryInformation(_channel) => { + #[cfg(feature = "system")] + { + let graphics_info = compositor.fetch_information(); + + let _ = std::thread::spawn(move || { + let information = + crate::system::information(graphics_info); + + let _ = _channel.send(information); + }); + } + } + }, + Action::Widget(operation) => { + let mut current_operation = Some(operation); + + while let Some(mut operation) = current_operation.take() { + for (id, ui) in interfaces.iter_mut() { + if let Some(window) = window_manager.get_mut(*id) { + ui.operate(&window.renderer, operation.as_mut()); + } + } + + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(()) => {} + operation::Outcome::Chain(next) => { + current_operation = Some(next); + } + } + } + } + Action::LoadFont { bytes, channel } => { + // TODO: Error handling (?) + compositor.load_font(bytes.clone()); + + let _ = channel.send(Ok(())); + } + Action::Exit => { + control_sender + .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); + } + } +} + +/// Build the user interface for every window. +pub fn build_user_interfaces<'a, P: Program, C>( + program: &'a P, + debug: &mut Debug, + window_manager: &mut WindowManager, + mut cached_user_interfaces: FxHashMap, + clipboard: &mut Clipboard, +) -> FxHashMap> +where + C: Compositor, + P::Theme: DefaultStyle, +{ + cached_user_interfaces + .drain() + .filter_map(|(id, cache)| { + let window = window_manager.get_mut(id)?; + let interface = 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() +} + +/// Returns true if the provided event should cause a [`Program`] to +/// exit. +pub fn user_force_quit( + event: &winit::event::WindowEvent, + _modifiers: winit::keyboard::ModifiersState, +) -> bool { + match event { + #[cfg(target_os = "macos")] + winit::event::WindowEvent::KeyboardInput { + event: + winit::event::KeyEvent { + logical_key: winit::keyboard::Key::Character(c), + state: winit::event::ElementState::Pressed, + .. + }, + .. + } if c == "q" && _modifiers.super_key() => true, + _ => false, + } +} diff --git a/winit/src/proxy.rs b/winit/src/proxy.rs index 5a56659f..a3e4e816 100644 --- a/winit/src/proxy.rs +++ b/winit/src/proxy.rs @@ -1,3 +1,4 @@ +use crate::Event; use crate::futures::futures::{ Future, Sink, StreamExt, channel::mpsc, @@ -7,13 +8,14 @@ use crate::futures::futures::{ use crate::graphics::shell; use crate::runtime::Action; use crate::runtime::window; +use std::hash::DefaultHasher; 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, } @@ -23,6 +25,7 @@ impl Clone for Proxy { raw: self.raw.clone(), sender: self.sender.clone(), notifier: self.notifier.clone(), + event_sender: self.event_sender.clone(), } } } @@ -32,11 +35,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; @@ -45,7 +51,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; } @@ -70,6 +77,7 @@ impl Proxy { raw, sender, notifier, + event_sender, }, worker, ) @@ -88,7 +96,9 @@ impl Proxy { /// Note: This skips the backpressure mechanism with an unbounded /// channel. Use sparingly! pub fn send_action(&self, action: Action) { - let _ = self.raw.send_event(action); + self.event_sender + .unbounded_send(Event::UserEvent(action)) + .expect("Send message to event loop"); } /// Frees an amount of slots for additional messages to be queued in diff --git a/winit/src/window.rs b/winit/src/window.rs index 24d63565..f5a1010d 100644 --- a/winit/src/window.rs +++ b/winit/src/window.rs @@ -1,6 +1,7 @@ mod state; use state::State; +use winit::dpi::PhysicalPosition; pub use crate::core::window::{Event, Id, RedrawRequest, Settings}; @@ -51,13 +52,13 @@ where pub fn insert( &mut self, id: Id, - window: Arc, + window: Arc, program: &program::Instance

, compositor: &mut C, exit_on_close_request: bool, system_theme: theme::Mode, ) -> &mut Window { - let state = State::new(program, id, &window, system_theme); + let state = State::new(program, id, system_theme, window.as_ref()); let surface_size = state.physical_size(); let surface_version = state.surface_version(); let surface = compositor.create_surface( @@ -82,6 +83,9 @@ where redraw_at: None, preedit: None, ime_state: None, + drag_resize_window_func: None, + prev_dnd_destination_rectangles_count: 0, + viewport_version: 0, }, ); @@ -125,6 +129,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, @@ -163,8 +171,18 @@ where C: Compositor, P::Theme: theme::Base, { - pub raw: Arc, + pub raw: Arc, pub state: State

, + pub viewport_version: u64, + 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 exit_on_close_request: bool, pub mouse_interaction: mouse::Interaction, pub surface: C::Surface, @@ -185,10 +203,12 @@ where self.raw .outer_position() .ok() - .map(|position| position.to_logical(self.raw.scale_factor())) - .map(|position| Point { - x: position.x, - y: position.y, + .map(|position: PhysicalPosition| { + position.to_logical(self.raw.scale_factor()) + }) + .map(|position: LogicalPosition| Point { + x: position.x as f32, + y: position.y as f32, }) } @@ -247,7 +267,7 @@ where pub fn update_mouse(&mut self, interaction: mouse::Interaction) { if interaction != self.mouse_interaction { if let Some(icon) = conversion::mouse_interaction(interaction) { - self.raw.set_cursor(icon); + self.raw.set_cursor(icon.into()); if self.mouse_interaction == mouse::Interaction::Hidden { self.raw.set_cursor_visible(true); @@ -285,8 +305,8 @@ where if self.ime_state != Some((cursor, purpose)) { self.raw.set_ime_cursor_area( - LogicalPosition::new(cursor.x, cursor.y), - LogicalSize::new(cursor.width, cursor.height), + LogicalPosition::new(cursor.x, cursor.y).into(), + LogicalSize::new(cursor.width, cursor.height).into(), ); self.raw.set_ime_purpose(conversion::ime_purpose(purpose)); diff --git a/winit/src/window/state.rs b/winit/src/window/state.rs index 8db36697..24b7f72c 100644 --- a/winit/src/window/state.rs +++ b/winit/src/window/state.rs @@ -4,7 +4,8 @@ use crate::core::{mouse, theme, window}; use crate::graphics::Viewport; use crate::program::{self, Program}; -use winit::event::{Touch, WindowEvent}; +use winit::dpi::LogicalPosition; +use winit::event::WindowEvent; use winit::window::Window; use std::fmt::{Debug, Formatter}; @@ -14,8 +15,8 @@ pub struct State where P::Theme: theme::Base, { - title: String, - scale_factor: f32, + pub(crate) title: String, + scale_factor: f64, viewport: Viewport, surface_version: u64, cursor_position: Option>, @@ -49,8 +50,8 @@ where pub fn new( program: &program::Instance

, window_id: window::Id, - window: &Window, system_theme: theme::Mode, + window: &dyn Window, ) -> Self { let title = program.title(window_id); let scale_factor = program.scale_factor(window_id); @@ -61,11 +62,11 @@ where let style = program.style(theme.as_ref().unwrap_or(&default_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), - window.scale_factor() as f32 * scale_factor, + window.scale_factor() as f64 * scale_factor, ) }; @@ -99,10 +100,16 @@ where self.viewport.logical_size() } - pub fn scale_factor(&self) -> f32 { + pub fn scale_factor(&self) -> f64 { 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 .map(|cursor_position| { @@ -135,19 +142,37 @@ where self.style.text_color } + /// Returns the current icon [`Color`] of the [`State`]. + pub fn icon_color(&self) -> Color { + self.style.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.surface_version = self.surface_version.wrapping_add(1); + } + + /// Processes the provided window event and updates the [`State`] accordingly. pub fn update( &mut self, program: &program::Instance

, - window: &Window, + window: &dyn Window, event: &WindowEvent, ) { 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( size, - window.scale_factor() as f32 * self.scale_factor, + window.scale_factor() * self.scale_factor, ); self.surface_version += 1; } @@ -155,21 +180,12 @@ where scale_factor: new_scale_factor, .. } => { - let size = self.viewport.physical_size(); - - self.viewport = Viewport::with_physical_size( - size, - *new_scale_factor as f32 * self.scale_factor, - ); - self.surface_version += 1; + self.update_scale_factor(*new_scale_factor); } - WindowEvent::CursorMoved { position, .. } - | WindowEvent::Touch(Touch { - location: position, .. - }) => { + WindowEvent::PointerMoved { position, .. } => { self.cursor_position = Some(*position); } - WindowEvent::CursorLeft { .. } => { + WindowEvent::PointerLeft { .. } => { self.cursor_position = None; } WindowEvent::ModifiersChanged(new_modifiers) => { @@ -193,7 +209,7 @@ where &mut self, program: &program::Instance

, window_id: window::Id, - window: &Window, + window: &dyn Window, ) { // Update window title let new_title = program.title(window_id); @@ -203,13 +219,24 @@ where self.title = new_title; } - // Update scale factor + // Update scale factor and size let new_scale_factor = program.scale_factor(window_id); - - if self.scale_factor != new_scale_factor { + 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( self.viewport.physical_size(), - window.scale_factor() as f32 * new_scale_factor, + window.scale_factor() * new_scale_factor, ); self.scale_factor = new_scale_factor;