From 6ce5e75b0b086f622f784286a969c572a1b96849 Mon Sep 17 00:00:00 2001 From: d Date: Sun, 9 Aug 2026 22:37:21 +0700 Subject: [PATCH] Fork of pop-os/cosmic-comp @ upstream --- .github/PULL_REQUEST_TEMPLATE.md | 8 + .github/workflows/ci.yml | 104 + .gitignore | 16 + Cargo.lock | 7219 +++++++++++++++++ Cargo.toml | 147 + LICENSE | 674 ++ Makefile | 67 + build.rs | 9 + cosmic-comp-config/Cargo.toml | 21 + cosmic-comp-config/src/input.rs | 216 + cosmic-comp-config/src/lib.rs | 249 + cosmic-comp-config/src/output/comp.rs | 137 + cosmic-comp-config/src/output/mod.rs | 6 + cosmic-comp-config/src/output/randr.rs | 95 + cosmic-comp-config/src/workspace.rs | 67 + data/cosmic-comp.service | 12 + data/cosmic-service | 32 + data/cosmic-session-pre.target | 8 + data/cosmic-session.target | 7 + data/cosmic.desktop | 8 + data/keybindings.ron | 128 + data/tiling-exceptions.ron | 53 + debian/changelog | 5 + debian/control | 35 + debian/copyright | 7 + debian/postinst | 2 + debian/rules | 22 + debian/source/format | 1 + debian/source/options | 2 + examples/customized-window-decorations.rs | 79 + flake.lock | 101 + flake.nix | 116 + i18n.toml | 4 + resources/cursor.rgba | Bin 0 -> 16384 bytes resources/i18n/af/cosmic_comp.ftl | 0 resources/i18n/ar/cosmic_comp.ftl | 28 + resources/i18n/be/cosmic_comp.ftl | 28 + resources/i18n/bg/cosmic_comp.ftl | 28 + resources/i18n/bn/cosmic_comp.ftl | 0 resources/i18n/ca/cosmic_comp.ftl | 0 resources/i18n/cs/cosmic_comp.ftl | 28 + resources/i18n/da/cosmic_comp.ftl | 0 resources/i18n/de/cosmic_comp.ftl | 28 + resources/i18n/el/cosmic_comp.ftl | 28 + resources/i18n/en-GB/cosmic_comp.ftl | 20 + resources/i18n/en/cosmic_comp.ftl | 28 + resources/i18n/eo/cosmic_comp.ftl | 0 resources/i18n/es-419/cosmic_comp.ftl | 28 + resources/i18n/es-MX/cosmic_comp.ftl | 0 resources/i18n/es/cosmic_comp.ftl | 28 + resources/i18n/et/cosmic_comp.ftl | 11 + resources/i18n/eu/cosmic_comp.ftl | 0 resources/i18n/fa/cosmic_comp.ftl | 28 + resources/i18n/fi/cosmic_comp.ftl | 28 + resources/i18n/fr/cosmic_comp.ftl | 28 + resources/i18n/fy/cosmic_comp.ftl | 0 resources/i18n/ga/cosmic_comp.ftl | 28 + resources/i18n/gd/cosmic_comp.ftl | 23 + resources/i18n/gu/cosmic_comp.ftl | 0 resources/i18n/he/cosmic_comp.ftl | 0 resources/i18n/hi/cosmic_comp.ftl | 0 resources/i18n/hr/cosmic_comp.ftl | 0 resources/i18n/hu/cosmic_comp.ftl | 28 + resources/i18n/id/cosmic_comp.ftl | 28 + resources/i18n/ie/cosmic_comp.ftl | 0 resources/i18n/is/cosmic_comp.ftl | 28 + resources/i18n/it/cosmic_comp.ftl | 28 + resources/i18n/ja/cosmic_comp.ftl | 28 + resources/i18n/jv/cosmic_comp.ftl | 0 resources/i18n/ka/cosmic_comp.ftl | 0 resources/i18n/kab/cosmic_comp.ftl | 27 + resources/i18n/kk/cosmic_comp.ftl | 28 + resources/i18n/kmr/cosmic_comp.ftl | 28 + resources/i18n/kn/cosmic_comp.ftl | 0 resources/i18n/ko/cosmic_comp.ftl | 28 + resources/i18n/li/cosmic_comp.ftl | 0 resources/i18n/lo/cosmic_comp.ftl | 0 resources/i18n/lt/cosmic_comp.ftl | 28 + resources/i18n/ml/cosmic_comp.ftl | 0 resources/i18n/ms/cosmic_comp.ftl | 0 resources/i18n/nb-NO/cosmic_comp.ftl | 28 + resources/i18n/nl/cosmic_comp.ftl | 28 + resources/i18n/nn/cosmic_comp.ftl | 1 + resources/i18n/oc/cosmic_comp.ftl | 0 resources/i18n/pa/cosmic_comp.ftl | 21 + resources/i18n/pl/cosmic_comp.ftl | 28 + resources/i18n/pt-BR/cosmic_comp.ftl | 28 + resources/i18n/pt/cosmic_comp.ftl | 28 + resources/i18n/ro/cosmic_comp.ftl | 23 + resources/i18n/ru/cosmic_comp.ftl | 28 + resources/i18n/sat/cosmic_comp.ftl | 0 resources/i18n/sk/cosmic_comp.ftl | 28 + resources/i18n/sl/cosmic_comp.ftl | 0 resources/i18n/sr-Cyrl/cosmic_comp.ftl | 28 + resources/i18n/sr-Latn/cosmic_comp.ftl | 23 + resources/i18n/sr/cosmic_comp.ftl | 28 + resources/i18n/sv/cosmic_comp.ftl | 28 + resources/i18n/ta/cosmic_comp.ftl | 0 resources/i18n/th/cosmic_comp.ftl | 0 resources/i18n/ti/cosmic_comp.ftl | 0 resources/i18n/tr/cosmic_comp.ftl | 28 + resources/i18n/uk/cosmic_comp.ftl | 28 + resources/i18n/uz/cosmic_comp.ftl | 0 resources/i18n/vi/cosmic_comp.ftl | 27 + resources/i18n/yue-Hant/cosmic_comp.ftl | 0 resources/i18n/zh-CN/cosmic_comp.ftl | 28 + resources/i18n/zh-TW/cosmic_comp.ftl | 28 + resources/icons/amd.svg | 1 + resources/icons/intel.svg | 12 + resources/icons/nvidia.svg | 1 + resources/protocols/wayland-drm.xml | 189 + rust-toolchain.toml | 3 + rustfmt.toml | 1 + src/backend/kms/device.rs | 1499 ++++ src/backend/kms/drm_helpers.rs | 354 + src/backend/kms/mod.rs | 1178 +++ src/backend/kms/render/gles.rs | 212 + src/backend/kms/render/mod.rs | 2 + src/backend/kms/render/pixman.rs | 165 + src/backend/kms/socket.rs | 104 + src/backend/kms/surface/mod.rs | 1979 +++++ src/backend/kms/surface/timings.rs | 391 + src/backend/mod.rs | 164 + src/backend/render/animations/mod.rs | 1 + src/backend/render/animations/spring.rs | 138 + src/backend/render/cursor.rs | 486 ++ src/backend/render/element.rs | 526 ++ src/backend/render/mod.rs | 1595 ++++ .../render/shaders/blur_downsample.frag | 30 + src/backend/render/shaders/blur_upsample.frag | 31 + .../render/shaders/clipped_surface.frag | 104 + src/backend/render/shaders/offscreen.frag | 90 + .../render/shaders/rounded_outline.frag | 64 + .../render/shaders/rounded_rectangle.frag | 37 + src/backend/render/shaders/shadow.frag | 139 + src/backend/render/shadow.rs | 191 + src/backend/render/wayland/blur_effect.rs | 705 ++ src/backend/render/wayland/clipped_surface.rs | 274 + src/backend/render/wayland/mod.rs | 145 + src/backend/winit.rs | 349 + src/backend/x11.rs | 549 ++ src/config/input_config.rs | 225 + src/config/key_bindings.rs | 56 + src/config/mod.rs | 992 +++ src/config/types.rs | 4 + src/dbus/a11y_keyboard_monitor.rs | 304 + src/dbus/logind.rs | 34 + src/dbus/mod.rs | 170 + src/dbus/name_owners.rs | 213 + src/dbus/power.rs | 86 + src/debug.rs | 351 + src/hooks.rs | 47 + src/input/actions.rs | 1180 +++ src/input/gestures/mod.rs | 173 + src/input/mod.rs | 2557 ++++++ src/lib.rs | 362 + src/logger/mod.rs | 57 + src/logger/serializer.rs | 131 + src/main.rs | 8 + src/session.rs | 161 + src/shell/element/mod.rs | 1607 ++++ src/shell/element/resize_indicator.rs | 342 + src/shell/element/stack.rs | 2108 +++++ src/shell/element/stack/tab.rs | 480 ++ src/shell/element/stack/tabs.rs | 1040 +++ src/shell/element/stack_hover.rs | 133 + src/shell/element/surface.rs | 1142 +++ src/shell/element/swap_indicator.rs | 135 + src/shell/element/window.rs | 1459 ++++ src/shell/focus/mod.rs | 793 ++ src/shell/focus/order.rs | 448 + src/shell/focus/target.rs | 850 ++ src/shell/grabs/delay.rs | 275 + src/shell/grabs/menu/default.rs | 674 ++ src/shell/grabs/menu/item.rs | 244 + src/shell/grabs/menu/mod.rs | 1106 +++ src/shell/grabs/mod.rs | 800 ++ src/shell/grabs/moving.rs | 974 +++ src/shell/layout/floating/grabs/mod.rs | 3 + src/shell/layout/floating/grabs/resize.rs | 582 ++ src/shell/layout/floating/mod.rs | 1693 ++++ src/shell/layout/mod.rs | 103 + src/shell/layout/tiling/blocker.rs | 62 + src/shell/layout/tiling/grabs/mod.rs | 5 + src/shell/layout/tiling/grabs/resize.rs | 565 ++ src/shell/layout/tiling/grabs/swap.rs | 107 + src/shell/layout/tiling/mod.rs | 5939 ++++++++++++++ src/shell/mod.rs | 5142 ++++++++++++ src/shell/seats.rs | 438 + src/shell/workspace.rs | 2288 ++++++ src/shell/zoom.rs | 1099 +++ src/state.rs | 1426 ++++ src/systemd.rs | 35 + src/theme.rs | 63 + src/utils/env.rs | 164 + src/utils/geometry.rs | 174 + src/utils/iced/mod.rs | 1118 +++ src/utils/iced/state.rs | 224 + src/utils/ids.rs | 39 + src/utils/mod.rs | 12 + src/utils/prelude.rs | 170 + src/utils/quirks.rs | 26 + src/utils/rlimit.rs | 31 + src/utils/screenshot.rs | 115 + src/utils/tween.rs | 47 + src/wayland/handlers/a11y.rs | 66 + src/wayland/handlers/background_effect.rs | 59 + src/wayland/handlers/buffer.rs | 10 + src/wayland/handlers/compositor.rs | 434 + src/wayland/handlers/corner_radius.rs | 75 + src/wayland/handlers/data_control/ext.rs | 8 + src/wayland/handlers/data_control/mod.rs | 4 + src/wayland/handlers/data_control/wlr.rs | 10 + src/wayland/handlers/data_device.rs | 136 + src/wayland/handlers/decoration.rs | 187 + src/wayland/handlers/dmabuf.rs | 92 + src/wayland/handlers/drm.rs | 27 + src/wayland/handlers/drm_lease.rs | 135 + src/wayland/handlers/drm_syncobj.rs | 14 + src/wayland/handlers/foreign_toplevel_list.rs | 12 + src/wayland/handlers/fractional_scale.rs | 59 + src/wayland/handlers/idle_inhibit.rs | 18 + src/wayland/handlers/idle_notify.rs | 11 + src/wayland/handlers/image_capture_source.rs | 52 + .../handlers/image_copy_capture/mod.rs | 474 ++ .../handlers/image_copy_capture/render.rs | 967 +++ .../handlers/image_copy_capture/user_data.rs | 249 + src/wayland/handlers/input_method.rs | 42 + src/wayland/handlers/keyboard_layout.rs | 10 + .../handlers/keyboard_shortcuts_inhibit.rs | 17 + src/wayland/handlers/layer_shell.rs | 83 + src/wayland/handlers/mod.rs | 45 + src/wayland/handlers/output.rs | 6 + src/wayland/handlers/output_configuration.rs | 237 + src/wayland/handlers/output_power.rs | 78 + src/wayland/handlers/overlap_notify.rs | 55 + src/wayland/handlers/pointer_constraints.rs | 123 + src/wayland/handlers/pointer_warp.rs | 44 + src/wayland/handlers/primary_selection.rs | 12 + src/wayland/handlers/seat.rs | 36 + src/wayland/handlers/security_context.rs | 55 + src/wayland/handlers/selection.rs | 78 + src/wayland/handlers/session_lock.rs | 69 + src/wayland/handlers/shm.rs | 10 + src/wayland/handlers/tablet_manager.rs | 13 + src/wayland/handlers/toplevel_info.rs | 67 + src/wayland/handlers/toplevel_management.rs | 295 + src/wayland/handlers/workspace.rs | 117 + src/wayland/handlers/xdg_activation.rs | 295 + src/wayland/handlers/xdg_foreign.rs | 10 + src/wayland/handlers/xdg_shell/mod.rs | 430 + src/wayland/handlers/xdg_shell/popup.rs | 227 + .../handlers/xwayland_keyboard_grab.rs | 50 + src/wayland/handlers/xwayland_shell.rs | 10 + src/wayland/mod.rs | 4 + src/wayland/protocols/a11y.rs | 239 + src/wayland/protocols/corner_radius.rs | 805 ++ src/wayland/protocols/drm.rs | 286 + src/wayland/protocols/image_capture_source.rs | 136 + src/wayland/protocols/keyboard_layout.rs | 185 + src/wayland/protocols/mod.rs | 13 + .../output_configuration/handlers/cosmic.rs | 315 + .../output_configuration/handlers/mod.rs | 4 + .../output_configuration/handlers/wlr.rs | 486 ++ .../protocols/output_configuration/mod.rs | 581 ++ src/wayland/protocols/output_power.rs | 229 + src/wayland/protocols/overlap_notify.rs | 483 ++ src/wayland/protocols/toplevel_info.rs | 718 ++ src/wayland/protocols/toplevel_management.rs | 302 + src/wayland/protocols/workspace/cosmic_v2.rs | 316 + src/wayland/protocols/workspace/ext.rs | 502 ++ src/wayland/protocols/workspace/mod.rs | 658 ++ src/xwayland.rs | 1298 +++ 273 files changed, 79114 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 build.rs create mode 100644 cosmic-comp-config/Cargo.toml create mode 100644 cosmic-comp-config/src/input.rs create mode 100644 cosmic-comp-config/src/lib.rs create mode 100644 cosmic-comp-config/src/output/comp.rs create mode 100644 cosmic-comp-config/src/output/mod.rs create mode 100644 cosmic-comp-config/src/output/randr.rs create mode 100644 cosmic-comp-config/src/workspace.rs create mode 100644 data/cosmic-comp.service create mode 100644 data/cosmic-service create mode 100644 data/cosmic-session-pre.target create mode 100644 data/cosmic-session.target create mode 100644 data/cosmic.desktop create mode 100644 data/keybindings.ron create mode 100644 data/tiling-exceptions.ron create mode 100644 debian/changelog create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/postinst create mode 100755 debian/rules create mode 100644 debian/source/format create mode 100644 debian/source/options create mode 100644 examples/customized-window-decorations.rs create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 i18n.toml create mode 100644 resources/cursor.rgba create mode 100644 resources/i18n/af/cosmic_comp.ftl create mode 100644 resources/i18n/ar/cosmic_comp.ftl create mode 100644 resources/i18n/be/cosmic_comp.ftl create mode 100644 resources/i18n/bg/cosmic_comp.ftl create mode 100644 resources/i18n/bn/cosmic_comp.ftl create mode 100644 resources/i18n/ca/cosmic_comp.ftl create mode 100644 resources/i18n/cs/cosmic_comp.ftl create mode 100644 resources/i18n/da/cosmic_comp.ftl create mode 100644 resources/i18n/de/cosmic_comp.ftl create mode 100644 resources/i18n/el/cosmic_comp.ftl create mode 100644 resources/i18n/en-GB/cosmic_comp.ftl create mode 100644 resources/i18n/en/cosmic_comp.ftl create mode 100644 resources/i18n/eo/cosmic_comp.ftl create mode 100644 resources/i18n/es-419/cosmic_comp.ftl create mode 100644 resources/i18n/es-MX/cosmic_comp.ftl create mode 100644 resources/i18n/es/cosmic_comp.ftl create mode 100644 resources/i18n/et/cosmic_comp.ftl create mode 100644 resources/i18n/eu/cosmic_comp.ftl create mode 100644 resources/i18n/fa/cosmic_comp.ftl create mode 100644 resources/i18n/fi/cosmic_comp.ftl create mode 100644 resources/i18n/fr/cosmic_comp.ftl create mode 100644 resources/i18n/fy/cosmic_comp.ftl create mode 100644 resources/i18n/ga/cosmic_comp.ftl create mode 100644 resources/i18n/gd/cosmic_comp.ftl create mode 100644 resources/i18n/gu/cosmic_comp.ftl create mode 100644 resources/i18n/he/cosmic_comp.ftl create mode 100644 resources/i18n/hi/cosmic_comp.ftl create mode 100644 resources/i18n/hr/cosmic_comp.ftl create mode 100644 resources/i18n/hu/cosmic_comp.ftl create mode 100644 resources/i18n/id/cosmic_comp.ftl create mode 100644 resources/i18n/ie/cosmic_comp.ftl create mode 100644 resources/i18n/is/cosmic_comp.ftl create mode 100644 resources/i18n/it/cosmic_comp.ftl create mode 100644 resources/i18n/ja/cosmic_comp.ftl create mode 100644 resources/i18n/jv/cosmic_comp.ftl create mode 100644 resources/i18n/ka/cosmic_comp.ftl create mode 100644 resources/i18n/kab/cosmic_comp.ftl create mode 100644 resources/i18n/kk/cosmic_comp.ftl create mode 100644 resources/i18n/kmr/cosmic_comp.ftl create mode 100644 resources/i18n/kn/cosmic_comp.ftl create mode 100644 resources/i18n/ko/cosmic_comp.ftl create mode 100644 resources/i18n/li/cosmic_comp.ftl create mode 100644 resources/i18n/lo/cosmic_comp.ftl create mode 100644 resources/i18n/lt/cosmic_comp.ftl create mode 100644 resources/i18n/ml/cosmic_comp.ftl create mode 100644 resources/i18n/ms/cosmic_comp.ftl create mode 100644 resources/i18n/nb-NO/cosmic_comp.ftl create mode 100644 resources/i18n/nl/cosmic_comp.ftl create mode 100644 resources/i18n/nn/cosmic_comp.ftl create mode 100644 resources/i18n/oc/cosmic_comp.ftl create mode 100644 resources/i18n/pa/cosmic_comp.ftl create mode 100644 resources/i18n/pl/cosmic_comp.ftl create mode 100644 resources/i18n/pt-BR/cosmic_comp.ftl create mode 100644 resources/i18n/pt/cosmic_comp.ftl create mode 100644 resources/i18n/ro/cosmic_comp.ftl create mode 100644 resources/i18n/ru/cosmic_comp.ftl create mode 100644 resources/i18n/sat/cosmic_comp.ftl create mode 100644 resources/i18n/sk/cosmic_comp.ftl create mode 100644 resources/i18n/sl/cosmic_comp.ftl create mode 100644 resources/i18n/sr-Cyrl/cosmic_comp.ftl create mode 100644 resources/i18n/sr-Latn/cosmic_comp.ftl create mode 100644 resources/i18n/sr/cosmic_comp.ftl create mode 100644 resources/i18n/sv/cosmic_comp.ftl create mode 100644 resources/i18n/ta/cosmic_comp.ftl create mode 100644 resources/i18n/th/cosmic_comp.ftl create mode 100644 resources/i18n/ti/cosmic_comp.ftl create mode 100644 resources/i18n/tr/cosmic_comp.ftl create mode 100644 resources/i18n/uk/cosmic_comp.ftl create mode 100644 resources/i18n/uz/cosmic_comp.ftl create mode 100644 resources/i18n/vi/cosmic_comp.ftl create mode 100644 resources/i18n/yue-Hant/cosmic_comp.ftl create mode 100644 resources/i18n/zh-CN/cosmic_comp.ftl create mode 100644 resources/i18n/zh-TW/cosmic_comp.ftl create mode 100644 resources/icons/amd.svg create mode 100644 resources/icons/intel.svg create mode 100644 resources/icons/nvidia.svg create mode 100644 resources/protocols/wayland-drm.xml create mode 100644 rust-toolchain.toml create mode 100644 rustfmt.toml create mode 100644 src/backend/kms/device.rs create mode 100644 src/backend/kms/drm_helpers.rs create mode 100644 src/backend/kms/mod.rs create mode 100644 src/backend/kms/render/gles.rs create mode 100644 src/backend/kms/render/mod.rs create mode 100644 src/backend/kms/render/pixman.rs create mode 100644 src/backend/kms/socket.rs create mode 100644 src/backend/kms/surface/mod.rs create mode 100644 src/backend/kms/surface/timings.rs create mode 100644 src/backend/mod.rs create mode 100644 src/backend/render/animations/mod.rs create mode 100644 src/backend/render/animations/spring.rs create mode 100644 src/backend/render/cursor.rs create mode 100644 src/backend/render/element.rs create mode 100644 src/backend/render/mod.rs create mode 100644 src/backend/render/shaders/blur_downsample.frag create mode 100644 src/backend/render/shaders/blur_upsample.frag create mode 100644 src/backend/render/shaders/clipped_surface.frag create mode 100644 src/backend/render/shaders/offscreen.frag create mode 100644 src/backend/render/shaders/rounded_outline.frag create mode 100644 src/backend/render/shaders/rounded_rectangle.frag create mode 100644 src/backend/render/shaders/shadow.frag create mode 100644 src/backend/render/shadow.rs create mode 100644 src/backend/render/wayland/blur_effect.rs create mode 100644 src/backend/render/wayland/clipped_surface.rs create mode 100644 src/backend/render/wayland/mod.rs create mode 100644 src/backend/winit.rs create mode 100644 src/backend/x11.rs create mode 100644 src/config/input_config.rs create mode 100644 src/config/key_bindings.rs create mode 100644 src/config/mod.rs create mode 100644 src/config/types.rs create mode 100644 src/dbus/a11y_keyboard_monitor.rs create mode 100644 src/dbus/logind.rs create mode 100644 src/dbus/mod.rs create mode 100644 src/dbus/name_owners.rs create mode 100644 src/dbus/power.rs create mode 100644 src/debug.rs create mode 100644 src/hooks.rs create mode 100644 src/input/actions.rs create mode 100644 src/input/gestures/mod.rs create mode 100644 src/input/mod.rs create mode 100644 src/lib.rs create mode 100644 src/logger/mod.rs create mode 100644 src/logger/serializer.rs create mode 100644 src/main.rs create mode 100644 src/session.rs create mode 100644 src/shell/element/mod.rs create mode 100644 src/shell/element/resize_indicator.rs create mode 100644 src/shell/element/stack.rs create mode 100644 src/shell/element/stack/tab.rs create mode 100644 src/shell/element/stack/tabs.rs create mode 100644 src/shell/element/stack_hover.rs create mode 100644 src/shell/element/surface.rs create mode 100644 src/shell/element/swap_indicator.rs create mode 100644 src/shell/element/window.rs create mode 100644 src/shell/focus/mod.rs create mode 100644 src/shell/focus/order.rs create mode 100644 src/shell/focus/target.rs create mode 100644 src/shell/grabs/delay.rs create mode 100644 src/shell/grabs/menu/default.rs create mode 100644 src/shell/grabs/menu/item.rs create mode 100644 src/shell/grabs/menu/mod.rs create mode 100644 src/shell/grabs/mod.rs create mode 100644 src/shell/grabs/moving.rs create mode 100644 src/shell/layout/floating/grabs/mod.rs create mode 100644 src/shell/layout/floating/grabs/resize.rs create mode 100644 src/shell/layout/floating/mod.rs create mode 100644 src/shell/layout/mod.rs create mode 100644 src/shell/layout/tiling/blocker.rs create mode 100644 src/shell/layout/tiling/grabs/mod.rs create mode 100644 src/shell/layout/tiling/grabs/resize.rs create mode 100644 src/shell/layout/tiling/grabs/swap.rs create mode 100644 src/shell/layout/tiling/mod.rs create mode 100644 src/shell/mod.rs create mode 100644 src/shell/seats.rs create mode 100644 src/shell/workspace.rs create mode 100644 src/shell/zoom.rs create mode 100644 src/state.rs create mode 100644 src/systemd.rs create mode 100644 src/theme.rs create mode 100644 src/utils/env.rs create mode 100644 src/utils/geometry.rs create mode 100644 src/utils/iced/mod.rs create mode 100644 src/utils/iced/state.rs create mode 100644 src/utils/ids.rs create mode 100644 src/utils/mod.rs create mode 100644 src/utils/prelude.rs create mode 100644 src/utils/quirks.rs create mode 100644 src/utils/rlimit.rs create mode 100644 src/utils/screenshot.rs create mode 100644 src/utils/tween.rs create mode 100644 src/wayland/handlers/a11y.rs create mode 100644 src/wayland/handlers/background_effect.rs create mode 100644 src/wayland/handlers/buffer.rs create mode 100644 src/wayland/handlers/compositor.rs create mode 100644 src/wayland/handlers/corner_radius.rs create mode 100644 src/wayland/handlers/data_control/ext.rs create mode 100644 src/wayland/handlers/data_control/mod.rs create mode 100644 src/wayland/handlers/data_control/wlr.rs create mode 100644 src/wayland/handlers/data_device.rs create mode 100644 src/wayland/handlers/decoration.rs create mode 100644 src/wayland/handlers/dmabuf.rs create mode 100644 src/wayland/handlers/drm.rs create mode 100644 src/wayland/handlers/drm_lease.rs create mode 100644 src/wayland/handlers/drm_syncobj.rs create mode 100644 src/wayland/handlers/foreign_toplevel_list.rs create mode 100644 src/wayland/handlers/fractional_scale.rs create mode 100644 src/wayland/handlers/idle_inhibit.rs create mode 100644 src/wayland/handlers/idle_notify.rs create mode 100644 src/wayland/handlers/image_capture_source.rs create mode 100644 src/wayland/handlers/image_copy_capture/mod.rs create mode 100644 src/wayland/handlers/image_copy_capture/render.rs create mode 100644 src/wayland/handlers/image_copy_capture/user_data.rs create mode 100644 src/wayland/handlers/input_method.rs create mode 100644 src/wayland/handlers/keyboard_layout.rs create mode 100644 src/wayland/handlers/keyboard_shortcuts_inhibit.rs create mode 100644 src/wayland/handlers/layer_shell.rs create mode 100644 src/wayland/handlers/mod.rs create mode 100644 src/wayland/handlers/output.rs create mode 100644 src/wayland/handlers/output_configuration.rs create mode 100644 src/wayland/handlers/output_power.rs create mode 100644 src/wayland/handlers/overlap_notify.rs create mode 100644 src/wayland/handlers/pointer_constraints.rs create mode 100644 src/wayland/handlers/pointer_warp.rs create mode 100644 src/wayland/handlers/primary_selection.rs create mode 100644 src/wayland/handlers/seat.rs create mode 100644 src/wayland/handlers/security_context.rs create mode 100644 src/wayland/handlers/selection.rs create mode 100644 src/wayland/handlers/session_lock.rs create mode 100644 src/wayland/handlers/shm.rs create mode 100644 src/wayland/handlers/tablet_manager.rs create mode 100644 src/wayland/handlers/toplevel_info.rs create mode 100644 src/wayland/handlers/toplevel_management.rs create mode 100644 src/wayland/handlers/workspace.rs create mode 100644 src/wayland/handlers/xdg_activation.rs create mode 100644 src/wayland/handlers/xdg_foreign.rs create mode 100644 src/wayland/handlers/xdg_shell/mod.rs create mode 100644 src/wayland/handlers/xdg_shell/popup.rs create mode 100644 src/wayland/handlers/xwayland_keyboard_grab.rs create mode 100644 src/wayland/handlers/xwayland_shell.rs create mode 100644 src/wayland/mod.rs create mode 100644 src/wayland/protocols/a11y.rs create mode 100644 src/wayland/protocols/corner_radius.rs create mode 100644 src/wayland/protocols/drm.rs create mode 100644 src/wayland/protocols/image_capture_source.rs create mode 100644 src/wayland/protocols/keyboard_layout.rs create mode 100644 src/wayland/protocols/mod.rs create mode 100644 src/wayland/protocols/output_configuration/handlers/cosmic.rs create mode 100644 src/wayland/protocols/output_configuration/handlers/mod.rs create mode 100644 src/wayland/protocols/output_configuration/handlers/wlr.rs create mode 100644 src/wayland/protocols/output_configuration/mod.rs create mode 100644 src/wayland/protocols/output_power.rs create mode 100644 src/wayland/protocols/overlap_notify.rs create mode 100644 src/wayland/protocols/toplevel_info.rs create mode 100644 src/wayland/protocols/toplevel_management.rs create mode 100644 src/wayland/protocols/workspace/cosmic_v2.rs create mode 100644 src/wayland/protocols/workspace/ext.rs create mode 100644 src/wayland/protocols/workspace/mod.rs create mode 100644 src/xwayland.rs diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..e6ca28b --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +- [ ] I have disclosed use of any AI generated code in my commit messages. + - If you are using an LLM, and do not fully understand the changes it is making to the code base, do not create a PR. + - In our experience, AI generated code often results in overly complex code that lacks enough context for a proper fix or feature inclusion. This results in considerably longer code reviews. Due to this, AI authored or partially authored PRs may be closed without comment. +- [ ] I understand these changes in full and will be able to respond to review comments. +- [ ] My change is accurately described in the commit message. +- [ ] My contribution is tested and working as described. +- [ ] I have read the [Developer Certificate of Origin](https://developercertificate.org/) and certify my contribution under its conditions. + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bc45535 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,104 @@ +name: Continuous Integration + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + format: + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v6 + - name: Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - name: Cargo cache + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }} + - name: Format + run: cargo +stable fmt --all -- --check + + clippy-check: + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v6 + - name: Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - name: Cargo cache + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }} + - name: System dependencies + run: sudo apt-get update; sudo apt-get install libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libdisplay-info-dev + - name: Clippy + run: cargo +stable clippy --all-features -- -D warnings + + check-features: + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v6 + - name: Rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Cargo cache + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }} + - name: System dependencies + run: sudo apt-get update; sudo apt-get install libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libdisplay-info-dev + - run: cargo +stable check --no-default-features + - run: cargo +stable check --features debug + - run: cargo +stable check --features profile-with-tracy + - run: cargo +stable check --features profile-with-tracy-gpu + + test: + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v6 + - name: Rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Cargo cache + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }} + - name: System dependencies + run: sudo apt-get update; sudo apt-get install libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libdisplay-info-dev libpixman-1-dev meson ninja-build + - name: Build cache + uses: actions/cache@v5 + with: + path: target + key: ${{ runner.os }}-build-${{ hashFiles('**/Cargo.toml') }} + - name: Build and install Libseat + run: | + wget https://git.sr.ht/~kennylevinsen/seatd/archive/0.9.1.tar.gz -O libseat-source.tar.gz + tar xf libseat-source.tar.gz + cd seatd-0.9.1 + meson setup -Dlibseat-builtin=enabled -Dserver=disabled -Dexamples=disabled -Dman-pages=disabled build . + ninja -C build + sudo meson install -C build + sudo ldconfig + - name: Test features + env: + RUST_BACKTRACE: full + run: cargo test --all-features diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93276fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +debian/* +!debian/cfs-tweak.service +!debian/changelog +!debian/control +!debian/copyright +!debian/cosmic-comp.install +!debian/postinst +!debian/rules +!debian/source +.cargo +target +vendor +vendor.tar +.vscode +*.log +/result diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..fd73305 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7219 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + +[[package]] +name = "almost" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aa2999eb46af81abb65c2d30d446778d7e613b60bbf4e174a027e80f90a3c14" + +[[package]] +name = "android-activity" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" +dependencies = [ + "android-properties", + "bitflags 2.11.0", + "cc", + "cesu8", + "jni", + "jni-sys", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 1.0.69", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "appendlist" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e149dc73cd30538307e7ffa2acd3d2221148eaeed4871f246657b1c3eaa1cbd2" + +[[package]] +name = "apply" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f47b57fc4521e3cae26a4d45b5227f8fadee4c345be0fefd8d5d1711afb8aeb9" + +[[package]] +name = "approx" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f2a05fd1bd10b2527e20a2cd32d8873d115b8b39fe219ee25f42a8aca6ba278" +dependencies = [ + "num-traits", +] + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "arc-swap" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" +dependencies = [ + "rustversion", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.4", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.1.4", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.4", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atomic_float" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628d228f918ac3b82fe590352cc719d30664a0c13ca3a60266fe02c7132d480a" + +[[package]] +name = "atomicwrites" +version = "0.4.2" +source = "git+https://github.com/jackpot51/rust-atomicwrites#043ab4859d53ffd3d55334685303d8df39c9f768" +dependencies = [ + "rustix 0.38.44", + "tempfile", + "windows-sys 0.48.0", +] + +[[package]] +name = "auto_enums" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65398a2893f41bce5c9259f6e1a4f03fbae40637c1bdc755b4f387f48c613b03" +dependencies = [ + "derive_utils", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "cfg_aliases", +] + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "btoi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b5ab9db53bcda568284df0fd39f6eac24ad6f7ba7ff1168b9e76eba6576b976" +dependencies = [ + "num-traits", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "calloop" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" +dependencies = [ + "async-task", + "bitflags 2.11.0", + "futures-core", + "polling", + "rustix 1.1.4", + "slab", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop", + "rustix 1.1.4", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-expr" +version = "0.20.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78cef5b5a1a6827c7322ae2a636368a573006b27cfa76c7ebd53e834daeaab6a" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cgmath" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a98d30140e3296250832bbaaff83b27dcd6fa3cc70fb6f1f3e5c9c0023b5317" +dependencies = [ + "approx 0.4.0", + "num-traits", +] + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.0", +] + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "clipboard_macos" +version = "0.1.0" +source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" +dependencies = [ + "objc", + "objc-foundation", + "objc_id", +] + +[[package]] +name = "clipboard_wayland" +version = "0.2.2" +source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" +dependencies = [ + "dnd", + "mime 0.1.0", + "smithay-clipboard", +] + +[[package]] +name = "clipboard_x11" +version = "0.4.2" +source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" +dependencies = [ + "thiserror 1.0.69", + "x11rb", +] + +[[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", + "core-graphics", + "foreign-types", + "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", + "core-graphics-types", + "libc", + "objc", +] + +[[package]] +name = "codespan-reporting" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "configparser" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57e3272f0190c3f1584272d613719ba5fc7df7f4942fe542e63d949cf3a649b" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + +[[package]] +name = "cosmic-comp" +version = "1.0.0" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "calloop", + "clap_lex", + "cosmic-comp-config", + "cosmic-config", + "cosmic-protocols", + "cosmic-settings-config", + "cosmic-settings-daemon-config", + "cosmic-text", + "egui", + "egui_plot", + "futures-executor", + "futures-util", + "glam 0.33.2", + "i18n-embed", + "i18n-embed-fl", + "iced_graphics", + "iced_tiny_skia", + "id_tree", + "indexmap 2.13.0", + "jiff", + "keyframe", + "libcosmic", + "libdisplay-info", + "libsystemd", + "log-panics", + "logind-zbus", + "ordered-float", + "parking_lot", + "png 0.18.1", + "profiling", + "rand 0.10.0", + "regex", + "ron 0.12.0", + "rust-embed", + "rustix 1.1.4", + "sanitize-filename", + "serde", + "serde_json", + "smallvec", + "smithay", + "smithay-egui", + "thiserror 2.0.18", + "tiny-skia", + "tracing", + "tracing-journald", + "tracing-subscriber", + "tracy-client", + "wayland-backend", + "wayland-scanner", + "xcursor", + "xdg", + "xdg-user", + "xkbcommon 0.9.0", + "zbus", +] + +[[package]] +name = "cosmic-comp-config" +version = "1.0.0" +dependencies = [ + "cosmic-config", + "cosmic-randr-shell", + "input", + "libdisplay-info", + "ron 0.12.0", + "serde", + "tracing", +] + +[[package]] +name = "cosmic-config" +version = "1.0.0" +source = "git+https://github.com/pop-os/libcosmic#d922ac380134c00208d2acf93947d44fb217eec5" +dependencies = [ + "atomicwrites", + "calloop", + "cosmic-config-derive", + "cosmic-settings-daemon", + "dirs", + "futures-util", + "iced_futures", + "known-folders", + "notify", + "ron 0.12.0", + "serde", + "tracing", + "xdg", + "zbus", +] + +[[package]] +name = "cosmic-config-derive" +version = "1.0.0" +source = "git+https://github.com/pop-os/libcosmic#d922ac380134c00208d2acf93947d44fb217eec5" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "cosmic-freedesktop-icons" +version = "0.4.0" +source = "git+https://github.com/pop-os/freedesktop-icons#ab4c57b8e416c6af9297cb04d101889896fd9a92" +dependencies = [ + "bstr", + "btoi", + "memchr", + "memmap2", + "thiserror 2.0.18", + "tracing", + "xdg", +] + +[[package]] +name = "cosmic-protocols" +version = "0.2.0" +source = "git+https://github.com/pop-os//cosmic-protocols?branch=main#32283d76a8d0342da74c4cc022a533c52dcf378f" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "wayland-server", +] + +[[package]] +name = "cosmic-randr-shell" +version = "0.1.0" +source = "git+https://github.com/pop-os/cosmic-randr/#6e8e795970fa06d434af22775e415b517f7552d3" +dependencies = [ + "kdl", + "slotmap", + "thiserror 2.0.18", +] + +[[package]] +name = "cosmic-settings-config" +version = "0.1.0" +source = "git+https://github.com/pop-os/cosmic-settings-daemon#e37160f14d1e7ee428f973cd2848b4e95f83dfe1" +dependencies = [ + "cosmic-config", + "ron 0.11.0", + "serde", + "serde_with", + "tracing", + "xkbcommon 0.9.0", +] + +[[package]] +name = "cosmic-settings-daemon" +version = "0.1.0" +source = "git+https://github.com/pop-os/dbus-settings-bindings#eed01dd3609e90e3c8cd043656734c500956c793" +dependencies = [ + "zbus", +] + +[[package]] +name = "cosmic-settings-daemon-config" +version = "0.1.0" +source = "git+https://github.com/pop-os/cosmic-settings-daemon#e37160f14d1e7ee428f973cd2848b4e95f83dfe1" +dependencies = [ + "cosmic-config", + "cosmic-theme", + "serde", +] + +[[package]] +name = "cosmic-text" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be17b688510d934ce13f48a2beba700e11583e281e0fda99c22bb256a14eda73" +dependencies = [ + "bitflags 2.11.0", + "fontdb", + "harfrust", + "linebender_resource_handle", + "log", + "rangemap", + "rustc-hash 2.1.1", + "self_cell", + "skrifa 0.40.0", + "smol_str", + "swash", + "sys-locale", + "unicode-bidi", + "unicode-linebreak", + "unicode-script", + "unicode-segmentation", +] + +[[package]] +name = "cosmic-theme" +version = "1.0.0" +source = "git+https://github.com/pop-os/libcosmic#d922ac380134c00208d2acf93947d44fb217eec5" +dependencies = [ + "almost", + "configparser", + "cosmic-config", + "csscolorparser", + "dirs", + "hex_color", + "palette", + "ron 0.12.0", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "cryoglyph" +version = "0.1.0" +source = "git+https://github.com/iced-rs/cryoglyph.git?rev=e429a025df36ab8145708acb309080ae3deec17a#e429a025df36ab8145708acb309080ae3deec17a" +dependencies = [ + "cosmic-text", + "etagere", + "lru", + "rustc-hash 2.1.1", + "wgpu", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "css-color" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42aaeae719fd78ce501d77c6cdf01f7e96f26bcd5617a4903a1c2b97e388543a" + +[[package]] +name = "csscolorparser" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "199f851bd3cb5004c09474252c7f74e7c047441ed0979bf3688a7106a13da952" +dependencies = [ + "num-traits", + "phf 0.13.1", + "serde", + "uncased", +] + +[[package]] +name = "ctor-lite" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e162d0c2e2068eb736b71e5597eff0b9944e6b973cd9f37b6a288ab9bf20e300" + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_setters" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e6f6fa1f03c14ae082120b84b3c7fbd7b8588d924cf2d7c3daf9afd49df8b9" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_utils" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccfae181bab5ab6c5478b2ccb69e4c68a02f8c3ec72f6616bfec9dbc599d2ee0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.0", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "dnd" +version = "0.1.0" +source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" +dependencies = [ + "bitflags 2.11.0", + "mime 0.1.0", + "raw-window-handle", + "smithay-client-toolkit", + "smithay-clipboard", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "drm" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0f8a69e60d75ae7dab4ef26a59ca99f2a89d4c142089b537775ae0c198bdcde" +dependencies = [ + "bitflags 2.11.0", + "bytemuck", + "drm-ffi 0.7.1", + "drm-fourcc", + "rustix 0.38.44", +] + +[[package]] +name = "drm" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bc8c5c6c2941f70a55c15f8d9f00f9710ebda3ffda98075f996a0e6c92756f" +dependencies = [ + "bitflags 2.11.0", + "bytemuck", + "drm-ffi 0.9.0", + "drm-fourcc", + "libc", + "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 0.6.1", + "rustix 0.38.44", +] + +[[package]] +name = "drm-ffi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e41459d99a9b529845f6d2c909eb9adf3b6d2f82635ae40be8de0601726e8b" +dependencies = [ + "drm-sys 0.8.0", + "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 = "drm-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bafb66c8dbc944d69e15cfcc661df7e703beffbaec8bd63151368b06c5f9858c" +dependencies = [ + "libc", + "linux-raw-sys 0.6.5", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecolor" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc4feb366740ded31a004a0e4452fbf84e80ef432ecf8314c485210229672fd1" +dependencies = [ + "bytemuck", + "emath", +] + +[[package]] +name = "egui" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dd34cec49ab55d85ebf70139cb1ccd29c977ef6b6ba4fe85489d6877ee9ef3" +dependencies = [ + "ahash", + "bitflags 2.11.0", + "emath", + "epaint", + "nohash-hasher", + "profiling", +] + +[[package]] +name = "egui_extras" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624659a2e972a46f4d5f646557906c55f1cd5a0836eddbe610fdf1afba1b4226" +dependencies = [ + "ahash", + "egui", + "enum-map", + "log", + "mime_guess2", + "profiling", + "resvg 0.37.0", +] + +[[package]] +name = "egui_glow" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "910906e3f042ea6d2378ec12a6fd07698e14ddae68aed2d819ffe944a73aab9e" +dependencies = [ + "ahash", + "bytemuck", + "egui", + "glow", + "log", + "memoffset", + "profiling", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "egui_plot" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1794c66fb727dac28dffed2e4b548e5118d1cccc331d368a35411d68725dde71" +dependencies = [ + "ahash", + "egui", + "emath", +] + +[[package]] +name = "emath" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enum-map" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" +dependencies = [ + "enum-map-derive", + "serde", +] + +[[package]] +name = "enum-map-derive" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "epaint" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fcc0f5a7c613afd2dee5e4b30c3e6acafb8ad6f0edb06068811f708a67c562" +dependencies = [ + "ab_glyph", + "ahash", + "bytemuck", + "ecolor", + "emath", + "epaint_default_fonts", + "nohash-hasher", + "parking_lot", + "profiling", +] + +[[package]] +name = "epaint_default_fonts" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7e7a64c02cf7a5b51e745a9e45f60660a286f151c238b9d397b3e923f5082f" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "etagere" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342" +dependencies = [ + "euclid", + "svg_fmt", +] + +[[package]] +name = "euclid" +version = "0.22.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "find-crate" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" +dependencies = [ + "toml 0.5.11", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "float_next_after" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" + +[[package]] +name = "fluent" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8137a6d5a2c50d6b0ebfcb9aaa91a28154e0a70605f112d30cb0cd4a78670477" +dependencies = [ + "fluent-bundle", + "unic-langid", +] + +[[package]] +name = "fluent-bundle" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01203cb8918f5711e73891b347816d932046f95f54207710bda99beaeb423bf4" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash 2.1.1", + "self_cell", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" +dependencies = [ + "memchr", + "thiserror 2.0.18", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "font-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a654f404bbcbd48ea58c617c2993ee91d1cb63727a37bf2323a4edeed1b8c5" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "font-types" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4d2d0cf79d38430cc9dc9aadec84774bff2e1ba30ae2bf6c16cfce9385a23" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree 0.20.0", +] + +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gbm" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce852e998d3ca5e4a97014fb31c940dc5ef344ec7d364984525fd11e8a547e6a" +dependencies = [ + "bitflags 2.11.0", + "drm 0.14.1", + "drm-fourcc", + "gbm-sys", + "libc", +] + +[[package]] +name = "gbm-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13a5f2acc785d8fb6bf6b7ab6bfb0ef5dad4f4d97e8e70bb8e470722312f76f" +dependencies = [ + "libc", +] + +[[package]] +name = "generator" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.4", + "windows-link", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core 0.10.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gif" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glam" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3" + +[[package]] +name = "glam" +version = "0.33.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f22fb22f065b308be0d8724e3706c7fa3fc2a6c7d6899df4cad7860e7a75436" + +[[package]] +name = "glow" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "grid" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9e2d4c0a8296178d8802098410ca05d86b17a10bb5ab559b3fb404c1f948220" + +[[package]] +name = "guillotiere" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62d5865c036cb1393e23c50693df631d3f5d7bcca4c04fe4cc0fd592e74a782" +dependencies = [ + "euclid", + "svg_fmt", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "harfrust" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9f40651a03bc0f7316bd75267ff5767e93017ef3cfffe76c6aa7252cc5a31c" +dependencies = [ + "bitflags 2.11.0", + "bytemuck", + "core_maths", + "read-fonts 0.37.0", + "smallvec", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", +] + +[[package]] +name = "heck" +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.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex_color" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d37f101bf4c633f7ca2e4b5e136050314503dd198e78e325ea602c327c484ef0" +dependencies = [ + "arrayvec", + "rand 0.8.5", + "serde", +] + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "i18n-config" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e06b90c8a0d252e203c94344b21e35a30f3a3a85dc7db5af8f8df9f3e0c63ef" +dependencies = [ + "basic-toml", + "log", + "serde", + "serde_derive", + "thiserror 1.0.69", + "unic-langid", +] + +[[package]] +name = "i18n-embed" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a217bbb075dcaefb292efa78897fc0678245ca67f265d12c351e42268fcb0305" +dependencies = [ + "arc-swap", + "fluent", + "fluent-langneg", + "fluent-syntax", + "i18n-embed-impl", + "intl-memoizer", + "log", + "parking_lot", + "rust-embed", + "sys-locale", + "thiserror 1.0.69", + "unic-langid", + "walkdir", +] + +[[package]] +name = "i18n-embed-fl" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e598ed73b67db92f61e04672e599eef2991a262a40e1666735b8a86d2e7e9f30" +dependencies = [ + "find-crate", + "fluent", + "fluent-syntax", + "i18n-config", + "i18n-embed", + "proc-macro-error2", + "proc-macro2", + "quote", + "strsim", + "syn", + "unic-langid", +] + +[[package]] +name = "i18n-embed-impl" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2cc0e0523d1fe6fc2c6f66e5038624ea8091b3e7748b5e8e0c84b1698db6c2" +dependencies = [ + "find-crate", + "i18n-config", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "iced" +version = "0.14.0" +source = "git+https://github.com/pop-os/libcosmic#d922ac380134c00208d2acf93947d44fb217eec5" +dependencies = [ + "dnd", + "iced_core", + "iced_debug", + "iced_futures", + "iced_program", + "iced_renderer", + "iced_runtime", + "iced_widget", + "image", + "mime 0.1.0", + "thiserror 2.0.18", + "window_clipboard", +] + +[[package]] +name = "iced_core" +version = "0.14.0" +source = "git+https://github.com/pop-os/libcosmic#d922ac380134c00208d2acf93947d44fb217eec5" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "dnd", + "glam 0.25.0", + "lilt", + "log", + "mime 0.1.0", + "num-traits", + "palette", + "raw-window-handle", + "rustc-hash 2.1.1", + "serde", + "smol_str", + "thiserror 2.0.18", + "unicode-segmentation", + "web-time", + "window_clipboard", +] + +[[package]] +name = "iced_debug" +version = "0.14.0" +source = "git+https://github.com/pop-os/libcosmic#d922ac380134c00208d2acf93947d44fb217eec5" +dependencies = [ + "iced_core", + "iced_futures", + "log", +] + +[[package]] +name = "iced_futures" +version = "0.14.0" +source = "git+https://github.com/pop-os/libcosmic#d922ac380134c00208d2acf93947d44fb217eec5" +dependencies = [ + "futures", + "iced_core", + "log", + "rustc-hash 2.1.1", + "wasm-bindgen-futures", + "wasmtimer", +] + +[[package]] +name = "iced_graphics" +version = "0.14.0" +source = "git+https://github.com/pop-os/libcosmic#d922ac380134c00208d2acf93947d44fb217eec5" +dependencies = [ + "bitflags 2.11.0", + "bytemuck", + "cosmic-text", + "half", + "iced_core", + "iced_futures", + "image", + "kamadak-exif", + "log", + "lyon_path", + "raw-window-handle", + "rustc-hash 2.1.1", + "thiserror 2.0.18", + "unicode-segmentation", +] + +[[package]] +name = "iced_program" +version = "0.14.0" +source = "git+https://github.com/pop-os/libcosmic#d922ac380134c00208d2acf93947d44fb217eec5" +dependencies = [ + "iced_graphics", + "iced_runtime", +] + +[[package]] +name = "iced_renderer" +version = "0.14.0" +source = "git+https://github.com/pop-os/libcosmic#d922ac380134c00208d2acf93947d44fb217eec5" +dependencies = [ + "iced_graphics", + "iced_tiny_skia", + "iced_wgpu", + "log", + "thiserror 2.0.18", +] + +[[package]] +name = "iced_runtime" +version = "0.14.0" +source = "git+https://github.com/pop-os/libcosmic#d922ac380134c00208d2acf93947d44fb217eec5" +dependencies = [ + "bytes", + "dnd", + "iced_core", + "iced_futures", + "raw-window-handle", + "thiserror 2.0.18", + "window_clipboard", +] + +[[package]] +name = "iced_tiny_skia" +version = "0.14.0" +source = "git+https://github.com/pop-os/libcosmic#d922ac380134c00208d2acf93947d44fb217eec5" +dependencies = [ + "bytemuck", + "cosmic-text", + "iced_debug", + "iced_graphics", + "kurbo 0.10.4", + "log", + "resvg 0.45.1", + "rustc-hash 2.1.1", + "softbuffer", + "tiny-skia", +] + +[[package]] +name = "iced_wgpu" +version = "0.14.0" +source = "git+https://github.com/pop-os/libcosmic#d922ac380134c00208d2acf93947d44fb217eec5" +dependencies = [ + "as-raw-xcb-connection", + "bitflags 2.11.0", + "bytemuck", + "cryoglyph", + "futures", + "glam 0.25.0", + "guillotiere", + "iced_debug", + "iced_graphics", + "log", + "lyon", + "raw-window-handle", + "resvg 0.45.1", + "rustc-hash 2.1.1", + "rustix 0.38.44", + "thiserror 2.0.18", + "tiny-xlib", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-sys", + "wgpu", + "x11rb", +] + +[[package]] +name = "iced_widget" +version = "0.14.2" +source = "git+https://github.com/pop-os/libcosmic#d922ac380134c00208d2acf93947d44fb217eec5" +dependencies = [ + "dnd", + "iced_renderer", + "iced_runtime", + "log", + "num-traits", + "ouroboros", + "rustc-hash 2.1.1", + "thiserror 2.0.18", + "unicode-segmentation", + "window_clipboard", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "id_tree" +version = "1.8.0" +source = "git+https://github.com/Drakulix/id-tree.git?branch=feature%2Fcopy_clone#632a57d6d49160e18d7300fa7edae52281ec5482" +dependencies = [ + "snowflake", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png 0.18.1", + "zune-core 0.5.1", + "zune-jpeg 0.5.12", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imagesize" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" + +[[package]] +name = "imagesize" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "inotify" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +dependencies = [ + "bitflags 2.11.0", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "input" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9793345a65d71317763a33066b5d8351f8760dde8d4930fe9e39b5f14a7959d" +dependencies = [ + "bitflags 2.11.0", + "input-sys", + "libc", + "log", + "udev", +] + +[[package]] +name = "input-sys" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eee07d8e02bd95bf52b2e642cf13d33701b94c6e4b04fbf1d1fb07e9cb19e7" + +[[package]] +name = "intl-memoizer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jiff" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819b44bc7c87d9117eb522f14d46e918add69ff12713c475946b0a29363ed1c2" +dependencies = [ + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-sys 0.61.2", +] + +[[package]] +name = "jiff-static" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "470252db18ecc35fd766c0891b1e3ec6cbbcd62507e85276c01bf75d8e94d4a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68971ebff725b9e2ca27a601c5eb38a4c5d64422c4cbab0c535f248087eda5c2" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4eacb0641a310445a4c513f2a5e23e19952e269c6a38887254d5f837a305506" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kamadak-exif" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1130d80c7374efad55a117d715a3af9368f0fa7a2c54573afc15a188cd984837" +dependencies = [ + "mutate_once", +] + +[[package]] +name = "kdl" +version = "6.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81a29e7b50079ff44549f68c0becb1c73d7f6de2a4ea952da77966daf3d4761e" +dependencies = [ + "miette", + "num", + "winnow 0.6.24", +] + +[[package]] +name = "keyboard-types" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d" +dependencies = [ + "bitflags 2.11.0", + "serde", +] + +[[package]] +name = "keyframe" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60708bf7981518d09095d6f5673ce5cf6a64f1e0d9708b554f670e6d9d2bd9a9" +dependencies = [ + "mint", + "num-traits", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "known-folders" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1886916523694cd6ea3d175f03a1e5010699a2a4cc13696d83d7bea1d80638" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + +[[package]] +name = "kurbo" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "kurbo" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1618d4ebd923e97d67e7cd363d80aef35fe961005cbbbb3d2dad8bdd1bc63440" +dependencies = [ + "arrayvec", + "smallvec", +] + +[[package]] +name = "kurbo" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libcosmic" +version = "1.0.0" +source = "git+https://github.com/pop-os/libcosmic#d922ac380134c00208d2acf93947d44fb217eec5" +dependencies = [ + "apply", + "auto_enums", + "cosmic-config", + "cosmic-freedesktop-icons", + "cosmic-settings-daemon", + "cosmic-theme", + "css-color", + "derive_setters", + "enumflags2", + "float-cmp 0.10.0", + "futures", + "i18n-embed", + "i18n-embed-fl", + "iced", + "iced_core", + "iced_futures", + "iced_renderer", + "iced_runtime", + "iced_tiny_skia", + "iced_widget", + "image", + "jiff", + "log", + "palette", + "phf 0.13.1", + "rust-embed", + "serde", + "slotmap", + "taffy", + "thiserror 2.0.18", + "tracing", + "unicode-segmentation", + "url", + "zbus", +] + +[[package]] +name = "libdisplay-info" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fd96dbb2381ff31f314f07accbdf8550febdcc5cd8761ecaf7c1763361c359" +dependencies = [ + "bitflags 2.11.0", + "libc", + "libdisplay-info-derive", + "libdisplay-info-sys", + "thiserror 2.0.18", +] + +[[package]] +name = "libdisplay-info-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc2c710cf5819e91220a446d9e64acc6814386cc22c509c3f0df83c0b874a98" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "libdisplay-info-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26590d55b8819f9c6b0d95d9d12dc9edbfd1f2413e88814a33b631ac049bb51f" +dependencies = [ + "semver", + "system-deps", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags 2.11.0", + "libc", + "redox_syscall 0.7.2", +] + +[[package]] +name = "libseat" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6656c69b6e0366ffb83faa232f3e4fda5fc91161722d140f2c95ce2e90b1ef31" +dependencies = [ + "errno", + "libseat-sys", + "log", +] + +[[package]] +name = "libseat-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a484fa48be5f62a8d3ffed767779d0ab808cfead91de98ef4362d469097dfb" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libsystemd" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19c97a761fc86953c5b885422b22c891dbf5bcb9dcc99d0110d6ce4c052759f0" +dependencies = [ + "hmac", + "libc", + "log", + "nix", + "nom", + "once_cell", + "serde", + "sha2", + "thiserror 2.0.18", + "uuid", +] + +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "lilt" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67562e5eff6b20553fa9be1c503356768420994e28f67e3eafe6f41910e57ad" +dependencies = [ + "web-time", +] + +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + +[[package]] +name = "linux-raw-sys" +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.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "log-panics" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f9dd8546191c1850ecf67d22f5ff00a935b890d0e84713159a55495cc2ac5f" +dependencies = [ + "backtrace", + "log", +] + +[[package]] +name = "logind-zbus" +version = "5.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469c962578b549a82f3d0cc72d0f77d1123780fa7121e2b03d78b0780f6ccac6" +dependencies = [ + "serde", + "zbus", +] + +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" + +[[package]] +name = "lyon" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbcb7d54d54c8937364c9d41902d066656817dce1e03a44e5533afebd1ef4352" +dependencies = [ + "lyon_algorithms", + "lyon_tessellation", +] + +[[package]] +name = "lyon_algorithms" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c0829e28c4f336396f250d850c3987e16ce6db057ffe047ce0dd54aab6b647" +dependencies = [ + "lyon_path", + "num-traits", +] + +[[package]] +name = "lyon_geom" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e260b6de923e6e47adfedf6243013a7a874684165a6a277594ee3906021b2343" +dependencies = [ + "arrayvec", + "euclid", + "num-traits", +] + +[[package]] +name = "lyon_path" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aeca86bcfd632a15984ba029b539ffb811e0a70bf55e814ef8b0f54f506fdeb" +dependencies = [ + "lyon_geom", + "num-traits", +] + +[[package]] +name = "lyon_tessellation" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3f586142e1280335b1bc89539f7c97dd80f08fc43e9ab1b74ef0a42b04aa353" +dependencies = [ + "float_next_after", + "lyon_path", + "num-traits", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "cfg-if", + "unicode-width", +] + +[[package]] +name = "mime" +version = "0.1.0" +source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" +dependencies = [ + "smithay-clipboard", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess2" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1706dc14a2e140dec0a7a07109d9a3d5890b81e85bd6c60b906b249a77adf0ca" +dependencies = [ + "mime 0.3.17", + "phf 0.11.3", + "phf_shared 0.11.3", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mint" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "mutate_once" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d2233c9842d08cfe13f9eac96e207ca6a2ea10b80259ebe8ad0268be27d2af" + +[[package]] +name = "naga" +version = "28.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "618f667225063219ddfc61251087db8a9aec3c3f0950c916b614e403486f1135" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.16.1", + "hexf-parse", + "indexmap 2.13.0", + "libm", + "log", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "thiserror 2.0.18", + "unicode-ident", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.11.0", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +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 = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.0", + "block2", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.0", + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.11.0", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.0", + "block2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "orbclient" +version = "0.3.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ad2c6bae700b7aa5d1cc30c59bdd3a1c180b09dbaea51e2ae2b8e1cf211fdd" +dependencies = [ + "libc", + "libredox", +] + +[[package]] +name = "ordered-float" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "ouroboros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "palette" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +dependencies = [ + "approx 0.5.1", + "fast-srgb8", + "palette_derive", + "phf 0.11.3", + "serde", +] + +[[package]] +name = "palette_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn", + "unicase", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn", + "uncased", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.2", + "unicase", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher 1.0.2", + "uncased", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pixman" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea217d496c19ac0a8e502b37078e1f683d16344adee9eb247a5d57c165e1edf" +dependencies = [ + "drm-fourcc", + "paste", + "pixman-sys", + "thiserror 1.0.69", +] + +[[package]] +name = "pixman-sys" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0483e89e81d7915defe83c51f23f6800594d64f6f4a21253ce87fd8444ada" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.5.2", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", + "yansi", +] + +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", + "tracy-client", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "pxfm" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20", + "getrandom 0.4.1", + "rand_core 0.10.0", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + +[[package]] +name = "rangemap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rctree" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f" + +[[package]] +name = "read-fonts" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358" +dependencies = [ + "bytemuck", + "font-types 0.10.1", +] + +[[package]] +name = "read-fonts" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5" +dependencies = [ + "bytemuck", + "core_maths", + "font-types 0.11.0", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d94dd2f7cd932d4dc02cc8b2b50dfd38bd079a4e5d79198b99743d7fcf9a4b4" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reis" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3fedd2777cde52c1be5e572efbec485eac7b801c47820eda388d4f13b9c4b" +dependencies = [ + "calloop", + "enumflags2", + "log", + "rustix 1.1.4", +] + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "resvg" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadccb3d99a9efb8e5e00c16fbb732cbe400db2ec7fc004697ee7d97d86cf1f4" +dependencies = [ + "log", + "pico-args", + "rgb", + "svgtypes 0.13.0", + "tiny-skia", + "usvg 0.37.0", +] + +[[package]] +name = "resvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" +dependencies = [ + "gif", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes 0.15.3", + "tiny-skia", + "usvg 0.45.1", + "zune-jpeg 0.4.21", +] + +[[package]] +name = "rgb" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "ron" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db09040cc89e461f1a265139777a2bde7f8d8c67c4936f700c63ce3e2904d468" +dependencies = [ + "base64 0.22.1", + "bitflags 2.11.0", + "serde", + "serde_derive", + "unicode-ident", +] + +[[package]] +name = "ron" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" +dependencies = [ + "bitflags 2.11.0", + "once_cell", + "serde", + "serde_derive", + "typeid", + "unicode-ident", +] + +[[package]] +name = "roxmltree" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "rust-embed" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" +dependencies = [ + "sha2", + "walkdir", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.11.0", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "sanitize-filename" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc984f4f9ceb736a7bb755c3e3bd17dc56370af2600c9780dcc48c66453da34d" +dependencies = [ + "regex", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "indexmap 2.13.0", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.13.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "skrifa" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841" +dependencies = [ + "bytemuck", + "read-fonts 0.35.0", +] + +[[package]] +name = "skrifa" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac" +dependencies = [ + "bytemuck", + "read-fonts 0.37.0", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "smithay" +version = "0.7.0" +source = "git+https://github.com/smithay/smithay.git?rev=1ed69cb#1ed69cb55e9d0e9f38887c3b7d53d24365c4b14f" +dependencies = [ + "aliasable", + "appendlist", + "ash", + "atomic_float", + "bitflags 2.11.0", + "calloop", + "cc", + "cursor-icon", + "downcast-rs", + "drm 0.14.1", + "drm-ffi 0.9.0", + "drm-fourcc", + "encoding_rs", + "errno", + "gbm", + "gl_generator", + "glam 0.33.2", + "glow", + "indexmap 2.13.0", + "input", + "libc", + "libloading", + "libseat", + "pixman", + "pkg-config", + "profiling", + "rand 0.9.2", + "reis", + "rustix 1.1.4", + "scopeguard", + "sha2", + "smallvec", + "tempfile", + "thiserror 2.0.18", + "tracing", + "tracy-client", + "udev", + "wayland-client", + "wayland-cursor", + "wayland-egl", + "wayland-protocols", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-server", + "winit", + "x11rb", + "xkbcommon 0.9.0", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" +dependencies = [ + "bitflags 2.11.0", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 1.1.4", + "thiserror 2.0.18", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-experimental", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.8.0" +source = "git+https://github.com/pop-os/smithay-clipboard?tag=sctk-0.20#859b02c88f45c554049a67c6ddeec1692ce0e20b" +dependencies = [ + "libc", + "raw-window-handle", + "smithay-client-toolkit", + "wayland-backend", +] + +[[package]] +name = "smithay-egui" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6794c95b83518f9f91ae8c2faee52302d521c0d541d016b4ed632374041d9c42" +dependencies = [ + "cgmath", + "egui", + "egui_extras", + "egui_glow", + "image", + "log", + "memoffset", + "smithay", + "xkbcommon 0.8.0", +] + +[[package]] +name = "smol_str" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f7a918bd2a9951d18ee6e48f076843e8e73a9a5d22cf05bcd4b7a81bdd04e17" +dependencies = [ + "borsh", + "serde_core", +] + +[[package]] +name = "snowflake" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27207bb65232eda1f588cf46db2fee75c0808d557f6b3cf19a75f5d6d7c94df1" + +[[package]] +name = "softbuffer" +version = "0.4.1" +source = "git+https://github.com/pop-os/softbuffer?tag=cosmic-4.0#c2b2c19ddb38ff17495643699f97cb1f2064a1be" +dependencies = [ + "as-raw-xcb-connection", + "bytemuck", + "cfg_aliases", + "cocoa", + "core-graphics", + "drm 0.11.1", + "fastrand", + "foreign-types", + "js-sys", + "log", + "memmap2", + "objc", + "raw-window-handle", + "redox_syscall 0.5.18", + "rustix 0.38.44", + "tiny-xlib", + "wasm-bindgen", + "wayland-backend", + "wayland-client", + "wayland-sys", + "web-sys", + "windows-sys 0.52.0", + "x11rb", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp 0.9.0", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "svg_fmt" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" + +[[package]] +name = "svgtypes" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e44e288cd960318917cbd540340968b90becc8bc81f171345d706e7a89d9d70" +dependencies = [ + "kurbo 0.9.5", + "siphasher 0.3.11", +] + +[[package]] +name = "svgtypes" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" +dependencies = [ + "kurbo 0.11.3", + "siphasher 1.0.2", +] + +[[package]] +name = "swash" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47846491253e976bdd07d0f9cc24b7daf24720d11309302ccbbc6e6b6e53550a" +dependencies = [ + "skrifa 0.37.0", + "yazi", + "zeno", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "system-deps" +version = "7.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c8f33736f986f16d69b6cb8b03f55ddcad5c41acc4ccc39dd88e84aa805e7f" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.9.12+spec-1.1.0", + "version-compare", +] + +[[package]] +name = "taffy" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ba83ebaf2954d31d05d67340fd46cebe99da2b7133b0dd68d70c65473a437b" +dependencies = [ + "arrayvec", + "grid", + "serde", + "slotmap", +] + +[[package]] +name = "target-lexicon" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" + +[[package]] +name = "tempfile" +version = "3.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +dependencies = [ + "fastrand", + "getrandom 0.4.1", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png 0.17.16", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tiny-xlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0324504befd01cab6e0c994f34b2ffa257849ee019d3fb3b64fb2c858887d89e" +dependencies = [ + "as-raw-xcb-connection", + "ctor-lite", + "libloading", + "pkg-config", + "tracing", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.13.0", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow 0.7.14", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime", + "toml_parser", + "winnow 0.7.14", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow 0.7.14", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-journald" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3a81ed245bfb62592b1e2bc153e77656d94ee6a0497683a65a12ccaf2438d0" +dependencies = [ + "libc", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracy-client" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f6fc3baeac5d86ab90c772e9e30620fc653bf1864295029921a15ef478e6a5" +dependencies = [ + "loom", + "once_cell", + "tracy-client-sys", +] + +[[package]] +name = "tracy-client-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f7c95348f20c1c913d72157b3c6dee6ea3e30b3d19502c5a7f6d3f160dacbf" +dependencies = [ + "cc", + "windows-targets 0.52.6", +] + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash 2.1.1", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "udev" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4e37e9ea4401fc841ff54b9ddfc9be1079b1e89434c1a6a865dd68980f7e9f" +dependencies = [ + "io-lifetimes", + "libc", + "libudev-sys", + "pkg-config", +] + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + +[[package]] +name = "uncased" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" +dependencies = [ + "version_check", +] + +[[package]] +name = "unic-langid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +dependencies = [ + "serde", + "tinystr", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "usvg" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b0a51b72ab80ca511d126b77feeeb4fb1e972764653e61feac30adc161a756" +dependencies = [ + "base64 0.21.7", + "log", + "pico-args", + "usvg-parser", + "usvg-tree", + "xmlwriter", +] + +[[package]] +name = "usvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" +dependencies = [ + "base64 0.22.1", + "data-url", + "flate2", + "fontdb", + "imagesize 0.13.0", + "kurbo 0.11.3", + "log", + "pico-args", + "roxmltree 0.20.0", + "rustybuzz", + "simplecss", + "siphasher 1.0.2", + "strict-num", + "svgtypes 0.15.3", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "usvg-parser" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd4e3c291f45d152929a31f0f6c819245e2921bfd01e7bd91201a9af39a2bdc" +dependencies = [ + "data-url", + "flate2", + "imagesize 0.12.0", + "kurbo 0.9.5", + "log", + "roxmltree 0.19.0", + "simplecss", + "siphasher 0.3.11", + "svgtypes 0.13.0", + "usvg-tree", +] + +[[package]] +name = "usvg-tree" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee3d202ebdb97a6215604b8f5b4d6ef9024efd623cf2e373a6416ba976ec7d3" +dependencies = [ + "rctree", + "strict-num", + "svgtypes 0.13.0", + "tiny-skia-path", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d7d0fce354c88b7982aec4400b3e7fcf723c32737cef571bd165f7613557ee" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee85afca410ac4abba5b584b12e77ea225db6ee5471d0aebaae0861166f9378a" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55839b71ba921e4f75b674cb16f843f4b1f3b26ddfcb3454de1cf65cc021ec0f" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf2e969c2d60ff52e7e98b7392ff1588bffdd1ccd4769eba27222fd3d621571" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0861f0dcdf46ea819407495634953cdcc8a8c7215ab799a7a7ce366be71c7b30" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + +[[package]] +name = "wasmtimer" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c598d6b99ea013e35844697fc4670d08339d5cda15588f193c6beedd12f644b" +dependencies = [ + "futures", + "js-sys", + "parking_lot", + "pin-utils", + "slab", + "wasm-bindgen", +] + +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.4", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.11.0", + "rustix 1.1.4", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.11.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix 1.1.4", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-egl" +version = "0.32.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b97bdb7c49e5bd9b7562f38ff84f0dad47079fdc9e926f691a787f6dbc05451" +dependencies = [ + "wayland-backend", + "wayland-sys", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", + "wayland-server", +] + +[[package]] +name = "wayland-protocols-experimental" +version = "20250721.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", + "wayland-server", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa98634619300a535a9a97f338aed9a5ff1e01a461943e8346ff4ae26007306b" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", + "wayland-server", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-server" +version = "0.31.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1846eb04c49182e04f4a099e2a830a2b745610bbc1d61246e206f29c7000a0" +dependencies = [ + "bitflags 2.11.0", + "downcast-rs", + "rustix 1.1.4", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10053fbf9a374174094915bbce141e87a6bf32ecd9a002980db4b638405e8962" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wgpu" +version = "28.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9cb534d5ffd109c7d1135f34cdae29e60eab94855a625dcfe1705f8bc7ad79f" +dependencies = [ + "arrayvec", + "bitflags 2.11.0", + "bytemuck", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "log", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "28.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d23f4642f53f666adcfd2d3218ab174d1e6681101aef18696b90cbe64d1c10f9" +dependencies = [ + "arrayvec", + "bit-set", + "bit-vec", + "bitflags 2.11.0", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "indexmap 2.13.0", + "log", + "naga", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.18", + "wgpu-core-deps-windows-linux-android", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "28.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ca976e72b2c9964eb243e281f6ce7f14a514e409920920dcda12ae40febaae" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-hal" +version = "28.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d6cb474beb218824dcc9e1ce679d973f719262789bfb27407da560cac20eeb" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libloading", + "log", + "naga", + "portable-atomic", + "portable-atomic-util", + "raw-window-handle", + "renderdoc-sys", + "thiserror 2.0.18", + "wgpu-types", +] + +[[package]] +name = "wgpu-types" +version = "28.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e18308757e594ed2cd27dddbb16a139c42a683819d32a2e0b1b0167552f5840c" +dependencies = [ + "bitflags 2.11.0", + "bytemuck", + "js-sys", + "log", + "web-sys", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window_clipboard" +version = "0.4.1" +source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" +dependencies = [ + "clipboard-win", + "clipboard_macos", + "clipboard_wayland", + "clipboard_x11", + "dnd", + "mime 0.1.0", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winit" +version = "0.31.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2879d2854d1a43e48f67322d4bd097afcb6eb8f8f775c8de0260a71aea1df1aa" +dependencies = [ + "bitflags 2.11.0", + "cfg_aliases", + "cursor-icon", + "dpi", + "libc", + "raw-window-handle", + "rustix 1.1.4", + "smol_str", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d9c0d2cd93efec3a9f9ad819cfaf0834782403af7c0d248c784ec0c61761df" +dependencies = [ + "android-activity", + "bitflags 2.11.0", + "dpi", + "ndk", + "raw-window-handle", + "smol_str", + "tracing", + "winit-core", +] + +[[package]] +name = "winit-appkit" +version = "0.31.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21310ca07851a49c348e0c2cc768e36b52ca65afda2c2354d78ed4b90074d8aa" +dependencies = [ + "bitflags 2.11.0", + "block2", + "dispatch2", + "dpi", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-video", + "objc2-foundation", + "raw-window-handle", + "smol_str", + "tracing", + "winit-common", + "winit-core", +] + +[[package]] +name = "winit-common" +version = "0.31.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45375fbac4cbb77260d83a30b1f9d8105880dbac99a9ae97f56656694680ff69" +dependencies = [ + "memmap2", + "objc2", + "objc2-core-foundation", + "smol_str", + "tracing", + "winit-core", + "x11-dl", + "xkbcommon-dl", +] + +[[package]] +name = "winit-core" +version = "0.31.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4f0ccd7abb43740e2c6124ac7cae7d865ecec74eec63783e8922577ac232583" +dependencies = [ + "bitflags 2.11.0", + "cursor-icon", + "dpi", + "keyboard-types", + "raw-window-handle", + "smol_str", + "web-time", +] + +[[package]] +name = "winit-orbital" +version = "0.31.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51ea1fb262e7209f265f12bd0cc792c399b14355675e65531e9c8a87db287d46" +dependencies = [ + "bitflags 2.11.0", + "dpi", + "orbclient", + "raw-window-handle", + "redox_syscall 0.5.18", + "smol_str", + "tracing", + "winit-core", +] + +[[package]] +name = "winit-uikit" +version = "0.31.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680a356e798837d8eb274d4556e83bceaf81698194e31aafc5cfb8a9f2fab643" +dependencies = [ + "bitflags 2.11.0", + "block2", + "dispatch2", + "dpi", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "raw-window-handle", + "smol_str", + "tracing", + "winit-common", + "winit-core", +] + +[[package]] +name = "winit-wayland" +version = "0.31.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce5afb2ba07da603f84b722c95f9f9396d2cedae3944fb6c0cda4a6f88de545" +dependencies = [ + "ahash", + "bitflags 2.11.0", + "calloop", + "cursor-icon", + "dpi", + "libc", + "memmap2", + "raw-window-handle", + "rustix 1.1.4", + "smithay-client-toolkit", + "smol_str", + "tracing", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "winit-common", + "winit-core", +] + +[[package]] +name = "winit-web" +version = "0.31.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c2490a953fb776fbbd5e295d54f1c3847f4f15b6c3929ec53c09acda6487a92" +dependencies = [ + "atomic-waker", + "bitflags 2.11.0", + "concurrent-queue", + "cursor-icon", + "dpi", + "js-sys", + "pin-project", + "raw-window-handle", + "smol_str", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "web-time", + "winit-core", +] + +[[package]] +name = "winit-win32" +version = "0.31.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "644ea78af0e858aa3b092e5d1c67c41995a98220c81813f1353b28bc8bb91eaa" +dependencies = [ + "bitflags 2.11.0", + "cursor-icon", + "dpi", + "raw-window-handle", + "smol_str", + "tracing", + "unicode-segmentation", + "windows-sys 0.59.0", + "winit-core", +] + +[[package]] +name = "winit-x11" +version = "0.31.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa5b600756534c7041aa93cd0d244d44b09fca1b89e202bd1cd80dd9f3636c46" +dependencies = [ + "bitflags 2.11.0", + "bytemuck", + "calloop", + "cursor-icon", + "dpi", + "libc", + "percent-encoding", + "raw-window-handle", + "rustix 1.1.4", + "smol_str", + "tracing", + "winit-common", + "winit-core", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "0.6.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.13.0", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 1.1.4", + "x11rb-protocol", + "xcursor", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + +[[package]] +name = "xdg" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5" + +[[package]] +name = "xdg-user" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d5cd803f28ce5a488c8b129858222998c0a06bbec81f9d1b71faed1f9f9f0e" +dependencies = [ + "home", + "libc", +] + +[[package]] +name = "xkbcommon" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d66ca9352cbd4eecbbc40871d8a11b4ac8107cfc528a6e14d7c19c69d0e1ac9" +dependencies = [ + "libc", + "memmap2", + "xkeysym", +] + +[[package]] +name = "xkbcommon" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a974f48060a14e95705c01f24ad9c3345022f4d97441b8a36beb7ed5c4a02d" +dependencies = [ + "libc", + "memmap2", + "xkeysym", +] + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.11.0", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yazi" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.4", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.3", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +dependencies = [ + "serde", + "winnow 1.0.3", + "zvariant", +] + +[[package]] +name = "zeno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "serde", + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core 0.4.12", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe" +dependencies = [ + "zune-core 0.5.1", +] + +[[package]] +name = "zvariant" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.3", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn", + "winnow 1.0.3", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3f8a9e7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,147 @@ +[package] +authors = ["Victoria Brekenfeld"] +edition = "2024" +license = "GPL-3.0-only" +name = "cosmic-comp" +version = "1.0.0" +rust-version = "1.93" + +[workspace] +members = ["cosmic-comp-config"] + +[dependencies] +anyhow = { version = "1.0.102", features = ["backtrace"] } +bitflags = "2.11.0" +calloop = { version = "0.14.4", features = ["executor", "stream"] } +cosmic-comp-config = { path = "cosmic-comp-config", features = [ + "libdisplay-info", + "output", +] } +cosmic-config = { git = "https://github.com/pop-os/libcosmic", features = [ + "calloop", + "macro", +] } +cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", rev = "160b086", default-features = false, features = [ + "server", +] } +cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon" } +cosmic-settings-daemon-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", features = [ + "greeter", +] } +cosmic-text = { version = "0.19", features = [ + "shape-run-cache", +] } +libdisplay-info = "0.3.0" +egui = { version = "0.31.1", optional = true } +egui_plot = { version = "0.31.0", optional = true } +i18n-embed = { version = "0.16", features = [ + "fluent-system", + "desktop-requester", +] } +i18n-embed-fl = "0.10" +iced_tiny_skia = { git = "https://github.com/pop-os/libcosmic" } +iced_graphics = { git = "https://github.com/pop-os/libcosmic/", features = ["geometry"] } +indexmap = "2.13" +keyframe = "1.1.1" +libcosmic = { git = "https://github.com/pop-os/libcosmic", default-features = false } +libsystemd = { version = "0.7", optional = true } +log-panics = { version = "2", features = ["with-backtrace"] } +ordered-float = "5.1" +png = "0.18" +regex = "1" +ron = "0.12" +rust-embed = { version = "8.11", features = ["debug-embed"] } +sanitize-filename = "0.6.0" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +thiserror = "2.0.18" +jiff = "0.2" +tiny-skia = "0.11" +tracing = { version = "0.1.44", features = [ + "max_level_debug", + "release_max_level_info", +] } +tracing-journald = "0.3.2" +tracing-subscriber = { version = "0.3.22", features = [ + "env-filter", + "tracing-log", +] } +tracy-client = { version = "0.18.4", default-features = false } +wayland-backend = "0.3.12" +wayland-scanner = "0.31.8" +xcursor = "0.3.10" +xdg = "^3.0" +xdg-user = "0.2.1" +xkbcommon = "0.9" +zbus = "5.14.0" +profiling = { version = "1.0" } +rustix = { version = "1.1.4", features = ["process"] } +rand = "0.10" +# CLI arguments +clap_lex = "1.0" +parking_lot = "0.12.5" +logind-zbus = { version = "5.3.2", optional = true } +futures-executor = { version = "0.3.32" } +futures-util = "0.3.32" +glam = { version = "0.33", default-features = false, features = ["std"] } +smallvec = "1.15.2" + +[dependencies.id_tree] +branch = "feature/copy_clone" +git = "https://github.com/Drakulix/id-tree.git" + +[dependencies.smithay] +version = "0.7.0" +default-features = false +features = [ + "backend_drm", + "backend_gbm", + "backend_egl", + "backend_libinput", + "backend_session_libseat", + "backend_udev", + "backend_winit", + "backend_vulkan", + "backend_x11", + "desktop", + "renderer_glow", + "renderer_multi", + "renderer_pixman", + "wayland_frontend", + "xwayland", +] + +[dependencies.smithay-egui] +version = "0.2.0" +features = ["svg"] +optional = true + +[features] +debug = ["egui", "egui_plot", "smithay-egui", "anyhow/backtrace"] +default = ["systemd"] +logind = ["logind-zbus"] +systemd = ["libsystemd", "logind"] +profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client/default"] +profile-with-tracy-gpu = ["profile-with-tracy", "smithay/tracy_gpu_profiling"] + +[profile.dev.package.tiny-skia] +opt-level = 2 + +[profile.dev.package.rustybuzz] +opt-level = 2 + +[profile.dev.package.ttf-parser] +opt-level = 2 + +[profile.fastdebug] +debug = true +inherits = "release" + +[profile.release] +lto = "fat" + +[patch."https://github.com/pop-os/cosmic-protocols"] +cosmic-protocols = { git = "https://github.com/pop-os//cosmic-protocols", branch = "main" } + +[patch.crates-io] +smithay = { git = "https://github.com/smithay/smithay.git", rev = "1ed69cb" } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..16f59ee --- /dev/null +++ b/Makefile @@ -0,0 +1,67 @@ +export prefix ?= /usr +sysconfdir ?= /etc +bindir = $(prefix)/bin +libdir = $(prefix)/lib +sharedir = $(prefix)/share + +BINARY = cosmic-comp +CARGO_TARGET_DIR ?= target +TARGET = debug +DEBUG ?= 0 + +.PHONY = all clean install uninstall vendor + +ifeq ($(DEBUG),0) + TARGET = release + ARGS += --release +endif + +VENDOR ?= 0 +ifneq ($(VENDOR),0) + ARGS += --offline --locked +endif + +TARGET_BIN="$(DESTDIR)$(bindir)/$(BINARY)" + +KEYBINDINGS_CONF="$(DESTDIR)$(sharedir)/cosmic/com.system76.CosmicSettings.Shortcuts/v1/defaults" +TILING_EXCEPTIONS_CONF="$(DESTDIR)$(sharedir)/cosmic/com.system76.CosmicSettings.WindowRules/v1/tiling_exception_defaults" + +all: extract-vendor + cargo build $(ARGS) + +clean: + cargo clean + +distclean: + rm -rf .cargo vendor vendor.tar target + +vendor: + mkdir -p .cargo + cargo vendor | head -n -1 > .cargo/config + echo 'directory = "vendor"' >> .cargo/config + [ -n "$(SOURCE_GIT_HASH)" ] && printf '\n[env]\nGIT_HASH = "%s"\n' "$(SOURCE_GIT_HASH)" >> .cargo/config || true + tar pcf vendor.tar vendor + rm -rf vendor + +extract-vendor: +ifeq ($(VENDOR),1) + rm -rf vendor; tar pxf vendor.tar +endif + +install: + install -Dm0755 "$(CARGO_TARGET_DIR)/$(TARGET)/$(BINARY)" "$(TARGET_BIN)" + install -Dm0644 "data/keybindings.ron" "$(KEYBINDINGS_CONF)" + install -Dm0644 "data/tiling-exceptions.ron" "$(TILING_EXCEPTIONS_CONF)" + +install-bare-session: install + install -Dm0644 "data/cosmic.desktop" "$(DESTDIR)$(sharedir)/wayland-sessions/cosmic.desktop" + install -Dm0644 "data/cosmic-session.target" "$(DESTDIR)$(libdir)/systemd/user/cosmic-session.target" + install -Dm0644 "data/cosmic-session-pre.target" "$(DESTDIR)$(libdir)/systemd/user/cosmic-session-pre.target" + install -Dm0644 "data/cosmic-comp.service" "$(DESTDIR)$(libdir)/systemd/user/cosmic-comp.service" + install -Dm0755 "data/cosmic-service" "$(DESTDIR)/$(bindir)/cosmic-service" + +uninstall: + rm "$(TARGET_BIN)" "$(KEYBINDINGS_CONF)" + +uninstall-bare-session: + rm "$(DESTDIR)$(sharedir)/wayland-sessions/cosmic.desktop" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..b1cfaa9 --- /dev/null +++ b/build.rs @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-3.0-only +use std::process::Command; + +fn main() { + if let Ok(output) = Command::new("git").args(["rev-parse", "HEAD"]).output() { + let git_hash = String::from_utf8(output.stdout).unwrap(); + println!("cargo:rustc-env=GIT_HASH={}", git_hash); + } +} diff --git a/cosmic-comp-config/Cargo.toml b/cosmic-comp-config/Cargo.toml new file mode 100644 index 0000000..f01a4bb --- /dev/null +++ b/cosmic-comp-config/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "cosmic-comp-config" +version = "1.0.0" +edition = "2024" + +[dependencies] +cosmic-config = { git = "https://github.com/pop-os/libcosmic/" } +cosmic-randr-shell = { git = "https://github.com/pop-os/cosmic-randr/", optional = true } +input = "0.10.0" +libdisplay-info = { version = "0.3.0", optional = true } +serde = { version = "1", features = ["derive"] } +ron = { version = "0.12", optional = true } +tracing = { version = "0.1.44", features = [ + "max_level_debug", + "release_max_level_info", +], optional = true } + +[features] +default = [] +output = ["ron", "tracing"] +randr = ["cosmic-randr-shell", "output"] diff --git a/cosmic-comp-config/src/input.rs b/cosmic-comp-config/src/input.rs new file mode 100644 index 0000000..df6f34c --- /dev/null +++ b/cosmic-comp-config/src/input.rs @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: GPL-3.0-only + +#![allow(non_snake_case)] + +pub use input::{AccelProfile, ClickMethod, ScrollMethod, TapButtonMap}; +use serde::{Deserialize, Serialize}; + +// Note: For the following values, None is used to represent the system default +// Configuration for input devices +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] +pub struct InputConfig { + pub state: DeviceState, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub acceleration: Option, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub calibration: Option<[f32; 6]>, + #[serde(with = "ClickMethodDef")] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub click_method: Option, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub disable_while_typing: Option, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub left_handed: Option, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub middle_button_emulation: Option, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub rotation_angle: Option, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub scroll_config: Option, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub tap_config: Option, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub map_to_output: Option, +} + +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] +pub struct AccelConfig { + #[serde(with = "AccelProfileDef")] + pub profile: Option, + pub speed: f64, +} + +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] +pub struct ScrollConfig { + #[serde(with = "ScrollMethodDef")] + pub method: Option, + pub natural_scroll: Option, + pub scroll_button: Option, + pub scroll_factor: Option, +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub enum DeviceState { + #[default] + Enabled, + Disabled, + DisabledOnExternalMouse, +} + +#[derive(Debug, Default, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash)] +pub enum TouchpadOverride { + #[default] + None, + ForceDisable, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct TapConfig { + pub enabled: bool, + #[serde(with = "TapButtonMapDef")] + pub button_map: Option, + pub drag: bool, + pub drag_lock: bool, +} + +mod ClickMethodDef { + use input::ClickMethod as ClickMethodOrig; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + #[derive(Debug, Serialize, Deserialize)] + pub enum ClickMethod { + ButtonAreas, + Clickfinger, + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let o = Option::deserialize(deserializer)?; + Ok(o.map(|x| match x { + ClickMethod::ButtonAreas => ClickMethodOrig::ButtonAreas, + ClickMethod::Clickfinger => ClickMethodOrig::Clickfinger, + })) + } + + pub fn serialize(arg: &Option, ser: S) -> Result + where + S: Serializer, + { + let arg = match arg { + Some(ClickMethodOrig::ButtonAreas) => Some(ClickMethod::ButtonAreas), + Some(ClickMethodOrig::Clickfinger) => Some(ClickMethod::Clickfinger), + Some(_) | None => None, + }; + Option::serialize(&arg, ser) + } +} + +mod AccelProfileDef { + use input::AccelProfile as AccelProfileOrig; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + #[derive(Debug, Serialize, Deserialize)] + enum AccelProfile { + Flat, + Adaptive, + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let o = Option::deserialize(deserializer)?; + Ok(o.map(|x| match x { + AccelProfile::Flat => AccelProfileOrig::Flat, + AccelProfile::Adaptive => AccelProfileOrig::Adaptive, + })) + } + + pub fn serialize(arg: &Option, ser: S) -> Result + where + S: Serializer, + { + let arg = match arg { + Some(AccelProfileOrig::Flat) => Some(AccelProfile::Flat), + Some(AccelProfileOrig::Adaptive) => Some(AccelProfile::Adaptive), + Some(_) | None => None, + }; + Option::serialize(&arg, ser) + } +} + +mod ScrollMethodDef { + use input::ScrollMethod as ScrollMethodOrig; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + #[derive(Debug, Serialize, Deserialize)] + pub enum ScrollMethod { + NoScroll, + TwoFinger, + Edge, + OnButtonDown, + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let o = Option::deserialize(deserializer)?; + Ok(o.map(|x| match x { + ScrollMethod::NoScroll => ScrollMethodOrig::NoScroll, + ScrollMethod::TwoFinger => ScrollMethodOrig::TwoFinger, + ScrollMethod::Edge => ScrollMethodOrig::Edge, + ScrollMethod::OnButtonDown => ScrollMethodOrig::OnButtonDown, + })) + } + + pub fn serialize(arg: &Option, ser: S) -> Result + where + S: Serializer, + { + let arg = match arg { + Some(ScrollMethodOrig::NoScroll) => Some(ScrollMethod::NoScroll), + Some(ScrollMethodOrig::TwoFinger) => Some(ScrollMethod::TwoFinger), + Some(ScrollMethodOrig::Edge) => Some(ScrollMethod::Edge), + Some(ScrollMethodOrig::OnButtonDown) => Some(ScrollMethod::OnButtonDown), + Some(_) | None => None, + }; + Option::serialize(&arg, ser) + } +} + +mod TapButtonMapDef { + use input::TapButtonMap as TapButtonMapOrig; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + #[derive(Debug, Serialize, Deserialize)] + pub enum TapButtonMap { + LeftRightMiddle, + LeftMiddleRight, + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let o = Option::deserialize(deserializer)?; + Ok(o.map(|x| match x { + TapButtonMap::LeftRightMiddle => TapButtonMapOrig::LeftRightMiddle, + TapButtonMap::LeftMiddleRight => TapButtonMapOrig::LeftMiddleRight, + })) + } + + pub fn serialize(arg: &Option, ser: S) -> Result + where + S: Serializer, + { + let arg = match arg { + Some(TapButtonMapOrig::LeftRightMiddle) => Some(TapButtonMap::LeftRightMiddle), + Some(TapButtonMapOrig::LeftMiddleRight) => Some(TapButtonMap::LeftMiddleRight), + Some(_) | None => None, + }; + Option::serialize(&arg, ser) + } +} diff --git a/cosmic-comp-config/src/lib.rs b/cosmic-comp-config/src/lib.rs new file mode 100644 index 0000000..e5e7388 --- /dev/null +++ b/cosmic-comp-config/src/lib.rs @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use cosmic_config::{CosmicConfigEntry, cosmic_config_derive::CosmicConfigEntry}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use crate::input::TouchpadOverride; + +pub mod input; +#[cfg(feature = "output")] +pub mod output; +pub mod workspace; + +#[derive(Debug, Deserialize, Serialize, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct EdidProduct { + pub manufacturer: [char; 3], + pub product: u16, + pub serial: Option, + pub manufacture_week: i32, + pub manufacture_year: i32, + pub model_year: Option, +} + +#[cfg(feature = "libdisplay-info")] +impl From for EdidProduct { + fn from(vp: libdisplay_info::edid::VendorProduct) -> Self { + Self { + manufacturer: vp.manufacturer, + product: vp.product, + serial: vp.serial, + manufacture_week: vp.manufacture_week, + manufacture_year: vp.manufacture_year, + model_year: vp.model_year, + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct KeyboardConfig { + /// Boot state for numlock + pub numlock_state: NumlockState, +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum NumlockState { + BootOn, + #[default] + BootOff, + LastBoot, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct AppearanceConfig { + pub clip_floating_windows: bool, + pub clip_tiled_windows: bool, + pub shadow_tiled_windows: bool, +} + +impl Default for AppearanceConfig { + fn default() -> Self { + AppearanceConfig { + clip_floating_windows: true, + clip_tiled_windows: true, + shadow_tiled_windows: false, + } + } +} + +#[derive(Clone, Debug, PartialEq, CosmicConfigEntry)] +#[version = 1] +pub struct CosmicCompConfig { + pub workspaces: workspace::WorkspaceConfig, + pub pinned_workspaces: Vec, + pub input_default: input::InputConfig, + pub input_touchpad: input::InputConfig, + pub input_touchpad_override: TouchpadOverride, + pub input_devices: HashMap, + pub xkb_config: XkbConfig, + pub keyboard_config: KeyboardConfig, + /// Autotiling enabled + pub autotile: bool, + /// Determines the behavior of the autotile variable + /// If set to Global, autotile applies to all windows in all workspaces + /// If set to PerWorkspace, autotile only applies to new windows, and new workspaces + pub autotile_behavior: TileBehavior, + /// Active hint enabled + pub active_hint: bool, + /// Enables changing keyboard focus to windows when the cursor passes into them + pub focus_follows_cursor: bool, + /// Enables warping the cursor to the focused window when focus changes due to keyboard input + pub cursor_follows_focus: bool, + /// The delay in milliseconds before focus follows mouse (if enabled) + pub focus_follows_cursor_delay: u64, + /// Let X11 applications scale themselves + pub descale_xwayland: XwaylandDescaling, + /// Let X11 applications snoop on certain key-presses to allow for global shortcuts + pub xwayland_eavesdropping: XwaylandEavesdropping, + /// The threshold before windows snap themselves to output edges + pub edge_snap_threshold: u32, + pub accessibility_zoom: ZoomConfig, + pub appearance_settings: AppearanceConfig, + /// Hide the cursor after this many seconds of pointer inactivity (None disables) + pub cursor_hide_timeout: Option, + pub activation_policy: ActivationPolicy, +} + +impl Default for CosmicCompConfig { + fn default() -> Self { + Self { + workspaces: Default::default(), + pinned_workspaces: Vec::new(), + input_default: Default::default(), + // By default, enable tap-to-click and disable-while-typing. + input_touchpad: input::InputConfig { + state: input::DeviceState::Enabled, + click_method: Some(input::ClickMethod::Clickfinger), + disable_while_typing: Some(true), + tap_config: Some(input::TapConfig { + enabled: true, + button_map: Some(input::TapButtonMap::LeftRightMiddle), + drag: true, + drag_lock: false, + }), + ..Default::default() + }, + input_touchpad_override: Default::default(), + input_devices: Default::default(), + xkb_config: Default::default(), + keyboard_config: Default::default(), + autotile: Default::default(), + autotile_behavior: Default::default(), + active_hint: true, + focus_follows_cursor: false, + cursor_follows_focus: false, + focus_follows_cursor_delay: 250, + descale_xwayland: XwaylandDescaling::Fractional, + xwayland_eavesdropping: XwaylandEavesdropping::default(), + edge_snap_threshold: 0, + accessibility_zoom: ZoomConfig::default(), + appearance_settings: AppearanceConfig::default(), + cursor_hide_timeout: None, + activation_policy: ActivationPolicy::default(), + } + } +} + +#[derive(Debug, Default, Copy, Clone, PartialEq, Deserialize, Serialize)] +pub enum TileBehavior { + #[default] + Global, + PerWorkspace, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct XkbConfig { + pub rules: String, + pub model: String, + pub layout: String, + pub variant: String, + pub options: Option, + #[serde(default = "default_repeat_delay")] + pub repeat_delay: u32, + #[serde(default = "default_repeat_rate")] + pub repeat_rate: u32, +} + +impl Default for XkbConfig { + fn default() -> XkbConfig { + XkbConfig { + rules: String::new(), + model: String::new(), + layout: String::new(), + variant: String::new(), + options: None, + repeat_delay: default_repeat_delay(), + repeat_rate: default_repeat_rate(), + } + } +} + +fn default_repeat_rate() -> u32 { + 25 +} + +fn default_repeat_delay() -> u32 { + 600 +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] +pub struct ZoomConfig { + pub start_on_login: bool, + pub show_overlay: bool, + pub increment: u32, + pub view_moves: ZoomMovement, + pub enable_mouse_zoom_shortcuts: bool, +} + +impl Default for ZoomConfig { + fn default() -> Self { + ZoomConfig { + start_on_login: false, + show_overlay: true, + increment: 50, + view_moves: ZoomMovement::Continuously, + enable_mouse_zoom_shortcuts: true, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] +pub enum ZoomMovement { + OnEdge, + Centered, + Continuously, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] +pub struct XwaylandEavesdropping { + pub keyboard: EavesdroppingKeyboardMode, + pub pointer: bool, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] +pub enum EavesdroppingKeyboardMode { + #[default] + None, + Modifiers, + Combinations, + All, +} + +#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, PartialEq, Eq)] +pub enum ActivationPolicy { + #[default] + Focus, + FocusIfActiveWorkspace, + Urgent, +} + +#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum XwaylandDescaling { + #[serde(rename = "true")] + Enabled, + #[serde(rename = "false")] + Disabled, + #[default] + Fractional, +} diff --git a/cosmic-comp-config/src/output/comp.rs b/cosmic-comp-config/src/output/comp.rs new file mode 100644 index 0000000..1810b9d --- /dev/null +++ b/cosmic-comp-config/src/output/comp.rs @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, fs::OpenOptions, path::Path}; +use tracing::{error, warn}; + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum OutputState { + #[serde(rename = "true")] + Enabled, + #[serde(rename = "false")] + Disabled, + Mirroring(String), +} + +fn default_state() -> OutputState { + OutputState::Enabled +} + +#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum AdaptiveSync { + #[serde(rename = "true")] + Enabled, + #[serde(rename = "false")] + Disabled, + Force, +} + +fn default_sync() -> AdaptiveSync { + AdaptiveSync::Enabled +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct OutputsConfig { + pub config: HashMap, Vec>, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct OutputConfig { + pub mode: ((i32, i32), Option), + #[serde(default = "default_sync")] + pub vrr: AdaptiveSync, + pub scale: f64, + pub transform: TransformDef, + pub position: (u32, u32), + #[serde(default = "default_state")] + pub enabled: OutputState, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_bpc: Option, + #[serde(default)] + pub xwayland_primary: bool, +} + +impl Default for OutputConfig { + fn default() -> OutputConfig { + OutputConfig { + mode: ((0, 0), None), + vrr: AdaptiveSync::Enabled, + scale: 1.0, + transform: TransformDef::Normal, + position: (0, 0), + enabled: OutputState::Enabled, + max_bpc: None, + xwayland_primary: false, + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct OutputInfo { + pub connector: String, + pub make: String, + pub model: String, +} + +pub fn load_outputs(path: Option>) -> OutputsConfig { + if let Some(path) = path.as_ref() { + let path: &Path = path.as_ref(); + if path.exists() { + match ron::de::from_reader::<_, OutputsConfig>( + OpenOptions::new().read(true).open(path).unwrap(), + ) { + Ok(mut config) => { + for (info, config) in config.config.iter_mut() { + let config_clone = config.clone(); + for conf in config.iter_mut() { + if let OutputState::Mirroring(conn) = &conf.enabled { + if let Some((j, _)) = info + .iter() + .enumerate() + .find(|(_, info)| &info.connector == conn) + { + if config_clone[j].enabled != OutputState::Enabled { + warn!( + "Invalid Mirroring tag, overriding with `Enabled` instead" + ); + conf.enabled = OutputState::Enabled; + } + } else { + warn!( + "Invalid Mirroring tag, overriding with `Enabled` instead" + ); + conf.enabled = OutputState::Enabled; + } + } + } + } + return config; + } + Err(err) => { + warn!(?err, "Failed to read output_config, resetting.."); + if let Err(err) = std::fs::remove_file(path) { + error!(?err, "Failed to remove output_config."); + } + } + }; + } + } + + OutputsConfig { + config: HashMap::new(), + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum TransformDef { + Normal, + _90, + _180, + _270, + Flipped, + Flipped90, + Flipped180, + Flipped270, +} diff --git a/cosmic-comp-config/src/output/mod.rs b/cosmic-comp-config/src/output/mod.rs new file mode 100644 index 0000000..3476301 --- /dev/null +++ b/cosmic-comp-config/src/output/mod.rs @@ -0,0 +1,6 @@ +/// Internal output configurations used by cosmic-comp +pub mod comp; + +#[cfg(feature = "randr")] +/// cosmic-randr style output configurations +pub mod randr; diff --git a/cosmic-comp-config/src/output/randr.rs b/cosmic-comp-config/src/output/randr.rs new file mode 100644 index 0000000..ac0aa0b --- /dev/null +++ b/cosmic-comp-config/src/output/randr.rs @@ -0,0 +1,95 @@ +use std::path::Path; + +use cosmic_randr_shell::{AdaptiveSyncState, List}; + +use crate::output::comp::OutputState; + +pub struct CompList { + infos: Vec, + outputs: Vec, +} + +impl From for cosmic_randr_shell::List { + fn from(CompList { infos, outputs }: CompList) -> cosmic_randr_shell::List { + let mut list = cosmic_randr_shell::List::default(); + for (info, output) in infos.into_iter().zip(outputs.into_iter()) { + let current = list.modes.insert(cosmic_randr_shell::Mode { + size: (output.mode.0.0 as u32, output.mode.0.1 as u32), + refresh_rate: output.mode.1.unwrap_or_default(), + // XXX not in config as far as i can tell + preferred: false, + }); + let modes = vec![current]; + + // for mode in output. {} + list.outputs.insert(cosmic_randr_shell::Output { + name: info.connector, + enabled: !matches!(output.enabled, OutputState::Disabled), + mirroring: match output.enabled { + OutputState::Mirroring(m) => Some(m), + _ => None, + }, + make: Some(info.make).filter(|make| make != "Unknown"), + model: if info.model.as_str() == "Unknown" { + String::new() + } else { + info.model + }, + position: (output.position.0 as i32, output.position.1 as i32), + scale: output.scale, + transform: Some(match output.transform { + crate::output::comp::TransformDef::Normal => { + cosmic_randr_shell::Transform::Normal + } + crate::output::comp::TransformDef::_90 => { + cosmic_randr_shell::Transform::Rotate90 + } + crate::output::comp::TransformDef::_180 => { + cosmic_randr_shell::Transform::Rotate180 + } + crate::output::comp::TransformDef::_270 => { + cosmic_randr_shell::Transform::Rotate270 + } + crate::output::comp::TransformDef::Flipped => { + cosmic_randr_shell::Transform::Flipped + } + crate::output::comp::TransformDef::Flipped90 => { + cosmic_randr_shell::Transform::Flipped90 + } + crate::output::comp::TransformDef::Flipped180 => { + cosmic_randr_shell::Transform::Flipped180 + } + crate::output::comp::TransformDef::Flipped270 => { + cosmic_randr_shell::Transform::Flipped270 + } + }), + modes, + current: Some(current), + adaptive_sync: Some(match output.vrr { + crate::output::comp::AdaptiveSync::Enabled => AdaptiveSyncState::Auto, + crate::output::comp::AdaptiveSync::Disabled => AdaptiveSyncState::Disabled, + crate::output::comp::AdaptiveSync::Force => AdaptiveSyncState::Always, + }), + xwayland_primary: Some(output.xwayland_primary), + // XXX no physical output size in the config + physical: (0, 0), + adaptive_sync_availability: None, + serial_number: String::new(), + }); + } + + list + } +} + +pub fn load_outputs(path: Option>) -> Vec { + let output_config = crate::output::comp::load_outputs(path); + output_config + .config + .into_iter() + .map(|(infos, outputs)| { + let comp_config = CompList { infos, outputs }; + List::from(comp_config) + }) + .collect() +} diff --git a/cosmic-comp-config/src/workspace.rs b/cosmic-comp-config/src/workspace.rs new file mode 100644 index 0000000..6267626 --- /dev/null +++ b/cosmic-comp-config/src/workspace.rs @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use serde::{Deserialize, Serialize}; + +use crate::EdidProduct; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct WorkspaceConfig { + pub workspace_mode: WorkspaceMode, + #[serde(default)] + pub workspace_layout: WorkspaceLayout, + #[serde(default)] + pub action_on_typing: Action, + #[serde(default = "default_wraparound")] + pub workspace_wraparound: bool, +} + +fn default_wraparound() -> bool { + true +} + +impl Default for WorkspaceConfig { + fn default() -> Self { + Self { + workspace_mode: WorkspaceMode::default(), + workspace_layout: WorkspaceLayout::default(), + action_on_typing: Action::default(), + workspace_wraparound: default_wraparound(), + } + } +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum WorkspaceMode { + #[default] + OutputBound, + Global, +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum WorkspaceLayout { + #[default] + Vertical, + Horizontal, +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum Action { + #[default] + None, + OpenLauncher, + OpenApplications, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct OutputMatch { + pub name: String, + pub edid: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PinnedWorkspace { + pub output: OutputMatch, + pub tiling_enabled: bool, + pub id: Option, + pub name: Option, +} diff --git a/data/cosmic-comp.service b/data/cosmic-comp.service new file mode 100644 index 0000000..5778da1 --- /dev/null +++ b/data/cosmic-comp.service @@ -0,0 +1,12 @@ +[Unit] +Description=Cosmic wayland compositor +BindsTo=cosmic-session.target +Wants=cosmic-session-pre.target +After=cosmic-session-pre.target +Before=cosmic-session.target + +[Service] +Type=notify +ExecStart=/usr/bin/cosmic-comp +Restart=never +ExecStopPost=/usr/bin/systemctl --user unset-environment DISPLAY WAYLAND_DISPLAY \ No newline at end of file diff --git a/data/cosmic-service b/data/cosmic-service new file mode 100644 index 0000000..badf742 --- /dev/null +++ b/data/cosmic-service @@ -0,0 +1,32 @@ +#!/bin/bash + +# From: https://people.debian.org/~mpitt/systemd.conf-2016-graphical-session.pdf + +# robustness: if the previous graphical session left some failed units, +# reset them so that they don't break this startup +for unit in $(systemctl --user --no-legend --state=failed --plain list-units | cut -f1 -d' '); do + partof="$(systemctl --user show -p PartOf --value "$unit")" + for target in cosmic-session.target graphical-session.target; do + if [ "$partof" = "$target" ]; then + systemctl --user reset-failed "$unit" + break + fi + done +done + +# /etc/profile contains a lot of important environment variables +source /etc/profile + +export XDG_CURRENT_DESKTOP=cosmic + +# save environment variables that will be added to systemd +new_env=$(systemctl --user show-environment | cut -d'=' -f 1 | sort | comm -13 - <(env | cut -d'=' -f 1 | sort)) + +# import environment variables from the login manager +systemctl --user import-environment + +# then start the service +systemctl --wait --user start cosmic-comp.service + +# cleanup environment +systemctl --user unset-environment $new_env \ No newline at end of file diff --git a/data/cosmic-session-pre.target b/data/cosmic-session-pre.target new file mode 100644 index 0000000..c9c6a5f --- /dev/null +++ b/data/cosmic-session-pre.target @@ -0,0 +1,8 @@ +[Unit] +Description=Cosmic session early services +Documentation=man:systemd.special(7) +RefuseManualStart=yes +StopWhenUnneeded=yes +BindsTo=graphical-session-pre.target +Before=graphical-session-pre.target +Before=cosmic-session.target \ No newline at end of file diff --git a/data/cosmic-session.target b/data/cosmic-session.target new file mode 100644 index 0000000..bf3997d --- /dev/null +++ b/data/cosmic-session.target @@ -0,0 +1,7 @@ +[Unit] +Description=Cosmic session +Documentation=man:systemd.special(7) +RefuseManualStart=yes +StopWhenUnneeded=yes +BindsTo=graphical-session.target +Before=graphical-session.target \ No newline at end of file diff --git a/data/cosmic.desktop b/data/cosmic.desktop new file mode 100644 index 0000000..48fb116 --- /dev/null +++ b/data/cosmic.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Name=Cosmic +Commment=This session logs you into Cosmic +Commment[sv]=Denna session loggar in dig till Cosmic +Exec=/usr/bin/cosmic-service +Type=Application +DesktopNames=pop:COSMIC +X-GDM-SessionRegisters=false diff --git a/data/keybindings.ron b/data/keybindings.ron new file mode 100644 index 0000000..6efd857 --- /dev/null +++ b/data/keybindings.ron @@ -0,0 +1,128 @@ +{ + (modifiers: [Super, Alt], key: "Escape"): Terminate, + (modifiers: [Super, Shift], key: "Escape"): System(LogOut), + (modifiers: [Super, Ctrl], key: "Escape"): Debug, + (modifiers: [Super], key: "Escape"): System(LockScreen), + (modifiers: [Super], key: "q"): Close, + (modifiers: [Alt], key: "F4"): Close, + + (modifiers: [Super], key: "Left"): Focus(Left), + (modifiers: [Super], key: "Right"): Focus(Right), + (modifiers: [Super], key: "Up"): Focus(Up), + (modifiers: [Super], key: "Down"): Focus(Down), + (modifiers: [Super], key: "h"): Focus(Left), + (modifiers: [Super], key: "j"): Focus(Down), + (modifiers: [Super], key: "k"): Focus(Up), + (modifiers: [Super], key: "l"): Focus(Right), + (modifiers: [Super], key: "u"): Focus(Out), + (modifiers: [Super], key: "i"): Focus(In), + (modifiers: [Super, Shift], key: "Left"): Move(Left), + (modifiers: [Super, Shift], key: "Right"): Move(Right), + (modifiers: [Super, Shift], key: "Up"): Move(Up), + (modifiers: [Super, Shift], key: "Down"): Move(Down), + (modifiers: [Super, Shift], key: "h"): Move(Left), + (modifiers: [Super, Shift], key: "j"): Move(Down), + (modifiers: [Super, Shift], key: "k"): Move(Up), + (modifiers: [Super, Shift], key: "l"): Move(Right), + + (modifiers: [Super], key: "1"): Workspace(1), + (modifiers: [Super], key: "2"): Workspace(2), + (modifiers: [Super], key: "3"): Workspace(3), + (modifiers: [Super], key: "4"): Workspace(4), + (modifiers: [Super], key: "5"): Workspace(5), + (modifiers: [Super], key: "6"): Workspace(6), + (modifiers: [Super], key: "7"): Workspace(7), + (modifiers: [Super], key: "8"): Workspace(8), + (modifiers: [Super], key: "9"): Workspace(9), + (modifiers: [Super], key: "0"): LastWorkspace, + (modifiers: [Super, Shift], key: "1"): MoveToWorkspace(1), + (modifiers: [Super, Shift], key: "2"): MoveToWorkspace(2), + (modifiers: [Super, Shift], key: "3"): MoveToWorkspace(3), + (modifiers: [Super, Shift], key: "4"): MoveToWorkspace(4), + (modifiers: [Super, Shift], key: "5"): MoveToWorkspace(5), + (modifiers: [Super, Shift], key: "6"): MoveToWorkspace(6), + (modifiers: [Super, Shift], key: "7"): MoveToWorkspace(7), + (modifiers: [Super, Shift], key: "8"): MoveToWorkspace(8), + (modifiers: [Super, Shift], key: "9"): MoveToWorkspace(9), + (modifiers: [Super, Shift], key: "0"): MoveToLastWorkspace, + + (modifiers: [Super, Ctrl], key: "Left"): PreviousWorkspace, + (modifiers: [Super, Ctrl], key: "Down"): NextWorkspace, + (modifiers: [Super, Ctrl], key: "Up"): PreviousWorkspace, + (modifiers: [Super, Ctrl], key: "Right"): NextWorkspace, + (modifiers: [Super, Ctrl], key: "h"): PreviousWorkspace, + (modifiers: [Super, Ctrl], key: "j"): NextWorkspace, + (modifiers: [Super, Ctrl], key: "k"): PreviousWorkspace, + (modifiers: [Super, Ctrl], key: "l"): NextWorkspace, + (modifiers: [Super, Shift, Ctrl], key: "Left"): MoveToPreviousWorkspace, + (modifiers: [Super, Shift, Ctrl], key: "Down"): MoveToNextWorkspace, + (modifiers: [Super, Shift, Ctrl], key: "Up"): MoveToPreviousWorkspace, + (modifiers: [Super, Shift, Ctrl], key: "Right"): MoveToNextWorkspace, + (modifiers: [Super, Shift, Ctrl], key: "h"): MoveToPreviousWorkspace, + (modifiers: [Super, Shift, Ctrl], key: "j"): MoveToNextWorkspace, + (modifiers: [Super, Shift, Ctrl], key: "k"): MoveToPreviousWorkspace, + (modifiers: [Super, Shift, Ctrl], key: "l"): MoveToNextWorkspace, + + (modifiers: [Super, Alt], key: "Left"): SwitchOutput(Left), + (modifiers: [Super, Alt], key: "Down"): SwitchOutput(Down), + (modifiers: [Super, Alt], key: "Up"): SwitchOutput(Up), + (modifiers: [Super, Alt], key: "Right"): SwitchOutput(Right), + (modifiers: [Super, Alt], key: "h"): SwitchOutput(Left), + (modifiers: [Super, Alt], key: "k"): SwitchOutput(Up), + (modifiers: [Super, Alt], key: "j"): SwitchOutput(Down), + (modifiers: [Super, Alt], key: "l"): SwitchOutput(Right), + (modifiers: [Super, Shift, Alt], key: "Left"): MoveToOutput(Left), + (modifiers: [Super, Shift, Alt], key: "Down"): MoveToOutput(Down), + (modifiers: [Super, Shift, Alt], key: "Up"): MoveToOutput(Up), + (modifiers: [Super, Shift, Alt], key: "Right"): MoveToOutput(Right), + (modifiers: [Super, Shift, Alt], key: "h"): MoveToOutput(Left), + (modifiers: [Super, Shift, Alt], key: "k"): MoveToOutput(Up), + (modifiers: [Super, Shift, Alt], key: "j"): MoveToOutput(Down), + (modifiers: [Super, Shift, Alt], key: "l"): MoveToOutput(Right), + + (modifiers: [Super], key: "o"): ToggleOrientation, + (modifiers: [Super], key: "s"): ToggleStacking, + (modifiers: [Super], key: "y"): ToggleTiling, + (modifiers: [Super], key: "g"): ToggleWindowFloating, + (modifiers: [Super], key: "x"): SwapWindow, + + (modifiers: [Super], key: "m"): Maximize, + (modifiers: [Super], key: "F11"): Fullscreen, + (modifiers: [Super], key: "r"): Resizing(Outwards), + (modifiers: [Super, Shift], key: "r"): Resizing(Inwards), + + (modifiers: [Super, Alt], key: "s"): System(ScreenReader), + (modifiers: [Super], key: "equal"): ZoomIn, + (modifiers: [Super], key: "minus"): ZoomOut, + (modifiers: [Super], key: "period"): ZoomIn, + (modifiers: [Super], key: "comma"): ZoomOut, + + (modifiers: [Super], key: "b"): System(WebBrowser), + (modifiers: [Super], key: "f"): System(HomeFolder), + (modifiers: [Super], key: "space"): System(InputSourceSwitch), + (modifiers: [Super], key: "t"): System(Terminal), + + (modifiers: [Super], key: "a"): System(AppLibrary), + (modifiers: [Super], key: "w"): System(WorkspaceOverview), + (modifiers: [Super], key: "slash"): System(Launcher), + (modifiers: [Super]): System(Launcher), + (modifiers: [Alt], key: "Tab"): System(WindowSwitcher), + (modifiers: [Alt, Shift], key: "Tab"): System(WindowSwitcherPrevious), + (modifiers: [Super], key: "Tab"): System(WindowSwitcher), + (modifiers: [Super, Shift], key: "Tab"): System(WindowSwitcherPrevious), + + (modifiers: [], key: "Print"): System(Screenshot), + (modifiers: [], key: "XF86AudioRaiseVolume"): System(VolumeRaise), + (modifiers: [], key: "XF86AudioLowerVolume"): System(VolumeLower), + (modifiers: [], key: "XF86AudioMute"): System(Mute), + (modifiers: [], key: "XF86AudioMicMute"): System(MuteMic), + (modifiers: [], key: "XF86MonBrightnessUp"): System(BrightnessUp), + (modifiers: [], key: "XF86MonBrightnessDown"): System(BrightnessDown), + (modifiers: [], key: "XF86AudioPlay"): System(PlayPause), + (modifiers: [], key: "XF86AudioPrev"): System(PlayPrev), + (modifiers: [], key: "XF86AudioNext"): System(PlayNext), + (modifiers: [], key: "XF86PowerOff"): System(PowerOff), + (modifiers: [], key: "XF86TouchpadToggle"): System(TouchpadToggle), + (modifiers: [Super, Ctrl], key: "XF86TouchpadToggle"): System(TouchpadToggle), + (modifiers: [], key: "XF86LaunchA"): System(WorkspaceOverview), +} diff --git a/data/tiling-exceptions.ron b/data/tiling-exceptions.ron new file mode 100644 index 0000000..2065c48 --- /dev/null +++ b/data/tiling-exceptions.ron @@ -0,0 +1,53 @@ +[ + // Any appid title only matching + ( + appid: ".*", + titles: [ + "Discord Updater", + ] + ), + // Empty appid title only matches + ( + appid: "", + titles: [ + "Steam", + "wl-clipboard", + ] + ), + + + // Appid matches + (appid: "Authy Desktop", titles: [".*"]), + (appid: "Com.github.amezin.ddterm", titles: [".*"]), + (appid: "Com.github.donadigo.eddy", titles: [".*"]), + (appid: "com.system76.CosmicFilesDialog", titles: [".*"]), + (appid: "com.system76.CosmicStoreDialog", titles: [".*"]), + (appid: "Enpass", titles: ["Enpass Assistant"]), + (appid: "Gjs", titles: ["Settings"]), + (appid: "Gnome-initial-setup", titles: [".*"]), + (appid: "Gnome-terminal", titles: ["Preferences - General"]), + (appid: "Guake", titles: [".*"]), + (appid: "Io.elementary.sideload", titles: [".*"]), + (appid: "KotatogramDesktop", titles: ["Media viewer"]), + (appid: "Mozilla VPN", titles: [".*"]), + (appid: "update-manager", titles: ["Software Updater"]), + (appid: "Solaar", titles: [".*"]), + (appid: "Steam", titles: ["^.*?(Guard|Login).*"]), + (appid: "TelegramDesktop", titles: ["Media viewer"]), + (appid: "Zotero", titles: ["Quick Format Citation"]), + (appid: "gjs", titles: [".*"]), + (appid: "gnome-screenshot", titles: [".*"]), + (appid: "ibus-.*", titles: [".*"]), + (appid: "jetbrains-toolbox", titles: [".*"]), + (appid: "jetbrains-webstorm", titles: ["Customize WebStorm", "License Activation", "Welcome to WebStorm"]), + (appid: "krunner", titles: [".*"]), + (appid: "pritunl", titles: [".*"]), + (appid: "re.sonny.Junction", titles: [".*"]), + (appid: "system76-driver", titles: [".*"]), + (appid: "tilda", titles: [".*"]), + (appid: "zoom", titles: [".*"]), + (appid: "Tor Browser", titles: [".*"]), + (appid: "^.*?action=join.*$", titles: [".*"]), + (appid: "^(slack|com.slack.Slack)", titles: ["^.*?(Huddle Preview).*"]), + (appid: "^(thunderbird|org.mozilla.thunderbird)(-esr|_esr)*", titles: ["^(Write:).*"]), +] diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..ef9f068 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +cosmic-comp (0.1) jammy; urgency=medium + + * Initial pre-release. + + -- Victoria Brekenfeld Wed, 09 Feb 2022 14:20:23 +0100 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..a3baf0e --- /dev/null +++ b/debian/control @@ -0,0 +1,35 @@ +Source: cosmic-comp +Section: x11 +Priority: optional +Maintainer: Victoria Brekenfeld +Build-Depends: + cargo, + cmake, + debhelper-compat (=10), + libegl1-mesa-dev, + libfontconfig-dev, + libgbm-dev, + libinput-dev, + libpixman-1-dev, + libseat-dev, + libsystemd-dev, + libudev-dev, + libwayland-dev, + libxcb1-dev, + libxkbcommon-dev, + libdisplay-info-dev, + rustc (>=1.57), +Standards-Version: 4.1.1 +Homepage: https://github.com/pop-os/cosmic-comp + +Package: cosmic-comp +Architecture: amd64 arm64 +Depends: + ${misc:Depends}, + ${shlibs:Depends}, + libegl1, + libwayland-server0, +Recommends: + cosmic-session, + libgl1-mesa-dri +Description: Wayland compositor of pop-os cosmic shell diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..85347b6 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,7 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: cosmic-comp +Upstream-Contact: Victoria Brekenfeld +Source: https://github.com/pop-os/cosmic-comp +Files: * +Copyright: System76 +License: GPL-3.0-only \ No newline at end of file diff --git a/debian/postinst b/debian/postinst new file mode 100644 index 0000000..f1901d7 --- /dev/null +++ b/debian/postinst @@ -0,0 +1,2 @@ +#!/bin/sh +set -x diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..5a26694 --- /dev/null +++ b/debian/rules @@ -0,0 +1,22 @@ +#!/usr/bin/make -f + +export sysconfdir = "/usr/share" +export VENDOR ?= 1 +CLEAN ?= 1 + +%: + dh $@ --with=systemd + +override_dh_auto_clean: +ifeq ($(CLEAN),1) + ischroot && make clean || make distclean +endif +ifeq ($(VENDOR),1) + ischroot || make vendor +endif + +override_dh_installinit: + dh_installinit -r + +override_dh_systemd_start: + dh_systemd_start -r \ No newline at end of file diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..9f67427 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (native) \ No newline at end of file diff --git a/debian/source/options b/debian/source/options new file mode 100644 index 0000000..97d96f6 --- /dev/null +++ b/debian/source/options @@ -0,0 +1,2 @@ +tar-ignore=target +tar-ignore=vendor \ No newline at end of file diff --git a/examples/customized-window-decorations.rs b/examples/customized-window-decorations.rs new file mode 100644 index 0000000..6b3baf8 --- /dev/null +++ b/examples/customized-window-decorations.rs @@ -0,0 +1,79 @@ +use cosmic::iced::{Color, Rectangle, Size}; +use cosmic::widget; +use cosmic::widget::canvas; +use cosmic_comp::hooks::{Decorations, Hooks}; +use cosmic_comp::shell::element::stack::{ + DefaultDecorations as DefaultStackDecorations, TAB_HEIGHT, +}; +use cosmic_comp::shell::element::window::{ + DefaultDecorations as DefaultWindowDecorations, SSD_HEIGHT, +}; +use std::sync::Arc; + +/// The struct implementing Decorations hook +#[derive(Debug)] +struct AddIndicator { + lower: Lower, + height: i32, +} + +/// An iced program drawing a circle, which we'll add to the window decorations +struct Circle { + radius: f32, + color: Color, +} + +impl + canvas::Program for Circle +{ + type State = (); + + fn draw( + &self, + _state: &(), + renderer: &Renderer, + _theme: &Theme, + bounds: Rectangle, + _cursor: cosmic::iced::mouse::Cursor, + ) -> Vec { + let bounds = bounds.size(); + let min = bounds.height.min(bounds.width); + let mut frame = canvas::Frame::new(renderer, Size::new(min, min)); + let circle = canvas::Path::circle(frame.center(), self.radius); + frame.fill(&circle, self.color); + vec![frame.into_geometry()] + } +} + +impl> + Decorations for AddIndicator +{ + fn view(&self, window: &Internal) -> cosmic::Element<'_, Message> { + let orig = self.lower.view(window); + widget::row([ + widget::column([canvas(Circle { + radius: (self.height as f32 / 2.) * 0.8, + color: Color::from_rgba(1.0, 0.0, 0.0, 1.0), + }) + .into()]) + .width(self.height as f32) + .into(), + orig, + ]) + .into() + } +} + +/// The customized cosmic-comp entrypoint +fn main() -> Result<(), Box> { + cosmic_comp::run(Hooks { + window_decorations: Some(Arc::new(AddIndicator { + height: SSD_HEIGHT, + lower: DefaultWindowDecorations, + })), + stack_decorations: Some(Arc::new(AddIndicator { + height: TAB_HEIGHT, + lower: DefaultStackDecorations, + })), + }) +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..6152e20 --- /dev/null +++ b/flake.lock @@ -0,0 +1,101 @@ +{ + "nodes": { + "crane": { + "locked": { + "lastModified": 1724974107, + "narHash": "sha256-69+1W0Ao5K9su569YUfUPANeN/Ea7aKu7xIZP1MSl9o=", + "owner": "ipetkov", + "repo": "crane", + "rev": "63396562b8e08efda3b3c66e32661b8a513055de", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "nix-filter": { + "locked": { + "lastModified": 1710156097, + "narHash": "sha256-1Wvk8UP7PXdf8bCCaEoMnOT1qe5/Duqgj+rL8sRQsSM=", + "owner": "numtide", + "repo": "nix-filter", + "rev": "3342559a24e85fc164b295c3444e8a139924675b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "nix-filter", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1725067332, + "narHash": "sha256-bMi5zhDwR6jdmN5mBHEu9gQQf9CibIEasA/6mc34Iek=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "192e7407cc66e2eccc3a6c5ad3834dd62fae3800", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "parts": { + "inputs": { + "nixpkgs-lib": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1725024810, + "narHash": "sha256-ODYRm8zHfLTH3soTFWE452ydPYz2iTvr9T8ftDMUQ3E=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "af510d4a62d071ea13925ce41c95e3dec816c01d", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "root": { + "inputs": { + "crane": "crane", + "nix-filter": "nix-filter", + "nixpkgs": "nixpkgs", + "parts": "parts", + "rust": "rust" + } + }, + "rust": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1724984647, + "narHash": "sha256-BC6MUq0CTdmAu/cueVcdWTI+S95s0mJcn19SoEgd7gU=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "87b6cffc276795b46ef544d7ed8d7fed6ad9c8e4", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..1fe2c57 --- /dev/null +++ b/flake.nix @@ -0,0 +1,116 @@ +{ + description = "Compositor for the COSMIC desktop environment"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + + parts.url = "github:hercules-ci/flake-parts"; + parts.inputs.nixpkgs-lib.follows = "nixpkgs"; + + crane.url = "github:ipetkov/crane"; + + rust.url = "github:oxalica/rust-overlay"; + rust.inputs.nixpkgs.follows = "nixpkgs"; + + nix-filter.url = "github:numtide/nix-filter"; + }; + + outputs = + inputs@{ + self, + nixpkgs, + parts, + crane, + rust, + nix-filter, + ... + }: + parts.lib.mkFlake { inherit inputs; } { + systems = [ + "aarch64-linux" + "x86_64-linux" + ]; + + perSystem = + { + self', + lib, + system, + ... + }: + let + pkgs = nixpkgs.legacyPackages.${system}.extend rust.overlays.default; + rust-toolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; + craneLib = (crane.mkLib pkgs).overrideToolchain rust-toolchain; + craneArgs = { + pname = "cosmic-comp"; + version = self.rev or "dirty"; + + src = nix-filter.lib.filter { + root = ./.; + include = [ + ./src + ./i18n.toml + ./Cargo.toml + ./Cargo.lock + ./resources + ./cosmic-comp-config + ]; + }; + + nativeBuildInputs = with pkgs; [ + pkg-config + autoPatchelfHook + cmake + ]; + + buildInputs = with pkgs; [ + wayland + systemd # For libudev + seatd # For libseat + libxkbcommon + libinput + mesa # For libgbm + fontconfig + stdenv.cc.cc.lib + pixman + libdisplay-info + ]; + + runtimeDependencies = with pkgs; [ + libglvnd # For libEGL + wayland # winit->wayland-sys wants to dlopen libwayland-egl.so + # for running in X11 + xorg.libX11 + xorg.libXcursor + xorg.libxcb + xorg.libXi + libxkbcommon + # for vulkan backend + vulkan-loader + ]; + }; + + cargoArtifacts = craneLib.buildDepsOnly craneArgs; + cosmic-comp = craneLib.buildPackage (craneArgs // { inherit cargoArtifacts; }); + in + { + apps.cosmic-comp = { + type = "app"; + program = lib.getExe self'.packages.default; + }; + + checks.cosmic-comp = cosmic-comp; + packages.default = cosmic-comp; + + devShells.default = craneLib.devShell { + LD_LIBRARY_PATH = lib.makeLibraryPath ( + __concatMap (d: d.runtimeDependencies) (__attrValues self'.checks) + ); + + # include build inputs + inputsFrom = [ cosmic-comp ]; + }; + }; + }; +} diff --git a/i18n.toml b/i18n.toml new file mode 100644 index 0000000..746887d --- /dev/null +++ b/i18n.toml @@ -0,0 +1,4 @@ +fallback_language = "en" + +[fluent] +assets_dir = "resources/i18n" \ No newline at end of file diff --git a/resources/cursor.rgba b/resources/cursor.rgba new file mode 100644 index 0000000000000000000000000000000000000000..729c1cc466b85bb2b1e9923d4a0bb7db559df4be GIT binary patch literal 16384 zcmeI1O-NKx6vxx~)X~vYbjq=Ws3dTaLKuP~m=FrJuvI9bL!v{}AX>ICKS>b^7eP@g zaT_j75OiUpE)+i&I*Qt0f{G}SV4*Xkr~kS51{afAtKOS?;mn;m^9KC=-nk#|y0NiQ zw6wHz{|s7TVPP2eq14=N_X8sGPfkv{z->Z=l!K|MDaYaAp%>vFA0K}N{>XxYf{<~5 z`!4vsUN6dk{P6J<4s_Ys*&*S8`N8e2udgTEe|A9A)6;cu5T(=UH19y+uc)Y?q@*O8 znVDe)e)P}az*JmZ9FAVIP2qPsodig-SS&O#F~JUKc6PQIIfKn+)2su9zpAQA@Uw&X z_;_}3c;uiFIcs8Kq9z>#<7Wp62?;bdHpU7Lutv5bXDliz(wu|e@v{TWe>5^O!VYL| zZmu13GZPyFn_Y&O&2;Gn1jIOssml$n{S z2?wX)X9rARH_#EWAc87NL3vr{!k{R;!iz`}>6hJTrA7XT?5IhhDYqZ}gwvKwzgJ8 zO-&6oH#gJn?yjhVm6eqb;J2owrRmYDss-ccxz1!V1@?n_ygsC2$uE_al>rCJbJ70({!cJoN7xWN?m6NSF$gniM^$s-aemxa!M?q{ zy}7fq^KEZ$kD{Za0}e_{O9|&hG(SJj<2?$&tQ>q!pakBkrz!mW+>h~0o12?ov$C=r zot>R6ETg@>J;2Y;17Ihg&-cBlsVNtfM)i;e|4W&l&;OWpZEfuf*0MXe@-iZIYirBB zzP?T=DJcO5d3kw!DSLR2b)V~A)n`OU_n%f*SKS#I8TY_*5n)C|mzS5{W!Bc#R>9Bf z0`fcua{7nE;kb#S7%3o?{NUifh#Tf0L8eE&eVNXJI#Z-Q$Lu z_ux#}h5ca;zDq?Y>mc50uNjR-u7S9|zP=aT-QBP7J{xffYa#Dqc>6-dj literal 0 HcmV?d00001 diff --git a/resources/i18n/af/cosmic_comp.ftl b/resources/i18n/af/cosmic_comp.ftl new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n/ar/cosmic_comp.ftl b/resources/i18n/ar/cosmic_comp.ftl new file mode 100644 index 0000000..3fc78b1 --- /dev/null +++ b/resources/i18n/ar/cosmic_comp.ftl @@ -0,0 +1,28 @@ +a11y-zoom-move-continuously = يتحرك العرض بإستمرار مع المؤشر +a11y-zoom-move-onedge = يتحرك العرض عندما يصل المؤشر إلي الحافة +a11y-zoom-move-centered = يتحرك العرض لإبقاء المؤشر في مركز الشاشة +a11y-zoom-settings = إعدادات المكبر... +grow-window = وَسِّع +shrink-window = قلِّص +swap-windows = بدِّل النافذتين +stack-windows = كدِّس النوافذ +unknown-keybinding = <غير محدد> +window-menu-minimize = صغِّر +window-menu-maximize = كبِّر +window-menu-fullscreen = ملء الشاشة +window-menu-tiled = نافذة عائمة +window-menu-screenshot = التقط لقطة شاشة +window-menu-move = حرِّك +window-menu-resize = تغيير الحجم +window-menu-move-prev-workspace = حرِّك إلى مساحة العمل السابقة +window-menu-move-next-workspace = حرِّك إلى مساحة العمل التالية +window-menu-stack = أنشئ كدسة نوافذ +window-menu-unstack-all = ألغِ تكديس النوافذ +window-menu-unstack = ألغِ تكديس النافذة +window-menu-sticky = نافذة ثابتة +window-menu-close = أغلِق +window-menu-close-all = أغلِق جميع النوافذ +window-menu-resize-edge-top = الأعلى +window-menu-resize-edge-left = الشمال +window-menu-resize-edge-right = اليمين +window-menu-resize-edge-bottom = الأسفل diff --git a/resources/i18n/be/cosmic_comp.ftl b/resources/i18n/be/cosmic_comp.ftl new file mode 100644 index 0000000..b9b21d5 --- /dev/null +++ b/resources/i18n/be/cosmic_comp.ftl @@ -0,0 +1,28 @@ +a11y-zoom-move-continuously = Прагляд рухаецца бесперапынна за указальнікам +a11y-zoom-move-onedge = Прагляд рухаецца, калі указальнік дасягае краю +a11y-zoom-move-centered = Прагляд рухаецца, каб утрымліваць указальнік у цэнтры +a11y-zoom-settings = Налады лупы... +grow-window = Павялічыць +shrink-window = Паменшыць +swap-windows = Памяняць вокны месцамі +stack-windows = Згрупаваць вокны +unknown-keybinding = <не зададзена> +window-menu-minimize = Згарнуць +window-menu-maximize = Разгарнуць +window-menu-fullscreen = На ўвесь экран +window-menu-tiled = Плавае акно +window-menu-screenshot = Зрабіць скрыншот +window-menu-move = Перамясціць +window-menu-resize = Змяніць памер +window-menu-move-prev-workspace = Перамясціць на папярэднюю працоўную прастору +window-menu-move-next-workspace = Перамясціць на наступную працоўную прастору +window-menu-stack = Стварыць групу вокнаў +window-menu-unstack-all = Разгрупаваць вокны +window-menu-unstack = Разгрупаваць акно +window-menu-sticky = Ліпкае акно +window-menu-close = Закрыць +window-menu-close-all = Закрыць усе вокны +window-menu-resize-edge-top = Уверх +window-menu-resize-edge-left = Улева +window-menu-resize-edge-right = Управа +window-menu-resize-edge-bottom = Уніз diff --git a/resources/i18n/bg/cosmic_comp.ftl b/resources/i18n/bg/cosmic_comp.ftl new file mode 100644 index 0000000..12c737c --- /dev/null +++ b/resources/i18n/bg/cosmic_comp.ftl @@ -0,0 +1,28 @@ +window-menu-close = Затваряне +window-menu-close-all = Затваряне на всички прозорци +window-menu-resize = Преоразмеряване +window-menu-move = Преместване +window-menu-fullscreen = На цял екран +window-menu-maximize = Максимизиране +window-menu-minimize = Минимизиране +a11y-zoom-settings = Настройки на лупата... +swap-windows = Разменяне на прозорци +window-menu-move-prev-workspace = Преместване в предишното работно пространство +window-menu-move-next-workspace = Преместване в следващото работно пространство +window-menu-resize-edge-top = Горе +window-menu-resize-edge-left = Ляво +window-menu-resize-edge-right = Дясно +window-menu-resize-edge-bottom = Долу +window-menu-screenshot = Снимане на екрана +a11y-zoom-move-continuously = Изгледът се движи непрекъснато с показалеца +a11y-zoom-move-onedge = Изгледът се движи, когато показалецът достигне края +a11y-zoom-move-centered = Изгледът се премества да запази показалеца центриран +grow-window = Уголемяване +shrink-window = Свиване +stack-windows = Подреждане на прозорците на куп +unknown-keybinding = +window-menu-tiled = Плаващ прозорец +window-menu-stack = Създаване на куп от прозорци +window-menu-unstack-all = Разделяне на прозорците от купа +window-menu-unstack = Разделяне на прозореца от купа +window-menu-sticky = Лепкав прозорец diff --git a/resources/i18n/bn/cosmic_comp.ftl b/resources/i18n/bn/cosmic_comp.ftl new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n/ca/cosmic_comp.ftl b/resources/i18n/ca/cosmic_comp.ftl new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n/cs/cosmic_comp.ftl b/resources/i18n/cs/cosmic_comp.ftl new file mode 100644 index 0000000..72c7c33 --- /dev/null +++ b/resources/i18n/cs/cosmic_comp.ftl @@ -0,0 +1,28 @@ +a11y-zoom-move-continuously = Zobrazení se pohybuje plynule s ukazatelem +a11y-zoom-move-onedge = Zobrazení se pohybuje, když ukazatel dosáhne okraje +a11y-zoom-move-centered = Zobrazení se pohybuje tak, aby ukazatel zůstal uprostřed +a11y-zoom-settings = Nastavení lupy... +grow-window = Zvětšit +shrink-window = Zmenšit +swap-windows = Prohodit okna +stack-windows = Seskupit okna +unknown-keybinding = +window-menu-minimize = Minimalizovat +window-menu-maximize = Maximalizovat +window-menu-tiled = Plovoucí okno +window-menu-screenshot = Pořídit snímek obrazovky +window-menu-move = Přesunout +window-menu-resize = Změnit velikost +window-menu-move-prev-workspace = Přesunout na předchozí pracovní plochu +window-menu-move-next-workspace = Přesunout na další pracovní plochu +window-menu-stack = Vytvořit zásobník oken +window-menu-unstack-all = Oddělit okna +window-menu-unstack = Oddělit okno +window-menu-sticky = Ponechat okno vždy nahoře +window-menu-close = Zavřít +window-menu-close-all = Zavřít všechna okna +window-menu-resize-edge-top = Nahoře +window-menu-resize-edge-left = Vlevo +window-menu-resize-edge-right = Vpravo +window-menu-resize-edge-bottom = Dole +window-menu-fullscreen = Režim celé obrazovky diff --git a/resources/i18n/da/cosmic_comp.ftl b/resources/i18n/da/cosmic_comp.ftl new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n/de/cosmic_comp.ftl b/resources/i18n/de/cosmic_comp.ftl new file mode 100644 index 0000000..4ca5d5e --- /dev/null +++ b/resources/i18n/de/cosmic_comp.ftl @@ -0,0 +1,28 @@ +a11y-zoom-move-continuously = Ansicht bewegt sich kontinuierlich mit dem Cursor +a11y-zoom-move-onedge = Ansicht bewegt sich, sobald der Cursor den Rand erreicht +a11y-zoom-move-centered = Ansicht bewegt sich, um den Cursor zentiert zu halten +a11y-zoom-settings = Vergrößerungsglas-Einstellungen ... +grow-window = Vergrößern +shrink-window = Verkleinern +swap-windows = Fenster tauschen +stack-windows = Fenster stapeln +unknown-keybinding = +window-menu-minimize = Minimieren +window-menu-maximize = Maximieren +window-menu-tiled = Fenster schweben lassen +window-menu-screenshot = Bildschirmfoto machen +window-menu-move = Verschieben +window-menu-resize = Größe anpassen +window-menu-move-prev-workspace = Zur vorherigen Arbeitsfläche verschieben +window-menu-move-next-workspace = Zur nächsten Arbeitsfläche verschieben +window-menu-stack = Fensterstapel erstellen +window-menu-unstack-all = Fensterstapel auflösen +window-menu-unstack = Fenster aus Stapel lösen +window-menu-sticky = Klebendes Fenster +window-menu-close = Schließen +window-menu-close-all = Alle Fenster schließen +window-menu-resize-edge-top = Oben +window-menu-resize-edge-left = Links +window-menu-resize-edge-right = Rechts +window-menu-resize-edge-bottom = Unten +window-menu-fullscreen = Vollbild diff --git a/resources/i18n/el/cosmic_comp.ftl b/resources/i18n/el/cosmic_comp.ftl new file mode 100644 index 0000000..85f3eed --- /dev/null +++ b/resources/i18n/el/cosmic_comp.ftl @@ -0,0 +1,28 @@ +a11y-zoom-move-continuously = Η προβολή μετακινείται συνεχώς με τον κέρσορα +window-menu-minimize = Ελαχιστοποίηση +window-menu-maximize = Μεγιστοποίηση +window-menu-fullscreen = Πλήρης οθόνη +window-menu-screenshot = Λήψη στιγμιότυπου οθόνης +window-menu-move = Μετακίνηση +window-menu-resize = Αλλαγή μεγέθους +window-menu-move-prev-workspace = Μετακίνηση στον προηγούμενο χώρο εργασίας +window-menu-move-next-workspace = Μετακίνηση στον επόμενο χώρο εργασίας +window-menu-stack = Δημιουργία στοίβας παραθύρων +window-menu-unstack-all = Κατάργηση στοίβας παραθύρων +window-menu-close = Κλείσιμο +window-menu-close-all = Κλείσιμο όλων των παραθύρων +window-menu-resize-edge-top = Πάνω +window-menu-resize-edge-left = Αριστερά +window-menu-resize-edge-right = Δεξιά +window-menu-resize-edge-bottom = Κάτω +shrink-window = Συρρίκνωση +swap-windows = Εναλλαγή παραθύρων +a11y-zoom-settings = Ρυθμίσεις μεγεθυντικού φακού... +a11y-zoom-move-onedge = Η προβολή μετακινείται όταν ο κέρσορας φτάνει στην άκρη +a11y-zoom-move-centered = Η προβολή μετακινείται για να διατηρεί τον κέρσορα στο κέντρο +grow-window = Ανάπτυξη +stack-windows = Στοίβαξη παραθύρων +window-menu-sticky = Καρφιτσωμένο παράθυρο +unknown-keybinding = <δεν έχει οριστεί> +window-menu-unstack = Κατάργηση στοίβας παραθύρου +window-menu-tiled = Αιώρηση παραθύρου diff --git a/resources/i18n/en-GB/cosmic_comp.ftl b/resources/i18n/en-GB/cosmic_comp.ftl new file mode 100644 index 0000000..0bd9c20 --- /dev/null +++ b/resources/i18n/en-GB/cosmic_comp.ftl @@ -0,0 +1,20 @@ +a11y-zoom-move-continuously = View moves continuously with pointer +a11y-zoom-move-onedge = View moves when pointer reaches edge +a11y-zoom-move-centered = View moves to keep pointer centred +a11y-zoom-settings = Magnifier settings... +grow-window = Grow +shrink-window = Shrink +swap-windows = Swap Windows +stack-windows = Stack Windows +unknown-keybinding = +window-menu-minimize = Minimise +window-menu-maximize = Maximise +window-menu-fullscreen = Fullscreen +window-menu-tiled = Float window +window-menu-screenshot = Take screenshot +window-menu-move = Move +window-menu-resize = Resize +window-menu-move-prev-workspace = Move to previous workspace +window-menu-move-next-workspace = Move to next workspace +window-menu-stack = Create window stack +window-menu-unstack-all = Unstack windows diff --git a/resources/i18n/en/cosmic_comp.ftl b/resources/i18n/en/cosmic_comp.ftl new file mode 100644 index 0000000..86d9861 --- /dev/null +++ b/resources/i18n/en/cosmic_comp.ftl @@ -0,0 +1,28 @@ +a11y-zoom-move-continuously = View moves continuously with pointer +a11y-zoom-move-onedge = View moves when pointer reaches edge +a11y-zoom-move-centered = View moves to keep pointer centered +a11y-zoom-settings = Magnifier settings... +grow-window = Grow +shrink-window = Shrink +swap-windows = Swap Windows +stack-windows = Stack Windows +unknown-keybinding = +window-menu-minimize = Minimize +window-menu-maximize = Maximize +window-menu-fullscreen = Fullscreen +window-menu-tiled = Float window +window-menu-screenshot = Take screenshot +window-menu-move = Move +window-menu-resize = Resize +window-menu-move-prev-workspace = Move to previous workspace +window-menu-move-next-workspace = Move to next workspace +window-menu-stack = Create window stack +window-menu-unstack-all = Unstack windows +window-menu-unstack = Unstack window +window-menu-sticky = Sticky window +window-menu-close = Close +window-menu-close-all = Close all windows +window-menu-resize-edge-top = Top +window-menu-resize-edge-left = Left +window-menu-resize-edge-right = Right +window-menu-resize-edge-bottom = Bottom \ No newline at end of file diff --git a/resources/i18n/eo/cosmic_comp.ftl b/resources/i18n/eo/cosmic_comp.ftl new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n/es-419/cosmic_comp.ftl b/resources/i18n/es-419/cosmic_comp.ftl new file mode 100644 index 0000000..7e741ef --- /dev/null +++ b/resources/i18n/es-419/cosmic_comp.ftl @@ -0,0 +1,28 @@ +grow-window = Expandir +shrink-window = Disminuir +swap-windows = Intercambiar ventanas +stack-windows = Apilar ventanas +unknown-keybinding = +window-menu-minimize = Minimizar +window-menu-maximize = Maximizar +window-menu-tiled = Ventana flotante +window-menu-screenshot = Tomar captura de pantalla +window-menu-move = Mover +window-menu-resize = Redimensionar +window-menu-move-prev-workspace = Mover al espacio de trabajo anterior +window-menu-move-next-workspace = Mover al espacio de trabajo siguiente +window-menu-stack = Crear pila de ventanas +window-menu-unstack-all = Desapilar ventanas +window-menu-unstack = Desapilar ventana +window-menu-sticky = Ventana siempre visible +window-menu-close = Cerrar +window-menu-close-all = Cerrar todas las ventanas +window-menu-resize-edge-top = Arriba +window-menu-resize-edge-left = Izquierda +window-menu-resize-edge-right = Derecha +window-menu-resize-edge-bottom = Abajo +a11y-zoom-move-continuously = La vista se mueve continuamente con el puntero +a11y-zoom-move-onedge = La vista se mueve cuando el puntero alcanza el borde +a11y-zoom-move-centered = La vista se mueve para mantener el puntero centrado +a11y-zoom-settings = Configuración de la ampliación de pantalla... +window-menu-fullscreen = Pantalla completa diff --git a/resources/i18n/es-MX/cosmic_comp.ftl b/resources/i18n/es-MX/cosmic_comp.ftl new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n/es/cosmic_comp.ftl b/resources/i18n/es/cosmic_comp.ftl new file mode 100644 index 0000000..29f4d1f --- /dev/null +++ b/resources/i18n/es/cosmic_comp.ftl @@ -0,0 +1,28 @@ +a11y-zoom-move-continuously = La vista se mueve siguiendo el movimiento del puntero +a11y-zoom-move-onedge = La vista se mueve cuando el puntero alcanza un borde +a11y-zoom-move-centered = La vista se mueve manteniendo el puntero centrado +a11y-zoom-settings = Configuración de la Lupa... +grow-window = Agrandar +shrink-window = Disminuir +swap-windows = Intercambiar ventanas +stack-windows = Apilar ventanas +unknown-keybinding = +window-menu-minimize = Minimizar +window-menu-maximize = Maximizar +window-menu-tiled = Ventana flotante +window-menu-screenshot = Tomar captura de pantalla +window-menu-move = Mover +window-menu-resize = Redimensionar +window-menu-move-prev-workspace = Mover al espacio de trabajo anterior +window-menu-move-next-workspace = Mover al espacio de trabajo siguiente +window-menu-stack = Crear pila de ventanas +window-menu-unstack-all = Dispersar ventanas +window-menu-unstack = Dispersar ventana +window-menu-sticky = Ventana siempre visible +window-menu-close = Cerrar +window-menu-close-all = Cerrar todas las ventanas +window-menu-resize-edge-top = Arriba +window-menu-resize-edge-left = Izquierda +window-menu-resize-edge-right = Derecha +window-menu-resize-edge-bottom = Abajo +window-menu-fullscreen = Pantalla completa diff --git a/resources/i18n/et/cosmic_comp.ftl b/resources/i18n/et/cosmic_comp.ftl new file mode 100644 index 0000000..5c27cc2 --- /dev/null +++ b/resources/i18n/et/cosmic_comp.ftl @@ -0,0 +1,11 @@ +window-menu-sticky = Kleepuv aken +window-menu-close = Sulge +window-menu-close-all = Sulge kõik aknad +window-menu-screenshot = Tee ekraanitõmmis +window-menu-move = Liiguta +window-menu-resize = Muuda suurust +window-menu-move-prev-workspace = Tõsta eelmisele tööalale +window-menu-move-next-workspace = Tõsta järgmisele tööalale +window-menu-minimize = Minimeeri +window-menu-maximize = Maksimeeri +window-menu-fullscreen = Täisekraanivaade diff --git a/resources/i18n/eu/cosmic_comp.ftl b/resources/i18n/eu/cosmic_comp.ftl new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n/fa/cosmic_comp.ftl b/resources/i18n/fa/cosmic_comp.ftl new file mode 100644 index 0000000..353dacc --- /dev/null +++ b/resources/i18n/fa/cosmic_comp.ftl @@ -0,0 +1,28 @@ +a11y-zoom-move-continuously = نمای بزرگنمایی به طور مداوم با نشانگر حرکت می‌کند +a11y-zoom-move-onedge = نمای بزرگنمایی با رسیدن نشانگر به لبه حرکت می‌کند +a11y-zoom-move-centered = نمای بزرگنمایی برای نگه داشتن نشانگر در مرکز حرکت می‌کند +a11y-zoom-settings = تنظیمات ذره‌بین... +grow-window = بزرگ کردن +shrink-window = کوچک کردن +swap-windows = جابجایی پنجره‌ها +stack-windows = روی هم چیدن پنجره‌ها +unknown-keybinding = <تنظیم‌نشده> +window-menu-minimize = کوچک‌سازی +window-menu-maximize = بزرگ‌سازی +window-menu-fullscreen = تمام‌صفحه +window-menu-tiled = شناور کردن پنجره +window-menu-screenshot = گرفتن اسکرین‌شات +window-menu-move = جابجایی +window-menu-resize = تغییر اندازه +window-menu-move-prev-workspace = انتقال به محیط کاری قبلی +window-menu-move-next-workspace = انتقال به محیط کاری بعدی +window-menu-stack = ایجاد چینش پنجره‌ها +window-menu-unstack-all = برچیدن پنجره‌ها +window-menu-unstack = برچیدن پنجره‌ +window-menu-sticky = پنجره چسبان +window-menu-close = بستن +window-menu-close-all = بستن همه پنجره‌ها +window-menu-resize-edge-top = بالا +window-menu-resize-edge-left = چپ +window-menu-resize-edge-right = راست +window-menu-resize-edge-bottom = پایین diff --git a/resources/i18n/fi/cosmic_comp.ftl b/resources/i18n/fi/cosmic_comp.ftl new file mode 100644 index 0000000..8055a3f --- /dev/null +++ b/resources/i18n/fi/cosmic_comp.ftl @@ -0,0 +1,28 @@ +a11y-zoom-move-continuously = Näkymä seuraa osoitinta +a11y-zoom-move-onedge = Näkymä liikkuu osoittimen osuessa reunaan +a11y-zoom-move-centered = Näkymä liikkuu pitäen osoittimen keskellä +a11y-zoom-settings = Suurennuslasin asetukset... +window-menu-minimize = Pienennä +window-menu-maximize = Suurenna +window-menu-fullscreen = Koko näyttö +window-menu-screenshot = Ota kuvakaappaus +window-menu-move = Siirrä +window-menu-resize = Muuta kokoa +window-menu-move-prev-workspace = Siirrä edelliseen työtilaan +window-menu-move-next-workspace = Siirrä seuraavaan työtilaan +window-menu-stack = Luo ikkunapino +window-menu-unstack-all = Poista ikkunat pinosta +window-menu-unstack = Poista ikkuna pinosta +window-menu-close = Sulje +window-menu-close-all = Sulje kaikki ikkunat +swap-windows = Vaihda ikkunoita +unknown-keybinding = +window-menu-tiled = Leijuva ikkuna +window-menu-sticky = Tahmea ikkuna +grow-window = Kasvata +shrink-window = Kutista +stack-windows = Pinoa ikkunat +window-menu-resize-edge-top = Ylös +window-menu-resize-edge-left = Vasemmalle +window-menu-resize-edge-right = Oikealle +window-menu-resize-edge-bottom = Alas diff --git a/resources/i18n/fr/cosmic_comp.ftl b/resources/i18n/fr/cosmic_comp.ftl new file mode 100644 index 0000000..053ccf5 --- /dev/null +++ b/resources/i18n/fr/cosmic_comp.ftl @@ -0,0 +1,28 @@ +grow-window = Agrandir +shrink-window = Réduire +swap-windows = Basculer les fenêtres +stack-windows = Regrouper les fenêtres +unknown-keybinding = +window-menu-minimize = Minimiser +window-menu-maximize = Maximiser +window-menu-tiled = Fenêtre flottante +window-menu-screenshot = Prendre une capture d'écran +window-menu-move = Déplacer +window-menu-resize = Redimensionner +window-menu-move-prev-workspace = Déplacer vers l'espace de travail précédent +window-menu-move-next-workspace = Déplacer vers l'espace de travail suivant +window-menu-stack = Créer un regroupement de fenêtres +window-menu-unstack-all = Dégrouper toutes les fenêtres +window-menu-unstack = Dégrouper la fenêtre +window-menu-sticky = Fenêtre épinglée +window-menu-close = Fermer +window-menu-close-all = Fermer toutes les fenêtres +window-menu-resize-edge-top = Haut +window-menu-resize-edge-left = Gauche +window-menu-resize-edge-right = Droite +window-menu-resize-edge-bottom = Bas +window-menu-fullscreen = Plein écran +a11y-zoom-move-onedge = La vue se déplace lorsque le curseur atteint le bord +a11y-zoom-move-centered = La vue se déplace pour maintenir le curseur centré +a11y-zoom-settings = Paramètres de la loupe... +a11y-zoom-move-continuously = La vue se déplace continuellement avec le curseur diff --git a/resources/i18n/fy/cosmic_comp.ftl b/resources/i18n/fy/cosmic_comp.ftl new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n/ga/cosmic_comp.ftl b/resources/i18n/ga/cosmic_comp.ftl new file mode 100644 index 0000000..f17cc3d --- /dev/null +++ b/resources/i18n/ga/cosmic_comp.ftl @@ -0,0 +1,28 @@ +a11y-zoom-move-continuously = Bogann an radharc go leanúnach leis an pointeoir +a11y-zoom-move-onedge = Bogann an radharc nuair a shroicheann an pointeoir an imeall +a11y-zoom-move-centered = Féach ar ghluaiseachtaí chun an pointeoir a choinneáil i lár an aonaigh +a11y-zoom-settings = Socruithe formhéadaitheora... +grow-window = Fás +shrink-window = Laghdaigh +swap-windows = Malartaigh fuinneoga +stack-windows = Fuinneoga Cruachta +unknown-keybinding = +window-menu-minimize = Íoslaghdaigh +window-menu-maximize = Uasmhéadaigh +window-menu-fullscreen = Lánscáileán +window-menu-tiled = Fuinneog snámhach +window-menu-screenshot = Tóg seat scáileán +window-menu-move = Bog +window-menu-resize = Athraigh méid +window-menu-move-prev-workspace = Bog go dtí an spás oibre roimhe seo +window-menu-move-next-workspace = Bog go dtí an chéad spás oibre eile +window-menu-stack = Cruigh cruach fuinneoige +window-menu-unstack-all = Díchruachadh fuinneoga +window-menu-unstack = Fuinneog a dhíchruachadh +window-menu-sticky = Fuinneog ghreamaitheach +window-menu-close = Dún +window-menu-close-all = Dún na fuinneoga go léir +window-menu-resize-edge-top = Barr +window-menu-resize-edge-left = Ar chlé +window-menu-resize-edge-right = Ar dheis +window-menu-resize-edge-bottom = Bun diff --git a/resources/i18n/gd/cosmic_comp.ftl b/resources/i18n/gd/cosmic_comp.ftl new file mode 100644 index 0000000..b993999 --- /dev/null +++ b/resources/i18n/gd/cosmic_comp.ftl @@ -0,0 +1,23 @@ +grow-window = Fàs +shrink-window = Seac +swap-windows = Mùth uinneagan +stack-windows = Stac uinneagan +unknown-keybinding = +window-menu-minimize = Fìor-lùghdaich +window-menu-maximize = Làn-mheudaich +window-menu-tiled = Uinneag fleòdraidh +window-menu-screenshot = Gabh dealbh-sgrìn +window-menu-move = Gluais +window-menu-resize = Ath-mheudachadh +window-menu-move-prev-workspace = Gluais gu àite-obrach roimhe +window-menu-move-next-workspace = Gluais chun ath àite-obrach +window-menu-stack = Cruthaich stac uinneig +window-menu-unstack-all = Thoir air falbh na h-uinneagan +window-menu-unstack = Thoir air falbh an uinneag +window-menu-sticky = Uinneag steigeach +window-menu-close = Dùin +window-menu-close-all = Dùin a h-uile uinneag +window-menu-resize-edge-top = Bàrr +window-menu-resize-edge-left = Clì +window-menu-resize-edge-right = Deas +window-menu-resize-edge-bottom = Bun diff --git a/resources/i18n/gu/cosmic_comp.ftl b/resources/i18n/gu/cosmic_comp.ftl new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n/he/cosmic_comp.ftl b/resources/i18n/he/cosmic_comp.ftl new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n/hi/cosmic_comp.ftl b/resources/i18n/hi/cosmic_comp.ftl new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n/hr/cosmic_comp.ftl b/resources/i18n/hr/cosmic_comp.ftl new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n/hu/cosmic_comp.ftl b/resources/i18n/hu/cosmic_comp.ftl new file mode 100644 index 0000000..8175592 --- /dev/null +++ b/resources/i18n/hu/cosmic_comp.ftl @@ -0,0 +1,28 @@ +a11y-zoom-move-continuously = A nézet folyamatosan követi a mutatót +a11y-zoom-move-onedge = A nézet csak akkor mozog, ha a mutató eléri a szélét +a11y-zoom-move-centered = A nézet úgy mozog, hogy a mutató középen maradjon +a11y-zoom-settings = Nagyító beállításai… +grow-window = Növelés +shrink-window = Zsugorítás +swap-windows = Ablakok felcserélése +stack-windows = Ablakok egymásra helyezése +unknown-keybinding = +window-menu-minimize = Minimalizálás +window-menu-maximize = Maximalizálás +window-menu-fullscreen = Teljes képernyő +window-menu-tiled = Ablak lebegtetése +window-menu-screenshot = Képernyőkép készítése +window-menu-move = Mozgatás +window-menu-resize = Átméretezés +window-menu-move-prev-workspace = Áthelyezés az előző munkaterületre +window-menu-move-next-workspace = Áthelyezés a következő munkaterületre +window-menu-stack = Ablakhalom létrehozása +window-menu-unstack-all = Ablakok szétválasztása +window-menu-unstack = Ablak szétválasztása +window-menu-sticky = Ragadós ablak +window-menu-close = Bezárás +window-menu-close-all = Összes ablak bezárása +window-menu-resize-edge-top = Felső +window-menu-resize-edge-left = Bal +window-menu-resize-edge-right = Jobb +window-menu-resize-edge-bottom = Alsó diff --git a/resources/i18n/id/cosmic_comp.ftl b/resources/i18n/id/cosmic_comp.ftl new file mode 100644 index 0000000..9742986 --- /dev/null +++ b/resources/i18n/id/cosmic_comp.ftl @@ -0,0 +1,28 @@ +a11y-zoom-move-continuously = Tampilan bergerak terus menerus dengan penunjuk +a11y-zoom-move-onedge = Tampilan bergerak saat penunjuk mencapai tepi +a11y-zoom-move-centered = Tampilan bergerak untuk menjaga penunjuk tetap di tengah +a11y-zoom-settings = Pengaturan kaca pembesar... +unknown-keybinding = +window-menu-resize-edge-top = Atas +window-menu-resize-edge-left = Kiri +window-menu-resize-edge-right = Kanan +window-menu-resize-edge-bottom = Bawah +window-menu-close = Tutup +window-menu-close-all = Tutup semua jendela +grow-window = Perbesar +shrink-window = Perkecil +swap-windows = Tukarkan Jendela +stack-windows = Tumpukkan Jendela +window-menu-screenshot = Ambil tangkapan layar +window-menu-move = Pindahkan +window-menu-resize = Ukur ulang +window-menu-minimize = Perkecil +window-menu-maximize = Perbesar +window-menu-fullscreen = Layar penuh +window-menu-tiled = Jendela apung +window-menu-move-prev-workspace = Pindahkan ke ruang kerja sebelumnya +window-menu-move-next-workspace = Pindahkan ke ruang kerja selanjutnya +window-menu-stack = Buat tumpukan jendela +window-menu-sticky = Jendela lengket +window-menu-unstack-all = Bongkar tumpukan jendela +window-menu-unstack = Bongkar tumpukan jendela diff --git a/resources/i18n/ie/cosmic_comp.ftl b/resources/i18n/ie/cosmic_comp.ftl new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n/is/cosmic_comp.ftl b/resources/i18n/is/cosmic_comp.ftl new file mode 100644 index 0000000..3fc523c --- /dev/null +++ b/resources/i18n/is/cosmic_comp.ftl @@ -0,0 +1,28 @@ +a11y-zoom-move-continuously = Skjásýn færist stöðugt með bendli +a11y-zoom-move-onedge = Skjásýn færist þegar bendill kemur að brún +a11y-zoom-move-centered = Skjásýn færist til að halda bendli í miðju +a11y-zoom-settings = Stillingar stækkunarglers... +stack-windows = Stafla gluggum +window-menu-minimize = Fela +window-menu-maximize = Fullstækka +window-menu-fullscreen = Fylla skjá +window-menu-tiled = Fleyta glugga +window-menu-screenshot = Taka skjámynd +window-menu-move = Færa +window-menu-resize = Breyta stærð +window-menu-move-prev-workspace = Færa á fyrra vinnusvæði +window-menu-move-next-workspace = Færa á næsta vinnusvæði +window-menu-stack = Búa til gluggastafla +window-menu-unstack-all = Afstafla gluggum +window-menu-unstack = Afstafla glugga +window-menu-close = Loka +window-menu-close-all = Loka öllum gluggum +window-menu-resize-edge-top = Efst +window-menu-resize-edge-left = Vinstri +window-menu-resize-edge-right = Hægri +window-menu-resize-edge-bottom = Neðst +swap-windows = Skipta út gluggum +grow-window = Víkka út +shrink-window = Draga saman +window-menu-sticky = Klístraður gluggi +unknown-keybinding = diff --git a/resources/i18n/it/cosmic_comp.ftl b/resources/i18n/it/cosmic_comp.ftl new file mode 100644 index 0000000..b1eb56c --- /dev/null +++ b/resources/i18n/it/cosmic_comp.ftl @@ -0,0 +1,28 @@ +grow-window = Crescere +shrink-window = Riduci +swap-windows = Scambia le finestre +stack-windows = Sovrapponi le finestre +unknown-keybinding = +window-menu-minimize = Minimizza +window-menu-maximize = Massimizza +window-menu-tiled = Finestra flottante +window-menu-screenshot = Scatta uno screenshot +window-menu-move = Sposta +window-menu-resize = Ridimensiona +window-menu-move-prev-workspace = Sposta all'area di lavoro precedente +window-menu-move-next-workspace = Sposta all'area di lavoro successiva +window-menu-stack = Crea una pila di finestre +window-menu-unstack-all = Disimpila le finestre +window-menu-unstack = Disimpila la finestra +window-menu-sticky = Finestra fissa +window-menu-close = Chiudi +window-menu-close-all = Chiudi tutte le finestre +window-menu-resize-edge-top = In alto +window-menu-resize-edge-left = Sinistra +window-menu-resize-edge-right = Destra +window-menu-resize-edge-bottom = In basso +window-menu-fullscreen = Schermo intero +a11y-zoom-settings = Impostazioni lente d'ingrandimento... +a11y-zoom-move-continuously = Segue sempre il cursorse +a11y-zoom-move-centered = Mantiene il cursore centrato +a11y-zoom-move-onedge = Segue il cursore ai bordi diff --git a/resources/i18n/ja/cosmic_comp.ftl b/resources/i18n/ja/cosmic_comp.ftl new file mode 100644 index 0000000..389359e --- /dev/null +++ b/resources/i18n/ja/cosmic_comp.ftl @@ -0,0 +1,28 @@ +grow-window = 伸びさせる +shrink-window = 縮ませる +swap-windows = ウィンドウを取り替える +stack-windows = ウィンドウを積む +unknown-keybinding = <設定されていない> +window-menu-minimize = 最小化 +window-menu-maximize = 最大化 +window-menu-tiled = フローティングにする +window-menu-screenshot = スクリーンショットを撮る +window-menu-move = 動かす +window-menu-resize = サイズ変更 +window-menu-move-prev-workspace = 前のワークスペースに切り替える +window-menu-move-next-workspace = 次のワークスペースに切り替える +window-menu-stack = ウィンドウ積み重ねを作成 +window-menu-unstack-all = すべてのウィンドウを下ろす +window-menu-unstack = ウィンドウを下ろす +window-menu-sticky = スティッキーウィンドウ +window-menu-close = 閉じる +window-menu-close-all = ウィンドウをすべて閉じる +window-menu-resize-edge-top = 上 +window-menu-resize-edge-left = 左 +window-menu-resize-edge-right = 右 +window-menu-resize-edge-bottom = 下 +a11y-zoom-move-continuously = ポインターに合わせてビューを継続的に移動 +a11y-zoom-move-onedge = ポインターが端に達するとビューを移動 +a11y-zoom-move-centered = ポインターを中央に維持するようにビューを移動 +a11y-zoom-settings = 拡大鏡の設定... +window-menu-fullscreen = フルスクリーン diff --git a/resources/i18n/jv/cosmic_comp.ftl b/resources/i18n/jv/cosmic_comp.ftl new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n/ka/cosmic_comp.ftl b/resources/i18n/ka/cosmic_comp.ftl new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n/kab/cosmic_comp.ftl b/resources/i18n/kab/cosmic_comp.ftl new file mode 100644 index 0000000..3d024e2 --- /dev/null +++ b/resources/i18n/kab/cosmic_comp.ftl @@ -0,0 +1,27 @@ +window-menu-fullscreen = Agdil aččuran +window-menu-close = Mdel +window-menu-resize-edge-top = D asawen +window-menu-resize-edge-left = Zelmaḍ +window-menu-resize-edge-right = Yeffus +window-menu-resize-edge-bottom = Ddaw +window-menu-screenshot = Ṭṭef tuṭṭfa n ugdil +a11y-zoom-settings = Iɣewwaṛen n usemɣeṛ… +grow-window = Semɣer +shrink-window = Semẓi +stack-windows = Settef isfuyla +window-menu-minimize = Senɣes +window-menu-maximize = Afellay +window-menu-tiled = Asfaylu yettifliwen +window-menu-move = Smutti +window-menu-move-prev-workspace = Smutti ɣer tallunt n umahil tuzwirt +window-menu-move-next-workspace = Smutti ɣer tallunt n umahil tuḍfirt +window-menu-stack = Rnu asettef n isfuyla +window-menu-unstack-all = Kkes asettef n isfuyla +window-menu-unstack = Kkes asettef n usfaylu +window-menu-sticky = Asfaylu amsenṭaḍ +window-menu-close-all = Mdel akk isfuyla +a11y-zoom-move-continuously = Timeẓri tezga dima tetteddu s usewwaṛ +a11y-zoom-move-onedge = Timeẓri tetteddu mi ara yaweḍ usewwaṛ ɣer yiri +a11y-zoom-move-centered = Timeẓri tetteddu iwakken ad tḥerzeḍ asewwaṛ d alemmas +swap-windows = Senfel isfuyla +window-menu-resize = Beddel tiddi diff --git a/resources/i18n/kk/cosmic_comp.ftl b/resources/i18n/kk/cosmic_comp.ftl new file mode 100644 index 0000000..2551c76 --- /dev/null +++ b/resources/i18n/kk/cosmic_comp.ftl @@ -0,0 +1,28 @@ +a11y-zoom-move-continuously = Көрініс курсормен бірге үздіксіз қозғалады +a11y-zoom-move-onedge = Көрініс курсор жиекке жеткенде қозғалады +a11y-zoom-move-centered = Көрініс курсорды ортада сақтау үшін қозғалады +a11y-zoom-settings = Үлкейткіш баптаулары... +grow-window = Үлкейту +shrink-window = Кішірейту +swap-windows = Терезелерді алмастыру +stack-windows = Терезелерді жинақтау +unknown-keybinding = <орнатылмаған> +window-menu-minimize = Қайыру +window-menu-maximize = Жазық қылу +window-menu-fullscreen = Толық экран +window-menu-tiled = Қалқымалы терезе +window-menu-screenshot = Скриншот түсіру +window-menu-move = Жылжыту +window-menu-resize = Өлшемін өзгерту +window-menu-move-prev-workspace = Алдыңғы жұмыс орнына жылжыту +window-menu-move-next-workspace = Келесі жұмыс орнына жылжыту +window-menu-stack = Терезелер жинағын жасау +window-menu-unstack-all = Терезелерді жинақтан шығару +window-menu-unstack = Терезені жинақтан шығару +window-menu-sticky = Жабысқақ терезе +window-menu-close = Жабу +window-menu-close-all = Барлық терезелерді жабу +window-menu-resize-edge-top = Жоғарғы +window-menu-resize-edge-left = Сол жақ +window-menu-resize-edge-right = Оң жақ +window-menu-resize-edge-bottom = Төменгі diff --git a/resources/i18n/kmr/cosmic_comp.ftl b/resources/i18n/kmr/cosmic_comp.ftl new file mode 100644 index 0000000..8f207a8 --- /dev/null +++ b/resources/i18n/kmr/cosmic_comp.ftl @@ -0,0 +1,28 @@ +grow-window = Mezin bike +shrink-window = Biçûk bike +window-menu-move = Bilivîne +window-menu-close = Bigire +window-menu-resize-edge-top = Jor +window-menu-resize-edge-left = Çep +window-menu-resize-edge-right = Rast +window-menu-resize-edge-bottom = Jêr +unknown-keybinding = +window-menu-minimize = Biçûk bike +window-menu-maximize = Mezin bike +window-menu-fullscreen = Dîmendera tije +window-menu-tiled = Çarçoveya herikbar +window-menu-screenshot = Wêneya dîmenderê bigire +window-menu-resize = Mezinahiyê ji nû ve saz bike +window-menu-move-prev-workspace = Bilivîne bo qada xebatê ya paş +window-menu-move-next-workspace = Bilivîne bo qada xebatê ya pêş +window-menu-stack = Komkirina çarçoveyê biafrîne +window-menu-unstack-all = Çarçoveyan kom neke +window-menu-unstack = Çarçoveyê kom neke +window-menu-sticky = Çarçoveya daliqandî +window-menu-close-all = Hemû çarçoveyan bigire +a11y-zoom-move-continuously = Dîtin bi nîşanker re bi awayekî domdar dilive +a11y-zoom-move-onedge = Dîtin dema nîşanker dikeve qeraxê dilive +a11y-zoom-move-centered = Dîtin dilive da ku nîşanker navîn bimîne +a11y-zoom-settings = Sazkariyên mezinkerê... +swap-windows = Çarçoveyan biguhêrîne +stack-windows = Çarçoveyan dalqîne diff --git a/resources/i18n/kn/cosmic_comp.ftl b/resources/i18n/kn/cosmic_comp.ftl new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n/ko/cosmic_comp.ftl b/resources/i18n/ko/cosmic_comp.ftl new file mode 100644 index 0000000..1c991c5 --- /dev/null +++ b/resources/i18n/ko/cosmic_comp.ftl @@ -0,0 +1,28 @@ +window-menu-resize-edge-top = 상단 +window-menu-resize-edge-left = 좌측 +window-menu-resize-edge-right = 우측 +window-menu-resize-edge-bottom = 하단 +window-menu-move = 이동 +window-menu-minimize = 최소화 +window-menu-maximize = 최대화 +window-menu-fullscreen = 전체 화면 +window-menu-move-prev-workspace = 이전 작업 공간으로 이동 +window-menu-move-next-workspace = 다음 작업 공간으로 이동 +a11y-zoom-settings = 돋보기 설정... +grow-window = 확대 +shrink-window = 축소 +swap-windows = 창 바꾸기 +stack-windows = 창 스택 +window-menu-tiled = 플로팅 창으로 전환 +window-menu-screenshot = 스크린샷 찍기 +window-menu-resize = 크기 조정 +window-menu-stack = 창 스택 생성 +window-menu-unstack-all = 모든 창 스택 해제 +window-menu-unstack = 창 스택 해제 +a11y-zoom-move-continuously = 포인터를 따라 화면이 계속 이동 +a11y-zoom-move-onedge = 포인터가 가장자리에 도달할 때 화면 이동 +a11y-zoom-move-centered = 포인터를 화면 중앙에 유지하며 이동 +window-menu-close = 닫기 +window-menu-close-all = 모든 창 닫기 +unknown-keybinding = +window-menu-sticky = 고정된 창 diff --git a/resources/i18n/li/cosmic_comp.ftl b/resources/i18n/li/cosmic_comp.ftl new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n/lo/cosmic_comp.ftl b/resources/i18n/lo/cosmic_comp.ftl new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n/lt/cosmic_comp.ftl b/resources/i18n/lt/cosmic_comp.ftl new file mode 100644 index 0000000..df08214 --- /dev/null +++ b/resources/i18n/lt/cosmic_comp.ftl @@ -0,0 +1,28 @@ +a11y-zoom-move-continuously = Vaizdo nuolatinis žymeklio sekimas +a11y-zoom-move-onedge = Vaizdo sekimas, kol žymeklis pasieks kraštą +a11y-zoom-move-centered = Rodyti vaizdą taip, kad žymeklis būtų centre +a11y-zoom-settings = Didinamojo stiklo nustatymai... +grow-window = Didinti +shrink-window = Mažinti +swap-windows = Apkeisti Langus Vietomis +stack-windows = Kaupti Langus +unknown-keybinding = +window-menu-minimize = Sumažinti +window-menu-maximize = Padidinti +window-menu-fullscreen = Visas ekranas +window-menu-tiled = Slankus langas +window-menu-screenshot = Padaryti ekrano kopiją +window-menu-move = Perkelti +window-menu-resize = Keisti dydį +window-menu-move-prev-workspace = Perkelti į ankstesnę darbo sritį +window-menu-move-next-workspace = Perkelti į sekančią darbo sritį +window-menu-stack = Sukurti langų krūvą +window-menu-unstack-all = Išskaidyti langus iš krūvos +window-menu-unstack = Atskirti langą nuo krūvos +window-menu-sticky = Lipnus langas +window-menu-close = Užverti +window-menu-close-all = Užverti visus langus +window-menu-resize-edge-top = Viršus +window-menu-resize-edge-left = Kairė +window-menu-resize-edge-right = Dešinė +window-menu-resize-edge-bottom = Apačia diff --git a/resources/i18n/ml/cosmic_comp.ftl b/resources/i18n/ml/cosmic_comp.ftl new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n/ms/cosmic_comp.ftl b/resources/i18n/ms/cosmic_comp.ftl new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n/nb-NO/cosmic_comp.ftl b/resources/i18n/nb-NO/cosmic_comp.ftl new file mode 100644 index 0000000..37bfbd6 --- /dev/null +++ b/resources/i18n/nb-NO/cosmic_comp.ftl @@ -0,0 +1,28 @@ +window-menu-tiled = Flytende vindu +a11y-zoom-settings = Forstørrelsesglass-innstillinger... +grow-window = Voks +shrink-window = Krymp +swap-windows = Bytt Vinduer +stack-windows = Stable Vinduer +unknown-keybinding = +window-menu-minimize = Minimer +window-menu-maximize = Maksimer +window-menu-fullscreen = Fullskjerm +window-menu-screenshot = Ta skjermbilde +window-menu-move = Flytt +window-menu-resize = Endre størrelse +window-menu-move-prev-workspace = Flytt til forrige arbeidsområde +window-menu-move-next-workspace = Flytt til neste arbeidsområde +window-menu-stack = Lag vindustabel +window-menu-unstack-all = Løsne vinduer +window-menu-unstack = Løsne vindu +window-menu-sticky = Festet vindu +window-menu-close = Lukk +window-menu-resize-edge-right = Høyre +window-menu-resize-edge-bottom = Nederst +window-menu-close-all = Lukk alle vinduer +window-menu-resize-edge-left = Venstre +window-menu-resize-edge-top = Øverst +a11y-zoom-move-centered = Visningen flytter seg for å holde pekeren sentrert +a11y-zoom-move-onedge = Visningen flytter seg når pekeren når kanten +a11y-zoom-move-continuously = Visningen flytter seg kontinuerlig med pekeren diff --git a/resources/i18n/nl/cosmic_comp.ftl b/resources/i18n/nl/cosmic_comp.ftl new file mode 100644 index 0000000..281fa29 --- /dev/null +++ b/resources/i18n/nl/cosmic_comp.ftl @@ -0,0 +1,28 @@ +a11y-zoom-move-continuously = Aanzicht met de cursor mee laten bewegen +a11y-zoom-move-onedge = Aanzicht verplaatst zich pas als de cursor een schermrand raakt +a11y-zoom-move-centered = Aanzicht houdt de cursor gecentreerd +a11y-zoom-settings = Vergrootglasinstellingen… +grow-window = Vergroten +shrink-window = Verkleinen +swap-windows = Vensters omwisselen +stack-windows = Vensters stapelen +unknown-keybinding = +window-menu-minimize = Minimaliseren +window-menu-maximize = Maximaliseren +window-menu-tiled = Venster laten zweven +window-menu-screenshot = Schermafdruk maken +window-menu-move = Verplaatsen +window-menu-resize = Grootte aanpassen +window-menu-move-prev-workspace = Naar vorig werkblad verplaatsen +window-menu-move-next-workspace = Naar volgend werkblad verplaatsen +window-menu-stack = Vensterstapel beginnen +window-menu-unstack-all = Vensterstapel opheffen +window-menu-unstack = Venster uit stapel halen +window-menu-sticky = Venster vastzetten +window-menu-close = Sluiten +window-menu-close-all = Alle vensters sluiten +window-menu-resize-edge-top = Boven +window-menu-resize-edge-left = Links +window-menu-resize-edge-right = Rechts +window-menu-resize-edge-bottom = Onder +window-menu-fullscreen = Volledig scherm diff --git a/resources/i18n/nn/cosmic_comp.ftl b/resources/i18n/nn/cosmic_comp.ftl new file mode 100644 index 0000000..1126c0f --- /dev/null +++ b/resources/i18n/nn/cosmic_comp.ftl @@ -0,0 +1 @@ +a11y-zoom-move-continuously = Visninga flyttar seg kontinuerleg med peikaren diff --git a/resources/i18n/oc/cosmic_comp.ftl b/resources/i18n/oc/cosmic_comp.ftl new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n/pa/cosmic_comp.ftl b/resources/i18n/pa/cosmic_comp.ftl new file mode 100644 index 0000000..819669c --- /dev/null +++ b/resources/i18n/pa/cosmic_comp.ftl @@ -0,0 +1,21 @@ +grow-window = ਵਧਾਓ +a11y-zoom-settings = ਵੱਡਦਰਸ਼ੀ ਸੈਟਿੰਗਾਂ... +shrink-window = ਸੰਘੁੜੋ +swap-windows = ਵਿੰਡੋਆਂ ਨੂੰ ਆਪਸ 'ਚ ਬਦਲੋ +stack-windows = ਵਿੰਡੋਆਂ ਦਾ ਢੇਰ ਲਾਓ +window-menu-minimize = ਘੱਟੋ-ਘੱਟ +window-menu-maximize = ਵੱਧ ਤੋਂ ਵੱਧ +window-menu-fullscreen = ਪੂਰੀ ਸਕਰੀਨ +window-menu-tiled = ਤਰਦੀ ਵਿੰਡੋ +window-menu-screenshot = ਸਕਰੀਨਸ਼ਾਟ ਲਵੋ +window-menu-move = ਭੇਜੋ +window-menu-resize = ਆਕਾਰ ਬਦਲੋ +window-menu-move-prev-workspace = ਪਿਛਲੇ ਵਰਕਸਪੇਸ ਵਿੱਚ ਭੇਜੋ +window-menu-move-next-workspace = ਅਗਲੇ ਵਰਕਸਪੇਸ ਵਿੱਚ ਭੇਜੋ +window-menu-stack = ਵਿੰਡੋ ਢੇਰੀ ਬਣਾਓ +window-menu-close = ਬੰਦ ਕਰੋ +window-menu-close-all = ਸਾਰੀਆਂ ਵਿੰਡੋਆਂ ਨੂੰ ਬੰਦ ਕਰੋ +window-menu-resize-edge-top = ਉੱਤੇ +window-menu-resize-edge-left = ਖੱਬੇ +window-menu-resize-edge-right = ਸੱਜੇ +window-menu-resize-edge-bottom = ਹੇਠਾਂ diff --git a/resources/i18n/pl/cosmic_comp.ftl b/resources/i18n/pl/cosmic_comp.ftl new file mode 100644 index 0000000..08a15b0 --- /dev/null +++ b/resources/i18n/pl/cosmic_comp.ftl @@ -0,0 +1,28 @@ +a11y-zoom-move-continuously = Widok podąża za wskaźnikiem +a11y-zoom-move-onedge = Widok przesuwa się gdy wskaźnik osiąga krawędź +a11y-zoom-move-centered = Widok przesuwa się tak, aby wskaźnik znajdował się w środku +a11y-zoom-settings = Ustawienia lupy… +grow-window = Powiększ +shrink-window = Zmniejsz +swap-windows = Zamień Okna Miejscami +stack-windows = Okna w Kartach +unknown-keybinding = +window-menu-minimize = Minimalizuj +window-menu-maximize = Maksymalizuj +window-menu-tiled = Pływające Okno +window-menu-screenshot = Zrób zrzut ekranu +window-menu-move = Przesuń +window-menu-resize = Zmień rozmiar +window-menu-move-prev-workspace = Przenieś do poprzedniego obszaru roboczego +window-menu-move-next-workspace = Przenieś do następnego obszaru roboczego +window-menu-stack = Zgrupuj okna w karty +window-menu-unstack-all = Rozdziel grupę okien +window-menu-unstack = Wydziel okno z grupy +window-menu-sticky = Zakrywające okno +window-menu-close = Zamknij +window-menu-close-all = Zamknij wszystkie okna +window-menu-resize-edge-top = Górnej +window-menu-resize-edge-left = Lewej +window-menu-resize-edge-right = Prawej +window-menu-resize-edge-bottom = Dolnej +window-menu-fullscreen = Tryb Pełnoekranowy diff --git a/resources/i18n/pt-BR/cosmic_comp.ftl b/resources/i18n/pt-BR/cosmic_comp.ftl new file mode 100644 index 0000000..fea3fcb --- /dev/null +++ b/resources/i18n/pt-BR/cosmic_comp.ftl @@ -0,0 +1,28 @@ +a11y-zoom-move-continuously = A visualização se move continuamente com o ponteiro +a11y-zoom-move-onedge = A visualização se move quando o ponteiro atinge a borda +a11y-zoom-move-centered = A visualização se move para manter o ponteiro centralizado +a11y-zoom-settings = Configurações do ampliador de tela... +grow-window = Aumentar +shrink-window = Diminuir +swap-windows = Trocar janelas +stack-windows = Empilhar janelas +unknown-keybinding = +window-menu-minimize = Minimizar +window-menu-maximize = Maximizar +window-menu-fullscreen = Tela cheia +window-menu-tiled = Janela flutuante +window-menu-screenshot = Fazer captura de tela +window-menu-move = Mover +window-menu-resize = Redimensionar +window-menu-move-prev-workspace = Mover para o espaço de trabalho anterior +window-menu-move-next-workspace = Mover para o próximo espaço de trabalho +window-menu-stack = Empilhar janelas +window-menu-unstack-all = Desempilhar janelas +window-menu-unstack = Desempilhar janela +window-menu-sticky = Manter a janela sempre na frente +window-menu-close = Fechar +window-menu-close-all = Fechar todas as janelas +window-menu-resize-edge-top = Para cima +window-menu-resize-edge-left = Para a esquerda +window-menu-resize-edge-right = Para a direita +window-menu-resize-edge-bottom = Para baixo diff --git a/resources/i18n/pt/cosmic_comp.ftl b/resources/i18n/pt/cosmic_comp.ftl new file mode 100644 index 0000000..af54c59 --- /dev/null +++ b/resources/i18n/pt/cosmic_comp.ftl @@ -0,0 +1,28 @@ +a11y-zoom-move-continuously = A visualização move-se continuamente com o cursor +a11y-zoom-move-onedge = A vista move-se quando o ponteiro atinge a extremidade +a11y-zoom-move-centered = A vista move-se para manter o ponteiro centrado +a11y-zoom-settings = Definições do ampliador... +grow-window = Aumentar +shrink-window = Diminuir +swap-windows = Trocar Janelas +stack-windows = Empilhar Janelas +unknown-keybinding = +window-menu-minimize = Minimizar +window-menu-maximize = Maximizar +window-menu-tiled = Janela flutuante +window-menu-screenshot = Tirar uma captura de ecrã +window-menu-move = Mover +window-menu-resize = Redimensionar +window-menu-move-prev-workspace = Mover para a área de trabalho anterior +window-menu-move-next-workspace = Mover para a área de trabalho seguinte +window-menu-stack = Criar pilha de janelas +window-menu-unstack-all = Desempilhar janelas +window-menu-unstack = Desempilhar janela +window-menu-sticky = Janela fixa +window-menu-close = Fechar +window-menu-close-all = Fechar todas as janelas +window-menu-resize-edge-top = Cima +window-menu-resize-edge-left = Esquerda +window-menu-resize-edge-right = Direita +window-menu-resize-edge-bottom = Baixo +window-menu-fullscreen = Ecrã inteiro diff --git a/resources/i18n/ro/cosmic_comp.ftl b/resources/i18n/ro/cosmic_comp.ftl new file mode 100644 index 0000000..ebd2c0d --- /dev/null +++ b/resources/i18n/ro/cosmic_comp.ftl @@ -0,0 +1,23 @@ +grow-window = Mărește +shrink-window = Micșorează +swap-windows = Schimbă ferestrele +stack-windows = Suprapune ferestrele +unknown-keybinding = +window-menu-minimize = Minimizează +window-menu-maximize = Maximizează +window-menu-tiled = Fereastră plutitoare +window-menu-screenshot = Capturează ecranul +window-menu-move = Mută +window-menu-resize = Redimensionează +window-menu-move-prev-workspace = Mută la spațiul de lucru anterior +window-menu-move-next-workspace = Mută la spațiul de lucru următor +window-menu-stack = Creează o stivă de ferestre +window-menu-unstack-all = Scoate toate ferestrele din stivă +window-menu-unstack = Scoate fereastra din stivă +window-menu-sticky = Fereastră vizibilă permanent +window-menu-close = Închide +window-menu-close-all = Închide toate ferestrele +window-menu-resize-edge-top = Sus +window-menu-resize-edge-left = Stânga +window-menu-resize-edge-right = Dreapta +window-menu-resize-edge-bottom = Jos diff --git a/resources/i18n/ru/cosmic_comp.ftl b/resources/i18n/ru/cosmic_comp.ftl new file mode 100644 index 0000000..c4be586 --- /dev/null +++ b/resources/i18n/ru/cosmic_comp.ftl @@ -0,0 +1,28 @@ +a11y-zoom-move-continuously = Лупа двигается за курсором +a11y-zoom-move-onedge = Лупа двигается когда курсор достигает края +a11y-zoom-move-centered = Лупа двигается с курсором в центре +a11y-zoom-settings = Параметры лупы... +grow-window = Увеличить +shrink-window = Уменьшить +swap-windows = Сменить окна +stack-windows = Стопка +unknown-keybinding = <не задано> +window-menu-minimize = Свернуть +window-menu-maximize = Развернуть +window-menu-fullscreen = Во весь экран +window-menu-tiled = Плавающее окно +window-menu-screenshot = Сделать снимок окна +window-menu-move = Переместить +window-menu-resize = Изменить размер +window-menu-move-prev-workspace = На пред. рабочий стол +window-menu-move-next-workspace = На след. рабочий стол +window-menu-stack = Создать стопку окон +window-menu-unstack-all = Распустить стопку окон +window-menu-unstack = Убрать из стопки +window-menu-sticky = Закрепить окно поверх других +window-menu-close = Закрыть +window-menu-close-all = Закрыть все окна +window-menu-resize-edge-top = Наверх +window-menu-resize-edge-left = Влево +window-menu-resize-edge-right = Вправо +window-menu-resize-edge-bottom = Вниз diff --git a/resources/i18n/sat/cosmic_comp.ftl b/resources/i18n/sat/cosmic_comp.ftl new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n/sk/cosmic_comp.ftl b/resources/i18n/sk/cosmic_comp.ftl new file mode 100644 index 0000000..d210e63 --- /dev/null +++ b/resources/i18n/sk/cosmic_comp.ftl @@ -0,0 +1,28 @@ +a11y-zoom-move-continuously = Zobrazenie sa pohybuje nepretržite s kurzorom +a11y-zoom-move-onedge = Zobrazenie sa pohybuje, keď kurzor dosiahne okraj +a11y-zoom-move-centered = Zobrazenie sa pohybuje, aby kurzor zostal v strede +a11y-zoom-settings = Nastavenia lupy... +grow-window = Zväčšiť +shrink-window = Zmenšiť +swap-windows = Vymeniť okná +stack-windows = Zoskupiť okná +unknown-keybinding = +window-menu-minimize = Minimalizovať +window-menu-maximize = Maximalizovať +window-menu-fullscreen = Celá obrazovka +window-menu-tiled = Plávajúce okno +window-menu-screenshot = Urobiť snímku obrazovky +window-menu-move = Presunúť +window-menu-resize = Zmeniť veľkosť +window-menu-move-prev-workspace = Presunúť na predchádzajúcu pracovnú plochu +window-menu-move-next-workspace = Presunúť na nasledujúcu pracovnú plochu +window-menu-stack = Vytvoriť skupinu okien +window-menu-unstack-all = Oddeliť všetky okná +window-menu-unstack = Oddeliť okno +window-menu-sticky = Prilepiť okno +window-menu-close = Zavrieť +window-menu-close-all = Zavrieť všetky okná +window-menu-resize-edge-top = Hore +window-menu-resize-edge-left = Vľavo +window-menu-resize-edge-right = Vpravo +window-menu-resize-edge-bottom = Dole diff --git a/resources/i18n/sl/cosmic_comp.ftl b/resources/i18n/sl/cosmic_comp.ftl new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n/sr-Cyrl/cosmic_comp.ftl b/resources/i18n/sr-Cyrl/cosmic_comp.ftl new file mode 100644 index 0000000..66148b8 --- /dev/null +++ b/resources/i18n/sr-Cyrl/cosmic_comp.ftl @@ -0,0 +1,28 @@ +grow-window = Повећај +shrink-window = Смањи +swap-windows = Замени прозоре +stack-windows = Групиши прозоре +unknown-keybinding = <није подешено> +window-menu-minimize = Умањи +window-menu-maximize = Увећај +window-menu-tiled = Плутајући прозор +window-menu-screenshot = Направи снимак апликације +window-menu-move = Помери +window-menu-resize = Промени величину +window-menu-move-prev-workspace = Помери на претходни радни простор +window-menu-move-next-workspace = Помери на следећи радни простор +window-menu-stack = Направи групу прозора +window-menu-unstack-all = Уклони све прозоре из групе +window-menu-unstack = Уклони прозор из групе +window-menu-sticky = Лепљиви прозор +window-menu-close = Затвори +window-menu-close-all = Затвори све прозоре +window-menu-resize-edge-top = Горе +window-menu-resize-edge-left = Лево +window-menu-resize-edge-right = Десно +window-menu-resize-edge-bottom = Доле +a11y-zoom-move-continuously = Види покрете непрекидно помоћу показивача +a11y-zoom-move-onedge = Покажи покрете када показивач достигне ивицу +a11y-zoom-move-centered = Покажи покрете зарад центрирања показивача +a11y-zoom-settings = Подешавања лупе... +window-menu-fullscreen = Цео екран diff --git a/resources/i18n/sr-Latn/cosmic_comp.ftl b/resources/i18n/sr-Latn/cosmic_comp.ftl new file mode 100644 index 0000000..6847c4b --- /dev/null +++ b/resources/i18n/sr-Latn/cosmic_comp.ftl @@ -0,0 +1,23 @@ +grow-window = Povećaj +shrink-window = Smanji +swap-windows = Zameni prozore +stack-windows = Grupiši prozore +unknown-keybinding = +window-menu-minimize = Umanji +window-menu-maximize = Uvećaj +window-menu-tiled = Plutajući prozor +window-menu-screenshot = Napravi snimak aplikacije +window-menu-move = Pomeri +window-menu-resize = Promeni veličinu +window-menu-move-prev-workspace = Pomeri na prethodni radni prostor +window-menu-move-next-workspace = Pomeri na sledeći radni prostor +window-menu-stack = Napravi grupu prozora +window-menu-unstack-all = Ukloni sve prozore iz grupe +window-menu-unstack = Ukloni prozor iz grupe +window-menu-sticky = Lepljivi prozor +window-menu-close = Zatvori +window-menu-close-all = Zatvori sve prozore +window-menu-resize-edge-top = Gore +window-menu-resize-edge-left = Levo +window-menu-resize-edge-right = Desno +window-menu-resize-edge-bottom = Dole diff --git a/resources/i18n/sr/cosmic_comp.ftl b/resources/i18n/sr/cosmic_comp.ftl new file mode 100644 index 0000000..d95ac4c --- /dev/null +++ b/resources/i18n/sr/cosmic_comp.ftl @@ -0,0 +1,28 @@ +a11y-zoom-move-continuously = Преглед се непрекидно помера показивачем +a11y-zoom-move-onedge = Преглед се помера када показивач достигне ивицу +a11y-zoom-move-centered = Преглед се помера како би се показивач задржао на средини +a11y-zoom-settings = Подешавања лупе... +grow-window = Увећај +shrink-window = Смањи +swap-windows = Замени прозоре +stack-windows = Наслагај прозоре +unknown-keybinding = <непостављено> +window-menu-minimize = Умањи +window-menu-maximize = Увећај +window-menu-fullscreen = Цео екран +window-menu-tiled = Плутајући прозор +window-menu-screenshot = Направи снимак екрана +window-menu-move = Помери +window-menu-resize = Промени величину +window-menu-move-prev-workspace = Помери на претходни радни простор +window-menu-move-next-workspace = Помери на следећи радни простор +window-menu-stack = Направи стек прозора +window-menu-unstack-all = Раздвоји прозоре +window-menu-unstack = Одвоји прозор +window-menu-sticky = Лепљив прозор +window-menu-close = Затвори +window-menu-close-all = Затвори све прозоре +window-menu-resize-edge-top = Горе +window-menu-resize-edge-left = Лево +window-menu-resize-edge-right = Десно +window-menu-resize-edge-bottom = Доле diff --git a/resources/i18n/sv/cosmic_comp.ftl b/resources/i18n/sv/cosmic_comp.ftl new file mode 100644 index 0000000..8df7651 --- /dev/null +++ b/resources/i18n/sv/cosmic_comp.ftl @@ -0,0 +1,28 @@ +a11y-zoom-move-continuously = Vyn flyttar sig kontinuerligt med pekaren +a11y-zoom-move-onedge = Vyn flyttar på sig när pekaren når kanten +a11y-zoom-move-centered = Vyn flyttar på sig för att behålla pekaren centrerad +a11y-zoom-settings = Inställningar för förstoringsglas... +grow-window = Väx +shrink-window = Krymp +swap-windows = Byt fönster +stack-windows = Stapla fönstren +unknown-keybinding = +window-menu-minimize = Minimera +window-menu-maximize = Maximera +window-menu-tiled = Flytande fönster +window-menu-screenshot = Ta skärmbild +window-menu-move = Flytta +window-menu-resize = Ändra storlek +window-menu-move-prev-workspace = Flytta till föregående arbetsyta +window-menu-move-next-workspace = Flytta till nästa arbetsyta +window-menu-stack = Skapa fönsterstapel +window-menu-unstack-all = Avstapla fönster +window-menu-unstack = Avstapla fönster +window-menu-sticky = Klistrigt fönster +window-menu-close = Stäng +window-menu-close-all = Stäng alla fönster +window-menu-resize-edge-top = Högst upp +window-menu-resize-edge-left = Vänster +window-menu-resize-edge-right = Höger +window-menu-resize-edge-bottom = Längst ned +window-menu-fullscreen = Helskärm diff --git a/resources/i18n/ta/cosmic_comp.ftl b/resources/i18n/ta/cosmic_comp.ftl new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n/th/cosmic_comp.ftl b/resources/i18n/th/cosmic_comp.ftl new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n/ti/cosmic_comp.ftl b/resources/i18n/ti/cosmic_comp.ftl new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n/tr/cosmic_comp.ftl b/resources/i18n/tr/cosmic_comp.ftl new file mode 100644 index 0000000..de5a546 --- /dev/null +++ b/resources/i18n/tr/cosmic_comp.ftl @@ -0,0 +1,28 @@ +grow-window = Büyüt +shrink-window = Küçült +swap-windows = Pencereleri Değiştir +stack-windows = Pencereleri Yığınla +unknown-keybinding = +window-menu-minimize = Küçült +window-menu-maximize = Büyüt +window-menu-tiled = Pencereyi kaydır +window-menu-screenshot = Ekran görüntüsü al +window-menu-move = Taşı +window-menu-resize = Yeniden boyutlandır +window-menu-move-prev-workspace = Önceki çalışma alanına taşı +window-menu-move-next-workspace = Sonraki çalışma alanına taşı +window-menu-stack = Pencere yığını oluştur +window-menu-unstack-all = Pencere yığınını dağıt +window-menu-unstack = Pencereyi yığından çıkar +window-menu-sticky = Yapışkan pencere +window-menu-close = Kapat +window-menu-close-all = Tüm pencereleri kapat +window-menu-resize-edge-top = Üst +window-menu-resize-edge-left = Sol +window-menu-resize-edge-right = Sağ +window-menu-resize-edge-bottom = Alt +a11y-zoom-move-continuously = Görünüm imleçle birlikte sürekli hareket eder +a11y-zoom-move-onedge = İmleç kenara ulaştığında görünüm hareket eder +a11y-zoom-move-centered = Görünüm imleci merkezde tutar +a11y-zoom-settings = Büyüteç ayarları... +window-menu-fullscreen = Tam ekran diff --git a/resources/i18n/uk/cosmic_comp.ftl b/resources/i18n/uk/cosmic_comp.ftl new file mode 100644 index 0000000..1c033ae --- /dev/null +++ b/resources/i18n/uk/cosmic_comp.ftl @@ -0,0 +1,28 @@ +grow-window = Збільшити +shrink-window = Зменшити +swap-windows = Поміняти місцями вікна +stack-windows = Групувати вікна +unknown-keybinding = +window-menu-minimize = Згорнути +window-menu-maximize = Розгорнути +window-menu-tiled = Плавуче вікно +window-menu-screenshot = Зробити знімок екрана +window-menu-move = Перемістити +window-menu-resize = Змінити розмір +window-menu-move-prev-workspace = Перейти до попереднього робочого простору +window-menu-move-next-workspace = Перейти до наступного робочого простору +window-menu-stack = Створити стек вікон +window-menu-unstack-all = Роз’єднати вікна +window-menu-unstack = Роз’єднати вікно +window-menu-sticky = Закріплене вікно +window-menu-close = Закрити +window-menu-close-all = Закрити всі вікна +window-menu-resize-edge-top = Верхній край +window-menu-resize-edge-left = Лівий край +window-menu-resize-edge-right = Правий край +window-menu-resize-edge-bottom = Нижній край +a11y-zoom-settings = Налаштування лупи... +window-menu-fullscreen = Розгорнути на повний екран +a11y-zoom-move-continuously = Лупа слідує за вказівником +a11y-zoom-move-onedge = Лупа рухається, коли вказівник торкається краю +a11y-zoom-move-centered = Лупа переміщується з вказівником, утримуючи його в центрі екрана diff --git a/resources/i18n/uz/cosmic_comp.ftl b/resources/i18n/uz/cosmic_comp.ftl new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n/vi/cosmic_comp.ftl b/resources/i18n/vi/cosmic_comp.ftl new file mode 100644 index 0000000..f4d0655 --- /dev/null +++ b/resources/i18n/vi/cosmic_comp.ftl @@ -0,0 +1,27 @@ +grow-window = Mở rộng +shrink-window = Thu nhỏ +swap-windows = Hoán đổi cửa sổ +stack-windows = Chồng các cửa sổ +unknown-keybinding = +window-menu-minimize = Thu nhỏ hết cỡ +window-menu-maximize = Phóng to hết cỡ +window-menu-tiled = Cửa sổ nổi +window-menu-screenshot = Chụp màn hình +window-menu-move = Di chuyển +window-menu-resize = Đổi cỡ +window-menu-move-prev-workspace = Đưa về không gian trước +window-menu-move-next-workspace = Đưa đến không gian kế +window-menu-stack = Tạo chồng cửa sổ +window-menu-unstack-all = Gỡ chồng cửa sổ +window-menu-unstack = Gỡ khỏi chồng +window-menu-sticky = Cửa sổ dính +window-menu-close = Đóng +window-menu-close-all = Đóng tất cả +window-menu-resize-edge-top = Trên +window-menu-resize-edge-left = Trái +window-menu-resize-edge-right = Phải +window-menu-resize-edge-bottom = Dưới +window-menu-fullscreen = Toàn màn hình +a11y-zoom-move-continuously = Khung cảnh chuyển liện tục với chuột +a11y-zoom-move-onedge = Khung cảnh chuyển khi chuột chạm rìa +a11y-zoom-settings = Cài đặt kính lúp... diff --git a/resources/i18n/yue-Hant/cosmic_comp.ftl b/resources/i18n/yue-Hant/cosmic_comp.ftl new file mode 100644 index 0000000..e69de29 diff --git a/resources/i18n/zh-CN/cosmic_comp.ftl b/resources/i18n/zh-CN/cosmic_comp.ftl new file mode 100644 index 0000000..dd59e97 --- /dev/null +++ b/resources/i18n/zh-CN/cosmic_comp.ftl @@ -0,0 +1,28 @@ +a11y-zoom-move-continuously = 视图随指针连续移动 +a11y-zoom-move-onedge = 指针到达边缘时视图移动 +a11y-zoom-move-centered = 视图移动时保持指针居中 +a11y-zoom-settings = 放大镜设置… +grow-window = 增大 +shrink-window = 缩小 +swap-windows = 交换窗口 +stack-windows = 堆叠窗口 +unknown-keybinding = <未设置> +window-menu-minimize = 最小化 +window-menu-maximize = 最大化 +window-menu-fullscreen = 全屏 +window-menu-tiled = 悬浮窗口 +window-menu-screenshot = 截图 +window-menu-move = 移动 +window-menu-resize = 调整大小 +window-menu-move-prev-workspace = 移动到上一个工作区 +window-menu-move-next-workspace = 移动到下一个工作区 +window-menu-stack = 创建堆叠窗口 +window-menu-unstack-all = 取消堆叠全部窗口 +window-menu-unstack = 取消堆叠窗口 +window-menu-sticky = 固定窗口 +window-menu-close = 关闭 +window-menu-close-all = 关闭所有窗口 +window-menu-resize-edge-top = 顶部 +window-menu-resize-edge-left = 左侧 +window-menu-resize-edge-right = 右侧 +window-menu-resize-edge-bottom = 底部 diff --git a/resources/i18n/zh-TW/cosmic_comp.ftl b/resources/i18n/zh-TW/cosmic_comp.ftl new file mode 100644 index 0000000..cd3bc22 --- /dev/null +++ b/resources/i18n/zh-TW/cosmic_comp.ftl @@ -0,0 +1,28 @@ +grow-window = 放大 +shrink-window = 縮小 +swap-windows = 交換視窗 +stack-windows = 堆疊視窗 +unknown-keybinding = <未設定> +window-menu-minimize = 最小化 +window-menu-maximize = 最大化 +window-menu-tiled = 浮動視窗 +window-menu-screenshot = 擷取螢幕畫面 +window-menu-move = 移動 +window-menu-resize = 調整大小 +window-menu-move-prev-workspace = 移至上一個工作區 +window-menu-move-next-workspace = 移至下一個工作區 +window-menu-stack = 建立視窗堆疊 +window-menu-unstack-all = 取消堆疊所有視窗 +window-menu-unstack = 取消堆疊視窗 +window-menu-sticky = 置頂視窗 +window-menu-close = 關閉 +window-menu-close-all = 關閉所有視窗 +window-menu-resize-edge-top = 上 +window-menu-resize-edge-left = 左 +window-menu-resize-edge-right = 右 +window-menu-resize-edge-bottom = 下 +a11y-zoom-move-continuously = 檢視畫面隨指標持續移動 +a11y-zoom-move-onedge = 檢視畫面在指標到達邊緣時移動 +a11y-zoom-move-centered = 檢視畫面移動以保持指標置中 +a11y-zoom-settings = 放大鏡設定... +window-menu-fullscreen = 全螢幕 diff --git a/resources/icons/amd.svg b/resources/icons/amd.svg new file mode 100644 index 0000000..59275ed --- /dev/null +++ b/resources/icons/amd.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/intel.svg b/resources/icons/intel.svg new file mode 100644 index 0000000..b725331 --- /dev/null +++ b/resources/icons/intel.svg @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/resources/icons/nvidia.svg b/resources/icons/nvidia.svg new file mode 100644 index 0000000..2b25de2 --- /dev/null +++ b/resources/icons/nvidia.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/protocols/wayland-drm.xml b/resources/protocols/wayland-drm.xml new file mode 100644 index 0000000..eaf2654 --- /dev/null +++ b/resources/protocols/wayland-drm.xml @@ -0,0 +1,189 @@ + + + + + Copyright © 2008-2011 Kristian Høgsberg + Copyright © 2010-2011 Intel Corporation + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that\n the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Bitmask of capabilities. + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..ae16192 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.93" +components = [ "rust-src" ] diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..3501136 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +style_edition = "2024" diff --git a/src/backend/kms/device.rs b/src/backend/kms/device.rs new file mode 100644 index 0000000..44682dc --- /dev/null +++ b/src/backend/kms/device.rs @@ -0,0 +1,1499 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{ + backend::{ + kms::render::gles::GbmGlowBackend, + render::{CLEAR_COLOR, CursorMode, GlMultiRenderer, init_shaders, output_elements}, + }, + config::{CompTransformDef, EdidProduct, ScreenFilter}, + shell::Shell, + utils::{env::dev_list_var, prelude::*}, + wayland::handlers::image_copy_capture::PendingImageCopyData, +}; + +use anyhow::{Context, Result}; +use cosmic_comp_config::output::comp::{AdaptiveSync, OutputConfig, OutputState}; +use smithay::{ + backend::{ + allocator::{ + Format, Fourcc, + format::FormatSet, + gbm::{GbmAllocator, GbmDevice}, + }, + drm::{ + DrmDevice, DrmDeviceFd, DrmEvent, DrmNode, NodeType, + compositor::{FrameError, FrameFlags}, + exporter::gbm::GbmFramebufferExporter, + output::{DrmOutputManager, LockedDrmOutputManager}, + }, + egl::{EGLContext, EGLDevice, EGLDisplay, context::ContextPriority}, + renderer::glow::GlowRenderer, + session::{Session, libseat::LibSeatSession}, + }, + desktop::utils::OutputPresentationFeedback, + output::{Mode as OutputMode, Output, PhysicalProperties, Scale, Subpixel}, + reexports::{ + calloop::{LoopHandle, RegistrationToken}, + drm::control::{Device as ControlDevice, ModeTypeFlags, connector, crtc}, + gbm::BufferObjectFlags as GbmBufferFlags, + rustix::fs::{Dev as dev_t, OFlags}, + wayland_server::DisplayHandle, + }, + utils::{Clock, DevPath, DeviceFd, Monotonic, Point, Transform}, + wayland::{ + drm_lease::{DrmLease, DrmLeaseState}, + drm_syncobj::supports_syncobj_eventfd, + }, +}; +use tracing::{error, info, warn}; +use wayland_backend::server::ClientId; + +use std::{ + borrow::BorrowMut, + cell::RefCell, + collections::{HashMap, HashSet}, + fmt, + os::fd::OwnedFd, + path::Path, + sync::{Arc, RwLock, atomic::AtomicBool, mpsc::Receiver}, + time::Duration, +}; + +use super::{drm_helpers, socket::Socket, surface::Surface}; + +#[derive(Debug)] +pub struct EGLInternals { + pub display: EGLDisplay, + pub device: EGLDevice, + pub context: EGLContext, +} + +pub type LockedGbmDrmOutputManager<'a> = LockedDrmOutputManager< + 'a, + GbmAllocator, + GbmFramebufferExporter, + Option<( + OutputPresentationFeedback, + Receiver, + Duration, + )>, + DrmDeviceFd, +>; + +pub type GbmDrmOutputManager = DrmOutputManager< + GbmAllocator, + GbmFramebufferExporter, + Option<( + OutputPresentationFeedback, + Receiver, + Duration, + )>, + DrmDeviceFd, +>; + +#[derive(Debug)] +pub struct Device { + pub inner: InnerDevice, + pub drm: GbmDrmOutputManager, + + pub texture_formats: FormatSet, + event_token: Option, + pub socket: Option, +} + +#[derive(Debug)] +struct ReusableDevice { + leasing_global: Option, + active_clients: HashSet, + socket: Option, +} + +#[derive(Debug)] +struct OldDeviceState { + fd: DeviceFd, + outputs: HashMap, + leased_connectors: HashSet, +} + +#[derive(Debug)] +pub struct LockedDevice<'a> { + pub inner: &'a mut InnerDevice, + pub drm: LockedGbmDrmOutputManager<'a>, +} + +pub struct InnerDevice { + pub dev_node: DrmNode, + pub render_node: DrmNode, + pub is_software: bool, + pub egl: Option, + + pub outputs: HashMap, + pub surfaces: HashMap, + pub gbm: GbmDevice, + + pub leased_connectors: Vec<(connector::Handle, crtc::Handle)>, + pub leasing_global: Option, + pub active_leases: Vec, + pub active_clients: HashSet, +} + +impl fmt::Debug for InnerDevice { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Device") + .field("dev_node", &self.dev_node) + .field("render_node", &self.render_node) + .field("is_software", &self.is_software) + .field("egl", &self.egl) + .field("outputs", &self.outputs) + .field("surfaces", &self.surfaces) + .field("gbm", &"..") + .field("leased_connectors", &self.leased_connectors) + .field("leasing_global", &self.leasing_global) + .field("active_leases", &self.active_leases) + .field("active_clients", &self.active_clients.len()) + .finish() + } +} + +pub fn init_egl(gbm: &GbmDevice) -> Result { + let path = gbm.dev_path(); + + let display = unsafe { EGLDisplay::new(gbm.clone()) }.with_context(|| { + format!( + "Failed to create EGLDisplay for device: {:?}", + path.as_deref().map(Path::display) + ) + })?; + let device = EGLDevice::device_for_display(&display).with_context(|| { + format!( + "Unable to find matching egl device for {:?}", + path.as_deref().map(Path::display) + ) + })?; + + let context = + EGLContext::new_with_priority(&display, ContextPriority::High).with_context(|| { + format!( + "Failed to create EGLContext for device {:?}:{:?}", + device, + path.as_deref().map(Path::display), + ) + })?; + + Ok(EGLInternals { + display, + device, + context, + }) +} + +impl State { + pub fn device_added( + &mut self, + dev: dev_t, + path: &Path, + dh: &DisplayHandle, + ) -> Result> { + if !self.backend.kms().session.is_active() { + return Ok(Vec::new()); + } + + if let Some(allowlist) = dev_list_var("COSMIC_DRM_ALLOW_DEVICES") { + let mut matched = false; + if let Ok(node) = DrmNode::from_dev_id(dev) { + let node = node + .node_with_type(NodeType::Render) + .and_then(|res| res.ok()) + .unwrap_or(node); + for ident in allowlist { + if ident.matches(&node) { + matched = true; + break; + } + } + if !matched { + info!( + "Skipping device {} due to COSMIC_DRM_ALLOW_DEVICE list.", + path.display() + ); + return Ok(Vec::new()); + } + } + } + if let Some(blocklist) = dev_list_var("COSMIC_DRM_BLOCK_DEVICES") + && let Ok(node) = DrmNode::from_dev_id(dev) + { + let node = node + .node_with_type(NodeType::Render) + .and_then(|res| res.ok()) + .unwrap_or(node); + for ident in blocklist { + if ident.matches(&node) { + info!( + "Skipping device {} due to COSMIC_DRM_BLOCK_DEVICE list.", + path.display() + ); + return Ok(Vec::new()); + } + } + } + + let mut device = Device::new( + dev, + path, + &mut self.backend.kms().session, + &mut self.common, + dh, + None, + )?; + let connectors = device.enumerate_surfaces()?.added; // There are no removed outputs on newly added devices + let mut wl_outputs = Vec::new(); + let mut w = self.common.shell.read().global_space().size.w as u32; + + { + for (conn, maybe_crtc) in connectors { + match device.inner.connector_added( + device.drm.device_mut(), + self.backend.kms().primary_node.clone(), + conn, + maybe_crtc, + (w, 0), + &self.common.event_loop_handle, + self.common.config.dynamic_conf.screen_filter().clone(), + self.common.shell.clone(), + self.common.startup_done.clone(), + ) { + Ok((output, should_expose)) => { + if should_expose { + w += output.geometry().size.w as u32; + wl_outputs.push(output.clone()); + } + device.inner.outputs.insert(conn, output); + } + Err(err) => { + warn!(?err, "Failed to initialize output, skipping"); + } + } + } + + // TODO atomic commit all surfaces together and drop surfaces, if it fails due to bandwidth + + let kms = self.backend.kms(); + kms.drm_devices.insert(device.inner.dev_node, device); + } + + self.common + .output_configuration_state + .add_heads(wl_outputs.iter()); + + self.backend.kms().refresh_used_devices()?; + Ok(wl_outputs) + } + + pub fn device_changed(&mut self, dev: dev_t) -> Result> { + if !self.backend.kms().session.is_active() { + return Ok(Vec::new()); + } + + let drm_node = DrmNode::from_dev_id(dev)?; + let mut outputs_removed = Vec::new(); + let mut outputs_added = Vec::new(); + + { + let backend = self.backend.kms(); + if let Some(device) = backend.drm_devices.get_mut(&drm_node) { + let changes = device.enumerate_surfaces()?; + + let mut w = self.common.shell.read().global_space().size.w as u32; + for conn in changes.removed { + // contains conns with updated crtcs, just drop the surface and re-create + if let Some(pos) = device + .inner + .leased_connectors + .iter() + .position(|(handle, _)| *handle == conn) + { + let _ = device.inner.leased_connectors.remove(pos); + if let Some(leasing_state) = device.inner.leasing_global.as_mut() { + leasing_state.withdraw_connector(conn); + } + } else if let Some(crtc) = device + .inner + .surfaces + .iter() + .find_map(|(crtc, surface)| (surface.connector == conn).then_some(crtc)) + .cloned() + { + device.inner.surfaces.remove(&crtc).unwrap(); + } + + if !changes.added.iter().any(|(c, _)| c == &conn) { + outputs_removed.push( + device + .inner + .outputs + .remove(&conn) + .expect("Connector without output?"), + ); + } + } + + for (conn, maybe_crtc) in changes.added { + match device.inner.connector_added( + device.drm.device_mut(), + backend.primary_node.clone(), + conn, + maybe_crtc, + (w, 0), + &self.common.event_loop_handle, + self.common.config.dynamic_conf.screen_filter().clone(), + self.common.shell.clone(), + self.common.startup_done.clone(), + ) { + Ok((output, should_expose)) => { + if should_expose { + w += output.geometry().size.w as u32; + outputs_added.push(output.clone()); + } + + device.inner.outputs.insert(conn, output); + } + Err(err) => { + warn!(?err, "Failed to initialize output, skipping"); + } + } + } + } + } + + self.common + .output_configuration_state + .remove_heads(outputs_removed.iter()); + self.common + .output_configuration_state + .add_heads(outputs_added.iter()); + + for output in outputs_removed { + self.common.remove_output(&output); + } + + self.backend.kms().refresh_used_devices()?; + Ok(outputs_added) + } + + pub fn reopen_device( + &mut self, + dev: dev_t, + path: &Path, + dh: &DisplayHandle, + ) -> Result> { + let backend = self.backend.kms(); + let drm_node = backend + .drm_devices + .values() + .find_map(|device| { + (device.inner.dev_node.dev_id() == dev).then_some(device.inner.dev_node) + }) + .with_context(|| format!("Couldn't find drm node for {}", dev))?; + + if let Some(mut device) = backend.drm_devices.shift_remove(&drm_node) { + let is_primary = backend + .primary_node + .read() + .unwrap() + .is_some_and(|node| node == device.inner.render_node); + + // all drm resources become inert + if let Some(token) = device.event_token.take() { + self.common.event_loop_handle.remove(token); + } + for (_, surface) in device.inner.surfaces.drain() { + surface.drop_and_join(); + } + if let Some(leasing_global) = device.inner.leasing_global.as_mut() { + for (connector, _) in &device.inner.leased_connectors { + leasing_global.withdraw_connector(*connector); + } + device.inner.active_leases.clear(); + } + + // These contain a reference to the file descriptor + backend.api.as_mut().remove_node(&device.inner.render_node); + // trigger enumeration + let _ = backend.api.devices(); + + for surface in backend + .drm_devices + .values_mut() + .flat_map(|device| device.inner.surfaces.values_mut()) + { + surface.remove_node(device.inner.render_node); + } + let syncobj_guard = + if is_primary && let Some(syncobj_state) = backend.syncobj_state.as_mut() { + Some(syncobj_state.close_device()) + } else { + None + }; + + let (reusable, mut old_state) = device.reuse(); + + match TryInto::::try_into(old_state.fd) { + Ok(fd) => { + if let Err(err) = backend.session.close(fd) { + warn!("Failed to close drm device fd: {}", err); + } + } + Err(_) => { + warn!(?drm_node, "Unable to close drm device fd cleanly."); + } + }; + + let mut new_device = Device::new( + dev, + path, + &mut backend.session, + &mut self.common, + dh, + Some(reusable), + )?; + + if let Some(guard) = syncobj_guard { + let import_device = new_device.drm.device().device_fd().clone(); + if supports_syncobj_eventfd(&import_device) { + guard.update_device(import_device); + } else { + drop(guard); + dh.remove_global::(backend.syncobj_state.take().unwrap().into_global()); + } + } + + let connectors = new_device.enumerate_surfaces()?.added; + let mut outputs_added = Vec::new(); + let mut w = self.common.shell.read().global_space().size.w as u32; + + { + for (conn, maybe_crtc) in connectors { + if let Some(output) = old_state.outputs.remove(&conn) { + let has_surface = if old_state.leased_connectors.contains(&conn) { + if let Some(crtc) = maybe_crtc { + new_device.inner.leased_connectors.push((conn, crtc)); + info!( + "Connector {} is non-desktop, setting up for leasing", + output.name() + ); + if let Some(lease_state) = new_device.inner.leasing_global.as_mut() + { + let physical = output.physical_properties(); + lease_state.add_connector::( + conn, + output.name(), + format!("{} {}", physical.make, physical.model), + ); + } + } else { + warn!( + "Connector {} is non-desktop, but we don't have a free crtc: not leasing", + output.name() + ); + } + false + } else { + let loc = output.current_location(); + populate_modes( + new_device.drm.device_mut(), + &output, + conn, + true, + (loc.x as u32, loc.y as u32), + ) + .with_context(|| "Failed to enumerate connector modes")?; + + if let Some(crtc) = maybe_crtc { + match Surface::new( + &output, + crtc, + conn, + backend.primary_node.clone(), + new_device.inner.dev_node, + new_device.inner.render_node, + &self.common.event_loop_handle, + self.common.config.dynamic_conf.screen_filter().clone(), + self.common.shell.clone(), + self.common.startup_done.clone(), + ) { + Ok(data) => { + new_device.inner.surfaces.insert(crtc, data); + true + } + Err(err) => { + error!(?crtc, "Failed to initialize surface: {}", err); + false + } + } + } else { + false + } + }; + + if !has_surface { + output + .user_data() + .get::>() + .unwrap() + .borrow_mut() + .enabled = OutputState::Disabled; + } + new_device.inner.outputs.insert(conn, output); + } else { + match new_device.inner.connector_added( + new_device.drm.device_mut(), + backend.primary_node.clone(), + conn, + maybe_crtc, + (w, 0), + &self.common.event_loop_handle, + self.common.config.dynamic_conf.screen_filter().clone(), + self.common.shell.clone(), + self.common.startup_done.clone(), + ) { + Ok((output, should_expose)) => { + if should_expose { + w += output.geometry().size.w as u32; + outputs_added.push(output.clone()); + } + + new_device.inner.outputs.insert(conn, output); + } + Err(err) => { + warn!(?err, "Failed to initialize output, skipping"); + } + } + } + } + } + + self.common + .output_configuration_state + .remove_heads(old_state.outputs.values()); + self.common + .output_configuration_state + .add_heads(outputs_added.iter()); + + for output in old_state.outputs.values() { + self.common.remove_output(output); + } + + backend.drm_devices.insert(drm_node, new_device); + backend.refresh_used_devices()?; + + Ok(outputs_added) + } else { + Ok(Vec::new()) + } + } + + pub fn device_removed(&mut self, dev: dev_t, dh: &DisplayHandle) -> Result<()> { + let backend = self.backend.kms(); + // we can't use DrmNode::from_node_id, because that assumes the node is still on sysfs + let drm_node = backend + .drm_devices + .values() + .find_map(|device| { + (device.inner.dev_node.dev_id() == dev).then_some(device.inner.dev_node) + }) + .with_context(|| format!("Couldn't find drm node for {}", dev))?; + + let mut outputs_removed = Vec::new(); + let device_fd = if let Some(mut device) = backend.drm_devices.shift_remove(&drm_node) { + if let Some(mut leasing_global) = device.inner.leasing_global.take() { + leasing_global.disable_global::(); + } + for (_, surface) in device.inner.surfaces.drain() { + outputs_removed.push(surface.output.clone()); + surface.drop_and_join(); + } + if let Some(token) = device.event_token.take() { + self.common.event_loop_handle.remove(token); + } + if let Some(socket) = device.socket.take() { + self.common.event_loop_handle.remove(socket.token); + self.common + .dmabuf_state + .destroy_global::(dh, socket.dmabuf_global); + dh.remove_global::(socket.drm_global); + } + backend.api.as_mut().remove_node(&device.inner.render_node); + backend + .primary_node + .write() + .unwrap() + .take_if(|node| node == &device.inner.render_node); + + Some(device.drm.device().device_fd().device_fd()) + } else { + None + }; + self.common + .output_configuration_state + .remove_heads(outputs_removed.iter()); + + if backend.session.is_active() { + for output in outputs_removed { + self.common.remove_output(&output); + } + } else { + self.common.output_configuration_state.update(); + } + + backend.refresh_used_devices()?; + + if let Some(fd) = device_fd { + match TryInto::::try_into(fd) { + Ok(fd) => { + if let Err(err) = backend.session.close(fd) { + warn!("Failed to close drm device fd: {}", err); + } + } + Err(_) => { + warn!(?drm_node, "Unable to close drm device fd cleanly."); + } + }; + } + Ok(()) + } + + pub fn refresh_output_config(&mut self) -> Result<()> { + self.common.config.read_outputs( + &mut self.common.output_configuration_state, + &mut self.backend, + &self.common.shell, + &self.common.event_loop_handle, + &mut self.common.workspace_state.update(), + &self.common.xdg_activation_state, + self.common.startup_done.clone(), + &self.common.clock, + )?; + self.common.refresh(); + Ok(()) + } +} + +pub struct OutputChanges { + pub added: Vec<(connector::Handle, Option)>, + pub removed: Vec, +} + +impl Device { + fn new( + dev: dev_t, + path: impl AsRef, + session: &mut LibSeatSession, + common: &mut Common, + dh: &DisplayHandle, + reuse: Option, + ) -> Result { + let path = path.as_ref(); + let fd = DrmDeviceFd::new(DeviceFd::from( + session + .open( + path, + OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK, + ) + .with_context(|| { + format!( + "Failed to optain file descriptor for drm device: {}", + path.display() + ) + })?, + )); + let (drm, notifier) = DrmDevice::new(fd.clone(), false) + .with_context(|| format!("Failed to initialize drm device for: {}", path.display()))?; + let dev_node = DrmNode::from_dev_id(dev)?; + + let gbm = GbmDevice::new(fd) + .with_context(|| format!("Failed to initialize GBM device for {}", path.display()))?; + let (render_node, render_formats, texture_formats, is_software) = { + let egl = init_egl(&gbm)?; + + let render_node = egl + .device + .try_get_render_node() + .ok() + .and_then(std::convert::identity) + .unwrap_or(dev_node); + let render_formats = egl.context.dmabuf_render_formats().clone(); + let texture_formats = egl.context.dmabuf_texture_formats().clone(); + + ( + render_node, + render_formats, + texture_formats, + egl.device.is_software(), + ) + }; + + let token = common + .event_loop_handle + .insert_source( + notifier, + move |event, metadata, state: &mut State| match event { + DrmEvent::VBlank(crtc) => { + if let Some(device) = state.backend.kms().drm_devices.get_mut(&dev_node) + && let Some(surface) = device.inner.surfaces.get_mut(&crtc) + { + surface.on_vblank(metadata.take()); + } + } + DrmEvent::Error(err) => { + warn!(?err, "Failed to read events of device {:?}.", dev); + } + }, + ) + .with_context(|| format!("Failed to add drm device to event loop: {}", dev))?; + + let ReusableDevice { + leasing_global, + active_clients, + socket, + } = reuse.unwrap_or_else(|| { + let socket = match (!is_software) + .then(|| common.create_socket(dh, render_node, texture_formats.clone())) + .transpose() + { + Ok(socket) => socket, + Err(err) => { + warn!( + ?err, + "Failed to initialize hardware-acceleration for clients on {}.", + render_node, + ); + None + } + }; + + let leasing_global = match (!is_software) + .then(|| DrmLeaseState::new::(dh, &dev_node)) + .transpose() + { + Ok(global) => global, + Err(err) => { + // TODO: replace with inspect_err, once stable + warn!( + ?err, + "Failed to initialize drm lease global for: {}", dev_node + ); + None + } + }; + + ReusableDevice { + leasing_global, + active_clients: HashSet::new(), + socket, + } + }); + + let drm = GbmDrmOutputManager::new( + drm, + GbmAllocator::new( + gbm.clone(), + GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT, + ), + GbmFramebufferExporter::new(gbm.clone(), render_node.into()), + Some(gbm.clone()), + [ + Fourcc::Abgr2101010, + Fourcc::Argb2101010, + Fourcc::Abgr8888, + Fourcc::Argb8888, + ], + render_formats, + ); + + Ok(Device { + drm, + inner: InnerDevice { + dev_node, + render_node, + is_software, + egl: None, + + outputs: HashMap::new(), + surfaces: HashMap::new(), + gbm, + + leased_connectors: Vec::new(), + leasing_global, + active_leases: Vec::new(), + active_clients, + }, + + texture_formats, + event_token: Some(token), + socket, + }) + } + + pub fn enumerate_surfaces(&mut self) -> Result { + // enumerate our outputs + let config = drm_helpers::display_configuration(self.drm.device_mut())?; + + let surfaces = self + .inner + .surfaces + .iter() + .map(|(c, s)| (s.connector, *c)) + .chain( + self.inner + .leased_connectors + .iter() + .map(|(conn, crtc)| (*conn, *crtc)), + ) + .collect::>(); + + let added = config + .iter() + .filter(|(conn, maybe)| match (surfaces.get(conn), maybe) { + (Some(current_crtc), Some(new_crtc)) => current_crtc != new_crtc, + // see `removed` + (Some(_), None) => true, + // if we already know about it, we don't consider it added + (None, _) => !self.inner.outputs.contains_key(conn), + }) + .map(|(conn, crtc)| (*conn, *crtc)) + .collect::>(); + + let removed = self + .inner + .outputs + .iter() + .filter(|(conn, _)| match config.get(conn) { + Some(Some(c)) => surfaces.get(conn).is_some_and(|crtc| c != crtc), + // if don't have a crtc, we need to drop the surface if it exists. + // so it needs to be in both `removed` AND `added`. + Some(None) => surfaces.contains_key(conn), + _ => true, + }) + .map(|(conn, _)| *conn) + .collect::>(); + + Ok(OutputChanges { added, removed }) + } + + pub fn lock(&mut self) -> LockedDevice<'_> { + LockedDevice { + inner: &mut self.inner, + drm: self.drm.lock(), + } + } + + fn reuse(self) -> (ReusableDevice, OldDeviceState) { + let device = ReusableDevice { + leasing_global: self.inner.leasing_global, + active_clients: self.inner.active_clients, + socket: self.socket, + }; + + let state = OldDeviceState { + fd: self.drm.device().device_fd().device_fd(), + outputs: self.inner.outputs, + leased_connectors: self + .inner + .leased_connectors + .iter() + .map(|(conn, _)| *conn) + .collect(), + }; + + (device, state) + } +} + +impl LockedDevice<'_> { + fn allow_frame_flags( + &mut self, + flag: bool, + flags: FrameFlags, + renderer: &mut GlMultiRenderer, + clock: &Clock, + shell: &Arc>, + ) -> Result<()> { + for surface in self.inner.surfaces.values_mut() { + surface.allow_frame_flags(flag, flags); + } + + if !flag { + let now = clock.now(); + let output_map = self + .inner + .surfaces + .iter() + .filter(|(_, s)| s.is_active()) + .map(|(crtc, surface)| (*crtc, surface.output.clone())) + .collect::>(); + + for (crtc, compositor) in self.drm.compositors().iter() { + let elements = match output_map.get(crtc) { + Some(output) => output_elements( + Some(&self.inner.render_node), + renderer, + shell, + now, + output, + CursorMode::All, + None, + None, + ) + .with_context(|| "Failed to render outputs")?, + None => Vec::new(), + }; + + let mut compositor = compositor.lock().unwrap(); + compositor.render_frame(renderer, &elements, CLEAR_COLOR, FrameFlags::empty())?; + if let Err(err) = compositor.commit_frame() + && !matches!(err, FrameError::EmptyFrame) + { + return Err(err.into()); + } + } + } + + Ok(()) + } + + pub fn allow_overlay_scanout( + &mut self, + flag: bool, + renderer: &mut GlMultiRenderer, + clock: &Clock, + shell: &Arc>, + ) -> Result<()> { + self.allow_frame_flags( + flag, + FrameFlags::ALLOW_OVERLAY_PLANE_SCANOUT, + renderer, + clock, + shell, + ) + } + + pub fn allow_primary_scanout_any( + &mut self, + flag: bool, + renderer: &mut GlMultiRenderer, + clock: &Clock, + shell: &Arc>, + ) -> Result<()> { + self.allow_frame_flags( + flag, + FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT_ANY, + renderer, + clock, + shell, + )?; + + for (crtc, comp) in self.drm.compositors() { + let Some(surface) = self.inner.surfaces.get_mut(crtc) else { + continue; + }; + let comp = comp.lock().unwrap(); + surface.primary_plane_formats = if flag { + comp.surface().plane_info().formats.clone() + } else { + // This certainly isn't perfect and might still miss the happy path, + // but it is surprisingly difficult to hack an api into smithay, + // to get the actual framebuffer format + let code = comp.format(); + FormatSet::from_iter(comp.modifiers().iter().map(|mo| Format { + code, + modifier: *mo, + })) + }; + surface.feedback.clear(); + } + + Ok(()) + } +} + +impl InnerDevice { + pub fn in_use(&self, primary: Option<&DrmNode>) -> bool { + Some(&self.render_node) == primary + || !self.surfaces.is_empty() + || !self.active_clients.is_empty() + } + + pub fn connector_added( + &mut self, + drm: &mut DrmDevice, + primary_node: Arc>>, + conn: connector::Handle, + maybe_crtc: Option, + position: (u32, u32), + evlh: &LoopHandle<'static, State>, + screen_filter: ScreenFilter, + shell: Arc>, + startup_done: Arc, + ) -> Result<(Output, bool)> { + let output = self + .outputs + .get(&conn) + .cloned() + .map(Ok) + .unwrap_or_else(|| create_output_for_conn(drm, conn)) + .context("Failed to create `Output`")?; + + let non_desktop = match drm_helpers::get_property_val(drm, conn, "non-desktop") { + Ok((val_type, value)) => val_type.convert_value(value).as_boolean().unwrap(), + Err(err) => { + warn!( + ?err, + "Failed to determine if connector is meant desktop usage, assuming so." + ); + false + } + }; + + if non_desktop { + if let Some(crtc) = maybe_crtc { + self.leased_connectors.push((conn, crtc)); + info!( + "Connector {} is non-desktop, setting up for leasing", + output.name() + ); + if let Some(lease_state) = self.leasing_global.as_mut() { + let physical = output.physical_properties(); + lease_state.add_connector::( + conn, + output.name(), + format!("{} {}", physical.make, physical.model), + ); + } + } else { + warn!( + "Connector {} is non-desktop, but we don't have a free crtc: not leasing", + output.name() + ); + } + + Ok((output, false)) + } else { + let new_config = output + .user_data() + .insert_if_missing(|| RefCell::new(OutputConfig::default())); + + populate_modes(drm, &output, conn, new_config, position) + .with_context(|| "Failed to enumerate connector modes")?; + + let has_surface = if let Some(crtc) = maybe_crtc { + match Surface::new( + &output, + crtc, + conn, + primary_node, + self.dev_node, + self.render_node, + evlh, + screen_filter, + shell, + startup_done, + ) { + Ok(data) => { + self.surfaces.insert(crtc, data); + true + } + Err(err) => { + error!(?crtc, "Failed to initialize surface: {}", err); + false + } + } + } else { + false + }; + + if !has_surface { + output + .user_data() + .get::>() + .unwrap() + .borrow_mut() + .enabled = OutputState::Disabled; + } + + Ok((output, true)) + } + } + + pub fn update_egl( + &mut self, + primary_node: Option<&DrmNode>, + api: &mut GbmGlowBackend, + ) -> Result { + if self.in_use(primary_node) { + if self.egl.is_none() { + let egl = init_egl(&self.gbm).context("Failed to create EGL context")?; + let mut renderer = unsafe { + GlowRenderer::new( + EGLContext::new_shared_with_priority( + &egl.display, + &egl.context, + ContextPriority::High, + ) + .context("Failed to create shared EGL context")?, + ) + .context("Failed to create GL renderer")? + }; + init_shaders(renderer.borrow_mut()).context("Failed to compile shaders")?; + api.add_node( + self.render_node, + GbmAllocator::new( + self.gbm.clone(), + // SCANOUT because stride bugs + GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT, + ), + renderer, + ); + self.egl = Some(egl); + } + Ok(true) + } else { + if self.egl.is_some() { + let _ = self.egl.take(); + api.remove_node(&self.render_node); + } + Ok(false) + } + } + + pub fn update_surface_nodes<'b>( + &mut self, + used_devices: &HashSet, + others: &[&'b mut Self], + ) -> Result<()> + where + Self: 'b, + { + for surface in self.surfaces.values_mut() { + let known_nodes = surface.known_nodes().clone(); + for gone_device in known_nodes.difference(used_devices) { + surface.remove_node(*gone_device); + } + for new_device in used_devices.difference(&known_nodes) { + let (render_node, egl, gbm) = if self.render_node == *new_device { + // we need to make sure to do partial borrows here, as device.surfaces is borrowed mutable + ( + self.render_node, + self.egl.as_ref().unwrap(), + self.gbm.clone(), + ) + } else { + let device = others + .iter() + .find(|d| d.render_node == *new_device) + .unwrap(); + ( + device.render_node, + device.egl.as_ref().unwrap(), + device.gbm.clone(), + ) + }; + + surface.add_node( + render_node, + GbmAllocator::new(gbm, GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT), + EGLContext::new_shared_with_priority( + &egl.display, + &egl.context, + ContextPriority::High, + ) + .context("Failed to create shared EGL context")?, + ); + } + } + + Ok(()) + } +} + +fn create_output_for_conn(drm: &mut DrmDevice, conn: connector::Handle) -> Result { + let conn_info = drm + .get_connector(conn, false) + .with_context(|| "Failed to query connector info")?; + let interface = drm_helpers::interface_name(drm, conn)?; + let edid_info = drm_helpers::edid_info(drm, conn) + .inspect_err(|err| warn!(?err, "failed to get EDID for {}", interface)) + .ok(); + let (phys_w, phys_h) = conn_info.size().unwrap_or((0, 0)); + + let output = Output::new( + interface, + PhysicalProperties { + size: (phys_w as i32, phys_h as i32).into(), + subpixel: match conn_info.subpixel() { + connector::SubPixel::HorizontalRgb => Subpixel::HorizontalRgb, + connector::SubPixel::HorizontalBgr => Subpixel::HorizontalBgr, + connector::SubPixel::VerticalRgb => Subpixel::VerticalRgb, + connector::SubPixel::VerticalBgr => Subpixel::VerticalBgr, + connector::SubPixel::None => Subpixel::None, + _ => Subpixel::Unknown, + }, + make: edid_info + .as_ref() + .and_then(|info| info.make()) + .unwrap_or_else(|| String::from("Unknown")), + model: edid_info + .as_ref() + .and_then(|info| info.model()) + .unwrap_or_else(|| String::from("Unknown")), + serial_number: edid_info + .as_ref() + .and_then(|info| info.serial()) + .unwrap_or_else(|| String::from("Unknown")), + }, + ); + if let Some(edid) = edid_info.as_ref().and_then(|x| x.edid()) { + output + .user_data() + .insert_if_missing(|| EdidProduct::from(edid.vendor_product())); + } + Ok(output) +} + +fn populate_modes( + drm: &mut DrmDevice, + output: &Output, + conn: connector::Handle, + new_config: bool, + position: (u32, u32), +) -> Result<()> { + let conn_info = drm.get_connector(conn, false)?; + let max_bpc = drm_helpers::get_max_bpc(drm, conn)?.map(|(_val, range)| range.end.min(16)); + let Some(mode) = conn_info + .modes() + .iter() + .find(|mode| mode.mode_type().contains(ModeTypeFlags::PREFERRED)) + .copied() + .or(conn_info.modes().first().copied()) + else { + anyhow::bail!("No mode found"); + }; + + let refresh_rate = drm_helpers::calculate_refresh_rate(mode); + let output_mode = OutputMode { + size: (mode.size().0 as i32, mode.size().1 as i32).into(), + refresh: refresh_rate as i32, + }; + + let mut modes = Vec::new(); + for mode in conn_info.modes() { + let refresh_rate = drm_helpers::calculate_refresh_rate(*mode); + let mode = OutputMode { + size: (mode.size().0 as i32, mode.size().1 as i32).into(), + refresh: refresh_rate as i32, + }; + modes.push(mode); + output.add_mode(mode); + } + for mode in output + .modes() + .into_iter() + .filter(|mode| !modes.contains(mode)) + { + output.delete_mode(mode); + } + output.set_preferred(output_mode); + + if new_config { + let scale = conn_info + .size() + .map(|size| calculate_scale(conn_info.interface(), size, mode.size())) + .unwrap_or(1.0); + let transform = drm_helpers::panel_orientation(drm, conn).unwrap_or(Transform::Normal); + output.change_current_state( + Some(output_mode), + Some(transform), + Some(Scale::Fractional(scale)), + Some(Point::from((position.0 as i32, position.1 as i32))), + ); + + let mut output_config = output + .user_data() + .get::>() + .unwrap() + .borrow_mut(); + *output_config = OutputConfig { + mode: ((output_mode.size.w, output_mode.size.h), Some(refresh_rate)), + position, + max_bpc, + scale, + transform: CompTransformDef::from(transform).0, + // Try opportunistic VRR by default, + // if not supported this will be turned off on `resume`, + // when we have the `Surface` to actually check for support. + vrr: AdaptiveSync::Enabled, + ..std::mem::take(&mut *output_config) + }; + } + + Ok(()) +} + +pub fn calculate_scale( + interface: connector::Interface, + monitor_size_mm: (u32, u32), + resolution: (u16, u16), +) -> f64 { + let (w_mm, h_mm) = monitor_size_mm; + let (w_px, h_px) = resolution; + let shorter_res = w_px.min(h_px); + let fallback_scale = match shorter_res { + px if px <= 1600 => 1.0, + _ => 2.0, + }; + if w_mm == 0 || h_mm == 0 { + // possibly projector, but could just be some no-brand display + return fallback_scale; + } + + let (w_in, h_in) = (w_mm as f64 / 25.4, h_mm as f64 / 25.4); + // due to edid's setting non-sensicle values, + // lets be really careful we don't devide by 0 (or non-normal values) when deriving values from size. + // (also the size should be positive, but the u16 arguments already force that.) + if !w_in.is_normal() || !h_in.is_normal() { + return fallback_scale; + } + + let diag = (w_in.powf(2.) * h_in.powf(2.)).sqrt(); + let dpi = (w_px as f64 / w_in + h_px as f64 / h_in) / 2.0; + + match diag { + _diag if diag < 20. || interface == connector::Interface::EmbeddedDisplayPort => { + // likely laptop + scale_from_dpi(dpi, 144, shorter_res) + } + // 16:10 has a height of 19.6, anything lower is most likely an ultrawide + _diag if diag >= 40. && h_in >= 19. => { + // likely TV + match shorter_res { + px if px <= 1200 => 1.0, + _ => 2.0, + } + } + _ => { + // likely desktop + scale_from_dpi(dpi, 120, shorter_res) + } + } +} + +pub fn scale_from_dpi(dpi: f64, step_size: u32, shorter_px: u16) -> f64 { + let scale = (dpi / step_size as f64) * 100.0; + + // snap to 50% + let scale = ((scale.round() as u32).next_multiple_of(50) as f64) / 100.0; + + // max values + let max = match shorter_px { + px if px <= 1200 => 1.0, // 125% is usually a worse experience than 100% atm + // for 1440p and weird variants we let the algorithm take over, but limit the max, this is inspired by what windows does + px if px <= 1600 => 1.5, + _ => 2.0, // never go higher than 200% by default though (Note for the future: Tablets? Phones?) + }; + + let min = match shorter_px { + px if px >= 2000 => 2.0, // 4k should default to 200%, as we prefer integer scales + _ => 1.0, // never go lower than 100% + }; + + scale.min(max).max(min) +} + +#[cfg(test)] +mod test { + use super::{calculate_scale, connector::Interface}; + + #[test] + fn test_scale() { + fn scale(interface: Interface, diag_in: f64, w_px: u16, h_px: u16) -> i32 { + let aspect = (w_px as f64) / (h_px as f64); + let diag_mm = diag_in * 25.4; + let h_mm = diag_mm / (aspect.powf(2.0) + 1.0).sqrt(); + let w_mm = h_mm * aspect; + (calculate_scale(interface, (w_mm as u32, h_mm as u32), (w_px, h_px)) * 100.0) as i32 + } + + // Laptops + for &iface in &[Interface::EmbeddedDisplayPort, Interface::LVDS] { + // 14 inch 3:2 + assert_eq!(scale(iface, 14.0, 3000, 2000), 200); + + // Various sizes 16:9 + for &diag_in in &[14.0, 15.6, 17.3] { + assert_eq!(scale(iface, diag_in, 1920, 1080), 100); + assert_eq!(scale(iface, diag_in, 2560, 1440), 150); + assert_eq!(scale(iface, diag_in, 3840, 2160), 200); + } + + // Various sizes 16:10 + for &diag_in in &[14.0, 16.0] { + assert_eq!(scale(iface, diag_in, 1920, 1200), 100); + assert_eq!(scale(iface, diag_in, 2560, 1600), 150); + assert_eq!(scale(iface, diag_in, 3840, 2400), 200); + } + } + + // Desktops + for &iface in &[Interface::DisplayPort, Interface::HDMIA, Interface::HDMIB] { + // 24 inch 16:9 + assert_eq!(scale(iface, 24.0, 1920, 1080), 100); + assert_eq!(scale(iface, 24.0, 2560, 1440), 150); + assert_eq!(scale(iface, 24.0, 3840, 2160), 200); + + // Larger sizes 16:9 + for &diag_in in &[27.0, 32.0, 38.0] { + assert_eq!(scale(iface, diag_in, 1920, 1080), 100); + assert_eq!(scale(iface, diag_in, 2560, 1440), 100); + assert_eq!(scale(iface, diag_in, 3840, 2160), 200); + } + + // Smaller sizes 21:9 + for &diag_in in &[26.0, 29.0] { + assert_eq!(scale(iface, diag_in, 2560, 1080), 100); + assert_eq!(scale(iface, diag_in, 3440, 1440), 150); + assert_eq!(scale(iface, diag_in, 5120, 2160), 200); + } + + // Larger sizes 21:9 + for &diag_in in &[34.0, 45.0] { + assert_eq!(scale(iface, diag_in, 2560, 1080), 100); + assert_eq!(scale(iface, diag_in, 3440, 1440), 100); + assert_eq!(scale(iface, diag_in, 5120, 2160), 200); + } + + // Various sizes 32:9 + for &diag_in in &[45.0, 49.0, 57.0] { + assert_eq!(scale(iface, diag_in, 3840, 1080), 100); + assert_eq!(scale(iface, diag_in, 5120, 1440), 100); + assert_eq!(scale(iface, diag_in, 7680, 2160), 200); + } + } + + // TVs + for &iface in &[Interface::DisplayPort, Interface::HDMIA, Interface::HDMIB] { + // Various sizes 16:9 + for &diag_in in &[40.0, 42.0, 48.0, 55.0, 65.0, 77.0, 83.0] { + assert_eq!(scale(iface, diag_in, 1920, 1080), 100); + assert_eq!(scale(iface, diag_in, 2560, 1440), 200); + assert_eq!(scale(iface, diag_in, 3840, 2160), 200); + } + } + + // Zero sized displays (projectors, invalid EDID) + for &iface in &[Interface::DisplayPort, Interface::HDMIA, Interface::HDMIB] { + assert_eq!(scale(iface, 0.0, 1920, 1080), 100); + assert_eq!(scale(iface, 0.0, 2560, 1440), 100); + assert_eq!(scale(iface, 0.0, 3840, 2160), 200); + } + } +} diff --git a/src/backend/kms/drm_helpers.rs b/src/backend/kms/drm_helpers.rs new file mode 100644 index 0000000..4665961 --- /dev/null +++ b/src/backend/kms/drm_helpers.rs @@ -0,0 +1,354 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use anyhow::{Context, Result, anyhow}; +use libdisplay_info::{edid::DisplayDescriptorTag, info::Info}; +use smithay::{ + backend::drm::DrmDevice, + reexports::drm::control::{ + AtomicCommitFlags, Device as ControlDevice, Mode, ModeFlags, PlaneType, ResourceHandle, + atomic::AtomicModeReq, + connector::{self, State as ConnectorState}, + crtc, + dumbbuffer::DumbBuffer, + property, + }, + utils::Transform, +}; +use std::{collections::HashMap, ops::Range}; + +pub fn display_configuration( + device: &mut DrmDevice, +) -> Result>> { + let res_handles = device.resource_handles()?; + let connectors = res_handles.connectors(); + + let mut map = HashMap::new(); + let mut cleanup = Vec::new(); + + // We expect the previous running drm master (likely the login mananger) + // to leave the drm device in a sensible state. + // That means, to reduce flickering, we try to keep an established mapping. + for conn in connectors + .iter() + .flat_map(|conn| device.get_connector(*conn, true).ok()) + { + if let Some(enc) = conn.current_encoder() + && let Some(crtc) = device.get_encoder(enc)?.crtc() + { + // If is is connected we found a mapping + if conn.state() == ConnectorState::Connected { + map.insert(conn.handle(), Some(crtc)); + // If not, the user just unplugged something, + // or the drm master did not cleanup? + // Well, I guess we cleanup after them. + } else { + cleanup.push(crtc); + } + } + } + + // But just in case we try to match all remaining connectors. + for conn in connectors + .iter() + .flat_map(|conn| device.get_connector(*conn, false).ok()) + .filter(|conn| conn.state() == ConnectorState::Connected) + .filter(|conn| !map.contains_key(&conn.handle())) + .collect::>() + .iter() + { + 'outer: for encoder_info in conn + .encoders() + .iter() + .flat_map(|encoder_handle| device.get_encoder(*encoder_handle)) + { + for crtc in res_handles.filter_crtcs(encoder_info.possible_crtcs()) { + if !map.values().any(|v| *v == Some(crtc)) { + map.insert(conn.handle(), Some(crtc)); + break 'outer; + } + } + } + + map.entry(conn.handle()).or_insert(None); + } + + // And then cleanup + if device.is_atomic() { + let mut req = AtomicModeReq::new(); + let mut has_changes = false; + let plane_handles = device.plane_handles()?; + + // We cannot just shortcut and use the legacy api for all cleanups because of this. + // (Technically a device does not need to be atomic for planes to be used, but nobody does this otherwise.) + for plane in plane_handles { + let info = device.get_plane(plane)?; + if let Some(crtc) = info.crtc() { + let is_primary = get_property_val(device, plane, "type").map( + |(val_type, val)| match val_type.convert_value(val) { + property::Value::Enum(Some(val)) => { + val.value() == PlaneType::Primary as u64 + } + _ => false, + }, + )?; + if !is_primary && !cleanup.contains(&crtc) { + let crtc_id = get_prop(device, plane, "CRTC_ID")?; + let fb_id = get_prop(device, plane, "FB_ID")?; + req.add_property(plane, crtc_id, property::Value::CRTC(None)); + req.add_property(plane, fb_id, property::Value::Framebuffer(None)); + has_changes = true; + } + } + } + // Skip an empty commit: a no-op modeset that also fails with EPERM + // without DRM master (e.g. a render-only secondary GPU). + if has_changes { + device.atomic_commit(AtomicCommitFlags::ALLOW_MODESET, req)?; + } + } else { + for crtc in res_handles.crtcs() { + #[allow(deprecated)] + let _ = device.set_cursor(*crtc, Option::<&DumbBuffer>::None); + } + } + disable_crtcs(device, &cleanup)?; + + Ok(map) +} + +/// Disables the given CRTCs and detaches their connectors/planes. +pub fn disable_crtcs(device: &mut DrmDevice, crtcs: &[crtc::Handle]) -> Result<()> { + if crtcs.is_empty() { + return Ok(()); + } + + let res_handles = device.resource_handles()?; + + if device.is_atomic() { + let mut req = AtomicModeReq::new(); + + for conn in res_handles + .connectors() + .iter() + .flat_map(|conn| device.get_connector(*conn, false).ok()) + .filter(|conn| { + if let Some(enc) = conn.current_encoder() + && let Ok(enc) = device.get_encoder(enc) + && let Some(crtc) = enc.crtc() + { + return crtcs.contains(&crtc); + } + false + }) + .map(|info| info.handle()) + { + let crtc_id = get_prop(device, conn, "CRTC_ID")?; + req.add_property(conn, crtc_id, property::Value::CRTC(None)); + } + + for plane in device.plane_handles()? { + let info = device.get_plane(plane)?; + if let Some(crtc) = info.crtc() + && crtcs.contains(&crtc) + { + let crtc_id = get_prop(device, plane, "CRTC_ID")?; + let fb_id = get_prop(device, plane, "FB_ID")?; + req.add_property(plane, crtc_id, property::Value::CRTC(None)); + req.add_property(plane, fb_id, property::Value::Framebuffer(None)); + } + } + + for crtc in crtcs { + let mode_id = get_prop(device, *crtc, "MODE_ID")?; + let active = get_prop(device, *crtc, "ACTIVE")?; + req.add_property(*crtc, active, property::Value::Boolean(false)); + req.add_property(*crtc, mode_id, property::Value::Unknown(0)); + } + + device.atomic_commit(AtomicCommitFlags::ALLOW_MODESET, req)?; + } else { + for crtc in crtcs { + // null commit (necessary to trigger removal on the kernel side with the legacy api.) + let _ = device.set_crtc(*crtc, None, (0, 0), &[], None); + } + } + + Ok(()) +} + +pub fn interface_name(device: &impl ControlDevice, connector: connector::Handle) -> Result { + let conn_info = device.get_connector(connector, false)?; + + let other_short_name; + let interface_short_name = match conn_info.interface() { + connector::Interface::DVII => "DVI-I", + connector::Interface::DVID => "DVI-D", + connector::Interface::DVIA => "DVI-A", + connector::Interface::SVideo => "S-VIDEO", + connector::Interface::DisplayPort => "DP", + connector::Interface::HDMIA => "HDMI-A", + connector::Interface::HDMIB => "HDMI-B", + connector::Interface::EmbeddedDisplayPort => "eDP", + other => { + other_short_name = format!("{:?}", other); + &other_short_name + } + }; + + Ok(format!( + "{}-{}", + interface_short_name, + conn_info.interface_id() + )) +} + +pub fn edid_info(device: &impl ControlDevice, connector: connector::Handle) -> Result { + let edid_prop = get_prop(device, connector, "EDID")?; + let edid_info = device.get_property(edid_prop)?; + + let mut edid = None; + let props = device.get_properties(connector)?; + let (ids, vals) = props.as_props_and_values(); + for (&id, &val) in ids.iter().zip(vals.iter()) { + if id == edid_prop { + if let property::Value::Blob(edid_blob) = edid_info.value_type().convert_value(val) { + let blob = device.get_property_blob(edid_blob)?; + edid = Some(Info::parse_edid(&blob).context("Unable to parse edid")?); + } + break; + } + } + + edid.ok_or(anyhow!("No EDID found")) +} + +pub fn get_prop( + device: &impl ControlDevice, + handle: impl ResourceHandle, + name: &str, +) -> Result { + let props = device.get_properties(handle)?; + let (prop_handles, _) = props.as_props_and_values(); + for prop in prop_handles { + let info = device.get_property(*prop)?; + if Some(name) == info.name().to_str().ok() { + return Ok(*prop); + } + } + anyhow::bail!("No prop found for {}", name) +} + +pub fn get_property_val( + device: &impl ControlDevice, + handle: impl ResourceHandle, + name: &str, +) -> Result<(property::ValueType, property::RawValue)> { + let props = device.get_properties(handle)?; + let (prop_handles, values) = props.as_props_and_values(); + for (&prop, &val) in prop_handles.iter().zip(values.iter()) { + let info = device.get_property(prop)?; + if Some(name) == info.name().to_str().ok() { + let val_type = info.value_type(); + return Ok((val_type, val)); + } + } + anyhow::bail!("No prop found for {}", name) +} + +// Returns refresh rate in milliherz +pub fn calculate_refresh_rate(mode: Mode) -> u32 { + let htotal = mode.hsync().2 as u32; + let vtotal = mode.vsync().2 as u32; + let mut refresh = + (mode.clock() as u64 * 1000000_u64 / htotal as u64 + vtotal as u64 / 2) / vtotal as u64; + + if mode.flags().contains(ModeFlags::INTERLACE) { + refresh *= 2; + } + if mode.flags().contains(ModeFlags::DBLSCAN) { + refresh /= 2; + } + if mode.vscan() > 1 { + refresh /= mode.vscan() as u64; + } + + refresh as u32 +} + +pub fn get_minimum_refresh_rate( + device: &impl ControlDevice, + connector: connector::Handle, +) -> Result> { + let info = edid_info(device, connector)?; + let edid = info.edid().context("EDID lacking into")?; + for descriptor in edid.display_descriptors() { + if descriptor.tag() == DisplayDescriptorTag::RangeLimits { + return Ok(Some( + descriptor + .range_limits() + .context("Invalid range limits descriptor")? + .min_vert_rate_hz as u32, + )); + } + } + + Ok(None) +} + +pub fn get_max_bpc( + dev: &impl ControlDevice, + conn: connector::Handle, +) -> Result)>> { + let Some(handle) = get_prop(dev, conn, "max bpc").ok() else { + return Ok(None); + }; + + let info = dev.get_property(handle)?; + let range = match info.value_type() { + property::ValueType::UnsignedRange(x, y) => (x as u32)..(y as u32), + _ => return Err(anyhow!("max bpc has wrong value type")), + }; + + let value = get_property_val(dev, conn, "max bpc").map(|(val_type, val)| { + match val_type.convert_value(val) { + property::Value::UnsignedRange(res) => res as u32, + _ => unreachable!(), + } + })?; + + Ok(Some((value, range))) +} + +pub fn set_max_bpc(dev: &impl ControlDevice, conn: connector::Handle, bpc: u32) -> Result { + let (_, range) = + get_max_bpc(dev, conn)?.ok_or(anyhow!("max bpc does not exist for connector"))?; + dev.set_property( + conn, + get_prop(dev, conn, "max bpc")?, + property::Value::UnsignedRange(bpc.clamp(range.start, range.end) as u64).into(), + ) + .map_err(Into::::into) + .and_then(|_| get_property_val(dev, conn, "max bpc")) + .map(|(val_type, val)| match val_type.convert_value(val) { + property::Value::UnsignedRange(val) => val as u32, + _ => unreachable!(), + }) +} + +pub fn panel_orientation(dev: &impl ControlDevice, conn: connector::Handle) -> Result { + let (val_type, val) = get_property_val(dev, conn, "panel orientation")?; + match val_type.convert_value(val) { + property::Value::Enum(Some(val)) => match val.value() { + // "Normal" + 0 => Ok(Transform::Normal), + // "Upside Down" + 1 => Ok(Transform::_180), + // "Left Side Up" + 2 => Ok(Transform::_90), + // "Right Side Up" + 3 => Ok(Transform::_270), + _ => Err(anyhow!("panel orientation has invalid value '{:?}'", val)), + }, + _ => Err(anyhow!("panel orientation has wrong value type")), + } +} diff --git a/src/backend/kms/mod.rs b/src/backend/kms/mod.rs new file mode 100644 index 0000000..16559e4 --- /dev/null +++ b/src/backend/kms/mod.rs @@ -0,0 +1,1178 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{ + config::{CompOutputConfig, ScreenFilter}, + shell::Shell, + state::BackendData, + utils::{env::dev_var, prelude::*}, + wayland::protocols::output_power::OutputPowerState, +}; + +use anyhow::{Context, Result}; +use calloop::LoopSignal; +use cosmic_comp_config::output::comp::{AdaptiveSync, OutputState}; +use indexmap::IndexMap; +use render::gles::GbmGlowBackend; +use smithay::{ + backend::{ + allocator::{Buffer, dmabuf::Dmabuf, format::FormatSet}, + drm::{DrmDeviceFd, DrmNode, NodeType, VrrSupport, output::DrmOutputRenderElements}, + egl::{EGLContext, EGLDevice, EGLDisplay}, + input::InputEvent, + libinput::{LibinputInputBackend, LibinputSessionInterface}, + renderer::{glow::GlowRenderer, multigpu::GpuManager}, + session::{Event as SessionEvent, Session, libseat::LibSeatSession}, + udev::{UdevBackend, UdevEvent, primary_gpu}, + }, + output::Output, + reexports::{ + calloop::{Dispatcher, EventLoop, LoopHandle}, + drm::{ + Device as _, + control::{Device as _, connector::Interface, crtc}, + }, + input::{self, Libinput}, + wayland_server::{Client, DisplayHandle}, + }, + utils::{Clock, DevPath, Monotonic, Size}, + wayland::{ + dmabuf::DmabufGlobal, + drm_syncobj::{DrmSyncobjState, supports_syncobj_eventfd}, + relative_pointer::RelativePointerManagerState, + }, +}; +use surface::GbmDrmOutput; +use tracing::{debug, error, info, warn}; + +use std::{ + collections::{HashMap, HashSet}, + path::Path, + sync::{Arc, RwLock, atomic::AtomicBool}, +}; + +mod device; +mod drm_helpers; +pub mod render; +mod socket; +mod surface; +use device::*; +pub(crate) use surface::Surface; +pub use surface::Timings; + +use super::render::{CLEAR_COLOR, CursorMode, output_elements}; + +#[derive(Debug)] +pub struct KmsState { + pub drm_devices: IndexMap, + pub input_devices: HashMap, + pub primary_node: Arc>>, + // Mesa llvmpipe renderer, if supported and there are no render nodes + pub software_renderer: Option, + pub api: GpuManager>, + + pub session: LibSeatSession, + libinput: Libinput, + + pub syncobj_state: Option, +} + +pub struct KmsGuard<'a> { + pub drm_devices: IndexMap>, + pub primary_node: Arc>>, + api: &'a mut GpuManager>, + session: &'a LibSeatSession, +} + +pub fn init_backend( + dh: &DisplayHandle, + event_loop: &mut EventLoop<'static, State>, + state: &mut State, +) -> Result<()> { + // establish session + let (session, notifier) = LibSeatSession::new().context("Failed to acquire session")?; + + // setup input + let libinput_context = init_libinput(dh, &session, &event_loop.handle()) + .context("Failed to initialize libinput backend")?; + + // watch for gpu events + let udev_dispatcher = init_udev(session.seat(), &event_loop.handle()) + .context("Failed to initialize udev connection")?; + + // handle session events + let loop_signal = event_loop.get_signal(); + let dispatcher = udev_dispatcher.clone(); + event_loop + .handle() + .insert_source(notifier, move |event, &mut (), state| match event { + SessionEvent::ActivateSession => { + state.resume_session( + dispatcher.clone(), + state.common.event_loop_handle.clone(), + loop_signal.clone(), + ); + } + SessionEvent::PauseSession => { + state.pause_session(); + } + }) + .map_err(|err| err.error) + .context("Failed to initialize session event source")?; + + // finish backend initialization + state.backend = BackendData::Kms(KmsState { + drm_devices: IndexMap::new(), + input_devices: HashMap::new(), + primary_node: Arc::new(RwLock::new(None)), + software_renderer: None, + api: GpuManager::new(GbmGlowBackend::new()).context("Failed to initialize gpu backend")?, + + session, + libinput: libinput_context, + + syncobj_state: None, + }); + + // manually add already present gpus + let mut outputs = Vec::new(); + for (dev, path) in udev_dispatcher.as_source_ref().device_list() { + match state.device_added(dev, path, dh) { + Ok(added) => outputs.extend(added), + Err(err) => warn!("Failed to add device {}: {:?}", path.display(), err), + } + } + + if let Err(err) = state.backend.kms().select_primary_gpu(dh) { + warn!("Failed to determine primary gpu: {}", err); + } + + if let Err(err) = state.refresh_output_config() { + info!( + ?err, + "Couldn't enable all found outputs, trying to disable outputs." + ); + if let Some(pos) = outputs + .iter() + .position(|o| o.is_internal()) + .or((!outputs.is_empty()).then_some(0)) + { + for (i, output) in outputs.iter().enumerate() { + output.config_mut().enabled = if i == pos { + OutputState::Enabled + } else { + OutputState::Disabled + }; + } + if let Err(err) = state.refresh_output_config() { + error!("Couldn't enable any output: {}", err); + } + } + } + + if state.common.with_xwayland { + // start x11 + let primary = *state.backend.kms().primary_node.read().unwrap(); + state.launch_xwayland(primary); + } else { + state.notify_ready(); + } + + Ok(()) +} + +fn init_libinput( + dh: &DisplayHandle, + session: &LibSeatSession, + evlh: &LoopHandle<'static, State>, +) -> Result { + let mut libinput_context = + Libinput::new_with_udev::>(session.clone().into()); + libinput_context + .udev_assign_seat(&session.seat()) + .map_err(|_| anyhow::anyhow!("Failed to assign seat to libinput"))?; + let libinput_backend = LibinputInputBackend::new(libinput_context.clone()); + + evlh.insert_source(libinput_backend, move |mut event, _, state| { + if let InputEvent::DeviceAdded { device } = &mut event { + state.common.config.read_device(device); + state + .backend + .kms() + .input_devices + .insert(device.name().into(), device.clone()); + } else if let InputEvent::DeviceRemoved { device } = &event { + state.backend.kms().input_devices.remove(&*device.name()); + } + + state.process_input_event(event); + + for output in state.common.shell.read().outputs() { + state.backend.kms().schedule_render(output); + } + }) + .map_err(|err| err.error) + .context("Failed to initialize libinput event source")?; + + // Create relative pointer global + RelativePointerManagerState::new::(dh); + + Ok(libinput_context) +} + +fn determine_boot_gpu(seat: String) -> Option { + let primary_node = primary_gpu(&seat) + .ok() + .flatten() + .and_then(|x| DrmNode::from_path(x).ok()); + primary_node.and_then(|x| x.node_with_type(NodeType::Render).and_then(Result::ok)) +} + +fn determine_primary_gpu( + drm_devices: &IndexMap, + seat: String, +) -> Result> { + if let Some(device) = dev_var("COSMIC_RENDER_DEVICE") + && let Some(node) = drm_devices.values().find_map(|dev| { + device + .matches(&dev.inner.render_node) + .then_some(dev.inner.render_node) + }) + { + return Ok(Some(node)); + } + + // try to find builtin display + for dev in drm_devices.values() { + if dev.inner.surfaces.values().any(|s| { + if let Ok(conn_info) = dev.drm.device().get_connector(s.connector, false) { + let i = conn_info.interface(); + i == Interface::EmbeddedDisplayPort || i == Interface::LVDS || i == Interface::DSI + } else { + false + } + }) { + return Ok(Some(dev.inner.render_node)); + } + } + + // else try to find the boot gpu + let boot = determine_boot_gpu(seat); + if let Some(boot) = boot + && drm_devices + .values() + .any(|dev| dev.inner.render_node == boot) + { + return Ok(Some(boot)); + } + + // else just take the first + Ok(drm_devices + .values() + .next() + .filter(|dev| !dev.inner.is_software) + .map(|dev| dev.inner.render_node)) +} + +/// Create `GlowRenderer` for `EGL_MESA_device_software` device, if present +fn software_renderer() -> anyhow::Result { + let mut devices = EGLDevice::enumerate()?; + let device = devices + .find(|device| { + device + .extensions() + .iter() + .any(|ext| ext == "EGL_MESA_device_software") + }) + .ok_or_else(|| anyhow::anyhow!("no EGL device found with `EGL_MESA_device_software`"))?; + let display = unsafe { EGLDisplay::new(device)? }; + let context = EGLContext::new(&display)?; + unsafe { Ok(GlowRenderer::new(context)?) } +} + +fn init_udev( + seat: String, + evlh: &LoopHandle<'static, State>, +) -> Result> { + let udev_backend = UdevBackend::new(&seat)?; + + let dispatcher = Dispatcher::new(udev_backend, move |event, _, state: &mut State| { + let dh = state.common.display_handle.clone(); + match match event { + UdevEvent::Added { + device_id, + ref path, + } => state + .device_added(device_id, path, &dh) + .with_context(|| format!("Failed to add drm device: {}", device_id)), + UdevEvent::Changed { device_id } => state + .device_changed(device_id) + .with_context(|| format!("Failed to update drm device: {}", device_id)), + UdevEvent::Removed { device_id } => state + .device_removed(device_id, &dh) + .with_context(|| format!("Failed to remove drm device: {}", device_id)) + .map(|_| Vec::new()), + } { + Ok(added) => { + debug!("Successfully handled udev event."); + + { + let backend = state.backend.kms(); + if matches!(event, UdevEvent::Added { .. } | UdevEvent::Removed { .. }) + && backend.primary_node.read().unwrap().is_none() + && let Err(err) = state.backend.kms().select_primary_gpu(&dh) + { + warn!("Failed to determine a new primary gpu: {}", err); + } + } + + if let Err(err) = state.refresh_output_config() { + warn!("Unable to load output config: {}", err); + if !added.is_empty() { + for output in added { + output.config_mut().enabled = OutputState::Disabled; + } + if let Err(err) = state.refresh_output_config() { + error!("Unrecoverable config error: {}", err); + } + } + } + } + Err(err) => { + error!(?err, "Error while handling udev event.") + } + } + }); + + evlh.register_dispatcher(dispatcher.clone()) + .context("Failed to register udev event source")?; + + Ok(dispatcher) +} + +impl State { + fn resume_session( + &mut self, + dispatcher: Dispatcher<'static, UdevBackend, Self>, + loop_handle: LoopHandle<'static, State>, + loop_signal: LoopSignal, + ) { + let backend = self.backend.kms(); + + // recreate all graphics contexts + backend + .clear_used_devices() + .expect("This should never fail"); + if let Err(err) = backend.refresh_used_devices() { + warn!(?err, "Failed to re-create graphics contexts"); + } + + // resume input + if let Err(err) = backend.libinput.resume() { + error!(?err, "Failed to resume libinput context."); + } + // active drm, resume leases + for device in backend.drm_devices.values_mut() { + if let Err(err) = device.drm.lock().activate(true) { + error!(?err, "Failed to resume drm device"); + continue; + } + if let Some(lease_state) = device.inner.leasing_global.as_mut() { + lease_state.resume::(); + } + } + + // update state and schedule new render, + // after processing the rest of the pending event loop events + let dispatcher = dispatcher.clone(); + loop_handle.insert_idle(move |state| { + // add new devices, update devices now + let mut added = Vec::new(); + for (dev, path) in dispatcher.as_source_ref().device_list() { + let drm_node = match DrmNode::from_dev_id(dev) { + Ok(node) => node, + Err(err) => { + error!(?err, "Failed to read drm device {}.", path.display(),); + continue; + } + }; + let dh = state.common.display_handle.clone(); + + if state.backend.kms().drm_devices.contains_key(&drm_node) { + match state.device_changed(dev) { + Ok(outputs) => added.extend(outputs), + Err(err) => { + error!( + ?err, + "Failed to update drm device {}. Re-opening", + path.display(), + ); + match state.reopen_device(dev, path, &dh) { + Ok(outputs) => added.extend(outputs), + Err(err) => { + error!( + ?err, + "Failed to re-open drm device {}. Device lost", + path.display(), + ); + if let Err(err) = state.device_removed(dev, &dh) { + error!( + ?err, + "Failed to close drm device {}.", + path.display(), + ); + }; + } + } + } + } + } else { + match state.device_added(dev, path, &dh) { + Ok(outputs) => added.extend(outputs), + Err(err) => error!(?err, "Failed to add drm device {}.", path.display(),), + } + } + } + + // update outputs + if let Err(err) = state.refresh_output_config() { + warn!("Unable to load output config: {}", err); + if !added.is_empty() { + for output in added { + output.config_mut().enabled = OutputState::Disabled; + } + if let Err(err) = state.refresh_output_config() { + error!("Unrecoverable config error: {}", err); + } + } + } + + OutputPowerState::refresh(state); + state.common.refresh(); + }); + loop_signal.wakeup(); + } + + fn pause_session(&mut self) { + let backend = self.backend.kms(); + backend.libinput.suspend(); + for device in backend.drm_devices.values_mut() { + if let Some(lease_state) = device.inner.leasing_global.as_mut() { + lease_state.suspend(); + } + for surface in device.inner.surfaces.values_mut() { + surface.suspend(); + } + device.drm.pause(); + } + } +} + +impl KmsState { + fn select_primary_gpu(&mut self, dh: &DisplayHandle) -> Result<()> { + // We don't have to check the allow/blocklist here, + // as any disallowed devices won't be in `self.drm_devices`. + + let mut primary_node = self.primary_node.write().unwrap(); + let _ = primary_node.take(); // if we error don't leave an old node in place + *primary_node = determine_primary_gpu(&self.drm_devices, self.session.seat())?; + + if let Some(node) = *primary_node { + info!("Using {} as primary gpu for rendering.", node); + self.software_renderer.take(); + } else if self.software_renderer.is_none() { + info!("Failed to find a suitable gpu, using software renderingr"); + self.software_renderer = match software_renderer() { + Ok(renderer) => Some(renderer), + Err(err) => { + error!(?err, "Failed to initialize software EGL renderer."); + None + } + }; + } + + if !crate::utils::env::bool_var("COSMIC_DISABLE_SYNCOBJ").unwrap_or(false) { + if let Some(primary_node) = primary_node + .as_ref() + .and_then(|node| node.node_with_type(NodeType::Primary).and_then(|x| x.ok())) + && let Some(device) = self.drm_devices.get(&primary_node) + { + let import_device = device.drm.device().device_fd().clone(); + if supports_syncobj_eventfd(&import_device) { + if let Some(state) = self.syncobj_state.as_mut() { + state.update_device(import_device); + } else { + let syncobj_state = DrmSyncobjState::new::(dh, import_device); + self.syncobj_state = Some(syncobj_state); + } + return Ok(()); + } + } + + if let Some(old_state) = self.syncobj_state.take() { + dh.remove_global::(old_state.into_global()); + } + } + + Ok(()) + } + + pub fn switch_vt(&mut self, num: i32) -> Result<(), anyhow::Error> { + self.session.change_vt(num).map_err(Into::into) + } + + pub fn dmabuf_imported( + &mut self, + client: Option, + global: &DmabufGlobal, + dmabuf: Dmabuf, + ) -> Result { + let mut device = self + .drm_devices + .values_mut() + .find(|device| { + device + .socket + .as_ref() + .map(|s| &s.dmabuf_global == global) + .unwrap_or(false) + }) + .context("Couldn't find gpu for dmabuf global")?; + + // If device advertised to client doesn't support format/modifier, select + // first device that does. This is needed for image-copy from + // output/toplevel on a different node. + // + // TODO: After + // https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/268, + // only try the device specified explicitly by the client, if set. + if !device.texture_formats.contains(&dmabuf.format()) { + device = self + .drm_devices + .values_mut() + .find(|device| device.texture_formats.contains(&dmabuf.format())) + .context("Dmabuf cannot be imported on any gpu")?; + } + + let new_client = if let Some(client) = client { + let new = device.inner.active_clients.insert(client.id()); + device.inner.update_egl( + self.primary_node.read().unwrap().as_ref(), + self.api.as_mut(), + )? && new + } else { + false + }; + + let egl = device + .inner + .egl + .as_ref() + .context("EGL initialization Error")?; + egl.display + .create_image_from_dmabuf(&dmabuf) + .inspect(|image| unsafe { + smithay::backend::egl::ffi::egl::DestroyImageKHR( + **egl.display.get_display_handle(), + *image, + ); + }) + .context("Failed to create EGLImage from dmabuf")?; + + let node = device.inner.render_node; + dmabuf.set_node(node); + + if new_client { + self.refresh_used_devices()?; + } + + Ok(node) + } + + pub fn schedule_render(&mut self, output: &Output) { + for surface in self + .drm_devices + .values() + .flat_map(|d| d.inner.surfaces.values()) + .filter(|s| s.output == *output || s.output.mirroring().is_some_and(|o| &o == output)) + { + surface.schedule_render(); + } + } + + pub fn target_node_for_output(&self, output: &Output) -> Option { + self.drm_devices + .values() + .find(|dev| dev.inner.surfaces.values().any(|s| s.output == *output)) + .map(|dev| &dev.inner.render_node) + .copied() + } + + pub fn update_screen_filter(&mut self, screen_filter: &ScreenFilter) -> Result<()> { + for device in self.drm_devices.values_mut() { + for surface in device.inner.surfaces.values_mut() { + surface.set_screen_filter(screen_filter.clone()); + } + } + + // We don't expect this to fail in a meaningful way. + // The shader is already compiled at this point and we don't rely on any features, + // that might not be available for any filters we currently expose. + // + // But we might conditionally fail here in the future. + Ok(()) + } + + fn clear_used_devices(&mut self) -> Result<()> { + let primary_node = self.primary_node.read().unwrap(); + let empty_devices = HashSet::new(); + + for device in self.drm_devices.values_mut() { + if device.inner.egl.take().is_some() { + self.api.as_mut().remove_node(&device.inner.render_node); + device.inner.update_surface_nodes(&empty_devices, &[])?; + } + } + + // trigger re-evaluation... urgh + if let Some(primary_node) = primary_node.as_ref() { + let _ = self.api.single_renderer(primary_node); + } + + Ok(()) + } + + pub fn refresh_used_devices(&mut self) -> Result<()> { + let primary_node = self.primary_node.read().unwrap(); + let mut used_devices = HashSet::new(); + + for device in self.drm_devices.values_mut() { + if device + .inner + .update_egl(primary_node.as_ref(), self.api.as_mut())? + { + used_devices.insert(device.inner.render_node); + } + } + + // trigger re-evaluation... urgh + if let Some(primary_node) = primary_node.as_ref() { + let _ = self.api.single_renderer(primary_node); + } + + // I hate this. I want partial borrows of hashmap values + let all_devices = self + .drm_devices + .values() + .map(|d| d.inner.render_node) + .collect::>(); + for node in all_devices { + let (mut device, others) = self + .drm_devices + .values_mut() + .map(|d| &mut d.inner) + .partition::, _>(|d| d.render_node == node); + device[0].update_surface_nodes(&used_devices, &others)?; + } + + Ok(()) + } + + pub fn lock_devices(&mut self) -> KmsGuard<'_> { + KmsGuard { + drm_devices: self + .drm_devices + .iter_mut() + .map(|(node, device)| (*node, device.lock())) + .collect(), + primary_node: self.primary_node.clone(), + api: &mut self.api, + session: &self.session, + } + } +} + +impl KmsGuard<'_> { + pub fn schedule_render(&mut self, output: &Output) { + for surface in self + .drm_devices + .values() + .flat_map(|d| d.inner.surfaces.values()) + .filter(|s| s.output == *output || s.output.mirroring().is_some_and(|o| &o == output)) + { + surface.schedule_render(); + } + } + + pub fn refresh_used_devices(&mut self) -> Result<()> { + let primary_node = self.primary_node.read().unwrap(); + let mut used_devices = HashSet::new(); + + for device in self.drm_devices.values_mut() { + if device + .inner + .update_egl(primary_node.as_ref(), self.api.as_mut())? + { + used_devices.insert(device.inner.render_node); + } + } + + // trigger re-evaluation... urgh + if let Some(primary_node) = primary_node.as_ref() { + let _ = self.api.single_renderer(primary_node); + } + + // I hate this. I want partial borrows of hashmap values + let all_devices = self + .drm_devices + .values() + .map(|d| d.inner.render_node) + .collect::>(); + for node in all_devices { + let (mut device, others) = self + .drm_devices + .values_mut() + .map(|d| &mut *d.inner) + .partition::, _>(|d| d.render_node == node); + device[0].update_surface_nodes(&used_devices, &others)?; + } + + Ok(()) + } + + pub fn all_outputs(&self) -> Vec { + self.drm_devices + .values() + .flat_map(|device| { + device + .inner + .outputs + .iter() + .filter(|(conn, _)| { + !device + .inner + .leased_connectors + .iter() + .any(|(leased_conn, _)| *conn == leased_conn) + }) + .map(|(_, output)| output.clone()) + }) + .collect() + } + + pub fn apply_config_for_outputs( + &mut self, + test_only: bool, + loop_handle: &LoopHandle<'static, State>, + screen_filter: &ScreenFilter, + shell: Arc>, + startup_done: Arc, + clock: &Clock, + ) -> Result<(), anyhow::Error> { + if !self.session.is_active() { + return Ok(()); + } + + for device in self.drm_devices.values_mut() { + // we only want outputs exposed to wayland - not leased ones + // but that is also not all surface, because that doesn't contain all detected, but unmapped outputs + let outputs = device + .inner + .outputs + .iter() + .filter(|(conn, _)| { + !device + .inner + .leased_connectors + .iter() + .any(|(leased_conn, _)| *conn == leased_conn) + }) + .map(|(_, output)| output.clone()) + .collect::>(); + + let mut new_pairings = HashMap::new(); + + // figure out potential new crtcs + // TODO: Right now we always keep crtcs of already enabled outputs, + // even if another configuration could potentially enable more outputs + + let res_handles = device.drm.device().resource_handles()?; + let free_crtcs = res_handles + .crtcs() + .iter() + .filter(|crtc| { + !device.inner.surfaces.contains_key(crtc) + // TODO: We can't do this. See https://github.com/Smithay/smithay/pull/1820 + //.is_some_and(|surface| surface.output.is_enabled()) + }) + .copied() + .collect::>(); + let open_conns = outputs + .iter() + .filter(|output| { + output.is_enabled() + && !device.inner.surfaces.values().any(|s| &s.output == *output) + }) + .flat_map(|output| { + device + .inner + .outputs + .iter() + .find_map(|(conn, o)| (output == o).then_some(*conn)) + }); + + for conn in open_conns { + let conn_info = device + .drm + .device() + .get_connector(conn, false) + .with_context(|| { + format!( + "Failed to query drm device: {:?}", + device.drm.device().dev_path().as_deref().map(Path::display), + ) + })?; + 'outer: for encoder_info in conn_info + .encoders() + .iter() + .flat_map(|encoder_handle| device.drm.device().get_encoder(*encoder_handle)) + { + for crtc in res_handles.filter_crtcs(encoder_info.possible_crtcs()) { + if free_crtcs.contains(&crtc) { + new_pairings.insert(conn, crtc); + break 'outer; + } + } + } + + if !new_pairings.contains_key(&conn) { + // test failed, we don't have a crtc for conn + anyhow::bail!("Missing crtc for {conn:?}, gpu doesn't have enough resources."); + } + } + + // first drop old surfaces + if !test_only { + let mut disabled_crtcs = Vec::new(); + device.inner.surfaces.retain(|crtc, surface| { + if outputs + .iter() + .any(|o| !o.is_enabled() && surface.output == *o) + { + disabled_crtcs.push(*crtc); + false + } else { + true + } + }); + + if let Err(err) = + drm_helpers::disable_crtcs(device.drm.device_mut(), &disabled_crtcs) + { + warn!("Failed to disable crtcs for disabled outputs: {err}"); + } + } + + // add new ones + let mut w = shell.read().global_space().size.w as u32; + if !test_only { + for (conn, crtc) in new_pairings { + let (output, _) = device.inner.connector_added( + device.drm.device_mut(), + self.primary_node.clone(), + conn, + Some(crtc), + (w, 0), + loop_handle, + screen_filter.clone(), + shell.clone(), + startup_done.clone(), + )?; + if output.mirroring().is_none() { + w += output.geometry().size.w as u32; + } + } + } + } + + if !test_only { + self.refresh_used_devices() + .context("Failed to enable devices")?; + } + + for device in self.drm_devices.values_mut() { + let now = clock.now(); + let output_map = device + .inner + .surfaces + .iter() + .filter(|(_, s)| s.is_active()) + .map(|(crtc, surface)| (*crtc, surface.output.clone())) + .collect::>(); + + // reconfigure existing + for (crtc, surface) in device.inner.surfaces.iter_mut() { + let output_config = CompOutputConfig(surface.output.config()); + + let drm = &mut device.drm; + let conn = surface.connector; + let conn_info = drm.device().get_connector(conn, false)?; + let mode = conn_info + .modes() + .iter() + // match the size + .filter(|mode| { + let (x, y) = mode.size(); + Size::from((x as i32, y as i32)) == output_config.mode_size() + }) + // and then select the closest refresh rate (e.g. to match 59.98 as 60) + .min_by_key(|mode| { + let refresh_rate = drm_helpers::calculate_refresh_rate(**mode); + (output_config.0.mode.1.unwrap() as i32 - refresh_rate as i32).abs() + }) + .ok_or(anyhow::anyhow!("Unable to find matching mode"))?; + + if !test_only { + if !surface.is_active() { + let mut planes = drm + .device() + .planes(crtc) + .with_context(|| "Failed to enumerate planes")?; + + let driver = drm.device().get_driver().ok(); + + // QUIRK: Using an overlay plane on a nvidia card breaks the display controller (wtf...) + if driver.as_ref().is_some_and(|driver| { + driver + .name() + .to_string_lossy() + .to_lowercase() + .contains("nvidia") + }) { + planes.overlay = vec![]; + } + // QUIRK: Cursor planes on evdi sometimes don't disappear correctly. + // TODO: Debug and figure out, as they can be a nice improvement. + if driver.as_ref().is_some_and(|driver| { + driver + .name() + .to_string_lossy() + .to_lowercase() + .contains("evdi") + }) { + planes.cursor = vec![]; + } + + let compositor: GbmDrmOutput = { + let mut renderer = self + .api + .single_renderer(&device.inner.render_node) + .with_context(|| "Failed to create renderer")?; + + let mut elements = DrmOutputRenderElements::default(); + for (crtc, output) in output_map.iter() { + let output_elements = output_elements( + Some(&device.inner.render_node), + &mut renderer, + &shell, + now, + output, + CursorMode::All, + None, + None, + ) + .with_context(|| "Failed to render outputs")?; + + elements.add_output(crtc, CLEAR_COLOR, output_elements); + } + + let compositor = drm + .initialize_output( + *crtc, + *mode, + &[conn], + &surface.output, + Some(planes.clone()), + &mut renderer, + &elements, + ) + .with_context(|| "Failed to create drm surface")?; + + let _ = renderer; + + compositor + }; + + if let Some(bpc) = output_config.0.max_bpc + && let Err(err) = drm_helpers::set_max_bpc(drm.device(), conn, bpc) + { + warn!( + ?bpc, + ?err, + "Failed to set max_bpc on connector: {}", + surface.output.name() + ); + } + + let vrr = output_config.0.vrr; + std::mem::drop(output_config); + + let compositor_ref = drm.compositors().get(crtc).unwrap().lock().unwrap(); + let vrr_support = compositor_ref + .vrr_supported( + compositor_ref + .pending_connectors() + .into_iter() + .next() + .unwrap(), + ) + .ok(); + + let primary_formats = compositor_ref.surface().plane_info().formats.clone(); + let overlay_formats = planes + .overlay + .iter() + .flat_map(|p| p.formats.iter().cloned()) + .collect::(); + surface.resume( + compositor, + primary_formats, + Some(overlay_formats).filter(|f| !f.indexset().is_empty()), + ); + + surface.output.set_adaptive_sync_support(vrr_support); + if match vrr_support { + Some(VrrSupport::RequiresModeset) if vrr == AdaptiveSync::Enabled => { + false + } + Some(VrrSupport::NotSupported) => false, + _ => true, + } { + surface.use_adaptive_sync(vrr); + surface.output.set_adaptive_sync(vrr); + } else { + surface.use_adaptive_sync(AdaptiveSync::Disabled); + surface.output.config_mut().vrr = AdaptiveSync::Disabled; + surface.output.set_adaptive_sync(AdaptiveSync::Disabled); + } + } else { + let vrr = output_config.0.vrr; + std::mem::drop(output_config); + if vrr != surface.output.adaptive_sync() { + if match surface.output.adaptive_sync_support() { + Some(VrrSupport::RequiresModeset) + if vrr == AdaptiveSync::Enabled => + { + true + } + Some(VrrSupport::NotSupported) => true, + _ => false, + } { + anyhow::bail!("Requested VRR mode unsupported"); + } + + surface.use_adaptive_sync(vrr); + surface.output.set_adaptive_sync(vrr); + } + + let mut renderer = self + .api + .single_renderer(&device.inner.render_node) + .with_context(|| "Failed to create renderer")?; + + let mut elements = DrmOutputRenderElements::default(); + for (crtc, output) in output_map.iter() { + let output_elements = output_elements( + Some(&device.inner.render_node), + &mut renderer, + &shell, + now, + output, + CursorMode::All, + None, + None, + ) + .with_context(|| "Failed to render outputs")?; + + elements.add_output(crtc, CLEAR_COLOR, output_elements); + } + + drm.use_mode(&surface.crtc, *mode, &mut renderer, &elements) + .context("Failed to apply new mode")?; + } + } + } + + // configure primary scanout allowance + if !device.inner.surfaces.is_empty() { + let mut renderer = self + .api + .single_renderer(&device.inner.render_node) + .with_context(|| "Failed to create renderer")?; + + device + .allow_primary_scanout_any( + device + .inner + .surfaces + .values() + .filter(|s| s.output.is_enabled() && s.output.mirroring().is_none()) + .count() + <= 1, + &mut renderer, + clock, + &shell, + ) + .context("Failed to switch primary-plane scanout flags")?; + + let mut elements = DrmOutputRenderElements::default(); + for (crtc, output) in output_map.iter() { + let output_elements = output_elements( + Some(&device.inner.render_node), + &mut renderer, + &shell, + now, + output, + CursorMode::All, + None, + None, + ) + .with_context(|| "Failed to render outputs")?; + + elements.add_output(crtc, CLEAR_COLOR, output_elements); + } + + if let Err(err) = device + .drm + .try_to_restore_modifiers(&mut renderer, &elements) + { + warn!(?err, "Failed to restore modifiers"); + } + } + } + + // we need to handle mirroring, after all outputs have been enabled + let all_outputs = self.all_outputs(); + for device in self.drm_devices.values_mut() { + for surface in device.inner.surfaces.values_mut() { + let mirrored_output = + if let OutputState::Mirroring(conn) = &surface.output.config().enabled { + Some( + all_outputs + .iter() + .find(|output| output.name() == *conn) + .cloned() + .ok_or(anyhow::anyhow!("Unable to find mirroring output"))?, + ) + } else { + None + }; + + if !test_only && mirrored_output != surface.output.mirroring() { + surface.set_mirroring(mirrored_output.clone()); + } + } + } + + Ok(()) + } +} diff --git a/src/backend/kms/render/gles.rs b/src/backend/kms/render/gles.rs new file mode 100644 index 0000000..f6e4486 --- /dev/null +++ b/src/backend/kms/render/gles.rs @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use clap_lex::OsStrExt; +use rustix::path::Arg; +use smithay::{ + backend::{ + SwapBuffersError, + allocator::{ + Allocator, + dmabuf::{AnyError, Dmabuf, DmabufAllocator}, + gbm::GbmAllocator, + }, + drm::{CreateDrmNodeError, DrmNode}, + renderer::{ + gles::{GlesError, GlesRenderer, ffi}, + glow::GlowRenderer, + multigpu::{ApiDevice, GraphicsApi}, + }, + }, + reexports::drm::control::Device, +}; +use std::{ + borrow::{Borrow, BorrowMut}, + cell::Cell, + ffi::{CStr, c_char}, +}; +use std::{ + collections::HashMap, + fmt, + os::unix::prelude::AsFd, + sync::atomic::{AtomicBool, Ordering}, +}; + +/// Errors raised by the [`GbmGlesBackend`] +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// OpenGL error + #[error(transparent)] + Gl(#[from] GlesError), + /// Error creating a drm node + #[error(transparent)] + DrmNode(#[from] CreateDrmNodeError), +} + +impl From for SwapBuffersError { + fn from(err: Error) -> SwapBuffersError { + match err { + x @ Error::DrmNode(_) => SwapBuffersError::ContextLost(Box::new(x)), + Error::Gl(x) => x.into(), + } + } +} + +/// A [`GraphicsApi`] utilizing user-provided GBM Devices and OpenGL ES for rendering. +pub struct GbmGlowBackend { + devices: HashMap, Cell>)>, + needs_enumeration: AtomicBool, +} + +impl fmt::Debug for GbmGlowBackend { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("GbmGlesBackend") + .field("devices", &self.devices.keys()) + .field("needs_enumeration", &self.needs_enumeration) + .finish() + } +} + +impl Default for GbmGlowBackend { + fn default() -> Self { + GbmGlowBackend { + devices: HashMap::new(), + needs_enumeration: AtomicBool::new(true), + } + } +} + +impl GbmGlowBackend { + pub fn new() -> Self { + GbmGlowBackend { + devices: HashMap::new(), + needs_enumeration: AtomicBool::new(false), + } + } + + pub fn current_devices(&self) -> impl Iterator { + self.devices.keys() + } + + pub fn add_node(&mut self, node: DrmNode, gbm: GbmAllocator, renderer: GlowRenderer) { + if self.devices.contains_key(&node) { + return; + } + + self.devices.insert(node, (gbm, Cell::new(Some(renderer)))); + self.needs_enumeration.store(true, Ordering::SeqCst); + } + + /// Remove a given node from the api + pub fn remove_node(&mut self, node: &DrmNode) { + if self.devices.remove(node).is_some() { + self.needs_enumeration.store(true, Ordering::SeqCst); + } + } +} + +impl GraphicsApi for GbmGlowBackend { + type Device = GbmGlowDevice; + type Error = Error; + + fn enumerate(&self, list: &mut Vec) -> Result<(), Self::Error> { + self.needs_enumeration.store(false, Ordering::SeqCst); + + // remove old stuff + list.retain(|renderer| { + self.devices + .keys() + .any(|node| renderer.node.dev_id() == node.dev_id()) + }); + + // add new stuff + let new_renderers = self + .devices + .iter() + // but don't replace already initialized renderers + .filter(|(node, _)| { + !list + .iter() + .any(|renderer| renderer.node.dev_id() == node.dev_id()) + }) + .flat_map(|(node, (allocator, renderer))| { + let mut renderer = renderer.replace(None)?; + let is_intel = allocator + .as_ref() + .get_driver() + .is_ok_and(|drv| drv.name().contains("i915")); + let intel_export_quirk = is_intel + && BorrowMut::::borrow_mut(&mut renderer) + .with_context(|gl| unsafe { + CStr::from_ptr(gl.GetString(ffi::RENDERER) as *const c_char) + }) + .is_ok_and(|name| { + name.as_str().is_ok_and(|name| { + name.contains("TGL") + || name.contains("MTL") + || name.contains("ARL") + || name.contains("LNL") + }) + }); + + Some(GbmGlowDevice { + node: *node, + renderer, + intel_export_quirk, + allocator: Box::new(DmabufAllocator(allocator.clone())), + }) + }) + .collect::>(); + list.extend(new_renderers); + + Ok(()) + } + + fn needs_enumeration(&self) -> bool { + self.needs_enumeration.load(Ordering::Acquire) + } + + fn identifier() -> &'static str { + "gbm_glow" + } +} + +/// [`ApiDevice`] of the [`GbmGlesBackend`] +pub struct GbmGlowDevice { + node: DrmNode, + renderer: GlowRenderer, + intel_export_quirk: bool, + allocator: Box>, +} + +impl fmt::Debug for GbmGlowDevice { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("GbmGlesDevice") + .field("node", &self.node) + .field("renderer", &self.renderer) + .finish_non_exhaustive() + } +} + +impl ApiDevice for GbmGlowDevice { + type Renderer = GlowRenderer; + + fn renderer(&self) -> &Self::Renderer { + &self.renderer + } + fn renderer_mut(&mut self) -> &mut Self::Renderer { + &mut self.renderer + } + fn allocator(&mut self) -> &mut dyn Allocator { + self.allocator.as_mut() + } + fn node(&self) -> &DrmNode { + &self.node + } + fn can_do_cross_device_imports(&self) -> bool { + !Borrow::::borrow(&self.renderer).is_software() + } + + fn should_do_cross_device_exports(&self) -> bool { + !self.intel_export_quirk + } +} diff --git a/src/backend/kms/render/mod.rs b/src/backend/kms/render/mod.rs new file mode 100644 index 0000000..210a833 --- /dev/null +++ b/src/backend/kms/render/mod.rs @@ -0,0 +1,2 @@ +pub mod gles; +pub mod pixman; diff --git a/src/backend/kms/render/pixman.rs b/src/backend/kms/render/pixman.rs new file mode 100644 index 0000000..c2a8603 --- /dev/null +++ b/src/backend/kms/render/pixman.rs @@ -0,0 +1,165 @@ +use std::{ + collections::HashMap, + fmt, + os::fd::AsFd, + sync::atomic::{AtomicBool, Ordering}, +}; + +use smithay::backend::{ + allocator::{ + Allocator, + dmabuf::{AnyError, Dmabuf, DmabufAllocator}, + gbm::{GbmAllocator, GbmBufferFlags, GbmDevice}, + }, + drm::DrmNode, + renderer::{ + multigpu::{ApiDevice, GraphicsApi}, + pixman::{PixmanError, PixmanRenderer}, + }, +}; +use tracing::warn; + +#[derive(Debug)] +pub struct GbmPixmanBackend { + devices: HashMap>, + needs_enumeration: AtomicBool, + allocator_flags: GbmBufferFlags, +} + +pub struct GbmPixmanDevice { + node: DrmNode, + allocator: Box>, + renderer: PixmanRenderer, +} + +impl fmt::Debug for GbmPixmanDevice { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("GbmPixmanDevice") + .field("node", &self.node) + .field("allocator", &"...") + .field("renderer", &self.renderer) + .finish() + } +} + +impl Default for GbmPixmanBackend { + fn default() -> Self { + Self::new() + } +} + +impl GbmPixmanBackend { + pub fn new() -> Self { + GbmPixmanBackend { + devices: HashMap::new(), + needs_enumeration: AtomicBool::new(false), + allocator_flags: GbmBufferFlags::RENDERING, + } + } + + pub fn with_allocator_flags(allocator_flags: GbmBufferFlags) -> Self { + GbmPixmanBackend { + devices: HashMap::new(), + needs_enumeration: AtomicBool::new(false), + allocator_flags, + } + } + + pub fn set_allocator_flags(&mut self, flags: GbmBufferFlags) { + self.allocator_flags = flags; + } + + pub fn add_node(&mut self, node: DrmNode, gbm: GbmDevice) { + if self.devices.contains_key(&node) { + return; + } + + let allocator = GbmAllocator::new(gbm, self.allocator_flags); + self.devices.insert(node, allocator); + self.needs_enumeration.store(true, Ordering::SeqCst); + } + + /// Remove a given node from the api + pub fn remove_node(&mut self, node: &DrmNode) { + if self.devices.remove(node).is_some() { + self.needs_enumeration.store(true, Ordering::SeqCst); + } + } +} + +impl GraphicsApi for GbmPixmanBackend { + type Device = GbmPixmanDevice; + + type Error = PixmanError; + + fn enumerate(&self, list: &mut Vec) -> Result<(), Self::Error> { + self.needs_enumeration.store(false, Ordering::SeqCst); + + list.retain(|renderer| { + self.devices + .keys() + .any(|node| renderer.node.dev_id() == node.dev_id()) + }); + + // add new stuff + let new_renderers = self + .devices + .iter() + .filter(|(node, _)| { + !list + .iter() + .any(|renderer| renderer.node.dev_id() == node.dev_id()) + }) + .map(|(node, gbm)| { + Ok(GbmPixmanDevice { + node: *node, + allocator: Box::new(DmabufAllocator(gbm.clone())) + as Box>, + renderer: PixmanRenderer::new()?, + }) + }) + .flat_map(|x: Result| match x { + Ok(x) => Some(x), + Err(x) => { + warn!("Skipping pixman device: {}", x); + None + } + }) + .collect::>(); + list.extend(new_renderers); + + Ok(()) + } + + fn identifier() -> &'static str { + "gbm_pixman" + } + + fn needs_enumeration(&self) -> bool { + self.needs_enumeration.load(Ordering::SeqCst) + } +} + +impl ApiDevice for GbmPixmanDevice { + type Renderer = PixmanRenderer; + + fn renderer(&self) -> &Self::Renderer { + &self.renderer + } + + fn renderer_mut(&mut self) -> &mut Self::Renderer { + &mut self.renderer + } + + fn allocator(&mut self) -> &mut dyn Allocator { + &mut self.allocator + } + + fn node(&self) -> &DrmNode { + &self.node + } + + fn can_do_cross_device_imports(&self) -> bool { + false + } +} diff --git a/src/backend/kms/socket.rs b/src/backend/kms/socket.rs new file mode 100644 index 0000000..f1c2b0e --- /dev/null +++ b/src/backend/kms/socket.rs @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use anyhow::{Context, Result, anyhow}; +use smithay::{ + backend::{ + allocator::format::FormatSet, + drm::{DrmNode, NodeType}, + }, + reexports::{ + calloop::RegistrationToken, + wayland_server::{Client, DisplayHandle, backend::GlobalId}, + }, + wayland::{ + dmabuf::{DmabufFeedbackBuilder, DmabufGlobal}, + socket::ListeningSocketSource, + }, +}; +use std::sync::Arc; +use tracing::{info, warn}; + +use crate::state::{ClientState, Common, State, advertised_node_for_client}; + +#[derive(Debug)] +pub struct Socket { + pub token: RegistrationToken, + pub drm_global: GlobalId, + pub dmabuf_global: DmabufGlobal, +} + +impl Common { + pub(super) fn create_socket( + &mut self, + dh: &DisplayHandle, + render_node: DrmNode, + formats: FormatSet, + ) -> Result { + let socket_name = format!( + "{}-{}", + self.socket.to_string_lossy(), + render_node + .dev_path() + .unwrap() + .file_name() + .unwrap() + .to_string_lossy() + ); + + // initialize globals + let filter = move |client: &Client| advertised_node_for_client(client) == Some(render_node); + + let feedback = DmabufFeedbackBuilder::new(render_node.dev_id(), formats.clone()) + .build() + .with_context(|| "Failed to create drm format shared memory table")?; + + let dmabuf_global = self + .dmabuf_state + .create_global_with_filter_and_default_feedback::(dh, &feedback, filter); + + let drm_global_id = self.wl_drm_state.create_global_with_filter::( + dh, + render_node + .dev_path_with_type(NodeType::Render) + .or_else(|| render_node.dev_path()) + .ok_or(anyhow!( + "Could not determine path for gpu node: {}", + render_node + ))?, + formats, + &dmabuf_global, + filter, + ); + + // add a special socket for the gpu + let listener = ListeningSocketSource::with_name(&socket_name) + .with_context(|| format!("Failed to bind socket to {}", socket_name))?; + let socket_name_clone = socket_name.clone(); + let token = self + .event_loop_handle + .insert_source(listener, move |client_stream, _, state: &mut State| { + if let Err(err) = state.common.display_handle.insert_client( + client_stream, + Arc::new(ClientState { + advertised_drm_node: Some(render_node), + ..state.new_client_state() + }), + ) { + warn!( + socket_name = socket_name_clone, + ?err, + "Error adding wayland client." + ); + } + }) + .context("Failed to add gpu-wayland socket to the event loop")?; + + info!(socket_name, ?render_node, "Added gpu-specific socket."); + + Ok(Socket { + token, + drm_global: drm_global_id, + dmabuf_global, + }) + } +} diff --git a/src/backend/kms/surface/mod.rs b/src/backend/kms/surface/mod.rs new file mode 100644 index 0000000..eb23237 --- /dev/null +++ b/src/backend/kms/surface/mod.rs @@ -0,0 +1,1979 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{ + backend::render::{ + CLEAR_COLOR, CursorMode, GlMultiError, GlMultiRenderer, PostprocessOutputConfig, + PostprocessShader, PostprocessState, + element::{CosmicElement, DamageElement}, + init_shaders, output_elements, + }, + config::ScreenFilter, + shell::Shell, + state::SurfaceDmabufFeedback, + utils::prelude::*, + wayland::handlers::{ + compositor::recursive_frame_time_estimation, + image_copy_capture::{FrameHolder, PendingImageCopyData, SessionData, submit_buffer}, + }, +}; + +use anyhow::{Context, Result}; +use calloop::channel::Channel; +use cosmic_comp_config::output::comp::AdaptiveSync; +use smithay::{ + backend::{ + allocator::{ + Fourcc, + format::FormatSet, + gbm::{GbmAllocator, GbmBuffer}, + }, + drm::{ + DrmDeviceFd, DrmEventMetadata, DrmEventTime, DrmNode, VrrSupport, + compositor::{ + BlitFrameResultError, FrameError, FrameFlags, PrimaryPlaneElement, + RenderFrameResult, + }, + exporter::gbm::GbmFramebufferExporter, + gbm::GbmFramebuffer, + output::DrmOutput, + }, + egl::EGLContext, + renderer::{ + Bind, Blit, BufferType, Frame, ImportDma, Offscreen, Renderer, RendererSuper, Texture, + TextureFilter, buffer_dimensions, buffer_type, + damage::Error as RenderError, + element::{ + Element, Kind, RenderElementStates, + texture::TextureRenderElement, + utils::{ + ConstrainAlign, ConstrainScaleBehavior, Relocate, RelocateRenderElement, + constrain_render_elements, + }, + }, + gles::{ + GlesRenderbuffer, GlesRenderer, GlesTexture, Uniform, element::TextureShaderElement, + }, + glow::GlowRenderer, + multigpu::{ApiDevice, Error as MultiError, GpuManager}, + sync::SyncPoint, + utils::with_renderer_surface_state, + }, + }, + desktop::utils::OutputPresentationFeedback, + output::{Output, OutputNoMode}, + reexports::{ + calloop::{ + EventLoop, LoopHandle, RegistrationToken, + channel::{Event, Sender, channel}, + timer::{TimeoutAction, Timer}, + }, + drm::control::{connector, crtc}, + wayland_protocols::wp::{ + linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_v1, + presentation_time::server::wp_presentation_feedback, + }, + wayland_server::protocol::wl_surface::WlSurface, + }, + utils::{Clock, Monotonic, Physical, Point, Rectangle, Transform}, + wayland::{ + dmabuf::{DmabufFeedbackBuilder, get_dmabuf}, + image_copy_capture::{ + CaptureFailureReason, Frame as ScreencopyFrame, SessionRef as ScreencopySessionRef, + }, + presentation::Refresh, + seat::WaylandFocus, + shm::{shm_format_to_fourcc, with_buffer_contents}, + }, +}; +use tracing::{error, info, trace, warn}; + +use std::{ + borrow::{Borrow, BorrowMut}, + collections::{HashMap, HashSet, hash_map}, + mem, + sync::{ + Arc, RwLock, + atomic::{AtomicBool, Ordering}, + mpsc::{Receiver, SyncSender}, + }, + thread::JoinHandle, + time::Duration, +}; + +mod timings; +pub use self::timings::Timings; + +use super::{drm_helpers, render::gles::GbmGlowBackend}; + +#[cfg(feature = "debug")] +use smithay_egui::EguiState; + +#[derive(Debug)] +pub struct Surface { + pub(crate) connector: connector::Handle, + pub(super) crtc: crtc::Handle, + pub(crate) output: Output, + known_nodes: HashSet, + + active: Arc, + pub feedback: HashMap, + pub(super) primary_plane_formats: FormatSet, + overlay_plane_formats: Option, + + loop_handle: LoopHandle<'static, State>, + thread_command: Sender, + thread_token: RegistrationToken, + thread: Option>, + + dpms: bool, +} + +pub struct SurfaceThreadState { + // rendering + api: GpuManager>, + primary_node: Arc>>, + target_node: DrmNode, + active: Arc, + vrr_mode: AdaptiveSync, + frame_flags: FrameFlags, + compositor: Option, + + state: QueueState, + timings: Timings, + frame_callback_seq: usize, + thread_sender: Sender, + + output: Output, + mirroring: Option, + screen_filter: ScreenFilter, + postprocess_textures: HashMap, + + shell: Arc>, + + loop_handle: LoopHandle<'static, Self>, + clock: Clock, + + #[cfg(feature = "debug")] + egui: EguiState, + + last_sequence: Option, + /// Tracy frame that goes from vblank to vblank. + vblank_frame: Option, + /// Frame name for the VBlank frame. + vblank_frame_name: tracy_client::FrameName, + /// Plot name for the time since presentation plot. + time_since_presentation_plot_name: tracy_client::PlotName, + /// Plot name for the presentation misprediction plot. + presentation_misprediction_plot_name: tracy_client::PlotName, + sequence_delta_plot_name: tracy_client::PlotName, +} + +pub type GbmDrmOutput = DrmOutput< + GbmAllocator, + GbmFramebufferExporter, + Option<( + OutputPresentationFeedback, + Receiver, + Duration, + )>, + DrmDeviceFd, +>; + +#[derive(Debug, Default)] +pub enum QueueState { + #[default] + Idle, + /// A redraw is queued. + Queued(RegistrationToken), + /// We submitted a frame to the KMS and waiting for it to be presented. + WaitingForVBlank { redraw_needed: bool }, + /// We did not submit anything to KMS and made a timer to fire at the estimated VBlank. + WaitingForEstimatedVBlank(RegistrationToken), + /// A redraw is queued on top of the above. + WaitingForEstimatedVBlankAndQueued { + estimated_vblank: RegistrationToken, + queued_render: RegistrationToken, + }, +} + +#[derive(Debug)] +pub enum ThreadCommand { + Suspend(SyncSender<()>), + Resume { + compositor: GbmDrmOutput, + }, + NodeAdded { + node: DrmNode, + gbm: GbmAllocator, + egl: EGLContext, + sync: SyncSender<()>, + }, + NodeRemoved { + node: DrmNode, + sync: SyncSender<()>, + }, + UpdateMirroring(Option), + UpdateScreenFilter(ScreenFilter), + VBlank(Option), + ScheduleRender, + AdaptiveSyncAvailable(SyncSender>), + UseAdaptiveSync(AdaptiveSync), + AllowFrameFlags(bool, FrameFlags), + End, + DpmsOff, +} + +#[derive(Debug)] +pub enum SurfaceCommand { + SendFrames(usize), + RenderStates(RenderElementStates), +} + +#[derive(Debug, Default)] +struct PrePostprocessData { + states: Option, + texture: Option, + cursor_texture: Option, + cursor_geometry: Option>, +} + +impl Surface { + pub fn new( + output: &Output, + crtc: crtc::Handle, + connector: connector::Handle, + primary_node: Arc>>, + dev_node: DrmNode, + target_node: DrmNode, + evlh: &LoopHandle<'static, State>, + screen_filter: ScreenFilter, + shell: Arc>, + startup_done: Arc, + ) -> Result { + let (tx, rx) = channel::(); + let (tx2, rx2) = channel::(); + let active = Arc::new(AtomicBool::new(false)); + + let active_clone = active.clone(); + let output_clone = output.clone(); + + let thread = std::thread::Builder::new() + .name(format!("surface-{}", output.name())) + .spawn(move || { + if let Err(err) = surface_thread( + output_clone, + primary_node, + target_node, + shell, + active_clone, + screen_filter, + tx2, + rx, + startup_done, + ) { + error!("Surface thread crashed: {}", err); + } + }) + .context("Failed to spawn surface thread")?; + + let output_clone = output.clone(); + let thread_token = evlh + .insert_source(rx2, move |command, _, state| match command { + Event::Msg(SurfaceCommand::SendFrames(sequence)) => { + if output_clone.mirroring().is_some() { + return; + } + state.common.send_frames(&output_clone, Some(sequence)); + } + Event::Msg(SurfaceCommand::RenderStates(states)) => { + if output_clone.mirroring().is_some() { + return; + } + state.common.update_primary_output(&output_clone, &states); + let kms = state.backend.kms(); + let surface = &mut kms + .drm_devices + .get_mut(&dev_node) + .unwrap() + .inner + .surfaces + .get_mut(&crtc) + .unwrap(); + + state + .common + .send_dmabuf_feedback(&output_clone, &states, |source_node| { + if let Some(cached_feedback) = surface.feedback.get(&source_node) { + Some(cached_feedback.clone()) + } else { + // If we have freed the node, because it didn't have any active buffers/surfaces, + // we might not be able to evaluate surface feedback yet. + let render_formats = + kms.api.single_renderer(&source_node).ok()?.dmabuf_formats(); + // In contrast we must have the target node, if we have an active surface + let target_formats = kms + .api + .single_renderer(&target_node) + .unwrap() + .dmabuf_formats(); + let feedback = get_surface_dmabuf_feedback( + source_node, + target_node, + render_formats, + target_formats, + surface.primary_plane_formats.clone(), + surface.overlay_plane_formats.clone(), + ); + surface.feedback.insert(source_node, feedback.clone()); + Some(feedback) + } + }); + } + Event::Closed => {} + }) + .map_err(|_| anyhow::anyhow!("Failed to establish channel to surface thread"))?; + + Ok(Surface { + connector, + crtc, + output: output.clone(), + known_nodes: HashSet::new(), + active, + feedback: HashMap::new(), + primary_plane_formats: FormatSet::default(), + overlay_plane_formats: None, + loop_handle: evlh.clone(), + thread_command: tx, + thread_token, + thread: Some(thread), + dpms: true, + }) + } + + pub fn known_nodes(&self) -> &HashSet { + &self.known_nodes + } + + pub fn is_active(&self) -> bool { + self.active.load(Ordering::SeqCst) + } + + pub fn add_node(&mut self, node: DrmNode, gbm: GbmAllocator, egl: EGLContext) { + self.known_nodes.insert(node); + let (tx, rx) = std::sync::mpsc::sync_channel(1); + let _ = self.thread_command.send(ThreadCommand::NodeAdded { + node, + gbm, + egl, + sync: tx, + }); + let _ = rx.recv(); + } + + pub fn remove_node(&mut self, node: DrmNode) { + self.known_nodes.remove(&node); + self.feedback.remove(&node); + let (tx, rx) = std::sync::mpsc::sync_channel(1); + let _ = self + .thread_command + .send(ThreadCommand::NodeRemoved { node, sync: tx }); + // Block so we can be sure the file descriptor is closed + // (which is relevant for the udev device_removed callback). + let _ = rx.recv(); + } + + pub fn on_vblank(&self, metadata: Option) { + let _ = self.thread_command.send(ThreadCommand::VBlank(metadata)); + } + + pub fn schedule_render(&self) { + if self.dpms { + let _ = self.thread_command.send(ThreadCommand::ScheduleRender); + } + } + + pub fn set_mirroring(&mut self, output: Option) { + let _ = self + .thread_command + .send(ThreadCommand::UpdateMirroring(output)); + } + + pub fn set_screen_filter(&mut self, config: ScreenFilter) { + let _ = self + .thread_command + .send(ThreadCommand::UpdateScreenFilter(config)); + } + + pub fn adaptive_sync_support(&self) -> Result { + let (tx, rx) = std::sync::mpsc::sync_channel(1); + let _ = self + .thread_command + .send(ThreadCommand::AdaptiveSyncAvailable(tx)); + rx.recv().context("Surface thread died")? + } + + pub fn use_adaptive_sync(&mut self, vrr: AdaptiveSync) { + let _ = self + .thread_command + .send(ThreadCommand::UseAdaptiveSync(vrr)); + } + + pub fn allow_frame_flags(&mut self, flag: bool, flags: FrameFlags) { + let _ = self + .thread_command + .send(ThreadCommand::AllowFrameFlags(flag, flags)); + } + + pub fn suspend(&mut self) { + let (tx, rx) = std::sync::mpsc::sync_channel(1); + let _ = self.thread_command.send(ThreadCommand::Suspend(tx)); + let _ = rx.recv(); + } + + pub fn resume( + &mut self, + compositor: GbmDrmOutput, + primary_plane_formats: FormatSet, + overlay_plane_formats: Option, + ) { + self.primary_plane_formats = primary_plane_formats; + self.overlay_plane_formats = overlay_plane_formats; + self.feedback.clear(); + self.active.store(true, Ordering::SeqCst); + self.dpms = true; + + let _ = self + .thread_command + .send(ThreadCommand::Resume { compositor }); + } + + pub fn get_dpms(&mut self) -> bool { + self.dpms + } + + pub fn set_dpms(&mut self, on: bool) { + if self.dpms != on { + self.dpms = on; + if on { + self.schedule_render(); + } else { + let _ = self.thread_command.send(ThreadCommand::DpmsOff); + } + } + } + + pub fn drop_and_join(mut self) { + let thread = self.thread.take(); + std::mem::drop(self); + if let Some(thread) = thread { + let name = thread.thread().name().unwrap().to_string(); + let _ = thread.join(); + info!("Thread {} terminated.", name) + } + } +} + +impl Drop for Surface { + fn drop(&mut self) { + let _ = self.thread_command.send(ThreadCommand::End); + self.loop_handle.remove(self.thread_token); + if let Some(thread) = self.thread.take() { + let _ = thread; + // We want to do this, but this currently deadlocks on `apply_config_for_outputs`. + /* + let name = thread.thread().name().unwrap().to_string(); + let _ = thread.join(); + info!("Thread {} terminated.", name) + */ + } + } +} + +fn surface_thread( + output: Output, + primary_node: Arc>>, + target_node: DrmNode, + shell: Arc>, + active: Arc, + screen_filter: ScreenFilter, + thread_sender: Sender, + thread_receiver: Channel, + startup_done: Arc, +) -> Result<()> { + let name = output.name(); + profiling::register_thread!(&format!("Surface Thread {}", name)); + + let mut event_loop = EventLoop::try_new().unwrap(); + + let api = GpuManager::new(GbmGlowBackend::::default()) + .context("Failed to initialize rendering api")?; + + #[cfg(feature = "debug")] + let egui = { + let state = + smithay_egui::EguiState::new(smithay::utils::Rectangle::from_size((400, 800).into())); + let visuals = egui::style::Visuals { + window_shadow: egui::Shadow::NONE, + ..Default::default() + }; + state.context().set_visuals(visuals); + state + }; + + let vblank_frame_name = tracy_client::FrameName::new_leak(format!("vblank on {name}")); + let time_since_presentation_plot_name = + tracy_client::PlotName::new_leak(format!("{name} time since presentation, ms")); + let presentation_misprediction_plot_name = + tracy_client::PlotName::new_leak(format!("{name} presentation misprediction, ms")); + let sequence_delta_plot_name = + tracy_client::PlotName::new_leak(format!("{name} sequence delta")); + + let mut state = SurfaceThreadState { + api, + primary_node, + target_node, + active, + compositor: None, + frame_flags: FrameFlags::DEFAULT, + vrr_mode: AdaptiveSync::Disabled, + + state: QueueState::Idle, + timings: Timings::new(None, None, false, target_node), + frame_callback_seq: 0, + thread_sender, + + output, + mirroring: None, + screen_filter, + postprocess_textures: HashMap::new(), + + shell, + loop_handle: event_loop.handle(), + clock: Clock::new(), + #[cfg(feature = "debug")] + egui, + + last_sequence: None, + vblank_frame: None, + vblank_frame_name, + time_since_presentation_plot_name, + presentation_misprediction_plot_name, + sequence_delta_plot_name, + }; + + let signal = event_loop.get_signal(); + event_loop + .handle() + .insert_source(thread_receiver, move |command, _, state| match command { + Event::Msg(ThreadCommand::Suspend(tx)) => state.suspend(tx), + Event::Msg(ThreadCommand::Resume { compositor }) => { + state.resume(compositor); + } + Event::Msg(ThreadCommand::NodeAdded { + node, + gbm, + egl, + sync, + }) => { + if let Err(err) = state.node_added(node, gbm, egl) { + warn!(?err, ?node, "Failed to add node to surface-thread"); + } + let _ = sync.send(()); + } + Event::Msg(ThreadCommand::NodeRemoved { node, sync }) => { + state.node_removed(node); + let _ = sync.send(()); + } + Event::Msg(ThreadCommand::VBlank(metadata)) => { + state.on_vblank(metadata); + } + Event::Msg(ThreadCommand::ScheduleRender) => { + if !startup_done.load(Ordering::SeqCst) { + return; + } + + state.queue_redraw(false); + } + Event::Msg(ThreadCommand::UpdateMirroring(mirroring_output)) => { + state.update_mirroring(mirroring_output); + } + Event::Msg(ThreadCommand::UpdateScreenFilter(filter_config)) => { + state.update_screen_filter(filter_config); + } + Event::Msg(ThreadCommand::AdaptiveSyncAvailable(result)) => { + if let Some(compositor) = state.compositor.as_mut() { + let _ = result.send( + compositor + .with_compositor(|c| { + c.vrr_supported(c.pending_connectors().into_iter().next().unwrap()) + }) + .map_err(Into::into), + ); + } else { + let _ = result.send(Err(anyhow::anyhow!("Set vrr with inactive surface"))); + } + } + Event::Msg(ThreadCommand::UseAdaptiveSync(vrr)) => { + state.vrr_mode = vrr; + } + Event::Msg(ThreadCommand::DpmsOff) => { + if let Some(compositor) = state.compositor.as_mut() { + if let Err(err) = compositor.with_compositor(|c| c.clear()) { + error!("Failed to set DPMS off: {:?}", err); + } + match std::mem::replace(&mut state.state, QueueState::Idle) { + QueueState::Idle => {} + QueueState::Queued(token) + | QueueState::WaitingForEstimatedVBlank(token) => { + state.loop_handle.remove(token); + } + QueueState::WaitingForVBlank { .. } => { + state.timings.discard_current_frame() + } + QueueState::WaitingForEstimatedVBlankAndQueued { + estimated_vblank, + queued_render, + } => { + state.loop_handle.remove(estimated_vblank); + state.loop_handle.remove(queued_render); + } + }; + } + } + Event::Msg(ThreadCommand::AllowFrameFlags(flag, mut flags)) => { + if crate::utils::env::bool_var("COSMIC_DISABLE_DIRECT_SCANOUT").unwrap_or(false) { + flags.remove(FrameFlags::ALLOW_SCANOUT); + } + if crate::utils::env::bool_var("COSMIC_DISABLE_OVERLAY_SCANOUT").unwrap_or(false) { + flags.remove(FrameFlags::ALLOW_OVERLAY_PLANE_SCANOUT); + } + + if flag { + state.frame_flags.insert(flags); + } else { + state.frame_flags.remove(flags); + } + } + Event::Closed | Event::Msg(ThreadCommand::End) => { + signal.stop(); + signal.wakeup(); + } + }) + .map_err(|insert_error| insert_error.error) + .context("Failed to listen for events")?; + + event_loop.run(None, &mut state, |_| {}).map_err(Into::into) +} + +impl SurfaceThreadState { + fn suspend(&mut self, tx: SyncSender<()>) { + self.active.store(false, Ordering::SeqCst); + let _ = self.compositor.take(); + + match std::mem::replace(&mut self.state, QueueState::Idle) { + QueueState::Idle => {} + QueueState::Queued(token) | QueueState::WaitingForEstimatedVBlank(token) => { + self.loop_handle.remove(token); + } + QueueState::WaitingForVBlank { .. } => self.timings.discard_current_frame(), + QueueState::WaitingForEstimatedVBlankAndQueued { + estimated_vblank, + queued_render, + } => { + self.loop_handle.remove(estimated_vblank); + self.loop_handle.remove(queued_render); + } + }; + + let _ = tx.send(()); + } + + fn resume(&mut self, compositor: GbmDrmOutput) { + let (mode, min_hz) = compositor.with_compositor(|c| { + ( + c.surface().pending_mode(), + drm_helpers::get_minimum_refresh_rate( + c.surface(), + c.pending_connectors().into_iter().next().unwrap(), + ) + .ok() + .flatten(), + ) + }); + let interval = + Duration::from_secs_f64(1_000. / drm_helpers::calculate_refresh_rate(mode) as f64); + self.timings.set_refresh_interval(Some(interval)); + + const SAFETY_MARGIN: u32 = 2; // Magic two frames margin taken from kwin to not trigger low-framerate-compensation + let min_min_refresh_interval = Duration::from_secs_f64(1. / 30.); // 30Hz + self.timings.set_min_refresh_interval(Some( + min_hz + .map(|min| Duration::from_secs_f64(1. / (min + SAFETY_MARGIN) as f64)) + .unwrap_or(min_min_refresh_interval) // alternatively use 30Hz + .max(min_min_refresh_interval), + )); + + if crate::utils::env::bool_var("COSMIC_DISABLE_DIRECT_SCANOUT").unwrap_or(false) { + self.frame_flags.remove(FrameFlags::ALLOW_SCANOUT); + } else if crate::utils::env::bool_var("COSMIC_DISABLE_OVERLAY_SCANOUT").unwrap_or(false) { + self.frame_flags + .remove(FrameFlags::ALLOW_OVERLAY_PLANE_SCANOUT); + } + self.compositor = Some(compositor); + } + + fn node_added( + &mut self, + node: DrmNode, + gbm: GbmAllocator, + egl: EGLContext, + ) -> Result<()> { + let mut renderer = + unsafe { GlowRenderer::new(egl) }.context("Failed to create renderer")?; + init_shaders(renderer.borrow_mut()).context("Failed to initialize shaders")?; + + self.api.as_mut().add_node(node, gbm, renderer); + + Ok(()) + } + + fn node_removed(&mut self, node: DrmNode) { + self.api.as_mut().remove_node(&node); + // force enumeration + let _ = self.api.devices(); + } + + #[profiling::function] + fn on_vblank(&mut self, metadata: Option) { + let Some(compositor) = self.compositor.as_mut() else { + return; + }; + + // handle edge-cases right after resume + if !matches!( + self.state, + QueueState::WaitingForVBlank { .. } | QueueState::Idle + ) { + match mem::replace(&mut self.state, QueueState::Idle) { + QueueState::WaitingForVBlank { .. } | QueueState::Idle => unreachable!(), + QueueState::Queued(token) | QueueState::WaitingForEstimatedVBlank(token) => { + self.loop_handle.remove(token); + } + QueueState::WaitingForEstimatedVBlankAndQueued { + estimated_vblank, + queued_render, + } => { + self.loop_handle.remove(estimated_vblank); + self.loop_handle.remove(queued_render); + } + } + } + if matches!(self.state, QueueState::Idle) { + return; + } + + let now = self.clock.now(); + let presentation_time = match metadata.as_ref().map(|data| &data.time) { + Some(DrmEventTime::Monotonic(tp)) => Some(*tp), + _ => None, + }; + let sequence = metadata.as_ref().map(|data| data.sequence).unwrap_or(0); + + // finish tracy frame + let _ = self.vblank_frame.take(); + + // mark last frame completed + if let Ok(Some(Some((mut feedback, frames, estimated_presentation_time)))) = + compositor.frame_submitted() + && self.mirroring.is_none() + { + let name = self.output.name(); + let message = if let Some(presentation_time) = presentation_time { + let misprediction_s = + presentation_time.as_secs_f64() - estimated_presentation_time.as_secs_f64(); + tracy_client::Client::running().unwrap().plot( + self.presentation_misprediction_plot_name, + misprediction_s * 1000., + ); + + let now = Duration::from(now); + if presentation_time > now { + let diff = presentation_time - now; + tracy_client::Client::running().unwrap().plot( + self.time_since_presentation_plot_name, + -diff.as_secs_f64() * 1000., + ); + format!("vblank on {name}, presentation is {diff:?} later") + } else { + let diff = now - presentation_time; + tracy_client::Client::running().unwrap().plot( + self.time_since_presentation_plot_name, + diff.as_secs_f64() * 1000., + ); + format!("vblank on {name}, presentation was {diff:?} ago") + } + } else { + format!("vblank on {name}, presentation time unknown") + }; + tracy_client::Client::running() + .unwrap() + .message(&message, 0); + + let (clock, flags) = if let Some(tp) = presentation_time { + ( + tp.into(), + wp_presentation_feedback::Kind::Vsync + | wp_presentation_feedback::Kind::HwClock + | wp_presentation_feedback::Kind::HwCompletion, + ) + } else { + ( + now, + wp_presentation_feedback::Kind::Vsync + | wp_presentation_feedback::Kind::HwCompletion, + ) + }; + + let rate = self + .output + .current_mode() + .map(|mode| Duration::from_secs_f64(1_000.0 / mode.refresh as f64)); + let refresh = match rate { + Some(rate) + if self + .compositor + .as_ref() + .is_some_and(|comp| comp.with_compositor(|c| c.vrr_enabled())) => + { + Refresh::Variable(rate) + } + Some(rate) => Refresh::Fixed(rate), + None => Refresh::Unknown, + }; + + if let Some(last_sequence) = self.last_sequence { + let delta = sequence as f64 - last_sequence as f64; + tracy_client::Client::running() + .unwrap() + .plot(self.sequence_delta_plot_name, delta); + } + self.last_sequence = Some(sequence); + + feedback.presented(clock, refresh, sequence as u64, flags); + + self.timings.presented(clock); + + while let Ok(pending_image_copy_data) = frames.recv() { + pending_image_copy_data.send_success_when_ready( + self.output.current_transform(), + &self.loop_handle, + clock, + ); + } + } + + let redraw_needed = match mem::replace(&mut self.state, QueueState::Idle) { + QueueState::Idle => unreachable!(), + QueueState::Queued(_) => unreachable!(), + QueueState::WaitingForVBlank { redraw_needed } => redraw_needed, + QueueState::WaitingForEstimatedVBlank(_) => unreachable!(), + QueueState::WaitingForEstimatedVBlankAndQueued { .. } => unreachable!(), + }; + + if redraw_needed || self.shell.read().animations_going() { + let vblank_frame = tracy_client::Client::running() + .unwrap() + .non_continuous_frame(self.vblank_frame_name); + self.vblank_frame = Some(vblank_frame); + + self.queue_redraw(false); + } + self.send_frame_callbacks(); + } + + #[profiling::function] + fn on_estimated_vblank(&mut self, force: bool) { + match mem::replace(&mut self.state, QueueState::Idle) { + QueueState::Idle => unreachable!(), + QueueState::Queued(_) => unreachable!(), + QueueState::WaitingForVBlank { .. } => unreachable!(), + QueueState::WaitingForEstimatedVBlank(_) => (), + // The timer fired just in front of a redraw. + QueueState::WaitingForEstimatedVBlankAndQueued { queued_render, .. } => { + self.state = QueueState::Queued(queued_render); + return; + } + } + + self.frame_callback_seq = self.frame_callback_seq.wrapping_add(1); + + if force || self.shell.read().animations_going() { + self.queue_redraw(false); + } + self.send_frame_callbacks(); + } + + fn queue_redraw(&mut self, force: bool) { + let Some(_compositor) = self.compositor.as_mut() else { + return; + }; + + if let QueueState::WaitingForVBlank { .. } = &self.state { + // We're waiting for VBlank, request a redraw afterwards. + self.state = QueueState::WaitingForVBlank { + redraw_needed: true, + }; + return; + } + + if !force { + match &self.state { + QueueState::Idle | QueueState::WaitingForEstimatedVBlank(_) => {} + + // A redraw is already queued. + QueueState::Queued(_) | QueueState::WaitingForEstimatedVBlankAndQueued { .. } => { + return; + } + _ => unreachable!(), + }; + } + + let estimated_presentation = self.timings.next_presentation_time(&self.clock); + let render_start = self.timings.next_render_time(&self.clock); + + let timer = if render_start.is_zero() { + trace!("Running late for frame."); + // TODO triple buffering + Timer::immediate() + } else { + Timer::from_duration(render_start) + }; + + let token = self + .loop_handle + .insert_source(timer, move |_time, _, state| { + if let Err(err) = state.redraw(estimated_presentation) { + let name = state.output.name(); + warn!(?name, "Failed to submit rendering: {:?}", err); + state.queue_redraw(true); + } + TimeoutAction::Drop + }) + .expect("Failed to schedule render"); + + match &self.state { + QueueState::Idle => { + self.state = QueueState::Queued(token); + } + QueueState::WaitingForEstimatedVBlank(estimated_vblank) => { + self.state = QueueState::WaitingForEstimatedVBlankAndQueued { + estimated_vblank: *estimated_vblank, + queued_render: token, + }; + } + QueueState::Queued(old_token) if force => { + self.loop_handle.remove(*old_token); + self.state = QueueState::Queued(token); + } + QueueState::WaitingForEstimatedVBlankAndQueued { + estimated_vblank, + queued_render, + } if force => { + self.loop_handle.remove(*queued_render); + self.state = QueueState::WaitingForEstimatedVBlankAndQueued { + estimated_vblank: *estimated_vblank, + queued_render: token, + }; + } + _ => unreachable!(), + } + } + + #[profiling::function] + fn redraw(&mut self, estimated_presentation: Duration) -> Result<()> { + let Some(compositor) = self.compositor.as_mut() else { + return Ok(()); + }; + + let render_node = render_node_for_output( + self.mirroring.as_ref().unwrap_or(&self.output), + self.primary_node + .read() + .unwrap() + .as_ref() + .unwrap_or(&self.target_node), + &self.target_node, + &self.shell.read(), + ); + + // Acquiring a renderer can fail transiently when the underlying DRM + // device is lost (e.g. after a GPU reset). + let mut renderer = if render_node != self.target_node { + self.api + .renderer(&render_node, &self.target_node, compositor.format()) + .map_err(|err| anyhow::format_err!("Failed to create renderer: {:?}", err))? + } else { + self.api + .single_renderer(&self.target_node) + .map_err(|err| anyhow::format_err!("Failed to create renderer: {:?}", err))? + }; + + self.timings.start_render(&self.clock); + + let mut additional_frame_flags = FrameFlags::empty(); + let mut remove_frame_flags = FrameFlags::empty(); + + let (has_active_fullscreen, fullscreen_drives_refresh_rate, animations_going) = { + let shell = self.shell.read(); + let animations_going = shell.animations_going(); + let output = self.mirroring.as_ref().unwrap_or(&self.output); + if let Some((_, workspace)) = shell.workspaces.active(output) { + let seat = shell.seats.last_active(); + if let Some(fullscreen_surface) = workspace.get_fullscreen(seat) { + const _30_FPS: Duration = Duration::from_nanos(1_000_000_000 / 30); + ( + true, + fullscreen_surface + .surface + .wl_surface() + .is_some_and(|surface| { + recursive_frame_time_estimation(&self.clock, &surface) + .is_some_and(|dur| dur <= _30_FPS) + }), + animations_going, + ) + } else { + (false, false, animations_going) + } + } else { + (false, false, animations_going) + } + }; + + if has_active_fullscreen || animations_going { + // skip overlay plane assign if we have a fullscreen surface or dynamic contents to save on tests + remove_frame_flags |= FrameFlags::ALLOW_OVERLAY_PLANE_SCANOUT; + } + + let mut vrr = matches!(self.vrr_mode, AdaptiveSync::Force); + + if self.vrr_mode == AdaptiveSync::Enabled { + vrr = has_active_fullscreen; + } + + let mut elements = output_elements( + Some(&render_node), + &mut renderer, + &self.shell, + self.clock.now(), + self.mirroring.as_ref().unwrap_or(&self.output), + CursorMode::All, + #[cfg(not(feature = "debug"))] + None, + #[cfg(feature = "debug")] + Some((&self.egui, &self.timings)), + Some(self.target_node), + ) + .map_err(|err| { + anyhow::format_err!("Failed to accumulate elements for rendering: {:?}", err) + })?; + + if vrr && fullscreen_drives_refresh_rate && !self.timings.past_min_render_time(&self.clock) + { + additional_frame_flags |= FrameFlags::SKIP_CURSOR_ONLY_UPDATES; + }; + self.timings.set_vrr(vrr); + self.timings.elements_done(&self.clock); + + // we can't use the elements after `compositor.render_frame`, + // so let's collect everything we need for screencopy now + let mut has_cursor_mode_none = false; + let frames = if self.mirroring.is_none() { + take_screencopy_frames(&self.output, &elements, &mut has_cursor_mode_none) + } else { + Default::default() + }; + + // actual rendering + let source_output = self + .mirroring + .as_ref() + .or((!self.screen_filter.is_noop()).then_some(&self.output)) + .filter(|output| { + PostprocessOutputConfig::for_output_untransformed(output) + != PostprocessOutputConfig::for_output(&self.output) + || !self.screen_filter.is_noop() + }); + + let mut pre_postprocess_data = PrePostprocessData::default(); + + let res = if let Some(source_output) = source_output { + let offscreen_output_config = + PostprocessOutputConfig::for_output_untransformed(source_output); + let postprocess_state = match self.postprocess_textures.entry(self.target_node) { + hash_map::Entry::Occupied(occupied) => { + let postprocess_state = occupied.into_mut(); + // If output config is different, re-create offscreen state + if postprocess_state.output_config != offscreen_output_config { + *postprocess_state = PostprocessState::new_with_renderer( + &mut renderer, + compositor.format(), + offscreen_output_config, + )? + } + postprocess_state + } + hash_map::Entry::Vacant(vacant) => { + vacant.insert(PostprocessState::new_with_renderer( + &mut renderer, + compositor.format(), + offscreen_output_config, + )?) + } + }; + + if has_cursor_mode_none && self.mirroring.is_none() { + // TODO: use `extract_if` once stablized + let cursor_element_count = elements + .iter() + .take_while(|elem| elem.kind() == Kind::Cursor) + .count(); + let cursor_elements = elements.drain(..cursor_element_count).collect::>(); + let scale = source_output.current_scale().fractional_scale().into(); + + let geometry: Option> = + cursor_elements.iter().fold(None, |acc, elem| { + let geometry = elem.geometry(scale); + if let Some(acc) = acc { + Some(acc.merge(geometry)) + } else { + Some(geometry) + } + }); + + if let Some(geometry) = geometry { + let cursor_elements = cursor_elements + .into_iter() + .map(|elem| { + RelocateRenderElement::from_element( + elem, + Point::from((-geometry.loc.x, -geometry.loc.y)), + Relocate::Relative, + ) + }) + .collect::>(); + + postprocess_state.track_cursor( + &mut renderer, + Fourcc::Abgr8888, + geometry.size, + scale, + )?; + + postprocess_state + .cursor_texture + .as_mut() + .unwrap() + .render() + .draw::<_, ::Error>(|tex| { + if self.mirroring.is_none() { + pre_postprocess_data.cursor_geometry = Some(geometry); + pre_postprocess_data.cursor_texture = Some(tex.clone()); + } + + let mut fb = renderer.bind(tex)?; + let res = match postprocess_state + .cursor_damage_tracker + .as_mut() + .unwrap() + .render_output( + &mut renderer, + &mut fb, + 1, + &cursor_elements, + [0.0, 0.0, 0.0, 0.0], + ) { + Ok(res) => res, + Err(RenderError::Rendering(err)) => return Err(err), + Err(RenderError::OutputNoMode(_)) => unreachable!(), + }; + + if self.mirroring.is_none() { + pre_postprocess_data.states = Some(res.states); + } + + renderer.wait(&res.sync)?; + std::mem::drop(fb); + + let transform = source_output.current_transform(); + let area = tex.size().to_logical(1, transform); + + Ok(res + .damage + .cloned() + .map(|v| { + v.into_iter() + .map(|r| r.to_logical(1).to_buffer(1, transform, &area)) + .collect::>() + }) + .unwrap_or_default()) + }) + .context("Failed to draw to offscreen render target")?; + } + } else { + postprocess_state.remove_cursor(); + } + + postprocess_state + .texture + .render() + .draw::<_, ::Error>(|tex| { + if self.mirroring.is_none() { + pre_postprocess_data.texture = Some(tex.clone()); + } + + let mut fb = renderer.bind(tex)?; + let res = match postprocess_state.damage_tracker.render_output( + &mut renderer, + &mut fb, + 1, + &elements, + CLEAR_COLOR, + ) { + Ok(res) => res, + Err(RenderError::Rendering(err)) => return Err(err), + Err(RenderError::OutputNoMode(_)) => unreachable!(), + }; + + if self.mirroring.is_none() { + if let Some(states) = pre_postprocess_data.states.as_mut() { + states.states.extend(res.states.states); + } else { + pre_postprocess_data.states = Some(res.states); + } + } + + renderer.wait(&res.sync)?; + std::mem::drop(fb); + + let transform = source_output.current_transform(); + let area = tex.size().to_logical(1, transform); + + Ok(res + .damage + .cloned() + .map(|v| { + v.into_iter() + .map(|r| r.to_logical(1).to_buffer(1, transform, &area)) + .collect::>() + }) + .unwrap_or_default()) + }) + .context("Failed to draw to offscreen render target")?; + + renderer = self + .api + .single_renderer(&self.target_node) + .map_err(|err| anyhow::format_err!("Failed to create renderer: {:?}", err))?; + + elements = postprocess_elements( + &mut renderer, + &self.output, + &pre_postprocess_data, + postprocess_state, + &self.screen_filter, + ); + + if let Err(err) = compositor.with_compositor(|c| c.use_vrr(vrr)) { + warn!("Unable to set adaptive VRR state: {}", err); + } + compositor.render_frame( + &mut renderer, + &elements, + [0.0, 0.0, 0.0, 0.0], + self.frame_flags + .union(additional_frame_flags) + .difference(remove_frame_flags), + ) + } else { + if let Err(err) = compositor.with_compositor(|c| c.use_vrr(vrr)) { + warn!("Unable to set adaptive VRR state: {}", err); + } + compositor.render_frame( + &mut renderer, + &elements, + CLEAR_COLOR, // TODO use a theme neutral color + self.frame_flags + .union(additional_frame_flags) + .difference(remove_frame_flags), + ) + }; + self.timings.draw_done(&self.clock); + + match res { + Ok(frame_result) => { + let (tx, rx) = std::sync::mpsc::channel(); + + let feedback = if !frame_result.is_empty && self.mirroring.is_none() { + Some(( + self.shell + .read() + .take_presentation_feedback(&self.output, &frame_result.states), + rx, + estimated_presentation, + )) + } else { + None + }; + + if frame_result.needs_sync() + && let PrimaryPlaneElement::Swapchain(elem) = &frame_result.primary_element + { + elem.sync.wait()?; + } + + match compositor.queue_frame(feedback) { + x @ Ok(()) | x @ Err(FrameError::EmptyFrame) => { + self.timings.submitted_for_presentation(&self.clock); + + // Update `state` after `queue_frame`, before any early return from errors + if x.is_ok() { + let new_state = QueueState::WaitingForVBlank { + redraw_needed: false, + }; + match mem::replace(&mut self.state, new_state) { + QueueState::Idle => unreachable!(), + QueueState::Queued(_) => (), + QueueState::WaitingForVBlank { .. } => unreachable!(), + QueueState::WaitingForEstimatedVBlank(estimated_vblank) + | QueueState::WaitingForEstimatedVBlankAndQueued { + estimated_vblank, + .. + } => { + self.loop_handle.remove(estimated_vblank); + } + }; + } + + let now = self.clock.now(); + for (session, frame, res) in frames { + if let Err(err) = send_screencopy_result( + &mut renderer, + &self.output, + &mut pre_postprocess_data, + &tx, + &frame_result, + &elements, + (&session, frame, res), + now.into(), + ) { + tracing::warn!(?err, "Failed to screencopy"); + } + } + + if self.mirroring.is_none() { + // If postprocessing, use states from first render + let states = pre_postprocess_data.states.unwrap_or(frame_result.states); + self.send_dmabuf_feedback(states); + } + + if x.is_ok() { + if self.mirroring.is_none() { + self.frame_callback_seq = self.frame_callback_seq.wrapping_add(1); + self.send_frame_callbacks(); + } + } else { + // we don't expect a vblank + let _ = self.vblank_frame.take(); + + self.queue_estimated_vblank( + estimated_presentation, + // Make sure we redraw to reevaluate, if we intentionally missed content + additional_frame_flags + .contains(FrameFlags::SKIP_CURSOR_ONLY_UPDATES), + ); + } + } + Err(err) => { + for (_session, frame, _) in frames { + frame.fail(CaptureFailureReason::Unknown); + } + return Err(err).with_context(|| "Failed to submit result for display"); + } + }; + } + Err(err) => { + compositor.reset_buffers(); + anyhow::bail!("Rendering failed: {}", err); + } + } + + for device in self.api.devices_mut()? { + device.renderer_mut().cleanup_texture_cache()?; + } + + Ok(()) + } + + fn queue_estimated_vblank(&mut self, target_presentation_time: Duration, force: bool) { + match mem::take(&mut self.state) { + QueueState::Idle => unreachable!(), + QueueState::Queued(_) => (), + QueueState::WaitingForVBlank { .. } => unreachable!(), + QueueState::WaitingForEstimatedVBlank(token) + | QueueState::WaitingForEstimatedVBlankAndQueued { + estimated_vblank: token, + .. + } => { + self.state = QueueState::WaitingForEstimatedVBlank(token); + return; + } + } + + let now = self.clock.now(); + let mut duration = target_presentation_time.saturating_sub(now.into()); + + // No use setting a zero timer, since we'll send frame callbacks anyway right after the call to + // render(). This can happen for example with unknown presentation time from DRM. + if duration.is_zero() { + duration += self.timings.refresh_interval(); + } + + trace!("queueing estimated vblank timer to fire in {duration:?}"); + + let timer = Timer::from_duration(duration); + let token = self + .loop_handle + .insert_source(timer, move |_, _, data| { + data.on_estimated_vblank(force); + TimeoutAction::Drop + }) + .unwrap(); + self.state = QueueState::WaitingForEstimatedVBlank(token); + } + + fn update_mirroring(&mut self, mirroring_output: Option) { + self.mirroring = mirroring_output; + self.postprocess_textures.clear(); + } + + fn update_screen_filter(&mut self, filter_config: ScreenFilter) { + self.screen_filter = filter_config; + self.postprocess_textures.clear(); + } + + fn send_frame_callbacks(&mut self) { + if self.mirroring.is_none() { + let _ = self + .thread_sender + .send(SurfaceCommand::SendFrames(self.frame_callback_seq)); + } + } + + fn send_dmabuf_feedback(&mut self, states: RenderElementStates) { + let _ = self + .thread_sender + .send(SurfaceCommand::RenderStates(states)); + } +} + +fn source_node_for_surface(w: &WlSurface) -> Option { + with_renderer_surface_state(w, |state| { + state + .buffer() + .and_then(|buffer| get_dmabuf(buffer).ok().and_then(|dmabuf| dmabuf.node())) + }) + .flatten() +} + +// TODO: Introduce can_shared_dmabuf_framebuffer for cases where we might select another gpu +// and composite on target if not possible to finally get rid of "primary" +#[profiling::function] +fn render_node_for_output( + output: &Output, + primary_node: &DrmNode, + target_node: &DrmNode, + shell: &Shell, +) -> DrmNode { + if target_node == primary_node { + return *target_node; + } + + let Some(workspace) = shell.active_space(output) else { + return *target_node; + }; + let fullscreens: Vec<_> = workspace + .get_fullscreen_surfaces() + .map(|f| f.surface.clone()) + .collect(); + let nodes = if !fullscreens.is_empty() { + fullscreens + } else { + workspace + .mapped() + .map(|mapped| mapped.active_window()) + .collect::>() + } + .into_iter() + .flat_map(|w| w.wl_surface().and_then(|s| source_node_for_surface(&s))) + .collect::>(); + + if nodes.contains(target_node) || nodes.is_empty() { + *target_node + } else { + *primary_node + } +} + +fn get_surface_dmabuf_feedback( + render_node: DrmNode, + target_node: DrmNode, + render_formats: FormatSet, + _target_formats: FormatSet, + primary_plane_formats: FormatSet, + overlay_plane_formats: Option, +) -> SurfaceDmabufFeedback { + // We limit the scan-out trache to formats we can also render from + // so that there is always a fallback render path available in case + // the supplied buffer can not be scanned out directly + + let primary_plane_formats = primary_plane_formats + .intersection(&render_formats) + .cloned() + .collect::(); + let overlay_plane_formats = overlay_plane_formats.map(|formats| { + formats + .intersection(&render_formats) + .cloned() + .collect::() + }); + let builder = DmabufFeedbackBuilder::new(render_node.dev_id(), render_formats); + + /* + // Sadly no implementation would pick this up as a preferred render tranche, + // where the combined formats would increase our chances of doing a dmabuf copy. + // .. So we should probably not advertise this on the off-chance it actually triggers bugs. + // + + let combined_formats = render_formats.intersection(&target_formats).cloned().collect::(); + if target_node != render_node.dev_id() && !combined_formats.is_empty() { + builder = builder.add_preference_tranche( + render_node.dev_id(), + None, + combined_formats, + ); + }; + + // We also can't advertise scan out tranches for the actual display device, + // as e.g. the nvidia driver might then send us dmabufs, that makes e.g. the iris hangs on import... + if target_node != render_node.dev_id() && !combined_formats.is_empty() { + builder = builder.add_preference_tranche( + target_node.dev_id(), + Some(zwp_linux_dmabuf_feedback_v1::TrancheFlags::Scanout), + combined_formats, + ); + }; + + // So no fun combinations, we gotta wait for dmabuf-v6 + */ + + let render_feedback = builder.clone().build().unwrap(); + let primary_scanout_feedback = (target_node == render_node).then(|| { + builder + .clone() + .add_preference_tranche( + render_node.dev_id(), + Some(zwp_linux_dmabuf_feedback_v1::TrancheFlags::Scanout), + primary_plane_formats, + ) + .build() + .unwrap() + }); + let overlay_scanout_feedback = overlay_plane_formats + .filter(|_| target_node == render_node) + .map(|formats| { + builder + .add_preference_tranche( + render_node.dev_id(), + Some(zwp_linux_dmabuf_feedback_v1::TrancheFlags::Scanout), + formats, + ) + .build() + .unwrap() + }); + + SurfaceDmabufFeedback { + render_feedback, + overlay_scanout_feedback, + primary_scanout_feedback, + } +} + +fn take_screencopy_frames( + output: &Output, + elements: &[CosmicElement], + has_cursor_mode_none: &mut bool, +) -> Vec<( + ScreencopySessionRef, + ScreencopyFrame, + Result<(Option>>, RenderElementStates), OutputNoMode>, +)> { + output + .take_pending_frames() + .into_iter() + .map(|(session, frame)| { + let additional_damage = frame.damage(); + let session_data = session.user_data().get::().unwrap(); + let mut damage_tracking = session_data.lock().unwrap(); + + let buffer = frame.buffer(); + let age = if matches!(buffer_type(&buffer), Some(BufferType::Shm)) { + // TODO re-use offscreen buffer to damage track screencopy to shm + 0 + } else { + 1 + }; + + if !additional_damage.is_empty() { + let area = output + .current_mode() + .unwrap() + /* TODO: Mode is Buffer..., why is this Physical in the first place */ + .size + .to_logical(1) + .to_buffer(1, Transform::Normal) + .to_f64(); + + let additional_damage_elements: Vec<_> = additional_damage + .into_iter() + .map(|rect| { + rect.to_f64() + .to_logical( + output.current_scale().fractional_scale(), + output.current_transform(), + &area, + ) + .to_i32_round() + }) + .map(DamageElement::new) + .collect(); + let _ = damage_tracking + .dt + .damage_output(age, &additional_damage_elements); + }; + + let res = damage_tracking.dt.damage_output(age, elements); + + if !session.draw_cursor() { + *has_cursor_mode_none = true; + } + + let res = res.map(|(a, b)| (a.cloned(), b)); + std::mem::drop(damage_tracking); + (session, frame, res) + }) + .collect() +} + +fn send_screencopy_result<'a>( + renderer: &mut GlMultiRenderer<'a>, + output: &Output, + pre_postprocess_data: &mut PrePostprocessData, + tx: &std::sync::mpsc::Sender, + frame_result: &RenderFrameResult>>, + elements: &[CosmicElement>], + (session, frame, res): ( + &ScreencopySessionRef, + ScreencopyFrame, + Result<(Option>>, RenderElementStates), OutputNoMode>, + ), + presentation_time: Duration, +) -> Result<()> { + let (damage, _) = res?; + + let mut sync = SyncPoint::default(); + let mut dmabuf_clone; + let mut render_buffer; + let buffer = frame.buffer(); + let mut shm_buffer = false; + let buffer_size = buffer_dimensions(&buffer).ok_or(RenderError::< + ::Error, + >::Rendering( + MultiError::ImportFailed + ))?; + let mut fb = if let Ok(dmabuf) = get_dmabuf(&buffer) { + dmabuf_clone = dmabuf.clone(); + Some( + renderer + .bind(&mut dmabuf_clone) + .map_err(RenderError::<::Error>::Rendering)?, + ) + } else { + shm_buffer = true; + let format = with_buffer_contents(&buffer, |_, _, data| shm_format_to_fourcc(data.format)) + .map_err(|_| OutputNoMode)? // eh, we have to do some error + .expect("We should be able to convert all hardcoded shm screencopy formats"); + + if pre_postprocess_data + .texture + .as_ref() + .is_some_and(|tex| tex.format() == Some(format)) + && (!session.draw_cursor() || pre_postprocess_data.cursor_texture.is_none()) + { + None + } else { + render_buffer = + Offscreen::::create_buffer(renderer, format, buffer_size) + .map_err(RenderError::<::Error>::Rendering)?; + Some( + renderer + .bind(&mut render_buffer) + .map_err(RenderError::<::Error>::Rendering)?, + ) + } + }; + + if let Some(ref damage) = damage { + let (output_size, output_scale, output_transform) = ( + output.current_mode().ok_or(OutputNoMode)?.size, + output.current_scale().fractional_scale(), + output.current_transform(), + ); + + let filter = (!session.draw_cursor()) + .then(|| { + elements.iter().filter_map(|elem| { + if let CosmicElement::Cursor(_) = elem { + Some(elem.id().clone()) + } else { + None + } + }) + }) + .into_iter() + .flatten(); + + // If the screen is rotated, we must convert damage to match output. + let adjusted = damage + .iter() + .copied() + .map(|rect| { + let logical = rect.to_logical(1); + logical + .to_buffer( + 1, + output_transform.invert(), + &buffer_size.to_logical(1, output_transform), + ) + .to_logical(1, Transform::Normal, &buffer_size) + .to_physical(1) + }) + .collect::>(); + + if let Some(tex) = pre_postprocess_data.texture.as_mut() { + let tex_fb = renderer + .bind(tex) + .map_err(RenderError::<::Error>::Rendering)?; + + if let Some(fb) = fb.as_mut() { + for rect in adjusted.iter().copied() { + // TODO: On Vulkan, may need to combine sync points instead of just using latest? + sync = renderer + .blit(&tex_fb, fb, rect, rect, TextureFilter::Linear) + .map_err( + RenderError::<::Error>::Rendering, + )?; + } + if let Some(cursor_geometry) = pre_postprocess_data + .cursor_geometry + .as_ref() + .filter(|_| session.draw_cursor()) + { + let cursor_damage = adjusted + .iter() + .filter_map(|rect| cursor_geometry.intersection(*rect)) + .map(|rect| Rectangle::new(rect.loc - cursor_geometry.loc, rect.size)) + .collect::>(); + let mut frame = renderer.render(fb, output_size, output_transform).map_err( + RenderError::<::Error>::Rendering, + )?; + frame + .as_mut() + .render_texture_from_to( + pre_postprocess_data.cursor_texture.as_ref().unwrap(), + Rectangle::new( + Point::from((0., 0.)), + cursor_geometry + .size + .to_logical(1) + .to_buffer(1, Transform::Normal) + .to_f64(), + ), + *cursor_geometry, + &cursor_damage, + &[*cursor_geometry], + Transform::Normal, + 1.0, + ) + .map_err(GlMultiError::Render) + .map_err( + RenderError::<::Error>::Rendering, + )?; + let sync = frame.finish().map_err( + RenderError::<::Error>::Rendering, + )?; + renderer.wait(&sync).map_err( + RenderError::<::Error>::Rendering, + )?; + } + } else { + fb = Some(tex_fb); + } + } else { + sync = frame_result + .blit_frame_result( + output_size, + output_transform, + output_scale, + renderer, + fb.as_mut().unwrap(), + adjusted, + filter, + ) + .map_err(|err| match err { + BlitFrameResultError::Rendering(err) => { + RenderError::<::Error>::Rendering(err) + } + BlitFrameResultError::Export(_) => { + RenderError::<::Error>::Rendering( + MultiError::DeviceMissing, + ) + } + })?; + }; + } + + let transform = output.current_transform(); + + if let Some(data) = submit_buffer( + frame, + renderer, + shm_buffer.then_some(fb.as_mut().unwrap()), + transform, + damage.as_deref(), + sync, + // Don't reference `Buffer`s since we blit from framebuffer/postprocess buffer + vec![], + )? { + if frame_result.is_empty { + data.frame + .success(transform, data.damage, presentation_time); + } else { + let _ = tx.send(data); + } + } + + Ok(()) +} + +fn postprocess_elements<'a>( + renderer: &mut GlMultiRenderer<'a>, + output: &Output, + pre_postprocess_data: &PrePostprocessData, + postprocess_state: &PostprocessState, + screen_filter: &ScreenFilter, +) -> Vec>> { + let postprocess_texture_shader = Borrow::::borrow(renderer.as_ref()) + .egl_context() + .user_data() + .get::() + .expect("OffscreenShader should be available through `init_shaders`"); + + let mut elements: [Option; 2] = [None, None]; + if let Some(cursor_texture) = postprocess_state.cursor_texture.as_ref() { + let cursor_geometry = pre_postprocess_data.cursor_geometry.unwrap(); + let texture_elem = TextureRenderElement::from_texture_render_buffer( + cursor_geometry.loc.to_f64(), + cursor_texture, + None, + Some(Rectangle::new( + Point::from((0., 0.)), + cursor_geometry.size.to_logical(1).to_f64(), + )), + Some( + cursor_geometry + .size + .to_f64() + .to_logical(output.current_scale().fractional_scale()) + .to_i32_round(), + ), + Kind::Cursor, + ); + + elements[0] = Some(TextureShaderElement::new( + texture_elem, + postprocess_texture_shader.0.clone(), + vec![ + Uniform::new("invert", if screen_filter.inverted { 1. } else { 0. }), + Uniform::new( + "color_mode", + screen_filter + .color_filter + .map(|val| val as u8 as f32) + .unwrap_or(0.), + ), + ], + )); + } + + let texture_elem = TextureRenderElement::from_texture_render_buffer( + (0., 0.), + &postprocess_state.texture, + None, + Some(Rectangle::new( + Point::from((0., 0.)), + postprocess_state.output_config.size.to_logical(1).to_f64(), + )), + Some( + postprocess_state + .output_config + .size + .to_f64() + .to_logical(postprocess_state.output_config.fractional_scale) + .to_i32_round(), + ), + Kind::Unspecified, + ); + elements[1] = Some(TextureShaderElement::new( + texture_elem, + postprocess_texture_shader.0.clone(), + vec![ + Uniform::new("invert", if screen_filter.inverted { 1. } else { 0. }), + Uniform::new( + "color_mode", + screen_filter + .color_filter + .map(|val| val as u8 as f32) + .unwrap_or(0.), + ), + ], + )); + + constrain_render_elements( + elements.into_iter().flatten(), + (0, 0), + Rectangle::from_size( + output + .geometry() + .size + .as_logical() + .to_physical_precise_round(output.current_scale().fractional_scale()), + ), + Rectangle::new(Point::from((0, 0)), postprocess_state.output_config.size), + ConstrainScaleBehavior::Fit, + ConstrainAlign::CENTER, + postprocess_state.output_config.fractional_scale, + ) + .map(CosmicElement::::Postprocess) + .collect::>() +} diff --git a/src/backend/kms/surface/timings.rs b/src/backend/kms/surface/timings.rs new file mode 100644 index 0000000..d2bc5ef --- /dev/null +++ b/src/backend/kms/surface/timings.rs @@ -0,0 +1,391 @@ +use std::{collections::VecDeque, num::NonZeroU64, time::Duration}; + +use smithay::{ + backend::drm::DrmNode, + utils::{Clock, Monotonic, Time}, +}; +use tracing::{debug, error}; + +const BASE_SAFETY_MARGIN: Duration = Duration::from_millis(3); +const SAMPLE_TIME_WINDOW: usize = 5; + +pub struct Timings { + refresh_interval_ns: Option, + min_refresh_interval_ns: Option, + vrr: bool, + vendor: Option, + + pub pending_frame: Option, + pub previous_frames: VecDeque, +} + +#[derive(Debug)] +pub struct PendingFrame { + render_start: Time, + render_duration_elements: Option, + render_duration_draw: Option, + presentation_submitted: Option>, +} + +#[derive(Debug)] +pub struct Frame { + pub render_start: Time, + pub render_duration_elements: Duration, + pub render_duration_draw: Duration, + pub presentation_submitted: Time, + pub presentation_presented: Time, +} + +impl Frame { + fn render_time(&self) -> Duration { + self.render_duration_elements + self.render_duration_draw + } + + fn submit_time(&self) -> Duration { + Time::elapsed(&self.render_start, self.presentation_submitted) + } + + fn frame_time(&self) -> Duration { + Time::elapsed(&self.render_start, self.presentation_presented) + } +} + +impl Timings { + const CLEANUP: usize = 360; + + pub fn new( + refresh_interval: Option, + min_interval: Option, + vrr: bool, + node: DrmNode, + ) -> Self { + let refresh_interval_ns = if let Some(interval) = &refresh_interval { + assert_eq!(interval.as_secs(), 0); + Some(NonZeroU64::new(interval.subsec_nanos().into()).unwrap()) + } else { + None + }; + + let min_refresh_interval_ns = if let Some(interval) = &min_interval { + assert_eq!(interval.as_secs(), 0); + Some(NonZeroU64::new(interval.subsec_nanos().into()).unwrap()) + } else { + None + }; + + let vendor = if let Ok(vendor) = std::fs::read_to_string(format!( + "/sys/class/drm/renderD{}/device/vendor", + node.minor() + )) { + u32::from_str_radix(&vendor.trim()[2..], 16).ok() + } else { + None + }; + + Self { + refresh_interval_ns, + min_refresh_interval_ns, + vrr, + vendor, + + pending_frame: None, + previous_frames: VecDeque::new(), + } + } + + pub fn refresh_interval(&self) -> Duration { + match self.refresh_interval_ns { + Some(ns) => Duration::from_nanos(ns.get()), + None => Duration::ZERO, + } + } + + pub fn set_refresh_interval(&mut self, interval: Option) { + self.refresh_interval_ns = interval + .map(|duration| duration.subsec_nanos() as u64) + .and_then(NonZeroU64::new); + + self.previous_frames.clear(); + } + + pub fn set_min_refresh_interval(&mut self, min_interval: Option) { + self.min_refresh_interval_ns = min_interval + .map(|duration| duration.subsec_nanos() as u64) + .and_then(NonZeroU64::new); + } + + pub fn set_vrr(&mut self, vrr: bool) { + self.vrr = vrr; + } + + pub fn vrr(&self) -> bool { + self.vrr + } + + pub fn start_render(&mut self, clock: &Clock) { + self.pending_frame = Some(PendingFrame { + render_start: clock.now(), + render_duration_elements: None, + render_duration_draw: None, + presentation_submitted: None, + }); + } + + pub fn elements_done(&mut self, clock: &Clock) { + if let Some(frame) = self.pending_frame.as_mut() { + frame.render_duration_elements = Some(Time::elapsed(&frame.render_start, clock.now())); + } + } + + pub fn draw_done(&mut self, clock: &Clock) { + if let Some(frame) = self.pending_frame.as_mut() { + frame.render_duration_draw = Some( + Time::elapsed(&frame.render_start, clock.now()) + - frame.render_duration_elements.unwrap_or(Duration::ZERO), + ); + } + } + + pub fn submitted_for_presentation(&mut self, clock: &Clock) { + if let Some(frame) = self.pending_frame.as_mut() { + frame.presentation_submitted = Some(clock.now()); + } + } + + pub fn presented(&mut self, value: Time) { + if let Some(frame) = self.pending_frame.take() { + let new_frame = Frame { + render_start: frame.render_start, + render_duration_elements: frame.render_duration_elements.unwrap_or_default(), + render_duration_draw: frame.render_duration_draw.unwrap_or_default(), + presentation_submitted: frame.presentation_submitted.unwrap(), + presentation_presented: value, + }; + if new_frame.render_start > new_frame.presentation_submitted { + debug!( + "frame time overflowed: {}", + new_frame.frame_time().as_millis() + ); + } + self.previous_frames.push_back(new_frame); + + if let Some(overflow) = self.previous_frames.len().checked_sub(Self::CLEANUP * 2) { + self.previous_frames = self.previous_frames.split_off(overflow + Self::CLEANUP); + } + } + } + + pub fn discard_current_frame(&mut self) { + let _ = self.pending_frame.take(); + } + + pub fn max_rendertime(&self) -> Duration { + self.previous_frames + .iter() + .map(|f| f.render_time()) + .max() + .unwrap_or(Duration::ZERO) + } + + pub fn min_rendertime(&self) -> Duration { + self.previous_frames + .iter() + .map(|f| f.render_time()) + .min() + .unwrap_or(Duration::ZERO) + } + + pub fn max_frametime(&self, window: usize) -> Duration { + self.previous_frames + .iter() + .rev() + .take(window) + .map(|f| f.frame_time()) + .max() + .unwrap_or(Duration::ZERO) + } + + pub fn min_frametime(&self, window: usize) -> Duration { + self.previous_frames + .iter() + .rev() + .take(window) + .map(|f| f.frame_time()) + .min() + .unwrap_or(Duration::ZERO) + } + + pub fn avg_rendertime(&self) -> Duration { + let Some(sum_rendertime) = self + .previous_frames + .iter() + .map(|f| f.render_time()) + .try_fold(Duration::ZERO, |acc, x| acc.checked_add(x)) + else { + return Duration::ZERO; + }; + + sum_rendertime + .checked_div(self.previous_frames.len() as u32) + .unwrap_or(Duration::ZERO) + } + + pub fn avg_submittime(&self, window: usize) -> Option { + if self.previous_frames.len() < window || window == 0 { + return None; + } + + Some( + self.previous_frames + .iter() + .rev() + .take(window) + .map(|f| f.submit_time()) + .try_fold(Duration::ZERO, |acc, x| acc.checked_add(x))? + / (window.min(self.previous_frames.len()) as u32), + ) + } + + pub fn avg_frametime(&self, window: usize) -> Option { + if self.previous_frames.len() < window || window == 0 { + return None; + } + + Some( + self.previous_frames + .iter() + .rev() + .take(window) + .map(|f| f.frame_time()) + .try_fold(Duration::ZERO, |acc, x| acc.checked_add(x))? + / (window.min(self.previous_frames.len()) as u32), + ) + } + + pub fn avg_fps(&self) -> f64 { + if self.previous_frames.is_empty() { + return 0.0; + } + let secs = match (self.previous_frames.front(), self.previous_frames.back()) { + (Some(Frame { render_start, .. }), Some(end_frame)) => { + Time::elapsed(render_start, end_frame.render_start) + end_frame.frame_time() + } + _ => { + return 0.0; + } + } + .as_secs_f64(); + + 1.0 / (secs / self.previous_frames.len() as f64) + } + + pub fn next_presentation_time(&self, clock: &Clock) -> Duration { + let mut now = clock.now().into(); + + let Some(refresh_interval_ns) = self.refresh_interval_ns else { + return Duration::ZERO; + }; + let Some(last_presentation_time): Option = self + .previous_frames + .back() + .map(|frame| frame.presentation_presented.into()) + else { + return Duration::ZERO; + }; + let refresh_interval_ns = refresh_interval_ns.get(); + + if now <= last_presentation_time { + // Got an early VBlank. + let orig_now = now; + now += Duration::from_nanos(refresh_interval_ns); + + if now < last_presentation_time { + // Not sure when this can happen. + error!( + now = ?orig_now, + ?last_presentation_time, + "got a 2+ early VBlank, {:?} until presentation", + last_presentation_time - now, + ); + now = last_presentation_time + Duration::from_nanos(refresh_interval_ns); + } + } + + let since_last = now - last_presentation_time; + let since_last_ns = + since_last.as_secs() * 1_000_000_000 + u64::from(since_last.subsec_nanos()); + let to_next_ns = (since_last_ns / refresh_interval_ns + 1) * refresh_interval_ns; + + // If VRR is enabled and more than one frame passed since last presentation, assume that we + // can present immediately. + if self.vrr && to_next_ns > refresh_interval_ns { + Duration::ZERO + } else { + last_presentation_time + Duration::from_nanos(to_next_ns) - now + } + } + + pub fn past_min_render_time(&self, clock: &Clock) -> bool { + let now: Duration = clock.now().into(); + let Some(min_refresh_interval_ns) = self.min_refresh_interval_ns else { + return true; + }; + let Some(last_presentation_time): Option = self + .previous_frames + .back() + .map(|frame| frame.presentation_presented.into()) + else { + return true; + }; + + let min_refresh_interval_ns = min_refresh_interval_ns.get(); + if now <= last_presentation_time { + return false; + } + + const MIN_MARGIN: Duration = Duration::from_millis(3); + let baseline = if let Some(refresh_interval_ns) = self.refresh_interval_ns { + MIN_MARGIN.max(Duration::from_nanos(refresh_interval_ns.get() / 2)) + } else { + MIN_MARGIN + }; + + let next_presentation_time = + last_presentation_time + Duration::from_nanos(min_refresh_interval_ns); + let deadline = next_presentation_time.saturating_sub( + if let Some(avg_submittime) = self.avg_submittime(SAMPLE_TIME_WINDOW) { + avg_submittime + } else { + baseline + } + BASE_SAFETY_MARGIN, + ); + + now >= deadline + } + + pub fn next_render_time(&self, clock: &Clock) -> Duration { + let Some(refresh_interval) = self.refresh_interval_ns else { + return Duration::ZERO; // we don't know what to expect, so render immediately. + }; + + const MIN_MARGIN: Duration = Duration::from_millis(3); + let baseline = MIN_MARGIN.max(Duration::from_nanos(refresh_interval.get() / 2)); + + let estimated_presentation_time = self.next_presentation_time(clock); + if estimated_presentation_time.is_zero() { + return Duration::ZERO; + } + + // HACK: Nvidia returns `page_flip`/`commit` early, so we have no information to optimize latency on submission. + if self.vendor == Some(0x10de) { + return Duration::ZERO; + } + + let Some(avg_submittime) = self.avg_submittime(SAMPLE_TIME_WINDOW) else { + return estimated_presentation_time.saturating_sub(baseline + BASE_SAFETY_MARGIN); + }; + + let margin = avg_submittime + BASE_SAFETY_MARGIN; + estimated_presentation_time.saturating_sub(margin) + } +} diff --git a/src/backend/mod.rs b/src/backend/mod.rs new file mode 100644 index 0000000..b9586b5 --- /dev/null +++ b/src/backend/mod.rs @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::state::State; +use crate::wayland::protocols::a11y::A11yHandler; +use anyhow::{Context, Result, anyhow}; +use cosmic_comp_config::NumlockState; +use cosmic_config::CosmicConfigEntry; +use cosmic_settings_daemon_config::greeter; +use smithay::reexports::{calloop::EventLoop, wayland_server::DisplayHandle}; +use tracing::{info, warn}; + +pub mod render; + +pub mod kms; +pub mod winit; +pub mod x11; +// TODO +// pub mod wayland; // tbd in smithay + +pub fn init_backend_auto( + dh: &DisplayHandle, + event_loop: &mut EventLoop<'static, State>, + state: &mut State, +) -> Result<()> { + let res = match std::env::var("COSMIC_BACKEND") { + Ok(x) if x == "x11" => x11::init_backend(dh, event_loop, state), + Ok(x) if x == "winit" => winit::init_backend(dh, event_loop, state), + Ok(x) if x == "kms" => kms::init_backend(dh, event_loop, state), + Ok(_) => unimplemented!("There is no backend with this identifier"), + Err(_) => { + if std::env::var_os("DISPLAY").is_some() + || std::env::var_os("WAYLAND_DISPLAY").is_some() + { + match x11::init_backend(dh, event_loop, state) { + Ok(_) => Ok(()), + Err(err) => { + warn!(?err, "Initializing X11 Backend failed."); + info!("Falling back to winit backend."); + winit::init_backend(dh, event_loop, state) + } + } + } else { + kms::init_backend(dh, event_loop, state) + } + } + }; + + if res.is_ok() { + let output = state + .common + .shell + .read() + .outputs() + .next() + .with_context(|| "Backend initialized without output") + .cloned()?; + let initial_seat = crate::shell::create_seat( + dh, + &mut state.common.seat_state, + &output, + &state.common.config, + "seat-0".into(), + ); + + let keyboard = initial_seat + .get_keyboard() + .ok_or_else(|| anyhow!("`shell::create_seat` did not setup keyboard"))?; + + state + .common + .shell + .write() + .seats + .add_seat(initial_seat.clone()); + + let greeter_state = match greeter::GreeterAccessibilityState::config() { + Ok(helper) => match greeter::GreeterAccessibilityState::get_entry(&helper) { + Ok(s) => s, + Err((errs, s)) => { + for err in errs { + tracing::error!("Error loading greeter state: {err:?}"); + } + s + } + }, + Err(_) => { + tracing::info!("`cosmic-greeter` state not found."); + greeter::GreeterAccessibilityState::default() + } + }; + + if let Some(magnifier) = greeter_state.magnifier { + let mut zoom = state.common.config.cosmic_conf.accessibility_zoom; + + zoom.start_on_login = magnifier; + if let Err(err) = state + .common + .config + .cosmic_conf + .set_accessibility_zoom(&state.common.config.cosmic_helper, zoom) + { + tracing::error!("Failed to set screen filter: {err:?}"); + } + } + + if let Some(inverted) = greeter_state.invert_colors + && inverted != state.a11y_state().screen_inverted() + { + state.request_screen_invert(inverted); + } + + if state + .common + .config + .cosmic_conf + .accessibility_zoom + .start_on_login + { + state.common.shell.write().trigger_zoom( + &initial_seat, + None, + 1.0 + (state.common.config.cosmic_conf.accessibility_zoom.increment as f64 / 100.), + &state.common.config.cosmic_conf.accessibility_zoom, + true, + &state.common.event_loop_handle, + ); + } + + let desired_numlock = state + .common + .config + .cosmic_conf + .keyboard_config + .numlock_state; + // Restore numlock state based on config. + let toggle_numlock = match desired_numlock { + NumlockState::BootOff => keyboard.modifier_state().num_lock, + NumlockState::BootOn => !keyboard.modifier_state().num_lock, + NumlockState::LastBoot => { + keyboard.modifier_state().num_lock + != state.common.config.dynamic_conf.numlock().last_state + } + }; + + // If we're enabling numlock... + if toggle_numlock { + /// Linux scancode for numlock key. + const NUMLOCK_SCANCODE: u32 = 69; + crate::config::change_modifier_state(&keyboard, NUMLOCK_SCANCODE, state); + } + { + { + state + .common + .startup_done + .store(true, std::sync::atomic::Ordering::SeqCst); + for output in state.common.shell.read().outputs() { + state.backend.schedule_render(output); + } + } + } + } + res +} diff --git a/src/backend/render/animations/mod.rs b/src/backend/render/animations/mod.rs new file mode 100644 index 0000000..cc5365e --- /dev/null +++ b/src/backend/render/animations/mod.rs @@ -0,0 +1 @@ +pub mod spring; diff --git a/src/backend/render/animations/spring.rs b/src/backend/render/animations/spring.rs new file mode 100644 index 0000000..72b4390 --- /dev/null +++ b/src/backend/render/animations/spring.rs @@ -0,0 +1,138 @@ +// From Niri (GPL-3.0) https://github.com/YaLTeR/niri/tree/db49deb7fd2fbe805ceec060aa4dec65009ad7a7 +use std::time::Duration; + +#[derive(Debug, Clone, Copy)] +pub struct SpringParams { + pub damping: f64, + pub mass: f64, + pub stiffness: f64, + pub epsilon: f64, +} + +#[derive(Debug, Clone, Copy)] +pub struct Spring { + pub from: f64, + pub to: f64, + pub initial_velocity: f64, + pub params: SpringParams, +} + +impl SpringParams { + pub fn new(damping_ratio: f64, stiffness: f64, epsilon: f64) -> Self { + let damping_ratio = damping_ratio.max(0.); + let stiffness = stiffness.max(0.); + let epsilon = epsilon.max(0.); + + let mass = 1.; + let critical_damping = 2. * (mass * stiffness).sqrt(); + let damping = damping_ratio * critical_damping; + + Self { + damping, + mass, + stiffness, + epsilon, + } + } +} + +impl Spring { + pub fn value_at(&self, t: Duration) -> f64 { + self.oscillate(t.as_secs_f64()) + } + + // Based on libadwaita (LGPL-2.1-or-later): + // https://gitlab.gnome.org/GNOME/libadwaita/-/blob/1.4.4/src/adw-spring-animation.c, + // which itself is based on (MIT): + // https://github.com/robb/RBBAnimation/blob/master/RBBAnimation/RBBSpringAnimation.m + /// Computes and returns the duration until the spring is at rest. + pub fn duration(&self) -> Duration { + const DELTA: f64 = 0.001; + + let beta = self.params.damping / (2. * self.params.mass); + + if beta.abs() <= f64::EPSILON || beta < 0. { + return Duration::MAX; + } + + let omega0 = (self.params.stiffness / self.params.mass).sqrt(); + + // As first ansatz for the overdamped solution, + // and general estimation for the oscillating ones + // we take the value of the envelope when it's < epsilon. + let mut x0 = -self.params.epsilon.ln() / beta; + + // f64::EPSILON is too small for this specific comparison, so we use + // f32::EPSILON even though it's doubles. + if (beta - omega0).abs() <= f64::from(f32::EPSILON) || beta < omega0 { + return Duration::from_secs_f64(x0); + } + + // Since the overdamped solution decays way slower than the envelope + // we need to use the value of the oscillation itself. + // Newton's root finding method is a good candidate in this particular case: + // https://en.wikipedia.org/wiki/Newton%27s_method + let mut y0 = self.oscillate(x0); + let m = (self.oscillate(x0 + DELTA) - y0) / DELTA; + + let mut x1 = (self.to - y0 + m * x0) / m; + let mut y1 = self.oscillate(x1); + + let mut i = 0; + while (self.to - y1).abs() > self.params.epsilon { + if i > 1000 { + return Duration::ZERO; + } + + x0 = x1; + y0 = y1; + + let m = (self.oscillate(x0 + DELTA) - y0) / DELTA; + + x1 = (self.to - y0 + m * x0) / m; + y1 = self.oscillate(x1); + i += 1; + } + + Duration::from_secs_f64(x1) + } + + /// Returns the spring position at a given time in seconds. + fn oscillate(&self, t: f64) -> f64 { + let b = self.params.damping; + let m = self.params.mass; + let k = self.params.stiffness; + let v0 = self.initial_velocity; + + let beta = b / (2. * m); + let omega0 = (k / m).sqrt(); + + let x0 = self.from - self.to; + + let envelope = (-beta * t).exp(); + + // Solutions of the form C1*e^(lambda1*x) + C2*e^(lambda2*x) + // for the differential equation m*ẍ+b*ẋ+kx = 0 + + // f64::EPSILON is too small for this specific comparison, so we use + // f32::EPSILON even though it's doubles. + if (beta - omega0).abs() <= f64::from(f32::EPSILON) { + // Critically damped. + self.to + envelope * (x0 + (beta * x0 + v0) * t) + } else if beta < omega0 { + // Underdamped. + let omega1 = ((omega0 * omega0) - (beta * beta)).sqrt(); + + self.to + + envelope + * (x0 * (omega1 * t).cos() + ((beta * x0 + v0) / omega1) * (omega1 * t).sin()) + } else { + // Overdamped. + let omega2 = ((beta * beta) - (omega0 * omega0)).sqrt(); + + self.to + + envelope + * (x0 * (omega2 * t).cosh() + ((beta * x0 + v0) / omega2) * (omega2 * t).sinh()) + } + } +} diff --git a/src/backend/render/cursor.rs b/src/backend/render/cursor.rs new file mode 100644 index 0000000..da01b27 --- /dev/null +++ b/src/backend/render/cursor.rs @@ -0,0 +1,486 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{ + backend::render::{ + element::AsGlowRenderer, + wayland::{SurfaceRenderElement, push_render_elements_from_surface_tree}, + }, + utils::prelude::*, + wayland::handlers::compositor::FRAME_TIME_FILTER, +}; +use smithay::{ + backend::{ + allocator::Fourcc, + renderer::{ + ImportAll, ImportMem, Renderer, + element::{ + Kind, + memory::{MemoryRenderBuffer, MemoryRenderBufferRenderElement}, + }, + }, + }, + desktop::utils::bbox_from_surface_tree, + input::{ + Seat, + pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus}, + }, + reexports::{ + calloop::{ + RegistrationToken, + timer::{TimeoutAction, Timer}, + }, + wayland_server::protocol::wl_surface, + }, + render_elements, + utils::{ + Buffer as BufferCoords, Logical, Monotonic, Physical, Point, Scale, Size, Time, Transform, + }, + wayland::compositor::{get_role, with_states}, +}; +use std::{ + collections::HashMap, + io::Read, + sync::Mutex, + time::{Duration, Instant}, +}; +use tracing::warn; +use xcursor::{ + CursorTheme, + parser::{Image, parse_xcursor}, +}; + +static FALLBACK_CURSOR_DATA: &[u8] = include_bytes!("../../../resources/cursor.rgba"); + +#[derive(Debug, Clone)] +pub struct Cursor { + icons: Vec, + size: u32, +} + +impl Cursor { + pub fn load(theme: &CursorTheme, shape: CursorIcon, size: u32) -> Cursor { + let icons = load_icon(theme, shape) + .map_err(|err| warn!(?err, "Unable to load xcursor, using fallback cursor")) + .or_else(|_| load_icon(theme, CursorIcon::Default)) + .unwrap_or_else(|_| { + vec![Image { + size: 32, + width: 64, + height: 64, + xhot: 1, + yhot: 1, + delay: 1, + pixels_rgba: Vec::from(FALLBACK_CURSOR_DATA), + pixels_argb: vec![], //unused + }] + }); + + Cursor { icons, size } + } + + pub fn get_image(&self, scale: u32, millis: u32) -> Image { + let size = self.size * scale; + frame(millis, size, &self.icons) + } +} + +fn nearest_images(size: u32, images: &[Image]) -> impl Iterator { + // Follow the nominal size of the cursor to choose the nearest + let nearest_image = images + .iter() + .min_by_key(|image| u32::abs_diff(size, image.size)) + .unwrap(); + + images.iter().filter(move |image| { + image.width == nearest_image.width && image.height == nearest_image.height + }) +} + +fn frame(mut millis: u32, size: u32, images: &[Image]) -> Image { + let total = nearest_images(size, images).fold(0, |acc, image| acc + image.delay); + + if total == 0 { + millis = 0; + } else { + millis %= total; + } + + for img in nearest_images(size, images) { + if millis <= img.delay { + return img.clone(); + } + millis -= img.delay; + } + + unreachable!() +} + +#[derive(thiserror::Error, Debug)] +enum Error { + #[error("Theme has no default cursor")] + NoDefaultCursor, + #[error("Error opening xcursor file: {0}")] + File(#[from] std::io::Error), +} + +fn cursor_aliases(name: &str) -> &[&str] { + match name { + "default" => &["default", "left_ptr", "arrow"], + "pointer" => &["pointer", "hand2", "hand"], + "text" => &["text", "xterm"], + "wait" => &["wait", "watch"], + "progress" => &["progress", "left_ptr_watch"], + + "ew-resize" => &["ew-resize", "h_double_arrow", "sb_h_double_arrow"], + "ns-resize" => &["ns-resize", "v_double_arrow", "sb_v_double_arrow"], + "nw-resize" => &["nw-resize", "top_left_corner"], + "ne-resize" => &["ne-resize", "top_right_corner"], + "sw-resize" => &["sw-resize", "bottom_left_corner"], + "se-resize" => &["se-resize", "bottom_right_corner"], + + "w-resize" => &["w-resize", "left_side"], + "e-resize" => &["e-resize", "right_side"], + "n-resize" => &["n-resize", "top_side"], + "s-resize" => &["s-resize", "bottom_side"], + + "move" => &["move", "fleur"], + "not-allowed" => &["not-allowed", "crossed_circle"], + "crosshair" => &["crosshair", "cross"], + "help" => &["help", "question_arrow", "left_ptr_help"], + + _ => &[], + } +} + +fn load_icon(theme: &CursorTheme, shape: CursorIcon) -> Result, Error> { + let shape_name = shape.to_string(); + + for name in cursor_aliases(&shape_name) + .iter() + .copied() + .chain(std::iter::once(shape_name.as_str())) + { + if let Some(icon_path) = theme.load_icon(name) { + let mut cursor_file = std::fs::File::open(&icon_path)?; + let mut cursor_data = Vec::new(); + cursor_file.read_to_end(&mut cursor_data)?; + + if let Some(images) = parse_xcursor(&cursor_data) { + return Ok(images); + } + } + } + + Err(Error::NoDefaultCursor) +} + +render_elements! { + pub CursorRenderElement where R: ImportAll + ImportMem + AsGlowRenderer, R::TextureId: Send; + Static=MemoryRenderBufferRenderElement, + Surface=SurfaceRenderElement, +} + +pub fn draw_surface_cursor( + renderer: &mut R, + surface: &wl_surface::WlSurface, + location: Point, + scale: impl Into>, + blur_strength: usize, + push: &mut dyn FnMut(CursorRenderElement, Point), +) where + R: Renderer + ImportAll + AsGlowRenderer, + R::TextureId: Clone + 'static, +{ + let scale = scale.into(); + let h = with_states(surface, |states| { + states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap() + .hotspot + .to_physical_precise_round(scale) + }); + + push_render_elements_from_surface_tree( + renderer, + surface, + location.to_physical(scale).to_i32_round(), + bbox_from_surface_tree(surface, location.to_i32_round()).to_f64(), + scale, + 1.0, + false, + [0; 4], + None, + blur_strength, + Kind::Cursor, + &mut |elem| push(elem.into(), h), + None, + ); +} + +#[profiling::function] +pub fn draw_dnd_icon( + renderer: &mut R, + surface: &wl_surface::WlSurface, + location: Point, + scale: impl Into>, + blur_strength: usize, + push: &mut dyn FnMut(SurfaceRenderElement), +) where + R: Renderer + ImportAll + AsGlowRenderer, + R::TextureId: Clone + 'static, +{ + if get_role(surface) != Some("dnd_icon") { + warn!( + ?surface, + "Trying to display as a dnd icon a surface that does not have the DndIcon role." + ); + } + let scale = scale.into(); + push_render_elements_from_surface_tree( + renderer, + surface, + location.to_physical(scale).to_i32_round(), + bbox_from_surface_tree(surface, location.to_i32_round()).to_f64(), + scale, + 1.0, + false, + [0; 4], + None, + blur_strength, + FRAME_TIME_FILTER, + push, + None, + ); +} + +pub type CursorState = Mutex; +pub struct CursorStateInner { + current_cursor: Option, + + cursor_theme: CursorTheme, + cursor_size: u32, + + cursors: HashMap, + current_image: Option, + image_cache: Vec<(Image, MemoryRenderBuffer)>, + + hidden: bool, + idle_timer: Option, + last_armed: Option, +} + +impl CursorStateInner { + pub fn set_shape(&mut self, shape: CursorIcon) { + self.current_cursor = Some(shape); + } + + pub fn unset_shape(&mut self) { + self.current_cursor = None; + } + + pub fn get_named_cursor(&mut self, shape: CursorIcon) -> &Cursor { + self.cursors + .entry(shape) + .or_insert_with(|| Cursor::load(&self.cursor_theme, shape, self.cursor_size)) + } + + pub fn size(&self) -> u32 { + self.cursor_size + } +} + +pub fn load_cursor_env() -> (String, u32) { + let name = std::env::var("XCURSOR_THEME") + .ok() + .unwrap_or_else(|| "default".into()); + let size = std::env::var("XCURSOR_SIZE") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(24); + (name, size) +} + +pub fn load_cursor_theme() -> (CursorTheme, u32) { + let (name, size) = load_cursor_env(); + (CursorTheme::load(&name), size) +} + +impl Default for CursorStateInner { + fn default() -> CursorStateInner { + let (theme, size) = load_cursor_theme(); + CursorStateInner { + current_cursor: None, + + cursor_size: size, + cursor_theme: theme, + + cursors: HashMap::new(), + current_image: None, + image_cache: Vec::new(), + + hidden: false, + idle_timer: None, + last_armed: None, + } + } +} + +#[profiling::function] +pub fn draw_cursor( + renderer: &mut R, + seat: &Seat, + location: Point, + scale: Scale, + buffer_scale: f64, + time: Time, + blur_strength: usize, + draw_default: bool, + push: &mut dyn FnMut(CursorRenderElement, Point), +) where + R: Renderer + ImportMem + ImportAll + AsGlowRenderer, + R::TextureId: Send + Clone + 'static, +{ + // draw the cursor as relevant + let cursor_status = seat.cursor_image_status(); + + let seat_userdata = seat.user_data(); + let mut state_ref = seat_userdata.get::().unwrap().lock().unwrap(); + let state = &mut *state_ref; + + if state.hidden { + return; + } + + let named_cursor = state.current_cursor.or(match cursor_status { + CursorImageStatus::Named(named_cursor) => Some(named_cursor), + _ => None, + }); + if let Some(current_cursor) = named_cursor { + if !draw_default && current_cursor == CursorIcon::Default { + return; + } + + let integer_scale = (scale.x.max(scale.y) * buffer_scale).ceil() as u32; + let frame = state + .get_named_cursor(current_cursor) + .get_image(integer_scale, time.as_millis()); + let actual_scale = (frame.size / state.size()).max(1); + + let pointer_images = &mut state.image_cache; + let maybe_image = pointer_images + .iter() + .find_map(|(image, texture)| if image == &frame { Some(texture) } else { None }); + let pointer_image = match maybe_image { + Some(image) => image, + None => { + let buffer = MemoryRenderBuffer::from_slice( + &frame.pixels_rgba, + Fourcc::Argb8888, + (frame.width as i32, frame.height as i32), + actual_scale as i32, + Transform::Normal, + None, + ); + pointer_images.push((frame.clone(), buffer)); + pointer_images.last().map(|(_, i)| i).unwrap() + } + }; + + let hotspot = Point::::from((frame.xhot as i32, frame.yhot as i32)) + .to_logical( + actual_scale as i32, + Transform::Normal, + &Size::from((frame.width as i32, frame.height as i32)), + ); + state.current_image = Some(frame); + + push( + CursorRenderElement::Static( + MemoryRenderBufferRenderElement::from_buffer( + renderer, + location.to_physical(scale), + pointer_image, + None, + None, + None, + Kind::Cursor, + ) + .expect("Failed to import cursor bitmap"), + ), + hotspot.to_physical_precise_round(scale), + ); + } else if let CursorImageStatus::Surface(ref wl_surface) = cursor_status { + draw_surface_cursor(renderer, wl_surface, location, scale, blur_strength, push); + } +} + +const ACTIVITY_THROTTLE: Duration = Duration::from_millis(100); + +/// Reveal the cursor and (re)arm the idle-hide timer; returns true if it was previously hidden +pub fn notify_cursor_activity(state: &State, seat: &Seat) -> bool { + let timeout = state.common.config.cosmic_conf.cursor_hide_timeout; + let loop_handle = &state.common.event_loop_handle; + let cursor_state = seat.user_data().get::().unwrap(); + let now = Instant::now(); + + let (was_hidden, old_token) = { + let mut inner = cursor_state.lock().unwrap(); + let was_hidden = inner.hidden; + inner.hidden = false; + + let throttled = timeout.is_some() + && !was_hidden + && inner.idle_timer.is_some() + && inner + .last_armed + .is_some_and(|t| now.duration_since(t) < ACTIVITY_THROTTLE); + if throttled { + return was_hidden; + } + + let old_token = inner.idle_timer.take(); + inner.last_armed = None; + (was_hidden, old_token) + }; + + if let Some(token) = old_token { + loop_handle.remove(token); + } + + if let Some(secs) = timeout { + let timer = Timer::from_duration(Duration::from_secs(secs as u64)); + let seat = seat.clone(); + if let Ok(token) = loop_handle.insert_source(timer, move |_, _, state| { + hide_cursor(state, &seat); + TimeoutAction::Drop + }) { + let mut inner = cursor_state.lock().unwrap(); + inner.idle_timer = Some(token); + inner.last_armed = Some(now); + } + } + + was_hidden +} + +fn hide_cursor(state: &mut State, seat: &Seat) { + if let Some(ptr) = seat.get_pointer() + && ptr.is_grabbed() + { + return; + } + let cursor_state = seat.user_data().get::().unwrap(); + { + let mut inner = cursor_state.lock().unwrap(); + inner.hidden = true; + inner.idle_timer = None; + inner.last_armed = None; + } + let outputs: Vec<_> = state.common.shell.read().outputs().cloned().collect(); + for output in outputs { + state.backend.schedule_render(&output); + } +} diff --git a/src/backend/render/element.rs b/src/backend/render/element.rs new file mode 100644 index 0000000..e1259dc --- /dev/null +++ b/src/backend/render/element.rs @@ -0,0 +1,526 @@ +use crate::{ + backend::{ + kms::render::gles::GbmGlowBackend, + render::{GlMultiError, wayland::SurfaceRenderElement}, + }, + shell::{CosmicMappedRenderElement, WorkspaceRenderElement}, + utils::iced::IcedRenderElement, +}; + +#[cfg(feature = "debug")] +use smithay::backend::renderer::element::texture::TextureRenderElement; +use smithay::{ + backend::{ + allocator::dmabuf::Dmabuf, + drm::DrmDeviceFd, + renderer::{ + Bind, Blit, ContextId, ExportMem, ImportAll, ImportMem, Offscreen, Renderer, + element::{ + Element, Id, Kind, RenderElement, UnderlyingStorage, + utils::{CropRenderElement, Relocate, RelocateRenderElement, RescaleRenderElement}, + }, + gles::{GlesError, GlesRenderbuffer, GlesTexture, element::TextureShaderElement}, + glow::{GlowFrame, GlowRenderer}, + multigpu::MultiTexture, + utils::{CommitCounter, DamageSet, OpaqueRegions}, + }, + }, + utils::{ + Buffer as BufferCoords, Logical, Physical, Point, Rectangle, Scale, user_data::UserDataMap, + }, +}; + +use super::{GlMultiRenderer, cursor::CursorRenderElement}; + +pub enum CosmicElement +where + R: AsGlowRenderer, + R::TextureId: Send + 'static, + CosmicMappedRenderElement: RenderElement, +{ + Workspace( + RelocateRenderElement>>>, + ), + Cursor(RescaleRenderElement>>), + Dnd(SurfaceRenderElement), + MoveGrab(RescaleRenderElement>), + Postprocess( + CropRenderElement>>, + ), + Zoom(IcedRenderElement), + Damage(DamageElement), + #[cfg(feature = "debug")] + Egui(TextureRenderElement), +} + +impl Element for CosmicElement +where + R: AsGlowRenderer, + R::TextureId: Send + 'static, + CosmicMappedRenderElement: RenderElement, +{ + fn id(&self) -> &Id { + match self { + CosmicElement::Workspace(elem) => elem.id(), + CosmicElement::Cursor(elem) => elem.id(), + CosmicElement::Dnd(elem) => elem.id(), + CosmicElement::MoveGrab(elem) => elem.id(), + CosmicElement::Postprocess(elem) => elem.id(), + CosmicElement::Zoom(elem) => elem.id(), + CosmicElement::Damage(elem) => elem.id(), + #[cfg(feature = "debug")] + CosmicElement::Egui(elem) => elem.id(), + } + } + + fn current_commit(&self) -> CommitCounter { + match self { + CosmicElement::Workspace(elem) => elem.current_commit(), + CosmicElement::Cursor(elem) => elem.current_commit(), + CosmicElement::Dnd(elem) => elem.current_commit(), + CosmicElement::MoveGrab(elem) => elem.current_commit(), + CosmicElement::Postprocess(elem) => elem.current_commit(), + CosmicElement::Zoom(elem) => elem.current_commit(), + CosmicElement::Damage(elem) => elem.current_commit(), + #[cfg(feature = "debug")] + CosmicElement::Egui(elem) => elem.current_commit(), + } + } + + fn src(&self) -> Rectangle { + match self { + CosmicElement::Workspace(elem) => elem.src(), + CosmicElement::Cursor(elem) => elem.src(), + CosmicElement::Dnd(elem) => elem.src(), + CosmicElement::MoveGrab(elem) => elem.src(), + CosmicElement::Postprocess(elem) => elem.src(), + CosmicElement::Zoom(elem) => elem.src(), + CosmicElement::Damage(elem) => elem.src(), + #[cfg(feature = "debug")] + CosmicElement::Egui(elem) => elem.src(), + } + } + + fn geometry(&self, scale: Scale) -> Rectangle { + match self { + CosmicElement::Workspace(elem) => elem.geometry(scale), + CosmicElement::Cursor(elem) => elem.geometry(scale), + CosmicElement::Dnd(elem) => elem.geometry(scale), + CosmicElement::MoveGrab(elem) => elem.geometry(scale), + CosmicElement::Postprocess(elem) => elem.geometry(scale), + CosmicElement::Zoom(elem) => elem.geometry(scale), + CosmicElement::Damage(elem) => elem.geometry(scale), + #[cfg(feature = "debug")] + CosmicElement::Egui(elem) => elem.geometry(scale), + } + } + + fn location(&self, scale: Scale) -> Point { + match self { + CosmicElement::Workspace(elem) => elem.location(scale), + CosmicElement::Cursor(elem) => elem.location(scale), + CosmicElement::Dnd(elem) => elem.location(scale), + CosmicElement::MoveGrab(elem) => elem.location(scale), + CosmicElement::Postprocess(elem) => elem.location(scale), + CosmicElement::Zoom(elem) => elem.location(scale), + CosmicElement::Damage(elem) => elem.location(scale), + #[cfg(feature = "debug")] + CosmicElement::Egui(elem) => elem.location(scale), + } + } + + fn transform(&self) -> smithay::utils::Transform { + match self { + CosmicElement::Workspace(elem) => elem.transform(), + CosmicElement::Cursor(elem) => elem.transform(), + CosmicElement::Dnd(elem) => elem.transform(), + CosmicElement::MoveGrab(elem) => elem.transform(), + CosmicElement::Postprocess(elem) => elem.transform(), + CosmicElement::Zoom(elem) => elem.transform(), + CosmicElement::Damage(elem) => elem.transform(), + #[cfg(feature = "debug")] + CosmicElement::Egui(elem) => elem.transform(), + } + } + + fn damage_since( + &self, + scale: Scale, + commit: Option, + ) -> DamageSet { + match self { + CosmicElement::Workspace(elem) => elem.damage_since(scale, commit), + CosmicElement::Cursor(elem) => elem.damage_since(scale, commit), + CosmicElement::Dnd(elem) => elem.damage_since(scale, commit), + CosmicElement::MoveGrab(elem) => elem.damage_since(scale, commit), + CosmicElement::Postprocess(elem) => elem.damage_since(scale, commit), + CosmicElement::Zoom(elem) => elem.damage_since(scale, commit), + CosmicElement::Damage(elem) => elem.damage_since(scale, commit), + #[cfg(feature = "debug")] + CosmicElement::Egui(elem) => elem.damage_since(scale, commit), + } + } + + fn opaque_regions(&self, scale: Scale) -> OpaqueRegions { + match self { + CosmicElement::Workspace(elem) => elem.opaque_regions(scale), + CosmicElement::Cursor(elem) => elem.opaque_regions(scale), + CosmicElement::Dnd(elem) => elem.opaque_regions(scale), + CosmicElement::MoveGrab(elem) => elem.opaque_regions(scale), + CosmicElement::Postprocess(elem) => elem.opaque_regions(scale), + CosmicElement::Zoom(elem) => elem.opaque_regions(scale), + CosmicElement::Damage(elem) => elem.opaque_regions(scale), + #[cfg(feature = "debug")] + CosmicElement::Egui(elem) => elem.opaque_regions(scale), + } + } + + fn alpha(&self) -> f32 { + match self { + CosmicElement::Workspace(elem) => elem.alpha(), + CosmicElement::Cursor(elem) => elem.alpha(), + CosmicElement::Dnd(elem) => elem.alpha(), + CosmicElement::MoveGrab(elem) => elem.alpha(), + CosmicElement::Postprocess(elem) => elem.alpha(), + CosmicElement::Zoom(elem) => elem.alpha(), + CosmicElement::Damage(elem) => elem.alpha(), + #[cfg(feature = "debug")] + CosmicElement::Egui(elem) => elem.alpha(), + } + } + + fn kind(&self) -> Kind { + match self { + CosmicElement::Workspace(elem) => elem.kind(), + CosmicElement::Cursor(elem) => elem.kind(), + CosmicElement::Dnd(elem) => elem.kind(), + CosmicElement::MoveGrab(elem) => elem.kind(), + CosmicElement::Postprocess(elem) => elem.kind(), + CosmicElement::Zoom(elem) => elem.kind(), + CosmicElement::Damage(elem) => elem.kind(), + #[cfg(feature = "debug")] + CosmicElement::Egui(elem) => elem.kind(), + } + } + + fn is_framebuffer_effect(&self) -> bool { + match self { + CosmicElement::Workspace(elem) => elem.is_framebuffer_effect(), + CosmicElement::Cursor(elem) => elem.is_framebuffer_effect(), + CosmicElement::Dnd(elem) => elem.is_framebuffer_effect(), + CosmicElement::MoveGrab(elem) => elem.is_framebuffer_effect(), + CosmicElement::Postprocess(elem) => elem.is_framebuffer_effect(), + CosmicElement::Zoom(elem) => elem.is_framebuffer_effect(), + CosmicElement::Damage(elem) => elem.is_framebuffer_effect(), + #[cfg(feature = "debug")] + CosmicElement::Egui(elem) => elem.is_framebuffer_effect(), + } + } +} + +impl RenderElement for CosmicElement +where + R: AsGlowRenderer, + R::TextureId: Send + 'static, + CosmicMappedRenderElement: RenderElement, +{ + fn draw( + &self, + frame: &mut R::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + cache: Option<&UserDataMap>, + ) -> Result<(), R::Error> { + match self { + CosmicElement::Workspace(elem) => { + elem.draw(frame, src, dst, damage, opaque_regions, cache) + } + CosmicElement::Cursor(elem) => { + elem.draw(frame, src, dst, damage, opaque_regions, cache) + } + CosmicElement::Dnd(elem) => elem.draw(frame, src, dst, damage, opaque_regions, cache), + CosmicElement::MoveGrab(elem) => { + elem.draw(frame, src, dst, damage, opaque_regions, cache) + } + CosmicElement::Postprocess(elem) => { + let glow_frame = R::glow_frame_mut(frame); + RenderElement::::draw( + elem, + glow_frame, + src, + dst, + damage, + opaque_regions, + cache, + ) + .map_err(R::from_gles_error) + } + CosmicElement::Zoom(elem) => elem.draw(frame, src, dst, damage, opaque_regions, cache), + CosmicElement::Damage(elem) => { + RenderElement::::draw(elem, frame, src, dst, damage, opaque_regions, cache) + } + #[cfg(feature = "debug")] + CosmicElement::Egui(elem) => { + let glow_frame = R::glow_frame_mut(frame); + RenderElement::::draw( + elem, + glow_frame, + src, + dst, + damage, + opaque_regions, + cache, + ) + .map_err(R::from_gles_error) + } + } + } + + fn underlying_storage(&self, renderer: &mut R) -> Option> { + match self { + CosmicElement::Workspace(elem) => elem.underlying_storage(renderer), + CosmicElement::Cursor(elem) => elem.underlying_storage(renderer), + CosmicElement::Dnd(elem) => elem.underlying_storage(renderer), + CosmicElement::MoveGrab(elem) => elem.underlying_storage(renderer), + CosmicElement::Postprocess(elem) => { + let glow_renderer = renderer.glow_renderer_mut(); + elem.underlying_storage(glow_renderer) + } + CosmicElement::Zoom(elem) => elem.underlying_storage(renderer), + CosmicElement::Damage(elem) => elem.underlying_storage(renderer), + #[cfg(feature = "debug")] + CosmicElement::Egui(elem) => { + let glow_renderer = renderer.glow_renderer_mut(); + elem.underlying_storage(glow_renderer) + } + } + } + + fn capture_framebuffer( + &self, + frame: &mut ::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + cache: &UserDataMap, + ) -> Result<(), ::Error> { + match self { + CosmicElement::Workspace(elem) => elem.capture_framebuffer(frame, src, dst, cache), + CosmicElement::Cursor(elem) => elem.capture_framebuffer(frame, src, dst, cache), + CosmicElement::Dnd(elem) => elem.capture_framebuffer(frame, src, dst, cache), + CosmicElement::MoveGrab(elem) => elem.capture_framebuffer(frame, src, dst, cache), + CosmicElement::Postprocess(elem) => { + let glow_frame = R::glow_frame_mut(frame); + RenderElement::::capture_framebuffer( + elem, glow_frame, src, dst, cache, + ) + .map_err(R::from_gles_error) + } + CosmicElement::Zoom(elem) => elem.capture_framebuffer(frame, src, dst, cache), + CosmicElement::Damage(elem) => { + RenderElement::::capture_framebuffer(elem, frame, src, dst, cache) + } + #[cfg(feature = "debug")] + CosmicElement::Egui(elem) => { + let glow_frame = R::glow_frame_mut(frame); + RenderElement::::capture_framebuffer( + elem, glow_frame, src, dst, cache, + ) + .map_err(R::from_gles_error) + } + } + } +} + +impl From>>> + for CosmicElement +where + R: AsGlowRenderer, + R::TextureId: Send + 'static, + CosmicMappedRenderElement: RenderElement, +{ + fn from(elem: CropRenderElement>>) -> Self { + Self::Workspace(RelocateRenderElement::from_element( + elem, + (0, 0), + Relocate::Relative, + )) + } +} + +impl From> for CosmicElement +where + R: AsGlowRenderer, + R::TextureId: Send + 'static, + CosmicMappedRenderElement: RenderElement, +{ + fn from(value: IcedRenderElement) -> Self { + Self::Zoom(value) + } +} + +impl From for CosmicElement +where + R: Renderer + ImportAll + ImportMem + AsGlowRenderer, + R::TextureId: Send + 'static, + CosmicMappedRenderElement: RenderElement, +{ + fn from(value: DamageElement) -> Self { + Self::Damage(value) + } +} + +#[cfg(feature = "debug")] +impl From> for CosmicElement +where + R: AsGlowRenderer, + R::TextureId: Send + 'static, + CosmicMappedRenderElement: RenderElement, +{ + fn from(elem: TextureRenderElement) -> Self { + Self::Egui(elem) + } +} + +pub trait AsGlowRenderer: + Renderer + + Offscreen + + Offscreen + + ImportAll + + ImportMem + + ExportMem + + Bind + + Blit +{ + fn glow_renderer(&self) -> &GlowRenderer; + fn glow_renderer_mut(&mut self) -> &mut GlowRenderer; + fn glow_frame<'a, 'frame, 'buffer>( + frame: &'a Self::Frame<'frame, 'buffer>, + ) -> &'a GlowFrame<'frame, 'buffer>; + fn glow_frame_mut<'a, 'frame, 'buffer>( + frame: &'a mut Self::Frame<'frame, 'buffer>, + ) -> &'a mut GlowFrame<'frame, 'buffer>; + fn tex_from_gl(context: &ContextId, texture: GlesTexture) -> Self::TextureId; + fn tex_to_gl( + context: &ContextId, + texture: &Self::TextureId, + ) -> Option; + fn from_gles_error(err: GlesError) -> Self::Error; +} + +impl AsGlowRenderer for GlowRenderer { + fn glow_renderer(&self) -> &GlowRenderer { + self + } + fn glow_renderer_mut(&mut self) -> &mut GlowRenderer { + self + } + fn glow_frame<'a, 'frame, 'buffer>( + frame: &'a Self::Frame<'frame, 'buffer>, + ) -> &'a GlowFrame<'frame, 'buffer> { + frame + } + fn glow_frame_mut<'a, 'frame, 'buffer>( + frame: &'a mut Self::Frame<'frame, 'buffer>, + ) -> &'a mut GlowFrame<'frame, 'buffer> { + frame + } + fn tex_from_gl(_context: &ContextId, texture: GlesTexture) -> Self::TextureId { + texture + } + fn tex_to_gl( + _context: &ContextId, + texture: &Self::TextureId, + ) -> Option { + Some(texture.clone()) + } + fn from_gles_error(err: GlesError) -> Self::Error { + err + } +} + +impl AsGlowRenderer for GlMultiRenderer<'_> { + fn glow_renderer(&self) -> &GlowRenderer { + self.as_ref() + } + fn glow_renderer_mut(&mut self) -> &mut GlowRenderer { + self.as_mut() + } + fn glow_frame<'b, 'frame, 'buffer>( + frame: &'b Self::Frame<'frame, 'buffer>, + ) -> &'b GlowFrame<'frame, 'buffer> { + frame.as_ref() + } + fn glow_frame_mut<'b, 'frame, 'buffer>( + frame: &'b mut Self::Frame<'frame, 'buffer>, + ) -> &'b mut GlowFrame<'frame, 'buffer> { + frame.as_mut() + } + fn tex_from_gl(context: &ContextId, texture: GlesTexture) -> Self::TextureId { + MultiTexture::from_native_texture::>(context, texture).unwrap() + } + fn tex_to_gl( + context: &ContextId, + texture: &Self::TextureId, + ) -> Option { + texture.get::>(context) + } + fn from_gles_error(err: GlesError) -> Self::Error { + GlMultiError::Render(err) + } +} + +pub struct DamageElement { + id: Id, + geometry: Rectangle, +} + +impl DamageElement { + pub fn new(geometry: Rectangle) -> DamageElement { + DamageElement { + id: Id::new(), + geometry, + } + } +} + +impl Element for DamageElement { + fn id(&self) -> &Id { + &self.id + } + + fn current_commit(&self) -> CommitCounter { + CommitCounter::default() + } + + fn src(&self) -> Rectangle { + Rectangle::from_size((1.0, 1.0).into()) + } + + fn geometry(&self, scale: Scale) -> Rectangle { + self.geometry.to_f64().to_physical(scale).to_i32_round() + } + + fn damage_since( + &self, + scale: Scale, + _commit: Option, + ) -> DamageSet { + DamageSet::from_slice(&[Rectangle::from_size(self.geometry(scale).size)]) + } +} + +impl RenderElement for DamageElement { + fn draw( + &self, + _frame: &mut R::Frame<'_, '_>, + _src: Rectangle, + _dst: Rectangle, + _damage: &[Rectangle], + _opaque_regions: &[Rectangle], + _cache: Option<&UserDataMap>, + ) -> Result<(), R::Error> { + Ok(()) + } +} diff --git a/src/backend/render/mod.rs b/src/backend/render/mod.rs new file mode 100644 index 0000000..e9cf651 --- /dev/null +++ b/src/backend/render/mod.rs @@ -0,0 +1,1595 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use std::{ + borrow::Borrow, + cell::RefCell, + collections::HashMap, + ops::ControlFlow, + sync::{Arc, Weak}, + time::Instant, +}; + +#[cfg(feature = "debug")] +use crate::debug::fps_ui; +use crate::{ + backend::{ + kms::render::gles::GbmGlowBackend, + render::{ + element::DamageElement, + shadow::{SHADOW_SHADER, ShadowShader}, + wayland::{ + SurfaceRenderElement, + blur_effect::BlurShaders, + clipped_surface::{CLIPPING_SHADER, ClippingShader}, + push_render_elements_from_surface_tree, + }, + }, + }, + config::ScreenFilter, + shell::{ + CosmicMappedRenderElement, OutputId, OverviewMode, SeatExt, Trigger, WorkspaceDelta, + WorkspaceRenderElement, + element::CosmicMappedKey, + focus::{FocusTarget, Stage, render_input_order, target::WindowGroup}, + grabs::{SeatMenuGrabState, SeatMoveGrabState}, + layout::tiling::ANIMATION_DURATION, + zoom::ZoomState, + }, + utils::{prelude::*, quirks::workspace_overview_is_open}, + wayland::{ + handlers::{ + compositor::FRAME_TIME_FILTER, + corner_radius::{pad_rect, surface_corners, surface_padding}, + data_device::get_dnd_icon, + image_copy_capture::{ + FrameHolder, SessionData, render_element_buffers, render_session, + }, + }, + protocols::workspace::WorkspaceHandle, + }, +}; + +use cosmic::Theme; +use smithay::{ + backend::{ + allocator::Fourcc, + drm::{DrmDeviceFd, DrmNode}, + renderer::{ + Color32F, Offscreen, Texture, TextureFilter, + damage::{Error as RenderError, OutputDamageTracker, RenderOutputResult}, + element::{ + Element, Id, Kind, NamespacedElement, RenderElement, WeakId, + texture::{TextureRenderBuffer, TextureRenderElement}, + utils::{ + ConstrainAlign, ConstrainScaleBehavior, CropRenderElement, Relocate, + RelocateRenderElement, RescaleRenderElement, constrain_render_elements, + }, + }, + gles::{ + GlesError, GlesPixelProgram, GlesRenderer, GlesTexProgram, GlesTexture, Uniform, + UniformName, UniformType, + element::{PixelShaderElement, TextureShaderElement}, + }, + glow::GlowRenderer, + multigpu::{Error as MultiError, MultiFrame, MultiRenderer}, + sync::SyncPoint, + }, + }, + desktop::utils::bbox_from_surface_tree, + input::Seat, + output::{Output, OutputModeSource, OutputNoMode}, + utils::{ + IsAlive, Logical, Monotonic, Physical, Point, Rectangle, Scale, Size, Time, Transform, + }, + wayland::{compositor::with_states, dmabuf::get_dmabuf, session_lock::LockSurface}, +}; + +#[cfg(feature = "debug")] +use smithay_egui::EguiState; + +pub mod animations; +pub mod cursor; +pub mod element; +pub mod shadow; +pub mod wayland; +use self::element::{AsGlowRenderer, CosmicElement}; + +use super::kms::Timings; + +pub type GlMultiRenderer<'a> = + MultiRenderer<'a, 'a, GbmGlowBackend, GbmGlowBackend>; +pub type GlMultiFrame<'a, 'frame, 'buffer> = + MultiFrame<'a, 'a, 'frame, 'buffer, GbmGlowBackend, GbmGlowBackend>; +pub type GlMultiError = MultiError, GbmGlowBackend>; + +pub enum RendererRef<'a> { + Glow(&'a mut GlowRenderer), + GlMulti(GlMultiRenderer<'a>), +} + +impl AsRef for RendererRef<'_> { + fn as_ref(&self) -> &GlowRenderer { + match self { + Self::Glow(renderer) => renderer, + Self::GlMulti(renderer) => renderer.as_ref(), + } + } +} + +impl AsMut for RendererRef<'_> { + fn as_mut(&mut self) -> &mut GlowRenderer { + match self { + Self::Glow(renderer) => renderer, + Self::GlMulti(renderer) => renderer.as_mut(), + } + } +} + +pub static CLEAR_COLOR: Color32F = Color32F::new(0.153, 0.161, 0.165, 1.0); +pub static OUTLINE_SHADER: &str = include_str!("./shaders/rounded_outline.frag"); +pub static RECTANGLE_SHADER: &str = include_str!("./shaders/rounded_rectangle.frag"); +pub static POSTPROCESS_SHADER: &str = include_str!("./shaders/offscreen.frag"); +pub static GROUP_COLOR: [f32; 3] = [0.788, 0.788, 0.788]; +pub static ACTIVE_GROUP_COLOR: [f32; 3] = [0.58, 0.922, 0.922]; + +pub struct IndicatorShader(pub GlesPixelProgram); + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub enum Usage { + OverviewBackdrop, + Overlay, + MoveGrabIndicator, + FocusIndicator, + PotentialGroupIndicator, + SnappingIndicator, + Border, +} + +#[derive(Clone)] +pub enum Key { + Static(WeakId), + Group(Weak<()>), + Window(Usage, CosmicMappedKey), +} +impl std::hash::Hash for Key { + fn hash(&self, state: &mut H) { + match self { + Key::Static(id) => id.hash(state), + Key::Group(arc) => (arc.as_ptr() as usize).hash(state), + Key::Window(usage, window) => { + usage.hash(state); + window.hash(state); + } + } + } +} +impl PartialEq for Key { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Key::Static(s1), Key::Static(s2)) => s1 == s2, + (Key::Group(g1), Key::Group(g2)) => Weak::ptr_eq(g1, g2), + (Key::Window(u1, w1), Key::Window(u2, w2)) => u1 == u2 && w1 == w2, + _ => false, + } + } +} +impl Eq for Key {} +impl From for Key { + fn from(group: WindowGroup) -> Self { + Key::Group(group.alive.clone()) + } +} +impl From for Key { + fn from(id: Id) -> Self { + Key::Static(id.downgrade()) + } +} + +#[derive(PartialEq)] +struct IndicatorSettings { + thickness: u8, + outer_radius: [u8; 4], + alpha: f32, + color: [f32; 3], + scale: f64, +} +type IndicatorCache = RefCell>; + +impl IndicatorShader { + pub fn get(renderer: &R) -> GlesPixelProgram { + Borrow::::borrow(renderer.glow_renderer()) + .egl_context() + .user_data() + .get::() + .expect("Custom Shaders not initialized") + .0 + .clone() + } + + pub fn focus_element( + renderer: &R, + key: impl Into, + mut element_geo: Rectangle, + thickness: u8, + inner_radius: [u8; 4], + alpha: f32, + scale: f64, + active_window_hint: [f32; 3], + ) -> PixelShaderElement { + let t = thickness as i32; + element_geo.loc -= (t, t).into(); + element_geo.size += (t * 2, t * 2).into(); + let outer_radius = inner_radius.map(|r| r + thickness); + + IndicatorShader::element( + renderer, + key, + element_geo, + thickness, + outer_radius, + alpha, + scale, + active_window_hint, + ) + } + + pub fn element( + renderer: &R, + key: impl Into, + geo: Rectangle, + thickness: u8, + outer_radius: [u8; 4], + alpha: f32, + scale: f64, + color: [f32; 3], + ) -> PixelShaderElement { + let settings = IndicatorSettings { + thickness, + outer_radius, + alpha, + scale, + color, + }; + + let user_data = Borrow::::borrow(renderer.glow_renderer()) + .egl_context() + .user_data(); + + user_data.insert_if_missing(|| IndicatorCache::new(HashMap::new())); + let mut cache = user_data.get::().unwrap().borrow_mut(); + cache.retain(|k, _| match k { + Key::Static(w) => w.upgrade().is_some(), + Key::Group(w) => w.upgrade().is_some(), + Key::Window(_, w) => w.alive(), + }); + + let key = key.into(); + if cache + .get(&key) + .filter(|(old_settings, _)| &settings == old_settings) + .is_none() + { + let thickness: f32 = ((thickness as f64 * scale) / scale) as f32; + let shader = Self::get(renderer); + + let elem = PixelShaderElement::new( + shader, + geo.as_logical(), + None, //TODO + alpha, + vec![ + Uniform::new( + "color", + [color[0] * alpha, color[1] * alpha, color[2] * alpha], + ), + Uniform::new("thickness", thickness), + Uniform::new( + "radius", + [ + outer_radius[3] as f32, + outer_radius[1] as f32, + outer_radius[0] as f32, + outer_radius[2] as f32, + ], + ), + Uniform::new("scale", scale as f32), + ], + Kind::Unspecified, + ); + cache.insert(key.clone(), (settings, elem)); + } + + let elem = &mut cache.get_mut(&key).unwrap().1; + if elem.geometry(1.0.into()).to_logical(1) != geo.as_logical() { + elem.resize(geo.as_logical(), None); + } + elem.clone() + } +} + +pub struct BackdropShader(pub GlesPixelProgram); + +#[derive(PartialEq)] +struct BackdropSettings { + radius: f32, + alpha: f32, + color: [f32; 3], +} +type BackdropCache = RefCell>; + +impl BackdropShader { + pub fn get(renderer: &R) -> GlesPixelProgram { + Borrow::::borrow(renderer.glow_renderer()) + .egl_context() + .user_data() + .get::() + .expect("Custom Shaders not initialized") + .0 + .clone() + } + + pub fn element( + renderer: &R, + key: impl Into, + geo: Rectangle, + radius: f32, + alpha: f32, + color: [f32; 3], + ) -> PixelShaderElement { + let settings = BackdropSettings { + radius, + alpha, + color, + }; + + let user_data = Borrow::::borrow(renderer.glow_renderer()) + .egl_context() + .user_data(); + + user_data.insert_if_missing(|| BackdropCache::new(HashMap::new())); + let mut cache = user_data.get::().unwrap().borrow_mut(); + cache.retain(|k, _| match k { + Key::Static(w) => w.upgrade().is_some(), + Key::Group(a) => a.upgrade().is_some(), + Key::Window(_, w) => w.alive(), + }); + + let key = key.into(); + if cache + .get(&key) + .filter(|(old_settings, _)| &settings == old_settings) + .is_none() + { + let shader = Self::get(renderer); + + let elem = PixelShaderElement::new( + shader, + geo.as_logical(), + None, // TODO + alpha, + vec![ + Uniform::new( + "color", + [color[0] * alpha, color[1] * alpha, color[2] * alpha], + ), + Uniform::new("radius", radius), + ], + Kind::Unspecified, + ); + cache.insert(key.clone(), (settings, elem)); + } + + let elem = &mut cache.get_mut(&key).unwrap().1; + if elem.geometry(1.0.into()).to_logical(1) != geo.as_logical() { + elem.resize(geo.as_logical(), None); + } + elem.clone() + } +} + +pub struct PostprocessShader(pub GlesTexProgram); + +pub fn init_shaders(renderer: &mut GlesRenderer) -> Result<(), GlesError> { + { + let egl_context = renderer.egl_context(); + if egl_context.user_data().get::().is_some() + && egl_context.user_data().get::().is_some() + && egl_context.user_data().get::().is_some() + { + return Ok(()); + } + } + + let outline_shader = renderer.compile_custom_pixel_shader( + OUTLINE_SHADER, + &[ + UniformName::new("color", UniformType::_3f), + UniformName::new("thickness", UniformType::_1f), + UniformName::new("scale", UniformType::_1f), + UniformName::new("radius", UniformType::_4f), + ], + )?; + let rectangle_shader = renderer.compile_custom_pixel_shader( + RECTANGLE_SHADER, + &[ + UniformName::new("color", UniformType::_3f), + UniformName::new("radius", UniformType::_1f), + ], + )?; + let postprocess_shader = renderer.compile_custom_texture_shader( + POSTPROCESS_SHADER, + &[ + UniformName::new("invert", UniformType::_1f), + UniformName::new("color_mode", UniformType::_1f), + ], + )?; + let clipping_shader = renderer.compile_custom_texture_shader( + CLIPPING_SHADER, + &[ + UniformName::new("geo_size", UniformType::_2f), + UniformName::new("corner_radius", UniformType::_4f), + UniformName::new("input_to_geo", UniformType::Matrix3x3), + UniformName::new("noise", UniformType::_1f), + ], + )?; + let shadow_shader = renderer.compile_custom_pixel_shader( + SHADOW_SHADER, + &[ + UniformName::new("shadow_color", UniformType::_4f), + UniformName::new("sigma", UniformType::_1f), + UniformName::new("input_to_geo", UniformType::Matrix3x3), + UniformName::new("geo_size", UniformType::_2f), + UniformName::new("corner_radius", UniformType::_4f), + UniformName::new("window_input_to_geo", UniformType::Matrix3x3), + UniformName::new("window_geo_size", UniformType::_2f), + UniformName::new("window_corner_radius", UniformType::_4f), + ], + )?; + let blur_shaders = BlurShaders::compile(renderer)?; + + let egl_context = renderer.egl_context(); + egl_context + .user_data() + .insert_if_missing(|| IndicatorShader(outline_shader)); + egl_context + .user_data() + .insert_if_missing(|| BackdropShader(rectangle_shader)); + egl_context + .user_data() + .insert_if_missing(|| PostprocessShader(postprocess_shader)); + egl_context + .user_data() + .insert_if_missing(|| ClippingShader(clipping_shader)); + egl_context + .user_data() + .insert_if_missing(|| ShadowShader(shadow_shader)); + egl_context.user_data().insert_if_missing(|| blur_shaders); + + Ok(()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CursorMode { + None, + NotDefault, + All, +} + +#[profiling::function] +pub fn cursor_elements<'a, 'frame, R>( + renderer: &mut R, + seats: impl Iterator>, + zoom_state: Option<&ZoomState>, + theme: &Theme, + blur_strength: usize, + now: Time, + output: &Output, + mode: CursorMode, + exclude_dnd_icon: bool, + scanout_node: Option, + push: &mut dyn FnMut(CosmicElement), +) where + R: AsGlowRenderer, + R::TextureId: Send + Clone + 'static, + CosmicMappedRenderElement: RenderElement, +{ + let scale = output.current_scale().fractional_scale(); + let (focal_point, zoom_scale) = zoom_state + .map(|state| { + ( + state.animating_focal_point(Some(output)).to_local(output), + state.animating_level(output), + ) + }) + .unwrap_or_else(|| ((0., 0.).into(), 1.)); + + for seat in seats { + let pointer = match seat.get_pointer() { + Some(ptr) => ptr, + None => continue, + }; + let location = pointer.current_location() - output.current_location().to_f64(); + + if mode != CursorMode::None { + cursor::draw_cursor( + renderer, + seat, + location, + scale.into(), + zoom_scale, + now, + blur_strength, + mode != CursorMode::NotDefault, + &mut |elem, hotspot| { + push(CosmicElement::Cursor(RescaleRenderElement::from_element( + RelocateRenderElement::from_element( + elem, + Point::from((-hotspot.x, -hotspot.y)), + Relocate::Relative, + ), + focal_point + .as_logical() + .to_physical(output.current_scale().fractional_scale()) + .to_i32_round(), + zoom_scale, + ))) + }, + ); + } + + if !exclude_dnd_icon && let Some(dnd_icon) = get_dnd_icon(seat) { + cursor::draw_dnd_icon( + renderer, + &dnd_icon.surface, + (location + dnd_icon.offset.to_f64()).to_i32_round(), + scale, + blur_strength, + &mut |elem| push(CosmicElement::Dnd(elem)), + ); + } + + let theme = theme.cosmic(); + if let Some(grab_state) = seat + .user_data() + .get::() + .unwrap() + .lock() + .unwrap() + .as_ref() + { + grab_state.render(renderer, output, theme, scanout_node, &mut |elem| { + push(CosmicElement::MoveGrab(RescaleRenderElement::from_element( + elem, + focal_point + .as_logical() + .to_physical(output.current_scale().fractional_scale()) + .to_i32_round(), + zoom_scale, + ))); + }) + } + + if let Some(grab_state) = seat + .user_data() + .get::() + .unwrap() + .lock() + .unwrap() + .as_ref() + { + let should_scale = !grab_state.is_in_screen_space(); + grab_state.render(renderer, output, &mut |elem| { + push(CosmicElement::MoveGrab(RescaleRenderElement::from_element( + elem.into(), + if should_scale { + focal_point + .as_logical() + .to_physical(output.current_scale().fractional_scale()) + .to_i32_round() + } else { + Point::from((0, 0)) + }, + if should_scale { zoom_scale } else { 1.0 }, + ))); + }) + } + } +} + +#[cfg(not(feature = "debug"))] +pub type EguiState = (); + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ElementFilter { + All, + ExcludeWorkspaceOverview, + LayerShellOnly, +} + +pub fn output_elements( + _gpu: Option<&DrmNode>, + renderer: &mut R, + shell: &Arc>, + now: Time, + output: &Output, + cursor_mode: CursorMode, + _fps: Option<(&EguiState, &Timings)>, + scanout_node: Option, +) -> Result>, RenderError> +where + R: AsGlowRenderer, + R::TextureId: Send + Clone + 'static, + CosmicMappedRenderElement: RenderElement, + WorkspaceRenderElement: RenderElement, +{ + #[cfg(feature = "debug")] + let mut debug_elements = { + let output_geo = output.geometry(); + let shell_guard = shell.read(); + let seats = shell_guard.seats.iter().cloned().collect::>(); + let debug_active = shell_guard.debug_active; + std::mem::drop(shell_guard); + let scale = output.current_scale().fractional_scale(); + + if let Some((state, timings)) = _fps { + vec![ + fps_ui( + _gpu, + debug_active, + &seats, + renderer.glow_renderer_mut(), + state, + timings, + Rectangle::from_size( + (output_geo.size.w.min(400), output_geo.size.h.min(800)).into(), + ), + scale, + ) + .map_err(R::from_gles_error) + .map_err(RenderError::Rendering)? + .into(), + ] + } else { + Vec::new() + } + }; + + let shell_guard = shell.read(); + let Some((previous_workspace, workspace)) = shell_guard.workspaces.active(output) else { + #[cfg(not(feature = "debug"))] + return Ok(Vec::new()); + #[cfg(feature = "debug")] + return Ok(debug_elements); + }; + + let (previous_idx, idx) = shell_guard.workspaces.active_num(output); + let previous_workspace = previous_workspace + .zip(previous_idx) + .map(|((w, start), idx)| (w.handle, idx, start)); + let workspace = (workspace.handle, idx); + + std::mem::drop(shell_guard); + + let element_filter = if workspace_overview_is_open(output) { + ElementFilter::LayerShellOnly + } else { + ElementFilter::All + }; + let zoom_state = shell.read().zoom_state().cloned(); + + #[allow(unused_mut)] + let workspace_elements = workspace_elements( + _gpu, + renderer, + shell, + zoom_state.as_ref(), + now, + output, + previous_workspace, + workspace, + cursor_mode, + element_filter, + scanout_node, + )?; + + #[cfg(feature = "debug")] + { + debug_elements.extend(workspace_elements); + Ok(debug_elements) + } + #[cfg(not(feature = "debug"))] + Ok(workspace_elements) +} + +#[profiling::function] +pub fn workspace_elements( + _gpu: Option<&DrmNode>, + renderer: &mut R, + shell: &Arc>, + zoom_level: Option<&ZoomState>, + now: Time, + output: &Output, + previous: Option<(WorkspaceHandle, usize, WorkspaceDelta)>, + current: (WorkspaceHandle, usize), + cursor_mode: CursorMode, + element_filter: ElementFilter, + scanout_node: Option, +) -> Result>, RenderError> +where + R: AsGlowRenderer, + R::TextureId: Send + Clone + 'static, + CosmicMappedRenderElement: RenderElement, + WorkspaceRenderElement: RenderElement, +{ + let mut elements = Vec::>::new(); + + let shell_ref = shell.read(); + let seats = shell_ref.seats.iter().cloned().collect::>(); + if seats.is_empty() { + return Ok(Vec::new()); + } + let theme = shell_ref.theme().clone(); + let blur_strength = (theme.cosmic().frosted as u8 + 1) as usize; + let scale = output.current_scale().fractional_scale(); + // we don't want to hold a shell lock across `cursor_elements`, + // that is prone to deadlock with the main-thread on some grabs. + std::mem::drop(shell_ref); + + cursor_elements( + renderer, + seats.iter(), + zoom_level, + &theme, + blur_strength, + now, + output, + cursor_mode, + element_filter == ElementFilter::ExcludeWorkspaceOverview, + scanout_node, + &mut |elem| elements.push(elem), + ); + + let shell = shell.read(); + let overview = shell.overview_mode(); + let (resize_mode, resize_indicator) = shell.resize_mode(); + let resize_indicator = resize_indicator.map(|indicator| (resize_mode, indicator)); + let swap_tree = if let Some(Trigger::KeyboardSwap(_, desc)) = overview.0.active_trigger() { + if current.0 != desc.handle { + shell + .workspaces + .space_for_handle(&desc.handle) + .map(|w| w.tiling_layer.tree()) + } else { + None + } + } else { + None + }; + let overview = ( + overview.0, + overview.1.map(|indicator| (indicator, swap_tree)), + ); + let last_active_seat = shell.seats.last_active(); + let move_active = last_active_seat + .user_data() + .get::() + .unwrap() + .lock() + .unwrap() + .is_some(); + let focused_output = last_active_seat.focused_or_active_output(); + let set = shell.workspaces.sets.get(output).ok_or(OutputNoMode)?; + let workspace = set + .workspaces + .iter() + .find(|w| w.handle == current.0) + .ok_or(OutputNoMode)?; + let is_active_space = workspace.output == focused_output; + let active_hint = if shell.active_hint { + theme.cosmic().active_hint as u8 + } else { + 0 + }; + + let output_size = output + .geometry() + .size + .as_logical() + .to_physical_precise_round(scale); + let (focal_point, zoom_scale) = zoom_level + .map(|state| { + ( + state.animating_focal_point(Some(output)).to_local(output), + state.animating_level(output), + ) + }) + .unwrap_or_else(|| ((0., 0.).into(), 1.)); + + let crop_to_output = |element: WorkspaceRenderElement| { + CropRenderElement::from_element( + RescaleRenderElement::from_element( + element, + focal_point + .as_logical() + .to_physical(output.current_scale().fractional_scale()) + .to_i32_round(), + zoom_scale, + ), + scale, + Rectangle::from_size(output_size), + ) + }; + + render_input_order::<()>(&shell, output, previous, current, element_filter, |stage| { + match stage { + Stage::ZoomUI => { + ZoomState::render(renderer, output, &mut |elem| { + elements.push(CosmicElement::Zoom(elem)) + }); + } + Stage::SessionLock(lock_surface) => { + session_lock_elements(renderer, output, lock_surface, blur_strength, &mut |elem| { + elements.extend(crop_to_output(elem.into()).map(Into::into)) + }) + } + Stage::LayerPopup { + popup, + location, + workspace_idx, + .. + } => { + let mut geometry = popup.geometry().as_global(); + geometry.loc += location; + + let radii = with_states(popup.wl_surface(), |states| { + surface_corners(states, geometry.size.as_logical()) + }) + .unwrap_or([0; 4]); + + let namespace = output + .user_data() + .get::() + .map(|id| id.namespace_for_workspace(workspace_idx)) + .unwrap_or(workspace_idx); + + push_render_elements_from_surface_tree( + renderer, + popup.wl_surface(), + location + .to_local(output) + .as_logical() + .to_physical_precise_round(scale), + geometry.to_local(output).as_logical().to_f64(), + Scale::from(scale), + 1.0, + false, + radii, + None, + blur_strength, + FRAME_TIME_FILTER, + &mut |elem| { + elements.extend( + crop_to_output(NamespacedElement::new(elem, namespace).into()) + .map(Into::into), + ) + }, + None, + ) + } + Stage::LayerSurface { + layer, + location, + workspace_idx, + } => { + let mut geometry = layer.geometry().as_global(); + geometry.loc += location; + let geometry = geometry.to_local(output).as_logical(); + + let padded = with_states(layer.wl_surface(), |states| { + surface_padding(states, geometry.size) + .and_then(|padding| pad_rect(geometry, &padding)) + }) + .unwrap_or(geometry); + let radii = with_states(layer.wl_surface(), |states| { + surface_corners(states, padded.size) + }) + .unwrap_or([0; 4]); + + let namespace = output + .user_data() + .get::() + .map(|id| id.namespace_for_workspace(workspace_idx)) + .unwrap_or(workspace_idx); + + push_render_elements_from_surface_tree( + renderer, + layer.wl_surface(), + location + .to_local(output) + .as_logical() + .to_physical_precise_round(scale), + geometry.to_f64(), + Scale::from(scale), + 1.0, + false, + radii, + padded.to_f64(), + blur_strength, + FRAME_TIME_FILTER, + &mut |elem| { + elements.extend( + crop_to_output(NamespacedElement::new(elem, namespace).into()) + .map(Into::into), + ) + }, + None, + ); + } + Stage::OverrideRedirect { surface, location } => { + if let Some(wl_surface) = surface.wl_surface() { + let mut geometry = surface.geometry().as_global(); + geometry.loc += location; + + push_render_elements_from_surface_tree( + renderer, + &wl_surface, + location + .to_local(output) + .as_logical() + .to_physical_precise_round(scale), + geometry.to_local(output).as_logical().to_f64(), + Scale::from(scale), + 1.0, + false, + [0; 4], + None, + blur_strength, + FRAME_TIME_FILTER, + &mut |elem| elements.extend(crop_to_output(elem.into()).map(Into::into)), + None, + ); + } + } + Stage::StickyPopups(layout) => { + let alpha = match &overview.0 { + OverviewMode::Started(_, started) => { + (1.0 - (Instant::now().duration_since(*started).as_millis() + / ANIMATION_DURATION.as_millis()) as f32) + .max(0.0) + * 0.4 + + 0.6 + } + OverviewMode::Ended(_, ended) => { + ((Instant::now().duration_since(*ended).as_millis() + / ANIMATION_DURATION.as_millis()) as f32) + * 0.4 + + 0.6 + } + OverviewMode::Active(_) => 0.6, + OverviewMode::None => 1.0, + }; + + layout.render_popups(renderer, alpha, scanout_node, &mut |elem| { + if let Some(elem) = crop_to_output(elem.into()) { + elements.push(elem.into()) + } + }); + } + Stage::Sticky(layout) => { + let alpha = match &overview.0 { + OverviewMode::Started(_, started) => { + (1.0 - (Instant::now().duration_since(*started).as_millis() + / ANIMATION_DURATION.as_millis()) as f32) + .max(0.0) + * 0.4 + + 0.6 + } + OverviewMode::Ended(_, ended) => { + ((Instant::now().duration_since(*ended).as_millis() + / ANIMATION_DURATION.as_millis()) as f32) + * 0.4 + + 0.6 + } + OverviewMode::Active(_) => 0.6, + OverviewMode::None => 1.0, + }; + + let current_focus = (!move_active && is_active_space) + .then_some(last_active_seat) + .map(|seat| workspace.focus_stack.get(seat)); + + layout.render( + renderer, + current_focus.as_ref().and_then(|stack| { + stack.last().and_then(|t| match t { + FocusTarget::Window(w) => Some(w), + _ => None, + }) + }), + resize_indicator.clone(), + active_hint, + alpha, + theme.cosmic(), + scanout_node, + &mut |elem| { + if let Some(elem) = crop_to_output(elem.into()) { + elements.push(elem.into()) + } + }, + ); + } + Stage::WorkspacePopups { workspace, offset } => { + workspace.render_popups( + renderer, + last_active_seat, + !move_active && is_active_space, + overview.clone(), + theme.cosmic(), + scanout_node, + &mut |elem| { + if let Some(elem) = crop_to_output(elem) { + elements.push(CosmicElement::Workspace( + RelocateRenderElement::from_element( + elem, + offset.to_physical_precise_round(scale), + Relocate::Relative, + ), + )); + } + }, + ); + } + Stage::Workspace { workspace, offset } => { + workspace.render( + renderer, + last_active_seat, + !move_active && is_active_space, + overview.clone(), + resize_indicator.clone(), + active_hint, + theme.cosmic(), + scanout_node, + &mut |elem| { + if let Some(elem) = crop_to_output(elem) { + elements.push(CosmicElement::Workspace( + RelocateRenderElement::from_element( + elem, + offset.to_physical_precise_round(scale), + Relocate::Relative, + ), + )); + } + }, + ); + } + }; + + ControlFlow::Continue(()) + })?; + + Ok(elements) +} + +fn session_lock_elements( + renderer: &mut R, + output: &Output, + lock_surface: Option<&LockSurface>, + blur_strength: usize, + push: &mut dyn FnMut(SurfaceRenderElement), +) where + R: AsGlowRenderer, + R::TextureId: Clone + 'static, +{ + if let Some(surface) = lock_surface { + let scale = Scale::from(output.current_scale().fractional_scale()); + push_render_elements_from_surface_tree( + renderer, + surface.wl_surface(), + (0, 0), + bbox_from_surface_tree(surface.wl_surface(), (0, 0)).to_f64(), + scale, + 1.0, + false, + [0; 4], + None, + blur_strength, + FRAME_TIME_FILTER, + push, + None, + ) + } +} + +// Used for mirroring and postprocessing +#[derive(Debug)] +pub struct PostprocessState { + pub texture: TextureRenderBuffer, + pub damage_tracker: OutputDamageTracker, + pub cursor_texture: Option>, + pub cursor_damage_tracker: Option, + pub output_config: PostprocessOutputConfig, +} + +impl PostprocessState { + pub fn new_with_renderer( + renderer: &mut R, + format: Fourcc, + output_config: PostprocessOutputConfig, + ) -> Result { + let size = output_config.size; + let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal); + let opaque_regions = vec![Rectangle::from_size(buffer_size)]; + + let texture = Offscreen::::create_buffer(renderer, format, buffer_size)?; + let texture_buffer = TextureRenderBuffer::from_texture( + renderer.glow_renderer(), + texture, + 1, + Transform::Normal, + Some(opaque_regions), + ); + + // Don't use `from_output` to avoid applying output transform + let damage_tracker = + OutputDamageTracker::new(size, output_config.fractional_scale, Transform::Normal); + + Ok(PostprocessState { + texture: texture_buffer, + damage_tracker, + cursor_texture: None, + cursor_damage_tracker: None, + output_config, + }) + } + + pub fn track_cursor( + &mut self, + renderer: &mut R, + format: Fourcc, + size: Size, + scale: Scale, + ) -> Result<(), R::Error> { + let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal); + + if let (Some(tex), Some(tracker)) = ( + self.cursor_texture.as_ref(), + self.cursor_damage_tracker.as_ref(), + ) && tex.format().is_some_and(|f| f == format) + && tracker.mode() + == &(OutputModeSource::Static { + size, + scale, + transform: Transform::Normal, + }) + { + return Ok(()); + } + + let texture = Offscreen::::create_buffer(renderer, format, buffer_size)?; + + let texture_buffer = TextureRenderBuffer::from_texture( + renderer.glow_renderer(), + texture, + 1, + Transform::Normal, + None, + ); + + let damage_tracker = OutputDamageTracker::new(size, scale, Transform::Normal); + + self.cursor_texture = Some(texture_buffer); + self.cursor_damage_tracker = Some(damage_tracker); + + Ok(()) + } + + pub fn remove_cursor(&mut self) { + self.cursor_texture.take(); + self.cursor_damage_tracker.take(); + } +} + +#[derive(Debug, PartialEq)] +pub struct PostprocessOutputConfig { + pub size: Size, + pub fractional_scale: f64, +} + +impl PostprocessOutputConfig { + pub fn for_output_untransformed(output: &Output) -> Self { + Self { + // Apply inverse of output transform to mode size to get correct size + // for an untransformed render. + size: output.current_transform().invert().transform_size( + output + .current_mode() + .map(|mode| mode.size) + .unwrap_or_default(), + ), + fractional_scale: output.current_scale().fractional_scale(), + } + } + + pub fn for_output(output: &Output) -> Self { + Self { + size: output + .current_mode() + .map(|mode| mode.size) + .unwrap_or_default(), + fractional_scale: output.current_scale().fractional_scale(), + } + } +} + +#[derive(Debug, Default)] +pub struct ScreenFilterStorage { + pub filter: ScreenFilter, + pub state: Option, +} + +#[profiling::function] +pub fn render_output<'d, R>( + gpu: Option<&DrmNode>, + renderer: &mut R, + target: &mut R::Framebuffer<'_>, + damage_tracker: &'d mut OutputDamageTracker, + age: usize, + shell: &Arc>, + now: Time, + output: &Output, + cursor_mode: CursorMode, + screen_filter: &'d mut ScreenFilterStorage, + loop_handle: &calloop::LoopHandle<'static, State>, +) -> Result, RenderError> +where + R: AsGlowRenderer, + R::TextureId: Send + Clone + 'static, + CosmicElement: RenderElement, + CosmicMappedRenderElement: RenderElement, + WorkspaceRenderElement: RenderElement, +{ + let shell_ref = shell.read(); + let (previous_workspace, workspace) = shell_ref + .workspaces + .active(output) + .ok_or(RenderError::OutputNoMode(OutputNoMode))?; + let (previous_idx, idx) = shell_ref.workspaces.active_num(output); + let previous_workspace = previous_workspace + .zip(previous_idx) + .map(|((w, start), idx)| (w.handle, idx, start)); + let workspace = (workspace.handle, idx); + let zoom_state = shell_ref.zoom_state().cloned(); + std::mem::drop(shell_ref); + + let element_filter = if workspace_overview_is_open(output) { + ElementFilter::LayerShellOnly + } else { + ElementFilter::All + }; + + let mut postprocess_texture = None; + let result = if !screen_filter.filter.is_noop() { + if screen_filter.state.as_ref().is_none_or(|state| { + state.output_config != PostprocessOutputConfig::for_output_untransformed(output) + }) { + screen_filter.state = Some( + PostprocessState::new_with_renderer( + renderer, + target.format().unwrap_or(Fourcc::Abgr8888), + PostprocessOutputConfig::for_output_untransformed(output), + ) + .map_err(RenderError::Rendering)?, + ); + } + + let state = screen_filter.state.as_mut().unwrap(); + let mut result = Err(RenderError::OutputNoMode(OutputNoMode)); + state + .texture + .render() + .draw::<_, RenderError>(|tex| { + let mut target = renderer.bind(tex).map_err(RenderError::Rendering)?; + result = render_workspace( + gpu, + renderer, + &mut target, + &mut state.damage_tracker, + 1, + None, + shell, + zoom_state.as_ref(), + now, + output, + previous_workspace, + workspace, + cursor_mode, + element_filter, + ); + std::mem::drop(target); + postprocess_texture = Some(tex.clone()); + + Ok(if let Ok((res, _)) = result.as_ref() { + renderer.wait(&res.sync).map_err(RenderError::Rendering)?; + let transform = output.current_transform(); + let area = tex.size().to_logical(1, transform); + + res.damage + .cloned() + .map(|v| { + v.into_iter() + .map(|r| r.to_logical(1).to_buffer(1, transform, &area)) + .collect::>() + }) + .unwrap_or_default() + } else { + Vec::new() + }) + })?; + + if result.is_ok() { + let texture_elem = TextureRenderElement::from_texture_render_buffer( + (0., 0.), + &state.texture, + Some(1.0), + None, + None, + Kind::Unspecified, + ); + + let postprocess_texture_shader = renderer + .glow_renderer_mut() + .egl_context() + .user_data() + .get::() + .expect("OffscreenShader should be available through `init_shaders`"); + let texture_geometry = + texture_elem.geometry(output.current_scale().fractional_scale().into()); + let elements = { + let texture_elem = TextureShaderElement::new( + texture_elem, + postprocess_texture_shader.0.clone(), + vec![ + Uniform::new( + "invert", + if screen_filter.filter.inverted { + 1. + } else { + 0. + }, + ), + Uniform::new( + "color_mode", + screen_filter + .filter + .color_filter + .map(|val| val as u8 as f32) + .unwrap_or(0.), + ), + ], + ); + constrain_render_elements( + std::iter::once(texture_elem), + (0, 0), + Rectangle::from_size( + output + .geometry() + .size + .as_logical() + .to_f64() + .to_physical(output.current_scale().fractional_scale()) + .to_i32_round(), + ), + texture_geometry, + ConstrainScaleBehavior::Fit, + ConstrainAlign::CENTER, + 1.0, + ) + .map(CosmicElement::Postprocess) + .collect::>() + }; + + damage_tracker.render_output(renderer, target, age, &elements, CLEAR_COLOR)?; + } + + result + } else { + render_workspace( + gpu, + renderer, + target, + damage_tracker, + age, + None, + shell, + zoom_state.as_ref(), + now, + output, + previous_workspace, + workspace, + cursor_mode, + element_filter, + ) + }; + + match result { + Ok((res, mut elements)) => { + for (session, frame) in output.take_pending_frames() { + if let Some(pending_image_copy_data) = render_session( + renderer, + session.user_data().get::().unwrap(), + frame, + output.current_transform(), + |buffer, renderer, offscreen, dt, age, additional_damage| { + let old_len = if !additional_damage.is_empty() { + let area = output + .current_mode() + .ok_or(RenderError::OutputNoMode(OutputNoMode)) + .map( + |mode| { + mode.size + .to_logical(1) + .to_buffer(1, Transform::Normal) + .to_f64() + }, /* TODO: Mode is Buffer..., why is this Physical in the first place */ + )?; + + let old_len = elements.len(); + let additional_damage_elements: Vec<_> = additional_damage + .into_iter() + .map(|rect| { + rect.to_f64() + .to_logical( + output.current_scale().fractional_scale(), + output.current_transform(), + &area, + ) + .to_i32_round() + }) + .map(DamageElement::new) + .collect(); + dt.damage_output(age, &additional_damage_elements)?; + + Some(old_len) + } else { + None + }; + + let res = dt.damage_output(age, &elements)?; + + if let Some(old_len) = old_len { + elements.truncate(old_len); + } + + let mut sync = SyncPoint::default(); + + if let (Some(damage), _) = &res { + // TODO: On Vulkan, may need to combine sync points instead of just using latest? + let blit_to_buffer = + |renderer: &mut R, blit_from: &mut R::Framebuffer<'_>| { + if let Ok(dmabuf) = get_dmabuf(buffer) { + let mut dmabuf_clone = dmabuf.clone(); + let mut fb = renderer.bind(&mut dmabuf_clone)?; + for rect in damage.iter() { + sync = renderer.blit( + blit_from, + &mut fb, + *rect, + *rect, + TextureFilter::Nearest, + )?; + } + } else { + let fb = offscreen + .expect("shm buffers should have offscreen target"); + for rect in damage.iter() { + sync = renderer.blit( + blit_from, + fb, + *rect, + *rect, + TextureFilter::Nearest, + )?; + } + } + + Result::<_, R::Error>::Ok(()) + }; + + // we would want to just assign a different framebuffer to a variable, depending on the code-path, + // but then rustc tries to equate the lifetime of target with the lifetime of our temporary fb... + // So instead of duplicating all the code, we use a closure.. + if let Some(tex) = postprocess_texture.as_mut() { + let mut fb = renderer.bind(tex).map_err(RenderError::Rendering)?; + blit_to_buffer(renderer, &mut fb) + .map_err(RenderError::Rendering)?; + } else { + blit_to_buffer(renderer, target).map_err(RenderError::Rendering)?; + } + } + + let buffers = render_element_buffers(renderer, &elements); + + Ok(( + RenderOutputResult { + damage: res.0, + sync, + states: res.1, + }, + buffers, + )) + }, + )? { + pending_image_copy_data.send_success_when_ready( + output.current_transform(), + loop_handle, + now, + ); + } + } + + Ok(res) + } + Err(err) => Err(err), + } +} + +#[profiling::function] +pub fn render_workspace<'d, R>( + gpu: Option<&DrmNode>, + renderer: &mut R, + target: &mut R::Framebuffer<'_>, + damage_tracker: &'d mut OutputDamageTracker, + age: usize, + additional_damage: Option>>, + shell: &Arc>, + zoom_level: Option<&ZoomState>, + now: Time, + output: &Output, + previous: Option<(WorkspaceHandle, usize, WorkspaceDelta)>, + current: (WorkspaceHandle, usize), + cursor_mode: CursorMode, + element_filter: ElementFilter, +) -> Result<(RenderOutputResult<'d>, Vec>), RenderError> +where + R: AsGlowRenderer, + R::TextureId: Send + Clone + 'static, + CosmicElement: RenderElement, + CosmicMappedRenderElement: RenderElement, + WorkspaceRenderElement: RenderElement, +{ + let mut elements: Vec> = if let Some(additional_damage) = additional_damage { + let output_geo = output.geometry().to_local(output).as_logical(); + additional_damage + .into_iter() + .filter_map(|rect| rect.intersection(output_geo)) + .map(DamageElement::new) + .map(CosmicElement::from) + .collect() + } else { + Vec::new() + }; + + elements.extend(workspace_elements( + gpu, + renderer, + shell, + zoom_level, + now, + output, + previous, + current, + cursor_mode, + element_filter, + None, + )?); + + let res = damage_tracker.render_output( + renderer, + target, + age, + &elements, + CLEAR_COLOR, // TODO use a theme neutral color + ); + + res.map(|res| (res, elements)) +} diff --git a/src/backend/render/shaders/blur_downsample.frag b/src/backend/render/shaders/blur_downsample.frag new file mode 100644 index 0000000..1fff752 --- /dev/null +++ b/src/backend/render/shaders/blur_downsample.frag @@ -0,0 +1,30 @@ +#version 100 + +//_DEFINES_ + +precision highp float; + +varying vec2 v_coords; + +uniform sampler2D tex; +uniform vec2 half_pixel; +uniform float offset; + +uniform float alpha; +#if defined(DEBUG_FLAGS) +uniform float tint; +#endif + +void main() { + vec4 sum = texture2D(tex, v_coords) * 4.0; + sum += texture2D(tex, v_coords - half_pixel * offset); + sum += texture2D(tex, v_coords + half_pixel * offset); + sum += texture2D(tex, v_coords + vec2(half_pixel.x, -half_pixel.y) * offset); + sum += texture2D(tex, v_coords - vec2(half_pixel.x, -half_pixel.y) * offset); + + if (sum.a == 0.0) { + gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0); + } else { + gl_FragColor = sum / sum.a; + } +} diff --git a/src/backend/render/shaders/blur_upsample.frag b/src/backend/render/shaders/blur_upsample.frag new file mode 100644 index 0000000..588fcb6 --- /dev/null +++ b/src/backend/render/shaders/blur_upsample.frag @@ -0,0 +1,31 @@ +#version 100 + +precision highp float; + +varying vec2 v_coords; + +uniform sampler2D tex; +uniform vec2 half_pixel; +uniform float offset; + +uniform float alpha; +#if defined(DEBUG_FLAGS) +uniform float tint; +#endif + +void main() { + vec4 sum = texture2D(tex, v_coords + vec2(-half_pixel.x * 2.0, 0.0) * offset); + sum += texture2D(tex, v_coords + vec2(-half_pixel.x, half_pixel.y) * offset) * 2.0; + sum += texture2D(tex, v_coords + vec2(0.0, half_pixel.y * 2.0) * offset); + sum += texture2D(tex, v_coords + vec2(half_pixel.x, half_pixel.y) * offset) * 2.0; + sum += texture2D(tex, v_coords + vec2(half_pixel.x * 2.0, 0.0) * offset); + sum += texture2D(tex, v_coords + vec2(half_pixel.x, -half_pixel.y) * offset) * 2.0; + sum += texture2D(tex, v_coords + vec2(0.0, -half_pixel.y * 2.0) * offset); + sum += texture2D(tex, v_coords + vec2(-half_pixel.x, -half_pixel.y) * offset) * 2.0; + + if (sum.a == 0.0) { + gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0); + } else { + gl_FragColor = sum / sum.a; + } +} diff --git a/src/backend/render/shaders/clipped_surface.frag b/src/backend/render/shaders/clipped_surface.frag new file mode 100644 index 0000000..d99d1a5 --- /dev/null +++ b/src/backend/render/shaders/clipped_surface.frag @@ -0,0 +1,104 @@ +// Taken from niri and modified, licensed GPL-3.0 + +#version 100 + +//_DEFINES_ + +#if defined(EXTERNAL) +#extension GL_OES_EGL_image_external : require +#endif + +precision highp float; +#if defined(EXTERNAL) +uniform samplerExternalOES tex; +#else +uniform sampler2D tex; +#endif + +uniform float alpha; +varying vec2 v_coords; + +#if defined(DEBUG_FLAGS) +uniform float tint; +#endif + +uniform vec2 geo_size; +uniform vec4 corner_radius; +uniform mat3 input_to_geo; +uniform float noise; + +float rounding_alpha(vec2 coords, vec2 size) { + vec2 center; + float radius; + + if (coords.x < corner_radius.x && coords.y < corner_radius.x) { + radius = corner_radius.x; + center = vec2(radius, radius); + } else if (size.x - corner_radius.y < coords.x && coords.y < corner_radius.y) { + radius = corner_radius.y; + center = vec2(size.x - radius, radius); + } else if (size.x - corner_radius.z < coords.x && size.y - corner_radius.z < coords.y) { + radius = corner_radius.z; + center = vec2(size.x - radius, size.y - radius); + } else if (coords.x < corner_radius.w && size.y - corner_radius.w < coords.y) { + radius = corner_radius.w; + center = vec2(radius, size.y - radius); + } else { + return 1.0; + } + + float dist = distance(coords, center); + float half_px = 0.5; + return 1.0 - smoothstep(radius - half_px, radius + half_px, dist); +} + +float hash(vec2 p) { + vec3 p3 = fract(vec3(p.xyx) * 727.727); + p3 += dot(p3, p3.xyz + 33.33); + return fract((p3.x + p3.y) * p3.z); +} + +void main() { + if (alpha <= 0.0) { + discard; + } + + vec4 color = texture2D(tex, v_coords); + #if defined(NO_ALPHA) + color = vec4(color.rgb, 1.0); + #endif + if (color.a <= 0.0) { + discard; + } + + vec3 coords_geo = input_to_geo * vec3(v_coords, 1.0); + if (coords_geo.x < 0.0 || 1.0 < coords_geo.x || coords_geo.y < 0.0 || 1.0 < coords_geo.y) { + // Clip outside geometry. + color = vec4(0.0); + } else { + // Apply corner rounding inside geometry. + color = color * rounding_alpha(coords_geo.xy * geo_size, geo_size); + } + + if (color.a <= 0.0) { + discard; + } + + if (noise > 0.0) { + // Add noise fx + // This can be used to achieve a glass look + float noiseHash = hash(v_coords); + float noiseAmount = (mod(noiseHash, 1.0) - 0.5); + color.rgb += noiseAmount * noise; + } + + // Apply final alpha and tint. + color *= alpha; + + #if defined(DEBUG_FLAGS) + if (tint == 1.0) + color = vec4(0.0, 0.2, 0.0, 0.2) + color * 0.8; + #endif + + gl_FragColor = color; +} diff --git a/src/backend/render/shaders/offscreen.frag b/src/backend/render/shaders/offscreen.frag new file mode 100644 index 0000000..d95859c --- /dev/null +++ b/src/backend/render/shaders/offscreen.frag @@ -0,0 +1,90 @@ +#version 100 + +//_DEFINES_ + +#if defined(EXTERNAL) +#extension GL_OES_EGL_image_external : require +#endif + +precision highp float; +#if defined(EXTERNAL) +uniform samplerExternalOES tex; +#else +uniform sampler2D tex; +#endif + +uniform float alpha; +varying vec2 v_coords; + +#if defined(DEBUG_FLAGS) +uniform float tint; +#endif + +uniform float invert; +uniform float color_mode; + +void main() { + vec4 color = texture2D(tex, v_coords); + +#if defined(NO_ALPHA) + color = vec4(color.rgb, 1.0) * alpha; +#else + color = color * alpha; +#endif + + // un-multiply + color.rgb /= color.a; + + // First invert then filter + + if (invert == 1.0) { + color.rgb = 1.0 - color.rgb; + } + + if (color_mode == 1.0) { // greyscale + float value = (color.r + color.g + color.b) / 3.0; + color = vec4(value, value, value, color.a); + } else if (color_mode >= 2.0) { + float L = (17.8824 * color.r) + (43.5161 * color.g) + (4.11935 * color.b); + float M = (3.45565 * color.r) + (27.1554 * color.g) + (3.86714 * color.b); + float S = (0.0299566 * color.r) + (0.184309 * color.g) + (1.46709 * color.b); + + float l, m, s; + if (color_mode == 2.0) { // Protanopia + l = 0.0 * L + 2.02344 * M + -2.52581 * S; + m = 0.0 * L + 1.0 * M + 0.0 * S; + s = 0.0 * L + 0.0 * M + 1.0 * S; + } else if (color_mode == 3.0) { // Deuteranopia + l = 1.0 * L + 0.0 * M + 0.0 * S; + m = 0.494207 * L + 0.0 * M + 1.24827 * S; + s = 0.0 * L + 0.0 * M + 1.0 * S; + } else if (color_mode == 4.0) { // Tritanopia + l = 1.0 * L + 0.0 * M + 0.0 * S; + m = 0.0 * L + 1.0 * M + 0.0 * S; + s = -0.395913 * L + 0.801109 * M + 0.0 * S; + } else { + // unknown + l = L; + m = M; + s = S; + } + + vec3 error; + error.r = (0.0809444479 * l) + (-0.130504409 * m) + (0.116721066 * s); + error.g = (-0.0102485335 * l) + (0.0540193266 * m) + (-0.113614708 * s); + error.b = (-0.000365296938 * l) + (-0.00412161469 * m) + (0.693511405 * s); + + vec3 diff = color.rgb - error; + vec3 correction; + correction.r = 0.0; + correction.g = (diff.r * 0.7) + (diff.g * 1.0); + correction.b = (diff.r * 0.7) + (diff.b * 1.0); + + color.rgb += correction; + } + + // re-multiply + color.rgb *= color.a; + + gl_FragColor = color; +} \ No newline at end of file diff --git a/src/backend/render/shaders/rounded_outline.frag b/src/backend/render/shaders/rounded_outline.frag new file mode 100644 index 0000000..d3266e6 --- /dev/null +++ b/src/backend/render/shaders/rounded_outline.frag @@ -0,0 +1,64 @@ +precision highp float; +uniform float alpha; +#if defined(DEBUG_FLAGS) +uniform float tint; +#endif +uniform vec2 size; +varying vec2 v_coords; + +uniform vec3 color; +uniform float thickness; +uniform vec4 radius; +uniform float scale; + +float rounding_alpha(vec2 coords, vec2 size, vec4 radius) { + vec2 center; + float r; + + if (coords.x < radius.x && coords.y < radius.x) { + r = radius.x; + center = vec2(r, r); + } else if (size.x - radius.y < coords.x && coords.y < radius.y) { + r = radius.y; + center = vec2(size.x - r, r); + } else if (size.x - radius.z < coords.x && size.y - radius.z < coords.y) { + r = radius.z; + center = vec2(size.x - r, size.y - r); + } else if (coords.x < radius.w && size.y - radius.w < coords.y) { + r = radius.w; + center = vec2(r, size.y - r); + } else { + return 1.0; + } + + float dist = distance(coords, center); + float half_px = 0.5 / scale; + return 1.0 - smoothstep(r - half_px, r + half_px, dist); +} + +void main() { + vec2 location = v_coords * size; + + float outer_alpha = rounding_alpha(v_coords * size, size, radius); + float inner_alpha = 1.0; + + if (thickness > 0.0) { + location -= vec2(thickness); + vec2 inner_size = size - vec2(thickness * 2.0); + if (0.0 <= location.x && location.x <= inner_size.x + && 0.0 <= location.y && location.y <= inner_size.y) + { + vec4 inner_radius = radius - vec4(thickness); + inner_alpha = 1.0 - rounding_alpha(location, inner_size, inner_radius); + } + } + + vec4 mix_color = mix(vec4(0.0, 0.0, 0.0, 0.0), vec4(color, alpha), outer_alpha * inner_alpha); + + #if defined(DEBUG_FLAGS) + if (tint == 1.0) + mix_color = vec4(0.0, 0.3, 0.0, 0.2) + mix_color * 0.8; + #endif + + gl_FragColor = mix_color; +} diff --git a/src/backend/render/shaders/rounded_rectangle.frag b/src/backend/render/shaders/rounded_rectangle.frag new file mode 100644 index 0000000..e743f51 --- /dev/null +++ b/src/backend/render/shaders/rounded_rectangle.frag @@ -0,0 +1,37 @@ +precision highp float; +uniform float alpha; +#if defined(DEBUG_FLAGS) +uniform float tint; +#endif +uniform vec2 size; +varying vec2 v_coords; + +uniform vec3 color; +uniform float radius; + +float rounded_box(in vec2 p, in vec2 b, in vec4 r) +{ + r.xy = (p.x > 0.0) ? r.xy : r.zw; + r.x = (p.y > 0.0) ? r.x : r.y; + vec2 q = abs(p) - b + r.x; + return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r.x; +} + +void main() { + vec2 center = size / 2.0; + vec2 location = v_coords * size; + vec4 mix_color; + vec4 corners = vec4(radius); + + float distance = rounded_box(location - center, size / 2.0, corners); + float smoothedAlpha = 1.0 - smoothstep(0.0, 1.0, distance); + + mix_color = mix(vec4(0.0, 0.0, 0.0, 0.0), vec4(color, alpha), smoothedAlpha); + + #if defined(DEBUG_FLAGS) + if (tint == 1.0) + mix_color = vec4(0.0, 0.3, 0.0, 0.2) + mix_color * 0.8; + #endif + + gl_FragColor = mix_color; +} diff --git a/src/backend/render/shaders/shadow.frag b/src/backend/render/shaders/shadow.frag new file mode 100644 index 0000000..fb72349 --- /dev/null +++ b/src/backend/render/shaders/shadow.frag @@ -0,0 +1,139 @@ +precision highp float; + +uniform float alpha; +uniform vec2 size; +varying vec2 v_coords; + +#if defined(DEBUG_FLAGS) +uniform float tint; +#endif + +uniform vec4 shadow_color; +uniform float sigma; + +uniform mat3 input_to_geo; +uniform vec2 geo_size; +uniform vec4 corner_radius; + +uniform mat3 window_input_to_geo; +uniform vec2 window_geo_size; +uniform vec4 window_corner_radius; + +// Taken from niri, based on: https://madebyevan.com/shaders/fast-rounded-rectangle-shadows/ +// +// License: CC0 (http://creativecommons.org/publicdomain/zero/1.0/) + +// A standard gaussian function, used for weighting samples +float gaussian(float x, float sigma) { + const float pi = 3.141592653589793; + return exp(-(x * x) / (2.0 * sigma * sigma)) / (sqrt(2.0 * pi) * sigma); +} + +// This approximates the error function, needed for the gaussian integral +vec2 erf(vec2 x) { + vec2 s = sign(x), a = abs(x); + x = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a; + x *= x; + return s - s / (x * x); +} + +// Return the blurred mask along the x dimension +float roundedBoxShadowX(float x, float y, float sigma, float corner, vec2 halfSize) { + float delta = min(halfSize.y - corner - abs(y), 0.0); + float curved = halfSize.x - corner + sqrt(max(0.0, corner * corner - delta * delta)); + vec2 integral = 0.5 + 0.5 * erf((x + vec2(-curved, curved)) * (sqrt(0.5) / sigma)); + return integral.y - integral.x; +} + +// Return the mask for the shadow of a box from lower to upper +float roundedBoxShadow(vec2 lower, vec2 upper, vec2 point, float sigma, float corner) { + // Center everything to make the math easier + vec2 center = (lower + upper) * 0.5; + vec2 halfSize = (upper - lower) * 0.5; + point -= center; + + // The signal is only non-zero in a limited range, so don't waste samples + float low = point.y - halfSize.y; + float high = point.y + halfSize.y; + float start = clamp(-3.0 * sigma, low, high); + float end = clamp(3.0 * sigma, low, high); + + // Accumulate samples (we can get away with surprisingly few samples) + float step = (end - start) / 4.0; + float y = start + step * 0.5; + float value = 0.0; + for (int i = 0; i < 4; i++) { + value += roundedBoxShadowX(point.x, point.y - y, sigma, corner, halfSize) * gaussian(y, sigma) * step; + y += step; + } + + return value; +} + +float rounding_alpha(vec2 coords, vec2 size, vec4 corner_radius) { + vec2 center; + float radius; + + if (coords.x < corner_radius.x && coords.y < corner_radius.x) { + radius = corner_radius.x; + center = vec2(radius, radius); + } else if (size.x - corner_radius.y < coords.x && coords.y < corner_radius.y) { + radius = corner_radius.y; + center = vec2(size.x - radius, radius); + } else if (size.x - corner_radius.z < coords.x && size.y - corner_radius.z < coords.y) { + radius = corner_radius.z; + center = vec2(size.x - radius, size.y - radius); + } else if (coords.x < corner_radius.w && size.y - corner_radius.w < coords.y) { + radius = corner_radius.w; + center = vec2(radius, size.y - radius); + } else { + return 1.0; + } + + float dist = distance(coords, center); + return 1.0 - smoothstep(radius - 0.5, radius + 0.5, dist); +} + +void main() { + vec3 coords_geo = input_to_geo * vec3(v_coords, 1.0); + vec3 coords_window_geo = window_input_to_geo * vec3(v_coords, 1.0); + + vec4 color = shadow_color; + + float shadow_value; + if (sigma < 0.1) { + // With low enough sigma just draw a rounded rectangle. + shadow_value = rounding_alpha(coords_geo.xy, geo_size, corner_radius); + } else { + shadow_value = roundedBoxShadow( + vec2(0.0, 0.0), + geo_size, + coords_geo.xy, + sigma, + // FIXME: figure out how to blur with different corner radii. + // + // GTK seems to call blurring separately for the rect and for the 4 corners: + // https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-4-16/gsk/gpu/shaders/gskgpuboxshadow.glsl + corner_radius.z + ); + } + color = color * shadow_value; + + // Cut out the inside of the window geometry if requested. + if (window_geo_size != vec2(0.0, 0.0)) { + if (0.0 <= coords_window_geo.x && coords_window_geo.x <= window_geo_size.x + && 0.0 <= coords_window_geo.y && coords_window_geo.y <= window_geo_size.y) { + float alpha = rounding_alpha(coords_window_geo.xy, window_geo_size, window_corner_radius); + color = color * (1.0 - alpha); + } + } + + color = color * alpha; + + #if defined(DEBUG_FLAGS) + if (tint == 1.0) + color = vec4(0.0, 0.2, 0.0, 0.2) + color * 0.8; + #endif + + gl_FragColor = color; +} diff --git a/src/backend/render/shadow.rs b/src/backend/render/shadow.rs new file mode 100644 index 0000000..37b1fde --- /dev/null +++ b/src/backend/render/shadow.rs @@ -0,0 +1,191 @@ +use std::{borrow::Borrow, cell::RefCell, collections::HashMap}; + +use glam::{Affine2, Mat3, Vec2}; +use smithay::{ + backend::renderer::{ + element::Kind, + gles::{ + GlesPixelProgram, GlesRenderer, Uniform, UniformValue, element::PixelShaderElement, + }, + }, + utils::{Coordinate, IsAlive, Point, Rectangle, Size}, +}; + +use crate::{ + backend::render::element::AsGlowRenderer, + shell::element::CosmicMappedKey, + utils::prelude::{Local, RectLocalExt}, +}; + +pub static SHADOW_SHADER: &str = include_str!("./shaders/shadow.frag"); +pub struct ShadowShader(pub GlesPixelProgram); + +#[derive(Debug, PartialEq)] +pub struct ShadowParameters { + geo: Rectangle, + scale: f64, + alpha: f32, + radius: [u8; 4], + dark_mode: bool, +} +type ShadowCache = RefCell>; + +impl ShadowShader { + pub fn get(renderer: &R) -> GlesPixelProgram { + Borrow::::borrow(renderer.glow_renderer()) + .egl_context() + .user_data() + .get::() + .expect("Custom Shaders not initialized") + .0 + .clone() + } + + pub fn element( + renderer: &R, + key: CosmicMappedKey, + geo: Rectangle, + radius: [u8; 4], + alpha: f32, + scale: f64, + dark_mode: bool, + ) -> PixelShaderElement { + let params = ShadowParameters { + geo, + scale, + alpha, + radius, + dark_mode, + }; + let ceil = |logical: f64| (logical * scale).ceil() / scale; + + let mut geo = geo.to_f64(); + let fractional_pixel = scale.ceil() / scale; + geo.loc.x += fractional_pixel; + geo.loc.y += fractional_pixel; + geo.size.w -= fractional_pixel * 2.; + geo.size.h -= fractional_pixel * 2.; + + let user_data = Borrow::::borrow(renderer.glow_renderer()) + .egl_context() + .user_data(); + + user_data.insert_if_missing(|| ShadowCache::new(HashMap::new())); + let mut cache = user_data.get::().unwrap().borrow_mut(); + cache.retain(|k, _| k.alive()); + + if cache + .get(&key) + .filter(|(old_params, _)| ¶ms == old_params) + .is_none() + { + let shader = Self::get(renderer); + + let softness = 25.; + let spread = 5.; + let offset = [0., 5.]; + let color = [0., 0., 0., if dark_mode { 0.45 } else { 0.35 }]; + let radius = radius.map(|r| ceil(r as f64)); + let radius = [ + radius[3], // top_left + radius[1], // top_right + radius[0], // bottom_right + radius[2], // bottom_left + ]; + + let width = softness; + let sigma = width / 2.; + let width = ceil(sigma * 3.); + + let offset = Point::new(ceil(offset[0]), ceil(offset[1])); + let spread = ceil(spread.abs()).copysign(spread); + let offset = offset - Point::new(spread, spread); + + let box_size = if spread >= 0. { + geo.size + Size::new(spread, spread).upscale(2.) + } else { + geo.size - Size::new(-spread, -spread).upscale(2.) + }; + + let win_radius = radius; + let radius = radius.map(|r| if r > 0. { r.saturating_add(spread) } else { 0. }); + let shader_size = box_size + Size::from((width, width)).upscale(2.); + let mut shader_geo = Rectangle::new(Point::from((-width, -width)), shader_size); + + let window_geo = Rectangle::new(Point::new(0., 0.) - offset - shader_geo.loc, geo.size); + let area_size = Vec2::new(shader_geo.size.w as f32, shader_geo.size.h as f32); + let geo_loc = Vec2::new(-shader_geo.loc.x as f32, -shader_geo.loc.y as f32); + shader_geo.loc += offset + geo.loc; + + let input_to_geo = Mat3::from( + Affine2::from_scale(area_size) + * Affine2::from_translation(Vec2::new( + -geo_loc.x / area_size.x, + -geo_loc.y / area_size.y, + )), + ); + + let window_geo_loc = Vec2::new(window_geo.loc.x as f32, window_geo.loc.y as f32); + let window_input_to_geo = Mat3::from( + Affine2::from_scale(area_size) + * Affine2::from_translation(Vec2::new( + -window_geo_loc.x / area_size.x, + -window_geo_loc.y / area_size.y, + )), + ); + + let element = PixelShaderElement::new( + shader, + shader_geo.to_i32_up().as_logical(), + None, + alpha, + vec![ + Uniform::new("shadow_color", color), + Uniform::new("sigma", sigma as f32), + Uniform::new( + "input_to_geo", + UniformValue::Matrix3x3 { + matrices: vec![*AsRef::<[f32; 9]>::as_ref(&input_to_geo)], + transpose: false, + }, + ), + Uniform::new("geo_size", [box_size.w as f32, box_size.h as f32]), + Uniform::new( + "corner_radius", + [ + radius[0] as f32, + radius[1] as f32, + radius[2] as f32, + radius[3] as f32, + ], + ), + Uniform::new( + "window_input_to_geo", + UniformValue::Matrix3x3 { + matrices: vec![*AsRef::<[f32; 9]>::as_ref(&window_input_to_geo)], + transpose: false, + }, + ), + Uniform::new( + "window_geo_size", + [window_geo.size.w as f32, window_geo.size.h as f32], + ), + Uniform::new( + "window_corner_radius", + [ + win_radius[0] as f32, + win_radius[1] as f32, + win_radius[2] as f32, + win_radius[3] as f32, + ], + ), + ], + Kind::Unspecified, + ); + + cache.insert(key.clone(), (params, element)); + } + + cache.get(&key).unwrap().1.clone() + } +} diff --git a/src/backend/render/wayland/blur_effect.rs b/src/backend/render/wayland/blur_effect.rs new file mode 100644 index 0000000..c4d27fa --- /dev/null +++ b/src/backend/render/wayland/blur_effect.rs @@ -0,0 +1,705 @@ +use std::{ + borrow::{Borrow, BorrowMut}, + sync::{LazyLock, Mutex}, +}; + +use glam::{Affine2, Mat3, Vec2}; +use smithay::{ + backend::{ + allocator::Fourcc, + renderer::{ + Bind, BlitFrame, Color32F, ContextId, Frame, FrameContext, ImportAll, Offscreen, + Renderer, Texture, TextureFilter, + element::{Element, Id, Kind, RenderElement}, + gles::{ + GlesError, GlesFrame, GlesRenderer, GlesTexProgram, GlesTexture, Uniform, + UniformName, UniformType, UniformValue, ffi, + }, + sync::SyncPoint, + utils::{CommitCounter, DamageSet}, + }, + }, + utils::{ + Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform, user_data::UserDataMap, + }, + wayland::compositor::SurfaceData, +}; +use tracing::trace; + +use crate::{ + backend::render::{element::AsGlowRenderer, wayland::clipped_surface::ClippingShader}, + wayland::handlers::background_effect::ComputedBlurRegionCachedState, +}; + +pub static BLUR_DOWNSAMPLE_SHADER: &str = include_str!("../shaders/blur_downsample.frag"); +pub static BLUR_UPSAMPLE_SHADER: &str = include_str!("../shaders/blur_upsample.frag"); + +const NOISE: f32 = 0.03; +const MAX_STEPS: usize = 15; + +#[derive(Debug, Clone, Copy, PartialEq)] +struct BlurParameters { + passes: usize, + offset: f64, + extended_radius: i32, +} + +static BLUR_PARAMS: LazyLock> = LazyLock::new(|| { + let mut params = Vec::new(); + + let mut remaining_steps = MAX_STEPS as isize; + let offsets = [ + // min offset, max offset, extended radius to avoid artifacts + (1.0, 2.0, 10), + (2.0, 3.0, 20), + (2.0, 5.0, 50), + (3.0, 8.0, 150), + ]; + + let sum = offsets.iter().map(|(min, max, _)| *max - *min).sum::(); + for (i, (min, max, extended_radius)) in offsets.into_iter().enumerate() { + let mut iter_num = f64::ceil((max - min) / sum * (MAX_STEPS as f64)) as usize; + remaining_steps -= iter_num as isize; + + if remaining_steps < 0 { + iter_num = iter_num.saturating_add_signed(remaining_steps); + } + + let diff = max - min; + for j in 1..=iter_num { + params.push(BlurParameters { + passes: i + 1, + offset: min + (diff / iter_num as f64) * j as f64, + extended_radius, + }); + } + } + + trace!("Computed blur values: {:#?}", ¶ms); + params +}); + +#[derive(Debug, Clone)] +pub struct BlurShaders { + down: GlesTexProgram, + up: GlesTexProgram, +} + +impl BlurShaders { + pub fn compile(renderer: &mut GlesRenderer) -> Result { + let up = renderer.compile_custom_texture_shader( + BLUR_UPSAMPLE_SHADER, + &[ + UniformName::new("half_pixel", UniformType::_2f), + UniformName::new("offset", UniformType::_1f), + ], + )?; + let down = renderer.compile_custom_texture_shader( + BLUR_DOWNSAMPLE_SHADER, + &[ + UniformName::new("half_pixel", UniformType::_2f), + UniformName::new("offset", UniformType::_1f), + ], + )?; + + Ok(BlurShaders { up, down }) + } + + pub fn get(renderer: &R) -> Self { + Borrow::::borrow(renderer.glow_renderer()) + .egl_context() + .user_data() + .get::() + .expect("Custom Shaders not initialized") + .clone() + } +} + +type BlurTexture = Mutex>; + +#[derive(Debug)] +pub struct BlurState { + pub id: Id, + pub renderer_id: Option>, + pub src: Size, + pub offset: f64, + pub passes: usize, + pub region: Vec>, + pub commit: CommitCounter, +} + +unsafe impl Send for BlurState {} +unsafe impl Sync for BlurState {} + +impl Default for BlurState { + fn default() -> Self { + BlurState { + id: Id::new(), + renderer_id: None, + src: Size::new(0., 0.), + offset: 0., + passes: 0, + region: Vec::new(), + commit: CommitCounter::default(), + } + } +} + +pub struct BlurElement { + id: Id, + commit: CommitCounter, + src: Size, + extended_offset: Point, + geometry: Rectangle, + scaling_shaders: BlurShaders, + render_shader: GlesTexProgram, + region: Vec>, + offset: f64, + passes: usize, + uniforms: Vec>, +} + +impl BlurElement { + pub fn from_state( + renderer: &mut R, + state: &mut BlurState, + geometry: Rectangle, + output_scale: f64, + radii: [u8; 4], + strength: usize, + ) -> Result, R::Error> { + let region = vec![Rectangle::from_size(geometry.size.to_i32_round())]; + + Self::internal( + renderer, + state, + geometry, + ®ion, + output_scale, + radii, + strength, + ) + } + + pub fn from_surface( + renderer: &mut R, + states: &SurfaceData, + geometry: Rectangle, + output_scale: f64, + radii: [u8; 4], + strength: usize, + ) -> Result, R::Error> { + let mut blur_region_state = states.cached_state.get::(); + let Some(region) = blur_region_state.current().blur_region.as_ref() else { + return Ok(None); + }; + + let state = states + .data_map + .get_or_insert_threadsafe::, _>(Default::default); + + Self::internal( + renderer, + &mut state.lock().unwrap(), + geometry, + region, + output_scale, + radii, + strength, + ) + } + + pub fn internal( + renderer: &mut R, + state: &mut BlurState, + geometry: Rectangle, + region: &Vec>, + output_scale: f64, + radii: [u8; 4], + strength: usize, + ) -> Result, R::Error> { + if strength == 0 || geometry.size.w == 0. || geometry.size.h == 0. { + return Ok(None); + } + + let geo = geometry.to_physical_precise_round(output_scale); + let mut extended_geo = geo; + let radius = BLUR_PARAMS[(strength + 2).min(MAX_STEPS - 1)].extended_radius as f64; + extended_geo.loc -= Point::::new(radius, radius); + extended_geo.size += Size::::new(radius, radius).upscale(2.); + + // compute input_to_geo so that it crops the extended capture radius + let geo_scale = { + let Scale { x, y } = geo.size / extended_geo.size; + Affine2::from_scale(Vec2::new(x as f32, y as f32)).inverse() + }; + let geo_translation = { + let offset = geo.loc - extended_geo.loc; + Affine2::from_translation(-Vec2::new( + (offset.x / extended_geo.size.w) as f32, + (offset.y / extended_geo.size.h) as f32, + )) + }; + let input_to_geo = Mat3::from(geo_scale * geo_translation); + + let uniforms = vec![ + Uniform::new("geo_size", (geometry.size.w as f32, geometry.size.h as f32)), + Uniform::new( + "corner_radius", + [ + radii[3] as f32, + radii[1] as f32, + radii[0] as f32, + radii[2] as f32, + ], + ), + Uniform::new( + "input_to_geo", + UniformValue::Matrix3x3 { + matrices: vec![*AsRef::<[f32; 9]>::as_ref(&input_to_geo)], + transpose: false, + }, + ), + Uniform::new("noise", UniformValue::_1f(NOISE)), + ]; + + let geometry = extended_geo.to_logical(output_scale); + let extended_offset = Point::::new(radius, radius).to_logical(output_scale); + + let renderer_id = renderer.glow_renderer().context_id(); + let src = geometry.size.to_buffer(output_scale, Transform::Normal); + let params = &BLUR_PARAMS[strength.min(MAX_STEPS - 1)]; + + let dirty = !(state + .renderer_id + .as_ref() + .is_some_and(|id| id == &renderer_id) + && state.offset == params.offset + && state.passes == params.passes + && &state.region == region + && state.src == src); + + state.renderer_id = Some(renderer_id); + state.offset = params.offset; + state.passes = params.passes; + state.region = region.clone(); + state.src = src; + if dirty { + state.commit.increment(); + } + + Ok(Some(BlurElement { + id: state.id.clone(), + commit: state.commit, + src, + geometry, + extended_offset, + scaling_shaders: BlurShaders::get(renderer), + render_shader: ClippingShader::get(renderer), + offset: state.offset, + passes: state.passes, + region: region + .iter() + .cloned() + .map(|mut rect| { + rect.loc += extended_offset.to_i32_round(); + rect + }) + .collect(), + uniforms, + })) + } +} + +impl Element for BlurElement { + fn id(&self) -> &Id { + &self.id + } + + fn current_commit(&self) -> CommitCounter { + self.commit + } + + fn src(&self) -> Rectangle { + Rectangle::from_size(self.src) + } + + fn geometry(&self, scale: Scale) -> Rectangle { + self.geometry.to_physical_precise_round(scale) + } + + fn transform(&self) -> Transform { + Transform::Normal + } + + fn damage_since( + &self, + scale: Scale, + commit: Option, + ) -> DamageSet { + if self.commit.distance(commit).is_none_or(|d| d > 0) { + DamageSet::from_slice(&[Rectangle::new( + self.extended_offset.to_physical_precise_round(scale), + self.geometry.size.to_physical_precise_round(scale) + - self + .extended_offset + .to_size() + .upscale(2.) + .to_physical_precise_round(scale), + )]) + } else { + DamageSet::default() + } + } + + fn alpha(&self) -> f32 { + 1.0 + } + + fn kind(&self) -> Kind { + Kind::default() + } + + fn is_framebuffer_effect(&self) -> bool { + true + } +} + +impl RenderElement for BlurElement +where + R::TextureId: Send + 'static, +{ + fn capture_framebuffer( + &self, + frame: &mut ::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + cache: &UserDataMap, + ) -> Result<(), ::Error> { + let transform = frame.transformation(); + let tex_size = self.src.to_i32_round(); + let glow_frame = ::glow_frame_mut(frame); + let gles_frame = BorrowMut::>::borrow_mut(glow_frame); + let mut renderer = gles_frame.renderer(); + + let texture_ref = cache.get_or_insert_threadsafe(BlurTexture::::default); + let mut texture_entry = texture_ref.lock().unwrap(); + if texture_entry.as_ref().is_some_and(|tex| { + tex.size() != tex_size + || R::tex_to_gl( + &renderer.as_ref().context_id(), + texture_entry.as_ref().unwrap(), + ) + .is_none() + }) { + texture_entry.take(); + } + if texture_entry.is_none() { + let gl_texture = renderer + .as_mut() + .create_buffer(Fourcc::Abgr8888, tex_size) + .map_err(R::from_gles_error)?; + *texture_entry = Some(R::tex_from_gl(&renderer.as_ref().context_id(), gl_texture)); + } + + let mut texture = R::tex_to_gl( + &renderer.as_ref().context_id(), + texture_entry.as_ref().unwrap(), + ) + .unwrap(); + let mut off_texture = renderer + .as_mut() + .create_buffer(Fourcc::Abgr8888, tex_size) + .map_err(R::from_gles_error)?; + std::mem::drop(renderer); + + let sync = blit_from_active_fb(gles_frame, src, dst, transform, &mut texture) + .map_err(R::from_gles_error)?; + gles_frame.wait(&sync).map_err(R::from_gles_error)?; + + let mut textures = [&mut texture, &mut off_texture]; + render_blur( + gles_frame.renderer().as_mut(), + &self.scaling_shaders, + &mut textures, + self.offset, + self.passes, + ) + .map_err(R::from_gles_error)?; + + Ok(()) + } + + fn draw( + &self, + frame: &mut R::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + cache: Option<&UserDataMap>, + ) -> Result<(), R::Error> { + let src_to_geo = self.geometry.size / self.src; + let src_log = src + .upscale(src_to_geo) + .to_logical(1., Transform::Normal, &Size::default()); + let scale = dst.size.to_f64() / src_log.size; + + let damage = self + .region + .iter() + .flat_map(|rect| { + let mut rect = rect.to_f64().intersection(src_log)?; + rect.loc -= src_log.loc; + Some(rect.to_physical_precise_round(scale)) + }) + .flat_map(|rect| damage.iter().flat_map(move |r| r.intersection(rect))) + .collect::>(); + let cache = cache.expect("Framebuffer element without cache?"); + let Some(texture) = cache.get::>() else { + return Err(R::from_gles_error(GlesError::BlitError)); + }; + let texture_ref = texture.lock().unwrap(); + + if let Some(tex) = texture_ref.as_ref() { + BorrowMut::::borrow_mut(::glow_frame_mut(frame)) + .override_default_tex_program(self.render_shader.clone(), self.uniforms.clone()); + frame.render_texture_from_to( + tex, + src, + dst, + &damage, + opaque_regions, + Transform::Normal, + 1.0, + )?; + BorrowMut::::borrow_mut(::glow_frame_mut(frame)) + .clear_tex_program_override(); + } + Ok(()) + } +} + +fn blit_from_active_fb( + frame: &mut GlesFrame<'_, '_>, + src: Rectangle, + dst: Rectangle, + transform: Transform, + to_texture: &mut GlesTexture, +) -> Result { + let tex_size = to_texture.size(); + let tex_size_phys = tex_size.to_logical(1, Transform::Normal).to_physical(1); + let fb_size = frame.output_size(); + + let mut renderer = frame.renderer(); + let mut fb = renderer.as_mut().bind(to_texture)?; + let sync = { + let mut subframe = renderer + .as_mut() + .render(&mut fb, tex_size_phys, Transform::Normal)?; + subframe.clear( + Color32F::TRANSPARENT, + &[Rectangle::from_size(tex_size_phys)], + )?; + subframe.finish()? + }; + + if transform != Transform::Normal { + // We need to copy to a temporary texture to do an actual + // render pass with `render_texture_from_to` to do the rotation. + // dst is in screen space, but we just want to do a 1:1 copy in + // buffer space from dst of the current fb, so we need to undo any + // transforms for the blit. + let dst_phys = transform.transform_rect_in(dst, &fb_size); + let dst_buffer = dst_phys + .to_logical(1) + .to_buffer(1, Transform::Normal, &Size::default()); + let mut tmp_texture = renderer + .as_mut() + .create_buffer(Fourcc::Abgr8888, dst_buffer.size)?; + let mut fb_tmp = renderer.as_mut().bind(&mut tmp_texture)?; + std::mem::drop(renderer); + frame.wait(&sync)?; + + let sync = frame.blit_to( + &mut fb_tmp, + dst_phys, + Rectangle::from_size(dst_phys.size), + TextureFilter::Linear, + )?; + frame.wait(&sync)?; + std::mem::drop(fb_tmp); + + // now we bind the target texture with `Transform::Normal` + // and render the temporary texture with inverse transform + // into src. + let mut renderer = frame.renderer(); + let mut frame = renderer + .as_mut() + .render(&mut fb, tex_size_phys, Transform::Normal)?; + frame.wait(&sync)?; + Frame::render_texture_from_to( + &mut frame, + &tmp_texture, + Rectangle::from_size(dst_buffer.size.to_f64()), + src.to_logical(1., Transform::Normal, &Size::default()) + .to_physical(1.) + .to_i32_round(), + &[Rectangle::from_size(dst.size)], + &[Rectangle::from_size(dst.size)], + transform.invert(), + 1.0, + )?; + std::mem::drop(tmp_texture); + frame.finish() + } else { + std::mem::drop(renderer); + frame.wait(&sync)?; + frame.blit_to( + &mut fb, + dst, + src.to_logical(1., Transform::Normal, &Size::default()) + .to_physical(1.) + .to_i32_round(), + TextureFilter::Linear, + ) + } +} + +fn render_blur( + renderer: &mut GlesRenderer, + shaders: &BlurShaders, + textures: &mut [&mut GlesTexture; 2], + offset: f64, + passes: usize, +) -> Result<(), GlesError> { + for i in 0..passes { + let tex_size = textures[0].size(); + let [src_tex, target_tex] = textures; + let mut fb = renderer.bind(*target_tex)?; + + let adjusted_tex_size = tex_size.downscale(1 << i); + let target_tex_size = tex_size + .downscale(1 << (i + 1)) + .to_logical(1, Transform::Normal) + .to_physical(1); + let half_pixel = [ + 0.5 / (adjusted_tex_size.w as f32), + 0.5 / (adjusted_tex_size.h as f32), + ]; + + let mut frame = renderer.render( + &mut fb, + tex_size.to_logical(1, Transform::Normal).to_physical(1), + Transform::Normal, + )?; + frame.clear( + Color32F::new(0., 0., 0., 0.), + &[Rectangle::from_size( + tex_size.to_logical(1, Transform::Normal).to_physical(1), + )], + )?; + frame.with_context(|gl| unsafe { + gl.TexParameteri( + ffi::TEXTURE_2D, + ffi::TEXTURE_WRAP_S, + ffi::CLAMP_TO_EDGE as i32, + ); + gl.TexParameteri( + ffi::TEXTURE_2D, + ffi::TEXTURE_WRAP_T, + ffi::CLAMP_TO_EDGE as i32, + ); + })?; + frame.render_texture_from_to( + src_tex, + Rectangle::from_size(adjusted_tex_size.to_f64()), + Rectangle::from_size(target_tex_size), + &[Rectangle::from_size(target_tex_size)], + &[Rectangle::from_size(target_tex_size)], + Transform::Normal, + 1.0, + Some(&shaders.down), + &[ + Uniform::new("half_pixel", half_pixel), + Uniform::new("offset", (offset / (1 << i) as f64) as f32), + ], + )?; + frame.with_context(|gl| unsafe { + gl.TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_WRAP_S, ffi::REPEAT as i32); + gl.TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_WRAP_T, ffi::REPEAT as i32); + })?; + let sync = frame.finish()?; + std::mem::drop(fb); + renderer.wait(&sync)?; + + textures.swap(0, 1); + } + + for i in 0..passes { + let tex_size = textures[0].size(); + let [src_tex, target_tex] = textures; + let mut fb = renderer.bind(*target_tex)?; + + let adjusted_tex_size = tex_size.downscale(1 << (passes - i)); + let target_tex_size = tex_size + .downscale(1 << (passes - i - 1)) + .to_logical(1, Transform::Normal) + .to_physical(1); + let half_pixel = [ + 0.5 / (adjusted_tex_size.w as f32), + 0.5 / (adjusted_tex_size.h as f32), + ]; + + let mut frame = renderer.render( + &mut fb, + tex_size.to_logical(1, Transform::Normal).to_physical(1), + Transform::Normal, + )?; + frame.clear( + Color32F::new(0., 0., 0., 0.), + &[Rectangle::from_size( + tex_size.to_logical(1, Transform::Normal).to_physical(1), + )], + )?; + frame.with_context(|gl| unsafe { + gl.TexParameteri( + ffi::TEXTURE_2D, + ffi::TEXTURE_WRAP_S, + ffi::CLAMP_TO_EDGE as i32, + ); + gl.TexParameteri( + ffi::TEXTURE_2D, + ffi::TEXTURE_WRAP_T, + ffi::CLAMP_TO_EDGE as i32, + ); + })?; + frame.render_texture_from_to( + src_tex, + Rectangle::from_size(adjusted_tex_size.to_f64()), + Rectangle::from_size(target_tex_size), + &[Rectangle::from_size(target_tex_size)], + &[Rectangle::from_size(target_tex_size)], + Transform::Normal, + 1.0, + Some(&shaders.up), + &[ + Uniform::new("half_pixel", half_pixel), + Uniform::new("offset", (offset / (1 << (passes - i)) as f64) as f32), + ], + )?; + frame.with_context(|gl| unsafe { + gl.TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_WRAP_S, ffi::REPEAT as i32); + gl.TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_WRAP_T, ffi::REPEAT as i32); + })?; + let sync = frame.finish()?; + std::mem::drop(fb); + renderer.wait(&sync)?; + + textures.swap(0, 1); + } + + // textures always end up the right way around with `self.texture` containing our final render, + // since we render PASSES * 2 (downscale and upscale), so the number of swaps is always even. + Ok(()) +} diff --git a/src/backend/render/wayland/clipped_surface.rs b/src/backend/render/wayland/clipped_surface.rs new file mode 100644 index 0000000..8322079 --- /dev/null +++ b/src/backend/render/wayland/clipped_surface.rs @@ -0,0 +1,274 @@ +// Taken and modified from niri, licensed GPL-3. + +use std::borrow::{Borrow, BorrowMut}; + +use glam::{Affine2, Mat3, Vec2}; +use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform}; +use smithay::{ + backend::renderer::{ + ImportAll, Renderer, + element::{ + Element, Id, Kind, RenderElement, UnderlyingStorage, + surface::WaylandSurfaceRenderElement, + }, + gles::{GlesFrame, GlesRenderer, GlesTexProgram, Uniform, UniformValue}, + utils::{CommitCounter, DamageSet, OpaqueRegions}, + }, + utils::user_data::UserDataMap, +}; + +use crate::backend::render::element::AsGlowRenderer; + +pub static CLIPPING_SHADER: &str = include_str!("../shaders/clipped_surface.frag"); +pub struct ClippingShader(pub GlesTexProgram); + +impl ClippingShader { + pub fn get(renderer: &R) -> GlesTexProgram { + Borrow::::borrow(renderer.glow_renderer()) + .egl_context() + .user_data() + .get::() + .expect("Custom Shaders not initialized") + .0 + .clone() + } +} + +#[derive(Debug)] +pub struct ClippedSurfaceRenderElement { + inner: WaylandSurfaceRenderElement, + program: GlesTexProgram, + radius: [u8; 4], + geometry: Rectangle, + uniforms: Vec>, +} + +impl ClippedSurfaceRenderElement +where + R: Renderer + ImportAll, +{ + pub fn new( + renderer: &mut R, + elem: WaylandSurfaceRenderElement, + scale: Scale, + geometry: Rectangle, + radius: [u8; 4], + ) -> Self + where + R: AsGlowRenderer, + { + let elem_geo = elem.geometry(scale); + let geo: Rectangle = geometry.to_physical_precise_round(scale); + let buf_size = elem.buffer_size(); + let view = elem.view(); + + let transform = elem.transform(); + let transform_matrix = Affine2::from_translation(Vec2::new(0.5, 0.5)) + * transform.matrix() + * Affine2::from_translation(-Vec2::new(0.5, 0.5)); + + let geo_scale = { + let Scale { x, y } = elem_geo.size.to_f64() / geo.size.to_f64(); + Affine2::from_scale(Vec2::new(x as f32, y as f32)) + }; + + let geo_translation = { + let offset = (elem_geo.loc - geo.loc).to_f64(); + Affine2::from_translation(Vec2::new( + (offset.x / elem_geo.size.w as f64) as f32, + (offset.y / elem_geo.size.h as f64) as f32, + )) + }; + + let buf_scale = { + let Scale { x, y } = buf_size.to_f64() / view.src.size.to_f64(); + Affine2::from_scale(Vec2::new(x as f32, y as f32)) + }; + + let buf_translation = Affine2::from_translation(Vec2::new( + (view.src.loc.x / buf_size.w as f64) as f32, + (view.src.loc.y / buf_size.h as f64) as f32, + )); + + let input_to_geo = Mat3::from( + transform_matrix * geo_scale * geo_translation * buf_scale * buf_translation, + ); + + let uniforms = vec![ + Uniform::new("geo_size", (geometry.size.w as f32, geometry.size.h as f32)), + Uniform::new( + "corner_radius", + [ + radius[3] as f32, + radius[1] as f32, + radius[0] as f32, + radius[2] as f32, + ], + ), + Uniform::new( + "input_to_geo", + UniformValue::Matrix3x3 { + matrices: vec![*AsRef::<[f32; 9]>::as_ref(&input_to_geo)], + transpose: false, + }, + ), + Uniform::new("noise", UniformValue::_1f(0.0)), + ]; + + Self { + inner: elem, + program: ClippingShader::get(renderer), + radius, + geometry, + uniforms, + } + } + + pub fn will_clip( + elem: &WaylandSurfaceRenderElement, + scale: Scale, + geometry: Rectangle, + radius: [u8; 4], + ) -> bool { + let elem_geo = elem.geometry(scale); + let geo = geometry.to_physical_precise_round(scale); + + let corners = Self::rounded_corners(geometry, radius); + let corners = corners + .into_iter() + .map(|rect| rect.to_physical_precise_up(scale)); + let geo = Rectangle::subtract_rects_many([geo], corners); + !Rectangle::subtract_rects_many([elem_geo], geo).is_empty() + } + + fn rounded_corners( + geo: Rectangle, + radius: [u8; 4], + ) -> [Rectangle; 4] { + let top_left = radius[3] as f64; + let top_right = radius[1] as f64; + let bottom_right = radius[0] as f64; + let bottom_left = radius[2] as f64; + + [ + Rectangle::new(geo.loc, Size::from((top_left, top_left))), + Rectangle::new( + Point::from((geo.loc.x + geo.size.w - top_right, geo.loc.y)), + Size::from((top_right, top_right)), + ), + Rectangle::new( + Point::from(( + geo.loc.x + geo.size.w - bottom_right, + geo.loc.y + geo.size.h - bottom_right, + )), + Size::from((bottom_right, bottom_right)), + ), + Rectangle::new( + Point::from((geo.loc.x, geo.loc.y + geo.size.h - bottom_left)), + Size::from((bottom_left, bottom_left)), + ), + ] + } +} + +impl Element for ClippedSurfaceRenderElement +where + R: Renderer + ImportAll + AsGlowRenderer, + R::TextureId: 'static, +{ + fn id(&self) -> &Id { + self.inner.id() + } + + fn current_commit(&self) -> CommitCounter { + self.inner.current_commit() + } + + fn geometry(&self, scale: Scale) -> Rectangle { + self.inner.geometry(scale) + } + + fn src(&self) -> Rectangle { + self.inner.src() + } + + fn transform(&self) -> Transform { + self.inner.transform() + } + + fn damage_since( + &self, + scale: Scale, + commit: Option, + ) -> DamageSet { + // FIXME: radius changes need to cause damage. + let damage = self.inner.damage_since(scale, commit); + + // Intersect with geometry, since we're clipping by it. + let mut geo = self.geometry.to_physical_precise_round(scale); + geo.loc -= self.geometry(scale).loc; + damage + .into_iter() + .filter_map(|rect| rect.intersection(geo)) + .collect() + } + + fn opaque_regions(&self, scale: Scale) -> OpaqueRegions { + let regions = self.inner.opaque_regions(scale); + + // Intersect with geometry, since we're clipping by it. + let mut geo = self.geometry.to_physical_precise_round(scale); + geo.loc -= self.geometry(scale).loc; + let regions = regions + .into_iter() + .filter_map(|rect| rect.intersection(geo)); + + // Subtract the rounded corners. + let corners = Self::rounded_corners(self.geometry, self.radius); + + let elem_loc = self.geometry(scale).loc; + let corners = corners.into_iter().map(|rect| { + let mut rect = rect.to_physical_precise_up(scale); + rect.loc -= elem_loc; + rect + }); + + OpaqueRegions::from_slice(&Rectangle::subtract_rects_many(regions, corners)) + } + + fn alpha(&self) -> f32 { + self.inner.alpha() + } + + fn kind(&self) -> Kind { + self.inner.kind() + } +} + +impl RenderElement for ClippedSurfaceRenderElement +where + R: AsGlowRenderer + Renderer + ImportAll, + R::TextureId: 'static, +{ + fn draw( + &self, + frame: &mut R::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + cache: Option<&UserDataMap>, + ) -> Result<(), R::Error> { + BorrowMut::::borrow_mut(::glow_frame_mut(frame)) + .override_default_tex_program(self.program.clone(), self.uniforms.clone()); + self.inner + .draw(frame, src, dst, damage, opaque_regions, cache)?; + BorrowMut::::borrow_mut(::glow_frame_mut(frame)) + .clear_tex_program_override(); + Ok(()) + } + + fn underlying_storage(&self, _renderer: &mut R) -> Option> { + None + } +} diff --git a/src/backend/render/wayland/mod.rs b/src/backend/render/wayland/mod.rs new file mode 100644 index 0000000..c2b7d4e --- /dev/null +++ b/src/backend/render/wayland/mod.rs @@ -0,0 +1,145 @@ +use smithay::{ + backend::renderer::{ + ImportAll, Renderer, + element::surface::{KindEvaluation, WaylandSurfaceRenderElement}, + utils::RendererSurfaceStateUserData, + }, + reexports::wayland_server::protocol::wl_surface, + render_elements, + utils::{Logical, Physical, Point, Rectangle, Scale}, + wayland::compositor::{self, TraversalAction}, +}; +use tracing::warn; + +use crate::backend::render::{ + element::AsGlowRenderer, + wayland::{blur_effect::BlurElement, clipped_surface::ClippedSurfaceRenderElement}, +}; + +pub mod blur_effect; +pub mod clipped_surface; + +render_elements! { + pub SurfaceRenderElement where R: AsGlowRenderer + ImportAll, R::TextureId: Send; + Blur=BlurElement, + Clipped=ClippedSurfaceRenderElement, + Wayland=WaylandSurfaceRenderElement, +} + +pub fn push_render_elements_from_surface_tree( + renderer: &mut R, + main_surface: &wl_surface::WlSurface, + location: impl Into>, + geometry: impl Into>, + scale: impl Into>, + alpha: f32, + should_clip: bool, + radii: [u8; 4], + blur_geometry: impl Into>>, + blur_strength: usize, + kind: impl Into, + push_above: &mut dyn FnMut(SurfaceRenderElement), + mut push_below: Option<&mut dyn FnMut(SurfaceRenderElement)>, +) where + R: Renderer + ImportAll + AsGlowRenderer, + R::TextureId: Clone + 'static, +{ + let location = location.into().to_f64(); + let geometry = geometry.into(); + let blur_geometry = blur_geometry.into(); + let scale = scale.into(); + let kind = kind.into(); + let mut passed_main = false; + + compositor::with_surface_tree_downward( + main_surface, + location, + |_, states, location| { + let mut location = *location; + let data = states.data_map.get::(); + + if let Some(data) = data { + if let Some(view) = data.lock().unwrap().view() { + location += view.offset.to_f64().to_physical(scale); + TraversalAction::DoChildren(location) + } else { + TraversalAction::SkipChildren + } + } else { + TraversalAction::SkipChildren + } + }, + |surface, states, location| { + let mut location = *location; + let kind = kind.eval(states); + let data = states.data_map.get::(); + let mut blur = Ok(None); + + if let Some(data) = data { + let has_view = if let Some(view) = data.lock().unwrap().view() { + location += view.offset.to_f64().to_physical(scale); + + true + } else { + false + }; + + if has_view { + match WaylandSurfaceRenderElement::from_surface( + renderer, surface, states, location, alpha, kind, + ) { + Ok(Some(surface)) => { + let blur_geo = blur_geometry.unwrap_or(geometry); + blur = BlurElement::from_surface( + renderer, + states, + blur_geo, + scale.x, + radii, + blur_strength, + ); + let elem: SurfaceRenderElement = if radii.iter().any(|r| *r != 0) + && should_clip + && ClippedSurfaceRenderElement::will_clip( + &surface, scale, geometry, radii, + ) { + ClippedSurfaceRenderElement::new( + renderer, surface, scale, geometry, radii, + ) + .into() + } else { + surface.into() + }; + if let Some(push_below) = push_below.as_mut() + && passed_main + { + push_below(elem); + } else { + push_above(elem); + } + } + Ok(None) => {} // surface is not mapped + Err(err) => { + warn!("Failed to import surface: {}", err); + } + }; + } + } + + if surface == main_surface { + passed_main = true; + } + + if let Ok(Some(elem)) = blur { + if let Some(push_below) = push_below.as_mut() + && passed_main + { + push_below(elem.into()); + } else { + push_above(elem.into()); + } + } + }, + |_, _, _| true, + ); +} diff --git a/src/backend/winit.rs b/src/backend/winit.rs new file mode 100644 index 0000000..0ff249a --- /dev/null +++ b/src/backend/winit.rs @@ -0,0 +1,349 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{ + backend::render, + config::ScreenFilter, + shell::{Devices, SeatExt}, + state::{BackendData, Common}, + utils::prelude::*, +}; +use anyhow::{Context, Result, anyhow}; +use cosmic_comp_config::output::comp::{OutputConfig, TransformDef}; +use smithay::{ + backend::{ + drm::NodeType, + egl::EGLDevice, + renderer::{ + ImportDma, + damage::{OutputDamageTracker, RenderOutputResult}, + glow::GlowRenderer, + }, + winit::{self, WinitEvent, WinitGraphicsBackend, WinitVirtualDevice}, + }, + desktop::layer_map_for_output, + output::{Mode, Output, PhysicalProperties, Scale, Subpixel}, + reexports::{ + calloop::{EventLoop, ping}, + wayland_protocols::wp::presentation_time::server::wp_presentation_feedback, + wayland_server::DisplayHandle, + winit::event_loop::pump_events::PumpStatus, + }, + utils::Transform, + wayland::{dmabuf::DmabufFeedbackBuilder, presentation::Refresh}, +}; +use std::{borrow::BorrowMut, cell::RefCell, time::Duration}; +use tracing::{error, info, warn}; + +use super::render::{CursorMode, ScreenFilterStorage, init_shaders}; + +#[derive(Debug)] +pub struct WinitState { + // The winit backend currently has no notion of multiple windows + pub backend: WinitGraphicsBackend, + output: Output, + damage_tracker: OutputDamageTracker, + screen_filter_state: ScreenFilterStorage, +} + +impl WinitState { + #[profiling::function] + pub fn render_output(&mut self, state: &mut Common) -> Result<()> { + let age = self.backend.buffer_age().unwrap_or(0); + let (renderer, mut fb) = self + .backend + .bind() + .with_context(|| "Failed to bind buffer")?; + match render::render_output( + None, + renderer, + &mut fb, + &mut self.damage_tracker, + age, + &state.shell, + state.clock.now(), + &self.output, + CursorMode::NotDefault, + &mut self.screen_filter_state, + &state.event_loop_handle, + ) { + Ok(RenderOutputResult { damage, states, .. }) => { + std::mem::drop(fb); + self.backend + .submit(damage.map(|x| x.as_slice())) + .with_context(|| "Failed to submit buffer for display")?; + state.send_frames(&self.output, None); + state.update_primary_output(&self.output, &states); + state.send_dmabuf_feedback(&self.output, &states, |_| None); + if damage.is_some() { + let mut output_presentation_feedback = state + .shell + .read() + .take_presentation_feedback(&self.output, &states); + output_presentation_feedback.presented( + state.clock.now(), + self.output + .current_mode() + .map(|mode| { + Refresh::Fixed(Duration::from_secs_f64( + 1_000.0 / mode.refresh as f64, + )) + }) + .unwrap_or(Refresh::Unknown), + 0, + wp_presentation_feedback::Kind::Vsync, + ); + } + } + Err(err) => { + anyhow::bail!("Rendering failed: {}", err); + } + }; + + Ok(()) + } + + pub fn all_outputs(&self) -> Vec { + vec![self.output.clone()] + } + + pub fn apply_config_for_outputs(&mut self, test_only: bool) -> Result<(), anyhow::Error> { + // TODO: if we ever have multiple winit outputs, don't ignore config.enabled + // reset size + let size = self.backend.window_size(); + let mut config = self + .output + .user_data() + .get::>() + .unwrap() + .borrow_mut(); + if config.mode.0 != (size.w, size.h) { + if !test_only { + config.mode = ((size.w, size.h), None); + } + Err(anyhow::anyhow!("Cannot set window size")) + } else { + Ok(()) + } + } + + pub fn update_screen_filter(&mut self, screen_filter: &ScreenFilter) -> Result<()> { + self.screen_filter_state.filter = screen_filter.clone(); + Ok(()) + } +} + +pub fn init_backend( + dh: &DisplayHandle, + event_loop: &mut EventLoop, + state: &mut State, +) -> Result<()> { + let (mut backend, mut input): (WinitGraphicsBackend, _) = + winit::init().map_err(|e| anyhow!("Failed to initilize winit backend: {e:?}"))?; + init_shaders(backend.renderer().borrow_mut()).context("Failed to initialize renderer")?; + + init_egl_client_side(dh, state, &mut backend)?; + + let name = "WINIT-0".to_string(); + let size = backend.window_size(); + let props = PhysicalProperties { + size: (0, 0).into(), + subpixel: Subpixel::Unknown, + make: "COSMIC".to_string(), + model: name.clone(), + serial_number: "Unknown".to_string(), + }; + let mode = Mode { + size: (size.w, size.h).into(), + refresh: 60_000, + }; + let output = Output::new(name, props); + output.add_mode(mode); + output.set_preferred(mode); + output.change_current_state( + Some(mode), + Some(Transform::Flipped180), + Some(Scale::Integer(1)), + Some((0, 0).into()), + ); + output.user_data().insert_if_missing(|| { + RefCell::new(OutputConfig { + mode: ((size.w, size.h), None), + transform: TransformDef::Flipped180, + ..Default::default() + }) + }); + + let (event_ping, event_source) = + ping::make_ping().with_context(|| "Failed to init eventloop timer for winit")?; + let (render_ping, render_source) = + ping::make_ping().with_context(|| "Failed to init eventloop timer for winit")?; + let event_ping_handle = event_ping.clone(); + let render_ping_handle = render_ping.clone(); + let mut token = Some( + event_loop + .handle() + .insert_source(render_source, move |_, _, state| { + if let Err(err) = state.backend.winit().render_output(&mut state.common) { + error!(?err, "Failed to render frame."); + render_ping.ping(); + } + profiling::finish_frame!(); + }) + .map_err(|_| anyhow::anyhow!("Failed to init eventloop timer for winit"))?, + ); + let event_loop_handle = event_loop.handle(); + event_loop + .handle() + .insert_source(event_source, move |_, _, state| { + match input + .dispatch_new_events(|event| state.process_winit_event(event, &render_ping_handle)) + { + PumpStatus::Continue => { + event_ping_handle.ping(); + render_ping_handle.ping(); + } + PumpStatus::Exit(_) => { + let output = state.backend.winit().output.clone(); + state.common.remove_output(&output); + if let Some(token) = token.take() { + event_loop_handle.remove(token); + } + } + }; + }) + .map_err(|_| anyhow::anyhow!("Failed to init eventloop timer for winit"))?; + event_ping.ping(); + + state.backend = BackendData::Winit(WinitState { + backend, + output: output.clone(), + damage_tracker: OutputDamageTracker::from_output(&output), + screen_filter_state: ScreenFilterStorage::default(), + }); + + state + .common + .output_configuration_state + .add_heads(std::iter::once(&output)); + { + state.common.add_output(&output); + if let Err(err) = state.common.config.read_outputs( + &mut state.common.output_configuration_state, + &mut state.backend, + &state.common.shell, + &state.common.event_loop_handle, + &mut state.common.workspace_state.update(), + &state.common.xdg_activation_state, + state.common.startup_done.clone(), + &state.common.clock, + ) { + error!("Unrecoverable output config error: {}", err); + } + state.common.refresh(); + } + + if state.common.with_xwayland { + state.launch_xwayland(None); + } else { + state.notify_ready(); + } + + Ok(()) +} + +fn init_egl_client_side( + dh: &DisplayHandle, + state: &mut State, + renderer: &mut WinitGraphicsBackend, +) -> Result<()> { + let render_node = EGLDevice::device_for_display(renderer.renderer().egl_context().display()) + .and_then(|device| device.try_get_render_node()); + + let dmabuf_formats = renderer.renderer().dmabuf_formats(); + + match render_node { + Ok(Some(node)) => { + let feedback = DmabufFeedbackBuilder::new(node.dev_id(), dmabuf_formats.clone()) + .build() + .unwrap(); + + let dmabuf_global = state + .common + .dmabuf_state + .create_global_with_default_feedback::(dh, &feedback); + + let render_node = render_node.unwrap().unwrap(); + let _drm_global_id = state.common.wl_drm_state.create_global::( + dh, + render_node + .dev_path_with_type(NodeType::Render) + .or_else(|| render_node.dev_path()) + .ok_or(anyhow!( + "Could not determine path for gpu node: {}", + render_node + ))?, + dmabuf_formats, + &dmabuf_global, + ); + + info!("EGL hardware-acceleration enabled."); + } + Ok(None) => { + warn!("Failed to query render node. Unable to initialize bind display to EGL.") + } + Err(err) => { + warn!( + ?err, + "Failed to egl device for display. Unable to initialize bind display to EGL." + ) + } + } + + Ok(()) +} + +impl State { + pub fn process_winit_event(&mut self, event: WinitEvent, render_ping: &ping::Ping) { + // here we can handle special cases for winit inputs + match event { + WinitEvent::Focus(true) => { + for seat in self.common.shell.read().seats.iter() { + let devices = seat.user_data().get::().unwrap(); + if devices.has_device(&WinitVirtualDevice) { + seat.set_active_output(&self.backend.winit().output); + break; + } + } + } + WinitEvent::Resized { size, .. } => { + let winit_state = self.backend.winit(); + let output = &winit_state.output; + let mode = Mode { + size, + refresh: 60_000, + }; + + { + let mut config = output + .user_data() + .get::>() + .unwrap() + .borrow_mut(); + config.mode.0 = size.into(); + } + output.delete_mode(output.current_mode().unwrap()); + output.set_preferred(mode); + output.change_current_state(Some(mode), None, None, None); + layer_map_for_output(output).arrange(); + self.common.output_configuration_state.update(); + render_ping.ping(); + } + WinitEvent::Redraw => render_ping.ping(), + WinitEvent::Input(event) => self.process_input_event(event), + WinitEvent::CloseRequested => { + self.common.should_stop = true; + } + _ => {} + }; + } +} diff --git a/src/backend/x11.rs b/src/backend/x11.rs new file mode 100644 index 0000000..bd0e1b3 --- /dev/null +++ b/src/backend/x11.rs @@ -0,0 +1,549 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{ + backend::render, + config::ScreenFilter, + shell::{Devices, SeatExt}, + state::{BackendData, Common}, + utils::prelude::*, +}; +use anyhow::{Context, Result, anyhow}; +use cosmic_comp_config::output::comp::OutputConfig; +use smithay::{ + backend::{ + allocator::{ + dmabuf::{Dmabuf, DmabufAllocator}, + gbm::{GbmAllocator, GbmBufferFlags}, + vulkan::{ImageUsageFlags, VulkanAllocator}, + }, + drm::{DrmDeviceFd, DrmNode, NodeType}, + egl::{EGLContext, EGLDevice, EGLDisplay}, + input::{Event, InputEvent}, + renderer::{ + Bind, ImportDma, + damage::{OutputDamageTracker, RenderOutputResult}, + glow::GlowRenderer, + }, + vulkan::{Instance, PhysicalDevice, version::Version}, + x11::{Window, WindowBuilder, X11Backend, X11Event, X11Handle, X11Input, X11Surface}, + }, + desktop::layer_map_for_output, + output::{Mode, Output, PhysicalProperties, Scale, Subpixel}, + reexports::{ + calloop::{EventLoop, LoopHandle, ping}, + gbm::Device as GbmDevice, + wayland_protocols::wp::presentation_time::server::wp_presentation_feedback, + wayland_server::DisplayHandle, + }, + utils::{DeviceFd, Transform}, + wayland::{dmabuf::DmabufFeedbackBuilder, presentation::Refresh}, +}; +use std::{borrow::BorrowMut, cell::RefCell, os::unix::io::OwnedFd, time::Duration}; +use tracing::{debug, error, info, warn}; + +use super::render::{ScreenFilterStorage, init_shaders}; + +#[derive(Debug)] +#[allow(clippy::large_enum_variant)] +enum Allocator { + Gbm(GbmAllocator), + Vulkan(PhysicalDevice), +} + +#[derive(Debug)] +pub struct X11State { + allocator: Allocator, + _egl: EGLDisplay, + pub renderer: GlowRenderer, + surfaces: Vec, + handle: X11Handle, +} + +impl X11State { + pub fn add_window(&mut self, handle: LoopHandle<'_, State>) -> Result { + let window = WindowBuilder::new() + .title("COSMIC") + .build(&self.handle) + .with_context(|| "Failed to create window")?; + let fourcc = window.format(); + let modifiers = Bind::::supported_formats(&self.renderer).unwrap(); + let filtered_modifiers = modifiers + .iter() + .filter(|format| format.code == fourcc) + .map(|format| format.modifier); + let surface = match &self.allocator { + Allocator::Gbm(gbm) => self + .handle + .create_surface(&window, DmabufAllocator(gbm.clone()), filtered_modifiers) + .with_context(|| "Failed to create surface")?, + Allocator::Vulkan(vulkan) => self + .handle + .create_surface( + &window, + DmabufAllocator( + VulkanAllocator::new(vulkan, ImageUsageFlags::COLOR_ATTACHMENT) + .with_context(|| "Failed to create vulkan allocator for window")?, + ), + filtered_modifiers, + ) + .with_context(|| "Failed to create surface")?, + }; + + let name = format!("X11-{}", self.surfaces.len()); + let size = window.size(); + let props = PhysicalProperties { + size: (0, 0).into(), + subpixel: Subpixel::Unknown, + make: "COSMIC".to_string(), + model: name.clone(), + serial_number: "Unknown".to_string(), + }; + let mode = Mode { + size: (size.w as i32, size.h as i32).into(), + refresh: 60_000, + }; + let output = Output::new(name, props); + output.add_mode(mode); + output.set_preferred(mode); + output.change_current_state( + Some(mode), + Some(Transform::Normal), + Some(Scale::Integer(1)), + Some((0, 0).into()), + ); + output.user_data().insert_if_missing(|| { + RefCell::new(OutputConfig { + mode: ((size.w as i32, size.h as i32), None), + ..Default::default() + }) + }); + + let output_ref = output.clone(); + let (ping, source) = + ping::make_ping().with_context(|| "Failed to create output event loop source")?; + let _token = handle + .insert_source(source, move |_, _, state| { + let x11_state = state.backend.x11(); + if let Some(surface) = x11_state + .surfaces + .iter_mut() + .find(|s| s.output == output_ref) + { + if let Err(err) = + surface.render_output(&mut x11_state.renderer, &mut state.common) + { + error!(?err, "Error rendering."); + } + surface.dirty = false; + surface.pending = true; + } + }) + .with_context(|| "Failed to add output to event loop")?; + + self.surfaces.push(Surface { + window, + surface, + damage_tracker: OutputDamageTracker::from_output(&output), + output: output.clone(), + render: ping.clone(), + dirty: false, + pending: true, + screen_filter_state: ScreenFilterStorage::default(), + }); + + // schedule first render + ping.ping(); + Ok(output) + } + + pub fn schedule_render(&mut self, output: &Output) { + if let Some(surface) = self.surfaces.iter_mut().find(|s| s.output == *output) { + surface.dirty = true; + if !surface.pending { + surface.render.ping(); + } + } + } + + pub fn all_outputs(&self) -> Vec { + self.surfaces.iter().map(|s| s.output.clone()).collect() + } + + pub fn apply_config_for_outputs(&mut self, test_only: bool) -> Result<(), anyhow::Error> { + // TODO: if we ever have multiple winit outputs, don't juse use the first and don't ignore OutputState + + let surface = self.surfaces.first().unwrap(); + let size = surface.window.size(); + let mut config = surface + .output + .user_data() + .get::>() + .unwrap() + .borrow_mut(); + + // reset size + if config.mode.0 != (size.w as i32, size.h as i32) { + if !test_only { + config.mode = ((size.w as i32, size.h as i32), None); + } + Err(anyhow::anyhow!("Cannot set window size")) + } else { + Ok(()) + } + } + + pub fn update_screen_filter(&mut self, screen_filter: &ScreenFilter) -> Result<()> { + for surface in &mut self.surfaces { + surface.screen_filter_state.filter = screen_filter.clone(); + } + Ok(()) + } +} + +#[derive(Debug)] +pub struct Surface { + window: Window, + damage_tracker: OutputDamageTracker, + surface: X11Surface, + output: Output, + render: ping::Ping, + dirty: bool, + pending: bool, + screen_filter_state: ScreenFilterStorage, +} + +impl Surface { + pub fn render_output(&mut self, renderer: &mut GlowRenderer, state: &mut Common) -> Result<()> { + let (mut buffer, age) = self + .surface + .buffer() + .with_context(|| "Failed to allocate buffer")?; + let mut fb = renderer + .bind(&mut buffer) + .with_context(|| "Failed to bind dmabuf")?; + match render::render_output( + None, + renderer, + &mut fb, + &mut self.damage_tracker, + age as usize, + &state.shell, + state.clock.now(), + &self.output, + render::CursorMode::NotDefault, + &mut self.screen_filter_state, + &state.event_loop_handle, + ) { + Ok(RenderOutputResult { damage, states, .. }) => { + self.surface + .submit() + .with_context(|| "Failed to submit buffer for display")?; + state.send_frames(&self.output, None); + state.update_primary_output(&self.output, &states); + state.send_dmabuf_feedback(&self.output, &states, |_| None); + if damage.is_some() { + let mut output_presentation_feedback = state + .shell + .read() + .take_presentation_feedback(&self.output, &states); + output_presentation_feedback.presented( + state.clock.now(), + self.output + .current_mode() + .map(|mode| { + Refresh::Fixed(Duration::from_secs_f64( + 1_000.0 / mode.refresh as f64, + )) + }) + .unwrap_or(Refresh::Unknown), + 0, + wp_presentation_feedback::Kind::Vsync, + ) + } + } + Err(err) => { + self.surface.reset_buffers(); + anyhow::bail!("Rendering failed: {}", err); + } + }; + + Ok(()) + } +} + +fn try_vulkan_allocator(node: &DrmNode) -> Option { + let instance = match Instance::new(Version::VERSION_1_2, None) { + Ok(instance) => instance, + Err(err) => { + warn!( + ?err, + "Failed to instanciate vulkan, falling back to gbm allocator.", + ); + return None; + } + }; + + let devices = match PhysicalDevice::enumerate(&instance) { + Ok(devices) => devices, + Err(err) => { + debug!(?err, "No vulkan devices, falling back to gbm."); + return None; + } + }; + + let Some(device) = devices + .filter(|phd| { + phd.has_device_extension(smithay::reexports::ash::ext::physical_device_drm::NAME) + }) + .find(|phd| { + phd.primary_node().unwrap() == Some(*node) || phd.render_node().unwrap() == Some(*node) + }) + else { + debug!(?node, "No vulkan device for node, falling back to gbm."); + return None; + }; + + Some(Allocator::Vulkan(device)) +} + +fn try_gbm_allocator(fd: OwnedFd) -> Option { + // Create the gbm device for buffer allocation. + let device = match GbmDevice::new(DrmDeviceFd::new(DeviceFd::from(fd))) { + Ok(gbm) => gbm, + Err(err) => { + error!(?err, "Failed to create GBM device."); + return None; + } + }; + + Some(Allocator::Gbm(GbmAllocator::new( + device, + GbmBufferFlags::RENDERING, + ))) +} + +pub fn init_backend( + dh: &DisplayHandle, + event_loop: &mut EventLoop, + state: &mut State, +) -> Result<()> { + let backend = X11Backend::new().with_context(|| "Failed to initilize X11 backend")?; + let handle = backend.handle(); + + // Obtain the DRM node the X server uses for direct rendering. + let (drm_node, fd) = handle + .drm_node() + .with_context(|| "Could not get DRM node used by X server")?; + + let device = EGLDevice::enumerate() + .with_context(|| "Failed to enumerate EGL devices")? + .find(|device| device.try_get_render_node().ok().flatten() == Some(drm_node)) + .with_context(|| format!("Failed to find EGLDevice for node {}", drm_node))?; + // Initialize EGL + let egl = unsafe { EGLDisplay::new(device) }.with_context(|| "Failed to create EGL display")?; + // Create the OpenGL context + let context = EGLContext::new(&egl).with_context(|| "Failed to create EGL context")?; + // Create a renderer + let mut renderer = + unsafe { GlowRenderer::new(context) }.with_context(|| "Failed to initialize renderer")?; + + init_shaders(renderer.borrow_mut()).context("Failed to initialize renderer")?; + init_egl_client_side(dh, state, drm_node, &mut renderer)?; + + state.backend = BackendData::X11(X11State { + handle, + allocator: try_vulkan_allocator(&drm_node) + .or_else(|| try_gbm_allocator(fd)) + .context("Failed to create allocator for x11")?, + _egl: egl, + renderer, + surfaces: Vec::new(), + }); + + let output = state + .backend + .x11() + .add_window(event_loop.handle()) + .with_context(|| "Failed to create wl_output")?; + state + .common + .output_configuration_state + .add_heads(std::iter::once(&output)); + { + state.common.add_output(&output); + if let Err(err) = state.common.config.read_outputs( + &mut state.common.output_configuration_state, + &mut state.backend, + &state.common.shell, + &state.common.event_loop_handle, + &mut state.common.workspace_state.update(), + &state.common.xdg_activation_state, + state.common.startup_done.clone(), + &state.common.clock, + ) { + error!("Unrecoverable output configuration error: {}", err); + } + state.common.refresh(); + } + + if state.common.with_xwayland { + state.launch_xwayland(None); + } else { + state.notify_ready(); + } + + event_loop + .handle() + .insert_source(backend, move |event, _, state| match event { + X11Event::CloseRequested { window_id } => { + // TODO: drain_filter + let mut outputs_removed = Vec::new(); + for surface in state + .backend + .x11() + .surfaces + .iter() + .filter(|s| s.window.id() == window_id) + { + surface.window.unmap(); + outputs_removed.push(surface.output.clone()); + } + state + .backend + .x11() + .surfaces + .retain(|s| s.window.id() != window_id); + for output in outputs_removed.into_iter() { + state.common.remove_output(&output); + } + } + X11Event::Resized { + new_size, + window_id, + } => { + let size = { (new_size.w as i32, new_size.h as i32).into() }; + let mode = Mode { + size, + refresh: 60_000, + }; + if let Some(surface) = state + .backend + .x11() + .surfaces + .iter_mut() + .find(|s| s.window.id() == window_id) + { + let output = &surface.output; + { + let mut config = output + .user_data() + .get::>() + .unwrap() + .borrow_mut(); + config.mode.0 = size.into(); + } + + output.delete_mode(output.current_mode().unwrap()); + output.change_current_state(Some(mode), None, None, None); + output.set_preferred(mode); + layer_map_for_output(output).arrange(); + state.common.output_configuration_state.update(); + surface.dirty = true; + if !surface.pending { + surface.render.ping(); + } + } + } + X11Event::Refresh { window_id } | X11Event::PresentCompleted { window_id } => { + if let Some(surface) = state + .backend + .x11() + .surfaces + .iter_mut() + .find(|s| s.window.id() == window_id) + { + if surface.dirty { + surface.render.ping(); + } else { + surface.pending = false; + } + } + } + X11Event::Input { + event, + window_id: _, + } => state.process_x11_event(event), + X11Event::Focus { .. } => {} // TODO: release all keys when losing focus and make sure to go through our keyboard filter code + }) + .map_err(|_| anyhow::anyhow!("Failed to insert X11 Backend into event loop"))?; + + Ok(()) +} + +fn init_egl_client_side( + dh: &DisplayHandle, + state: &mut State, + render_node: DrmNode, + renderer: &mut R, +) -> Result<()> +where + R: ImportDma, +{ + let default_feedback = + DmabufFeedbackBuilder::new(render_node.dev_id(), renderer.dmabuf_formats()) + .build() + .unwrap(); + let dmabuf_global = state + .common + .dmabuf_state + .create_global_with_default_feedback::(dh, &default_feedback); + let _drm_global_id = state.common.wl_drm_state.create_global::( + dh, + render_node + .dev_path_with_type(NodeType::Render) + .or_else(|| render_node.dev_path()) + .ok_or(anyhow!( + "Could not determine path for gpu node: {}", + render_node + ))?, + renderer.dmabuf_formats(), + &dmabuf_global, + ); + + info!("EGL hardware-acceleration enabled."); + + Ok(()) +} + +impl State { + pub fn process_x11_event(&mut self, event: InputEvent) { + // here we can handle special cases for x11 inputs, like mapping them to windows + if let InputEvent::PointerMotionAbsolute { event } = &event + && let Some(window) = event.window() + { + let output = self + .backend + .x11() + .surfaces + .iter() + .find(|surface| &surface.window == window.as_ref()) + .map(|surface| surface.output.clone()) + .unwrap(); + + let device = event.device(); + for seat in self.common.shell.read().seats.iter() { + let devices = seat.user_data().get::().unwrap(); + if devices.has_device(&device) { + seat.set_active_output(&output); + break; + } + } + }; + + self.process_input_event(event); + // TODO actually figure out the output + for output in self.common.shell.read().outputs() { + self.backend.x11().schedule_render(output); + } + } +} diff --git a/src/config/input_config.rs b/src/config/input_config.rs new file mode 100644 index 0000000..ae74da6 --- /dev/null +++ b/src/config/input_config.rs @@ -0,0 +1,225 @@ +use smithay::reexports::input::{ + Device as InputDevice, DeviceConfigError, DragLockState, ScrollMethod, SendEventsMode, +}; +use tracing::warn; + +use cosmic_comp_config::input::*; + +#[allow(dead_code)] +pub fn for_device(device: &InputDevice) -> InputConfig { + InputConfig { + state: match device.config_send_events_mode() { + x if x.contains(SendEventsMode::ENABLED) => DeviceState::Enabled, + x if x.contains(SendEventsMode::DISABLED_ON_EXTERNAL_MOUSE) => { + DeviceState::DisabledOnExternalMouse + } + x if x.contains(SendEventsMode::DISABLED) => DeviceState::Disabled, + _ => DeviceState::Disabled, + }, + acceleration: if device.config_accel_is_available() { + Some(AccelConfig { + profile: device.config_accel_profile(), + speed: device.config_accel_speed(), + }) + } else { + None + }, + calibration: device.config_calibration_matrix(), + click_method: device.config_click_method(), + disable_while_typing: if device.config_dwt_is_available() { + Some(device.config_dwt_enabled()) + } else { + None + }, + left_handed: if device.config_left_handed_is_available() { + Some(device.config_left_handed()) + } else { + None + }, + middle_button_emulation: if device.config_middle_emulation_is_available() { + Some(device.config_middle_emulation_enabled()) + } else { + None + }, + rotation_angle: if device.config_rotation_is_available() { + Some(device.config_rotation_angle()) + } else { + None + }, + scroll_config: if device + .config_scroll_methods() + .iter() + .any(|x| *x != ScrollMethod::NoScroll) + { + Some(ScrollConfig { + method: device.config_scroll_method(), + natural_scroll: if device.config_scroll_has_natural_scroll() { + Some(device.config_scroll_natural_scroll_enabled()) + } else { + None + }, + scroll_button: if device.config_scroll_method() == Some(ScrollMethod::OnButtonDown) + { + Some(device.config_scroll_button()) + } else { + None + }, + scroll_factor: None, + }) + } else { + None + }, + tap_config: if device.config_tap_finger_count() > 0 { + Some(TapConfig { + enabled: device.config_tap_enabled(), + button_map: device.config_tap_button_map(), + drag: device.config_tap_drag_enabled(), + drag_lock: device.config_tap_drag_lock_enabled(), + }) + } else { + None + }, + map_to_output: None, + } +} + +// Get setting from `device_config` if present, then `default_config` +// Returns `is_default` to indicate this is a default value. +pub fn get_config<'a, T: 'a, F: Fn(&'a InputConfig) -> Option>( + device_config: Option<&'a InputConfig>, + default_config: &'a InputConfig, + f: F, +) -> Option<(T, bool)> { + if let Some(setting) = device_config.and_then(&f) { + Some((setting, false)) + } else { + f(default_config).map(|setting| (setting, true)) + } +} + +fn config_set_error( + device: &InputDevice, + setting: &str, + value: T, + err: DeviceConfigError, + is_default: bool, +) { + if !(is_default && err == DeviceConfigError::Unsupported) { + warn!( + ?err, + "Failed to apply {} {:?} for device {:?}.", + setting, + value, + device.name(), + ); + } +} + +pub fn update_device( + device: &mut InputDevice, + device_config: Option<&InputConfig>, + default_config: &InputConfig, +) { + macro_rules! config { + ($f:expr) => { + get_config(device_config, default_config, $f) + }; + } + + let state = device_config.unwrap_or(default_config).state; + if let Err(err) = match state { + DeviceState::Enabled => device.config_send_events_set_mode(SendEventsMode::ENABLED), + DeviceState::Disabled => device.config_send_events_set_mode(SendEventsMode::DISABLED), + DeviceState::DisabledOnExternalMouse => { + device.config_send_events_set_mode(SendEventsMode::DISABLED_ON_EXTERNAL_MOUSE) + } + } { + warn!( + ?err, + "Failed to apply mode {:?} for device {:?}.", + state, + device.name(), + ); + } + if let Some((accel, is_default)) = config!(|x| x.acceleration.as_ref()) { + if let Some(profile) = accel.profile + && let Err(err) = device.config_accel_set_profile(profile) + { + config_set_error(device, "acceleration profile", profile, err, is_default); + } + if let Err(err) = device.config_accel_set_speed(accel.speed) { + config_set_error(device, "acceleration speed", accel.speed, err, is_default); + } + } + if let Some((matrix, is_default)) = config!(|x| x.calibration) + && let Err(err) = device.config_calibration_set_matrix(matrix) + { + config_set_error(device, "calibration matrix", matrix, err, is_default); + } + if let Some((method, is_default)) = config!(|x| x.click_method) + && let Err(err) = device.config_click_set_method(method) + { + config_set_error(device, "click method", method, err, is_default); + } + if let Some((dwt, is_default)) = config!(|x| x.disable_while_typing) + && let Err(err) = device.config_dwt_set_enabled(dwt) + { + config_set_error(device, "disable-while-typing", dwt, err, is_default); + } + if let Some((left, is_default)) = config!(|x| x.left_handed) + && let Err(err) = device.config_left_handed_set(left) + { + config_set_error(device, "left-handed", left, err, is_default); + } + if let Some((middle, is_default)) = config!(|x| x.middle_button_emulation) + && let Err(err) = device.config_middle_emulation_set_enabled(middle) + { + config_set_error(device, "middle-button-emulation", middle, err, is_default); + } + if let Some((angle, is_default)) = config!(|x| x.rotation_angle) + && let Err(err) = device.config_rotation_set_angle(angle) + { + config_set_error(device, "rotation-angle", angle, err, is_default); + } + if let Some((scroll, is_default)) = config!(|x| x.scroll_config.as_ref()) { + if let Some(method) = scroll.method + && let Err(err) = device.config_scroll_set_method(method) + { + config_set_error(device, "scroll method", scroll, err, is_default); + } + if let Some(natural) = scroll.natural_scroll + && let Err(err) = device.config_scroll_set_natural_scroll_enabled(natural) + { + config_set_error(device, "natural scrolling", natural, err, is_default); + } + if let Some(button) = scroll.scroll_button + && let Err(err) = device.config_scroll_set_button(button) + { + config_set_error(device, "scroll button", button, err, is_default); + } + } + if let Some((tap, is_default)) = config!(|x| x.tap_config.as_ref()) { + if let Err(err) = device.config_tap_set_enabled(tap.enabled) { + config_set_error(device, "tap-to-click", tap.enabled, err, is_default); + } + if let Some(button_map) = tap.button_map + && let Err(err) = device.config_tap_set_button_map(button_map) + { + config_set_error(device, "button map", button_map, err, is_default); + } + if let Err(err) = device.config_tap_set_drag_enabled(tap.drag) { + config_set_error(device, "tap-drag", tap.drag, err, is_default); + } + // TODO: Use `DragLockState::EnabledStick` by default on libinput 1.27.0+, but + // make it configurable: + // https://lore.freedesktop.org/wayland-devel/20241119043937.GA2118681@quokka + let drag_lock_state = if tap.drag_lock { + DragLockState::EnabledTimeout + } else { + DragLockState::Disabled + }; + if let Err(err) = device.config_tap_set_drag_lock_enabled(drag_lock_state) { + config_set_error(device, "tap-drag-lock", tap.drag_lock, err, is_default); + } + } +} diff --git a/src/config/key_bindings.rs b/src/config/key_bindings.rs new file mode 100644 index 0000000..006f1d3 --- /dev/null +++ b/src/config/key_bindings.rs @@ -0,0 +1,56 @@ +use cosmic_settings_config::shortcuts::State as KeyState; +use cosmic_settings_config::shortcuts::{self, Modifiers}; +use smithay::input::keyboard::ModifiersState; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Action { + /// Behaviors managed internally by cosmic-comp. + Private(PrivateAction), + /// Behaviors managed via cosmic-settings. + Shortcut(shortcuts::Action), +} + +#[derive(Clone, Debug, Eq, PartialEq)] +// Behaviors which are internally defined and emitted. +pub enum PrivateAction { + Escape, + Resizing( + shortcuts::action::ResizeDirection, + shortcuts::action::ResizeEdge, + shortcuts::State, + ), +} + +/// Convert `cosmic_settings_config::shortcuts::State` to `smithay::backend::input::KeyState`. +pub fn cosmic_keystate_to_smithay(value: KeyState) -> smithay::backend::input::KeyState { + match value { + KeyState::Pressed => smithay::backend::input::KeyState::Pressed, + KeyState::Released => smithay::backend::input::KeyState::Released, + } +} + +/// Convert `smithay::backend::input::KeyState` to `cosmic_settings_config::shortcuts::State`. +pub fn cosmic_keystate_from_smithay(value: smithay::backend::input::KeyState) -> KeyState { + match value { + smithay::backend::input::KeyState::Pressed => KeyState::Pressed, + smithay::backend::input::KeyState::Released => KeyState::Released, + } +} + +/// Compare `cosmic_settings_config::shortcuts::Modifiers` to `smithay::input::keyboard::ModifiersState`. +pub fn cosmic_modifiers_eq_smithay(this: &Modifiers, other: &ModifiersState) -> bool { + this.ctrl == other.ctrl + && this.alt == other.alt + && this.shift == other.shift + && this.logo == other.logo +} + +/// Convert `smithay::input::keyboard::ModifiersState` to `cosmic_settings_config::shortcuts::Modifiers` +pub fn cosmic_modifiers_from_smithay(value: ModifiersState) -> Modifiers { + Modifiers { + ctrl: value.ctrl, + alt: value.alt, + shift: value.shift, + logo: value.logo, + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..1d9b9c9 --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,992 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{ + shell::Shell, + state::{BackendData, State}, + utils::prelude::OutputExt, + wayland::protocols::{ + output_configuration::OutputConfigurationState, workspace::WorkspaceUpdateGuard, + }, +}; +use anyhow::Context; +use cosmic_config::{ConfigGet, CosmicConfigEntry}; +use cosmic_settings_config::window_rules::ApplicationException; +use cosmic_settings_config::{Shortcuts, shortcuts, window_rules}; +use serde::{Deserialize, Serialize}; +use smithay::utils::{Clock, Monotonic}; +use smithay::wayland::xdg_activation::XdgActivationState; +pub use smithay::{ + backend::input::{self as smithay_input, KeyState}, + input::keyboard::{Keysym, ModifiersState, keysyms as KeySyms}, + output::{Mode, Output}, + reexports::{ + calloop::LoopHandle, + input::{ + AccelProfile, ClickMethod, Device as InputDevice, ScrollMethod, SendEventsMode, + TapButtonMap, + }, + }, + utils::{Logical, Physical, Point, SERIAL_COUNTER, Size, Transform}, +}; +use std::{ + cell::{Ref, RefCell}, + collections::{BTreeMap, HashMap}, + fs::OpenOptions, + io::Write, + path::PathBuf, + sync::{Arc, atomic::AtomicBool}, +}; +use tracing::{error, warn}; + +mod input_config; +pub mod key_bindings; +mod types; + +use cosmic::config::CosmicTk; +pub use cosmic_comp_config::EdidProduct; +use cosmic_comp_config::{ + ActivationPolicy, AppearanceConfig, CosmicCompConfig, KeyboardConfig, TileBehavior, XkbConfig, + XwaylandDescaling, XwaylandEavesdropping, ZoomConfig, + input::{DeviceState as InputDeviceState, InputConfig, TouchpadOverride}, + output::comp::{ + OutputConfig, OutputInfo, OutputState, OutputsConfig, TransformDef, load_outputs, + }, + workspace::WorkspaceConfig, +}; +pub use key_bindings::{Action, PrivateAction}; +use types::WlXkbConfig; + +#[derive(Debug)] +pub struct Config { + pub dynamic_conf: DynamicConfig, + pub cosmic_helper: cosmic_config::Config, + /// cosmic-config comp configuration for `com.system76.CosmicComp` + pub cosmic_conf: CosmicCompConfig, + /// cosmic-config context for `com.system76.CosmicSettings.Shortcuts` + pub settings_context: cosmic_config::Config, + /// Key bindings from `com.system76.CosmicSettings.Shortcuts` + pub shortcuts: Shortcuts, + // Tiling exceptions from `com.system76.CosmicSettings.WindowRules` + pub tiling_exceptions: Vec, + /// System actions from `com.system76.CosmicSettings.Shortcuts` + pub system_actions: BTreeMap, +} + +#[derive(Debug)] +pub struct DynamicConfig { + outputs: (Option, OutputsConfig), + numlock: (Option, NumlockStateConfig), + accessibility_filter: (Option, ScreenFilter), +} + +#[derive(Default, Debug, Deserialize, Serialize)] +pub struct NumlockStateConfig { + pub last_state: bool, +} + +pub struct CompOutputConfig<'a>(pub Ref<'a, OutputConfig>); + +impl CompOutputConfig<'_> { + pub fn mode_size(&self) -> Size { + self.0.mode.0.into() + } + + pub fn mode_refresh(&self) -> u32 { + self.0.mode.1.unwrap_or(60_000) + } + + pub fn transformed_size(&self) -> Size { + self.transform().transform_size(self.mode_size()) + } + + pub fn output_mode(&self) -> Mode { + Mode { + size: self.mode_size(), + refresh: self.mode_refresh() as i32, + } + } + + pub fn transform(&self) -> Transform { + Transform::from(CompTransformDef(self.0.transform)) + } +} + +pub struct CompTransformDef(pub TransformDef); + +impl From for CompTransformDef { + fn from(transform: Transform) -> Self { + let def = match transform { + Transform::Normal => TransformDef::Normal, + Transform::_90 => TransformDef::_90, + Transform::_180 => TransformDef::_180, + Transform::_270 => TransformDef::_270, + Transform::Flipped => TransformDef::Flipped, + Transform::Flipped90 => TransformDef::Flipped90, + Transform::Flipped180 => TransformDef::Flipped180, + Transform::Flipped270 => TransformDef::Flipped270, + }; + CompTransformDef(def) + } +} + +impl From for Transform { + fn from(comp_transform: CompTransformDef) -> Self { + match comp_transform.0 { + TransformDef::Normal => Transform::Normal, + TransformDef::_90 => Transform::_90, + TransformDef::_180 => Transform::_180, + TransformDef::_270 => Transform::_270, + TransformDef::Flipped => Transform::Flipped, + TransformDef::Flipped90 => Transform::Flipped90, + TransformDef::Flipped180 => Transform::Flipped180, + TransformDef::Flipped270 => Transform::Flipped270, + } + } +} + +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)] +pub struct ScreenFilter { + pub inverted: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub color_filter: Option, +} + +impl ScreenFilter { + pub fn is_noop(&self) -> bool { + !self.inverted && self.color_filter.is_none() + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u8)] +// these values need to match with offscreen.frag +pub enum ColorFilter { + Greyscale = 1, + Protanopia = 2, + Deuteranopia = 3, + Tritanopia = 4, +} + +impl Config { + pub fn load(loop_handle: &LoopHandle<'_, State>) -> Config { + let config = cosmic_config::Config::new("com.system76.CosmicComp", 1).unwrap(); + let source = cosmic_config::calloop::ConfigWatchSource::new(&config).unwrap(); + loop_handle + .insert_source(source, |(config, keys), (), state| { + config_changed(config, keys, state); + }) + .expect("Failed to add cosmic-config to the event loop"); + let xdg = xdg::BaseDirectories::new(); + + let cosmic_comp_config = + CosmicCompConfig::get_entry(&config).unwrap_or_else(|(errs, c)| { + if cfg!(debug_assertions) { + for err in errs { + warn!(?err, ""); + } + } + c + }); + + // Listen for updates to the toolkit config + if let Ok(tk_config) = cosmic_config::Config::new("com.system76.CosmicTk", 1) { + fn handle_new_toolkit_config(config: CosmicTk, state: &mut State) { + if cosmic::icon_theme::default() != config.icon_theme { + cosmic::icon_theme::set_default(config.icon_theme.clone()); + state.common.update_xwayland_settings(); + } + + let mut workspace_guard = state.common.workspace_state.update(); + state.common.shell.write().update_toolkit( + config, + &state.common.xdg_activation_state, + &mut workspace_guard, + ); + } + + let config = CosmicTk::get_entry(&tk_config).unwrap_or_else(|(errs, c)| { + if cfg!(debug_assertions) { + for err in errs { + warn!(?err, ""); + } + } + c + }); + let _ = loop_handle.insert_idle(move |state| { + handle_new_toolkit_config(config, state); + }); + + match cosmic_config::calloop::ConfigWatchSource::new(&tk_config) { + Ok(source) => { + if let Err(err) = + loop_handle.insert_source(source, |(config, _keys), (), state| { + let config = + CosmicTk::get_entry(&config).unwrap_or_else(|(errs, c)| { + if cfg!(debug_assertions) { + for err in errs { + warn!(?err, ""); + } + } + c + }); + handle_new_toolkit_config(config, state); + }) + { + warn!(?err, "Failed to watch com.system76.CosmicTk config"); + } + } + Err(err) => warn!( + ?err, + "failed to create config watch source for com.system76.CosmicTk" + ), + } + } + + // Source key bindings from com.system76.CosmicSettings.Shortcuts + let settings_context = shortcuts::context().expect("Failed to load shortcuts config"); + let system_actions = shortcuts::system_actions(&settings_context); + let shortcuts = shortcuts::shortcuts(&settings_context); + + // Listen for updates to the keybindings config. + match cosmic_config::calloop::ConfigWatchSource::new(&settings_context) { + Ok(source) => { + if let Err(err) = loop_handle.insert_source(source, |(config, keys), (), state| { + for key in keys { + match key.as_str() { + // Reload the keyboard shortcuts config. + "custom" | "defaults" => { + state.common.config.shortcuts = shortcuts::shortcuts(&config); + } + + "system_actions" => { + state.common.config.system_actions = + shortcuts::system_actions(&config); + } + + _ => (), + } + } + }) { + warn!( + ?err, + "Failed to watch com.system76.CosmicSettings.Shortcuts config" + ); + } + } + Err(err) => warn!( + ?err, + "failed to create config watch source for com.system76.CosmicSettings.Shortcuts" + ), + }; + + let window_rules_context = + window_rules::context().expect("Failed to load window rules config"); + let tiling_exceptions = window_rules::tiling_exceptions(&window_rules_context); + + match cosmic_config::calloop::ConfigWatchSource::new(&window_rules_context) { + Ok(source) => { + if let Err(err) = loop_handle.insert_source(source, |(config, keys), (), state| { + for key in keys { + match key.as_str() { + "tiling_exception_defaults" | "tiling_exception_custom" => { + let new_exceptions = window_rules::tiling_exceptions(&config); + state.common.config.tiling_exceptions = new_exceptions; + state.common.shell.write().update_tiling_exceptions( + state.common.config.tiling_exceptions.iter(), + ); + } + _ => (), + } + } + }) { + warn!( + ?err, + "Failed to watch com.system76.CosmicSettings.WindowRules config" + ); + } + } + Err(err) => warn!( + ?err, + "failed to create config watch source for com.system76.CosmicSettings.WindowRules" + ), + }; + + let _ = loop_handle.insert_idle(|state| { + let filter_conf = state.common.config.dynamic_conf.screen_filter(); + state + .common + .a11y_state + .set_screen_inverted(filter_conf.inverted); + state + .common + .a11y_state + .set_screen_filter(filter_conf.color_filter); + }); + + Config { + dynamic_conf: Self::load_dynamic(&xdg), + cosmic_conf: cosmic_comp_config, + cosmic_helper: config, + settings_context, + shortcuts, + system_actions, + tiling_exceptions, + } + } + + fn load_dynamic(xdg: &xdg::BaseDirectories) -> DynamicConfig { + let output_path = xdg.place_state_file("cosmic-comp/outputs.ron").ok(); + let outputs = load_outputs(output_path.as_ref()); + let numlock_path = xdg.place_state_file("cosmic-comp/numlock.ron").ok(); + let numlock = Self::load_numlock(&numlock_path); + + let filter_path = xdg + .place_state_file("cosmic-comp/a11y_screen_filter.ron") + .ok(); + let filter = Self::load_filter_state(&filter_path); + + DynamicConfig { + outputs: (output_path, outputs), + numlock: (numlock_path, numlock), + accessibility_filter: (filter_path, filter), + } + } + + fn load_numlock(path: &Option) -> NumlockStateConfig { + path.as_deref() + .filter(|path| path.exists()) + .and_then(|path| { + ron::de::from_reader::<_, NumlockStateConfig>( + OpenOptions::new().read(true).open(path).unwrap(), + ) + .map_err(|err| { + warn!(?err, "Failed to read numlock.ron, resetting.."); + if let Err(err) = std::fs::remove_file(path) { + error!(?err, "Failed to remove numlock.ron."); + } + }) + .ok() + }) + .unwrap_or_default() + } + + fn load_filter_state(path: &Option) -> ScreenFilter { + if let Some(path) = path.as_ref() + && path.exists() + { + match ron::de::from_reader::<_, ScreenFilter>( + OpenOptions::new().read(true).open(path).unwrap(), + ) { + Ok(config) => return config, + Err(err) => { + warn!(?err, "Failed to read screen_filter state, resetting.."); + if let Err(err) = std::fs::remove_file(path) { + error!(?err, "Failed to remove screen_filter state."); + } + } + }; + } + + ScreenFilter { + inverted: false, + color_filter: None, + } + } + + pub fn shortcut_for_action(&self, action: &shortcuts::Action) -> Option { + self.shortcuts.shortcut_for_action(action) + } + + pub fn read_outputs( + &mut self, + output_state: &mut OutputConfigurationState, + backend: &mut BackendData, + shell: &Arc>, + loop_handle: &LoopHandle<'static, State>, + workspace_state: &mut WorkspaceUpdateGuard<'_, State>, + xdg_activation_state: &XdgActivationState, + startup_done: Arc, + clock: &Clock, + ) -> anyhow::Result<()> { + let outputs = output_state.outputs().collect::>(); + let mut infos = outputs + .iter() + .cloned() + .map(Into::::into) + .map(|i| i.0) + .collect::>(); + infos.sort(); + + if let Some(configs) = self + .dynamic_conf + .outputs() + .config + .get(&infos) + .filter(|configs| { + if configs + .iter() + .all(|config| config.enabled == OutputState::Disabled) + { + if !configs.is_empty() { + error!( + "Broken config, all outputs disabled. Resetting... {:?}", + configs + ); + } + false + } else { + true + } + }) + .cloned() + { + let known_good_configs = outputs + .iter() + .map(|output| { + output + .user_data() + .get::>() + .unwrap() + .borrow() + .clone() + }) + .collect::>(); + + let mut found_outputs = Vec::new(); + for (name, output_config) in infos.iter().map(|o| &o.connector).zip(configs) { + let output = outputs.iter().find(|o| &o.name() == name).unwrap().clone(); + let enabled = output_config.enabled.clone(); + *output + .user_data() + .get::>() + .unwrap() + .borrow_mut() = output_config; + found_outputs.push((output.clone(), enabled)); + } + + let mut backend = backend.lock(); + if let Err(err) = backend.apply_config_for_outputs( + false, + loop_handle, + self.dynamic_conf.screen_filter(), + shell.clone(), + workspace_state, + xdg_activation_state, + startup_done.clone(), + clock, + ) { + warn!(?err, "Failed to set new config."); + found_outputs.clear(); + for (output, output_config) in outputs.clone().into_iter().zip(known_good_configs) { + let enabled = output_config.enabled.clone(); + *output + .user_data() + .get::>() + .unwrap() + .borrow_mut() = output_config; + found_outputs.push((output.clone(), enabled)); + } + + backend + .apply_config_for_outputs( + false, + loop_handle, + self.dynamic_conf.screen_filter(), + shell.clone(), + workspace_state, + xdg_activation_state, + startup_done, + clock, + ) + .context("Failed to reset config")?; + + for (output, enabled) in found_outputs { + if enabled == OutputState::Enabled { + output_state.enable_head(&output); + } else { + output_state.disable_head(&output); + } + } + } else { + for (output, enabled) in found_outputs { + if enabled == OutputState::Enabled { + output_state.enable_head(&output); + } else { + output_state.disable_head(&output); + } + } + } + + output_state.update(); + self.write_outputs(output_state.outputs()); + } else { + if outputs + .iter() + .all(|o| o.config().enabled == OutputState::Disabled) + { + for output in &outputs { + output.config_mut().enabled = OutputState::Enabled; + } + } + + // we don't have a config, so lets generate somewhat sane positions + let mut w = 0; + if !outputs.iter().any(|o| o.config().xwayland_primary) { + // if we don't have a primary output for xwayland from a previous config, pick one + if let Some(primary) = outputs.iter().find(|o| o.mirroring().is_none()) { + primary.config_mut().xwayland_primary = true; + } + } + for output in outputs.iter().filter(|o| o.mirroring().is_none()) { + { + let mut config = output.config_mut(); + config.position = (w, 0); + } + w += output.geometry().size.w as u32; + } + + let mut backend = backend.lock(); + backend + .apply_config_for_outputs( + false, + loop_handle, + self.dynamic_conf.screen_filter(), + shell.clone(), + workspace_state, + xdg_activation_state, + startup_done.clone(), + clock, + ) + .context("Failed to set new config")?; + + for output in outputs { + if output + .user_data() + .get::>() + .unwrap() + .borrow() + .enabled + == OutputState::Enabled + { + output_state.enable_head(&output); + } else { + output_state.disable_head(&output); + } + } + output_state.update(); + self.write_outputs(output_state.outputs()); + } + + Ok(()) + } + + pub fn write_outputs( + &mut self, + outputs: impl Iterator>, + ) { + let mut infos = outputs + .map(|o| { + let o = o.borrow(); + ( + Into::::into(o.clone()).0, + o.user_data() + .get::>() + .unwrap() + .borrow() + .clone(), + ) + }) + .collect::>(); + infos.sort_by(|(a, _), (b, _)| a.cmp(b)); + let (infos, configs) = infos.into_iter().unzip(); + self.dynamic_conf + .outputs_mut() + .config + .insert(infos, configs); + } + + pub fn xkb_config(&self) -> XkbConfig { + self.cosmic_conf.xkb_config.clone() + } + + pub fn read_device(&self, device: &mut InputDevice) { + let (device_config, default_config) = self.get_device_config(device); + input_config::update_device(device, device_config.as_ref(), default_config); + } + + pub fn scroll_factor(&self, device: &InputDevice) -> f64 { + let (device_config, default_config) = self.get_device_config(device); + input_config::get_config(device_config.as_ref(), default_config, |x| { + x.scroll_config.as_ref()?.scroll_factor + }) + .map_or(1.0, |x| x.0) + } + + pub fn map_to_output(&self, device: &InputDevice) -> Option { + let (device_config, default_config) = self.get_device_config(device); + Some( + input_config::get_config(device_config.as_ref(), default_config, |x| { + x.map_to_output.clone() + })? + .0, + ) + } + + fn get_device_config(&self, device: &InputDevice) -> (Option, &InputConfig) { + let is_touchpad = device.config_tap_finger_count() > 0; + + let default_config = if is_touchpad { + &self.cosmic_conf.input_touchpad + } else { + &self.cosmic_conf.input_default + }; + + let mut device_config = self.cosmic_conf.input_devices.get(&*device.name()).cloned(); + if is_touchpad && self.cosmic_conf.input_touchpad_override == TouchpadOverride::ForceDisable + { + device_config = Some({ + let mut config = device_config.unwrap_or_default(); + config.state = InputDeviceState::Disabled; + config + }); + } + + (device_config, default_config) + } +} + +pub struct PersistenceGuard<'a, T: Serialize>(Option, &'a mut T); + +impl std::ops::Deref for PersistenceGuard<'_, T> { + type Target = T; + fn deref(&self) -> &T { + self.1 + } +} + +impl std::ops::DerefMut for PersistenceGuard<'_, T> { + fn deref_mut(&mut self) -> &mut T { + self.1 + } +} + +impl Drop for PersistenceGuard<'_, T> { + fn drop(&mut self) { + if let Some(path) = self.0.as_ref() { + let content = match ron::ser::to_string_pretty(&self.1, Default::default()) { + Ok(content) => content, + Err(err) => { + warn!("Failed to serialize: {:?}", err); + return; + } + }; + + let mut writer = match OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .open(path) + { + Ok(writer) => writer, + Err(err) => { + warn!(?err, "Failed to persist {}.", path.display()); + return; + } + }; + + if let Err(err) = writer.write_all(content.as_bytes()) { + warn!(?err, "Failed to persist {}", path.display()); + } else { + let _ = writer.flush(); + } + } + } +} + +impl DynamicConfig { + pub fn outputs(&self) -> &OutputsConfig { + &self.outputs.1 + } + + pub fn outputs_mut(&mut self) -> PersistenceGuard<'_, OutputsConfig> { + PersistenceGuard(self.outputs.0.clone(), &mut self.outputs.1) + } + + pub fn numlock(&self) -> &NumlockStateConfig { + &self.numlock.1 + } + + pub fn numlock_mut(&mut self) -> PersistenceGuard<'_, NumlockStateConfig> { + PersistenceGuard(self.numlock.0.clone(), &mut self.numlock.1) + } + + pub fn screen_filter(&self) -> &ScreenFilter { + &self.accessibility_filter.1 + } + + pub fn screen_filter_mut(&mut self) -> PersistenceGuard<'_, ScreenFilter> { + PersistenceGuard( + self.accessibility_filter.0.clone(), + &mut self.accessibility_filter.1, + ) + } +} + +pub fn xkb_config_to_wl(config: &XkbConfig) -> WlXkbConfig<'_> { + WlXkbConfig { + rules: &config.rules, + model: &config.model, + layout: &config.layout, + variant: &config.variant, + options: config.options.clone(), + } +} + +fn get_config( + config: &cosmic_config::Config, + key: &str, +) -> T { + config.get(key).unwrap_or_else(|err| { + error!(?err, "Failed to read config '{}'", key); + T::default() + }) +} + +fn update_input(state: &mut State) { + if let BackendData::Kms(kms_state) = &mut state.backend { + for device in kms_state.input_devices.values_mut() { + state.common.config.read_device(device); + } + } +} + +pub fn change_modifier_state( + keyboard: &smithay::input::keyboard::KeyboardHandle, + scan_code: u32, + state: &mut State, +) { + /// Offset used to convert Linux scancode to X11 keycode. + const X11_KEYCODE_OFFSET: u32 = 8; + + let mut input = |key_state, scan_code| { + let time = state.common.clock.now().as_millis(); + let _ = keyboard.input( + state, + smithay_input::Keycode::new(scan_code + X11_KEYCODE_OFFSET), + key_state, + SERIAL_COUNTER.next_serial(), + time, + |_, _, _| smithay::input::keyboard::FilterResult::<()>::Forward, + ); + }; + + input(smithay_input::KeyState::Pressed, scan_code); + input(smithay_input::KeyState::Released, scan_code); +} + +fn config_changed(config: cosmic_config::Config, keys: Vec, state: &mut State) { + for key in &keys { + match key.as_str() { + "xkb_config" => { + let value = get_config::(&config, "xkb_config"); + let seats = state + .common + .shell + .read() + .seats + .iter() + .cloned() + .collect::>(); + for seat in seats.into_iter() { + if let Some(keyboard) = seat.get_keyboard() { + let old_modifier_state = keyboard.modifier_state(); + keyboard.change_repeat_info( + (value.repeat_rate as i32).abs(), // Negative values are illegal + (value.repeat_delay as i32).abs(), + ); + if let Err(err) = keyboard.set_xkb_config(state, xkb_config_to_wl(&value)) { + error!(?err, "Failed to load provided xkb config"); + // TODO Revert to default? + } + + // Press and release the numlock key to update modifiers. + if old_modifier_state.num_lock != keyboard.modifier_state().num_lock { + const NUMLOCK_SCANCODE: u32 = 69; + change_modifier_state(&keyboard, NUMLOCK_SCANCODE, state); + } + if old_modifier_state.caps_lock != keyboard.modifier_state().caps_lock { + const CAPSLOCK_SCANCODE: u32 = 58; + change_modifier_state(&keyboard, CAPSLOCK_SCANCODE, state); + } + } + } + state.common.config.cosmic_conf.xkb_config = value; + } + "keyboard_config" => { + let value = get_config::(&config, "keyboard_config"); + state.common.config.cosmic_conf.keyboard_config = value; + let shell = state.common.shell.read(); + let seat = shell.seats.last_active(); + state.common.config.dynamic_conf.numlock_mut().last_state = + seat.get_keyboard().unwrap().modifier_state().num_lock; + } + "input_default" => { + let value = get_config::(&config, "input_default"); + state.common.config.cosmic_conf.input_default = value; + update_input(state); + } + "input_touchpad" => { + let value = get_config::(&config, "input_touchpad"); + state.common.config.cosmic_conf.input_touchpad = value; + update_input(state); + } + "input_touchpad_override" => { + let value = get_config::(&config, "input_touchpad_override"); + state.common.config.cosmic_conf.input_touchpad_override = value; + update_input(state) + } + "input_devices" => { + let value = get_config::>(&config, "input_devices"); + state.common.config.cosmic_conf.input_devices = value; + update_input(state); + } + "workspaces" => { + state.common.config.cosmic_conf.workspaces = + get_config::(&config, "workspaces"); + state.common.update_config(); + } + "autotile" => { + let new = get_config::(&config, "autotile"); + if new != state.common.config.cosmic_conf.autotile { + state.common.config.cosmic_conf.autotile = new; + + let mut shell = state.common.shell.write(); + let shell_ref = &mut *shell; + shell_ref.workspaces.update_autotile( + new, + &mut state.common.workspace_state.update(), + shell_ref.seats.iter(), + ); + } + } + "autotile_behavior" => { + let new = get_config::(&config, "autotile_behavior"); + if new != state.common.config.cosmic_conf.autotile_behavior { + state.common.config.cosmic_conf.autotile_behavior = new; + + let mut shell = state.common.shell.write(); + let shell_ref = &mut *shell; + shell_ref.workspaces.update_autotile_behavior( + new, + &mut state.common.workspace_state.update(), + shell_ref.seats.iter(), + ); + } + } + "active_hint" => { + let new = get_config::(&config, "active_hint"); + if new != state.common.config.cosmic_conf.active_hint { + state.common.config.cosmic_conf.active_hint = new; + state.common.update_config(); + } + } + "descale_xwayland" => { + let new = get_config::(&config, "descale_xwayland"); + if new != state.common.config.cosmic_conf.descale_xwayland { + state.common.config.cosmic_conf.descale_xwayland = new; + state.common.update_xwayland_settings(); + } + } + "xwayland_eavesdropping" => { + let new = get_config::(&config, "xwayland_eavesdropping"); + if new != state.common.config.cosmic_conf.xwayland_eavesdropping { + state.common.config.cosmic_conf.xwayland_eavesdropping = new; + state + .common + .xwayland_reset_eavesdropping(SERIAL_COUNTER.next_serial()); + } + } + "focus_follows_cursor" => { + let new = get_config::(&config, "focus_follows_cursor"); + if new != state.common.config.cosmic_conf.focus_follows_cursor { + state.common.config.cosmic_conf.focus_follows_cursor = new; + } + } + "cursor_follows_focus" => { + let new = get_config::(&config, "cursor_follows_focus"); + if new != state.common.config.cosmic_conf.cursor_follows_focus { + state.common.config.cosmic_conf.cursor_follows_focus = new; + } + } + "focus_follows_cursor_delay" => { + let new = get_config::(&config, "focus_follows_cursor_delay"); + if new != state.common.config.cosmic_conf.focus_follows_cursor_delay { + state.common.config.cosmic_conf.focus_follows_cursor_delay = new; + } + } + "edge_snap_threshold" => { + let new = get_config::(&config, "edge_snap_threshold"); + if new != state.common.config.cosmic_conf.edge_snap_threshold { + state.common.config.cosmic_conf.edge_snap_threshold = new; + } + } + "accessibility_zoom" => { + let new = get_config::(&config, "accessibility_zoom"); + if new != state.common.config.cosmic_conf.accessibility_zoom { + state.common.config.cosmic_conf.accessibility_zoom = new; + state.common.update_config(); + } + } + "appearance_settings" => { + let new = get_config::(&config, "appearance_settings"); + if new != state.common.config.cosmic_conf.appearance_settings { + state.common.config.cosmic_conf.appearance_settings = new; + state.common.update_config(); + for output in state.common.shell.read().outputs() { + state.backend.schedule_render(output); + } + } + } + "cursor_hide_timeout" => { + let new = get_config::>(&config, "cursor_hide_timeout"); + if new != state.common.config.cosmic_conf.cursor_hide_timeout { + state.common.config.cosmic_conf.cursor_hide_timeout = new; + let seats: Vec<_> = state.common.shell.read().seats.iter().cloned().collect(); + let mut needs_render = false; + for seat in seats { + needs_render |= + crate::backend::render::cursor::notify_cursor_activity(state, &seat); + } + if needs_render { + let outputs: Vec<_> = + state.common.shell.read().outputs().cloned().collect(); + for output in outputs { + state.backend.schedule_render(&output); + } + } + } + } + "activation_policy" => { + let new = get_config::(&config, "activation_policy"); + if new != state.common.config.cosmic_conf.activation_policy { + state.common.config.cosmic_conf.activation_policy = new; + } + } + _ => {} + } + } +} + +#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct CompOutputInfo(OutputInfo); + +impl From for CompOutputInfo { + fn from(o: Output) -> CompOutputInfo { + let physical = o.physical_properties(); + CompOutputInfo(OutputInfo { + connector: o.name(), + make: physical.make, + model: physical.model, + }) + } +} diff --git a/src/config/types.rs b/src/config/types.rs new file mode 100644 index 0000000..e962000 --- /dev/null +++ b/src/config/types.rs @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: GPL-3.0-only +#![allow(non_snake_case)] + +pub use smithay::input::keyboard::XkbConfig as WlXkbConfig; diff --git a/src/dbus/a11y_keyboard_monitor.rs b/src/dbus/a11y_keyboard_monitor.rs new file mode 100644 index 0000000..c97aa59 --- /dev/null +++ b/src/dbus/a11y_keyboard_monitor.rs @@ -0,0 +1,304 @@ +// https://gitlab.gnome.org/GNOME/mutter/-/blob/main/data/dbus-interfaces/org.freedesktop.a11y.xml + +use smithay::{ + backend::input::KeyState, + input::keyboard::{KeysymHandle, ModifiersState}, +}; +use std::{ + collections::{HashMap, HashSet}, + sync::{Arc, Mutex}, +}; +use tracing::debug; +use xkbcommon::xkb::Keysym; +use zbus::{ + message::Header, + names::{UniqueName, WellKnownName}, + object_server::SignalEmitter, +}; + +use super::name_owners::NameOwners; + +static ALLOWED_NAMES: &[WellKnownName] = &[WellKnownName::from_static_str_unchecked( + "org.gnome.Orca.KeyboardMonitor", +)]; + +// As defined in at-spi2-core +const ATSPI_DEVICE_A11Y_MANAGER_VIRTUAL_MOD_START: u32 = 15; + +#[derive(PartialEq, Eq, Debug)] +struct KeyGrab { + pub mods: u32, + pub virtual_mods: HashSet, + pub key: Keysym, +} + +impl KeyGrab { + fn new(virtual_mods: &[Keysym], key: Keysym, raw_mods: u32) -> Self { + let mods = raw_mods & ((1 << ATSPI_DEVICE_A11Y_MANAGER_VIRTUAL_MOD_START) - 1); + let virtual_mods = virtual_mods + .iter() + .copied() + .enumerate() + .filter(|(i, _)| { + raw_mods & (1 << (ATSPI_DEVICE_A11Y_MANAGER_VIRTUAL_MOD_START + *i as u32)) != 0 + }) + .map(|(_, x)| x) + .collect(); + Self { + mods, + virtual_mods, + key, + } + } +} + +#[derive(Debug, Default)] +struct Client { + grabbed: bool, + watched: bool, + virtual_mods: HashSet, + key_grabs: Vec, +} + +#[derive(Debug, Default)] +struct Clients(HashMap, Client>); + +impl Clients { + fn get(&mut self, name: &UniqueName<'_>) -> &mut Client { + self.0.entry(name.to_owned()).or_default() + } +} + +#[derive(Debug)] +pub struct A11yKeyboardMonitorState { + executor: calloop::futures::Scheduler<()>, + clients: Arc>, + active_virtual_mods: HashSet, + conn: zbus::Connection, + name_owners: NameOwners, +} + +impl A11yKeyboardMonitorState { + pub async fn new( + conn: &zbus::Connection, + name_owners: &NameOwners, + executor: &calloop::futures::Scheduler<()>, + ) -> zbus::Result { + let clients = Arc::new(Mutex::new(Clients::default())); + + let keyboard_monitor = KeyboardMonitor { + clients: clients.clone(), + name_owners: name_owners.clone(), + }; + conn.object_server() + .at("/org/freedesktop/a11y/Manager", keyboard_monitor) + .await?; + conn.request_name("org.freedesktop.a11y.Manager").await?; + + Ok(Self { + executor: executor.clone(), + clients, + active_virtual_mods: HashSet::new(), + conn: conn.clone(), + name_owners: name_owners.clone(), + }) + } + + pub fn has_virtual_mod(&self, keysym: Keysym) -> bool { + self.clients + .lock() + .unwrap() + .0 + .values() + .any(|client| client.virtual_mods.contains(&keysym)) + } + + pub fn add_active_virtual_mod(&mut self, keysym: Keysym) { + self.active_virtual_mods.insert(keysym); + } + + pub fn remove_active_virtual_mod(&mut self, keysym: Keysym) -> bool { + self.active_virtual_mods.remove(&keysym) + } + + pub fn active_virtual_mods(&self) -> &HashSet { + &self.active_virtual_mods + } + + pub fn has_keyboard_grab(&self) -> bool { + self.clients + .lock() + .unwrap() + .0 + .values() + .any(|client| client.grabbed) + } + + /// Key grab exists for mods, key, with active virtual mods + pub fn has_key_grab(&self, modifiers: &ModifiersState, key: Keysym) -> bool { + self.clients + .lock() + .unwrap() + .0 + .values() + .flat_map(|client| &client.key_grabs) + .any(|grab| { + grab.mods == modifiers.serialized.depressed + && grab.virtual_mods == self.active_virtual_mods + && grab.key == key + }) + } + + pub fn key_event(&self, modifiers: &ModifiersState, keysym: &KeysymHandle, state: KeyState) { + let has_key_grab = self.has_key_grab(modifiers, keysym.modified_sym()); + let clients = self.clients.lock().unwrap(); + for (unique_name, client) in clients.0.iter() { + if !client.watched && !has_key_grab { + continue; + } + + let mut signal_context = + SignalEmitter::new(&self.conn, "/org/freedesktop/a11y/Manager").unwrap(); + // Instead of sending signal to all clients, send only to authorized + // clients with registed watches. + signal_context = signal_context.set_destination(unique_name.clone().into()); + + let released = match state { + KeyState::Pressed => false, + KeyState::Released => true, + }; + let unichar = { + let xkb = keysym.xkb().lock().unwrap(); + unsafe { xkb.state() }.key_get_utf32(keysym.raw_code()) + }; + let future = KeyboardMonitor::key_event( + signal_context, + released, + modifiers.serialized.depressed, + keysym.modified_sym().raw(), + unichar, + keysym.raw_code().raw() as u16, + ); + let _ = self.executor.schedule(async { + let _ = future.await; + }); + } + } + + pub fn refresh(&mut self) { + // Remove clients and associated grabs when unique names are no longer + // present on bus, or no longer hold approved name on bus. + self.clients + .lock() + .unwrap() + .0 + .retain(|k, _| self.name_owners.check_owner_no_poll(k, ALLOWED_NAMES)) + } +} + +struct KeyboardMonitor { + clients: Arc>, + name_owners: NameOwners, +} + +impl KeyboardMonitor { + async fn check_sender_allowed(&self, sender: &UniqueName<'_>) -> zbus::fdo::Result<()> { + if self.name_owners.check_owner(sender, ALLOWED_NAMES).await { + Ok(()) + } else { + Err(zbus::fdo::Error::AccessDenied("Access denied".to_string())) + } + } +} + +#[zbus::interface(name = "org.freedesktop.a11y.KeyboardMonitor")] +impl KeyboardMonitor { + async fn grab_keyboard(&mut self, #[zbus(header)] header: Header<'_>) -> zbus::fdo::Result<()> { + if let Some(sender) = header.sender() { + self.check_sender_allowed(sender).await?; + let mut clients = self.clients.lock().unwrap(); + clients.get(sender).grabbed = true; + debug!("grab keyboard by {}", sender); + } + Ok(()) + } + + async fn ungrab_keyboard( + &mut self, + #[zbus(header)] header: Header<'_>, + ) -> zbus::fdo::Result<()> { + if let Some(sender) = header.sender() { + self.check_sender_allowed(sender).await?; + let mut clients = self.clients.lock().unwrap(); + clients.get(sender).grabbed = false; + debug!("ungrab keyboard by {}", sender); + } + Ok(()) + } + + async fn watch_keyboard( + &mut self, + #[zbus(header)] header: Header<'_>, + ) -> zbus::fdo::Result<()> { + if let Some(sender) = header.sender() { + self.check_sender_allowed(sender).await?; + let mut clients = self.clients.lock().unwrap(); + clients.get(sender).watched = true; + debug!("watch keyboard by {}", sender); + } + Ok(()) + } + + async fn unwatch_keyboard( + &mut self, + #[zbus(header)] header: Header<'_>, + ) -> zbus::fdo::Result<()> { + if let Some(sender) = header.sender() { + self.check_sender_allowed(sender).await?; + let mut clients = self.clients.lock().unwrap(); + clients.get(sender).watched = false; + debug!("unwatch keyboard by {}", sender); + } + Ok(()) + } + + async fn set_key_grabs( + &self, + #[zbus(header)] header: Header<'_>, + virtual_mods: Vec, + keystrokes: Vec<(u32, u32)>, + ) -> zbus::fdo::Result<()> { + let virtual_mods = virtual_mods + .into_iter() + .map(Keysym::from) + .collect::>(); + let key_grabs = keystrokes + .into_iter() + .map(|(k, mods)| KeyGrab::new(&virtual_mods, Keysym::from(k), mods)) + .collect::>(); + + if let Some(sender) = header.sender() { + self.check_sender_allowed(sender).await?; + let mut clients = self.clients.lock().unwrap(); + let client = clients.get(sender); + debug!( + "key grabs set by {}: {:?}", + sender, + (&virtual_mods, &key_grabs) + ); + client.virtual_mods = virtual_mods.into_iter().collect::>(); + client.key_grabs = key_grabs; + } + Ok(()) + } + + #[zbus(signal)] + async fn key_event( + ctx: SignalEmitter<'_>, + released: bool, + state: u32, + keysym: u32, + unichar: u32, + keycode: u16, + ) -> zbus::Result<()>; +} diff --git a/src/dbus/logind.rs b/src/dbus/logind.rs new file mode 100644 index 0000000..53e3b2c --- /dev/null +++ b/src/dbus/logind.rs @@ -0,0 +1,34 @@ +use std::os::fd::OwnedFd; + +use anyhow::{Context, Result}; +use logind_zbus::manager::{InhibitType::HandleLidSwitch, ManagerProxy}; + +use crate::state::Common; + +pub fn inhibit_lid(common: &Common) -> Result { + let fd = futures_executor::block_on(async { + let conn = common.dbus_state.system_conn().await?; + let manager = ManagerProxy::new(conn).await?; + manager + .inhibit( + HandleLidSwitch, + "cosmic-comp", + "External output connected", + "block", + ) + .await + })?; + + Ok(fd.into()) +} + +pub fn lid_closed(common: &Common) -> Result { + futures_executor::block_on(async { + let conn = common.dbus_state.system_conn().await?; + let manager = ManagerProxy::new(conn).await?; + manager + .lid_closed() + .await + .context("Failed to talk to logind") + }) +} diff --git a/src/dbus/mod.rs b/src/dbus/mod.rs new file mode 100644 index 0000000..5b9754c --- /dev/null +++ b/src/dbus/mod.rs @@ -0,0 +1,170 @@ +use crate::{ + state::{BackendData, Common, State}, + utils::prelude::OutputExt, +}; +use anyhow::{Context, Result}; +use calloop::{InsertError, LoopHandle, stream::StreamSource}; +use cosmic_comp_config::output::comp::OutputState; +use std::{ + cell::{RefCell, RefMut}, + collections::HashMap, + rc::Rc, +}; +use tracing::{error, warn}; + +pub mod a11y_keyboard_monitor; +use a11y_keyboard_monitor::A11yKeyboardMonitorState; +#[cfg(feature = "logind")] +pub mod logind; +mod name_owners; +mod power; + +#[derive(Clone, Debug)] +pub struct DBusState(Rc); + +#[derive(Debug)] +struct DBusStateInner { + evlh: LoopHandle<'static, State>, + executor: calloop::futures::Scheduler<()>, + session_conn: zbus::Result, + system_conn: zbus::Result, + a11y_keyboard_monitor: RefCell>, +} + +impl DBusState { + pub fn init(evlh: &LoopHandle<'static, State>) -> Self { + let (source, executor) = calloop::futures::executor().unwrap(); + let session_conn = futures_executor::block_on(zbus::Connection::session()); + let system_conn = futures_executor::block_on(zbus::Connection::system()); + let state = Self(Rc::new(DBusStateInner { + evlh: evlh.clone(), + executor, + session_conn, + system_conn, + a11y_keyboard_monitor: RefCell::new(None), + })); + evlh.insert_source(source, |_, _, _| {}).unwrap(); + let state_clone = state.clone(); + state.spawn(async move { + if let Err(err) = init_session(&state_clone).await { + tracing::error!("Failed to initialize session DBus connection: {}", err); + } + }); + let state_clone = state.clone(); + state.spawn(async move { + if let Err(err) = init_system(&state_clone).await { + tracing::error!("Failed to initialize system DBus connection: {}", err); + } + }); + state + } + + pub fn a11y_keyboard_monitor( + &self, + ) -> Option> { + RefMut::filter_map(self.0.a11y_keyboard_monitor.borrow_mut(), |x| x.as_mut()).ok() + } + + // TODO Lazy async init when we don't have anything blocking main thread + async fn session_conn(&self) -> zbus::Result<&zbus::Connection> { + self.0.session_conn.as_ref().map_err(|err| err.clone()) + } + + async fn system_conn(&self) -> zbus::Result<&zbus::Connection> { + self.0.system_conn.as_ref().map_err(|err| err.clone()) + } + + fn spawn(&self, fut: impl Future + 'static) { + let _ = self.0.executor.schedule(fut); + } +} + +async fn init_session(state: &DBusState) -> zbus::Result<()> { + let conn = state.session_conn().await?; + let name_owners = name_owners::NameOwners::new(conn, &state.0.executor).await?; + let a11y_keyboard_monitor_state = + A11yKeyboardMonitorState::new(conn, &name_owners, &state.0.executor).await?; + *state.0.a11y_keyboard_monitor.borrow_mut() = Some(a11y_keyboard_monitor_state); + Ok(()) +} + +async fn init_system(state: &DBusState) -> zbus::Result<()> { + let conn = state.system_conn().await?.clone(); + let evlh = state.0.evlh.clone(); + state.spawn(async move { + if let Err(err) = power_hot_plug_task(conn, evlh).await { + tracing::warn!(?err, "Failed to initialize dbus handlers"); + } + }); + Ok(()) +} + +async fn power_hot_plug_task( + conn: zbus::Connection, + evlh: LoopHandle<'static, State>, +) -> Result<()> { + match power::init(&conn).await { + Ok(power_daemon) => { + if let Ok(stream) = power_daemon.receive_hot_plug_detect().await { + let source = StreamSource::new(stream).unwrap(); + evlh.insert_source(source, |_, _, state| { + let nodes = match &mut state.backend { + BackendData::Kms(kms) => { + kms.drm_devices.keys().cloned().collect::>() + } + _ => Vec::new(), + }; + let mut added = Vec::new(); + for node in nodes { + match state.device_changed(node.dev_id()) { + Ok(outputs) => added.extend(outputs), + Err(err) => { + tracing::error!(?err, "Failed to update drm device {}.", node) + } + } + } + if let Err(err) = state.refresh_output_config() { + warn!("Unable to load output config: {}", err); + if !added.is_empty() { + for output in added { + output.config_mut().enabled = OutputState::Disabled; + } + if let Err(err) = state.refresh_output_config() { + error!("Unrecoverable config error: {}", err); + } + } + } + }) + .map_err(|InsertError { error, .. }| error) + .with_context(|| "Failed to add channel to event_loop")?; + } + } + Err(err) => { + tracing::info!(?err, "Failed to connect to com.system76.PowerDaemon"); + } + }; + Ok(()) +} + +/// Updated the D-Bus activation environment with `WAYLAND_DISPLAY` and +/// `DISPLAY` variables. +pub fn ready(common: &Common) -> Result<()> { + futures_executor::block_on(async { + let conn = common.dbus_state.session_conn().await?; + let dbus = zbus::fdo::DBusProxy::new(conn).await?; + + dbus.update_activation_environment(HashMap::from([ + ("WAYLAND_DISPLAY", common.socket.to_str().unwrap()), + ( + "DISPLAY", + &common + .xwayland_state + .as_ref() + .map(|s| format!(":{}", s.display)) + .unwrap_or_default(), + ), + ])) + .await + })?; + Ok(()) +} diff --git a/src/dbus/name_owners.rs b/src/dbus/name_owners.rs new file mode 100644 index 0000000..a2715c7 --- /dev/null +++ b/src/dbus/name_owners.rs @@ -0,0 +1,213 @@ +//! Helper for tracking if a caller of a DBus method owns a name on a bus +//! +//! Compare to Mutter's `MetaDbusAccessChecker` + +use futures_util::{ + StreamExt, + stream::{FusedStream, FuturesUnordered}, +}; +use std::{ + collections::{HashMap, HashSet}, + future::{Future, poll_fn}, + sync::{Arc, Mutex, Weak}, + task::{Context, Poll, Waker}, +}; +use zbus::{ + fdo, + names::{BusName, UniqueName, WellKnownName}, +}; + +#[derive(Debug)] +struct Inner { + dbus: fdo::DBusProxy<'static>, + name_owners: HashMap, Option>>, + unique_names: HashSet>, + stream: fdo::NameOwnerChangedStream, + // Waker from `update_task` is stored, so that task will still be woken after + // polling elsewhere. + waker: Waker, + enforce: bool, +} + +impl Drop for Inner { + fn drop(&mut self) { + // Wake `update_task` so it can terminate + self.waker.wake_by_ref(); + } +} + +impl Inner { + /// Process all events so far on `stream`, and update `name_owners`. + fn update_if_needed(&mut self) { + let mut context = Context::from_waker(&self.waker); + while !self.stream.is_terminated() + && let Poll::Ready(val) = self.stream.poll_next_unpin(&mut context) + { + let Some(val) = val else { + break; + }; + let Ok(args) = val.args() else { + break; + }; + match args.name { + BusName::Unique(name) => { + if args.new_owner.is_some() { + self.unique_names.insert(name.to_owned()); + } else { + self.unique_names.remove(&name.to_owned()); + } + } + BusName::WellKnown(name) => { + if let Some(owner) = &*args.new_owner { + self.name_owners + .insert(name.to_owned(), Some(owner.to_owned())); + } else { + self.name_owners.remove(&name.to_owned()); + } + } + } + } + } +} + +/// This task polls the steam regularly, to make sure events on the stream aren't just +/// buffered indefinitely if `check_owner` is never called. +fn update_task(inner: Weak>) -> impl Future { + poll_fn(move |context| { + if let Some(inner) = inner.upgrade() { + let mut inner = inner.lock().unwrap(); + inner.waker = context.waker().clone(); + inner.update_if_needed(); + // Nothing to do now until waker is invoked + Poll::Pending + } else { + // All strong references have been dropped, so task has nothing left to do. + Poll::Ready(()) + } + }) +} + +/// Track which DBus unique names own which well-known names, so protocols can be restricted to +/// only certain names. +/// +/// Enforcement can be disabled by setting `COSMIC_ENFORCE_DBUS_OWNERS`. +#[derive(Clone, Debug)] +pub struct NameOwners(Arc>); + +impl NameOwners { + pub async fn new( + connection: &zbus::Connection, + executor: &calloop::futures::Scheduler<()>, + ) -> zbus::Result { + let dbus = fdo::DBusProxy::new(connection).await?; + let stream = dbus.receive_name_owner_changed().await?; + + let enforce = crate::utils::env::bool_var("COSMIC_ENFORCE_DBUS_OWNERS").unwrap_or(true); + + let names = dbus.list_names().await?; + let unique_names = names + .iter() + .filter_map(|n| match n.inner() { + BusName::Unique(name) => Some(name.to_owned()), + BusName::WellKnown(_) => None, + }) + .collect(); + let name_owners = names + .iter() + .filter_map(|n| match n.inner() { + BusName::Unique(_) => None, + BusName::WellKnown(name) => Some((name.to_owned(), None)), + }) + .collect(); + + let inner = Arc::new(Mutex::new(Inner { + dbus, + name_owners, + unique_names, + stream, + waker: Waker::noop().clone(), + enforce, + })); + + if enforce { + let _ = executor.schedule(update_task(Arc::downgrade(&inner))); + } + + Ok(NameOwners(inner)) + } + + #[allow(dead_code)] + pub fn has_unique_name(&self, name: &UniqueName<'_>) -> bool { + let mut inner = self.0.lock().unwrap(); + inner.update_if_needed(); + inner.unique_names.contains(name) + } + + /// Check if the unique name `name` owns at least one of the well-known names in `allowed_names`. + /// + /// Does not poll with `GetNameOwner` for well-known names that were already on the bus, + /// but have not already been populated by an earlier `check_owner()` call. + pub fn check_owner_no_poll( + &self, + name: &UniqueName<'_>, + allowed_names: &[WellKnownName<'_>], + ) -> bool { + let mut inner = self.0.lock().unwrap(); + + // Make sure latest events from stream have been processed + inner.update_if_needed(); + + if !inner.unique_names.contains(name) { + // If unique is no longer on bus, no longer a valid client + false + } else if !inner.enforce { + // If client exists, and we aren't enforcing owners, nothing + // more to check. + true + } else { + allowed_names + .iter() + .any(|n| inner.name_owners.get(n).and_then(|x| x.as_ref()) == Some(name)) + } + } + + /// Lazily populate `name_owenrs` with owners of well known names + /// from `names` that were advertised by `ListNames`. + /// + /// This way we avoid calling `GetNameOwner` for every single well-known + /// name on the bus. Since there don't seem to be a way to populate that + /// without a call per name. + async fn poll_name_owners(&self, names: &[WellKnownName<'_>]) { + let mut futures = { + let inner = self.0.lock().unwrap(); + names + .iter() + .filter(|n| inner.name_owners.get(*n) == Some(&None)) + .map(|n| { + let dbus = inner.dbus.clone(); + async move { (n, dbus.get_name_owner(BusName::WellKnown(n.as_ref())).await) } + }) + .collect::>() + }; + while let Some((n, owner)) = futures.next().await { + let mut inner = self.0.lock().unwrap(); + if let Ok(owner) = owner { + // If values is no longer `None`, this has raced against the + // name owner changed stream. In which case, keep that value. + if inner.name_owners.get(n) == Some(&None) { + inner.name_owners.insert(n.to_owned(), Some(owner.into())); + } + } + } + } + + /// Check if the unique name `name` owns at least one of the well-known names in `allowed_names`. + pub async fn check_owner( + &self, + name: &UniqueName<'_>, + allowed_names: &[WellKnownName<'_>], + ) -> bool { + self.poll_name_owners(allowed_names).await; + self.check_owner_no_poll(name, allowed_names) + } +} diff --git a/src/dbus/power.rs b/src/dbus/power.rs new file mode 100644 index 0000000..a20e566 --- /dev/null +++ b/src/dbus/power.rs @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +//! # DBus interface proxy for: `com.system76.PowerDaemon` +//! +//! This code was generated by `zbus-xmlgen` `3.0.0` from DBus introspection data. +//! Source: `Interface '/com/system76/PowerDaemon' from service 'com.system76.PowerDaemon' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the +//! [Writing a client proxy](https://dbus.pages.freedesktop.org/zbus/client.html) +//! section of the zbus documentation. +//! +//! This DBus object implements +//! [standard DBus interfaces](https://dbus.freedesktop.org/doc/dbus-specification.html), +//! (`org.freedesktop.DBus.*`) for which the following zbus proxies can be used: +//! +//! * [`zbus::fdo::IntrospectableProxy`] +//! +//! …consequently `zbus-xmlgen` did not generate code for the above interfaces. + +use zbus::Connection; + +#[zbus::proxy( + interface = "com.system76.PowerDaemon", + default_service = "com.system76.PowerDaemon", + default_path = "/com/system76/PowerDaemon" +)] +pub trait PowerDaemon { + /// Balanced method + fn balanced(&self) -> zbus::Result<()>; + + /// Battery method + fn battery(&self) -> zbus::Result<()>; + + /// GetChargeProfiles method + fn get_charge_profiles( + &self, + ) -> zbus::Result>>; + + /// GetChargeThresholds method + fn get_charge_thresholds(&self) -> zbus::Result<(u8, u8)>; + + /// GetDefaultGraphics method + fn get_default_graphics(&self) -> zbus::Result; + + /// GetExternalDisplaysRequireDGPU method + fn get_external_displays_require_dgpu(&self) -> zbus::Result; + + /// GetGraphics method + fn get_graphics(&self) -> zbus::Result; + + /// GetGraphicsPower method + fn get_graphics_power(&self) -> zbus::Result; + + /// GetProfile method + fn get_profile(&self) -> zbus::Result; + + /// GetSwitchable method + fn get_switchable(&self) -> zbus::Result; + + /// Performance method + fn performance(&self) -> zbus::Result<()>; + + /// SetChargeThresholds method + fn set_charge_thresholds(&self, thresholds: &(u8, u8)) -> zbus::Result<()>; + + /// SetGraphics method + fn set_graphics(&self, vendor: &str) -> zbus::Result<()>; + + /// SetGraphicsPower method + fn set_graphics_power(&self, power: bool) -> zbus::Result<()>; + + /// HotPlugDetect signal + #[zbus(signal)] + fn hot_plug_detect(&self, port: u64) -> zbus::Result<()>; + + /// PowerProfileSwitch signal + #[zbus(signal)] + fn power_profile_switch(&self, profile: &str) -> zbus::Result<()>; +} + +pub async fn init(conn: &Connection) -> anyhow::Result> { + let proxy = PowerDaemonProxy::new(conn).await?; + proxy.0.introspect().await?; + Ok(proxy) +} diff --git a/src/debug.rs b/src/debug.rs new file mode 100644 index 0000000..e50596c --- /dev/null +++ b/src/debug.rs @@ -0,0 +1,351 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{ + State, + backend::kms::Timings, + shell::focus::target::{KeyboardFocusTarget, PointerFocusTarget, PointerFocusToplevel}, +}; +use egui::Color32; +use smithay::{ + backend::{ + drm::DrmNode, + renderer::{ + element::texture::TextureRenderElement, + gles::{GlesError, GlesTexture}, + glow::GlowRenderer, + }, + }, + desktop::WindowSurface, + input::{Seat, keyboard::xkb}, + reexports::wayland_server::Resource, + utils::{Logical, Rectangle, Time}, +}; +use smithay_egui::EguiState; + +pub const ELEMENTS_COLOR: Color32 = Color32::from_rgb(70, 198, 115); +pub const RENDER_COLOR: Color32 = Color32::from_rgb(29, 114, 58); +pub const SUBMITTED_COLOR: Color32 = Color32::from_rgb(253, 178, 39); +pub const DISPLAY_COLOR: Color32 = Color32::from_rgb(41, 184, 209); + +pub fn fps_ui( + gpu: Option<&DrmNode>, + debug_active: bool, + seats: &[Seat], + renderer: &mut GlowRenderer, + state: &EguiState, + timings: &Timings, + area: Rectangle, + scale: f64, +) -> Result, GlesError> { + use egui_plot::{Bar, BarChart, Legend, Plot}; + + let (max, min, avg, avg_fps) = ( + timings.max_rendertime().as_secs_f64(), + timings.min_rendertime().as_secs_f64(), + timings.avg_rendertime().as_secs_f64(), + timings.avg_fps(), + ); + + let amount = avg_fps.round() as usize * 2; + let (max_disp, min_disp, avg_disp) = ( + timings.max_frametime(amount).as_secs_f64(), + timings.min_frametime(amount).as_secs_f64(), + timings + .avg_frametime(amount) + .unwrap_or_default() + .as_secs_f64(), + ); + + let ((bars_elements, bars_render), (bars_submitted, bars_displayed)): ( + (Vec, Vec), + (Vec, Vec), + ) = timings + .previous_frames + .iter() + .rev() + .take(amount) + .rev() + .enumerate() + .map(|(i, frame)| { + let elements_val = frame.render_duration_elements.as_secs_f64(); + let render_val = frame.render_duration_draw.as_secs_f64(); + let submitted_val = + Time::elapsed(&frame.render_start, frame.presentation_submitted).as_secs_f64(); + let displayed_val = + Time::elapsed(&frame.render_start, frame.presentation_presented).as_secs_f64(); + + let transformed_elements = + ((elements_val - min_disp) / (max_disp - min_disp) * 255.0).round() as u8; + let transformed_render = + ((render_val - min_disp) / (max_disp - min_disp) * 255.0).round() as u8; + let transformed_submitted = + ((submitted_val - min_disp) / (max_disp - min_disp) * 255.0).round() as u8; + let transformed_displayed = + ((displayed_val - min_disp) / (max_disp - min_disp) * 255.0).round() as u8; + ( + ( + Bar::new(i as f64, transformed_elements as f64).fill(ELEMENTS_COLOR), + Bar::new(i as f64, transformed_render as f64).fill(RENDER_COLOR), + ), + ( + Bar::new(i as f64, transformed_submitted as f64).fill(SUBMITTED_COLOR), + Bar::new(i as f64, transformed_displayed as f64).fill(DISPLAY_COLOR), + ), + ) + }) + .unzip(); + + state.render( + |ctx| { + egui::Area::new("main".into()) + .anchor(egui::Align2::LEFT_TOP, (10.0, 10.0)) + .show(ctx, |ui| { + ui.label(format!( + "cosmic-comp version {}", + std::env!("CARGO_PKG_VERSION") + )); + if let Some(hash) = std::option_env!("GIT_HASH").and_then(|x| x.get(0..10)) { + ui.label(format!(": {hash}")); + } + + if !debug_active { + ui.label("Press Super+Ctrl+Escape for debug overlay"); + } else { + ui.set_max_width(300.0); + ui.separator(); + + if let Some(gpu) = gpu { + ui.horizontal(|ui| { + let resp = ui.label( + egui::RichText::new(format!("renderD{}", gpu.minor())).code(), + ); + if let Ok(vendor) = std::fs::read_to_string(format!( + "/sys/class/drm/renderD{}/device/vendor", + gpu.minor() + )) && let Some(img) = match vendor.trim() { + "0x10de" => { + Some(egui::include_image!("../resources/icons/nvidia.svg")) + } + "0x1002" => { + Some(egui::include_image!("../resources/icons/amd.svg")) + } + "0x8086" => { + Some(egui::include_image!("../resources/icons/intel.svg")) + } + _ => None, + } { + ui.add(egui::Image::new(img).max_height(resp.rect.height())); + } + }); + } + ui.label(egui::RichText::new(format!("VRR: {}", timings.vrr())).code()); + ui.label(egui::RichText::new(format!("FPS: {:>7.3}", avg_fps)).heading()); + ui.label("Render Times:"); + ui.label(egui::RichText::new(format!("avg: {:>7.6}", avg)).code()); + ui.label(egui::RichText::new(format!("min: {:>7.6}", min)).code()); + ui.label(egui::RichText::new(format!("max: {:>7.6}", max)).code()); + ui.label("Frame Times:"); + ui.label(egui::RichText::new(format!("avg: {:>7.6}", avg_disp)).code()); + ui.label(egui::RichText::new(format!("min: {:>7.6}", min_disp)).code()); + ui.label(egui::RichText::new(format!("max: {:>7.6}", max_disp)).code()); + + let elements_chart = BarChart::new(bars_elements.clone()).vertical(); + let render_chart = BarChart::new(bars_render.clone()) + .stack_on(&[&elements_chart]) + .vertical(); + let submitted_chart = BarChart::new(bars_submitted.clone()) + .stack_on(&[&elements_chart, &render_chart]) + .vertical(); + let display_chart = BarChart::new(bars_displayed.clone()) + .stack_on(&[&elements_chart, &render_chart, &submitted_chart]) + .vertical(); + + Plot::new("FPS") + .legend(Legend::default()) + .view_aspect(50.0) + .include_x(0.0) + .include_x(amount as f64) + .include_y(0.0) + .include_y(300) + .show_x(false) + .show(ui, |plot_ui| { + plot_ui.bar_chart(elements_chart); + plot_ui.bar_chart(render_chart); + plot_ui.bar_chart(submitted_chart); + plot_ui.bar_chart(display_chart); + }); + + ui.separator(); + ui.label(egui::RichText::new("Input States").heading()); + for seat in seats { + ui.label(egui::RichText::new(format!("\t{}", seat.name())).strong()); + if let Some(ptr) = seat.get_pointer() { + egui::Frame::NONE + .fill(egui::Color32::DARK_GRAY) + .corner_radius(5.) + .inner_margin(10.) + .show(ui, |ui| { + ui.label( + egui::RichText::new(format!( + "Pos: {:?}", + ptr.current_location() + )) + .code(), + ); + ui.label( + egui::RichText::new(format!( + "Focus: {}", + format_pointer_focus(ptr.current_focus()) + )) + .code(), + ); + ui.label( + egui::RichText::new(format!( + "Grabbed: {:?}", + ptr.is_grabbed() + )) + .code(), + ); + }); + } + if let Some(kbd) = seat.get_keyboard() { + egui::Frame::NONE + .fill(egui::Color32::DARK_GRAY) + .corner_radius(5.) + .inner_margin(10.) + .show(ui, |ui| { + let mut keysyms = format!( + "Keys: {:?}", + kbd.with_pressed_keysyms(|syms| syms + .into_iter() + .map(|k| xkb::keysym_get_name(k.modified_sym())) + .fold(String::new(), |mut list, val| { + list.push_str(&format!("{}, ", val)); + list + })) + ); + keysyms.truncate(keysyms.len().saturating_sub(2)); + ui.label(egui::RichText::new(keysyms).code()); + + let mods = kbd.modifier_state(); + ui.label( + egui::RichText::new(format!( + "Mods: Ctrl {} / Alt {} / Logo {} / Shift {}", + mods.ctrl, mods.alt, mods.logo, mods.shift, + )) + .code(), + ); + + ui.label( + egui::RichText::new(format!( + "Focus: {}", + format_keyboard_focus(kbd.current_focus()) + )) + .code(), + ); + ui.label( + egui::RichText::new(format!( + "Grabbed: {:?}", + kbd.is_grabbed() + )) + .code(), + ); + }); + } + } + } + }); + }, + renderer, + area, + scale, + 0.8, + ) +} + +fn format_pointer_focus(focus: Option) -> String { + use PointerFocusTarget::*; + + match focus { + Some(WlSurface { surface, toplevel }) => match toplevel { + Some(PointerFocusToplevel::Surface(window)) => { + format!( + "Window {} ({})", + match window.0.underlying_surface() { + WindowSurface::Wayland(_) => surface.id().protocol_id(), + WindowSurface::X11(x) => x.window_id(), + }, + window.title() + ) + } + Some(PointerFocusToplevel::Popup(_)) => { + format!("Popup {}", surface.id().protocol_id()) + } + _ => format!("Surface {}", surface.id().protocol_id()), + }, + Some(X11Surface { surface, toplevel }) => match toplevel { + Some(window) => { + format!("Window {} ({})", surface.window_id(), window.title()) + } + _ => format!("X11Surface {}", surface.window_id()), + }, + Some(StackUI(stack)) => format!( + "Stack SSD {} ({})", + match stack.active().0.underlying_surface() { + WindowSurface::Wayland(t) => t.wl_surface().id().protocol_id(), + WindowSurface::X11(x) => x.window_id(), + }, + stack.active().title() + ), + Some(WindowUI(window)) => format!( + "Window SSD {} ({})", + match window.surface().0.underlying_surface() { + WindowSurface::Wayland(t) => t.wl_surface().id().protocol_id(), + WindowSurface::X11(x) => x.window_id(), + }, + window.surface().title() + ), + Some(ResizeFork(_)) => String::from("Resize UI"), + Some(ZoomUI(_)) => String::from("Zoom UI"), + None => "None".to_string(), + } +} + +fn format_keyboard_focus(focus: Option) -> String { + use KeyboardFocusTarget::*; + + match focus { + Some(Element(x)) => match x { + x if x.is_stack() => format!( + "Stacked Window {} ({})", + match x.active_window().0.underlying_surface() { + WindowSurface::Wayland(t) => t.wl_surface().id().protocol_id(), + WindowSurface::X11(x) => x.window_id(), + }, + x.active_window().title() + ), + x if x.is_window() => format!( + "Window {} ({})", + match x.active_window().0.underlying_surface() { + WindowSurface::Wayland(t) => t.wl_surface().id().protocol_id(), + WindowSurface::X11(x) => x.window_id(), + }, + x.active_window().title() + ), + _ => unreachable!(), + }, + Some(Fullscreen(x)) => format!( + "Fullscreen {} ({})", + match x.0.underlying_surface() { + WindowSurface::Wayland(t) => t.wl_surface().id().protocol_id(), + WindowSurface::X11(x) => x.window_id(), + }, + x.title() + ), + Some(LayerSurface(x)) => format!("LayerSurface {}", x.wl_surface().id().protocol_id()), + Some(Popup(x)) => format!("Popup {}", x.wl_surface().id().protocol_id()), + Some(Group(_)) => "Window Group".to_string(), + Some(LockSurface(x)) => format!("LockSurface {}", x.wl_surface().id().protocol_id()), + None => "None".to_string(), + } +} diff --git a/src/hooks.rs b/src/hooks.rs new file mode 100644 index 0000000..c530704 --- /dev/null +++ b/src/hooks.rs @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::shell::element::stack::{ + CosmicStackInternal, DefaultDecorations as DefaultStackDecorations, Message as StackMessage, +}; +use crate::shell::element::window::{ + CosmicWindowInternal, DefaultDecorations as DefaultWindowDecorations, Message as WindowMessage, +}; +use std::sync::{Arc, OnceLock}; + +/// An _unstable_ interface to customize cosmic-comp at compile-time by providing +/// hooks to be run in specific code paths. +#[derive(Default, Debug, Clone)] +pub struct Hooks { + pub window_decorations: + Option + Send + Sync>>, + pub stack_decorations: + Option + Send + Sync>>, +} + +pub static HOOKS: OnceLock = OnceLock::new(); + +pub trait Decorations: std::fmt::Debug { + fn view(&self, state: &Internal) -> cosmic::Element<'_, Message>; +} + +impl Decorations + for Option + Send + Sync>> +{ + fn view(&self, window: &CosmicWindowInternal) -> cosmic::Element<'_, WindowMessage> { + match self { + None => DefaultWindowDecorations.view(window), + Some(deco) => deco.view(window), + } + } +} + +impl Decorations + for Option + Send + Sync>> +{ + fn view(&self, window: &CosmicStackInternal) -> cosmic::Element<'_, StackMessage> { + match self { + None => DefaultStackDecorations.view(window), + Some(deco) => deco.view(window), + } + } +} diff --git a/src/input/actions.rs b/src/input/actions.rs new file mode 100644 index 0000000..3d01769 --- /dev/null +++ b/src/input/actions.rs @@ -0,0 +1,1180 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{ + config::{Action, PrivateAction}, + shell::{ + FocusResult, InvalidWorkspaceIndex, MoveResult, SeatExt, Trigger, WorkspaceDelta, + focus::{FocusTarget, target::KeyboardFocusTarget}, + layout::tiling::SwapWindowGrab, + }, + utils::prelude::*, + wayland::{ + handlers::xdg_activation::ActivationContext, protocols::workspace::WorkspaceUpdateGuard, + }, +}; +use cosmic_comp_config::{TileBehavior, workspace::WorkspaceLayout}; +use cosmic_config::ConfigSet; +use cosmic_settings_config::shortcuts; +use cosmic_settings_config::shortcuts::action::{Direction, FocusDirection}; +use smithay::{ + input::{Seat, pointer::MotionEvent}, + utils::{Point, Serial}, +}; +#[cfg(not(feature = "debug"))] +use tracing::info; +use tracing::{error, warn}; + +use std::{os::unix::process::CommandExt, thread}; + +use super::gestures; + +fn propagate_by_default(action: &shortcuts::Action) -> bool { + matches!( + action, + shortcuts::Action::Focus(_) | shortcuts::Action::Move(_) + ) +} + +impl State { + pub fn handle_action( + &mut self, + action: Action, + seat: &Seat, + serial: Serial, + time: u32, + pattern: shortcuts::Binding, + direction: Option, + ) { + // TODO: Detect if started from login manager or tty, and only allow + // `Terminate` if it will return to login manager. + if self.common.shell.read().session_lock.is_some() + && !matches!( + action, + Action::Shortcut(shortcuts::Action::Terminate) + | Action::Shortcut(shortcuts::Action::Debug) + | Action::Shortcut(shortcuts::Action::System( + shortcuts::action::System::InputSourceSwitch + | shortcuts::action::System::BrightnessDown + | shortcuts::action::System::BrightnessUp + | shortcuts::action::System::VolumeLower + | shortcuts::action::System::VolumeRaise + | shortcuts::action::System::Mute, + )) + ) + { + return; + } + + match action { + Action::Shortcut(action) => { + let propagate = propagate_by_default(&action); + self.handle_shortcut_action( + action, seat, serial, time, pattern, direction, propagate, + ) + } + Action::Private(PrivateAction::Escape) => { + { + let mut shell = self.common.shell.write(); + shell.set_overview_mode(None, self.common.event_loop_handle.clone()); + shell.set_resize_mode( + None, + &self.common.config, + self.common.event_loop_handle.clone(), + ); + } + let pointer = seat.get_pointer().unwrap(); + let keyboard = seat.get_keyboard().unwrap(); + if pointer.is_grabbed() { + pointer.unset_grab(self, serial, time); + } + if keyboard.is_grabbed() { + keyboard.unset_grab(self); + } + } + + Action::Private(PrivateAction::Resizing(direction, edge, state)) => { + if state == shortcuts::State::Pressed { + self.common + .shell + .write() + .resize(seat, direction, edge.into()); + } else { + self.common + .shell + .write() + .finish_resize(direction, edge.into()); + } + } + } + } + + pub fn handle_swipe_action(&mut self, action: gestures::SwipeAction, seat: &Seat) { + use gestures::SwipeAction; + let wraparound: bool = self + .common + .config + .cosmic_conf + .workspaces + .workspace_wraparound; + + match action { + SwipeAction::NextWorkspace => { + let _ = to_next_workspace( + &mut self.common.shell.write(), + seat, + true, + wraparound, + &mut self.common.workspace_state.update(), + ); + } + SwipeAction::PrevWorkspace => { + let _ = to_previous_workspace( + &mut self.common.shell.write(), + seat, + true, + wraparound, + &mut self.common.workspace_state.update(), + ); + } + } + } + + #[profiling::function] + pub fn handle_shortcut_action( + &mut self, + action: shortcuts::Action, + seat: &Seat, + serial: Serial, + time: u32, + pattern: shortcuts::Binding, + direction: Option, + propagate: bool, + ) { + use shortcuts::Action; + + match action { + Action::Terminate => { + self.common.should_stop = true; + } + + #[cfg(feature = "debug")] + Action::Debug => { + let mut shell = self.common.shell.write(); + shell.debug_active = !shell.debug_active; + for mapped in shell.workspaces.spaces().flat_map(|w| w.mapped()) { + mapped.set_debug(shell.debug_active); + } + } + + #[cfg(not(feature = "debug"))] + Action::Debug => { + info!("Debug overlay not included in this build.") + } + + Action::Close => { + if let Some(focus_target) = seat.get_keyboard().unwrap().current_focus() { + self.common.shell.read().close_focused(&focus_target); + } + } + + Action::Workspace(key_num) => { + let current_output = seat.active_output(); + let workspace = match key_num { + 0 => 9, + x => x - 1, + }; + let _ = self.common.shell.write().activate( + ¤t_output, + workspace as usize, + WorkspaceDelta::new_shortcut(), + &mut self.common.workspace_state.update(), + ); + } + + Action::LastWorkspace => { + let current_output = seat.active_output(); + let mut shell = self.common.shell.write(); + let workspace = shell.workspaces.len(¤t_output).saturating_sub(1); + let _ = shell.activate( + ¤t_output, + workspace, + WorkspaceDelta::new_shortcut(), + &mut self.common.workspace_state.update(), + ); + } + + Action::NextWorkspace => { + if let Some(direction) = pattern.inferred_direction() + && (((direction == Direction::Left || direction == Direction::Right) + && self.common.config.cosmic_conf.workspaces.workspace_layout + == WorkspaceLayout::Vertical) + || ((direction == Direction::Up || direction == Direction::Down) + && self.common.config.cosmic_conf.workspaces.workspace_layout + == WorkspaceLayout::Horizontal)) + { + return; + } + + let next = to_next_workspace( + &mut self.common.shell.write(), + seat, + false, + self.common + .config + .cosmic_conf + .workspaces + .workspace_wraparound, + &mut self.common.workspace_state.update(), + ); + if next.is_err() + && propagate + && let Some(inferred) = pattern.inferred_direction() + { + self.handle_shortcut_action( + Action::SwitchOutput(inferred), + seat, + serial, + time, + pattern, + direction, + true, + ) + } + } + + Action::PreviousWorkspace => { + if let Some(direction) = pattern.inferred_direction() + && (((direction == Direction::Left || direction == Direction::Right) + && self.common.config.cosmic_conf.workspaces.workspace_layout + == WorkspaceLayout::Vertical) + || ((direction == Direction::Up || direction == Direction::Down) + && self.common.config.cosmic_conf.workspaces.workspace_layout + == WorkspaceLayout::Horizontal)) + { + return; + } + + let previous = to_previous_workspace( + &mut self.common.shell.write(), + seat, + false, + self.common + .config + .cosmic_conf + .workspaces + .workspace_wraparound, + &mut self.common.workspace_state.update(), + ); + if previous.is_err() + && propagate + && let Some(inferred) = pattern.inferred_direction() + { + self.handle_shortcut_action( + Action::SwitchOutput(inferred), + seat, + serial, + time, + pattern, + direction, + true, + ) + }; + } + + x @ Action::MoveToWorkspace(_) | x @ Action::SendToWorkspace(_) => { + let Some(focused_output) = seat.focused_output() else { + return; + }; + let follow = matches!(x, Action::MoveToWorkspace(_)); + let workspace = match x { + Action::MoveToWorkspace(0) | Action::SendToWorkspace(0) => 9, + Action::MoveToWorkspace(x) | Action::SendToWorkspace(x) => x - 1, + _ => unreachable!(), + }; + let res = self.common.shell.write().move_current( + seat, + (&focused_output, Some(workspace as usize)), + follow, + None, + &mut self.common.workspace_state.update(), + &self.common.event_loop_handle, + ); + if let Ok(Some((target, _point))) = res { + Shell::set_focus( + self, + Some(&target), + seat, + None, + matches!(x, Action::MoveToWorkspace(_)), + ); + } + } + + x @ Action::MoveToLastWorkspace | x @ Action::SendToLastWorkspace => { + let Some(focused_output) = seat.focused_output() else { + return; + }; + let mut shell = self.common.shell.write(); + let workspace = shell.workspaces.len(&focused_output).saturating_sub(1); + let res = shell.move_current( + seat, + (&focused_output, Some(workspace)), + matches!(x, Action::MoveToLastWorkspace), + None, + &mut self.common.workspace_state.update(), + &self.common.event_loop_handle, + ); + // If the active workspace changed, the cursor_follows_focus should probably be checked + if let Ok(Some((target, _point))) = res { + std::mem::drop(shell); + Shell::set_focus( + self, + Some(&target), + seat, + None, + matches!(x, Action::MoveToLastWorkspace), + ); + } + } + + x @ Action::MoveToNextWorkspace | x @ Action::SendToNextWorkspace => { + if let Some(direction) = pattern.inferred_direction() + && (((direction == Direction::Left || direction == Direction::Right) + && self.common.config.cosmic_conf.workspaces.workspace_layout + == WorkspaceLayout::Vertical) + || ((direction == Direction::Up || direction == Direction::Down) + && self.common.config.cosmic_conf.workspaces.workspace_layout + == WorkspaceLayout::Horizontal)) + { + return; + } + let Some(focused_output) = seat.focused_output() else { + return; + }; + + let res = { + let mut shell = self.common.shell.write(); + shell + .workspaces + .active_num(&focused_output) + .1 + .checked_add(1) + .ok_or(InvalidWorkspaceIndex) + .and_then(|workspace| { + shell.move_current( + seat, + (&focused_output, Some(workspace)), + matches!(x, Action::MoveToNextWorkspace), + direction, + &mut self.common.workspace_state.update(), + &self.common.event_loop_handle, + ) + }) + }; + + match res { + Ok(Some((target, _point))) => { + // If the active workspace changed, the cursor_follows_focus should probably be checked + Shell::set_focus( + self, + Some(&target), + seat, + None, + matches!(x, Action::MoveToNextWorkspace), + ); + } + Ok(None) => {} + Err(_) if propagate => { + if let Some(inferred) = pattern.inferred_direction() { + self.handle_shortcut_action( + if matches!(x, Action::MoveToNextWorkspace) { + Action::MoveToOutput(inferred) + } else { + Action::SendToOutput(inferred) + }, + seat, + serial, + time, + pattern, + direction, + true, + ) + } + } + Err(_) => { + let wraparound = self + .common + .config + .cosmic_conf + .workspaces + .workspace_wraparound; + + if wraparound { + // cycle through + self.handle_shortcut_action( + if matches!(x, Action::MoveToNextWorkspace) { + Action::MoveToWorkspace(1) + } else { + Action::SendToWorkspace(1) + }, + seat, + serial, + time, + pattern, + direction, + false, + ) + } + } + } + } + + x @ Action::MoveToPreviousWorkspace | x @ Action::SendToPreviousWorkspace => { + if let Some(direction) = pattern.inferred_direction() + && (((direction == Direction::Left || direction == Direction::Right) + && self.common.config.cosmic_conf.workspaces.workspace_layout + == WorkspaceLayout::Vertical) + || ((direction == Direction::Up || direction == Direction::Down) + && self.common.config.cosmic_conf.workspaces.workspace_layout + == WorkspaceLayout::Horizontal)) + { + return; + } + let Some(focused_output) = seat.focused_output() else { + return; + }; + + let res = { + let mut shell = self.common.shell.write(); + shell + .workspaces + .active_num(&focused_output) + .1 + .checked_sub(1) + .ok_or(InvalidWorkspaceIndex) + .and_then(|workspace| { + shell.move_current( + seat, + (&focused_output, Some(workspace)), + matches!(x, Action::MoveToPreviousWorkspace), + direction, + &mut self.common.workspace_state.update(), + &self.common.event_loop_handle, + ) + }) + }; + + match res { + Ok(Some((target, _point))) => { + Shell::set_focus( + self, + Some(&target), + seat, + None, + matches!(x, Action::MoveToPreviousWorkspace), + ); + } + Ok(None) => {} + Err(_) if propagate => { + if let Some(inferred) = pattern.inferred_direction() { + self.handle_shortcut_action( + if matches!(x, Action::MoveToPreviousWorkspace) { + Action::MoveToOutput(inferred) + } else { + Action::SendToOutput(inferred) + }, + seat, + serial, + time, + pattern, + direction, + true, + ) + } + } + Err(_) => { + let wraparound = self + .common + .config + .cosmic_conf + .workspaces + .workspace_wraparound; + + if wraparound { + // cycle through + self.handle_shortcut_action( + if matches!(x, Action::MoveToPreviousWorkspace) { + Action::MoveToLastWorkspace + } else { + Action::SendToLastWorkspace + }, + seat, + serial, + time, + pattern, + direction, + false, + ) + } + } + } + } + + Action::SwitchOutput(direction) => { + let current_output = seat.active_output(); + let mut shell = self.common.shell.write(); + + let next_output = shell.next_output(¤t_output, direction).cloned(); + + if let Some(next_output) = next_output { + let res = { + let mut workspace_guard = self.common.workspace_state.update(); + if propagate + && let Some((serial, prev_output, prev_idx)) = + shell.previous_workspace_idx.take() + && seat.last_modifier_change().is_some_and(|s| s == serial) + && prev_output == current_output + { + let _ = shell.activate( + ¤t_output, + prev_idx, + WorkspaceDelta::new_shortcut(), + &mut workspace_guard, + ); + } + + let idx = shell.workspaces.active_num(&next_output).1; + let res = shell.activate( + &next_output, + idx, + WorkspaceDelta::new_shortcut(), + &mut workspace_guard, + ); + seat.set_active_output(&next_output); + res + }; + + if let Ok(new_pos) = res { + let workspace = shell.workspaces.active(&next_output).unwrap().1; + let new_target = workspace + .focus_stack + .get(seat) + .last() + .cloned() + .map(Into::::into); + std::mem::drop(shell); + + let update_cursor = self.common.config.cosmic_conf.cursor_follows_focus; + Shell::set_focus(self, new_target.as_ref(), seat, None, update_cursor); + + if let Some(ptr) = seat.get_pointer() { + // Update cursor position if `set_focus` didn't already + if !update_cursor { + ptr.motion( + self, + None, + &MotionEvent { + location: new_pos.to_f64().as_logical(), + serial, + time, + }, + ); + } + ptr.frame(self); + } + } + } + } + + #[allow(deprecated)] + Action::NextOutput => { + warn!("Skipping deprecated shortcut NextOutput"); + } + + #[allow(deprecated)] + Action::PreviousOutput => { + warn!("Skipping deprecated shortcut PreviousOutput"); + } + + action @ Action::MoveToOutput(_) | action @ Action::SendToOutput(_) => { + let is_move_action = matches!(action, Action::MoveToOutput(_)); + let direction = match action { + Action::MoveToOutput(dir) => dir, + Action::SendToOutput(dir) => dir, + _ => unreachable!(), + }; + + let Some(focused_output) = seat.focused_output() else { + return; + }; + let mut shell = self.common.shell.write(); + let next_output = shell.next_output(&focused_output, direction).cloned(); + + if let Some(next_output) = next_output { + let res = { + let mut workspace_guard = self.common.workspace_state.update(); + let res = shell.move_current( + seat, + (&next_output, None), + is_move_action, + Some(direction), + &mut workspace_guard, + &self.common.event_loop_handle, + ); + + if is_move_action + && propagate + && let Some((_, prev_output, prev_idx)) = + shell.previous_workspace_idx.take() + && prev_output == focused_output + { + let _ = shell.activate( + &focused_output, + prev_idx, + WorkspaceDelta::new_shortcut(), + &mut workspace_guard, + ); + } + res + }; + + if let Ok(Some((target, new_pos))) = res { + std::mem::drop(shell); + Shell::set_focus(self, Some(&target), seat, None, is_move_action); + if let Some(ptr) = seat.get_pointer() { + ptr.motion( + self, + None, + &MotionEvent { + location: new_pos.to_f64().as_logical(), + serial, + time, + }, + ); + ptr.frame(self); + } + } + } + } + + #[allow(deprecated)] + Action::MoveToNextOutput | Action::SendToNextOutput => { + warn!("Ignoring deprecated action Move/SendToNextOutput"); + } + + #[allow(deprecated)] + Action::MoveToPreviousOutput | Action::SendToPreviousOutput => { + warn!("Ignoring deprecated action Move/SendToPreviousOutput"); + } + + Action::MigrateWorkspaceToOutput(direction) => { + let active_output = seat.active_output(); + let (active, next_output) = { + let shell = self.common.shell.read(); + + ( + shell.active_space(&active_output).unwrap().handle, + shell.next_output(&active_output, direction).cloned(), + ) + }; + + if let Some(next_output) = next_output { + let mut shell = self.common.shell.write(); + let mut workspace_state = self.common.workspace_state.update(); + shell.workspaces.migrate_workspace( + &active_output, + &next_output, + &active, + &mut workspace_state, + ); + // Activate workspace on new set, and set that output as active + if let Some(new_idx) = shell + .workspaces + .sets + .get(&next_output) + .and_then(|set| set.workspaces.iter().position(|w| w.handle == active)) + { + let res = shell.activate( + &next_output, + new_idx, + WorkspaceDelta::new_shortcut(), + &mut workspace_state, + ); + drop(workspace_state); + drop(shell); + if res.is_ok() { + self.handle_shortcut_action( + Action::SwitchOutput(direction), + seat, + serial, + time, + pattern, + Some(direction), + true, + ) + } + } + } + } + + #[allow(deprecated)] + Action::MigrateWorkspaceToNextOutput => { + warn!("Ignoring deprecated action MigrateWorkspaceToNextOutput"); + } + + #[allow(deprecated)] + Action::MigrateWorkspaceToPreviousOutput => { + warn!("Ignoring deprecated action MigrateWorkspaceToPreviousOutput"); + } + + Action::Focus(focus) => { + let result = self.common.shell.read().next_focus(focus, seat); + + match result { + FocusResult::None => { + let dir = match focus { + FocusDirection::Down => Some(Direction::Down), + FocusDirection::Up => Some(Direction::Up), + FocusDirection::Left => Some(Direction::Left), + FocusDirection::Right => Some(Direction::Right), + _ => None, + }; + + if let Some(direction) = dir { + if let Some(last_mod_serial) = seat.last_modifier_change() { + let mut shell = self.common.shell.write(); + if !shell + .previous_workspace_idx + .as_ref() + .is_some_and(|(serial, _, _)| *serial == last_mod_serial) + { + let current_output = seat.active_output(); + let workspace_idx = + shell.workspaces.active_num(¤t_output).1; + shell.previous_workspace_idx = Some(( + last_mod_serial, + current_output.downgrade(), + workspace_idx, + )); + } + } + + let action = match ( + direction, + self.common.config.cosmic_conf.workspaces.workspace_layout, + ) { + (Direction::Left, WorkspaceLayout::Horizontal) + | (Direction::Up, WorkspaceLayout::Vertical) => { + Action::PreviousWorkspace + } + (Direction::Right, WorkspaceLayout::Horizontal) + | (Direction::Down, WorkspaceLayout::Vertical) => { + Action::NextWorkspace + } + _ => Action::SwitchOutput(direction), + }; + + self.handle_shortcut_action( + action, + seat, + serial, + time, + pattern, + Some(direction), + true, + ) + } + } + FocusResult::Handled => {} + FocusResult::Some(target) => { + Shell::set_focus(self, Some(&target), seat, None, true); + } + } + } + + Action::Move(direction) => { + let res = self + .common + .shell + .write() + .move_current_element(direction, seat); + match res { + MoveResult::MoveFurther(_move_further) => { + if let Some(last_mod_serial) = seat.last_modifier_change() { + let mut shell = self.common.shell.write(); + if !shell + .previous_workspace_idx + .as_ref() + .is_some_and(|(serial, _, _)| *serial == last_mod_serial) + { + let current_output = seat.active_output(); + let workspace_idx = shell.workspaces.active_num(¤t_output).1; + shell.previous_workspace_idx = Some(( + last_mod_serial, + current_output.downgrade(), + workspace_idx, + )); + } + } + + let action = match ( + direction, + self.common.config.cosmic_conf.workspaces.workspace_layout, + ) { + (Direction::Left, WorkspaceLayout::Horizontal) + | (Direction::Up, WorkspaceLayout::Vertical) => { + Action::MoveToPreviousWorkspace + } + (Direction::Right, WorkspaceLayout::Horizontal) + | (Direction::Down, WorkspaceLayout::Vertical) => { + Action::MoveToNextWorkspace + } + _ => Action::MoveToOutput(direction), + }; + + self.handle_shortcut_action( + action, + seat, + serial, + time, + pattern, + Some(direction), + true, + ) + } + MoveResult::ShiftFocus(shift) => { + Shell::set_focus(self, Some(&shift), seat, None, true); + } + _ => { + let current_output = seat.active_output(); + let mut shell = self.common.shell.write(); + let workspace = shell.active_space(¤t_output).unwrap(); + if let Some(FocusTarget::Window(focused_window)) = + workspace.focus_stack.get(seat).last() + && workspace.is_tiled(&focused_window.active_window()) + { + shell.set_overview_mode( + Some(Trigger::KeyboardMove(pattern.modifiers)), + self.common.event_loop_handle.clone(), + ); + } + } + } + } + + Action::SwapWindow => { + let Some(focused_output) = seat.focused_output() else { + return; + }; + let mut shell = self.common.shell.write(); + + let workspace = shell.active_space_mut(&focused_output).unwrap(); + let keyboard_handle = seat.get_keyboard().unwrap(); + + if let Some(focus) = keyboard_handle.current_focus() + && let Some(descriptor) = workspace.node_desc(focus) + { + let grab = SwapWindowGrab::new(seat.clone(), descriptor.clone()); + drop(shell); + keyboard_handle.set_grab(self, grab, serial); + let mut shell = self.common.shell.write(); + shell.set_overview_mode( + Some(Trigger::KeyboardSwap(pattern, descriptor)), + self.common.event_loop_handle.clone(), + ); + } + } + + Action::Minimize => { + let mut shell = self.common.shell.write(); + if let Some(focused_window) = seat + .get_keyboard() + .unwrap() + .current_focus() + .and_then(|f| f.active_window()) + { + shell.minimize_request(&focused_window); + } + } + + Action::Maximize => { + let mut shell = self.common.shell.write(); + if let Some(KeyboardFocusTarget::Element(window)) = + seat.get_keyboard().unwrap().current_focus() + { + shell.maximize_toggle(&window, seat, &self.common.event_loop_handle); + } + } + + Action::Fullscreen => { + let Some(focused_output) = seat.focused_output() else { + return; + }; + let mut shell = self.common.shell.write(); + match seat.get_keyboard().unwrap().current_focus() { + Some(KeyboardFocusTarget::Element(window)) => { + if let Some(target) = shell.fullscreen_request( + &window.active_window(), + focused_output, + &self.common.event_loop_handle, + ) { + std::mem::drop(shell); + Shell::set_focus(self, Some(&target), seat, Some(serial), true); + } + } + Some(KeyboardFocusTarget::Fullscreen(surface)) => { + if let Some(target) = + shell.unfullscreen_request(&surface, &self.common.event_loop_handle) + { + std::mem::drop(shell); + Shell::set_focus(self, Some(&target), seat, Some(serial), true); + } + } + _ => {} + } + } + + Action::Resizing(direction) => self.common.shell.write().set_resize_mode( + Some((pattern, direction)), + &self.common.config, + self.common.event_loop_handle.clone(), + ), + // NOTE: implementation currently assumes actions that apply to outputs should apply to the active output + // rather than the output that has keyboard focus + Action::ToggleOrientation => { + let output = seat.active_output(); + let mut shell = self.common.shell.write(); + let workspace = shell.active_space_mut(&output).unwrap(); + workspace.tiling_layer.update_orientation(None, seat); + } + + Action::Orientation(orientation) => { + let output = seat.active_output(); + let mut shell = self.common.shell.write(); + let workspace = shell.active_space_mut(&output).unwrap(); + workspace + .tiling_layer + .update_orientation(Some(orientation), seat); + } + + Action::ToggleStacking => { + let res = self + .common + .shell + .write() + .toggle_stacking_focused(seat, &self.common.event_loop_handle); + if let Some(new_focus) = res { + Shell::set_focus(self, Some(&new_focus), seat, Some(serial), false); + } + } + + Action::ToggleTiling => { + if matches!( + self.common.config.cosmic_conf.autotile_behavior, + TileBehavior::Global + ) { + let autotile = !self.common.config.cosmic_conf.autotile; + self.common.config.cosmic_conf.autotile = autotile; + + { + let mut shell = self.common.shell.write(); + let shell_ref = &mut *shell; + shell_ref.workspaces.update_autotile( + self.common.config.cosmic_conf.autotile, + &mut self.common.workspace_state.update(), + shell_ref.seats.iter(), + ); + } + let config = self.common.config.cosmic_helper.clone(); + thread::spawn(move || { + if let Err(err) = config.set("autotile", autotile) { + error!(?err, "Failed to update autotile key"); + } + }); + } else { + let output = seat.active_output(); + let mut shell = self.common.shell.write(); + let workspace = shell.workspaces.active_mut(&output).unwrap(); + let mut guard = self.common.workspace_state.update(); + workspace.toggle_tiling(seat, &mut guard); + } + } + + Action::ToggleWindowFloating => { + let Some(output) = seat.focused_output() else { + return; + }; + let mut shell = self.common.shell.write(); + let workspace = shell.active_space_mut(&output).unwrap(); + workspace.toggle_floating_window_focused(seat); + } + + Action::ToggleSticky => { + self.common.shell.write().toggle_sticky_current(seat); + } + + // Gets the configured command for a given system action. + Action::System(system) => { + if let Some(command) = self.common.config.system_actions.get(&system) { + self.spawn_command(command.clone()); + } + } + + Action::Spawn(command) => self.spawn_command(command), + + x @ Action::ZoomIn | x @ Action::ZoomOut => { + let change = { + let increment = + self.common.config.cosmic_conf.accessibility_zoom.increment as f64 / 100.0; + match x { + Action::ZoomIn => increment, + Action::ZoomOut => -increment, + _ => unreachable!(), + } + }; + + self.update_zoom(seat, change, true); + } + + // Do nothing + Action::Disable => (), + } + } + + pub fn spawn_command(&mut self, command: String) { + let mut shell = self.common.shell.write(); + + let (token, data) = self.common.xdg_activation_state.create_external_token(None); + let (token, data) = (token.clone(), data.clone()); + + let output = shell.seats.last_active().active_output(); + let workspace = shell.active_space_mut(&output).unwrap(); + let handle = workspace.handle; + std::mem::drop(shell); + data.user_data + .insert_if_missing(move || ActivationContext::Workspace(handle)); + + let wayland_display = self.common.socket.clone(); + let display = self + .common + .xwayland_state + .as_ref() + .map(|s| format!(":{}", s.display)) + .unwrap_or_default(); + + let mut cmd = std::process::Command::new("/bin/sh"); + + cmd.arg("-c") + .arg(&command) + .env("WAYLAND_DISPLAY", &wayland_display) + .env("DISPLAY", &display) + .env("XDG_ACTIVATION_TOKEN", &*token) + .env("DESKTOP_STARTUP_ID", &*token) + .env_remove("COSMIC_SESSION_SOCK"); + unsafe { + cmd.pre_exec(|| { + crate::utils::rlimit::restore_nofile_limit(); + Ok(()) + }) + }; + + std::thread::spawn(move || match cmd.spawn() { + Ok(mut child) => { + let _res = child.wait(); + } + Err(err) => { + tracing::warn!(?err, "Failed to spawn \"{}\"", command); + } + }); + } + + pub fn update_zoom(&mut self, seat: &Seat, change: f64, animate: bool) { + let output = seat.active_output(); + let mut shell = self.common.shell.write(); + let (zoom_seat, current_level) = shell + .zoom_state() + .map(|state| (state.current_seat(), state.current_level(&output))) + .unwrap_or_else(|| (seat.clone(), 1.0)); + + if current_level == 1. && change <= 0. { + return; + } + + if zoom_seat == *seat { + let new_level = (current_level + change).max(1.0); + shell.trigger_zoom( + seat, + Some(&output), + new_level, + &self.common.config.cosmic_conf.accessibility_zoom, + animate, + &self.common.event_loop_handle, + ); + } + } +} + +fn to_next_workspace( + shell: &mut Shell, + seat: &Seat, + gesture: bool, + wraparound: bool, + workspace_state: &mut WorkspaceUpdateGuard<'_, State>, +) -> Result, InvalidWorkspaceIndex> { + let current_output = seat.active_output(); + let active = shell.workspaces.active_num(¤t_output).1; + let mut workspace = active.checked_add(1).ok_or(InvalidWorkspaceIndex)?; + + if workspace >= shell.workspaces.len(¤t_output) { + workspace = if wraparound { 0 } else { active } + } + if workspace == active { + return Err(InvalidWorkspaceIndex); + } + + shell.activate( + ¤t_output, + workspace, + if gesture { + WorkspaceDelta::new_gesture(true) + } else { + WorkspaceDelta::new_shortcut() + }, + workspace_state, + ) +} + +fn to_previous_workspace( + shell: &mut Shell, + seat: &Seat, + gesture: bool, + wraparound: bool, + workspace_state: &mut WorkspaceUpdateGuard<'_, State>, +) -> Result, InvalidWorkspaceIndex> { + let current_output = seat.active_output(); + let active = shell.workspaces.active_num(¤t_output).1; + let workspace = active.checked_sub(1).unwrap_or(if wraparound { + shell + .workspaces + .len(¤t_output) + .checked_sub(1) + .ok_or(InvalidWorkspaceIndex)? + } else { + 0 + }); + + if workspace == active { + return Err(InvalidWorkspaceIndex); + } + + shell.activate( + ¤t_output, + workspace, + if gesture { + WorkspaceDelta::new_gesture(false) + } else { + WorkspaceDelta::new_shortcut() + }, + workspace_state, + ) +} diff --git a/src/input/gestures/mod.rs b/src/input/gestures/mod.rs new file mode 100644 index 0000000..6c9c2f3 --- /dev/null +++ b/src/input/gestures/mod.rs @@ -0,0 +1,173 @@ +use cosmic_settings_config::shortcuts::action::Direction; +use smithay::utils::{Logical, Point}; +use std::{collections::VecDeque, time::Duration}; +use tracing::trace; + +const HISTORY_LIMIT: Duration = Duration::from_millis(150); +const DECELERATION_TOUCHPAD: f64 = 0.997; + +#[derive(Debug, Clone, Copy)] +pub struct SwipeEvent { + delta: f64, + timestamp: Duration, +} + +#[derive(Debug, Clone, Copy)] +pub enum SwipeAction { + NextWorkspace, + PrevWorkspace, +} + +#[derive(Debug, Clone)] +pub struct GestureState { + pub fingers: u32, + pub direction: Option, + pub action: Option, + pub delta: f64, + // Delta tracking inspired by Niri (GPL-3.0) https://github.com/YaLTeR/niri/tree/v0.1.3 + pub history: VecDeque, +} + +impl GestureState { + pub fn new(fingers: u32) -> Self { + GestureState { + fingers, + direction: None, + action: None, + delta: 0.0, + history: VecDeque::new(), + } + } + + pub fn update(&mut self, movement: Point, timestamp: Duration) -> bool { + let first_update = self.direction.is_none(); + + if first_update { + // Find proper direction + self.direction = if movement.x.abs() > movement.y.abs() { + if movement.x > 0.0 { + Some(Direction::Right) + } else { + Some(Direction::Left) + } + } else if movement.y > 0.0 { + Some(Direction::Down) + } else { + Some(Direction::Up) + } + } + + let delta = match self.direction { + Some(Direction::Left) => -movement.x, + Some(Direction::Right) => movement.x, + Some(Direction::Up) => -movement.y, + Some(Direction::Down) => movement.y, + None => 0.0, + }; + + self.push(delta, timestamp); + first_update + } + + /// Pushes a new reading into the tracker. + fn push(&mut self, delta: f64, timestamp: Duration) { + // For the events that we care about, timestamps should always increase + // monotonically. + if let Some(last) = self.history.back() + && timestamp < last.timestamp + { + trace!( + "ignoring event with timestamp {timestamp:?} earlier than last {:?}", + last.timestamp + ); + return; + } + + self.history.push_back(SwipeEvent { delta, timestamp }); + self.delta += delta; + + self.trim_history(); + } + + /// Computes the current gesture velocity. + pub fn velocity(&self) -> f64 { + let (Some(first), Some(last)) = (self.history.front(), self.history.back()) else { + return 0.; + }; + + let total_time = (last.timestamp - first.timestamp).as_secs_f64(); + if total_time == 0. { + return 0.; + } + + let total_delta = self.history.iter().map(|event| event.delta).sum::(); + total_delta / total_time + } + + /// Computes the gesture end position after decelerating to a halt. + pub fn projected_end_pos(&self) -> f64 { + let vel = self.velocity(); + self.delta - vel / (1000. * DECELERATION_TOUCHPAD.ln()) + } + + fn trim_history(&mut self) { + let Some(&SwipeEvent { timestamp, .. }) = self.history.back() else { + return; + }; + + while let Some(first) = self.history.front() { + if timestamp <= first.timestamp + HISTORY_LIMIT { + break; + } + + let _ = self.history.pop_front(); + } + } +} + +impl Default for GestureState { + fn default() -> Self { + GestureState::new(0) + } +} + +// rubber_band.rs from Niri (GPL-3.0) https://github.com/YaLTeR/niri/blob/db49deb7fd2fbe805ceec060aa4dec65009ad7a7/src/rubber_band.rs +#[derive(Debug, Clone, Copy)] +pub struct RubberBand { + pub stiffness: f64, + pub limit: f64, +} + +impl RubberBand { + pub fn band(&self, x: f64) -> f64 { + let c = self.stiffness; + let d = self.limit; + + (1. - (1. / (x * c / d + 1.))) * d + } + + pub fn derivative(&self, x: f64) -> f64 { + let c = self.stiffness; + let d = self.limit; + + c * d * d / (c * x + d).powi(2) + } + + pub fn clamp(&self, min: f64, max: f64, x: f64) -> f64 { + let clamped = x.clamp(min, max); + let sign = if x < clamped { -1. } else { 1. }; + let diff = (x - clamped).abs(); + + clamped + sign * self.band(diff) + } + + pub fn clamp_derivative(&self, min: f64, max: f64, x: f64) -> f64 { + if min <= x && x <= max { + return 1.; + } + + let clamped = x.clamp(min, max); + let diff = (x - clamped).abs(); + self.derivative(diff) + } +} diff --git a/src/input/mod.rs b/src/input/mod.rs new file mode 100644 index 0000000..39bff9d --- /dev/null +++ b/src/input/mod.rs @@ -0,0 +1,2557 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{ + backend::render::{ElementFilter, cursor::notify_cursor_activity}, + config::{ + Action, Config, PrivateAction, + key_bindings::{ + cosmic_keystate_from_smithay, cosmic_modifiers_eq_smithay, + cosmic_modifiers_from_smithay, + }, + }, + input::gestures::{GestureState, SwipeAction}, + shell::{ + LastModifierChange, SeatExt, Trigger, + focus::{ + Stage, render_input_order, + target::{KeyboardFocusTarget, PointerFocusTarget}, + }, + grabs::{ReleaseMode, ResizeEdge}, + layout::{ + floating::ResizeGrabMarker, + tiling::{NodeDesc, SwapWindowGrab, TilingLayout}, + }, + zoom::ZoomState, + }, + utils::{prelude::*, quirks::workspace_overview_is_open}, + wayland::handlers::{ + image_copy_capture::{SessionHolder, cursor_capture_constraints}, + xwayland_keyboard_grab::XWaylandGrabSeat, + }, +}; +use calloop::{ + RegistrationToken, + timer::{TimeoutAction, Timer}, +}; +use cosmic_comp_config::{NumlockState, workspace::WorkspaceLayout}; +use cosmic_settings_config::shortcuts; +use cosmic_settings_config::shortcuts::action::{Direction, ResizeDirection}; +#[cfg(feature = "logind")] +use smithay::backend::input::{Switch, SwitchState, SwitchToggleEvent}; +use smithay::{ + backend::input::{ + AbsolutePositionEvent, Axis, AxisRelativeDirection, AxisSource, Device, DeviceCapability, + GestureBeginEvent, GestureEndEvent, GesturePinchUpdateEvent as _, + GestureSwipeUpdateEvent as _, InputBackend, InputEvent, KeyState, KeyboardKeyEvent, + PointerAxisEvent, ProximityState, TabletToolButtonEvent, TabletToolEvent, + TabletToolProximityEvent, TabletToolTipEvent, TabletToolTipState, TouchEvent, + }, + desktop::{PopupKeyboardGrab, WindowSurfaceType, utils::under_from_surface_tree}, + input::{ + Seat, + keyboard::{FilterResult, KeysymHandle, ModifiersState}, + pointer::{ + AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, + GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, + GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, MotionEvent, + PointerGrab, PointerHandle, RelativeMotionEvent, + }, + touch::{DownEvent, MotionEvent as TouchMotionEvent, UpEvent}, + }, + output::Output, + reexports::{ + input::Device as InputDevice, + wayland_server::{Resource as _, protocol::wl_surface::WlSurface}, + }, + utils::{Clock, Logical, Monotonic, Point, Rectangle, SERIAL_COUNTER, Serial}, + wayland::{ + compositor::CompositorHandler, + image_copy_capture::CursorSessionRef, + keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitorSeat, + pointer_constraints::{PointerConstraint, with_pointer_constraint}, + seat::WaylandFocus, + tablet_manager::{TabletDescriptor, TabletSeatTrait}, + }, +}; +use tracing::{error, trace}; +use xkbcommon::xkb::{Keycode, Keysym}; + +use std::{ + any::Any, + borrow::Cow, + cell::RefCell, + collections::HashSet, + ops::ControlFlow, + time::{Duration, Instant}, +}; + +pub mod actions; +pub mod gestures; + +/// Used for debouncing focus updates due to pointer motion, if after the focus change is +/// triggered the event will cancel if the pointer moves to the original target +#[derive(Debug)] +pub struct PointerFocusState { + //the window under the cursor prior to it's movement + originally_focused_window: Option, + //the window under the cursor after it's movement + scheduled_focused_window: Option, + token: RegistrationToken, +} + +#[derive(Default)] +pub struct SupressedKeys(RefCell)>>); +#[derive(Default)] +pub struct SupressedButtons(RefCell>); +#[derive(Default, Debug)] +pub struct ModifiersShortcutQueue(RefCell>); + +impl SupressedKeys { + fn add(&self, keysym: &KeysymHandle, token: impl Into>) { + self.0.borrow_mut().push((keysym.raw_code(), token.into())); + } + + fn filter(&self, keysym: &KeysymHandle) -> Option> { + let mut keys = self.0.borrow_mut(); + let (removed, remaining) = keys + .drain(..) + .partition(|(key, _)| *key == keysym.raw_code()); + *keys = remaining; + + if removed.is_empty() { + return None; + } + + Some( + removed + .into_iter() + .filter_map(|(_, token)| token) + .collect::>(), + ) + } +} + +impl SupressedButtons { + fn add(&self, button: u32) { + self.0.borrow_mut().insert(button); + } + + fn remove(&self, button: u32) -> bool { + self.0.borrow_mut().remove(&button) + } +} + +impl ModifiersShortcutQueue { + pub fn set(&self, binding: shortcuts::Binding) { + let mut set = self.0.borrow_mut(); + *set = Some(binding); + } + + pub fn take(&self, binding: &shortcuts::Binding) -> bool { + let mut set = self.0.borrow_mut(); + if set.is_some() && set.as_ref().unwrap() == binding { + *set = None; + true + } else { + false + } + } + + pub fn clear(&self) { + let mut set = self.0.borrow_mut(); + *set = None; + } +} + +impl State { + #[profiling::function] + pub fn process_input_event(&mut self, event: InputEvent) + where + ::Device: 'static, + { + crate::wayland::handlers::output_power::set_all_surfaces_dpms_on(self); + + use smithay::backend::input::Event; + match event { + InputEvent::DeviceAdded { device } => { + let shell = self.common.shell.read(); + let seat = shell.seats.last_active(); + let led_state = seat.get_keyboard().unwrap().led_state(); + seat.devices().add_device(&device, led_state); + if device.has_capability(DeviceCapability::TabletTool) { + seat.tablet_seat().add_tablet::( + &self.common.display_handle, + &TabletDescriptor::from(&device), + ); + } + } + InputEvent::DeviceRemoved { device } => { + for seat in &mut self.common.shell.read().seats.iter() { + let devices = seat.devices(); + if devices.has_device(&device) { + devices.remove_device(&device); + if device.has_capability(DeviceCapability::TabletTool) { + seat.tablet_seat() + .remove_tablet(&TabletDescriptor::from(&device)); + if seat.tablet_seat().count_tablets() == 0 { + seat.tablet_seat().clear_tools(); + } + } + break; + } + } + } + + InputEvent::Keyboard { event, .. } => { + use smithay::backend::input::KeyboardKeyEvent; + + let maybe_seat = self + .common + .shell + .read() + .seats + .for_device(&event.device()) + .cloned(); + if let Some(seat) = maybe_seat { + self.common.idle_notifier_state.notify_activity(&seat); + + let keycode = event.key_code(); + let state = event.state(); + trace!(?keycode, ?state, "key"); + + let serial = SERIAL_COUNTER.next_serial(); + let time = Event::time_msec(&event); + let keyboard = seat.get_keyboard().unwrap(); + let previous_modifiers = keyboard.modifier_state(); + if let Some((action, pattern)) = keyboard + .input( + self, + keycode, + state, + serial, + time, + |data, modifiers, handle| { + if previous_modifiers != *modifiers { + *seat + .user_data() + .get::() + .unwrap() + .0 + .lock() + .unwrap() = Some(serial); + } + + let current_focus = seat.get_keyboard().unwrap().current_focus(); + let shortcuts_inhibited = current_focus.as_ref().is_some_and(|f| { + f.wl_surface() + .map(|surface| { + seat.keyboard_shortcuts_inhibitor_for_surface(&surface) + .map(|inhibitor| inhibitor.is_active()) + .unwrap_or(false) + || seat.has_active_xwayland_grab(&surface) + }) + .unwrap_or(false) + }); + let sym = handle.modified_sym(); + + let result = Self::filter_keyboard_input( + data, &event, &seat, modifiers, handle, serial, + ); + + if (matches!(result, FilterResult::Forward) + && !seat.get_keyboard().unwrap().is_grabbed() + && !shortcuts_inhibited + && !matches!( + current_focus, + Some(KeyboardFocusTarget::LockSurface(_)) + )) + // we don't want to accidentally leave any keys pressed + // and do more filtering in `xwayland_notify_key_event` + // for released keys + || state == KeyState::Released + { + data.common.xwayland_notify_key_event( + sym, keycode, state, serial, time, + ); + } + + result + }, + ) + .flatten() + { + if pattern.key.is_none() && state == KeyState::Released { + // we still want to send release-events and not have apps stuck on some modifiers. + keyboard.input(self, keycode, state, serial, time, |_, _, _| { + FilterResult::<()>::Forward + }); + } + self.handle_action(action, &seat, serial, time, pattern, None) + } + + // If we want to track numlock state so it can be reused on the next boot... + if let NumlockState::LastBoot = + self.common.config.cosmic_conf.keyboard_config.numlock_state + { + // .. and the state has been updated ... + if self.common.config.dynamic_conf.numlock().last_state + != keyboard.modifier_state().num_lock + { + // ... then record the updated state. + // The call to `numlock_mut` will generate a `PersistenceGuard`. The + // `PersistenceGuard` will write to a file when it's dropped here. + self.common.config.dynamic_conf.numlock_mut().last_state = + keyboard.modifier_state().num_lock; + } + } + } + } + + InputEvent::PointerMotion { event, .. } => { + use smithay::backend::input::PointerMotionEvent; + + let shell = self.common.shell.write(); + if let Some(seat) = shell.seats.for_device(&event.device()).cloned() { + self.common.idle_notifier_state.notify_activity(&seat); + notify_cursor_activity(self, &seat); + let current_output = seat.active_output(); + + let mut position = seat.get_pointer().unwrap().current_location().as_global(); + + let under = State::surface_under(position, ¤t_output, &shell) + .map(|(target, pos)| (target, pos.as_logical())); + + let ptr = seat.get_pointer().unwrap(); + + let mut pointer_locked = false; + let mut pointer_confined = false; + let mut confine_region = None; + if let Some((surface, surface_loc)) = under + .as_ref() + .and_then(|(target, l)| Some((target.wl_surface()?, l))) + { + with_pointer_constraint(&surface, &ptr, |constraint| match constraint { + Some(constraint) if constraint.is_active() => { + // Constraint does not apply if not within region + if !constraint.region().is_none_or(|x| { + x.contains( + (ptr.current_location() - *surface_loc).to_i32_floor(), + ) + }) { + return; + } + match &*constraint { + PointerConstraint::Locked(_locked) => { + pointer_locked = true; + } + PointerConstraint::Confined(confine) => { + pointer_confined = true; + confine_region = confine.region().cloned(); + } + } + } + _ => {} + }); + } + + std::mem::drop(shell); + ptr.relative_motion( + self, + under.clone(), + &RelativeMotionEvent { + delta: event.delta(), + delta_unaccel: event.delta_unaccel(), + utime: event.time(), + }, + ); + + if pointer_locked { + ptr.frame(self); + return; + } + + let original_position = position; + position += event.delta().as_global(); + let shell = self.common.shell.read(); + let output = shell + .outputs() + .find(|output| output.geometry().to_f64().contains(position)) + .cloned() + .unwrap_or(current_output.clone()); + drop(shell); + let output_geometry = output.geometry(); + + let scale = output.current_scale().fractional_scale(); + let physical = output + .current_mode() + .map(|mode| output.current_transform().transform_size(mode.size)) + .unwrap_or_default(); + let logical = physical.to_f64().to_logical(scale); + let output_geometry_loc = output_geometry.loc.to_f64(); + // output_geometry.size is a rounded value and may undershoot/overshoot the accurate logical size + // We constrain the position with: + // - output_geometry.size so that we don't send leave events to a fullscreen app + // - logical size so that the position doesn't end up outside the actual size of the output + // See https://github.com/pop-os/cosmic-comp/pull/2568 + let max_x = (output_geometry_loc.x + + logical.w.min(output_geometry.size.w as f64)) + .next_down(); + let max_y = (output_geometry_loc.y + + logical.h.min(output_geometry.size.h as f64)) + .next_down(); + position.x = position.x.clamp(output_geometry_loc.x, max_x); + position.y = position.y.clamp(output_geometry_loc.y, max_y); + + if ptr.is_grabbed() { + if seat + .user_data() + .get::() + .map(|marker| marker.get()) + .unwrap_or(false) + && output != current_output + { + ptr.frame(self); + return; + } + //If the pointer isn't grabbed, we should check if the focused element should be updated + } else if self.common.config.cosmic_conf.focus_follows_cursor { + let shell = self.common.shell.read(); + let old_keyboard_target = + State::element_under(original_position, ¤t_output, &shell, &seat); + let new_keyboard_target = + State::element_under(position, &output, &shell, &seat); + + if old_keyboard_target != new_keyboard_target + && new_keyboard_target.is_some() + { + let create_source = if self.common.pointer_focus_state.is_none() { + true + } else { + let PointerFocusState { + originally_focused_window, + scheduled_focused_window, + token, + } = self.common.pointer_focus_state.as_ref().unwrap(); + + if &new_keyboard_target == originally_focused_window { + //if we moved to the original window, just cancel the event + self.common.event_loop_handle.remove(*token); + //clear the state + self.common.pointer_focus_state = None; + false + } else if &new_keyboard_target != scheduled_focused_window { + //if we moved to a new window, update the scheduled focus + self.common.event_loop_handle.remove(*token); + true + } else { + //the state doesn't need to be updated or cleared + false + } + }; + + if create_source { + // prevent popups from being unfocusable if there is a gap between them and their parent + let delay = calloop::timer::Timer::from_duration( + //default to 250ms + std::time::Duration::from_millis( + self.common.config.cosmic_conf.focus_follows_cursor_delay, + ), + ); + let seat = seat.clone(); + let token = self + .common + .event_loop_handle + .insert_source(delay, move |_, _, state| { + let target = state + .common + .pointer_focus_state + .as_ref() + .unwrap() + .scheduled_focused_window + .clone(); + //clear it prior in case the user twitches in the microsecond it + //takes this function to run + state.common.pointer_focus_state = None; + + Shell::set_focus( + state, + target.as_ref(), + &seat, + Some(SERIAL_COUNTER.next_serial()), + false, + ); + + TimeoutAction::Drop + }) + .ok(); + if token.is_some() { + let originally_focused_window = + if self.common.pointer_focus_state.is_none() { + old_keyboard_target + } else { + // In this case, the pointer has moved to a new window (neither original, nor scheduled) + // so we should preserve the original window for the focus state + self.common + .pointer_focus_state + .as_ref() + .unwrap() + .originally_focused_window + .clone() + }; + + self.common.pointer_focus_state = Some(PointerFocusState { + originally_focused_window, + scheduled_focused_window: new_keyboard_target, + token: token.unwrap(), + }); + } + } + } + } + + // If confined, help user to update valid coordinates can improve user experience + let shell = self.common.shell.read(); + let new_under = if pointer_confined && let Some((surface, surface_loc)) = &under + { + let is_legal = |pos: Point, shell: &Shell| { + let new_under = State::surface_under(pos, &output, shell) + .map(|(target, pos)| (target, pos.as_logical())); + + // TODO: We might need a solution that allows constraints to bypass the surface without affecting the constraints themselves + if new_under.as_ref().and_then(|(under, _)| under.wl_surface()) + != surface.wl_surface() + { + return (false, None); + } + + match surface { + PointerFocusTarget::WlSurface { surface, .. } => { + if under_from_surface_tree( + surface, + position.as_logical() - surface_loc.to_f64(), + (0, 0), + WindowSurfaceType::ALL, + ) + .is_none() + { + return (false, None); + } + } + PointerFocusTarget::X11Surface { surface, .. } => { + if surface + .surface_under( + position.as_logical() - surface_loc.to_f64(), + (0, 0), + WindowSurfaceType::ALL, + ) + .is_none() + { + return (false, None); + } + } + _ => {} + } + + if let Some(region) = &confine_region + && !region + .contains((pos.as_logical() - *surface_loc).to_i32_floor()) + { + return (false, None); + } + (true, new_under) + }; + + match is_legal(position, &shell) { + (true, under) => under, + _ => { + let y_only_pos = Point::new(original_position.x, position.y); + let x_only_pos = Point::new(position.x, original_position.y); + match is_legal(y_only_pos, &shell) { + (true, under) => { + position = y_only_pos; + under + } + _ => match is_legal(x_only_pos, &shell) { + (true, under) => { + position = x_only_pos; + under + } + _ => { + position = original_position; + None + } + }, + } + } + } + } else { + State::surface_under(position, &output, &shell) + .map(|(target, pos)| (target, pos.as_logical())) + }; + + drop(shell); + if pointer_confined && new_under.is_none() { + ptr.frame(self); + return; + } + + let serial = SERIAL_COUNTER.next_serial(); + ptr.motion( + self, + under, + &MotionEvent { + location: position.as_logical(), + serial, + time: event.time_msec(), + }, + ); + ptr.frame(self); + + // If pointer is now in a constraint region and window is in focused, activate it + if let Some((under, surface_location)) = new_under + .and_then(|(target, loc)| Some((target.wl_surface()?.into_owned(), loc))) + { + let shell = self.common.shell.read(); + let is_focused = seat + .get_keyboard() + .and_then(|k| k.current_focus()) + .is_some_and(|f| f.has_surface(&shell, &under)); + + if is_focused { + with_pointer_constraint(&under, &ptr, |constraint| match constraint { + Some(constraint) if !constraint.is_active() => { + let region = match &*constraint { + PointerConstraint::Locked(locked) => locked.region(), + PointerConstraint::Confined(confined) => confined.region(), + }; + let point = + (ptr.current_location() - surface_location).to_i32_floor(); + if region.is_none_or(|region| region.contains(point)) { + constraint.activate(); + } + } + _ => {} + }); + } + } + + let mut shell = self.common.shell.write(); + shell.update_pointer_position(position.to_local(&output), &output); + shell.update_focal_point( + &seat, + original_position, + self.common.config.cosmic_conf.accessibility_zoom.view_moves, + ); + + if output != current_output { + for session in cursor_sessions_for_output(&shell, ¤t_output) { + session.set_cursor_pos(None); + } + seat.set_active_output(&output); + } + + update_output_image_copy_cursor_position( + &shell, + &self.common.clock, + &output, + &seat, + position, + ); + } + } + InputEvent::PointerMotionAbsolute { event, .. } => { + let maybe_seat = self + .common + .shell + .read() + .seats + .for_device(&event.device()) + .cloned(); + if let Some(seat) = maybe_seat { + self.common.idle_notifier_state.notify_activity(&seat); + notify_cursor_activity(self, &seat); + let output = seat.active_output(); + let output_geometry = output.geometry(); + let position = output_geometry.loc.to_f64() + + smithay::backend::input::AbsolutePositionEvent::position_transformed( + &event, + output_geometry.size.as_logical(), + ) + .as_global(); + let serial = SERIAL_COUNTER.next_serial(); + let under = State::surface_under(position, &output, &self.common.shell.write()) + .map(|(target, pos)| (target, pos.as_logical())); + + let ptr = seat.get_pointer().unwrap(); + ptr.motion( + self, + under, + &MotionEvent { + location: position.as_logical(), + serial, + time: event.time_msec(), + }, + ); + ptr.frame(self); + + let shell = self.common.shell.read(); + update_output_image_copy_cursor_position( + &shell, + &self.common.clock, + &output, + &seat, + position, + ); + } + } + InputEvent::PointerButton { event, .. } => { + use smithay::backend::input::{ButtonState, PointerButtonEvent}; + + // + let Some(seat) = self + .common + .shell + .read() + .seats + .for_device(&event.device()) + .cloned() + else { + return; + }; + self.common.idle_notifier_state.notify_activity(&seat); + notify_cursor_activity(self, &seat); + + let current_focus = seat.get_keyboard().unwrap().current_focus(); + let shortcuts_inhibited = current_focus.as_ref().is_some_and(|f| { + f.wl_surface() + .map(|surface| { + seat.keyboard_shortcuts_inhibitor_for_surface(&surface) + .map(|inhibitor| inhibitor.is_active()) + .unwrap_or(false) + || seat.has_active_xwayland_grab(&surface) + }) + .unwrap_or(false) + }); + + let serial = SERIAL_COUNTER.next_serial(); + let button = event.button_code(); + + let mut pass_event = !seat.supressed_buttons().remove(button); + if event.state() == ButtonState::Pressed { + // change the keyboard focus unless the pointer is grabbed + // We test for any matching surface type here but always use the root + // (in case of a window the toplevel) surface for the focus. + // see: https://gitlab.freedesktop.org/wayland/wayland/-/issues/294 + if !seat.get_pointer().unwrap().is_grabbed() { + let output = seat.active_output(); + + let global_position = + seat.get_pointer().unwrap().current_location().as_global(); + let under = { + let shell = self.common.shell.read(); + State::element_under(global_position, &output, &shell, &seat) + }; + // Grabbing a tiling resize handle (the gap between tiles) must not change keyboard focus + let on_resize_fork = matches!( + seat.get_pointer().unwrap().current_focus(), + Some(PointerFocusTarget::ResizeFork(_)) + ); + if let Some(target) = under.filter(|_| !on_resize_fork) { + if let Some(surface) = target.toplevel().map(Cow::into_owned) + && seat.get_keyboard().unwrap().modifier_state().logo + && !shortcuts_inhibited + { + let seat_clone = seat.clone(); + let mouse_button = PointerButtonEvent::button(&event); + + let mut supress_button = || { + // If the logo is held then the pointer event is + // aimed at the compositor and shouldn't be passed + // to the application. + pass_event = false; + seat.supressed_buttons().add(button); + }; + + fn dispatch_grab + 'static>( + grab: Option<(G, smithay::input::pointer::Focus)>, + seat: Seat, + serial: Serial, + state: &mut State, + ) { + if let Some((target, focus)) = grab { + seat.modifiers_shortcut_queue().clear(); + + seat.get_pointer() + .unwrap() + .set_grab(state, target, serial, focus); + } + } + + if let Some(mouse_button) = mouse_button { + match mouse_button { + smithay::backend::input::MouseButton::Left => { + supress_button(); + self.common.event_loop_handle.insert_idle( + move |state| { + let mut shell = state.common.shell.write(); + let res = shell.move_request( + &surface, + &seat_clone, + serial, + ReleaseMode::NoMouseButtons, + false, + &state.common.config, + &state.common.event_loop_handle, + false, + ); + drop(shell); + dispatch_grab(res, seat_clone, serial, state); + }, + ); + } + smithay::backend::input::MouseButton::Right => { + supress_button(); + self.common.event_loop_handle.insert_idle( + move |state| { + let mut shell = state.common.shell.write(); + let Some(target_elem) = + shell.element_for_surface(&surface) + else { + return; + }; + let Some(geom) = shell + .space_for(target_elem) + .and_then(|f| { + f.element_geometry(target_elem) + }) + .or_else(|| { + shell + .workspaces + .sets + .get(&output) + .and_then(|set| { + set.sticky_layer + .element_geometry( + target_elem, + ) + }) + }) + else { + return; + }; + let geom = geom.to_f64(); + let center = + geom.loc + geom.size.downscale(2.0); + let offset = + center.to_global(&output) - global_position; + let edge = + match (offset.x > 0.0, offset.y > 0.0) { + (true, true) => ResizeEdge::TOP_LEFT, + (false, true) => ResizeEdge::TOP_RIGHT, + (true, false) => { + ResizeEdge::BOTTOM_LEFT + } + (false, false) => { + ResizeEdge::BOTTOM_RIGHT + } + }; + let res = shell.resize_request( + &surface, + &seat_clone, + serial, + edge, + state + .common + .config + .cosmic_conf + .edge_snap_threshold, + false, + ); + drop(shell); + dispatch_grab(res, seat_clone, serial, state); + }, + ); + } + _ => {} + } + } + } + + Shell::set_focus(self, Some(&target), &seat, Some(serial), false); + } + } + } else { + let mut shell = self.common.shell.write(); + if let Some(Trigger::Pointer(action_button)) = + shell.overview_mode().0.active_trigger() + && *action_button == button + { + shell.set_overview_mode(None, self.common.event_loop_handle.clone()); + } + std::mem::drop(shell); + }; + + if pass_event + && !matches!(current_focus, Some(KeyboardFocusTarget::LockSurface(_))) + && !shortcuts_inhibited + { + self.common.xwayland_notify_pointer_button_event( + button, + event.state(), + serial, + event.time_msec(), + ); + } + + let ptr = seat.get_pointer().unwrap(); + if pass_event { + ptr.button( + self, + &ButtonEvent { + button, + state: event.state(), + serial, + time: event.time_msec(), + }, + ); + ptr.frame(self); + } else if event.state() == ButtonState::Released { + ptr.unset_grab(self, serial, event.time_msec()) + } + } + InputEvent::PointerAxis { event, .. } => { + let scroll_factor = + if let Some(device) = ::downcast_ref::(&event.device()) { + self.common.config.scroll_factor(device) + } else { + 1.0 + }; + + let maybe_seat = self + .common + .shell + .read() + .seats + .for_device(&event.device()) + .cloned(); + if let Some(seat) = maybe_seat { + self.common.idle_notifier_state.notify_activity(&seat); + notify_cursor_activity(self, &seat); + + if seat.get_keyboard().unwrap().modifier_state().logo + && self + .common + .config + .cosmic_conf + .accessibility_zoom + .enable_mouse_zoom_shortcuts + { + seat.modifiers_shortcut_queue().clear(); + if let Some(mut percentage) = event + .amount_v120(Axis::Vertical) + .map(|val| val / 120.) + .or_else(|| event.amount(Axis::Vertical)) + .map(|val| val * scroll_factor) + { + if event.relative_direction(Axis::Vertical) + == AxisRelativeDirection::Inverted + { + percentage *= -1.; + } + + if event.source() == AxisSource::Wheel { + percentage *= 5.; + } + + let change = -(percentage / 100.); + self.update_zoom(&seat, change, event.source() == AxisSource::Wheel); + } + } else { + let mut frame = AxisFrame::new(event.time_msec()).source(event.source()); + if let Some(horizontal_amount) = event.amount(Axis::Horizontal) { + if horizontal_amount != 0.0 { + frame = frame + .relative_direction( + Axis::Horizontal, + event.relative_direction(Axis::Horizontal), + ) + .value(Axis::Horizontal, scroll_factor * horizontal_amount); + if let Some(discrete) = event.amount_v120(Axis::Horizontal) { + frame = frame.v120( + Axis::Horizontal, + (discrete * scroll_factor).round() as i32, + ); + } + } else if event.source() == AxisSource::Finger { + frame = frame.stop(Axis::Horizontal); + } + } + if let Some(vertical_amount) = event.amount(Axis::Vertical) { + if vertical_amount != 0.0 { + frame = frame + .relative_direction( + Axis::Vertical, + event.relative_direction(Axis::Vertical), + ) + .value(Axis::Vertical, scroll_factor * vertical_amount); + if let Some(discrete) = event.amount_v120(Axis::Vertical) { + frame = frame.v120( + Axis::Vertical, + (discrete * scroll_factor).round() as i32, + ); + } + } else if event.source() == AxisSource::Finger { + frame = frame.stop(Axis::Vertical); + } + } + let ptr = seat.get_pointer().unwrap(); + ptr.axis(self, frame); + ptr.frame(self); + } + } + } + + InputEvent::GestureSwipeBegin { event, .. } => { + let maybe_seat = self + .common + .shell + .read() + .seats + .for_device(&event.device()) + .cloned(); + if let Some(seat) = maybe_seat { + self.common.idle_notifier_state.notify_activity(&seat); + if event.fingers() >= 3 && !workspace_overview_is_open(&seat.active_output()) { + self.common.gesture_state = Some(GestureState::new(event.fingers())); + } else { + let serial = SERIAL_COUNTER.next_serial(); + let pointer = seat.get_pointer().unwrap(); + pointer.gesture_swipe_begin( + self, + &GestureSwipeBeginEvent { + serial, + time: event.time_msec(), + fingers: event.fingers(), + }, + ); + } + } + } + InputEvent::GestureSwipeUpdate { event, .. } => { + let maybe_seat = self + .common + .shell + .read() + .seats + .for_device(&event.device()) + .cloned(); + if let Some(seat) = maybe_seat { + self.common.idle_notifier_state.notify_activity(&seat); + let mut activate_action: Option = None; + if let Some(ref mut gesture_state) = self.common.gesture_state { + let first_update = gesture_state.update( + event.delta(), + Duration::from_millis(event.time_msec() as u64), + ); + // Decide on action if first update + if first_update { + let mut natural_scroll = false; + if let Some(scroll_config) = + &self.common.config.cosmic_conf.input_touchpad.scroll_config + && let Some(natural) = scroll_config.natural_scroll + { + natural_scroll = natural; + } + activate_action = match gesture_state.fingers { + 3 => None, // TODO: 3 finger gestures + 4 => { + if self.common.config.cosmic_conf.workspaces.workspace_layout + == WorkspaceLayout::Horizontal + { + match gesture_state.direction { + Some(Direction::Left) => { + if natural_scroll { + Some(SwipeAction::NextWorkspace) + } else { + Some(SwipeAction::PrevWorkspace) + } + } + Some(Direction::Right) => { + if natural_scroll { + Some(SwipeAction::PrevWorkspace) + } else { + Some(SwipeAction::NextWorkspace) + } + } + _ => None, // TODO: Other actions + } + } else { + match gesture_state.direction { + Some(Direction::Up) => { + if natural_scroll { + Some(SwipeAction::NextWorkspace) + } else { + Some(SwipeAction::PrevWorkspace) + } + } + Some(Direction::Down) => { + if natural_scroll { + Some(SwipeAction::PrevWorkspace) + } else { + Some(SwipeAction::NextWorkspace) + } + } + _ => None, // TODO: Other actions + } + } + } + _ => None, + }; + + gesture_state.action = activate_action; + } + + match gesture_state.action { + Some(x @ SwipeAction::NextWorkspace) + | Some(x @ SwipeAction::PrevWorkspace) => { + self.common.shell.write().update_workspace_delta( + &seat.active_output(), + gesture_state.delta, + matches!(x, SwipeAction::NextWorkspace), + ) + } + _ => {} + } + } else { + let pointer = seat.get_pointer().unwrap(); + pointer.gesture_swipe_update( + self, + &GestureSwipeUpdateEvent { + time: event.time_msec(), + delta: event.delta(), + }, + ); + } + + if let Some(action) = activate_action { + self.handle_swipe_action(action, &seat); + } + } + } + InputEvent::GestureSwipeEnd { event, .. } => { + let maybe_seat = self + .common + .shell + .read() + .seats + .for_device(&event.device()) + .cloned(); + if let Some(seat) = maybe_seat { + self.common.idle_notifier_state.notify_activity(&seat); + if let Some(ref gesture_state) = self.common.gesture_state { + match gesture_state.action { + Some(SwipeAction::NextWorkspace) | Some(SwipeAction::PrevWorkspace) => { + let velocity = gesture_state.velocity(); + let norm_velocity = + if self.common.config.cosmic_conf.workspaces.workspace_layout + == WorkspaceLayout::Horizontal + { + velocity / seat.active_output().geometry().size.w as f64 + } else { + velocity / seat.active_output().geometry().size.h as f64 + }; + let _ = self.common.shell.write().end_workspace_swipe( + &seat.active_output(), + norm_velocity, + &mut self.common.workspace_state.update(), + ); + } + _ => {} + } + self.common.gesture_state = None; + } else { + let serial = SERIAL_COUNTER.next_serial(); + let pointer = seat.get_pointer().unwrap(); + pointer.gesture_swipe_end( + self, + &GestureSwipeEndEvent { + serial, + time: event.time_msec(), + cancelled: event.cancelled(), + }, + ); + } + } + } + InputEvent::GesturePinchBegin { event, .. } => { + let maybe_seat = self + .common + .shell + .read() + .seats + .for_device(&event.device()) + .cloned(); + if let Some(seat) = maybe_seat { + self.common.idle_notifier_state.notify_activity(&seat); + let serial = SERIAL_COUNTER.next_serial(); + let pointer = seat.get_pointer().unwrap(); + pointer.gesture_pinch_begin( + self, + &GesturePinchBeginEvent { + serial, + time: event.time_msec(), + fingers: event.fingers(), + }, + ); + } + } + InputEvent::GesturePinchUpdate { event, .. } => { + let maybe_seat = self + .common + .shell + .read() + .seats + .for_device(&event.device()) + .cloned(); + if let Some(seat) = maybe_seat { + self.common.idle_notifier_state.notify_activity(&seat); + let pointer = seat.get_pointer().unwrap(); + pointer.gesture_pinch_update( + self, + &GesturePinchUpdateEvent { + time: event.time_msec(), + delta: event.delta(), + scale: event.scale(), + rotation: event.rotation(), + }, + ); + } + } + InputEvent::GesturePinchEnd { event, .. } => { + let maybe_seat = self + .common + .shell + .read() + .seats + .for_device(&event.device()) + .cloned(); + if let Some(seat) = maybe_seat { + self.common.idle_notifier_state.notify_activity(&seat); + let serial = SERIAL_COUNTER.next_serial(); + let pointer = seat.get_pointer().unwrap(); + pointer.gesture_pinch_end( + self, + &GesturePinchEndEvent { + serial, + time: event.time_msec(), + cancelled: event.cancelled(), + }, + ); + } + } + InputEvent::GestureHoldBegin { event, .. } => { + let maybe_seat = self + .common + .shell + .read() + .seats + .for_device(&event.device()) + .cloned(); + if let Some(seat) = maybe_seat { + self.common.idle_notifier_state.notify_activity(&seat); + let serial = SERIAL_COUNTER.next_serial(); + let pointer = seat.get_pointer().unwrap(); + pointer.gesture_hold_begin( + self, + &GestureHoldBeginEvent { + serial, + time: event.time_msec(), + fingers: event.fingers(), + }, + ); + } + } + InputEvent::GestureHoldEnd { event, .. } => { + let maybe_seat = self + .common + .shell + .read() + .seats + .for_device(&event.device()) + .cloned(); + if let Some(seat) = maybe_seat { + self.common.idle_notifier_state.notify_activity(&seat); + let serial = SERIAL_COUNTER.next_serial(); + let pointer = seat.get_pointer().unwrap(); + pointer.gesture_hold_end( + self, + &GestureHoldEndEvent { + serial, + time: event.time_msec(), + cancelled: event.cancelled(), + }, + ); + } + } + + InputEvent::TouchDown { event, .. } => { + let shell = self.common.shell.write(); + if let Some(seat) = shell.seats.for_device(&event.device()).cloned() { + self.common.idle_notifier_state.notify_activity(&seat); + let Some(output) = + mapped_output_for_device(&self.common.config, &shell, &event.device()) + .cloned() + else { + return; + }; + + let position = + transform_output_mapped_position(&output, &event, shell.zoom_state()); + let under = State::surface_under(position, &output, &shell) + .map(|(target, pos)| (target, pos.as_logical())); + + std::mem::drop(shell); + + let serial = SERIAL_COUNTER.next_serial(); + let touch = seat.get_touch().unwrap(); + touch.down( + self, + under, + &DownEvent { + slot: event.slot(), + location: position.as_logical(), + serial, + time: event.time_msec(), + }, + ); + } + } + InputEvent::TouchMotion { event, .. } => { + let shell = self.common.shell.write(); + if let Some(seat) = shell.seats.for_device(&event.device()).cloned() { + self.common.idle_notifier_state.notify_activity(&seat); + let Some(output) = + mapped_output_for_device(&self.common.config, &shell, &event.device()) + .cloned() + else { + return; + }; + + let position = + transform_output_mapped_position(&output, &event, shell.zoom_state()); + let under = State::surface_under(position, &output, &shell) + .map(|(target, pos)| (target, pos.as_logical())); + + std::mem::drop(shell); + + let touch = seat.get_touch().unwrap(); + touch.motion( + self, + under, + &TouchMotionEvent { + slot: event.slot(), + location: position.as_logical(), + time: event.time_msec(), + }, + ); + } + } + InputEvent::TouchUp { event, .. } => { + let mut shell = self.common.shell.write(); + if let Some(Trigger::Touch(slot)) = shell.overview_mode().0.active_trigger() + && *slot == event.slot() + { + shell.set_overview_mode(None, self.common.event_loop_handle.clone()); + } + + let maybe_seat = shell.seats.for_device(&event.device()).cloned(); + if let Some(seat) = maybe_seat { + self.common.idle_notifier_state.notify_activity(&seat); + std::mem::drop(shell); + let serial = SERIAL_COUNTER.next_serial(); + let touch = seat.get_touch().unwrap(); + touch.up( + self, + &UpEvent { + slot: event.slot(), + time: event.time_msec(), + serial, + }, + ); + } + } + InputEvent::TouchCancel { event, .. } => { + let maybe_seat = self + .common + .shell + .read() + .seats + .for_device(&event.device()) + .cloned(); + if let Some(seat) = maybe_seat { + self.common.idle_notifier_state.notify_activity(&seat); + let touch = seat.get_touch().unwrap(); + touch.cancel(self); + } + } + InputEvent::TouchFrame { event, .. } => { + let maybe_seat = self + .common + .shell + .read() + .seats + .for_device(&event.device()) + .cloned(); + if let Some(seat) = maybe_seat { + self.common.idle_notifier_state.notify_activity(&seat); + let touch = seat.get_touch().unwrap(); + touch.frame(self); + } + } + + InputEvent::TabletToolAxis { event, .. } => { + let shell = self.common.shell.write(); + if let Some(seat) = shell.seats.for_device(&event.device()).cloned() { + self.common.idle_notifier_state.notify_activity(&seat); + notify_cursor_activity(self, &seat); + let Some(output) = + mapped_output_for_device(&self.common.config, &shell, &event.device()) + .cloned() + else { + return; + }; + + let position = + transform_output_mapped_position(&output, &event, shell.zoom_state()); + let under = State::surface_under(position, &output, &shell) + .map(|(target, pos)| (target, pos.as_logical())); + + std::mem::drop(shell); + + let pointer = seat.get_pointer().unwrap(); + pointer.motion( + self, + under.clone(), + &MotionEvent { + location: position.as_logical(), + serial: SERIAL_COUNTER.next_serial(), + time: self.common.clock.now().as_millis(), + }, + ); + + let tablet_seat = seat.tablet_seat(); + + let tablet = tablet_seat.get_tablet(&TabletDescriptor::from(&event.device())); + let tool = tablet_seat.get_tool(&event.tool()); + + if let (Some(tablet), Some(tool)) = (tablet, tool) { + if event.pressure_has_changed() { + tool.pressure(event.pressure()); + } + if event.distance_has_changed() { + tool.distance(event.distance()); + } + if event.tilt_has_changed() { + tool.tilt(event.tilt()); + } + if event.slider_has_changed() { + tool.slider_position(event.slider_position()); + } + if event.rotation_has_changed() { + tool.rotation(event.rotation()); + } + if event.wheel_has_changed() { + tool.wheel(event.wheel_delta(), event.wheel_delta_discrete()); + } + + tool.motion( + position.as_logical(), + under + .and_then(|(f, loc)| f.wl_surface().map(|s| (s.into_owned(), loc))), + &tablet, + SERIAL_COUNTER.next_serial(), + event.time_msec(), + ); + } + } + } + InputEvent::TabletToolProximity { event, .. } => { + let shell = self.common.shell.write(); + if let Some(seat) = shell.seats.for_device(&event.device()).cloned() { + self.common.idle_notifier_state.notify_activity(&seat); + notify_cursor_activity(self, &seat); + let Some(output) = + mapped_output_for_device(&self.common.config, &shell, &event.device()) + .cloned() + else { + return; + }; + + let position = + transform_output_mapped_position(&output, &event, shell.zoom_state()); + let under = State::surface_under(position, &output, &shell) + .map(|(target, pos)| (target, pos.as_logical())); + + std::mem::drop(shell); + + let pointer = seat.get_pointer().unwrap(); + pointer.motion( + self, + under.clone(), + &MotionEvent { + location: position.as_logical(), + serial: SERIAL_COUNTER.next_serial(), + time: self.common.clock.now().as_millis(), + }, + ); + + let tablet_seat = seat.tablet_seat(); + + let tablet = tablet_seat.get_tablet(&TabletDescriptor::from(&event.device())); + let dh = self.common.display_handle.clone(); + let tool = tablet_seat.add_tool::(self, &dh, &event.tool()); + + if let Some(tablet) = tablet { + match event.state() { + ProximityState::In => { + if let Some(under) = under.and_then(|(f, loc)| { + f.wl_surface().map(|s| (s.into_owned(), loc)) + }) { + tool.proximity_in( + position.as_logical(), + under, + &tablet, + SERIAL_COUNTER.next_serial(), + event.time_msec(), + ) + } + } + ProximityState::Out => tool.proximity_out(event.time_msec()), + } + } + } + } + InputEvent::TabletToolTip { event, .. } => { + let maybe_seat = self + .common + .shell + .read() + .seats + .for_device(&event.device()) + .cloned(); + if let Some(seat) = maybe_seat { + self.common.idle_notifier_state.notify_activity(&seat); + notify_cursor_activity(self, &seat); + if let Some(tool) = seat.tablet_seat().get_tool(&event.tool()) { + match event.tip_state() { + TabletToolTipState::Down => { + tool.tip_down(SERIAL_COUNTER.next_serial(), event.time_msec()); + } + TabletToolTipState::Up => { + tool.tip_up(event.time_msec()); + } + } + } + } + } + InputEvent::TabletToolButton { event, .. } => { + let maybe_seat = self + .common + .shell + .read() + .seats + .for_device(&event.device()) + .cloned(); + if let Some(seat) = maybe_seat { + self.common.idle_notifier_state.notify_activity(&seat); + notify_cursor_activity(self, &seat); + if let Some(tool) = seat.tablet_seat().get_tool(&event.tool()) { + tool.button( + event.button(), + event.button_state(), + SERIAL_COUNTER.next_serial(), + event.time_msec(), + ); + } + } + } + InputEvent::Special(_) => {} + #[allow(unused_variables)] + InputEvent::SwitchToggle { event } => { + #[cfg(feature = "logind")] + if event.switch() == Some(Switch::Lid) && self.common.inhibit_lid_fd.is_some() { + let backend = self.backend.lock(); + let output = backend + .all_outputs() + .iter() + .find(|o| o.is_internal()) + .cloned(); + let closed = event.state() == SwitchState::On; + + if closed { + backend + .disable_internal_output(&mut self.common.output_configuration_state); + } else { + backend.enable_internal_output(&mut self.common.output_configuration_state); + } + std::mem::drop(backend); + + if let Err(err) = self.refresh_output_config() { + if !closed { + tracing::warn!(?err, "Failed to re-enable internal connector"); + if let Some(output) = output { + use cosmic_comp_config::output::comp::OutputState; + + output.config_mut().enabled = OutputState::Disabled; + if let Err(err) = self.refresh_output_config() { + error!("Unrecoverable output configuration error: {}", err); + } + } + } else { + // Disabling an output should never fail. + error!("Unrecoverable output configuration error: {}", err); + } + } + } + } + } + } + + /// Determine is key event should be intercepted as a key binding, or forwarded to surface + #[profiling::function] + pub fn filter_keyboard_input>( + &mut self, + event: &E, + seat: &Seat, + modifiers: &ModifiersState, + handle: KeysymHandle<'_>, + serial: Serial, + ) -> FilterResult> { + // Pre-compute for layout-agnostic shortcut matching + let raw_syms = handle.raw_syms(); + let latin_sym = handle.raw_latin_sym_or_raw_current_sym(); + let key_matches = |binding_key: Keysym| -> bool { + raw_syms.contains(&binding_key) || latin_sym.is_some_and(|sym| sym == binding_key) + }; + + let mut shell = self.common.shell.write(); + + let keyboard = seat.get_keyboard().unwrap(); + let pointer = seat.get_pointer().unwrap(); + // We're only interested in filtering keyboard grabs if we initiated them. + // The easiest way to check that is to check the type of the grab. + let keyboard_grabbed = keyboard.with_grab(|_serial, grab| { + grab.is::() || grab.is::>() + }) == Some(true); + let is_grabbed = keyboard_grabbed || pointer.is_grabbed(); + + let current_focus = keyboard.current_focus(); + //this should fall back to active output since there may not be a focused output + let focused_output = seat.focused_or_active_output(); + + let shortcuts_inhibited = current_focus.as_ref().is_some_and(|f| { + f.wl_surface() + .map(|surface| { + seat.keyboard_shortcuts_inhibitor_for_surface(&surface) + .map(|inhibitor| inhibitor.is_active()) + .unwrap_or(false) + || seat.has_active_xwayland_grab(&surface) + }) + .unwrap_or(false) + }); + + if let Some(a11y_keyboard_monitor) = self.common.dbus_state.a11y_keyboard_monitor() { + a11y_keyboard_monitor.key_event(modifiers, &handle, event.state()); + } + + // Leave move overview mode, if any modifier was released + if let Some(Trigger::KeyboardMove(action_modifiers)) = + shell.overview_mode().0.active_trigger() + && ((action_modifiers.ctrl && !modifiers.ctrl) + || (action_modifiers.alt && !modifiers.alt) + || (action_modifiers.logo && !modifiers.logo) + || (action_modifiers.shift && !modifiers.shift)) + { + shell.set_overview_mode(None, self.common.event_loop_handle.clone()); + } + // Leave swap overview mode, if any key was released + if let Some(Trigger::KeyboardSwap(action_pattern, old_descriptor)) = + shell.overview_mode().0.active_trigger() + && ((action_pattern.modifiers.ctrl && !modifiers.ctrl) + || (action_pattern.modifiers.alt && !modifiers.alt) + || (action_pattern.modifiers.logo && !modifiers.logo) + || (action_pattern.modifiers.shift && !modifiers.shift) + || (action_pattern.key.is_some() + && key_matches(action_pattern.key.unwrap()) + && event.state() == KeyState::Released)) + { + shell.set_overview_mode(None, self.common.event_loop_handle.clone()); + + self.keyboard_swap( + seat, + &mut shell, + old_descriptor, + current_focus, + &focused_output, + ); + } + + // Leave or update resize mode, if modifiers changed or initial key was released + if let Some(action_pattern) = shell.resize_mode().0.active_binding() { + if action_pattern.key.is_some() + && event.state() == KeyState::Released + && key_matches(action_pattern.key.unwrap()) + { + shell.set_resize_mode( + None, + &self.common.config, + self.common.event_loop_handle.clone(), + ); + } else if !cosmic_modifiers_eq_smithay(&action_pattern.modifiers, modifiers) { + let mut new_pattern = action_pattern.clone(); + new_pattern.modifiers = cosmic_modifiers_from_smithay(*modifiers); + let enabled = + self.common + .config + .shortcuts + .iter() + .find_map(move |(binding, action)| { + if binding == &new_pattern + && matches!(action, shortcuts::Action::Resizing(_)) + { + let shortcuts::Action::Resizing(direction) = action else { + unreachable!() + }; + Some((new_pattern.clone(), *direction)) + } else { + None + } + }); + shell.set_resize_mode( + enabled, + &self.common.config, + self.common.event_loop_handle.clone(), + ); + } + } + + // Special case resizing with regards to arrow keys + if let Some(direction) = shell.resize_mode().0.active_direction() { + let resize_edge = match handle.modified_sym() { + Keysym::Left | Keysym::h | Keysym::H => Some(ResizeEdge::LEFT), + Keysym::Down | Keysym::j | Keysym::J => Some(ResizeEdge::BOTTOM), + Keysym::Up | Keysym::k | Keysym::K => Some(ResizeEdge::TOP), + Keysym::Right | Keysym::l | Keysym::L => Some(ResizeEdge::RIGHT), + _ => None, + }; + + if let Some(mut edge) = resize_edge { + if direction == ResizeDirection::Inwards { + edge.flip_direction(); + } + let action = Action::Private(PrivateAction::Resizing( + direction, + edge.into(), + cosmic_keystate_from_smithay(event.state()), + )); + let key_pattern = shortcuts::Binding { + modifiers: cosmic_modifiers_from_smithay(*modifiers), + keycode: None, + key: Some(handle.modified_sym()), + description: None, + }; + + if event.state() == KeyState::Released { + if let Some(tokens) = seat.supressed_keys().filter(&handle) { + for token in tokens { + self.common.event_loop_handle.remove(token); + } + } + } else { + let seat_clone = seat.clone(); + let action_clone = action.clone(); + let key_pattern_clone = key_pattern.clone(); + let start = Instant::now(); + let time = event.time_msec(); + let token = self + .common + .event_loop_handle + .insert_source( + Timer::from_duration(Duration::from_millis(200)), + move |current, _, state| { + let duration = current.duration_since(start).as_millis(); + state.handle_action( + action_clone.clone(), + &seat_clone, + serial, + time.overflowing_add(duration as u32).0, + key_pattern_clone.clone(), + None, + ); + calloop::timer::TimeoutAction::ToDuration(Duration::from_millis(25)) + }, + ) + .ok(); + + seat.supressed_keys().add(&handle, token); + } + return FilterResult::Intercept(Some((action, key_pattern))); + } + } + + std::mem::drop(shell); + + // cancel grabs + if is_grabbed + && handle.modified_sym() == Keysym::Escape + && event.state() == KeyState::Pressed + && !modifiers.alt + && !modifiers.ctrl + && !modifiers.logo + && !modifiers.shift + { + seat.supressed_keys().add(&handle, None); + return FilterResult::Intercept(Some(( + Action::Private(PrivateAction::Escape), + shortcuts::Binding { + modifiers: shortcuts::Modifiers::default(), + keycode: None, + key: Some(Keysym::Escape), + description: None, + }, + ))); + } + + if let Some(mut a11y_keyboard_monitor) = self.common.dbus_state.a11y_keyboard_monitor() { + if event.state() == KeyState::Released { + let removed = + a11y_keyboard_monitor.remove_active_virtual_mod(handle.modified_sym()); + // If `Caps_Lock` is a virtual modifier, and is in locked state, clear it + if removed + && handle.modified_sym() == Keysym::Caps_Lock + && (modifiers.serialized.locked & 2) != 0 + { + let seat = seat.clone(); + let key_code = event.key_code(); + self.common.event_loop_handle.insert_idle(move |state| { + if let Some(keyboard) = seat.get_keyboard() { + let serial = SERIAL_COUNTER.next_serial(); + let time = state.common.clock.now().as_millis(); + keyboard.input( + state, + key_code, + KeyState::Pressed, + serial, + time, + |_, _, _| FilterResult::<()>::Forward, + ); + let serial = SERIAL_COUNTER.next_serial(); + keyboard.input( + state, + key_code, + KeyState::Released, + serial, + time, + |_, _, _| FilterResult::<()>::Forward, + ); + } + }); + } + } else if event.state() == KeyState::Pressed + && a11y_keyboard_monitor.has_virtual_mod(handle.modified_sym()) + { + a11y_keyboard_monitor.add_active_virtual_mod(handle.modified_sym()); + + tracing::debug!( + "active virtual mods: {:?}", + a11y_keyboard_monitor.active_virtual_mods() + ); + seat.supressed_keys().add(&handle, None); + + return FilterResult::Intercept(None); + } + } + + // Skip released events for initially surpressed keys + if event.state() == KeyState::Released + && let Some(tokens) = seat.supressed_keys().filter(&handle) + { + for token in tokens { + self.common.event_loop_handle.remove(token); + } + return FilterResult::Intercept(None); + } + + // Handle VT switches + if event.state() == KeyState::Pressed + && (Keysym::XF86_Switch_VT_1.raw()..=Keysym::XF86_Switch_VT_12.raw()) + .contains(&handle.modified_sym().raw()) + { + if let Err(err) = self.backend.kms().switch_vt( + (handle.modified_sym().raw() - Keysym::XF86_Switch_VT_1.raw() + 1) as i32, + ) { + error!(?err, "Failed switching virtual terminal."); + } + seat.supressed_keys().add(&handle, None); + return FilterResult::Intercept(None); + } + + if let Some(a11y_keyboard_monitor) = self.common.dbus_state.a11y_keyboard_monitor() + && event.state() == KeyState::Pressed + && (a11y_keyboard_monitor.has_keyboard_grab() + || a11y_keyboard_monitor.has_key_grab(modifiers, handle.modified_sym())) + { + let modifiers_queue = seat.modifiers_shortcut_queue(); + modifiers_queue.clear(); + seat.supressed_keys().add(&handle, None); + return FilterResult::Intercept(None); + } + + // handle the rest of the global shortcuts + let mut clear_queue = true; + if !shortcuts_inhibited { + let modifiers_queue = seat.modifiers_shortcut_queue(); + + for (binding, action) in self.common.config.shortcuts.iter() { + if *action == shortcuts::Action::Disable { + continue; + } + + // is this a released (triggered) modifier-only binding? + if binding.key.is_none() + && event.state() == KeyState::Released + && !cosmic_modifiers_eq_smithay(&binding.modifiers, modifiers) + && modifiers_queue.take(binding) + { + modifiers_queue.clear(); + return FilterResult::Intercept(Some(( + Action::Shortcut(action.clone()), + binding.clone(), + ))); + } + + // could this potentially become a modifier-only binding? + if binding.key.is_none() + && event.state() == KeyState::Pressed + && cosmic_modifiers_eq_smithay(&binding.modifiers, modifiers) + { + modifiers_queue.set(binding.clone()); + clear_queue = false; + } + + // is this a normal binding? + if binding.key.is_some() + && event.state() == KeyState::Pressed + && key_matches(binding.key.unwrap()) + && cosmic_modifiers_eq_smithay(&binding.modifiers, modifiers) + { + modifiers_queue.clear(); + seat.supressed_keys().add(&handle, None); + return FilterResult::Intercept(Some(( + Action::Shortcut(action.clone()), + binding.clone(), + ))); + } + } + } + + // no binding + if clear_queue { + seat.modifiers_shortcut_queue().clear(); + } + // keys are passed through to apps + FilterResult::Forward + } + + fn keyboard_swap( + &self, + seat: &Seat, + shell: &mut Shell, + old_descriptor: &NodeDesc, + current_focus: Option, + focused_output: &Output, + ) { + if let Some(focus) = current_focus { + if let Some(new_descriptor) = shell + .workspaces + .active(focused_output) + .unwrap() + .1 + .node_desc(focus) + { + let mut spaces = shell.workspaces.spaces_mut(); + if old_descriptor.handle != new_descriptor.handle { + let (mut old_w, mut other_w) = + spaces.partition::, _>(|w| w.handle == old_descriptor.handle); + if let Some(old_workspace) = old_w.get_mut(0) + && let Some(new_workspace) = other_w + .iter_mut() + .find(|w| w.handle == new_descriptor.handle) + { + { + let mut stack = new_workspace.focus_stack.get_mut(seat); + for elem in old_descriptor.focus_stack.iter().flat_map(|node_id| { + old_workspace.tiling_layer.element_for_node(node_id) + }) { + stack.append(elem.clone()); + } + } + { + let mut stack = old_workspace.focus_stack.get_mut(seat); + for elem in new_descriptor.focus_stack.iter().flat_map(|node_id| { + new_workspace.tiling_layer.element_for_node(node_id) + }) { + stack.append(elem.clone()); + } + } + if let Some(focus) = TilingLayout::swap_trees( + &mut old_workspace.tiling_layer, + Some(&mut new_workspace.tiling_layer), + old_descriptor, + &new_descriptor, + ) { + let seat = seat.clone(); + self.common.event_loop_handle.insert_idle(move |state| { + Shell::set_focus(state, Some(&focus), &seat, None, true); + }); + } + old_workspace.refresh_focus_stack(); + new_workspace.refresh_focus_stack(); + } + } else if let Some(workspace) = spaces.find(|w| w.handle == new_descriptor.handle) { + if let Some(focus) = TilingLayout::swap_trees( + &mut workspace.tiling_layer, + None, + old_descriptor, + &new_descriptor, + ) { + std::mem::drop(spaces); + let seat = seat.clone(); + self.common.event_loop_handle.insert_idle(move |state| { + Shell::set_focus(state, Some(&focus), &seat, None, true); + }); + } + workspace.refresh_focus_stack(); + } + } + } else { + let new_workspace = shell.workspaces.active(focused_output).unwrap().1.handle; + if new_workspace != old_descriptor.handle { + let spaces = shell.workspaces.spaces_mut(); + let (mut old_w, mut other_w) = + spaces.partition::, _>(|w| w.handle == old_descriptor.handle); + if let Some(old_workspace) = old_w.get_mut(0) + && let Some(new_workspace) = + other_w.iter_mut().find(|w| w.handle == new_workspace) + && new_workspace.tiling_layer.windows().next().is_none() + { + { + let mut stack = new_workspace.focus_stack.get_mut(seat); + for elem in old_descriptor.focus_stack.iter().flat_map(|node_id| { + old_workspace.tiling_layer.element_for_node(node_id) + }) { + stack.append(elem.clone()); + } + } + if let Some(focus) = TilingLayout::move_tree( + &mut old_workspace.tiling_layer, + &mut new_workspace.tiling_layer, + &new_workspace.handle, + seat, + new_workspace.focus_stack.get(seat).iter(), + old_descriptor.clone(), + None, + ) { + let seat = seat.clone(); + self.common.event_loop_handle.insert_idle(move |state| { + Shell::set_focus(state, Some(&focus), &seat, None, true); + }); + } + old_workspace.refresh_focus_stack(); + } + } + } + } + + #[profiling::function] + pub fn element_under( + global_pos: Point, + output: &Output, + shell: &Shell, + seat: &Seat, + ) -> Option { + let (previous_workspace, workspace) = shell.workspaces.active(output)?; + let (previous_idx, idx) = shell.workspaces.active_num(output); + let previous_workspace = previous_workspace + .zip(previous_idx) + .map(|((w, start), idx)| (w.handle, idx, start)); + let workspace = (workspace.handle, idx); + let element_filter = if workspace_overview_is_open(output) { + ElementFilter::LayerShellOnly + } else { + ElementFilter::All + }; + + render_input_order( + shell, + output, + previous_workspace, + workspace, + element_filter, + |stage| { + match stage { + Stage::ZoomUI => {} + Stage::SessionLock(lock_surface) => { + return ControlFlow::Break(Ok(lock_surface + .cloned() + .map(KeyboardFocusTarget::LockSurface))); + } + Stage::LayerPopup { + layer, + popup, + location, + .. + } => { + if layer.can_receive_keyboard_focus() { + let surface = popup.wl_surface(); + if under_from_surface_tree( + surface, + global_pos.as_logical(), + location.as_logical(), + WindowSurfaceType::POPUP | WindowSurfaceType::SUBSURFACE, + ) + .is_some() + { + return ControlFlow::Break(Ok(Some( + KeyboardFocusTarget::LayerSurface(layer), + ))); + } + } + } + Stage::LayerSurface { + layer, location, .. + } => { + if under_from_surface_tree( + layer.wl_surface(), + global_pos.as_logical(), + location.as_logical(), + WindowSurfaceType::TOPLEVEL | WindowSurfaceType::SUBSURFACE, + ) + .is_some() + { + return ControlFlow::Break(Ok(if layer.can_receive_keyboard_focus() { + Some(KeyboardFocusTarget::LayerSurface(layer)) + } else { + // Don't change keyboard focus if in input region of layer shell + // surface, but surface doesn't have keyboard interactivity. + None + })); + } + } + Stage::OverrideRedirect { .. } => { + // Override redirect windows take a grab on their own via + // the Xwayland keyboard grab protocol. Don't focus them via click. + } + Stage::StickyPopups(layout) => { + if let Some(element) = + layout.popup_element_under(global_pos.to_local(output), seat) + { + return ControlFlow::Break(Ok(Some(element))); + } + } + Stage::Sticky(layout) => { + if let Some(element) = + layout.toplevel_element_under(global_pos.to_local(output), seat) + { + return ControlFlow::Break(Ok(Some(element))); + } + } + Stage::WorkspacePopups { workspace, offset } => { + let location = global_pos + offset.as_global().to_f64(); + let output = workspace.output(); + let output_geo = output.geometry().to_local(output); + if Rectangle::new(offset.as_local(), output_geo.size) + .intersection(output_geo) + .is_some_and(|geometry| { + geometry.contains(global_pos.to_local(output).to_i32_floor()) + }) + && let Some(element) = workspace.popup_element_under(location, seat) + { + return ControlFlow::Break(Ok(Some(element))); + } + } + Stage::Workspace { workspace, offset } => { + let location = global_pos + offset.as_global().to_f64(); + let output = workspace.output(); + let output_geo = output.geometry().to_local(output); + if Rectangle::new(offset.as_local(), output_geo.size) + .intersection(output_geo) + .is_some_and(|geometry| { + geometry.contains(global_pos.to_local(output).to_i32_floor()) + }) + && let Some(element) = workspace.toplevel_element_under(location, seat) + { + return ControlFlow::Break(Ok(Some(element))); + } + } + } + ControlFlow::Continue(()) + }, + ) + .ok() + .flatten() + } + + #[profiling::function] + pub fn surface_under( + global_pos: Point, + output: &Output, + shell: &Shell, + ) -> Option<(PointerFocusTarget, Point)> { + let (previous_workspace, workspace) = shell.workspaces.active(output)?; + let (previous_idx, idx) = shell.workspaces.active_num(output); + let previous_workspace = previous_workspace + .zip(previous_idx) + .map(|((w, start), idx)| (w.handle, idx, start)); + let workspace = (workspace.handle, idx); + + let element_filter = if workspace_overview_is_open(output) { + ElementFilter::LayerShellOnly + } else { + ElementFilter::All + }; + + let relative_pos = global_pos.to_local(output); + let output_geo = output.geometry(); + let overview = shell.overview_mode().0; + let seat = shell.seats.last_active(); + + render_input_order( + shell, + output, + previous_workspace, + workspace, + element_filter, + |stage| { + match stage { + Stage::ZoomUI => { + if let Some(zoom_state) = shell.zoom_state() + && let Some((target, loc)) = + zoom_state.surface_under(output, global_pos) + { + return ControlFlow::Break(Ok(Some((target, loc)))); + } + } + Stage::SessionLock(lock_surface) => { + return ControlFlow::Break(Ok(lock_surface.and_then(|surface| { + let location = output_geo.loc; + if let Some((surface, surface_loc)) = under_from_surface_tree( + surface.wl_surface(), + global_pos.as_logical(), + location.as_logical(), + WindowSurfaceType::ALL, + ) { + Some(( + PointerFocusTarget::WlSurface { + surface, + toplevel: None, + }, + surface_loc.as_global().to_f64(), + )) + } else { + None + } + }))); + } + Stage::LayerPopup { + popup, location, .. + } => { + let surface = popup.wl_surface(); + if let Some((surface, surface_loc)) = under_from_surface_tree( + surface, + global_pos.as_logical(), + location.as_logical(), + WindowSurfaceType::ALL, + ) { + return ControlFlow::Break(Ok(Some(( + PointerFocusTarget::WlSurface { + surface, + toplevel: None, + }, + surface_loc.as_global().to_f64(), + )))); + } + } + Stage::LayerSurface { + layer, location, .. + } => { + let surface = layer.wl_surface(); + if let Some((surface, surface_loc)) = under_from_surface_tree( + surface, + global_pos.as_logical(), + location.as_logical(), + WindowSurfaceType::ALL, + ) { + return ControlFlow::Break(Ok(Some(( + PointerFocusTarget::WlSurface { + surface, + toplevel: None, + }, + surface_loc.as_global().to_f64(), + )))); + } + } + Stage::OverrideRedirect { surface, location } => { + if let Some((_, surface_loc)) = surface.surface_under( + global_pos.as_logical(), + location.as_logical(), + WindowSurfaceType::ALL, + ) { + return ControlFlow::Break(Ok(Some(( + PointerFocusTarget::X11Surface { + surface: surface.clone(), + toplevel: None, + }, + surface_loc.as_global().to_f64(), + )))); + } + } + Stage::StickyPopups(floating_layer) => { + if let Some(under) = floating_layer + .popup_surface_under(relative_pos, seat) + .map(|(target, point)| (target, point.to_global(output))) + { + return ControlFlow::Break(Ok(Some(under))); + } + } + Stage::Sticky(floating_layer) => { + if let Some(under) = floating_layer + .toplevel_surface_under(relative_pos, seat) + .map(|(target, point)| (target, point.to_global(output))) + { + return ControlFlow::Break(Ok(Some(under))); + } + } + Stage::WorkspacePopups { workspace, offset } => { + let global_pos = global_pos + offset.to_f64().as_global(); + if let Some(under) = + workspace.popup_surface_under(global_pos, overview.clone(), seat) + { + return ControlFlow::Break(Ok(Some(under))); + } + } + Stage::Workspace { workspace, offset } => { + let global_pos = global_pos + offset.to_f64().as_global(); + if let Some(under) = + workspace.toplevel_surface_under(global_pos, overview.clone(), seat) + { + return ControlFlow::Break(Ok(Some(under))); + } + } + } + + ControlFlow::Continue(()) + }, + ) + .ok() + .flatten() + } + + pub fn apply_cursor_hint( + &mut self, + surface: &WlSurface, + pointer: &PointerHandle, + mut location: Point, + ) { + let Some(client) = surface.client() else { + return; + }; + location = location.downscale(self.client_compositor_state(&client).client_scale()); + + let point_and_output = { + let shell = self.common.shell.read(); + let found = shell.workspaces.sets.iter().find_map(|(out, set)| { + set.surface_geometry_offset_from_toplevel(surface) + .map(|(geometry, surface_offset)| (out, geometry, surface_offset)) + }); + + if let Some((output, geometry, surface_offset)) = found { + let pos_in_element = location + surface_offset.to_f64(); + let window_size = geometry.size.to_f64(); + + let is_legal = |p: Point| { + let in_window = + p.x >= 0.0 && p.y >= 0.0 && p.x < window_size.w && p.y < window_size.h; + if !in_window { + return false; + } + + with_pointer_constraint(surface, pointer, |constraint| { + if let Some(constraint) = constraint + && let Some(region) = constraint.region() + { + let point_in_surface = (p - surface_offset.to_f64()).to_i32_floor(); + return region.contains(point_in_surface); + } + true + }) + }; + + let workspace_origin = output.geometry().loc.to_f64(); + let origin = geometry.loc.to_f64(); + + if is_legal(pos_in_element) { + let x = workspace_origin.x + origin.x + pos_in_element.x; + let y = workspace_origin.y + origin.y + pos_in_element.y; + Some((Point::<_, Global>::new(x, y), output.clone())) + } else { + None + } + } else { + None + } + }; + + if let Some((point, output)) = point_and_output { + // TODO: Replace with `wl_pointer.warp` + let original_position = pointer.current_location(); + let serial = SERIAL_COUNTER.next_serial(); + let under = State::surface_under(point, &output, &self.common.shell.write()) + .map(|(target, pos)| (target, pos.as_logical())); + let time = self.common.clock.now(); + pointer.relative_motion( + self, + under.clone(), + &RelativeMotionEvent { + delta: (0., 0.).into(), + delta_unaccel: (0., 0.).into(), + utime: time.as_micros(), + }, + ); + pointer.motion( + self, + under, + &MotionEvent { + location: point.as_logical(), + serial, + time: time.as_millis(), + }, + ); + pointer.frame(self); + + let mut shell = self.common.shell.write(); + shell.update_pointer_position(point.to_local(&output), &output); + + let seat = shell + .seats + .iter() + .find(|s| s.get_pointer().as_ref() == Some(pointer)) + .cloned(); + + if let Some(seat) = seat { + shell.update_focal_point( + &seat, + original_position.as_global(), + self.common.config.cosmic_conf.accessibility_zoom.view_moves, + ); + + update_output_image_copy_cursor_position( + &shell, + &self.common.clock, + &output, + &seat, + point, + ); + } + } + } +} + +// Output and workspace sessions for the given output +fn cursor_sessions_for_output<'a>( + shell: &'a Shell, + output: &'a Output, +) -> impl Iterator + 'a { + shell + .active_space(output) + .into_iter() + .flat_map(|workspace| { + workspace + .cursor_sessions() + .into_iter() + .chain(output.cursor_sessions()) + }) +} + +fn transform_output_mapped_position( + output: &Output, + event: &E, + zoom_state: Option<&ZoomState>, +) -> Point +where + B: InputBackend, + E: AbsolutePositionEvent, + B::Device: 'static, +{ + let geometry = zoom_state + .and_then(|_| output.zoomed_geometry()) + .unwrap_or_else(|| output.geometry()); + let transform = output.current_transform(); + let size = transform + .invert() + .transform_size(geometry.size.as_logical()); + geometry.loc.to_f64() + + transform + .transform_point_in(event.position_transformed(size), &size.to_f64()) + .as_global() +} + +// TODO Is it possible to determine mapping for external touchscreen? +// Support map_to_region like sway? +fn mapped_output_for_device<'a, D: Device + 'static>( + config: &Config, + shell: &'a Shell, + device: &D, +) -> Option<&'a Output> { + let map_to_output = if let Some(device) = ::downcast_ref::(device) { + config + .map_to_output(device) + .and_then(|name| shell.outputs().find(|output| output.name() == name)) + } else { + None + }; + map_to_output.or_else(|| shell.builtin_output()) +} + +pub fn update_output_image_copy_cursor_position( + shell: &Shell, + clock: &Clock, + output: &Output, + seat: &Seat, + position: Point, +) { + let output_geometry = output.geometry(); + for session in cursor_sessions_for_output(&shell, &output) { + if let Some(cursor_geometry) = seat.cursor_geometry( + (position - output_geometry.loc.to_f64()) + .as_logical() + .to_buffer( + output.current_scale().fractional_scale(), + output.current_transform(), + &output_geometry.size.to_f64().as_logical(), + ), + clock.now(), + ) { + let constraints = cursor_capture_constraints(Some(cursor_geometry)); + if session + .current_constraints() + .map(|current_constraints| current_constraints.size != constraints.size) + .unwrap_or(true) + { + session.update_constraints(constraints); + } + session.set_cursor_hotspot(cursor_geometry.hotspot); + session.set_cursor_pos(Some(cursor_geometry.geometry.loc)); + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f728294 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,362 @@ +#![allow( + clippy::too_many_arguments, + clippy::type_complexity, + clippy::len_without_is_empty, + clippy::collapsible_match +)] +// SPDX-License-Identifier: GPL-3.0-only + +use calloop::timer::{TimeoutAction, Timer}; +use smithay::{ + reexports::{ + calloop::{EventLoop, Interest, Mode, PostAction, generic::Generic}, + wayland_server::{Display, DisplayHandle}, + }, + wayland::socket::ListeningSocketSource, +}; + +use anyhow::{Context, Result}; +use state::{BackendData, LastRefresh, State}; +use std::{ + env, + ffi::OsString, + os::unix::process::CommandExt, + process, + sync::Arc, + time::{Duration, Instant}, +}; +use tracing::{error, info, warn}; +use wayland::protocols::{ + keyboard_layout::KeyboardLayoutState, overlap_notify::OverlapNotifyState, +}; + +use crate::wayland::handlers::compositor::client_compositor_state; + +use clap_lex::RawArgs; + +use std::error::Error; + +pub mod backend; +pub mod config; +pub mod dbus; +#[cfg(feature = "debug")] +pub mod debug; +pub mod hooks; +pub mod input; +mod logger; +pub mod session; +pub mod shell; +pub mod state; +#[cfg(feature = "systemd")] +pub mod systemd; +pub mod theme; +pub mod utils; +pub mod wayland; +pub mod xwayland; + +#[cfg(feature = "profile-with-tracy")] +#[global_allocator] +static GLOBAL: profiling::tracy_client::ProfiledAllocator = + profiling::tracy_client::ProfiledAllocator::new(std::alloc::System, 10); + +// called by the Xwayland source, either after starting or failing +impl State { + fn notify_ready(&mut self) { + // TODO: Don't notify again, but potentially import updated env-variables + // into systemd and the session? + self.ready.call_once(|| { + // potentially tell systemd we are setup now + if let state::BackendData::Kms(_) = &self.backend { + #[cfg(feature = "systemd")] + systemd::ready(&self.common); + if let Err(err) = dbus::ready(&self.common) { + error!(?err, "Failed to update the D-Bus activation environment"); + } + } + + // potentially tell the session we are setup now + if let Err(err) = + session::run_socket(self.common.event_loop_handle.clone(), &self.common) + { + warn!(?err, "Failed to setup cosmic-session communication"); + } + + self.common.kiosk_child = if let Some(mut command) = self.kiosk_command.take() { + // Run command in kiosk mode + command.envs( + session::get_env(&self.common).expect("WAYLAND_DISPLAY should be valid UTF-8"), + ); + unsafe { + command.pre_exec(|| { + utils::rlimit::restore_nofile_limit(); + Ok(()) + }) + }; + + info!("Running {:?}", command.get_program()); + command + .spawn() + .map_err(|err| { + // TODO: replace with `inspect_err` once stable + error!(?err, "Error running kiosk child."); + err + }) + .ok() + } else { + None + }; + }); + } +} + +pub fn run(hooks: crate::hooks::Hooks) -> Result<(), Box> { + let raw_args = RawArgs::from_args(); + let mut cursor = raw_args.cursor(); + raw_args.next_os(&mut cursor); + let git_hash = option_env!("GIT_HASH").unwrap_or("unknown"); + + let mut kiosk_command = None; + let mut with_xwayland = true; + // Parse the arguments + while let Some(arg) = raw_args.next_os(&mut cursor) { + match arg.to_str() { + Some("--help") | Some("-h") => { + print_help(env!("CARGO_PKG_VERSION"), git_hash); + return Ok(()); + } + Some("--no-xwayland") => { + tracing::info!("Running without Xwayland"); + with_xwayland = false; + } + Some("--version") | Some("-V") => { + println!( + "cosmic-comp {} (git commit {})", + env!("CARGO_PKG_VERSION"), + git_hash + ); + return Ok(()); + } + _ => { + let mut cmd = process::Command::new(arg); + cmd.args(raw_args.remaining(&mut cursor)); + kiosk_command = Some(cmd); + } + } + } + + // setup logger + logger::init_logger()?; + info!("Cosmic starting up!"); + + profiling::register_thread!("Main Thread"); + #[cfg(feature = "profile-with-tracy")] + tracy_client::Client::start(); + + utils::rlimit::increase_nofile_limit(); + // This needs to be done before any potential program launches + // (e.g. Xwayland) as it handles passed file descriptors. + if let Err(err) = session::setup_socket() { + warn!("Session error: {:?}", err); + }; + + // init hook globals + hooks::HOOKS.set(hooks) + .expect("Hooks global has already been initialized. Running multiple instances of COSMIC in one process is not supported."); + + // init event loop + let mut event_loop = EventLoop::try_new().with_context(|| "Failed to initialize event loop")?; + // init wayland + let (display, socket) = init_wayland_display(&mut event_loop)?; + // init state + let mut state = state::State::new( + &display, + socket, + event_loop.handle(), + event_loop.get_signal(), + with_xwayland, + kiosk_command, + ); + // init backend + backend::init_backend_auto(&display, &mut event_loop, &mut state)?; + + if let Err(err) = theme::watch_theme(event_loop.handle()) { + warn!(?err, "Failed to watch theme"); + } + + // run the event loop + event_loop.run(None, &mut state, |state| { + // shall we shut down? + if state.common.should_stop { + info!("Shutting down"); + state.common.event_loop_signal.stop(); + state.common.event_loop_signal.wakeup(); + return; + } + + // trigger routines + let clients = state.common.shell.write().update_animations(); + { + let dh = state.common.display_handle.clone(); + for client in clients.values() { + client_compositor_state(client).blocker_cleared(state, &dh); + } + } + + refresh(state); + + { + let shell = state.common.shell.read(); + if shell.animations_going() { + for output in shell.outputs().cloned().collect::>().into_iter() { + state.backend.schedule_render(&output); + } + } + } + + // send out events + let _ = state.common.display_handle.flush_clients(); + + // check if kiosk child is running + if let Some(child) = state.common.kiosk_child.as_mut() { + match child.try_wait() { + // Kiosk child exited with status + Ok(Some(exit_status)) => { + info!("Command exited with status {:?}", exit_status); + // Stop cleanly so surface threads are joined before exit() (signal -> 1). + state.common.kiosk_exit_code = Some(exit_status.code().unwrap_or(1)); + state.common.should_stop = true; + } + // Command still running + Ok(None) => {} + // Kiosk child disappeared, exiting with error + Err(err) => { + warn!(?err, "Failed to wait for command"); + state.common.kiosk_exit_code = Some(1); + state.common.should_stop = true; + } + } + } + })?; + + // kill kiosk child if loop exited + if let Some(mut child) = state.common.kiosk_child.take() { + let _ = child.kill(); + } + + let kiosk_exit_code = state.common.kiosk_exit_code; + + // Join surface threads before exit() so no thread is mid-eglCreateSync when + // Mesa's atexit handlers run and corrupt the heap (issue #2375). Safe here + // because the event loop has stopped; an unconditional join in Surface::Drop + // would instead deadlock against apply_config_for_outputs. + if let BackendData::Kms(kms) = &mut state.backend { + // Release master first so the surface drop path skips its blocking commit. + for device in kms.drm_devices.values_mut() { + device.drm.pause(); + } + for device in kms.drm_devices.values_mut() { + for (_, surface) in device.inner.surfaces.drain() { + surface.drop_and_join(); + } + } + } + + // drop eventloop & state before logger + std::mem::drop(event_loop); + std::mem::drop(state); + + if let Some(code) = kiosk_exit_code { + process::exit(code); + } + + Ok(()) +} + +fn print_help(version: &str, git_rev: &str) { + println!( + r#"cosmic-comp {version} (git commit {git_rev}) +System76 + +Designed for the COSMIC™ desktop environment, cosmic-comp is a Wayland Compositor. + +Project home page: https://github.com/pop-os/cosmic-comp + +Options: + -h, --help Show this message + --no-xwayland Run without Xwayland + -v, --version Show the version of cosmic-comp"# + ); +} + +fn init_wayland_display( + event_loop: &mut EventLoop, +) -> Result<(DisplayHandle, OsString)> { + let display = Display::new().unwrap(); + let handle = display.handle(); + + let source = ListeningSocketSource::new_auto().unwrap(); + let socket_name = source.socket_name().to_os_string(); + info!("Listening on {:?}", socket_name); + + event_loop + .handle() + .insert_source(source, |client_stream, _, state| { + let client_state = state.new_client_state(); + if let Err(err) = state + .common + .display_handle + .insert_client(client_stream, Arc::new(client_state)) + { + warn!(?err, "Error adding wayland client") + }; + }) + .with_context(|| "Failed to init the wayland socket source.")?; + event_loop + .handle() + .insert_source( + Generic::new(display, Interest::READ, Mode::Level), + move |_, display, state| { + // SAFETY: We don't drop the display + match unsafe { display.get_mut().dispatch_clients(state) } { + Ok(_) => Ok(PostAction::Continue), + Err(err) => { + error!(?err, "I/O error on the Wayland display"); + state.common.should_stop = true; + Err(err) + } + } + }, + ) + .with_context(|| "Failed to init the wayland event source.")?; + + Ok((handle, socket_name)) +} + +fn refresh(state: &mut State) { + if matches!(state.last_refresh, LastRefresh::Scheduled(_)) { + return; + } + + if matches!(state.last_refresh, LastRefresh::At(instant) if Instant::now().duration_since(instant) < Duration::from_millis(150)) + { + if let Ok(token) = state.common.event_loop_handle.insert_source( + Timer::from_duration(Duration::from_millis(150)), + |_, _, state| { + state.last_refresh = LastRefresh::None; + TimeoutAction::Drop + }, + ) { + state.last_refresh = LastRefresh::Scheduled(token); + return; + } else { + warn!("Failed to schedule refresh"); + } + } + + state.common.refresh(); + state::Common::refresh_focus(state); + OverlapNotifyState::refresh(state); + state.common.update_x11_stacking_order(); + KeyboardLayoutState::refresh(state); + state.last_refresh = LastRefresh::At(Instant::now()); +} diff --git a/src/logger/mod.rs b/src/logger/mod.rs new file mode 100644 index 0000000..3e5347d --- /dev/null +++ b/src/logger/mod.rs @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use std::str::FromStr; + +use anyhow::Result; + +use tracing::{debug, info, warn}; +use tracing_journald as journald; +use tracing_subscriber::{EnvFilter, filter::Directive, fmt, prelude::*}; + +pub fn init_logger() -> Result<()> { + let level = if cfg!(debug_assertions) { + "debug" + } else { + "warn" + }; + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| { + EnvFilter::new(if cfg!(debug_assertions) { + "info" + } else { + "warn" + }) + }) + .add_directive(Directive::from_str("cosmic_text=error").unwrap()) + .add_directive(Directive::from_str("calloop=error").unwrap()) + .add_directive(Directive::from_str(&format!("smithay={level}")).unwrap()) + .add_directive(Directive::from_str(&format!("cosmic_comp={level}")).unwrap()); + + let fmt_layer = fmt::layer().compact(); + + match journald::layer() { + Ok(journald_layer) => tracing_subscriber::registry() + .with(fmt_layer) + .with(journald_layer) + .with(filter) + .init(), + Err(err) => { + tracing_subscriber::registry() + .with(fmt_layer) + .with(filter) + .init(); + warn!(?err, "Failed to init journald logging."); + } + }; + log_panics::init(); + + info!("Version: {}", std::env!("CARGO_PKG_VERSION")); + if cfg!(feature = "debug") { + debug!( + "Debug build ({})", + std::option_env!("GIT_HASH").unwrap_or("Unknown") + ); + } + + Ok(()) +} diff --git a/src/logger/serializer.rs b/src/logger/serializer.rs new file mode 100644 index 0000000..c2c3afb --- /dev/null +++ b/src/logger/serializer.rs @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: MPL-2.0 OR MIT OR Apache-2.0 +// Taken from slog_json: +// https://github.com/slog-rs/json/blob/c45d09422d7114222ee5b7f6b0123de2605c7864/src/lib.rs#L39-L160 + +use serde::ser::SerializeMap; +use serde::serde_if_integer128; +use slog::Key; +use std::{fmt, io, result}; + +use std::cell::RefCell; +use std::fmt::Write; + +thread_local! { + static TL_BUF: RefCell = RefCell::new(String::with_capacity(128)) +} + +/// `slog::Serializer` adapter for `serde::Serializer` +/// +/// Newtype to wrap serde Serializer, so that `Serialize` can be implemented +/// for it +pub struct SerdeSerializer { + /// Current state of map serializing: `serde::Serializer::MapState` + ser_map: S::SerializeMap, +} + +impl SerdeSerializer { + /// Start serializing map of values + pub fn start(ser: S, len: Option) -> result::Result { + let ser_map = ser.serialize_map(len).map_err(|e| { + io::Error::new( + io::ErrorKind::Other, + format!("serde serialization error: {}", e), + ) + })?; + Ok(SerdeSerializer { ser_map }) + } + + /// Finish serialization, and return the serializer + pub fn end(self) -> result::Result { + self.ser_map.end() + } +} + +macro_rules! impl_m( + ($s:expr, $key:expr, $val:expr) => ({ + let k_s: &str = $key.as_ref(); + $s.ser_map.serialize_entry(k_s, $val) + .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("serde serialization error: {}", e)))?; + Ok(()) + }); +); + +impl slog::Serializer for SerdeSerializer +where + S: serde::Serializer, +{ + fn emit_bool(&mut self, key: Key, val: bool) -> slog::Result { + impl_m!(self, key, &val) + } + + fn emit_unit(&mut self, key: Key) -> slog::Result { + impl_m!(self, key, &()) + } + + fn emit_char(&mut self, key: Key, val: char) -> slog::Result { + impl_m!(self, key, &val) + } + + fn emit_none(&mut self, key: Key) -> slog::Result { + let val: Option<()> = None; + impl_m!(self, key, &val) + } + fn emit_u8(&mut self, key: Key, val: u8) -> slog::Result { + impl_m!(self, key, &val) + } + fn emit_i8(&mut self, key: Key, val: i8) -> slog::Result { + impl_m!(self, key, &val) + } + fn emit_u16(&mut self, key: Key, val: u16) -> slog::Result { + impl_m!(self, key, &val) + } + fn emit_i16(&mut self, key: Key, val: i16) -> slog::Result { + impl_m!(self, key, &val) + } + fn emit_usize(&mut self, key: Key, val: usize) -> slog::Result { + impl_m!(self, key, &val) + } + fn emit_isize(&mut self, key: Key, val: isize) -> slog::Result { + impl_m!(self, key, &val) + } + fn emit_u32(&mut self, key: Key, val: u32) -> slog::Result { + impl_m!(self, key, &val) + } + fn emit_i32(&mut self, key: Key, val: i32) -> slog::Result { + impl_m!(self, key, &val) + } + fn emit_f32(&mut self, key: Key, val: f32) -> slog::Result { + impl_m!(self, key, &val) + } + fn emit_u64(&mut self, key: Key, val: u64) -> slog::Result { + impl_m!(self, key, &val) + } + fn emit_i64(&mut self, key: Key, val: i64) -> slog::Result { + impl_m!(self, key, &val) + } + fn emit_f64(&mut self, key: Key, val: f64) -> slog::Result { + impl_m!(self, key, &val) + } + serde_if_integer128! { + fn emit_u128(&mut self, key: Key, val: u128) -> slog::Result { + impl_m!(self, key, &val) + } + fn emit_i128(&mut self, key: Key, val: i128) -> slog::Result { + impl_m!(self, key, &val) + } + } + fn emit_str(&mut self, key: Key, val: &str) -> slog::Result { + impl_m!(self, key, &val) + } + fn emit_arguments(&mut self, key: Key, val: &fmt::Arguments) -> slog::Result { + TL_BUF.with(|buf| { + let mut buf = buf.borrow_mut(); + + buf.write_fmt(*val).unwrap(); + + let res = { || impl_m!(self, key, &*buf) }(); + buf.clear(); + res + }) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..7ae0599 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-only + +fn main() { + if let Err(err) = cosmic_comp::run(Default::default()) { + tracing::error!("Error occured in main(): {}", err); + std::process::exit(1); + } +} diff --git a/src/session.rs b/src/session.rs new file mode 100644 index 0000000..0b4025a --- /dev/null +++ b/src/session.rs @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use smithay::reexports::{ + calloop::{Interest, LoopHandle, Mode, PostAction, generic::Generic}, + rustix, +}; + +use anyhow::{Context, Result, anyhow}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + io::{Read, Write}, + os::unix::{ + io::{AsFd, BorrowedFd, FromRawFd, RawFd}, + net::UnixStream, + }, +}; +use tracing::{error, warn}; + +use crate::state::{Common, State}; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case", tag = "message")] +pub enum Message { + SetEnv { variables: HashMap }, +} + +struct StreamWrapper { + stream: UnixStream, + buffer: Vec, + size: u16, + read_bytes: usize, +} +impl AsFd for StreamWrapper { + fn as_fd(&self) -> BorrowedFd<'_> { + self.stream.as_fd() + } +} +impl From for StreamWrapper { + fn from(stream: UnixStream) -> StreamWrapper { + StreamWrapper { + stream, + buffer: Vec::new(), + size: 0, + read_bytes: 0, + } + } +} + +unsafe fn set_cloexec(fd: RawFd) -> rustix::io::Result<()> { + if fd == -1 { + return Err(rustix::io::Errno::BADF); + } + let fd = unsafe { BorrowedFd::borrow_raw(fd) }; + let flags = rustix::io::fcntl_getfd(fd)?; + rustix::io::fcntl_setfd(fd, flags | rustix::io::FdFlags::CLOEXEC) +} + +pub fn get_env(common: &Common) -> Result> { + let mut env = HashMap::new(); + env.insert( + String::from("WAYLAND_DISPLAY"), + common + .socket + .clone() + .into_string() + .map_err(|_| anyhow!("wayland socket is no valid utf-8 string?"))?, + ); + if let Some(display) = common.xwayland_state.as_ref().map(|s| s.display) { + env.insert(String::from("DISPLAY"), format!(":{}", display)); + } + Ok(env) +} + +pub fn setup_socket() -> Result<()> { + if let Ok(fd_num) = std::env::var("COSMIC_SESSION_SOCK") + && let Ok(fd) = fd_num.parse::() + { + let res = unsafe { set_cloexec(fd) }.with_context(|| "Failed to setup session socket"); + if res.is_err() { + unsafe { rustix::io::close(fd) }; + } + res + } else { + Ok(()) + } +} + +pub fn run_socket(handle: LoopHandle, common: &Common) -> Result<()> { + if let Ok(fd_num) = std::env::var("COSMIC_SESSION_SOCK") { + if let Ok(fd) = fd_num.parse::() { + let mut session_socket = unsafe { UnixStream::from_raw_fd(fd) }; + + let env = get_env(common)?; + let message = serde_json::to_string(&Message::SetEnv { variables: env }) + .with_context(|| "Failed to encode environment variables into json")?; + let bytes = message.into_bytes(); + let len = (bytes.len() as u16).to_ne_bytes(); + session_socket + .write_all(&len) + .with_context(|| "Failed to write message len")?; + session_socket + .write_all(&bytes) + .with_context(|| "Failed to write message bytes")?; + + handle.insert_source( + Generic::new(StreamWrapper::from(session_socket), Interest::READ, Mode::Level), + move |_, stream, _state| { + // SAFETY: We don't drop the stream! + let stream = unsafe { stream.get_mut() }; + + if stream.size == 0 { + let mut len = [0u8; 2]; + match stream.stream.read_exact(&mut len) { + Ok(()) => { + stream.size = u16::from_ne_bytes(len); + stream.buffer = vec![0; stream.size as usize]; + }, + Err(err) => { + warn!(?err, "Error reading from session socket"); + return Ok(PostAction::Remove); + } + } + } + + stream.read_bytes += match stream.stream.read(&mut stream.buffer) { + Ok(size) => size, + Err(err) => { + error!(?err, "Error reading from session socket"); + return Ok(PostAction::Remove); + } + }; + + if stream.read_bytes != 0 && stream.read_bytes == stream.size as usize { + stream.size = 0; + stream.read_bytes = 0; + match std::str::from_utf8(&stream.buffer) { + Ok(message) => { + match serde_json::from_str::<'_, Message>(message) { + Ok(Message::SetEnv { .. }) => warn!("Got SetEnv from session? What is this?"), + _ => warn!("Unknown session socket message, are you using incompatible cosmic-session and cosmic-comp versions?"), + }; + Ok(PostAction::Continue) + }, + Err(err) => { + warn!(?err, "Invalid message from session sock"); + Ok(PostAction::Continue) + } + } + } else { + Ok(PostAction::Continue) + } + }, + ).with_context(|| "Failed to init the cosmic session socket source")?; + } else { + error!(socket = fd_num, "COSMIC_SESSION_SOCK is no valid RawFd."); + } + }; + + Ok(()) +} diff --git a/src/shell/element/mod.rs b/src/shell/element/mod.rs new file mode 100644 index 0000000..1473671 --- /dev/null +++ b/src/shell/element/mod.rs @@ -0,0 +1,1607 @@ +use crate::{ + backend::render::element::AsGlowRenderer, + state::State, + utils::{ + iced::{IcedElementInternal, IcedRenderElement}, + prelude::*, + }, +}; +use calloop::LoopHandle; +use cosmic_comp_config::AppearanceConfig; +use id_tree::NodeId; +use smithay::{ + backend::{ + drm::DrmNode, + input::KeyState, + renderer::{ + element::{ + Element, Kind, RenderElement, UnderlyingStorage, + utils::{CropRenderElement, RelocateRenderElement, RescaleRenderElement}, + }, + gles::element::PixelShaderElement, + glow::GlowRenderer, + utils::{DamageSet, OpaqueRegions}, + }, + }, + desktop::{WindowSurfaceType, space::SpaceElement}, + input::{ + Seat, + keyboard::{KeyboardTarget, KeysymHandle, ModifiersState}, + }, + output::Output, + reexports::wayland_server::{backend::ObjectId, protocol::wl_surface::WlSurface}, + space_elements, + utils::{ + Buffer as BufferCoords, IsAlive, Logical, Physical, Point, Rectangle, Scale, Serial, Size, + user_data::UserDataMap, + }, + wayland::seat::WaylandFocus, + xwayland::{X11Surface, xwm::X11Relatable}, +}; +use stack::CosmicStackInternal; +use window::CosmicWindowInternal; + +use std::{ + borrow::Cow, + fmt, + hash::Hash, + sync::{Arc, Mutex, Weak, atomic::AtomicBool}, +}; + +pub mod surface; +use self::stack::MoveResult; +pub use self::surface::CosmicSurface; +pub mod stack; +pub use self::stack::CosmicStack; +pub mod window; +pub use self::window::CosmicWindow; +pub mod resize_indicator; +pub mod stack_hover; +pub mod swap_indicator; + +#[cfg(feature = "debug")] +use egui_plot::{Corner, Legend, Plot, PlotPoints, Polygon}; +#[cfg(feature = "debug")] +use smithay::backend::renderer::{element::texture::TextureRenderElement, gles::GlesTexture}; +#[cfg(feature = "debug")] +use smithay::desktop::WindowSurface; +#[cfg(feature = "debug")] +use tracing::debug; + +use super::{ + ManagedLayer, + focus::target::PointerFocusTarget, + layout::{ + floating::{ResizeState, TiledCorners}, + tiling::NodeDesc, + }, +}; +use cosmic_settings_config::shortcuts::action::{Direction, FocusDirection}; + +space_elements! { + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + CosmicMappedInternal; + Window=CosmicWindow, + Stack=CosmicStack, +} + +#[derive(Debug, Clone, Copy)] +pub struct MaximizedState { + pub original_geometry: Rectangle, + pub original_layer: ManagedLayer, + pub original_snapped: Option, +} + +#[derive(Clone)] +pub struct CosmicMapped { + element: CosmicMappedInternal, + + // associated data + pub maximized_state: Arc>>, + + //tiling + pub tiling_node_id: Arc>>, + //floating + pub(super) resize_state: Arc>>, + pub last_geometry: Arc>>>, + pub moved_since_mapped: Arc, + pub floating_tiled: Arc>>, + //sticky + pub previous_layer: Arc>>, + + #[cfg(feature = "debug")] + debug: Arc>>, +} + +impl fmt::Debug for CosmicMapped { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("CosmicMapped") + .field("element", &self.element) + .field("maximized_state", &self.maximized_state) + .field("tiling_node_id", &self.tiling_node_id) + .field("resize_state", &self.resize_state) + .field("last_geometry", &self.last_geometry) + .field("moved_since_mapped", &self.moved_since_mapped) + .field("floating_tiled", &self.floating_tiled) + .finish() + } +} + +#[derive(Clone, Debug)] +pub struct CosmicMappedKey(CosmicMappedKeyInner); +#[derive(Clone, Debug)] +enum CosmicMappedKeyInner { + Window(Weak>>), + Stack(Weak>>), +} + +impl Hash for CosmicMappedKey { + fn hash(&self, state: &mut H) { + match &self.0 { + CosmicMappedKeyInner::Window(weak) => weak.as_ptr().hash(state), + CosmicMappedKeyInner::Stack(weak) => weak.as_ptr().hash(state), + } + } +} + +impl IsAlive for CosmicMappedKey { + fn alive(&self) -> bool { + match &self.0 { + CosmicMappedKeyInner::Window(weak) => weak.strong_count() > 0, + CosmicMappedKeyInner::Stack(weak) => weak.strong_count() > 0, + } + } +} + +impl PartialEq for CosmicMappedKey { + fn eq(&self, other: &Self) -> bool { + match (&self.0, &other.0) { + (CosmicMappedKeyInner::Window(weak1), CosmicMappedKeyInner::Window(weak2)) => { + Weak::ptr_eq(weak1, weak2) + } + (CosmicMappedKeyInner::Stack(weak1), CosmicMappedKeyInner::Stack(weak2)) => { + Weak::ptr_eq(weak1, weak2) + } + _ => false, + } + } +} +impl Eq for CosmicMappedKey {} + +impl PartialEq for CosmicMapped { + fn eq(&self, other: &CosmicMappedKey) -> bool { + match (&self.element, &other.0) { + (CosmicMappedInternal::Window(window), CosmicMappedKeyInner::Window(weak)) => { + Arc::as_ptr(&window.0.0) == weak.as_ptr() + } + (CosmicMappedInternal::Stack(stack), CosmicMappedKeyInner::Stack(weak)) => { + Arc::as_ptr(&stack.0.0) == weak.as_ptr() + } + _ => false, + } + } +} + +impl PartialEq for CosmicMapped { + fn eq(&self, other: &Self) -> bool { + self.element == other.element + } +} + +impl Eq for CosmicMapped {} + +impl Hash for CosmicMapped { + fn hash(&self, state: &mut H) { + self.element.hash(state) + } +} + +impl CosmicMapped { + pub fn windows(&self) -> impl Iterator)> + '_ { + match &self.element { + CosmicMappedInternal::Stack(stack) => { + Box::new(stack.surfaces().map(|w| (w, stack.offset()))) + as Box)>> + } + CosmicMappedInternal::Window(window) => { + Box::new(std::iter::once((window.surface(), window.offset()))) + } + _ => Box::new(std::iter::empty()), + } + } + + pub fn active_window(&self) -> CosmicSurface { + match &self.element { + CosmicMappedInternal::Stack(stack) => stack.active(), + CosmicMappedInternal::Window(win) => win.surface(), + _ => unreachable!(), + } + } + + pub fn has_active_window(&self, window: &CosmicSurface) -> bool { + match &self.element { + CosmicMappedInternal::Stack(stack) => stack.has_active(window), + CosmicMappedInternal::Window(win) => win.contains_surface(window), + _ => unreachable!(), + } + } + + pub fn active_window_offset(&self) -> Point { + match &self.element { + CosmicMappedInternal::Stack(stack) => stack.offset(), + CosmicMappedInternal::Window(window) => window.offset(), + _ => unreachable!(), + } + } + + pub fn active_window_geometry(&self) -> Rectangle { + match &self.element { + CosmicMappedInternal::Stack(stack) => { + let win = stack.active(); + let location = stack.offset(); + let size = win.geometry().size; + Rectangle::new(location, size) + } + CosmicMappedInternal::Window(win) => { + let location = win.offset(); + let size = win.geometry().size; + Rectangle::new(location, size) + } + _ => unreachable!(), + } + } + + pub fn set_active(&self, window: &S) + where + CosmicSurface: PartialEq, + { + if let CosmicMappedInternal::Stack(stack) = &self.element { + stack.set_active(window); + } + } + + pub fn focus_window(&self, window: &CosmicSurface) { + if let CosmicMappedInternal::Stack(stack) = &self.element { + stack.set_active(window) + } + } + + pub fn has_surface(&self, surface: &WlSurface, surface_type: WindowSurfaceType) -> bool { + self.windows() + .any(|(w, _)| w.has_surface(surface, surface_type)) + } + + pub fn surface_offset(&self, surface: &WlSurface) -> Option> { + self.windows().find_map(|(window, window_offset)| { + window + .surface_offset(surface) + .map(|offset| window_offset + offset) + }) + } + + /// Give the pointer target under a relative offset into this element. + /// + /// Returns Target + Offset relative to the target + pub fn focus_under( + &self, + relative_pos: Point, + surface_type: WindowSurfaceType, + seat: &Seat, + ) -> Option<(PointerFocusTarget, Point)> { + match &self.element { + CosmicMappedInternal::Stack(stack) => stack.focus_under(relative_pos, surface_type), + CosmicMappedInternal::Window(window) => { + window.focus_under(relative_pos, surface_type, Some(seat)) + } + _ => unreachable!(), + } + } + + pub fn handle_move(&self, direction: Direction) -> MoveResult { + if let CosmicMappedInternal::Stack(stack) = &self.element { + stack.handle_move(direction) + } else { + MoveResult::Default + } + } + + pub fn handle_focus( + &self, + seat: &Seat, + direction: FocusDirection, + swap: Option, + ) -> bool { + if let CosmicMappedInternal::Stack(stack) = &self.element { + stack.handle_focus(seat, direction, swap) + } else { + false + } + } + + pub fn set_resizing(&self, resizing: bool) { + for window in match &self.element { + CosmicMappedInternal::Stack(s) => { + Box::new(s.surfaces()) as Box> + } + CosmicMappedInternal::Window(w) => Box::new(std::iter::once(w.surface())), + _ => unreachable!(), + } { + window.set_resizing(resizing); + } + } + + pub fn is_resizing(&self, pending: bool) -> Option { + let window = match &self.element { + CosmicMappedInternal::Stack(s) => s.active(), + CosmicMappedInternal::Window(w) => w.surface(), + _ => unreachable!(), + }; + + window.is_resizing(pending) + } + + pub fn set_tiled(&self, tiled: bool) { + match &self.element { + CosmicMappedInternal::Stack(s) => s.set_tiled(tiled), + CosmicMappedInternal::Window(w) => w.set_tiled(tiled), + _ => unreachable!(), + } + } + + pub fn is_tiled(&self, pending: bool) -> Option { + let window = match &self.element { + CosmicMappedInternal::Stack(s) => s.active(), + CosmicMappedInternal::Window(w) => w.surface(), + _ => unreachable!(), + }; + + window.is_tiled(pending) + } + + pub fn set_fullscreen(&self, fullscreen: bool) { + for window in match &self.element { + CosmicMappedInternal::Stack(s) => { + Box::new(s.surfaces()) as Box> + } + CosmicMappedInternal::Window(w) => Box::new(std::iter::once(w.surface())), + _ => unreachable!(), + } { + window.set_fullscreen(fullscreen); + } + } + + pub fn is_fullscreen(&self, pending: bool) -> bool { + let window = match &self.element { + CosmicMappedInternal::Stack(s) => s.active(), + CosmicMappedInternal::Window(w) => w.surface(), + _ => unreachable!(), + }; + + window.is_fullscreen(pending) + } + + pub fn set_maximized(&self, maximized: bool) { + for window in match &self.element { + CosmicMappedInternal::Stack(s) => { + Box::new(s.surfaces()) as Box> + } + CosmicMappedInternal::Window(w) => Box::new(std::iter::once(w.surface())), + _ => unreachable!(), + } { + window.set_maximized(maximized) + } + } + + pub fn is_maximized(&self, pending: bool) -> bool { + let window = match &self.element { + CosmicMappedInternal::Stack(s) => s.active(), + CosmicMappedInternal::Window(w) => w.surface(), + _ => unreachable!(), + }; + + window.is_maximized(pending) + } + + pub fn set_activated(&self, activated: bool) { + match &self.element { + CosmicMappedInternal::Stack(s) => s.set_activate(activated), + CosmicMappedInternal::Window(w) => w.set_activate(activated), + _ => unreachable!(), + } + } + + pub fn is_activated(&self, pending: bool) -> bool { + let window = match &self.element { + CosmicMappedInternal::Stack(s) => s.active(), + CosmicMappedInternal::Window(w) => w.surface(), + _ => unreachable!(), + }; + + window.is_activated(pending) + } + + pub fn is_minimized(&self) -> bool { + self.active_window().is_minimized() + } + + pub fn set_minimized(&self, minimized: bool) { + for (w, _) in self.windows() { + w.set_minimized(minimized); + } + } + + pub fn pending_size(&self) -> Option> { + match &self.element { + CosmicMappedInternal::Stack(s) => s.pending_size(), + CosmicMappedInternal::Window(w) => w.pending_size(), + _ => unreachable!(), + } + } + + pub fn last_server_size(&self) -> Option> { + match &self.element { + CosmicMappedInternal::Stack(s) => s.last_server_size(), + CosmicMappedInternal::Window(w) => w.last_server_size(), + _ => unreachable!(), + } + } + + pub fn set_geometry(&self, geo: Rectangle) { + match &self.element { + CosmicMappedInternal::Stack(s) => s.set_geometry(geo), + CosmicMappedInternal::Window(w) => w.set_geometry(geo), + _ => {} + } + } + + pub fn on_commit(&self, surface: &WlSurface) { + match &self.element { + CosmicMappedInternal::Stack(s) => s.on_commit(surface), + CosmicMappedInternal::Window(w) => w.on_commit(surface), + _ => {} + } + } + + pub fn min_size(&self) -> Option> { + match &self.element { + CosmicMappedInternal::Stack(stack) => stack.min_size(), + CosmicMappedInternal::Window(window) => window.min_size(), + _ => unreachable!(), + } + } + + pub fn max_size(&self) -> Option> { + match &self.element { + CosmicMappedInternal::Stack(stack) => stack.max_size(), + CosmicMappedInternal::Window(window) => window.max_size(), + _ => unreachable!(), + } + } + + pub fn set_bounds(&self, size: impl Into>>) { + let size = size.into(); + for (surface, _) in self.windows() { + surface.set_bounds(size) + } + } + + pub fn latest_size_committed(&self) -> bool { + match &self.element { + CosmicMappedInternal::Stack(s) => s.surfaces().any(|s| s.latest_size_committed()), + CosmicMappedInternal::Window(w) => w.surface().latest_size_committed(), + _ => unreachable!(), + } + } + + pub fn configure(&self) -> Option { + match &self.element { + CosmicMappedInternal::Stack(s) => { + let active = s.active(); + for surface in s.surfaces().filter(|s| s != &active) { + surface.send_configure(); + } + active.send_configure() + } + CosmicMappedInternal::Window(w) => w.surface().send_configure(), + _ => unreachable!(), + } + } + + pub fn send_close(&self) { + let window = match &self.element { + CosmicMappedInternal::Stack(s) => s.active(), + CosmicMappedInternal::Window(w) => w.surface(), + _ => unreachable!(), + }; + + window.close(); + } + + pub fn is_window(&self) -> bool { + matches!(&self.element, CosmicMappedInternal::Window(_)) + } + + pub fn is_stack(&self) -> bool { + matches!(&self.element, CosmicMappedInternal::Stack(_)) + } + + pub fn stack_ref(&self) -> Option<&CosmicStack> { + match &self.element { + CosmicMappedInternal::Stack(stack) => Some(stack), + _ => None, + } + } + + pub fn convert_to_stack( + &mut self, + (output, overlap): (&Output, Rectangle), + theme: cosmic::Theme, + appearance: AppearanceConfig, + ) { + if let CosmicMappedInternal::Window(window) = &self.element { + let surface = window.surface(); + let activated = surface.is_activated(true); + let handle = window.loop_handle(); + + let stack = CosmicStack::new(std::iter::once(surface), handle, theme, appearance); + if let Some(geo) = *self.last_geometry.lock().unwrap() { + stack.set_geometry(geo.to_global(output)); + } + stack.output_enter(output, overlap); + stack.set_activate(activated); + stack.active().send_configure(); + stack.refresh(); + + self.element = CosmicMappedInternal::Stack(stack); + } + } + + pub fn convert_to_surface( + &mut self, + surface: CosmicSurface, + (output, overlap): (&Output, Rectangle), + theme: cosmic::Theme, + appearance: AppearanceConfig, + ) { + let handle = self.loop_handle(); + surface.try_force_undecorated(false); + surface.set_tiled(false); + let window = CosmicWindow::new(surface, handle, theme, appearance); + + if let Some(geo) = *self.last_geometry.lock().unwrap() { + window.set_geometry(geo.to_global(output)); + } + window.output_enter(output, overlap); + window.set_activate(self.is_activated(true)); + window.surface().send_configure(); + window.refresh(); + + self.element = CosmicMappedInternal::Window(window); + } + + pub(super) fn loop_handle(&self) -> LoopHandle<'static, crate::state::State> { + match &self.element { + CosmicMappedInternal::Stack(stack) => stack.loop_handle(), + CosmicMappedInternal::Window(window) => window.loop_handle(), + _ => unreachable!(), + } + } + + #[cfg(feature = "debug")] + pub fn set_debug(&self, flag: bool) { + let mut debug = self.debug.lock().unwrap(); + if flag { + *debug = Some(smithay_egui::EguiState::new(Rectangle::new( + (10, 10).into(), + (100, 100).into(), + ))); + } else { + debug.take(); + } + } + + pub fn push_popup_render_elements( + &self, + renderer: &mut R, + location: smithay::utils::Point, + scale: smithay::utils::Scale, + alpha: f32, + scanout_node: Option, + push: &mut dyn FnMut(CosmicMappedRenderElement), + ) where + R: AsGlowRenderer, + R::TextureId: Send + Clone + 'static, + CosmicMappedRenderElement: RenderElement, + { + match &self.element { + CosmicMappedInternal::Stack(s) => s.push_popup_render_elements( + renderer, + location, + scale, + alpha, + scanout_node, + &mut |elem| push(elem.into()), + ), + CosmicMappedInternal::Window(w) => w.push_popup_render_elements( + renderer, + location, + scale, + alpha, + scanout_node, + &mut |elem| push(elem.into()), + ), + _ => unreachable!(), + } + } + + pub fn shadow_render_element( + &self, + renderer: &mut R, + location: smithay::utils::Point, + max_size: Option>, + output_scale: smithay::utils::Scale, + scale: f64, + alpha: f32, + ) -> Option + where + R: AsGlowRenderer, + R::TextureId: Send + Clone + 'static, + CosmicMappedRenderElement: RenderElement, + C: From>, + { + if !self.element.alive() { + return None; + } + + match &self.element { + CosmicMappedInternal::Stack(s) => s + .shadow_render_element::>( + renderer, + location, + max_size, + output_scale, + scale, + alpha, + ) + .map(Into::into), + CosmicMappedInternal::Window(w) => w + .shadow_render_element::>( + renderer, + location, + max_size, + output_scale, + scale, + alpha, + ) + .map(Into::into), + _ => unreachable!(), + } + } + + pub fn push_render_elements( + &self, + renderer: &mut R, + location: smithay::utils::Point, + max_size: Option>, + scale: smithay::utils::Scale, + alpha: f32, + scanout_override: Option, + scanout_node: Option, + push_above: &mut dyn FnMut(CosmicMappedRenderElement), + push_below: &mut dyn FnMut(CosmicMappedRenderElement), + ) where + R: AsGlowRenderer, + R::TextureId: Send + Clone + 'static, + CosmicMappedRenderElement: RenderElement, + { + #[cfg(feature = "debug")] + if let Some(debug) = self.debug.lock().unwrap().as_mut() { + let window = self.active_window(); + let window_geo = window.geometry(); + let (min_size, max_size, size) = ( + window.min_size_without_ssd(), + window.max_size_without_ssd(), + window.geometry().size, + ); + + let area = Rectangle::::new( + location.to_f64().to_logical(scale).to_i32_round(), + self.bbox().size, + ); + + let glow_renderer = renderer.glow_renderer_mut(); + match debug.render( + |ctx| { + egui::Area::new("window".into()) + .anchor( + egui::Align2::RIGHT_TOP, + [ + -window_geo.loc.x as f32 - 10.0, + window_geo.loc.y as f32 - 10.0, + ], + ) + .show(ctx, |ui| { + egui::Frame::NONE + .fill(egui::Color32::BLACK) + .corner_radius(5.0) + .inner_margin(10.0) + .show(ui, |ui| { + ui.heading(window.title()); + ui.horizontal(|ui| { + ui.label("App ID: "); + ui.label(window.app_id()); + }); + ui.label(match window.0.underlying_surface() { + WindowSurface::Wayland(_) => "Protocol: Wayland", + WindowSurface::X11(_) => "Protocol: X11", + }); + if let WindowSurface::X11(surf) = window.0.underlying_surface() + { + let geo = surf.geometry(); + ui.label(format!( + "X11 Geo: {}x{}x{}x{}", + geo.loc.x, geo.loc.y, geo.size.w, geo.size.h + )); + } + ui.horizontal(|ui| { + ui.label("States: "); + if window.is_maximized(true) { + ui.label("🗖"); + } + if window.is_fullscreen(true) { + ui.label("⬜"); + } + if window.is_activated(true) { + ui.label("🖱"); + } + if window.is_resizing(true).is_some() { + ui.label("↔"); + } + }); + + let plot = Plot::new("Sizes") + .legend(Legend::default().position(Corner::RightBottom)) + .data_aspect(1.0) + .view_aspect(1.0) + .show_x(false) + .show_y(false) + .width(200.0) + .height(200.0); + plot.show(ui, |plot_ui| { + let center = if let Some(max_size) = max_size { + ((max_size.w + 20) / 2, (max_size.h + 20) / 2) + } else { + (100, 100) + }; + + if let Some(max_size) = max_size { + let max_size_rect = + Polygon::new(PlotPoints::new(vec![ + [10.0, 10.0], + [max_size.w as f64 + 10.0, 10.0], + [ + max_size.w as f64 + 10.0, + max_size.h as f64 + 10.0, + ], + [10.0, max_size.h as f64 + 10.0], + [10.0, 10.0], + ])); + plot_ui.polygon( + max_size_rect + .name(format!("{}x{}", max_size.w, max_size.h)), + ); + } + + let size_rect = Polygon::new(PlotPoints::new(vec![ + [ + (center.0 - size.w / 2) as f64, + (center.1 - size.h / 2) as f64, + ], + [ + (center.0 + size.w / 2) as f64, + (center.1 - size.h / 2) as f64, + ], + [ + (center.0 + size.w / 2) as f64, + (center.1 + size.h / 2) as f64, + ], + [ + (center.0 - size.w / 2) as f64, + (center.1 + size.h / 2) as f64, + ], + [ + (center.0 - size.w / 2) as f64, + (center.1 - size.h / 2) as f64, + ], + ])); + plot_ui.polygon( + size_rect.name(format!("{}x{}", size.w, size.h)), + ); + + if let Some(min_size) = min_size { + let min_size_rect = + Polygon::new(PlotPoints::new(vec![ + [ + (center.0 - min_size.w / 2) as f64, + (center.1 - min_size.h / 2) as f64, + ], + [ + (center.0 + min_size.w / 2) as f64, + (center.1 - min_size.h / 2) as f64, + ], + [ + (center.0 + min_size.w / 2) as f64, + (center.1 + min_size.h / 2) as f64, + ], + [ + (center.0 - min_size.w / 2) as f64, + (center.1 + min_size.h / 2) as f64, + ], + [ + (center.0 - min_size.w / 2) as f64, + (center.1 - min_size.h / 2) as f64, + ], + ])); + plot_ui.polygon( + min_size_rect + .name(format!("{}x{}", min_size.w, min_size.h)), + ); + } + }) + }) + }); + }, + glow_renderer, + area, + scale.x, + 0.8, + ) { + Ok(element) => push_above(element.into()), + Err(err) => { + debug!(?err, "Error rendering debug overlay."); + } + } + }; + + match &self.element { + CosmicMappedInternal::Stack(s) => s.push_render_elements( + renderer, + location, + max_size, + scale, + alpha, + scanout_override, + scanout_node, + &mut |elem| push_above(elem.into()), + &mut |elem| push_below(elem.into()), + ), + CosmicMappedInternal::Window(w) => w.push_render_elements( + renderer, + location, + max_size, + scale, + alpha, + scanout_override, + scanout_node, + &mut |elem| push_above(elem.into()), + &mut |elem| push_below(elem.into()), + ), + _ => unreachable!(), + } + } + + pub(crate) fn update_theme(&self, theme: cosmic::Theme) { + match &self.element { + CosmicMappedInternal::Window(w) => w.set_theme(theme), + CosmicMappedInternal::Stack(s) => s.set_theme(theme), + CosmicMappedInternal::_GenericCatcher(_) => {} + } + } + + pub(crate) fn update_appearance_conf(&self, appearance: &AppearanceConfig) { + match &self.element { + CosmicMappedInternal::Window(w) => w.update_appearance_conf(appearance), + CosmicMappedInternal::Stack(s) => s.update_appearance_conf(appearance), + CosmicMappedInternal::_GenericCatcher(_) => {} + } + } + + pub(crate) fn force_redraw(&self) { + match &self.element { + CosmicMappedInternal::Window(w) => w.force_redraw(), + CosmicMappedInternal::Stack(s) => s.force_redraw(), + CosmicMappedInternal::_GenericCatcher(_) => {} + } + } + + pub fn key(&self) -> CosmicMappedKey { + CosmicMappedKey(match &self.element { + CosmicMappedInternal::Stack(stack) => { + CosmicMappedKeyInner::Stack(Arc::downgrade(&stack.0.0)) + } + CosmicMappedInternal::Window(window) => { + CosmicMappedKeyInner::Window(Arc::downgrade(&window.0.0)) + } + _ => unreachable!(), + }) + } + + pub fn ssd_height(&self, pending: bool) -> Option { + match &self.element { + CosmicMappedInternal::Window(w) => (!w.surface().is_decorated(pending)) + .then_some(crate::shell::element::window::SSD_HEIGHT), + CosmicMappedInternal::Stack(_) => Some(crate::shell::element::stack::TAB_HEIGHT), + _ => unreachable!(), + } + } + + pub fn corner_radius(&self, geometry_size: Size, default_radius: u8) -> [u8; 4] { + match &self.element { + CosmicMappedInternal::Window(w) => w.corner_radius(geometry_size, default_radius), + CosmicMappedInternal::Stack(s) => s.corner_radius(geometry_size, default_radius), + _ => unreachable!(), + } + } +} + +impl IsAlive for CosmicMapped { + fn alive(&self) -> bool { + self.element.alive() + } +} + +impl SpaceElement for CosmicMapped { + fn bbox(&self) -> Rectangle { + SpaceElement::bbox(&self.element) + } + fn is_in_input_region(&self, point: &Point) -> bool { + SpaceElement::is_in_input_region(&self.element, point) + } + fn set_activate(&self, activated: bool) { + SpaceElement::set_activate(&self.element, activated) + } + fn output_enter(&self, output: &Output, overlap: Rectangle) { + SpaceElement::output_enter(&self.element, output, overlap) + } + fn output_leave(&self, output: &Output) { + SpaceElement::output_leave(&self.element, output) + } + fn geometry(&self) -> Rectangle { + SpaceElement::geometry(&self.element) + } + fn z_index(&self) -> u8 { + SpaceElement::z_index(&self.element) + } + #[profiling::function] + fn refresh(&self) { + SpaceElement::refresh(&self.element) + } +} + +impl X11Relatable for CosmicMapped { + fn is_window(&self, window: &X11Surface) -> bool { + self.active_window().x11_surface() == Some(window) + } +} + +impl KeyboardTarget for CosmicMapped { + fn enter( + &self, + seat: &Seat, + data: &mut State, + keys: Vec>, + serial: Serial, + ) { + match &self.element { + CosmicMappedInternal::Stack(s) => KeyboardTarget::enter(s, seat, data, keys, serial), + CosmicMappedInternal::Window(w) => KeyboardTarget::enter(w, seat, data, keys, serial), + _ => {} + } + } + fn leave(&self, seat: &Seat, data: &mut State, serial: Serial) { + match &self.element { + CosmicMappedInternal::Stack(s) => KeyboardTarget::leave(s, seat, data, serial), + CosmicMappedInternal::Window(w) => KeyboardTarget::leave(w, seat, data, serial), + _ => {} + } + } + fn key( + &self, + seat: &Seat, + data: &mut State, + key: KeysymHandle<'_>, + state: KeyState, + serial: Serial, + time: u32, + ) { + match &self.element { + CosmicMappedInternal::Stack(s) => { + KeyboardTarget::key(s, seat, data, key, state, serial, time) + } + CosmicMappedInternal::Window(w) => { + KeyboardTarget::key(w, seat, data, key, state, serial, time) + } + _ => {} + } + } + fn modifiers( + &self, + seat: &Seat, + data: &mut State, + modifiers: ModifiersState, + serial: Serial, + ) { + match &self.element { + CosmicMappedInternal::Stack(s) => { + KeyboardTarget::modifiers(s, seat, data, modifiers, serial) + } + CosmicMappedInternal::Window(w) => { + KeyboardTarget::modifiers(w, seat, data, modifiers, serial) + } + _ => {} + } + } +} + +impl WaylandFocus for CosmicMapped { + fn wl_surface(&self) -> Option> { + match &self.element { + CosmicMappedInternal::Window(w) => { + w.surface().wl_surface().map(|s| Cow::Owned(s.into_owned())) + } + CosmicMappedInternal::Stack(s) => { + s.active().wl_surface().map(|s| Cow::Owned(s.into_owned())) + } + _ => None, + } + } + + fn same_client_as(&self, object_id: &ObjectId) -> bool { + match &self.element { + CosmicMappedInternal::Window(w) => w.surface().same_client_as(object_id), + CosmicMappedInternal::Stack(s) => s.active().same_client_as(object_id), + _ => false, + } + } +} + +impl From for CosmicMapped { + fn from(w: CosmicWindow) -> Self { + CosmicMapped { + element: CosmicMappedInternal::Window(w), + maximized_state: Arc::new(Mutex::new(None)), + tiling_node_id: Arc::new(Mutex::new(None)), + resize_state: Arc::new(Mutex::new(None)), + last_geometry: Arc::new(Mutex::new(None)), + moved_since_mapped: Arc::new(AtomicBool::new(false)), + floating_tiled: Arc::new(Mutex::new(None)), + previous_layer: Arc::new(Mutex::new(None)), + #[cfg(feature = "debug")] + debug: Arc::new(Mutex::new(None)), + } + } +} + +impl From for CosmicMapped { + fn from(s: CosmicStack) -> Self { + CosmicMapped { + element: CosmicMappedInternal::Stack(s), + maximized_state: Arc::new(Mutex::new(None)), + tiling_node_id: Arc::new(Mutex::new(None)), + resize_state: Arc::new(Mutex::new(None)), + last_geometry: Arc::new(Mutex::new(None)), + moved_since_mapped: Arc::new(AtomicBool::new(false)), + floating_tiled: Arc::new(Mutex::new(None)), + previous_layer: Arc::new(Mutex::new(None)), + #[cfg(feature = "debug")] + debug: Arc::new(Mutex::new(None)), + } + } +} + +pub enum CosmicMappedRenderElement +where + R: AsGlowRenderer, + R::TextureId: 'static, +{ + Stack(self::stack::CosmicStackRenderElement), + Window(self::window::CosmicWindowRenderElement), + TiledStack( + CropRenderElement< + RelocateRenderElement>>, + >, + ), + TiledWindow( + CropRenderElement< + RelocateRenderElement>>, + >, + ), + TiledOverlay( + CropRenderElement>>, + ), + MovingStack( + RelocateRenderElement>>, + ), + MovingWindow( + RelocateRenderElement>>, + ), + GrabbedStack(RescaleRenderElement>), + GrabbedWindow(RescaleRenderElement>), + FocusIndicator(PixelShaderElement), + Overlay(PixelShaderElement), + StackHoverIndicator(IcedRenderElement), + #[cfg(feature = "debug")] + Egui(TextureRenderElement), +} + +impl Element for CosmicMappedRenderElement +where + R: AsGlowRenderer, + R::TextureId: Send + 'static, +{ + fn id(&self) -> &smithay::backend::renderer::element::Id { + match self { + CosmicMappedRenderElement::Stack(elem) => elem.id(), + CosmicMappedRenderElement::Window(elem) => elem.id(), + CosmicMappedRenderElement::TiledStack(elem) => elem.id(), + CosmicMappedRenderElement::TiledWindow(elem) => elem.id(), + CosmicMappedRenderElement::TiledOverlay(elem) => elem.id(), + CosmicMappedRenderElement::MovingStack(elem) => elem.id(), + CosmicMappedRenderElement::MovingWindow(elem) => elem.id(), + CosmicMappedRenderElement::GrabbedStack(elem) => elem.id(), + CosmicMappedRenderElement::GrabbedWindow(elem) => elem.id(), + CosmicMappedRenderElement::FocusIndicator(elem) => elem.id(), + CosmicMappedRenderElement::Overlay(elem) => elem.id(), + CosmicMappedRenderElement::StackHoverIndicator(elem) => elem.id(), + #[cfg(feature = "debug")] + CosmicMappedRenderElement::Egui(elem) => elem.id(), + } + } + + fn current_commit(&self) -> smithay::backend::renderer::utils::CommitCounter { + match self { + CosmicMappedRenderElement::Stack(elem) => elem.current_commit(), + CosmicMappedRenderElement::Window(elem) => elem.current_commit(), + CosmicMappedRenderElement::TiledStack(elem) => elem.current_commit(), + CosmicMappedRenderElement::TiledWindow(elem) => elem.current_commit(), + CosmicMappedRenderElement::TiledOverlay(elem) => elem.current_commit(), + CosmicMappedRenderElement::MovingStack(elem) => elem.current_commit(), + CosmicMappedRenderElement::MovingWindow(elem) => elem.current_commit(), + CosmicMappedRenderElement::GrabbedStack(elem) => elem.current_commit(), + CosmicMappedRenderElement::GrabbedWindow(elem) => elem.current_commit(), + CosmicMappedRenderElement::FocusIndicator(elem) => elem.current_commit(), + CosmicMappedRenderElement::Overlay(elem) => elem.current_commit(), + CosmicMappedRenderElement::StackHoverIndicator(elem) => elem.current_commit(), + #[cfg(feature = "debug")] + CosmicMappedRenderElement::Egui(elem) => elem.current_commit(), + } + } + + fn src(&self) -> Rectangle { + match self { + CosmicMappedRenderElement::Stack(elem) => elem.src(), + CosmicMappedRenderElement::Window(elem) => elem.src(), + CosmicMappedRenderElement::TiledStack(elem) => elem.src(), + CosmicMappedRenderElement::TiledWindow(elem) => elem.src(), + CosmicMappedRenderElement::TiledOverlay(elem) => elem.src(), + CosmicMappedRenderElement::MovingStack(elem) => elem.src(), + CosmicMappedRenderElement::MovingWindow(elem) => elem.src(), + CosmicMappedRenderElement::GrabbedStack(elem) => elem.src(), + CosmicMappedRenderElement::GrabbedWindow(elem) => elem.src(), + CosmicMappedRenderElement::FocusIndicator(elem) => elem.src(), + CosmicMappedRenderElement::Overlay(elem) => elem.src(), + CosmicMappedRenderElement::StackHoverIndicator(elem) => elem.src(), + #[cfg(feature = "debug")] + CosmicMappedRenderElement::Egui(elem) => elem.src(), + } + } + + fn geometry(&self, scale: Scale) -> Rectangle { + match self { + CosmicMappedRenderElement::Stack(elem) => elem.geometry(scale), + CosmicMappedRenderElement::Window(elem) => elem.geometry(scale), + CosmicMappedRenderElement::TiledStack(elem) => elem.geometry(scale), + CosmicMappedRenderElement::TiledWindow(elem) => elem.geometry(scale), + CosmicMappedRenderElement::TiledOverlay(elem) => elem.geometry(scale), + CosmicMappedRenderElement::MovingStack(elem) => elem.geometry(scale), + CosmicMappedRenderElement::MovingWindow(elem) => elem.geometry(scale), + CosmicMappedRenderElement::GrabbedStack(elem) => elem.geometry(scale), + CosmicMappedRenderElement::GrabbedWindow(elem) => elem.geometry(scale), + CosmicMappedRenderElement::FocusIndicator(elem) => elem.geometry(scale), + CosmicMappedRenderElement::Overlay(elem) => elem.geometry(scale), + CosmicMappedRenderElement::StackHoverIndicator(elem) => elem.geometry(scale), + #[cfg(feature = "debug")] + CosmicMappedRenderElement::Egui(elem) => elem.geometry(scale), + } + } + + fn location(&self, scale: Scale) -> Point { + match self { + CosmicMappedRenderElement::Stack(elem) => elem.location(scale), + CosmicMappedRenderElement::Window(elem) => elem.location(scale), + CosmicMappedRenderElement::TiledStack(elem) => elem.location(scale), + CosmicMappedRenderElement::TiledWindow(elem) => elem.location(scale), + CosmicMappedRenderElement::TiledOverlay(elem) => elem.location(scale), + CosmicMappedRenderElement::MovingStack(elem) => elem.location(scale), + CosmicMappedRenderElement::MovingWindow(elem) => elem.location(scale), + CosmicMappedRenderElement::GrabbedStack(elem) => elem.location(scale), + CosmicMappedRenderElement::GrabbedWindow(elem) => elem.location(scale), + CosmicMappedRenderElement::FocusIndicator(elem) => elem.location(scale), + CosmicMappedRenderElement::Overlay(elem) => elem.location(scale), + CosmicMappedRenderElement::StackHoverIndicator(elem) => elem.location(scale), + #[cfg(feature = "debug")] + CosmicMappedRenderElement::Egui(elem) => elem.location(scale), + } + } + + fn transform(&self) -> smithay::utils::Transform { + match self { + CosmicMappedRenderElement::Stack(elem) => elem.transform(), + CosmicMappedRenderElement::Window(elem) => elem.transform(), + CosmicMappedRenderElement::TiledStack(elem) => elem.transform(), + CosmicMappedRenderElement::TiledWindow(elem) => elem.transform(), + CosmicMappedRenderElement::TiledOverlay(elem) => elem.transform(), + CosmicMappedRenderElement::MovingStack(elem) => elem.transform(), + CosmicMappedRenderElement::MovingWindow(elem) => elem.transform(), + CosmicMappedRenderElement::GrabbedStack(elem) => elem.transform(), + CosmicMappedRenderElement::GrabbedWindow(elem) => elem.transform(), + CosmicMappedRenderElement::FocusIndicator(elem) => elem.transform(), + CosmicMappedRenderElement::Overlay(elem) => elem.transform(), + CosmicMappedRenderElement::StackHoverIndicator(elem) => elem.transform(), + #[cfg(feature = "debug")] + CosmicMappedRenderElement::Egui(elem) => elem.transform(), + } + } + + fn damage_since( + &self, + scale: Scale, + commit: Option, + ) -> DamageSet { + match self { + CosmicMappedRenderElement::Stack(elem) => elem.damage_since(scale, commit), + CosmicMappedRenderElement::Window(elem) => elem.damage_since(scale, commit), + CosmicMappedRenderElement::TiledStack(elem) => elem.damage_since(scale, commit), + CosmicMappedRenderElement::TiledWindow(elem) => elem.damage_since(scale, commit), + CosmicMappedRenderElement::TiledOverlay(elem) => elem.damage_since(scale, commit), + CosmicMappedRenderElement::MovingStack(elem) => elem.damage_since(scale, commit), + CosmicMappedRenderElement::MovingWindow(elem) => elem.damage_since(scale, commit), + CosmicMappedRenderElement::GrabbedStack(elem) => elem.damage_since(scale, commit), + CosmicMappedRenderElement::GrabbedWindow(elem) => elem.damage_since(scale, commit), + CosmicMappedRenderElement::FocusIndicator(elem) => elem.damage_since(scale, commit), + CosmicMappedRenderElement::Overlay(elem) => elem.damage_since(scale, commit), + CosmicMappedRenderElement::StackHoverIndicator(elem) => { + elem.damage_since(scale, commit) + } + #[cfg(feature = "debug")] + CosmicMappedRenderElement::Egui(elem) => elem.damage_since(scale, commit), + } + } + + fn opaque_regions(&self, scale: Scale) -> OpaqueRegions { + match self { + CosmicMappedRenderElement::Stack(elem) => elem.opaque_regions(scale), + CosmicMappedRenderElement::Window(elem) => elem.opaque_regions(scale), + CosmicMappedRenderElement::TiledStack(elem) => elem.opaque_regions(scale), + CosmicMappedRenderElement::TiledWindow(elem) => elem.opaque_regions(scale), + CosmicMappedRenderElement::TiledOverlay(elem) => elem.opaque_regions(scale), + CosmicMappedRenderElement::MovingStack(elem) => elem.opaque_regions(scale), + CosmicMappedRenderElement::MovingWindow(elem) => elem.opaque_regions(scale), + CosmicMappedRenderElement::GrabbedStack(elem) => elem.opaque_regions(scale), + CosmicMappedRenderElement::GrabbedWindow(elem) => elem.opaque_regions(scale), + CosmicMappedRenderElement::FocusIndicator(elem) => elem.opaque_regions(scale), + CosmicMappedRenderElement::Overlay(elem) => elem.opaque_regions(scale), + CosmicMappedRenderElement::StackHoverIndicator(elem) => elem.opaque_regions(scale), + #[cfg(feature = "debug")] + CosmicMappedRenderElement::Egui(elem) => elem.opaque_regions(scale), + } + } + + fn alpha(&self) -> f32 { + match self { + CosmicMappedRenderElement::Stack(elem) => elem.alpha(), + CosmicMappedRenderElement::Window(elem) => elem.alpha(), + CosmicMappedRenderElement::TiledStack(elem) => elem.alpha(), + CosmicMappedRenderElement::TiledWindow(elem) => elem.alpha(), + CosmicMappedRenderElement::TiledOverlay(elem) => elem.alpha(), + CosmicMappedRenderElement::MovingStack(elem) => elem.alpha(), + CosmicMappedRenderElement::MovingWindow(elem) => elem.alpha(), + CosmicMappedRenderElement::GrabbedStack(elem) => elem.alpha(), + CosmicMappedRenderElement::GrabbedWindow(elem) => elem.alpha(), + CosmicMappedRenderElement::FocusIndicator(elem) => elem.alpha(), + CosmicMappedRenderElement::Overlay(elem) => elem.alpha(), + CosmicMappedRenderElement::StackHoverIndicator(elem) => elem.alpha(), + #[cfg(feature = "debug")] + CosmicMappedRenderElement::Egui(elem) => elem.alpha(), + } + } + + fn kind(&self) -> Kind { + match self { + CosmicMappedRenderElement::Stack(elem) => elem.kind(), + CosmicMappedRenderElement::Window(elem) => elem.kind(), + CosmicMappedRenderElement::TiledStack(elem) => elem.kind(), + CosmicMappedRenderElement::TiledWindow(elem) => elem.kind(), + CosmicMappedRenderElement::TiledOverlay(elem) => elem.kind(), + CosmicMappedRenderElement::MovingStack(elem) => elem.kind(), + CosmicMappedRenderElement::MovingWindow(elem) => elem.kind(), + CosmicMappedRenderElement::GrabbedStack(elem) => elem.kind(), + CosmicMappedRenderElement::GrabbedWindow(elem) => elem.kind(), + CosmicMappedRenderElement::FocusIndicator(elem) => elem.kind(), + CosmicMappedRenderElement::Overlay(elem) => elem.kind(), + CosmicMappedRenderElement::StackHoverIndicator(elem) => elem.kind(), + #[cfg(feature = "debug")] + CosmicMappedRenderElement::Egui(elem) => elem.kind(), + } + } + + fn is_framebuffer_effect(&self) -> bool { + match self { + CosmicMappedRenderElement::Stack(elem) => elem.is_framebuffer_effect(), + CosmicMappedRenderElement::Window(elem) => elem.is_framebuffer_effect(), + CosmicMappedRenderElement::TiledStack(elem) => elem.is_framebuffer_effect(), + CosmicMappedRenderElement::TiledWindow(elem) => elem.is_framebuffer_effect(), + CosmicMappedRenderElement::TiledOverlay(elem) => elem.is_framebuffer_effect(), + CosmicMappedRenderElement::MovingStack(elem) => elem.is_framebuffer_effect(), + CosmicMappedRenderElement::MovingWindow(elem) => elem.is_framebuffer_effect(), + CosmicMappedRenderElement::GrabbedStack(elem) => elem.is_framebuffer_effect(), + CosmicMappedRenderElement::GrabbedWindow(elem) => elem.is_framebuffer_effect(), + CosmicMappedRenderElement::FocusIndicator(elem) => elem.is_framebuffer_effect(), + CosmicMappedRenderElement::Overlay(elem) => elem.is_framebuffer_effect(), + CosmicMappedRenderElement::StackHoverIndicator(elem) => elem.is_framebuffer_effect(), + #[cfg(feature = "debug")] + CosmicMappedRenderElement::Egui(elem) => elem.is_framebuffer_effect(), + } + } +} + +impl RenderElement for CosmicMappedRenderElement +where + R: AsGlowRenderer, + R::TextureId: Send + 'static, +{ + fn draw( + &self, + frame: &mut R::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + cache: Option<&UserDataMap>, + ) -> Result<(), R::Error> { + match self { + CosmicMappedRenderElement::Stack(elem) => { + elem.draw(frame, src, dst, damage, opaque_regions, cache) + } + CosmicMappedRenderElement::Window(elem) => { + elem.draw(frame, src, dst, damage, opaque_regions, cache) + } + CosmicMappedRenderElement::TiledStack(elem) => { + elem.draw(frame, src, dst, damage, opaque_regions, cache) + } + CosmicMappedRenderElement::TiledWindow(elem) => { + elem.draw(frame, src, dst, damage, opaque_regions, cache) + } + CosmicMappedRenderElement::TiledOverlay(elem) => RenderElement::::draw( + elem, + R::glow_frame_mut(frame), + src, + dst, + damage, + opaque_regions, + cache, + ) + .map_err(R::from_gles_error), + CosmicMappedRenderElement::MovingStack(elem) => { + elem.draw(frame, src, dst, damage, opaque_regions, cache) + } + CosmicMappedRenderElement::MovingWindow(elem) => { + elem.draw(frame, src, dst, damage, opaque_regions, cache) + } + CosmicMappedRenderElement::GrabbedStack(elem) => { + elem.draw(frame, src, dst, damage, opaque_regions, cache) + } + CosmicMappedRenderElement::GrabbedWindow(elem) => { + elem.draw(frame, src, dst, damage, opaque_regions, cache) + } + CosmicMappedRenderElement::FocusIndicator(elem) => RenderElement::::draw( + elem, + R::glow_frame_mut(frame), + src, + dst, + damage, + opaque_regions, + cache, + ) + .map_err(R::from_gles_error), + CosmicMappedRenderElement::Overlay(elem) => RenderElement::::draw( + elem, + R::glow_frame_mut(frame), + src, + dst, + damage, + opaque_regions, + cache, + ) + .map_err(R::from_gles_error), + CosmicMappedRenderElement::StackHoverIndicator(elem) => { + elem.draw(frame, src, dst, damage, opaque_regions, cache) + } + #[cfg(feature = "debug")] + CosmicMappedRenderElement::Egui(elem) => { + let glow_frame = R::glow_frame_mut(frame); + RenderElement::::draw( + elem, + glow_frame, + src, + dst, + damage, + opaque_regions, + cache, + ) + .map_err(R::from_gles_error) + } + } + } + + fn underlying_storage(&self, renderer: &mut R) -> Option> { + match self { + CosmicMappedRenderElement::Stack(elem) => elem.underlying_storage(renderer), + CosmicMappedRenderElement::Window(elem) => elem.underlying_storage(renderer), + CosmicMappedRenderElement::TiledStack(elem) => elem.underlying_storage(renderer), + CosmicMappedRenderElement::TiledWindow(elem) => elem.underlying_storage(renderer), + CosmicMappedRenderElement::TiledOverlay(elem) => { + elem.underlying_storage(renderer.glow_renderer_mut()) + } + CosmicMappedRenderElement::MovingStack(elem) => elem.underlying_storage(renderer), + CosmicMappedRenderElement::MovingWindow(elem) => elem.underlying_storage(renderer), + CosmicMappedRenderElement::GrabbedStack(elem) => elem.underlying_storage(renderer), + CosmicMappedRenderElement::GrabbedWindow(elem) => elem.underlying_storage(renderer), + CosmicMappedRenderElement::FocusIndicator(elem) => { + elem.underlying_storage(renderer.glow_renderer_mut()) + } + CosmicMappedRenderElement::Overlay(elem) => { + elem.underlying_storage(renderer.glow_renderer_mut()) + } + CosmicMappedRenderElement::StackHoverIndicator(elem) => { + elem.underlying_storage(renderer) + } + #[cfg(feature = "debug")] + CosmicMappedRenderElement::Egui(elem) => { + let glow_renderer = renderer.glow_renderer_mut(); + elem.underlying_storage(glow_renderer) + } + } + } + + fn capture_framebuffer( + &self, + frame: &mut R::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + cache: &UserDataMap, + ) -> Result<(), R::Error> { + match self { + CosmicMappedRenderElement::Stack(elem) => { + elem.capture_framebuffer(frame, src, dst, cache) + } + CosmicMappedRenderElement::Window(elem) => { + elem.capture_framebuffer(frame, src, dst, cache) + } + CosmicMappedRenderElement::TiledStack(elem) => { + elem.capture_framebuffer(frame, src, dst, cache) + } + CosmicMappedRenderElement::TiledWindow(elem) => { + elem.capture_framebuffer(frame, src, dst, cache) + } + CosmicMappedRenderElement::TiledOverlay(elem) => { + RenderElement::::capture_framebuffer( + elem, + R::glow_frame_mut(frame), + src, + dst, + cache, + ) + .map_err(R::from_gles_error) + } + CosmicMappedRenderElement::MovingStack(elem) => { + elem.capture_framebuffer(frame, src, dst, cache) + } + CosmicMappedRenderElement::MovingWindow(elem) => { + elem.capture_framebuffer(frame, src, dst, cache) + } + CosmicMappedRenderElement::GrabbedStack(elem) => { + elem.capture_framebuffer(frame, src, dst, cache) + } + CosmicMappedRenderElement::GrabbedWindow(elem) => { + elem.capture_framebuffer(frame, src, dst, cache) + } + CosmicMappedRenderElement::FocusIndicator(elem) => { + RenderElement::::capture_framebuffer( + elem, + R::glow_frame_mut(frame), + src, + dst, + cache, + ) + .map_err(R::from_gles_error) + } + CosmicMappedRenderElement::Overlay(elem) => { + RenderElement::::capture_framebuffer( + elem, + R::glow_frame_mut(frame), + src, + dst, + cache, + ) + .map_err(R::from_gles_error) + } + CosmicMappedRenderElement::StackHoverIndicator(elem) => { + elem.capture_framebuffer(frame, src, dst, cache) + } + #[cfg(feature = "debug")] + CosmicMappedRenderElement::Egui(elem) => { + let glow_frame = R::glow_frame_mut(frame); + RenderElement::::capture_framebuffer( + elem, glow_frame, src, dst, cache, + ) + .map_err(R::from_gles_error) + } + } + } +} + +impl From> for CosmicMappedRenderElement +where + R: AsGlowRenderer, + R::TextureId: 'static, + CosmicMappedRenderElement: RenderElement, +{ + fn from(elem: stack::CosmicStackRenderElement) -> Self { + CosmicMappedRenderElement::Stack(elem) + } +} +impl From> for CosmicMappedRenderElement +where + R: AsGlowRenderer, + R::TextureId: 'static, + CosmicMappedRenderElement: RenderElement, +{ + fn from(elem: window::CosmicWindowRenderElement) -> Self { + CosmicMappedRenderElement::Window(elem) + } +} + +impl From for CosmicMappedRenderElement +where + R: AsGlowRenderer, + R::TextureId: 'static, + CosmicMappedRenderElement: RenderElement, +{ + fn from(elem: PixelShaderElement) -> Self { + CosmicMappedRenderElement::FocusIndicator(elem) + } +} + +impl From> for CosmicMappedRenderElement +where + R: AsGlowRenderer, + R::TextureId: 'static, + CosmicMappedRenderElement: RenderElement, +{ + fn from(elem: IcedRenderElement) -> Self { + CosmicMappedRenderElement::StackHoverIndicator(elem) + } +} + +#[cfg(feature = "debug")] +impl From> for CosmicMappedRenderElement +where + R: AsGlowRenderer, + R::TextureId: 'static, + CosmicMappedRenderElement: RenderElement, +{ + fn from(elem: TextureRenderElement) -> Self { + CosmicMappedRenderElement::Egui(elem) + } +} diff --git a/src/shell/element/resize_indicator.rs b/src/shell/element/resize_indicator.rs new file mode 100644 index 0000000..fb5441c --- /dev/null +++ b/src/shell/element/resize_indicator.rs @@ -0,0 +1,342 @@ +use std::sync::{Arc, Mutex}; + +use crate::{ + backend::render::element::AsGlowRenderer, + config::Config, + fl, + shell::grabs::ResizeEdge, + utils::iced::{IcedElement, IcedRenderElement, Program}, +}; + +use calloop::LoopHandle; +use cosmic::{ + Apply, + iced::{ + Alignment, + core::{Background, Border, Color, Length}, + widget::{container, row, space}, + }, + theme, + widget::{icon::from_name, text}, +}; +use cosmic_settings_config::shortcuts::action::{Action, ResizeDirection}; +use smithay::{ + backend::renderer::ImportMem, + desktop::space::SpaceElement, + output::Output, + utils::{Logical, Physical, Point, Rectangle, Scale, Size}, +}; + +#[derive(Debug, Clone)] +pub struct ResizeIndicator { + edges: ResizeEdge, + direction: Arc>, + size: Size, + + center_elem: IcedElement, + left_elem: IcedElement, + top_elem: IcedElement, + right_elem: IcedElement, + down_elem: IcedElement, +} + +const ARROW_SIZE: i32 = 36; + +impl ResizeIndicator { + pub fn new( + direction: ResizeDirection, + config: &Config, + evlh: LoopHandle<'static, crate::state::State>, + mut theme: cosmic::Theme, + ) -> ResizeIndicator { + theme.transparent = theme.cosmic().frosted_system_interface; + let direction = Arc::new(Mutex::new(direction)); + + ResizeIndicator { + edges: ResizeEdge::all(), + direction: direction.clone(), + size: Size::default(), + center_elem: IcedElement::new( + ResizeIndicatorInternal { + shortcut1: config + .shortcuts + .iter() + .find_map(|(pattern, action)| { + (*action == Action::Resizing(ResizeDirection::Outwards)) + .then_some(pattern) + }) + .map(|pattern| format!("{}: ", pattern.to_string())) + .unwrap_or_else(|| crate::fl!("unknown-keybinding")), + shortcut2: config + .shortcuts + .iter() + .find_map(|(pattern, action)| { + (*action == Action::Resizing(ResizeDirection::Inwards)) + .then_some(pattern) + }) + .map(|pattern| format!("{}: ", pattern.to_string())) + .unwrap_or_else(|| crate::fl!("unknown-keybinding")), + }, + Size::from((1, 1)), + evlh.clone(), + theme.clone(), + ), + left_elem: IcedElement::new( + ResizeIndicatorArrow { + direction: direction.clone(), + icon_outwards: "go-previous-symbolic", + icon_inwards: "go-next-symbolic", + }, + Size::from((ARROW_SIZE, ARROW_SIZE)), + evlh.clone(), + theme.clone(), + ), + top_elem: IcedElement::new( + ResizeIndicatorArrow { + direction: direction.clone(), + icon_outwards: "go-up-symbolic", + icon_inwards: "go-down-symbolic", + }, + Size::from((ARROW_SIZE, ARROW_SIZE)), + evlh.clone(), + theme.clone(), + ), + right_elem: IcedElement::new( + ResizeIndicatorArrow { + direction: direction.clone(), + icon_outwards: "go-next-symbolic", + icon_inwards: "go-previous-symbolic", + }, + Size::from((ARROW_SIZE, ARROW_SIZE)), + evlh.clone(), + theme.clone(), + ), + down_elem: IcedElement::new( + ResizeIndicatorArrow { + direction, + icon_outwards: "go-down-symbolic", + icon_inwards: "go-up-symbolic", + }, + Size::from((ARROW_SIZE, ARROW_SIZE)), + evlh.clone(), + theme.clone(), + ), + } + } + + pub fn resize(&mut self, size: Size) { + let minimum = self.center_elem.minimum_size(); + let new_size = Size::::new(size.w.min(minimum.w), size.h.min(minimum.h)); + self.center_elem.resize(new_size); + self.size = size; + } + + pub fn push_render_elements( + &self, + renderer: &mut R, + location: Point, + scale: Scale, + alpha: f32, + push_above: &mut dyn FnMut(IcedRenderElement), + mut push_below: Option<&mut dyn FnMut(IcedRenderElement)>, + ) where + R: AsGlowRenderer + ImportMem, + R::TextureId: Send + Clone + 'static, + { + let elem_size = self.center_elem.current_size(); + let center_location = Point::new( + self.size.w.saturating_sub(elem_size.w) / 2, + self.size.h.saturating_sub(elem_size.h) / 2, + ); + let left_location = Point::new(0, self.size.h.saturating_sub(ARROW_SIZE) / 2); + let top_location = Point::new(self.size.w.saturating_sub(ARROW_SIZE) / 2, 0); + let right_location = Point::new( + self.size.w.saturating_sub(ARROW_SIZE), + self.size.h.saturating_sub(ARROW_SIZE) / 2, + ); + let down_location = Point::new( + self.size.w.saturating_sub(ARROW_SIZE) / 2, + self.size.h.saturating_sub(ARROW_SIZE), + ); + let radii = self + .center_elem + .with_theme(|theme| theme.cosmic().radius_s()) + .map(|x| x.round() as u8); + + self.center_elem.push_render_elements( + renderer, + location + center_location.to_physical_precise_round(scale), + scale, + alpha, + radii, + push_above, + if let Some(push_below) = push_below.as_mut() { + Some(&mut **push_below) + } else { + None + }, + ); + let mut render = move |elem: &IcedElement, + loc: Point| { + elem.push_render_elements( + renderer, + location + loc.to_physical_precise_round(scale), + scale, + alpha, + radii, + push_above, + if let Some(push_below) = push_below.as_mut() { + Some(&mut **push_below) + } else { + None + }, + ); + }; + + if self.edges.contains(ResizeEdge::LEFT) { + render(&self.left_elem, left_location); + } + if self.edges.contains(ResizeEdge::TOP) { + render(&self.top_elem, top_location); + } + if self.edges.contains(ResizeEdge::RIGHT) { + render(&self.right_elem, right_location); + } + if self.edges.contains(ResizeEdge::BOTTOM) { + render(&self.down_elem, down_location); + } + } + + pub fn set_edges(&mut self, edges: ResizeEdge) { + self.edges = edges; + } + + pub fn set_direction(&self, direction: ResizeDirection) { + let mut dir_ref = self.direction.lock().unwrap(); + if *dir_ref != direction { + *dir_ref = direction; + self.left_elem.force_redraw(); + self.top_elem.force_redraw(); + self.right_elem.force_redraw(); + self.down_elem.force_redraw(); + } + } + + pub fn output_enter(&self, output: &Output) { + self.center_elem + .output_enter(output, Rectangle::default() /*unused*/); + self.left_elem + .output_enter(output, Rectangle::default() /*unused*/); + self.top_elem + .output_enter(output, Rectangle::default() /*unused*/); + self.right_elem + .output_enter(output, Rectangle::default() /*unused*/); + self.down_elem + .output_enter(output, Rectangle::default() /*unused*/); + } + + pub fn output_leave(&self, output: &Output) { + self.center_elem.output_leave(output); + self.left_elem.output_leave(output); + self.top_elem.output_leave(output); + self.right_elem.output_leave(output); + self.down_elem.output_leave(output); + } +} + +pub struct ResizeIndicatorInternal { + shortcut1: String, + shortcut2: String, +} + +impl Program for ResizeIndicatorInternal { + type Message = (); + + fn view(&self) -> cosmic::Element<'_, Self::Message> { + row(vec![ + text::heading(&self.shortcut1).into(), + text::body(fl!("grow-window")).into(), + space::horizontal().width(40).into(), + text::heading(&self.shortcut2).into(), + text::body(fl!("shrink-window")).into(), + ]) + .apply(container) + .align_x(Alignment::Center) + .align_y(Alignment::Center) + .padding(16) + .apply(container) + .class(theme::Container::custom(|theme| { + let mut background = theme.cosmic().accent_color(); + if theme.transparent { + background.alpha = theme + .cosmic() + .alpha_map + .blurred_alpha(theme.cosmic().frosted); + } + + container::Style { + snap: true, + icon_color: Some(Color::from(theme.cosmic().accent.on)), + text_color: Some(Color::from(theme.cosmic().accent.on)), + background: Some(Background::Color(background.into())), + border: Border { + radius: theme.cosmic().radius_s().into(), + width: 0.0, + color: Color::TRANSPARENT, + }, + shadow: Default::default(), + } + })) + .width(Length::Shrink) + .height(Length::Shrink) + .into() + } +} + +pub struct ResizeIndicatorArrow { + direction: Arc>, + icon_outwards: &'static str, + icon_inwards: &'static str, +} + +impl Program for ResizeIndicatorArrow { + type Message = (); + + fn view(&self) -> cosmic::Element<'_, Self::Message> { + from_name( + if *self.direction.lock().unwrap() == ResizeDirection::Outwards { + self.icon_outwards + } else { + self.icon_inwards + }, + ) + .size(20) + .prefer_svg(true) + .apply(container) + .padding(8) + .class(theme::Container::custom(|theme| { + let mut background = theme.cosmic().accent_color(); + if theme.transparent { + background.alpha = theme + .cosmic() + .alpha_map + .blurred_alpha(theme.cosmic().frosted); + } + + container::Style { + snap: true, + icon_color: Some(Color::from(theme.cosmic().accent.on)), + text_color: Some(Color::from(theme.cosmic().accent.on)), + background: Some(Background::Color(background.into())), + border: Border { + radius: theme.cosmic().radius_s().into(), + width: 0.0, + color: Color::TRANSPARENT, + }, + shadow: Default::default(), + } + })) + .width(Length::Shrink) + .into() + } +} diff --git a/src/shell/element/stack.rs b/src/shell/element/stack.rs new file mode 100644 index 0000000..d466f4e --- /dev/null +++ b/src/shell/element/stack.rs @@ -0,0 +1,2108 @@ +use super::{ + CosmicSurface, + window::{Focus, RESIZE_BORDER}, +}; +use crate::{ + backend::render::{ + IndicatorShader, Key, Usage, cursor::CursorState, element::AsGlowRenderer, + shadow::ShadowShader, wayland::SurfaceRenderElement, + }, + hooks::{Decorations, HOOKS}, + shell::{ + element::{CosmicMappedKey, CosmicMappedKeyInner}, + focus::target::PointerFocusTarget, + grabs::{ReleaseMode, ResizeEdge}, + layout::tiling::NodeDesc, + }, + state::State, + utils::{ + iced::{IcedElement, IcedRenderElement, Program}, + prelude::*, + }, +}; +use calloop::LoopHandle; +use cosmic::{ + Apply, Element as CosmicElement, Theme, + iced::{ + Alignment, + core::{Background, Border, Color, Length, border::Radius}, + id::Id, + runtime::Task, + widget::{self as iced_widget, scrollable::AbsoluteOffset}, + }, + theme, widget as cosmic_widget, +}; +use cosmic_comp_config::AppearanceConfig; +use cosmic_settings_config::shortcuts; +use shortcuts::action::{Direction, FocusDirection}; +use smithay::{ + backend::{ + drm::DrmNode, + input::KeyState, + renderer::{ + ImportAll, ImportMem, Renderer, + element::{Element, Id as RendererId, Kind, RenderElement, UnderlyingStorage}, + gles::element::PixelShaderElement, + glow::GlowRenderer, + utils::{CommitCounter, DamageSet, OpaqueRegions}, + }, + }, + desktop::{WindowSurfaceType, space::SpaceElement}, + input::{ + Seat, + keyboard::{KeyboardTarget, KeysymHandle, ModifiersState}, + pointer::{ + AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent, + GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, + GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, MotionEvent, + PointerTarget, RelativeMotionEvent, + }, + touch::{ + DownEvent, FrameMarker, MotionEvent as TouchMotionEvent, OrientationEvent, ShapeEvent, + TouchTarget, UpEvent, + }, + }, + output::Output, + reexports::wayland_server::protocol::wl_surface::WlSurface, + utils::{ + Buffer, IsAlive, Logical, Physical, Point, Rectangle, Scale, Serial, Size, Transform, + user_data::UserDataMap, + }, + wayland::seat::WaylandFocus, +}; +use std::{ + borrow::Cow, + fmt, + hash::Hash, + sync::{ + Arc, LazyLock, Mutex, + atomic::{AtomicBool, AtomicU8, AtomicU32, AtomicUsize, Ordering}, + }, +}; + +mod tab; +mod tabs; + +use self::{ + tab::{Tab, TabMessage}, + tabs::Tabs, +}; + +static SCROLLABLE_ID: LazyLock = LazyLock::new(|| Id::new("scrollable")); + +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct CosmicStack(pub(super) IcedElement); + +impl fmt::Debug for CosmicStack { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("CosmicStack") + .field("internal", &self.0) + .finish_non_exhaustive() + } +} + +#[derive(Debug)] +pub struct CosmicStackInternal { + windows: Mutex>, + active: AtomicUsize, + activated: AtomicBool, + group_focused: AtomicBool, + previous_index: Mutex>, + scroll_to_focus: AtomicBool, + previous_keyboard: AtomicUsize, + pointer_entered: AtomicU8, + touch_serial: AtomicU32, + reenter: AtomicBool, + potential_drag: Mutex>, + override_alive: AtomicBool, + geometry: Mutex>>, + mask: Mutex>, + tiled: AtomicBool, + theme: Mutex, + appearance_conf: Mutex, +} + +impl CosmicStackInternal { + pub fn swap_focus(&self, focus: Option) -> Option { + let value = focus.map_or(0, |x| x as u8); + unsafe { Focus::from_u8(self.pointer_entered.swap(value, Ordering::SeqCst)) } + } + + pub fn current_focus(&self) -> Option { + unsafe { Focus::from_u8(self.pointer_entered.load(Ordering::SeqCst)) } + } +} + +pub const TAB_HEIGHT: i32 = 24; + +#[derive(Debug, Clone)] +pub enum MoveResult { + Handled, + MoveOut(CosmicSurface, LoopHandle<'static, crate::state::State>), + Default, +} + +impl CosmicStack { + pub fn new>( + windows: impl Iterator, + handle: LoopHandle<'static, crate::state::State>, + mut theme: cosmic::Theme, + appearance: AppearanceConfig, + ) -> CosmicStack { + theme.transparent = theme.cosmic().frosted_windows; + let windows = windows.map(Into::into).collect::>(); + assert!(!windows.is_empty()); + + for window in &windows { + window.try_force_undecorated(true); + window.set_tiled(true); + window.send_configure(); + } + + let width = windows[0].geometry().size.w; + CosmicStack(IcedElement::new( + CosmicStackInternal { + windows: Mutex::new(windows), + active: AtomicUsize::new(0), + activated: AtomicBool::new(false), + group_focused: AtomicBool::new(false), + previous_index: Mutex::new(None), + scroll_to_focus: AtomicBool::new(false), + previous_keyboard: AtomicUsize::new(0), + pointer_entered: AtomicU8::new(0), + touch_serial: AtomicU32::new(0), + reenter: AtomicBool::new(false), + potential_drag: Mutex::new(None), + override_alive: AtomicBool::new(true), + geometry: Mutex::new(None), + mask: Mutex::new(None), + tiled: AtomicBool::new(false), + theme: Mutex::new(theme.clone()), + appearance_conf: Mutex::new(appearance), + }, + (width, TAB_HEIGHT), + handle, + theme, + )) + } + + pub fn add_window( + &self, + window: impl Into, + idx: Option, + moved_into: Option<&Seat>, + ) { + let window = window.into(); + window.try_force_undecorated(true); + window.set_tiled(true); + self.0.with_program(|p| { + let last_mod_serial = moved_into.and_then(|seat| seat.last_modifier_change()); + let mut prev_idx = p.previous_index.lock().unwrap(); + if !prev_idx.is_some_and(|(serial, _)| Some(serial) == last_mod_serial) { + *prev_idx = last_mod_serial.map(|s| (s, p.active.load(Ordering::SeqCst))); + } + + if let Some(mut geo) = *p.geometry.lock().unwrap() { + geo.loc.y += TAB_HEIGHT; + geo.size.h -= TAB_HEIGHT; + window.set_geometry(geo, TAB_HEIGHT as u32); + } + window.send_configure(); + if let Some(idx) = idx { + p.windows.lock().unwrap().insert(idx, window); + let old_idx = p.active.swap(idx, Ordering::SeqCst); + if old_idx == idx { + p.reenter.store(true, Ordering::SeqCst); + p.previous_keyboard.store(old_idx, Ordering::SeqCst); + } + } else { + let mut windows = p.windows.lock().unwrap(); + windows.push(window); + p.active.store(windows.len() - 1, Ordering::SeqCst); + } + p.scroll_to_focus.store(true, Ordering::SeqCst); + }); + self.0 + .resize(Size::from((self.active().geometry().size.w, TAB_HEIGHT))); + self.0.force_redraw() + } + + pub fn remove_window(&self, window: &CosmicSurface) { + self.0.with_program(|p| { + let mut windows = p.windows.lock().unwrap(); + if windows.len() == 1 { + p.override_alive.store(false, Ordering::SeqCst); + let window = windows.first().unwrap(); + window.try_force_undecorated(false); + window.set_tiled(false); + return; + } + + let Some(idx) = windows.iter().position(|w| w == window) else { + return; + }; + if idx == p.active.load(Ordering::SeqCst) { + p.reenter.store(true, Ordering::SeqCst); + } + let window = windows.remove(idx); + window.try_force_undecorated(false); + window.set_tiled(false); + + p.active.fetch_min(windows.len() - 1, Ordering::SeqCst); + p.previous_index + .lock() + .unwrap() + .take_if(|(_, idx)| *idx >= windows.len()); + }); + self.0 + .resize(Size::from((self.active().geometry().size.w, TAB_HEIGHT))); + self.0.force_redraw() + } + + pub fn remove_idx(&self, idx: usize) -> Option { + let window = self.0.with_program(|p| { + let mut windows = p.windows.lock().unwrap(); + if windows.len() == 1 { + p.override_alive.store(false, Ordering::SeqCst); + let window = windows.first().unwrap(); + window.try_force_undecorated(false); + window.set_tiled(false); + return Some(window.clone()); + } + if windows.len() <= idx { + return None; + } + if idx == p.active.load(Ordering::SeqCst) { + p.reenter.store(true, Ordering::SeqCst); + } + let window = windows.remove(idx); + window.try_force_undecorated(false); + window.set_tiled(false); + + p.active.fetch_min(windows.len() - 1, Ordering::SeqCst); + p.previous_index + .lock() + .unwrap() + .take_if(|(_, idx)| *idx >= windows.len()); + + Some(window) + }); + self.0 + .resize(Size::from((self.active().geometry().size.w, TAB_HEIGHT))); + self.0.force_redraw(); + window + } + + pub fn len(&self) -> usize { + self.0.with_program(|p| p.windows.lock().unwrap().len()) + } + + pub fn handle_focus( + &self, + seat: &Seat, + direction: FocusDirection, + swap: Option, + ) -> bool { + let (result, update) = self.0.with_program(|p| { + let last_mod_serial = seat.last_modifier_change(); + let mut prev_idx = p.previous_index.lock().unwrap(); + if !prev_idx.is_some_and(|(serial, _)| Some(serial) == last_mod_serial) { + *prev_idx = last_mod_serial.map(|s| (s, p.active.load(Ordering::SeqCst))); + } + + match direction { + FocusDirection::Left => { + if !p.group_focused.load(Ordering::SeqCst) { + if let Ok(old) = + p.active + .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |val| { + val.checked_sub(1) + }) + { + p.previous_keyboard.store(old, Ordering::SeqCst); + p.scroll_to_focus.store(true, Ordering::SeqCst); + (true, true) + } else { + let new = prev_idx.unwrap().1; + let old = p.active.swap(new, Ordering::SeqCst); + if old != new { + p.previous_keyboard.store(old, Ordering::SeqCst); + p.scroll_to_focus.store(true, Ordering::SeqCst); + (false, true) + } else { + (false, false) + } + } + } else { + (false, false) + } + } + FocusDirection::Right => { + if !p.group_focused.load(Ordering::SeqCst) { + let max = p.windows.lock().unwrap().len(); + if let Ok(old) = + p.active + .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |val| { + if val < max - 1 { Some(val + 1) } else { None } + }) + { + p.previous_keyboard.store(old, Ordering::SeqCst); + p.scroll_to_focus.store(true, Ordering::SeqCst); + (true, true) + } else { + let new = prev_idx.unwrap().1; + let old = p.active.swap(new, Ordering::SeqCst); + if old != new { + p.previous_keyboard.store(old, Ordering::SeqCst); + p.scroll_to_focus.store(true, Ordering::SeqCst); + (false, true) + } else { + (false, false) + } + } + } else { + (false, false) + } + } + FocusDirection::Out if swap.is_none() => { + if !p.group_focused.swap(true, Ordering::SeqCst) { + p.windows.lock().unwrap().iter().for_each(|w| { + w.set_activated(false); + w.send_configure(); + }); + (true, true) + } else { + (false, false) + } + } + FocusDirection::In if swap.is_none() => { + if !p.group_focused.swap(false, Ordering::SeqCst) { + p.windows + .lock() + .unwrap() + .iter() + .enumerate() + .for_each(|(i, w)| { + w.set_activated(p.active.load(Ordering::SeqCst) == i); + w.send_configure(); + }); + + (true, true) + } else { + (false, false) + } + } + FocusDirection::Up | FocusDirection::Down => { + if !p.group_focused.load(Ordering::SeqCst) { + let new = prev_idx.unwrap().1; + let old = p.active.swap(new, Ordering::SeqCst); + if old != new { + p.previous_keyboard.store(old, Ordering::SeqCst); + p.scroll_to_focus.store(true, Ordering::SeqCst); + } + (false, true) + } else { + (false, false) + } + } + _ => (false, false), + } + }); + + if update { + self.0 + .resize(Size::from((self.active().geometry().size.w, TAB_HEIGHT))); + } + + result + } + + pub fn handle_move(&self, direction: Direction) -> MoveResult { + let loop_handle = self.0.loop_handle(); + let result = self.0.with_program(|p| { + let prev_idx = p.previous_index.lock().unwrap(); + + if p.group_focused.load(Ordering::SeqCst) { + return MoveResult::Default; + } + + let active = p.active.load(Ordering::SeqCst); + let mut windows = p.windows.lock().unwrap(); + + let next = match direction { + Direction::Left => active.checked_sub(1), + Direction::Right => (active + 1 < windows.len()).then_some(active + 1), + Direction::Down | Direction::Up => None, + }; + + if let Some(val) = next { + let old = p.active.swap(val, Ordering::SeqCst); + windows.swap(old, val); + p.previous_keyboard.store(old, Ordering::SeqCst); + p.scroll_to_focus.store(true, Ordering::SeqCst); + MoveResult::Handled + } else { + if windows.len() == 1 { + return MoveResult::Default; + } + let window = windows.remove(active); + if let Some(prev_idx) = prev_idx + .map(|(_, idx)| idx) + .filter(|idx| *idx < windows.len()) + { + p.active.store(prev_idx, Ordering::SeqCst); + p.scroll_to_focus.store(true, Ordering::SeqCst); + } else if active == windows.len() { + p.active.store(active - 1, Ordering::SeqCst); + p.scroll_to_focus.store(true, Ordering::SeqCst); + } + window.try_force_undecorated(false); + window.set_tiled(false); + + MoveResult::MoveOut(window, loop_handle) + } + }); + + if !matches!(result, MoveResult::Default) { + self.0 + .resize(Size::from((self.active().geometry().size.w, TAB_HEIGHT))); + } + + result + } + + pub fn active(&self) -> CosmicSurface { + self.0 + .with_program(|p| p.windows.lock().unwrap()[p.active.load(Ordering::SeqCst)].clone()) + } + + pub fn has_active(&self, window: &CosmicSurface) -> bool { + self.0 + .with_program(|p| &p.windows.lock().unwrap()[p.active.load(Ordering::SeqCst)] == window) + } + + pub fn whole_stack_focused(&self) -> bool { + self.0 + .with_program(|p| p.group_focused.load(Ordering::SeqCst)) + } + + pub fn set_active(&self, window: &S) + where + CosmicSurface: PartialEq, + { + self.0.with_program(|p| { + if let Some(val) = p.windows.lock().unwrap().iter().position(|w| w == window) { + let old = p.active.swap(val, Ordering::SeqCst); + if old != val { + p.previous_keyboard.store(old, Ordering::SeqCst); + } + } + }); + self.0 + .resize(Size::from((self.active().geometry().size.w, TAB_HEIGHT))); + self.0.force_redraw() + } + + pub fn set_tiled(&self, tiled: bool) { + self.0 + .with_program(|p| p.tiled.store(tiled, Ordering::Release)); + } + + pub fn surfaces(&self) -> impl Iterator { + self.0.with_program(|p| { + p.windows + .lock() + .unwrap() + .iter() + .cloned() + .collect::>() + .into_iter() + }) + } + + pub fn focus_under( + &self, + mut relative_pos: Point, + surface_type: WindowSurfaceType, + ) -> Option<(PointerFocusTarget, Point)> { + self.0.with_program(|p| { + let mut stack_ui = None; + let geo = p.windows.lock().unwrap()[p.active.load(Ordering::SeqCst)].geometry(); + + if surface_type.contains(WindowSurfaceType::TOPLEVEL) { + let point_i32 = relative_pos.to_i32_floor::(); + if (point_i32.x - geo.loc.x >= -RESIZE_BORDER && point_i32.x - geo.loc.x < 0) + || (point_i32.y - geo.loc.y >= -RESIZE_BORDER && point_i32.y - geo.loc.y < 0) + || (point_i32.x - geo.loc.x >= geo.size.w + && point_i32.x - geo.loc.x < geo.size.w + RESIZE_BORDER) + || (point_i32.y - geo.loc.y >= geo.size.h + TAB_HEIGHT + && point_i32.y - geo.loc.y < geo.size.h + TAB_HEIGHT + RESIZE_BORDER) + { + stack_ui = Some(( + PointerFocusTarget::StackUI(self.clone()), + Point::from((0., 0.)), + )); + } + + if point_i32.y - geo.loc.y < TAB_HEIGHT { + stack_ui = Some(( + PointerFocusTarget::StackUI(self.clone()), + Point::from((0., 0.)), + )); + } + } + + relative_pos.y -= TAB_HEIGHT as f64; + + let active_window = &p.windows.lock().unwrap()[p.active.load(Ordering::SeqCst)]; + stack_ui.or_else(|| { + active_window.focus_under(relative_pos, surface_type).map( + |(target, surface_offset)| { + ( + target, + surface_offset.to_f64() + Point::from((0., TAB_HEIGHT as f64)), + ) + }, + ) + }) + }) + } + + pub fn offset(&self) -> Point { + Point::from((0, TAB_HEIGHT)) + } + + pub fn pending_size(&self) -> Option> { + self.0 + .with_program(|p| (*p.geometry.lock().unwrap()).map(|geo| geo.size.as_logical())) + } + + pub fn last_server_size(&self) -> Option> { + let mut size = self.active().last_server_size()?; + // if stacked the window doesn't have SSD, instead it has a tab + size.h += TAB_HEIGHT; + Some(size) + } + + pub fn set_geometry(&self, geo: Rectangle) { + self.0.with_program(|p| { + let loc = (geo.loc.x, geo.loc.y + TAB_HEIGHT); + let size = (geo.size.w, geo.size.h - TAB_HEIGHT); + + let win_geo = Rectangle::new(loc.into(), size.into()); + for window in p.windows.lock().unwrap().iter() { + window.set_geometry(win_geo, TAB_HEIGHT as u32); + } + + *p.geometry.lock().unwrap() = Some(geo); + p.mask.lock().unwrap().take(); + }); + } + + pub fn on_commit(&self, surface: &WlSurface) { + if let Some(surface) = self.surfaces().find(|w| w == surface) { + surface.0.on_commit(); + if self.active() == surface { + self.0 + .resize(Size::from((surface.geometry().size.w, TAB_HEIGHT))); + } + } + } + + fn keyboard_leave_if_previous( + &self, + seat: &Seat, + data: &mut State, + serial: Serial, + ) -> usize { + self.0.with_program(|p| { + let active = p.active.load(Ordering::SeqCst); + let previous = p.previous_keyboard.swap(active, Ordering::SeqCst); + if previous != active || p.reenter.swap(false, Ordering::SeqCst) { + let windows = p.windows.lock().unwrap(); + if let Some(previous_surface) = windows.get(previous) + && previous != active + { + KeyboardTarget::leave(previous_surface, seat, data, serial); + } + KeyboardTarget::enter( + &windows[active], + seat, + data, + Vec::new(), /* TODO */ + serial, + ) + } + active + }) + } + + pub(in super::super) fn focus_stack(&self) { + self.0 + .with_program(|p| p.group_focused.store(true, Ordering::SeqCst)); + } + + pub(in super::super) fn loop_handle(&self) -> LoopHandle<'static, crate::state::State> { + self.0.loop_handle() + } + + pub fn push_popup_render_elements( + &self, + renderer: &mut R, + location: Point, + scale: Scale, + alpha: f32, + scanout_node: Option, + push: &mut dyn FnMut(CosmicStackRenderElement), + ) where + R: AsGlowRenderer, + R::TextureId: Send + Clone + 'static, + { + let window_loc = location + Point::from((0, (TAB_HEIGHT as f64 * scale.y) as i32)); + self.0.with_program(|p| { + let windows = p.windows.lock().unwrap(); + let active = p.active.load(Ordering::SeqCst); + let theme = p.theme.lock().unwrap(); + let frosted = if theme.cosmic().frosted_windows { + (theme.cosmic().frosted as u8 + 1) as usize + } else { + 0 + }; + + windows[active].push_popup_render_elements( + renderer, + window_loc, + scale, + alpha, + scanout_node, + frosted, + &mut |elem| push(elem.into()), + ) + }) + } + + pub fn shadow_render_element( + &self, + renderer: &mut R, + location: Point, + max_size: Option>, + output_scale: Scale, + scale: f64, + alpha: f32, + ) -> Option + where + R: AsGlowRenderer, + R::TextureId: Send + Clone + 'static, + C: From>, + { + self.0.with_program(|p| { + let windows = p.windows.lock().unwrap(); + let active = p.active.load(Ordering::SeqCst); + let activated = p.activated.load(Ordering::Acquire); + let theme = p.theme.lock().unwrap(); + let appearance = p.appearance_conf.lock().unwrap(); + let tiled = p.tiled.load(Ordering::Acquire); + + if windows[active].is_maximized(false) { + return None; + } + + let round = appearance.clip_tiled_windows || !tiled; + if tiled && !appearance.shadow_tiled_windows { + return None; + } + let radii = if round { + theme + .cosmic() + .radius_s() + .map(|x| if x < 4.0 { x } else { x + 4.0 }) + .map(|x| (x * scale as f32).round() as u8) + } else { + [0, 0, 0, 0] + }; + + let mut geo = SpaceElement::geometry(&windows[active]).to_f64(); + geo.size.h += TAB_HEIGHT as f64; + if let Some(max_size) = max_size { + geo.size = geo.size.clamp(Size::default(), max_size.to_f64()); + } + + geo = geo.upscale(scale); + geo.loc += location.to_f64().to_logical(output_scale); + + let window_key = + CosmicMappedKey(CosmicMappedKeyInner::Stack(Arc::downgrade(&self.0.0))); + + Some( + CosmicStackRenderElement::Shadow(ShadowShader::element( + renderer, + window_key, + geo.to_i32_round().as_local(), + radii, + if activated { alpha } else { alpha * 0.75 }, + output_scale.x, + theme.cosmic().is_dark, + )) + .into(), + ) + }) + } + + pub fn push_render_elements( + &self, + renderer: &mut R, + location: Point, + max_size: Option>, + scale: Scale, + alpha: f32, + scanout_override: Option, + scanout_node: Option, + push_above: &mut dyn FnMut(CosmicStackRenderElement), + push_below: &mut dyn FnMut(CosmicStackRenderElement), + ) where + R: AsGlowRenderer, + R::TextureId: Send + Clone + 'static, + { + if !self + .0 + .with_program(|p| p.override_alive.load(Ordering::Acquire)) + { + return; + } + + let geometry = self + .0 + .with_program(|p| p.windows.lock().unwrap()[p.active.load(Ordering::SeqCst)].geometry()) + .to_physical_precise_round(scale); + let stack_loc = location + geometry.loc; + let window_loc = location + Point::from((0, (TAB_HEIGHT as f64 * scale.y) as i32)); + let frosted = self.0.with_program(|p| { + let theme = p.theme.lock().unwrap(); + if theme.cosmic().frosted_windows { + (theme.cosmic().frosted as u8 + 1) as usize + } else { + 0 + } + }); + let radii = self.0.with_program(|p| { + let windows = p.windows.lock().unwrap(); + let active = p.active.load(Ordering::SeqCst); + let appearance = p.appearance_conf.lock().unwrap(); + let theme = p.theme.lock().unwrap(); + let tiled = p.tiled.load(Ordering::Acquire); + let maximized = windows[active].is_maximized(false); + let round = (appearance.clip_tiled_windows || !tiled) && !maximized; + round.then(|| { + theme + .cosmic() + .radius_s() + .map(|x| if x < 4.0 { x } else { x + 4.0 }) + .map(|x| x.round() as u8) + }) + }); + + self.0.with_program(|p| { + let windows = p.windows.lock().unwrap(); + let active = p.active.load(Ordering::SeqCst); + let theme = p.theme.lock().unwrap(); + let maximized = windows[active].is_maximized(false); + + let mut geo = SpaceElement::geometry(&windows[active]).to_f64(); + geo.loc += location.to_f64().to_logical(scale); + geo.size.h += TAB_HEIGHT as f64; + if let Some(max_size) = max_size { + geo.size = geo.size.clamp(Size::default(), max_size.to_f64()); + } + + let window_key = + CosmicMappedKey(CosmicMappedKeyInner::Stack(Arc::downgrade(&self.0.0))); + + if !maximized { + let (r, g, b, a) = theme.cosmic().bg_divider().into_components(); + push_above(CosmicStackRenderElement::Border(IndicatorShader::element( + renderer, + Key::Window(Usage::Border, window_key.clone()), + geo.to_i32_round().as_local(), + 1, + radii.unwrap_or([0; 4]), + a * alpha, + scale.x, + [r, g, b], + ))); + }; + + let radii = radii.map(|[a, _, c, _]| [a, 0, c, 0]); + windows[active].push_render_elements( + renderer, + window_loc, + scale, + alpha, + scanout_override, + scanout_node, + radii.is_some(), + radii.unwrap_or([0; 4]), + frosted, + &mut |elem| push_above(elem.into()), + Some(&mut |elem| push_below(elem.into())), + ); + }); + + self.0.push_render_elements( + renderer, + stack_loc, + scale, + alpha, + radii.map(|[_, b, _, d]| [0, b, 0, d]).unwrap_or([0; 4]), + &mut |elem| push_above(elem.into()), + Some(&mut |elem| push_below(elem.into())), + ); + } + + pub(crate) fn set_theme(&self, mut theme: cosmic::Theme) { + theme.transparent = theme.cosmic().frosted_windows; + self.0.with_program(|p| { + *p.theme.lock().unwrap() = theme.clone(); + }); + self.0.set_theme(theme); + } + + pub fn update_appearance_conf(&self, appearance: &AppearanceConfig) { + if self.0.with_program(|p| { + let mut conf = p.appearance_conf.lock().unwrap(); + if &*conf != appearance { + *conf = *appearance; + true + } else { + false + } + }) { + self.0.force_redraw(); + self.0.force_update(); + } + } + + pub(crate) fn force_redraw(&self) { + self.0.force_redraw(); + } + + fn start_drag(&self, data: &mut State, seat: &Seat, serial: Serial) { + if let Some(dragged_out) = self + .0 + .with_program(|p| p.potential_drag.lock().unwrap().take()) + && let Some(surface) = self + .0 + .with_program(|p| p.windows.lock().unwrap().get(dragged_out).cloned()) + { + let seat = seat.clone(); + surface.try_force_undecorated(false); + surface.send_configure(); + if let Some(surface) = surface.wl_surface().map(Cow::into_owned) { + let _ = data.common.event_loop_handle.insert_idle(move |state| { + let res = state.common.shell.write().move_request( + &surface, + &seat, + serial, + ReleaseMode::NoMouseButtons, + true, + &state.common.config, + &state.common.event_loop_handle, + false, + ); + if let Some((grab, focus)) = res { + if grab.is_touch_grab() { + seat.get_touch().unwrap().set_grab(state, grab, serial); + } else { + seat.get_pointer() + .unwrap() + .set_grab(state, grab, serial, focus); + } + } + }); + } + } + } + + pub fn min_size(&self) -> Option> { + self.surfaces() + .fold(None, |min_size, window| { + let win_min_size = window.min_size_without_ssd(); + match (min_size, win_min_size) { + (None, None) => None, + (None, x) | (x, None) => x, + (Some(min1), Some(min2)) => { + Some((min1.w.max(min2.w), min1.h.max(min2.h)).into()) + } + } + }) + .map(|size| size + (0, TAB_HEIGHT).into()) + } + pub fn max_size(&self) -> Option> { + let theoretical_max = self + .surfaces() + .fold(None, |max_size, window| { + let win_max_size = window.max_size_without_ssd(); + match (max_size, win_max_size) { + (None, None) => None, + (None, x) | (x, None) => x, + (Some(max1), Some(max2)) => Some( + ( + if max1.w == 0 { + max2.w + } else if max2.w == 0 { + max1.w + } else { + max1.w.min(max2.w) + }, + if max1.h == 0 { + max2.h + } else if max2.h == 0 { + max1.h + } else { + max1.h.min(max2.h) + }, + ) + .into(), + ), + } + }) + .map(|size| size + (0, TAB_HEIGHT).into()); + // The problem is, with accumulated sizes, the minimum size could be larger than our maximum... + let min_size = self.min_size(); + match (theoretical_max, min_size) { + (None, _) => None, + (Some(max), None) => Some(max), + (Some(max), Some(min)) => Some((max.w.max(min.w), max.h.max(min.h)).into()), + } + } + + pub fn corner_radius(&self, geometry_size: Size, default_radius: u8) -> [u8; 4] { + self.0.with_program(|p| { + let active_window = &p.windows.lock().unwrap()[p.active.load(Ordering::SeqCst)]; + let is_tiled = p.tiled.load(Ordering::Acquire); + let appearance = p.appearance_conf.lock().unwrap(); + let maximized = active_window.is_maximized(false); + + let round = (appearance.clip_tiled_windows || !is_tiled) && !maximized; + let radii = p + .theme + .lock() + .unwrap() + .cosmic() + .radius_s() + .map(|x| if x < 4.0 { x } else { x + 4.0 }) + .map(|val| val.round() as u8); + + if !round { + let mut corners = active_window + .corner_radius(geometry_size) + .unwrap_or([default_radius; 4]); + + corners[1] = 0; + corners[3] = 0; + + corners + } else { + let mut corners = active_window.corner_radius(geometry_size).unwrap_or(radii); + + corners[0] = radii[0].max(corners[0]); + corners[1] = radii[1]; + corners[2] = radii[2].max(corners[2]); + corners[3] = radii[3]; + + corners + } + }) + } +} + +#[derive(Debug, Clone, Copy)] +pub enum Message { + DragStart, + Menu, + TabMenu(usize), + PotentialTabDragStart(usize), + Activate(usize), + Close(usize), + ScrollForward, + ScrollBack, + Scrolled, +} + +impl TabMessage for Message { + fn activate(idx: usize) -> Self { + Message::Activate(idx) + } + + fn scroll_back() -> Self { + Message::ScrollBack + } + + fn scroll_further() -> Self { + Message::ScrollForward + } + + fn populate_scroll(&mut self, mut current_offset: AbsoluteOffset) -> Option { + match self { + Message::ScrollBack => Some({ + current_offset.x -= 10.; + current_offset + }), + Message::ScrollForward => Some({ + current_offset.x += 10.; + current_offset + }), + _ => None, + } + } + + fn scrolled() -> Self { + Message::Scrolled + } +} + +impl Program for CosmicStackInternal { + type Message = Message; + + fn update( + &mut self, + message: Self::Message, + loop_handle: &LoopHandle<'static, crate::state::State>, + last_seat: Option<&(Seat, Serial)>, + ) -> Task { + match message { + Message::DragStart => { + if let Some((seat, serial)) = last_seat.cloned() { + let active = self.active.load(Ordering::SeqCst); + if let Some(surface) = self.windows.lock().unwrap()[active] + .wl_surface() + .map(Cow::into_owned) + { + loop_handle.insert_idle(move |state| { + let res = state.common.shell.write().move_request( + &surface, + &seat, + serial, + ReleaseMode::NoMouseButtons, + false, + &state.common.config, + &state.common.event_loop_handle, + false, + ); + if let Some((grab, focus)) = res { + if grab.is_touch_grab() { + seat.get_touch().unwrap().set_grab(state, grab, serial); + } else { + seat.get_pointer() + .unwrap() + .set_grab(state, grab, serial, focus); + } + } + }); + } + } + } + Message::PotentialTabDragStart(idx) => { + *self.potential_drag.lock().unwrap() = Some(idx); + } + Message::Activate(idx) => { + *self.potential_drag.lock().unwrap() = None; + if let Some(surface) = self.windows.lock().unwrap().get(idx).cloned() { + loop_handle.insert_idle(move |state| { + if let Some(mapped) = + state.common.shell.read().element_for_surface(&surface) + { + mapped.stack_ref().unwrap().set_active(&surface); + } + }); + self.scroll_to_focus.store(true, Ordering::SeqCst); + } + } + Message::Close(idx) => { + if let Some(val) = self.windows.lock().unwrap().get(idx) { + val.close() + } + } + Message::Scrolled => { + self.scroll_to_focus.store(false, Ordering::SeqCst); + } + Message::Menu => { + if let Some((seat, serial)) = last_seat.cloned() { + let active = self.active.load(Ordering::SeqCst); + if let Some(surface) = self.windows.lock().unwrap()[active] + .wl_surface() + .map(Cow::into_owned) + { + loop_handle.insert_idle(move |state| { + let shell = state.common.shell.read(); + if let Some(mapped) = shell.element_for_surface(&surface).cloned() { + let position = if let Some((output, set)) = + shell.workspaces.sets.iter().find(|(_, set)| { + set.sticky_layer.mapped().any(|m| m == &mapped) + }) { + set.sticky_layer + .element_geometry(&mapped) + .unwrap() + .loc + .to_global(output) + } else if let Some(workspace) = shell.space_for(&mapped) { + let Some(elem_geo) = workspace.element_geometry(&mapped) else { + return; + }; + elem_geo.loc.to_global(&workspace.output) + } else { + return; + }; + + let mut cursor = seat + .get_pointer() + .unwrap() + .current_location() + .to_i32_round(); + cursor.y -= TAB_HEIGHT; + let res = shell.menu_request( + false, + &surface, + &seat, + serial, + cursor - position.as_logical(), + true, + &state.common.config, + &state.common.event_loop_handle, + ); + + std::mem::drop(shell); + if let Some((grab, focus)) = res { + seat.get_pointer() + .unwrap() + .set_grab(state, grab, serial, focus); + } + } + }); + } + } + } + Message::TabMenu(idx) => { + if let Some((seat, serial)) = last_seat.cloned() + && let Some(surface) = self.windows.lock().unwrap()[idx] + .wl_surface() + .map(Cow::into_owned) + { + loop_handle.insert_idle(move |state| { + let shell = state.common.shell.read(); + if let Some(mapped) = shell.element_for_surface(&surface).cloned() + && let Some(workspace) = shell.space_for(&mapped) + { + let Some(elem_geo) = workspace.element_geometry(&mapped) else { + return; + }; + let position = elem_geo.loc.to_global(&workspace.output); + + let mut cursor = seat + .get_pointer() + .unwrap() + .current_location() + .to_i32_round(); + cursor.y -= TAB_HEIGHT; + let res = shell.menu_request( + false, + &surface, + &seat, + serial, + cursor - position.as_logical(), + false, + &state.common.config, + &state.common.event_loop_handle, + ); + + std::mem::drop(shell); + if let Some((grab, focus)) = res { + seat.get_pointer() + .unwrap() + .set_grab(state, grab, serial, focus); + } + } + }); + } + } + _ => unreachable!(), + } + Task::none() + } + + fn view(&self) -> CosmicElement<'_, Self::Message> { + HOOKS.get().unwrap().stack_decorations.view(self) + } + + fn foreground( + &self, + pixels: &mut tiny_skia::PixmapMut<'_>, + damage: &[Rectangle], + scale: f32, + theme: &Theme, + ) { + if self.group_focused.load(Ordering::SeqCst) { + let border = Rectangle::new( + (0, ((TAB_HEIGHT as f32 * scale) - scale).floor() as i32).into(), + (pixels.width() as i32, scale.ceil() as i32).into(), + ); + + let mut paint = tiny_skia::Paint::default(); + let (b, g, r, a) = theme.cosmic().accent_color().into_components(); + paint.set_color(tiny_skia::Color::from_rgba(r, g, b, a).unwrap()); + + for rect in damage { + if let Some(overlap) = rect.intersection(border) { + pixels.fill_rect( + tiny_skia::Rect::from_xywh( + overlap.loc.x as f32, + overlap.loc.y as f32, + overlap.size.w as f32, + overlap.size.h as f32, + ) + .unwrap(), + &paint, + Default::default(), + None, + ) + } + } + } + } +} + +#[derive(Debug)] +pub struct DefaultDecorations; + +impl Decorations for DefaultDecorations { + fn view(&self, stack: &CosmicStackInternal) -> cosmic::Element<'_, Message> { + let windows = stack.windows.lock().unwrap(); + if stack.geometry.lock().unwrap().is_none() { + return iced_widget::row(Vec::new()).into(); + }; + let active = stack.active.load(Ordering::SeqCst); + let group_focused = stack.group_focused.load(Ordering::SeqCst); + + let elements = vec![ + cosmic_widget::icon::from_name("window-stack-symbolic") + .size(16) + .prefer_svg(true) + .icon() + .class(if group_focused { + theme::Svg::custom(|theme| iced_widget::svg::Style { + color: Some(if theme.cosmic().is_dark { + Color::BLACK + } else { + Color::WHITE + }), + }) + } else { + theme::Svg::Default + }) + .apply(iced_widget::container) + .padding([4, 24]) + .align_y(Alignment::Center) + .apply(iced_widget::mouse_area) + .on_press(Message::DragStart) + .on_right_press(Message::Menu) + .into(), + CosmicElement::new( + Tabs::new( + windows.iter().enumerate().map(|(i, w)| { + let user_data = w.user_data(); + user_data.insert_if_missing(Id::unique); + Tab::new( + w.title(), + w.app_id(), + user_data.get::().unwrap().clone(), + ) + .on_press(Message::PotentialTabDragStart(i)) + .on_right_click(Message::TabMenu(i)) + .on_close(Message::Close(i)) + }), + active, + windows[active].is_activated(false), + group_focused, + ) + .id(SCROLLABLE_ID.clone()) + .force_visible( + stack + .scroll_to_focus + .load(Ordering::SeqCst) + .then_some(active), + ) + .height(Length::Fill) + .width(Length::Fill), + ), + iced_widget::space::horizontal() + .width(Length::Fixed(0.0)) + .apply(iced_widget::container) + .padding([64, 24]) + .apply(iced_widget::mouse_area) + .on_press(Message::DragStart) + .on_right_press(Message::Menu) + .into(), + ]; + + let radius = if windows[active].is_maximized(false) + || (stack.tiled.load(Ordering::Acquire) + && !stack.appearance_conf.lock().unwrap().clip_tiled_windows) + { + Radius::from(0.0) + } else { + let radii = stack + .theme + .lock() + .unwrap() + .cosmic() + .radius_s() + .map(|x| if x < 4.0 { x } else { x + 4.0 }); + Radius::from([radii[0], radii[1], 0., 0.]) + }; + let group_focused = stack.group_focused.load(Ordering::SeqCst); + + iced_widget::row(elements) + .height(TAB_HEIGHT as u16) + .width(Length::Fill) + .apply(iced_widget::container) + .align_y(Alignment::Center) + .class(theme::Container::custom(move |theme| { + let cosmic_theme = theme.cosmic(); + + let mut background = if group_focused { + cosmic_theme.accent_color() + } else { + cosmic_theme.primary_container_color() + }; + if cosmic_theme.frosted_windows { + background.alpha = cosmic_theme.alpha_map.blurred_alpha(cosmic_theme.frosted); + } + + iced_widget::container::Style { + snap: true, + icon_color: Some( + cosmic_theme + .background(cosmic_theme.frosted_windows) + .on + .into(), + ), + text_color: Some( + cosmic_theme + .background(cosmic_theme.frosted_windows) + .on + .into(), + ), + background: Some(Background::Color(background.into())), + border: Border { + radius, + width: 0.0, + color: Color::TRANSPARENT, + }, + shadow: Default::default(), + } + })) + .into() + } +} + +impl IsAlive for CosmicStack { + fn alive(&self) -> bool { + self.0.with_program(|p| { + p.override_alive.load(Ordering::SeqCst) + && p.windows.lock().unwrap().iter().any(IsAlive::alive) + }) + } +} + +impl SpaceElement for CosmicStack { + fn bbox(&self) -> Rectangle { + self.0.with_program(|p| { + let mut bbox = + SpaceElement::bbox(&p.windows.lock().unwrap()[p.active.load(Ordering::SeqCst)]); + bbox.loc -= Point::from((RESIZE_BORDER, RESIZE_BORDER)); + bbox.size += Size::from((RESIZE_BORDER * 2, RESIZE_BORDER * 2)); + bbox.size.h += TAB_HEIGHT; + bbox + }) + } + fn is_in_input_region(&self, point: &Point) -> bool { + self.focus_under(*point, WindowSurfaceType::ALL).is_some() + } + fn set_activate(&self, activated: bool) { + SpaceElement::set_activate(&self.0, activated); + let changed = self.0.with_program(|p| { + if !p.group_focused.load(Ordering::SeqCst) { + p.windows + .lock() + .unwrap() + .iter() + .enumerate() + .for_each(|(i, w)| { + w.set_activated(activated && p.active.load(Ordering::SeqCst) == i); + w.send_configure(); + }); + } + p.activated.swap(activated, Ordering::SeqCst) != activated + }); + + if changed { + self.0.force_redraw(); + } + } + fn output_enter(&self, output: &Output, overlap: Rectangle) { + SpaceElement::output_enter(&self.0, output, overlap); + self.0.with_program(|p| { + p.windows + .lock() + .unwrap() + .iter() + .for_each(|w| SpaceElement::output_enter(w, output, overlap)) + }) + } + fn output_leave(&self, output: &Output) { + SpaceElement::output_leave(&self.0, output); + self.0.with_program(|p| { + p.windows + .lock() + .unwrap() + .iter() + .for_each(|w| SpaceElement::output_leave(w, output)) + }) + } + fn geometry(&self) -> Rectangle { + self.0.with_program(|p| { + let mut geo = + SpaceElement::geometry(&p.windows.lock().unwrap()[p.active.load(Ordering::SeqCst)]); + geo.size.h += TAB_HEIGHT; + geo + }) + } + fn z_index(&self) -> u8 { + self.0.with_program(|p| { + SpaceElement::z_index(&p.windows.lock().unwrap()[p.active.load(Ordering::SeqCst)]) + }) + } + fn refresh(&self) { + self.0.with_program(|p| { + let mut windows = p.windows.lock().unwrap(); + + // don't let the stack become empty + let old_active = p.active.load(Ordering::SeqCst); + let active = windows[old_active].clone(); + windows.retain(IsAlive::alive); + if windows.is_empty() { + windows.push(active); + } + + let len = windows.len(); + let _ = p + .active + .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |active| { + (active >= len).then_some(len - 1) + }); + let active = p.active.load(Ordering::SeqCst); + + if old_active != active { + self.0 + .resize(Size::from((self.active().geometry().size.w, TAB_HEIGHT))); + self.0.force_redraw(); + } + + windows.iter().enumerate().for_each(|(i, w)| { + if i == active { + w.set_suspended(false); + } else { + w.set_suspended(true); + } + w.send_configure(); + + SpaceElement::refresh(w) + }); + }); + SpaceElement::refresh(&self.0); + } +} + +impl KeyboardTarget for CosmicStack { + fn enter( + &self, + seat: &Seat, + data: &mut State, + keys: Vec>, + serial: Serial, + ) { + self.0.with_program(|p| { + let active = p.active.load(Ordering::SeqCst); + p.previous_keyboard.store(active, Ordering::SeqCst); + KeyboardTarget::enter( + &p.windows.lock().unwrap()[p.active.load(Ordering::SeqCst)], + seat, + data, + keys, + serial, + ) + }) + } + fn leave(&self, seat: &Seat, data: &mut State, serial: Serial) { + let active = self.keyboard_leave_if_previous(seat, data, serial); + self.0.force_redraw(); + self.0.with_program(|p| { + p.group_focused.store(false, Ordering::SeqCst); + KeyboardTarget::leave(&p.windows.lock().unwrap()[active], seat, data, serial) + }) + } + fn key( + &self, + seat: &Seat, + data: &mut State, + key: KeysymHandle<'_>, + state: KeyState, + serial: Serial, + time: u32, + ) { + let active = self.keyboard_leave_if_previous(seat, data, serial); + self.0.with_program(|p| { + if !p.group_focused.load(Ordering::SeqCst) { + KeyboardTarget::key( + &p.windows.lock().unwrap()[active], + seat, + data, + key, + state, + serial, + time, + ) + } + }) + } + fn modifiers( + &self, + seat: &Seat, + data: &mut State, + modifiers: ModifiersState, + serial: Serial, + ) { + let active = self.keyboard_leave_if_previous(seat, data, serial); + self.0.with_program(|p| { + if !p.group_focused.load(Ordering::SeqCst) { + KeyboardTarget::modifiers( + &p.windows.lock().unwrap()[active], + seat, + data, + modifiers, + serial, + ) + } + }) + } +} + +impl PointerTarget for CosmicStack { + fn enter(&self, seat: &Seat, data: &mut State, event: &MotionEvent) { + let mut event = event.clone(); + self.0.with_program(|p| { + let active_window = &p.windows.lock().unwrap()[p.active.load(Ordering::SeqCst)]; + let Some(next) = Focus::under(active_window, TAB_HEIGHT, event.location) else { + return; + }; + let _old_focus = p.swap_focus(Some(next)); + + let mut cursor_state = seat + .user_data() + .get::() + .unwrap() + .lock() + .unwrap(); + cursor_state.set_shape(next.cursor_shape()); + seat.set_cursor_image_status(CursorImageStatus::default_named()); + }); + + event.location -= self.0.with_program(|p| { + p.windows.lock().unwrap()[p.active.load(Ordering::SeqCst)] + .geometry() + .loc + .to_f64() + }); + PointerTarget::enter(&self.0, seat, data, &event) + } + + fn motion(&self, seat: &Seat, data: &mut State, event: &MotionEvent) { + let mut event = event.clone(); + self.0.with_program(|p| { + let active = p.active.load(Ordering::SeqCst); + let active_window = &p.windows.lock().unwrap()[active]; + let Some(next) = Focus::under(active_window, TAB_HEIGHT, event.location) else { + return; + }; + let _previous = p.swap_focus(Some(next)); + + let mut cursor_state = seat + .user_data() + .get::() + .unwrap() + .lock() + .unwrap(); + cursor_state.set_shape(next.cursor_shape()); + seat.set_cursor_image_status(CursorImageStatus::default_named()); + }); + + let active_window_geo = self.0.with_program(|p| { + p.windows.lock().unwrap()[p.active.load(Ordering::SeqCst)].geometry() + }); + event.location -= active_window_geo.loc.to_f64(); + + PointerTarget::motion(&self.0, seat, data, &event); + if event.location.y < 0.0 + || event.location.y > TAB_HEIGHT as f64 + || event.location.x < 64.0 + || event.location.x > (active_window_geo.size.w as f64 - 64.0) + { + self.start_drag(data, seat, event.serial); + } + } + + fn relative_motion( + &self, + _seat: &Seat, + _data: &mut State, + _event: &RelativeMotionEvent, + ) { + } + + fn button(&self, seat: &Seat, data: &mut State, event: &ButtonEvent) { + match self.0.with_program(|p| p.current_focus()) { + Some(Focus::Header) => PointerTarget::button(&self.0, seat, data, event), + Some(x) => { + let serial = event.serial; + let seat = seat.clone(); + let Some(surface) = self.0.with_program(|p| { + let window = &p.windows.lock().unwrap()[p.active.load(Ordering::SeqCst)]; + window.wl_surface().map(Cow::into_owned) + }) else { + return; + }; + self.0.loop_handle().insert_idle(move |state| { + let res = state.common.shell.write().resize_request( + &surface, + &seat, + serial, + match x { + Focus::ResizeTop => ResizeEdge::TOP, + Focus::ResizeTopLeft => ResizeEdge::TOP_LEFT, + Focus::ResizeTopRight => ResizeEdge::TOP_RIGHT, + Focus::ResizeBottom => ResizeEdge::BOTTOM, + Focus::ResizeBottomLeft => ResizeEdge::BOTTOM_LEFT, + Focus::ResizeBottomRight => ResizeEdge::BOTTOM_RIGHT, + Focus::ResizeLeft => ResizeEdge::LEFT, + Focus::ResizeRight => ResizeEdge::RIGHT, + Focus::Header => unreachable!(), + }, + state.common.config.cosmic_conf.edge_snap_threshold, + false, + ); + if let Some((grab, focus)) = res { + if grab.is_touch_grab() { + seat.get_touch().unwrap().set_grab(state, grab, serial); + } else { + seat.get_pointer() + .unwrap() + .set_grab(state, grab, serial, focus); + } + } + }); + } + None => {} + } + } + + fn axis(&self, seat: &Seat, data: &mut State, frame: AxisFrame) { + if let Some(Focus::Header) = self.0.with_program(|p| p.current_focus()) { + PointerTarget::axis(&self.0, seat, data, frame) + } + } + + fn frame(&self, seat: &Seat, data: &mut State) { + if let Some(Focus::Header) = self.0.with_program(|p| p.current_focus()) { + PointerTarget::frame(&self.0, seat, data) + } + } + + fn leave(&self, seat: &Seat, data: &mut State, serial: Serial, time: u32) { + self.0.with_program(|p| { + let mut cursor_state = seat + .user_data() + .get::() + .unwrap() + .lock() + .unwrap(); + cursor_state.unset_shape(); + let _previous = p.swap_focus(None); + }); + + PointerTarget::leave(&self.0, seat, data, serial, time); + + if let Some(dragged_out) = self + .0 + .with_program(|p| p.potential_drag.lock().unwrap().take()) + && let Some(surface) = self + .0 + .with_program(|p| p.windows.lock().unwrap().get(dragged_out).cloned()) + { + let seat = seat.clone(); + surface.try_force_undecorated(false); + surface.send_configure(); + if let Some(surface) = surface.wl_surface().map(Cow::into_owned) { + let _ = data.common.event_loop_handle.insert_idle(move |state| { + let res = state.common.shell.write().move_request( + &surface, + &seat, + serial, + ReleaseMode::NoMouseButtons, + true, + &state.common.config, + &state.common.event_loop_handle, + false, + ); + if let Some((grab, focus)) = res { + if grab.is_touch_grab() { + seat.get_touch().unwrap().set_grab(state, grab, serial); + } else { + seat.get_pointer() + .unwrap() + .set_grab(state, grab, serial, focus); + } + } + }); + } + } + } + + fn gesture_swipe_begin( + &self, + _seat: &Seat, + _data: &mut State, + _event: &GestureSwipeBeginEvent, + ) { + } + + fn gesture_swipe_update( + &self, + _seat: &Seat, + _data: &mut State, + _event: &GestureSwipeUpdateEvent, + ) { + } + + fn gesture_swipe_end( + &self, + _seat: &Seat, + _data: &mut State, + _event: &GestureSwipeEndEvent, + ) { + } + + fn gesture_pinch_begin( + &self, + _seat: &Seat, + _data: &mut State, + _event: &GesturePinchBeginEvent, + ) { + } + + fn gesture_pinch_update( + &self, + _seat: &Seat, + _data: &mut State, + _event: &GesturePinchUpdateEvent, + ) { + } + + fn gesture_pinch_end( + &self, + _seat: &Seat, + _data: &mut State, + _event: &GesturePinchEndEvent, + ) { + } + + fn gesture_hold_begin( + &self, + _seat: &Seat, + _data: &mut State, + _event: &GestureHoldBeginEvent, + ) { + } + + fn gesture_hold_end( + &self, + _seat: &Seat, + _data: &mut State, + _event: &GestureHoldEndEvent, + ) { + } +} + +impl TouchTarget for CosmicStack { + fn down(&self, seat: &Seat, data: &mut State, event: &DownEvent) { + let mut event = event.clone(); + let active_window_geo = self.0.with_program(|p| { + p.windows.lock().unwrap()[p.active.load(Ordering::SeqCst)].geometry() + }); + event.location -= active_window_geo.loc.to_f64(); + self.0 + .with_program(|p| p.touch_serial.store(event.serial.into(), Ordering::Release)); + TouchTarget::down(&self.0, seat, data, &event) + } + + fn up(&self, seat: &Seat, data: &mut State, event: &UpEvent) { + TouchTarget::up(&self.0, seat, data, event) + } + + fn motion(&self, seat: &Seat, data: &mut State, event: &TouchMotionEvent) { + let mut event = event.clone(); + let active_window_geo = self.0.with_program(|p| { + p.windows.lock().unwrap()[p.active.load(Ordering::SeqCst)].geometry() + }); + event.location -= active_window_geo.loc.to_f64(); + TouchTarget::motion(&self.0, seat, data, &event); + + if event.location.y < 0.0 + || event.location.y > TAB_HEIGHT as f64 + || event.location.x < 64.0 + || event.location.x > (active_window_geo.size.w as f64 - 64.0) + { + self.start_drag( + data, + seat, + self.0 + .with_program(|p| p.touch_serial.load(Ordering::Acquire)) + .into(), + ); + } + } + + fn frame(&self, seat: &Seat, data: &mut State, frame: FrameMarker) { + TouchTarget::frame(&self.0, seat, data, frame) + } + + fn cancel(&self, seat: &Seat, data: &mut State, frame: FrameMarker) { + TouchTarget::cancel(&self.0, seat, data, frame) + } + + fn shape(&self, seat: &Seat, data: &mut State, event: &ShapeEvent) { + TouchTarget::shape(&self.0, seat, data, event) + } + + fn orientation(&self, seat: &Seat, data: &mut State, event: &OrientationEvent) { + TouchTarget::orientation(&self.0, seat, data, event) + } + + fn last_frame(&self, seat: &Seat, data: &mut State) -> Option { + TouchTarget::last_frame(&self.0, seat, data) + } +} + +pub enum CosmicStackRenderElement { + Header(IcedRenderElement), + Shadow(PixelShaderElement), + Border(PixelShaderElement), + Window(SurfaceRenderElement), +} + +impl From> + for CosmicStackRenderElement +{ + fn from(value: IcedRenderElement) -> Self { + Self::Header(value) + } +} + +impl From> + for CosmicStackRenderElement +{ + fn from(value: SurfaceRenderElement) -> Self { + Self::Window(value) + } +} + +impl Element for CosmicStackRenderElement +where + R: Renderer + ImportAll + ImportMem + AsGlowRenderer, + R::TextureId: Send + 'static, +{ + fn id(&self) -> &RendererId { + match self { + CosmicStackRenderElement::Header(elem) => elem.id(), + CosmicStackRenderElement::Shadow(elem) => elem.id(), + CosmicStackRenderElement::Border(elem) => elem.id(), + CosmicStackRenderElement::Window(elem) => elem.id(), + } + } + + fn current_commit(&self) -> CommitCounter { + match self { + CosmicStackRenderElement::Header(elem) => elem.current_commit(), + CosmicStackRenderElement::Shadow(elem) => elem.current_commit(), + CosmicStackRenderElement::Border(elem) => elem.current_commit(), + CosmicStackRenderElement::Window(elem) => elem.current_commit(), + } + } + + fn src(&self) -> Rectangle { + match self { + CosmicStackRenderElement::Header(elem) => elem.src(), + CosmicStackRenderElement::Shadow(elem) => elem.src(), + CosmicStackRenderElement::Border(elem) => elem.src(), + CosmicStackRenderElement::Window(elem) => elem.src(), + } + } + + fn geometry(&self, scale: Scale) -> Rectangle { + match self { + CosmicStackRenderElement::Header(elem) => elem.geometry(scale), + CosmicStackRenderElement::Shadow(elem) => elem.geometry(scale), + CosmicStackRenderElement::Border(elem) => elem.geometry(scale), + CosmicStackRenderElement::Window(elem) => elem.geometry(scale), + } + } + + fn location(&self, scale: Scale) -> Point { + match self { + CosmicStackRenderElement::Header(elem) => elem.location(scale), + CosmicStackRenderElement::Shadow(elem) => elem.location(scale), + CosmicStackRenderElement::Border(elem) => elem.location(scale), + CosmicStackRenderElement::Window(elem) => elem.location(scale), + } + } + + fn transform(&self) -> Transform { + match self { + CosmicStackRenderElement::Header(elem) => elem.transform(), + CosmicStackRenderElement::Shadow(elem) => elem.transform(), + CosmicStackRenderElement::Border(elem) => elem.transform(), + CosmicStackRenderElement::Window(elem) => elem.transform(), + } + } + + fn damage_since( + &self, + scale: Scale, + commit: Option, + ) -> DamageSet { + match self { + CosmicStackRenderElement::Header(elem) => elem.damage_since(scale, commit), + CosmicStackRenderElement::Shadow(elem) => elem.damage_since(scale, commit), + CosmicStackRenderElement::Border(elem) => elem.damage_since(scale, commit), + CosmicStackRenderElement::Window(elem) => elem.damage_since(scale, commit), + } + } + + fn opaque_regions(&self, scale: Scale) -> OpaqueRegions { + match self { + CosmicStackRenderElement::Header(elem) => elem.opaque_regions(scale), + CosmicStackRenderElement::Shadow(elem) => elem.opaque_regions(scale), + CosmicStackRenderElement::Border(elem) => elem.opaque_regions(scale), + CosmicStackRenderElement::Window(elem) => elem.opaque_regions(scale), + } + } + + fn alpha(&self) -> f32 { + match self { + CosmicStackRenderElement::Header(elem) => elem.alpha(), + CosmicStackRenderElement::Shadow(elem) => elem.alpha(), + CosmicStackRenderElement::Border(elem) => elem.alpha(), + CosmicStackRenderElement::Window(elem) => elem.alpha(), + } + } + + fn kind(&self) -> Kind { + match self { + CosmicStackRenderElement::Header(elem) => elem.kind(), + CosmicStackRenderElement::Shadow(elem) => elem.kind(), + CosmicStackRenderElement::Border(elem) => elem.kind(), + CosmicStackRenderElement::Window(elem) => elem.kind(), + } + } + + fn is_framebuffer_effect(&self) -> bool { + match self { + CosmicStackRenderElement::Header(elem) => elem.is_framebuffer_effect(), + CosmicStackRenderElement::Shadow(elem) => elem.is_framebuffer_effect(), + CosmicStackRenderElement::Border(elem) => elem.is_framebuffer_effect(), + CosmicStackRenderElement::Window(elem) => elem.is_framebuffer_effect(), + } + } +} + +impl RenderElement for CosmicStackRenderElement +where + R: AsGlowRenderer, + R::TextureId: Send + 'static, +{ + fn draw( + &self, + frame: &mut R::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + cache: Option<&UserDataMap>, + ) -> Result<(), ::Error> { + match self { + CosmicStackRenderElement::Header(elem) => { + elem.draw(frame, src, dst, damage, opaque_regions, cache) + } + CosmicStackRenderElement::Shadow(elem) | CosmicStackRenderElement::Border(elem) => { + RenderElement::::draw( + elem, + R::glow_frame_mut(frame), + src, + dst, + damage, + opaque_regions, + cache, + ) + .map_err(R::from_gles_error) + } + CosmicStackRenderElement::Window(elem) => { + elem.draw(frame, src, dst, damage, opaque_regions, cache) + } + } + } + + fn underlying_storage(&self, renderer: &mut R) -> Option> { + match self { + CosmicStackRenderElement::Header(elem) => elem.underlying_storage(renderer), + CosmicStackRenderElement::Shadow(elem) | CosmicStackRenderElement::Border(elem) => { + elem.underlying_storage(renderer.glow_renderer_mut()) + } + CosmicStackRenderElement::Window(elem) => elem.underlying_storage(renderer), + } + } + + fn capture_framebuffer( + &self, + frame: &mut R::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + cache: &UserDataMap, + ) -> Result<(), ::Error> { + match self { + CosmicStackRenderElement::Header(elem) => { + elem.capture_framebuffer(frame, src, dst, cache) + } + CosmicStackRenderElement::Shadow(elem) | CosmicStackRenderElement::Border(elem) => { + RenderElement::::capture_framebuffer( + elem, + R::glow_frame_mut(frame), + src, + dst, + cache, + ) + .map_err(R::from_gles_error) + } + CosmicStackRenderElement::Window(elem) => { + elem.capture_framebuffer(frame, src, dst, cache) + } + } + } +} diff --git a/src/shell/element/stack/tab.rs b/src/shell/element/stack/tab.rs new file mode 100644 index 0000000..f8fda52 --- /dev/null +++ b/src/shell/element/stack/tab.rs @@ -0,0 +1,480 @@ +use cosmic::{ + Apply, + font::Font, + iced::{ + Background, + core::{ + Border, Clipboard, Color, Length, Rectangle, Shell, Size, alignment, event, + layout::{Layout, Limits, Node}, + mouse, overlay, renderer, + text::{Ellipsize, EllipsizeHeightLimit, Shaping, Wrapping}, + widget::{Id, Widget, operation::Operation, tree::Tree}, + }, + widget::{self, container::draw_background, rule::FillMode, scrollable::AbsoluteOffset}, + }, + theme, + widget::{Icon, icon::from_name, text}, +}; + +#[derive(Clone, Copy)] +pub(super) enum TabRuleTheme { + ActiveActivated, + ActiveDeactivated, + Default, +} + +impl From for theme::Rule { + fn from(theme: TabRuleTheme) -> Self { + match theme { + TabRuleTheme::ActiveActivated => Self::custom(|theme| widget::rule::Style { + color: theme.cosmic().accent_color().into(), + snap: true, + radius: 0.0.into(), + fill_mode: FillMode::Full, + }), + TabRuleTheme::ActiveDeactivated => Self::custom(|theme| widget::rule::Style { + color: theme.cosmic().palette.neutral_5.into(), + snap: true, + radius: 0.0.into(), + fill_mode: FillMode::Full, + }), + TabRuleTheme::Default => Self::custom(|theme| widget::rule::Style { + color: theme.cosmic().palette.neutral_5.into(), + snap: true, + radius: 8.0.into(), + fill_mode: FillMode::Padded(4), + }), + } + } +} + +#[derive(Clone, Copy)] +pub(super) enum TabBackgroundTheme { + ActiveActivated, + ActiveDeactivated, + Default, +} + +impl From for theme::Container<'_> { + fn from(background_theme: TabBackgroundTheme) -> Self { + match background_theme { + TabBackgroundTheme::ActiveActivated => { + Self::custom(move |theme| widget::container::Style { + snap: true, + icon_color: Some(Color::from(theme.cosmic().accent_text_color())), + text_color: Some(Color::from(theme.cosmic().accent_text_color())), + background: Some(Background::Color({ + let mut color = theme + .cosmic() + .primary(theme.cosmic().frosted_windows) + .component + .selected; + if theme.cosmic().frosted_windows { + color.alpha = theme + .cosmic() + .alpha_map + .blurred_alpha(theme.cosmic().frosted); + } + color.into() + })), + border: Border { + radius: 0.0.into(), + width: 0.0, + color: Color::TRANSPARENT, + }, + shadow: Default::default(), + }) + } + TabBackgroundTheme::ActiveDeactivated => { + Self::custom(move |theme| widget::container::Style { + snap: true, + icon_color: None, + text_color: None, + background: Some(Background::Color({ + let mut color = theme + .cosmic() + .primary(theme.cosmic().frosted_windows) + .component + .base; + if theme.cosmic().frosted_windows { + color.alpha = theme + .cosmic() + .alpha_map + .blurred_alpha(theme.cosmic().frosted); + } + color.into() + })), + border: Border { + radius: 0.0.into(), + width: 0.0, + color: Color::TRANSPARENT, + }, + shadow: Default::default(), + }) + } + TabBackgroundTheme::Default => Self::Transparent, + } + } +} + +pub trait TabMessage: Clone { + fn activate(idx: usize) -> Self; + + fn scroll_further() -> Self; + fn scroll_back() -> Self; + fn populate_scroll(&mut self, current_offset: AbsoluteOffset) -> Option; + fn scrolled() -> Self; +} + +pub struct Tab { + id: Id, + app_icon: Icon, + title: String, + font: Font, + close_message: Option, + press_message: Option, + right_click_message: Option, + rule_theme: TabRuleTheme, + background_theme: TabBackgroundTheme, + active: bool, +} + +impl Tab { + pub fn new(title: impl Into, app_id: impl Into, id: Id) -> Self { + Tab { + id, + app_icon: from_name(app_id.into()).size(16).icon(), + title: title.into(), + font: cosmic::font::default(), + close_message: None, + press_message: None, + right_click_message: None, + rule_theme: TabRuleTheme::Default, + background_theme: TabBackgroundTheme::Default, + active: false, + } + } + + pub fn on_press(mut self, message: Message) -> Self { + self.press_message = Some(message); + self + } + + pub fn on_right_click(mut self, message: Message) -> Self { + self.right_click_message = Some(message); + self + } + + pub fn on_close(mut self, message: Message) -> Self { + self.close_message = Some(message); + self + } + + pub(super) fn font(mut self, font: Font) -> Self { + self.font = font; + self + } + + pub(super) fn rule_style(mut self, theme: TabRuleTheme) -> Self { + self.rule_theme = theme; + self + } + + pub(super) fn background_style(mut self, theme: TabBackgroundTheme) -> Self { + self.background_theme = theme; + self + } + + pub(super) fn non_active(mut self) -> Self { + self.active = false; + self + } + + pub(super) fn active(mut self) -> Self { + self.active = true; + self + } + + pub(super) fn internal<'a>(self, idx: usize) -> TabInternal<'a, Message> { + let mut close_button = from_name("window-close-symbolic") + .size(16) + .prefer_svg(true) + .icon() + .apply(widget::button) + .padding(0) + .class(theme::iced::Button::Text); + if let Some(close_message) = self.close_message { + close_button = close_button.on_press(close_message); + } + + let items = vec![ + widget::rule::vertical(4).class(self.rule_theme).into(), + self.app_icon + .clone() + .apply(widget::container) + .width(Length::Shrink) + .padding([2, 4]) + .center_y(Length::Fill) + .into(), + text::body(self.title) + .font(self.font) + .wrapping(Wrapping::None) + .shaping(Shaping::Advanced) + .ellipsize(Ellipsize::End(EllipsizeHeightLimit::Lines(1))) + .align_y(alignment::Vertical::Center) + .height(Length::Fill) + .width(Length::Fill) + .into(), + close_button + .apply(widget::container) + .width(Length::Shrink) + .padding([2, 4]) + .center_y(Length::Fill) + .align_x(alignment::Horizontal::Right) + .into(), + ]; + + TabInternal { + id: self.id, + idx, + active: self.active, + background: self.background_theme.into(), + elements: items, + press_message: self.press_message, + right_click_message: self.right_click_message, + } + } +} + +const TAB_HEIGHT: i32 = 24; +pub const MIN_ACTIVE_TAB_WIDTH: i32 = 140; +const MIN_TAB_WIDTH: i32 = 38; + +const TEXT_BREAKPOINT: i32 = 44; +const CLOSE_BREAKPOINT: i32 = 125; + +pub(super) struct TabInternal<'a, Message: TabMessage> { + id: Id, + idx: usize, + active: bool, + background: theme::Container<'a>, + elements: Vec>, + press_message: Option, + right_click_message: Option, +} + +impl Widget for TabInternal<'_, Message> +where + Message: TabMessage, +{ + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn children(&self) -> Vec { + self.elements.iter().map(Tree::new).collect() + } + + fn set_id(&mut self, id: Id) { + self.id = id; + } + + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(&mut self.elements); + } + + fn size(&self) -> Size { + Size::new(Length::Fill, Length::Fill) + } + + fn layout(&mut self, tree: &mut Tree, renderer: &cosmic::Renderer, limits: &Limits) -> Node { + let min_size = Size { + height: TAB_HEIGHT as f32, + width: if self.active { + MIN_ACTIVE_TAB_WIDTH as f32 + } else { + MIN_TAB_WIDTH as f32 + }, + }; + let limits = limits + .min_width(min_size.width) + .min_height(min_size.height) + .width(Length::Fill) + .height(Length::Fill); + let size = limits + .resolve(Length::Shrink, Length::Shrink, min_size) + .max(min_size); + + let limits = Limits::new(size, size) + .min_width(size.width) + .min_height(size.height) + .width(size.width) + .height(size.height); + cosmic::iced::core::layout::flex::resolve( + cosmic::iced::core::layout::flex::Axis::Horizontal, + renderer, + &limits, + Length::Fill, + Length::Fill, + 0.into(), + 8., + cosmic::iced::Alignment::Center, + if size.width >= CLOSE_BREAKPOINT as f32 { + &mut self.elements + } else if size.width >= TEXT_BREAKPOINT as f32 { + &mut self.elements[0..3] + } else { + &mut self.elements[0..2] + }, + &mut tree.children, + ) + } + + fn operate( + &mut self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &cosmic::Renderer, + operation: &mut dyn Operation<()>, + ) { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { + self.elements + .iter_mut() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), layout)| { + child + .as_widget_mut() + .operate(state, layout, renderer, operation); + }); + }); + } + + fn update( + &mut self, + tree: &mut Tree, + event: &event::Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &cosmic::Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) { + for ((child, state), layout) in self + .elements + .iter_mut() + .zip(&mut tree.children) + .zip(layout.children()) + { + child.as_widget_mut().update( + state, event, layout, cursor, renderer, clipboard, shell, viewport, + ); + } + + if !shell.is_event_captured() && cursor.is_over(layout.bounds()) { + if matches!( + event, + event::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + ) && let Some(message) = self.press_message.clone() + { + shell.publish(message); + shell.capture_event(); + return; + } + if matches!( + event, + event::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) + ) && let Some(message) = self.right_click_message.clone() + { + shell.publish(message); + shell.capture_event(); + return; + } + if matches!( + event, + event::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + ) { + shell.publish(Message::activate(self.idx)); + shell.capture_event(); + } + } + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &cosmic::Renderer, + ) -> mouse::Interaction { + self.elements + .iter() + .zip(&tree.children) + .zip(layout.children()) + .map(|((child, state), layout)| { + child + .as_widget() + .mouse_interaction(state, layout, cursor, viewport, renderer) + }) + .max() + .unwrap_or_default() + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut cosmic::Renderer, + theme: &cosmic::Theme, + renderer_style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + use cosmic::widget::container::Catalog; + let style = theme.style(&self.background); + + draw_background(renderer, &style, layout.bounds()); + + for ((child, state), layout) in self + .elements + .iter() + .zip(&tree.children) + .zip(layout.children()) + { + child.as_widget().draw( + state, + renderer, + theme, + &renderer::Style { + icon_color: style.text_color.unwrap_or(renderer_style.text_color), + text_color: style.text_color.unwrap_or(renderer_style.text_color), + scale_factor: renderer_style.scale_factor, + }, + layout, + cursor, + viewport, + ); + } + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'b>, + renderer: &cosmic::Renderer, + viewport: &Rectangle, + translation: cosmic::iced::Vector, + ) -> Option> { + overlay::from_children( + &mut self.elements, + tree, + layout, + renderer, + viewport, + translation, + ) + } +} diff --git a/src/shell/element/stack/tabs.rs b/src/shell/element/stack/tabs.rs new file mode 100644 index 0000000..86218db --- /dev/null +++ b/src/shell/element/stack/tabs.rs @@ -0,0 +1,1040 @@ +use super::tab::{MIN_ACTIVE_TAB_WIDTH, Tab, TabBackgroundTheme, TabMessage, TabRuleTheme}; +use cosmic::{ + Apply, + iced::{ + Element, + core::{ + Background, Border, Clipboard, Color, Length, Point, Rectangle, Renderer, Shell, Size, + Vector, event, + layout::{Layout, Limits, Node}, + mouse, overlay, renderer, + widget::{ + Widget, + operation::{ + Operation, Scrollable, + scrollable::{AbsoluteOffset, RelativeOffset}, + }, + tree::{self, Tree}, + }, + }, + id::Id, + widget::{ + self, + container::{Catalog, draw_background}, + }, + }, + theme, + widget::icon::from_name, +}; +use keyframe::{ + ease, + functions::{EaseInOutCubic, EaseOutCubic}, +}; +use std::{ + collections::{HashMap, HashSet, VecDeque}, + time::{Duration, Instant}, +}; + +pub struct Tabs<'a, Message> { + elements: Vec>, + id: Option, + height: Length, + width: Length, + group_focused: bool, + scroll_to: Option, +} + +#[derive(Debug, Clone, Copy)] +struct ScrollAnimationState { + start_time: Instant, + start: Offset, + end: Offset, + extra: Offset, +} + +#[derive(Debug, Clone)] +struct TabAnimationState { + previous_bounds: HashMap, + next_bounds: HashMap, + start_time: Instant, +} + +/// The local state of [`Tabs`]. +#[derive(Debug, Clone)] +pub struct State { + offset_x: Offset, + scroll_animation: Option, + scroll_to: Option, + last_state: Option>, + tab_animations: VecDeque, +} + +impl Scrollable for State { + fn snap_to(&mut self, offset: RelativeOffset>) { + self.offset_x = Offset::Relative(offset.x.unwrap_or(0.0).clamp(0.0, 1.0)); + } + + fn scroll_to(&mut self, offset: AbsoluteOffset>) { + let new_offset = Offset::Absolute(offset.x.unwrap_or(0.0).max(0.0)); + self.scroll_animation = Some(ScrollAnimationState { + start_time: Instant::now(), + start: self.offset_x, + end: new_offset, + extra: Offset::Absolute(0.), + }); + self.offset_x = new_offset; + } + + fn scroll_by( + &mut self, + offset: AbsoluteOffset, + _bounds: Rectangle, + _content_bounds: Rectangle, + ) { + self.scroll_animation = Some(ScrollAnimationState { + start_time: Instant::now(), + start: self.offset_x, + end: self.offset_x, + extra: Offset::Absolute(offset.x.max(0.0)), + }); + } +} + +impl Default for State { + fn default() -> Self { + State { + offset_x: Offset::Absolute(0.), + scroll_animation: None, + scroll_to: None, + last_state: None, + tab_animations: VecDeque::new(), + } + } +} + +#[derive(Debug, Clone, Copy)] +enum Offset { + Absolute(f32), + Relative(f32), +} + +impl Offset { + fn absolute(self, viewport: f32, content: f32) -> f32 { + match self { + Offset::Absolute(absolute) => absolute.min((content - viewport).max(0.0)), + Offset::Relative(percentage) => ((content - viewport) * percentage).max(0.0), + } + } +} + +const SCROLL_ANIMATION_DURATION: Duration = Duration::from_millis(200); +const TAB_ANIMATION_DURATION: Duration = Duration::from_millis(150); + +impl Tabs<'_, Message> +where + Message: TabMessage + 'static, +{ + pub fn new( + tabs: impl ExactSizeIterator>, + active: usize, + activated: bool, + group_focused: bool, + ) -> Self { + let tabs = tabs.into_iter().enumerate().map(|(i, tab)| { + let rule = if activated { + TabRuleTheme::ActiveActivated + } else { + TabRuleTheme::ActiveDeactivated + }; + + let tab = if i == active { + tab.rule_style(rule) + .background_style(if activated { + TabBackgroundTheme::ActiveActivated + } else { + TabBackgroundTheme::ActiveDeactivated + }) + .font(cosmic::font::semibold()) + .active() + } else if i.checked_sub(1) == Some(active) { + tab.rule_style(rule).non_active() + } else { + tab.non_active() + }; + + Element::new(tab.internal(i)) + }); + + let tabs_rule = widget::rule::vertical(4).class(if tabs.len() - 1 == active { + if activated { + TabRuleTheme::ActiveActivated + } else { + TabRuleTheme::ActiveDeactivated + } + } else { + TabRuleTheme::Default + }); + + let rule_style = if group_focused { + TabRuleTheme::ActiveActivated + } else { + TabRuleTheme::Default + }; + + let prev_button = from_name("go-previous-symbolic") + .size(16) + .prefer_svg(true) + .icon() + .apply(widget::button) + .class(theme::iced::Button::Text) + .on_press(Message::scroll_back()); + + let next_button = from_name("go-next-symbolic") + .size(16) + .prefer_svg(true) + .icon() + .apply(widget::button) + .class(theme::iced::Button::Text) + .on_press(Message::scroll_further()); + + let mut elements = Vec::with_capacity(tabs.len() + 5); + + elements.push(widget::rule::vertical(4).class(rule_style).into()); + elements.push(prev_button.into()); + elements.extend(tabs); + elements.push(tabs_rule.into()); + elements.push(next_button.into()); + elements.push(widget::rule::vertical(4).class(rule_style).into()); + + Tabs { + elements, + id: None, + width: Length::Fill, + height: Length::Shrink, + group_focused, + scroll_to: None, + } + } + + pub fn id(mut self, id: impl Into) -> Self { + self.id = Some(id.into()); + self + } + + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + pub fn force_visible(mut self, idx: Option) -> Self { + self.scroll_to = idx; + self + } +} + +impl State { + fn next_tab_animation(&self) -> Option<&TabAnimationState> { + let now = Instant::now(); + + self.tab_animations + .iter() + .find(|anim| now.duration_since(anim.start_time) <= TAB_ANIMATION_DURATION) + } + + pub fn offset(&self, bounds: Rectangle, content_bounds: Size) -> Vector { + if let Some(animation) = self.scroll_animation { + let percentage = { + let percentage = Instant::now() + .duration_since(animation.start_time) + .as_millis() as f32 + / SCROLL_ANIMATION_DURATION.as_millis() as f32; + + ease(EaseInOutCubic, 0.0, 1.0, percentage) + }; + + Vector::new( + animation.start.absolute(bounds.width, content_bounds.width) + + animation.extra.absolute(bounds.width, content_bounds.width) * percentage + + (animation.end.absolute(bounds.width, content_bounds.width) + - animation.start.absolute(bounds.width, content_bounds.width)) + * percentage, + 0., + ) + } else { + Vector::new( + self.offset_x.absolute(bounds.width, content_bounds.width), + 0., + ) + } + } + + pub fn cleanup_old_animations(&mut self) { + let start_time = Instant::now(); + + if let Some(animation) = self.scroll_animation.as_ref() + && start_time.duration_since(animation.start_time) > SCROLL_ANIMATION_DURATION + { + self.scroll_animation.take(); + } + + Self::discard_expired_tab_animations(&mut self.tab_animations, start_time); + } + + /// Remove expired tab animations from the queue. + fn discard_expired_tab_animations( + tab_animations: &mut VecDeque, + start_time: Instant, + ) { + if let Some(mut animation) = tab_animations.pop_front() { + let mut set_next_start = false; + + while start_time.duration_since(animation.start_time) > TAB_ANIMATION_DURATION { + set_next_start = true; + + if let Some(next) = tab_animations.pop_front() { + animation = next; + continue; + } + + return; + } + + if set_next_start { + animation.start_time = start_time; + } + + tab_animations.push_front(animation); + } + } +} + +impl Widget for Tabs<'_, Message> +where + Message: TabMessage, +{ + fn size(&self) -> Size { + Size::new(self.width, self.height) + } + + fn id(&self) -> Option { + self.id.clone() + } + + fn set_id(&mut self, id: Id) { + self.id = Some(id); + } + + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::Some(Box::::default()) + } + + fn children(&self) -> Vec { + self.elements.iter().map(Tree::new).collect() + } + + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(&mut self.elements); + } + + #[allow(clippy::too_many_lines)] + fn layout(&mut self, tree: &mut Tree, renderer: &cosmic::Renderer, limits: &Limits) -> Node { + let limits = limits.width(self.width).height(self.height); + let element_count = self.elements.len(); + // calculate the smallest possible size + let child_limits = Limits::new( + Size::new(0.0, limits.min().height), + Size::new(f32::INFINITY, limits.max().height), + ) + .width(Length::Shrink) + .height(Length::Shrink); + + let mut nodes = self.elements[2..element_count - 2] + .iter_mut() + .zip(tree.children.iter_mut().skip(2)) + .map(|(tab, tab_tree)| { + tab.as_widget_mut() + .layout(tab_tree, renderer, &child_limits) + }) + .collect::>(); + + // sum up + let min_size = nodes + .iter() + .map(Node::size) + .fold(Size::new(0., 0.), |a, b| Size { + width: a.width + b.width, + height: a.height.max(b.height), + }); + let size = limits.resolve(self.width, self.height, min_size); + + if min_size.width <= size.width { + // we don't need to scroll + + let element_count = self.elements.len(); + // can we make every tab equal weight and keep the active large enough? + let children = if (size.width / (element_count as f32 - 5.)).ceil() as i32 + >= MIN_ACTIVE_TAB_WIDTH + { + // just use a flex layout + cosmic::iced::core::layout::flex::resolve( + cosmic::iced::core::layout::flex::Axis::Horizontal, + renderer, + &limits, + self.width, + self.height, + 0.into(), + 0., + cosmic::iced::Alignment::Center, + &mut self.elements[2..element_count - 2], + &mut tree.children[2..element_count - 2], + ) + .children() + .to_vec() + } else { + // otherwise we need a more manual approach + let min_width = + (size.width - MIN_ACTIVE_TAB_WIDTH as f32 - 4.) / (element_count as f32 - 6.); + let mut offset = 0.; + + let mut nodes = self.elements[2..element_count - 3] + .iter_mut() + .zip(tree.children[2..element_count - 3].iter_mut()) + .map(|(tab, tab_tree)| { + let child_limits = Limits::new( + Size::new(min_width, limits.min().height), + Size::new(f32::INFINITY, limits.max().height), + ) + .width(Length::Shrink) + .height(Length::Shrink); + + let mut node = + tab.as_widget_mut() + .layout(tab_tree, renderer, &child_limits); + node = node.move_to(Point::new(offset, 0.)); + offset += node.bounds().width; + node + }) + .collect::>(); + nodes.push({ + let node = Node::new(Size::new(4., limits.max().height)); + node.move_to(Point::new(offset, 0.)) + }); + nodes + }; + + // and add placeholder nodes for the not rendered scroll-buttons/rules + Node::with_children( + size, + vec![ + Node::new(Size::new(0., 0.)), + Node::with_children(Size::new(0., 0.), vec![Node::new(Size::new(0., 0.))]), + ] + .into_iter() + .chain(children) + .chain(vec![ + Node::with_children(Size::new(0., 0.), vec![Node::new(Size::new(0., 0.))]), + Node::new(Size::new(0., 0.)), + ]) + .collect::>(), + ) + } else { + // we scroll, so use the computed min size, but add scroll buttons. + let mut offset = 30.; + for node in &mut nodes { + *node = node.clone().move_to(Point::new(offset, 0.)); + offset += node.bounds().width; + } + let last_position = Point::new(size.width - 34., 0.); + nodes.remove(nodes.len() - 1); + + Node::with_children( + size, + vec![Node::new(Size::new(4., size.height)), { + let mut node = Node::with_children( + Size::new(16., 16.), + vec![Node::new(Size::new(16., 16.))], + ); + node = node.move_to(Point::new(9., (size.height - 16.) / 2.)); + node + }] + .into_iter() + .chain(nodes) + .chain(vec![ + { + let mut node = Node::new(Size::new(4., size.height)); + node = node.move_to(last_position); + node + }, + { + let mut node = Node::with_children( + Size::new(16., 16.), + vec![Node::new(Size::new(16., 16.))], + ); + node = + node.move_to(last_position + Vector::new(9., (size.height - 16.) / 2.)); + node + }, + { + let mut node = Node::new(Size::new(4., size.height)); + node = node.move_to(last_position + Vector::new(30., 0.)); + node + }, + ]) + .collect(), + ) + } + } + + #[allow(clippy::too_many_lines)] + fn draw( + &self, + tree: &Tree, + renderer: &mut cosmic::Renderer, + theme: &cosmic::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + let state = tree.state.downcast_ref::(); + + let mut bounds = layout.bounds(); + let content_bounds = layout + .children() + .skip(2) + .take(self.elements.len() - 5) + .fold(Size::new(0., 0.), |a, b| Size { + width: a.width + b.bounds().width, + height: b.bounds().height, + }); + + let background_style = Catalog::style( + theme, + &theme::Container::custom(|_theme| widget::container::Style { + snap: true, + icon_color: None, + text_color: None, + background: Some(Background::Color(Color::TRANSPARENT)), + border: Border { + radius: 0.0.into(), + width: 0.0, + color: Color::TRANSPARENT, + }, + shadow: Default::default(), + }), + ); + draw_background(renderer, &background_style, bounds); + + let scrolling = content_bounds.width.floor() > bounds.width; + if scrolling { + bounds.width -= 64.; + bounds.x += 30.; + } + let offset = state.offset(bounds, content_bounds); + let offset_viewport = Rectangle { + x: bounds.x + offset.x, + y: bounds.y + offset.y, + ..bounds + }; + + if scrolling { + // we have scroll buttons + for ((scroll, state), layout) in self + .elements + .iter() + .take(2) + .zip(&tree.children) + .zip(layout.children()) + { + scroll + .as_widget() + .draw(state, renderer, theme, style, layout, cursor, viewport); + } + } + + renderer.with_layer(bounds, |renderer| { + renderer.with_translation(Vector::new(-offset.x, -offset.y), |renderer| { + let tab_animation = state.next_tab_animation(); + + let percentage = if let Some(animation) = tab_animation { + let percentage = Instant::now() + .duration_since(animation.start_time) + .as_millis() as f32 + / TAB_ANIMATION_DURATION.as_millis() as f32; + ease(EaseOutCubic, 0.0, 1.0, percentage) + } else { + 1.0 + }; + + for ((tab, wstate), layout) in self.elements[2..self.elements.len() - 3] + .iter() + .zip(tree.children.iter().skip(2)) + .zip(layout.children().skip(2)) + { + let bounds = if let Some(animation) = tab_animation { + let id = tab.as_widget().id().unwrap(); + let previous = + animation + .previous_bounds + .get(&id) + .copied() + .unwrap_or(Rectangle { + x: layout.position().x, + y: layout.position().y, + width: 0., + height: layout.bounds().height, + }); + let next = animation + .next_bounds + .get(&id) + .copied() + .unwrap_or(Rectangle { + x: layout.position().x, + y: layout.position().y, + width: 0., + height: layout.bounds().height, + }); + Rectangle { + x: previous.x + (next.x - previous.x) * percentage, + y: previous.y + (next.y - previous.y) * percentage, + width: previous.width + (next.width - previous.width) * percentage, + height: next.height, + } + } else { + layout.bounds() + }; + + let cursor = match cursor { + mouse::Cursor::Available(point) => mouse::Cursor::Available(point + offset), + mouse::Cursor::Unavailable => mouse::Cursor::Unavailable, + mouse::Cursor::Levitating(point) => { + mouse::Cursor::Levitating(point + offset) + } + }; + + renderer.with_layer(bounds, |renderer| { + renderer.with_translation( + Vector { + x: bounds.x - layout.position().x, + y: bounds.y - layout.position().y, + }, + |renderer| { + tab.as_widget().draw( + wstate, + renderer, + theme, + style, + layout, + cursor, + &offset_viewport, + ); + }, + ); + }); + } + }); + }); + self.elements[self.elements.len() - 3].as_widget().draw( + &tree.children[self.elements.len() - 3], + renderer, + theme, + style, + layout.children().nth(self.elements.len() - 3).unwrap(), + cursor, + viewport, + ); + + if !scrolling && self.group_focused { + // HACK, overdraw our rule at the edges + self.elements[0].as_widget().draw( + &tree.children[2].children[0], + renderer, + theme, + style, + layout.children().nth(2).unwrap().children().nth(0).unwrap(), + cursor, + viewport, + ); + self.elements[self.elements.len() - 1].as_widget().draw( + &tree.children[self.elements.len() - 3], + renderer, + theme, + style, + layout.children().nth(self.elements.len() - 3).unwrap(), + cursor, + viewport, + ); + } + + if scrolling { + // we have scroll buttons + for ((scroll, state), layout) in self.elements + [self.elements.len() - 2..self.elements.len()] + .iter() + .zip(tree.children.iter().skip(self.elements.len() - 2)) + .zip(layout.children().skip(self.elements.len() - 2)) + { + scroll + .as_widget() + .draw(state, renderer, theme, style, layout, cursor, viewport); + } + } + } + + fn operate( + &mut self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &cosmic::Renderer, + operation: &mut dyn Operation<()>, + ) { + let state = tree.state.downcast_mut::(); + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + let content_bounds = content_layout.bounds(); + + state.cleanup_old_animations(); + + operation.scrollable( + self.id.as_ref(), + bounds, + content_bounds, + Vector { x: 0.0, y: 0.0 }, + state, + ); + let element_count = self.elements.len(); + operation.traverse(&mut |operation| { + self.elements[2..element_count - 3] + .iter_mut() + .zip(tree.children.iter_mut().skip(2)) + .zip(layout.children().skip(2)) + .for_each(|((child, state), layout)| { + child + .as_widget_mut() + .operate(state, layout, renderer, operation); + }); + }); + } + + #[allow(clippy::too_many_lines)] + fn update( + &mut self, + tree: &mut Tree, + event: &event::Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &cosmic::Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) { + let state = tree.state.downcast_mut::(); + state.cleanup_old_animations(); + + let mut bounds = layout.bounds(); + let content_bounds = layout.children().fold(Size::new(0., 0.), |a, b| Size { + width: a.width + b.bounds().width, + height: b.bounds().height, + }); + let scrolling = content_bounds.width.floor() > bounds.width; + + let current_state = self.elements[2..self.elements.len() - 3] + .iter() + .zip(layout.children().skip(2)) + .map(|(element, layout)| (element.as_widget().id().unwrap(), layout.bounds())) + .collect::>(); + + if state.last_state.is_none() { + state.last_state = Some(current_state.clone()); + } + let last_state = state.last_state.as_mut().unwrap(); + let unknown_keys = current_state + .keys() + .collect::>() + .symmetric_difference(&last_state.keys().collect::>()) + .next() + .is_some(); + + enum Difference { + NewOrRemoved, + Movement, + Focus, + } + + let changes = if unknown_keys { + Some(Difference::NewOrRemoved) + } else { + current_state + .iter() + .filter_map(|(a_id, a_bounds)| { + let Some(b_bounds) = last_state.get(a_id) else { + return Some(Difference::Movement); + }; + (a_bounds != b_bounds).then(|| { + if a_bounds.position() == b_bounds.position() { + Difference::Focus + } else { + Difference::Movement + } + }) + }) + .fold(None, |a, b| match (a, b) { + (None | Some(Difference::Movement), x) => Some(x), + (a, _) => a, + }) + }; + + if unknown_keys || changes.is_some() { + if !scrolling || !matches!(changes, Some(Difference::Focus)) { + let start_time = Instant::now(); + + State::discard_expired_tab_animations(&mut state.tab_animations, start_time); + + // new tab_animation + state.tab_animations.push_back(TabAnimationState { + previous_bounds: last_state.clone(), + next_bounds: current_state.clone(), + start_time, + }); + } + + // update last_state + *last_state = current_state; + } + + if scrolling { + bounds.x += 30.; + bounds.width -= 64.; + } + let offset = state.offset(bounds, content_bounds); + + if let Some(idx) = self.scroll_to { + state.scroll_to = Some(idx); + } + if let Some(idx) = state.scroll_to.take() { + if scrolling { + let tab_bounds = layout.children().nth(idx + 2).unwrap().bounds(); + let left_offset = tab_bounds.x - layout.bounds().x - 30.; + let right_offset = left_offset + tab_bounds.width + 4.; + let scroll_width = bounds.width; + let current_start = offset.x; + let current_end = current_start + scroll_width; + + //assert!((right_offset - left_offset) <= (current_end - current_start)); + if (left_offset - current_start).is_sign_negative() + || (current_end - right_offset).is_sign_negative() + { + let new_offset = if (left_offset - current_start).abs() + < (right_offset - current_end).abs() + { + AbsoluteOffset { + x: left_offset, + y: 0., + } + } else { + AbsoluteOffset { + x: right_offset - scroll_width, + y: 0., + } + }; + + state.scroll_animation = Some(ScrollAnimationState { + start_time: Instant::now(), + start: Offset::Absolute(offset.x), + end: Offset::Absolute(new_offset.x), + extra: Offset::Absolute(0.), + }); + state.offset_x = Offset::Absolute(new_offset.x); + } + } + shell.publish(Message::scrolled()); + } + + let mut messages = Vec::new(); + let mut internal_shell = Shell::new(&mut messages); + + let len = self.elements.len(); + if scrolling && cursor.position().is_some_and(|pos| pos.x < bounds.x) { + for ((child, state), layout) in self.elements[0..2] + .iter_mut() + .zip(&mut tree.children) + .zip(layout.children()) + { + child.as_widget_mut().update( + state, + event, + layout, + cursor, + renderer, + clipboard, + &mut internal_shell, + viewport, + ); + } + } else if scrolling + && cursor + .position() + .is_some_and(|pos| pos.x >= bounds.x + bounds.width) + { + for ((child, state), layout) in self.elements[len - 3..len] + .iter_mut() + .zip(tree.children.iter_mut().skip(len - 3)) + .zip(layout.children().skip(len - 3)) + { + child.as_widget_mut().update( + state, + event, + layout, + cursor, + renderer, + clipboard, + &mut internal_shell, + viewport, + ) + } + } else { + for ((child, state), layout) in self.elements[2..len - 3] + .iter_mut() + .zip(tree.children.iter_mut().skip(2)) + .zip(layout.children().skip(2)) + { + let cursor = match cursor { + mouse::Cursor::Available(point) => mouse::Cursor::Available(point + offset), + mouse::Cursor::Unavailable => mouse::Cursor::Unavailable, + mouse::Cursor::Levitating(point) => mouse::Cursor::Levitating(point + offset), + }; + + child.as_widget_mut().update( + state, + event, + layout, + cursor, + renderer, + clipboard, + &mut internal_shell, + viewport, + ) + } + }; + + for mut message in messages { + if let Some(offset) = message.populate_scroll(AbsoluteOffset { + x: state.offset_x.absolute(bounds.width, content_bounds.width), + y: 0., + }) { + state.scroll_to(offset.into()); + continue; + } + + shell.publish(message); + } + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &cosmic::Renderer, + ) -> mouse::Interaction { + let state = tree.state.downcast_ref::(); + + let mut bounds = layout.bounds(); + let content_bounds = layout.children().fold(Size::new(0., 0.), |a, b| Size { + width: a.width + b.bounds().width, + height: b.bounds().height, + }); + let scrolling = content_bounds.width.floor() > bounds.width; + + if scrolling { + bounds.width -= 64.; + bounds.x += 30.; + } + let offset = state.offset(bounds, content_bounds); + let offset_viewport = &Rectangle { + y: bounds.y + offset.y, + x: bounds.x + offset.x, + ..bounds + }; + + if scrolling && cursor.position().is_some_and(|pos| pos.x < bounds.x) { + self.elements[0..2] + .iter() + .zip(&tree.children) + .zip(layout.children()) + .map(|((child, state), layout)| { + child + .as_widget() + .mouse_interaction(state, layout, cursor, viewport, renderer) + }) + .max() + } else if scrolling + && cursor + .position() + .is_some_and(|pos| pos.x >= bounds.x + bounds.width) + { + self.elements[self.elements.len() - 3..self.elements.len()] + .iter() + .zip(tree.children.iter().skip(self.elements.len() - 3)) + .zip(layout.children().skip(self.elements.len() - 3)) + .map(|((child, state), layout)| { + child + .as_widget() + .mouse_interaction(state, layout, cursor, viewport, renderer) + }) + .max() + } else { + self.elements[2..self.elements.len() - 3] + .iter() + .zip(tree.children.iter().skip(2)) + .zip(layout.children().skip(2)) + .map(|((child, state), layout)| { + let cursor = match cursor { + mouse::Cursor::Available(point) => mouse::Cursor::Available(point + offset), + mouse::Cursor::Unavailable => mouse::Cursor::Unavailable, + mouse::Cursor::Levitating(point) => { + mouse::Cursor::Levitating(point + offset) + } + }; + + child.as_widget().mouse_interaction( + state, + layout, + cursor, + offset_viewport, + renderer, + ) + }) + .max() + } + .unwrap_or_default() + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'b>, + renderer: &cosmic::Renderer, + viewport: &Rectangle, + translation: cosmic::iced::Vector, + ) -> Option> { + overlay::from_children( + &mut self.elements, + tree, + layout, + renderer, + viewport, + translation, + ) + } +} diff --git a/src/shell/element/stack_hover.rs b/src/shell/element/stack_hover.rs new file mode 100644 index 0000000..f123840 --- /dev/null +++ b/src/shell/element/stack_hover.rs @@ -0,0 +1,133 @@ +use crate::{ + backend::render::element::AsGlowRenderer, + fl, + utils::iced::{IcedElement, IcedRenderElement, Program}, +}; + +use calloop::LoopHandle; +use cosmic::{ + Apply, + iced::{ + Alignment, + core::{Background, Border, Color, Length}, + widget::{container, row}, + }, + theme, + widget::{icon::from_name, space, text}, +}; +use smithay::{ + backend::renderer::ImportMem, + desktop::space::SpaceElement, + output::Output, + utils::{Logical, Physical, Point, Rectangle, Scale, Size}, +}; + +#[derive(Debug)] +pub struct StackHover { + location: Point, + elem: IcedElement, +} + +impl StackHover { + pub fn new( + evlh: LoopHandle<'static, crate::state::State>, + size: Size, + mut theme: cosmic::Theme, + ) -> StackHover { + theme.transparent = theme.cosmic().frosted_system_interface; + + let elem = IcedElement::new(StackHoverInternal, size, evlh, theme); + let minimum = elem.minimum_size(); + let new_size = Size::::new(size.w.min(minimum.w), size.h.min(minimum.h)); + let location = Point::new( + size.w.saturating_sub(new_size.w) / 2, + size.h.saturating_sub(new_size.h) / 2, + ); + elem.resize(new_size); + + StackHover { location, elem } + } + + pub fn push_render_elements( + &self, + renderer: &mut R, + location: Point, + scale: Scale, + alpha: f32, + push_above: &mut dyn FnMut(IcedRenderElement), + push_below: Option<&mut dyn FnMut(IcedRenderElement)>, + ) where + R: AsGlowRenderer + ImportMem, + R::TextureId: Send + Clone + 'static, + { + self.elem.push_render_elements( + renderer, + location + self.location.to_physical_precise_round(scale), + scale, + alpha, + self.elem + .with_theme(|theme| theme.cosmic().radius_s()) + .map(|x| x.round() as u8), + push_above, + push_below, + ); + } + + pub fn output_enter(&self, output: &Output) { + self.elem + .output_enter(output, Rectangle::default() /*unused*/); + } + + pub fn output_leave(&self, output: &Output) { + self.elem.output_leave(output); + } +} + +pub struct StackHoverInternal; + +impl Program for StackHoverInternal { + type Message = (); + + fn view(&self) -> cosmic::Element<'_, Self::Message> { + row(vec![ + from_name("window-stack-symbolic") + .size(32) + .prefer_svg(true) + .icon() + .into(), + space::horizontal().width(16).into(), + text::title3(fl!("stack-windows")).into(), + ]) + .align_y(Alignment::Center) + .apply(container) + .align_x(Alignment::Center) + .align_y(Alignment::Center) + .padding(16) + .apply(container) + .class(theme::Container::custom(|theme| { + let mut background = theme.cosmic().accent_color(); + if theme.transparent { + background.alpha = theme + .cosmic() + .alpha_map + .blurred_alpha(theme.cosmic().frosted); + } + + container::Style { + snap: true, + icon_color: Some(Color::from(theme.cosmic().accent.on)), + text_color: Some(Color::from(theme.cosmic().accent.on)), + background: Some(Background::Color(background.into())), + border: Border { + radius: theme.cosmic().radius_s().into(), + width: 0.0, + color: Color::TRANSPARENT, + }, + shadow: Default::default(), + } + })) + .width(Length::Shrink) + .height(Length::Shrink) + .into() + } +} diff --git a/src/shell/element/surface.rs b/src/shell/element/surface.rs new file mode 100644 index 0000000..e146efe --- /dev/null +++ b/src/shell/element/surface.rs @@ -0,0 +1,1142 @@ +use crate::{ + backend::render::{ + element::AsGlowRenderer, + wayland::{SurfaceRenderElement, push_render_elements_from_surface_tree}, + }, + shell::focus::target::PointerFocusTarget, + wayland::handlers::{ + background_effect::ComputedBlurRegionCachedState, compositor::frame_time_filter_fn, + corner_radius::surface_corners, + }, +}; +use std::{ + borrow::Cow, + sync::{ + Mutex, + atomic::{AtomicBool, Ordering}, + }, + time::Duration, +}; + +use smithay::{ + backend::{ + drm::DrmNode, + renderer::{ + ImportAll, Renderer, buffer_has_alpha, + element::{Kind, RenderElementStates, surface::KindEvaluation}, + utils::RendererSurfaceStateUserData, + }, + }, + desktop::{ + PopupManager, WeakWindow, Window, WindowSurface, WindowSurfaceType, space::SpaceElement, + utils::OutputPresentationFeedback, + }, + input::{ + Seat, + keyboard::{KeyboardTarget, KeysymHandle, ModifiersState}, + }, + output::Output, + reexports::{ + wayland_protocols::{ + wp::presentation_time::server::wp_presentation_feedback::Kind as PresentationKind, + xdg::{ + decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode, + shell::server::xdg_toplevel::State as ToplevelState, + }, + }, + wayland_protocols_misc::server_decoration::server::org_kde_kwin_server_decoration::Mode as KdeMode, + wayland_server::protocol::wl_surface::WlSurface, + }, + utils::{ + IsAlive, Logical, Physical, Point, Rectangle, Scale, Serial, Size, user_data::UserDataMap, + }, + wayland::{ + alpha_modifier::AlphaModifierSurfaceCachedState, + compositor::{ + SubsurfaceCachedState, SurfaceData, TraversalAction, get_parent, with_states, + with_surface_tree_downward, + }, + dmabuf::get_dmabuf, + seat::WaylandFocus, + shell::xdg::{ + SurfaceCachedState, ToplevelCachedState, ToplevelSurface, XdgToplevelSurfaceData, + }, + }, + xwayland::{X11Surface, xwm::X11Relatable}, +}; +use tracing::trace; + +use crate::{ + state::{State, SurfaceDmabufFeedback}, + utils::prelude::*, + wayland::handlers::{ + compositor::FRAME_TIME_FILTER, + decoration::{KdeDecorationData, PreferredDecorationMode}, + }, +}; + +/// The [`DrmNode`] the surface's currently committed buffer was allocated on, if it is a dmabuf. +fn buffer_node(data: &SurfaceData) -> Option { + let surface_state = data.data_map.get::()?; + let surface_state = surface_state.lock().unwrap(); + surface_state + .buffer() + .and_then(|buffer| get_dmabuf(buffer).ok()) + .and_then(|dmabuf| dmabuf.node()) +} + +fn is_likely_translucent(alpha: f32, data: &SurfaceData) -> bool { + if alpha < 1.0 { + return true; + } + + let mut alpha_modifier_state = data.cached_state.get::(); + let alpha_multiplier = alpha_modifier_state + .current() + .multiplier_f32() + .unwrap_or(1.0); + if alpha_multiplier < 1.0 { + return true; + } + + let Some(surface_state) = data.data_map.get::() else { + return false; + }; + let surface_state = surface_state.lock().unwrap(); + if surface_state + .buffer() + .is_none_or(|buffer| !buffer_has_alpha(buffer).unwrap_or(true)) + { + return false; + } + + let mut blur_state = data.cached_state.get::(); + blur_state + .current() + .blur_region + .as_ref() + .is_some_and(|region| !region.is_empty()) +} + +/// Build the [`KindEvaluation`] for a window's surface tree. +/// +/// `scanout_node`, when set, is the scan-out target [`DrmNode`] of the output currently being +/// rendered: only buffers allocated on that node may be promoted to overlay scan-out candidates. +/// It is `None` for render passes that never scan out to a plane (e.g. screen-copy). +fn scanout_kind_eval( + scanout_override: Option, + scanout_node: Option, + alpha: f32, +) -> KindEvaluation { + match (scanout_override, scanout_node) { + // Forced off. + (Some(false), _) => Kind::Unspecified.into(), + // No node restriction: preserve the previous behaviour exactly. + (Some(true), None) => Kind::ScanoutCandidate.into(), + (None, None) => FRAME_TIME_FILTER, + // Node restriction in effect: only buffers on the scan-out node may be candidates. + (Some(true), Some(node)) => KindEvaluation::Closure(Box::new(move |data| { + if buffer_node(data) == Some(node) && !is_likely_translucent(alpha, data) { + Kind::ScanoutCandidate + } else { + Kind::Unspecified + } + })), + (None, Some(node)) => KindEvaluation::Closure(Box::new(move |data| { + if buffer_node(data) == Some(node) && !is_likely_translucent(alpha, data) { + frame_time_filter_fn(data) + } else { + Kind::Unspecified + } + })), + } +} + +#[derive(Debug, Clone, PartialEq, Hash, Eq)] +pub struct CosmicSurface(pub Window); + +#[derive(Debug, Clone)] +pub struct WeakCosmicSurface(pub WeakWindow); + +impl From for CosmicSurface { + fn from(s: ToplevelSurface) -> Self { + CosmicSurface(Window::new_wayland_window(s)) + } +} + +impl From for CosmicSurface { + fn from(w: Window) -> Self { + CosmicSurface(w) + } +} + +impl From for CosmicSurface { + fn from(s: X11Surface) -> Self { + CosmicSurface(Window::new_x11_window(s)) + } +} + +impl PartialEq for CosmicSurface { + fn eq(&self, other: &WlSurface) -> bool { + self.wl_surface().is_some_and(|s| &*s == other) + } +} + +impl PartialEq for CosmicSurface { + fn eq(&self, other: &ToplevelSurface) -> bool { + self.wl_surface().is_some_and(|s| &*s == other.wl_surface()) + } +} + +impl PartialEq for CosmicSurface { + fn eq(&self, other: &X11Surface) -> bool { + self.x11_surface() == Some(other) + } +} + +impl PartialEq for CosmicSurface { + fn eq(&self, other: &WeakCosmicSurface) -> bool { + other.upgrade().is_some_and(|other| other == *self) + } +} + +#[derive(Default)] +struct Minimized(AtomicBool); + +#[derive(Default)] +struct Sticky(AtomicBool); + +#[derive(Default)] +struct GlobalGeometry(Mutex>>); + +impl CosmicSurface { + pub fn title(&self) -> String { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => with_states(toplevel.wl_surface(), |states| { + states + .data_map + .get::() + .unwrap() + .lock() + .unwrap() + .title + .clone() + .unwrap_or_default() + }), + WindowSurface::X11(surface) => surface.title().replace('\0', ""), + } + } + + pub fn corner_radius(&self, geometry_size: Size) -> Option<[u8; 4]> { + self.wl_surface().and_then(|surface| { + with_states(&surface, |states| surface_corners(states, geometry_size)) + }) + } + + pub fn app_id(&self) -> String { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => with_states(toplevel.wl_surface(), |states| { + states + .data_map + .get::() + .unwrap() + .lock() + .unwrap() + .app_id + .clone() + .unwrap_or_default() + }), + WindowSurface::X11(surface) => surface.class().replace('\0', ""), + } + } + + pub fn pending_size(&self) -> Option> { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => toplevel.with_pending_state(|state| state.size), + WindowSurface::X11(surface) => Some(surface.geometry().size), + } + } + + pub fn has_pending_changes(&self) -> bool { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => toplevel.has_pending_changes(), + WindowSurface::X11(_surface) => false, + } + } + + pub fn last_server_size(&self) -> Option> { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => with_states(toplevel.wl_surface(), |states| { + let attributes = states + .data_map + .get::() + .unwrap() + .lock() + .unwrap(); + attributes.current_server_state().size + }), + WindowSurface::X11(_) => None, + } + } + + pub fn global_geometry(&self) -> Option> { + *self + .0 + .user_data() + .get_or_insert_threadsafe(GlobalGeometry::default) + .0 + .lock() + .unwrap() + } + + pub fn set_geometry(&self, geo: Rectangle, ssd_height: u32) { + { + let mut geo = geo; + geo.size.h += ssd_height as i32; + geo.loc.y -= ssd_height as i32; + + *self + .0 + .user_data() + .get_or_insert_threadsafe(GlobalGeometry::default) + .0 + .lock() + .unwrap() = Some(geo); + } + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => { + toplevel.with_pending_state(|state| state.size = Some(geo.size.as_logical())) + } + WindowSurface::X11(surface) => { + let _ = + surface.configure_with_sync(geo.as_logical() + surface.frame_extents(), None); + } + } + } + + pub fn set_bounds(&self, size: impl Into>>) { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => { + toplevel.with_pending_state(|state| state.bounds = size.into()) + } + WindowSurface::X11(_surface) => {} + } + } + + pub fn is_activated(&self, pending: bool) -> bool { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => with_toplevel_state(toplevel, pending, |state| { + state.is_some_and(|state| state.states.contains(ToplevelState::Activated)) + }), + WindowSurface::X11(surface) => surface.is_activated(), + } + } + + pub fn set_activated(&self, activated: bool) { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => toplevel.with_pending_state(|state| { + if activated { + state.states.set(ToplevelState::Activated); + } else { + state.states.unset(ToplevelState::Activated); + } + }), + WindowSurface::X11(surface) => { + let _ = surface.set_activated(activated); + } + } + } + + pub fn is_decorated(&self, pending: bool) -> bool { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => { + let kde_state = with_states(toplevel.wl_surface(), |states| { + states + .data_map + .get::() + .and_then(|data| data.lock().unwrap().mode.map(|m| m != KdeMode::Server)) + }); + + let xdg_state = with_toplevel_state(toplevel, pending, |state| { + state.and_then(|state| { + state + .decoration_mode + .map(|mode| mode == DecorationMode::ClientSide) + }) + }); + + kde_state.or(xdg_state).unwrap_or(true) + } + WindowSurface::X11(surface) => surface.is_decorated(), + } + } + + pub fn try_force_undecorated(&self, enable: bool) { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => { + if enable { + let previous_decoration_state = toplevel.with_committed_state(|state| { + state.map_or_else(Default::default, |state| state.decoration_mode) + }); + if PreferredDecorationMode::is_unset(&self.0) { + PreferredDecorationMode::update(&self.0, previous_decoration_state); + } + toplevel.with_pending_state(|pending| { + pending.decoration_mode = Some(DecorationMode::ServerSide); + }); + with_states(toplevel.wl_surface(), |data| { + if let Some(kde_data) = data.data_map.get::() { + for obj in kde_data.lock().unwrap().objs.iter() { + obj.mode(KdeMode::Server); + } + } + }) + } else { + let previous_mode = PreferredDecorationMode::mode(&self.0); + toplevel.with_pending_state(|pending| { + pending.decoration_mode = previous_mode; + }); + with_states(toplevel.wl_surface(), |data| { + if let Some(kde_data) = data.data_map.get::() { + for obj in kde_data.lock().unwrap().objs.iter() { + obj.mode(KdeMode::Server); + } + } + }) + } + } + WindowSurface::X11(_surface) => {} + } + } + + pub fn is_resizing(&self, pending: bool) -> Option { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => { + Some(with_toplevel_state(toplevel, pending, |state| { + state.is_some_and(|state| state.states.contains(ToplevelState::Resizing)) + })) + } + WindowSurface::X11(surface) => surface.pending_configure().map(|_| true), + } + } + + pub fn set_resizing(&self, resizing: bool) { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => toplevel.with_pending_state(|state| { + if resizing { + state.states.set(ToplevelState::Resizing); + } else { + state.states.unset(ToplevelState::Resizing); + } + }), + WindowSurface::X11(_surface) => {} + } + } + + pub fn is_tiled(&self, pending: bool) -> Option { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => { + Some(with_toplevel_state(toplevel, pending, |state| { + state.is_some_and(|state| state.states.contains(ToplevelState::TiledLeft)) + })) + } + WindowSurface::X11(_surface) => None, + } + } + + pub fn set_tiled(&self, tiled: bool) { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => toplevel.with_pending_state(|state| { + if tiled { + state.states.set(ToplevelState::TiledLeft); + state.states.set(ToplevelState::TiledRight); + state.states.set(ToplevelState::TiledTop); + state.states.set(ToplevelState::TiledBottom); + } else { + state.states.unset(ToplevelState::TiledLeft); + state.states.unset(ToplevelState::TiledRight); + state.states.unset(ToplevelState::TiledTop); + state.states.unset(ToplevelState::TiledBottom); + } + }), + WindowSurface::X11(_surface) => {} + } + } + + pub fn is_fullscreen(&self, pending: bool) -> bool { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => with_toplevel_state(toplevel, pending, |state| { + state.is_some_and(|state| state.states.contains(ToplevelState::Fullscreen)) + }), + WindowSurface::X11(surface) => surface.is_fullscreen(), + } + } + + pub fn set_fullscreen(&self, fullscreen: bool) { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => toplevel.with_pending_state(|state| { + if fullscreen { + state.states.set(ToplevelState::Fullscreen); + } else { + state.states.unset(ToplevelState::Fullscreen); + } + }), + WindowSurface::X11(surface) => { + let _ = surface.set_fullscreen(fullscreen); + } + } + } + + pub fn is_maximized(&self, pending: bool) -> bool { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => with_toplevel_state(toplevel, pending, |state| { + state.is_some_and(|state| state.states.contains(ToplevelState::Maximized)) + }), + WindowSurface::X11(surface) => surface.is_maximized(), + } + } + + pub fn set_maximized(&self, maximized: bool) { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => toplevel.with_pending_state(|state| { + if maximized { + state.states.set(ToplevelState::Maximized); + } else { + state.states.unset(ToplevelState::Maximized); + } + }), + WindowSurface::X11(surface) => { + let _ = surface.set_maximized(maximized); + } + } + } + + pub fn is_minimized(&self) -> bool { + self.0 + .user_data() + .get_or_insert_threadsafe(Minimized::default) + .0 + .load(Ordering::SeqCst) + } + + pub fn set_minimized(&self, minimized: bool) { + self.0 + .user_data() + .get_or_insert_threadsafe(Minimized::default) + .0 + .store(minimized, Ordering::SeqCst); + if let WindowSurface::X11(surface) = self.0.underlying_surface() { + let _ = surface.set_hidden(minimized); + } + } + + pub fn is_sticky(&self) -> bool { + self.0 + .user_data() + .get_or_insert_threadsafe(Sticky::default) + .0 + .load(Ordering::SeqCst) + } + + pub fn set_sticky(&self, sticky: bool) { + self.0 + .user_data() + .get_or_insert_threadsafe(Sticky::default) + .0 + .store(sticky, Ordering::SeqCst); + if let WindowSurface::X11(surface) = self.0.underlying_surface() { + let _ = surface.set_sticky(sticky); + } + } + + pub fn set_suspended(&self, suspended: bool) { + if let WindowSurface::Wayland(window) = self.0.underlying_surface() { + window.with_pending_state(|state| { + if suspended { + state.states.set(ToplevelState::Suspended); + } else { + state.states.unset(ToplevelState::Suspended); + } + }); + } + } + + pub fn min_size_without_ssd(&self) -> Option> { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => { + Some(with_states(toplevel.wl_surface(), |states| { + states + .cached_state + .get::() + .current() + .min_size + })) + .filter(|size| !(size.w == 0 && size.h == 0)) + } + WindowSurface::X11(surface) => surface.min_size(), + } + } + + pub fn max_size_without_ssd(&self) -> Option> { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => { + Some(with_states(toplevel.wl_surface(), |states| { + states + .cached_state + .get::() + .current() + .max_size + })) + .filter(|size| !(size.w == 0 && size.h == 0)) + } + WindowSurface::X11(surface) => surface.max_size(), + } + } + + pub fn serial_acked(&self, serial: &Serial) -> bool { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => with_states(toplevel.wl_surface(), |states| { + let attrs = states + .data_map + .get::() + .unwrap() + .lock() + .unwrap(); + attrs + .last_acked + .as_ref() + .is_some_and(|configure| configure.serial >= *serial) + }), + WindowSurface::X11(_surface) => true, + } + } + + pub fn serial_past(&self, serial: &Serial) -> bool { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => with_states(toplevel.wl_surface(), |states| { + let mut guard = states.cached_state.get::(); + guard + .current() + .last_acked + .as_ref() + .is_some_and(|configure| configure.serial >= *serial) + }), + WindowSurface::X11(_surface) => true, + } + } + + pub fn latest_size_committed(&self) -> bool { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => { + with_states(toplevel.wl_surface(), |states| { + let attributes = states + .data_map + .get::() + .unwrap() + .lock() + .unwrap(); + + let current_server = attributes.current_server_state(); + let mut guard = states.cached_state.get::(); + if guard + .current() + .last_acked + .as_ref() + .is_some_and(|configure| configure.state.size == current_server.size) + { + // The window had committed for our previous size change, so we can + // change the size again. + trace!( + "current size matches server size: {:?}", + guard.current().last_acked.as_ref().unwrap().state.size + ); + true + } else { + // The window had not committed for our previous size change yet. + // This throttling is done because some clients do not batch size requests, + // leading to bad behavior with very fast input devices (i.e. a 1000 Hz + // mouse). This throttling also helps interactive resize transactions + // preserve visual consistency. + trace!("throttling resize"); + false + } + }) + } + WindowSurface::X11(surface) => surface.pending_configure().is_none(), + } + } + + pub fn force_configure(&self) -> Option { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => Some(toplevel.send_configure()), + WindowSurface::X11(surface) => { + let _ = surface.configure(None); + None + } + } + } + + pub fn send_configure(&self) -> Option { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => toplevel.send_pending_configure(), + WindowSurface::X11(_) => None, + } + } + + pub fn close(&self) { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => toplevel.send_close(), + WindowSurface::X11(surface) => { + let _ = surface.close(); + } + } + } + + pub fn has_surface(&self, surface: &WlSurface, surface_type: WindowSurfaceType) -> bool { + let Some(toplevel) = self.wl_surface() else { + return false; + }; + + if surface_type.contains(WindowSurfaceType::TOPLEVEL) && *toplevel == *surface { + return true; + } + + if surface_type.contains(WindowSurfaceType::SUBSURFACE) { + use std::sync::atomic::Ordering; + + let found = AtomicBool::new(false); + with_surface_tree_downward( + &toplevel, + surface, + |_, _, search| TraversalAction::DoChildren(search), + |s, _, search| { + found.fetch_or(s == *search, Ordering::SeqCst); + }, + |_, _, _| !found.load(Ordering::SeqCst), + ); + if found.load(Ordering::SeqCst) { + return true; + } + } + + if surface_type.contains(WindowSurfaceType::POPUP) { + PopupManager::popups_for_surface(&toplevel).any(|(p, _)| p.wl_surface() == surface) + } else { + false + } + } + + pub fn surface_offset(&self, surface: &WlSurface) -> Option> { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => { + Self::surface_tree_offset(toplevel.wl_surface(), surface) + } + WindowSurface::X11(surface_x11) => { + if surface_x11.wl_surface().as_ref() == Some(surface) { + Some(Point::default()) + } else { + None + } + } + } + } + + pub fn surface_tree_offset( + root: &WlSurface, + surface: &WlSurface, + ) -> Option> { + let mut offset = Point::::default(); + let mut parent = surface.clone(); + loop { + if parent == *root { + return Some(offset); + } else if let Some(s) = get_parent(&parent) { + offset += with_states(&parent, |states| { + states + .cached_state + .get::() + .current() + .location + }); + parent = s; + } else { + // `parent` is now root of subsurface tree; `surface` is not a subsurface child of `root` + break; + } + } + + for (popup, popup_offset) in PopupManager::popups_for_surface(root) { + if let Some(offset) = Self::surface_tree_offset(popup.wl_surface(), surface) { + return Some(popup_offset + offset); + } + } + + None + } + + pub fn focus_under( + &self, + relative_pos: Point, + surface_type: WindowSurfaceType, + ) -> Option<(PointerFocusTarget, Point)> { + if let Some(xsurface) = self.x11_surface() { + xsurface + .surface_under(relative_pos, Point::default(), surface_type) + .map(|(_surface, surface_offset)| { + ( + PointerFocusTarget::X11Surface { + surface: xsurface.clone(), + toplevel: Some(self.clone()), + }, + surface_offset.to_f64(), + ) + }) + } else { + self.0 + .surface_under(relative_pos, surface_type) + .map(|(surface, surface_offset)| { + ( + PointerFocusTarget::WlSurface { + surface, + toplevel: Some(self.clone().into()), + }, + surface_offset.to_f64(), + ) + }) + } + } + + pub fn on_commit(&self) { + self.0.on_commit(); + } + + pub fn send_frame( + &self, + output: &Output, + time: T, + throttle: Option, + primary_scan_out_output: F, + ) where + T: Into, + F: FnMut(&WlSurface, &SurfaceData) -> Option + Copy, + { + self.0 + .send_frame(output, time, throttle, primary_scan_out_output); + } + + pub fn send_dmabuf_feedback( + &self, + output: &Output, + feedback: &SurfaceDmabufFeedback, + _render_element_states: &RenderElementStates, + primary_scan_out_output: F1, + ) where + F1: FnMut(&WlSurface, &SurfaceData) -> Option + Copy, + { + let is_fullscreen = self.is_fullscreen(false); + + self.0 + .send_dmabuf_feedback(output, primary_scan_out_output, |_, data| { + if is_fullscreen { + feedback + .primary_scanout_feedback + .as_ref() + .unwrap_or(&feedback.render_feedback) + } else if frame_time_filter_fn(data) == Kind::ScanoutCandidate { + feedback + .overlay_scanout_feedback + .as_ref() + .unwrap_or(&feedback.render_feedback) + } else { + &feedback.render_feedback + } + }) + } + + pub fn take_presentation_feedback( + &self, + output_feedback: &mut OutputPresentationFeedback, + primary_scan_out_output: F1, + presentation_feedback_flags: F2, + ) where + F1: FnMut(&WlSurface, &SurfaceData) -> Option + Copy, + F2: FnMut(&WlSurface, &SurfaceData) -> PresentationKind + Copy, + { + self.0.take_presentation_feedback( + output_feedback, + primary_scan_out_output, + presentation_feedback_flags, + ) + } + + pub fn with_surfaces(&self, processor: F) + where + F: FnMut(&WlSurface, &SurfaceData), + { + self.0.with_surfaces(processor) + } + + pub fn user_data(&self) -> &UserDataMap { + self.0.user_data() + } + + pub fn push_popup_render_elements( + &self, + renderer: &mut R, + location: Point, + scale: Scale, + alpha: f32, + scanout_node: Option, + blur_strength: usize, + push: &mut dyn FnMut(SurfaceRenderElement), + ) where + R: Renderer + ImportAll + AsGlowRenderer, + R::TextureId: Clone + 'static, + { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => { + let surface = toplevel.wl_surface(); + for (popup, popup_offset) in PopupManager::popups_for_surface(surface) { + let offset = (self.0.geometry().loc + popup_offset - popup.geometry().loc) + .to_physical_precise_round(scale); + let mut geometry = popup.geometry().to_f64(); + geometry.loc += location.to_f64().to_logical(scale) + popup_offset.to_f64(); + let radii = with_states(popup.wl_surface(), |states| { + surface_corners(states, geometry.size.to_i32_round()) + }) + .unwrap_or([0; 4]); + + push_render_elements_from_surface_tree( + renderer, + popup.wl_surface(), + location + offset, + geometry, + scale, + alpha, + false, + radii, + None, + blur_strength, + scanout_kind_eval(None, scanout_node, alpha), + push, + None, + ) + } + } + WindowSurface::X11(_) => {} + } + } + + pub fn push_render_elements( + &self, + renderer: &mut R, + location: Point, + scale: Scale, + alpha: f32, + scanout_override: Option, + scanout_node: Option, + should_clip: bool, + radii: [u8; 4], + blur_strength: usize, + push_above: &mut dyn FnMut(SurfaceRenderElement), + push_below: Option<&mut dyn FnMut(SurfaceRenderElement)>, + ) where + R: Renderer + ImportAll + AsGlowRenderer, + R::TextureId: Clone + 'static, + { + let mut geometry = self.0.geometry().to_f64(); + geometry.loc += location.to_f64().to_logical(scale); + + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => { + let surface = toplevel.wl_surface(); + + push_render_elements_from_surface_tree( + renderer, + surface, + location, + geometry, + scale, + alpha, + should_clip, + radii, + None, + blur_strength, + scanout_kind_eval(scanout_override, scanout_node, alpha), + push_above, + push_below, + ) + } + WindowSurface::X11(surface) => { + let Some(surface) = surface.wl_surface() else { + return; + }; + + push_render_elements_from_surface_tree( + renderer, + &surface, + location, + geometry, + scale, + alpha, + should_clip, + radii, + None, + blur_strength, + scanout_kind_eval(scanout_override, scanout_node, alpha), + push_above, + push_below, + ) + } + } + } + + pub fn x11_surface(&self) -> Option<&X11Surface> { + self.0.x11_surface() + } + + pub fn downgrade(&self) -> WeakCosmicSurface { + WeakCosmicSurface(self.0.downgrade()) + } +} + +impl WeakCosmicSurface { + pub fn upgrade(&self) -> Option { + self.0.upgrade().map(CosmicSurface) + } +} + +impl IsAlive for CosmicSurface { + fn alive(&self) -> bool { + self.0.alive() + } +} + +impl SpaceElement for CosmicSurface { + fn geometry(&self) -> Rectangle { + SpaceElement::geometry(&self.0) + } + + fn bbox(&self) -> Rectangle { + SpaceElement::bbox(&self.0) + } + + fn is_in_input_region(&self, point: &Point) -> bool { + SpaceElement::is_in_input_region(&self.0, point) + } + + fn z_index(&self) -> u8 { + SpaceElement::z_index(&self.0) + } + + fn set_activate(&self, activated: bool) { + SpaceElement::set_activate(&self.0, activated) + } + + fn output_enter( + &self, + output: &Output, + overlap: smithay::utils::Rectangle, + ) { + SpaceElement::output_enter(&self.0, output, overlap) + } + + fn output_leave(&self, output: &Output) { + SpaceElement::output_leave(&self.0, output) + } + + #[profiling::function] + fn refresh(&self) { + SpaceElement::refresh(&self.0) + } +} + +impl KeyboardTarget for CosmicSurface { + fn enter( + &self, + seat: &Seat, + data: &mut State, + mut keys: Vec>, + serial: smithay::utils::Serial, + ) { + if self.0.is_x11() { + keys = vec![]; + } + + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => { + KeyboardTarget::enter(toplevel.wl_surface(), seat, data, keys, serial) + } + WindowSurface::X11(x11) => KeyboardTarget::enter(x11, seat, data, keys, serial), + } + } + + fn leave(&self, seat: &Seat, data: &mut State, serial: smithay::utils::Serial) { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => { + KeyboardTarget::leave(toplevel.wl_surface(), seat, data, serial) + } + WindowSurface::X11(x11) => KeyboardTarget::leave(x11, seat, data, serial), + } + } + + fn key( + &self, + seat: &Seat, + data: &mut State, + key: KeysymHandle<'_>, + state: smithay::backend::input::KeyState, + serial: smithay::utils::Serial, + time: u32, + ) { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => { + KeyboardTarget::key(toplevel.wl_surface(), seat, data, key, state, serial, time) + } + WindowSurface::X11(x11) => { + KeyboardTarget::key(x11, seat, data, key, state, serial, time) + } + } + } + + fn modifiers( + &self, + seat: &Seat, + data: &mut State, + modifiers: ModifiersState, + serial: smithay::utils::Serial, + ) { + match self.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => { + KeyboardTarget::modifiers(toplevel.wl_surface(), seat, data, modifiers, serial) + } + WindowSurface::X11(x11) => { + KeyboardTarget::modifiers(x11, seat, data, modifiers, serial) + } + } + } +} + +impl WaylandFocus for CosmicSurface { + fn wl_surface(&self) -> Option> { + self.0.wl_surface() + } +} + +impl X11Relatable for CosmicSurface { + fn is_window(&self, window: &X11Surface) -> bool { + self.x11_surface() == Some(window) + } +} + +fn with_toplevel_state) -> T>( + toplevel: &ToplevelSurface, + pending: bool, + cb: F, +) -> T { + if pending { + toplevel.with_pending_state(|pending| cb(Some(pending))) + } else { + toplevel.with_committed_state(cb) + } +} diff --git a/src/shell/element/swap_indicator.rs b/src/shell/element/swap_indicator.rs new file mode 100644 index 0000000..6f9498d --- /dev/null +++ b/src/shell/element/swap_indicator.rs @@ -0,0 +1,135 @@ +use crate::{ + backend::render::element::AsGlowRenderer, + fl, + utils::iced::{IcedElement, IcedRenderElement, Program}, +}; + +use calloop::LoopHandle; +use cosmic::{ + Apply, + iced::{ + core::{Alignment, Background, Border, Color, Length}, + widget::{container, row, space}, + }, + theme, + widget::{icon::from_name, text}, +}; +use smithay::{ + backend::renderer::ImportMem, + desktop::space::SpaceElement, + output::Output, + utils::{Logical, Physical, Point, Rectangle, Scale, Size}, +}; + +#[derive(Debug, Clone)] +pub struct SwapIndicator { + location: Point, + elem: IcedElement, +} + +impl SwapIndicator { + pub fn new( + evlh: LoopHandle<'static, crate::state::State>, + mut theme: cosmic::Theme, + ) -> SwapIndicator { + theme.transparent = theme.cosmic().frosted_system_interface; + SwapIndicator { + location: Point::default(), + elem: IcedElement::new(SwapIndicatorInternal, Size::from((1, 1)), evlh, theme), + } + } + + pub fn resize(&mut self, size: Size) { + let minimum = self.elem.minimum_size(); + let new_size = Size::::new(size.w.min(minimum.w), size.h.min(minimum.h)); + let location = Point::new( + size.w.saturating_sub(new_size.w) / 2, + size.h.saturating_sub(new_size.h) / 2, + ); + self.elem.resize(new_size); + self.location = location; + } + + pub fn push_render_elements( + &self, + renderer: &mut R, + location: Point, + scale: Scale, + alpha: f32, + push_above: &mut dyn FnMut(IcedRenderElement), + push_below: Option<&mut dyn FnMut(IcedRenderElement)>, + ) where + R: AsGlowRenderer + ImportMem, + R::TextureId: Send + Clone + 'static, + { + self.elem.push_render_elements( + renderer, + location + self.location.to_physical_precise_round(scale), + scale, + alpha, + self.elem + .with_theme(|theme| theme.cosmic().radius_s()) + .map(|x| x.round() as u8), + push_above, + push_below, + ); + } + + pub fn output_enter(&self, output: &Output) { + self.elem + .output_enter(output, Rectangle::default() /*unused*/); + } + + pub fn output_leave(&self, output: &Output) { + self.elem.output_leave(output); + } +} + +pub struct SwapIndicatorInternal; + +impl Program for SwapIndicatorInternal { + type Message = (); + + fn view(&self) -> cosmic::Element<'_, Self::Message> { + row(vec![ + from_name("window-swap-symbolic") + .size(32) + .prefer_svg(true) + .icon() + .into(), + space::horizontal().width(16).into(), + text::title3(fl!("swap-windows")).into(), + ]) + .align_y(Alignment::Center) + .apply(container) + .align_x(Alignment::Center) + .align_y(Alignment::Center) + .padding(16) + .apply(container) + .class(theme::Container::custom(|theme| { + let mut background = theme.cosmic().accent_color(); + if theme.transparent { + background.alpha = theme + .cosmic() + .alpha_map + .blurred_alpha(theme.cosmic().frosted); + } + + container::Style { + snap: true, + icon_color: Some(Color::from(theme.cosmic().accent.on)), + text_color: Some(Color::from(theme.cosmic().accent.on)), + background: Some(Background::Color(background.into())), + border: Border { + radius: theme.cosmic().radius_s().into(), + width: 0.0, + color: Color::TRANSPARENT, + }, + shadow: Default::default(), + } + })) + .width(Length::Shrink) + .height(Length::Shrink) + .into() + } +} diff --git a/src/shell/element/window.rs b/src/shell/element/window.rs new file mode 100644 index 0000000..86c2ce7 --- /dev/null +++ b/src/shell/element/window.rs @@ -0,0 +1,1459 @@ +use crate::{ + backend::render::{ + IndicatorShader, Key, Usage, cursor::CursorState, element::AsGlowRenderer, + shadow::ShadowShader, wayland::SurfaceRenderElement, + }, + hooks::{Decorations, HOOKS}, + shell::{ + element::{CosmicMappedKey, CosmicMappedKeyInner}, + focus::target::PointerFocusTarget, + grabs::{ReleaseMode, ResizeEdge}, + }, + state::State, + utils::{ + iced::{IcedElement, IcedRenderElement, Program}, + prelude::*, + }, +}; +use calloop::LoopHandle; +use cosmic::iced::{Color, Task}; +use cosmic_comp_config::AppearanceConfig; +use smithay::{ + backend::{ + drm::DrmNode, + input::KeyState, + renderer::{ + ImportAll, ImportMem, Renderer, + element::{Element, Id as RendererId, Kind, RenderElement, UnderlyingStorage}, + gles::element::PixelShaderElement, + glow::GlowRenderer, + utils::{CommitCounter, DamageSet, OpaqueRegions}, + }, + }, + desktop::{WindowSurfaceType, space::SpaceElement}, + input::{ + Seat, + keyboard::{KeyboardTarget, KeysymHandle, ModifiersState}, + pointer::{ + AxisFrame, ButtonEvent, CursorIcon, CursorImageStatus, GestureHoldBeginEvent, + GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, + GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, + GestureSwipeUpdateEvent, MotionEvent, PointerTarget, RelativeMotionEvent, + }, + touch::{ + DownEvent, FrameMarker, MotionEvent as TouchMotionEvent, OrientationEvent, ShapeEvent, + TouchTarget, UpEvent, + }, + }, + output::Output, + reexports::wayland_server::protocol::wl_surface::WlSurface, + utils::{ + Buffer, IsAlive, Logical, Physical, Point, Rectangle, Scale, Serial, Size, Transform, + user_data::UserDataMap, + }, + wayland::{pointer_constraints::with_pointer_constraint, seat::WaylandFocus}, +}; +use std::{ + borrow::Cow, + fmt, + hash::Hash, + sync::{ + Arc, Mutex, + atomic::{AtomicBool, AtomicU8, Ordering}, + }, +}; +use wayland_backend::server::ObjectId; + +use super::CosmicSurface; + +pub const SSD_HEIGHT: i32 = 36; +pub const RESIZE_BORDER: i32 = 10; + +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct CosmicWindow(pub(super) IcedElement); + +impl fmt::Debug for CosmicWindow { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("CosmicWindow") + .field("internal", &self.0) + .finish_non_exhaustive() + } +} + +#[derive(Debug)] +pub struct CosmicWindowInternal { + pub(super) window: CosmicSurface, + activated: AtomicBool, + /// TODO: This needs to be per seat + pointer_entered: AtomicU8, + last_title: Mutex, + tiled: AtomicBool, + theme: Mutex, + appearance_conf: Mutex, +} + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Focus { + Header = 1, + ResizeTop, + ResizeLeft, + ResizeRight, + ResizeBottom, + ResizeTopRight, + ResizeTopLeft, + ResizeBottomRight, + ResizeBottomLeft, +} + +impl Focus { + pub fn under( + surface: &CosmicSurface, + header_height: i32, + location: Point, + ) -> Option { + let geo = surface.geometry(); + let loc = location.to_i32_floor::() - geo.loc; + if loc.y < 0 && loc.x < 0 { + Some(Focus::ResizeTopLeft) + } else if loc.y < 0 && loc.x >= geo.size.w { + Some(Focus::ResizeTopRight) + } else if loc.y < 0 { + Some(Focus::ResizeTop) + } else if loc.y >= header_height + geo.size.h && loc.x < 0 { + Some(Focus::ResizeBottomLeft) + } else if loc.y >= header_height + geo.size.h && loc.x >= geo.size.w { + Some(Focus::ResizeBottomRight) + } else if loc.y >= header_height + geo.size.h { + Some(Focus::ResizeBottom) + } else if loc.x < 0 { + Some(Focus::ResizeLeft) + } else if loc.x >= geo.size.w { + Some(Focus::ResizeRight) + } else if loc.y < header_height { + Some(Focus::Header) + } else { + None + } + } + + pub fn cursor_shape(&self) -> CursorIcon { + match self { + Focus::ResizeTopLeft => CursorIcon::NwResize, + Focus::ResizeTopRight => CursorIcon::NeResize, + Focus::ResizeTop => CursorIcon::NResize, + Focus::ResizeBottomLeft => CursorIcon::SwResize, + Focus::ResizeBottomRight => CursorIcon::SeResize, + Focus::ResizeBottom => CursorIcon::SResize, + Focus::ResizeLeft => CursorIcon::WResize, + Focus::ResizeRight => CursorIcon::EResize, + Focus::Header => CursorIcon::Default, + } + } + + /// # Safety + /// `value` must be in the range of `Focus` + pub unsafe fn from_u8(value: u8) -> Option { + match value { + 0 => None, + focus => unsafe { Some(std::mem::transmute::(focus)) }, + } + } +} + +impl CosmicWindowInternal { + pub fn swap_focus(&self, focus: Option) -> Option { + let value = focus.map_or(0, |x| x as u8); + unsafe { Focus::from_u8(self.pointer_entered.swap(value, Ordering::SeqCst)) } + } + + pub fn current_focus(&self) -> Option { + unsafe { Focus::from_u8(self.pointer_entered.load(Ordering::SeqCst)) } + } + + /// returns if the window has any current or pending server-side decorations + pub fn has_ssd(&self, pending: bool) -> bool { + !self.window.is_decorated(pending) + } + + /// returns if the window is currently tiled + pub fn is_tiled(&self) -> bool { + self.tiled.load(Ordering::Acquire) + } + + fn has_tiled_state(&self) -> bool { + self.window.is_tiled(false).unwrap_or(false) + } +} + +impl CosmicWindow { + pub fn new( + window: impl Into, + handle: LoopHandle<'static, crate::state::State>, + mut theme: cosmic::Theme, + appearance: AppearanceConfig, + ) -> CosmicWindow { + let window = window.into(); + let width = window.geometry().size.w; + let last_title = window.title(); + theme.transparent = theme.cosmic().frosted_windows; + + if appearance.clip_floating_windows { + window.set_tiled(true); + } + + CosmicWindow(IcedElement::new( + CosmicWindowInternal { + window, + activated: AtomicBool::new(false), + pointer_entered: AtomicU8::new(0), + last_title: Mutex::new(last_title), + tiled: AtomicBool::new(false), + theme: Mutex::new(theme.clone()), + appearance_conf: Mutex::new(appearance), + }, + (width, SSD_HEIGHT), + handle, + theme, + )) + } + + pub fn pending_size(&self) -> Option> { + self.0.with_program(|p| { + let mut size = p.window.pending_size()?; + if p.has_ssd(true) { + size.h += SSD_HEIGHT; + } + Some(size) + }) + } + + pub fn last_server_size(&self) -> Option> { + self.0.with_program(|p| { + let mut size = p.window.last_server_size()?; + if p.has_ssd(false) { + size.h += SSD_HEIGHT; + } + Some(size) + }) + } + + pub fn set_geometry(&self, geo: Rectangle) { + self.0.with_program(|p| { + let ssd_height = if p.has_ssd(true) { SSD_HEIGHT } else { 0 }; + let loc = (geo.loc.x, geo.loc.y + ssd_height); + let size = (geo.size.w, std::cmp::max(geo.size.h - ssd_height, 0)); + p.window + .set_geometry(Rectangle::new(loc.into(), size.into()), ssd_height as u32); + }); + } + + pub fn on_commit(&self, surface: &WlSurface) { + let mut geo = None; + self.0.with_program(|p| { + if &p.window == surface { + p.window.0.on_commit(); + geo = Some(p.window.geometry()); + } + }); + if let Some(geo) = geo { + self.0.resize(Size::from((geo.size.w, SSD_HEIGHT))); + } + } + + pub fn surface(&self) -> CosmicSurface { + self.0.with_program(|p| p.window.clone()) + } + + pub fn focus_under( + &self, + mut relative_pos: Point, + surface_type: WindowSurfaceType, + seat: Option<&Seat>, + ) -> Option<(PointerFocusTarget, Point)> { + let has_constraint = if let Some(seat) = seat + && let Some(pointer) = seat.get_pointer() + && let Some(surface) = self.wl_surface() + && with_pointer_constraint(&surface, &pointer, |constraint| { + constraint.is_some_and(|c| c.is_active()) + }) { + true + } else { + false + }; + + self.0.with_program(|p| { + let mut offset = Point::from((0., 0.)); + let mut window_ui = None; + let has_ssd = p.has_ssd(false); + + if (!has_constraint && (has_ssd || p.has_tiled_state())) + && surface_type.contains(WindowSurfaceType::TOPLEVEL) + { + let geo = p.window.geometry(); + + let point_i32 = relative_pos.to_i32_floor::(); + let ssd_height = if has_ssd { SSD_HEIGHT } else { 0 }; + + if (point_i32.x - geo.loc.x >= -RESIZE_BORDER && point_i32.x - geo.loc.x < 0) + || (point_i32.y - geo.loc.y >= -RESIZE_BORDER && point_i32.y - geo.loc.y < 0) + || (point_i32.x - geo.loc.x >= geo.size.w + && point_i32.x - geo.loc.x < geo.size.w + RESIZE_BORDER) + || (point_i32.y - geo.loc.y >= geo.size.h + ssd_height + && point_i32.y - geo.loc.y < geo.size.h + ssd_height + RESIZE_BORDER) + { + window_ui = Some(( + PointerFocusTarget::WindowUI(self.clone()), + Point::from((0., 0.)), + )); + } + + if has_ssd && (point_i32.y - geo.loc.y < SSD_HEIGHT) { + window_ui = Some(( + PointerFocusTarget::WindowUI(self.clone()), + Point::from((0., 0.)), + )); + } + } + + if has_ssd { + relative_pos.y -= SSD_HEIGHT as f64; + offset.y += SSD_HEIGHT as f64; + } + + window_ui.or_else(|| { + p.window + .focus_under(relative_pos, surface_type) + .map(|(target, surface_offset)| (target, offset + surface_offset)) + }) + }) + } + + pub fn contains_surface(&self, window: &CosmicSurface) -> bool { + self.0.with_program(|p| &p.window == window) + } + + pub fn offset(&self) -> Point { + let has_ssd = self.0.with_program(|p| p.has_ssd(false)); + if has_ssd { + Point::from((0, SSD_HEIGHT)) + } else { + Point::from((0, 0)) + } + } + + pub(super) fn loop_handle(&self) -> LoopHandle<'static, crate::state::State> { + self.0.loop_handle() + } + + pub fn push_popup_render_elements( + &self, + renderer: &mut R, + location: Point, + scale: Scale, + alpha: f32, + scanout_node: Option, + push: &mut dyn FnMut(CosmicWindowRenderElement), + ) where + R: Renderer + AsGlowRenderer + ImportAll + ImportMem, + R::TextureId: Send + Clone + 'static, + { + let has_ssd = self.0.with_program(|p| p.has_ssd(false)); + + let window_loc = if has_ssd { + location + Point::from((0, (SSD_HEIGHT as f64 * scale.y) as i32)) + } else { + location + }; + + self.0.with_program(|p| { + let theme = p.theme.lock().unwrap(); + let frosted = if theme.cosmic().frosted_windows { + (theme.cosmic().frosted as u8 + 1) as usize + } else { + 0 + }; + + p.window.push_popup_render_elements( + renderer, + window_loc, + scale, + alpha, + scanout_node, + frosted, + &mut |elem| push(elem.into()), + ) + }) + } + + pub fn shadow_render_element( + &self, + renderer: &mut R, + location: Point, + max_size: Option>, + output_scale: Scale, + scale: f64, + alpha: f32, + ) -> Option + where + R: AsGlowRenderer, + R::TextureId: Send + Clone + 'static, + C: From>, + { + self.0.with_program(|p| { + let has_ssd = p.has_ssd(false); + let is_tiled = p.is_tiled(); + let activated = p.window.is_activated(false); + let appearance = p.appearance_conf.lock().unwrap(); + let theme = p.theme.lock().unwrap(); + + if p.window.is_maximized(false) { + return None; + } + + let clip = (!is_tiled && appearance.clip_floating_windows) + || (is_tiled && appearance.clip_tiled_windows); + let should_draw_shadow = if is_tiled { + appearance.shadow_tiled_windows + } else { + appearance.clip_floating_windows || has_ssd + }; + + if !should_draw_shadow { + return None; + } + let mut radii = theme + .cosmic() + .radius_s() + .map(|x| if x < 4.0 { x } else { x + 4.0 }) + .map(|x| (x * scale as f32).round() as u8); + if has_ssd && !clip { + // bottom corners + radii[0] = 0; + radii[2] = 0; + if is_tiled { + // top corners + radii[1] = 0; + radii[3] = 0; + } + } + + let mut geo = SpaceElement::geometry(&p.window).to_f64(); + if has_ssd { + geo.size.h += SSD_HEIGHT as f64; + } + geo = geo.upscale(scale); + geo.loc += location.to_f64().to_logical(output_scale); + if let Some(max_size) = max_size { + geo.size = geo.size.clamp(Size::default(), max_size.to_f64()); + } + + let window_key = + CosmicMappedKey(CosmicMappedKeyInner::Window(Arc::downgrade(&self.0.0))); + + Some( + CosmicWindowRenderElement::Shadow(ShadowShader::element( + renderer, + window_key, + geo.to_i32_round().as_local(), + radii, + if activated { alpha } else { alpha * 0.75 }, + output_scale.x, + theme.cosmic().is_dark, + )) + .into(), + ) + }) + } + + pub fn push_render_elements( + &self, + renderer: &mut R, + location: Point, + max_size: Option>, + scale: Scale, + alpha: f32, + scanout_override: Option, + scanout_node: Option, + push_above: &mut dyn FnMut(CosmicWindowRenderElement), + push_below: &mut dyn FnMut(CosmicWindowRenderElement), + ) where + R: AsGlowRenderer, + R::TextureId: Send + Clone + 'static, + { + let (has_ssd, is_tiled, is_maximized, mut radii, appearance) = self.0.with_program(|p| { + ( + p.has_ssd(false), + p.is_tiled(), + p.window.is_maximized(false), + p.theme + .lock() + .unwrap() + .cosmic() + .radius_s() + .map(|x| if x < 4.0 { x } else { x + 4.0 }) + .map(|x| x.round() as u8), + *p.appearance_conf.lock().unwrap(), + ) + }); + let clip = ((!is_tiled && appearance.clip_floating_windows) + || (is_tiled && appearance.clip_tiled_windows)) + && !is_maximized; + if has_ssd && !clip { + // bottom corners + radii[0] = 0; + radii[2] = 0; + if is_tiled { + // top corners + radii[1] = 0; + radii[3] = 0; + } + } + + let window_loc = if has_ssd { + location + Point::from((0, (SSD_HEIGHT as f64 * scale.y) as i32)) + } else { + location + }; + + let (mut geo, bg_divider) = self.0.with_program(|p| { + ( + SpaceElement::geometry(&p.window).to_f64(), + p.theme.lock().unwrap().cosmic().bg_divider(), + ) + }); + geo.loc += location.to_f64().to_logical(scale); + if has_ssd { + geo.size.h += SSD_HEIGHT as f64; + } + if let Some(max_size) = max_size { + geo.size = geo.size.clamp(Size::default(), max_size.to_f64()); + } + + if (has_ssd || clip) && !is_maximized { + let window_key = + CosmicMappedKey(CosmicMappedKeyInner::Window(Arc::downgrade(&self.0.0))); + + let (r, g, b, a) = bg_divider.into_components(); + let elem = CosmicWindowRenderElement::Border(IndicatorShader::element( + renderer, + Key::Window(Usage::Border, window_key.clone()), + geo.to_i32_round().as_local(), + 1, + radii, + a * alpha, + scale.x, + [r, g, b], + )); + push_above(elem); + } + + self.0.with_program(|p| { + let mut radii = radii; + if has_ssd { + radii[1] = 0; + radii[3] = 0; + } + let theme = p.theme.lock().unwrap(); + let frosted = if theme.cosmic().frosted_windows { + (theme.cosmic().frosted as u8 + 1) as usize + } else { + 0 + }; + p.window.push_render_elements( + renderer, + window_loc, + scale, + alpha, + scanout_override, + scanout_node, + clip, + radii, + frosted, + &mut |elem| push_above(elem.into()), + Some(&mut |elem| push_below(elem.into())), + ); + }); + + if has_ssd { + radii[0] = 0; + radii[2] = 0; + let ssd_loc = location + + self + .0 + .with_program(|p| p.window.geometry().loc.to_physical_precise_round(scale)); + self.0.push_render_elements( + renderer, + ssd_loc, + scale, + alpha, + radii, + &mut |elem| push_above(elem.into()), + Some(&mut |elem| push_below(elem.into())), + ); + } + } + + pub(crate) fn set_theme(&self, mut theme: cosmic::Theme) { + theme.transparent = theme.cosmic().frosted_windows; + self.0.with_program(|p| { + *p.theme.lock().unwrap() = theme.clone(); + }); + self.0.set_theme(theme); + } + + pub fn update_appearance_conf(&self, appearance: &AppearanceConfig) { + self.0.with_program(|p| { + let mut conf = p.appearance_conf.lock().unwrap(); + if &*conf != appearance { + *conf = *appearance; + if appearance.clip_floating_windows { + p.window.set_tiled(true); + } else if !p.tiled.load(Ordering::Acquire) { + p.window.set_tiled(false); + } + p.window.send_configure(); + } + }) + } + + pub(crate) fn force_redraw(&self) { + self.0.force_redraw(); + } + + pub fn min_size(&self) -> Option> { + self.0 + .with_program(|p| p.window.min_size_without_ssd()) + .map(|size| { + if self.0.with_program(|p| !p.window.is_decorated(false)) { + size + (0, SSD_HEIGHT).into() + } else { + size + } + }) + } + pub fn max_size(&self) -> Option> { + self.0 + .with_program(|p| p.window.max_size_without_ssd()) + .map(|size| { + if self.0.with_program(|p| !p.window.is_decorated(false)) { + size + (0, SSD_HEIGHT).into() + } else { + size + } + }) + } + + pub fn set_tiled(&self, tiled: bool) { + self.0.with_program(|p| { + p.tiled.store(tiled, Ordering::Release); + if !p.appearance_conf.lock().unwrap().clip_floating_windows { + p.window.set_tiled(tiled); + } + }); + } + + pub fn corner_radius(&self, geometry_size: Size, default_radius: u8) -> [u8; 4] { + self.0.with_program(|p| { + let has_ssd = p.has_ssd(false); + let is_tiled = p.is_tiled(); + let appearance = p.appearance_conf.lock().unwrap(); + + let clip = ((!is_tiled && appearance.clip_floating_windows) + || (is_tiled && appearance.clip_tiled_windows)) + && !p.window.is_maximized(false); + let round = + (!is_tiled || appearance.clip_tiled_windows) && !p.window.is_maximized(false); + let radii = if round { + { + p.theme + .lock() + .unwrap() + .cosmic() + .radius_s() + .map(|x| if x < 4.0 { x } else { x + 4.0 }) + .map(|x| x.round() as u8) + } + } else { + [0; 4] + }; + + match (has_ssd, clip) { + (has_ssd, true) => { + let mut corners = p.window.corner_radius(geometry_size).unwrap_or(radii); + + corners[0] = radii[0].max(corners[0]); + corners[1] = if has_ssd { + radii[1] + } else { + radii[1].max(corners[1]) + }; + corners[2] = radii[2].max(corners[2]); + corners[3] = if has_ssd { + radii[3] + } else { + radii[3].max(corners[3]) + }; + + corners + } + (true, false) => p + .window + .corner_radius(geometry_size) + .map(|[a, _, c, _]| [a, radii[1], c, radii[3]]) + .unwrap_or([default_radius, radii[1], default_radius, radii[3]]), + (false, false) => p + .window + .corner_radius(geometry_size) + .unwrap_or([default_radius; 4]), + } + }) + } +} + +#[derive(Debug, Clone, Copy)] +pub enum Message { + DragStart, + Minimize, + Maximize, + Close, + Menu, +} + +impl Program for CosmicWindowInternal { + type Message = Message; + + fn update( + &mut self, + message: Self::Message, + loop_handle: &LoopHandle<'static, crate::state::State>, + last_seat: Option<&(Seat, Serial)>, + ) -> Task { + match message { + Message::DragStart => { + if let Some((seat, serial)) = last_seat.cloned() + && let Some(surface) = self.window.wl_surface().map(Cow::into_owned) + { + loop_handle.insert_idle(move |state| { + let res = state.common.shell.write().move_request( + &surface, + &seat, + serial, + ReleaseMode::NoMouseButtons, + false, + &state.common.config, + &state.common.event_loop_handle, + false, + ); + if let Some((grab, focus)) = res { + if grab.is_touch_grab() { + seat.get_touch().unwrap().set_grab(state, grab, serial); + } else { + seat.get_pointer() + .unwrap() + .set_grab(state, grab, serial, focus); + } + } + }); + } + } + Message::Minimize => { + if let Some(surface) = self.window.wl_surface().map(Cow::into_owned) { + loop_handle.insert_idle(move |state| { + let mut shell = state.common.shell.write(); + shell.minimize_request(&surface) + }); + } + } + Message::Maximize => { + if let Some(surface) = self.window.wl_surface().map(Cow::into_owned) { + loop_handle.insert_idle(move |state| { + let mut shell = state.common.shell.write(); + if let Some(mapped) = shell.element_for_surface(&surface).cloned() { + let seat = shell.seats.last_active().clone(); + shell.maximize_toggle(&mapped, &seat, &state.common.event_loop_handle) + } + }); + } + } + Message::Close => self.window.close(), + Message::Menu => { + if let Some((seat, serial)) = last_seat.cloned() + && let Some(surface) = self.window.wl_surface().map(Cow::into_owned) + { + loop_handle.insert_idle(move |state| { + let shell = state.common.shell.read(); + if let Some(mapped) = shell.element_for_surface(&surface).cloned() { + let position = if let Some((output, set)) = + shell.workspaces.sets.iter().find(|(_, set)| { + set.sticky_layer.mapped().any(|m| m == &mapped) + }) { + set.sticky_layer + .element_geometry(&mapped) + .unwrap() + .loc + .to_global(output) + } else if let Some(workspace) = shell.space_for(&mapped) { + let Some(elem_geo) = workspace.element_geometry(&mapped) else { + return; + }; + elem_geo.loc.to_global(&workspace.output) + } else { + return; + }; + + let pointer = seat.get_pointer().unwrap(); + let mut cursor = pointer.current_location().to_i32_round(); + cursor.y -= SSD_HEIGHT; + + let res = shell.menu_request( + false, + &surface, + &seat, + serial, + cursor - position.as_logical(), + false, + &state.common.config, + &state.common.event_loop_handle, + ); + + std::mem::drop(shell); + if let Some((grab, focus)) = res { + pointer.set_grab(state, grab, serial, focus); + } + } + }); + } + } + } + Task::none() + } + + fn background_color(&self, theme: &cosmic::Theme) -> Color { + if self.window.is_maximized(false) { + theme + .cosmic() + .background(theme.cosmic().frosted_windows) + .base + .into() + } else { + Color::TRANSPARENT + } + } + + fn view(&self) -> cosmic::Element<'_, Self::Message> { + HOOKS.get().unwrap().window_decorations.view(self) + } +} + +#[derive(Debug)] +pub struct DefaultDecorations; + +impl Decorations for DefaultDecorations { + fn view(&self, win: &CosmicWindowInternal) -> cosmic::Element<'_, Message> { + let sharp_corners = win.window.is_maximized(false) + || (win.is_tiled() && !win.appearance_conf.lock().unwrap().clip_tiled_windows); + + let mut header = cosmic::widget::header_bar() + .title(win.last_title.lock().unwrap().clone()) + .on_drag(Message::DragStart) + .on_close(Message::Close) + .focused(win.window.is_activated(false)) + .on_double_click(Message::Maximize) + .on_right_click(Message::Menu) + .is_ssd(true) + .sharp_corners(sharp_corners); + + if cosmic::config::show_minimize() { + header = header.on_minimize(Message::Minimize) + } + if cosmic::config::show_maximize() { + header = header.on_maximize(Message::Maximize) + } + + header.into() + } +} + +impl IsAlive for CosmicWindow { + fn alive(&self) -> bool { + self.0.with_program(|p| p.window.alive()) + } +} + +impl SpaceElement for CosmicWindow { + fn bbox(&self) -> Rectangle { + self.0.with_program(|p| { + let mut bbox = SpaceElement::bbox(&p.window); + let has_ssd = p.has_ssd(false); + + if has_ssd || p.has_tiled_state() { + bbox.loc -= Point::from((RESIZE_BORDER, RESIZE_BORDER)); + bbox.size += Size::from((RESIZE_BORDER * 2, RESIZE_BORDER * 2)); + } + if has_ssd { + bbox.size.h += SSD_HEIGHT; + } + + bbox + }) + } + fn is_in_input_region(&self, point: &Point) -> bool { + self.focus_under(*point, WindowSurfaceType::ALL, None) + .is_some() + } + fn set_activate(&self, activated: bool) { + if self + .0 + .with_program(|p| p.activated.load(Ordering::SeqCst) != activated) + { + SpaceElement::set_activate(&self.0, activated); + self.0.force_redraw(); + self.0.with_program(|p| { + p.activated.store(activated, Ordering::SeqCst); + SpaceElement::set_activate(&p.window, activated); + }); + } + } + #[profiling::function] + fn output_enter(&self, output: &Output, overlap: Rectangle) { + SpaceElement::output_enter(&self.0, output, overlap); + self.0 + .with_program(|p| SpaceElement::output_enter(&p.window, output, overlap)); + } + #[profiling::function] + fn output_leave(&self, output: &Output) { + SpaceElement::output_leave(&self.0, output); + self.0 + .with_program(|p| SpaceElement::output_leave(&p.window, output)); + } + fn geometry(&self) -> Rectangle { + self.0.with_program(|p| { + let mut geo = SpaceElement::geometry(&p.window); + if p.has_ssd(false) { + geo.size.h += SSD_HEIGHT; + } + geo + }) + } + fn z_index(&self) -> u8 { + self.0.with_program(|p| SpaceElement::z_index(&p.window)) + } + #[profiling::function] + fn refresh(&self) { + if self.0.with_program(|p| { + SpaceElement::refresh(&p.window); + if !p.has_ssd(true) { + return false; + } + + let title = p.window.title(); + let mut last_title = p.last_title.lock().unwrap(); + if *last_title != title { + *last_title = title; + true + } else { + false + } + }) { + self.0.force_update(); + } else { + SpaceElement::refresh(&self.0); + } + } +} + +impl KeyboardTarget for CosmicWindow { + fn enter( + &self, + seat: &Seat, + data: &mut State, + keys: Vec>, + serial: Serial, + ) { + self.0 + .with_program(|p| KeyboardTarget::enter(&p.window, seat, data, keys, serial)) + } + fn leave(&self, seat: &Seat, data: &mut State, serial: Serial) { + self.0 + .with_program(|p| KeyboardTarget::leave(&p.window, seat, data, serial)) + } + fn key( + &self, + seat: &Seat, + data: &mut State, + key: KeysymHandle<'_>, + state: KeyState, + serial: Serial, + time: u32, + ) { + self.0 + .with_program(|p| KeyboardTarget::key(&p.window, seat, data, key, state, serial, time)) + } + fn modifiers( + &self, + seat: &Seat, + data: &mut State, + modifiers: ModifiersState, + serial: Serial, + ) { + self.0 + .with_program(|p| KeyboardTarget::modifiers(&p.window, seat, data, modifiers, serial)) + } +} + +impl PointerTarget for CosmicWindow { + fn enter(&self, seat: &Seat, data: &mut State, event: &MotionEvent) { + let mut event = event.clone(); + self.0.with_program(|p| { + let has_ssd = p.has_ssd(false); + if has_ssd || p.has_tiled_state() { + let Some(next) = Focus::under( + &p.window, + if has_ssd { SSD_HEIGHT } else { 0 }, + event.location, + ) else { + return; + }; + + let old_focus = p.swap_focus(Some(next)); + assert_eq!(old_focus, None); + + let cursor_state = seat.user_data().get::().unwrap(); + cursor_state.lock().unwrap().set_shape(next.cursor_shape()); + seat.set_cursor_image_status(CursorImageStatus::default_named()); + } + }); + + event.location -= self.0.with_program(|p| p.window.geometry().loc.to_f64()); + PointerTarget::enter(&self.0, seat, data, &event) + } + + fn motion(&self, seat: &Seat, data: &mut State, event: &MotionEvent) { + let mut event = event.clone(); + self.0.with_program(|p| { + let has_ssd = p.has_ssd(false); + if has_ssd || p.has_tiled_state() { + let Some(next) = Focus::under( + &p.window, + if has_ssd { SSD_HEIGHT } else { 0 }, + event.location, + ) else { + return; + }; + let _previous = p.swap_focus(Some(next)); + + let cursor_state = seat.user_data().get::().unwrap(); + cursor_state.lock().unwrap().set_shape(next.cursor_shape()); + seat.set_cursor_image_status(CursorImageStatus::default_named()); + } + }); + + event.location -= self.0.with_program(|p| p.window.geometry().loc.to_f64()); + PointerTarget::motion(&self.0, seat, data, &event) + } + + fn relative_motion( + &self, + _seat: &Seat, + _data: &mut State, + _event: &RelativeMotionEvent, + ) { + } + + fn button(&self, seat: &Seat, data: &mut State, event: &ButtonEvent) { + match self.0.with_program(|p| p.current_focus()) { + Some(Focus::Header) => PointerTarget::button(&self.0, seat, data, event), + Some(x) => { + let serial = event.serial; + let seat = seat.clone(); + let Some(surface) = self.wl_surface().map(Cow::into_owned) else { + return; + }; + + // Only start a resize if the left button was pressed + if event.state != smithay::backend::input::ButtonState::Pressed + || event.button != 0x110 + { + return; + } + self.0.loop_handle().insert_idle(move |state| { + let res = state.common.shell.write().resize_request( + &surface, + &seat, + serial, + match x { + Focus::ResizeTop => ResizeEdge::TOP, + Focus::ResizeTopLeft => ResizeEdge::TOP_LEFT, + Focus::ResizeTopRight => ResizeEdge::TOP_RIGHT, + Focus::ResizeBottom => ResizeEdge::BOTTOM, + Focus::ResizeBottomLeft => ResizeEdge::BOTTOM_LEFT, + Focus::ResizeBottomRight => ResizeEdge::BOTTOM_RIGHT, + Focus::ResizeLeft => ResizeEdge::LEFT, + Focus::ResizeRight => ResizeEdge::RIGHT, + Focus::Header => unreachable!(), + }, + state.common.config.cosmic_conf.edge_snap_threshold, + false, + ); + + if let Some((grab, focus)) = res { + if grab.is_touch_grab() { + seat.get_touch().unwrap().set_grab(state, grab, serial); + } else { + seat.get_pointer() + .unwrap() + .set_grab(state, grab, serial, focus); + } + } + }); + } + None => {} + } + } + + fn axis(&self, seat: &Seat, data: &mut State, frame: AxisFrame) { + if let Some(Focus::Header) = self.0.with_program(|p| p.current_focus()) { + PointerTarget::axis(&self.0, seat, data, frame) + } + } + + fn frame(&self, seat: &Seat, data: &mut State) { + if let Some(Focus::Header) = self.0.with_program(|p| p.current_focus()) { + PointerTarget::frame(&self.0, seat, data) + } + } + + fn leave(&self, seat: &Seat, data: &mut State, serial: Serial, time: u32) { + self.0.with_program(|p| { + let cursor_state = seat.user_data().get::().unwrap(); + cursor_state.lock().unwrap().unset_shape(); + let _previous = p.swap_focus(None); + }); + PointerTarget::leave(&self.0, seat, data, serial, time) + } + + fn gesture_swipe_begin( + &self, + _seat: &Seat, + _data: &mut State, + _event: &GestureSwipeBeginEvent, + ) { + } + + fn gesture_swipe_update( + &self, + _seat: &Seat, + _data: &mut State, + _event: &GestureSwipeUpdateEvent, + ) { + } + + fn gesture_swipe_end( + &self, + _seat: &Seat, + _data: &mut State, + _event: &GestureSwipeEndEvent, + ) { + } + + fn gesture_pinch_begin( + &self, + _seat: &Seat, + _data: &mut State, + _event: &GesturePinchBeginEvent, + ) { + } + + fn gesture_pinch_update( + &self, + _seat: &Seat, + _data: &mut State, + _event: &GesturePinchUpdateEvent, + ) { + } + + fn gesture_pinch_end( + &self, + _seat: &Seat, + _data: &mut State, + _event: &GesturePinchEndEvent, + ) { + } + + fn gesture_hold_begin( + &self, + _seat: &Seat, + _data: &mut State, + _event: &GestureHoldBeginEvent, + ) { + } + + fn gesture_hold_end( + &self, + _seat: &Seat, + _data: &mut State, + _event: &GestureHoldEndEvent, + ) { + } +} + +impl TouchTarget for CosmicWindow { + fn down(&self, seat: &Seat, data: &mut State, event: &DownEvent) { + let mut event = event.clone(); + self.0.with_program(|p| { + event.location -= p.window.geometry().loc.to_f64(); + }); + TouchTarget::down(&self.0, seat, data, &event) + } + + fn up(&self, seat: &Seat, data: &mut State, event: &UpEvent) { + TouchTarget::up(&self.0, seat, data, event) + } + + fn motion(&self, seat: &Seat, data: &mut State, event: &TouchMotionEvent) { + let mut event = event.clone(); + event.location -= self.0.with_program(|p| p.window.geometry().loc.to_f64()); + TouchTarget::motion(&self.0, seat, data, &event) + } + + fn frame(&self, seat: &Seat, data: &mut State, frame: FrameMarker) { + TouchTarget::frame(&self.0, seat, data, frame) + } + + fn cancel(&self, seat: &Seat, data: &mut State, frame: FrameMarker) { + TouchTarget::cancel(&self.0, seat, data, frame) + } + + fn shape(&self, seat: &Seat, data: &mut State, event: &ShapeEvent) { + TouchTarget::shape(&self.0, seat, data, event) + } + + fn orientation(&self, seat: &Seat, data: &mut State, event: &OrientationEvent) { + TouchTarget::orientation(&self.0, seat, data, event) + } + + fn last_frame(&self, seat: &Seat, data: &mut State) -> Option { + TouchTarget::last_frame(&self.0, seat, data) + } +} + +impl WaylandFocus for CosmicWindow { + fn wl_surface(&self) -> Option> { + self.0.with_program(|p| { + p.window + .wl_surface() + .map(|s| Cow::Owned(Cow::into_owned(s))) + }) + } + + fn same_client_as(&self, object_id: &ObjectId) -> bool { + self.0.with_program(|p| p.window.same_client_as(object_id)) + } +} + +pub enum CosmicWindowRenderElement { + Header(IcedRenderElement), + Shadow(PixelShaderElement), + Border(PixelShaderElement), + Window(SurfaceRenderElement), +} + +impl From> + for CosmicWindowRenderElement +{ + fn from(value: IcedRenderElement) -> Self { + Self::Header(value) + } +} + +impl From> + for CosmicWindowRenderElement +{ + fn from(value: SurfaceRenderElement) -> Self { + Self::Window(value) + } +} + +impl Element for CosmicWindowRenderElement +where + R: Renderer + ImportAll + ImportMem + AsGlowRenderer, + R::TextureId: Send + 'static, +{ + fn id(&self) -> &RendererId { + match self { + CosmicWindowRenderElement::Header(elem) => elem.id(), + CosmicWindowRenderElement::Shadow(elem) => elem.id(), + CosmicWindowRenderElement::Border(elem) => elem.id(), + CosmicWindowRenderElement::Window(elem) => elem.id(), + } + } + + fn current_commit(&self) -> CommitCounter { + match self { + CosmicWindowRenderElement::Header(elem) => elem.current_commit(), + CosmicWindowRenderElement::Shadow(elem) => elem.current_commit(), + CosmicWindowRenderElement::Border(elem) => elem.current_commit(), + CosmicWindowRenderElement::Window(elem) => elem.current_commit(), + } + } + + fn src(&self) -> Rectangle { + match self { + CosmicWindowRenderElement::Header(elem) => elem.src(), + CosmicWindowRenderElement::Shadow(elem) => elem.src(), + CosmicWindowRenderElement::Border(elem) => elem.src(), + CosmicWindowRenderElement::Window(elem) => elem.src(), + } + } + + fn geometry(&self, scale: Scale) -> Rectangle { + match self { + CosmicWindowRenderElement::Header(elem) => elem.geometry(scale), + CosmicWindowRenderElement::Shadow(elem) => elem.geometry(scale), + CosmicWindowRenderElement::Border(elem) => elem.geometry(scale), + CosmicWindowRenderElement::Window(elem) => elem.geometry(scale), + } + } + + fn location(&self, scale: Scale) -> Point { + match self { + CosmicWindowRenderElement::Header(elem) => elem.location(scale), + CosmicWindowRenderElement::Shadow(elem) => elem.location(scale), + CosmicWindowRenderElement::Border(elem) => elem.location(scale), + CosmicWindowRenderElement::Window(elem) => elem.location(scale), + } + } + + fn transform(&self) -> Transform { + match self { + CosmicWindowRenderElement::Header(elem) => elem.transform(), + CosmicWindowRenderElement::Shadow(elem) => elem.transform(), + CosmicWindowRenderElement::Border(elem) => elem.transform(), + CosmicWindowRenderElement::Window(elem) => elem.transform(), + } + } + + fn damage_since( + &self, + scale: Scale, + commit: Option, + ) -> DamageSet { + match self { + CosmicWindowRenderElement::Header(elem) => elem.damage_since(scale, commit), + CosmicWindowRenderElement::Shadow(elem) => elem.damage_since(scale, commit), + CosmicWindowRenderElement::Border(elem) => elem.damage_since(scale, commit), + CosmicWindowRenderElement::Window(elem) => elem.damage_since(scale, commit), + } + } + + fn opaque_regions(&self, scale: Scale) -> OpaqueRegions { + match self { + CosmicWindowRenderElement::Header(elem) => elem.opaque_regions(scale), + CosmicWindowRenderElement::Shadow(elem) => elem.opaque_regions(scale), + CosmicWindowRenderElement::Border(elem) => elem.opaque_regions(scale), + CosmicWindowRenderElement::Window(elem) => elem.opaque_regions(scale), + } + } + + fn alpha(&self) -> f32 { + match self { + CosmicWindowRenderElement::Header(elem) => elem.alpha(), + CosmicWindowRenderElement::Shadow(elem) => elem.alpha(), + CosmicWindowRenderElement::Border(elem) => elem.alpha(), + CosmicWindowRenderElement::Window(elem) => elem.alpha(), + } + } + + fn kind(&self) -> Kind { + match self { + CosmicWindowRenderElement::Header(elem) => elem.kind(), + CosmicWindowRenderElement::Shadow(elem) => elem.kind(), + CosmicWindowRenderElement::Border(elem) => elem.kind(), + CosmicWindowRenderElement::Window(elem) => elem.kind(), + } + } + + fn is_framebuffer_effect(&self) -> bool { + match self { + CosmicWindowRenderElement::Header(elem) => elem.is_framebuffer_effect(), + CosmicWindowRenderElement::Shadow(elem) => elem.is_framebuffer_effect(), + CosmicWindowRenderElement::Border(elem) => elem.is_framebuffer_effect(), + CosmicWindowRenderElement::Window(elem) => elem.is_framebuffer_effect(), + } + } +} + +impl RenderElement for CosmicWindowRenderElement +where + R: AsGlowRenderer, + R::TextureId: Send + 'static, +{ + fn draw( + &self, + frame: &mut ::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + cache: Option<&UserDataMap>, + ) -> Result<(), ::Error> { + match self { + CosmicWindowRenderElement::Header(elem) => { + elem.draw(frame, src, dst, damage, opaque_regions, cache) + } + CosmicWindowRenderElement::Shadow(elem) | CosmicWindowRenderElement::Border(elem) => { + RenderElement::::draw( + elem, + R::glow_frame_mut(frame), + src, + dst, + damage, + opaque_regions, + cache, + ) + .map_err(R::from_gles_error) + } + CosmicWindowRenderElement::Window(elem) => { + elem.draw(frame, src, dst, damage, opaque_regions, cache) + } + } + } + + fn underlying_storage(&self, renderer: &mut R) -> Option> { + match self { + CosmicWindowRenderElement::Header(elem) => elem.underlying_storage(renderer), + CosmicWindowRenderElement::Shadow(elem) | CosmicWindowRenderElement::Border(elem) => { + elem.underlying_storage(renderer.glow_renderer_mut()) + } + CosmicWindowRenderElement::Window(elem) => elem.underlying_storage(renderer), + } + } + + fn capture_framebuffer( + &self, + frame: &mut ::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + cache: &UserDataMap, + ) -> Result<(), ::Error> { + match self { + CosmicWindowRenderElement::Header(elem) => { + elem.capture_framebuffer(frame, src, dst, cache) + } + CosmicWindowRenderElement::Shadow(elem) | CosmicWindowRenderElement::Border(elem) => { + RenderElement::::capture_framebuffer( + elem, + R::glow_frame_mut(frame), + src, + dst, + cache, + ) + .map_err(R::from_gles_error) + } + CosmicWindowRenderElement::Window(elem) => { + elem.capture_framebuffer(frame, src, dst, cache) + } + } + } +} diff --git a/src/shell/focus/mod.rs b/src/shell/focus/mod.rs new file mode 100644 index 0000000..cab37a8 --- /dev/null +++ b/src/shell/focus/mod.rs @@ -0,0 +1,793 @@ +use crate::{ + shell::{CosmicSurface, MinimizedWindow, Shell, Trigger, element::CosmicMapped}, + state::{Common, State}, + utils::prelude::*, + wayland::handlers::{xdg_shell::PopupGrabData, xwayland_keyboard_grab::XWaylandGrabSeatData}, +}; +use indexmap::IndexSet; +use smithay::{ + desktop::{PopupUngrabStrategy, layer_map_for_output}, + input::{Seat, pointer::MotionEvent}, + output::Output, + reexports::wayland_server::{Resource, protocol::wl_surface::WlSurface}, + utils::{IsAlive, Point, SERIAL_COUNTER, Serial}, + wayland::{ + pointer_constraints::with_pointer_constraint, + seat::WaylandFocus, + selection::{data_device::set_data_device_focus, primary_selection::set_primary_focus}, + shell::wlr_layer::{KeyboardInteractivity, Layer}, + }, +}; +use std::{borrow::Cow, hash::Hash, mem, sync::Mutex}; + +use tracing::{debug, trace}; + +pub use self::order::{Stage, render_input_order}; +use self::target::{KeyboardFocusTarget, WindowGroup}; + +use super::{SeatExt, grabs::SeatMoveGrabState, layout::floating::FloatingLayout}; + +mod order; +pub mod target; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FocusTarget { + Window(CosmicMapped), + Fullscreen(CosmicSurface), +} + +impl PartialEq for FocusTarget { + fn eq(&self, other: &CosmicMapped) -> bool { + matches!(self, FocusTarget::Window(mapped) if mapped == other) + } +} + +impl PartialEq for FocusTarget { + fn eq(&self, other: &CosmicSurface) -> bool { + matches!(self, FocusTarget::Fullscreen(surface) if surface == other) + } +} + +impl indexmap::Equivalent for CosmicMapped { + fn equivalent(&self, key: &FocusTarget) -> bool { + key == self + } +} + +impl indexmap::Equivalent for CosmicSurface { + fn equivalent(&self, key: &FocusTarget) -> bool { + key == self + } +} + +impl Hash for FocusTarget { + fn hash(&self, state: &mut H) { + match self { + FocusTarget::Window(window) => window.hash(state), + FocusTarget::Fullscreen(surface) => surface.hash(state), + } + } +} + +impl From for FocusTarget { + fn from(value: CosmicMapped) -> Self { + Self::Window(value) + } +} + +impl From for FocusTarget { + fn from(value: CosmicSurface) -> Self { + Self::Fullscreen(value) + } +} + +impl From for KeyboardFocusTarget { + fn from(val: FocusTarget) -> Self { + match val { + FocusTarget::Window(mapped) => KeyboardFocusTarget::Element(mapped), + FocusTarget::Fullscreen(surface) => KeyboardFocusTarget::Fullscreen(surface), + } + } +} + +impl FocusTarget { + pub fn alive(&self) -> bool { + match self { + FocusTarget::Window(mapped) => mapped.alive(), + FocusTarget::Fullscreen(surface) => surface.alive(), + } + } + + fn is_minimized(&self) -> bool { + match self { + FocusTarget::Window(mapped) => mapped.is_minimized(), + FocusTarget::Fullscreen(surface) => surface.is_minimized(), + } + } + + pub fn wl_surface(&self) -> Option { + match self { + FocusTarget::Window(mapped) => mapped.active_window().wl_surface().map(Cow::into_owned), + FocusTarget::Fullscreen(surface) => surface.wl_surface().map(Cow::into_owned), + } + } +} + +pub struct FocusStack<'a>(pub(super) Option<&'a IndexSet>); +pub struct FocusStackMut<'a>(pub(super) &'a mut IndexSet); + +impl FocusStack<'_> { + /// returns the last unminimized window in the focus stack that is still alive + pub fn last(&self) -> Option<&FocusTarget> { + self.0 + .as_ref() + .and_then(|set| set.iter().rev().find(|w| w.alive() && !w.is_minimized())) + } + + pub fn iter(&self) -> impl Iterator { + self.0 + .iter() + .flat_map(|set| set.iter().rev().filter(|w| w.alive() && !w.is_minimized())) + } +} + +impl FocusStackMut<'_> { + pub fn append(&mut self, target: impl Into) { + let target = target.into(); + self.0.retain(|w| w.alive()); + self.0.shift_remove(&target); + self.0.insert(target); + } + + pub fn remove(&mut self, target: &T) -> bool + where + T: Hash + indexmap::Equivalent, + { + self.0.shift_remove(target) + } + + pub fn last(&self) -> Option<&FocusTarget> { + self.0.iter().rev().find(|w| w.alive() && !w.is_minimized()) + } + + pub fn iter(&self) -> impl Iterator { + self.0 + .iter() + .rev() + .filter(|w| w.alive() && !w.is_minimized()) + } +} + +pub struct ActiveFocus(Mutex>); + +impl ActiveFocus { + fn set(seat: &Seat, target: Option) { + if !seat + .user_data() + .insert_if_missing_threadsafe(|| ActiveFocus(Mutex::new(target.clone()))) + { + *seat + .user_data() + .get::() + .unwrap() + .0 + .lock() + .unwrap() = target; + } + } + + fn get(seat: &Seat) -> Option { + seat.user_data() + .get::() + .and_then(|a| a.0.lock().unwrap().clone()) + } +} + +impl Shell { + /// Set the keyboard focus to the given target + /// Note: `update_cursor` is used to determine whether to update the pointer location if cursor_follows_focus is enabled + /// if the focus change was due to a pointer event, this should be set to false + #[profiling::function] + pub fn set_focus( + state: &mut State, + target: Option<&KeyboardFocusTarget>, + seat: &Seat, + serial: Option, + update_cursor: bool, + ) { + let focus_target = match target { + Some(KeyboardFocusTarget::Element(mapped)) => Some(FocusTarget::Window(mapped.clone())), + Some(KeyboardFocusTarget::Fullscreen(surface)) => { + Some(FocusTarget::Fullscreen(surface.clone())) + } + _ => None, + }; + + if let Some(target) = focus_target { + state.common.shell.write().append_focus_stack(target, seat); + } + + update_focus_state(seat, target, state, serial, update_cursor); + + state.common.shell.write().update_active(); + } + + // We suppress Element(X) to Fullscreen(X) transition to avoid + // loss of focus by the X window when having a transition to fullscreen + // but in the case of X11 unmap/map the leave/enter needs to happen for the X11 + // internal state to be focused on the window + pub fn set_focus_on_x11_map( + state: &mut State, + target: &KeyboardFocusTarget, + seat: &Seat, + update_cursor: bool, + ) { + let need_reset = seat + .get_keyboard() + .and_then(|keyboard| keyboard.current_focus()) + .and_then(|current| current.x11_surface()) + .is_some_and(|current| Some(current) == target.x11_surface()); + + if need_reset { + update_focus_state(seat, None, state, None, false); + } + + Shell::set_focus(state, Some(target), seat, None, update_cursor); + } + + pub fn append_focus_stack(&mut self, target: impl Into, seat: &Seat) { + let target = target.into(); + if target.is_minimized() { + return; + } + + // update FocusStack and notify layouts about new focus (if any window) + let workspace = target + .wl_surface() + .and_then(|surface| self.workspace_for_surface(&surface)); + let workspace = if let Some(workspace) = workspace { + self.workspaces.space_for_handle_mut(&workspace.0).unwrap() + } else { + //should this be the active output or the focused output? + self.active_space_mut(&seat.focused_or_active_output()) + .unwrap() + }; + + let mut focus_stack = workspace.focus_stack.get_mut(seat); + if Some(&target) != focus_stack.last() { + trace!(?target, "Focusing window."); + focus_stack.append(target); + // also remove popup grabs, if we are switching focus + if let Some(mut popup_grab) = seat + .user_data() + .get::() + .and_then(|x| x.take()) + && !popup_grab.has_ended() + { + popup_grab.ungrab(PopupUngrabStrategy::All); + } + } + } + + fn update_active(&mut self) { + // update activate status + let focused_windows = self + .seats + .iter() + .filter_map(|seat| { + if matches!( + seat.get_keyboard().unwrap().current_focus(), + Some(KeyboardFocusTarget::Group(_)) | Some(KeyboardFocusTarget::LockSurface(_)) + ) { + return None; + } + + let output = seat.focused_or_active_output(); + let space = self.active_space(&output).unwrap(); + let stack = space.focus_stack.get(seat); + stack.last().and_then(|target| match target { + FocusTarget::Window(window) => Some(window.clone()), + FocusTarget::Fullscreen(_) => None, + }) + }) + .collect::>(); + + for output in self.outputs().cloned().collect::>().into_iter() { + let set = self.workspaces.sets.get_mut(&output).unwrap(); + for focused in focused_windows.iter() { + raise_with_children(&mut set.sticky_layer, focused); + } + for window in set.sticky_layer.mapped() { + window.set_activated(focused_windows.contains(window)); + window.configure(); + } + for window in set + .minimized_windows + .iter() + .flat_map(MinimizedWindow::mapped) + { + window.set_activated(false); + window.configure(); + } + + let workspace = &mut set.workspaces[set.active]; + for fs in workspace.get_fullscreen_surfaces() { + let is_focused = self.seats.iter().any(|seat| { + if let Some(KeyboardFocusTarget::Fullscreen(s)) = + seat.get_keyboard().unwrap().current_focus() + { + s == fs.surface + } else { + false + } + }); + fs.surface.set_activated(is_focused); + fs.surface.send_configure(); + } + for focused in focused_windows.iter() { + raise_with_children(&mut workspace.floating_layer, focused); + } + for window in workspace.mapped() { + window.set_activated(focused_windows.contains(window)); + window.configure(); + } + for m in workspace.minimized_windows.iter() { + if let Some(window) = m.mapped() { + window.set_activated(false); + window.configure(); + } else { + for surface in m.windows() { + surface.set_activated(false); + surface.send_configure(); + } + } + } + + for (i, workspace) in set.workspaces.iter().enumerate() { + if i == set.active { + continue; + } + for window in workspace.mapped() { + window.set_activated(false); + window.configure(); + } + } + } + } +} + +/// Internal, used to ensure that ActiveFocus, KeyboardFocusTarget, and FocusedOutput are all in sync +#[profiling::function] +fn update_focus_state( + seat: &Seat, + target: Option<&KeyboardFocusTarget>, + state: &mut State, + serial: Option, + should_update_cursor: bool, +) { + // update keyboard focus + if let Some(keyboard) = seat.get_keyboard() { + // remove constraint when target changed + let old_focus = keyboard.current_focus(); + if let Some(old_target) = old_focus + && target != Some(&old_target) + && let Some(surface) = old_target.wl_surface() + && let Some(pointer) = seat.get_pointer() + { + with_pointer_constraint(&surface, &pointer, |constraint| { + if let Some(constraint) = constraint { + constraint.deactivate(); + } + }); + } + + if should_update_cursor + && state.common.config.cosmic_conf.cursor_follows_focus + && target.is_some() + { + //need to borrow mutably for surface under + let shell = state.common.shell.read(); + // get the top left corner of the target element + let geometry = shell.focused_geometry(target.unwrap()); + if let Some(geometry) = geometry { + // get the center of the target element + let window_center = Point::from((geometry.size.w / 2, geometry.size.h / 2)); + let new_pos = (geometry.loc + window_center).to_f64(); + + // create a pointer target from the target element + let output = shell + .outputs() + .find(|output| output.geometry().to_f64().contains(new_pos)) + .cloned() + .unwrap_or(seat.active_output()); + + crate::input::update_output_image_copy_cursor_position( + &shell, + &state.common.clock, + &output, + seat, + new_pos, + ); + + let focus = State::surface_under(new_pos, &output, &shell) + .map(|(focus, loc)| (focus, loc.as_logical())); + //drop here to avoid multiple borrows + mem::drop(shell); + seat.get_pointer().unwrap().motion( + state, + focus, + &MotionEvent { + location: new_pos.as_logical(), + serial: SERIAL_COUNTER.next_serial(), + time: 0, + }, + ); + } + } + + if target.is_some_and(|t| { + matches!( + t, + KeyboardFocusTarget::LayerSurface(layer_surface) if layer_surface.cached_state().keyboard_interactivity == KeyboardInteractivity::Exclusive + ) + }) { + keyboard.unset_grab(state); + } + let serial = serial.unwrap_or_else(|| SERIAL_COUNTER.next_serial()); + state + .common + .xwayland_notify_focus_change(target.cloned(), serial); + ActiveFocus::set(seat, target.cloned()); + keyboard.set_focus(state, target.cloned(), serial); + std::mem::drop(keyboard); + + //update the focused output or set it to the active output + if target.is_some() { + // Get focused output calls visible_output_for_surface internally + // what should happen if the target is some, but it's not visible? + // should this be an error? + seat.set_focused_output( + state + .common + .shell + .read() + .get_output_for_focus(seat) + .as_ref(), + ) + } else { + seat.set_focused_output(None); + }; + } +} + +fn raise_with_children(floating_layer: &mut FloatingLayout, focused: &CosmicMapped) { + if floating_layer.mapped().any(|m| m == focused) { + floating_layer.space.raise_element(focused, true); + for element in floating_layer + .space + .elements() + .filter(|elem| elem != &focused) + .filter(|elem| { + let parent = elem + .active_window() + .0 + .toplevel() + .and_then(|toplevel| toplevel.parent()); + parent.is_some_and(|parent| { + focused + .active_window() + .wl_surface() + .map(Cow::into_owned) + .map(|focused| parent == focused) + .unwrap_or(false) + }) + }) + .cloned() + .collect::>() + .into_iter() + { + raise_with_children(floating_layer, &element); + } + } +} + +impl Common { + #[profiling::function] + pub fn refresh_focus(state: &mut State) { + let seats = state + .common + .shell + .read() + .seats + .iter() + .cloned() + .collect::>(); + for seat in &seats { + let mut xwayland_grab = seat + .user_data() + .get_or_insert(XWaylandGrabSeatData::default) + .grab + .lock() + .unwrap(); + xwayland_grab.take_if(|(surface, g)| !g.grab().is_alive() || !surface.alive()); + + { + let shell = state.common.shell.read(); + let focused_output = seat.focused_output(); + let active_output = seat.active_output(); + + // If the focused or active output is not in the list of outputs, switch to the first output + if focused_output.is_some_and(|f| !shell.outputs().any(|o| &f == o)) { + seat.set_focused_output(None); + } + if !shell.outputs().any(|o| o == &active_output) { + if let Some(other) = shell.outputs().next() { + seat.set_active_output(other); + } + continue; + } + } + + update_pointer_focus(state, seat); + + let output = seat.focused_or_active_output(); + let mut shell = state.common.shell.write(); + let last_known_focus = ActiveFocus::get(seat); + + if let Some(target) = last_known_focus { + if target.alive() { + if focus_target_is_valid(&mut shell, seat, &output, target) { + continue; // Focus is valid + } else { + trace!("Wrong Window, focus fixup"); + } + } else { + if let KeyboardFocusTarget::Popup(_) = target + && let Some(popup_grab) = seat + .user_data() + .get::() + .and_then(|x| x.take()) + && !popup_grab.has_ended() + && let Some(new) = popup_grab.current_grab() + { + trace!("restore focus to previous popup grab"); + std::mem::drop(shell); + // TODO: verify whether cursor should be updated at end of popup grab + update_focus_state( + seat, + Some(&new), + state, + Some(SERIAL_COUNTER.next_serial()), + false, + ); + seat.user_data() + .get_or_insert::(PopupGrabData::default) + .set(Some(popup_grab)); + continue; + } + trace!("Surface dead, focus fixup"); + } + } else { + let workspace = shell.active_space(&output).unwrap(); + let focus_stack = workspace.focus_stack.get(seat); + + if focus_stack.last().is_none() { + continue; // Focus is valid + } else { + trace!("No previous window, focus fixup"); + } + } + + // fixup focus + { + // also remove popup grabs, if we are switching focus + if let Some(mut popup_grab) = seat + .user_data() + .get::() + .and_then(|x| x.take()) + && !popup_grab.has_ended() + { + popup_grab.ungrab(PopupUngrabStrategy::All); + } + + // update keyboard focus + let target = update_focus_target(&shell, seat, &output); + std::mem::drop(shell); + //I can probably feature gate this condition + debug!("Restoring focus to {:?}", target.as_ref()); + + update_focus_state(seat, target.as_ref(), state, None, false); + } + } + + // Update clipboard and primary focus + // + // For now, needs to be here instead of in `focus_changed` to update focus + // when the active element of a stack changes. + for seat in &seats { + if let Some(keyboard) = seat.get_keyboard() { + let focus = keyboard.current_focus(); + let client = focus + .as_ref() + .and_then(|t| t.wl_surface()) + .and_then(|s| state.common.display_handle.get_client(s.id()).ok()); + set_data_device_focus(&state.common.display_handle, seat, client.clone()); + set_primary_focus(&state.common.display_handle, seat, client); + } + } + + state.common.shell.write().update_active() + } +} + +fn focus_target_is_valid( + shell: &mut Shell, + seat: &Seat, + output: &Output, + target: KeyboardFocusTarget, +) -> bool { + // If a session lock is active, only lock surfaces can be focused + if shell.session_lock.is_some() { + return matches!(target, KeyboardFocusTarget::LockSurface(_)); + } + + // If an exclusive layer shell surface exists (on any output), only exclusive + // shell surfaces can have focus, on the highest layer with exclusive surfaces. + if let Some(layer) = exclusive_layer_surface_layer(shell) { + return if let KeyboardFocusTarget::LayerSurface(layer_surface) = target { + let data = layer_surface.cached_state(); + (data.keyboard_interactivity, data.layer) == (KeyboardInteractivity::Exclusive, layer) + } else { + false + }; + } + + match target { + KeyboardFocusTarget::Element(mapped) => { + if seat + .user_data() + .get::() + .is_some_and(|state| { + state + .lock() + .unwrap() + .as_ref() + .is_some_and(|state| state.element() == mapped) + }) + { + return true; + } + + let is_sticky = shell + .workspaces + .sets + .get(output) + .unwrap() + .sticky_layer + .mapped() + .any(|m| m == &mapped); + + let workspace = shell.active_space(output).unwrap(); + let focus_stack = workspace.focus_stack.get(seat); + let is_in_focus_stack = focus_stack.last().map(|m| m == &mapped).unwrap_or(false); + if is_sticky && !is_in_focus_stack { + shell.append_focus_stack(mapped, seat); + } + + is_sticky || is_in_focus_stack + } + KeyboardFocusTarget::LayerSurface(layer) => { + layer_map_for_output(output).layers().any(|l| l == &layer) + } + KeyboardFocusTarget::Group(WindowGroup { node, .. }) => shell + .workspaces + .active(output) + .unwrap() + .1 + .tiling_layer + .has_node(&node), + KeyboardFocusTarget::Fullscreen(window) => { + let workspace = shell.active_space(output).unwrap(); + workspace + .get_fullscreen_surfaces() + .any(|f| f.surface == window) + } + KeyboardFocusTarget::Popup(_) => true, + KeyboardFocusTarget::LockSurface(_) => false, + } +} + +fn update_focus_target( + shell: &Shell, + seat: &Seat, + output: &Output, +) -> Option { + if let Some(session_lock) = &shell.session_lock { + session_lock + .surfaces + .get(output) + .cloned() + .map(KeyboardFocusTarget::from) + } else if let Some(layer) = exclusive_layer_surface_layer(shell) { + layer_map_for_output(output) + .layers() + .find(|layer_surface| { + let data = layer_surface.cached_state(); + (data.keyboard_interactivity, data.layer) + == (KeyboardInteractivity::Exclusive, layer) + }) + .cloned() + .map(KeyboardFocusTarget::from) + } else { + let workspace = shell.active_space(output).unwrap(); + + if let Some(Trigger::KeyboardSwap(_, desc)) = shell.overview_mode().0.active_trigger() + && workspace.handle == desc.handle + && workspace.tiling_layer.has_node(&desc.node) + && let Some(focus) = workspace.tiling_layer.node_desc_to_focus(desc) + { + return Some(focus); + } + + workspace + .focus_stack + .get(seat) + .last() + .cloned() + .map(Into::::into) + .or_else(|| { + workspace + .mapped() + .next() + .cloned() + .map(KeyboardFocusTarget::Element) + .or_else(|| { + workspace + .get_fullscreen(seat) + .cloned() + .map(|fs| KeyboardFocusTarget::Fullscreen(fs.surface)) + }) + }) + } +} + +fn update_pointer_focus(state: &mut State, seat: &Seat) { + if let Some(pointer) = seat.get_pointer() { + let output = seat.active_output(); + let position = pointer.current_location().as_global(); + + let shell = state.common.shell.write(); + let under = State::surface_under(position, &output, &shell) + .map(|(target, pos)| (target, pos.as_logical())); + drop(shell); + + if pointer.current_focus().as_ref() != under.as_ref().map(|(target, _)| target) { + pointer.motion( + state, + under, + &MotionEvent { + location: pointer.current_location(), + serial: SERIAL_COUNTER.next_serial(), + time: state.common.clock.now().as_millis(), + }, + ); + } + } +} + +// Get the top-most layer, if any, with at least one surface with exclusive keyboard interactivity. +// Only considers surface in `Top` or `Overlay` layer. +fn exclusive_layer_surface_layer(shell: &Shell) -> Option { + let mut layer = None; + for output in shell.outputs() { + for layer_surface in layer_map_for_output(output).layers() { + let data = layer_surface.cached_state(); + if data.keyboard_interactivity == KeyboardInteractivity::Exclusive + && data.layer as u32 >= layer.unwrap_or(Layer::Top) as u32 + { + layer = Some(data.layer); + } + } + } + layer +} diff --git a/src/shell/focus/order.rs b/src/shell/focus/order.rs new file mode 100644 index 0000000..fb904c0 --- /dev/null +++ b/src/shell/focus/order.rs @@ -0,0 +1,448 @@ +use std::{ops::ControlFlow, time::Instant}; + +use cosmic_comp_config::workspace::WorkspaceLayout; +use keyframe::{ease, functions::EaseInOutCubic}; +use smithay::{ + desktop::{LayerSurface, PopupKind, PopupManager, layer_map_for_output}, + output::{Output, OutputNoMode}, + utils::{Logical, Point}, + wayland::{session_lock::LockSurface, shell::wlr_layer::Layer}, + xwayland::X11Surface, +}; + +use crate::{ + backend::render::ElementFilter, + shell::{ + SeatExt, Shell, Workspace, WorkspaceDelta, + focus::target::KeyboardFocusTarget, + layout::{floating::FloatingLayout, tiling::ANIMATION_DURATION}, + }, + utils::{ + geometry::*, + prelude::OutputExt, + quirks::{WORKSPACE_OVERVIEW_NAMESPACE, workspace_overview_is_open}, + }, + wayland::protocols::workspace::WorkspaceHandle, +}; + +pub enum Stage<'a> { + ZoomUI, + SessionLock(Option<&'a LockSurface>), + LayerPopup { + layer: LayerSurface, + popup: &'a PopupKind, + location: Point, + workspace_idx: usize, + }, + LayerSurface { + layer: LayerSurface, + location: Point, + workspace_idx: usize, + }, + OverrideRedirect { + surface: &'a X11Surface, + location: Point, + }, + StickyPopups(&'a FloatingLayout), + Sticky(&'a FloatingLayout), + WorkspacePopups { + workspace: &'a Workspace, + offset: Point, + }, + Workspace { + workspace: &'a Workspace, + offset: Point, + }, +} + +pub fn render_input_order( + shell: &Shell, + output: &Output, + previous: Option<(WorkspaceHandle, usize, WorkspaceDelta)>, + current: (WorkspaceHandle, usize), + element_filter: ElementFilter, + callback: impl FnMut(Stage) -> ControlFlow, ()>, +) -> Result { + match render_input_order_internal(shell, output, previous, current, element_filter, callback) { + ControlFlow::Break(result) => result, + ControlFlow::Continue(_) => Ok(R::default()), + } +} + +fn render_input_order_internal( + shell: &Shell, + output: &Output, + previous: Option<(WorkspaceHandle, usize, WorkspaceDelta)>, + current: (WorkspaceHandle, usize), + element_filter: ElementFilter, + mut callback: impl FnMut(Stage) -> ControlFlow, ()>, +) -> ControlFlow, ()> { + if shell + .zoom_state + .as_ref() + .is_some_and(|state| state.show_overlay && state.current_level(output) != 1.0) + { + callback(Stage::ZoomUI)?; + } + + // Session Lock + if let Some(session_lock) = &shell.session_lock { + return callback(Stage::SessionLock(session_lock.surfaces.get(output))); + } + + // Overlay-level layer shell + // overlay is above everything + for (layer, popup, location) in layer_popups(output, Layer::Overlay, element_filter) { + callback(Stage::LayerPopup { + layer, + popup: &popup, + location, + workspace_idx: current.1, + })?; + } + for (layer, location) in layer_surfaces(output, Layer::Overlay, element_filter) { + callback(Stage::LayerSurface { + layer, + location, + workspace_idx: current.1, + })?; + } + + // calculate a bunch of stuff for workspace transitions + + let Some(set) = shell.workspaces.sets.get(output) else { + return ControlFlow::Break(Err(OutputNoMode)); + }; + let Some(workspace) = set.workspaces.iter().find(|w| w.handle == current.0) else { + return ControlFlow::Break(Err(OutputNoMode)); + }; + let output_size = output.geometry().size; + + // this is more hacky than I would like.. + let seat = shell.seats.last_active(); + let fullscreen = workspace.get_fullscreen(seat); + let is_active_workspace = seat.focused_output().is_some_and(|output| { + shell + .active_space(&output) + .is_some_and(|w| w.handle == workspace.handle) + }); + let focus_stack_is_valid_fullscreen = workspace + .focus_stack + .get(seat) + .last() + .zip(fullscreen) + .is_some_and(|(target, fullscreen)| target == &fullscreen.surface); + let overview_is_open = workspace_overview_is_open(output); + let has_focused_fullscreen = if is_active_workspace { + let current_focus = seat.get_keyboard().unwrap().current_focus(); + matches!(current_focus, Some(KeyboardFocusTarget::Fullscreen(_))) + || (current_focus.is_none() + && focus_stack_is_valid_fullscreen + && !workspace_overview_is_open(output)) + } else { + focus_stack_is_valid_fullscreen && !overview_is_open + }; + let has_fullscreen = fullscreen.is_some() && !overview_is_open; + + let (previous, current_offset) = match previous.as_ref() { + Some((previous, previous_idx, start)) => { + let layout = shell.workspaces.layout; + + let Some(workspace) = shell.workspaces.space_for_handle(previous) else { + return ControlFlow::Break(Err(OutputNoMode)); + }; + let has_fullscreen = workspace.get_fullscreen(seat).is_some(); + + let (forward, percentage) = match start { + WorkspaceDelta::Shortcut(st) => ( + *previous_idx < current.1, + ease( + EaseInOutCubic, + 0.0, + 1.0, + Instant::now().duration_since(*st).as_millis() as f32 + / ANIMATION_DURATION.as_millis() as f32, + ), + ), + WorkspaceDelta::Gesture { + percentage: prog, + forward, + } => (*forward, *prog as f32), + WorkspaceDelta::GestureEnd { + start, + spring, + forward, + } => ( + *forward, + (spring.value_at(Instant::now().duration_since(*start)) as f32).clamp(0.0, 1.0), + ), + }; + + let offset = Point::::from(match (layout, forward) { + (WorkspaceLayout::Vertical, true) => { + (0, (-output_size.h as f32 * percentage).round() as i32) + } + (WorkspaceLayout::Vertical, false) => { + (0, (output_size.h as f32 * percentage).round() as i32) + } + (WorkspaceLayout::Horizontal, true) => { + ((-output_size.w as f32 * percentage).round() as i32, 0) + } + (WorkspaceLayout::Horizontal, false) => { + ((output_size.w as f32 * percentage).round() as i32, 0) + } + }); + + ( + Some((previous, previous_idx, has_fullscreen, offset)), + Point::::from(match (layout, forward) { + (WorkspaceLayout::Vertical, true) => (0, output_size.h + offset.y), + (WorkspaceLayout::Vertical, false) => (0, -(output_size.h - offset.y)), + (WorkspaceLayout::Horizontal, true) => (output_size.w + offset.x, 0), + (WorkspaceLayout::Horizontal, false) => (-(output_size.w - offset.x), 0), + }), + ) + } + None => (None, Point::default()), + }; + + // Top-level layer shell popups + if !has_focused_fullscreen { + for (layer, popup, location) in layer_popups(output, Layer::Top, element_filter) { + callback(Stage::LayerPopup { + layer, + popup: &popup, + location, + workspace_idx: current.1, + })?; + } + } + + if element_filter != ElementFilter::LayerShellOnly { + // overlay redirect windows + // they need to be over sticky windows, because they could be popups of sticky windows, + // and we can't differenciate that. + for (surface, location) in shell + .override_redirect_windows + .iter() + .rev() + .filter(|or| { + (*or) + .last_configure() + .as_global() + .intersection(output.geometry()) + .is_some() + }) + .map(|or| (or, or.last_configure().loc.as_global())) + { + callback(Stage::OverrideRedirect { surface, location })?; + } + + // sticky window popups + callback(Stage::StickyPopups(&set.sticky_layer))?; + } + + if element_filter != ElementFilter::LayerShellOnly { + // previous workspace popups + if let Some((previous_handle, _, _, offset)) = previous.as_ref() { + let Some(workspace) = shell.workspaces.space_for_handle(previous_handle) else { + return ControlFlow::Break(Err(OutputNoMode)); + }; + + callback(Stage::WorkspacePopups { + workspace, + offset: *offset, + })?; + } + + // current workspace popups + let Some(workspace) = shell.workspaces.space_for_handle(¤t.0) else { + return ControlFlow::Break(Err(OutputNoMode)); + }; + + callback(Stage::WorkspacePopups { + workspace, + offset: current_offset, + })?; + } + + if !has_focused_fullscreen { + // bottom layer popups + for (layer, popup, location) in layer_popups(output, Layer::Bottom, element_filter) { + callback(Stage::LayerPopup { + layer, + popup: &popup, + location, + workspace_idx: current.1, + })?; + } + } + + if let Some((_, idx, has_fullscreen, offset)) = previous.as_ref() + && !has_fullscreen + { + // previous bottom layer popups + for (layer, popup, location) in layer_popups(output, Layer::Bottom, element_filter) { + callback(Stage::LayerPopup { + layer, + popup: &popup, + location: location + offset.as_global(), + workspace_idx: **idx, + })?; + } + } + + if !has_fullscreen { + // background layer popups + for (layer, popup, location) in layer_popups(output, Layer::Background, element_filter) { + callback(Stage::LayerPopup { + layer, + popup: &popup, + location, + workspace_idx: current.1, + })?; + } + } + + if let Some((_, idx, has_fullscreen, offset)) = previous.as_ref() + && !has_fullscreen + { + // previous background layer popups + for (layer, popup, location) in layer_popups(output, Layer::Background, element_filter) { + callback(Stage::LayerPopup { + layer, + popup: &popup, + location: location + offset.as_global(), + workspace_idx: **idx, + })?; + } + } + + if !has_focused_fullscreen { + // top-layer shell + for (layer, location) in layer_surfaces(output, Layer::Top, element_filter) { + callback(Stage::LayerSurface { + layer, + location, + workspace_idx: current.1, + })?; + } + } + + // sticky windows + if element_filter != ElementFilter::LayerShellOnly { + callback(Stage::Sticky(&set.sticky_layer))?; + } + + if element_filter != ElementFilter::LayerShellOnly { + // workspace windows + callback(Stage::Workspace { + workspace, + offset: current_offset, + })?; + + // previous workspace windows + if let Some((previous_handle, _, _, offset)) = previous.as_ref() { + let Some(workspace) = shell.workspaces.space_for_handle(previous_handle) else { + return ControlFlow::Break(Err(OutputNoMode)); + }; + callback(Stage::Workspace { + workspace, + offset: *offset, + })?; + } + } + + if !has_focused_fullscreen { + // bottom layer + for (layer, mut location) in layer_surfaces(output, Layer::Bottom, element_filter) { + location += current_offset.as_global(); + callback(Stage::LayerSurface { + layer, + location, + workspace_idx: current.1, + })?; + } + } + + if let Some((_, idx, has_fullscreen, offset)) = previous.as_ref() + && !has_fullscreen + { + // previous bottom layer + for (layer, mut location) in layer_surfaces(output, Layer::Bottom, element_filter) { + location += offset.as_global(); + callback(Stage::LayerSurface { + layer, + location, + workspace_idx: **idx, + })?; + } + } + + if !has_fullscreen { + // background layer + for (layer, mut location) in layer_surfaces(output, Layer::Background, element_filter) { + location += current_offset.as_global(); + callback(Stage::LayerSurface { + layer, + location, + workspace_idx: current.1, + })?; + } + } + + if let Some((_, idx, has_fullscreen, offset)) = previous.as_ref() + && !has_fullscreen + { + // previous background layer + for (layer, mut location) in layer_surfaces(output, Layer::Background, element_filter) { + location += offset.as_global(); + callback(Stage::LayerSurface { + layer, + location, + workspace_idx: **idx, + })?; + } + } + + ControlFlow::Continue(()) +} + +fn layer_popups( + output: &Output, + layer: Layer, + element_filter: ElementFilter, +) -> impl Iterator)> + '_ { + layer_surfaces(output, layer, element_filter).flat_map(move |(surface, location)| { + let location_clone = location; + let surface_clone = surface.clone(); + PopupManager::popups_for_surface(surface.wl_surface()).map(move |(popup, popup_offset)| { + let offset = (popup_offset - popup.geometry().loc).as_global(); + (surface_clone.clone(), popup, (location_clone + offset)) + }) + }) +} + +fn layer_surfaces( + output: &Output, + layer: Layer, + element_filter: ElementFilter, +) -> impl Iterator)> + '_ { + // we want to avoid deadlocks on the layer-map in callbacks, so we need to clone the layer surfaces + let layers = { + let layer_map = layer_map_for_output(output); + layer_map + .layers_on(layer) + .rev() + .map(|s| (s.clone(), layer_map.layer_geometry(s).unwrap())) + .collect::>() + }; + + layers + .into_iter() + .filter(move |(s, _)| { + !(element_filter == ElementFilter::ExcludeWorkspaceOverview + && s.namespace() == WORKSPACE_OVERVIEW_NAMESPACE) + }) + .map(|(surface, geometry)| (surface, geometry.loc.as_local().to_global(output))) +} diff --git a/src/shell/focus/target.rs b/src/shell/focus/target.rs new file mode 100644 index 0000000..80b1a34 --- /dev/null +++ b/src/shell/focus/target.rs @@ -0,0 +1,850 @@ +use std::{ + borrow::Cow, + sync::{Arc, Weak}, + time::Duration, +}; + +use crate::{ + shell::{ + CosmicSurface, CursorGeometry, SeatExt, + element::{CosmicMapped, CosmicStack, CosmicWindow}, + layout::tiling::ResizeForkTarget, + zoom::ZoomFocusTarget, + }, + utils::prelude::*, + wayland::handlers::{image_copy_capture::SessionHolder, xdg_shell::popup::get_popup_toplevel}, +}; +use id_tree::NodeId; +use smithay::{ + backend::input::KeyState, + desktop::{LayerSurface, PopupKind, WindowSurface, WindowSurfaceType, space::SpaceElement}, + input::{ + Seat, + dnd::{DndFocus, OfferData, Source}, + keyboard::{KeyboardTarget, KeysymHandle, ModifiersState}, + pointer::{ + AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, + GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, + GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, + MotionEvent as PointerMotionEvent, PointerTarget, RelativeMotionEvent, + }, + touch::{ + DownEvent, FrameMarker, MotionEvent as TouchMotionEvent, OrientationEvent, ShapeEvent, + TouchTarget, UpEvent, + }, + }, + reexports::wayland_server::{ + Client, DisplayHandle, Resource, backend::ObjectId, protocol::wl_surface::WlSurface, + }, + utils::{IsAlive, Logical, Point, Serial, Transform}, + wayland::{ + compositor::with_states, seat::WaylandFocus, selection::data_device::WlOfferData, + session_lock::LockSurface, shell::xdg::SurfaceCachedState, + }, + xwayland::{ + X11Surface, + xwm::{XwmId, XwmOfferData}, + }, +}; + +#[derive(Debug, Clone, PartialEq)] +pub enum PointerFocusTarget { + WlSurface { + surface: WlSurface, + toplevel: Option, + }, + X11Surface { + surface: X11Surface, + toplevel: Option, + }, + StackUI(CosmicStack), + WindowUI(CosmicWindow), + ResizeFork(ResizeForkTarget), + ZoomUI(ZoomFocusTarget), +} + +#[derive(Debug, Clone, PartialEq)] +#[allow(clippy::large_enum_variant)] +pub enum PointerFocusToplevel { + Surface(CosmicSurface), + Popup(PopupKind), +} + +impl From for PointerFocusToplevel { + fn from(value: CosmicSurface) -> Self { + PointerFocusToplevel::Surface(value) + } +} + +impl From for PointerFocusToplevel { + fn from(value: PopupKind) -> Self { + PointerFocusToplevel::Popup(value) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum KeyboardFocusTarget { + Element(CosmicMapped), + Fullscreen(CosmicSurface), + Group(WindowGroup), + LayerSurface(LayerSurface), + Popup(PopupKind), + LockSurface(LockSurface), +} + +// TODO: This should be TryFrom, but PopupGrab needs to be able to convert. Fix this in smithay +impl From for PointerFocusTarget { + fn from(target: KeyboardFocusTarget) -> Self { + match target { + KeyboardFocusTarget::Element(elem) => { + let window = elem.active_window(); + + if let Some(xsurface) = window.x11_surface() { + PointerFocusTarget::X11Surface { + surface: xsurface.clone(), + toplevel: Some(window), + } + } else { + PointerFocusTarget::WlSurface { + surface: window.wl_surface().unwrap().into_owned(), + toplevel: Some(window.into()), + } + } + } + KeyboardFocusTarget::Fullscreen(elem) => { + if let Some(xsurface) = elem.x11_surface() { + PointerFocusTarget::X11Surface { + surface: xsurface.clone(), + toplevel: Some(elem), + } + } else { + PointerFocusTarget::WlSurface { + surface: elem.wl_surface().unwrap().into_owned(), + toplevel: Some(elem.into()), + } + } + } + KeyboardFocusTarget::LayerSurface(layer) => PointerFocusTarget::WlSurface { + surface: layer.wl_surface().clone(), + toplevel: None, + }, + KeyboardFocusTarget::Popup(popup) => PointerFocusTarget::WlSurface { + surface: popup.wl_surface().clone(), + toplevel: Some(popup.into()), + }, + KeyboardFocusTarget::LockSurface(lock) => PointerFocusTarget::WlSurface { + surface: lock.wl_surface().clone(), + toplevel: None, + }, + _ => unreachable!("A group cannot start a popup grab"), + } + } +} + +impl PointerFocusTarget { + fn inner_pointer_target(&self) -> &dyn PointerTarget { + match self { + PointerFocusTarget::WlSurface { surface, .. } => surface, + PointerFocusTarget::X11Surface { surface, .. } => surface, + PointerFocusTarget::StackUI(u) => u, + PointerFocusTarget::WindowUI(u) => u, + PointerFocusTarget::ResizeFork(f) => f, + PointerFocusTarget::ZoomUI(e) => e, + } + } + + fn inner_touch_target(&self) -> &dyn TouchTarget { + match self { + PointerFocusTarget::WlSurface { surface, .. } => surface, + PointerFocusTarget::X11Surface { surface, .. } => surface, + PointerFocusTarget::StackUI(u) => u, + PointerFocusTarget::WindowUI(u) => u, + PointerFocusTarget::ResizeFork(f) => f, + PointerFocusTarget::ZoomUI(e) => e, + } + } + + pub fn under_surface>>( + surface: &CosmicSurface, + point: P, + ) -> Option<(Self, Point)> { + match surface.0.underlying_surface() { + WindowSurface::Wayland(_toplevel) => surface + .0 + .surface_under(point, WindowSurfaceType::ALL) + .map(|(wl_surface, point)| { + ( + Self::WlSurface { + surface: wl_surface, + toplevel: Some(surface.clone().into()), + }, + point, + ) + }), + WindowSurface::X11(x11_surface) => Some(( + Self::X11Surface { + surface: x11_surface.clone(), + toplevel: Some(surface.clone()), + }, + Point::default(), + )), + } + } + + pub fn toplevel(&self, shell: &Shell) -> Option { + match &self { + PointerFocusTarget::WlSurface { + toplevel: Some(PointerFocusToplevel::Surface(surface)), + .. + } => Some(surface.clone()), + PointerFocusTarget::WlSurface { + toplevel: Some(PointerFocusToplevel::Popup(PopupKind::Xdg(popup))), + .. + } => get_popup_toplevel(&PopupKind::Xdg(popup.clone())) + .and_then(|s| shell.element_for_surface(&s).map(|mapped| (mapped, s))) + .and_then(|(m, s)| { + m.windows() + .find(|(w, _)| w.wl_surface().map(|s2| s == *s2).unwrap_or(false)) + .map(|(s, _)| s) + }), + PointerFocusTarget::X11Surface { + toplevel: Some(surface), + .. + } => Some(surface.clone()), + PointerFocusTarget::StackUI(stack) => Some(stack.active()), + PointerFocusTarget::WindowUI(window) => Some(window.surface()), + _ => None, + } + } + + pub fn is_client(&self, client: &Client) -> bool { + match self { + PointerFocusTarget::WlSurface { surface, .. } => { + surface.client().is_some_and(|c| c == *client) + } + PointerFocusTarget::X11Surface { surface, .. } => surface + .wl_surface() + .is_some_and(|s| s.client().is_some_and(|c| c == *client)), + _ => false, + } + } + + // Update image copy cursor position/hotspot for enter/motion event + fn update_image_copy_cursor_position( + &self, + seat: &Seat, + data: &mut State, + event: &PointerMotionEvent, + ) { + let Some(toplevel) = self.toplevel(&data.common.shell.read()) else { + return; + }; + let cursor_sessions = toplevel.cursor_sessions(); + if cursor_sessions.is_empty() { + return; + } + + let cursor_pos = if let Some(wl_surface) = self.wl_surface() { + let surface_offset = toplevel + .surface_offset(&wl_surface) + .unwrap_or(Point::from((0, 0))) + .to_f64(); + let geometry_loc = toplevel + .wl_surface() + .and_then(|s| { + with_states(&s, |states| { + states + .cached_state + .get::() + .current() + .geometry + .map(|g| g.loc.to_f64()) + }) + }) + .unwrap_or_default(); + Some( + (event.location - geometry_loc + surface_offset) + .to_buffer(1.0, Transform::Normal, &toplevel.geometry().size.to_f64()) + .to_i32_round(), + ) + } else { + // If cursor is in SSD, instead of a `wl_surface`, it is outside the captured bounds + None + }; + + let cursor_hotspot = if let Some(CursorGeometry { hotspot, .. }) = + seat.cursor_geometry((0.0, 0.0), Duration::from_millis(event.time as u64).into()) + { + hotspot + } else { + Point::from((0, 0)) + }; + + for session in cursor_sessions { + session.set_cursor_pos(cursor_pos); + session.set_cursor_hotspot(cursor_hotspot); + } + } +} + +impl KeyboardFocusTarget { + fn inner_keyboard_target(&self) -> Option<&dyn KeyboardTarget> { + match self { + KeyboardFocusTarget::Element(w) => Some(w), + KeyboardFocusTarget::Fullscreen(w) => Some(w), + KeyboardFocusTarget::Group(_) => None, + KeyboardFocusTarget::LayerSurface(l) => Some(l.wl_surface()), + KeyboardFocusTarget::Popup(p) => Some(p.wl_surface()), + KeyboardFocusTarget::LockSurface(l) => Some(l.wl_surface()), + } + } + + pub fn toplevel(&self) -> Option> { + match self { + KeyboardFocusTarget::Element(mapped) => mapped.wl_surface(), + KeyboardFocusTarget::Popup(PopupKind::Xdg(xdg)) => { + get_popup_toplevel(&PopupKind::Xdg(xdg.clone())).map(Cow::Owned) + } + _ => None, + } + } + + pub fn windows(&self) -> impl Iterator + '_ { + match self { + KeyboardFocusTarget::Element(mapped) => Box::new(mapped.windows().map(|(s, _)| s)) + as Box>, + KeyboardFocusTarget::Fullscreen(surface) => Box::new(std::iter::once(surface.clone())), + _ => Box::new(std::iter::empty()), + } + } + + pub fn active_window(&self) -> Option { + match self { + KeyboardFocusTarget::Element(mapped) => Some(mapped.active_window()), + KeyboardFocusTarget::Fullscreen(surface) => Some(surface.clone()), + _ => None, + } + } + + pub fn x11_surface(&self) -> Option { + match self { + KeyboardFocusTarget::Element(mapped) => mapped.active_window().x11_surface().cloned(), + KeyboardFocusTarget::Fullscreen(surface) => surface.x11_surface().cloned(), + _ => None, + } + } + + pub fn is_xwm(&self, xwm: XwmId) -> bool { + if let Some(surface) = self.x11_surface() { + surface.xwm_id().unwrap() == xwm + } else { + false + } + } + + pub fn has_surface(&self, shell: &Shell, surface: &WlSurface) -> bool { + if let Some(fe) = shell.focused_element(self) { + fe.has_surface(surface, WindowSurfaceType::ALL) + } else if let KeyboardFocusTarget::Fullscreen(s) = self { + s.has_surface(surface, WindowSurfaceType::ALL) + } else if let Some(root) = WaylandFocus::wl_surface(self) { + CosmicSurface::surface_tree_offset(&root, surface).is_some() + } else { + false + } + } +} + +#[derive(Debug, Clone)] +pub struct WindowGroup { + pub node: NodeId, + pub alive: Weak<()>, + pub focus_stack: Vec, +} + +impl PartialEq for WindowGroup { + fn eq(&self, other: &Self) -> bool { + self.node == other.node && Weak::ptr_eq(&self.alive, &other.alive) + } +} + +impl IsAlive for PointerFocusTarget { + fn alive(&self) -> bool { + match self { + // XXX? does this change anything + PointerFocusTarget::WlSurface { surface, .. } => surface.alive(), + PointerFocusTarget::X11Surface { surface, .. } => surface.alive(), + PointerFocusTarget::StackUI(e) => e.alive(), + PointerFocusTarget::WindowUI(e) => e.alive(), + PointerFocusTarget::ResizeFork(f) => f.alive(), + PointerFocusTarget::ZoomUI(_) => true, + } + } +} + +impl IsAlive for KeyboardFocusTarget { + fn alive(&self) -> bool { + match self { + KeyboardFocusTarget::Element(e) => e.alive(), + KeyboardFocusTarget::Fullscreen(f) => f.alive(), + KeyboardFocusTarget::Group(g) => g.alive.upgrade().is_some(), + KeyboardFocusTarget::LayerSurface(l) => l.alive(), + KeyboardFocusTarget::Popup(p) => p.alive(), + KeyboardFocusTarget::LockSurface(l) => l.alive(), + } + } +} + +impl PointerTarget for PointerFocusTarget { + fn enter(&self, seat: &Seat, data: &mut State, event: &PointerMotionEvent) { + self.update_image_copy_cursor_position(seat, data, event); + self.inner_pointer_target().enter(seat, data, event); + } + fn motion(&self, seat: &Seat, data: &mut State, event: &PointerMotionEvent) { + self.update_image_copy_cursor_position(seat, data, event); + self.inner_pointer_target().motion(seat, data, event); + } + fn relative_motion(&self, seat: &Seat, data: &mut State, event: &RelativeMotionEvent) { + self.inner_pointer_target() + .relative_motion(seat, data, event); + } + fn button(&self, seat: &Seat, data: &mut State, event: &ButtonEvent) { + self.inner_pointer_target().button(seat, data, event); + } + fn axis(&self, seat: &Seat, data: &mut State, frame: AxisFrame) { + self.inner_pointer_target().axis(seat, data, frame); + } + fn frame(&self, seat: &Seat, data: &mut State) { + self.inner_pointer_target().frame(seat, data); + } + fn leave(&self, seat: &Seat, data: &mut State, serial: Serial, time: u32) { + let toplevel = self.toplevel(&data.common.shell.read()); + if let Some(element) = toplevel { + for session in element.cursor_sessions() { + session.set_cursor_pos(None); + session.set_cursor_hotspot((0, 0)); + } + } + + self.inner_pointer_target().leave(seat, data, serial, time); + } + fn gesture_swipe_begin( + &self, + seat: &Seat, + data: &mut State, + event: &GestureSwipeBeginEvent, + ) { + self.inner_pointer_target() + .gesture_swipe_begin(seat, data, event); + } + fn gesture_swipe_update( + &self, + seat: &Seat, + data: &mut State, + event: &GestureSwipeUpdateEvent, + ) { + self.inner_pointer_target() + .gesture_swipe_update(seat, data, event); + } + fn gesture_swipe_end( + &self, + seat: &Seat, + data: &mut State, + event: &GestureSwipeEndEvent, + ) { + self.inner_pointer_target() + .gesture_swipe_end(seat, data, event); + } + fn gesture_pinch_begin( + &self, + seat: &Seat, + data: &mut State, + event: &GesturePinchBeginEvent, + ) { + self.inner_pointer_target() + .gesture_pinch_begin(seat, data, event); + } + fn gesture_pinch_update( + &self, + seat: &Seat, + data: &mut State, + event: &GesturePinchUpdateEvent, + ) { + self.inner_pointer_target() + .gesture_pinch_update(seat, data, event); + } + fn gesture_pinch_end( + &self, + seat: &Seat, + data: &mut State, + event: &GesturePinchEndEvent, + ) { + self.inner_pointer_target() + .gesture_pinch_end(seat, data, event); + } + fn gesture_hold_begin( + &self, + seat: &Seat, + data: &mut State, + event: &GestureHoldBeginEvent, + ) { + self.inner_pointer_target() + .gesture_hold_begin(seat, data, event); + } + fn gesture_hold_end(&self, seat: &Seat, data: &mut State, event: &GestureHoldEndEvent) { + self.inner_pointer_target() + .gesture_hold_end(seat, data, event); + } +} + +impl TouchTarget for PointerFocusTarget { + fn down(&self, seat: &Seat, data: &mut State, event: &DownEvent) { + self.inner_touch_target().down(seat, data, event); + } + + fn up(&self, seat: &Seat, data: &mut State, event: &UpEvent) { + self.inner_touch_target().up(seat, data, event); + } + + fn motion(&self, seat: &Seat, data: &mut State, event: &TouchMotionEvent) { + self.inner_touch_target().motion(seat, data, event); + } + + fn frame(&self, seat: &Seat, data: &mut State, frame: FrameMarker) { + self.inner_touch_target().frame(seat, data, frame); + } + + fn cancel(&self, seat: &Seat, data: &mut State, frame: FrameMarker) { + self.inner_touch_target().cancel(seat, data, frame); + } + + fn shape(&self, seat: &Seat, data: &mut State, event: &ShapeEvent) { + self.inner_touch_target().shape(seat, data, event); + } + + fn orientation(&self, seat: &Seat, data: &mut State, event: &OrientationEvent) { + self.inner_touch_target().orientation(seat, data, event); + } + + fn last_frame(&self, seat: &Seat, data: &mut State) -> Option { + self.inner_touch_target().last_frame(seat, data) + } +} + +pub enum CosmicOfferData { + Wl(WlOfferData), + X11(XwmOfferData), +} + +impl OfferData for CosmicOfferData { + fn disable(&self) { + match self { + CosmicOfferData::Wl(data) => data.disable(), + CosmicOfferData::X11(data) => data.disable(), + } + } + + fn drop(&self) { + match self { + CosmicOfferData::Wl(data) => data.drop(), + CosmicOfferData::X11(data) => data.drop(), + } + } + + fn validated(&self) -> bool { + match self { + CosmicOfferData::Wl(data) => data.validated(), + CosmicOfferData::X11(data) => data.validated(), + } + } +} + +impl DndFocus for PointerFocusTarget { + type OfferData + = CosmicOfferData + where + S: Source; + + fn enter( + &self, + data: &mut State, + dh: &DisplayHandle, + source: Arc, + seat: &Seat, + location: Point, + serial: &Serial, + ) -> Option> { + match self { + PointerFocusTarget::WlSurface { surface, .. } => { + DndFocus::enter(surface, data, dh, source, seat, location, serial) + .map(CosmicOfferData::Wl) + } + PointerFocusTarget::X11Surface { surface, .. } => { + DndFocus::enter(surface, data, dh, source, seat, location, serial) + .map(CosmicOfferData::X11) + } + _ => None, + } + } + + fn motion( + &self, + data: &mut State, + offer: Option<&mut CosmicOfferData>, + seat: &Seat, + location: Point, + time: u32, + ) { + match self { + PointerFocusTarget::WlSurface { surface, .. } => { + let offer = match offer { + Some(CosmicOfferData::Wl(offer)) => Some(offer), + None => None, + _ => return, + }; + DndFocus::motion(surface, data, offer, seat, location, time) + } + PointerFocusTarget::X11Surface { surface, .. } => { + let offer = match offer { + Some(CosmicOfferData::X11(offer)) => Some(offer), + None => None, + _ => return, + }; + DndFocus::motion(surface, data, offer, seat, location, time) + } + _ => {} + } + } + + fn leave( + &self, + data: &mut State, + offer: Option<&mut CosmicOfferData>, + seat: &Seat, + ) { + match self { + PointerFocusTarget::WlSurface { surface, .. } => { + let offer = match offer { + Some(CosmicOfferData::Wl(offer)) => Some(offer), + None => None, + _ => return, + }; + DndFocus::leave(surface, data, offer, seat) + } + PointerFocusTarget::X11Surface { surface, .. } => { + let offer = match offer { + Some(CosmicOfferData::X11(offer)) => Some(offer), + None => None, + _ => return, + }; + DndFocus::leave(surface, data, offer, seat) + } + _ => {} + } + } + + fn drop( + &self, + data: &mut State, + offer: Option<&mut CosmicOfferData>, + seat: &Seat, + ) { + match self { + PointerFocusTarget::WlSurface { surface, .. } => { + let offer = match offer { + Some(CosmicOfferData::Wl(offer)) => Some(offer), + None => None, + _ => return, + }; + DndFocus::drop(surface, data, offer, seat) + } + PointerFocusTarget::X11Surface { surface, .. } => { + let offer = match offer { + Some(CosmicOfferData::X11(offer)) => Some(offer), + None => None, + _ => return, + }; + DndFocus::drop(surface, data, offer, seat) + } + _ => {} + } + } +} + +impl KeyboardTarget for KeyboardFocusTarget { + fn enter( + &self, + seat: &Seat, + data: &mut State, + keys: Vec>, + serial: Serial, + ) { + if let Some(inner) = self.inner_keyboard_target() { + inner.enter(seat, data, keys, serial); + } + } + fn leave(&self, seat: &Seat, data: &mut State, serial: Serial) { + if let Some(inner) = self.inner_keyboard_target() { + inner.leave(seat, data, serial); + } + } + fn key( + &self, + seat: &Seat, + data: &mut State, + key: KeysymHandle<'_>, + state: KeyState, + serial: Serial, + time: u32, + ) { + if let Some(inner) = self.inner_keyboard_target() { + inner.key(seat, data, key, state, serial, time); + } + } + fn modifiers( + &self, + seat: &Seat, + data: &mut State, + modifiers: ModifiersState, + serial: Serial, + ) { + if let Some(inner) = self.inner_keyboard_target() { + inner.modifiers(seat, data, modifiers, serial); + } + } + fn replace( + &self, + replaced: Self, + seat: &Seat, + data: &mut State, + keys: Vec>, + modifiers: ModifiersState, + serial: Serial, + ) { + if !replaced + .wl_surface() + .is_some_and(|s| Some(s) == self.wl_surface()) + && !replaced + .x11_surface() + .is_some_and(|s| Some(s) == self.x11_surface()) + { + KeyboardTarget::leave(&replaced, seat, data, serial); + KeyboardTarget::enter(self, seat, data, keys, serial); + KeyboardTarget::modifiers(self, seat, data, modifiers, serial); + } + } +} + +impl WaylandFocus for KeyboardFocusTarget { + fn wl_surface(&self) -> Option> { + match self { + KeyboardFocusTarget::Element(w) => WaylandFocus::wl_surface(w), + KeyboardFocusTarget::Fullscreen(w) => WaylandFocus::wl_surface(w), + KeyboardFocusTarget::Group(_) => None, + KeyboardFocusTarget::LayerSurface(l) => Some(Cow::Borrowed(l.wl_surface())), + KeyboardFocusTarget::Popup(p) => Some(Cow::Borrowed(p.wl_surface())), + KeyboardFocusTarget::LockSurface(l) => Some(Cow::Borrowed(l.wl_surface())), + } + } + fn same_client_as(&self, object_id: &ObjectId) -> bool { + match self { + KeyboardFocusTarget::Element(w) => WaylandFocus::same_client_as(w, object_id), + KeyboardFocusTarget::Fullscreen(w) => WaylandFocus::same_client_as(w, object_id), + KeyboardFocusTarget::Group(_) => false, + KeyboardFocusTarget::LayerSurface(l) => l.wl_surface().id().same_client_as(object_id), + KeyboardFocusTarget::Popup(p) => p.wl_surface().id().same_client_as(object_id), + KeyboardFocusTarget::LockSurface(l) => l.wl_surface().id().same_client_as(object_id), + } + } +} + +impl WaylandFocus for PointerFocusTarget { + fn wl_surface(&self) -> Option> { + Some(match self { + PointerFocusTarget::WlSurface { surface, .. } => Cow::Borrowed(surface), + PointerFocusTarget::X11Surface { surface, .. } => Cow::Owned(surface.wl_surface()?), + PointerFocusTarget::ResizeFork(_) + | PointerFocusTarget::StackUI(_) + | PointerFocusTarget::WindowUI(_) + | PointerFocusTarget::ZoomUI(_) => { + return None; + } + }) + } + fn same_client_as(&self, object_id: &ObjectId) -> bool { + match self { + PointerFocusTarget::WlSurface { surface, .. } => surface.id().same_client_as(object_id), + PointerFocusTarget::X11Surface { surface, .. } => surface + .wl_surface() + .is_some_and(|s| s.id().same_client_as(object_id)), + PointerFocusTarget::StackUI(stack) => stack + .active() + .wl_surface() + .is_some_and(|s| s.id().same_client_as(object_id)), + PointerFocusTarget::WindowUI(window) => window + .wl_surface() + .is_some_and(|s| s.id().same_client_as(object_id)), + PointerFocusTarget::ResizeFork(_) | PointerFocusTarget::ZoomUI(_) => false, + } + } +} + +impl From for PointerFocusTarget { + fn from(p: PopupKind) -> Self { + PointerFocusTarget::WlSurface { + surface: p.wl_surface().clone(), + toplevel: None, + } + } +} + +impl From for PointerFocusTarget { + fn from(f: ResizeForkTarget) -> Self { + PointerFocusTarget::ResizeFork(f) + } +} + +impl From for PointerFocusTarget { + fn from(l: LockSurface) -> Self { + PointerFocusTarget::WlSurface { + surface: l.wl_surface().clone(), + toplevel: None, + } + } +} + +impl From for KeyboardFocusTarget { + fn from(w: CosmicMapped) -> Self { + KeyboardFocusTarget::Element(w) + } +} + +impl From for KeyboardFocusTarget { + fn from(s: CosmicSurface) -> Self { + KeyboardFocusTarget::Fullscreen(s) + } +} + +impl From for KeyboardFocusTarget { + fn from(w: WindowGroup) -> Self { + KeyboardFocusTarget::Group(w) + } +} + +impl From for KeyboardFocusTarget { + fn from(l: LayerSurface) -> Self { + KeyboardFocusTarget::LayerSurface(l) + } +} + +impl From for KeyboardFocusTarget { + fn from(p: PopupKind) -> Self { + KeyboardFocusTarget::Popup(p) + } +} + +impl From for KeyboardFocusTarget { + fn from(l: LockSurface) -> Self { + KeyboardFocusTarget::LockSurface(l) + } +} diff --git a/src/shell/grabs/delay.rs b/src/shell/grabs/delay.rs new file mode 100644 index 0000000..96d33dc --- /dev/null +++ b/src/shell/grabs/delay.rs @@ -0,0 +1,275 @@ +use smithay::{ + input::{ + Seat, SeatHandler, + pointer::{ + AxisFrame, ButtonEvent, Focus, GestureHoldBeginEvent, GestureHoldEndEvent, + GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, + GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, + GrabStartData as PointerGrabStartData, MotionEvent as PointerMotionEvent, PointerGrab, + PointerInnerHandle, RelativeMotionEvent, + }, + touch::{ + DownEvent, GrabStartData as TouchGrabStartData, MotionEvent as TouchMotionEvent, + OrientationEvent, ShapeEvent, TouchGrab, TouchInnerHandle, UpEvent, + }, + }, + utils::{Logical, Point, SERIAL_COUNTER, Serial}, +}; + +use crate::state::State; + +use super::GrabStartData; + +pub struct DelayGrab { + grab_factory: Option Option<(G, Focus)>>>, + seat: Seat, + serial: Option, + start_data: GrabStartData, +} + +unsafe impl Send for DelayGrab {} + +impl DelayGrab { + pub fn new( + factory: impl FnOnce(&mut State) -> Option<(G, Focus)> + 'static, + seat: Seat, + serial: Option, + start_data: GrabStartData, + ) -> Self { + DelayGrab { + grab_factory: Some(Box::new(factory)), + seat, + serial, + start_data, + } + } + + pub fn is_touch_grab(&self) -> bool { + match self.start_data { + GrabStartData::Touch(_) => true, + GrabStartData::Pointer(_) => false, + } + } +} + +impl> PointerGrab for DelayGrab { + fn motion( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + focus: Option<(::PointerFocus, Point)>, + event: &PointerMotionEvent, + ) { + handle.motion(data, focus, event); + + let distance = self.start_data.distance(event.location); + if distance >= 1. + && let Some(factory) = self.grab_factory.take() + { + let serial = self.serial.unwrap_or(event.serial); + let seat = self.seat.clone(); + data.common.event_loop_handle.insert_idle(move |data| { + if let Some((grab, focus)) = factory(data) { + seat.get_pointer() + .unwrap() + .set_grab(data, grab, serial, focus); + } + }); + } + } + + fn relative_motion( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + focus: Option<(::PointerFocus, Point)>, + event: &RelativeMotionEvent, + ) { + handle.relative_motion(data, focus, event); + } + + fn button( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &ButtonEvent, + ) { + handle.button(data, event); + if handle.current_pressed().is_empty() { + handle.unset_grab(self, data, event.serial, event.time, true); + } + } + + fn axis( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + details: AxisFrame, + ) { + handle.axis(data, details); + } + + fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) { + handle.frame(data); + } + + fn gesture_swipe_begin( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureSwipeBeginEvent, + ) { + handle.gesture_swipe_begin(data, event); + } + + fn gesture_swipe_update( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureSwipeUpdateEvent, + ) { + handle.gesture_swipe_update(data, event); + } + + fn gesture_swipe_end( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureSwipeEndEvent, + ) { + handle.gesture_swipe_end(data, event); + } + + fn gesture_pinch_begin( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GesturePinchBeginEvent, + ) { + handle.gesture_pinch_begin(data, event); + } + + fn gesture_pinch_update( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GesturePinchUpdateEvent, + ) { + handle.gesture_pinch_update(data, event); + } + + fn gesture_pinch_end( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GesturePinchEndEvent, + ) { + handle.gesture_pinch_end(data, event); + } + + fn gesture_hold_begin( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureHoldBeginEvent, + ) { + handle.gesture_hold_begin(data, event); + } + + fn gesture_hold_end( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureHoldEndEvent, + ) { + handle.gesture_hold_end(data, event); + } + + fn start_data(&self) -> &PointerGrabStartData { + match &self.start_data { + GrabStartData::Pointer(start_data) => start_data, + _ => unreachable!(), + } + } + + fn unset(&mut self, _data: &mut State) {} +} + +impl> TouchGrab for DelayGrab { + fn down( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + focus: Option<(::TouchFocus, Point)>, + event: &DownEvent, + ) { + handle.down(data, focus, event); + } + + fn up(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, event: &UpEvent) { + handle.up(data, event); + + if event.slot == TouchGrab::start_data(self).slot { + handle.unset_grab(self, data); + } + } + + fn motion( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + focus: Option<(::TouchFocus, Point)>, + event: &TouchMotionEvent, + ) { + handle.motion(data, focus, event); + + let distance = self.start_data.distance(event.location); + if distance >= 1. + && let Some(factory) = self.grab_factory.take() + { + let seat = self.seat.clone(); + let serial = self.serial.unwrap_or_else(|| SERIAL_COUNTER.next_serial()); + data.common.event_loop_handle.insert_idle(move |data| { + if let Some((grab, _)) = factory(data) { + seat.get_touch().unwrap().set_grab(data, grab, serial); + } + }); + } + } + + fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>) { + handle.frame(data) + } + + fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>) { + handle.cancel(data); + handle.unset_grab(self, data); + } + + fn shape( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + event: &ShapeEvent, + ) { + handle.shape(data, event) + } + + fn orientation( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + event: &OrientationEvent, + ) { + handle.orientation(data, event); + } + + fn start_data(&self) -> &TouchGrabStartData { + match &self.start_data { + GrabStartData::Touch(start_data) => start_data, + _ => unreachable!(), + } + } + + fn unset(&mut self, _data: &mut State) {} +} diff --git a/src/shell/grabs/menu/default.rs b/src/shell/grabs/menu/default.rs new file mode 100644 index 0000000..c7418b6 --- /dev/null +++ b/src/shell/grabs/menu/default.rs @@ -0,0 +1,674 @@ +use cosmic_settings_config::shortcuts::Action; +use smithay::{ + input::pointer::MotionEvent, reexports::wayland_server::protocol::wl_surface::WlSurface, + utils::SERIAL_COUNTER, wayland::seat::WaylandFocus, +}; + +use crate::{ + config::Config, + fl, + shell::{ + CosmicSurface, PointGlobalExt, Shell, + element::{CosmicMapped, CosmicWindow}, + grabs::ReleaseMode, + }, + state::State, + utils::{prelude::SeatExt, screenshot::screenshot_window}, + wayland::protocols::workspace::WorkspaceHandle, +}; + +use super::{Item, ResizeEdge}; + +fn toggle_stacking(state: &mut State, mapped: &CosmicMapped) { + let mut shell = state.common.shell.write(); + let seat = shell.seats.last_active().clone(); + if let Some(new_focus) = shell.toggle_stacking(&seat, mapped) { + std::mem::drop(shell); + Shell::set_focus(state, Some(&new_focus), &seat, None, false); + } +} + +fn prev_workspace( + shell: &Shell, + surface: &WlSurface, +) -> Option<(WorkspaceHandle, WorkspaceHandle)> { + let (current_handle, output) = shell.workspace_for_surface(surface)?; + shell + .workspaces + .spaces_for_output(&output) + .enumerate() + .find_map(|(i, space)| (space.handle == current_handle).then_some(i)) + .and_then(|i| i.checked_sub(1)) + .and_then(|i| shell.workspaces.get(i, &output)) + .map(|space| (current_handle, space.handle)) +} + +fn next_workspace( + shell: &Shell, + surface: &WlSurface, +) -> Option<(WorkspaceHandle, WorkspaceHandle)> { + let (current_handle, output) = shell.workspace_for_surface(surface)?; + shell + .workspaces + .spaces_for_output(&output) + .skip_while(|space| space.handle != current_handle) + .nth(1) + .map(|space| (current_handle, space.handle)) +} + +fn move_fullscreen_prev_workspace(state: &mut State, surface: &CosmicSurface) { + let mut shell = state.common.shell.write(); + let Some(wl_surface) = surface.wl_surface() else { + return; + }; + let Some((from, to)) = prev_workspace(&shell, &wl_surface) else { + return; + }; + + let seat = shell.seats.last_active().clone(); + let res = shell.move_window( + Some(&seat), + surface, + &from, + &to, + true, + None, + &mut state.common.workspace_state.update(), + &state.common.event_loop_handle, + ); + if let Some((target, _)) = res { + std::mem::drop(shell); + Shell::set_focus(state, Some(&target), &seat, None, true); + } +} + +fn move_fullscreen_next_workspace(state: &mut State, surface: &CosmicSurface) { + let mut shell = state.common.shell.write(); + let Some(wl_surface) = surface.wl_surface() else { + return; + }; + let Some((from, to)) = next_workspace(&shell, &wl_surface) else { + return; + }; + + let seat = shell.seats.last_active().clone(); + let res = shell.move_window( + Some(&seat), + surface, + &from, + &to, + true, + None, + &mut state.common.workspace_state.update(), + &state.common.event_loop_handle, + ); + if let Some((target, _)) = res { + std::mem::drop(shell); + Shell::set_focus(state, Some(&target), &seat, None, true); + } +} + +fn move_element_prev_workspace(state: &mut State, mapped: &CosmicMapped) { + let mut shell = state.common.shell.write(); + let window = mapped.active_window(); + let Some(wl_surface) = window.wl_surface() else { + return; + }; + let Some((from, to)) = prev_workspace(&shell, &wl_surface) else { + return; + }; + + let seat = shell.seats.last_active().clone(); + let res = shell.move_element( + Some(&seat), + mapped, + &from, + &to, + true, + None, + &mut state.common.workspace_state.update(), + ); + if let Some((target, _)) = res { + std::mem::drop(shell); + Shell::set_focus(state, Some(&target), &seat, None, true); + } +} + +fn move_element_next_workspace(state: &mut State, mapped: &CosmicMapped) { + let mut shell = state.common.shell.write(); + let window = mapped.active_window(); + let Some(wl_surface) = window.wl_surface() else { + return; + }; + let Some((from, to)) = next_workspace(&shell, &wl_surface) else { + return; + }; + + let seat = shell.seats.last_active().clone(); + let res = shell.move_element( + Some(&seat), + mapped, + &from, + &to, + true, + None, + &mut state.common.workspace_state.update(), + ); + if let Some((target, _point)) = res { + std::mem::drop(shell); + Shell::set_focus(state, Some(&target), &seat, None, true) + } +} + +pub fn tab_items( + stack: &CosmicMapped, + tab: &CosmicSurface, + is_tiled: bool, + config: &Config, +) -> impl Iterator { + let unstack_clone_stack = stack.clone(); + let unstack_clone_tab = tab.clone(); + let screenshot_clone = tab.clone(); + let close_clone = tab.clone(); + + vec![ + Item::new(fl!("window-menu-unstack"), move |handle| { + let mapped = unstack_clone_stack.clone(); + let surface = unstack_clone_tab.clone(); + let _ = handle.insert_idle(move |state| { + mapped.stack_ref().unwrap().remove_window(&surface); + let mapped: CosmicMapped = CosmicWindow::new( + surface, + state.common.event_loop_handle.clone(), + state.common.theme.clone(), + state.common.config.cosmic_conf.appearance_settings, + ) + .into(); + + let mut shell = state.common.shell.write(); + let seat = shell.seats.last_active().clone(); + let output = seat.active_output(); + let workspace = shell.workspaces.active_mut(&output).unwrap(); + if is_tiled { + for mapped in workspace + .mapped() + .filter(|m| m.maximized_state.lock().unwrap().is_some()) + .cloned() + .collect::>() + .into_iter() + { + workspace.unmaximize_request(&mapped); + } + let focus_stack = workspace.focus_stack.get(&seat); + workspace + .tiling_layer + .map(mapped, Some(focus_stack.iter()), None); + } else { + workspace.floating_layer.map(mapped, None) + } + }); + }), + Item::Separator, + Item::new(fl!("window-menu-screenshot"), move |handle| { + let tab = screenshot_clone.clone(); + let _ = handle.insert_idle(move |state| screenshot_window(state, &tab)); + }), + Item::Separator, + Item::new(fl!("window-menu-close"), move |_handle| { + close_clone.close(); + }) + .shortcut(config.shortcut_for_action(&Action::Close)), + ] + .into_iter() +} + +pub fn window_items( + window: &CosmicMapped, + is_tiled: bool, + is_stacked: bool, + is_sticky: bool, + tiling_enabled: bool, + possible_resizes: ResizeEdge, + config: &Config, +) -> impl Iterator { + let minimize_clone = window.clone(); + let maximize_clone = window.clone(); + let fullscreen_clone = window.clone(); + let tile_clone = window.clone(); + let move_prev_clone = window.clone(); + let move_next_clone = window.clone(); + let move_clone = window.clone(); + let resize_top_clone = window.clone(); + let resize_left_clone = window.clone(); + let resize_right_clone = window.clone(); + let resize_bottom_clone = window.clone(); + let unstack_clone = window.clone(); + let screenshot_clone = window.clone(); + let stack_clone = window.clone(); + let sticky_clone = window.clone(); + let close_clone = window.clone(); + + vec![ + (!is_stacked).then_some( + Item::new(fl!("window-menu-stack"), move |handle| { + let mapped = stack_clone.clone(); + let _ = handle.insert_idle(move |state| toggle_stacking(state, &mapped)); + }) + .shortcut(config.shortcut_for_action(&Action::ToggleStacking)), + ), + is_stacked.then_some( + Item::new(fl!("window-menu-unstack-all"), move |handle| { + let mapped = unstack_clone.clone(); + let _ = handle.insert_idle(move |state| { + toggle_stacking(state, &mapped); + }); + }) + .shortcut(config.shortcut_for_action(&Action::ToggleStacking)), + ), + Some(Item::Separator), + Some( + Item::new(fl!("window-menu-minimize"), move |handle| { + let mapped = minimize_clone.clone(); + let _ = handle.insert_idle(move |state| { + state + .common + .shell + .write() + .minimize_request(&mapped.active_window()); + }); + }) + .shortcut(config.shortcut_for_action(&Action::Minimize)), + ), + Some( + Item::new(fl!("window-menu-maximize"), move |handle| { + let mapped = maximize_clone.clone(); + let _ = handle.insert_idle(move |state| { + let mut shell = state.common.shell.write(); + let seat = shell.seats.last_active().clone(); + shell.maximize_toggle(&mapped, &seat, &state.common.event_loop_handle); + }); + }) + .shortcut(config.shortcut_for_action(&Action::Maximize)) + .toggled(window.is_maximized(false)), + ), + Some( + Item::new(fl!("window-menu-fullscreen"), move |handle| { + let mapped = fullscreen_clone.clone(); + let _ = handle.insert_idle(move |state| { + let mut shell = state.common.shell.write(); + let seat = shell.seats.last_active().clone(); + let output = seat.active_output(); + if let Some(target) = shell.fullscreen_request( + &mapped.active_window(), + output, + &state.common.event_loop_handle, + ) { + std::mem::drop(shell); + Shell::set_focus(state, Some(&target), &seat, None, false); + } + }); + }) + .shortcut(config.shortcut_for_action(&Action::Fullscreen)) + .toggled(false), + ), + (tiling_enabled && !is_sticky).then_some( + Item::new(fl!("window-menu-tiled"), move |handle| { + let tile_clone = tile_clone.clone(); + let _ = handle.insert_idle(move |state| { + let mut shell = state.common.shell.write(); + let seat = shell.seats.last_active().clone(); + if let Some(ws) = shell.space_for_mut(&tile_clone) { + ws.toggle_floating_window(&seat, &tile_clone); + } + }); + }) + .shortcut(config.shortcut_for_action(&Action::ToggleWindowFloating)) + .toggled(!is_tiled), + ), + Some(Item::Separator), + // TODO: Where to save? + Some(Item::new(fl!("window-menu-screenshot"), move |handle| { + let mapped = screenshot_clone.clone(); + let _ = + handle.insert_idle(move |state| screenshot_window(state, &mapped.active_window())); + })), + Some(Item::Separator), + Some(Item::new(fl!("window-menu-move"), move |handle| { + let move_clone = move_clone.clone(); + let _ = handle.insert_idle(move |state| { + if let Some(surface) = move_clone.wl_surface() { + let mut shell = state.common.shell.write(); + let seat = shell.seats.last_active().clone(); + let res = shell.move_request( + &surface, + &seat, + None, + ReleaseMode::Click, + false, + &state.common.config, + &state.common.event_loop_handle, + false, + ); + + std::mem::drop(shell); + if let Some((grab, focus)) = res { + if grab.is_touch_grab() { + seat.get_touch().unwrap().set_grab( + state, + grab, + SERIAL_COUNTER.next_serial(), + ) + } else { + seat.get_pointer().unwrap().set_grab( + state, + grab, + SERIAL_COUNTER.next_serial(), + focus, + ); + } + } + } + }); + })), + Some(Item::new_submenu( + fl!("window-menu-resize"), + vec![ + Item::new(fl!("window-menu-resize-edge-top"), move |handle| { + let resize_clone = resize_top_clone.clone(); + let _ = handle.insert_idle(move |state| { + let mut shell = state.common.shell.write(); + let seat = shell.seats.last_active().clone(); + let res = shell.menu_resize_request( + &resize_clone, + &seat, + ResizeEdge::TOP, + state.common.config.cosmic_conf.edge_snap_threshold, + ); + + std::mem::drop(shell); + if let Some(((target, loc), (grab, focus))) = res { + let serial = SERIAL_COUNTER.next_serial(); + if grab.is_touch_grab() { + seat.get_touch().unwrap().set_grab(state, grab, serial); + } else { + let pointer = seat.get_pointer().unwrap(); + pointer.motion( + state, + target, + &MotionEvent { + location: loc.as_logical().to_f64(), + serial, + time: state.common.clock.now().as_millis(), + }, + ); + pointer.frame(state); + pointer.set_grab(state, grab, serial, focus); + } + } + }); + }) + .disabled(!possible_resizes.contains(ResizeEdge::TOP)), + Item::new(fl!("window-menu-resize-edge-left"), move |handle| { + let resize_clone = resize_left_clone.clone(); + let _ = handle.insert_idle(move |state| { + let mut shell = state.common.shell.write(); + let seat = shell.seats.last_active().clone(); + let res = shell.menu_resize_request( + &resize_clone, + &seat, + ResizeEdge::LEFT, + state.common.config.cosmic_conf.edge_snap_threshold, + ); + + std::mem::drop(shell); + if let Some(((target, loc), (grab, focus))) = res { + let serial = SERIAL_COUNTER.next_serial(); + if grab.is_touch_grab() { + seat.get_touch().unwrap().set_grab(state, grab, serial); + } else { + let pointer = seat.get_pointer().unwrap(); + pointer.motion( + state, + target, + &MotionEvent { + location: loc.as_logical().to_f64(), + serial, + time: state.common.clock.now().as_millis(), + }, + ); + pointer.frame(state); + pointer.set_grab(state, grab, serial, focus); + } + } + }); + }) + .disabled(!possible_resizes.contains(ResizeEdge::LEFT)), + Item::new(fl!("window-menu-resize-edge-right"), move |handle| { + let resize_clone = resize_right_clone.clone(); + let _ = handle.insert_idle(move |state| { + let mut shell = state.common.shell.write(); + let seat = shell.seats.last_active().clone(); + let res = shell.menu_resize_request( + &resize_clone, + &seat, + ResizeEdge::RIGHT, + state.common.config.cosmic_conf.edge_snap_threshold, + ); + + std::mem::drop(shell); + if let Some(((target, loc), (grab, focus))) = res { + let serial = SERIAL_COUNTER.next_serial(); + if grab.is_touch_grab() { + seat.get_touch().unwrap().set_grab(state, grab, serial); + } else { + let pointer = seat.get_pointer().unwrap(); + pointer.motion( + state, + target, + &MotionEvent { + location: loc.as_logical().to_f64(), + serial, + time: state.common.clock.now().as_millis(), + }, + ); + pointer.frame(state); + pointer.set_grab(state, grab, serial, focus); + } + } + }); + }) + .disabled(!possible_resizes.contains(ResizeEdge::RIGHT)), + Item::new(fl!("window-menu-resize-edge-bottom"), move |handle| { + let resize_clone = resize_bottom_clone.clone(); + let _ = handle.insert_idle(move |state| { + let mut shell = state.common.shell.write(); + let seat = shell.seats.last_active().clone(); + let res = shell.menu_resize_request( + &resize_clone, + &seat, + ResizeEdge::BOTTOM, + state.common.config.cosmic_conf.edge_snap_threshold, + ); + + std::mem::drop(shell); + if let Some(((target, loc), (grab, focus))) = res { + let serial = SERIAL_COUNTER.next_serial(); + if grab.is_touch_grab() { + seat.get_touch().unwrap().set_grab(state, grab, serial); + } else { + let pointer = seat.get_pointer().unwrap(); + pointer.motion( + state, + target, + &MotionEvent { + location: loc.as_logical().to_f64(), + serial, + time: state.common.clock.now().as_millis(), + }, + ); + pointer.frame(state); + pointer.set_grab(state, grab, serial, focus); + } + } + }); + }) + .disabled(!possible_resizes.contains(ResizeEdge::BOTTOM)), + ], + )), + Some( + Item::new(fl!("window-menu-move-prev-workspace"), move |handle| { + let mapped = move_prev_clone.clone(); + let _ = + handle.insert_idle(move |state| move_element_prev_workspace(state, &mapped)); + }) + .shortcut(config.shortcut_for_action(&Action::MoveToPreviousWorkspace)) + .disabled(is_sticky), + ), + Some( + Item::new(fl!("window-menu-move-next-workspace"), move |handle| { + let mapped = move_next_clone.clone(); + let _ = + handle.insert_idle(move |state| move_element_next_workspace(state, &mapped)); + }) + .shortcut(config.shortcut_for_action(&Action::MoveToNextWorkspace)) + .disabled(is_sticky), + ), + Some(Item::Separator), + Some( + Item::new(fl!("window-menu-sticky"), move |handle| { + let mapped = sticky_clone.clone(); + let _ = handle.insert_idle(move |state| { + let mut shell = state.common.shell.write(); + let seat = shell.seats.last_active().clone(); + shell.toggle_sticky(&seat, &mapped); + }); + }) + .toggled(is_sticky), + ), + Some(Item::Separator), + if is_stacked { + Some(Item::new(fl!("window-menu-close-all"), move |_handle| { + for (window, _) in close_clone.windows() { + window.close(); + } + })) + } else { + Some( + Item::new(fl!("window-menu-close"), move |_handle| { + close_clone.send_close(); + }) + .shortcut(config.shortcut_for_action(&Action::Close)), + ) + }, + ] + .into_iter() + .flatten() +} + +pub fn fullscreen_items(window: &CosmicSurface, config: &Config) -> impl Iterator { + let minimize_clone = window.clone(); + let fullscreen_clone = window.clone(); + let move_prev_clone = window.clone(); + let move_next_clone = window.clone(); + let move_clone = window.clone(); + let screenshot_clone = window.clone(); + let close_clone = window.clone(); + + vec![ + Some( + Item::new(fl!("window-menu-minimize"), move |handle| { + let window = minimize_clone.clone(); + let _ = handle.insert_idle(move |state| { + state.common.shell.write().minimize_request(&window); + }); + }) + .shortcut(config.shortcut_for_action(&Action::Minimize)), + ), + Some( + Item::new(fl!("window-menu-fullscreen"), move |handle| { + let window = fullscreen_clone.clone(); + let _ = handle.insert_idle(move |state| { + let mut shell = state.common.shell.write(); + if let Some(target) = + shell.unfullscreen_request(&window, &state.common.event_loop_handle) + { + let seat = shell.seats.last_active().clone(); + std::mem::drop(shell); + Shell::set_focus(state, Some(&target), &seat, None, true); + } + }); + }) + .shortcut(config.shortcut_for_action(&Action::Fullscreen)) + .toggled(true), + ), + Some(Item::Separator), + // TODO: Where to save? + Some(Item::new(fl!("window-menu-screenshot"), move |handle| { + let window = screenshot_clone.clone(); + let _ = handle.insert_idle(move |state| screenshot_window(state, &window)); + })), + Some(Item::Separator), + Some(Item::new(fl!("window-menu-move"), move |handle| { + let move_clone = move_clone.clone(); + let _ = handle.insert_idle(move |state| { + if let Some(surface) = move_clone.wl_surface() { + let mut shell = state.common.shell.write(); + let seat = shell.seats.last_active().clone(); + let res = shell.move_request( + &surface, + &seat, + None, + ReleaseMode::Click, + false, + &state.common.config, + &state.common.event_loop_handle, + false, + ); + + std::mem::drop(shell); + if let Some((grab, focus)) = res { + if grab.is_touch_grab() { + seat.get_touch().unwrap().set_grab( + state, + grab, + SERIAL_COUNTER.next_serial(), + ) + } else { + seat.get_pointer().unwrap().set_grab( + state, + grab, + SERIAL_COUNTER.next_serial(), + focus, + ); + } + } + } + }); + })), + Some( + Item::new(fl!("window-menu-move-prev-workspace"), move |handle| { + let window = move_prev_clone.clone(); + let _ = + handle.insert_idle(move |state| move_fullscreen_prev_workspace(state, &window)); + }) + .shortcut(config.shortcut_for_action(&Action::MoveToPreviousWorkspace)), + ), + Some( + Item::new(fl!("window-menu-move-next-workspace"), move |handle| { + let window = move_next_clone.clone(); + let _ = + handle.insert_idle(move |state| move_fullscreen_next_workspace(state, &window)); + }) + .shortcut(config.shortcut_for_action(&Action::MoveToNextWorkspace)), + ), + Some(Item::Separator), + Some( + Item::new(fl!("window-menu-close"), move |_handle| { + close_clone.close(); + }) + .shortcut(config.shortcut_for_action(&Action::Close)), + ), + ] + .into_iter() + .flatten() +} diff --git a/src/shell/grabs/menu/item.rs b/src/shell/grabs/menu/item.rs new file mode 100644 index 0000000..f254df7 --- /dev/null +++ b/src/shell/grabs/menu/item.rs @@ -0,0 +1,244 @@ +use cosmic::{ + iced::{ + Alignment, Element, + core::{ + Background, Border, Clipboard, Color, Event, Layout, Length, Rectangle, + Renderer as IcedRenderer, Shell, Size, layout, mouse, overlay, + renderer::{Quad, Style}, + widget::{Id, Tree, Widget, tree}, + }, + }, + widget::button::Catalog, +}; + +pub struct SubmenuItem<'a, Message> { + elem: cosmic::Element<'a, Message>, + idx: usize, + styling: ::Class, +} + +impl<'a, Message> SubmenuItem<'a, Message> { + pub fn new(elem: impl Into>, idx: usize) -> Self { + Self { + elem: elem.into(), + idx, + styling: Default::default(), + } + } + + pub fn style(mut self, style: ::Class) -> Self { + self.styling = style; + self + } +} + +pub trait CursorEvents { + fn cursor_entered(idx: usize, bounds: Rectangle) -> Self; + fn cursor_left(idx: usize, bounds: Rectangle) -> Self; +} + +struct State { + cursor_over: bool, +} + +impl Widget for SubmenuItem<'_, Message> +where + Message: CursorEvents, +{ + fn id(&self) -> Option { + None + } + + fn size(&self) -> Size { + self.elem.as_widget().size() + } + + fn layout( + &mut self, + state: &mut Tree, + renderer: &cosmic::Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let state = &mut state.children[0]; + let node = self.elem.as_widget_mut().layout(state, renderer, limits); + layout::Node::with_children(node.size(), vec![node]) + } + + fn draw( + &self, + state: &Tree, + renderer: &mut cosmic::Renderer, + theme: &cosmic::Theme, + style: &Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + let widget_state = state.state.downcast_ref::(); + let mut styling = if widget_state.cursor_over { + theme.hovered(true, false, &self.styling) + } else { + theme.active(true, false, &self.styling) + }; + if matches!(self.styling, cosmic::theme::Button::MenuItem) { + match theme.list_item_position { + Some((Alignment::Start, _)) => { + styling.border_radius = + styling.border_radius.bottom(theme.cosmic().radius_0()[3]); + } + Some((Alignment::End, _)) => { + styling.border_radius = styling.border_radius.top(theme.cosmic().radius_0()[0]); + } + Some((Alignment::Center, _)) => {} + None => { + styling.border_radius = theme.cosmic().radius_0().into(); + } + }; + } + + renderer.fill_quad( + Quad { + snap: true, + bounds: layout.bounds(), + border: Border { + radius: styling.border_radius, + width: 0., + color: styling.border_color, + }, + shadow: Default::default(), + }, + styling + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + ); + + let state = &state.children[0]; + let layout = layout.children().next().unwrap(); + self.elem.as_widget().draw( + state, + renderer, + theme, + &Style { + text_color: styling.text_color.unwrap_or(style.text_color), + icon_color: styling.icon_color.unwrap_or(style.text_color), + ..*style + }, + layout, + cursor, + viewport, + ) + } + + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State { cursor_over: false }) + } + + fn children(&self) -> Vec { + vec![Tree::new(&self.elem)] + } + + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(std::slice::from_mut(&mut self.elem)) + } + + fn operate( + &mut self, + state: &mut Tree, + layout: Layout<'_>, + renderer: &cosmic::Renderer, + operation: &mut dyn cosmic::widget::Operation<()>, + ) { + let state = &mut state.children[0]; + let layout = layout.children().next().unwrap(); + self.elem + .as_widget_mut() + .operate(state, layout, renderer, operation) + } + + fn update( + &mut self, + state: &mut Tree, + event: &Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &cosmic::Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) { + let mut bounds = layout.bounds(); + + // fix padding 1 and event... don't ask. + bounds.x -= 1.; + bounds.width += 2.; + + let is_over = cursor.is_over(bounds); + let widget_state = state.state.downcast_mut::(); + match event { + Event::Mouse(mouse::Event::CursorEntered) + | Event::Mouse(mouse::Event::CursorMoved { .. }) + if is_over && !widget_state.cursor_over => + { + shell.publish(Message::cursor_entered(self.idx, bounds)); + widget_state.cursor_over = true; + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Mouse(mouse::Event::CursorLeft) + if !is_over && widget_state.cursor_over => + { + shell.publish(Message::cursor_left(self.idx, bounds)); + widget_state.cursor_over = false; + } + _ => {} + }; + + let state = &mut state.children[0]; + let layout = layout.children().next().unwrap(); + self.elem.as_widget_mut().update( + state, event, layout, cursor, renderer, clipboard, shell, viewport, + ); + } + + fn mouse_interaction( + &self, + state: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &cosmic::Renderer, + ) -> mouse::Interaction { + let state = &state.children[0]; + let layout = layout.children().next().unwrap(); + self.elem + .as_widget() + .mouse_interaction(state, layout, cursor, viewport, renderer) + } + + fn overlay<'b>( + &'b mut self, + state: &'b mut Tree, + layout: Layout<'b>, + renderer: &cosmic::Renderer, + viewport: &Rectangle, + translation: cosmic::iced::Vector, + ) -> Option> { + let state = &mut state.children[0]; + let layout = layout.children().next().unwrap(); + self.elem + .as_widget_mut() + .overlay(state, layout, renderer, viewport, translation) + } +} + +impl<'a, Message> From> for cosmic::Element<'a, Message> +where + Message: CursorEvents + 'a, +{ + fn from(val: SubmenuItem<'a, Message>) -> Self { + Element::new(val) + } +} diff --git a/src/shell/grabs/menu/mod.rs b/src/shell/grabs/menu/mod.rs new file mode 100644 index 0000000..72ea065 --- /dev/null +++ b/src/shell/grabs/menu/mod.rs @@ -0,0 +1,1106 @@ +use std::{ + fmt, + sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, + }, +}; + +use calloop::LoopHandle; +use cosmic::{ + Apply as _, Task, + iced::{ + Alignment, Background, + core::{Border, Length, Rectangle as IcedRectangle, alignment::Horizontal}, + widget::{self as iced_widget, Row, text::Style as TextStyle}, + }, + theme, + widget::{button, divider, icon::from_name, menu::menu_column::MenuColumn, space, text}, +}; +use smithay::{ + backend::{ + input::{ButtonState, TouchSlot}, + renderer::ImportMem, + }, + desktop::space::SpaceElement, + input::{ + Seat, + pointer::{ + AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, + GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, + GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, + GrabStartData as PointerGrabStartData, MotionEvent as PointerMotionEvent, PointerGrab, + PointerInnerHandle, PointerTarget, RelativeMotionEvent, + }, + touch::{ + DownEvent, GrabStartData as TouchGrabStartData, MotionEvent as TouchMotionEvent, + TouchGrab, TouchInnerHandle, TouchTarget, UpEvent, + }, + }, + output::Output, + utils::{Logical, Point, Rectangle, Serial, Size}, +}; + +use crate::{ + backend::render::element::AsGlowRenderer, + shell::{SeatExt, focus::target::PointerFocusTarget}, + state::State, + utils::{ + iced::{IcedElement, IcedRenderElement, Program}, + prelude::*, + }, +}; + +use super::{GrabStartData, ResizeEdge}; + +mod default; +mod item; +pub use self::default::*; + +pub struct MenuGrabState { + elements: Arc>>, + screen_space_relative: Option, + scale: Arc>, +} +pub type SeatMenuGrabState = Mutex>; + +impl MenuGrabState { + pub fn render( + &self, + renderer: &mut R, + output: &Output, + push: &mut dyn FnMut(IcedRenderElement), + ) where + R: AsGlowRenderer + ImportMem, + R::TextureId: Send + Clone + 'static, + { + let scale = output.current_scale().fractional_scale(); + for elem in self.elements.lock().unwrap().iter() { + elem.iced.push_render_elements( + renderer, + elem.position + .to_local(output) + .as_logical() + .to_physical_precise_round(scale), + scale.into(), + 1.0, + elem.iced + .with_theme(|theme| theme.cosmic().radius_s()) + .map(|x| x.round() as u8), + push, + None, + ) + } + } + + pub fn is_in_screen_space(&self) -> bool { + self.screen_space_relative.is_some() + } + + pub fn set_theme(&self, theme: cosmic::Theme) { + for element in &*self.elements.lock().unwrap() { + element.iced.set_theme(theme.clone()) + } + } +} + +#[derive(Clone)] +pub enum Item { + Separator, + Submenu { + title: String, + items: Vec, + }, + Entry { + title: String, + shortcut: Option, + on_press: Arc) + Send + Sync>>, + toggled: bool, + submenu: bool, + disabled: bool, + }, +} + +impl fmt::Debug for Item { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Separator => write!(f, "Separator"), + Self::Submenu { title, items } => f + .debug_struct("Submenu") + .field("title", title) + .field("items", items) + .finish(), + Self::Entry { + title, + shortcut, + on_press: _, + toggled, + submenu, + disabled, + } => f + .debug_struct("Entry") + .field("title", title) + .field("shortcut", shortcut) + .field("on_press", &"...") + .field("toggled", toggled) + .field("submenu", submenu) + .field("disabled", disabled) + .finish(), + } + } +} + +impl Item { + pub fn new, F: Fn(&LoopHandle<'_, State>) + Send + Sync + 'static>( + title: S, + on_press: F, + ) -> Item { + Item::Entry { + title: title.into(), + shortcut: None, + on_press: Arc::new(Box::new(on_press)), + toggled: false, + submenu: false, + disabled: false, + } + } + + pub fn new_submenu>(title: S, items: Vec) -> Item { + Item::Submenu { + title: title.into(), + items, + } + } + + pub fn shortcut(mut self, shortcut: impl Into>) -> Self { + if let Item::Entry { + shortcut: ref mut s, + .. + } = self + { + *s = shortcut.into(); + } + self + } + + pub fn toggled(mut self, toggled: bool) -> Self { + if let Item::Entry { + toggled: ref mut t, .. + } = self + { + *t = toggled; + } + self + } + + pub fn disabled(mut self, disabled: bool) -> Self { + if let Item::Entry { + disabled: ref mut d, + .. + } = self + { + *d = disabled; + } + self + } +} + +/// Menu that comes up when right-clicking an application header bar +#[derive(Debug)] +pub struct ContextMenu { + items: Vec, + selected: AtomicBool, + row_width: Mutex>, +} + +impl ContextMenu { + pub fn new(items: Vec) -> ContextMenu { + ContextMenu { + items, + selected: AtomicBool::new(false), + row_width: Mutex::new(None), + } + } + + pub fn set_row_width(&self, width: f32) { + *self.row_width.lock().unwrap() = Some(width); + } +} + +#[derive(Debug, Clone)] +pub enum Message { + ItemEntered(usize, IcedRectangle), + ItemPressed(usize), + ItemLeft(usize, IcedRectangle), +} + +impl item::CursorEvents for Message { + fn cursor_entered(idx: usize, bounds: IcedRectangle) -> Self { + Message::ItemEntered(idx, bounds) + } + + fn cursor_left(idx: usize, bounds: IcedRectangle) -> Self { + Message::ItemLeft(idx, bounds) + } +} + +impl Program for ContextMenu { + type Message = Message; + + fn update( + &mut self, + message: Self::Message, + loop_handle: &LoopHandle<'static, crate::state::State>, + last_seat: Option<&(Seat, Serial)>, + ) -> Task { + match message { + Message::ItemPressed(idx) => { + if let Some(Item::Entry { on_press, .. }) = self.items.get_mut(idx) { + (on_press)(loop_handle); + self.selected.store(true, Ordering::SeqCst); + } + // TODO: If Submenu, then also expand on "Pressed" for touch events. + // But right now we don't have any touch responsive menus with submenus + } + Message::ItemEntered(idx, bounds) => { + if let Some(Item::Submenu { items, .. }) = self.items.get_mut(idx) + && let Some((seat, _)) = last_seat.cloned() + { + let items = items.clone(); + let _ = loop_handle.insert_idle(move |state| { + let grab_state = seat + .user_data() + .get::() + .unwrap() + .lock() + .unwrap(); + + if let Some(grab_state) = &*grab_state { + let mut elements = grab_state.elements.lock().unwrap(); + + let position = elements.last().unwrap().position; + let mut theme = state.common.theme.clone(); + theme.transparent = theme.cosmic().frosted_system_interface; + let element = IcedElement::new( + ContextMenu::new(items), + Size::default(), + state.common.event_loop_handle.clone(), + theme, + ); + + let min_size = element.minimum_size(); + element.with_program(|p| { + *p.row_width.lock().unwrap() = Some(min_size.w as f32); + }); + element.resize(min_size); + + let output = seat.active_output(); + let position = [ + // to the right -> down + Rectangle::new( + position + + Point::from(( + bounds.width.floor() as i32, + bounds.y.ceil() as i32, + )), + min_size.as_global(), + ), + // to the right -> up + Rectangle::new( + position + + Point::from(( + bounds.width.floor() as i32, + bounds.y.ceil() as i32 + bounds.height.ceil() as i32 + - min_size.h, + )), + min_size.as_global(), + ), + // to the left -> down + Rectangle::new( + position + + Point::from((-min_size.w + 1, bounds.y.ceil() as i32)), + min_size.as_global(), + ), + // to the left -> up + Rectangle::new( + position + + Point::from(( + -min_size.w + 1, + bounds.y.ceil() as i32 + bounds.height.ceil() as i32 + - min_size.h, + )), + min_size.as_global(), + ), + ] + .iter() + .rev() // preference of max_by_key is backwards + .max_by_key(|rect| { + output + .geometry() + .intersection(**rect) + .map(|rect| rect.size.w * rect.size.h) + }) + .unwrap() + .loc; + element.output_enter(&output, element.bbox()); + element.set_additional_scale(*grab_state.scale.lock().unwrap()); + + elements.push(Element { + iced: element, + position, + pointer_entered: false, + touch_entered: None, + }) + } + }); + } + } + Message::ItemLeft(idx, _) => { + if let Some(Item::Submenu { .. }) = self.items.get_mut(idx) + && let Some((seat, _)) = last_seat.cloned() + { + let _ = loop_handle.insert_idle(move |_| { + let grab_state = seat + .user_data() + .get::() + .unwrap() + .lock() + .unwrap(); + + if let Some(grab_state) = &*grab_state { + let mut elements = grab_state.elements.lock().unwrap(); + elements.pop(); + } + }); + } + } + }; + + Task::none() + } + + fn view(&self) -> cosmic::Element<'_, Self::Message> { + let width = self + .row_width + .lock() + .unwrap() + .map(Length::Fixed) + .unwrap_or(Length::Shrink); + let mode = match width { + Length::Shrink => Length::Shrink, + _ => Length::Fill, + }; + + MenuColumn::with_children(self.items.iter().enumerate().map(|(idx, item)| { + match item { + Item::Separator => divider::horizontal::light() + .class(theme::Rule::Default) + .into(), + Item::Submenu { title, .. } => Row::with_children(vec![ + space::horizontal().width(16).into(), + text::body(title).width(mode).into(), + from_name("go-next-symbolic") + .size(16) + .prefer_svg(true) + .icon() + .into(), + ]) + .spacing(8) + .width(width) + .padding([8, 16]) + .align_y(Alignment::Center) + .apply(|row| item::SubmenuItem::new(row, idx)) + .style(theme::Button::MenuItem) + .into(), + Item::Entry { + title, + shortcut, + toggled, + disabled, + .. + } => { + let mut components = vec![ + if *toggled { + from_name("object-select-symbolic") + .size(16) + .prefer_svg(true) + .icon() + .class(theme::Svg::custom(|theme| iced_widget::svg::Style { + color: Some(theme.cosmic().accent.base.into()), + })) + .into() + } else { + space::horizontal().width(16).into() + }, + text::body(title) + .width(mode) + .class(if *disabled { + theme::Text::Custom(|theme| { + let mut color = theme.cosmic().background(false).component.on; + color.alpha *= 0.5; + TextStyle { + color: Some(color.into()), + ..Default::default() + } + }) + } else { + theme::Text::Default + }) + .into(), + space::horizontal().width(16).into(), + ]; + if let Some(shortcut) = shortcut.as_ref() { + components.push( + text::body(shortcut) + .align_x(Horizontal::Right) + .width(Length::Shrink) + .class(theme::Text::Custom(|theme| { + let mut color = theme.cosmic().background(false).component.on; + color.alpha *= 0.75; + TextStyle { + color: Some(color.into()), + ..Default::default() + } + })) + .into(), + ); + } + + Row::with_children(components) + .spacing(8) + .width(mode) + .align_y(Alignment::Center) + .apply(button::custom) + .width(width) + .padding([8, 16]) + .on_press_maybe((!disabled).then_some(Message::ItemPressed(idx))) + .class(theme::Button::MenuItem) + .into() + } + } + })) + .width(Length::Shrink) + .apply(iced_widget::container) + .padding(1) + .class(theme::Container::custom(|theme| { + let cosmic = theme.cosmic(); + let component = &cosmic.background(theme.cosmic().frosted_windows).component; + iced_widget::container::Style { + snap: true, + icon_color: Some(cosmic.accent.base.into()), + text_color: Some(component.on.into()), + background: Some(Background::Color(component.base.into())), + border: Border { + radius: cosmic.radius_s().into(), + width: 1.0, + color: component.divider.into(), + }, + shadow: Default::default(), + } + })) + .width(Length::Shrink) + .into() + } +} + +pub struct Element { + iced: IcedElement, + position: Point, + pointer_entered: bool, + touch_entered: Option, +} + +pub struct MenuGrab { + elements: Arc>>, + start_data: GrabStartData, + seat: Seat, + screen_space_relative: Option, + scale: Arc>, +} + +impl PointerGrab for MenuGrab { + fn motion( + &mut self, + state: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + _focus: Option<(PointerFocusTarget, Point)>, + event: &PointerMotionEvent, + ) { + { + let mut guard = self.elements.lock().unwrap(); + let elements = &mut *guard; + let event_location = if let Some(output) = self.screen_space_relative.as_ref() { + if state.common.shell.read().zoom_state().is_some() { + event + .location + .as_global() + .to_zoomed(output) + .to_global(output) + .as_logical() + } else { + event.location + } + } else { + event.location + }; + + if let Some(i) = elements.iter().position(|elem| { + let mut bbox = elem.iced.bbox(); + bbox.loc = elem.position.as_logical(); + + bbox.contains(event_location.to_i32_floor()) + }) { + let element = &mut elements[i]; + + let new_event = PointerMotionEvent { + location: event_location - element.position.as_logical().to_f64(), + serial: event.serial, + time: event.time, + }; + if !element.pointer_entered { + PointerTarget::enter(&element.iced, &self.seat, state, &new_event); + element.pointer_entered = true; + } else { + PointerTarget::motion(&element.iced, &self.seat, state, &new_event); + } + } else { + elements + .iter_mut() + .filter(|element| element.pointer_entered) + .skip(1) + .for_each(|element| { + PointerTarget::leave( + &element.iced, + &self.seat, + state, + event.serial, + event.time, + ); + element.pointer_entered = false; + }) + } + } + handle.motion(state, None, event); + } + + fn relative_motion( + &mut self, + state: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + _focus: Option<(PointerFocusTarget, Point)>, + event: &RelativeMotionEvent, + ) { + // While the grab is active, no client has pointer focus + handle.relative_motion(state, None, event); + } + + fn button( + &mut self, + state: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &ButtonEvent, + ) { + let any_entered = self + .elements + .lock() + .unwrap() + .iter() + .any(|elem| elem.pointer_entered); + if !any_entered { + if event.state == ButtonState::Pressed { + handle.unset_grab(self, state, event.serial, event.time, true); + } + } else { + let selected = { + let elements = self.elements.lock().unwrap(); + let mut selected = false; + for element in elements.iter().filter(|elem| elem.pointer_entered) { + PointerTarget::button(&element.iced, &self.seat, state, event); + selected = true; + } + selected + }; + if selected && event.state == ButtonState::Released { + handle.unset_grab(self, state, event.serial, event.time, true); + } else { + handle.button(state, event); + } + } + } + + fn axis( + &mut self, + state: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + details: AxisFrame, + ) { + handle.axis(state, details); + } + + fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) { + handle.frame(data) + } + + fn gesture_swipe_begin( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureSwipeBeginEvent, + ) { + handle.gesture_swipe_begin(data, event) + } + + fn gesture_swipe_update( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureSwipeUpdateEvent, + ) { + handle.gesture_swipe_update(data, event) + } + + fn gesture_swipe_end( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureSwipeEndEvent, + ) { + handle.gesture_swipe_end(data, event) + } + + fn gesture_pinch_begin( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GesturePinchBeginEvent, + ) { + handle.gesture_pinch_begin(data, event) + } + + fn gesture_pinch_update( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GesturePinchUpdateEvent, + ) { + handle.gesture_pinch_update(data, event) + } + + fn gesture_pinch_end( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GesturePinchEndEvent, + ) { + handle.gesture_pinch_end(data, event) + } + + fn gesture_hold_begin( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureHoldBeginEvent, + ) { + handle.gesture_hold_begin(data, event) + } + + fn gesture_hold_end( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureHoldEndEvent, + ) { + handle.gesture_hold_end(data, event) + } + + fn start_data(&self) -> &PointerGrabStartData { + match &self.start_data { + GrabStartData::Pointer(start_data) => start_data, + _ => unreachable!(), + } + } + + fn unset(&mut self, _data: &mut State) {} +} + +impl TouchGrab for MenuGrab { + fn down( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + _focus: Option<(PointerFocusTarget, Point)>, + event: &DownEvent, + ) { + { + let mut guard = self.elements.lock().unwrap(); + let elements = &mut *guard; + let event_location = if let Some(output) = self.screen_space_relative.as_ref() { + if data.common.shell.read().zoom_state().is_some() { + event + .location + .as_global() + .to_zoomed(output) + .to_global(output) + .as_logical() + } else { + event.location + } + } else { + event.location + }; + + if let Some(i) = elements.iter().position(|elem| { + let mut bbox = elem.iced.bbox(); + bbox.loc = elem.position.as_logical(); + + bbox.contains(event_location.to_i32_floor()) + }) { + let element = &mut elements[i]; + + let new_event = DownEvent { + slot: event.slot, + location: event_location - element.position.as_logical().to_f64(), + serial: event.serial, + time: event.time, + }; + if element.touch_entered.is_none() { + TouchTarget::down(&element.iced, &self.seat, data, &new_event); + element.touch_entered = Some(event.slot); + } + } + } + handle.down(data, None, event); + } + + fn up(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, event: &UpEvent) { + { + let elements = self.elements.lock().unwrap(); + for element in elements.iter().filter(|elem| { + elem.touch_entered + .as_ref() + .is_some_and(|slot| *slot == event.slot) + }) { + TouchTarget::up(&element.iced, &self.seat, data, event); + } + } + handle.unset_grab(self, data); + } + + fn motion( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + _focus: Option<(PointerFocusTarget, Point)>, + event: &TouchMotionEvent, + ) { + { + let elements = self.elements.lock().unwrap(); + for element in elements.iter().filter(|elem| { + elem.touch_entered + .as_ref() + .is_some_and(|slot| *slot == event.slot) + }) { + TouchTarget::motion(&element.iced, &self.seat, data, event); + } + } + handle.motion(data, None, event); + } + + fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>) { + handle.frame(data); + } + + fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>) { + { + let mut elements = self.elements.lock().unwrap(); + for element in elements.iter_mut() { + let _ = element.touch_entered.take(); + } + } + handle.cancel(data); + } + + fn shape( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + event: &smithay::input::touch::ShapeEvent, + ) { + handle.shape(data, event); + } + + fn orientation( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + event: &smithay::input::touch::OrientationEvent, + ) { + handle.orientation(data, event); + } + + fn start_data(&self) -> &TouchGrabStartData { + match &self.start_data { + GrabStartData::Touch(start_data) => start_data, + _ => unreachable!(), + } + } + + fn unset(&mut self, _data: &mut State) {} +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MenuAlignment { + pub x: AxisAlignment, + pub y: AxisAlignment, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AxisAlignment { + Corner(u32), + Centered, + PreferCentered, +} + +impl MenuAlignment { + pub const CORNER: Self = MenuAlignment { + x: AxisAlignment::Corner(0), + y: AxisAlignment::Corner(0), + }; + pub const PREFER_CENTERED: Self = MenuAlignment { + x: AxisAlignment::PreferCentered, + y: AxisAlignment::PreferCentered, + }; + pub const CENTERED: Self = MenuAlignment { + x: AxisAlignment::Centered, + y: AxisAlignment::Centered, + }; + pub const HORIZONTALLY_CENTERED: Self = MenuAlignment { + x: AxisAlignment::Centered, + y: AxisAlignment::Corner(0), + }; + pub const VERTICALLY_CENTERED: Self = MenuAlignment { + x: AxisAlignment::Corner(0), + y: AxisAlignment::Centered, + }; + + pub fn horizontally_centered(offset: u32, fixed: bool) -> MenuAlignment { + MenuAlignment { + x: if fixed { + AxisAlignment::Centered + } else { + AxisAlignment::PreferCentered + }, + y: AxisAlignment::Corner(offset), + } + } + + pub fn vertically_centered(offset: u32, fixed: bool) -> MenuAlignment { + MenuAlignment { + x: AxisAlignment::Corner(offset), + y: if fixed { + AxisAlignment::Centered + } else { + AxisAlignment::PreferCentered + }, + } + } + + fn rectangles( + &self, + position: Point, + size: Size, + ) -> Vec> { + fn for_alignment( + position: Point, + size: Size, + x: AxisAlignment, + y: AxisAlignment, + ) -> Vec> { + match (x, y) { + (AxisAlignment::Corner(x_offset), AxisAlignment::Corner(y_offset)) => { + let offset = Point::from((x_offset as i32, y_offset as i32)); + vec![ + Rectangle::new(position + offset, size), // normal + Rectangle::new( + position - Point::from((size.w, 0)) + + Point::from((-(x_offset as i32), y_offset as i32)), + size, + ), // flipped left + Rectangle::new( + position + - Point::from((0, size.h)) + - Point::from((x_offset as i32, -(y_offset as i32))), + size, + ), // flipped up + Rectangle::new(position - size.to_point() - offset, size), // flipped left & up + ] + } + (AxisAlignment::Centered, AxisAlignment::Corner(offset)) => { + let x = position.x - ((size.w as f64 / 2.).round() as i32); + vec![ + Rectangle::new(Point::from((x, position.y + offset as i32)), size), // below + Rectangle::new(Point::from((x, position.y - size.h - offset as i32)), size), // above + ] + } + (AxisAlignment::Corner(offset), AxisAlignment::Centered) => { + let y = position.y - ((size.h as f64 / 2.).round() as i32); + vec![ + Rectangle::new(Point::from((position.x + offset as i32, y)), size), // left + Rectangle::new(Point::from((position.x - size.w - offset as i32, y)), size), // right + ] + } + (AxisAlignment::Centered, AxisAlignment::Centered) => { + vec![Rectangle::new( + position - size.to_f64().downscale(2.).to_i32_round().to_point(), + size, + )] + } + (AxisAlignment::PreferCentered, AxisAlignment::PreferCentered) => for_alignment( + position, + size, + AxisAlignment::Centered, + AxisAlignment::Centered, + ) + .into_iter() + .chain(for_alignment( + position, + size, + AxisAlignment::Centered, + AxisAlignment::Corner(0), + )) + .chain(for_alignment( + position, + size, + AxisAlignment::Corner(0), + AxisAlignment::Centered, + )) + .chain(for_alignment( + position, + size, + AxisAlignment::Corner(0), + AxisAlignment::Corner(0), + )) + .collect(), + (AxisAlignment::PreferCentered, y) => { + for_alignment(position, size, AxisAlignment::Centered, y) + .into_iter() + .chain(for_alignment(position, size, AxisAlignment::Corner(0), y)) + .collect() + } + (x, AxisAlignment::PreferCentered) => { + for_alignment(position, size, x, AxisAlignment::Centered) + .into_iter() + .chain(for_alignment(position, size, x, AxisAlignment::Corner(0))) + .collect() + } + } + } + + for_alignment(position, size, self.x, self.y) + } +} + +impl MenuGrab { + pub fn new( + start_data: GrabStartData, + seat: &Seat, + items: impl Iterator, + position: Point, + alignment: MenuAlignment, + screen_space_relative: Option, + handle: LoopHandle<'static, crate::state::State>, + theme: cosmic::Theme, + ) -> MenuGrab { + let items = items.collect::>(); + let element = IcedElement::new(ContextMenu::new(items), Size::default(), handle, theme); + let min_size = element.minimum_size(); + element.with_program(|p| { + *p.row_width.lock().unwrap() = Some(min_size.w as f32); + }); + element.resize(min_size); + + let output = seat.active_output(); + // TODO: This feels a lot like cheap xdg-positioner. Refactor and unify + let position = alignment + .rectangles( + position, + min_size + .to_f64() + .upscale(screen_space_relative.unwrap_or(1.)) + .to_i32_round() + .as_global(), + ) + .iter() + .rev() // preference of max_by_key is backwards + .max_by_key(|rect| { + output + .geometry() + .intersection(**rect) + .map(|rect| rect.size.w * rect.size.h) + }) + .unwrap() + .loc; + + element.output_enter(&output, element.bbox()); + if let Some(scale) = screen_space_relative { + element.set_additional_scale(scale); + } + + let elements = Arc::new(Mutex::new(vec![Element { + iced: element, + position, + pointer_entered: false, + touch_entered: None, + }])); + + let scale = Arc::new(Mutex::new(screen_space_relative.unwrap_or(1.))); + let screen_space_relative = screen_space_relative.is_some().then_some(output); + + let grab_state = MenuGrabState { + elements: elements.clone(), + screen_space_relative: screen_space_relative.clone(), + scale: scale.clone(), + }; + + *seat + .user_data() + .get::() + .unwrap() + .lock() + .unwrap() = Some(grab_state); + + MenuGrab { + elements, + start_data, + seat: seat.clone(), + screen_space_relative, + scale, + } + } + + pub fn set_additional_scale(&self, scale: f64) { + *self.scale.lock().unwrap() = scale; + for element in &*self.elements.lock().unwrap() { + element.iced.set_additional_scale(scale); + } + } + + pub fn is_touch_grab(&self) -> bool { + match self.start_data { + GrabStartData::Touch(_) => true, + GrabStartData::Pointer(_) => false, + } + } +} + +impl Drop for MenuGrab { + fn drop(&mut self) { + self.seat + .user_data() + .get::() + .unwrap() + .lock() + .unwrap() + .take(); + } +} diff --git a/src/shell/grabs/mod.rs b/src/shell/grabs/mod.rs new file mode 100644 index 0000000..515916f --- /dev/null +++ b/src/shell/grabs/mod.rs @@ -0,0 +1,800 @@ +use calloop::LoopHandle; +use cosmic_settings_config::shortcuts; +use smithay::{ + input::{ + Seat, + pointer::{ + AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, + GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, + GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, + GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, PointerInnerHandle, + RelativeMotionEvent, + }, + touch::{ + DownEvent, GrabStartData as TouchGrabStartData, MotionEvent as TouchMotionEvent, + OrientationEvent, ShapeEvent, TouchGrab, TouchInnerHandle, UpEvent, + }, + }, + output::Output, + reexports::{ + wayland_protocols::xdg::shell::server::xdg_toplevel, + wayland_server::protocol::wl_surface::WlSurface, + }, + utils::{Logical, Point, Serial}, + xwayland::xwm, +}; + +use crate::{ + shell::{CosmicMapped, ManagedLayer}, + state::State, + utils::prelude::Global, +}; + +use super::{ + focus::target::PointerFocusTarget, + layout::{floating::ResizeSurfaceGrab, tiling::ResizeForkGrab}, +}; + +#[derive(Debug, Clone)] +pub enum GrabStartData { + Touch(TouchGrabStartData), + Pointer(PointerGrabStartData), +} + +impl GrabStartData { + pub fn focus(&self) -> Option<&(PointerFocusTarget, Point)> { + match self { + Self::Touch(touch) => touch.focus.as_ref(), + Self::Pointer(pointer) => pointer.focus.as_ref(), + } + } + + pub fn set_focus(&mut self, focus: Option<(PointerFocusTarget, Point)>) { + match self { + Self::Touch(touch) => touch.focus = focus, + Self::Pointer(pointer) => pointer.focus = focus, + } + } + + pub fn location(&self) -> Point { + match self { + Self::Touch(touch) => touch.location, + Self::Pointer(pointer) => pointer.location, + } + } + + pub fn set_location(&mut self, location: Point) { + match self { + Self::Touch(touch) => touch.location = location, + Self::Pointer(pointer) => pointer.location = location, + } + } + + pub fn distance(&self, cursor_location: Point) -> f64 { + let old = self.location(); + let new = cursor_location; + + ((new.x - old.x).powi(2) + (new.y - old.y).powi(2)).sqrt() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ReleaseMode { + Click, + NoMouseButtons, +} + +mod menu; +pub use self::menu::*; +mod moving; +pub use self::moving::SeatMoveGrabState; +mod delay; + +bitflags::bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct ResizeEdge: u32 { + const TOP = 0b0001; + const BOTTOM = 0b0010; + const LEFT = 0b0100; + const RIGHT = 0b1000; + + const TOP_LEFT = Self::TOP.bits() | Self::LEFT.bits(); + const BOTTOM_LEFT = Self::BOTTOM.bits() | Self::LEFT.bits(); + + const TOP_RIGHT = Self::TOP.bits() | Self::RIGHT.bits(); + const BOTTOM_RIGHT = Self::BOTTOM.bits() | Self::RIGHT.bits(); + } +} + +impl ResizeEdge { + pub fn flip_direction(&mut self) { + let mut new_edge = ResizeEdge::empty(); + if self.contains(ResizeEdge::TOP) { + new_edge.insert(ResizeEdge::BOTTOM); + } + if self.contains(ResizeEdge::BOTTOM) { + new_edge.insert(ResizeEdge::TOP); + } + if self.contains(ResizeEdge::LEFT) { + new_edge.insert(ResizeEdge::RIGHT); + } + if self.contains(ResizeEdge::RIGHT) { + new_edge.insert(ResizeEdge::LEFT); + } + *self = new_edge; + } +} + +impl From for ResizeEdge { + fn from(edge: shortcuts::action::ResizeEdge) -> Self { + match edge { + shortcuts::action::ResizeEdge::Bottom => ResizeEdge::BOTTOM, + shortcuts::action::ResizeEdge::BottomLeft => ResizeEdge::BOTTOM_LEFT, + shortcuts::action::ResizeEdge::BottomRight => ResizeEdge::BOTTOM_RIGHT, + shortcuts::action::ResizeEdge::Left => ResizeEdge::LEFT, + shortcuts::action::ResizeEdge::Right => ResizeEdge::RIGHT, + shortcuts::action::ResizeEdge::Top => ResizeEdge::TOP, + shortcuts::action::ResizeEdge::TopLeft => ResizeEdge::TOP_LEFT, + shortcuts::action::ResizeEdge::TopRight => ResizeEdge::TOP_RIGHT, + } + } +} + +impl From for shortcuts::action::ResizeEdge { + fn from(val: ResizeEdge) -> Self { + match val { + ResizeEdge::BOTTOM => shortcuts::action::ResizeEdge::Bottom, + ResizeEdge::BOTTOM_LEFT => shortcuts::action::ResizeEdge::BottomLeft, + ResizeEdge::BOTTOM_RIGHT => shortcuts::action::ResizeEdge::BottomRight, + ResizeEdge::LEFT => shortcuts::action::ResizeEdge::Left, + ResizeEdge::RIGHT => shortcuts::action::ResizeEdge::Right, + ResizeEdge::TOP => shortcuts::action::ResizeEdge::Top, + ResizeEdge::TOP_LEFT => shortcuts::action::ResizeEdge::TopLeft, + _ => shortcuts::action::ResizeEdge::TopRight, + } + } +} + +impl From for ResizeEdge { + #[inline] + fn from(x: xdg_toplevel::ResizeEdge) -> Self { + Self::from_bits(x.into()).unwrap() + } +} + +impl From for xdg_toplevel::ResizeEdge { + #[inline] + fn from(x: ResizeEdge) -> Self { + Self::try_from(x.bits()).unwrap() + } +} + +impl From for ResizeEdge { + #[inline] + fn from(x: xwm::ResizeEdge) -> Self { + match x { + xwm::ResizeEdge::Top => ResizeEdge::TOP, + xwm::ResizeEdge::Bottom => ResizeEdge::BOTTOM, + xwm::ResizeEdge::Left => ResizeEdge::LEFT, + xwm::ResizeEdge::Right => ResizeEdge::RIGHT, + xwm::ResizeEdge::TopLeft => ResizeEdge::TOP_LEFT, + xwm::ResizeEdge::BottomLeft => ResizeEdge::BOTTOM_LEFT, + xwm::ResizeEdge::TopRight => ResizeEdge::TOP_RIGHT, + xwm::ResizeEdge::BottomRight => ResizeEdge::BOTTOM_RIGHT, + } + } +} + +pub enum ResizeGrab { + Floating(ResizeSurfaceGrab), + Tiling(ResizeForkGrab), +} + +impl From for ResizeGrab { + fn from(grab: ResizeSurfaceGrab) -> Self { + ResizeGrab::Floating(grab) + } +} + +impl From for ResizeGrab { + fn from(grab: ResizeForkGrab) -> Self { + ResizeGrab::Tiling(grab) + } +} + +impl ResizeGrab { + pub fn is_touch_grab(&self) -> bool { + match self { + ResizeGrab::Floating(grab) => grab.is_touch_grab(), + ResizeGrab::Tiling(grab) => grab.is_touch_grab(), + } + } +} + +impl PointerGrab for ResizeGrab { + fn motion( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + focus: Option<(PointerFocusTarget, Point)>, + event: &MotionEvent, + ) { + match self { + ResizeGrab::Floating(grab) => PointerGrab::motion(grab, data, handle, focus, event), + ResizeGrab::Tiling(grab) => PointerGrab::motion(grab, data, handle, focus, event), + } + } + + fn relative_motion( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + focus: Option<(PointerFocusTarget, Point)>, + event: &RelativeMotionEvent, + ) { + match self { + ResizeGrab::Floating(grab) => grab.relative_motion(data, handle, focus, event), + ResizeGrab::Tiling(grab) => grab.relative_motion(data, handle, focus, event), + } + } + + fn button( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &ButtonEvent, + ) { + match self { + ResizeGrab::Floating(grab) => grab.button(data, handle, event), + ResizeGrab::Tiling(grab) => grab.button(data, handle, event), + } + } + + fn axis( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + details: AxisFrame, + ) { + match self { + ResizeGrab::Floating(grab) => grab.axis(data, handle, details), + ResizeGrab::Tiling(grab) => grab.axis(data, handle, details), + } + } + + fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) { + match self { + ResizeGrab::Floating(grab) => PointerGrab::frame(grab, data, handle), + ResizeGrab::Tiling(grab) => PointerGrab::frame(grab, data, handle), + } + } + + fn gesture_swipe_begin( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureSwipeBeginEvent, + ) { + match self { + ResizeGrab::Floating(grab) => grab.gesture_swipe_begin(data, handle, event), + ResizeGrab::Tiling(grab) => grab.gesture_swipe_begin(data, handle, event), + } + } + + fn gesture_swipe_update( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureSwipeUpdateEvent, + ) { + match self { + ResizeGrab::Floating(grab) => grab.gesture_swipe_update(data, handle, event), + ResizeGrab::Tiling(grab) => grab.gesture_swipe_update(data, handle, event), + } + } + + fn gesture_swipe_end( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureSwipeEndEvent, + ) { + match self { + ResizeGrab::Floating(grab) => grab.gesture_swipe_end(data, handle, event), + ResizeGrab::Tiling(grab) => grab.gesture_swipe_end(data, handle, event), + } + } + + fn gesture_pinch_begin( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GesturePinchBeginEvent, + ) { + match self { + ResizeGrab::Floating(grab) => grab.gesture_pinch_begin(data, handle, event), + ResizeGrab::Tiling(grab) => grab.gesture_pinch_begin(data, handle, event), + } + } + + fn gesture_pinch_update( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GesturePinchUpdateEvent, + ) { + match self { + ResizeGrab::Floating(grab) => grab.gesture_pinch_update(data, handle, event), + ResizeGrab::Tiling(grab) => grab.gesture_pinch_update(data, handle, event), + } + } + + fn gesture_pinch_end( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GesturePinchEndEvent, + ) { + match self { + ResizeGrab::Floating(grab) => grab.gesture_pinch_end(data, handle, event), + ResizeGrab::Tiling(grab) => grab.gesture_pinch_end(data, handle, event), + } + } + + fn gesture_hold_begin( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureHoldBeginEvent, + ) { + match self { + ResizeGrab::Floating(grab) => grab.gesture_hold_begin(data, handle, event), + ResizeGrab::Tiling(grab) => grab.gesture_hold_begin(data, handle, event), + } + } + + fn gesture_hold_end( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureHoldEndEvent, + ) { + match self { + ResizeGrab::Floating(grab) => grab.gesture_hold_end(data, handle, event), + ResizeGrab::Tiling(grab) => grab.gesture_hold_end(data, handle, event), + } + } + + fn start_data(&self) -> &PointerGrabStartData { + match self { + ResizeGrab::Floating(grab) => PointerGrab::start_data(grab), + ResizeGrab::Tiling(grab) => PointerGrab::start_data(grab), + } + } + + fn unset(&mut self, data: &mut State) { + match self { + ResizeGrab::Floating(grab) => PointerGrab::unset(grab, data), + ResizeGrab::Tiling(grab) => PointerGrab::unset(grab, data), + } + } +} + +impl TouchGrab for ResizeGrab { + fn down( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + focus: Option<(PointerFocusTarget, Point)>, + event: &DownEvent, + ) { + match self { + ResizeGrab::Floating(grab) => TouchGrab::down(grab, data, handle, focus, event), + ResizeGrab::Tiling(grab) => TouchGrab::down(grab, data, handle, focus, event), + } + } + + fn up(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, event: &UpEvent) { + match self { + ResizeGrab::Floating(grab) => TouchGrab::up(grab, data, handle, event), + ResizeGrab::Tiling(grab) => TouchGrab::up(grab, data, handle, event), + } + } + + fn motion( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + focus: Option<(PointerFocusTarget, Point)>, + event: &TouchMotionEvent, + ) { + match self { + ResizeGrab::Floating(grab) => TouchGrab::motion(grab, data, handle, focus, event), + ResizeGrab::Tiling(grab) => TouchGrab::motion(grab, data, handle, focus, event), + } + } + + fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>) { + match self { + ResizeGrab::Floating(grab) => TouchGrab::frame(grab, data, handle), + ResizeGrab::Tiling(grab) => TouchGrab::frame(grab, data, handle), + } + } + + fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>) { + match self { + ResizeGrab::Floating(grab) => TouchGrab::cancel(grab, data, handle), + ResizeGrab::Tiling(grab) => TouchGrab::cancel(grab, data, handle), + } + } + + fn shape( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + event: &ShapeEvent, + ) { + match self { + ResizeGrab::Floating(grab) => TouchGrab::shape(grab, data, handle, event), + ResizeGrab::Tiling(grab) => TouchGrab::shape(grab, data, handle, event), + } + } + + fn orientation( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + event: &OrientationEvent, + ) { + match self { + ResizeGrab::Floating(grab) => TouchGrab::orientation(grab, data, handle, event), + ResizeGrab::Tiling(grab) => TouchGrab::orientation(grab, data, handle, event), + } + } + + fn start_data(&self) -> &TouchGrabStartData { + match self { + ResizeGrab::Floating(grab) => TouchGrab::start_data(grab), + ResizeGrab::Tiling(grab) => TouchGrab::start_data(grab), + } + } + + fn unset(&mut self, data: &mut State) { + match self { + ResizeGrab::Floating(grab) => TouchGrab::unset(grab, data), + ResizeGrab::Tiling(grab) => TouchGrab::unset(grab, data), + } + } +} + +pub enum MoveGrab { + Move(moving::MoveGrab), + Delayed(delay::DelayGrab), +} + +impl MoveGrab { + pub fn new( + start_data: GrabStartData, + window: CosmicMapped, + seat: &Seat, + initial_window_location: Point, + cursor_output: Output, + indicator_thickness: u8, + edge_snap_threshold: f64, + previous_layer: ManagedLayer, + release: ReleaseMode, + evlh: LoopHandle<'static, State>, + ) -> MoveGrab { + MoveGrab::Move(moving::MoveGrab::new( + start_data, + window, + seat, + initial_window_location, + cursor_output, + indicator_thickness, + edge_snap_threshold, + previous_layer, + release, + evlh, + )) + } + + pub fn delayed( + start_data: GrabStartData, + surface: &WlSurface, + seat: &Seat, + serial: Option, + release: ReleaseMode, + move_out_of_stack: bool, + ) -> MoveGrab { + let surface = surface.clone(); + let seat_clone = seat.clone(); + + MoveGrab::Delayed(delay::DelayGrab::new( + move |data| { + data.common.shell.write().move_request( + &surface, + &seat_clone, + serial, + release, + move_out_of_stack, + &data.common.config, + &data.common.event_loop_handle, + false, + ) + }, + seat.clone(), + serial, + start_data, + )) + } + + pub fn is_tiling_grab(&self) -> bool { + match self { + MoveGrab::Move(m) => m.is_tiling_grab(), + _ => false, + } + } + + pub fn is_touch_grab(&self) -> bool { + match self { + MoveGrab::Move(m) => m.is_touch_grab(), + MoveGrab::Delayed(d) => d.is_touch_grab(), + } + } +} +impl PointerGrab for MoveGrab { + fn motion( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + focus: Option<(PointerFocusTarget, Point)>, + event: &MotionEvent, + ) { + match self { + MoveGrab::Move(grab) => PointerGrab::motion(grab, data, handle, focus, event), + MoveGrab::Delayed(grab) => PointerGrab::motion(grab, data, handle, focus, event), + } + } + + fn relative_motion( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + focus: Option<(PointerFocusTarget, Point)>, + event: &RelativeMotionEvent, + ) { + match self { + MoveGrab::Move(grab) => grab.relative_motion(data, handle, focus, event), + MoveGrab::Delayed(grab) => grab.relative_motion(data, handle, focus, event), + } + } + + fn button( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &ButtonEvent, + ) { + match self { + MoveGrab::Move(grab) => grab.button(data, handle, event), + MoveGrab::Delayed(grab) => grab.button(data, handle, event), + } + } + + fn axis( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + details: AxisFrame, + ) { + match self { + MoveGrab::Move(grab) => grab.axis(data, handle, details), + MoveGrab::Delayed(grab) => grab.axis(data, handle, details), + } + } + + fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) { + match self { + MoveGrab::Move(grab) => PointerGrab::frame(grab, data, handle), + MoveGrab::Delayed(grab) => PointerGrab::frame(grab, data, handle), + } + } + + fn gesture_swipe_begin( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureSwipeBeginEvent, + ) { + match self { + MoveGrab::Move(grab) => grab.gesture_swipe_begin(data, handle, event), + MoveGrab::Delayed(grab) => grab.gesture_swipe_begin(data, handle, event), + } + } + + fn gesture_swipe_update( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureSwipeUpdateEvent, + ) { + match self { + MoveGrab::Move(grab) => grab.gesture_swipe_update(data, handle, event), + MoveGrab::Delayed(grab) => grab.gesture_swipe_update(data, handle, event), + } + } + + fn gesture_swipe_end( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureSwipeEndEvent, + ) { + match self { + MoveGrab::Move(grab) => grab.gesture_swipe_end(data, handle, event), + MoveGrab::Delayed(grab) => grab.gesture_swipe_end(data, handle, event), + } + } + + fn gesture_pinch_begin( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GesturePinchBeginEvent, + ) { + match self { + MoveGrab::Move(grab) => grab.gesture_pinch_begin(data, handle, event), + MoveGrab::Delayed(grab) => grab.gesture_pinch_begin(data, handle, event), + } + } + + fn gesture_pinch_update( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GesturePinchUpdateEvent, + ) { + match self { + MoveGrab::Move(grab) => grab.gesture_pinch_update(data, handle, event), + MoveGrab::Delayed(grab) => grab.gesture_pinch_update(data, handle, event), + } + } + + fn gesture_pinch_end( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GesturePinchEndEvent, + ) { + match self { + MoveGrab::Move(grab) => grab.gesture_pinch_end(data, handle, event), + MoveGrab::Delayed(grab) => grab.gesture_pinch_end(data, handle, event), + } + } + + fn gesture_hold_begin( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureHoldBeginEvent, + ) { + match self { + MoveGrab::Move(grab) => grab.gesture_hold_begin(data, handle, event), + MoveGrab::Delayed(grab) => grab.gesture_hold_begin(data, handle, event), + } + } + + fn gesture_hold_end( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureHoldEndEvent, + ) { + match self { + MoveGrab::Move(grab) => grab.gesture_hold_end(data, handle, event), + MoveGrab::Delayed(grab) => grab.gesture_hold_end(data, handle, event), + } + } + + fn start_data(&self) -> &PointerGrabStartData { + match self { + MoveGrab::Move(grab) => PointerGrab::start_data(grab), + MoveGrab::Delayed(grab) => PointerGrab::start_data(grab), + } + } + + fn unset(&mut self, data: &mut State) { + match self { + MoveGrab::Move(grab) => PointerGrab::unset(grab, data), + MoveGrab::Delayed(grab) => PointerGrab::unset(grab, data), + } + } +} + +impl TouchGrab for MoveGrab { + fn down( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + focus: Option<(PointerFocusTarget, Point)>, + event: &DownEvent, + ) { + match self { + MoveGrab::Move(grab) => TouchGrab::down(grab, data, handle, focus, event), + MoveGrab::Delayed(grab) => TouchGrab::down(grab, data, handle, focus, event), + } + } + + fn up(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, event: &UpEvent) { + match self { + MoveGrab::Move(grab) => TouchGrab::up(grab, data, handle, event), + MoveGrab::Delayed(grab) => TouchGrab::up(grab, data, handle, event), + } + } + + fn motion( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + focus: Option<(PointerFocusTarget, Point)>, + event: &TouchMotionEvent, + ) { + match self { + MoveGrab::Move(grab) => TouchGrab::motion(grab, data, handle, focus, event), + MoveGrab::Delayed(grab) => TouchGrab::motion(grab, data, handle, focus, event), + } + } + + fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>) { + match self { + MoveGrab::Move(grab) => TouchGrab::frame(grab, data, handle), + MoveGrab::Delayed(grab) => TouchGrab::frame(grab, data, handle), + } + } + + fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>) { + match self { + MoveGrab::Move(grab) => TouchGrab::cancel(grab, data, handle), + MoveGrab::Delayed(grab) => TouchGrab::cancel(grab, data, handle), + } + } + + fn shape( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + event: &ShapeEvent, + ) { + match self { + MoveGrab::Move(grab) => TouchGrab::shape(grab, data, handle, event), + MoveGrab::Delayed(grab) => TouchGrab::shape(grab, data, handle, event), + } + } + + fn orientation( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + event: &OrientationEvent, + ) { + match self { + MoveGrab::Move(grab) => TouchGrab::orientation(grab, data, handle, event), + MoveGrab::Delayed(grab) => TouchGrab::orientation(grab, data, handle, event), + } + } + + fn start_data(&self) -> &TouchGrabStartData { + match self { + MoveGrab::Move(grab) => TouchGrab::start_data(grab), + MoveGrab::Delayed(grab) => TouchGrab::start_data(grab), + } + } + + fn unset(&mut self, data: &mut State) { + match self { + MoveGrab::Move(grab) => TouchGrab::unset(grab, data), + MoveGrab::Delayed(grab) => TouchGrab::unset(grab, data), + } + } +} diff --git a/src/shell/grabs/moving.rs b/src/shell/grabs/moving.rs new file mode 100644 index 0000000..d871483 --- /dev/null +++ b/src/shell/grabs/moving.rs @@ -0,0 +1,974 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{ + backend::render::{ + BackdropShader, IndicatorShader, Key, Usage, cursor::CursorState, element::AsGlowRenderer, + }, + shell::{ + CosmicMapped, CosmicSurface, Direction, ManagedLayer, + element::{CosmicMappedRenderElement, stack_hover::StackHover}, + focus::target::{KeyboardFocusTarget, PointerFocusTarget}, + layout::floating::TiledCorners, + }, + utils::prelude::*, + wayland::protocols::toplevel_info::{toplevel_enter_output, toplevel_enter_workspace}, +}; + +use calloop::LoopHandle; +use cosmic::theme::CosmicTheme; +use smallvec::SmallVec; +use smithay::{ + backend::{ + drm::DrmNode, + input::ButtonState, + renderer::{ + ImportAll, ImportMem, + element::{RenderElement, utils::RescaleRenderElement}, + }, + }, + desktop::{WindowSurfaceType, layer_map_for_output, space::SpaceElement}, + input::{ + Seat, + pointer::{ + AxisFrame, ButtonEvent, CursorIcon, GestureHoldBeginEvent, GestureHoldEndEvent, + GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, + GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, + GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, PointerInnerHandle, + RelativeMotionEvent, + }, + touch::{self, GrabStartData as TouchGrabStartData, TouchGrab, TouchInnerHandle}, + }, + output::Output, + utils::{IsAlive, Logical, Point, Rectangle, SERIAL_COUNTER, Scale}, +}; +use std::{ + collections::HashSet, + sync::{Mutex, atomic::Ordering}, + time::Instant, +}; + +use super::{GrabStartData, ReleaseMode}; + +pub type SeatMoveGrabState = Mutex>; + +const RESCALE_ANIMATION_DURATION: f64 = 150.0; + +pub struct MoveGrabState { + window: CosmicMapped, + window_offset: Point, + indicator_thickness: u8, + start: Instant, + previous: ManagedLayer, + snapping_zone: Option, + stacking_indicator: Option<(StackHover, Point)>, + location: Point, + cursor_output: Output, +} + +impl MoveGrabState { + #[profiling::function] + pub fn render( + &self, + renderer: &mut R, + output: &Output, + theme: &CosmicTheme, + scanout_node: Option, + push: &mut dyn FnMut(CosmicMappedRenderElement), + ) where + R: AsGlowRenderer + ImportAll + ImportMem, + R::TextureId: Send + Clone + 'static, + CosmicMappedRenderElement: RenderElement, + { + let scale = if self.previous == ManagedLayer::Tiling { + 0.6 + ((1.0 + - (Instant::now().duration_since(self.start).as_millis() as f64 + / RESCALE_ANIMATION_DURATION) + .min(1.0)) + * 0.4) + } else { + 1.0 + }; + let alpha = if &self.cursor_output == output { + 1.0 + } else { + 0.4 + }; + + let mut window_geo = self.window.geometry(); + window_geo.loc += self.location.to_i32_round() + self.window_offset; + if output + .geometry() + .as_logical() + .intersection(window_geo) + .is_none() + { + return; + } + + let output_scale: Scale = output.current_scale().fractional_scale().into(); + let scaling_offset = + self.window_offset - self.window_offset.to_f64().upscale(scale).to_i32_round(); + let render_location = self.location.to_i32_round() - output.geometry().loc.as_logical() + + self.window_offset + - scaling_offset; + + for (indicator, location) in self.stacking_indicator.iter() { + indicator.push_render_elements( + renderer, + location.to_physical_precise_round(output_scale), + output_scale, + 1.0, + &mut |elem| push(elem.into()), + None, + ); + } + + self.window.push_popup_render_elements::( + renderer, + (render_location - self.window.geometry().loc).to_physical_precise_round(output_scale), + output_scale, + alpha, + scanout_node, + push, + ); + + let active_window_hint = crate::theme::active_window_hint(theme); + let radius = self + .element() + .corner_radius(window_geo.size, self.indicator_thickness); + + if self.indicator_thickness > 0 { + push( + IndicatorShader::focus_element( + renderer, + Key::Window(Usage::MoveGrabIndicator, self.window.key()), + Rectangle::new( + render_location, + self.window + .geometry() + .size + .to_f64() + .upscale(scale) + .to_i32_round(), + ) + .as_local(), + self.indicator_thickness, + radius, + alpha, + output_scale.x, + [ + active_window_hint.red, + active_window_hint.green, + active_window_hint.blue, + ], + ) + .into(), + ) + } + + let map_window_element = |elem| match elem { + CosmicMappedRenderElement::Stack(stack) => { + CosmicMappedRenderElement::GrabbedStack(RescaleRenderElement::from_element( + stack, + render_location + .to_physical_precise_round(output.current_scale().fractional_scale()), + scale, + )) + } + CosmicMappedRenderElement::Window(window) => { + CosmicMappedRenderElement::GrabbedWindow(RescaleRenderElement::from_element( + window, + render_location + .to_physical_precise_round(output.current_scale().fractional_scale()), + scale, + )) + } + x => x, + }; + + let mut lower_elements = SmallVec::<[CosmicMappedRenderElement; 4]>::new_const(); + self.window.push_render_elements( + renderer, + (render_location - self.window.geometry().loc).to_physical_precise_round(output_scale), + None, + output_scale, + alpha, + Some(false), + scanout_node, + &mut |elem| push(map_window_element(elem)), + &mut |elem| lower_elements.push(map_window_element(elem)), + ); + if let Some(shadow_element) = self.window.shadow_render_element( + renderer, + (render_location - self.window.geometry().loc).to_physical_precise_round(output_scale), + None, + output_scale, + scale, + alpha, + ) { + push(shadow_element); + } + for elem in lower_elements.into_iter() { + push(elem); + } + + let non_exclusive_geometry = { + let layers = layer_map_for_output(output); + layers.non_exclusive_zone() + }; + + let gaps = (theme.gaps.0 as i32, theme.gaps.1 as i32); + let thickness = self.indicator_thickness.max(1); + + if let Some(t) = &self.snapping_zone + && &self.cursor_output == output + { + let base_color = theme.palette.neutral_9; + let overlay_geometry = t.overlay_geometry(non_exclusive_geometry, gaps); + + push( + IndicatorShader::element( + renderer, + Key::Window(Usage::SnappingIndicator, self.window.key()), + overlay_geometry, + thickness, + [ + theme.radius_s()[0] as u8, + theme.radius_s()[1] as u8, + theme.radius_s()[2] as u8, + theme.radius_s()[3] as u8, + ], + 1.0, + output_scale.x, + [ + active_window_hint.red, + active_window_hint.green, + active_window_hint.blue, + ], + ) + .into(), + ); + push( + BackdropShader::element( + renderer, + Key::Window(Usage::SnappingIndicator, self.window.key()), + t.overlay_geometry(non_exclusive_geometry, gaps), + theme.radius_s()[0], // TODO: Fix once shaders support 4 corner radii customization + 0.4, + [base_color.red, base_color.green, base_color.blue], + ) + .into(), + ) + } + } + + pub fn element(&self) -> CosmicMapped { + self.window.clone() + } + + pub fn window(&self) -> CosmicSurface { + self.window.active_window() + } +} + +struct NotSend(pub T); +unsafe impl Send for NotSend {} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SnappingZone { + Maximize, + Top, + TopLeft, + Left, + BottomLeft, + Bottom, + BottomRight, + Right, + TopRight, +} + +const SNAP_RANGE: i32 = 32; +const SNAP_RANGE_MAXIMIZE: i32 = 22; +const SNAP_RANGE_TOP: i32 = 16; + +impl SnappingZone { + pub fn contains( + &self, + point: Point, + output_geometry: Rectangle, + ) -> bool { + if !output_geometry.contains(point) { + return false; + } + let top_zone_32 = point.y < output_geometry.loc.y + SNAP_RANGE_MAXIMIZE; + let top_zone_56 = point.y < output_geometry.loc.y + SNAP_RANGE_MAXIMIZE + SNAP_RANGE_TOP; + let left_zone = point.x < output_geometry.loc.x + SNAP_RANGE; + let right_zone = point.x > output_geometry.loc.x + output_geometry.size.w - SNAP_RANGE; + let bottom_zone = point.y > output_geometry.loc.y + output_geometry.size.h - SNAP_RANGE; + let left_6th = point.x < output_geometry.loc.x + (output_geometry.size.w / 6); + let right_6th = point.x > output_geometry.loc.x + (output_geometry.size.w * 5 / 6); + let top_4th = point.y < output_geometry.loc.y + (output_geometry.size.h / 4); + let bottom_4th = point.y > output_geometry.loc.y + (output_geometry.size.h * 3 / 4); + match self { + SnappingZone::Maximize => top_zone_32 && !left_6th && !right_6th, + SnappingZone::Top => top_zone_56 && !top_zone_32 && !left_6th && !right_6th, + SnappingZone::TopLeft => (top_zone_56 && left_6th) || (left_zone && top_4th), + SnappingZone::Left => left_zone && !top_4th && !bottom_4th, + SnappingZone::BottomLeft => (bottom_zone && left_6th) || (left_zone && bottom_4th), + SnappingZone::Bottom => bottom_zone && !left_6th && !right_6th, + SnappingZone::BottomRight => (bottom_zone && right_6th) || (right_zone && bottom_4th), + SnappingZone::Right => right_zone && !top_4th && !bottom_4th, + SnappingZone::TopRight => (top_zone_56 && right_6th) || (right_zone && top_4th), + } + } + pub fn overlay_geometry( + &self, + non_exclusive_geometry: Rectangle, + gaps: (i32, i32), + ) -> Rectangle { + match self { + SnappingZone::Maximize => non_exclusive_geometry.as_local(), + SnappingZone::Top => TiledCorners::Top.relative_geometry(non_exclusive_geometry, gaps), + SnappingZone::TopLeft => { + TiledCorners::TopLeft.relative_geometry(non_exclusive_geometry, gaps) + } + SnappingZone::Left => { + TiledCorners::Left.relative_geometry(non_exclusive_geometry, gaps) + } + SnappingZone::BottomLeft => { + TiledCorners::BottomLeft.relative_geometry(non_exclusive_geometry, gaps) + } + SnappingZone::Bottom => { + TiledCorners::Bottom.relative_geometry(non_exclusive_geometry, gaps) + } + SnappingZone::BottomRight => { + TiledCorners::BottomRight.relative_geometry(non_exclusive_geometry, gaps) + } + SnappingZone::Right => { + TiledCorners::Right.relative_geometry(non_exclusive_geometry, gaps) + } + SnappingZone::TopRight => { + TiledCorners::TopRight.relative_geometry(non_exclusive_geometry, gaps) + } + } + } +} + +pub struct MoveGrab { + window: CosmicMapped, + start_data: GrabStartData, + seat: Seat, + cursor_output: Output, + window_outputs: HashSet, + previous: ManagedLayer, + release: ReleaseMode, + edge_snap_threshold: f64, + // SAFETY: This is only used on drop which will always be on the main thread + evlh: NotSend>, +} + +impl MoveGrab { + fn update_location(&mut self, state: &mut State, location: Point) { + let mut shell = state.common.shell.write(); + + let Some(current_output) = shell + .outputs() + .find(|output| { + output + .geometry() + .as_logical() + .overlaps_or_touches(Rectangle::new(location.to_i32_floor(), (0, 0).into())) + }) + .cloned() + else { + return; + }; + if self.cursor_output != current_output { + shell + .workspaces + .active_mut(&self.cursor_output) + .unwrap() + .tiling_layer + .cleanup_drag(); + self.cursor_output = current_output.clone(); + } + + let mut borrow = self + .seat + .user_data() + .get::() + .map(|s| s.lock().unwrap()); + if let Some(grab_state) = borrow.as_mut().and_then(|s| s.as_mut()) { + grab_state.location = location; + grab_state.cursor_output = self.cursor_output.clone(); + + let mut window_geo = self.window.geometry(); + window_geo.loc += location.to_i32_round() + grab_state.window_offset; + + if matches!(self.previous, ManagedLayer::Floating | ManagedLayer::Sticky) { + let loc = grab_state.window_offset.to_f64() + grab_state.location; + let size = window_geo.size.to_f64(); + let output_geom = self.cursor_output.geometry().to_f64().as_logical(); + let output_loc = output_geom.loc; + let output_size = output_geom.size; + + grab_state.location.x = if (loc.x - output_loc.x).abs() < self.edge_snap_threshold { + output_loc.x - grab_state.window_offset.x as f64 + } else if ((loc.x + size.w) - (output_loc.x + output_size.w)).abs() + < self.edge_snap_threshold + { + output_loc.x + output_size.w - grab_state.window_offset.x as f64 - size.w + } else { + grab_state.location.x + }; + grab_state.location.y = if (loc.y - output_loc.y).abs() < self.edge_snap_threshold { + output_loc.y - grab_state.window_offset.y as f64 + } else if ((loc.y + size.h) - (output_loc.y + output_size.h)).abs() + < self.edge_snap_threshold + { + output_loc.y + output_size.h - grab_state.window_offset.y as f64 - size.h + } else { + grab_state.location.y + }; + } + + for output in shell.outputs() { + if let Some(overlap) = output.geometry().as_logical().intersection(window_geo) { + if self.window_outputs.insert(output.clone()) { + self.window.output_enter(output, overlap); + if let Some(indicator) = + grab_state.stacking_indicator.as_ref().map(|x| &x.0) + { + indicator.output_enter(output); + } + } + } else if self.window_outputs.remove(output) { + self.window.output_leave(output); + if let Some(indicator) = grab_state.stacking_indicator.as_ref().map(|x| &x.0) { + indicator.output_leave(output); + } + } + } + + let indicator_location = shell.stacking_indicator(¤t_output, self.previous); + if indicator_location.is_some() != grab_state.stacking_indicator.is_some() { + grab_state.stacking_indicator = indicator_location.map(|geo| { + let size = geo.size.as_logical(); + let element = StackHover::new( + state.common.event_loop_handle.clone(), + size, + state.common.theme.clone(), + ); + for output in &self.window_outputs { + element.output_enter(output); + } + (element, geo.loc.as_logical()) + }); + } + + // Check for overlapping with zones + if grab_state.previous == ManagedLayer::Floating { + let output_geometry = current_output.geometry().to_local(¤t_output); + grab_state.snapping_zone = [ + SnappingZone::Maximize, + SnappingZone::Top, + SnappingZone::TopLeft, + SnappingZone::Left, + SnappingZone::BottomLeft, + SnappingZone::Bottom, + SnappingZone::BottomRight, + SnappingZone::Right, + SnappingZone::TopRight, + ] + .iter() + .find(|&x| { + x.contains( + location + .as_global() + .to_local(¤t_output) + .to_i32_floor(), + output_geometry, + ) + }) + .cloned(); + } + } + drop(borrow); + } +} + +impl PointerGrab for MoveGrab { + fn motion( + &mut self, + state: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + _focus: Option<(PointerFocusTarget, Point)>, + event: &MotionEvent, + ) { + self.update_location(state, event.location); + + // While the grab is active, no client has pointer focus + handle.motion(state, None, event); + if !self.window.alive() { + handle.unset_grab(self, state, event.serial, event.time, true); + } + } + + fn relative_motion( + &mut self, + state: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + _focus: Option<(PointerFocusTarget, Point)>, + event: &RelativeMotionEvent, + ) { + // While the grab is active, no client has pointer focus + handle.relative_motion(state, None, event); + } + + fn button( + &mut self, + state: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &ButtonEvent, + ) { + handle.button(state, event); + match self.release { + ReleaseMode::NoMouseButtons => { + if handle.current_pressed().is_empty() { + handle.unset_grab(self, state, event.serial, event.time, true); + } + } + ReleaseMode::Click => { + if event.state == ButtonState::Pressed { + handle.unset_grab(self, state, event.serial, event.time, true); + } + } + } + } + + fn axis( + &mut self, + state: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + details: AxisFrame, + ) { + handle.axis(state, details); + } + + fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) { + handle.frame(data) + } + + fn gesture_swipe_begin( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureSwipeBeginEvent, + ) { + handle.gesture_swipe_begin(data, event) + } + + fn gesture_swipe_update( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureSwipeUpdateEvent, + ) { + handle.gesture_swipe_update(data, event) + } + + fn gesture_swipe_end( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureSwipeEndEvent, + ) { + handle.gesture_swipe_end(data, event) + } + + fn gesture_pinch_begin( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GesturePinchBeginEvent, + ) { + handle.gesture_pinch_begin(data, event) + } + + fn gesture_pinch_update( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GesturePinchUpdateEvent, + ) { + handle.gesture_pinch_update(data, event) + } + + fn gesture_pinch_end( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GesturePinchEndEvent, + ) { + handle.gesture_pinch_end(data, event) + } + + fn gesture_hold_begin( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureHoldBeginEvent, + ) { + handle.gesture_hold_begin(data, event) + } + + fn gesture_hold_end( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureHoldEndEvent, + ) { + handle.gesture_hold_end(data, event) + } + + fn start_data(&self) -> &PointerGrabStartData { + match &self.start_data { + GrabStartData::Pointer(start_data) => start_data, + _ => unreachable!(), + } + } + + fn unset(&mut self, _data: &mut State) {} +} + +impl TouchGrab for MoveGrab { + fn down( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + _focus: Option<(PointerFocusTarget, Point)>, + event: &touch::DownEvent, + ) { + handle.down(data, None, event) + } + + fn up( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + event: &touch::UpEvent, + ) { + if event.slot == >::start_data(self).slot { + handle.unset_grab(self, data); + } + + handle.up(data, event); + } + + fn motion( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + _focus: Option<(PointerFocusTarget, Point)>, + event: &touch::MotionEvent, + ) { + if event.slot == >::start_data(self).slot { + self.update_location(data, event.location); + } + + handle.motion(data, None, event); + } + + fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>) { + handle.frame(data) + } + + fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>) { + handle.unset_grab(self, data); + } + + fn shape( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + event: &touch::ShapeEvent, + ) { + handle.shape(data, event) + } + + fn orientation( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + event: &touch::OrientationEvent, + ) { + handle.orientation(data, event) + } + + fn start_data(&self) -> &TouchGrabStartData { + match &self.start_data { + GrabStartData::Touch(start_data) => start_data, + _ => unreachable!(), + } + } + + fn unset(&mut self, _data: &mut State) {} +} + +impl MoveGrab { + pub fn new( + start_data: GrabStartData, + window: CosmicMapped, + seat: &Seat, + initial_window_location: Point, + cursor_output: Output, + indicator_thickness: u8, + edge_snap_threshold: f64, + previous_layer: ManagedLayer, + release: ReleaseMode, + evlh: LoopHandle<'static, State>, + ) -> MoveGrab { + // false-positive: `Output`s hash is based on it's inner ptr + #[allow(clippy::mutable_key_type)] + let mut outputs = HashSet::new(); + outputs.insert(cursor_output.clone()); + window.output_enter(&cursor_output, window.geometry()); // not accurate but... + window.moved_since_mapped.store(true, Ordering::SeqCst); + + let grab_state = MoveGrabState { + window: window.clone(), + window_offset: (initial_window_location + - start_data.location().as_global().to_i32_round()) + .as_logical(), + indicator_thickness, + start: Instant::now(), + stacking_indicator: None, + snapping_zone: None, + previous: previous_layer, + location: start_data.location(), + cursor_output: cursor_output.clone(), + }; + + *seat + .user_data() + .get::() + .unwrap() + .lock() + .unwrap() = Some(grab_state); + + { + let cursor_state = seat.user_data().get::().unwrap(); + cursor_state.lock().unwrap().set_shape(CursorIcon::Grabbing); + } + + MoveGrab { + window, + start_data, + seat: seat.clone(), + cursor_output, + window_outputs: outputs, + previous: previous_layer, + release, + edge_snap_threshold, + evlh: NotSend(evlh), + } + } + + pub fn is_tiling_grab(&self) -> bool { + self.previous == ManagedLayer::Tiling + } + + pub fn is_touch_grab(&self) -> bool { + match self.start_data { + GrabStartData::Touch(_) => true, + GrabStartData::Pointer(_) => false, + } + } +} + +impl Drop for MoveGrab { + fn drop(&mut self) { + // No more buttons are pressed, release the grab. + let output = self.cursor_output.clone(); + let seat = self.seat.clone(); + // false-positive: `Output`s hash is based on it's inner ptr + #[allow(clippy::mutable_key_type)] + let window_outputs = self.window_outputs.drain().collect::>(); + let previous = self.previous; + let window = self.window.clone(); + let is_touch_grab = matches!(self.start_data, GrabStartData::Touch(_)); + let cursor_output = self.cursor_output.clone(); + + let _ = self.evlh.0.insert_idle(move |state| { + let position: Option<(CosmicMapped, Point)> = if let Some(grab_state) = + seat.user_data() + .get::() + .and_then(|s| s.lock().unwrap().take()) + { + if grab_state.window.alive() { + let window_location = + (grab_state.location.to_i32_round() + grab_state.window_offset).as_global(); + let mut shell = state.common.shell.write(); + + let workspace_handle = shell.active_space(&output).unwrap().handle; + for old_output in window_outputs.iter().filter(|o| *o != &output) { + grab_state.window.output_leave(old_output); + } + + for (window, _) in grab_state.window.windows() { + toplevel_enter_output(&window, &output); + if previous != ManagedLayer::Sticky { + toplevel_enter_workspace(&window, &workspace_handle); + } + } + + match previous { + ManagedLayer::Sticky => { + grab_state.window.set_geometry(Rectangle::new( + window_location, + grab_state.window.geometry().size.as_global(), + )); + let set = shell.workspaces.sets.get_mut(&output).unwrap(); + let (window, location) = set + .sticky_layer + .drop_window(grab_state.window, window_location.to_local(&output)); + + Some((window, location.to_global(&output))) + } + ManagedLayer::Tiling + if shell.active_space(&output).unwrap().tiling_enabled => + { + let (window, location) = shell + .active_space_mut(&output) + .unwrap() + .tiling_layer + .drop_window(grab_state.window); + Some((window, location.to_global(&output))) + } + _ => { + grab_state.window.set_geometry(Rectangle::new( + window_location, + grab_state.window.geometry().size.as_global(), + )); + let theme = shell.theme.clone(); + let workspace = shell.active_space_mut(&output).unwrap(); + let (window, location) = workspace.floating_layer.drop_window( + grab_state.window, + window_location.to_local(&workspace.output), + ); + + if matches!(previous, ManagedLayer::Floating) + && let Some(sz) = grab_state.snapping_zone + { + // `last_geometry` was set to the pre-drag geometry(in FloatingLayout::unmap). + // Snapshot it here and restore it after so "restore-to-floating" goes back to where the user had the window. + let pre_drag_geometry = *window.last_geometry.lock().unwrap(); + + if sz == SnappingZone::Maximize { + shell.maximize_toggle( + &window, + &seat, + &state.common.event_loop_handle, + ); + if let Some(geo) = pre_drag_geometry + && let Some(state) = + window.maximized_state.lock().unwrap().as_mut() + { + state.original_geometry = geo; + } + } else { + let directions = match sz { + SnappingZone::Maximize => vec![], + SnappingZone::Top => vec![Direction::Up], + SnappingZone::TopLeft => { + vec![Direction::Up, Direction::Left] + } + SnappingZone::Left => vec![Direction::Left], + SnappingZone::BottomLeft => { + vec![Direction::Down, Direction::Left] + } + SnappingZone::Bottom => vec![Direction::Down], + SnappingZone::BottomRight => { + vec![Direction::Down, Direction::Right] + } + SnappingZone::Right => vec![Direction::Right], + SnappingZone::TopRight => { + vec![Direction::Up, Direction::Right] + } + }; + for direction in directions { + workspace.floating_layer.move_element( + direction, + &seat, + ManagedLayer::Floating, + &theme, + &window, + ); + } + if let Some(geo) = pre_drag_geometry { + *window.last_geometry.lock().unwrap() = Some(geo); + } + } + } + Some((window, location.to_global(&output))) + } + } + } else { + None + } + } else { + None + }; + + let mut shell = state.common.shell.write(); + shell + .workspaces + .active_mut(&cursor_output) + .unwrap() + .tiling_layer + .cleanup_drag(); + shell.set_overview_mode(None, state.common.event_loop_handle.clone()); + drop(shell); + + { + let cursor_state = seat.user_data().get::().unwrap(); + cursor_state.lock().unwrap().unset_shape(); + } + + if let Some((mapped, position)) = position { + let serial = SERIAL_COUNTER.next_serial(); + if !is_touch_grab { + let pointer = seat.get_pointer().unwrap(); + let current_location = pointer.current_location(); + + if let Some((target, offset)) = mapped.focus_under( + current_location - position.as_logical().to_f64(), + WindowSurfaceType::ALL, + &seat, + ) { + pointer.motion( + state, + Some(( + target, + position.as_logical().to_f64() - window.geometry().loc.to_f64() + + offset, + )), + &MotionEvent { + location: pointer.current_location(), + serial, + time: state.common.clock.now().as_millis(), + }, + ); + } + } + Shell::set_focus( + state, + Some(&KeyboardFocusTarget::from(mapped)), + &seat, + Some(serial), + false, + ) + } + }); + } +} diff --git a/src/shell/layout/floating/grabs/mod.rs b/src/shell/layout/floating/grabs/mod.rs new file mode 100644 index 0000000..1ec0805 --- /dev/null +++ b/src/shell/layout/floating/grabs/mod.rs @@ -0,0 +1,3 @@ +mod resize; + +pub use self::resize::*; diff --git a/src/shell/layout/floating/grabs/resize.rs b/src/shell/layout/floating/grabs/resize.rs new file mode 100644 index 0000000..f9e5cbf --- /dev/null +++ b/src/shell/layout/floating/grabs/resize.rs @@ -0,0 +1,582 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use std::sync::atomic::{AtomicBool, Ordering}; + +use crate::{ + backend::render::cursor::CursorState, + shell::{ + element::CosmicMapped, + focus::target::PointerFocusTarget, + grabs::{GrabStartData, ReleaseMode, ResizeEdge}, + }, + utils::prelude::*, +}; +use smithay::{ + backend::input::ButtonState, + desktop::{WindowSurface, space::SpaceElement}, + input::{ + Seat, + pointer::{ + AxisFrame, ButtonEvent, CursorIcon, GestureHoldBeginEvent, GestureHoldEndEvent, + GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, + GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, + GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, PointerInnerHandle, + RelativeMotionEvent, + }, + touch::{ + DownEvent, GrabStartData as TouchGrabStartData, MotionEvent as TouchMotionEvent, + OrientationEvent, ShapeEvent, TouchGrab, TouchInnerHandle, UpEvent, + }, + }, + output::Output, + utils::{IsAlive, Logical, Point, Rectangle, Size}, +}; +use tracing::debug; + +/// Information about the resize operation. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct ResizeData { + /// The edges the surface is being resized with. + pub edges: ResizeEdge, + /// The initial window location. + pub initial_window_location: Point, + /// The initial window size (geometry width and height). + pub initial_window_size: Size, +} + +/// State of the resize operation. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum ResizeState { + /// The surface is currently being resized. + Resizing(ResizeData), + /// The resize has finished, and the surface needs to commit its final state. + WaitingForCommit(ResizeData), +} + +pub struct ResizeSurfaceGrab { + start_data: GrabStartData, + seat: Seat, + window: CosmicMapped, + edges: ResizeEdge, + output: Output, + edge_snap_threshold: u32, + initial_window_location: Point, + initial_window_size: Size, + last_window_size: Size, + release: ReleaseMode, +} + +impl ResizeSurfaceGrab { + // Returns `true` if grab should be unset + fn update_location(&mut self, location: Point) -> bool { + // It is impossible to get `min_size` and `max_size` of dead toplevel, so we return early. + if !self.window.alive() { + self.seat + .user_data() + .get::() + .unwrap() + .0 + .store(false, Ordering::SeqCst); + return true; + } + + let (mut dx, mut dy) = (location - self.start_data.location().as_global()).into(); + + let mut new_window_width = self.initial_window_size.w; + let mut new_window_height = self.initial_window_size.h; + + let left_right = ResizeEdge::LEFT | ResizeEdge::RIGHT; + let top_bottom = ResizeEdge::TOP | ResizeEdge::BOTTOM; + + let mut location_diff = Point::default(); + + if self.edges.intersects(left_right) { + if self.edges.intersects(ResizeEdge::LEFT) { + location_diff.x += dx; + dx = -dx; + } + + new_window_width = (self.initial_window_size.w as f64 + dx) as i32; + + // If the resizing vertical edge is close to our output's edge in the same direction, snap to it. + let output_geom = self.output.geometry().to_local(&self.output); + if self.edges.intersects(ResizeEdge::LEFT) { + if (self.initial_window_location.x - dx as i32 - output_geom.loc.x).unsigned_abs() + < self.edge_snap_threshold + { + new_window_width = self.initial_window_size.w - output_geom.loc.x + + self.initial_window_location.x; + } + } else if (self.initial_window_location.x + self.initial_window_size.w + dx as i32 + - output_geom.loc.x + - output_geom.size.w) + .unsigned_abs() + < self.edge_snap_threshold + { + new_window_width = + output_geom.loc.x - self.initial_window_location.x + output_geom.size.w; + } + } + + if self.edges.intersects(top_bottom) { + if self.edges.intersects(ResizeEdge::TOP) { + location_diff.y += dy; + dy = -dy; + } + + new_window_height = (self.initial_window_size.h as f64 + dy) as i32; + + // If the resizing horizontal edge is close to our output's edge in the same direction, snap to it. + let output_geom = self.output.geometry().to_local(&self.output); + if self.edges.intersects(ResizeEdge::TOP) { + if (self.initial_window_location.y - dy as i32 - output_geom.loc.y).unsigned_abs() + < self.edge_snap_threshold + { + new_window_height = self.initial_window_size.h - output_geom.loc.y + + self.initial_window_location.y; + } + } else if (self.initial_window_location.y + self.initial_window_size.h + dy as i32 + - output_geom.loc.y + - output_geom.size.h) + .unsigned_abs() + < self.edge_snap_threshold + { + new_window_height = + output_geom.loc.y - self.initial_window_location.y + output_geom.size.h; + } + } + + let (min_size, max_size) = (self.window.min_size(), self.window.max_size()); + + let min_width = min_size.map(|s| s.w).unwrap_or(360); + let min_height = min_size.map(|s| s.h).unwrap_or(240); + let max_width = max_size.map(|s| s.w).unwrap_or(i32::MAX); + let max_height = max_size.map(|s| s.h).unwrap_or(i32::MAX); + + new_window_width = new_window_width.max(min_width).min(max_width); + new_window_height = new_window_height.max(min_height).min(max_height); + + self.last_window_size = (new_window_width, new_window_height).into(); + + let win_loc = + (self.initial_window_location + location_diff.to_i32_round()).to_global(&self.output); + self.window.set_resizing(true); + self.window + .set_geometry(Rectangle::new(win_loc, self.last_window_size.as_global())); + if self.window.latest_size_committed() { + self.window.configure(); + } + + false + } + + pub fn is_touch_grab(&self) -> bool { + match self.start_data { + GrabStartData::Touch(_) => true, + GrabStartData::Pointer(_) => false, + } + } +} + +impl PointerGrab for ResizeSurfaceGrab { + fn motion( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + _focus: Option<(PointerFocusTarget, Point)>, + event: &MotionEvent, + ) { + // While the grab is active, no client has pointer focus + handle.motion(data, None, event); + + if self.update_location(event.location.as_global()) { + handle.unset_grab(self, data, event.serial, event.time, true); + } + } + + fn relative_motion( + &mut self, + state: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + _focus: Option<(PointerFocusTarget, Point)>, + event: &RelativeMotionEvent, + ) { + // While the grab is active, no client has pointer focus + handle.relative_motion(state, None, event); + } + + fn button( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &ButtonEvent, + ) { + handle.button(data, event); + match self.release { + ReleaseMode::NoMouseButtons => { + if handle.current_pressed().is_empty() { + handle.unset_grab(self, data, event.serial, event.time, true); + } + } + ReleaseMode::Click => { + if event.state == ButtonState::Pressed { + handle.unset_grab(self, data, event.serial, event.time, true); + } + } + } + } + + fn axis( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + details: AxisFrame, + ) { + handle.axis(data, details) + } + + fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) { + handle.frame(data) + } + + fn gesture_swipe_begin( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureSwipeBeginEvent, + ) { + handle.gesture_swipe_begin(data, event) + } + + fn gesture_swipe_update( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureSwipeUpdateEvent, + ) { + handle.gesture_swipe_update(data, event) + } + + fn gesture_swipe_end( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureSwipeEndEvent, + ) { + handle.gesture_swipe_end(data, event) + } + + fn gesture_pinch_begin( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GesturePinchBeginEvent, + ) { + handle.gesture_pinch_begin(data, event) + } + + fn gesture_pinch_update( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GesturePinchUpdateEvent, + ) { + handle.gesture_pinch_update(data, event) + } + + fn gesture_pinch_end( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GesturePinchEndEvent, + ) { + handle.gesture_pinch_end(data, event) + } + + fn gesture_hold_begin( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureHoldBeginEvent, + ) { + handle.gesture_hold_begin(data, event) + } + + fn gesture_hold_end( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureHoldEndEvent, + ) { + handle.gesture_hold_end(data, event) + } + + fn start_data(&self) -> &PointerGrabStartData { + match &self.start_data { + GrabStartData::Pointer(start_data) => start_data, + _ => unreachable!(), + } + } + + fn unset(&mut self, _data: &mut State) { + self.ungrab(); + } +} + +impl TouchGrab for ResizeSurfaceGrab { + fn down( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + _focus: Option<(PointerFocusTarget, Point)>, + event: &DownEvent, + ) { + handle.down(data, None, event) + } + + fn up(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, event: &UpEvent) { + if event.slot == >::start_data(self).slot { + handle.unset_grab(self, data); + } + + handle.up(data, event); + } + + fn motion( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + _focus: Option<(PointerFocusTarget, Point)>, + event: &TouchMotionEvent, + ) { + if event.slot == >::start_data(self).slot + && self.update_location(event.location.as_global()) + { + handle.unset_grab(self, data); + } + + handle.motion(data, None, event); + } + + fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>) { + handle.frame(data) + } + + fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>) { + handle.unset_grab(self, data); + } + + fn shape( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + event: &ShapeEvent, + ) { + handle.shape(data, event) + } + + fn orientation( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + event: &OrientationEvent, + ) { + handle.orientation(data, event) + } + + fn start_data(&self) -> &TouchGrabStartData { + match &self.start_data { + GrabStartData::Touch(start_data) => start_data, + _ => unreachable!(), + } + } + + fn unset(&mut self, _data: &mut State) { + self.ungrab(); + } +} + +pub struct ResizeGrabMarker(AtomicBool); + +impl ResizeGrabMarker { + pub fn get(&self) -> bool { + self.0.load(Ordering::SeqCst) + } +} + +impl ResizeSurfaceGrab { + pub fn new( + start_data: GrabStartData, + mapped: CosmicMapped, + edges: ResizeEdge, + output: Output, + edge_snap_threshold: u32, + initial_window_location: Point, + initial_window_size: Size, + seat: &Seat, + release: ReleaseMode, + ) -> ResizeSurfaceGrab { + let resize_state = ResizeState::Resizing(ResizeData { + edges, + initial_window_location, + initial_window_size, + }); + + *mapped.resize_state.lock().unwrap() = Some(resize_state); + seat.user_data() + .get_or_insert::(|| ResizeGrabMarker(AtomicBool::new(true))) + .0 + .store(true, Ordering::SeqCst); + + let shape = match edges { + ResizeEdge::TOP_LEFT => Some(CursorIcon::NwResize), + ResizeEdge::TOP_RIGHT => Some(CursorIcon::NeResize), + ResizeEdge::BOTTOM_LEFT => Some(CursorIcon::SwResize), + ResizeEdge::BOTTOM_RIGHT => Some(CursorIcon::SeResize), + ResizeEdge::TOP => Some(CursorIcon::NResize), + ResizeEdge::RIGHT => Some(CursorIcon::EResize), + ResizeEdge::BOTTOM => Some(CursorIcon::SResize), + ResizeEdge::LEFT => Some(CursorIcon::WResize), + _ => None, + }; + if let Some(shape) = shape { + let cursor_state = seat.user_data().get::().unwrap(); + cursor_state.lock().unwrap().set_shape(shape); + } + + ResizeSurfaceGrab { + start_data, + seat: seat.clone(), + window: mapped, + edges, + output, + initial_window_location, + initial_window_size, + last_window_size: initial_window_size, + release, + edge_snap_threshold, + } + } + + pub fn apply_resize_to_location(window: CosmicMapped, shell: &mut Shell) { + let mut resize_state = window.resize_state.lock().unwrap(); + + if resize_state.is_none() { + return; + } + + let (output, floating_layer) = if let Some((output, set)) = shell + .workspaces + .sets + .iter_mut() + .find(|(_, set)| set.sticky_layer.mapped().any(|m| m == &window)) + { + (output, &mut set.sticky_layer) + } else if let Some(workspace) = shell.space_for_mut(&window) { + (&workspace.output, &mut workspace.floating_layer) + } else { + return; + }; + + if let Some(location) = floating_layer + .space + .element_location(&window) + .map(PointExt::as_local) + .map(|p| p.to_global(output)) + { + let mut new_location = None; + + // If the window is being resized by top or left, its location must be adjusted + // accordingly. + match *resize_state { + Some(ResizeState::Resizing(resize_data)) + | Some(ResizeState::WaitingForCommit(resize_data)) => { + let ResizeData { + edges, + initial_window_location, + initial_window_size, + } = resize_data; + let initial_window_location = initial_window_location.to_global(output); + + if edges.intersects(ResizeEdge::TOP_LEFT) { + let size = window.geometry().size; + let mut new = location; + if edges.intersects(ResizeEdge::LEFT) { + new.x = initial_window_location.x + (initial_window_size.w - size.w); + } + if edges.intersects(ResizeEdge::TOP) { + new.y = initial_window_location.y + (initial_window_size.h - size.h); + } + + new_location = Some(new); + } + } + _ => {} + }; + + // Finish resizing. + if let Some(ResizeState::WaitingForCommit(_)) = *resize_state + && !window.is_resizing(false).unwrap_or(false) + { + *resize_state = None; + } + std::mem::drop(resize_state); + + if let Some(new_location) = new_location { + for (window, offset) in window.windows() { + match window.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => { + update_reactive_popups( + toplevel, + new_location + offset.as_global(), + floating_layer.space.outputs(), + ); + } + WindowSurface::X11(surface) => { + let mut geometry = surface.last_configure(); + geometry.loc += (location - new_location).as_logical(); + let _ = surface.configure(geometry); + } + } + } + floating_layer.space.map_element( + window, + new_location.to_local(output).as_logical(), + false, + ); + } + } + } + + fn ungrab(&mut self) { + // No more buttons are pressed, release the grab. + self.seat + .user_data() + .get::() + .unwrap() + .0 + .store(false, Ordering::SeqCst); + + // If toplevel is dead, we can't resize it, so we return early. + if !self.window.alive() { + return; + } + + self.window.set_resizing(false); + self.window.configure(); + + let mut resize_state = self.window.resize_state.lock().unwrap(); + if let Some(ResizeState::Resizing(resize_data)) = *resize_state { + *resize_state = Some(ResizeState::WaitingForCommit(resize_data)); + } else { + debug!("unexpected resize state: {:?}", resize_state); + } + } +} + +impl Drop for ResizeSurfaceGrab { + fn drop(&mut self) { + let cursor_state = self.seat.user_data().get::().unwrap(); + cursor_state.lock().unwrap().unset_shape(); + } +} diff --git a/src/shell/layout/floating/mod.rs b/src/shell/layout/floating/mod.rs new file mode 100644 index 0000000..95d7264 --- /dev/null +++ b/src/shell/layout/floating/mod.rs @@ -0,0 +1,1693 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use std::{ + collections::{HashMap, VecDeque}, + sync::atomic::{AtomicBool, Ordering}, + time::{Duration, Instant}, +}; + +use cosmic_comp_config::AppearanceConfig; +use cosmic_settings_config::shortcuts::action::ResizeDirection; +use keyframe::{ease, functions::EaseInOutCubic}; +use smallvec::SmallVec; +use smithay::{ + backend::{ + drm::DrmNode, + renderer::element::{ + RenderElement, + utils::{Relocate, RelocateRenderElement, RescaleRenderElement}, + }, + }, + desktop::{PopupKind, Space, WindowSurfaceType, layer_map_for_output, space::SpaceElement}, + input::Seat, + output::Output, + utils::{IsAlive, Logical, Point, Rectangle, Scale, Size}, + wayland::seat::WaylandFocus, +}; + +use crate::{ + backend::render::{IndicatorShader, Key, Usage, element::AsGlowRenderer}, + shell::{ + CosmicSurface, Direction, ManagedLayer, MoveResult, ResizeMode, + element::{ + CosmicMapped, CosmicMappedRenderElement, CosmicWindow, MaximizedState, + resize_indicator::ResizeIndicator, + stack::{CosmicStackRenderElement, MoveResult as StackMoveResult, TAB_HEIGHT}, + window::CosmicWindowRenderElement, + }, + focus::{ + FocusStackMut, + target::{KeyboardFocusTarget, PointerFocusTarget}, + }, + grabs::{GrabStartData, ReleaseMode, ResizeEdge}, + }, + state::State, + utils::{prelude::*, tween::EaseRectangle}, + wayland::handlers::xdg_shell::popup::get_popup_toplevel, +}; + +mod grabs; +pub use self::grabs::*; + +pub const ANIMATION_DURATION: Duration = Duration::from_millis(200); +pub const MINIMIZE_ANIMATION_DURATION: Duration = Duration::from_millis(320); + +#[derive(Debug, Default)] +pub struct FloatingLayout { + pub(crate) space: Space, + last_output_size: Size, + spawn_order: Vec, + animations: HashMap, + hovered_stack: Option<(CosmicMapped, Rectangle)>, + dirty: AtomicBool, + pub theme: cosmic::Theme, + pub appearance: AppearanceConfig, +} + +#[derive(Debug)] +enum Animation { + Tiled { + start: Instant, + previous_geometry: Rectangle, + }, + Minimize { + start: Instant, + previous_geometry: Rectangle, + target_geometry: Rectangle, + }, + Unminimize { + start: Instant, + previous_geometry: Rectangle, + target_geometry: Rectangle, + }, +} + +impl Animation { + fn start(&self) -> &Instant { + match self { + Animation::Tiled { start, .. } => start, + Animation::Minimize { start, .. } => start, + Animation::Unminimize { start, .. } => start, + } + } + + fn alpha(&self) -> f32 { + match self { + Animation::Tiled { .. } => 1.0, + Animation::Minimize { start, .. } => { + let percentage = Instant::now() + .duration_since(*start) + .min(MINIMIZE_ANIMATION_DURATION) + .as_secs_f32() + / MINIMIZE_ANIMATION_DURATION.as_secs_f32(); + 1.0 - ((percentage - 0.5).max(0.0) * 2.0) + } + Animation::Unminimize { start, .. } => { + let percentage = Instant::now() + .duration_since(*start) + .min(MINIMIZE_ANIMATION_DURATION) + .as_secs_f32() + / MINIMIZE_ANIMATION_DURATION.as_secs_f32(); + (percentage * 2.0).min(1.0) + } + } + } + + fn previous_geometry(&self) -> &Rectangle { + match self { + Animation::Tiled { + previous_geometry, .. + } => previous_geometry, + Animation::Minimize { + previous_geometry, .. + } => previous_geometry, + Animation::Unminimize { + previous_geometry, .. + } => previous_geometry, + } + } + + fn geometry( + &self, + output_geometry: Rectangle, + current_geometry: Rectangle, + tiled_state: Option<&TiledCorners>, + gaps: (i32, i32), + ) -> Rectangle { + let (duration, target_rect) = match self { + Animation::Minimize { + target_geometry, .. + } + | Animation::Unminimize { + target_geometry, .. + } => (MINIMIZE_ANIMATION_DURATION, *target_geometry), + Animation::Tiled { .. } => { + let target_geometry = if let Some(target_rect) = + tiled_state.map(|state| state.relative_geometry(output_geometry, gaps)) + { + target_rect + } else { + current_geometry + }; + (ANIMATION_DURATION, target_geometry) + } + }; + let previous_rect = *self.previous_geometry(); + let start = *self.start(); + let now = Instant::now(); + let progress = + now.duration_since(start).min(duration).as_secs_f64() / duration.as_secs_f64(); + + ease( + EaseInOutCubic, + EaseRectangle(previous_rect), + EaseRectangle(target_rect), + progress, + ) + .unwrap() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum TiledCorners { + Top, + TopRight, + Right, + BottomRight, + Bottom, + BottomLeft, + Left, + TopLeft, +} + +impl TiledCorners { + pub fn relative_geometry( + &self, + output_geometry: Rectangle, + gaps: (i32, i32), + ) -> Rectangle { + let (_, inner) = gaps; + let (loc, size) = match self { + TiledCorners::Bottom => ( + Point::from(( + output_geometry.loc.x + inner, + output_geometry.loc.y + (output_geometry.size.h / 2) + inner / 2, + )), + Size::from(( + output_geometry.size.w - inner * 2, + output_geometry.size.h / 2 - inner * 3 / 2, + )), + ), + TiledCorners::BottomLeft => ( + Point::from(( + output_geometry.loc.x + inner, + output_geometry.loc.y + (output_geometry.size.h / 2) + inner / 2, + )), + Size::from(( + output_geometry.size.w / 2 - inner * 3 / 2, + output_geometry.size.h / 2 - inner * 3 / 2, + )), + ), + TiledCorners::BottomRight => ( + Point::from(( + output_geometry.loc.x + (output_geometry.size.w / 2) + inner / 2, + output_geometry.loc.y + (output_geometry.size.h / 2) + inner / 2, + )), + Size::from(( + output_geometry.size.w / 2 - inner * 3 / 2, + output_geometry.size.h / 2 - inner * 3 / 2, + )), + ), + TiledCorners::Left => ( + Point::from((output_geometry.loc.x + inner, output_geometry.loc.y + inner)), + Size::from(( + output_geometry.size.w / 2 - inner * 3 / 2, + output_geometry.size.h - inner * 2, + )), + ), + TiledCorners::Top => ( + Point::from((output_geometry.loc.x + inner, output_geometry.loc.y + inner)), + Size::from(( + output_geometry.size.w - inner * 2, + output_geometry.size.h / 2 - inner * 3 / 2, + )), + ), + TiledCorners::TopLeft => ( + Point::from((output_geometry.loc.x + inner, output_geometry.loc.y + inner)), + Size::from(( + output_geometry.size.w / 2 - inner * 3 / 2, + output_geometry.size.h / 2 - inner * 3 / 2, + )), + ), + TiledCorners::TopRight => ( + Point::from(( + output_geometry.loc.x + (output_geometry.size.w / 2) + inner / 2, + output_geometry.loc.y + inner, + )), + Size::from(( + output_geometry.size.w / 2 - inner * 3 / 2, + output_geometry.size.h / 2 - inner * 3 / 2, + )), + ), + TiledCorners::Right => ( + Point::from(( + output_geometry.loc.x + (output_geometry.size.w / 2) + inner / 2, + output_geometry.loc.y + inner, + )), + Size::from(( + output_geometry.size.w / 2 - inner * 3 / 2, + output_geometry.size.h - inner * 2, + )), + ), + }; + + Rectangle::new(loc, size).as_local() + } +} + +impl FloatingLayout { + pub fn new( + theme: cosmic::Theme, + appearance: AppearanceConfig, + output: &Output, + ) -> FloatingLayout { + let mut layout = Self { + theme, + last_output_size: output.geometry().size.as_local(), + appearance, + ..Default::default() + }; + layout.space.map_output(output, (0, 0)); + layout + } + + pub fn set_output(&mut self, output: &Output) { + let old_output = self.space.outputs().next().unwrap().clone(); + self.space.unmap_output(&old_output); + self.space.map_output(output, (0, 0)); + + let old_output_geometry = { + let layers = layer_map_for_output(&old_output); + layers.non_exclusive_zone() + } + .to_f64(); + let output_geometry = { + let layers = layer_map_for_output(output); + layers.non_exclusive_zone() + }; + + for mapped in self + .space + .elements() + .filter(|w| w.alive()) + .cloned() + .collect::>() + .into_iter() + { + let tiled_state = *mapped.floating_tiled.lock().unwrap(); + if let Some(tiled_state) = tiled_state { + let geometry = tiled_state.relative_geometry(output_geometry, self.gaps()); + self.map_internal( + mapped, + Some(geometry.loc), + Some(geometry.size.as_logical()), + None, + ); + } else { + let Some(geometry) = self.space.element_geometry(&mapped) else { + continue; + }; + let geometry = geometry.to_f64(); + let new_loc = ( + ((geometry.loc.x - old_output_geometry.loc.x).max(0.) + / old_output_geometry.size.w + * output_geometry.size.w as f64) + .round() as i32 + + output_geometry.loc.x, + ((geometry.loc.y - old_output_geometry.loc.y).max(0.) + / old_output_geometry.size.h + * output_geometry.size.h as f64) + .round() as i32 + + output_geometry.loc.y, + ); + self.map_internal(mapped, Some(Point::from(new_loc)), None, None); + } + } + + self.last_output_size = output.geometry().size.as_local(); + self.recalculate(); + } + + pub fn map( + &mut self, + mapped: impl Into, + position: impl Into>>, + ) { + let mapped = mapped.into(); + let position = position.into(); + + self.map_internal(mapped, position, None, None) + } + + pub fn map_maximized( + &mut self, + mapped: CosmicMapped, + previous_geometry: Rectangle, + animate: bool, + ) { + let output = self.space.outputs().next().unwrap().clone(); + let layers = layer_map_for_output(&output); + let geometry = layers.non_exclusive_zone().as_local(); + + mapped.set_bounds(geometry.size.as_logical()); + mapped.set_tiled(true); + mapped.set_maximized(true); + mapped.set_geometry(geometry.to_global(&output)); + mapped.configure(); + + if let Some(pos) = self.spawn_order.iter().position(|m| m == &mapped) { + self.spawn_order.truncate(pos); + } + + mapped.moved_since_mapped.store(true, Ordering::SeqCst); + + if animate { + if let Some(existing_anim) = self.animations.get_mut(&mapped) { + match existing_anim { + Animation::Unminimize { + target_geometry, .. + } => { + *target_geometry = geometry; + } + Animation::Minimize { .. } | Animation::Tiled { .. } => {} + } + } else { + self.animations.insert( + mapped.clone(), + Animation::Tiled { + start: Instant::now(), + previous_geometry, + }, + ); + } + } else { + self.animations.remove(&mapped); + } + // Restore to the corner this was snapped to if any + let snapped = mapped.floating_tiled.lock().unwrap().take(); + if let Some(snapped) = snapped + && let Some(state) = mapped.maximized_state.lock().unwrap().as_mut() + && state.original_snapped.is_none() + { + state.original_snapped = Some(snapped); + }; + self.space + .map_element(mapped, geometry.loc.as_logical(), true); + self.space.refresh(); + } + + pub(in crate::shell) fn map_internal( + &mut self, + mapped: CosmicMapped, + position: Option>, + size: Option>, + prev: Option>, + ) { + let already_mapped = self.space.element_geometry(&mapped).map(RectExt::as_local); + let mut win_geo = mapped.geometry().as_local(); + + let output = self.space.outputs().next().unwrap().clone(); + let layers = layer_map_for_output(&output); + let output_geometry = layers.non_exclusive_zone(); + mapped.set_bounds(output_geometry.size); + let last_geometry = *mapped.last_geometry.lock().unwrap(); + let min_size = mapped.min_size().unwrap_or((320, 240).into()); + + if let Some(size) = size + .map(SizeExt::as_local) + .or(last_geometry.map(|g| g.size)) + { + win_geo.size = size; + } else { + let max_size = mapped.max_size().unwrap_or( + ( + min_size.w.max(output_geometry.size.w / 3 * 2), + min_size.h.max(output_geometry.size.h / 3 * 2), + ) + .into(), + ); + + // if the current geometry is too large + if win_geo.size.w > max_size.w { + // try a more reasonable size + let mut width = output_geometry.size.w / 3 * 2; + if max_size.w != 0 { + // don't go larger then the max_size ... + width = std::cmp::min(max_size.w, width); + } + if min_size.w != 0 { + // ... but also don't go smaller than the min_size + width = std::cmp::max(min_size.w, width); + } + win_geo.size.w = width; + } + // but no matter the supported sizes, don't be larger than our non-exclusive-zone + win_geo.size.w = std::cmp::min(win_geo.size.w, output_geometry.size.w); + + if win_geo.size.h > max_size.h { + // try a more reasonable size + let mut height = output_geometry.size.h / 3 * 2; + if max_size.h != 0 { + // don't go larger then the max_size ... + height = std::cmp::min(max_size.h, height); + } + if min_size.h != 0 { + // ... but also don't go smaller than the min_size + height = std::cmp::max(min_size.h, height); + } + win_geo.size.h = height; + } + // but no matter the supported sizes, don't be larger than our non-exclusive-zone + win_geo.size.h = std::cmp::min(win_geo.size.h, output_geometry.size.h); + } + + let position = position + .or_else(|| last_geometry.map(|g| g.loc)) + .unwrap_or_else(|| { + // cleanup moved windows + if let Some(pos) = self + .spawn_order + .iter() + .position(|w| !w.alive() || w.moved_since_mapped.load(Ordering::SeqCst)) + { + self.spawn_order.truncate(pos); + } + + let three_fours_width = (output_geometry.size.w / 4 * 3).max(360); + + // figure out new position + let pos = self + .spawn_order + .last() + .and_then(|window| self.space.element_geometry(window)) + .filter(|geo| { + geo.size.w < three_fours_width + && win_geo.size.w < three_fours_width + && output_geometry.contains_rect(*geo) + }) + .map(|geometry| { + let mut geometry: Rectangle = Rectangle::new( + (geometry.loc.x as u32, geometry.loc.y as u32).into(), + (geometry.size.w as u32, geometry.size.h as u32).into(), + ); + + // move down + geometry.loc.y += 48; + + // do we need to address the height? + let new_column = if geometry.loc.y + min_size.h as u32 + <= (output_geometry.loc.y + output_geometry.size.h - 16) as u32 + { + // alternate to the sides + let offset = if self + .spawn_order + .iter() + .flat_map(|w| self.space.element_geometry(w)) + .filter(|geo| geo.size.w < three_fours_width) + .count() + % 2 + == 0 + { + (geometry.loc.x + geometry.size.w) + .checked_sub(96 + (win_geo.size.w as u32)) + } else { + (geometry.loc.x + geometry.size.w) + .checked_sub((win_geo.size.w as u32).saturating_sub(48)) + }; + + if let Some(offset) = offset { + geometry.loc.x = offset; + // do we need to resize? + if geometry.loc.y as i32 + win_geo.size.h + > output_geometry.loc.y + output_geometry.size.h - 16 + { + win_geo.size.h = + (output_geometry.loc.y + output_geometry.size.h - 16) + - geometry.loc.y as i32; + } + + false + } else { + true + } + } else { + true + }; + + if new_column { + let min_y = self + .spawn_order + .iter() + .flat_map(|w| { + self.space + .element_geometry(w) + .filter(|geo| geo.size.w < three_fours_width) + .map(|geo| geo.loc.y) + }) + .min() + .unwrap() as u32; + geometry.loc.y = min_y.saturating_sub(16); + + match geometry.loc.x.checked_sub(144) { + Some(new_x) => geometry.loc.x = new_x, + None => { + // if we go out to the left, cycle around to the right + geometry.loc.x = + ((output_geometry.loc.x + output_geometry.size.w) as u32) + .saturating_sub(geometry.size.w + 16) + } + }; + } + + // check padding again + if geometry.loc.x < (output_geometry.loc.x + 16) as u32 { + geometry.loc.x = (output_geometry.loc.x + 16) as u32; + } + if geometry.loc.y < (output_geometry.loc.y + 16) as u32 { + geometry.loc.y = (output_geometry.loc.y + 16) as u32; + } + // if the width would be too high, we wouldn't be here + if geometry.loc.y as i32 + win_geo.size.h + > (output_geometry.loc.y + output_geometry.size.h - 16) + { + win_geo.size.h = output_geometry.loc.y + output_geometry.size.h + - 16 + - geometry.loc.y as i32; + } + + Point::::from((geometry.loc.x as i32, geometry.loc.y as i32)) + }) + .unwrap_or_else(|| { + ( + output_geometry.loc.x + output_geometry.size.w / 2 - win_geo.size.w / 2, + output_geometry.loc.y + + (output_geometry.size.h / 2 - win_geo.size.h / 2) + .min(output_geometry.size.h / 8), + ) + .into() + }) + .as_local(); + + mapped.moved_since_mapped.store(false, Ordering::SeqCst); + self.spawn_order.push(mapped.clone()); + + pos + }); + + mapped.set_tiled(false); + mapped.set_geometry(Rectangle::new(position, win_geo.size).to_global(&output)); + mapped.configure(); + + if let Some(previous_geometry) = prev.or(already_mapped) { + self.animations.insert( + mapped.clone(), + Animation::Tiled { + start: Instant::now(), + previous_geometry, + }, + ); + } + self.space.map_element(mapped, position.as_logical(), false); + self.space.refresh(); + } + + pub fn remap_minimized( + &mut self, + mapped: CosmicMapped, + from: Rectangle, + position: Point, + ) { + if !mapped.alive() { + return; + } + + let output = self.space.outputs().next().unwrap().clone(); + let layers = layer_map_for_output(&output); + let geometry = layers.non_exclusive_zone().as_local(); + mapped.set_bounds(geometry.size.as_logical()); + let window_size = mapped.geometry().size; + + if mapped.is_maximized(false) { + mapped.set_geometry(geometry.to_global(&output)); + mapped.configure(); + } else { + mapped.set_geometry(Rectangle::new( + position.to_global(&output), + window_size.as_global(), + )); + } + + self.space + .map_element(mapped.clone(), position.as_logical(), true); + self.space.refresh(); + let target_geometry = self.space.element_geometry(&mapped).unwrap().as_local(); + + self.animations.insert( + mapped, + Animation::Unminimize { + start: Instant::now(), + previous_geometry: from, + target_geometry, + }, + ); + } + + pub fn unmap( + &mut self, + window: &CosmicMapped, + to: Option>, + ) -> Option> { + let mut mapped_geometry = self.space.element_geometry(window).map(RectExt::as_local)?; + let _ = self.animations.remove(window); + + if let Some(to) = to { + self.animations.insert( + window.clone(), + Animation::Minimize { + start: Instant::now(), + previous_geometry: if window.is_maximized(false) { + let output = self.space.outputs().next().unwrap(); + let layers = layer_map_for_output(output); + layers.non_exclusive_zone().as_local() + } else { + mapped_geometry + }, + target_geometry: to, + }, + ); + } + + if window.floating_tiled.lock().unwrap().take().is_some() { + if let Some(last_size) = window.last_geometry.lock().unwrap().map(|geo| geo.size) { + let geometry = Rectangle::new(mapped_geometry.loc, last_size); + window.set_tiled(false); + window.set_geometry(geometry.to_global(self.space.outputs().next().unwrap())); + window.configure(); + mapped_geometry.size = last_size; + } + } else if !window.is_maximized(true) { + if window.active_window().has_pending_changes() + && let Some(pending_size) = window.pending_size() + { + mapped_geometry.size = pending_size.as_local(); + } else if let Some(server_size) = window.last_server_size() { + mapped_geometry.size = server_size.as_local(); + } + *window.last_geometry.lock().unwrap() = Some(mapped_geometry); + } + + self.space.unmap_elem(window); + if let Some(pos) = self.spawn_order.iter().position(|w| w == window) { + self.spawn_order.truncate(pos); + } + window.moved_since_mapped.store(true, Ordering::SeqCst); + Some(mapped_geometry) + } + + pub fn drop_window( + &mut self, + window: CosmicMapped, + position: Point, + ) -> (CosmicMapped, Point) { + if self + .hovered_stack + .as_ref() + .is_some_and(|(stack, _)| stack == &window || !stack.alive()) + { + let _ = self.hovered_stack.take(); + } + + if let Some((mapped, geo)) = self.hovered_stack.take() { + let stack = mapped.stack_ref().unwrap(); + for surface in window.windows().map(|s| s.0) { + stack.add_window(surface, None, None); + } + (mapped, geo.loc) + } else { + self.map_internal(window.clone(), Some(position), None, None); + (window, position) + } + } + + pub fn element_geometry(&self, elem: &CosmicMapped) -> Option> { + self.space.element_geometry(elem).map(RectExt::as_local) + } + + pub fn popup_element_under( + &self, + location: Point, + seat: &Seat, + ) -> Option { + self.space + .elements() + .rev() + .map(|e| { + ( + e, + self.space.element_location(e).unwrap() - e.geometry().loc, + ) + }) + .filter(|(e, render_location)| { + let mut bbox = e.bbox(); + bbox.loc += *render_location; + bbox.to_f64().contains(location.as_logical()) + }) + .find_map(|(e, render_location)| { + let render_location = render_location.as_local().to_f64(); + let point = location - render_location; + if e.focus_under( + point.as_logical(), + WindowSurfaceType::POPUP | WindowSurfaceType::SUBSURFACE, + seat, + ) + .is_some() + { + Some(e.clone().into()) + } else { + None + } + }) + } + + pub fn toplevel_element_under( + &self, + location: Point, + seat: &Seat, + ) -> Option { + self.space + .elements() + .rev() + .map(|e| { + ( + e, + self.space.element_location(e).unwrap() - e.geometry().loc, + ) + }) + .filter(|(e, render_location)| { + let mut bbox = e.bbox(); + bbox.loc += *render_location; + bbox.to_f64().contains(location.as_logical()) + }) + .find_map(|(e, render_location)| { + let render_location = render_location.as_local().to_f64(); + let point = location - render_location; + if e.focus_under( + point.as_logical(), + WindowSurfaceType::TOPLEVEL | WindowSurfaceType::SUBSURFACE, + seat, + ) + .is_some() + { + Some(e.clone().into()) + } else { + None + } + }) + } + + pub fn popup_surface_under( + &self, + location: Point, + seat: &Seat, + ) -> Option<(PointerFocusTarget, Point)> { + self.space + .elements() + .rev() + .map(|e| { + ( + e, + self.space.element_location(e).unwrap() - e.geometry().loc, + ) + }) + .filter(|(e, render_location)| { + let mut bbox = e.bbox(); + bbox.loc += *render_location; + bbox.to_f64().contains(location.as_logical()) + }) + .find_map(|(e, render_location)| { + let render_location = render_location.as_local().to_f64(); + let point = location - render_location; + e.focus_under( + point.as_logical(), + WindowSurfaceType::POPUP | WindowSurfaceType::SUBSURFACE, + seat, + ) + .map(|(surface, surface_offset)| { + (surface, render_location + surface_offset.as_local()) + }) + }) + } + + pub fn toplevel_surface_under( + &self, + location: Point, + seat: &Seat, + ) -> Option<(PointerFocusTarget, Point)> { + self.space + .elements() + .rev() + .map(|e| { + ( + e, + self.space.element_location(e).unwrap() - e.geometry().loc, + ) + }) + .filter(|(e, render_location)| { + let mut bbox = e.bbox(); + bbox.loc += *render_location; + bbox.to_f64().contains(location.as_logical()) + }) + .find_map(|(e, render_location)| { + let render_location = render_location.as_local().to_f64(); + let point = location - render_location; + e.focus_under( + point.as_logical(), + WindowSurfaceType::TOPLEVEL | WindowSurfaceType::SUBSURFACE, + seat, + ) + .map(|(surface, surface_offset)| { + (surface, render_location + surface_offset.as_local()) + }) + }) + } + + pub fn update_pointer_position(&mut self, location: Option>) { + let Some(location) = location else { + self.hovered_stack.take(); + return; + }; + + let res = self + .space + .element_under(location.as_logical()) + .map(|(mapped, p)| (mapped.clone(), p.as_local())); + + if let Some((mapped, _)) = res.as_ref() { + let geometry = self.space.element_geometry(mapped).unwrap(); + let offset = location.y.round() as i32 - geometry.loc.y; + if mapped.is_stack() && offset.is_positive() && offset <= TAB_HEIGHT { + self.hovered_stack = Some((mapped.clone(), geometry.as_local())); + } else { + self.hovered_stack.take(); + } + } else { + self.hovered_stack.take(); + } + } + + pub fn stacking_indicator(&self) -> Option> { + self.hovered_stack.as_ref().map(|(_, geo)| *geo) + } + + pub fn resize_request( + &mut self, + mapped: &CosmicMapped, + seat: &Seat, + start_data: GrabStartData, + edges: ResizeEdge, + edge_snap_threshold: u32, + release: ReleaseMode, + ) -> Option { + if seat.get_pointer().is_some() { + let location = self.space.element_location(mapped)?.as_local(); + let size = mapped.geometry().size; + mapped.moved_since_mapped.store(true, Ordering::SeqCst); + + Some(grabs::ResizeSurfaceGrab::new( + start_data, + mapped.clone(), + edges, + self.space.outputs().next().cloned().unwrap(), + edge_snap_threshold, + location, + size, + seat, + release, + )) + } else { + None + } + } + + pub fn resize( + &mut self, + focused: &KeyboardFocusTarget, + direction: ResizeDirection, + edge: ResizeEdge, + amount: i32, + ) -> bool { + let Some(toplevel) = focused.toplevel() else { + return false; + }; + let Some(mapped) = self + .space + .elements() + .find(|m| m.has_surface(&toplevel, WindowSurfaceType::TOPLEVEL)) + else { + return false; + }; + if mapped.is_maximized(true) { + return false; + } + + let Some(original_geo) = self.space.element_geometry(mapped) else { + return false; // we don't have that window + }; + let mut geo = original_geo; + + if edge.contains(ResizeEdge::RIGHT) || edge.contains(ResizeEdge::LEFT) { + if direction == ResizeDirection::Inwards { + geo.size.w = (geo.size.w as u32).saturating_sub(amount as u32) as i32; + } else { + geo.size.w += amount; + } + if edge.contains(ResizeEdge::LEFT) { + if direction == ResizeDirection::Inwards { + geo.loc.x += amount; + } else { + geo.loc.x = (geo.loc.x as u32).saturating_sub(amount as u32) as i32; + } + } + } + if edge.contains(ResizeEdge::BOTTOM) || edge.contains(ResizeEdge::TOP) { + if direction == ResizeDirection::Inwards { + geo.size.h = (geo.size.h as u32).saturating_sub(amount as u32) as i32; + } else { + geo.size.h += amount; + } + if edge.contains(ResizeEdge::TOP) { + if direction == ResizeDirection::Inwards { + geo.loc.y += amount; + } else { + geo.loc.y = (geo.loc.y as u32).saturating_sub(amount as u32) as i32; + } + } + } + + let bounding_box = self + .space + .output_geometry(self.space.outputs().next().unwrap()) + .unwrap(); + let (min_size, max_size) = (mapped.min_size(), mapped.max_size()); + let min_width = min_size.map(|s| s.w).unwrap_or(360); + let min_height = min_size.map(|s| s.h).unwrap_or(240); + let max_width = max_size.map(|s| s.w).unwrap_or(i32::MAX); + let max_height = max_size.map(|s| s.h).unwrap_or(i32::MAX); + + geo.size.w = min_width.max(geo.size.w).min(max_width); + geo.size.h = min_height.max(geo.size.h).min(max_height); + geo = geo.intersection(bounding_box).unwrap(); + + *mapped.resize_state.lock().unwrap() = Some(ResizeState::Resizing(ResizeData { + edges: edge, + initial_window_location: original_geo.loc.as_local(), + initial_window_size: original_geo.size, + })); + + mapped.moved_since_mapped.store(true, Ordering::SeqCst); + mapped.set_resizing(true); + mapped.set_geometry( + geo.as_local() + .to_global(self.space.outputs().next().unwrap()), + ); + if mapped.latest_size_committed() { + mapped.configure(); + } + + true + } + + pub fn toggle_stacking( + &mut self, + mapped: &CosmicMapped, + mut focus_stack: FocusStackMut, + ) -> Option { + if !self.space.elements().any(|m| m == mapped) { + return None; + } + + let output = self.space.outputs().next().unwrap().clone(); + let mut mapped = mapped.clone(); + let geo = self.space.element_geometry(&mapped).unwrap(); + let location = geo.loc; + + if mapped.is_window() { + // if it is just a window + self.space.unmap_elem(&mapped); + mapped.convert_to_stack( + (&output, mapped.bbox()), + self.theme.clone(), + self.appearance, + ); + self.map_internal( + mapped.clone(), + Some(location.as_local()), + Some(geo.size), + None, + ); + focus_stack.append(mapped.clone()); + Some(KeyboardFocusTarget::Element(mapped)) + } else { + // if we have a stack + let mut surfaces = mapped.windows().map(|(s, _)| s).collect::>(); + let first = surfaces.pop_front().expect("Stack without a window?"); + let focused = mapped.active_window(); + + self.space.unmap_elem(&mapped); + let handle = mapped.loop_handle(); + mapped.convert_to_surface( + first, + (&output, mapped.bbox()), + self.theme.clone(), + self.appearance, + ); + let mut new_elements = vec![mapped.clone()]; + + // map the rest + for other in surfaces { + other.try_force_undecorated(false); + other.set_tiled(false); + let focused = other == focused; + let window = CosmicMapped::from(CosmicWindow::new( + other, + handle.clone(), + self.theme.clone(), + self.appearance, + )); + window.output_enter(&output, window.bbox()); + + { + let layer_map = layer_map_for_output(&output); + window.set_bounds(layer_map.non_exclusive_zone().size); + } + + if focused { + new_elements.insert(0, window.clone()); + } else { + new_elements.push(window.clone()); + } + self.map(window, None); + } + self.space.map_element(mapped.clone(), location, false); + self.space.refresh(); + + for elem in new_elements.into_iter().rev() { + focus_stack.append(elem); + } + + Some(KeyboardFocusTarget::Element(mapped)) + } + } + + pub fn toggle_stacking_focused( + &mut self, + seat: &Seat, + focus_stack: FocusStackMut, + ) -> Option { + let Some(KeyboardFocusTarget::Element(elem)) = seat.get_keyboard().unwrap().current_focus() + else { + return None; + }; + + self.toggle_stacking(&elem, focus_stack) + } + + pub fn move_element( + &mut self, + direction: Direction, + seat: &Seat, + layer: ManagedLayer, + theme: &cosmic::Theme, + element: &CosmicMapped, + ) -> MoveResult { + match element.handle_move(direction) { + StackMoveResult::Handled => MoveResult::Done, + StackMoveResult::MoveOut(surface, loop_handle) => { + let mapped: CosmicMapped = + CosmicWindow::new(surface, loop_handle, theme.clone(), self.appearance).into(); + let output = seat.active_output(); + let pos = self.space.element_geometry(element).unwrap().loc + + match direction { + Direction::Up => Point::from((5, -10)), + Direction::Down => Point::from((5, 10)), + Direction::Left => Point::from((-10, 5)), + Direction::Right => Point::from((10, 5)), + }; + let position = self + .space + .output_geometry(&output) + .unwrap() + .overlaps({ + let mut geo = mapped.geometry(); + geo.loc += pos; + geo + }) + .then_some(pos); + + self.map_internal(mapped.clone(), position.map(PointExt::as_local), None, None); + MoveResult::ShiftFocus(KeyboardFocusTarget::Element(mapped)) + } + StackMoveResult::Default => { + let mut tiled_state = element.floating_tiled.lock().unwrap(); + + let output = self.space.outputs().next().unwrap().clone(); + let layers = layer_map_for_output(&output); + let output_geometry = layers.non_exclusive_zone(); + std::mem::drop(layers); + + let current_geometry = self + .space + .element_geometry(element) + .map(RectExt::as_local) + .unwrap(); + let start_rectangle = if let Some(anim) = self.animations.remove(element) { + anim.geometry( + output_geometry, + current_geometry, + tiled_state.as_ref(), + self.gaps(), + ) + } else { + current_geometry + }; + + let new_state = match (direction, &*tiled_state) { + // figure out if we are moving between workspaces/outputs + ( + Direction::Up, + Some(TiledCorners::Top) + | Some(TiledCorners::TopLeft) + | Some(TiledCorners::TopRight), + ) + | ( + Direction::Down, + Some(TiledCorners::Bottom) + | Some(TiledCorners::BottomLeft) + | Some(TiledCorners::BottomRight), + ) + | ( + Direction::Left, + Some(TiledCorners::Left) + | Some(TiledCorners::TopLeft) + | Some(TiledCorners::BottomLeft), + ) + | ( + Direction::Right, + Some(TiledCorners::Right) + | Some(TiledCorners::TopRight) + | Some(TiledCorners::BottomRight), + ) => { + return MoveResult::MoveFurther(KeyboardFocusTarget::Element( + element.clone(), + )); + } + + // to we go maximized? + (Direction::Up, Some(TiledCorners::Bottom)) + | (Direction::Down, Some(TiledCorners::Top)) + | (Direction::Left, Some(TiledCorners::Right)) + | (Direction::Right, Some(TiledCorners::Left)) => { + std::mem::drop(tiled_state); + + let mut maximized_state = element.maximized_state.lock().unwrap(); + *maximized_state = Some(MaximizedState { + original_geometry: start_rectangle, + original_layer: layer, + original_snapped: None, + }); + std::mem::drop(maximized_state); + + self.map_maximized(element.clone(), start_rectangle, true); + return MoveResult::Done; + } + + // figure out if we need to quater tile + (Direction::Up, Some(TiledCorners::Left)) + | (Direction::Left, Some(TiledCorners::Top)) => TiledCorners::TopLeft, + (Direction::Right, Some(TiledCorners::Top)) + | (Direction::Up, Some(TiledCorners::Right)) => TiledCorners::TopRight, + (Direction::Down, Some(TiledCorners::Left)) + | (Direction::Left, Some(TiledCorners::Bottom)) => TiledCorners::BottomLeft, + (Direction::Right, Some(TiledCorners::Bottom)) + | (Direction::Down, Some(TiledCorners::Right)) => TiledCorners::BottomRight, + // figure out if we need to extend a quater tile + (Direction::Up, Some(TiledCorners::BottomLeft)) + | (Direction::Down, Some(TiledCorners::TopLeft)) => TiledCorners::Left, + (Direction::Up, Some(TiledCorners::BottomRight)) + | (Direction::Down, Some(TiledCorners::TopRight)) => TiledCorners::Right, + (Direction::Left, Some(TiledCorners::TopRight)) + | (Direction::Right, Some(TiledCorners::TopLeft)) => TiledCorners::Top, + (Direction::Left, Some(TiledCorners::BottomRight)) + | (Direction::Right, Some(TiledCorners::BottomLeft)) => TiledCorners::Bottom, + // else we have a simple case + (Direction::Up, _) => TiledCorners::Top, + (Direction::Right, _) => TiledCorners::Right, + (Direction::Down, _) => TiledCorners::Bottom, + (Direction::Left, _) => TiledCorners::Left, + }; + + let new_geo = new_state.relative_geometry(output_geometry, self.gaps()); + let (new_pos, new_size) = (new_geo.loc, new_geo.size); + element.set_tiled(true); // TODO: More fine grained? + element.set_maximized(false); + + if tiled_state.is_none() { + let last_geometry = element + .maximized_state + .lock() + .unwrap() + .take() + .map(|state| state.original_geometry) + .or_else(|| self.space.element_geometry(element).map(RectExt::as_local)); + + *element.last_geometry.lock().unwrap() = last_geometry; + } + + *tiled_state = Some(new_state); + std::mem::drop(tiled_state); + + element.moved_since_mapped.store(true, Ordering::SeqCst); + let element = element.clone(); + self.map_internal( + element, + Some(new_pos), + Some(new_size.as_logical()), + Some(start_rectangle), + ); + + MoveResult::Done + } + } + } + + pub fn move_current_element( + &mut self, + direction: Direction, + seat: &Seat, + layer: ManagedLayer, + theme: cosmic::Theme, + ) -> MoveResult { + let Some(target) = seat.get_keyboard().unwrap().current_focus() else { + return MoveResult::None; + }; + + let Some(focused) = (match target { + KeyboardFocusTarget::Popup(popup) => { + let Some(toplevel_surface) = (match popup { + PopupKind::Xdg(_) => get_popup_toplevel(&popup), + PopupKind::InputMethod(_) => unreachable!(), + }) else { + return MoveResult::None; + }; + self.space + .elements() + .find(|elem| elem.wl_surface().as_deref() == Some(&toplevel_surface)) + } + KeyboardFocusTarget::Element(elem) => self.space.elements().find(|x| *x == &elem), + _ => None, + }) else { + return MoveResult::None; + }; + + self.move_element(direction, seat, layer, &theme, &focused.clone()) + } + + pub fn mapped(&self) -> impl Iterator { + self.space.elements().rev() + } + + pub fn windows(&self) -> impl Iterator + '_ { + self.mapped().flat_map(|e| e.windows().map(|(w, _)| w)) + } + + pub fn recalculate(&mut self) { + let output = self.space.outputs().next().unwrap().clone(); + let output_size = output.geometry().size.as_local(); + let old_output_size = Some(self.last_output_size).filter(|size| *size != output_size); + + let geometry = layer_map_for_output(&output) + .non_exclusive_zone() + .as_local(); + + // update elements + for mapped in self + .space + .elements() + .cloned() + .collect::>() + .into_iter() + { + mapped.set_bounds(geometry.size.as_logical()); + let prev = self.space.element_geometry(&mapped).map(RectExt::as_local); + + let window_geometry = if mapped.is_maximized(true) { + geometry + } else { + prev.map(|mut rect| { + if let Some(old_size) = old_output_size { + rect = Rectangle::new( + Point::new( + (rect.loc.x as f64 + rect.size.w as f64 / 2.) / old_size.w as f64 + * output_size.w as f64 + - rect.size.w as f64 / 2., + (rect.loc.y as f64 + rect.size.h as f64 / 2.) / old_size.h as f64 + * output_size.h as f64 + - rect.size.h as f64 / 2., + ), + rect.size.to_f64(), + ) + .to_i32_round(); + } + Rectangle::new(rect.loc.constrain(geometry), rect.size) + }) + .unwrap_or_else(|| { + Rectangle::new(Point::from((0, 0)), mapped.geometry().size.as_local()) + }) + }; + mapped.set_geometry(window_geometry.to_global(&output)); + + let is_activated = mapped.is_activated(false); + mapped.configure(); + self.space + .map_element(mapped, window_geometry.loc.as_logical(), is_activated); + } + + self.last_output_size = output_size; + self.refresh(); + } + + #[profiling::function] + pub fn refresh(&mut self) { + self.space.refresh(); + + if let Some(pos) = self.spawn_order.iter().position(|w| !w.alive()) { + self.spawn_order.truncate(pos); + } + + for element in self + .space + .elements() + .filter(|e| self.space.outputs_for_element(e).is_empty()) + .cloned() + .collect::>() + .into_iter() + { + // TODO what about windows leaving to the top with no headerbar to drag? can that happen? (Probably if the user is moving outputs down) + *element.last_geometry.lock().unwrap() = None; + self.map_internal(element, None, None, None); + } + } + + pub fn animations_going(&self) -> bool { + self.dirty.swap(false, Ordering::SeqCst) || !self.animations.is_empty() + } + + pub fn update_animation_state(&mut self) { + let was_empty = self.animations.is_empty(); + self.animations.retain(|_, anim| { + let duration = match anim { + Animation::Tiled { .. } => ANIMATION_DURATION, + _ => MINIMIZE_ANIMATION_DURATION, + }; + Instant::now().duration_since(*anim.start()) < duration + }); + if self.animations.is_empty() != was_empty { + self.dirty.store(true, Ordering::SeqCst); + } + } + + pub fn merge(&mut self, other: FloatingLayout) { + for element in other.space.elements() { + let elem_loc = other + .space + .element_geometry(element) + .unwrap() + .loc + .as_local(); + self.map_internal(element.clone(), Some(elem_loc), None, None); + } + self.refresh(); //fixup any out of bounds elements + } + #[profiling::function] + pub fn render_popups( + &self, + renderer: &mut R, + alpha: f32, + scanout_node: Option, + push: &mut dyn FnMut(CosmicMappedRenderElement), + ) where + R: AsGlowRenderer, + R::TextureId: Send + Clone + 'static, + CosmicMappedRenderElement: RenderElement, + CosmicWindowRenderElement: RenderElement, + CosmicStackRenderElement: RenderElement, + { + let output = self.space.outputs().next().unwrap(); + let output_scale = output.current_scale().fractional_scale(); + + for elem in self + .animations + .iter() + .filter(|(_, anim)| matches!(anim, Animation::Minimize { .. })) + .map(|(elem, _)| elem) + .chain(self.space.elements().rev()) + { + let (geometry, alpha) = self + .animations + .get(elem) + .map(|anim| (*anim.previous_geometry(), alpha * anim.alpha())) + .unwrap_or_else(|| (self.space.element_geometry(elem).unwrap().as_local(), alpha)); + + let render_location = geometry.loc - elem.geometry().loc.as_local(); + elem.push_popup_render_elements( + renderer, + render_location + .as_logical() + .to_physical_precise_round(output_scale), + output_scale.into(), + alpha, + scanout_node, + push, + ); + } + } + + #[profiling::function] + pub fn render( + &self, + renderer: &mut R, + focused: Option<&CosmicMapped>, + mut resize_indicator: Option<(ResizeMode, ResizeIndicator)>, + indicator_thickness: u8, + alpha: f32, + theme: &cosmic::theme::CosmicTheme, + scanout_node: Option, + push: &mut dyn FnMut(CosmicMappedRenderElement), + ) where + R: AsGlowRenderer, + R::TextureId: Send + Clone + 'static, + CosmicMappedRenderElement: RenderElement, + CosmicWindowRenderElement: RenderElement, + CosmicStackRenderElement: RenderElement, + { + let output = self.space.outputs().next().unwrap(); + let output_geometry = { + let layers = layer_map_for_output(output); + layers.non_exclusive_zone() + }; + let output_scale = output.current_scale().fractional_scale(); + let mut lower_elements = SmallVec::<[_; 4]>::new_const(); + + for elem in self + .animations + .iter() + .filter(|(_, anim)| matches!(anim, Animation::Minimize { .. })) + .map(|(elem, _)| elem) + .chain(self.space.elements().rev()) + { + let (mut geometry, alpha) = self + .animations + .get(elem) + .map(|anim| (*anim.previous_geometry(), alpha * anim.alpha())) + .unwrap_or_else(|| (self.space.element_geometry(elem).unwrap().as_local(), alpha)); + let render_location = geometry.loc - elem.geometry().loc.as_local(); + + let maybe_map = if let Some(anim) = self.animations.get(elem) { + let original_geo = anim.previous_geometry(); + geometry = anim.geometry( + output_geometry, + self.space + .element_geometry(elem) + .map(RectExt::as_local) + .unwrap_or(geometry), + elem.floating_tiled.lock().unwrap().as_ref(), + self.gaps(), + ); + + let buffer_size = elem.geometry().size; + let scale = Scale { + x: geometry.size.w as f64 / buffer_size.w as f64, + y: geometry.size.h as f64 / buffer_size.h as f64, + }; + + Some(move |element| match element { + CosmicMappedRenderElement::Stack(elem) => { + CosmicMappedRenderElement::MovingStack({ + let rescaled = RescaleRenderElement::from_element( + elem, + original_geo + .loc + .as_logical() + .to_physical_precise_round(output_scale), + scale, + ); + + RelocateRenderElement::from_element( + rescaled, + (geometry.loc - original_geo.loc) + .as_logical() + .to_physical_precise_round(output_scale), + Relocate::Relative, + ) + }) + } + CosmicMappedRenderElement::Window(elem) => { + CosmicMappedRenderElement::MovingWindow({ + let rescaled = RescaleRenderElement::from_element( + elem, + original_geo + .loc + .as_logical() + .to_physical_precise_round(output_scale), + scale, + ); + + RelocateRenderElement::from_element( + rescaled, + (geometry.loc - original_geo.loc) + .as_logical() + .to_physical_precise_round(output_scale), + Relocate::Relative, + ) + }) + } + x => x, + }) + } else { + None + }; + + if focused == Some(elem) && !elem.is_maximized(false) { + let active_window_hint = crate::theme::active_window_hint(theme); + let radius = elem.corner_radius(geometry.size.as_logical(), indicator_thickness); + + if let Some((mode, resize)) = resize_indicator.as_mut() { + let mut resize_geometry = geometry; + resize_geometry.loc -= (18, 18).into(); + resize_geometry.size += (36, 36).into(); + + resize.resize(resize_geometry.size.as_logical()); + resize.output_enter(output); + resize.push_render_elements( + renderer, + resize_geometry + .loc + .as_logical() + .to_physical_precise_round(output_scale), + output_scale.into(), + alpha * mode.alpha().unwrap_or(1.0), + &mut |elem| push(CosmicMappedRenderElement::Window(elem.into())), + None, + ); + } + + if indicator_thickness > 0 { + let element = IndicatorShader::focus_element( + renderer, + Key::Window(Usage::FocusIndicator, elem.key()), + geometry, + indicator_thickness, + radius, + alpha, + output_scale, + [ + active_window_hint.red, + active_window_hint.green, + active_window_hint.blue, + ], + ); + push(element.into()); + } + } + + let map_anim = |elem| { + if let Some(map) = maybe_map { + map(elem) + } else { + elem + } + }; + + elem.push_render_elements( + renderer, + render_location + .as_logical() + .to_physical_precise_round(output_scale), + None, + output_scale.into(), + alpha, + None, + scanout_node, + &mut |elem| push(map_anim(elem)), + &mut |elem| lower_elements.push(map_anim(elem)), + ); + if let Some(shadow_element) = elem.shadow_render_element( + renderer, + render_location + .as_logical() + .to_physical_precise_round(output_scale), + None, + output_scale.into(), + 1., + alpha, + ) { + push(map_anim(shadow_element)); + } + for elem in lower_elements.drain(..) { + push(elem); + } + } + } + + pub fn snap_to_corner(&self, mapped: &CosmicMapped, corners: &TiledCorners) { + *mapped.floating_tiled.lock().unwrap() = Some(*corners); + mapped.set_tiled(true); + let snapped_geo = self.snapped_geometry(corners); + let output = self.space.outputs().next().unwrap(); + mapped.set_geometry(snapped_geo.to_global(output)); + mapped.configure(); + } + + fn snapped_geometry(&self, corners: &TiledCorners) -> Rectangle { + let output = self.space.outputs().next().unwrap().clone(); + let layers = layer_map_for_output(&output); + let non_exclusive = layers.non_exclusive_zone(); + std::mem::drop(layers); + corners.relative_geometry(non_exclusive, self.gaps()) + } + + fn gaps(&self) -> (i32, i32) { + let g = self.theme.cosmic().gaps; + (g.0 as i32, g.1 as i32) + } +} diff --git a/src/shell/layout/mod.rs b/src/shell/layout/mod.rs new file mode 100644 index 0000000..987f2f9 --- /dev/null +++ b/src/shell/layout/mod.rs @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use cosmic_settings_config::{shortcuts::action::Orientation, window_rules::ApplicationException}; +use regex::{Regex, RegexSet}; +use smithay::{ + desktop::WindowSurface, + wayland::{compositor::with_states, shell::xdg::XdgToplevelSurfaceData}, + xwayland::xwm::WmWindowType, +}; +use tracing::warn; + +use super::CosmicSurface; + +pub mod floating; +pub mod tiling; + +pub fn is_dialog(window: &CosmicSurface) -> bool { + // Check "window type" + match window.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => { + if with_states(toplevel.wl_surface(), |states| { + let attrs = states + .data_map + .get::() + .unwrap() + .lock() + .unwrap(); + attrs.parent.is_some() + }) { + return true; + } + } + WindowSurface::X11(surface) => { + if surface.is_override_redirect() + || surface.is_modal() + || !matches!( + surface.window_type(), + None | Some(WmWindowType::Normal) | Some(WmWindowType::Utility) + ) + { + return true; + } + } + }; + + // Check if sizing suggest dialog + let max_size = window.max_size_without_ssd(); + let min_size = window.min_size_without_ssd(); + + if min_size.is_some() && min_size == max_size { + return true; + } + + false +} + +#[derive(Debug, Clone, Default)] +pub struct TilingExceptions { + app_ids: RegexSet, + titles: RegexSet, +} + +impl TilingExceptions { + pub fn new<'a, I>(exceptions_config: I) -> Self + where + I: Iterator, + { + let mut app_ids = Vec::new(); + let mut titles = Vec::new(); + + for exception in exceptions_config { + if let Err(e) = Regex::new(&exception.appid) { + warn!("Invalid regex for appid: {}, {}", exception.appid, e); + continue; + } + if let Err(e) = Regex::new(&exception.title) { + warn!("Invalid regex for title: {}, {}", exception.appid, e); + continue; + } + + app_ids.push(exception.appid.clone()); + titles.push(exception.title.clone()); + } + + Self { + app_ids: RegexSet::new(app_ids).unwrap(), + titles: RegexSet::new(titles).unwrap(), + } + } +} + +pub fn has_floating_exception(exceptions: &TilingExceptions, window: &CosmicSurface) -> bool { + // else take a look at our exceptions + let appid_matches = exceptions.app_ids.matches(&window.app_id()); + let title_matches = exceptions.titles.matches(&window.title()); + for idx in appid_matches.into_iter() { + if title_matches.matched(idx) { + return true; + } + } + + false +} diff --git a/src/shell/layout/tiling/blocker.rs b/src/shell/layout/tiling/blocker.rs new file mode 100644 index 0000000..ccf9473 --- /dev/null +++ b/src/shell/layout/tiling/blocker.rs @@ -0,0 +1,62 @@ +use crate::shell::element::CosmicSurface; +use smithay::{ + reexports::wayland_server::{Client, Resource, backend::ClientId}, + utils::{IsAlive, Serial}, + wayland::{ + compositor::{Blocker, BlockerState}, + seat::WaylandFocus, + }, +}; +use std::{ + collections::HashMap, + time::{Duration, Instant}, +}; + +#[derive(Debug, Clone)] +pub struct TilingBlocker { + pub necessary_acks: Vec<(CosmicSurface, Serial)>, + start: Instant, +} + +impl Blocker for TilingBlocker { + fn state(&self) -> BlockerState { + if self.is_ready() { + BlockerState::Released + } else { + BlockerState::Pending + } + } +} + +impl TilingBlocker { + pub fn new(configures: impl IntoIterator) -> Self { + TilingBlocker { + necessary_acks: configures.into_iter().collect(), + start: Instant::now(), + } + } + + pub fn is_ready(&self) -> bool { + Instant::now().duration_since(self.start) >= Duration::from_millis(300) + || self + .necessary_acks + .iter() + .all(|(surf, serial)| !surf.alive() || surf.serial_acked(serial)) + } + + pub fn is_processed(&self) -> bool { + Instant::now().duration_since(self.start) >= Duration::from_millis(500) + || self + .necessary_acks + .iter() + .all(|(surf, serial)| !surf.alive() || surf.serial_past(serial)) + } + + pub fn clients(&self) -> HashMap { + self.necessary_acks + .iter() + .flat_map(|(surface, _)| surface.wl_surface().and_then(|s| s.client())) + .map(|client| (client.id(), client)) + .collect::>() + } +} diff --git a/src/shell/layout/tiling/grabs/mod.rs b/src/shell/layout/tiling/grabs/mod.rs new file mode 100644 index 0000000..056c6fa --- /dev/null +++ b/src/shell/layout/tiling/grabs/mod.rs @@ -0,0 +1,5 @@ +mod resize; +mod swap; + +pub use self::resize::*; +pub use self::swap::*; diff --git a/src/shell/layout/tiling/grabs/resize.rs b/src/shell/layout/tiling/grabs/resize.rs new file mode 100644 index 0000000..9b13511 --- /dev/null +++ b/src/shell/layout/tiling/grabs/resize.rs @@ -0,0 +1,565 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{ + backend::render::cursor::CursorState, + shell::{ + focus::target::PointerFocusTarget, + grabs::{GrabStartData, ReleaseMode}, + layout::Orientation, + }, + utils::prelude::*, +}; +use id_tree::{NodeId, Tree}; +use smithay::{ + backend::input::ButtonState, + input::{ + Seat, + pointer::{ + AxisFrame, ButtonEvent, CursorIcon, Focus, GestureHoldBeginEvent, GestureHoldEndEvent, + GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, + GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, + GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, PointerInnerHandle, + PointerTarget, RelativeMotionEvent, + }, + touch::{ + DownEvent, FrameMarker, GrabStartData as TouchGrabStartData, + MotionEvent as TouchMotionEvent, OrientationEvent, ShapeEvent, TouchGrab, + TouchInnerHandle, TouchTarget, UpEvent, + }, + }, + output::WeakOutput, + utils::{IsAlive, Logical, Point}, +}; + +use super::super::{Data, TilingLayout}; + +#[derive(Debug, Clone, PartialEq)] +pub struct ResizeForkTarget { + pub node: NodeId, + pub output: WeakOutput, + pub left_up_idx: usize, + pub orientation: Orientation, +} + +impl IsAlive for ResizeForkTarget { + fn alive(&self) -> bool { + self.output.upgrade().is_some() + } +} + +impl PointerTarget for ResizeForkTarget { + fn enter(&self, seat: &Seat, _data: &mut State, _event: &MotionEvent) { + let user_data = seat.user_data(); + let cursor_state = user_data.get::().unwrap(); + cursor_state + .lock() + .unwrap() + .set_shape(match self.orientation { + Orientation::Horizontal => CursorIcon::RowResize, + Orientation::Vertical => CursorIcon::ColResize, + }); + } + + fn leave( + &self, + seat: &Seat, + _data: &mut State, + _serial: smithay::utils::Serial, + _time: u32, + ) { + let user_data = seat.user_data(); + let cursor_state = user_data.get::().unwrap(); + cursor_state.lock().unwrap().unset_shape(); + } + + fn button(&self, seat: &Seat, data: &mut State, event: &ButtonEvent) { + if event.button == 0x110 && event.state == ButtonState::Pressed { + let seat = seat.clone(); + let node = self.node.clone(); + let output = self.output.clone(); + let left_up_idx = self.left_up_idx; + let orientation = self.orientation; + let serial = event.serial; + let button = event.button; + data.common.event_loop_handle.insert_idle(move |state| { + let pointer = seat.get_pointer().unwrap(); + let location = pointer.current_location(); + pointer.set_grab( + state, + ResizeForkGrab::new( + GrabStartData::Pointer(PointerGrabStartData { + focus: None, + button, + location, + }), + location.as_global(), + node, + left_up_idx, + orientation, + output, + ReleaseMode::NoMouseButtons, + ), + serial, + Focus::Clear, + ) + }); + } + } + + fn motion(&self, _seat: &Seat, _data: &mut State, _event: &MotionEvent) {} + fn relative_motion( + &self, + _seat: &Seat, + _data: &mut State, + _event: &RelativeMotionEvent, + ) { + } + fn axis(&self, _seat: &Seat, _data: &mut State, _frame: AxisFrame) {} + fn frame(&self, _seat: &Seat, _data: &mut State) {} + fn gesture_swipe_begin(&self, _: &Seat, _: &mut State, _: &GestureSwipeBeginEvent) {} + fn gesture_swipe_update(&self, _: &Seat, _: &mut State, _: &GestureSwipeUpdateEvent) {} + fn gesture_swipe_end(&self, _: &Seat, _: &mut State, _: &GestureSwipeEndEvent) {} + fn gesture_pinch_begin(&self, _: &Seat, _: &mut State, _: &GesturePinchBeginEvent) {} + fn gesture_pinch_update(&self, _: &Seat, _: &mut State, _: &GesturePinchUpdateEvent) {} + fn gesture_pinch_end(&self, _: &Seat, _: &mut State, _: &GesturePinchEndEvent) {} + fn gesture_hold_begin(&self, _: &Seat, _: &mut State, _: &GestureHoldBeginEvent) {} + fn gesture_hold_end(&self, _: &Seat, _: &mut State, _: &GestureHoldEndEvent) {} +} + +impl TouchTarget for ResizeForkTarget { + fn down(&self, seat: &Seat, data: &mut State, event: &DownEvent) { + let seat = seat.clone(); + let node = self.node.clone(); + let output = self.output.clone(); + let left_up_idx = self.left_up_idx; + let orientation = self.orientation; + let serial = event.serial; + let slot = event.slot; + let location = event.location; + data.common.event_loop_handle.insert_idle(move |state| { + let touch = seat.get_touch().unwrap(); + touch.set_grab( + state, + ResizeForkGrab::new( + GrabStartData::Touch(TouchGrabStartData { + focus: None, + slot, + location, + }), + location.as_global(), + node, + left_up_idx, + orientation, + output, + ReleaseMode::NoMouseButtons, + ), + serial, + ) + }); + } + + fn up(&self, _seat: &Seat, _data: &mut State, _event: &UpEvent) {} + fn motion(&self, _seat: &Seat, _data: &mut State, _event: &TouchMotionEvent) {} + fn frame(&self, _seat: &Seat, _data: &mut State, _frame: FrameMarker) {} + fn cancel(&self, _seat: &Seat, _data: &mut State, _frame: FrameMarker) {} + fn shape(&self, _seat: &Seat, _data: &mut State, _event: &ShapeEvent) {} + fn orientation(&self, _seat: &Seat, _data: &mut State, _event: &OrientationEvent) {} + fn last_frame(&self, _seat: &Seat, _data: &mut State) -> Option { + None + } +} + +pub struct ResizeForkGrab { + start_data: GrabStartData, + last_loc: Point, + old_tree: Option>, + accumulated_delta: f64, + node: NodeId, + output: WeakOutput, + left_up_idx: usize, + orientation: Orientation, + release: ReleaseMode, +} + +impl ResizeForkGrab { + pub fn new( + start_data: GrabStartData, + pointer_loc: Point, + node: NodeId, + idx: usize, + orientation: Orientation, + output: WeakOutput, + release: ReleaseMode, + ) -> ResizeForkGrab { + ResizeForkGrab { + start_data, + last_loc: pointer_loc, + old_tree: None, + accumulated_delta: 0.0, + node, + output, + left_up_idx: idx, + orientation, + release, + } + } +} + +impl ResizeForkGrab { + // Returns `true` if grab should be unset + fn update_location( + &mut self, + data: &mut State, + location: Point, + force: bool, + ) -> bool { + let delta = location - self.last_loc.as_logical(); + self.last_loc = location.as_global(); + + if let Some(output) = self.output.upgrade() { + let mut shell = data.common.shell.write(); + let Some(workspace) = shell.active_space_mut(&output) else { + return false; + }; + let tiling_layer = &mut workspace.tiling_layer; + let gaps = tiling_layer.gaps(); + + let mut tree = tiling_layer.queue.trees.back().unwrap().0.copy_clone(); + match &mut self.old_tree { + Some(old_tree) => { + // it would be so nice to just `zip` here, but `zip` just returns `None` once either returns `None`. + let mut iter_a = old_tree + .root_node_id() + .into_iter() + .flat_map(|root_id| old_tree.traverse_pre_order_ids(root_id).unwrap()); + let mut iter_b = tree + .root_node_id() + .into_iter() + .flat_map(|root_id| tree.traverse_pre_order_ids(root_id).unwrap()); + + // so lets do it manually + let mut equal = true; + let mut a = iter_a.next(); + let mut b = iter_b.next(); + while a.is_some() || b.is_some() { + equal = a == b; + if !equal { + break; + } + a = iter_a.next(); + b = iter_b.next(); + } + + if !equal { + *old_tree = tree.copy_clone(); + self.accumulated_delta = 0.0; + } else { + tree = old_tree.copy_clone(); + } + } + x @ None => { + *x = Some(tree.copy_clone()); + } + }; + if tree.get(&self.node).is_ok() { + let delta = match self.orientation { + Orientation::Vertical => delta.x, + Orientation::Horizontal => delta.y, + } + .round(); + self.accumulated_delta += delta; + + // check that we are still alive + let mut iter = tree + .children_ids(&self.node) + .unwrap() + .skip(self.left_up_idx); + let first_elem = iter.next(); + let second_elem = iter.next(); + if first_elem.is_none() || second_elem.is_none() { + return true; + }; + + if self.accumulated_delta.round() as i32 == 0 { + return false; + } + + match tree.get_mut(&self.node).unwrap().data_mut() { + Data::Group { + sizes, orientation, .. + } => { + if sizes[self.left_up_idx] + sizes[self.left_up_idx + 1] + < match orientation { + Orientation::Vertical => 720, + Orientation::Horizontal => 480, + } + { + return false; + }; + + let old_size = sizes[self.left_up_idx]; + sizes[self.left_up_idx] = (old_size + + self.accumulated_delta.round() as i32) + .max(if self.orientation == Orientation::Vertical { + 360 + } else { + 240 + }); + let diff = old_size - sizes[self.left_up_idx]; + let next_size = sizes[self.left_up_idx + 1] + diff; + sizes[self.left_up_idx + 1] = + next_size.max(if self.orientation == Orientation::Vertical { + 360 + } else { + 240 + }); + let next_diff = next_size - sizes[self.left_up_idx + 1]; + sizes[self.left_up_idx] += next_diff; + } + _ => unreachable!(), + } + + let should_configure = force + || tree + .traverse_pre_order(&self.node) + .unwrap() + .all(|node| match node.data() { + Data::Mapped { mapped, .. } => mapped.latest_size_committed(), + _ => true, + }); + if should_configure { + let blocker = TilingLayout::update_positions(&output, &mut tree, gaps); + tiling_layer.queue.push_tree(tree, None, blocker); + } + } else { + return true; + } + } + false + } + + pub fn is_touch_grab(&self) -> bool { + match self.start_data { + GrabStartData::Touch(_) => true, + GrabStartData::Pointer(_) => false, + } + } +} + +impl PointerGrab for ResizeForkGrab { + fn motion( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + _focus: Option<(PointerFocusTarget, Point)>, + event: &MotionEvent, + ) { + // While the grab is active, no client has pointer focus + handle.motion(data, None, event); + + if self.update_location(data, event.location, false) { + handle.unset_grab(self, data, event.serial, event.time, true); + } + } + + fn relative_motion( + &mut self, + state: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + _focus: Option<(PointerFocusTarget, Point)>, + event: &RelativeMotionEvent, + ) { + // While the grab is active, no client has pointer focus + handle.relative_motion(state, None, event); + } + + fn button( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &ButtonEvent, + ) { + handle.button(data, event); + match self.release { + ReleaseMode::NoMouseButtons => { + if handle.current_pressed().is_empty() { + handle.unset_grab(self, data, event.serial, event.time, true); + } + } + ReleaseMode::Click => { + if event.state == ButtonState::Pressed { + handle.unset_grab(self, data, event.serial, event.time, true); + } + } + } + } + + fn axis( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + details: AxisFrame, + ) { + handle.axis(data, details) + } + + fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) { + handle.frame(data) + } + + fn gesture_swipe_begin( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureSwipeBeginEvent, + ) { + handle.gesture_swipe_begin(data, event) + } + + fn gesture_swipe_update( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureSwipeUpdateEvent, + ) { + handle.gesture_swipe_update(data, event) + } + + fn gesture_swipe_end( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureSwipeEndEvent, + ) { + handle.gesture_swipe_end(data, event) + } + + fn gesture_pinch_begin( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GesturePinchBeginEvent, + ) { + handle.gesture_pinch_begin(data, event) + } + + fn gesture_pinch_update( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GesturePinchUpdateEvent, + ) { + handle.gesture_pinch_update(data, event) + } + + fn gesture_pinch_end( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GesturePinchEndEvent, + ) { + handle.gesture_pinch_end(data, event) + } + + fn gesture_hold_begin( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureHoldBeginEvent, + ) { + handle.gesture_hold_begin(data, event) + } + + fn gesture_hold_end( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureHoldEndEvent, + ) { + handle.gesture_hold_end(data, event) + } + + fn start_data(&self) -> &PointerGrabStartData { + match &self.start_data { + GrabStartData::Pointer(start_data) => start_data, + _ => unreachable!(), + } + } + + fn unset(&mut self, data: &mut State) { + self.update_location(data, self.last_loc.as_logical(), true); + } +} + +impl TouchGrab for ResizeForkGrab { + fn down( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + _focus: Option<(PointerFocusTarget, Point)>, + event: &DownEvent, + ) { + handle.down(data, None, event) + } + + fn up(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, event: &UpEvent) { + if event.slot == >::start_data(self).slot { + handle.unset_grab(self, data); + } + + handle.up(data, event); + } + + fn motion( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + _focus: Option<(PointerFocusTarget, Point)>, + event: &TouchMotionEvent, + ) { + if event.slot == >::start_data(self).slot + && self.update_location(data, event.location, false) + { + handle.unset_grab(self, data); + } + + handle.motion(data, None, event); + } + + fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>) { + handle.frame(data) + } + + fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>) { + handle.unset_grab(self, data); + } + + fn shape( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + event: &ShapeEvent, + ) { + handle.shape(data, event) + } + + fn start_data(&self) -> &TouchGrabStartData { + match &self.start_data { + GrabStartData::Touch(start_data) => start_data, + _ => unreachable!(), + } + } + + fn orientation( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + event: &OrientationEvent, + ) { + handle.orientation(data, event) + } + + fn unset(&mut self, data: &mut State) { + self.update_location(data, self.last_loc.as_logical(), true); + } +} diff --git a/src/shell/layout/tiling/grabs/swap.rs b/src/shell/layout/tiling/grabs/swap.rs new file mode 100644 index 0000000..4c22209 --- /dev/null +++ b/src/shell/layout/tiling/grabs/swap.rs @@ -0,0 +1,107 @@ +use cosmic_settings_config::shortcuts; +use smithay::{ + backend::input::{KeyState, Keycode}, + input::{ + Seat, SeatHandler, + keyboard::{ + GrabStartData as KeyboardGrabStartData, KeyboardGrab, KeyboardInnerHandle, + ModifiersState, + }, + }, + utils::Serial, +}; + +use crate::{ + config::key_bindings::cosmic_modifiers_from_smithay, + shell::{Trigger, layout::tiling::NodeDesc}, + state::State, +}; + +pub struct SwapWindowGrab { + seat: Seat, + desc: NodeDesc, +} + +impl SwapWindowGrab { + pub fn new(seat: Seat, desc: NodeDesc) -> Self { + SwapWindowGrab { seat, desc } + } +} + +impl KeyboardGrab for SwapWindowGrab { + fn input( + &mut self, + data: &mut State, + handle: &mut KeyboardInnerHandle<'_, State>, + keycode: Keycode, + state: KeyState, + modifiers: Option, + serial: Serial, + time: u32, + ) { + if !matches!(&data.common.shell.read().overview_mode.active_trigger(), Some(Trigger::KeyboardSwap(_, d)) if d == &self.desc) + { + handle.unset_grab(self, data, serial, false); + return; + } + + if state == KeyState::Released { + return; + } + + let syms = handle.keysym_handle(keycode).raw_syms(); + let focus_bindings = &data + .common + .config + .shortcuts + .iter() + .filter(|(_, action)| matches!(action, shortcuts::Action::Focus(_))) + .map(|(pattern, action)| { + let shortcuts::Action::Focus(direction) = action else { + unreachable!() + }; + (pattern.key, *direction) + }) + .collect::>(); + let Some(direction) = syms.iter().find_map(|sym| { + focus_bindings.iter().find_map(|(key, direction)| { + (key.is_some() && sym == key.as_ref().unwrap()).then_some(*direction) + }) + }) else { + return; + }; + + data.handle_shortcut_action( + shortcuts::Action::Focus(direction), + &self.seat, + serial, + time, + shortcuts::Binding { + modifiers: modifiers + .map(cosmic_modifiers_from_smithay) + .unwrap_or_default(), + keycode: None, + key: Some(handle.keysym_handle(keycode).modified_sym()), + description: None, + }, + None, + true, + ); + } + + fn set_focus( + &mut self, + data: &mut State, + handle: &mut KeyboardInnerHandle<'_, State>, + focus: Option<::KeyboardFocus>, + serial: Serial, + ) { + handle.set_focus(data, focus, serial) + } + + fn start_data(&self) -> &KeyboardGrabStartData { + &KeyboardGrabStartData { focus: None } + } + + fn unset(&mut self, _state: &mut State) {} +} diff --git a/src/shell/layout/tiling/mod.rs b/src/shell/layout/tiling/mod.rs new file mode 100644 index 0000000..2d95cd2 --- /dev/null +++ b/src/shell/layout/tiling/mod.rs @@ -0,0 +1,5939 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{ + backend::render::{ + ACTIVE_GROUP_COLOR, BackdropShader, GROUP_COLOR, IndicatorShader, Key, Usage, + element::AsGlowRenderer, + }, + shell::{ + CosmicSurface, Direction, FocusResult, MoveResult, OverviewMode, ResizeMode, Trigger, + element::{ + CosmicMapped, CosmicMappedRenderElement, CosmicStack, CosmicWindow, + resize_indicator::ResizeIndicator, + stack::{ + CosmicStackRenderElement, MoveResult as StackMoveResult, + TAB_HEIGHT as STACK_TAB_HEIGHT, + }, + swap_indicator::SwapIndicator, + window::CosmicWindowRenderElement, + }, + focus::{ + FocusStackMut, FocusTarget, + target::{KeyboardFocusTarget, PointerFocusTarget, WindowGroup}, + }, + grabs::ResizeEdge, + layout::Orientation, + }, + utils::{prelude::*, tween::EaseRectangle}, + wayland::{ + handlers::xdg_shell::popup::get_popup_toplevel, + protocols::{ + toplevel_info::{ + toplevel_enter_output, toplevel_enter_workspace, toplevel_leave_output, + toplevel_leave_workspace, + }, + workspace::WorkspaceHandle, + }, + }, +}; + +use cosmic_comp_config::AppearanceConfig; +use cosmic_settings_config::shortcuts::action::{FocusDirection, ResizeDirection}; +use id_tree::{InsertBehavior, MoveBehavior, Node, NodeId, NodeIdError, RemoveBehavior, Tree}; +use keyframe::{ + ease, + functions::{EaseInOutCubic, Linear}, +}; +use smallvec::SmallVec; +use smithay::{ + backend::{ + drm::DrmNode, + renderer::{ + element::{ + Id, RenderElement, + utils::{ + ConstrainAlign, ConstrainScaleBehavior, RescaleRenderElement, + constrain_render_elements, + }, + }, + glow::GlowRenderer, + }, + }, + desktop::{PopupKind, WindowSurfaceType, layer_map_for_output, space::SpaceElement}, + input::Seat, + output::Output, + reexports::wayland_server::Client, + utils::{IsAlive, Logical, Physical, Point, Rectangle, Scale, Size}, + wayland::{compositor::add_blocker, seat::WaylandFocus}, +}; +use std::{ + collections::{HashMap, VecDeque}, + sync::{Arc, Weak}, + time::{Duration, Instant}, +}; +use tracing::trace; +use wayland_backend::server::ClientId; + +mod blocker; +mod grabs; +pub use self::blocker::*; +pub use self::grabs::*; + +pub const ANIMATION_DURATION: Duration = Duration::from_millis(200); +pub const MINIMIZE_ANIMATION_DURATION: Duration = Duration::from_millis(320); +pub const MOUSE_ANIMATION_DELAY: Duration = Duration::from_millis(150); +pub const INITIAL_MOUSE_ANIMATION_DELAY: Duration = Duration::from_millis(500); + +#[derive(Debug, Clone, PartialEq)] +pub struct NodeDesc { + pub handle: WorkspaceHandle, + pub node: NodeId, + pub stack_window: Option, + pub focus_stack: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +enum TargetZone { + Initial, + InitialPlaceholder(NodeId), + WindowStack(NodeId, Rectangle), + WindowSplit(NodeId, Direction), + GroupEdge(NodeId, Direction), + GroupInterior(NodeId, usize), +} + +impl TargetZone { + fn is_window_zone(&self) -> bool { + matches!( + self, + TargetZone::WindowStack(..) | TargetZone::WindowSplit(_, _) + ) + } +} + +#[derive(Debug, Clone, Default)] +struct TreeQueue { + trees: VecDeque<(Tree, Duration, Option)>, + animation_start: Option, +} + +impl TreeQueue { + pub fn push_tree( + &mut self, + tree: Tree, + duration: impl Into>, + blocker: Option, + ) { + self.trees + .push_back((tree, duration.into().unwrap_or(Duration::ZERO), blocker)) + } +} + +#[derive(Debug, Clone)] +pub struct TilingLayout { + output: Output, + queue: TreeQueue, + backdrop_id: Id, + swapping_stack_surface_id: Id, + last_overview_hover: Option<(Option, TargetZone)>, + pub theme: cosmic::Theme, + pub appearance: AppearanceConfig, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum PillIndicator { + Outer(Direction), + Inner(usize), +} + +#[derive(Debug, Clone)] +pub enum Data { + Group { + orientation: Orientation, + sizes: Vec, + last_geometry: Rectangle, + alive: Arc<()>, + pill_indicator: Option, + }, + Mapped { + mapped: CosmicMapped, + last_geometry: Rectangle, + minimize_rect: Option>, + }, + Placeholder { + id: Id, + last_geometry: Rectangle, + type_: PlaceholderType, + }, +} + +#[derive(Debug, Clone)] +pub enum PlaceholderType { + GrabbedWindow, + DropZone, +} + +impl Data { + fn new_group(orientation: Orientation, geo: Rectangle) -> Data { + Data::Group { + orientation, + sizes: vec![ + match orientation { + Orientation::Vertical => geo.size.w / 2, + Orientation::Horizontal => geo.size.h / 2, + }; + 2 + ], + last_geometry: geo, + alive: Arc::new(()), + pill_indicator: None, + } + } + + fn is_group(&self) -> bool { + matches!(self, Data::Group { .. }) + } + fn is_mapped(&self, mapped: Option<&CosmicMapped>) -> bool { + match mapped { + Some(m) => matches!(self, Data::Mapped { mapped, .. } if m == mapped), + None => matches!(self, Data::Mapped { .. }), + } + } + fn is_stack(&self) -> bool { + match self { + Data::Mapped { mapped, .. } => mapped.is_stack(), + _ => false, + } + } + fn is_placeholder(&self) -> bool { + matches!(self, Data::Placeholder { .. }) + } + + fn orientation(&self) -> Orientation { + match self { + Data::Group { orientation, .. } => *orientation, + _ => panic!("Not a group"), + } + } + + fn add_window(&mut self, idx: usize) { + match self { + Data::Group { + sizes, + last_geometry, + orientation, + .. + } => { + let last_length = match orientation { + Orientation::Horizontal => last_geometry.size.h, + Orientation::Vertical => last_geometry.size.w, + }; + let equal_sizing = last_length / (sizes.len() as i32 + 1); // new window size + let remainder = last_length - equal_sizing; // size for the rest of the windowns + + for size in sizes.iter_mut() { + *size = ((*size as f64 / last_length as f64) * remainder as f64).round() as i32; + } + let used_size: i32 = sizes.iter().sum(); + let new_size = last_length - used_size; + + sizes.insert(idx, new_size); + } + _ => panic!("Adding window to leaf?"), + } + } + + fn swap_windows(&mut self, i: usize, j: usize) { + match self { + Data::Group { sizes, .. } => { + sizes.swap(i, j); + } + _ => panic!("Swapping windows to a leaf?"), + } + } + + fn remove_window(&mut self, idx: usize) { + match self { + Data::Group { + sizes, + last_geometry, + orientation, + .. + } => { + let last_length = match orientation { + Orientation::Horizontal => last_geometry.size.h, + Orientation::Vertical => last_geometry.size.w, + }; + let old_size = sizes.remove(idx); + let remaining_size: i32 = sizes.iter().sum(); + + for size in sizes.iter_mut() { + *size += + ((*size as f64 / remaining_size as f64) * old_size as f64).round() as i32; + } + let used_size: i32 = sizes.iter().sum(); + let overflow = last_length - used_size; + if overflow != 0 { + *sizes.last_mut().unwrap() += overflow; + } + } + _ => panic!("Added window to leaf?"), + } + } + + fn geometry(&self) -> &Rectangle { + match self { + Data::Group { last_geometry, .. } => last_geometry, + Data::Mapped { last_geometry, .. } => last_geometry, + Data::Placeholder { last_geometry, .. } => last_geometry, + } + } + + fn update_geometry(&mut self, geo: Rectangle) { + match self { + Data::Group { + orientation, + sizes, + last_geometry, + .. + } => { + let previous_length = match orientation { + Orientation::Horizontal => last_geometry.size.h, + Orientation::Vertical => last_geometry.size.w, + }; + let new_length = match orientation { + Orientation::Horizontal => geo.size.h, + Orientation::Vertical => geo.size.w, + }; + + sizes.iter_mut().for_each(|len| { + *len = (((*len as f64) / (previous_length as f64)) * (new_length as f64)) + .round() as i32; + }); + let sum: i32 = sizes.iter().sum(); + + // fix rounding issues + if sum != new_length { + let diff = new_length - sum; + *sizes.last_mut().unwrap() += diff; + } + *last_geometry = geo; + } + Data::Mapped { last_geometry, .. } | Data::Placeholder { last_geometry, .. } => { + *last_geometry = geo; + } + } + } + + fn len(&self) -> usize { + match self { + Data::Group { sizes, .. } => sizes.len(), + _ => 1, + } + } +} + +#[derive(Debug, Clone)] +enum FocusedNodeData { + Group(Vec, Weak<()>), + Window(CosmicMapped), +} + +#[derive(Debug, Clone)] +pub struct RestoreTilingState { + pub parent: Option, + pub sibling: Option, + pub orientation: Orientation, + pub idx: usize, + pub sizes: Vec, +} + +impl TilingLayout { + pub fn new( + theme: cosmic::Theme, + appearance: AppearanceConfig, + output: &Output, + ) -> TilingLayout { + TilingLayout { + queue: TreeQueue { + trees: { + let mut queue = VecDeque::new(); + queue.push_back((Tree::new(), Duration::ZERO, None)); + queue + }, + animation_start: None, + }, + output: output.clone(), + backdrop_id: Id::new(), + swapping_stack_surface_id: Id::new(), + last_overview_hover: None, + theme, + appearance, + } + } + + pub fn set_output(&mut self, output: &Output) { + let gaps = self.gaps(); + let mut tree = self.queue.trees.back().unwrap().0.copy_clone(); + + for node in tree + .root_node_id() + .and_then(|root_id| tree.traverse_pre_order(root_id).ok()) + .into_iter() + .flatten() + { + if let Data::Mapped { mapped, .. } = node.data() { + mapped.output_leave(&self.output); + mapped.output_enter(output, mapped.bbox()); + } + } + + let blocker = TilingLayout::update_positions(output, &mut tree, gaps); + self.queue.push_tree(tree, None, blocker); + self.output = output.clone(); + } + + pub fn map<'a>( + &mut self, + window: CosmicMapped, + focus_stack: Option + 'a>, + direction: Option, + ) { + window.output_enter(&self.output, window.bbox()); + window.set_bounds(self.output.geometry().size.as_logical()); + self.map_internal(window, focus_stack, direction, None); + } + + pub fn map_internal<'a>( + &mut self, + window: impl Into, + focus_stack: Option + 'a>, + direction: Option, + minimize_rect: Option>, + ) { + let gaps = self.gaps(); + + let mut tree = self.queue.trees.back().unwrap().0.copy_clone(); + let last_active = focus_stack + .and_then(|focus_stack| TilingLayout::last_active_window(&tree, focus_stack)) + .map(|(node_id, _)| node_id); + let duration = if minimize_rect.is_some() { + MINIMIZE_ANIMATION_DURATION + } else { + ANIMATION_DURATION + }; + + TilingLayout::map_to_tree( + &mut tree, + window, + &self.output, + last_active, + direction, + minimize_rect, + ); + let blocker = TilingLayout::update_positions(&self.output, &mut tree, gaps); + self.queue.push_tree(tree, duration, blocker); + } + + pub fn remap<'a>( + &mut self, + window: CosmicMapped, + from: Option>, + tiling_state: Option, + focus_stack: Option + 'a>, + ) { + let gaps = self.gaps(); + let mut tree = self.queue.trees.back().unwrap().0.copy_clone(); + window.output_enter(&self.output, window.bbox()); + window.set_bounds(self.output.geometry().size.as_logical()); + + if let Some(RestoreTilingState { + parent, + sibling, + orientation, + idx, + mut sizes, + }) = tiling_state + { + if let Some(node) = parent.as_ref().and_then(|parent| tree.get_mut(parent).ok()) + && let Data::Group { + orientation: current_orientation, + sizes: current_sizes, + .. + } = node.data_mut() + { + let parent_id = parent.unwrap(); + + if *current_orientation == orientation && sizes.len() == current_sizes.len() + 1 { + let previous_length: i32 = sizes.iter().copied().sum(); + let new_length: i32 = current_sizes.iter().copied().sum(); + if previous_length != new_length { + // rescale sizes + sizes.iter_mut().for_each(|len| { + *len = (((*len as f64) / (previous_length as f64)) + * (new_length as f64)) + .round() as i32; + }); + let sum: i32 = sizes.iter().sum(); + + // fix rounding issues + if sum != new_length { + let diff = new_length - sum; + *sizes.last_mut().unwrap() += diff; + } + } + } + + *current_sizes = sizes; + let new_node = Node::new(Data::Mapped { + mapped: window.clone(), + last_geometry: Rectangle::from_size((100, 100).into()), + minimize_rect: from, + }); + let new_id = tree + .insert(new_node, InsertBehavior::UnderNode(&parent_id)) + .unwrap(); + tree.make_nth_sibling(&new_id, idx).unwrap(); + *window.tiling_node_id.lock().unwrap() = Some(new_id); + + let blocker = TilingLayout::update_positions(&self.output, &mut tree, gaps); + self.queue + .push_tree(tree, MINIMIZE_ANIMATION_DURATION, blocker); + return; + } + + if sibling + .as_ref() + .is_some_and(|sibling| tree.get(sibling).is_ok()) + { + let sibling_id = sibling.unwrap(); + let new_node = Node::new(Data::Mapped { + mapped: window.clone(), + last_geometry: Rectangle::from_size((100, 100).into()), + minimize_rect: from, + }); + + let new_id = tree.insert(new_node, InsertBehavior::AsRoot).unwrap(); + let group_id = + TilingLayout::new_group(&mut tree, &sibling_id, &new_id, orientation).unwrap(); + tree.make_nth_sibling(&new_id, idx).unwrap(); + if let Data::Group { + sizes: default_sizes, + last_geometry, + .. + } = tree.get_mut(&group_id).unwrap().data_mut() + { + match orientation { + Orientation::Horizontal => { + last_geometry.size.h = sizes.iter().copied().sum() + } + Orientation::Vertical => last_geometry.size.w = sizes.iter().copied().sum(), + }; + *default_sizes = sizes; + } + + *window.tiling_node_id.lock().unwrap() = Some(new_id); + + let blocker = TilingLayout::update_positions(&self.output, &mut tree, gaps); + self.queue + .push_tree(tree, MINIMIZE_ANIMATION_DURATION, blocker); + return; + } + } + + // else add as new_window + self.map_internal(window, focus_stack, None, from); + } + + fn map_to_tree( + tree: &mut Tree, + window: impl Into, + output: &Output, + node: Option, + direction: Option, + minimize_rect: Option>, + ) { + let window = window.into(); + let new_window = Node::new(Data::Mapped { + mapped: window.clone(), + last_geometry: Rectangle::from_size((100, 100).into()), + minimize_rect, + }); + + let window_id = if let Some(direction) = direction { + if let Some(root_id) = tree.root_node_id().cloned() { + let orientation = match direction { + Direction::Left | Direction::Right => Orientation::Vertical, + Direction::Up | Direction::Down => Orientation::Horizontal, + }; + + let new_id = tree.insert(new_window, InsertBehavior::AsRoot).unwrap(); + TilingLayout::new_group(tree, &root_id, &new_id, orientation).unwrap(); + tree.make_nth_sibling( + &new_id, + match direction { + Direction::Left | Direction::Up => 1, + Direction::Right | Direction::Down => 0, + }, + ) + .unwrap(); + new_id + } else { + tree.insert(new_window, InsertBehavior::AsRoot).unwrap() + } + } else if let Some(ref node_id) = node { + let orientation = { + let window_size = tree.get(node_id).unwrap().data().geometry().size; + if window_size.w > window_size.h { + Orientation::Vertical + } else { + Orientation::Horizontal + } + }; + let new_id = tree.insert(new_window, InsertBehavior::AsRoot).unwrap(); + TilingLayout::new_group(tree, node_id, &new_id, orientation).unwrap(); + new_id + } else { + // nothing? then we add to the root + if let Some(root_id) = tree.root_node_id().cloned() { + let orientation = { + let output_size = output.geometry().size; + if output_size.w > output_size.h { + Orientation::Vertical + } else { + Orientation::Horizontal + } + }; + let new_id = tree.insert(new_window, InsertBehavior::AsRoot).unwrap(); + TilingLayout::new_group(tree, &root_id, &new_id, orientation).unwrap(); + new_id + } else { + tree.insert(new_window, InsertBehavior::AsRoot).unwrap() + } + }; + + *window.tiling_node_id.lock().unwrap() = Some(window_id); + } + + pub fn replace_window(&mut self, old: &CosmicMapped, new: &CosmicMapped) { + let gaps = self.gaps(); + let Some(old_id) = old.tiling_node_id.lock().unwrap().clone() else { + return; + }; + let mut tree = self.queue.trees.back().unwrap().0.copy_clone(); + + if let Ok(node) = tree.get_mut(&old_id) { + let data = node.data_mut(); + match data { + Data::Mapped { mapped, .. } => { + assert_eq!(mapped, old); + *mapped = new.clone(); + *new.tiling_node_id.lock().unwrap() = Some(old_id); + *old.tiling_node_id.lock().unwrap() = None; + } + _ => unreachable!("Tiling id points to group"), + } + + old.output_leave(&self.output); + new.output_enter(&self.output, new.bbox()); + + let blocker = TilingLayout::update_positions(&self.output, &mut tree, gaps); + self.queue.push_tree(tree, ANIMATION_DURATION, blocker); + } + } + + pub fn move_tree<'a>( + this: &mut Self, + other: &mut Self, + other_handle: &WorkspaceHandle, + seat: &Seat, + focus_stack: impl Iterator + 'a, + desc: NodeDesc, + direction: Option, + ) -> Option { + let this_handle = &desc.handle; + let mut this_tree = this.queue.trees.back().unwrap().0.copy_clone(); + let mut other_tree = other.queue.trees.back().unwrap().0.copy_clone(); + + match desc.stack_window { + Some(stack_surface) => { + let node = this_tree.get(&desc.node).ok()?; + let Data::Mapped { + mapped: this_mapped, + .. + } = node.data() + else { + return None; + }; + let this_stack = this_mapped.stack_ref()?; + this_stack.remove_window(&stack_surface); + if !this_stack.alive() { + let _ = this.unmap(this_mapped, None); + } + + let mapped: CosmicMapped = CosmicWindow::new( + stack_surface, + this_stack.loop_handle(), + this.theme.clone(), + this.appearance, + ) + .into(); + if this.output != other.output { + mapped.output_leave(&this.output); + mapped.output_enter(&other.output, mapped.bbox()); + for (ref surface, _) in mapped.windows() { + toplevel_leave_output(surface, &this.output); + toplevel_enter_output(surface, &other.output); + } + } + for (ref surface, _) in mapped.windows() { + toplevel_leave_workspace(surface, this_handle); + toplevel_enter_workspace(surface, other_handle); + } + + mapped.set_tiled(true); + other.map(mapped.clone(), Some(focus_stack), direction); + Some(KeyboardFocusTarget::Element(mapped)) + } + None => { + let node = this_tree.get(&desc.node).ok()?; + let mut children = node + .children() + .iter() + .map(|child_id| (desc.node.clone(), child_id.clone())) + .collect::>(); + let node = Node::new(node.data().clone()); + + let id = match other_tree.root_node_id() { + None => other_tree.insert(node, InsertBehavior::AsRoot).unwrap(), + Some(_) => { + let focused_node = seat + .get_keyboard() + .unwrap() + .current_focus() + .and_then(|target| { + TilingLayout::currently_focused_node(&other_tree, target) + }) + .map(|(id, _)| id) + .unwrap_or(other_tree.root_node_id().unwrap().clone()); + let orientation = if let Some(direction) = direction { + match direction { + Direction::Left | Direction::Right => Orientation::Vertical, + Direction::Up | Direction::Down => Orientation::Horizontal, + } + } else { + let window_size = other_tree + .get(&focused_node) + .unwrap() + .data() + .geometry() + .size; + if window_size.w > window_size.h { + Orientation::Vertical + } else { + Orientation::Horizontal + } + }; + let id = other_tree.insert(node, InsertBehavior::AsRoot).unwrap(); + TilingLayout::new_group(&mut other_tree, &focused_node, &id, orientation) + .unwrap(); + other_tree + .make_nth_sibling( + &id, + match direction { + Some(Direction::Right) | Some(Direction::Down) => 0, + _ => 1, + }, + ) + .unwrap(); + id + } + }; + + for (parent_id, _) in children.iter_mut() { + *parent_id = id.clone(); + } + if let Data::Mapped { mapped, .. } = other_tree.get_mut(&id).unwrap().data_mut() { + if this.output != other.output { + mapped.output_leave(&this.output); + mapped.output_enter(&other.output, mapped.bbox()); + for (ref surface, _) in mapped.windows() { + toplevel_leave_output(surface, &this.output); + toplevel_enter_output(surface, &other.output); + } + } + for (ref surface, _) in mapped.windows() { + toplevel_leave_workspace(surface, this_handle); + toplevel_enter_workspace(surface, other_handle); + } + + *mapped.tiling_node_id.lock().unwrap() = Some(id.clone()); + } + + while !children.is_empty() { + for (parent_id, child_id) in std::mem::take(&mut children) { + let new_children = this_tree + .children_ids(&child_id) + .unwrap() + .cloned() + .collect::>(); + let child_node = this_tree + .remove_node(child_id, RemoveBehavior::OrphanChildren) + .unwrap(); + let maybe_mapped = match child_node.data() { + Data::Mapped { mapped, .. } => Some(mapped.clone()), + _ => None, + }; + let new_id = other_tree + .insert(child_node, InsertBehavior::UnderNode(&parent_id)) + .unwrap(); + if let Some(mapped) = maybe_mapped { + if this.output != other.output { + mapped.output_leave(&this.output); + mapped.output_enter(&other.output, mapped.bbox()); + for (ref surface, _) in mapped.windows() { + toplevel_leave_output(surface, &this.output); + toplevel_enter_output(surface, &other.output); + } + } + for (ref surface, _) in mapped.windows() { + toplevel_leave_workspace(surface, this_handle); + toplevel_enter_workspace(surface, other_handle); + } + + *mapped.tiling_node_id.lock().unwrap() = Some(new_id.clone()); + } + children.extend( + new_children + .into_iter() + .map(|new_child| (new_id.clone(), new_child)), + ); + } + } + let this_gaps = this.gaps(); + let other_gaps = other.gaps(); + + TilingLayout::unmap_internal(&mut this_tree, &desc.node); + let blocker = + TilingLayout::update_positions(&this.output, &mut this_tree, this_gaps); + this.queue.push_tree(this_tree, ANIMATION_DURATION, blocker); + + let blocker = + TilingLayout::update_positions(&other.output, &mut other_tree, other_gaps); + other + .queue + .push_tree(other_tree, ANIMATION_DURATION, blocker); + + other.node_desc_to_focus(&NodeDesc { + handle: *other_handle, + node: id.clone(), + stack_window: None, + focus_stack: Vec::new(), // node_desc_to_focus doesn't use this + }) + } + } + } + + pub fn swap_trees( + this: &mut Self, + mut other: Option<&mut Self>, + this_desc: &NodeDesc, + other_desc: &NodeDesc, + ) -> Option { + let other_output = other + .as_ref() + .map(|other| other.output.clone()) + .unwrap_or(this.output.clone()); + if this.output == other_output + && this_desc.handle == other_desc.handle + && this_desc.node == other_desc.node + && (this_desc.stack_window == other_desc.stack_window + || this_desc.stack_window.is_some() != other_desc.stack_window.is_some()) + { + return None; + } + + let mut this_tree = this.queue.trees.back().unwrap().0.copy_clone(); + let mut other_tree = match other.as_mut() { + Some(other) => Some(other.queue.trees.back().unwrap().0.copy_clone()), + None => { + if this.output != other_output { + Some(this.queue.trees.back().unwrap().0.copy_clone()) + } else { + None + } + } + }; + + match (&this_desc.stack_window, &other_desc.stack_window) { + (None, None) if other_tree.is_none() => { + if this_desc.node != other_desc.node { + this_tree + .swap_nodes( + &this_desc.node, + &other_desc.node, + id_tree::SwapBehavior::TakeChildren, + ) + .unwrap(); + } + } + (None, None) => { + let Some(other_tree) = other_tree.as_mut() else { + unreachable!() + }; + let this_node = this_tree.get_mut(&this_desc.node).ok()?; + let other_node = other_tree.get_mut(&other_desc.node).ok()?; + + if let Data::Mapped { mapped, .. } = this_node.data_mut() { + if this.output != other_output { + mapped.output_leave(&this.output); + mapped.output_enter(&other_output, mapped.bbox()); + for (ref surface, _) in mapped.windows() { + toplevel_leave_output(surface, &this.output); + toplevel_enter_output(surface, &other_output); + } + } + for (ref surface, _) in mapped.windows() { + toplevel_leave_workspace(surface, &this_desc.handle); + toplevel_enter_workspace(surface, &other_desc.handle); + } + *mapped.tiling_node_id.lock().unwrap() = Some(other_desc.node.clone()); + } + if let Data::Mapped { mapped, .. } = other_node.data_mut() { + if this.output != other_output { + mapped.output_leave(&other_output); + mapped.output_enter(&this.output, mapped.bbox()); + for (ref surface, _) in mapped.windows() { + toplevel_leave_output(surface, &other_output); + toplevel_enter_output(surface, &this.output); + } + } + for (ref surface, _) in mapped.windows() { + toplevel_leave_workspace(surface, &other_desc.handle); + toplevel_enter_workspace(surface, &this_desc.handle); + } + *mapped.tiling_node_id.lock().unwrap() = Some(this_desc.node.clone()); + } + + // swap data + let other_data = other_node.data().clone(); + other_node.replace_data(this_node.replace_data(other_data)); + + // swap children + let mut this_children = this_node + .children() + .iter() + .map(|child_id| (other_desc.node.clone(), child_id.clone())) + .collect::>(); + let mut other_children = other_node + .children() + .iter() + .map(|child_id| (this_desc.node.clone(), child_id.clone())) + .collect::>(); + + while !this_children.is_empty() { + for (parent_id, child_id) in std::mem::take(&mut this_children) { + let new_children = this_tree + .children_ids(&child_id) + .unwrap() + .cloned() + .collect::>(); + let child_node = this_tree + .remove_node(child_id, RemoveBehavior::OrphanChildren) + .unwrap(); + let maybe_mapped = match child_node.data() { + Data::Mapped { mapped, .. } => Some(mapped.clone()), + _ => None, + }; + let new_id = other_tree + .insert(child_node, InsertBehavior::UnderNode(&parent_id)) + .unwrap(); + if let Some(mapped) = maybe_mapped { + if this.output != other_output { + mapped.output_leave(&this.output); + mapped.output_enter(&other_output, mapped.bbox()); + for (ref surface, _) in mapped.windows() { + toplevel_leave_output(surface, &this.output); + toplevel_enter_output(surface, &other_output); + } + } + for (ref surface, _) in mapped.windows() { + toplevel_leave_workspace(surface, &this_desc.handle); + toplevel_enter_workspace(surface, &other_desc.handle); + } + *mapped.tiling_node_id.lock().unwrap() = Some(new_id.clone()); + } + this_children.extend( + new_children + .into_iter() + .map(|new_child| (new_id.clone(), new_child)), + ); + } + } + + while !other_children.is_empty() { + for (parent_id, child_id) in std::mem::take(&mut other_children) { + let new_children = other_tree + .children_ids(&child_id) + .unwrap() + .cloned() + .collect::>(); + let child_node = other_tree + .remove_node(child_id, RemoveBehavior::OrphanChildren) + .unwrap(); + let maybe_mapped = match child_node.data() { + Data::Mapped { mapped, .. } => Some(mapped.clone()), + _ => None, + }; + let new_id = this_tree + .insert(child_node, InsertBehavior::UnderNode(&parent_id)) + .unwrap(); + if let Some(mapped) = maybe_mapped { + if this.output != other_output { + mapped.output_leave(&other_output); + mapped.output_enter(&this.output, mapped.bbox()); + for (ref surface, _) in mapped.windows() { + toplevel_leave_output(surface, &other_output); + toplevel_enter_output(surface, &this.output); + } + } + for (ref surface, _) in mapped.windows() { + toplevel_leave_workspace(surface, &other_desc.handle); + toplevel_enter_workspace(surface, &this_desc.handle); + } + *mapped.tiling_node_id.lock().unwrap() = Some(new_id.clone()); + } + other_children.extend( + new_children + .into_iter() + .map(|new_child| (new_id.clone(), new_child)), + ); + } + } + } + (Some(this_surface), None) => { + if !this_surface.alive() { + return None; + } + let this_node = this_tree.get_mut(&this_desc.node).ok()?; + let Data::Mapped { + mapped: this_mapped, + .. + } = this_node.data() + else { + return None; + }; + let this_mapped = this_mapped.clone(); + let geometry = *this_node.data().geometry(); + assert!(this_mapped.is_stack()); + + let other_tree = other_tree.as_mut().unwrap_or(&mut this_tree); + if other_tree.get(&other_desc.node).is_err() { + return None; + } + + let surfaces = other_tree + .traverse_pre_order(&other_desc.node) + .unwrap() + .flat_map(|node| match node.data() { + Data::Mapped { mapped, .. } => Some(mapped.windows().map(|(s, _)| s)), + _ => None, + }) + .flatten() + .collect::>(); + let cleanup = other_tree + .children_ids(&other_desc.node) + .unwrap() + .cloned() + .collect::>(); + for node in cleanup { + let _ = other_tree.remove_node(node, RemoveBehavior::DropChildren); + } + + let this_stack = this_mapped.stack_ref()?; + let this_idx = this_stack + .surfaces() + .position(|s| &s == this_surface) + .unwrap(); + for (i, surface) in surfaces.into_iter().enumerate() { + if this.output != other_output { + surface.output_leave(&other_output); + surface.output_enter(&this.output, surface.bbox()); + toplevel_leave_output(&surface, &other_output); + toplevel_enter_output(&surface, &this.output); + } + if this_desc.handle != other_desc.handle { + toplevel_leave_workspace(&surface, &other_desc.handle); + toplevel_enter_workspace(&surface, &this_desc.handle); + } + this_stack.add_window(surface, Some(this_idx + i), None); + } + if this.output != other_output { + this_surface.output_leave(&this.output); + toplevel_leave_output(this_surface, &this.output); + toplevel_enter_output(this_surface, &other_output); + } + if this_desc.handle != other_desc.handle { + toplevel_leave_workspace(this_surface, &this_desc.handle); + toplevel_enter_workspace(this_surface, &other_desc.handle); + } + this_stack.remove_window(this_surface); + + let mapped: CosmicMapped = CosmicWindow::new( + this_surface.clone(), + this_stack.loop_handle(), + this.theme.clone(), + this.appearance, + ) + .into(); + mapped.set_tiled(true); + mapped.refresh(); + mapped.output_enter(&other_output, mapped.bbox()); + + *mapped.tiling_node_id.lock().unwrap() = Some(other_desc.node.clone()); + other_tree + .get_mut(&other_desc.node) + .unwrap() + .replace_data(Data::Mapped { + mapped, + last_geometry: geometry, + minimize_rect: None, + }); + } + (None, Some(other_surface)) => { + if !other_surface.alive() { + return None; + } + + let other_tree = other_tree.as_mut().unwrap_or(&mut this_tree); + let other_node = other_tree.get_mut(&other_desc.node).ok()?; + let Data::Mapped { + mapped: other_mapped, + .. + } = other_node.data() + else { + return None; + }; + let other_mapped = other_mapped.clone(); + let geometry = *other_node.data().geometry(); + assert!(other_mapped.is_stack()); + + let surfaces = this_tree + .traverse_pre_order(&this_desc.node) + .unwrap() + .flat_map(|node| match node.data() { + Data::Mapped { mapped, .. } => Some(mapped.windows().map(|(s, _)| s)), + _ => None, + }) + .flatten() + .collect::>(); + let cleanup = this_tree + .children_ids(&this_desc.node) + .unwrap() + .cloned() + .collect::>(); + for node in cleanup { + let _ = this_tree.remove_node(node, RemoveBehavior::DropChildren); + } + + let other_stack = other_mapped.stack_ref()?; + let other_idx = other_stack + .surfaces() + .position(|s| &s == other_surface) + .unwrap(); + for (i, surface) in surfaces.into_iter().enumerate() { + if this.output != other_output { + surface.output_leave(&this.output); + surface.output_enter(&other_output, surface.bbox()); + toplevel_leave_output(&surface, &this.output); + toplevel_enter_output(&surface, &other_output); + } + if this_desc.handle != other_desc.handle { + toplevel_leave_workspace(&surface, &this_desc.handle); + toplevel_enter_workspace(&surface, &other_desc.handle); + } + other_stack.add_window(surface, Some(other_idx + i), None); + } + if this.output != other_output { + other_surface.output_leave(&other_output); + toplevel_leave_output(other_surface, &other_output); + toplevel_enter_output(other_surface, &this.output); + } + if this_desc.handle != other_desc.handle { + toplevel_leave_workspace(other_surface, &other_desc.handle); + toplevel_enter_workspace(other_surface, &this_desc.handle); + } + other_stack.remove_window(other_surface); + + let mapped: CosmicMapped = CosmicWindow::new( + other_surface.clone(), + other_stack.loop_handle(), + this.theme.clone(), + this.appearance, + ) + .into(); + mapped.set_tiled(true); + mapped.refresh(); + mapped.output_enter(&this.output, mapped.bbox()); + + *mapped.tiling_node_id.lock().unwrap() = Some(this_desc.node.clone()); + this_tree + .get_mut(&this_desc.node) + .unwrap() + .replace_data(Data::Mapped { + mapped, + last_geometry: geometry, + minimize_rect: None, + }); + } + (Some(this_surface), Some(other_surface)) => { + if !this_surface.alive() || !other_surface.alive() { + return None; + } + + let this_mapped = + this_tree + .get(&this_desc.node) + .ok() + .and_then(|node| match node.data() { + Data::Mapped { mapped, .. } => Some(mapped.clone()), + _ => None, + })?; + let other_tree = other_tree.as_mut().unwrap_or(&mut this_tree); + let other_mapped = + other_tree + .get(&other_desc.node) + .ok() + .and_then(|node| match node.data() { + Data::Mapped { mapped, .. } => Some(mapped.clone()), + _ => None, + })?; + + let this_stack = this_mapped.stack_ref()?; + let other_stack = other_mapped.stack_ref()?; + + let this_idx = this_stack + .surfaces() + .position(|s| &s == this_surface) + .unwrap(); + let other_idx = other_stack + .surfaces() + .position(|s| &s == other_surface) + .unwrap(); + let this_was_active = &this_stack.active() == this_surface; + let other_was_active = &other_stack.active() == other_surface; + this_stack.add_window(other_surface.clone(), Some(this_idx), None); + this_stack.remove_window(this_surface); + other_stack.add_window(this_surface.clone(), Some(other_idx), None); + + if this.output != other_output { + toplevel_leave_output(this_surface, &this.output); + toplevel_leave_output(other_surface, &other_output); + toplevel_enter_output(this_surface, &other_output); + toplevel_enter_output(other_surface, &this.output); + } + if this_desc.handle != other_desc.handle { + toplevel_leave_workspace(this_surface, &this_desc.handle); + toplevel_leave_workspace(other_surface, &other_desc.handle); + toplevel_enter_workspace(this_surface, &other_desc.handle); + toplevel_enter_workspace(other_surface, &this_desc.handle); + } + + other_stack.remove_window(other_surface); + if this_was_active { + this_stack.set_active(other_surface); + } + if other_was_active { + other_stack.set_active(this_surface); + } + + return other + .as_ref() + .unwrap_or(&this) + .node_desc_to_focus(other_desc); + } + } + + let this_gaps = this.gaps(); + let blocker = TilingLayout::update_positions(&this.output, &mut this_tree, this_gaps); + this.queue.push_tree(this_tree, ANIMATION_DURATION, blocker); + + let has_other_tree = other_tree.is_some(); + if let Some(mut other_tree) = other_tree { + let (other_queue, gaps) = if let Some(other) = other.as_mut() { + let other_gaps = other.gaps(); + (&mut other.queue, other_gaps) + } else { + (&mut this.queue, this_gaps) + }; + let blocker = TilingLayout::update_positions(&other_output, &mut other_tree, gaps); + other_queue.push_tree(other_tree, ANIMATION_DURATION, blocker); + } + + match (&this_desc.stack_window, &other_desc.stack_window) { + (None, None) if !has_other_tree => this.node_desc_to_focus(this_desc), + //(None, Some(_)) => None, + _ => other + .as_ref() + .unwrap_or(&this) + .node_desc_to_focus(other_desc), + } + } + + pub fn node_desc_to_focus(&self, desc: &NodeDesc) -> Option { + let tree = &self.queue.trees.back().unwrap().0; + let data = tree.get(&desc.node).ok()?.data(); + match data { + Data::Mapped { mapped, .. } => Some(KeyboardFocusTarget::Element(mapped.clone())), + Data::Group { alive, .. } => Some(KeyboardFocusTarget::Group(WindowGroup { + node: desc.node.clone(), + alive: Arc::downgrade(alive), + focus_stack: tree + .children_ids(&desc.node) + .unwrap() + .cloned() + .collect::>(), + })), + _ => None, + } + } + + pub fn tree(&self) -> &Tree { + &self.queue.trees.back().unwrap().0 + } + + pub fn unmap( + &mut self, + window: &CosmicMapped, + to: Option>, + ) -> Result, NodeIdError> { + let node_id = window + .tiling_node_id + .lock() + .unwrap() + .clone() + .ok_or(NodeIdError::NodeIdNoLongerValid)?; + let state = { + let tree = &self.queue.trees.back().unwrap().0; + tree.get(&node_id).unwrap().parent().and_then(|parent_id| { + let parent = tree.get(parent_id).unwrap(); + let idx = parent + .children() + .iter() + .position(|id| id == &node_id) + .unwrap(); + if let Data::Group { + orientation, sizes, .. + } = parent.data() + { + if sizes.len() == 2 { + // this group will be flattened + Some(RestoreTilingState { + parent: None, + sibling: parent.children().iter().find(|&id| id != &node_id).cloned(), + orientation: *orientation, + idx, + sizes: sizes.clone(), + }) + } else { + Some(RestoreTilingState { + parent: Some(parent_id.clone()), + sibling: None, + orientation: *orientation, + idx, + sizes: sizes.clone(), + }) + } + } else { + None + } + }) + }; + + if self.unmap_window_internal(window, to.is_some()) { + if let Some(to) = to { + let tree = &mut self + .queue + .trees + .get_mut(self.queue.trees.len() - 2) + .unwrap() + .0; + if let Data::Mapped { + minimize_rect: minimize_to, + .. + } = tree.get_mut(&node_id).unwrap().data_mut() + { + *minimize_to = Some(to); + } + } + + window.output_leave(&self.output); + window.set_tiled(false); + *window.tiling_node_id.lock().unwrap() = None; + } else { + return Err(NodeIdError::InvalidNodeIdForTree); + } + + Ok(state) + } + + pub fn unmap_as_placeholder( + &mut self, + window: &CosmicMapped, + type_: PlaceholderType, + ) -> Option { + let node_id = window.tiling_node_id.lock().unwrap().take()?; + + // Initialize last_overview_hover to the placeholder position so that + // dropping without mouse movement restores the window to its original position + if matches!(type_, PlaceholderType::GrabbedWindow) { + self.last_overview_hover = + Some((None, TargetZone::InitialPlaceholder(node_id.clone()))); + } + + let data = self + .queue + .trees + .back_mut() + .unwrap() + .0 + .get_mut(&node_id) + .unwrap() + .data_mut(); + *data = Data::Placeholder { + id: Id::new(), + last_geometry: *data.geometry(), + type_, + }; + + window.output_leave(&self.output); + window.set_tiled(false); + Some(node_id) + } + + fn unmap_window_internal(&mut self, mapped: &CosmicMapped, minimizing: bool) -> bool { + let tiling_node_id = mapped.tiling_node_id.lock().unwrap().as_ref().cloned(); + let gaps = self.gaps(); + + if let Some(node_id) = tiling_node_id + && self + .queue + .trees + .back() + .unwrap() + .0 + .get(&node_id) + .map(|node| node.data().is_mapped(Some(mapped))) + .unwrap_or(false) + { + let mut tree = self.queue.trees.back().unwrap().0.copy_clone(); + + TilingLayout::unmap_internal(&mut tree, &node_id); + + let duration = if minimizing { + MINIMIZE_ANIMATION_DURATION + } else { + ANIMATION_DURATION + }; + let blocker = TilingLayout::update_positions(&self.output, &mut tree, gaps); + self.queue.push_tree(tree, duration, blocker); + + return true; + } + false + } + + fn unmap_internal(tree: &mut Tree, node: &NodeId) { + let parent_id = tree.get(node).ok().and_then(|node| node.parent()).cloned(); + let position = parent_id.as_ref().and_then(|parent_id| { + tree.children_ids(parent_id) + .unwrap() + .position(|id| id == node) + }); + let parent_parent_id = parent_id.as_ref().and_then(|parent_id| { + tree.get(parent_id) + .ok() + .and_then(|node| node.parent()) + .cloned() + }); + + // remove self + trace!(?node, "Removing node."); + let _ = tree.remove_node(node.clone(), RemoveBehavior::DropChildren); + + // fixup parent node + if let Some(id) = parent_id { + let position = position.unwrap(); + let group = tree.get_mut(&id).unwrap().data_mut(); + assert!(group.is_group()); + + if group.len() > 2 { + group.remove_window(position); + } else { + trace!("Removing Group"); + let other_child = tree.children_ids(&id).unwrap().next().cloned().unwrap(); + let fork_pos = parent_parent_id.as_ref().and_then(|parent_id| { + tree.children_ids(parent_id).unwrap().position(|i| i == &id) + }); + let _ = tree.remove_node(id.clone(), RemoveBehavior::OrphanChildren); + tree.move_node( + &other_child, + parent_parent_id + .as_ref() + .map(MoveBehavior::ToParent) + .unwrap_or(MoveBehavior::ToRoot), + ) + .unwrap(); + if let Some(old_pos) = fork_pos { + tree.make_nth_sibling(&other_child, old_pos).unwrap(); + } + } + } + } + + // TODO: Move would needs this to be accurate during animations + pub fn element_geometry(&self, elem: &CosmicMapped) -> Option> { + if let Some(id) = elem.tiling_node_id.lock().unwrap().as_ref() { + let node = self.queue.trees.back().unwrap().0.get(id).ok()?; + let data = node.data(); + assert!(data.is_mapped(Some(elem))); + let geo = *data.geometry(); + Some(geo) + } else { + None + } + } + + pub fn move_current_node(&mut self, direction: Direction, seat: &Seat) -> MoveResult { + let gaps = self.gaps(); + + let mut tree = self.queue.trees.back().unwrap().0.copy_clone(); + + let Some(target) = seat.get_keyboard().unwrap().current_focus() else { + return MoveResult::None; + }; + let Some((node_id, data)) = TilingLayout::currently_focused_node(&tree, target) else { + return MoveResult::None; + }; + + // stacks may handle movement internally + if let FocusedNodeData::Window(window) = data.clone() { + match window.handle_move(direction) { + StackMoveResult::Handled => return MoveResult::Done, + StackMoveResult::MoveOut(surface, loop_handle) => { + let mapped: CosmicMapped = CosmicWindow::new( + surface, + loop_handle, + self.theme.clone(), + self.appearance, + ) + .into(); + mapped.output_enter(&self.output, mapped.bbox()); + let orientation = match direction { + Direction::Left | Direction::Right => Orientation::Vertical, + Direction::Up | Direction::Down => Orientation::Horizontal, + }; + + let new_node = Node::new(Data::Mapped { + mapped: mapped.clone(), + last_geometry: Rectangle::from_size((100, 100).into()), + minimize_rect: None, + }); + let new_id = tree.insert(new_node, InsertBehavior::AsRoot).unwrap(); + TilingLayout::new_group(&mut tree, &node_id, &new_id, orientation).unwrap(); + tree.make_nth_sibling( + &new_id, + match direction { + Direction::Left | Direction::Up => 0, + Direction::Right | Direction::Down => 1, + }, + ) + .unwrap(); + *mapped.tiling_node_id.lock().unwrap() = Some(new_id); + + let blocker = TilingLayout::update_positions(&self.output, &mut tree, gaps); + self.queue.push_tree(tree, ANIMATION_DURATION, blocker); + return MoveResult::ShiftFocus(mapped.into()); + } + StackMoveResult::Default => {} // continue normally + } + } + + let mut child_id = node_id.clone(); + // Without a parent to start with, just return + let Some(og_parent) = tree.get(&node_id).unwrap().parent().cloned() else { + return match data { + FocusedNodeData::Window(window) => MoveResult::MoveFurther(window.into()), + FocusedNodeData::Group(focus_stack, alive) => MoveResult::MoveFurther( + WindowGroup { + node: node_id, + alive, + focus_stack, + } + .into(), + ), + }; + }; + let og_idx = tree + .children_ids(&og_parent) + .unwrap() + .position(|id| id == &child_id) + .unwrap(); + let mut maybe_parent = Some(og_parent.clone()); + + while let Some(parent) = maybe_parent { + let parent_data = tree.get(&parent).unwrap().data(); + let orientation = parent_data.orientation(); + let len = parent_data.len(); + + // which child are we? + let idx = tree + .children_ids(&parent) + .unwrap() + .position(|id| id == &child_id) + .unwrap(); + + // if the orientation does not match.. + if matches!( + (orientation, direction), + (Orientation::Horizontal, Direction::Right) + | (Orientation::Horizontal, Direction::Left) + | (Orientation::Vertical, Direction::Up) + | (Orientation::Vertical, Direction::Down) + ) { + // ...create a new group with our parent (cleanup will remove any one-child-groups afterwards) + TilingLayout::new_group( + &mut tree, + &parent, + &node_id, + match direction { + Direction::Left | Direction::Right => Orientation::Vertical, + Direction::Up | Direction::Down => Orientation::Horizontal, + }, + ) + .unwrap(); + tree.make_nth_sibling( + &node_id, + if direction == Direction::Left || direction == Direction::Up { + 0 + } else { + 1 + }, + ) + .unwrap(); + + tree.get_mut(&og_parent) + .unwrap() + .data_mut() + .remove_window(og_idx); + + let blocker = TilingLayout::update_positions(&self.output, &mut tree, gaps); + self.queue.push_tree(tree, ANIMATION_DURATION, blocker); + return MoveResult::Done; + } + + // now if the orientation matches + + // if we are not already in this group, we just move into it (up) + if child_id != node_id { + tree.move_node(&node_id, MoveBehavior::ToParent(&parent)) + .unwrap(); + tree.make_nth_sibling( + &node_id, + if direction == Direction::Left || direction == Direction::Up { + idx + } else { + idx + 1 + }, + ) + .unwrap(); + tree.get_mut(&parent).unwrap().data_mut().add_window(idx); + tree.get_mut(&og_parent) + .unwrap() + .data_mut() + .remove_window(og_idx); + + let blocker = TilingLayout::update_positions(&self.output, &mut tree, gaps); + self.queue.push_tree(tree, ANIMATION_DURATION, blocker); + return MoveResult::Done; + } + + // we can maybe move inside the group, if we don't run out of elements + if let Some(next_idx) = match (orientation, direction) { + (Orientation::Horizontal, Direction::Down) + | (Orientation::Vertical, Direction::Right) + if idx < (len - 1) => + { + Some(idx + 1) + } + (Orientation::Horizontal, Direction::Up) + | (Orientation::Vertical, Direction::Left) + if idx > 0 => + { + Some(idx - 1) + } + _ => None, + } { + // if we can, we need to check the next element and move "into" it + let next_child_id = tree + .children_ids(&parent) + .unwrap() + .nth(next_idx) + .unwrap() + .clone(); + + let result = if tree.get(&next_child_id).unwrap().data().is_stack() + && tree.get(&node_id).unwrap().data().is_mapped(None) + && !tree.get(&node_id).unwrap().data().is_stack() + && len == 2 + { + let node = tree + .remove_node(node_id, RemoveBehavior::DropChildren) + .unwrap(); + + let stack_data = tree.get_mut(&next_child_id).unwrap().data_mut(); + let mapped = match stack_data { + Data::Mapped { mapped, .. } => mapped.clone(), + _ => unreachable!(), + }; + let stack = mapped.stack_ref().unwrap(); + + let surface = match node.data() { + Data::Mapped { mapped, .. } => mapped.active_window(), + _ => unreachable!(), + }; + stack.add_window( + surface, + match direction { + Direction::Right => Some(0), + _ => None, + }, + Some(seat), + ); + tree.get_mut(&og_parent) + .unwrap() + .data_mut() + .remove_window(og_idx); + + MoveResult::ShiftFocus(mapped.into()) + } else if tree.get(&next_child_id).unwrap().data().is_group() && len == 2 { + // if it is a group, we want to move into the group + tree.move_node(&node_id, MoveBehavior::ToParent(&next_child_id)) + .unwrap(); + let group_orientation = tree.get(&next_child_id).unwrap().data().orientation(); + match (group_orientation, direction) { + (Orientation::Horizontal, Direction::Down) + | (Orientation::Vertical, Direction::Right) => { + tree.make_first_sibling(&node_id).unwrap(); + tree.get_mut(&next_child_id) + .unwrap() + .data_mut() + .add_window(0); + } + (Orientation::Horizontal, Direction::Up) + | (Orientation::Vertical, Direction::Left) => { + tree.make_last_sibling(&node_id).unwrap(); + let group = tree.get_mut(&next_child_id).unwrap().data_mut(); + group.add_window(group.len()); + } + _ => { + // we want the middle + let group_len = tree.get(&next_child_id).unwrap().data().len(); + if group_len.is_multiple_of(2) { + tree.make_nth_sibling(&node_id, group_len / 2).unwrap(); + tree.get_mut(&next_child_id) + .unwrap() + .data_mut() + .add_window(group_len / 2); + } else { + // we move again by making a new fork + let old_id = tree + .children_ids(&next_child_id) + .unwrap() + .nth(group_len / 2) + .unwrap() + .clone(); + TilingLayout::new_group( + &mut tree, + &old_id, + &node_id, + !group_orientation, + ) + .unwrap(); + tree.make_nth_sibling( + &node_id, + if direction == Direction::Left || direction == Direction::Up { + 1 + } else { + 0 + }, + ) + .unwrap(); + } + } + }; + tree.get_mut(&og_parent) + .unwrap() + .data_mut() + .remove_window(og_idx); + + MoveResult::Done + } else if len == 2 && child_id == node_id { + // if we are just us two in the group, lets swap + tree.make_nth_sibling(&node_id, next_idx).unwrap(); + // also swap sizes + tree.get_mut(&og_parent) + .unwrap() + .data_mut() + .swap_windows(idx, next_idx); + + MoveResult::Done + } else { + // else we make a new fork + TilingLayout::new_group(&mut tree, &next_child_id, &node_id, orientation) + .unwrap(); + tree.make_nth_sibling( + &node_id, + if direction == Direction::Left || direction == Direction::Up { + 1 + } else { + 0 + }, + ) + .unwrap(); + tree.get_mut(&og_parent) + .unwrap() + .data_mut() + .remove_window(og_idx); + + MoveResult::Done + }; + + let blocker = TilingLayout::update_positions(&self.output, &mut tree, gaps); + self.queue.push_tree(tree, ANIMATION_DURATION, blocker); + return result; + } + + // We have reached the end of our parent group, try to move out even higher. + maybe_parent = tree.get(&parent).unwrap().parent().cloned(); + child_id = parent.clone(); + } + + match data { + FocusedNodeData::Window(window) => MoveResult::MoveFurther(window.into()), + FocusedNodeData::Group(focus_stack, alive) => MoveResult::MoveFurther( + WindowGroup { + node: node_id, + alive, + focus_stack, + } + .into(), + ), + } + } + + pub fn next_focus<'a>( + &self, + direction: FocusDirection, + seat: &Seat, + focus_stack: impl Iterator + 'a, + swap_desc: Option, + ) -> FocusResult { + let tree = &self.queue.trees.back().unwrap().0; + + let Some(target) = seat.get_keyboard().unwrap().current_focus() else { + return FocusResult::None; + }; + let Some(focused) = TilingLayout::currently_focused_node(tree, target).or_else(|| { + TilingLayout::last_active_window(tree, focus_stack) + .map(|(id, mapped)| (id, FocusedNodeData::Window(mapped))) + }) else { + return FocusResult::None; + }; + + let (last_node_id, data) = focused; + + if direction == FocusDirection::In { + if swap_desc + .as_ref() + .map(|desc| desc.node == last_node_id) + .unwrap_or(false) + { + return FocusResult::Handled; // abort + } + + if let FocusedNodeData::Group(mut stack, _) = data.clone() { + let maybe_id = stack.pop().unwrap(); + let id = if tree + .children_ids(&last_node_id) + .unwrap() + .any(|id| id == &maybe_id) + { + Some(maybe_id) + } else { + tree.children_ids(&last_node_id).unwrap().next().cloned() + }; + + if let Some(id) = id { + return match tree.get(&id).unwrap().data() { + Data::Mapped { mapped, .. } => { + if let Some(stack) = mapped.stack_ref() { + stack.focus_stack(); + } + FocusResult::Some(mapped.clone().into()) + } + Data::Group { alive, .. } => FocusResult::Some( + WindowGroup { + node: id, + alive: Arc::downgrade(alive), + focus_stack: stack, + } + .into(), + ), + Data::Placeholder { .. } => FocusResult::None, + }; + } + } + } + + let mut node_id = last_node_id.clone(); + while let Some(group) = tree.get(&node_id).unwrap().parent() { + let child = node_id.clone(); + let group_data = tree.get(group).unwrap().data(); + let main_orientation = group_data.orientation(); + assert!(group_data.is_group()); + + if direction == FocusDirection::Out { + if swap_desc + .as_ref() + .map(|desc| { + tree.traverse_pre_order_ids(group) + .unwrap() + .any(|node| node == desc.node) + }) + .unwrap_or(false) + { + return FocusResult::Handled; // abort + } + + return FocusResult::Some( + WindowGroup { + node: group.clone(), + alive: match group_data { + Data::Group { alive, .. } => Arc::downgrade(alive), + _ => unreachable!(), + }, + focus_stack: match data { + FocusedNodeData::Group(mut stack, _) => { + stack.push(child); + stack + } + _ => vec![child], + }, + } + .into(), + ); + } + + // which child are we? + let idx = tree + .children_ids(group) + .unwrap() + .position(|id| id == &child) + .unwrap(); + let len = group_data.len(); + + let focus_subtree = match (main_orientation, direction) { + (Orientation::Horizontal, FocusDirection::Down) + | (Orientation::Vertical, FocusDirection::Right) + if idx < (len - 1) => + { + tree.children_ids(group).unwrap().nth(idx + 1) + } + (Orientation::Horizontal, FocusDirection::Up) + | (Orientation::Vertical, FocusDirection::Left) + if idx > 0 => + { + tree.children_ids(group).unwrap().nth(idx - 1) + } + _ => None, // continue iterating + }; + + if focus_subtree.is_some() { + let mut node_id = focus_subtree; + while node_id.is_some() { + if let Some(desc) = swap_desc.as_ref() + && let Some(replacement_id) = tree + .ancestor_ids(node_id.unwrap()) + .unwrap() + .find(|anchestor| *anchestor == &desc.node) + .or_else(|| { + tree.children_ids(node_id.unwrap()) + .unwrap() + .find(|child| *child == &desc.node) + }) + { + return match tree.get(replacement_id).unwrap().data() { + Data::Group { alive, .. } => { + FocusResult::Some(KeyboardFocusTarget::Group(WindowGroup { + node: replacement_id.clone(), + alive: Arc::downgrade(alive), + focus_stack: tree + .children_ids(replacement_id) + .unwrap() + .cloned() + .collect(), + })) + } + Data::Mapped { mapped, .. } => { + if let Some(stack) = mapped.stack_ref() + && desc.stack_window.is_none() + && replacement_id == &desc.node + { + stack.focus_stack(); + } + FocusResult::Some(KeyboardFocusTarget::Element(mapped.clone())) + } + _ => unreachable!(), + }; + } + + match tree.get(node_id.unwrap()).unwrap().data() { + Data::Group { orientation, .. } if orientation == &main_orientation => { + // if the group is layed out in the direction we care about, + // we can just use the first or last element (depending on the direction) + match direction { + FocusDirection::Down | FocusDirection::Right => { + node_id = tree + .children_ids(node_id.as_ref().unwrap()) + .unwrap() + .next(); + } + FocusDirection::Up | FocusDirection::Left => { + node_id = tree + .children_ids(node_id.as_ref().unwrap()) + .unwrap() + .last(); + } + _ => unreachable!(), + } + } + Data::Group { .. } => { + let center = { + let geo = tree.get(&last_node_id).unwrap().data().geometry(); + let mut point = geo.loc; + match direction { + FocusDirection::Down => { + point += Point::from((geo.size.w / 2 - 1, geo.size.h)) + } + FocusDirection::Up => point.x += geo.size.w / 2 - 1, + FocusDirection::Left => point.y += geo.size.h / 2 - 1, + FocusDirection::Right => { + point += Point::from((geo.size.w, geo.size.h / 2 - 1)) + } + _ => unreachable!(), + }; + point.to_f64() + }; + + let distance = |candidate: &&NodeId| -> f64 { + let geo = tree.get(candidate).unwrap().data().geometry(); + let mut point = geo.loc; + match direction { + FocusDirection::Up => { + point += Point::from((geo.size.w / 2, geo.size.h)) + } + FocusDirection::Down => point.x += geo.size.w, + FocusDirection::Right => point.y += geo.size.h / 2, + FocusDirection::Left => { + point += Point::from((geo.size.w, geo.size.h / 2)) + } + _ => unreachable!(), + }; + let point = point.to_f64(); + ((point.x - center.x).powi(2) + (point.y - center.y).powi(2)).sqrt() + }; + + node_id = tree + .children_ids(node_id.as_ref().unwrap()) + .unwrap() + .min_by(|node1, node2| { + distance(node1).abs().total_cmp(&distance(node2).abs()) + }); + } + Data::Mapped { mapped, .. } => { + if let Some(stack) = mapped.stack_ref() + && swap_desc + .as_ref() + .map(|desc| { + desc.stack_window.is_none() + && &desc.node == node_id.unwrap() + }) + .unwrap_or(false) + { + stack.focus_stack(); + } + return FocusResult::Some(mapped.clone().into()); + } + Data::Placeholder { .. } => return FocusResult::None, + } + } + } else { + node_id = group.clone(); + } + } + + FocusResult::None + } + + pub fn update_orientation(&mut self, new_orientation: Option, seat: &Seat) { + let gaps = self.gaps(); + + let Some(target) = seat.get_keyboard().unwrap().current_focus() else { + return; + }; + + let mut tree = self.queue.trees.back().unwrap().0.copy_clone(); + if let Some((last_active, _)) = TilingLayout::currently_focused_node(&tree, target) + && let Some(group) = tree.get(&last_active).unwrap().parent().cloned() + && let &mut Data::Group { + ref mut orientation, + ref mut sizes, + ref last_geometry, + .. + } = tree.get_mut(&group).unwrap().data_mut() + { + let previous_length = match orientation { + Orientation::Horizontal => last_geometry.size.h, + Orientation::Vertical => last_geometry.size.w, + }; + let new_orientation = new_orientation.unwrap_or(!*orientation); + let new_length = match new_orientation { + Orientation::Horizontal => last_geometry.size.h, + Orientation::Vertical => last_geometry.size.w, + }; + + sizes.iter_mut().for_each(|len| { + *len = (((*len as f64) / (previous_length as f64)) * (new_length as f64)).round() + as i32; + }); + let sum: i32 = sizes.iter().sum(); + if sum < new_length { + *sizes.last_mut().unwrap() += new_length - sum; + } + + *orientation = new_orientation; + + let blocker = TilingLayout::update_positions(&self.output, &mut tree, gaps); + self.queue.push_tree(tree, ANIMATION_DURATION, blocker); + } + } + + pub fn toggle_stacking( + &mut self, + mapped: &CosmicMapped, + mut focus_stack: FocusStackMut, + ) -> Option { + let gaps = self.gaps(); + let node_id = mapped.tiling_node_id.lock().unwrap().clone()?; + let mut tree = self.queue.trees.back().unwrap().0.copy_clone(); + if tree.get(&node_id).is_err() { + return None; + } + + let result = if mapped.is_window() { + // if it is just a window + match tree.get_mut(&node_id).unwrap().data_mut() { + Data::Mapped { mapped, .. } => { + mapped.convert_to_stack( + (&self.output, mapped.bbox()), + self.theme.clone(), + self.appearance, + ); + focus_stack.append(mapped.clone()); + KeyboardFocusTarget::Element(mapped.clone()) + } + _ => unreachable!(), + } + } else { + // if we have a stack + let mut surfaces = mapped.windows().map(|(s, _)| s); + let first = surfaces.next().expect("Stack without a window?"); + let focused = mapped.active_window(); + + let mut new_elements = Vec::new(); + + let handle = match tree.get_mut(&node_id).unwrap().data_mut() { + Data::Mapped { mapped, .. } => { + let handle = mapped.loop_handle(); + mapped.convert_to_surface( + first, + (&self.output, mapped.bbox()), + self.theme.clone(), + self.appearance, + ); + new_elements.push(mapped.clone()); + handle + } + _ => unreachable!(), + }; + + // map the rest + let mut current_node = node_id.clone(); + for other in surfaces { + other.try_force_undecorated(false); + other.set_tiled(false); + let focused = other == focused; + let window = CosmicMapped::from(CosmicWindow::new( + other, + handle.clone(), + self.theme.clone(), + self.appearance, + )); + window.output_enter(&self.output, window.bbox()); + + { + let layer_map = layer_map_for_output(&self.output); + window.set_bounds(layer_map.non_exclusive_zone().size); + } + + TilingLayout::map_to_tree( + &mut tree, + window.clone(), + &self.output, + Some(current_node), + None, + None, + ); + + let node = window.tiling_node_id.lock().unwrap().clone().unwrap(); + if focused { + new_elements.insert(0, window); + } else { + new_elements.push(window); + } + current_node = node; + } + + let node = tree.get(&node_id).unwrap(); + let node_id = if current_node != node_id { + node.parent().cloned().unwrap_or(node_id) + } else { + node_id + }; + + for elem in new_elements.iter().rev() { + focus_stack.append(elem.clone()); + } + + match tree.get(&node_id).unwrap().data() { + Data::Group { alive, .. } => KeyboardFocusTarget::Group(WindowGroup { + node: node_id.clone(), + alive: Arc::downgrade(alive), + focus_stack: vec![ + new_elements[0] + .tiling_node_id + .lock() + .unwrap() + .clone() + .unwrap(), + ], + }), + Data::Mapped { mapped, .. } => KeyboardFocusTarget::Element(mapped.clone()), + _ => unreachable!(), + } + }; + + let blocker = TilingLayout::update_positions(&self.output, &mut tree, gaps); + self.queue.push_tree(tree, ANIMATION_DURATION, blocker); + + Some(result) + } + + pub fn toggle_stacking_focused( + &mut self, + seat: &Seat, + mut focus_stack: FocusStackMut, + ) -> Option { + let gaps = self.gaps(); + let target = seat.get_keyboard().unwrap().current_focus()?; + let mut tree = self.queue.trees.back().unwrap().0.copy_clone(); + if let Some((last_active, last_active_data)) = + TilingLayout::currently_focused_node(&tree, target) + { + match last_active_data { + FocusedNodeData::Window(mapped) => { + return self.toggle_stacking(&mapped, focus_stack); + } + FocusedNodeData::Group(_, _) => { + let mut handle = None; + let surfaces = tree + .traverse_pre_order(&last_active) + .unwrap() + .flat_map(|node| match node.data() { + Data::Mapped { mapped, .. } => { + if handle.is_none() { + handle = Some(mapped.loop_handle()); + } + Some(mapped.windows().map(|(s, _)| s)) + } + _ => None, + }) + .flatten() + .collect::>(); + + if surfaces.is_empty() { + return None; + } + let handle = handle.unwrap(); + let stack = CosmicStack::new( + surfaces.into_iter(), + handle, + self.theme.clone(), + self.appearance, + ); + + for child in tree + .children_ids(&last_active) + .unwrap() + .cloned() + .collect::>() + .into_iter() + { + tree.remove_node(child, RemoveBehavior::DropChildren) + .unwrap(); + } + let data = tree.get_mut(&last_active).unwrap().data_mut(); + + let geo = *data.geometry(); + stack.output_enter(&self.output, stack.bbox()); + stack.set_activate(true); + stack.active().send_configure(); + stack.refresh(); + + let mapped = CosmicMapped::from(stack); + *mapped.last_geometry.lock().unwrap() = Some(geo); + *mapped.tiling_node_id.lock().unwrap() = Some(last_active); + focus_stack.append(mapped.clone()); + *data = Data::Mapped { + mapped: mapped.clone(), + last_geometry: geo, + minimize_rect: None, + }; + + let blocker = TilingLayout::update_positions(&self.output, &mut tree, gaps); + self.queue.push_tree(tree, ANIMATION_DURATION, blocker); + + return Some(KeyboardFocusTarget::Element(mapped)); + } + } + } + + None + } + + pub fn recalculate(&mut self) { + let gaps = self.gaps(); + + let mut tree = self.queue.trees.back().unwrap().0.copy_clone(); + let blocker = TilingLayout::update_positions(&self.output, &mut tree, gaps); + self.queue.push_tree(tree, ANIMATION_DURATION, blocker); + } + + #[profiling::function] + pub fn refresh(&mut self) { + let dead_windows = self + .mapped() + .map(|(w, _)| w.clone()) + .filter(|w| !w.alive()) + .collect::>(); + for dead_window in dead_windows.iter() { + self.unmap_window_internal(dead_window, false); + } + + for (mapped, _) in self.mapped() { + mapped.refresh(); + } + } + + pub fn animations_going(&self) -> bool { + self.queue.animation_start.is_some() + } + + pub fn update_animation_state(&mut self) -> HashMap { + let mut clients = HashMap::new(); + let mut ready_trees = 0; + for (_, _, blocker) in self.queue.trees.iter().skip(1) { + if let Some(blocker) = blocker.as_ref() { + if blocker.is_processed() { + ready_trees += 1; + } + if blocker.is_ready() { + clients.extend(blocker.clients()); + continue; + } + break; + } else { + ready_trees += 1; + } + } + + if let Some(start) = self.queue.animation_start { + let duration_since_start = Instant::now().duration_since(start); + if duration_since_start + >= self + .queue + .trees + .get(1) + .expect("Animation going without second tree?") + .1 + { + let _ = self.queue.animation_start.take(); + let _ = self.queue.trees.pop_front(); + ready_trees -= 1; + let front = self.queue.trees.front_mut().unwrap(); + if let Some(root_id) = front.0.root_node_id() { + for node in front + .0 + .traverse_pre_order_ids(root_id) + .unwrap() + .collect::>() + .into_iter() + { + if let Data::Mapped { minimize_rect, .. } = + front.0.get_mut(&node).unwrap().data_mut() + { + minimize_rect.take(); + } + } + } + let _ = front.2.take(); + } else { + return clients; + } + } + + // merge + let other_duration = if ready_trees > 1 { + self.queue + .trees + .drain(1..ready_trees) + .fold(None, |res, (_, duration, _)| { + Some( + res.map(|old_duration: Duration| old_duration.max(duration)) + .unwrap_or(duration), + ) + }) + } else { + None + }; + + // start + if ready_trees > 0 { + let (_, duration, _) = self.queue.trees.get_mut(1).unwrap(); + *duration = other_duration + .map(|other| other.max(*duration)) + .unwrap_or(*duration); + self.queue.animation_start = Some(Instant::now()); + } + + clients + } + + pub fn possible_resizes(tree: &Tree, mut node_id: NodeId) -> ResizeEdge { + let mut edges = ResizeEdge::empty(); + + while let Some(group_id) = tree.get(&node_id).unwrap().parent().cloned() { + let orientation = tree.get(&group_id).unwrap().data().orientation(); + + let node_idx = tree + .children_ids(&group_id) + .unwrap() + .position(|id| id == &node_id) + .unwrap(); + let total = tree.children_ids(&group_id).unwrap().count(); + if orientation == Orientation::Vertical { + if node_idx > 0 { + edges.insert(ResizeEdge::LEFT); + } + if node_idx < total - 1 { + edges.insert(ResizeEdge::RIGHT); + } + } else { + if node_idx > 0 { + edges.insert(ResizeEdge::TOP); + } + if node_idx < total - 1 { + edges.insert(ResizeEdge::BOTTOM); + } + } + + node_id = group_id; + } + + edges + } + + pub fn resize_request( + &self, + mut node_id: NodeId, + edge: ResizeEdge, + ) -> Option<(NodeId, usize, Orientation)> { + let tree = self.tree(); + + while let Some(group_id) = tree.get(&node_id).unwrap().parent().cloned() { + let orientation = tree.get(&group_id).unwrap().data().orientation(); + let node_idx = tree + .children_ids(&group_id) + .unwrap() + .position(|id| id == &node_id) + .unwrap(); + let total = tree.children_ids(&group_id).unwrap().count(); + if orientation == Orientation::Vertical { + if node_idx > 0 && edge.contains(ResizeEdge::LEFT) { + return Some((group_id, node_idx - 1, orientation)); + } + if node_idx < total - 1 && edge.contains(ResizeEdge::RIGHT) { + return Some((group_id, node_idx, orientation)); + } + } else { + if node_idx > 0 && edge.contains(ResizeEdge::TOP) { + return Some((group_id, node_idx - 1, orientation)); + } + if node_idx < total - 1 && edge.contains(ResizeEdge::BOTTOM) { + return Some((group_id, node_idx, orientation)); + } + } + + node_id = group_id; + } + + None + } + + pub fn resize( + &mut self, + focused: &KeyboardFocusTarget, + direction: ResizeDirection, + edges: ResizeEdge, + amount: i32, + ) -> bool { + let gaps = self.gaps(); + + let mut tree = self.queue.trees.back().unwrap().0.copy_clone(); + let Some(root_id) = tree.root_node_id() else { + return false; + }; + let Some(mut node_id) = (match TilingLayout::currently_focused_node(&tree, focused.clone()) + { + Some((_id, FocusedNodeData::Window(mapped))) => + // we need to make sure the id belongs to this tree.. + { + tree.traverse_pre_order_ids(root_id) + .unwrap() + .find(|id| tree.get(id).unwrap().data().is_mapped(Some(&mapped))) + } + Some((id, FocusedNodeData::Group(_, _))) => Some(id), // in this case the workspace handle was already matched, so the id is to be trusted + _ => None, + }) else { + return false; + }; + + while let Some(group_id) = tree.get(&node_id).unwrap().parent().cloned() { + let orientation = tree.get(&group_id).unwrap().data().orientation(); + if !((orientation == Orientation::Vertical + && (edges.contains(ResizeEdge::LEFT) || edges.contains(ResizeEdge::RIGHT))) + || (orientation == Orientation::Horizontal + && (edges.contains(ResizeEdge::TOP) || edges.contains(ResizeEdge::BOTTOM)))) + { + node_id = group_id.clone(); + continue; + } + + let node_idx = tree + .children_ids(&group_id) + .unwrap() + .position(|id| id == &node_id) + .unwrap(); + let Some(other_idx) = (match edges { + x if x.intersects(ResizeEdge::TOP_LEFT) => node_idx.checked_sub(1), + _ => { + if tree.children_ids(&group_id).unwrap().count() - 1 > node_idx { + Some(node_idx + 1) + } else { + None + } + } + }) else { + node_id = group_id.clone(); + continue; + }; + + let data = tree.get_mut(&group_id).unwrap().data_mut(); + + match data { + Data::Group { sizes, .. } => { + let (shrink_idx, grow_idx) = if direction == ResizeDirection::Inwards { + (node_idx, other_idx) + } else { + (other_idx, node_idx) + }; + + if sizes[shrink_idx] + sizes[grow_idx] + < match orientation { + Orientation::Vertical => 720, + Orientation::Horizontal => 480, + } + { + return true; + }; + + let old_size = sizes[shrink_idx]; + sizes[shrink_idx] = + (old_size - amount).max(if orientation == Orientation::Vertical { + 360 + } else { + 240 + }); + let diff = old_size - sizes[shrink_idx]; + sizes[grow_idx] += diff; + } + _ => unreachable!(), + } + + let should_configure = + tree.traverse_pre_order(&group_id) + .unwrap() + .all(|node| match node.data() { + Data::Mapped { mapped, .. } => mapped.latest_size_committed(), + _ => true, + }); + if should_configure { + let blocker = TilingLayout::update_positions(&self.output, &mut tree, gaps); + self.queue.push_tree(tree, None, blocker); + } + + return true; + } + + true + } + + pub fn stacking_indicator(&self) -> Option> { + if let Some(TargetZone::WindowStack(_, geo)) = + self.last_overview_hover.as_ref().map(|(_, zone)| zone) + { + Some(*geo) + } else { + None + } + } + + pub fn cleanup_drag(&mut self) { + let old_tree = &self.queue.trees.back().unwrap().0; + let mut new_tree = None; + + if let Some(root) = old_tree.root_node_id() { + for id in old_tree.traverse_pre_order_ids(root).unwrap() { + match old_tree.get(&id).map(|node| node.data()) { + Ok(Data::Placeholder { .. }) => { + // Copy a tree on write + let new_tree = new_tree.get_or_insert_with(|| old_tree.copy_clone()); + TilingLayout::unmap_internal(new_tree, &id) + } + Ok(Data::Group { pill_indicator, .. }) if pill_indicator.is_some() => { + let new_tree = new_tree.get_or_insert_with(|| old_tree.copy_clone()); + match new_tree.get_mut(&id).unwrap().data_mut() { + Data::Group { pill_indicator, .. } => { + *pill_indicator = None; + } + _ => unreachable!(), + } + } + _ => {} + } + } + + // If anything was changed, push updated tree + if let Some(mut new_tree) = new_tree { + let blocker = + TilingLayout::update_positions(&self.output, &mut new_tree, self.gaps()); + self.queue.push_tree(new_tree, ANIMATION_DURATION, blocker); + } + } + } + + pub fn drop_window(&mut self, window: CosmicMapped) -> (CosmicMapped, Point) { + let gaps = self.gaps(); + + let mut tree = self.queue.trees.back().unwrap().0.copy_clone(); + + window.output_enter(&self.output, window.bbox()); + { + let layer_map = layer_map_for_output(&self.output); + window.set_bounds(layer_map.non_exclusive_zone().size); + } + + let mapped = match self.last_overview_hover.as_ref().map(|(_, zone)| zone) { + Some(TargetZone::GroupEdge(group_id, direction)) if tree.get(group_id).is_ok() => { + let new_id = tree + .insert( + Node::new(Data::Mapped { + mapped: window.clone(), + last_geometry: Rectangle::from_size((100, 100).into()), + minimize_rect: None, + }), + InsertBehavior::UnderNode(group_id), + ) + .unwrap(); + + let orientation = if matches!(direction, Direction::Left | Direction::Right) { + Orientation::Vertical + } else { + Orientation::Horizontal + }; + if tree.get(group_id).unwrap().data().orientation() != orientation { + TilingLayout::new_group(&mut tree, group_id, &new_id, orientation).unwrap(); + } else { + let data = tree.get_mut(group_id).unwrap().data_mut(); + let len = data.len(); + data.add_window(if matches!(direction, Direction::Left | Direction::Up) { + 0 + } else { + len + }); + } + if matches!(direction, Direction::Left | Direction::Up) { + tree.make_first_sibling(&new_id).unwrap(); + } else { + tree.make_last_sibling(&new_id).unwrap(); + } + *window.tiling_node_id.lock().unwrap() = Some(new_id); + window + } + Some(TargetZone::GroupInterior(group_id, idx)) if tree.get(group_id).is_ok() => { + let new_id = tree + .insert( + Node::new(Data::Mapped { + mapped: window.clone(), + last_geometry: Rectangle::from_size((100, 100).into()), + minimize_rect: None, + }), + InsertBehavior::UnderNode(group_id), + ) + .unwrap(); + let idx = { + let data = tree.get_mut(group_id).unwrap().data_mut(); + let bound_idx = data.len().min(*idx + 1); + data.add_window(bound_idx); + bound_idx + }; + tree.make_nth_sibling(&new_id, idx).unwrap(); + *window.tiling_node_id.lock().unwrap() = Some(new_id); + window + } + Some(TargetZone::InitialPlaceholder(node_id)) if tree.get(node_id).is_ok() => { + let data = tree.get_mut(node_id).unwrap().data_mut(); + let geo = *data.geometry(); + + *data = Data::Mapped { + mapped: window.clone(), + last_geometry: geo, + minimize_rect: None, + }; + *window.tiling_node_id.lock().unwrap() = Some(node_id.clone()); + window + } + Some(TargetZone::WindowSplit(window_id, direction)) if tree.get(window_id).is_ok() => { + let new_id = tree + .insert( + Node::new(Data::Mapped { + mapped: window.clone(), + last_geometry: Rectangle::from_size((100, 100).into()), + minimize_rect: None, + }), + InsertBehavior::UnderNode(window_id), + ) + .unwrap(); + let orientation = if matches!(direction, Direction::Left | Direction::Right) { + Orientation::Vertical + } else { + Orientation::Horizontal + }; + TilingLayout::new_group(&mut tree, window_id, &new_id, orientation).unwrap(); + if matches!(direction, Direction::Left | Direction::Up) { + tree.make_first_sibling(&new_id).unwrap(); + } + *window.tiling_node_id.lock().unwrap() = Some(new_id.clone()); + window + } + Some(TargetZone::WindowStack(window_id, _)) if tree.get(window_id).is_ok() => { + match tree.get_mut(window_id).unwrap().data_mut() { + Data::Mapped { mapped, .. } => { + mapped.convert_to_stack( + (&self.output, mapped.bbox()), + self.theme.clone(), + self.appearance, + ); + let Some(stack) = mapped.stack_ref() else { + unreachable!() + }; + for surface in window.windows().map(|s| s.0) { + stack.add_window(surface, None, None); + } + mapped.clone() + } + _ => unreachable!(), + } + } + _ => { + TilingLayout::map_to_tree( + &mut tree, + window.clone(), + &self.output, + None, + None, + None, + ); + window + } + }; + + if let Some(root) = tree.root_node_id() { + for id in tree + .traverse_pre_order_ids(root) + .unwrap() + .collect::>() + .into_iter() + { + match tree.get_mut(&id).map(|node| node.data_mut()) { + Ok(Data::Placeholder { .. }) => TilingLayout::unmap_internal(&mut tree, &id), + Ok(Data::Group { pill_indicator, .. }) if pill_indicator.is_some() => { + pill_indicator.take(); + } + _ => {} + } + } + } + + let blocker = TilingLayout::update_positions(&self.output, &mut tree, gaps); + self.queue.push_tree(tree, ANIMATION_DURATION, blocker); + + let location = self.element_geometry(&mapped).unwrap().loc; + (mapped, location) + } + + fn last_active_window<'a>( + tree: &Tree, + mut focus_stack: impl Iterator, + ) -> Option<(NodeId, CosmicMapped)> { + focus_stack.find_map(|target| { + tree.root_node_id().and_then(|root| { + tree.traverse_pre_order_ids(root).unwrap().find_map(|id| { + let Ok(Data::Mapped { mapped, .. }) = tree.get(&id).map(|n| n.data()) else { + return None; + }; + + if target == mapped { + Some((id, mapped.clone())) + } else { + None + } + }) + }) + }) + } + + fn currently_focused_node( + tree: &Tree, + mut target: KeyboardFocusTarget, + ) -> Option<(NodeId, FocusedNodeData)> { + // if the focus is currently on a popup, treat it's toplevel as the target + if let KeyboardFocusTarget::Popup(popup) = target { + let toplevel_surface = match popup { + PopupKind::Xdg(_) => get_popup_toplevel(&popup), + PopupKind::InputMethod(_) => unreachable!(), + }?; + let root_id = tree.root_node_id()?; + let node = + tree.traverse_pre_order(root_id) + .unwrap() + .find(|node| match node.data() { + Data::Mapped { mapped, .. } => mapped + .windows() + .any(|(w, _)| w.wl_surface().as_deref() == Some(&toplevel_surface)), + _ => false, + })?; + + target = KeyboardFocusTarget::Element(match node.data() { + Data::Mapped { mapped, .. } => mapped.clone(), + _ => unreachable!(), + }); + } + + match target { + KeyboardFocusTarget::Element(mapped) => { + let node_id = mapped.tiling_node_id.lock().unwrap().clone()?; + let node = tree.get(&node_id).ok()?; + let data = node.data(); + if data.is_mapped(Some(&mapped)) { + return Some((node_id, FocusedNodeData::Window(mapped))); + } + } + KeyboardFocusTarget::Group(window_group) => { + let node = tree.get(&window_group.node).ok()?; + if node.data().is_group() { + return Some(( + window_group.node, + FocusedNodeData::Group(window_group.focus_stack, window_group.alive), + )); + } + } + _ => {} + }; + + None + } + + fn new_group( + tree: &mut Tree, + old_id: &NodeId, + new_id: &NodeId, + orientation: Orientation, + ) -> Result { + let new_group = Node::new(Data::new_group( + orientation, + Rectangle::from_size((100, 100).into()), + )); + let old = tree.get(old_id)?; + let parent_id = old.parent().cloned(); + let pos = parent_id.as_ref().and_then(|parent_id| { + tree.children_ids(parent_id) + .unwrap() + .position(|id| id == old_id) + }); + + let group_id = tree + .insert( + new_group, + if let Some(parent) = parent_id.as_ref() { + InsertBehavior::UnderNode(parent) + } else { + InsertBehavior::AsRoot + }, + ) + .unwrap(); + + tree.move_node(old_id, MoveBehavior::ToParent(&group_id)) + .unwrap(); + // keep position + if let Some(old_pos) = pos { + tree.make_nth_sibling(&group_id, old_pos).unwrap(); + } + tree.move_node(new_id, MoveBehavior::ToParent(&group_id)) + .unwrap(); + + Ok(group_id) + } + + fn has_adjacent_node(tree: &Tree, node: &NodeId, direction: Direction) -> bool { + let mut search_node = node; + match tree.ancestor_ids(node) { + Ok(mut iter) => iter.any(|parent_id| { + let parent = tree.get(parent_id).unwrap(); + let children = parent.children(); + let idx = children.iter().position(|id| id == search_node).unwrap(); + search_node = parent_id; + + match direction { + Direction::Up => { + parent.data().orientation() == Orientation::Horizontal && idx > 0 + } + Direction::Down => { + parent.data().orientation() == Orientation::Horizontal + && idx < (children.len() - 1) + } + Direction::Left => { + parent.data().orientation() == Orientation::Vertical && idx > 0 + } + Direction::Right => { + parent.data().orientation() == Orientation::Vertical + && idx < (children.len() - 1) + } + } + }), + Err(_) => false, + } + } + + fn has_sibling_node(tree: &Tree, node: &NodeId, direction: Direction) -> bool { + match tree.get(node).ok().and_then(|node| node.parent()) { + Some(parent_id) => { + let parent = tree.get(parent_id).unwrap(); + let children = parent.children(); + let idx = children.iter().position(|id| id == node).unwrap(); + + match direction { + Direction::Up => { + parent.data().orientation() == Orientation::Horizontal && idx > 0 + } + Direction::Down => { + parent.data().orientation() == Orientation::Horizontal + && idx < (children.len() - 1) + } + Direction::Left => { + parent.data().orientation() == Orientation::Vertical && idx > 0 + } + Direction::Right => { + parent.data().orientation() == Orientation::Vertical + && idx < (children.len() - 1) + } + } + } + None => false, + } + } + + #[profiling::function] + fn update_positions( + output: &Output, + tree: &mut Tree, + gaps: (i32, i32), + ) -> Option { + if let Some(root_id) = tree.root_node_id() { + let mut configures = Vec::new(); + + let (outer, inner) = gaps; + let mut geo = layer_map_for_output(output).non_exclusive_zone().as_local(); + geo.loc.x += outer; + geo.loc.y += outer; + geo.size.w -= outer * 2; + geo.size.h -= outer * 2; + let mut stack = vec![geo]; + + for node_id in tree + .traverse_pre_order_ids(root_id) + .unwrap() + .collect::>() + .into_iter() + { + let node = tree.get_mut(&node_id).unwrap(); + let data = node.data_mut(); + + // flatten tree + if data.is_group() && data.len() == 1 { + // RemoveBehavior::LiftChildren sadly does not what we want: lifting them into the same place. + // So we need to fix that manually.. + let idx = node.parent().cloned().map(|parent_id| { + tree.children_ids(&parent_id) + .unwrap() + .position(|id| id == &node_id) + .unwrap() + }); + let child_id = tree + .children_ids(&node_id) + .unwrap() + .next() + .cloned() + .unwrap(); + tree.remove_node(node_id, RemoveBehavior::LiftChildren) + .unwrap(); + if let Some(idx) = idx { + tree.make_nth_sibling(&child_id, idx).unwrap(); + } else { + // additionally `RemoveBehavior::LiftChildren` doesn't work, when removing the root-node, + // even with just one child. *sigh* + tree.move_node(&child_id, MoveBehavior::ToRoot).unwrap(); + } + + continue; + } + + if let Some(mut geo) = stack.pop() { + let node = tree.get(&node_id).unwrap(); + let data = node.data(); + if data.is_mapped(None) { + let gap = ( + ( + if TilingLayout::has_adjacent_node(tree, &node_id, Direction::Left) + { + inner / 2 + } else { + inner + }, + if TilingLayout::has_adjacent_node(tree, &node_id, Direction::Up) { + inner / 2 + } else { + inner + }, + ), + ( + if TilingLayout::has_adjacent_node(tree, &node_id, Direction::Right) + { + inner / 2 + } else { + inner + }, + if TilingLayout::has_adjacent_node(tree, &node_id, Direction::Down) + { + inner / 2 + } else { + inner + }, + ), + ); + geo.loc += gap.0.into(); + geo.size -= gap.0.into(); + geo.size -= gap.1.into(); + } + + let node = tree.get_mut(&node_id).unwrap(); + let data = node.data_mut(); + data.update_geometry(geo); + + match data { + Data::Group { + orientation, sizes, .. + } => match orientation { + Orientation::Horizontal => { + let mut previous: i32 = sizes.iter().sum(); + for size in sizes.iter().rev() { + previous -= *size; + stack.push(Rectangle::new( + (geo.loc.x, geo.loc.y + previous).into(), + (geo.size.w, *size).into(), + )); + } + } + Orientation::Vertical => { + let mut previous: i32 = sizes.iter().sum(); + for size in sizes.iter().rev() { + previous -= *size; + stack.push(Rectangle::new( + (geo.loc.x + previous, geo.loc.y).into(), + (*size, geo.size.h).into(), + )); + } + } + }, + Data::Mapped { mapped, .. } => { + if !(mapped.is_fullscreen(true) || mapped.is_maximized(true)) { + mapped.set_tiled(true); + let internal_geometry = geo.to_global(output); + mapped.set_geometry(internal_geometry); + if let Some(serial) = mapped.configure() { + configures.push((mapped.active_window(), serial)); + } + } + } + Data::Placeholder { .. } => {} + } + } + } + + if !configures.is_empty() { + let blocker = TilingBlocker::new(configures); + for (surface, _) in &blocker.necessary_acks { + if let Some(surface) = surface.wl_surface() { + add_blocker(&surface, blocker.clone()); + } + } + return Some(blocker); + } + } + + None + } + + pub fn popup_element_under( + &self, + location_f64: Point, + seat: &Seat, + ) -> Option { + let location = location_f64.to_i32_floor(); + + for (mapped, geo) in self.mapped() { + if !mapped.bbox().contains((location - geo.loc).as_logical()) { + continue; + } + + if mapped + .focus_under( + (location_f64 - geo.loc.to_f64()).as_logical() + mapped.geometry().loc.to_f64(), + WindowSurfaceType::POPUP | WindowSurfaceType::SUBSURFACE, + seat, + ) + .is_some() + { + return Some(mapped.clone().into()); + } + } + + None + } + + pub fn toplevel_element_under( + &self, + location_f64: Point, + seat: &Seat, + ) -> Option { + let location = location_f64.to_i32_floor(); + + for (mapped, geo) in self.mapped() { + // Tiled windows are rendered cropped to their tile (`geo`), so input must be bound to the tile as well + if !geo.contains(location) { + continue; + } + if !mapped.bbox().contains((location - geo.loc).as_logical()) { + continue; + } + + if mapped + .focus_under( + (location_f64 - geo.loc.to_f64()).as_logical() + mapped.geometry().loc.to_f64(), + WindowSurfaceType::TOPLEVEL | WindowSurfaceType::SUBSURFACE, + seat, + ) + .is_some() + { + return Some(mapped.clone().into()); + } + } + + None + } + + pub fn popup_surface_under( + &self, + location_f64: Point, + overview: OverviewMode, + seat: &Seat, + ) -> Option<(PointerFocusTarget, Point)> { + let location = location_f64.to_i32_floor(); + + if matches!(overview, OverviewMode::None) { + for (mapped, geo) in self.mapped() { + if !mapped.bbox().contains((location - geo.loc).as_logical()) { + continue; + } + if mapped.is_maximized(false) { + continue; + } + if let Some((target, surface_offset)) = mapped.focus_under( + (location_f64 - geo.loc.to_f64()).as_logical() + mapped.geometry().loc.to_f64(), + WindowSurfaceType::POPUP | WindowSurfaceType::SUBSURFACE, + seat, + ) { + return Some(( + target, + geo.loc.to_f64() - mapped.geometry().loc.as_local().to_f64() + + surface_offset.as_local(), + )); + } + } + } + + None + } + + pub fn toplevel_surface_under( + &self, + location_f64: Point, + overview: OverviewMode, + seat: &Seat, + ) -> Option<(PointerFocusTarget, Point)> { + let tree = &self.queue.trees.back().unwrap().0; + let root = tree.root_node_id()?; + let location = location_f64.to_i32_floor(); + + if matches!(overview, OverviewMode::None) { + for (mapped, geo) in self.mapped() { + // Tiled windows are rendered cropped to their tile (`geo`), so input must be bound to the tile as well + if !geo.contains(location) { + continue; + } + if !mapped.bbox().contains((location - geo.loc).as_logical()) { + continue; + } + if mapped.is_maximized(false) { + continue; + } + if let Some((target, surface_offset)) = mapped.focus_under( + (location_f64 - geo.loc.to_f64()).as_logical() + mapped.geometry().loc.to_f64(), + WindowSurfaceType::TOPLEVEL | WindowSurfaceType::SUBSURFACE, + seat, + ) { + return Some(( + target, + geo.loc.to_f64() - mapped.geometry().loc.as_local().to_f64() + + surface_offset.as_local(), + )); + } + } + + let mut result = None; + let mut lookup = Some(root.clone()); + while let Some(node) = lookup { + let data = tree.get(&node).unwrap().data(); + if data.geometry().contains(location) { + result = Some(node.clone()); + } + + lookup = None; + if result.is_some() && data.is_group() { + for child_id in tree.children_ids(&node).unwrap() { + if tree + .get(child_id) + .unwrap() + .data() + .geometry() + .contains(location) + { + lookup = Some(child_id.clone()); + break; + } + } + } + } + + match result.map(|id| (id.clone(), tree.get(&id).unwrap().data().clone())) { + Some(( + _, + Data::Mapped { + mapped, + last_geometry, + .. + }, + )) => { + let test_point = (location_f64 - last_geometry.loc.to_f64() + + mapped.geometry().loc.to_f64().as_local()) + .as_logical(); + mapped + .focus_under( + test_point, + WindowSurfaceType::TOPLEVEL | WindowSurfaceType::SUBSURFACE, + seat, + ) + .map(|(surface, surface_offset)| { + ( + surface, + last_geometry.loc.to_f64() + - mapped.geometry().loc.as_local().to_f64() + + surface_offset.as_local(), + ) + }) + } + Some(( + id, + Data::Group { + orientation, + last_geometry, + .. + }, + )) => { + let idx = tree + .children(&id) + .unwrap() + .position(|node| { + let data = node.data(); + match orientation { + Orientation::Vertical => location.x < data.geometry().loc.x, + Orientation::Horizontal => location.y < data.geometry().loc.y, + } + }) + .and_then(|x| x.checked_sub(1))?; + Some(( + ResizeForkTarget { + node: id.clone(), + output: self.output.downgrade(), + left_up_idx: idx, + orientation, + } + .into(), + (last_geometry.loc + + tree + .children(&id) + .unwrap() + .nth(idx) + .map(|node| { + let geo = node.data().geometry(); + geo.loc + geo.size + }) + .unwrap()) + .to_f64(), + )) + } + _ => None, + } + } else { + None + } + } + + pub fn update_pointer_position( + &mut self, + location_f64: Option>, + overview: OverviewMode, + ) { + let gaps = self.gaps(); + let last_overview_hover = &mut self.last_overview_hover; + let tree = &self.queue.trees.back().unwrap().0; + let Some(root) = tree.root_node_id() else { + if matches!( + overview.active_trigger(), + Some(Trigger::Pointer(_) | Trigger::Touch(_)) + ) && location_f64.is_some() + { + let mut tree = tree.copy_clone(); + tree.insert( + Node::new(Data::Placeholder { + id: Id::new(), + last_geometry: Rectangle::from_size((100, 100).into()), + type_: PlaceholderType::DropZone, + }), + InsertBehavior::AsRoot, + ) + .unwrap(); + let blocker = TilingLayout::update_positions(&self.output, &mut tree, gaps); + self.queue.push_tree(tree, ANIMATION_DURATION, blocker); + } + return; + }; + + if !matches!( + overview, + OverviewMode::Started(_, _) | OverviewMode::Active(_) + ) || location_f64.is_none() + { + last_overview_hover.take(); + return; + } + + let location_f64 = location_f64.unwrap(); + let location = location_f64.to_i32_floor(); + + if matches!( + overview.active_trigger(), + Some(Trigger::Pointer(_) | Trigger::Touch(_)) + ) { + let non_exclusive_zone = layer_map_for_output(&self.output) + .non_exclusive_zone() + .as_local(); + let geometries = geometries_for_groupview( + tree, + Option::<&mut GlowRenderer>::None, + non_exclusive_zone, + None, + self.output.current_scale().fractional_scale(), + 1.0, + overview.alpha().unwrap(), + &self.backdrop_id, + Some(None), + None, + None, + self.theme.cosmic(), + &mut |_| {}, + ); + + let mut result = None; + let mut lookup = Some(root.clone()); + while let Some(node) = lookup { + let data = tree.get(&node).unwrap().data(); + if geometries + .get(&node) + .map(|geo| geo.contains(location)) + .unwrap_or(false) + { + result = Some(node.clone()); + } + + lookup = None; + if result.is_some() && data.is_group() { + if tree.children(&node).unwrap().any(|child| { + matches!( + child.data(), + Data::Placeholder { + type_: PlaceholderType::DropZone, + .. + } + ) + }) { + break; + } + for child_id in tree.children_ids(&node).unwrap() { + if geometries + .get(child_id) + .map(|geo| geo.contains(location)) + .unwrap_or(false) + { + lookup = Some(child_id.clone()); + break; + } + } + } + } + + if let Some(res_id) = result { + let Some(mut last_geometry) = geometries.get(&res_id).copied() else { + return; + }; + let node = tree.get(&res_id).unwrap(); + let data = node.data().clone(); + + let group_zone = if let Data::Group { orientation, .. } = &data { + if node.children().iter().any(|child_id| { + tree.get(child_id) + .ok() + .map(|child| { + matches!( + child.data(), + Data::Placeholder { + type_: PlaceholderType::DropZone, + .. + } + ) + }) + .unwrap_or(false) + }) { + None + } else { + let left_edge = match &*last_overview_hover { + Some((_, TargetZone::GroupEdge(id, Direction::Left))) + if *id == res_id => + { + let zone = Rectangle::new( + last_geometry.loc, + (80, last_geometry.size.h).into(), + ); + last_geometry.loc.x += 80; + last_geometry.size.w -= 80; + zone + } + _ => { + let zone = Rectangle::new( + last_geometry.loc, + (32, last_geometry.size.h).into(), + ); + last_geometry.loc.x += 32; + last_geometry.size.w -= 32; + zone + } + }; + let top_edge = match &*last_overview_hover { + Some((_, TargetZone::GroupEdge(id, Direction::Up))) + if *id == res_id => + { + let zone = Rectangle::new( + last_geometry.loc, + (last_geometry.size.w, 80).into(), + ); + last_geometry.loc.y += 80; + last_geometry.size.h -= 80; + zone + } + _ => { + let zone = Rectangle::new( + last_geometry.loc, + (last_geometry.size.w, 32).into(), + ); + last_geometry.loc.y += 32; + last_geometry.size.h -= 32; + zone + } + }; + let right_edge = match &*last_overview_hover { + Some((_, TargetZone::GroupEdge(id, Direction::Right))) + if *id == res_id => + { + let zone = Rectangle::new( + ( + last_geometry.loc.x + last_geometry.size.w - 80, + last_geometry.loc.y, + ) + .into(), + (80, last_geometry.size.h).into(), + ); + last_geometry.size.w -= 80; + zone + } + _ => { + let zone = Rectangle::new( + ( + last_geometry.loc.x + last_geometry.size.w - 32, + last_geometry.loc.y, + ) + .into(), + (32, last_geometry.size.h).into(), + ); + last_geometry.size.w -= 32; + zone + } + }; + let bottom_edge = match &*last_overview_hover { + Some((_, TargetZone::GroupEdge(id, Direction::Down))) + if *id == res_id => + { + let zone = Rectangle::new( + ( + last_geometry.loc.x, + last_geometry.loc.y + last_geometry.size.h - 80, + ) + .into(), + (last_geometry.size.w, 80).into(), + ); + last_geometry.size.h -= 80; + zone + } + _ => { + let zone = Rectangle::new( + ( + last_geometry.loc.x, + last_geometry.loc.y + last_geometry.size.h - 32, + ) + .into(), + (last_geometry.size.w, 32).into(), + ); + last_geometry.size.h -= 32; + zone + } + }; + + if left_edge.contains(location) { + Some(TargetZone::GroupEdge(res_id.clone(), Direction::Left)) + } else if right_edge.contains(location) { + Some(TargetZone::GroupEdge(res_id.clone(), Direction::Right)) + } else if top_edge.contains(location) { + Some(TargetZone::GroupEdge(res_id.clone(), Direction::Up)) + } else if bottom_edge.contains(location) { + Some(TargetZone::GroupEdge(res_id.clone(), Direction::Down)) + } else { + let idx = tree + .children_ids(&res_id) + .unwrap() + .position(|node| { + let Some(geo) = geometries.get(node) else { + return false; + }; + match orientation { + Orientation::Vertical => location.x < geo.loc.x, + Orientation::Horizontal => location.y < geo.loc.y, + } + }) + .and_then(|x| x.checked_sub(1)) + .unwrap_or(0); + Some(TargetZone::GroupInterior(res_id.clone(), idx)) + } + } + } else { + None + }; + + let target_zone = group_zone.unwrap_or_else(|| match &data { + Data::Placeholder { .. } => TargetZone::InitialPlaceholder(res_id), + Data::Group { .. } | Data::Mapped { .. } => { + let id = if data.is_group() { + tree.get(&res_id) + .unwrap() + .children() + .iter() + .find(|child_id| tree.get(child_id).unwrap().data().is_mapped(None)) + .expect("Placeholder group without real window?") + .clone() + } else { + res_id + }; + + let third_width = (last_geometry.size.w as f64 / 3.0).round() as i32; + let third_height = (last_geometry.size.h as f64 / 3.0).round() as i32; + let stack_region = Rectangle::from_extremities( + ( + last_geometry.loc.x + third_width, + last_geometry.loc.y + third_height, + ), + ( + last_geometry.loc.x + 2 * third_width, + last_geometry.loc.y + 2 * third_height, + ), + ); + + if stack_region.contains(location) { + TargetZone::WindowStack(id, last_geometry) + } else { + let left_right = { + let relative_loc = location_f64.x - last_geometry.loc.x as f64; + if relative_loc < last_geometry.size.w as f64 / 2.0 { + (Direction::Left, relative_loc / last_geometry.size.w as f64) + } else { + ( + Direction::Right, + 1.0 - (relative_loc / last_geometry.size.w as f64), + ) + } + }; + let up_down = { + let relative_loc = location_f64.y - last_geometry.loc.y as f64; + if relative_loc < last_geometry.size.h as f64 / 2.0 { + (Direction::Up, relative_loc / last_geometry.size.h as f64) + } else { + ( + Direction::Down, + 1.0 - (relative_loc / last_geometry.size.h as f64), + ) + } + }; + + let direction = if left_right.1 < up_down.1 { + left_right.0 + } else { + up_down.0 + }; + + TargetZone::WindowSplit(id, direction) + } + } + }); + + match &mut *last_overview_hover { + last_overview_hover @ None => { + *last_overview_hover = Some(( + None, + tree.traverse_pre_order_ids(root) + .unwrap() + .find(|id| { + matches!( + tree.get(id).unwrap().data(), + Data::Placeholder { + type_: PlaceholderType::GrabbedWindow, + .. + } + ) + }) + .map(TargetZone::InitialPlaceholder) + .unwrap_or(TargetZone::Initial), + )); + } + Some((instant, old_target_zone)) => { + if *old_target_zone != target_zone { + let overdue = if let Some(instant) = instant { + match old_target_zone { + TargetZone::InitialPlaceholder(_) => { + Instant::now().duration_since(*instant) + > INITIAL_MOUSE_ANIMATION_DELAY + } + _ => { + Instant::now().duration_since(*instant) + > MOUSE_ANIMATION_DELAY + } + } + } else { + *instant = Some(Instant::now()); + false + }; + + if overdue { + let duration = if target_zone.is_window_zone() + && !old_target_zone.is_window_zone() + { + ANIMATION_DURATION * 2 + } else { + ANIMATION_DURATION + }; + + let mut tree = tree.copy_clone(); + + // remove old placeholders + let removed = if let TargetZone::InitialPlaceholder(node_id) = + old_target_zone + { + if tree.get(node_id).is_ok() { + TilingLayout::unmap_internal(&mut tree, node_id); + } + true + } else if let TargetZone::WindowSplit(node_id, _) = old_target_zone + { + if let Some(children) = tree + .get(node_id) + .ok() + .and_then(|node| node.parent()) + .and_then(|parent_id| tree.get(parent_id).ok()) + .map(|node| node.children().clone()) + { + for id in children { + let matches = matches!( + tree.get(&id).unwrap().data(), + Data::Placeholder { + type_: PlaceholderType::DropZone, + .. + } + ); + + if matches { + TilingLayout::unmap_internal(&mut tree, &id); + break; + } + } + } + true + } else if let TargetZone::GroupEdge(node_id, _) = old_target_zone { + if let Ok(node) = tree.get_mut(node_id) { + match node.data_mut() { + Data::Group { pill_indicator, .. } => { + *pill_indicator = None; + } + _ => unreachable!(), + } + } + true + } else if let TargetZone::GroupInterior(node_id, _) = + old_target_zone + { + if let Ok(node) = tree.get_mut(node_id) { + match node.data_mut() { + Data::Group { pill_indicator, .. } => { + *pill_indicator = None; + } + _ => unreachable!(), + } + } + true + } else { + false + }; + + // add placeholders + let added = if let TargetZone::WindowSplit(node_id, dir) = + &target_zone + { + let id = tree + .insert( + Node::new(Data::Placeholder { + id: Id::new(), + last_geometry: Rectangle::from_size( + (100, 100).into(), + ), + type_: PlaceholderType::DropZone, + }), + InsertBehavior::UnderNode(node_id), + ) + .unwrap(); + let orientation = + if matches!(dir, Direction::Left | Direction::Right) { + Orientation::Vertical + } else { + Orientation::Horizontal + }; + TilingLayout::new_group(&mut tree, node_id, &id, orientation) + .unwrap(); + if matches!(dir, Direction::Left | Direction::Up) { + tree.make_first_sibling(&id).unwrap(); + } + + true + } else if let TargetZone::GroupEdge(node_id, direction) = + &target_zone + { + if let Ok(node) = tree.get_mut(node_id) { + match node.data_mut() { + Data::Group { pill_indicator, .. } => { + *pill_indicator = + Some(PillIndicator::Outer(*direction)); + } + _ => unreachable!(), + } + true + } else { + false + } + } else if let TargetZone::GroupInterior(node_id, idx) = &target_zone + { + if let Ok(node) = tree.get_mut(node_id) { + match node.data_mut() { + Data::Group { pill_indicator, .. } => { + *pill_indicator = Some(PillIndicator::Inner(*idx)); + } + _ => unreachable!(), + } + true + } else { + false + } + } else { + false + }; + + if removed || added { + let blocker = TilingLayout::update_positions( + &self.output, + &mut tree, + gaps, + ); + self.queue.push_tree(tree, duration, blocker); + } + + *instant = None; + *old_target_zone = target_zone; + } + } else { + *instant = None; + } + } + } + } + } + } + + pub fn mapped(&self) -> impl Iterator)> { + let tree = &self.queue.trees.back().unwrap().0; + let iter = tree.root_node_id().map(|root| { + tree.traverse_pre_order(root) + .unwrap() + .filter(|node| node.data().is_mapped(None)) + .filter(|node| match node.data() { + Data::Mapped { mapped, .. } => mapped.is_activated(false), + _ => unreachable!(), + }) + .map(|node| match node.data() { + Data::Mapped { + mapped, + last_geometry, + .. + } => (mapped, *last_geometry), + _ => unreachable!(), + }) + .chain( + tree.traverse_pre_order(root) + .unwrap() + .filter(|node| node.data().is_mapped(None)) + .filter(|node| match node.data() { + Data::Mapped { mapped, .. } => !mapped.is_activated(false), + _ => unreachable!(), + }) + .map(|node| match node.data() { + Data::Mapped { + mapped, + last_geometry, + .. + } => (mapped, *last_geometry), + _ => unreachable!(), + }), + ) + }); + iter.into_iter().flatten() + } + + pub fn windows(&self) -> impl Iterator)> + '_ { + self.mapped().flat_map(|(mapped, geo)| { + mapped.windows().map(move |(w, p)| { + (w, { + let mut geo = geo; + geo.loc += p.as_local(); + geo.size -= p.to_size().as_local(); + geo + }) + }) + }) + } + + pub fn element_for_node(&self, node: &NodeId) -> Option<&CosmicMapped> { + let tree = &self.queue.trees.back().unwrap().0; + tree.get(node).ok().and_then(|node| match node.data() { + Data::Mapped { mapped, .. } => Some(mapped), + _ => None, + }) + } + + pub fn has_node(&self, node: &NodeId) -> bool { + let tree = &self.queue.trees.back().unwrap().0; + tree.root_node_id() + .map(|root| { + tree.traverse_pre_order_ids(root) + .unwrap() + .any(|id| &id == node) + }) + .unwrap_or(false) + } + + pub fn merge(&mut self, mut other: TilingLayout) { + let gaps = self.gaps(); + + let src = other.queue.trees.pop_back().unwrap().0; + let mut dst = self.queue.trees.back().unwrap().0.copy_clone(); + + let orientation = match self.output.geometry().size { + x if x.w >= x.h => Orientation::Vertical, + _ => Orientation::Horizontal, + }; + TilingLayout::merge_trees(src, &mut dst, orientation); + + let blocker = TilingLayout::update_positions(&self.output, &mut dst, gaps); + self.queue.push_tree(dst, ANIMATION_DURATION, blocker); + } + + fn merge_trees(src: Tree, dst: &mut Tree, orientation: Orientation) { + if let Some(dst_root_id) = dst.root_node_id().cloned() { + let mut stack = Vec::new(); + + if let Some(src_root_id) = src.root_node_id() { + let root_node = src.get(src_root_id).unwrap(); + let new_node = Node::new(root_node.data().clone()); + let new_id = dst + .insert(new_node, InsertBehavior::UnderNode(&dst_root_id)) + .unwrap(); + if let &mut Data::Mapped { ref mut mapped, .. } = + dst.get_mut(&new_id).unwrap().data_mut() + { + *mapped.tiling_node_id.lock().unwrap() = Some(new_id.clone()); + } + TilingLayout::new_group(dst, &dst_root_id, &new_id, orientation).unwrap(); + stack.push((src_root_id.clone(), new_id)); + } + + while let Some((src_id, dst_id)) = stack.pop() { + for child_id in src.children_ids(&src_id).unwrap() { + let src_node = src.get(child_id).unwrap(); + let new_node = Node::new(src_node.data().clone()); + let new_child_id = dst + .insert(new_node, InsertBehavior::UnderNode(&dst_id)) + .unwrap(); + if let &mut Data::Mapped { ref mut mapped, .. } = + dst.get_mut(&new_child_id).unwrap().data_mut() + { + *mapped.tiling_node_id.lock().unwrap() = Some(new_child_id.clone()); + } + stack.push((child_id.clone(), new_child_id)); + } + } + } else { + *dst = src; + } + } + + #[profiling::function] + pub fn render( + &self, + renderer: &mut R, + seat: Option<&Seat>, + focused: Option<&CosmicMapped>, + non_exclusive_zone: Rectangle, + overview: (OverviewMode, Option<(SwapIndicator, Option<&Tree>)>), + resize_indicator: Option<(ResizeMode, ResizeIndicator)>, + indicator_thickness: u8, + theme: &cosmic::theme::CosmicTheme, + scanout_node: Option, + push: &mut dyn FnMut(CosmicMappedRenderElement), + ) where + R: AsGlowRenderer, + R::TextureId: Send + Clone + 'static, + CosmicMappedRenderElement: RenderElement, + CosmicWindowRenderElement: RenderElement, + CosmicStackRenderElement: RenderElement, + { + let output_scale = self.output.current_scale().fractional_scale(); + + let (target_tree, duration, _) = if self.queue.animation_start.is_some() { + self.queue + .trees + .get(1) + .expect("Animation ongoing, should have two trees") + } else { + self.queue.trees.front().unwrap() + }; + let reference_tree = self + .queue + .animation_start + .is_some() + .then(|| &self.queue.trees.front().unwrap().0); + + let percentage = if let Some(animation_start) = self.queue.animation_start { + if *duration == Duration::ZERO { + 1.0 + } else { + let now = Instant::now(); + let total = duration.as_millis() as f32; + let since = now.duration_since(animation_start).as_millis() as f32; + let percentage = since / total; + debug_assert!(!percentage.is_nan()); + debug_assert!(!percentage.is_infinite()); + ease(EaseInOutCubic, 0.0, 1.0, percentage) + } + } else { + 1.0 + }; + let draw_groups = overview.0.alpha(); + + let is_overview = !matches!(overview.0, OverviewMode::None); + let is_mouse_tiling = (matches!(overview.0.trigger(), Some(Trigger::Pointer(_)))) + .then(|| self.last_overview_hover.as_ref().map(|(_, zone)| zone)); + let swap_desc = if let Some(Trigger::KeyboardSwap(_, desc)) = overview.0.trigger() { + Some(desc.clone()) + } else { + None + }; + + // all gone windows and fade them out + let old_geometries = if let Some(reference_tree) = reference_tree.as_ref() { + let geometries = if let Some(transition) = draw_groups { + Some(geometries_for_groupview( + reference_tree, + &mut *renderer, + non_exclusive_zone, + seat, // TODO: Would be better to be an old focus, + // but for that we have to associate focus with a tree (and animate focus changes properly) + output_scale, + 1.0 - transition, + transition, + &self.backdrop_id, + is_mouse_tiling, + swap_desc.clone(), + overview.1.as_ref().and_then(|(_, tree)| *tree), + theme, + &mut |_elem| {}, + )) + } else { + None + }; + + // all old windows we want to fade out + render_old_tree_windows( + reference_tree, + target_tree, + renderer, + geometries.clone(), + output_scale, + percentage, + indicator_thickness, + swap_desc.is_some(), + theme, + scanout_node, + push, + ); + + geometries + } else { + None + }; + + let mut group_elements = SmallVec::<[_; 4]>::new_const(); + let geometries = if let Some(transition) = draw_groups { + Some(geometries_for_groupview( + target_tree, + &mut *renderer, + non_exclusive_zone, + seat, + output_scale, + transition, + transition, + &self.backdrop_id, + is_mouse_tiling, + swap_desc.clone(), + overview.1.as_ref().and_then(|(_, tree)| *tree), + theme, + &mut |elem| group_elements.push(elem), + )) + } else { + None + }; + + // all alive windows + render_new_tree_windows( + target_tree, + reference_tree, + renderer, + non_exclusive_zone, + geometries, + old_geometries, + is_overview, + seat, + focused, + &self.output, + percentage, + draw_groups, + if let Some(transition) = draw_groups { + let diff = (4u8.abs_diff(indicator_thickness) as f32 * transition).round() as u8; + if 3 > indicator_thickness { + indicator_thickness + diff + } else { + indicator_thickness - diff + } + } else { + indicator_thickness + }, + overview, + resize_indicator, + swap_desc.clone(), + &self.swapping_stack_surface_id, + &self.backdrop_id, + theme, + scanout_node, + push, + ); + + // tiling hints + for elem in group_elements.into_iter() { + push(elem); + } + } + + #[profiling::function] + pub fn render_popups( + &self, + renderer: &mut R, + seat: Option<&Seat>, + non_exclusive_zone: Rectangle, + overview: (OverviewMode, Option<(SwapIndicator, Option<&Tree>)>), + theme: &cosmic::theme::CosmicTheme, + scanout_node: Option, + push: &mut dyn FnMut(CosmicMappedRenderElement), + ) where + R: AsGlowRenderer, + R::TextureId: Send + Clone + 'static, + CosmicMappedRenderElement: RenderElement, + CosmicWindowRenderElement: RenderElement, + CosmicStackRenderElement: RenderElement, + { + let output_scale = self.output.current_scale().fractional_scale(); + + let (target_tree, duration, _) = if self.queue.animation_start.is_some() { + self.queue + .trees + .get(1) + .expect("Animation ongoing, should have two trees") + } else { + self.queue.trees.front().unwrap() + }; + let reference_tree = self + .queue + .animation_start + .is_some() + .then(|| &self.queue.trees.front().unwrap().0); + + let percentage = if let Some(animation_start) = self.queue.animation_start { + let percentage = Instant::now().duration_since(animation_start).as_millis() as f32 + / duration.as_millis() as f32; + ease(EaseInOutCubic, 0.0, 1.0, percentage) + } else { + 1.0 + }; + let draw_groups = overview.0.alpha(); + + let is_mouse_tiling = (matches!(overview.0.trigger(), Some(Trigger::Pointer(_)))) + .then(|| self.last_overview_hover.as_ref().map(|(_, zone)| zone)); + let swap_desc = if let Some(Trigger::KeyboardSwap(_, desc)) = overview.0.trigger() { + Some(desc.clone()) + } else { + None + }; + + // all gone windows and fade them out + let old_geometries = if let Some(reference_tree) = reference_tree.as_ref() { + let geometries = if let Some(transition) = draw_groups { + Some(geometries_for_groupview( + reference_tree, + &mut *renderer, + non_exclusive_zone, + seat, // TODO: Would be better to be an old focus, + // but for that we have to associate focus with a tree (and animate focus changes properly) + output_scale, + 1.0 - transition, + transition, + &self.backdrop_id, + is_mouse_tiling, + swap_desc.clone(), + overview.1.as_ref().and_then(|(_, tree)| *tree), + theme, + &mut |_| {}, + )) + } else { + None + }; + + // all old windows we want to fade out + render_old_tree_popups( + reference_tree, + target_tree, + renderer, + geometries.clone(), + output_scale, + percentage, + swap_desc.is_some(), + scanout_node, + push, + ); + + geometries + } else { + None + }; + + let geometries = if let Some(transition) = draw_groups { + Some(geometries_for_groupview( + target_tree, + &mut *renderer, + non_exclusive_zone, + seat, + output_scale, + transition, + transition, + &self.backdrop_id, + is_mouse_tiling, + swap_desc.clone(), + overview.1.as_ref().and_then(|(_, tree)| *tree), + theme, + &mut |_| {}, + )) + } else { + None + }; + + // all alive windows + render_new_tree_popups( + target_tree, + reference_tree, + renderer, + geometries, + old_geometries, + seat, + &self.output, + percentage, + overview, + swap_desc.clone(), + scanout_node, + push, + ); + } + + fn gaps(&self) -> (i32, i32) { + let g = self.theme.cosmic().gaps; + (g.0 as i32, g.1 as i32) + } +} + +const GAP_KEYBOARD: i32 = 8; +const GAP_MOUSE: i32 = 32; +const PLACEHOLDER_GAP_MOUSE: i32 = 8; +const WINDOW_BACKDROP_BORDER: i32 = 4; +const WINDOW_BACKDROP_GAP: i32 = 12; + +const MAX_SWAP_WINDOW_SIZE: (i32, i32) = (360, 240); + +fn swap_factor(size: Size) -> f64 { + let target_w = std::cmp::min(size.w, MAX_SWAP_WINDOW_SIZE.0); + let target_h = std::cmp::min(size.h, MAX_SWAP_WINDOW_SIZE.1); + (target_w as f64 / size.w as f64).min(target_h as f64 / size.h as f64) +} + +fn swap_geometry( + size: Size, + relative_to: Rectangle, +) -> Rectangle { + let factor = swap_factor(size); + + let new_size = Size::from(( + (size.w as f64 * factor).round() as i32, + (size.h as f64 * factor).round() as i32, + )); + + let loc = Point::from(( + relative_to.loc.x + relative_to.size.w - new_size.w, + relative_to.loc.y, + )); + + Rectangle::new(loc, new_size) +} + +fn geometries_for_groupview<'a, R>( + tree: &Tree, + renderer: impl Into>, + non_exclusive_zone: Rectangle, + seat: Option<&Seat>, + scale: f64, + alpha: f32, + transition: f32, + backdrop_id: &Id, + mouse_tiling: Option>, + swap_desc: Option, + swap_tree: Option<&Tree>, + _theme: &cosmic::theme::CosmicTheme, + push: &mut dyn FnMut(CosmicMappedRenderElement), +) -> HashMap> +where + R: AsGlowRenderer + 'a, + R::TextureId: 'static, + CosmicMappedRenderElement: RenderElement, + CosmicWindowRenderElement: RenderElement, +{ + // we need to recalculate geometry for all elements, if we are drawing groups + let gap: i32 = (if mouse_tiling.is_some() { + GAP_MOUSE + } else { + GAP_KEYBOARD + } as f32 + * transition) + .round() as i32; + let mut renderer = renderer.into(); + + let root = tree.root_node_id(); + let mut stack = Vec::new(); + if swap_tree.is_some() { + // push bogos value, that will get ignored anyway + stack.push((Rectangle::from_size((320, 240).into()), 0)); + } + + let has_root = root.is_some(); + if has_root { + stack.push((non_exclusive_zone, 0)); + } + + let mut push = |elem| { + if has_root { + push(elem) + } + }; + + let mut geometries: HashMap> = HashMap::new(); + let alpha = alpha * transition; + + let focused = seat + .and_then(|seat| { + seat.get_keyboard() + .unwrap() + .current_focus() + .and_then(|target| TilingLayout::currently_focused_node(tree, target)) + }) + .map(|(id, _)| id); + let focused_geo = focused + .as_ref() + .map(|focused_id| *tree.get(focused_id).unwrap().data().geometry()); + + let has_potential_groups = if let Some(focused_id) = focused.as_ref() { + let focused_node = tree.get(focused_id).unwrap(); + if let Some(parent) = focused_node.parent() { + let parent_node = tree.get(parent).unwrap(); + parent_node.children().len() > 2 + } else { + false + } + } else { + false + }; + + for (tree, node_id) in root + .into_iter() + .flat_map(|root| tree.traverse_pre_order_ids(root).unwrap()) + .map(|id| (tree, id)) + .chain( + swap_tree + .as_ref() + .filter(|_| swap_desc.as_ref().unwrap().stack_window.is_none()) + .into_iter() + .flat_map(|tree| { + tree.traverse_pre_order_ids(&swap_desc.as_ref().unwrap().node) + .unwrap() + }) + .map(|id| (*swap_tree.as_ref().unwrap(), id)), + ) + { + if let Some((mut geo, depth)) = stack.pop() { + let node: &Node = tree.get(&node_id).unwrap(); + let data = node.data(); + + let is_placeholder_sibling = node + .parent() + .and_then(|parent_id| tree.children_ids(parent_id).ok()) + .map(|mut siblings| { + siblings.any(|child_id| tree.get(child_id).unwrap().data().is_placeholder()) + }) + .unwrap_or(false); + + let render_potential_group = swap_desc.is_none() + && has_potential_groups + && (if let Some(focused_id) = focused.as_ref() { + // `focused` can move into us directly + if let Some(parent) = node.parent() { + let parent_data = tree.get(parent).unwrap().data(); + + let idx = tree + .children_ids(parent) + .unwrap() + .position(|id| id == &node_id) + .unwrap(); + if let Some(focused_idx) = tree + .children_ids(parent) + .unwrap() + .position(|id| id == focused_id) + { + // only direct neighbors + focused_idx.abs_diff(idx) == 1 + // skip neighbors, if this is a group of two + && parent_data.len() > 2 + } else { + false + } + } else { + false + } + } else { + false + }); + + let (element_gap_left, element_gap_up, element_gap_right, element_gap_down) = { + let gap = if is_placeholder_sibling { + PLACEHOLDER_GAP_MOUSE + } else { + gap + }; + ( + if TilingLayout::has_sibling_node(tree, &node_id, Direction::Left) + && (mouse_tiling.is_some() || depth > 0) + { + gap / 2 + } else { + 0 + }, + if TilingLayout::has_sibling_node(tree, &node_id, Direction::Up) + && (mouse_tiling.is_some() || depth > 0) + { + gap / 2 + } else { + 0 + }, + if TilingLayout::has_sibling_node(tree, &node_id, Direction::Right) + && (mouse_tiling.is_some() || depth > 0) + { + gap / 2 + } else { + 0 + }, + if TilingLayout::has_sibling_node(tree, &node_id, Direction::Down) + && (mouse_tiling.is_some() || depth > 0) + { + gap / 2 + } else { + 0 + }, + ) + }; + + let group_color = GROUP_COLOR; + + match data { + Data::Group { + orientation, + last_geometry, + sizes, + alive, + pill_indicator, + } => { + let render_active_child = if let Some(focused_id) = focused.as_ref() { + !has_potential_groups + && swap_desc.is_none() + && node + .children() + .iter() + .any(|child_id| child_id == focused_id) + } else { + false + }; + + geo.loc += (element_gap_left, element_gap_up).into(); + geo.size -= (element_gap_left, element_gap_up).into(); + geo.size -= (element_gap_right, element_gap_down).into(); + + geometries.insert(node_id.clone(), geo); + + if let Some(renderer) = renderer.as_mut() { + if (render_potential_group || render_active_child) && Some(&node_id) != root + { + push( + IndicatorShader::element( + *renderer, + Key::Group(Arc::downgrade(alive)), + geo, + 4, + [if render_active_child { 16 } else { 8 }; 4], + alpha * if render_potential_group { 0.40 } else { 1.0 }, + scale, + group_color, + ) + .into(), + ); + } + if mouse_tiling.is_some() + && pill_indicator.is_some() + && Some(&node_id) != root + { + push( + IndicatorShader::element( + *renderer, + Key::Group(Arc::downgrade(alive)), + geo, + 4, + [8; 4], + alpha * 0.40, + scale, + group_color, + ) + .into(), + ); + } + + if mouse_tiling.is_some() + && node + .parent() + .map(|parent_id| { + matches!( + tree.get(parent_id).unwrap().data(), + Data::Group { + pill_indicator: Some(_), + .. + } + ) + }) + .unwrap_or(false) + { + // test if parent pill-indicator is adjacent + let parent_id = node.parent().unwrap(); + let parent = tree.get(parent_id).unwrap(); + let own_idx = tree + .children_ids(parent_id) + .unwrap() + .position(|child_id| child_id == &node_id) + .unwrap(); + let draw_outline = match parent.data() { + Data::Group { + pill_indicator, + orientation, + .. + } => match pill_indicator { + Some(PillIndicator::Inner(pill_idx)) => { + *pill_idx == own_idx || pill_idx + 1 == own_idx + } + Some(PillIndicator::Outer(dir)) => match (dir, orientation) { + (Direction::Left, Orientation::Horizontal) + | (Direction::Right, Orientation::Horizontal) + | (Direction::Up, Orientation::Vertical) + | (Direction::Down, Orientation::Vertical) => true, + (Direction::Left, Orientation::Vertical) + | (Direction::Up, Orientation::Horizontal) => own_idx == 0, + (Direction::Right, Orientation::Vertical) + | (Direction::Down, Orientation::Horizontal) => { + own_idx + 1 == parent.data().len() + } + }, + None => unreachable!(), + }, + _ => unreachable!(), + }; + + if draw_outline { + push( + IndicatorShader::element( + *renderer, + Key::Group(Arc::downgrade(alive)), + geo, + 4, + [8; 4], + alpha * 0.15, + scale, + group_color, + ) + .into(), + ); + } + } + } + + geo.loc += (gap, gap).into(); + geo.size -= (gap * 2, gap * 2).into(); + + if mouse_tiling.is_some() + && let Some(PillIndicator::Outer(direction)) = pill_indicator + { + let (pill_geo, remaining_geo) = match direction { + Direction::Left => ( + Rectangle::new( + (geo.loc.x, geo.loc.y).into(), + (16, geo.size.h).into(), + ), + Rectangle::new( + (geo.loc.x + 48, geo.loc.y).into(), + (geo.size.w - 48, geo.size.h).into(), + ), + ), + Direction::Up => ( + Rectangle::new( + (geo.loc.x, geo.loc.y).into(), + (geo.size.w, 16).into(), + ), + Rectangle::new( + (geo.loc.x, geo.loc.y + 48).into(), + (geo.size.w, geo.size.h - 48).into(), + ), + ), + Direction::Right => ( + Rectangle::new( + (geo.loc.x + geo.size.w - 16, geo.loc.y).into(), + (16, geo.size.h).into(), + ), + Rectangle::new(geo.loc, (geo.size.w - 48, geo.size.h).into()), + ), + Direction::Down => ( + Rectangle::new( + (geo.loc.x, geo.loc.y + geo.size.h - 16).into(), + (geo.size.w, 16).into(), + ), + Rectangle::new(geo.loc, (geo.size.w, geo.size.h - 48).into()), + ), + }; + + if let Some(renderer) = renderer.as_mut() { + push( + BackdropShader::element( + *renderer, + backdrop_id.clone(), + pill_geo, + 8., + alpha * 0.4, + group_color, + ) + .into(), + ); + } + + geo = remaining_geo; + }; + + if matches!(swap_desc, Some(ref desc) if desc.node == node_id) { + if let Some(renderer) = renderer.as_mut() { + push( + BackdropShader::element( + *renderer, + Key::Group(Arc::downgrade(alive)), + geo, + 8., + alpha + * if focused + .as_ref() + .map(|focused| { + focused == &swap_desc.as_ref().unwrap().node + }) + .unwrap_or(false) + { + 0.4 + } else { + 0.15 + }, + group_color, + ) + .into(), + ); + } + let swap_geo = swap_geometry( + geo.size.as_logical(), + focused_geo.unwrap_or({ + let mut geo = non_exclusive_zone; + geo.loc += (WINDOW_BACKDROP_BORDER, WINDOW_BACKDROP_BORDER).into(); + geo.size -= + (WINDOW_BACKDROP_BORDER * 2, WINDOW_BACKDROP_BORDER * 2).into(); + geo + }), + ); + geo = ease( + Linear, + EaseRectangle(geo), + EaseRectangle(swap_geo), + transition, + ) + .unwrap(); + geometries.insert(node_id.clone(), geo); + }; + + let previous_length = match orientation { + Orientation::Horizontal => last_geometry.size.h, + Orientation::Vertical => last_geometry.size.w, + }; + let new_length = match orientation { + Orientation::Horizontal => geo.size.h, + Orientation::Vertical => geo.size.w, + }; + + let mut sizes = sizes + .iter() + .map(|len| { + (((*len as f64) / (previous_length as f64)) * (new_length as f64)) + .round() as i32 + }) + .collect::>(); + let sum: i32 = sizes.iter().sum(); + if sum < new_length { + *sizes.last_mut().unwrap() += new_length - sum; + } + + match orientation { + Orientation::Horizontal => { + let mut previous: i32 = sizes.iter().sum(); + for (idx, size) in sizes.iter().enumerate().rev() { + previous -= *size; + let mut geo = Rectangle::new( + (geo.loc.x, geo.loc.y + previous).into(), + (geo.size.w, *size).into(), + ); + if mouse_tiling.is_some() + && let Some(PillIndicator::Inner(pill_idx)) = pill_indicator + { + if *pill_idx == idx { + geo.size.h -= 32; + } + if idx + .checked_sub(1) + .map(|idx| idx == *pill_idx) + .unwrap_or(false) + { + if let Some(renderer) = renderer.as_mut() { + push( + BackdropShader::element( + *renderer, + backdrop_id.clone(), + Rectangle::new( + (geo.loc.x, geo.loc.y - 8).into(), + (geo.size.w, 16).into(), + ), + 8., + alpha * 0.4, + group_color, + ) + .into(), + ); + } + geo.loc.y += 32; + geo.size.h -= 32; + } + } + stack.push((geo, depth + 1)); + } + } + Orientation::Vertical => { + let mut previous: i32 = sizes.iter().sum(); + for (idx, size) in sizes.iter().enumerate().rev() { + previous -= *size; + let mut geo = Rectangle::new( + (geo.loc.x + previous, geo.loc.y).into(), + (*size, geo.size.h).into(), + ); + if mouse_tiling.is_some() + && let Some(PillIndicator::Inner(pill_idx)) = pill_indicator + { + if *pill_idx == idx { + geo.size.w -= 32; + } + if idx + .checked_sub(1) + .map(|idx| idx == *pill_idx) + .unwrap_or(false) + { + if let Some(renderer) = renderer.as_mut() { + push( + BackdropShader::element( + *renderer, + backdrop_id.clone(), + Rectangle::new( + (geo.loc.x - 8, geo.loc.y).into(), + (16, geo.size.h).into(), + ), + 8., + alpha * 0.4, + group_color, + ) + .into(), + ); + } + geo.loc.x += 32; + geo.size.w -= 32; + } + } + stack.push((geo, depth + 1)); + } + } + } + } + Data::Mapped { mapped, .. } => { + geo.loc += (element_gap_left, element_gap_up).into(); + geo.size -= (element_gap_left, element_gap_up).into(); + geo.size -= (element_gap_right, element_gap_down).into(); + + if let Some(renderer) = renderer.as_mut() { + if render_potential_group { + push( + IndicatorShader::element( + *renderer, + Key::Window(Usage::PotentialGroupIndicator, mapped.key()), + geo, + 4, + [8; 4], + alpha * 0.40, + scale, + group_color, + ) + .into(), + ); + geo.loc += (gap, gap).into(); + geo.size -= (gap * 2, gap * 2).into(); + } + + let accent = ACTIVE_GROUP_COLOR; + + if focused + .as_ref() + .map(|focused_id| { + !tree + .ancestor_ids(&node_id) + .unwrap() + .any(|id| id == focused_id) + }) + .unwrap_or(mouse_tiling.is_some()) + { + let color = match mouse_tiling { + Some(Some(TargetZone::WindowStack(stack_id, _))) + if *stack_id == node_id => + { + accent + } + _ => group_color, + }; + geo.loc += (WINDOW_BACKDROP_BORDER, WINDOW_BACKDROP_BORDER).into(); + geo.size -= + (WINDOW_BACKDROP_BORDER * 2, WINDOW_BACKDROP_BORDER * 2).into(); + push( + BackdropShader::element( + *renderer, + Key::Window(Usage::OverviewBackdrop, mapped.key()), + geo, + 8., + alpha + * if focused + .as_ref() + .map(|focused_id| focused_id == &node_id) + .unwrap_or(color == accent) + { + 0.4 + } else { + 0.15 + }, + color, + ) + .into(), + ); + } + + geo.loc += (WINDOW_BACKDROP_GAP, WINDOW_BACKDROP_GAP).into(); + geo.size -= (WINDOW_BACKDROP_GAP * 2, WINDOW_BACKDROP_GAP * 2).into(); + } + + if matches!(swap_desc, Some(ref desc) if desc.node == node_id && desc.stack_window.is_none()) + { + let swap_geo = swap_geometry( + geo.size.as_logical(), + focused_geo.unwrap_or({ + let mut geo = non_exclusive_zone; + geo.loc += (WINDOW_BACKDROP_BORDER, WINDOW_BACKDROP_BORDER).into(); + geo.size -= + (WINDOW_BACKDROP_BORDER * 2, WINDOW_BACKDROP_BORDER * 2).into(); + geo + }), + ); + geo = ease( + Linear, + EaseRectangle(geo), + EaseRectangle(swap_geo), + transition, + ) + .unwrap(); + }; + + geometries.insert(node_id.clone(), geo); + } + Data::Placeholder { id, .. } => { + geo.loc += (element_gap_left, element_gap_up).into(); + geo.size -= (element_gap_left, element_gap_up).into(); + geo.size -= (element_gap_right, element_gap_down).into(); + + if let Some(renderer) = renderer.as_mut() { + geo.loc += (WINDOW_BACKDROP_BORDER, WINDOW_BACKDROP_BORDER).into(); + geo.size -= (WINDOW_BACKDROP_BORDER * 2, WINDOW_BACKDROP_BORDER * 2).into(); + push( + BackdropShader::element( + *renderer, + id.clone(), + geo, + 8., + alpha * 0.4, + group_color, + ) + .into(), + ); + } + + geometries.insert(node_id.clone(), geo); + } + } + } + } + + geometries +} + +fn render_old_tree_popups( + reference_tree: &Tree, + target_tree: &Tree, + renderer: &mut R, + geometries: Option>>, + output_scale: f64, + percentage: f32, + is_swap_mode: bool, + scanout_node: Option, + push: &mut dyn FnMut(CosmicMappedRenderElement), +) where + R: AsGlowRenderer, + R::TextureId: Send + Clone + 'static, + CosmicMappedRenderElement: RenderElement, + CosmicWindowRenderElement: RenderElement, + CosmicStackRenderElement: RenderElement, +{ + render_old_tree( + reference_tree, + target_tree, + geometries, + output_scale, + percentage, + is_swap_mode, + |mapped, elem_geometry, geo, alpha, _| { + mapped.push_popup_render_elements( + renderer, + geo.loc.as_logical().to_physical_precise_round(output_scale) - elem_geometry.loc, + Scale::from(output_scale), + alpha, + scanout_node, + push, + ) + }, + ) +} + +fn render_old_tree_windows( + reference_tree: &Tree, + target_tree: &Tree, + renderer: &mut R, + geometries: Option>>, + output_scale: f64, + percentage: f32, + indicator_thickness: u8, + is_swap_mode: bool, + theme: &cosmic::theme::CosmicTheme, + scanout_node: Option, + push: &mut dyn FnMut(CosmicMappedRenderElement), +) where + R: AsGlowRenderer, + R::TextureId: Send + Clone + 'static, + CosmicMappedRenderElement: RenderElement, + CosmicWindowRenderElement: RenderElement, + CosmicStackRenderElement: RenderElement, +{ + let window_hint = crate::theme::active_window_hint(theme); + let mut lower_elements = Vec::default(); + let mut shadow_elements = SmallVec::<[_; 4]>::new_const(); + + let window_map = + |elem, geo: Rectangle, elem_geometry: Rectangle| match elem { + CosmicMappedRenderElement::Stack(elem) => constrain_render_elements( + std::iter::once(elem), + geo.loc.as_logical().to_physical_precise_round(output_scale) - elem_geometry.loc, + geo.as_logical().to_physical_precise_round(output_scale), + elem_geometry, + ConstrainScaleBehavior::Stretch, + ConstrainAlign::CENTER, + output_scale, + ) + .next() + .map(CosmicMappedRenderElement::TiledStack), + CosmicMappedRenderElement::Window(elem) => constrain_render_elements( + std::iter::once(elem), + geo.loc.as_logical().to_physical_precise_round(output_scale) - elem_geometry.loc, + geo.as_logical().to_physical_precise_round(output_scale), + elem_geometry, + ConstrainScaleBehavior::Stretch, + ConstrainAlign::CENTER, + output_scale, + ) + .next() + .map(CosmicMappedRenderElement::TiledWindow), + x => Some(x), + }; + + render_old_tree( + reference_tree, + target_tree, + geometries, + output_scale, + percentage, + is_swap_mode, + |mapped, elem_geometry, geo, alpha, is_minimizing| { + let radius = mapped.corner_radius(geo.size.as_logical(), indicator_thickness); + if is_minimizing && indicator_thickness > 0 { + push(CosmicMappedRenderElement::FocusIndicator( + IndicatorShader::focus_element( + renderer, + Key::Window(Usage::FocusIndicator, mapped.clone().key()), + geo, + indicator_thickness, + radius, + alpha, + output_scale, + [window_hint.red, window_hint.green, window_hint.blue], + ), + )); + } + + mapped.push_render_elements( + renderer, + geo.loc.as_logical().to_physical_precise_round(output_scale) - elem_geometry.loc, + Some(geo.size.as_logical()), + Scale::from(output_scale), + alpha, + None, + scanout_node, + &mut |elem| { + if let Some(elem) = window_map(elem, geo, elem_geometry) { + push(elem); + } + }, + &mut |elem| { + if let Some(elem) = window_map(elem, geo, elem_geometry) { + lower_elements.push(elem); + } + }, + ); + + shadow_elements.extend(mapped.shadow_render_element( + renderer, + geo.loc.as_logical().to_physical_precise_round(output_scale) - elem_geometry.loc, + Some(geo.size.as_logical()), + Scale::from(output_scale), + 1., + alpha, + )); + }, + ); + + for elem in shadow_elements { + push(elem); + } + for elem in lower_elements { + push(elem); + } +} + +fn render_old_tree( + reference_tree: &Tree, + target_tree: &Tree, + geometries: Option>>, + output_scale: f64, + percentage: f32, + is_swap_mode: bool, + mut processor: impl FnMut(&CosmicMapped, Rectangle, Rectangle, f32, bool), +) { + if let Some(root) = reference_tree.root_node_id() { + let geometries = geometries.unwrap_or_default(); + reference_tree + .traverse_pre_order_ids(root) + .unwrap() + .filter(|node_id| reference_tree.get(node_id).unwrap().data().is_mapped(None)) + .map( + |node_id| match reference_tree.get(&node_id).unwrap().data() { + Data::Mapped { + mapped, + last_geometry, + minimize_rect, + .. + } => ( + mapped, + last_geometry, + geometries.get(&node_id).copied(), + minimize_rect, + ), + _ => unreachable!(), + }, + ) + .filter(|(mapped, _, _, _)| !mapped.is_maximized(false)) + .filter(|(mapped, _, _, _)| { + if let Some(root) = target_tree.root_node_id() { + is_swap_mode + || !target_tree + .traverse_pre_order(root) + .unwrap() + .any(|node| node.data().is_mapped(Some(mapped))) + } else { + true + } + }) + .for_each(|(mapped, original_geo, mut scaled_geo, minimize_geo)| { + if let Some(minimize_geo) = minimize_geo { + scaled_geo = Some( + ease( + EaseInOutCubic, + EaseRectangle(*original_geo), + EaseRectangle(*minimize_geo), + percentage, + ) + .unwrap(), + ); + } + + let (scale, offset) = scaled_geo + .map(|adapted_geo| scale_to_center(original_geo, &adapted_geo)) + .unwrap_or_else(|| (1.0, (0, 0).into())); + let geo = scaled_geo + .map(|adapted_geo| { + Rectangle::new( + adapted_geo.loc + offset, + (original_geo.size.to_f64() * scale).to_i32_round(), + ) + }) + .unwrap_or(*original_geo); + + let alpha = if minimize_geo.is_some() { + 1.0 - ((percentage - 0.5).max(0.0) * 2.0) + } else { + 1.0 - percentage + }; + + let elem_geometry = mapped.geometry().to_physical_precise_round(output_scale); + + processor(mapped, elem_geometry, geo, alpha, minimize_geo.is_some()) + }); + } +} + +fn render_new_tree_popups( + target_tree: &Tree, + reference_tree: Option<&Tree>, + renderer: &mut R, + geometries: Option>>, + old_geometries: Option>>, + seat: Option<&Seat>, + output: &Output, + percentage: f32, + overview: (OverviewMode, Option<(SwapIndicator, Option<&Tree>)>), + swap_desc: Option, + scanout_node: Option, + push: &mut dyn FnMut(CosmicMappedRenderElement), +) where + R: AsGlowRenderer, + R::TextureId: Send + Clone + 'static, + CosmicMappedRenderElement: RenderElement, + CosmicWindowRenderElement: RenderElement, + CosmicStackRenderElement: RenderElement, +{ + let output_scale = output.current_scale().fractional_scale(); + + let is_active_output = seat + .map(|seat| &seat.active_output() == output) + .unwrap_or(false); + + let (_, swap_tree) = overview.1.unzip(); + let swap_desc = swap_desc.filter(|_| is_active_output); + let swap_tree = swap_tree.flatten().filter(|_| is_active_output); + + render_new_tree( + target_tree, + reference_tree, + geometries, + old_geometries, + percentage, + swap_tree, + swap_desc.as_ref(), + |_node_id, data, geo, _original_geo, alpha, _| { + if let Data::Mapped { mapped, .. } = data { + let elem_geometry = mapped.geometry().to_physical_precise_round(output_scale); + + mapped.push_popup_render_elements( + renderer, + geo.loc.as_logical().to_physical_precise_round(output_scale) + - elem_geometry.loc, + Scale::from(output_scale), + alpha, + scanout_node, + push, + ); + } + }, + ); +} + +fn render_new_tree_windows( + target_tree: &Tree, + reference_tree: Option<&Tree>, + renderer: &mut R, + non_exclusive_zone: Rectangle, + geometries: Option>>, + old_geometries: Option>>, + is_overview: bool, + seat: Option<&Seat>, + focused: Option<&CosmicMapped>, + output: &Output, + percentage: f32, + transition: Option, + indicator_thickness: u8, + overview: (OverviewMode, Option<(SwapIndicator, Option<&Tree>)>), + mut resize_indicator: Option<(ResizeMode, ResizeIndicator)>, + swap_desc: Option, + swapping_stack_surface_id: &Id, + backdrop_id: &Id, + theme: &cosmic::theme::CosmicTheme, + scanout_node: Option, + push: &mut dyn FnMut(CosmicMappedRenderElement), +) where + R: AsGlowRenderer, + R::TextureId: Send + Clone + 'static, + CosmicMappedRenderElement: RenderElement, + CosmicWindowRenderElement: RenderElement, + CosmicStackRenderElement: RenderElement, +{ + let focused = match seat.and_then(|seat| seat.get_keyboard().unwrap().current_focus()) { + Some(target @ KeyboardFocusTarget::Group(_)) => { + TilingLayout::currently_focused_node(target_tree, target).map(|(id, _)| id) + } + _ => focused.and_then(|mapped| { + let node_id = mapped.tiling_node_id.lock().unwrap().clone()?; + target_tree + .get(&node_id) + .ok() + .filter(|node| node.data().is_mapped(Some(mapped))) + .map(|_| node_id) + }), + }; + let focused_geo = if let Some(focused) = focused.as_ref() { + geometries + .as_ref() + .and_then(|geometries| geometries.get(focused)) + .or_else(|| { + target_tree + .get(focused) + .ok() + .map(|node| node.data().geometry()) + }) + .cloned() + } else { + None + } + .unwrap_or({ + let mut geo = non_exclusive_zone; + geo.loc += (WINDOW_BACKDROP_BORDER, WINDOW_BACKDROP_BORDER).into(); + geo.size -= (WINDOW_BACKDROP_BORDER * 2, WINDOW_BACKDROP_BORDER * 2).into(); + geo + }); + + let is_active_output = seat + .map(|seat| &seat.active_output() == output) + .unwrap_or(false); + + let mut animating_window_upper_elements = Vec::new(); + let mut animating_window_lower_elements = Vec::new(); + let mut animating_shadow_elements = SmallVec::<[CosmicMappedRenderElement; 4]>::new_const(); + + let mut window_upper_elements = Vec::new(); + let mut window_lower_elements = Vec::new(); + let mut shadow_elements = SmallVec::<[CosmicMappedRenderElement; 4]>::new_const(); + + let mut group_backdrop = None; + let mut indicators = SmallVec::<[CosmicMappedRenderElement; 2]>::new_const(); + let mut resize_elements = SmallVec::<[CosmicMappedRenderElement; 10]>::new_const(); + let mut swap_elements = SmallVec::<[CosmicMappedRenderElement; 4]>::new_const(); + + let output_scale = output.current_scale().fractional_scale(); + + let (mut swap_indicator, swap_tree) = overview.1.unzip(); + let swap_desc = swap_desc.filter(|_| is_active_output); + let swap_tree = swap_tree.flatten().filter(|_| is_active_output); + let window_hint = crate::theme::active_window_hint(theme); + let group_color = GROUP_COLOR; + + // render placeholder, if we are swapping to an empty workspace + if target_tree.root_node_id().is_none() && swap_desc.is_some() { + window_upper_elements.push( + BackdropShader::element( + renderer, + backdrop_id.clone(), + focused_geo, + 8., + transition.unwrap_or(1.0) * 0.4, + group_color, + ) + .into(), + ); + } + + // render single stack window when swapping separately + if let Some(window) = swap_desc + .as_ref() + .and_then(|desc| desc.stack_window.clone()) + { + let window_geo = window.geometry(); + let origin = { + let mut geo = focused_geo; + geo.loc.x += STACK_TAB_HEIGHT; + geo.size.h -= STACK_TAB_HEIGHT; + geo + }; + let target = swap_geometry(window_geo.size, focused_geo); + let swap_geo = ease( + Linear, + EaseRectangle(origin), + EaseRectangle(target), + transition.unwrap_or(1.0), + ) + .unwrap(); + let scale = swap_geo.size.to_f64() / origin.size.to_f64(); + + let radius = theme + .radius_s() + .map(|x| if x < 4.0 { x } else { x + 4.0 }) + .map(|val| (val * scale.x.min(scale.y) as f32).round() as u8); + swap_elements.push(CosmicMappedRenderElement::FocusIndicator( + IndicatorShader::focus_element( + renderer, + Key::from(swapping_stack_surface_id.clone()), + swap_geo, + 4, + radius, + transition.unwrap_or(1.0), + output_scale, + [window_hint.red, window_hint.green, window_hint.blue], + ), + )); + + let render_loc = + (swap_geo.loc.as_logical() - window_geo.loc).to_physical_precise_round(output_scale); + + window.push_render_elements( + renderer, + render_loc, + output_scale.into(), + 1.0, + None, + scanout_node, + false, + [0, 0, 0, 0], + // TODO + 0, + &mut |elem| { + swap_elements.push(CosmicMappedRenderElement::GrabbedWindow( + RescaleRenderElement::from_element( + elem.into(), + swap_geo + .loc + .as_logical() + .to_physical_precise_round(output_scale), + ease( + Linear, + 1.0, + swap_factor(window_geo.size), + transition.unwrap_or(1.0), + ), + ), + )); + }, + None, + ); + } + + // render actual tree nodes + render_new_tree( + target_tree, + reference_tree, + geometries, + old_geometries, + percentage, + swap_tree, + swap_desc.as_ref(), + |node_id, data, geo, original_geo, alpha, animating| { + if swap_desc.as_ref().map(|desc| &desc.node) == Some(&node_id) + || focused.as_ref() == Some(&node_id) + { + if indicator_thickness > 0 || data.is_group() { + let mut geo = geo; + + let scale = geo.size.to_f64() / original_geo.size.to_f64(); + let radius = match data { + Data::Mapped { mapped, .. } + if swap_desc + .as_ref() + .map(|desc| &desc.node) + .is_none_or(|n| n != &node_id) => + { + mapped + .corner_radius(geo.size.as_logical(), indicator_thickness) + .map(|val| (val as f64 * scale.x.min(scale.y)).round() as u8) + } + _ => theme + .radius_s() + .map(|x| if x < 4.0 { x } else { x + 4.0 }) + .map(|val| (val * scale.x.min(scale.y) as f32).round() as u8), + }; + + if data.is_group() { + let outer_gap: i32 = (if is_overview { GAP_KEYBOARD } else { 4 } as f32 + * percentage) + .round() as i32; + geo.loc += (outer_gap, outer_gap).into(); + geo.size -= (outer_gap * 2, outer_gap * 2).into(); + + let backdrop = BackdropShader::element( + renderer, + match data { + Data::Group { alive, .. } => Key::Group(Arc::downgrade(alive)), + _ => unreachable!(), + }, + geo, + radius[0] as f32, + 0.4, + group_color, + ); + + if focused.as_ref() == Some(&node_id) { + group_backdrop = Some(backdrop); + } else { + indicators.push(backdrop.into()); + } + } + if !swap_desc + .as_ref() + .map(|desc| desc.stack_window.is_some()) + .unwrap_or(false) + || focused.as_ref() == Some(&node_id) + { + indicators.push(CosmicMappedRenderElement::FocusIndicator( + IndicatorShader::focus_element( + renderer, + match data { + Data::Mapped { mapped, .. } => { + Key::Window(Usage::FocusIndicator, mapped.clone().key()) + } + Data::Group { alive, .. } => Key::Group(Arc::downgrade(alive)), + _ => unreachable!(), + }, + geo, + if data.is_group() { + 4 + } else { + indicator_thickness + }, + radius, + alpha, + output_scale, + [window_hint.red, window_hint.green, window_hint.blue], + ), + )); + } + + if focused.as_ref() == Some(&node_id) + && (swap_desc.as_ref().map(|desc| &desc.node) != Some(&node_id) + || swap_desc + .as_ref() + .and_then(|swap_desc| swap_desc.stack_window.as_ref()) + .zip(focused.as_ref()) + .map(|(stack_window, focused_id)| { + target_tree + .get(focused_id) + .ok() + .map(|focused| match focused.data() { + Data::Mapped { mapped, .. } => mapped + .stack_ref() + .map(|stack| &stack.active() != stack_window) + .unwrap_or(false), + _ => false, + }) + .unwrap_or(false) + }) + .unwrap_or(false)) + && let Some(swap) = swap_indicator.as_mut() + { + let size = geo.size.as_logical(); + swap.resize(size); + swap.output_enter(output); + swap.push_render_elements( + renderer, + geo.loc.as_logical().to_physical_precise_round(output_scale), + output_scale.into(), + alpha * overview.0.alpha().unwrap_or(1.0), + &mut |elem| swap_elements.push(elem.into()), + None, + ); + } + } + + if let Some((mode, resize)) = resize_indicator.as_mut() { + let mut geo = geo; + geo.loc -= (18, 18).into(); + geo.size += (36, 36).into(); + + resize.resize(geo.size.as_logical()); + resize.output_enter(output); + let possible_edges = + TilingLayout::possible_resizes(target_tree, node_id.clone()); + if !possible_edges.is_empty() { + resize.set_edges(possible_edges); + resize.push_render_elements( + renderer, + geo.loc.as_logical().to_physical_precise_round(output_scale), + output_scale.into(), + alpha * mode.alpha().unwrap_or(1.0), + &mut |elem| { + resize_elements.push(elem.into()); + }, + None, + ); + } + } + } + + if let Data::Mapped { mapped, .. } = data { + let elem_geometry = mapped.geometry().to_physical_precise_round(output_scale); + + let scale = geo.size.to_f64() / original_geo.size.to_f64(); + // In overview mode, don't pass max_size to avoid pre-clipping. + // Let constrain_render_elements handle scaling instead. + let max_size = if is_overview { + None + } else { + Some(geo.size.as_logical()) + }; + let shadow_element = mapped.shadow_render_element( + renderer, + geo.loc.as_logical().to_physical_precise_round(output_scale) + - elem_geometry.loc, + max_size, + Scale::from(output_scale), + scale.x.min(scale.y), + alpha, + ); + + let (behavior, align) = if is_overview { + (ConstrainScaleBehavior::Fit, ConstrainAlign::CENTER) + } else if animating { + (ConstrainScaleBehavior::Stretch, ConstrainAlign::TOP_LEFT) + } else { + (ConstrainScaleBehavior::CutOff, ConstrainAlign::TOP_LEFT) + }; + + let map_elem = |element| match element { + CosmicMappedRenderElement::Stack(elem) => constrain_render_elements( + std::iter::once(elem), + geo.loc.as_logical().to_physical_precise_round(output_scale) + - elem_geometry.loc, + geo.as_logical().to_physical_precise_round(output_scale), + elem_geometry, + behavior, + align, + output_scale, + ) + .next() + .map(CosmicMappedRenderElement::TiledStack), + CosmicMappedRenderElement::Window(elem) => constrain_render_elements( + std::iter::once(elem), + geo.loc.as_logical().to_physical_precise_round(output_scale) + - elem_geometry.loc, + geo.as_logical().to_physical_precise_round(output_scale), + elem_geometry, + behavior, + align, + output_scale, + ) + .next() + .map(CosmicMappedRenderElement::TiledWindow), + CosmicMappedRenderElement::Overlay(elem) => constrain_render_elements( + std::iter::once(elem), + geo.loc.as_logical().to_physical_precise_round(output_scale) + - elem_geometry.loc, + geo.as_logical().to_physical_precise_round(output_scale), + elem_geometry, + behavior, + align, + output_scale, + ) + .next() + .map(CosmicMappedRenderElement::TiledOverlay), + x => Some(x), + }; + + let mut upper_elements = SmallVec::<[CosmicMappedRenderElement; 4]>::new_const(); + let mut lower_elements = SmallVec::<[CosmicMappedRenderElement; 4]>::new_const(); + mapped.push_render_elements( + renderer, + //original_location, + geo.loc.as_logical().to_physical_precise_round(output_scale) + - elem_geometry.loc, + max_size, + Scale::from(output_scale), + alpha, + None, + scanout_node, + &mut |elem| { + if let Some(elem) = map_elem(elem) { + upper_elements.push(elem) + } + }, + &mut |elem| { + if let Some(elem) = map_elem(elem) { + lower_elements.push(elem) + } + }, + ); + + if swap_desc + .as_ref() + .filter(|swap_desc| swap_desc.node == node_id) + .and_then(|swap_desc| swap_desc.stack_window.as_ref()) + .zip(focused.as_ref()) + .map(|(stack_window, focused_id)| { + target_tree + .get(focused_id) + .ok() + .map(|focused| match focused.data() { + Data::Mapped { mapped, .. } => mapped + .stack_ref() + .map(|stack| &stack.active() == stack_window) + .unwrap_or(false), + _ => false, + }) + .unwrap_or(false) + }) + .unwrap_or(false) + { + let mut active_geo = mapped.active_window_geometry().as_local(); + active_geo.loc += geo.loc - mapped.geometry().loc.as_local(); + upper_elements.insert( + 0, + CosmicMappedRenderElement::Overlay(BackdropShader::element( + renderer, + Key::Window(Usage::Overlay, mapped.key()), + active_geo, + 0.0, + 0.3, + group_color, + )), + ) + } + + if swap_desc + .as_ref() + .map(|swap_desc| { + (swap_desc.node == node_id + || target_tree.ancestor_ids(&node_id).ok().is_none_or( + |mut ancestors| ancestors.any(|id| &swap_desc.node == id), + )) + && swap_desc.stack_window.is_none() + }) + .unwrap_or(false) + { + swap_elements.extend(upper_elements); + swap_elements.extend(shadow_element); + swap_elements.extend(lower_elements); + } else if animating { + animating_window_upper_elements.extend(upper_elements); + animating_shadow_elements.extend(shadow_element); + animating_window_lower_elements.extend(lower_elements); + } else { + window_upper_elements.extend(upper_elements); + shadow_elements.extend(shadow_element); + window_lower_elements.extend(lower_elements); + } + } + }, + ); + + for elem in resize_elements + .into_iter() + .chain(swap_elements) + .chain(indicators) + .chain(window_upper_elements) + .chain(shadow_elements) + .chain(window_lower_elements) + .chain(animating_window_upper_elements) + .chain(animating_shadow_elements) + .chain(animating_window_lower_elements) + .chain(group_backdrop.into_iter().map(Into::into)) + { + push(elem); + } +} + +fn render_new_tree( + target_tree: &Tree, + reference_tree: Option<&Tree>, + geometries: Option>>, + old_geometries: Option>>, + percentage: f32, + swap_tree: Option<&Tree>, + swap_desc: Option<&NodeDesc>, + mut processor: impl FnMut(NodeId, &Data, Rectangle, &Rectangle, f32, bool), +) { + let old_geometries = old_geometries.unwrap_or_default(); + let geometries = geometries.unwrap_or_default(); + target_tree + .root_node_id() + .into_iter() + .flat_map(|root| target_tree.traverse_pre_order_ids(root).unwrap()) + .map(|id| (target_tree, id)) + .chain( + swap_tree + .into_iter() + .flat_map(|tree| { + let sub_root = &swap_desc.unwrap().node; + if swap_desc.unwrap().stack_window.is_none() { + Some( + tree.traverse_pre_order_ids(sub_root) + .unwrap() + .map(move |id| (tree, id)), + ) + } else { + None + } + }) + .flatten(), + ) + .for_each(|(target_tree, node_id)| { + let data = target_tree.get(&node_id).unwrap().data(); + let (original_geo, scaled_geo) = (data.geometry(), geometries.get(&node_id)); + + let (old_original_geo, old_scaled_geo) = + if let Some(reference_tree) = reference_tree.as_ref() { + if let Some(root) = reference_tree.root_node_id() { + reference_tree + .traverse_pre_order_ids(root) + .unwrap() + .find(|id| &node_id == id) + .map(|node_id| { + ( + reference_tree.get(&node_id).unwrap().data().geometry(), + old_geometries.get(&node_id), + ) + }) + } else { + None + } + } else { + None + } + .unzip(); + let mut old_geo = old_original_geo.map(|original_geo| { + let (scale, offset) = old_scaled_geo + .unwrap() + .map(|adapted_geo| scale_to_center(original_geo, adapted_geo)) + .unwrap_or_else(|| (1.0, (0, 0).into())); + ( + old_scaled_geo + .unwrap() + .map(|adapted_geo| { + Rectangle::new( + adapted_geo.loc + offset, + (original_geo.size.to_f64() * scale).to_i32_round(), + ) + }) + .unwrap_or(*original_geo), + 1.0, + ) + }); + + let was_minimized = if let Data::Mapped { + minimize_rect: Some(minimize_rect), + .. + } = &data + { + old_geo = Some((*minimize_rect, (percentage * 2.0).min(1.0))); + true + } else { + false + }; + + let (scale, offset) = scaled_geo + .map(|adapted_geo| scale_to_center(original_geo, adapted_geo)) + .unwrap_or_else(|| (1.0, (0, 0).into())); + let new_geo = scaled_geo + .map(|adapted_geo| { + Rectangle::new( + adapted_geo.loc + offset, + ( + (original_geo.size.w as f64 * scale).round() as i32, + (original_geo.size.h as f64 * scale).round() as i32, + ) + .into(), + ) + }) + .unwrap_or(*original_geo); + + let (geo, alpha, animating) = if let Some((old_geo, alpha)) = old_geo.filter(|_| { + swap_desc + .map(|desc| desc.node != node_id && desc.stack_window.is_none()) + .unwrap_or(true) + }) { + ( + if was_minimized { + ease( + EaseInOutCubic, + EaseRectangle(old_geo), + EaseRectangle(new_geo), + percentage, + ) + .unwrap() + } else { + ease( + Linear, + EaseRectangle(old_geo), + EaseRectangle(new_geo), + percentage, + ) + .unwrap() + }, + alpha, + old_geo != new_geo, + ) + } else { + (new_geo, percentage, false) + }; + + if let Data::Mapped { mapped, .. } = data + && mapped.is_maximized(false) + { + return; + } + processor(node_id, data, geo, original_geo, alpha, animating) + }); +} + +fn scale_to_center( + old_geo: &Rectangle, + new_geo: &Rectangle, +) -> (f64, Point) { + let scale_w = new_geo.size.w as f64 / old_geo.size.w as f64; + let scale_h = new_geo.size.h as f64 / old_geo.size.h as f64; + + if scale_w > scale_h { + ( + scale_h, + ( + ((new_geo.size.w as f64 - old_geo.size.w as f64 * scale_h) / 2.0).round() as i32, + 0, + ) + .into(), + ) + } else { + ( + scale_w, + ( + 0, + ((new_geo.size.h as f64 - old_geo.size.h as f64 * scale_w) / 2.0).round() as i32, + ) + .into(), + ) + } +} diff --git a/src/shell/mod.rs b/src/shell/mod.rs new file mode 100644 index 0000000..39d692e --- /dev/null +++ b/src/shell/mod.rs @@ -0,0 +1,5142 @@ +use calloop::LoopHandle; +use focus::target::WindowGroup; +use grabs::{MenuAlignment, SeatMoveGrabState}; +use indexmap::IndexMap; +use layout::TilingExceptions; +use std::{ + collections::HashMap, + sync::{Mutex, atomic::Ordering}, + thread, + time::{Duration, Instant}, +}; +use wayland_backend::server::ClientId; + +use crate::{ + shell::{ + element::CosmicStack, focus::FocusTarget, grabs::fullscreen_items, + layout::tiling::PlaceholderType, + }, + utils, + wayland::{ + handlers::data_device::{self, get_dnd_icon}, + protocols::workspace::{State as WState, WorkspaceCapabilities}, + }, +}; +use cosmic_comp_config::{ + AppearanceConfig, TileBehavior, ZoomConfig, ZoomMovement, + workspace::{PinnedWorkspace, WorkspaceLayout, WorkspaceMode}, +}; +use cosmic_config::ConfigSet; +use cosmic_protocols::workspace::v2::server::zcosmic_workspace_handle_v2::TilingState; +use cosmic_settings_config::shortcuts::action::{Direction, FocusDirection, ResizeDirection}; +use cosmic_settings_config::{shortcuts, window_rules::ApplicationException}; +use keyframe::{ease, functions::EaseInOutCubic}; +use smithay::{ + backend::{input::TouchSlot, renderer::element::RenderElementStates}, + desktop::{ + LayerSurface, PopupKind, WindowSurface, WindowSurfaceType, layer_map_for_output, + space::SpaceElement, + utils::{ + OutputPresentationFeedback, surface_presentation_feedback_flags_from_states, + surface_primary_scanout_output, take_presentation_feedback_surface_tree, + }, + }, + input::{ + Seat, + pointer::{ + CursorImageStatus, CursorImageSurfaceData, Focus, GrabStartData as PointerGrabStartData, + }, + }, + output::{Output, WeakOutput}, + reexports::{ + wayland_protocols::ext::session_lock::v1::server::ext_session_lock_v1::ExtSessionLockV1, + wayland_server::{Client, protocol::wl_surface::WlSurface}, + }, + utils::{IsAlive, Logical, Point, Rectangle, Serial, Size}, + wayland::{ + compositor::{SurfaceAttributes, get_parent, with_states}, + seat::WaylandFocus, + session_lock::LockSurface, + shell::{ + wlr_layer::{KeyboardInteractivity, Layer, LayerSurfaceCachedState}, + xdg::{XDG_POPUP_ROLE, XdgPopupSurfaceData}, + }, + xdg_activation::XdgActivationState, + xwayland_keyboard_grab::XWaylandKeyboardGrab, + }, + xwayland::X11Surface, +}; +use tracing::error; + +use crate::{ + backend::render::animations::spring::{Spring, SpringParams}, + config::Config, + utils::{prelude::*, quirks::WORKSPACE_OVERVIEW_NAMESPACE}, + wayland::{ + handlers::{ + toplevel_management::minimize_rectangle, xdg_activation::ActivationContext, + xdg_shell::popup::get_popup_toplevel, + }, + protocols::{ + toplevel_info::{ + ToplevelInfoState, toplevel_enter_output, toplevel_enter_workspace, + toplevel_leave_output, toplevel_leave_workspace, + }, + workspace::{ + WorkspaceGroupHandle, WorkspaceHandle, WorkspaceState, WorkspaceUpdateGuard, + }, + }, + }, +}; + +pub mod element; +pub mod focus; +pub mod grabs; +pub mod layout; +mod seats; +mod workspace; +pub mod zoom; +pub use self::element::{CosmicMapped, CosmicMappedRenderElement, CosmicSurface}; +pub use self::seats::*; +pub use self::workspace::*; +use self::zoom::{OutputZoomState, ZoomState}; + +use self::{ + element::{ + CosmicWindow, MaximizedState, resize_indicator::ResizeIndicator, + swap_indicator::SwapIndicator, + }, + focus::target::{KeyboardFocusTarget, PointerFocusTarget}, + grabs::{ + GrabStartData, Item, MenuGrab, MoveGrab, ReleaseMode, ResizeEdge, ResizeGrab, tab_items, + window_items, + }, + layout::{ + floating::{FloatingLayout, ResizeState}, + tiling::{NodeDesc, ResizeForkGrab, TilingLayout}, + }, +}; + +const ANIMATION_DURATION: Duration = Duration::from_millis(200); +const GESTURE_MAX_LENGTH: f64 = 150.0; +const GESTURE_POSITION_THRESHOLD: f64 = 0.5; +const GESTURE_VELOCITY_THRESHOLD: f64 = 0.02; +const MOVE_GRAB_Y_OFFSET: f64 = 16.; +const ACTIVATION_TOKEN_EXPIRE_TIME: Duration = Duration::from_secs(5); + +#[derive(Debug, Clone)] +pub enum Trigger { + KeyboardSwap(shortcuts::Binding, NodeDesc), + KeyboardMove(shortcuts::Modifiers), + Pointer(u32), + Touch(TouchSlot), +} + +#[derive(Debug, Clone)] +pub enum OverviewMode { + None, + Started(Trigger, Instant), + Active(Trigger), + Ended(Option, Instant), +} + +impl OverviewMode { + pub fn alpha(&self) -> Option { + match self { + OverviewMode::Started(_, start) => { + let percentage = Instant::now().duration_since(*start).as_millis() as f32 + / ANIMATION_DURATION.as_millis() as f32; + Some(ease(EaseInOutCubic, 0.0, 1.0, percentage)) + } + OverviewMode::Active(_) => Some(1.0), + OverviewMode::Ended(_, end) => { + let percentage = Instant::now().duration_since(*end).as_millis() as f32 + / ANIMATION_DURATION.as_millis() as f32; + if percentage < 1.0 { + Some(ease(EaseInOutCubic, 1.0, 0.0, percentage)) + } else { + None + } + } + OverviewMode::None => None, + } + } + + pub fn is_active(&self) -> bool { + matches!(self, OverviewMode::Started(_, _) | OverviewMode::Active(_)) + } + + pub fn active_trigger(&self) -> Option<&Trigger> { + if let OverviewMode::Started(trigger, _) | OverviewMode::Active(trigger) = self { + Some(trigger) + } else { + None + } + } + + pub fn trigger(&self) -> Option<&Trigger> { + self.active_trigger().or({ + if let OverviewMode::Ended(trigger, _) = self { + trigger.as_ref() + } else { + None + } + }) + } +} + +#[derive(Debug, Clone)] +pub enum ResizeMode { + None, + Started(shortcuts::Binding, Instant, ResizeDirection), + Active(shortcuts::Binding, ResizeDirection), + Ended(Instant, ResizeDirection), +} + +impl ResizeMode { + pub fn alpha(&self) -> Option { + match self { + ResizeMode::Started(_, start, _) => { + let percentage = Instant::now().duration_since(*start).as_millis() as f32 + / ANIMATION_DURATION.as_millis() as f32; + Some(ease(EaseInOutCubic, 0.0, 1.0, percentage)) + } + ResizeMode::Active(_, _) => Some(1.0), + ResizeMode::Ended(end, _) => { + let percentage = Instant::now().duration_since(*end).as_millis() as f32 + / ANIMATION_DURATION.as_millis() as f32; + if percentage < 1.0 { + Some(ease(EaseInOutCubic, 1.0, 0.0, percentage)) + } else { + None + } + } + ResizeMode::None => None, + } + } + + pub fn active_binding(&self) -> Option<&shortcuts::Binding> { + if let ResizeMode::Started(binding, _, _) | ResizeMode::Active(binding, _) = self { + Some(binding) + } else { + None + } + } + + pub fn active_direction(&self) -> Option { + if let ResizeMode::Started(_, _, direction) | ResizeMode::Active(_, direction) = self { + Some(*direction) + } else { + None + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ActivationKey { + Wayland(WlSurface), + X11(u32), +} + +impl From<&CosmicSurface> for ActivationKey { + fn from(value: &CosmicSurface) -> Self { + match value.0.underlying_surface() { + WindowSurface::Wayland(toplevel) => { + ActivationKey::Wayland(toplevel.wl_surface().clone()) + } + WindowSurface::X11(s) => ActivationKey::X11(s.window_id()), + } + } +} + +#[derive(Debug)] +pub struct PendingWindow { + pub surface: CosmicSurface, + pub seat: Seat, + pub fullscreen: Option, + pub maximized: bool, + pub sticky: bool, +} + +#[derive(Debug)] +pub struct PendingLayer { + pub surface: LayerSurface, + pub seat: Seat, + pub output: Output, +} + +#[derive(Debug)] +pub struct Shell { + pub workspaces: Workspaces, + + // Can't make this into a HashSet. See https://github.com/pop-os/cosmic-comp/pull/1902 + pub pending_windows: Vec, + pub pending_layers: Vec, + pub pending_activations: HashMap, + pub override_redirect_windows: Vec, + pub session_lock: Option, + pub seats: Seats, + pub previous_workspace_idx: Option<(Serial, WeakOutput, usize)>, + pub xwayland_keyboard_grab: Option>, + + theme: cosmic::Theme, + pub active_hint: bool, + overview_mode: OverviewMode, + swap_indicator: Option, + resize_mode: ResizeMode, + resize_state: Option<( + KeyboardFocusTarget, + ResizeDirection, + ResizeEdge, + i32, + usize, + Output, + )>, + resize_indicator: Option, + zoom_state: Option, + appearance_conf: AppearanceConfig, + tiling_exceptions: TilingExceptions, + + #[cfg(feature = "debug")] + pub debug_active: bool, +} + +#[derive(Debug)] +pub struct SessionLock { + pub ext_session_lock: ExtSessionLockV1, + pub surfaces: HashMap, +} + +#[derive(Debug, Clone, Copy)] +pub enum WorkspaceDelta { + Shortcut(Instant), + Gesture { + percentage: f64, + forward: bool, + }, + GestureEnd { + start: Instant, + spring: Spring, + forward: bool, + }, + // InvalidGesture(f64), TODO + // InvalidGestureEnd(Instant, Spring), TODO +} + +impl WorkspaceDelta { + pub fn new_gesture(forward: bool) -> Self { + WorkspaceDelta::Gesture { + percentage: 0.0, + forward, + } + } + + pub fn new_gesture_end(delta: f64, velocity: f64, forward: bool) -> Self { + let params: SpringParams = SpringParams::new(1.0, 1000.0, 0.0001); + WorkspaceDelta::GestureEnd { + start: Instant::now(), + forward, + spring: Spring { + from: delta, + to: 1.0, + initial_velocity: velocity, + params, + }, + } + } + + pub fn new_shortcut() -> Self { + WorkspaceDelta::Shortcut(Instant::now()) + } + + pub fn is_animating(&self) -> bool { + matches!( + self, + WorkspaceDelta::Shortcut(_) | WorkspaceDelta::GestureEnd { .. } + ) + } +} + +#[derive(Debug)] +pub struct WorkspaceSet { + previously_active: Option<(usize, WorkspaceDelta)>, + pub active: usize, + pub group: WorkspaceGroupHandle, + tiling_enabled: bool, + output: Output, + theme: cosmic::Theme, + appearance: AppearanceConfig, + pub sticky_layer: FloatingLayout, + pub minimized_windows: Vec, + pub workspaces: Vec, +} + +fn create_workspace( + state: &mut WorkspaceUpdateGuard<'_, State>, + output: &Output, + group_handle: &WorkspaceGroupHandle, + active: bool, + tiling: bool, + theme: cosmic::Theme, + appearance: AppearanceConfig, +) -> Workspace { + let workspace_handle = state + .create_workspace( + group_handle, + if tiling { + TilingState::TilingEnabled + } else { + TilingState::FloatingOnly + }, + // TODO Set id for persistent workspaces + None, + ) + .unwrap(); + if active { + state.add_workspace_state(&workspace_handle, WState::Active); + } + state.set_workspace_capabilities( + &workspace_handle, + WorkspaceCapabilities::Activate + | WorkspaceCapabilities::SetTilingState + | WorkspaceCapabilities::Pin + | WorkspaceCapabilities::Move, + ); + Workspace::new( + workspace_handle, + output.clone(), + tiling, + theme.clone(), + appearance, + ) +} + +fn create_workspace_from_pinned( + pinned: &PinnedWorkspace, + state: &mut WorkspaceUpdateGuard<'_, State>, + output: &Output, + group_handle: &WorkspaceGroupHandle, + active: bool, + theme: cosmic::Theme, + appearance: AppearanceConfig, +) -> Workspace { + let workspace_handle = state + .create_workspace( + group_handle, + if pinned.tiling_enabled { + TilingState::TilingEnabled + } else { + TilingState::FloatingOnly + }, + pinned.id.clone(), + ) + .unwrap(); + state.add_workspace_state(&workspace_handle, WState::Pinned); + if active { + state.add_workspace_state(&workspace_handle, WState::Active); + } + state.set_workspace_capabilities( + &workspace_handle, + WorkspaceCapabilities::Activate + | WorkspaceCapabilities::SetTilingState + | WorkspaceCapabilities::Pin + | WorkspaceCapabilities::Move, + ); + + if let Some(ref name) = pinned.name { + state.set_workspace_name(&workspace_handle, name); + } + + Workspace::from_pinned( + pinned, + workspace_handle, + output.clone(), + theme.clone(), + appearance, + ) +} + +/* We will probably need this again at some point +fn merge_workspaces( + mut workspace: Workspace, + into: &mut Workspace, + workspace_state: &mut WorkspaceUpdateGuard<'_, State>, + toplevel_info_state: &mut ToplevelInfoState, +) { + if into.fullscreen.is_some() { + // Don't handle the returned original workspace, for this nieche case. + let _ = workspace.remove_fullscreen(); + } + + for element in workspace.mapped() { + // fixup toplevel state + for (toplevel, _) in element.windows() { + toplevel_info_state.toplevel_leave_workspace(&toplevel, &workspace.handle); + toplevel_info_state.toplevel_enter_workspace(&toplevel, &into.handle); + } + } + // TODO: merge minimized windows + into.tiling_layer.merge(workspace.tiling_layer); + into.floating_layer.merge(workspace.floating_layer); + workspace_state.remove_workspace(workspace.handle); +} +*/ + +impl WorkspaceSet { + fn new( + state: &mut WorkspaceUpdateGuard<'_, State>, + output: &Output, + tiling_enabled: bool, + theme: &cosmic::Theme, + appearance: AppearanceConfig, + ) -> WorkspaceSet { + let group_handle = state.create_workspace_group(); + let sticky_layer = FloatingLayout::new(theme.clone(), appearance, output); + + WorkspaceSet { + previously_active: None, + active: 0, + group: group_handle, + tiling_enabled, + theme: theme.clone(), + sticky_layer, + minimized_windows: Vec::new(), + workspaces: Vec::new(), + output: output.clone(), + appearance, + } + } + + fn activate( + &mut self, + idx: usize, + workspace_delta: WorkspaceDelta, + state: &mut WorkspaceUpdateGuard<'_, State>, + ) -> Result { + if idx >= self.workspaces.len() { + return Err(InvalidWorkspaceIndex); + } + + // Animate if workspaces overview isn't open + let layer_map = layer_map_for_output(&self.output); + let animate = !layer_map + .layers() + .any(|l| l.namespace() == WORKSPACE_OVERVIEW_NAMESPACE); + + if self.active != idx { + let old_active = self.active; + state.remove_workspace_state(&self.workspaces[old_active].handle, WState::Active); + state.remove_workspace_state(&self.workspaces[old_active].handle, WState::Urgent); + state.remove_workspace_state(&self.workspaces[idx].handle, WState::Urgent); + state.add_workspace_state(&self.workspaces[idx].handle, WState::Active); + self.previously_active = if animate { + Some((old_active, workspace_delta)) + } else { + None + }; + self.active = idx; + Ok(true) + } else { + // snap to workspace, when in between workspaces due to swipe gesture + if let Some((p_idx, p_delta)) = self.previously_active { + if matches!(p_delta, WorkspaceDelta::Gesture { .. }) + && matches!(workspace_delta, WorkspaceDelta::GestureEnd { .. }) + { + self.previously_active = Some((p_idx, workspace_delta)); + } else { + self.previously_active = None; + } + return Ok(true); + } + Ok(false) + } + } + + fn activate_previous( + &mut self, + workspace_delta: WorkspaceDelta, + state: &mut WorkspaceUpdateGuard<'_, State>, + ) -> Result { + if let Some((idx, _)) = self.previously_active { + return self.activate(idx, workspace_delta, state); + } + Err(InvalidWorkspaceIndex) + } + + fn update_workspace_delta(&mut self, delta: f64, forward: bool) { + let easing = delta.clamp(0.0, GESTURE_MAX_LENGTH).abs() / GESTURE_MAX_LENGTH; + if let Some((idx, _)) = self.previously_active { + self.previously_active = Some(( + idx, + WorkspaceDelta::Gesture { + percentage: easing, + forward, + }, + )); + } + } + + fn set_output(&mut self, new_output: &Output, explicit: bool) { + self.sticky_layer.set_output(new_output); + for window in self.sticky_layer.windows() { + toplevel_leave_output(&window, &self.output); + toplevel_enter_output(&window, new_output); + } + for workspace in &mut self.workspaces { + workspace.set_output(new_output, explicit); + } + self.output = new_output.clone(); + } + + fn refresh(&mut self) { + if let Some((_, start)) = self.previously_active { + match start { + WorkspaceDelta::Shortcut(st) => { + if Instant::now().duration_since(st).as_millis() as f32 + >= ANIMATION_DURATION.as_millis() as f32 + { + self.previously_active = None; + } + } + WorkspaceDelta::GestureEnd { start, spring, .. } => { + if Instant::now().duration_since(start).as_millis() + > spring.duration().as_millis() + { + self.previously_active = None; + } + } + _ => {} + } + } else { + self.workspaces[self.active].refresh(); + } + self.sticky_layer.refresh(); + } + + fn add_empty_workspace(&mut self, state: &mut WorkspaceUpdateGuard) { + let workspace = create_workspace( + state, + &self.output, + &self.group, + false, + self.tiling_enabled, + self.theme.clone(), + self.appearance, + ); + workspace_set_idx( + state, + self.workspaces.len() as u8 + 1, + &workspace.handle, + workspace.name.as_deref(), + // this method is only used by code paths related to dynamic workspaces, so this should be fine + ); + self.workspaces.push(workspace); + } + + fn ensure_last_empty( + &mut self, + state: &mut WorkspaceUpdateGuard, + xdg_activation_state: &XdgActivationState, + ) { + // add empty at the end, if necessary + if self + .workspaces + .last() + .is_none_or(|last| !last.is_empty() || last.pinned) + { + self.add_empty_workspace(state); + } + + // remove other empty workspaces + let len = self.workspaces.len(); + let kept: Vec = self + .workspaces + .iter() + .enumerate() + .map(|(i, workspace)| { + let previous_is_empty = i > 0 + && self + .workspaces + .get(i - 1) + .is_some_and(|w| w.is_empty() && !w.pinned); + let keep = if workspace.can_auto_remove(xdg_activation_state) { + // Keep empty workspace if it's active, or it's the last workspace, + // and the previous worspace is not both active and empty. + i == self.active + || (i == len - 1 && !(i == self.active + 1 && previous_is_empty)) + } else { + true + }; + if !keep { + state.remove_workspace(workspace.handle); + } + keep + }) + .collect(); + + let mut iter = kept.iter(); + self.workspaces.retain(|_| *iter.next().unwrap()); + self.active -= kept + .iter() + .take(self.active + 1) + .filter(|kept| !**kept) + .count(); + + if kept.iter().any(|val| !(*val)) { + self.update_workspace_idxs(state); + } + } + + fn update_workspace_idxs(&self, state: &mut WorkspaceUpdateGuard<'_, State>) { + for (i, workspace) in self.workspaces.iter().enumerate() { + workspace_set_idx( + state, + i as u8 + 1, + &workspace.handle, + workspace.name.as_deref(), + ); + } + } + + fn post_remove_workspace( + &mut self, + workspace_state: &mut WorkspaceUpdateGuard<'_, State>, + previous_active_handle: &WorkspaceHandle, + ) { + if self.workspaces.is_empty() { + self.add_empty_workspace(workspace_state); + } + self.update_workspace_idxs(workspace_state); + self.active = self + .workspaces + .iter() + .position(|w| w.handle == *previous_active_handle) + .unwrap_or_else(|| { + let idx = self.workspaces.len() - 1; + let workspace = &self.workspaces[idx]; + workspace_state.add_workspace_state(&workspace.handle, WState::Active); + idx + }); + } + + // Remove a workspace from the set, and return it, for adding to a different + // workspace set + fn remove_workspace( + &mut self, + workspace_state: &mut WorkspaceUpdateGuard<'_, State>, + handle: &WorkspaceHandle, + ) -> Option { + let previous_active_handle = self.workspaces[self.active].handle; + let idx = self.workspaces.iter().position(|w| w.handle == *handle)?; + let workspace = self.workspaces.remove(idx); + self.post_remove_workspace(workspace_state, &previous_active_handle); + Some(workspace) + } + + // Remove all workspaces matched by the callback from the set + fn remove_workspaces( + &mut self, + workspace_state: &mut WorkspaceUpdateGuard<'_, State>, + cb: impl Fn(&Workspace) -> bool, + ) -> Vec { + let previous_active_handle = self.workspaces[self.active].handle; + let (prefers, doesnt) = self.workspaces.drain(..).partition(cb); + self.workspaces = doesnt; + self.post_remove_workspace(workspace_state, &previous_active_handle); + prefers + } + + pub fn surface_geometry_offset_from_toplevel( + &self, + surface: &WlSurface, + ) -> Option<(Rectangle, Point)> { + let mut root = surface.clone(); + + while let Some(parent) = get_parent(&root) { + root = parent; + } + + while smithay::wayland::compositor::get_role(&root) == Some(XDG_POPUP_ROLE) { + let parent = with_states(&root, |states| { + states + .data_map + .get::() + .and_then(|m| m.lock().unwrap().parent.as_ref().cloned()) + }); + if let Some(parent) = parent { + root = parent; + } else { + break; + } + } + + self.sticky_layer + .mapped() + .find(|w| { + w.windows() + .any(|(w, _)| w.wl_surface().as_deref() == Some(&root)) + }) + .and_then(|w| { + self.sticky_layer + .element_geometry(w) + .zip(w.surface_offset(surface)) + }) + .or_else(|| { + self.workspaces.iter().find_map(|workspace| { + workspace + .get_fullscreen_surfaces() + .find_map(|fs| { + (fs.surface.wl_surface().as_deref() == Some(&root)) + .then(|| { + fs.surface.surface_offset(surface).map(|offset| { + (workspace.fullscreen_geometry_for(fs), offset) + }) + }) + .flatten() + }) + .or_else(|| { + workspace.mapped().find_map(|w| { + w.windows() + .any(|(w, _)| w.wl_surface().as_deref() == Some(&root)) + .then(|| { + workspace.element_geometry(w).zip(w.surface_offset(surface)) + }) + .flatten() + }) + }) + }) + }) + .or_else(|| { + layer_map_for_output(&self.output).layers().find_map(|l| { + (l.wl_surface() == &root) + .then(|| { + CosmicSurface::surface_tree_offset(l.wl_surface(), surface) + .map(|offset| (l.geometry().as_local(), offset)) + }) + .flatten() + }) + }) + } +} + +#[derive(Debug)] +pub struct Workspaces { + pub sets: IndexMap, + backup_set: Option, + pub layout: WorkspaceLayout, + mode: WorkspaceMode, + autotile: bool, + autotile_behavior: TileBehavior, + theme: cosmic::Theme, + appearance: AppearanceConfig, + // Persisted workspace to add on first `output_add` + persisted_workspaces: Vec, +} + +impl Workspaces { + pub fn new(config: &Config, theme: cosmic::Theme) -> Workspaces { + Workspaces { + sets: IndexMap::new(), + backup_set: None, + layout: config.cosmic_conf.workspaces.workspace_layout, + mode: config.cosmic_conf.workspaces.workspace_mode, + autotile: config.cosmic_conf.autotile, + autotile_behavior: config.cosmic_conf.autotile_behavior, + theme, + appearance: config.cosmic_conf.appearance_settings, + persisted_workspaces: config.cosmic_conf.pinned_workspaces.clone(), + } + } + + pub fn add_output( + &mut self, + output: &Output, + workspace_state: &mut WorkspaceUpdateGuard<'_, State>, + ) { + if self.sets.contains_key(output) { + return; + } + + let mut set = self + .backup_set + .take() + .map(|mut set| { + set.set_output(output, false); + set + }) + .unwrap_or_else(|| { + WorkspaceSet::new( + workspace_state, + output, + self.autotile, + &self.theme, + self.appearance, + ) + }); + workspace_state.add_group_output(&set.group, output); + + // If this is the first output added, create workspaces for pinned workspaces from config + for pinned in std::mem::take(&mut self.persisted_workspaces) { + let workspace = create_workspace_from_pinned( + &pinned, + workspace_state, + output, + &set.group, + false, + self.theme.clone(), + self.appearance, + ); + set.workspaces.push(workspace); + } + + // Remove workspaces that prefer this output from other sets + let mut moved_workspaces = self + .sets + .values_mut() + .flat_map(|other_set| { + other_set.remove_workspaces(workspace_state, |w| w.prefers_output(output)) + }) + .collect::>(); + + // Add `moved_workspaces` to set, and update output and index of workspaces + for workspace in &mut moved_workspaces { + workspace_state.remove_workspace_state(&workspace.handle, WState::Active); + workspace_state.move_workspace_to_group(set.group, workspace.handle); + } + set.workspaces.extend(moved_workspaces); + if set.workspaces.is_empty() { + set.add_empty_workspace(workspace_state); + } + set.update_workspace_idxs(workspace_state); + for (i, workspace) in set.workspaces.iter_mut().enumerate() { + workspace.set_output(output, false); + workspace.refresh(); + if i == set.active { + workspace_state.add_workspace_state(&workspace.handle, WState::Active); + } + } + self.sets.insert(output.clone(), set); + } + + pub fn remove_output<'a>( + &mut self, + output: &Output, + seats: impl Iterator>, + workspace_state: &mut WorkspaceUpdateGuard<'_, State>, + xdg_activation_state: &XdgActivationState, + ) { + if !self.sets.contains_key(output) { + return; + } + + if let Some(set) = self.sets.shift_remove(output) { + { + let map = layer_map_for_output(output); + for surface in map.layers() { + surface.layer_surface().send_close(); + } + } + + // TODO: Heuristic which output to move to. + // It is supposed to be the *most* internal, we just pick the first one for now + // and hope enumeration order works in our favor. + let new_output = self.sets.get_index(0).map(|(o, _)| o.clone()); + if let Some(new_output) = new_output { + for seat in seats { + if &seat.active_output() == output { + seat.set_active_output(&new_output); + } + if seat.focused_output().as_ref() == Some(output) { + seat.set_focused_output(None); + } + } + + let new_set = self.sets.get_mut(&new_output).unwrap(); + let workspace_group = new_set.group; + for (i, mut workspace) in set.workspaces.into_iter().enumerate() { + if workspace.can_auto_remove(xdg_activation_state) { + workspace_state.remove_workspace(workspace.handle); + } else { + // update workspace protocol state + workspace_state.remove_workspace_state(&workspace.handle, WState::Active); + workspace_state.move_workspace_to_group(workspace_group, workspace.handle); + + // update mapping + workspace.set_output(&new_output, false); + workspace.refresh(); + new_set.workspaces.push(workspace); + + // If workspace was active, and the new set's active workspace is empty, make this workspace + // active on the new set. Instead of leaving an empty workspace active, and a previously active + // workspace hidden. + if i == set.active && new_set.workspaces[new_set.active].is_empty() { + workspace_state.remove_workspace_state( + &new_set.workspaces[new_set.active].handle, + WState::Active, + ); + new_set.active = new_set.workspaces.len() - 1; + workspace_state.add_workspace_state( + &new_set.workspaces[new_set.active].handle, + WState::Active, + ); + } + } + } + + for window in set.sticky_layer.mapped() { + for (surface, _) in window.windows() { + toplevel_leave_output(&surface, output); + toplevel_enter_output(&surface, &new_output); + } + } + new_set.sticky_layer.merge(set.sticky_layer); + for window in set.minimized_windows.iter() { + for surface in window.windows() { + toplevel_leave_output(&surface, output); + toplevel_enter_output(&surface, &new_output); + } + } + new_set.minimized_windows.extend(set.minimized_windows); + + if self.mode == WorkspaceMode::OutputBound { + workspace_state.remove_workspace_group(set.group); + } else { + workspace_state.remove_group_output(&workspace_group, output); + } + } else { + workspace_state.remove_group_output(&set.group, output); + self.backup_set = Some(set); + } + + self.refresh(workspace_state, xdg_activation_state) + } + } + + // Move workspace from one output to another, explicitly by the user + pub fn migrate_workspace( + &mut self, + from: &Output, + to: &Output, + handle: &WorkspaceHandle, + workspace_state: &mut WorkspaceUpdateGuard<'_, State>, + ) { + // If workspaces span across outputs, they can't be moved to a different output + if self.mode == WorkspaceMode::Global { + return; + } + + if !self.sets.contains_key(to) || from == to { + return; + } + + if let Some(mut workspace) = self + .sets + .get_mut(from) + .and_then(|set| set.remove_workspace(workspace_state, handle)) + { + let new_set = self.sets.get_mut(to).unwrap(); + workspace_state.remove_workspace_state(&workspace.handle, WState::Active); + workspace_state.move_workspace_to_group(new_set.group, workspace.handle); + workspace.set_output(to, true); + workspace.refresh(); + new_set.workspaces.insert(new_set.active + 1, workspace); + new_set.update_workspace_idxs(workspace_state); + } + } + + // Move a workspace before/after a different workspace + pub fn move_workspace( + &mut self, + handle: &WorkspaceHandle, + other_handle: &WorkspaceHandle, + workspace_state: &mut WorkspaceUpdateGuard<'_, State>, + after: bool, + ) { + if handle == other_handle { + return; + } + + let (Some(old_output), Some(new_output)) = ( + self.space_for_handle(handle).map(|w| w.output.clone()), + self.space_for_handle(other_handle) + .map(|w| w.output.clone()), + ) else { + return; + }; + + match self.mode { + WorkspaceMode::OutputBound => { + // Check which workspace is active on the new set; before removing from the + // old set in cause we're moving an active workspace within the same set. + let new_set = &mut self.sets[&new_output]; + let previous_active_handle = new_set.workspaces[new_set.active].handle; + + // Remove workspace from old set + let old_set = &mut self.sets[&old_output]; + let mut workspace = if new_output != old_output { + old_set.remove_workspace(workspace_state, handle).unwrap() + } else { + // If set is the same, just remove it here without adding empty workspace, + // updating `active`, etc. + let idx = old_set + .workspaces + .iter() + .position(|w| w.handle == *handle) + .unwrap(); + old_set.workspaces.remove(idx) + }; + + let new_set = &mut self.sets[&new_output]; + + if new_output != old_output { + workspace_state.remove_workspace_state(&workspace.handle, WState::Active); + workspace_state.move_workspace_to_group(new_set.group, workspace.handle); + workspace.set_output(&new_output, true); + workspace.refresh(); + } + + // Insert workspace into new set, relative to `other_handle` + let idx = new_set + .workspaces + .iter() + .position(|w| w.handle == *other_handle) + .unwrap(); + let insert_idx = if after { idx + 1 } else { idx }; + new_set.workspaces.insert(insert_idx, workspace); + + new_set.active = new_set + .workspaces + .iter() + .position(|w| w.handle == previous_active_handle) + .unwrap(); + + new_set.update_workspace_idxs(workspace_state); + } + WorkspaceMode::Global => { + let old_set = &mut self.sets[&old_output]; + let old_idx = old_set + .workspaces + .iter() + .position(|w| w.handle == *handle) + .unwrap(); + + let new_set = &mut self.sets[&new_output]; + let other_idx = new_set + .workspaces + .iter() + .position(|w| w.handle == *other_handle) + .unwrap(); + + // Move workspace at given index on every output + for set in self.sets.values_mut() { + if old_idx < set.workspaces.len() && other_idx < set.workspaces.len() { + let previous_active_handle = set.workspaces[set.active].handle; + + if other_idx > old_idx { + let insert_idx = if after { other_idx } else { other_idx - 1 }; + set.workspaces[old_idx..=insert_idx].rotate_left(1); + } else { + let insert_idx = if after { other_idx + 1 } else { other_idx }; + set.workspaces[insert_idx..=old_idx].rotate_right(1); + } + + set.active = set + .workspaces + .iter() + .position(|w| w.handle == previous_active_handle) + .unwrap(); + + set.update_workspace_idxs(workspace_state); + } + } + } + } + } + + pub fn update_config( + &mut self, + config: &Config, + workspace_state: &mut WorkspaceUpdateGuard<'_, State>, + xdg_activation_state: &XdgActivationState, + ) { + let old_mode = self.mode; + self.mode = config.cosmic_conf.workspaces.workspace_mode; + self.layout = config.cosmic_conf.workspaces.workspace_layout; + self.appearance = config.cosmic_conf.appearance_settings; + + for set in self.sets.values_mut() { + set.appearance = self.appearance; + set.sticky_layer.appearance = self.appearance; + for workspace in set.workspaces.iter_mut() { + workspace.floating_layer.appearance = self.appearance; + workspace.tiling_layer.appearance = self.appearance; + } + } + + if self.sets.len() <= 1 { + return; + } + + match (old_mode, self.mode) { + (WorkspaceMode::Global, WorkspaceMode::OutputBound) => { + // We basically just unlink the existing spaces, so nothing needs to be updated + } + (WorkspaceMode::OutputBound, WorkspaceMode::Global) => { + // lets construct an iterator of all the pairs of workspaces we have to "merge" + let mut pairs = Vec::new(); + if let Some(max) = self.sets.values().map(|set| set.workspaces.len()).max() { + let offset = self.sets.values().map(|set| set.active).max().unwrap(); + for i in 0..max { + pairs.push( + self.sets + .values() + .map(|set| { + let idx = set.active as isize + i as isize - offset as isize; + if idx < 0 || idx >= set.workspaces.len() as isize { + None + } else { + Some(idx) + } + }) + .collect::>(), + ); + } + } + + for (j, pair) in pairs.iter().enumerate() { + for (i, x) in pair.iter().enumerate() { + // Fill up sets, where necessary + if x.is_none() { + // create missing workspace + let (output, set) = self.sets.get_index_mut(i).unwrap(); + set.workspaces.insert( + j, + create_workspace( + workspace_state, + output, + &set.group, + false, + config.cosmic_conf.autotile, + self.theme.clone(), + self.appearance, + ), + ); + } + // Otherwise we are fine + } + } + } + _ => {} + }; + + self.refresh(workspace_state, xdg_activation_state) + } + + pub fn recalculate(&mut self) { + for set in self.sets.values_mut() { + set.sticky_layer.recalculate(); + set.workspaces.iter_mut().for_each(|w| w.recalculate()); + } + } + + pub fn refresh( + &mut self, + workspace_state: &mut WorkspaceUpdateGuard<'_, State>, + xdg_activation_state: &XdgActivationState, + ) { + match self.mode { + WorkspaceMode::Global => { + let Some(max) = self.sets.values().map(|set| set.workspaces.len()).max() else { + return; + }; + + for set in self + .sets + .values_mut() + .filter(|set| set.workspaces.len() < max) + { + while set.workspaces.len() < max { + set.add_empty_workspace(workspace_state) + } + } + + // add empty at the end, if necessary + if self + .sets + .values() + .flat_map(|set| set.workspaces.last()) + .any(|w| !w.is_empty() || w.pinned) + { + for set in self.sets.values_mut() { + set.add_empty_workspace(workspace_state); + } + } + + // remove empty workspaces in between, if they are not active + let len = self.sets[0].workspaces.len(); + let mut active = self.sets[0].active; + let mut keep = vec![true; len]; + // false-positive: we iterate over multiple sets + #[allow(clippy::needless_range_loop)] + for i in 0..len { + let has_windows = self + .sets + .values() + .any(|s| !s.workspaces[i].can_auto_remove(xdg_activation_state)); + + if !has_windows && i != active && i != len - 1 { + for workspace in self.sets.values().map(|s| &s.workspaces[i]) { + workspace_state.remove_workspace(workspace.handle); + } + keep[i] = false; + } + } + + self.sets.values_mut().for_each(|s| { + let mut iter = keep.iter(); + s.workspaces.retain(|_| *iter.next().unwrap()); + }); + active -= keep.iter().take(active + 1).filter(|keep| !**keep).count(); + self.sets.values_mut().for_each(|s| { + s.active = active; + }); + + if keep.iter().any(|val| !(*val)) { + for set in self.sets.values_mut() { + set.update_workspace_idxs(workspace_state); + } + } + } + WorkspaceMode::OutputBound => { + for set in self.sets.values_mut() { + set.ensure_last_empty(workspace_state, xdg_activation_state); + } + } + } + + for set in self.sets.values_mut() { + set.refresh() + } + } + + pub fn get(&self, num: usize, output: &Output) -> Option<&Workspace> { + self.sets + .get(output) + .and_then(|set| set.workspaces.get(num)) + } + + pub fn get_mut(&mut self, num: usize, output: &Output) -> Option<&mut Workspace> { + self.sets + .get_mut(output) + .and_then(|set| set.workspaces.get_mut(num)) + } + + pub fn active( + &self, + output: &Output, + ) -> Option<(Option<(&Workspace, WorkspaceDelta)>, &Workspace)> { + self.sets + .get(output) + .or(self.backup_set.as_ref()) + .map(|set| { + ( + set.previously_active.and_then(|(idx, start)| { + set.workspaces.get(idx).map(|previous| (previous, start)) + }), + &set.workspaces[set.active], + ) + }) + } + + pub fn active_mut(&mut self, output: &Output) -> Option<&mut Workspace> { + self.sets + .get_mut(output) + .or(self.backup_set.as_mut()) + .map(|set| &mut set.workspaces[set.active]) + } + + pub fn active_num(&self, output: &Output) -> (Option, usize) { + let set = self.sets.get(output).or(self.backup_set.as_ref()).unwrap(); + (set.previously_active.map(|(idx, _)| idx), set.active) + } + + pub fn idx_for_handle(&self, output: &Output, handle: &WorkspaceHandle) -> Option { + let set = self.sets.get(output).unwrap(); + set.workspaces + .iter() + .enumerate() + .find_map(|(i, w)| (&w.handle == handle).then_some(i)) + } + + pub fn len(&self, output: &Output) -> usize { + let set = self.sets.get(output).unwrap(); + set.workspaces.len() + } + + pub fn iter(&self) -> impl Iterator { + self.sets.iter() + } + + pub fn iter_mut(&mut self) -> impl Iterator { + self.sets.iter_mut() + } + + pub fn spaces(&self) -> impl Iterator { + self.sets.values().flat_map(|set| set.workspaces.iter()) + } + + pub fn space_for_handle(&self, handle: &WorkspaceHandle) -> Option<&Workspace> { + self.spaces().find(|w| &w.handle == handle) + } + + pub fn space_for_handle_mut(&mut self, handle: &WorkspaceHandle) -> Option<&mut Workspace> { + self.spaces_mut().find(|w| &w.handle == handle) + } + + pub fn spaces_for_output(&self, output: &Output) -> impl Iterator { + self.sets + .get(output) + .into_iter() + .flat_map(|set| set.workspaces.iter()) + } + + pub fn spaces_mut(&mut self) -> impl Iterator { + Box::new( + self.sets + .values_mut() + .flat_map(|set| set.workspaces.iter_mut()), + ) + } + + pub fn set_theme(&mut self, theme: cosmic::Theme) { + for (_, s) in &mut self.sets { + s.theme = theme.clone(); + + s.sticky_layer.theme = theme.clone(); + s.sticky_layer.mapped().for_each(|m| { + m.update_theme(theme.clone()); + }); + + for w in &mut s.workspaces { + w.tiling_layer.theme = theme.clone(); + w.floating_layer.theme = theme.clone(); + + w.mapped().for_each(|m| { + m.update_theme(theme.clone()); + }); + } + } + + self.force_redraw(); + } + + pub fn force_redraw(&mut self) { + for (_, s) in &mut self.sets { + s.sticky_layer.mapped().for_each(|m| { + m.force_redraw(); + }); + s.sticky_layer.refresh(); + + for w in &mut s.workspaces { + w.mapped().for_each(|m| { + m.force_redraw(); + }); + + w.refresh(); + w.dirty.store(true, Ordering::Relaxed); + w.recalculate(); + } + } + } + + pub fn update_autotile_behavior<'a>( + &mut self, + behavior: TileBehavior, + guard: &mut WorkspaceUpdateGuard<'_, State>, + seats: impl Iterator>, + ) { + self.autotile_behavior = behavior; + self.apply_tile_change(guard, seats); + } + + fn apply_tile_change<'a>( + &mut self, + guard: &mut WorkspaceUpdateGuard<'_, State>, + seats: impl Iterator>, + ) { + let seats = seats.cloned().collect::>(); + for (_, set) in &mut self.sets { + set.tiling_enabled = self.autotile; + + if matches!(self.autotile_behavior, TileBehavior::Global) { + // must apply change to all workspaces now + for w in &mut set.workspaces { + if w.tiling_enabled == self.autotile { + continue; + } + for s in &seats { + w.toggle_tiling(s, guard); + } + } + } + } + } + + pub fn update_autotile<'a>( + &mut self, + autotile: bool, + guard: &mut WorkspaceUpdateGuard<'_, State>, + seats: impl Iterator>, + ) { + self.autotile = autotile; + self.apply_tile_change(guard, seats); + } + + pub fn persist(&self, config: &Config) { + let pinned_workspaces: Vec = self + .sets + .values() + .flat_map(|set| &set.workspaces) + .flat_map(|w| w.to_pinned()) + .collect(); + let config = config.cosmic_helper.clone(); + thread::spawn(move || { + if let Err(err) = config.set("pinned_workspaces", pinned_workspaces) { + error!(?err, "Failed to update pinned_workspaces key"); + } + }); + } +} + +#[derive(Debug)] +pub struct InvalidWorkspaceIndex; + +utils::id_gen!(next_output_id, OUTPUT_ID, OUTPUT_IDS); +pub struct OutputId(usize); + +impl OutputId { + pub fn namespace_for_workspace(&self, idx: usize) -> usize { + self.0 | (idx << 32) + } +} + +impl Drop for OutputId { + fn drop(&mut self) { + OUTPUT_IDS.lock().unwrap().remove(&self.0); + } +} + +impl Common { + pub fn add_output(&mut self, output: &Output) { + let mut shell = self.shell.write(); + shell + .workspaces + .add_output(output, &mut self.workspace_state.update()); + + output + .user_data() + .insert_if_missing_threadsafe(|| OutputId(next_output_id())); + + if let Some(state) = shell.zoom_state.as_ref() { + output.user_data().insert_if_missing_threadsafe(|| { + Mutex::new(OutputZoomState::new( + &state.seat, + output, + 1.0, + state.increment, + state.movement, + self.event_loop_handle.clone(), + shell.theme.clone(), + )) + }); + } + + std::mem::drop(shell); + self.refresh(); // fixes indicies of any moved workspaces + } + + pub fn remove_output(&mut self, output: &Output) { + let mut shell = self.shell.write(); + let shell_ref = &mut *shell; + shell_ref.workspaces.remove_output( + output, + shell_ref.seats.iter(), + &mut self.workspace_state.update(), + &self.xdg_activation_state, + ); + + std::mem::drop(shell); + self.refresh(); // cleans up excess of workspaces and empty workspaces + } + + pub fn update_config(&mut self) { + let mut shell = self.shell.write(); + let shell_ref = &mut *shell; + shell_ref.active_hint = self.config.cosmic_conf.active_hint; + shell_ref.appearance_conf = self.config.cosmic_conf.appearance_settings; + if let Some(zoom_state) = shell_ref.zoom_state.as_mut() { + zoom_state.increment = self.config.cosmic_conf.accessibility_zoom.increment; + zoom_state.movement = self.config.cosmic_conf.accessibility_zoom.view_moves; + zoom_state.show_overlay = self.config.cosmic_conf.accessibility_zoom.show_overlay; + + for output in shell_ref.workspaces.sets.keys() { + let output_state = output.user_data().get::>().unwrap(); + let mut output_state_ref = output_state.lock().unwrap(); + let level = output_state_ref.level; + output_state_ref.update(level, false, zoom_state.movement, zoom_state.increment); + } + } + + let mut workspace_state = self.workspace_state.update(); + shell_ref.workspaces.update_config( + &self.config, + &mut workspace_state, + &self.xdg_activation_state, + ); + + for mapped in shell_ref.mapped() { + mapped.update_appearance_conf(&self.config.cosmic_conf.appearance_settings); + } + } + + #[profiling::function] + pub fn refresh(&mut self) { + self.xdg_activation_state + .retain_tokens(|_, data| data.timestamp.elapsed() < ACTIVATION_TOKEN_EXPIRE_TIME); + self.shell.write().refresh( + &self.xdg_activation_state, + &mut self.workspace_state.update(), + ); + self.popups.cleanup(); + self.toplevel_info_state.refresh(&self.workspace_state); + self.refresh_idle_inhibit(); + if let Some(mut a11y_keyboard_monitor) = self.dbus_state.a11y_keyboard_monitor() { + a11y_keyboard_monitor.refresh(); + } + self.image_copy_capture_state.cleanup(); + } + + pub fn refresh_idle_inhibit(&mut self) { + self.idle_inhibiting_surfaces.retain(|s| s.alive()); + + let is_inhibited = self.idle_inhibiting_surfaces.iter().any(|surface| { + with_states(surface, |states| { + surface_primary_scanout_output(surface, states).is_some() + }) + }); + self.idle_notifier_state.set_is_inhibited(is_inhibited); + } + + #[profiling::function] + pub fn on_commit(&mut self, surface: &WlSurface) { + { + let shell = self.shell.read(); + + for seat in shell.seats.iter() { + if let Some(move_grab) = seat.user_data().get::() + && let Some(grab_state) = move_grab.lock().unwrap().as_ref() + { + let mapped = grab_state.element(); + if mapped.active_window().wl_surface().as_deref() == Some(surface) { + mapped.on_commit(surface); + } + } + + data_device::on_commit(surface, seat); + } + + let is_cursor_image = shell.seats.iter().any(|seat| { + matches!(seat.cursor_image_status(), CursorImageStatus::Surface(ref cursor_surface) if cursor_surface == surface) + }); + + if is_cursor_image { + with_states(surface, |states| { + let cursor_image_attributes = states.data_map.get::(); + + if let Some(mut cursor_image_attributes) = + cursor_image_attributes.map(|attrs| attrs.lock().unwrap()) + { + let buffer_delta = states + .cached_state + .get::() + .current() + .buffer_delta + .take(); + if let Some(buffer_delta) = buffer_delta { + cursor_image_attributes.hotspot -= buffer_delta; + } + } + }); + } + + if let Some(mapped) = shell.element_for_surface(surface) { + mapped.on_commit(surface); + } + if let Some(fs) = shell + .workspaces + .spaces() + .flat_map(|w| w.get_fullscreen_surfaces()) + .find(|f| f.surface == *surface) + { + fs.surface.on_commit() + }; + } + self.popups.commit(surface); + } +} + +impl Shell { + pub fn new(config: &Config) -> Self { + let theme = cosmic::theme::system_preference(); + + let tiling_exceptions = layout::TilingExceptions::new(config.tiling_exceptions.iter()); + + Shell { + workspaces: Workspaces::new(config, theme.clone()), + seats: Seats::new(), + + pending_windows: Vec::new(), + pending_layers: Vec::new(), + pending_activations: HashMap::new(), + override_redirect_windows: Vec::new(), + session_lock: None, + previous_workspace_idx: None, + xwayland_keyboard_grab: None, + + theme, + active_hint: config.cosmic_conf.active_hint, + overview_mode: OverviewMode::None, + swap_indicator: None, + resize_mode: ResizeMode::None, + resize_state: None, + resize_indicator: None, + appearance_conf: config.cosmic_conf.appearance_settings, + zoom_state: None, + tiling_exceptions, + + #[cfg(feature = "debug")] + debug_active: false, + } + } + + pub fn activate( + &mut self, + output: &Output, + idx: usize, + workspace_delta: WorkspaceDelta, + workspace_state: &mut WorkspaceUpdateGuard<'_, State>, + ) -> Result, InvalidWorkspaceIndex> { + match &mut self.workspaces.mode { + WorkspaceMode::OutputBound => { + if let Some(set) = self.workspaces.sets.get_mut(output) { + if matches!( + self.overview_mode.active_trigger(), + Some(Trigger::Pointer(_) | Trigger::Touch(_)) + ) { + set.workspaces[set.active].tiling_layer.cleanup_drag(); + } + set.activate(idx, workspace_delta, workspace_state)?; + + let output_geo = output.geometry(); + Ok( + output_geo.loc + + Point::from((output_geo.size.w / 2, output_geo.size.h / 2)), + ) + } else { + Err(InvalidWorkspaceIndex) + } + } + WorkspaceMode::Global => { + for set in self.workspaces.sets.values_mut() { + set.activate(idx, workspace_delta, workspace_state)?; + } + let output_geo = output.geometry(); + Ok(output_geo.loc + Point::from((output_geo.size.w / 2, output_geo.size.h / 2))) + } + } + } + + pub fn update_workspace_delta(&mut self, output: &Output, delta: f64, forward: bool) { + match &mut self.workspaces.mode { + WorkspaceMode::OutputBound => { + if let Some(set) = self.workspaces.sets.get_mut(output) { + set.update_workspace_delta(delta, forward); + } + } + WorkspaceMode::Global => { + for set in self.workspaces.sets.values_mut() { + set.update_workspace_delta(delta, forward); + } + } + } + } + + pub fn end_workspace_swipe( + &mut self, + output: &Output, + velocity: f64, + workspace_state: &mut WorkspaceUpdateGuard<'_, State>, + ) -> Result, InvalidWorkspaceIndex> { + match &mut self.workspaces.mode { + WorkspaceMode::OutputBound => { + if let Some(set) = self.workspaces.sets.get_mut(output) { + if matches!( + self.overview_mode.active_trigger(), + Some(Trigger::Pointer(_) | Trigger::Touch(_)) + ) { + set.workspaces[set.active].tiling_layer.cleanup_drag(); + } + if let Some(( + _, + WorkspaceDelta::Gesture { + percentage: delta, + forward, + }, + )) = set.previously_active + { + if (velocity > 0.0 && velocity.abs() >= GESTURE_VELOCITY_THRESHOLD) + || (velocity.abs() < GESTURE_VELOCITY_THRESHOLD + && delta.abs() > GESTURE_POSITION_THRESHOLD) + { + set.activate( + set.active, + WorkspaceDelta::new_gesture_end( + delta.abs(), + velocity.abs(), + forward, + ), + workspace_state, + )?; + } else { + set.activate_previous( + WorkspaceDelta::new_gesture_end( + 1.0 - delta.abs(), + velocity.abs(), + !forward, + ), + workspace_state, + )?; + } + } + + let output_geo = output.geometry(); + Ok( + output_geo.loc + + Point::from((output_geo.size.w / 2, output_geo.size.h / 2)), + ) + } else { + Err(InvalidWorkspaceIndex) + } + } + WorkspaceMode::Global => { + for set in self.workspaces.sets.values_mut() { + if let Some(( + _, + WorkspaceDelta::Gesture { + percentage: delta, + forward, + }, + )) = set.previously_active + { + if (velocity > 0.0 && velocity.abs() >= GESTURE_VELOCITY_THRESHOLD) + || (velocity.abs() < GESTURE_VELOCITY_THRESHOLD + && delta.abs() > GESTURE_POSITION_THRESHOLD) + { + set.activate( + set.active, + WorkspaceDelta::new_gesture_end( + delta.abs(), + velocity.abs(), + forward, + ), + workspace_state, + )?; + } else { + set.activate_previous( + WorkspaceDelta::new_gesture_end( + 1.0 - delta.abs(), + velocity.abs(), + !forward, + ), + workspace_state, + )?; + } + } + } + Err(InvalidWorkspaceIndex) + } + } + } + + pub fn active_space(&self, output: &Output) -> Option<&Workspace> { + self.workspaces.active(output).map(|(_, active)| active) + } + + pub fn active_space_mut(&mut self, output: &Output) -> Option<&mut Workspace> { + self.workspaces.active_mut(output) + } + + /// get the parent output of the window which has keyboard focus (for a given seat) + pub fn get_output_for_focus(&self, seat: &Seat) -> Option { + let mut focus_target = seat.get_keyboard().unwrap().current_focus()?; + + if let KeyboardFocusTarget::Popup(popup) = &focus_target { + let new_target = match popup { + PopupKind::Xdg(popup) => { + if let Some(parent) = popup.get_parent_surface() { + self.element_for_surface(&parent).cloned() + } else { + None + } + } + PopupKind::InputMethod(popup) => { + if let Some(parent) = popup.get_parent() { + self.element_for_surface(&parent.surface).cloned() + } else { + None + } + } + }?; + + focus_target = KeyboardFocusTarget::Element(new_target); + } + + match focus_target { + KeyboardFocusTarget::Element(elem) => { + if seat + .user_data() + .get::() + .is_some_and(|state| { + state + .lock() + .unwrap() + .as_ref() + .is_some_and(|state| state.element() == elem) + }) + { + return Some(seat.active_output()); + } + + self.outputs() + .find(|output| { + let is_sticky = self + .workspaces + .sets + .get(*output) + .unwrap() + .sticky_layer + .mapped() + .any(|m| m == &elem); + + let workspace = self.active_space(output).unwrap(); + let is_mapped = workspace.mapped().any(|m| m == &elem); + + is_sticky || is_mapped + }) + .cloned() + } + KeyboardFocusTarget::Fullscreen(elem) => self + .outputs() + .find(|output| { + let workspace = self.active_space(output).unwrap(); + workspace + .get_fullscreen_surfaces() + .any(|f| f.surface == elem) + }) + .cloned(), + KeyboardFocusTarget::Group(WindowGroup { node, .. }) => self + .outputs() + .find(|output| { + self.workspaces + .active(output) + .unwrap() + .1 + .tiling_layer + .has_node(&node) + }) + .cloned(), + KeyboardFocusTarget::LayerSurface(layer) => self + .outputs() + .find(|output| layer_map_for_output(output).layers().any(|l| l == &layer)) + .cloned(), + KeyboardFocusTarget::LockSurface(surface) => self + .session_lock + .as_ref()? + .surfaces + .iter() + .find_map(|(output, s)| (s == &surface).then_some(output)) + .cloned(), + KeyboardFocusTarget::Popup(_) => unreachable!(), + } + } + + /// Coerce a keyboard focus target into a CosmicMapped element. This is useful when performing window specific + /// actions, such as closing a window + pub fn focused_element(&self, focus_target: &KeyboardFocusTarget) -> Option { + match focus_target { + KeyboardFocusTarget::Element(window) => Some(window).cloned(), + KeyboardFocusTarget::Popup(PopupKind::Xdg(popup)) => { + if let Some(parent) = popup.get_parent_surface() { + self.element_for_surface(&parent).cloned() + } else { + None + } + } + KeyboardFocusTarget::Popup(PopupKind::InputMethod(popup)) => { + if let Some(parent) = popup.get_parent() { + self.element_for_surface(&parent.surface).cloned() + } else { + None + } + } + _ => None, + } + } + + /// Close the focused keyboard focus target + pub fn close_focused(&self, focus_target: &KeyboardFocusTarget) { + match focus_target { + KeyboardFocusTarget::Group(_group) => { + //TODO: decide if we want close actions to apply to groups + } + KeyboardFocusTarget::Fullscreen(surface) => { + surface.close(); + } + x => { + if let Some(mapped) = self.focused_element(x) { + mapped.send_close(); + } + } + } + } + + pub fn refresh_active_space(&mut self, output: &Output) { + if let Some(w) = self.workspaces.active_mut(output) { + w.refresh() + } + } + + pub fn visible_output_for_surface(&self, surface: &WlSurface) -> Option<&Output> { + if let Some(session_lock) = &self.session_lock { + return session_lock + .surfaces + .iter() + .find(|(_, v)| v.wl_surface() == surface) + .map(|(k, _)| k); + } + + self.outputs() + // layer map surface? + .find(|o| { + let map = layer_map_for_output(o); + map.layer_for_surface(surface, WindowSurfaceType::ALL) + .is_some() + }) + // pending layer map surface? + .or_else(|| { + self.pending_layers.iter().find_map(|pending| { + let mut found = false; + pending.surface.with_surfaces(|s, _| { + if s == surface { + found = true; + } + }); + found.then_some(&pending.output) + }) + }) + // override redirect window? + .or_else(|| { + self.outputs().find(|o| { + self.override_redirect_windows.iter().any(|or| { + if or.wl_surface().as_ref() == Some(surface) { + or.geometry() + .as_global() + .intersection(o.geometry()) + .is_some() + } else { + false + } + }) + }) + }) + // sticky window ? + .or_else(|| { + self.outputs().find(|o| { + self.workspaces.sets[*o] + .sticky_layer + .mapped() + .any(|e| e.has_surface(surface, WindowSurfaceType::ALL)) + }) + }) + // normal window? + .or_else(|| { + self.outputs().find(|o| { + let workspace = self.active_space(o).unwrap(); + + workspace + .get_fullscreen_surfaces() + .any(|f| f.surface.has_surface(surface, WindowSurfaceType::ALL)) + || workspace + .mapped() + .any(|e| e.has_surface(surface, WindowSurfaceType::ALL)) + }) + }) + // cursor and drag surfaces + .or_else(|| { + self.outputs().find(|o| { + self.seats + .iter() + .filter(|seat| seat.active_output() == **o) + .any(|seat| { + let cursor_status = seat.cursor_image_status(); + if let CursorImageStatus::Surface(s) = cursor_status + && s == *surface + { + return true; + } + + if let Some(move_grab) = seat.user_data().get::() + && let Some(grab_state) = move_grab.lock().unwrap().as_ref() + { + for (window, _) in grab_state.element().windows() { + let mut matches = false; + window.0.with_surfaces(|s, _| { + matches |= s == surface; + }); + if matches { + return true; + } + } + } + + get_dnd_icon(seat).is_some_and(|icon| icon.surface == *surface) + }) + }) + }) + } + + pub fn workspace_for_surface(&self, surface: &WlSurface) -> Option<(WorkspaceHandle, Output)> { + match self.outputs().find(|o| { + let map = layer_map_for_output(o); + map.layer_for_surface(surface, WindowSurfaceType::ALL) + .is_some() + }) { + Some(output) => self + .workspaces + .spaces() + .find(move |workspace| workspace.output() == output) + .map(|w| (w.handle, output.clone())), + None => self + .workspaces + .spaces() + .find(|w| { + w.get_fullscreen_surfaces() + .any(|f| f.surface.has_surface(surface, WindowSurfaceType::ALL)) + || w.mapped() + .any(|e| e.has_surface(surface, WindowSurfaceType::ALL)) + || w.minimized_windows.iter().any(|m| { + m.mapped() + .is_some_and(|m| m.has_surface(surface, WindowSurfaceType::ALL)) + }) + }) + .map(|w| (w.handle, w.output().clone())), + } + } + + pub fn element_for_surface(&self, surface: &S) -> Option<&CosmicMapped> + where + CosmicSurface: PartialEq, + { + self.workspaces.sets.values().find_map(|set| { + set.minimized_windows + .iter() + .find(|w| w.windows().any(|s| &s == surface)) + .and_then(|w| w.mapped()) + .or_else(|| { + set.sticky_layer + .mapped() + .find(|w| w.windows().any(|(s, _)| &s == surface)) + }) + .or_else(|| { + set.workspaces + .iter() + .find_map(|w| w.element_for_surface(surface)) + }) + }) + } + + pub fn is_surface_mapped(&self, surface: &S) -> bool + where + CosmicSurface: PartialEq, + { + self.workspaces.sets.values().any(|set| { + set.minimized_windows + .iter() + .any(|w| w.windows().any(|s| &s == surface)) + || set + .sticky_layer + .mapped() + .any(|m| m.windows().any(|(s, _)| &s == surface)) + || set.workspaces.iter().any(|w| { + w.get_fullscreen_surfaces().any(|f| &f.surface == surface) + || w.minimized_windows + .iter() + .any(|m| m.windows().any(|s| &s == surface)) + || w.floating_layer + .mapped() + .any(|m| m.windows().any(|(s, _)| &s == surface)) + || w.tiling_layer + .mapped() + .any(|(m, _)| m.windows().any(|(s, _)| &s == surface)) + }) + }) + } + + pub fn space_for(&self, mapped: &CosmicMapped) -> Option<&Workspace> { + self.workspaces.spaces().find(|workspace| { + workspace.mapped().any(|m| m == mapped) + || workspace + .minimized_windows + .iter() + .any(|m| m.mapped() == Some(mapped)) + }) + } + + pub fn space_for_mut(&mut self, mapped: &CosmicMapped) -> Option<&mut Workspace> { + self.workspaces.spaces_mut().find(|workspace| { + workspace.mapped().any(|m| m == mapped) + || workspace + .minimized_windows + .iter() + .any(|m| m.mapped() == Some(mapped)) + }) + } + + pub fn outputs(&self) -> impl DoubleEndedIterator { + self.workspaces.sets.keys() + } + + pub fn next_output(&self, current_output: &Output, direction: Direction) -> Option<&Output> { + let current_output_geo = current_output.geometry(); + self.outputs() + .filter(|o| *o != current_output) + .filter(|o| { + let geo = o.geometry(); + match direction { + Direction::Left | Direction::Right => { + geo.loc.y < current_output_geo.loc.y + current_output_geo.size.h + && geo.loc.y + geo.size.h > current_output_geo.loc.y + } + Direction::Up | Direction::Down => { + geo.loc.x < current_output_geo.loc.x + current_output_geo.size.w + && geo.loc.x + geo.size.w > current_output_geo.loc.x + } + } + }) + .filter_map(|o| { + let origin = o.geometry().loc; + let res = match direction { + Direction::Up => current_output_geo.loc.y - origin.y, + Direction::Down => origin.y - current_output_geo.loc.y, + Direction::Left => current_output_geo.loc.x - origin.x, + Direction::Right => origin.x - current_output_geo.loc.x, + }; + if res > 0 { Some((o, res)) } else { None } + }) + .min_by_key(|(_, res)| *res) + .map(|(o, _)| o) + } + + pub fn builtin_output(&self) -> Option<&Output> { + self.outputs().find(|output| output.is_internal()) + } + + pub fn global_space(&self) -> Rectangle { + self.outputs() + .fold( + Option::>::None, + |maybe_geo, output| match maybe_geo { + Some(rect) => Some(rect.merge(output.geometry())), + None => Some(output.geometry()), + }, + ) + .unwrap_or_else(Rectangle::default) + } + + pub fn animations_going(&self) -> bool { + self.workspaces.sets.values().any(|set| { + set.previously_active + .as_ref() + .is_some_and(|(_, delta)| delta.is_animating()) + || set.sticky_layer.animations_going() + }) || !matches!( + self.overview_mode, + OverviewMode::None | OverviewMode::Active(_) + ) || !matches!( + self.resize_mode, + ResizeMode::None | ResizeMode::Active(_, _) + ) || self + .workspaces + .spaces() + .any(|workspace| workspace.animations_going()) + || self.zoom_state.as_ref().is_some_and(|_| { + self.outputs().any(|o| { + o.user_data() + .get::>() + .is_some_and(|state| state.lock().unwrap().is_animating()) + }) + }) + } + + pub fn update_animations(&mut self) -> HashMap { + let mut clients = HashMap::new(); + for set in self.workspaces.sets.values_mut() { + set.sticky_layer.update_animation_state(); + } + for workspace in self.workspaces.spaces_mut() { + clients.extend(workspace.update_animations()); + } + clients + } + + pub fn set_overview_mode( + &mut self, + enabled: Option, + evlh: LoopHandle<'static, crate::state::State>, + ) { + if let Some(trigger) = enabled { + if !matches!( + self.overview_mode, + OverviewMode::Started(_, _) | OverviewMode::Active(_) + ) { + if matches!(trigger, Trigger::KeyboardSwap(_, _)) { + self.swap_indicator = Some(SwapIndicator::new(evlh, self.theme.clone())); + } + self.overview_mode = OverviewMode::Started(trigger, Instant::now()); + } + } else if matches!( + self.overview_mode, + OverviewMode::Started(_, _) | OverviewMode::Active(_) + ) { + let (reverse_duration, trigger) = + if let OverviewMode::Started(trigger, start) = self.overview_mode.clone() { + ( + ANIMATION_DURATION + - Instant::now().duration_since(start).min(ANIMATION_DURATION), + Some(trigger), + ) + } else { + (Duration::ZERO, self.overview_mode.active_trigger().cloned()) + }; + self.overview_mode = OverviewMode::Ended(trigger, Instant::now() - reverse_duration); + } + } + + pub fn overview_mode(&self) -> (OverviewMode, Option) { + if let OverviewMode::Started(trigger, timestamp) = &self.overview_mode + && Instant::now().duration_since(*timestamp) > ANIMATION_DURATION + { + return ( + OverviewMode::Active(trigger.clone()), + self.swap_indicator.clone(), + ); + } + if let OverviewMode::Ended(_, timestamp) = &self.overview_mode + && Instant::now().duration_since(*timestamp) > ANIMATION_DURATION + { + return (OverviewMode::None, None); + } + + (self.overview_mode.clone(), self.swap_indicator.clone()) + } + + pub fn set_resize_mode( + &mut self, + enabled: Option<(shortcuts::Binding, ResizeDirection)>, + config: &Config, + evlh: LoopHandle<'static, crate::state::State>, + ) { + if let Some((pattern, direction)) = enabled { + if let ResizeMode::Started(old_pattern, _, old_direction) = &mut self.resize_mode { + *old_pattern = pattern; + *old_direction = direction; + } else { + self.resize_mode = ResizeMode::Started(pattern, Instant::now(), direction); + } + self.resize_indicator = Some(ResizeIndicator::new( + direction, + config, + evlh, + self.theme.clone(), + )); + } else if let Some(direction) = self.resize_mode.active_direction() { + self.resize_mode = ResizeMode::Ended(Instant::now(), direction); + if let Some((_, direction, edge, _, _, _)) = self.resize_state.as_ref() { + self.finish_resize(*direction, *edge); + } + } + } + + pub fn resize_mode(&self) -> (ResizeMode, Option) { + if let ResizeMode::Started(binding, timestamp, direction) = &self.resize_mode + && Instant::now().duration_since(*timestamp) > ANIMATION_DURATION + { + return ( + ResizeMode::Active(binding.clone(), *direction), + self.resize_indicator.clone(), + ); + } + if let ResizeMode::Ended(timestamp, _) = self.resize_mode + && Instant::now().duration_since(timestamp) > ANIMATION_DURATION + { + return (ResizeMode::None, None); + } + + (self.resize_mode.clone(), self.resize_indicator.clone()) + } + + pub fn stacking_indicator( + &self, + output: &Output, + layer: ManagedLayer, + ) -> Option> { + match layer { + ManagedLayer::Sticky => self + .workspaces + .sets + .get(output) + .and_then(|set| set.sticky_layer.stacking_indicator()), + ManagedLayer::Floating => self + .active_space(output)? + .floating_layer + .stacking_indicator(), + ManagedLayer::Tiling => self.active_space(output)?.tiling_layer.stacking_indicator(), + ManagedLayer::Fullscreen => None, + } + } + + pub fn appearance_config(&self) -> AppearanceConfig { + self.appearance_conf + } + + pub fn trigger_zoom( + &mut self, + seat: &Seat, + output: Option<&Output>, + level: f64, + zoom_config: &ZoomConfig, + animate: bool, + loop_handle: &LoopHandle<'static, State>, + ) { + if self.zoom_state.is_none() && level == 1. { + return; + } + + let outputs = output.map(|o| vec![o]).unwrap_or(self.outputs().collect()); + if self.zoom_state.is_none() { + for output in self.outputs() { + output.user_data().insert_if_missing_threadsafe(|| { + Mutex::new(OutputZoomState::new( + seat, + output, + 1.0, + zoom_config.increment, + zoom_config.view_moves, + loop_handle.clone(), + self.theme.clone(), + )) + }); + } + } + + let mut toggled = self.zoom_state.is_none(); + if let Some(old_state) = self.zoom_state.as_ref() + && &old_state.seat != seat + { + return; + } + + for output in &outputs { + let output_state = output.user_data().get::>().unwrap(); + output_state.lock().unwrap().update( + level, + animate, + zoom_config.view_moves, + zoom_config.increment, + ); + } + + let all_outputs_off = self.outputs().all(|o| { + o.user_data() + .get::>() + .unwrap() + .lock() + .unwrap() + .current_level() + == 1.0 + }); + toggled = toggled || all_outputs_off; + + if toggled { + let value = !all_outputs_off; + let _ = loop_handle.insert_idle(move |state| { + state.common.a11y_state.set_screen_magnifier(value); + }); + } + + self.zoom_state = Some(ZoomState { + seat: seat.clone(), + show_overlay: zoom_config.show_overlay, + increment: zoom_config.increment, + movement: zoom_config.view_moves, + }); + + self.update_focal_point( + seat, + seat.get_pointer().unwrap().current_location().as_global(), + zoom_config.view_moves, + ); + } + + pub fn update_focal_point( + &mut self, + seat: &Seat, + original_position: Point, + movement: ZoomMovement, + ) { + if let Some(state) = self.zoom_state.as_mut() { + if &state.seat != seat { + return; + } + + let cursor_position = seat.get_pointer().unwrap().current_location().as_global(); + + state.update_focal_point( + &seat.active_output(), + cursor_position, + original_position, + movement, + ); + } + } + + pub fn zoom_state(&self) -> Option<&ZoomState> { + self.zoom_state.as_ref() + } + + fn refresh( + &mut self, + xdg_activation_state: &XdgActivationState, + workspace_state: &mut WorkspaceUpdateGuard<'_, State>, + ) { + match &self.overview_mode { + OverviewMode::Started(trigger, timestamp) + if Instant::now().duration_since(*timestamp) > ANIMATION_DURATION => + { + self.overview_mode = OverviewMode::Active(trigger.clone()); + } + OverviewMode::Ended(_, timestamp) + if Instant::now().duration_since(*timestamp) > ANIMATION_DURATION => + { + self.overview_mode = OverviewMode::None; + self.swap_indicator = None; + } + _ => {} + } + + match &self.resize_mode { + ResizeMode::Started(binding, timestamp, direction) + if Instant::now().duration_since(*timestamp) > ANIMATION_DURATION => + { + self.resize_mode = ResizeMode::Active(binding.clone(), *direction); + } + ResizeMode::Ended(timestamp, _) + if Instant::now().duration_since(*timestamp) > ANIMATION_DURATION => + { + self.resize_mode = ResizeMode::None; + self.resize_indicator = None; + } + _ => {} + } + + if self.zoom_state.is_some() { + let mut all_outputs_off = true; + for output in self.outputs() { + all_outputs_off = all_outputs_off + && output + .user_data() + .get::>() + .unwrap() + .lock() + .unwrap() + .refresh(); + } + + if all_outputs_off { + self.zoom_state.take(); + } + } + + self.workspaces + .refresh(workspace_state, xdg_activation_state); + + for output in self.outputs() { + let mut map = layer_map_for_output(output); + map.cleanup(); + } + + self.override_redirect_windows.retain(|or| or.alive()); + self.override_redirect_windows + .iter() + .for_each(|or| or.refresh()); + + self.pending_layers + .retain(|pending| pending.surface.alive()); + self.pending_windows + .retain(|pending| pending.surface.alive()); + } + + pub fn update_pointer_position(&mut self, location: Point, output: &Output) { + for (o, set) in self.workspaces.sets.iter_mut() { + if o == output { + set.sticky_layer.update_pointer_position(Some(location)); + for (i, workspace) in set.workspaces.iter_mut().enumerate() { + if i == set.active { + workspace + .update_pointer_position(Some(location), self.overview_mode.clone()); + } else { + workspace.update_pointer_position(None, self.overview_mode.clone()); + } + } + } else { + set.sticky_layer.update_pointer_position(None); + for workspace in &mut set.workspaces { + workspace.update_pointer_position(None, self.overview_mode.clone()); + } + } + } + } + + pub fn remap_unfullscreened_window( + &mut self, + surface: CosmicSurface, + mut state: Option, + loop_handle: &LoopHandle<'static, State>, + ) -> CosmicMapped { + if let Some(FullscreenRestoreState::Stack { state: stack_state }) = &state { + if let Some(mapped) = self.mapped().find(|m| **m == stack_state.stack) + && let Some(stack) = mapped.stack_ref() + { + let idx = stack_state.idx.min(stack.len()); + stack.add_window(surface, Some(idx), None); + return mapped.clone(); + } else { + state = None; + } + } + + let window = if state.as_ref().is_some_and(|s| s.was_stack()) { + CosmicMapped::from(CosmicStack::new( + std::iter::once(surface), + loop_handle.clone(), + self.theme.clone(), + self.appearance_conf, + )) + } else { + CosmicMapped::from(CosmicWindow::new( + surface, + loop_handle.clone(), + self.theme.clone(), + self.appearance_conf, + )) + }; + + if let Some(FullscreenRestoreState::Sticky { output, state, .. }) = &state { + let output = output + .upgrade() + .unwrap_or_else(|| self.seats.last_active().active_output()); + toplevel_enter_output(&window.active_window(), &output); + let set = self + .workspaces + .sets + .get_mut(&output) + .or(self.workspaces.backup_set.as_mut()) + .unwrap(); + set.sticky_layer.map_internal( + window.clone(), + Some(state.geometry.loc), + Some(state.geometry.size.as_logical()), + Some(set.output.geometry().to_local(&set.output)), + ); + return window; + } + + let seat = self.seats.last_active(); + let workspace = match &state { + Some(FullscreenRestoreState::Floating { workspace, .. }) + | Some(FullscreenRestoreState::Tiling { workspace, .. }) => { + let workspace = self.workspaces.space_for_handle_mut(workspace); + let workspace = match workspace { + Some(workspace) => workspace, + None => self.workspaces.active_mut(&seat.active_output()).unwrap(), + }; + toplevel_enter_output(&window.active_window(), &workspace.output); + toplevel_enter_workspace(&window.active_window(), &workspace.handle); + + workspace + } + None => self.workspaces.active_mut(&seat.active_output()).unwrap(), + Some(FullscreenRestoreState::Sticky { .. } | FullscreenRestoreState::Stack { .. }) => { + unreachable!() + } + }; + let fullscreen_geometry = workspace.output.geometry().to_local(&workspace.output); + + match state { + None => { + toplevel_enter_output(&window.active_window(), &workspace.output); + toplevel_enter_workspace(&window.active_window(), &workspace.handle); + + if workspace.tiling_enabled { + workspace.tiling_layer.remap( + window.clone(), + Some(fullscreen_geometry), + None, + Some(workspace.focus_stack.get(seat).iter()), + ); + } else { + workspace.floating_layer.map_internal( + window.clone(), + None, + None, + Some(fullscreen_geometry), + ); + } + } + Some(FullscreenRestoreState::Floating { + state: + FloatingRestoreData { + was_maximized, + was_snapped, + geometry, + .. + }, + .. + }) => { + workspace.floating_layer.map_internal( + window.clone(), + Some(geometry.loc), + Some(geometry.size.as_logical()), + Some(fullscreen_geometry), + ); + if was_maximized { + let mut state = window.maximized_state.lock().unwrap(); + *state = Some(MaximizedState { + original_geometry: geometry, + original_layer: ManagedLayer::Floating, + original_snapped: was_snapped, + }); + std::mem::drop(state); + workspace.floating_layer.map_maximized( + window.clone(), + fullscreen_geometry, + true, + ); + } else if let Some(corners) = was_snapped { + workspace.floating_layer.snap_to_corner(&window, &corners); + } + } + Some(FullscreenRestoreState::Tiling { + state: + TilingRestoreData { + state, + was_maximized, + }, + .. + }) => { + if workspace.tiling_enabled { + let focus_stack = workspace.focus_stack.get(seat); + workspace.tiling_layer.remap( + window.clone(), + Some(fullscreen_geometry), + state, + Some(focus_stack.iter()), + ); + if was_maximized { + let previous_geometry = + workspace.tiling_layer.element_geometry(&window).unwrap(); + let mut state = window.maximized_state.lock().unwrap(); + *state = Some(MaximizedState { + original_geometry: previous_geometry, + original_layer: ManagedLayer::Tiling, + original_snapped: None, + }); + std::mem::drop(state); + workspace.floating_layer.map_maximized( + window.clone(), + fullscreen_geometry, + true, + ); + } + } else { + workspace.floating_layer.map_internal( + window.clone(), + None, + None, + Some(fullscreen_geometry), + ); + + if was_maximized { + let geometry = workspace.floating_layer.element_geometry(&window).unwrap(); + let mut state = window.maximized_state.lock().unwrap(); + *state = Some(MaximizedState { + original_geometry: geometry, + original_layer: ManagedLayer::Floating, + original_snapped: None, + }); + std::mem::drop(state); + workspace.floating_layer.map_maximized( + window.clone(), + fullscreen_geometry, + true, + ); + } + } + } + Some(FullscreenRestoreState::Sticky { .. } | FullscreenRestoreState::Stack { .. }) => { + unreachable!() + } + } + + window + } + + #[must_use] + pub fn map_window( + &mut self, + window: &CosmicSurface, + toplevel_info: &mut ToplevelInfoState, + workspace_state: &mut WorkspaceState, + loop_handle: &LoopHandle<'static, State>, + ) -> Option { + let pos = self + .pending_windows + .iter() + .position(|pending| &pending.surface == window) + .unwrap(); + let PendingWindow { + surface: window, + seat, + fullscreen: output, + maximized: should_be_maximized, + sticky: mut should_be_sticky, + } = self.pending_windows.remove(pos); + + if !should_be_sticky + && let Some(toplevel) = window.0.toplevel() + && let Some(parent) = toplevel.parent() + && let Some(elem) = self.element_for_surface(&parent) + { + should_be_sticky = self + .workspaces + .sets + .values() + .any(|set| set.sticky_layer.mapped().any(|m| m == elem)); + }; + + let pending_activation = self.pending_activations.remove(&(&window).into()); + let workspace_handle = match pending_activation { + Some(ActivationContext::Workspace(handle)) => Some(handle), + _ => None, + }; + + let should_be_fullscreen = output.is_some(); + let mut output = output.unwrap_or_else(|| seat.active_output()); + + // this is beyond stupid, just to make the borrow checker happy + let workspace = if let Some(handle) = workspace_handle.filter(|handle| { + self.workspaces + .spaces() + .any(|space| &space.handle == handle) + }) { + self.workspaces + .spaces_mut() + .find(|space| space.handle == handle) + .unwrap() + } else { + self.workspaces.active_mut(&output).unwrap() // a seat's active output always has a workspace + }; + if output != workspace.output { + output = workspace.output.clone(); + } + + let active_handle = self.active_space(&output).unwrap().handle; + let workspace = if let Some(handle) = workspace_handle.filter(|handle| { + self.workspaces + .spaces() + .any(|space| &space.handle == handle) + }) { + self.workspaces + .spaces_mut() + .find(|space| space.handle == handle) + .unwrap() + } else { + self.workspaces.active_mut(&output).unwrap() + }; + + toplevel_info.new_toplevel(&window, workspace_state); + toplevel_enter_output(&window, &output); + toplevel_enter_workspace(&window, &workspace.handle); + + let mut workspace_state = workspace_state.update(); + + let workspace_output = workspace.output.clone(); + let was_activated = workspace_handle.is_some() + && (workspace_output != seat.active_output() || active_handle != workspace.handle); + let workspace_handle = workspace.handle; + let is_dialog = layout::is_dialog(&window); + let floating_exception = layout::has_floating_exception(&self.tiling_exceptions, &window); + + if should_be_fullscreen { + workspace.map_fullscreen(&window, &seat, None, None); + if was_activated { + workspace_state.add_workspace_state(&workspace_handle, WState::Urgent); + } + + return (workspace_output == seat.active_output() && active_handle == workspace_handle) + .then_some(KeyboardFocusTarget::Fullscreen(window)); + } + + let maybe_focused = workspace.focus_stack.get(&seat).iter().next().cloned(); + if let Some(FocusTarget::Window(focused)) = maybe_focused + && let Some(stack) = focused.stack_ref() + && !is_dialog + && !should_be_maximized + && !(workspace.is_tiled(&focused.active_window()) && floating_exception) + { + stack.add_window(window, None, None); + if was_activated { + workspace_state.add_workspace_state(&workspace_handle, WState::Urgent); + } + return (workspace_output == seat.active_output() && active_handle == workspace_handle) + .then_some(KeyboardFocusTarget::Element(focused)); + } + + let mapped = CosmicMapped::from(CosmicWindow::new( + window.clone(), + loop_handle.clone(), + self.theme.clone(), + self.appearance_conf, + )); + #[cfg(feature = "debug")] + { + mapped.set_debug(self.debug_active); + } + + let workspace_empty = workspace.mapped().next().is_none(); + if is_dialog || floating_exception || !workspace.tiling_enabled { + workspace.floating_layer.map(mapped.clone(), None); + } else { + for mapped in workspace + .mapped() + .filter(|m| m.maximized_state.lock().unwrap().is_some()) + .cloned() + .collect::>() + .into_iter() + { + workspace.unmaximize_request(&mapped); + } + let focus_stack = workspace.focus_stack.get(&seat); + workspace + .tiling_layer + .map(mapped.clone(), Some(focus_stack.iter()), None); + } + + if should_be_sticky { + self.toggle_sticky(&seat, &mapped); + } + + if should_be_maximized { + self.maximize_request(&mapped, &seat, false, loop_handle); + } + + let new_target = if (workspace_output == seat.active_output() + && active_handle == workspace_handle) + || should_be_sticky + { + // TODO: enforce focus stealing prevention by also checking the same rules as for the else case. + Some(KeyboardFocusTarget::from(mapped.clone())) + } else { + if workspace_empty || was_activated { + self.append_focus_stack(mapped, &seat); + workspace_state.add_workspace_state(&workspace_handle, WState::Urgent); + } + None + }; + + let active_space = self.active_space(&output).unwrap(); + for mapped in active_space.mapped() { + self.update_reactive_popups(mapped); + } + + new_target + } + + pub fn map_override_redirect(&mut self, window: X11Surface) { + let geo = window.geometry(); + for (output, overlap) in self.outputs().cloned().filter_map(|o| { + o.geometry() + .as_logical() + .intersection(geo) + .map(|overlap| (o, overlap)) + }) { + window.output_enter(&output, overlap); + } + + self.override_redirect_windows.push(window); + } + + #[must_use] + pub fn map_layer(&mut self, layer_surface: &LayerSurface) -> Option { + let pos = self + .pending_layers + .iter() + .position(|pending| &pending.surface == layer_surface) + .unwrap(); + let pending = self.pending_layers.remove(pos); + + let wants_focus = { + with_states(pending.surface.wl_surface(), |states| { + let mut state = states.cached_state.get::(); + matches!(state.current().layer, Layer::Top | Layer::Overlay) + && state.current().keyboard_interactivity != KeyboardInteractivity::None + }) + }; + + { + let mut map = layer_map_for_output(&pending.output); + map.map_layer(&pending.surface).unwrap(); + } + for workspace in self.workspaces.spaces_mut() { + workspace.tiling_layer.recalculate(); + } + + wants_focus.then(|| pending.surface.into()) + } + + pub fn unmap_surface( + &mut self, + surface: &S, + seat: &Seat, + toplevel_info: &mut ToplevelInfoState, + ) -> Option + where + CosmicSurface: PartialEq, + { + for set in self.workspaces.sets.values_mut() { + let sticky_res = set.sticky_layer.mapped().find_map(|m| { + m.windows() + .position(|(s, _)| &s == surface) + .map(|idx| (idx, m.clone())) + }); + let surface = if let Some((idx, mapped)) = sticky_res { + if let Some(stack) = mapped.stack_ref() { + stack.remove_idx(idx) + } else { + set.sticky_layer.unmap(&mapped, None); + Some(mapped.active_window()) + } + } else if let Some(idx) = set + .minimized_windows + .iter() + .position(|w| w.windows().any(|s| &s == surface)) + { + if let Some(stack) = set + .minimized_windows + .get_mut(idx) + .unwrap() + .mapped_mut() + .and_then(|m| m.stack_ref()) + { + let idx = stack.surfaces().position(|s| &s == surface); + idx.and_then(|idx| stack.remove_idx(idx)) + } else { + Some( + set.minimized_windows + .remove(idx) + .mapped() + .unwrap() + .active_window(), + ) + } + } else if let Some((surface, _)) = set + .workspaces + .iter_mut() + .find_map(|w| w.unmap_surface(surface)) + { + Some(surface) + } else { + None + }; + + if let Some(surface) = surface { + toplevel_info.remove_toplevel(&surface); + return Some(PendingWindow { + surface, + seat: seat.clone(), + fullscreen: None, + maximized: false, + sticky: false, + }); + } + } + + None + } + + pub fn move_current( + &mut self, + seat: &Seat, + to: (&Output, Option), + follow: bool, + direction: Option, + workspace_state: &mut WorkspaceUpdateGuard<'_, State>, + evlh: &LoopHandle<'static, State>, + ) -> Result)>, InvalidWorkspaceIndex> { + let (to_output, to_idx) = to; + let to_idx = to_idx.unwrap_or(self.workspaces.active_num(to_output).1); + let from_output = seat.focused_or_active_output(); + let from_idx = self.workspaces.active_num(&from_output).1; + + if &from_output == to_output && to_idx == self.workspaces.active_num(&from_output).1 { + return Ok(None); + } + + if &from_output == to_output + && to_idx.checked_sub(1).is_some_and(|idx| idx == from_idx) + && to_idx == self.workspaces.len(to_output) - 1 + && self + .workspaces + .get(from_idx, &from_output) + .is_some_and(|w| w.len() == 1) + && self + .workspaces + .get(to_idx, to_output) + .is_some_and(|w| w.is_empty()) + { + return Err(InvalidWorkspaceIndex); + } + + let to = self + .workspaces + .get(to_idx, to_output) + .map(|ws| ws.handle) + .ok_or(InvalidWorkspaceIndex)?; + + let from_workspace = self + .workspaces + .active_mut(&from_output) + .ok_or(InvalidWorkspaceIndex)?; + let from = from_workspace.handle; + + match seat.get_keyboard().unwrap().current_focus() { + Some(KeyboardFocusTarget::Group(WindowGroup { + node, focus_stack, .. + })) => { + let new_pos = if follow { + seat.set_active_output(to_output); + self.workspaces + .idx_for_handle(to_output, &to) + .and_then(|to_idx| { + self.activate( + to_output, + to_idx, + WorkspaceDelta::new_shortcut(), + workspace_state, + ) + .ok() + }) + } else { + None + }; + + let spaces = self.workspaces.spaces_mut(); + let (mut from_w, mut other_w) = spaces.partition::, _>(|w| w.handle == from); + if let Some(from_workspace) = from_w.get_mut(0) + && let Some(to_workspace) = other_w.iter_mut().find(|w| w.handle == to) + { + { + let mut stack = to_workspace.focus_stack.get_mut(seat); + for elem in focus_stack.iter().flat_map(|node_id| { + from_workspace.tiling_layer.element_for_node(node_id) + }) { + stack.append(elem.clone()); + } + } + + if to_workspace.tiling_enabled { + for mapped in to_workspace + .mapped() + .filter(|m| m.maximized_state.lock().unwrap().is_some()) + .cloned() + .collect::>() + .into_iter() + { + to_workspace.unmaximize_request(&mapped); + } + } + + let res = TilingLayout::move_tree( + &mut from_workspace.tiling_layer, + &mut to_workspace.tiling_layer, + &to, + seat, + to_workspace.focus_stack.get(seat).iter(), + NodeDesc { + handle: from, + node, + stack_window: None, + focus_stack, + }, + direction, + ); + from_workspace.refresh_focus_stack(); + to_workspace.refresh_focus_stack(); + + if !to_workspace.tiling_enabled { + to_workspace.tiling_enabled = true; + for mapped in to_workspace + .tiling_layer + .mapped() + .map(|(mapped, _)| mapped.clone()) + .collect::>() + .into_iter() + { + to_workspace.toggle_floating_window(seat, &mapped); + } + to_workspace.tiling_enabled = false; + } + + return Ok(res.zip(new_pos)); + } + + Ok(None) + } + Some(KeyboardFocusTarget::Fullscreen(surface)) => Ok(self.move_window( + Some(seat), + &surface, + &from, + &to, + follow, + direction, + workspace_state, + evlh, + )), + Some(KeyboardFocusTarget::Element(mapped)) => Ok(self.move_element( + Some(seat), + &mapped, + &from, + &to, + follow, + direction, + workspace_state, + )), + _ => Ok(None), + } + } + + #[must_use] + pub fn move_window( + &mut self, + seat: Option<&Seat>, + window: &CosmicSurface, + from: &WorkspaceHandle, + to: &WorkspaceHandle, + follow: bool, + direction: Option, + workspace_state: &mut WorkspaceUpdateGuard<'_, State>, + evlh: &LoopHandle<'static, State>, + ) -> Option<(KeyboardFocusTarget, Point)> { + let from_output = self.workspaces.space_for_handle(from)?.output.clone(); + let to_output = self.workspaces.space_for_handle(to)?.output.clone(); + let to_is_tiling = self.workspaces.space_for_handle(to).unwrap().tiling_enabled; + + let from_workspace = self.workspaces.space_for_handle_mut(from).unwrap(); // checked above + + let is_minimized = window.is_minimized(); + let is_fullscreen = from_workspace + .get_fullscreen_surfaces() + .any(|f| &f.surface == window); + let mut window_state = if is_fullscreen { + let (_, previous_state, previous_geometry) = + from_workspace.take_fullscreen(window).unwrap(); + WorkspaceRestoreData::Fullscreen(previous_state.zip(previous_geometry).map( + |(previous_state, previous_geometry)| FullscreenRestoreData { + previous_state, + previous_geometry, + }, + )) + } else { + from_workspace.unmap_surface(window)?.1 + }; + + toplevel_leave_workspace(window, from); + if from_output != to_output { + toplevel_leave_output(window, &from_output); + toplevel_enter_output(window, &to_output); + } + toplevel_enter_workspace(window, to); + + // we can't restore to a given position + if let WorkspaceRestoreData::Tiling(state) = &mut window_state { + state.state.take(); + } + // update fullscreen state to restore to the new workspace + if let WorkspaceRestoreData::Fullscreen(Some(FullscreenRestoreData { + previous_state: previous, + .. + })) = &mut window_state + { + if to_is_tiling && !from_workspace.tiling_enabled { + *previous = FullscreenRestoreState::Tiling { + workspace: *to, + state: TilingRestoreData { + state: None, + was_maximized: previous.was_maximized(), + }, + was_stack: previous.was_stack(), + }; + } else if let FullscreenRestoreState::Tiling { workspace, .. } + | FullscreenRestoreState::Floating { workspace, .. } = previous + { + *workspace = *to; + } + } + + if is_minimized { + let to_workspace = self.workspaces.space_for_handle_mut(to).unwrap(); // checked above + let minimized_window = match window_state { + WorkspaceRestoreData::Floating(previous) => { + let window = CosmicMapped::from(CosmicWindow::new( + window.clone(), + evlh.clone(), + self.theme.clone(), + self.appearance_conf, + )); + window.set_minimized(true); + MinimizedWindow::Floating { window, previous } + } + WorkspaceRestoreData::Tiling(previous) => { + let window = CosmicMapped::from(CosmicWindow::new( + window.clone(), + evlh.clone(), + self.theme.clone(), + self.appearance_conf, + )); + window.set_minimized(true); + MinimizedWindow::Tiling { window, previous } + } + WorkspaceRestoreData::Fullscreen(previous) => { + window.set_minimized(true); + MinimizedWindow::Fullscreen { + surface: window.clone(), + previous, + } + } + _ => { + unreachable!() + } // MinimizedWindow always has restore data + }; + to_workspace.minimized_windows.push(minimized_window); + return None; + } + + for mapped in from_workspace + .mapped() + .cloned() + .collect::>() + .into_iter() + { + self.update_reactive_popups(&mapped); + } + + let new_pos = if follow { + if let Some(seat) = seat { + seat.set_active_output(&to_output); + } + self.workspaces + .idx_for_handle(&to_output, to) + .and_then(|to_idx| { + self.activate( + &to_output, + to_idx, + WorkspaceDelta::new_shortcut(), + workspace_state, + ) + .ok() + }) + } else { + None + }; + + let to_workspace = self.workspaces.space_for_handle_mut(to).unwrap(); // checked above + let to_mapped = to_workspace.mapped().cloned().collect::>(); + + let focus_target: KeyboardFocusTarget = + if !matches!(window_state, WorkspaceRestoreData::Fullscreen(_)) + && !to_workspace.tiling_enabled + { + let mapped = CosmicMapped::from(CosmicWindow::new( + window.clone(), + evlh.clone(), + self.theme.clone(), + self.appearance_conf, + )); + let position = match window_state { + WorkspaceRestoreData::Floating(data) => Some( + data.position_relative(to_workspace.output.geometry().size.as_logical()), + ), + _ => None, + }; + to_workspace.floating_layer.map(mapped.clone(), position); + mapped.into() + } else if !matches!(window_state, WorkspaceRestoreData::Fullscreen(_)) + && to_workspace.tiling_enabled + { + let mapped = CosmicMapped::from(CosmicWindow::new( + window.clone(), + evlh.clone(), + self.theme.clone(), + self.appearance_conf, + )); + for mapped in to_workspace + .mapped() + .filter(|m| m.maximized_state.lock().unwrap().is_some()) + .cloned() + .collect::>() + .into_iter() + { + to_workspace.unmaximize_request(&mapped); + } + let focus_stack = seat.map(|seat| to_workspace.focus_stack.get(seat)); + to_workspace.tiling_layer.map( + mapped.clone(), + focus_stack.as_ref().map(|x| x.iter()), + direction, + ); + mapped.into() + } else if let WorkspaceRestoreData::Fullscreen(previous) = window_state { + to_workspace.map_fullscreen( + window, + None, + previous.clone().map(|p| p.previous_state), + previous.map(|p| p.previous_geometry), + ); + window.clone().into() + } else { + unreachable!() // TODO: sticky + }; + + for mapped in to_mapped.into_iter() { + self.update_reactive_popups(&mapped); + } + + new_pos.map(|pos| (focus_target, pos)) + } + + #[must_use] + pub fn move_element( + &mut self, + seat: Option<&Seat>, + mapped: &CosmicMapped, + from: &WorkspaceHandle, + to: &WorkspaceHandle, + follow: bool, + direction: Option, + workspace_state: &mut WorkspaceUpdateGuard<'_, State>, + ) -> Option<(KeyboardFocusTarget, Point)> { + let from_output = self.workspaces.space_for_handle(from)?.output.clone(); + let to_output = self.workspaces.space_for_handle(to)?.output.clone(); + + let from_workspace = self.workspaces.space_for_handle_mut(from).unwrap(); // checked above + let window_state = from_workspace.unmap_element(mapped)?; + let elements = from_workspace.mapped().cloned().collect::>(); + + for (toplevel, _) in mapped.windows() { + toplevel_leave_workspace(&toplevel, from); + if from_output != to_output { + toplevel_leave_output(&toplevel, &from_output); + } + } + for mapped in elements.into_iter() { + self.update_reactive_popups(&mapped); + } + let new_pos = if follow { + if let Some(seat) = seat { + seat.set_active_output(&to_output); + } + self.workspaces + .idx_for_handle(&to_output, to) + .and_then(|to_idx| { + self.activate( + &to_output, + to_idx, + WorkspaceDelta::new_shortcut(), + workspace_state, + ) + .ok() + }) + } else { + None + }; + + let to_workspace = self.workspaces.space_for_handle_mut(to).unwrap(); // checked above + if !to_workspace.tiling_enabled { + let (position, was_maximized, was_snapped) = match &window_state { + WorkspaceRestoreData::Floating(data) => ( + Some(data.position_relative(to_workspace.output.geometry().size.as_logical())), + data.was_maximized, + data.was_snapped, + ), + _ => (None, false, None), + }; + to_workspace.floating_layer.map(mapped.clone(), position); + if was_maximized { + let geometry = to_workspace + .floating_layer + .element_geometry(mapped) + .unwrap(); + *mapped.maximized_state.lock().unwrap() = Some(MaximizedState { + original_geometry: geometry, + original_layer: ManagedLayer::Floating, + original_snapped: was_snapped, + }); + to_workspace + .floating_layer + .map_maximized(mapped.clone(), geometry, false); + } else if let Some(corners) = was_snapped { + to_workspace.floating_layer.snap_to_corner(mapped, &corners); + } + } else { + for mapped in to_workspace + .mapped() + .filter(|m| m.maximized_state.lock().unwrap().is_some()) + .cloned() + .collect::>() + .into_iter() + { + to_workspace.unmaximize_request(&mapped); + } + let focus_stack = seat.map(|seat| to_workspace.focus_stack.get(seat)); + to_workspace.tiling_layer.map( + mapped.clone(), + focus_stack.as_ref().map(|x| x.iter()), + direction, + ); + } + + let focus_target = KeyboardFocusTarget::from(mapped.clone()); + + for mapped in to_workspace + .mapped() + .cloned() + .collect::>() + .into_iter() + { + self.update_reactive_popups(&mapped); + } + for (toplevel, _) in mapped.windows() { + if from_output != to_output { + toplevel_enter_output(&toplevel, &to_output); + } + toplevel_enter_workspace(&toplevel, to); + } + + new_pos.map(|pos| (focus_target, pos)) + } + + pub fn update_reactive_popups(&self, mapped: &CosmicMapped) { + if let Some(workspace) = self.space_for(mapped) + && let Some(element_loc) = workspace + .element_geometry(mapped) + .map(|geo| geo.loc.to_global(&workspace.output)) + { + for (window, offset) in mapped.windows() { + if let Some(toplevel) = window.0.toplevel() { + let window_geo_offset = window.geometry().loc.as_global(); + update_reactive_popups( + toplevel, + element_loc + offset.as_global() + window_geo_offset, + self.outputs(), + ); + } + } + } + } + + pub fn menu_request( + &self, + is_client_initiated: bool, + surface: &WlSurface, + seat: &Seat, + serial: impl Into>, + location: Point, + target_stack: bool, + config: &Config, + evlh: &LoopHandle<'static, State>, + ) -> Option<(MenuGrab, Focus)> { + let serial = serial.into(); + let Some(GrabStartData::Pointer(start_data)) = + check_grab_preconditions(seat, serial, is_client_initiated.then_some(surface)) + else { + return None; // TODO: an application can send a menu request for a touch event + }; + + let items_for_element = |mapped: &CosmicMapped, + is_tiled: bool, + is_sticky: bool, + tiling_enabled: bool, + edge: ResizeEdge| { + let is_stacked = mapped.is_stack(); + + if target_stack || !is_stacked { + Box::new( + window_items( + mapped, + is_tiled, + is_stacked, + is_sticky, + tiling_enabled, + edge, + config, + ) + .collect::>() + .into_iter(), + ) as Box> + } else { + let (tab, _) = mapped + .windows() + .find(|(s, _)| s.wl_surface().as_deref() == Some(surface)) + .unwrap(); + Box::new( + tab_items(mapped, &tab, is_tiled, config) + .collect::>() + .into_iter(), + ) as Box> + } + }; + + let (global_position, menu_items) = if let Some((set, mapped, relative_loc)) = + self.workspaces.sets.values().find_map(|set| { + set.sticky_layer + .mapped() + .find_map(|m| { + m.windows() + .find(|(w, _)| w == surface) + .map(|(_, loc)| (m, loc)) + }) + .map(|(mapped, relative_loc)| (set, mapped, relative_loc)) + }) { + let output = set.output.clone(); + let global_position = (set.sticky_layer.element_geometry(mapped).unwrap().loc + + relative_loc.as_local() + + location.as_local()) + .to_global(&output); + ( + global_position, + items_for_element(mapped, false, true, false, ResizeEdge::all()), + ) + } else if let Some((workspace, output)) = self.workspace_for_surface(surface) { + let workspace = self.workspaces.space_for_handle(&workspace).unwrap(); + + if let Some(fs) = workspace + .get_fullscreen_surfaces() + .find(|f| &f.surface == surface) + { + let window = &fs.surface; + let global_position = (workspace.fullscreen_geometry_for(fs).loc + + location.as_local()) + .to_global(&output); + + ( + global_position, + Box::new(fullscreen_items(window, config)) as Box>, + ) + } else { + let mapped = workspace.element_for_surface(surface)?; + let elem_geo = workspace.element_geometry(mapped)?; + let relative_loc = mapped.active_window_geometry().loc; + let global_position = + (elem_geo.loc + relative_loc.as_local() + location.as_local()) + .to_global(&output); + let is_tiled = workspace.is_tiled(&mapped.active_window()); + let edge = if is_tiled { + mapped + .tiling_node_id + .lock() + .unwrap() + .clone() + .map(|node_id| { + TilingLayout::possible_resizes(workspace.tiling_layer.tree(), node_id) + }) + .unwrap_or(ResizeEdge::empty()) + } else { + ResizeEdge::all() + }; + + ( + global_position, + items_for_element(mapped, is_tiled, false, workspace.tiling_enabled, edge), + ) + } + } else { + return None; + }; + + let mut theme = self.theme.clone(); + theme.transparent = theme.cosmic().frosted_windows; + let grab = MenuGrab::new( + GrabStartData::Pointer(start_data), + seat, + menu_items, + global_position, + MenuAlignment::CORNER, + None, + evlh.clone(), + theme, + ); + + Some((grab, Focus::Keep)) + } + + pub fn move_request( + &mut self, + surface: &WlSurface, + seat: &Seat, + serial: impl Into>, + release: ReleaseMode, + move_out_of_stack: bool, + config: &Config, + evlh: &LoopHandle<'static, State>, + client_initiated: bool, + ) -> Option<(MoveGrab, Focus)> { + if self.overview_mode().0.is_active() { + return None; + } + + let serial = serial.into(); + let mut element_geo = None; + + let mut start_data = + check_grab_preconditions(seat, serial, client_initiated.then_some(surface))?; + + if client_initiated + && start_data.distance(seat.get_pointer().unwrap().current_location()) < 1. + { + return Some(( + MoveGrab::delayed( + start_data, + surface, + seat, + serial, + release, + move_out_of_stack, + ), + Focus::Keep, + )); + } + + let maybe_fullscreen_workspace = self + .workspaces + .spaces_mut() + .find(|w| w.get_fullscreen_surfaces().any(|f| &f.surface == surface)); + if let Some(workspace) = maybe_fullscreen_workspace { + let fs = workspace + .get_fullscreen_surfaces() + .find(|f| &f.surface == surface) + .unwrap(); + element_geo = Some(workspace.fullscreen_geometry_for(fs)); + let (surface, state, _) = workspace.remove_fullscreen_surface(surface).unwrap(); + self.remap_unfullscreened_window(surface, state, evlh); + }; + + let old_mapped = self.element_for_surface(surface).cloned()?; + if old_mapped.is_minimized() { + return None; + } + + let (window, _) = old_mapped + .windows() + .find(|(w, _)| w.wl_surface().as_deref() == Some(surface)) + .unwrap(); + + let mapped = if move_out_of_stack { + let new_mapped: CosmicMapped = CosmicWindow::new( + window.clone(), + evlh.clone(), + self.theme.clone(), + self.appearance_conf, + ) + .into(); + if old_mapped.is_maximized(false) { + new_mapped.set_maximized(false); + } + start_data.set_focus(new_mapped.focus_under( + (0., 0.).into(), + WindowSurfaceType::ALL, + seat, + )); + new_mapped + } else { + old_mapped.clone() + }; + + if move_out_of_stack { + // Update focus stack to set focus to the window being dragged out of + // the stack. + if let Some(workspace) = self.space_for_mut(&old_mapped) { + let mut stack = workspace.focus_stack.get_mut(seat); + stack.append(mapped.clone()); + } + } + + let trigger = match &start_data { + GrabStartData::Pointer(start_data) => Trigger::Pointer(start_data.button), + GrabStartData::Touch(start_data) => Trigger::Touch(start_data.slot), + }; + let active_hint = if config.cosmic_conf.active_hint { + self.theme.cosmic().active_hint as u8 + } else { + 0 + }; + let pointer = seat.get_pointer().unwrap(); + let pos = pointer.current_location().as_global(); + + let cursor_output = if let Some(output) = self + .outputs() + .find(|output| { + output + .geometry() + .as_logical() + .overlaps_or_touches(Rectangle::new( + start_data.location().to_i32_floor(), + (0, 0).into(), + )) + }) + .cloned() + { + output + } else { + seat.active_output() + }; + + let (initial_window_location, layer, workspace_handle) = + if let Some(workspace) = self.space_for_mut(&old_mapped) { + let elem_geo = element_geo.or_else(|| workspace.element_geometry(&old_mapped))?; + let mut initial_window_location = elem_geo.loc.to_global(workspace.output()); + + let mut new_size = if old_mapped.maximized_state.lock().unwrap().is_some() { + // If surface is maximized then unmaximize it + workspace + .unmaximize_request(&old_mapped) + .map(|geo| geo.size.as_logical()) + } else { + None + }; + + let layer = if if mapped == old_mapped { + let was_floating = workspace.floating_layer.unmap(&mapped, None); + let was_tiled = workspace + .tiling_layer + .unmap_as_placeholder(&mapped, PlaceholderType::GrabbedWindow); + assert!(was_floating.is_some() != was_tiled.is_some()); + if was_floating.is_some_and(|geo| geo.size != elem_geo.size) { + new_size = was_floating.map(|geo| geo.size.as_logical()); + } + was_tiled.is_some() + } else { + workspace + .tiling_layer + .mapped() + .any(|(m, _)| m == &old_mapped) + } { + ManagedLayer::Tiling + } else { + ManagedLayer::Floating + }; + + // if this changed the width, the window was tiled in floating mode + if let Some(new_size) = new_size { + let output = workspace.output(); + let ratio = pos.to_local(output).x / (elem_geo.loc.x + elem_geo.size.w) as f64; + + initial_window_location = Point::from(( + pos.x - (new_size.w as f64 * ratio), + pos.y - MOVE_GRAB_Y_OFFSET, + )) + .to_i32_round(); + } + + (initial_window_location, layer, workspace.handle) + } else { + let sticky_layer = self + .workspaces + .sets + .get_mut(&cursor_output) + .filter(|set| set.sticky_layer.mapped().any(|m| m == &old_mapped)) + .map(|set| &mut set.sticky_layer)?; + let elem_geo = sticky_layer.element_geometry(&old_mapped).unwrap(); + let mut initial_window_location = elem_geo.loc.to_global(&cursor_output); + + let mut new_size = + if let Some(state) = old_mapped.maximized_state.lock().unwrap().take() { + // If surface is maximized then unmaximize it + old_mapped.set_maximized(false); + let new_size = state.original_geometry.size.as_logical(); + sticky_layer.map_internal( + mapped.clone(), + Some(state.original_geometry.loc), + Some(new_size), + None, + ); + + Some(new_size) + } else { + None + }; + + if mapped == old_mapped + && let Some(geo) = sticky_layer.unmap(&mapped, None) + && geo.size != elem_geo.size + { + new_size = Some(geo.size.as_logical()); + } + + if let Some(new_size) = new_size { + let ratio = + pos.to_local(&cursor_output).x / (elem_geo.loc.x + elem_geo.size.w) as f64; + initial_window_location = Point::::from(( + pos.x - (new_size.w as f64 * ratio), + pos.y - MOVE_GRAB_Y_OFFSET, + )) + .to_i32_round(); + } + + ( + initial_window_location, + ManagedLayer::Sticky, + self.active_space(&cursor_output).unwrap().handle, + ) + }; + + toplevel_leave_workspace(&window, &workspace_handle); + toplevel_leave_output(&window, &cursor_output); + + if move_out_of_stack { + old_mapped.stack_ref().unwrap().remove_window(&window); + self.workspaces + .space_for_handle_mut(&workspace_handle) + .unwrap() + .refresh(); + } + + mapped.set_activate(true); + mapped.configure(); + + let grab = MoveGrab::new( + start_data, + mapped, + seat, + initial_window_location, + cursor_output, + active_hint, + config.cosmic_conf.edge_snap_threshold as f64, + layer, + release, + evlh.clone(), + ); + + if grab.is_tiling_grab() { + self.set_overview_mode(Some(trigger), evlh.clone()); + } + + Some((grab, Focus::Clear)) + } + + // Just to avoid a longer lived shell reference + /// Get the window geometry of a keyboard focus target + pub fn focused_geometry(&self, target: &KeyboardFocusTarget) -> Option> { + match target { + KeyboardFocusTarget::Fullscreen(surface) => surface + .wl_surface() + .and_then(|s| self.workspace_for_surface(&s)) + .and_then(|(handle, _)| self.workspaces.space_for_handle(&handle)) + .map(|workspace| { + workspace + .fullscreen_geometry_for_surface(surface) + .to_global(workspace.output()) + }), + _ => { + if let Some(element) = self.focused_element(target) { + self.element_geometry(&element) + } else { + None + } + } + } + } + + pub fn element_geometry(&self, mapped: &CosmicMapped) -> Option> { + if let Some(set) = self + .workspaces + .sets + .values() + .find(|set| set.sticky_layer.mapped().any(|m| m == mapped)) + { + let geometry = set + .sticky_layer + .element_geometry(mapped) + .unwrap() + .to_global(&set.output); + Some(geometry) + } else if let Some(workspace) = self.space_for(mapped) { + let geometry = workspace + .element_geometry(mapped) + .unwrap() + .to_global(workspace.output()); + Some(geometry) + } else { + None + } + } + + #[must_use] + pub fn next_focus(&self, direction: FocusDirection, seat: &Seat) -> FocusResult { + let overview = self.overview_mode().0; + let Some(target) = seat.get_keyboard().unwrap().current_focus() else { + return FocusResult::None; + }; + let output = seat.active_output(); + + if matches!(target, KeyboardFocusTarget::Fullscreen(_)) { + return FocusResult::None; + } + + let set = self.workspaces.sets.get(&output).unwrap(); + let sticky_layer = &set.sticky_layer; + let workspace = &set.workspaces[set.active]; + + let Some(focused) = (match target { + KeyboardFocusTarget::Popup(popup) => { + let Some(toplevel_surface) = (match popup { + PopupKind::Xdg(_) => get_popup_toplevel(&popup), + PopupKind::InputMethod(_) => unreachable!(), + }) else { + return FocusResult::None; + }; + sticky_layer + .space + .elements() + .chain(workspace.mapped()) + .find(|elem| elem.wl_surface().as_deref() == Some(&toplevel_surface)) + } + KeyboardFocusTarget::Element(elem) => sticky_layer + .space + .elements() + .chain(workspace.mapped()) + .find(|e| *e == &elem), + KeyboardFocusTarget::Group { .. } => { + let focus_stack = workspace.focus_stack.get(seat); + let swap_desc = match overview.active_trigger() { + Some(Trigger::KeyboardSwap(_, desc)) => Some(desc.clone()), + _ => None, + }; + + return workspace.tiling_layer.next_focus( + direction, + seat, + focus_stack.iter(), + swap_desc, + ); + } + _ => None, + }) + .cloned() else { + return FocusResult::None; + }; + + if focused.handle_focus(seat, direction, None) { + return FocusResult::Handled; + } + + if workspace.is_tiled(&focused.active_window()) { + if focused.is_maximized(false) { + return FocusResult::None; + } + + let focus_stack = workspace.focus_stack.get(seat); + let swap_desc = match overview.active_trigger() { + Some(Trigger::KeyboardSwap(_, desc)) => Some(desc.clone()), + _ => None, + }; + + workspace + .tiling_layer + .next_focus(direction, seat, focus_stack.iter(), swap_desc) + } else { + let floating_layer = &set.workspaces[set.active].floating_layer; + + let geometry = sticky_layer + .space + .element_geometry(&focused) + .or_else(|| floating_layer.space.element_geometry(&focused)) + .unwrap(); + + let elements = sticky_layer + .space + .elements() + .chain(floating_layer.space.elements()) + .filter(|elem| *elem != &focused) + .map(|elem| { + ( + elem, + sticky_layer + .space + .element_geometry(elem) + .or_else(|| floating_layer.space.element_geometry(elem)) + .unwrap(), + ) + }); + + let next = match direction { + FocusDirection::Up => elements + .filter(|(_, other_geo)| other_geo.loc.y <= geometry.loc.y) + .min_by_key(|(_, other_geo)| { + let res = geometry.loc.y - other_geo.loc.y; + if res.is_positive() { res } else { i32::MAX } + }), + FocusDirection::Down => elements + .filter(|(_, other_geo)| other_geo.loc.y > geometry.loc.y) + .max_by_key(|(_, other_geo)| { + let res = geometry.loc.y - other_geo.loc.y; + if res.is_negative() { res } else { i32::MIN } + }), + FocusDirection::Left => elements + .filter(|(_, other_geo)| other_geo.loc.x <= geometry.loc.x) + .min_by_key(|(_, other_geo)| { + let res = geometry.loc.x - other_geo.loc.x; + if res.is_positive() { res } else { i32::MAX } + }), + FocusDirection::Right => elements + .filter(|(_, other_geo)| other_geo.loc.x > geometry.loc.x) + .max_by_key(|(_, other_geo)| { + let res = geometry.loc.x - other_geo.loc.x; + if res.is_negative() { res } else { i32::MIN } + }), + _ => return FocusResult::None, + } + .map(|(other, _)| other); + + next.map(|elem| FocusResult::Some(KeyboardFocusTarget::Element(elem.clone()))) + .unwrap_or(FocusResult::None) + } + } + + #[must_use] + pub fn move_current_element(&mut self, direction: Direction, seat: &Seat) -> MoveResult { + let Some(output) = seat.focused_output() else { + return MoveResult::None; + }; + let workspace = self.active_space(&output).unwrap(); + let focus_stack = workspace.focus_stack.get(seat); + match focus_stack.last().cloned() { + Some(FocusTarget::Fullscreen(surface)) => { + MoveResult::MoveFurther(KeyboardFocusTarget::Fullscreen(surface)) + } + Some(FocusTarget::Window(mapped)) => { + if let Some(set) = self + .workspaces + .sets + .values_mut() + .find(|set| set.sticky_layer.mapped().any(|m| &mapped == m)) + { + set.sticky_layer.move_current_element( + direction, + seat, + ManagedLayer::Sticky, + self.theme.clone(), + ) + } else { + let theme = self.theme.clone(); + if mapped + .maximized_state + .lock() + .unwrap() + .as_ref() + .is_some_and(|state| state.original_layer == ManagedLayer::Tiling) + { + self.unmaximize_request(&mapped); + } + + let workspace = self.active_space_mut(&output).unwrap(); + workspace + .floating_layer + .move_current_element(direction, seat, ManagedLayer::Floating, theme) + .or_else(|| workspace.tiling_layer.move_current_node(direction, seat)) + } + } + _ => MoveResult::None, + } + } + + pub fn menu_resize_request( + &mut self, + mapped: &CosmicMapped, + seat: &Seat, + edge: ResizeEdge, + edge_snap_threshold: u32, + ) -> Option<( + ( + Option<(PointerFocusTarget, Point)>, + Point, + ), + (ResizeGrab, Focus), + )> { + if mapped.is_fullscreen(true) || mapped.is_maximized(true) { + return None; + } + + let mut start_data = check_grab_preconditions(seat, None, None)?; + + let (floating_layer, geometry) = if let Some(set) = self + .workspaces + .sets + .values_mut() + .find(|set| set.sticky_layer.mapped().any(|m| m == mapped)) + { + let geometry = set + .sticky_layer + .element_geometry(mapped) + .unwrap() + .to_global(&set.output); + (&mut set.sticky_layer, geometry) + } else { + let workspace = self.space_for_mut(mapped)?; + let geometry = workspace + .element_geometry(mapped) + .unwrap() + .to_global(workspace.output()); + (&mut workspace.floating_layer, geometry) + }; + + let new_loc = if edge.contains(ResizeEdge::LEFT) { + Point::::from((geometry.loc.x, geometry.loc.y + (geometry.size.h / 2))) + } else if edge.contains(ResizeEdge::RIGHT) { + Point::::from(( + geometry.loc.x + geometry.size.w, + geometry.loc.y + (geometry.size.h / 2), + )) + } else if edge.contains(ResizeEdge::TOP) { + Point::::from((geometry.loc.x + (geometry.size.w / 2), geometry.loc.y)) + } else if edge.contains(ResizeEdge::BOTTOM) { + Point::::from(( + geometry.loc.x + (geometry.size.w / 2), + geometry.loc.y + geometry.size.h, + )) + } else { + return None; + }; + + let element_offset = (new_loc - geometry.loc).as_logical(); + let focus = mapped + .focus_under(element_offset.to_f64(), WindowSurfaceType::ALL, seat) + .map(|(target, surface_offset)| (target, (surface_offset + element_offset.to_f64()))); + start_data.set_location(new_loc.as_logical().to_f64()); + start_data.set_focus(focus.clone()); + + let grab: ResizeGrab = if let Some(grab) = floating_layer.resize_request( + mapped, + seat, + start_data.clone(), + edge, + edge_snap_threshold, + ReleaseMode::Click, + ) { + grab.into() + } else { + let ws = self.space_for_mut(mapped)?; + let node_id = mapped.tiling_node_id.lock().unwrap().clone()?; + let (node, left_up_idx, orientation) = ws.tiling_layer.resize_request(node_id, edge)?; + ResizeForkGrab::new( + start_data, + new_loc.to_f64(), + node, + left_up_idx, + orientation, + ws.output.downgrade(), + ReleaseMode::Click, + ) + .into() + }; + + Some(((focus, new_loc), (grab, Focus::Keep))) + } + + pub fn maximize_toggle( + &mut self, + window: &CosmicMapped, + seat: &Seat, + loop_handle: &LoopHandle<'static, State>, + ) { + if window.is_maximized(true) { + self.unmaximize_request(window); + } else { + if window.is_fullscreen(true) { + return; + } + self.maximize_request(window, seat, true, loop_handle); + } + } + + pub fn minimize_request(&mut self, surface: &S) + where + CosmicSurface: PartialEq, + { + if let Some((set, mapped)) = self.workspaces.sets.values_mut().find_map(|set| { + let mapped = set + .sticky_layer + .mapped() + .find(|m| &m.active_window() == surface) + .cloned(); + mapped.map(|m| (set, m)) + }) { + let to = minimize_rectangle(&set.output, &mapped.active_window()); + let geo = set.sticky_layer.unmap(&mapped, Some(to)).unwrap(); + set.minimized_windows.push(MinimizedWindow::Floating { + window: mapped.clone(), + previous: FloatingRestoreData { + geometry: geo, + output_size: set.output.geometry().size.as_logical(), + was_maximized: false, + was_snapped: None, + }, + }); + } else if let Some((workspace, window)) = + self.workspaces.sets.values_mut().find_map(|set| { + set.workspaces.iter_mut().find_map(|workspace| { + let window = workspace + .get_fullscreen_surfaces() + .map(|f| f.surface.clone()) + .chain(workspace.mapped().map(|m| m.active_window())) + .find(|s| s == surface); + window.map(|s| (workspace, s)) + }) + }) + { + let to = minimize_rectangle(workspace.output(), &window); + if let Some(minimized) = workspace.minimize(surface, to) { + workspace.minimized_windows.push(minimized); + } + } + } + + pub fn unminimize_request( + &mut self, + surface: &S, + seat: &Seat, + loop_handle: &LoopHandle<'static, State>, + ) where + CosmicSurface: PartialEq, + { + if let Some((set, window)) = self.workspaces.sets.values_mut().find_map(|set| { + set.minimized_windows + .iter() + .position(|m| m.windows().any(|s| &s == surface)) + .map(|i| set.minimized_windows.swap_remove(i)) + .map(|window| (set, window)) + }) { + let MinimizedWindow::Floating { window, previous } = window else { + unreachable!("None sticky window in WorkspaceSet minimized_windows"); + }; + + let from = minimize_rectangle(&set.output, &window.active_window()); + let previous_position = + previous.position_relative(set.output.geometry().size.as_logical()); + if window.is_stack() { + window.set_active(surface); + } + set.sticky_layer + .remap_minimized(window, from, previous_position); + } else { + let Some((workspace, window)) = self.workspaces.spaces_mut().find_map(|w| { + w.minimized_windows + .iter() + .position(|m| m.windows().any(|s| &s == surface)) + .map(|i| w.minimized_windows.swap_remove(i)) + .map(|window| (w, window)) + }) else { + return; + }; + + if window.mapped().is_some_and(|m| m.is_stack()) { + window.mapped().unwrap().set_active(surface); + } + let from = minimize_rectangle(workspace.output(), &window.active_window()); + if let Some((surface, restore, _)) = workspace.unminimize(window, from, seat) { + toplevel_leave_output(&surface, &workspace.output); + toplevel_leave_workspace(&surface, &workspace.handle); + self.remap_unfullscreened_window(surface, restore, loop_handle); + } + } + } + + pub fn maximize_request( + &mut self, + mapped: &CosmicMapped, + seat: &Seat, + animate: bool, + loop_handle: &LoopHandle<'static, State>, + ) { + self.unminimize_request(&mapped.active_window(), seat, loop_handle); + + let (original_layer, floating_layer, original_geometry) = if let Some(set) = self + .workspaces + .sets + .values_mut() + .find(|set| set.sticky_layer.mapped().any(|m| m == mapped)) + { + let geometry = set.sticky_layer.element_geometry(mapped).unwrap(); + (ManagedLayer::Sticky, &mut set.sticky_layer, geometry) + } else if let Some(workspace) = self.space_for_mut(mapped) { + let layer = if workspace.is_tiled(&mapped.active_window()) { + ManagedLayer::Tiling + } else { + ManagedLayer::Floating + }; + let geometry = workspace.element_geometry(mapped).unwrap(); + (layer, &mut workspace.floating_layer, geometry) + } else { + return; + }; + + let mut state = mapped.maximized_state.lock().unwrap(); + if state.is_none() { + *state = Some(MaximizedState { + original_geometry, + original_layer, + original_snapped: None, + }); + std::mem::drop(state); + floating_layer.map_maximized(mapped.clone(), original_geometry, animate); + } + } + + pub fn unmaximize_request(&mut self, mapped: &CosmicMapped) -> Option> { + if let Some(set) = self.workspaces.sets.values_mut().find(|set| { + set.sticky_layer.mapped().any(|m| m == mapped) + || set + .minimized_windows + .iter() + .any(|m| m.mapped().is_some_and(|m| m == mapped)) + }) { + let mut state = mapped.maximized_state.lock().unwrap(); + if let Some(state) = state.take() { + assert_eq!(state.original_layer, ManagedLayer::Sticky); + + if let Some(minimized) = set + .minimized_windows + .iter_mut() + .find(|m| m.mapped().is_some_and(|m| m == mapped)) + { + minimized.unmaximize(state.original_geometry, state.original_snapped); + } else { + mapped.set_maximized(false); + set.sticky_layer.map_internal( + mapped.clone(), + Some(state.original_geometry.loc), + Some(state.original_geometry.size.as_logical()), + None, + ); + // Re-apply the snap if the window was snapped when it was maximized. + if let Some(corners) = state.original_snapped { + set.sticky_layer.snap_to_corner(mapped, &corners); + } + } + Some(state.original_geometry.size.as_logical()) + } else { + None + } + } else if let Some(workspace) = self.space_for_mut(mapped) { + workspace + .unmaximize_request(mapped) + .map(|geo| geo.size.as_logical()) + } else { + None + } + } + + pub fn resize_request( + &mut self, + surface: &WlSurface, + seat: &Seat, + serial: impl Into>, + edges: ResizeEdge, + edge_snap_threshold: u32, + client_initiated: bool, + ) -> Option<(ResizeGrab, Focus)> { + let serial = serial.into(); + let start_data = + check_grab_preconditions(seat, serial, client_initiated.then_some(surface))?; + let mapped = self.element_for_surface(surface).cloned()?; + if mapped.is_maximized(true) { + return None; + } + + // Reject duplicate resize requests (e.g. Steam sends two + // _NET_WM_MOVERESIZE messages). Creating a new grab while one is + // already active would corrupt the resize state when the old grab's + // ungrab() overwrites the freshly-set Resizing state. + if let Some(ResizeState::Resizing(data)) = *mapped.resize_state.lock().unwrap() { + tracing::warn!( + app_id = mapped.active_window().app_id(), + active_edges = ?data.edges, + requested_edges = ?edges, + "Rejecting duplicate resize request while resize is already active", + ); + return None; + } + + let floating_layer = if let Some(set) = self + .workspaces + .sets + .values_mut() + .find(|set| set.sticky_layer.mapped().any(|m| m == &mapped)) + { + &mut set.sticky_layer + } else { + let workspace = self.space_for_mut(&mapped)?; + &mut workspace.floating_layer + }; + + let grab: ResizeGrab = if let Some(grab) = floating_layer.resize_request( + &mapped, + seat, + start_data.clone(), + edges, + edge_snap_threshold, + ReleaseMode::NoMouseButtons, + ) { + grab.into() + } else { + let ws = self.space_for_mut(&mapped)?; + let node_id = mapped.tiling_node_id.lock().unwrap().clone()?; + let (node, left_up_idx, orientation) = + ws.tiling_layer.resize_request(node_id, edges)?; + ResizeForkGrab::new( + start_data, + seat.get_pointer().unwrap().current_location().as_global(), + node, + left_up_idx, + orientation, + ws.output.downgrade(), + ReleaseMode::NoMouseButtons, + ) + .into() + }; + + Some((grab, Focus::Clear)) + } + + pub fn resize(&mut self, seat: &Seat, direction: ResizeDirection, edge: ResizeEdge) { + let Some(output) = seat.focused_output() else { + return; + }; + let (_, idx) = self.workspaces.active_num(&output); + let Some(focused) = seat.get_keyboard().unwrap().current_focus() else { + return; + }; + let amount = (self + .resize_state + .take() + .map(|(_, _, _, amount, _, _)| amount) + .unwrap_or(10) + + 2) + .min(20); + + if self + .workspaces + .sets + .get_mut(&output) + .unwrap() + .sticky_layer + .resize(&focused, direction, edge, amount) + { + self.resize_state = Some((focused, direction, edge, amount, idx, output)); + } else if let Some(workspace) = self.workspaces.get_mut(idx, &output) + && workspace.resize(&focused, direction, edge, amount) + { + self.resize_state = Some((focused, direction, edge, amount, idx, output)); + } + } + + pub fn finish_resize(&mut self, direction: ResizeDirection, edge: ResizeEdge) { + if let Some((old_focused, old_direction, old_edge, _, idx, output)) = + self.resize_state.take() + && old_direction == direction + && old_edge == edge + { + let Some(toplevel) = old_focused.toplevel() else { + return; + }; + let Some(mapped) = self + .workspaces + .sets + .values() + .find_map(|set| { + set.sticky_layer + .mapped() + .find(|m| m.has_surface(&toplevel, WindowSurfaceType::TOPLEVEL)) + }) + .cloned() + .or_else(|| { + let workspace = self.workspaces.get(idx, &output).unwrap(); + workspace + .mapped() + .find(|m| m.has_surface(&toplevel, WindowSurfaceType::TOPLEVEL)) + .cloned() + }) + else { + return; + }; + + let mut resize_state = mapped.resize_state.lock().unwrap(); + if let Some(ResizeState::Resizing(data)) = *resize_state { + mapped.set_resizing(false); + *resize_state = Some(ResizeState::WaitingForCommit(data)); + } + } + } + + #[must_use] + pub fn toggle_stacking( + &mut self, + seat: &Seat, + window: &CosmicMapped, + ) -> Option { + if let Some(set) = self + .workspaces + .sets + .values_mut() + .find(|set| set.sticky_layer.mapped().any(|m| m == window)) + { + let workspace = &mut set.workspaces[set.active]; + set.sticky_layer + .toggle_stacking(window, workspace.focus_stack.get_mut(seat)) + } else if let Some(workspace) = self.space_for_mut(window) { + if workspace.tiling_layer.mapped().any(|(m, _)| m == window) { + workspace + .tiling_layer + .toggle_stacking(window, workspace.focus_stack.get_mut(seat)) + } else if workspace.floating_layer.mapped().any(|w| w == window) { + workspace + .floating_layer + .toggle_stacking(window, workspace.focus_stack.get_mut(seat)) + } else { + None + } + } else { + None + } + } + + #[must_use] + pub fn toggle_stacking_focused( + &mut self, + seat: &Seat, + loop_handle: &LoopHandle<'static, State>, + ) -> Option { + let focused_output = seat.focused_output()?; + let set = self.workspaces.sets.get_mut(&focused_output).unwrap(); + let workspace = &mut set.workspaces[set.active]; + + if matches!( + seat.get_keyboard().unwrap().current_focus(), + Some(KeyboardFocusTarget::Fullscreen(_)) + ) { + return None; + } + + let maybe_window = workspace.focus_stack.get(seat).iter().next().cloned(); + if let Some(FocusTarget::Window(window)) = maybe_window { + let was_maximized = window.is_maximized(false); + if was_maximized { + workspace.unmaximize_request(&window); + } + + let res = if set.sticky_layer.mapped().any(|m| m == &window) { + set.sticky_layer + .toggle_stacking_focused(seat, workspace.focus_stack.get_mut(seat)) + } else if workspace.tiling_layer.mapped().any(|(m, _)| m == &window) { + workspace + .tiling_layer + .toggle_stacking_focused(seat, workspace.focus_stack.get_mut(seat)) + } else if workspace.floating_layer.mapped().any(|w| w == &window) { + workspace + .floating_layer + .toggle_stacking_focused(seat, workspace.focus_stack.get_mut(seat)) + } else { + None + }; + + if was_maximized && let Some(KeyboardFocusTarget::Element(mapped)) = res.as_ref() { + self.maximize_request(mapped, seat, false, loop_handle); + } + + res + } else { + None + } + } + + pub fn toggle_sticky(&mut self, seat: &Seat, mapped: &CosmicMapped) { + // clean from focus-stacks + for workspace in self.workspaces.spaces_mut() { + for seat in self.seats.iter() { + let mut stack = workspace.focus_stack.get_mut(seat); + stack.remove(mapped); + } + } + + if let Some(workspace) = self.space_for_mut(mapped) { + let previous_layer = if workspace.is_tiled(&mapped.active_window()) { + workspace.toggle_floating_window(seat, mapped); + ManagedLayer::Tiling + } else { + ManagedLayer::Floating + }; + let Some(geometry) = workspace.element_geometry(mapped) else { + return; + }; + workspace.unmap_element(mapped); + + *mapped.previous_layer.lock().unwrap() = Some(previous_layer); + let output = workspace.output().clone(); + let handle = workspace.handle; + + for (window, _) in mapped.windows() { + window.set_sticky(true); + toplevel_leave_workspace(&window, &handle); + } + + let set = self.workspaces.sets.get_mut(&output).unwrap(); + set.sticky_layer.map(mapped.clone(), geometry.loc); + + let mut state = mapped.maximized_state.lock().unwrap(); + if let Some(MaximizedState { + original_geometry, + original_layer: _, + original_snapped, + }) = *state + { + *state = Some(MaximizedState { + original_geometry, + original_layer: ManagedLayer::Sticky, + original_snapped, + }); + std::mem::drop(state); + set.workspaces[set.active].floating_layer.map_maximized( + mapped.clone(), + geometry, + false, + ); + } + } else if let Some(set) = self + .workspaces + .sets + .values_mut() + .find(|set| set.sticky_layer.mapped().any(|m| m == mapped)) + { + let geometry = set.sticky_layer.unmap(mapped, None).unwrap(); + + let workspace = &mut set.workspaces[set.active]; + for (window, _) in mapped.windows() { + toplevel_enter_workspace(&window, &workspace.handle); + window.set_sticky(false); + } + let previous_layer = mapped + .previous_layer + .lock() + .unwrap() + .take() + .unwrap_or(ManagedLayer::Floating); + + match previous_layer { + ManagedLayer::Tiling if workspace.tiling_enabled => { + let focus_stack = workspace.focus_stack.get(seat); + workspace + .tiling_layer + .map(mapped.clone(), Some(focus_stack.iter()), None); + } + ManagedLayer::Sticky => unreachable!(), + _ => workspace.floating_layer.map(mapped.clone(), geometry.loc), + } + + let mut state = mapped.maximized_state.lock().unwrap(); + if let Some(MaximizedState { + original_geometry, + original_layer: _, + original_snapped, + }) = *state + { + *state = Some(MaximizedState { + original_geometry, + original_layer: previous_layer, + original_snapped, + }); + std::mem::drop(state); + workspace + .floating_layer + .map_maximized(mapped.clone(), geometry, false); + } + } + + self.append_focus_stack(mapped.clone(), seat); + } + + pub fn toggle_sticky_current(&mut self, seat: &Seat) { + if matches!( + seat.get_keyboard().unwrap().current_focus(), + Some(KeyboardFocusTarget::Fullscreen(_)) + ) { + return; + } + let set = self.workspaces.sets.get_mut(&seat.active_output()).unwrap(); + let workspace = &mut set.workspaces[set.active]; + + let maybe_window = workspace.focus_stack.get(seat).iter().next().cloned(); + if let Some(FocusTarget::Window(mapped)) = maybe_window { + self.toggle_sticky(seat, &mapped); + } + } + + #[must_use] + pub fn fullscreen_request( + &mut self, + surface: &S, + output: Output, + _loop_handle: &LoopHandle<'static, State>, + ) -> Option + where + CosmicSurface: PartialEq, + { + let mapped = self.element_for_surface(surface).cloned()?; + let seat = self.seats.last_active().clone(); + let window; + + if let Some((old_output, set)) = self + .workspaces + .sets + .iter_mut() + .find(|(_, set)| set.sticky_layer.mapped().any(|m| m == &mapped)) + { + let mut from = set.sticky_layer.element_geometry(&mapped).unwrap(); + let mut was_maximized = false; + let mut restore_state = None; + let was_stack = mapped.is_stack(); + window = if let Some(stack) = mapped.stack_ref() + && stack.len() > 1 + { + let idx = stack.surfaces().position(|s| &s == surface)?; + let surface = stack.remove_idx(idx)?; + restore_state = Some(FullscreenRestoreState::Stack { + state: StackRestoreData { + stack: mapped.key(), + idx, + }, + }); + surface + } else { + // Must be set before `map_internal`/`unmap` below, as both may call + // intermediate `configure()`, which would send a configure event without the + // fullscreen state, causing clients like Chromium to cancel the transition. + mapped.set_fullscreen(true); + + if let Some(state) = mapped.maximized_state.lock().unwrap().take() { + mapped.set_maximized(false); + set.sticky_layer.map_internal( + mapped.clone(), + Some(state.original_geometry.loc), + Some(state.original_geometry.size.as_logical()), + None, + ); + was_maximized = true; + } + + from = set.sticky_layer.unmap(&mapped, None).unwrap(); + mapped.active_window() + }; + + toplevel_leave_output(&window, old_output); + let old_output = old_output.downgrade(); + let workspace_handle = self.active_space(&output).unwrap().handle; + toplevel_enter_output(&window, &output); + toplevel_enter_workspace(&window, &workspace_handle); + + let workspace = self.active_space_mut(&output).unwrap(); + workspace.map_fullscreen( + &window, + &seat, + Some(restore_state.unwrap_or(FullscreenRestoreState::Sticky { + output: old_output, + state: FloatingRestoreData { + geometry: from, + output_size: workspace.output.geometry().size.as_logical(), + was_maximized, + was_snapped: None, + }, + was_stack, + })), + Some(from), + ); + } else { + let workspace = self.space_for_mut(&mapped)?; + if mapped.is_minimized() { + // TODO: Rewrite the `MinimizedWindow` to restore to fullscreen + return None; + } + + // Must be set before `unmap_surface()`. + // `Workspace::unmap_surface` may call intermediate `configure()` internally, which would send + // a configure event without the fullscreen state, causing clients like Chromium to cancel the transition. + mapped.set_fullscreen(true); + + let from = workspace.element_geometry(&mapped).unwrap(); + let (surface, state) = workspace.unmap_surface(surface).unwrap(); + window = surface; + let handle = workspace.handle; + + toplevel_leave_output(&window, &workspace.output); + toplevel_leave_workspace(&window, &workspace.handle); + + let workspace = self.active_space_mut(&output).unwrap(); + toplevel_enter_output(&window, &output); + toplevel_enter_workspace(&window, &workspace.handle); + + workspace.map_fullscreen( + &window, + &seat, + match state { + WorkspaceRestoreData::Floating(floating_state) => { + Some(FullscreenRestoreState::Floating { + workspace: handle, + state: floating_state, + was_stack: mapped.is_stack(), + }) + } + WorkspaceRestoreData::Tiling(tiling_state) => { + Some(FullscreenRestoreState::Tiling { + workspace: handle, + state: tiling_state, + was_stack: mapped.is_stack(), + }) + } + WorkspaceRestoreData::Stack(stack_state) => { + Some(FullscreenRestoreState::Stack { state: stack_state }) + } + WorkspaceRestoreData::Fullscreen(_) => unreachable!(), + }, + Some(from), + ); + }; + + Some(KeyboardFocusTarget::Fullscreen(window)) + } + + pub fn unfullscreen_request( + &mut self, + surface: &S, + loop_handle: &LoopHandle<'static, State>, + ) -> Option + where + CosmicSurface: PartialEq, + { + let maybe_workspace = self.workspaces.iter_mut().find_map(|(_, s)| { + s.workspaces + .iter_mut() + .find(|w| w.get_fullscreen_surfaces().any(|f| &f.surface == surface)) + }); + + if let Some(workspace) = maybe_workspace { + let (old_fullscreen, restore, _) = + workspace.remove_fullscreen_surface(surface).unwrap(); + toplevel_leave_output(&old_fullscreen, &workspace.output); + toplevel_leave_workspace(&old_fullscreen, &workspace.handle); + + let window = self.remap_unfullscreened_window(old_fullscreen, restore, loop_handle); + Some(KeyboardFocusTarget::Element(window)) + } else { + None + } + } + + pub fn update_toolkit( + &mut self, + toolkit: cosmic::config::CosmicTk, + xdg_activation_state: &XdgActivationState, + workspace_state: &mut WorkspaceUpdateGuard<'_, State>, + ) { + let mut container = cosmic::config::COSMIC_TK.write().unwrap(); + if *container != toolkit { + *container = toolkit; + drop(container); + self.refresh(xdg_activation_state, workspace_state); + self.workspaces.force_redraw(); + } + } + + pub fn set_theme( + &mut self, + theme: cosmic::Theme, + xdg_activation_state: &XdgActivationState, + workspace_state: &mut WorkspaceUpdateGuard<'_, State>, + ) { + self.theme = theme.clone(); + self.refresh(xdg_activation_state, workspace_state); + self.workspaces.set_theme(theme.clone()); + } + + pub fn theme(&self) -> &cosmic::Theme { + &self.theme + } + + pub fn update_tiling_exceptions<'a, I>(&mut self, exceptions: I) + where + I: Iterator, + { + self.tiling_exceptions = layout::TilingExceptions::new(exceptions); + } + + pub fn take_presentation_feedback( + &self, + output: &Output, + render_element_states: &RenderElementStates, + ) -> OutputPresentationFeedback { + let mut output_presentation_feedback = OutputPresentationFeedback::new(output); + + if let Some(active) = self.active_space(output) { + active.mapped().for_each(|mapped| { + mapped.active_window().take_presentation_feedback( + &mut output_presentation_feedback, + surface_primary_scanout_output, + |surface, _| { + surface_presentation_feedback_flags_from_states( + surface, + None, + render_element_states, + ) + }, + ); + }); + } + + self.override_redirect_windows.iter().for_each(|or| { + if let Some(wl_surface) = or.wl_surface() { + take_presentation_feedback_surface_tree( + &wl_surface, + &mut output_presentation_feedback, + surface_primary_scanout_output, + |surface, _| { + surface_presentation_feedback_flags_from_states( + surface, + None, + render_element_states, + ) + }, + ) + } + }); + + let map = smithay::desktop::layer_map_for_output(output); + for layer_surface in map.layers() { + let namespace = self.workspaces.active_num(output).1; + layer_surface.take_presentation_feedback( + &mut output_presentation_feedback, + surface_primary_scanout_output, + |surface, _| { + surface_presentation_feedback_flags_from_states( + surface, + Some(namespace), + render_element_states, + ) + }, + ); + } + + output_presentation_feedback + } + + pub fn mapped(&self) -> impl Iterator { + self.workspaces.iter().flat_map(|(_, set)| { + set.sticky_layer + .mapped() + .chain(set.minimized_windows.iter().flat_map(|m| m.mapped())) + .chain(set.workspaces.iter().flat_map(|w| { + w.mapped() + .chain(w.minimized_windows.iter().flat_map(|m| m.mapped())) + })) + }) + } +} + +fn workspace_set_idx( + state: &mut WorkspaceUpdateGuard<'_, State>, + idx: u8, + handle: &WorkspaceHandle, + name: Option<&str>, +) { + state.set_workspace_name(handle, name.unwrap_or(&format!("{}", idx))); + state.set_workspace_coordinates(handle, &[idx as u32]); +} + +pub fn check_grab_preconditions( + seat: &Seat, + serial: Option, + client_initiated: Option<&WlSurface>, +) -> Option { + use smithay::reexports::wayland_server::Resource; + + let pointer = seat.get_pointer().unwrap(); + let touch = seat.get_touch().unwrap(); + + let start_data = + if serial.is_some_and(|serial| touch.has_grab(serial)) { + GrabStartData::Touch(touch.grab_start_data().unwrap()) + } else { + GrabStartData::Pointer(pointer.grab_start_data().unwrap_or_else(|| { + PointerGrabStartData { + focus: pointer.current_focus().map(|f| (f, Point::from((0., 0.)))), + button: 0x110, + location: pointer.current_location(), + } + })) + }; + + if let Some(surface) = client_initiated { + // Check that this surface has a click or touch down grab. + if !match serial { + Some(serial) => pointer.has_grab(serial) || touch.has_grab(serial), + None => pointer.is_grabbed() | touch.is_grabbed(), + } { + return None; + } + + // If the focus was for a different surface, ignore the request. + if start_data.focus().is_none() + || !start_data.focus().unwrap().0.same_client_as(&surface.id()) + { + return None; + } + } + + Some(start_data) +} diff --git a/src/shell/seats.rs b/src/shell/seats.rs new file mode 100644 index 0000000..11211e9 --- /dev/null +++ b/src/shell/seats.rs @@ -0,0 +1,438 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use std::{any::Any, cell::RefCell, collections::HashMap, sync::Mutex}; + +use crate::{ + backend::render::cursor::CursorState, + config::{Config, xkb_config_to_wl}, + input::{ModifiersShortcutQueue, SupressedButtons, SupressedKeys}, + state::State, +}; +use smithay::{ + backend::input::{Device, DeviceCapability}, + desktop::utils::bbox_from_surface_tree, + input::{ + Seat, SeatState, + keyboard::{LedState, XkbConfig}, + pointer::{CursorImageAttributes, CursorImageStatus}, + }, + output::Output, + reexports::{ + input::Device as InputDevice, + wayland_server::{DisplayHandle, protocol::wl_surface::WlSurface}, + }, + utils::{Buffer, IsAlive, Logical, Monotonic, Point, Rectangle, Serial, Time, Transform}, + wayland::compositor::with_states, +}; +use tracing::warn; + +use super::grabs::{SeatMenuGrabState, SeatMoveGrabState}; + +crate::utils::id_gen!(next_seat_id, SEAT_ID, SEAT_IDS); + +// for more information on seats, see: +// +/// Seats are an abstraction over a set of input devices grouped together, such as a keyboard, pointer and touch device. +/// i.e. Those used by a user to operate the computer. +#[derive(Debug)] +pub struct Seats { + seats: Vec>, + last_active: Option>, +} + +impl Default for Seats { + fn default() -> Self { + Self::new() + } +} + +impl Seats { + pub fn new() -> Seats { + Seats { + seats: Vec::new(), + last_active: None, + } + } + + pub fn add_seat(&mut self, seat: Seat) { + if self.seats.is_empty() { + self.last_active = Some(seat.clone()); + } + self.seats.push(seat); + } + + pub fn remove_seat(&mut self, seat: &Seat) { + self.seats.retain(|s| s != seat); + if self.seats.is_empty() { + self.last_active = None; + } else if self.last_active.as_ref().is_some_and(|s| s == seat) { + self.last_active = Some(self.seats[0].clone()); + } + } + + pub fn iter(&self) -> impl Iterator> { + self.seats.iter() + } + + pub fn last_active(&self) -> &Seat { + self.last_active.as_ref().expect("No seat?") + } + + pub fn update_last_active(&mut self, seat: &Seat) { + self.last_active = Some(seat.clone()); + } + + pub fn for_device(&self, device: &D) -> Option<&Seat> { + self.iter().find(|seat| { + let userdata = seat.user_data(); + let devices = userdata.get::().unwrap(); + devices.has_device(device) + }) + } +} + +impl Devices { + pub fn add_device( + &self, + device: &D, + led_state: LedState, + ) -> Vec { + let id = device.id(); + let mut map = self.capabilities.borrow_mut(); + let caps = [ + DeviceCapability::Keyboard, + DeviceCapability::Pointer, + DeviceCapability::TabletTool, + ] + .iter() + .cloned() + .filter(|c| device.has_capability(*c)) + .collect::>(); + let new_caps = caps + .iter() + .cloned() + .filter(|c| map.values().flatten().all(|has| *c != *has)) + .collect::>(); + map.insert(id, caps); + + if device.has_capability(DeviceCapability::Keyboard) + && let Some(device) = ::downcast_ref::(device) + { + let mut device = device.clone(); + device.led_update(led_state.into()); + self.keyboards.borrow_mut().push(device); + } + + new_caps + } + + pub fn has_device(&self, device: &D) -> bool { + self.capabilities.borrow().contains_key(&device.id()) + } + + pub fn remove_device(&self, device: &D) -> Vec { + let id = device.id(); + + let mut keyboards = self.keyboards.borrow_mut(); + if let Some(idx) = keyboards.iter().position(|x| x.id() == id) { + keyboards.remove(idx); + } + + let mut map = self.capabilities.borrow_mut(); + map.remove(&id) + .unwrap_or_default() + .into_iter() + .filter(|c| map.values().flatten().all(|has| *c != *has)) + .collect() + } + + pub fn update_led_state(&self, led_state: LedState) { + for keyboard in self.keyboards.borrow_mut().iter_mut() { + keyboard.led_update(led_state.into()); + } + } +} + +#[derive(Default)] +pub struct Devices { + capabilities: RefCell>>, + // Used for updating keyboard leds on kms backend + keyboards: RefCell>, +} + +impl Default for SeatId { + fn default() -> SeatId { + SeatId(next_seat_id()) + } +} + +impl Drop for SeatId { + fn drop(&mut self) { + SEAT_IDS.lock().unwrap().remove(&self.0); + } +} + +#[repr(transparent)] +struct SeatId(pub usize); + +/// The output which contains the cursor associated with a seat. +struct ActiveOutput(pub Mutex); + +/// The output which currently has keyboard focus +struct FocusedOutput(pub Mutex>); + +#[derive(Default)] +pub struct PointerConstraintHint(pub Mutex)>>); + +#[derive(Default)] +pub struct LastModifierChange(pub Mutex>); + +pub fn create_seat( + dh: &DisplayHandle, + seat_state: &mut SeatState, + output: &Output, + config: &Config, + name: String, +) -> Seat { + let mut seat = seat_state.new_wl_seat(dh, name); + let userdata = seat.user_data(); + userdata.insert_if_missing_threadsafe(SeatId::default); + userdata.insert_if_missing(Devices::default); + userdata.insert_if_missing(SupressedKeys::default); + userdata.insert_if_missing(SupressedButtons::default); + userdata.insert_if_missing(ModifiersShortcutQueue::default); + userdata.insert_if_missing(LastModifierChange::default); + userdata.insert_if_missing_threadsafe(SeatMoveGrabState::default); + userdata.insert_if_missing_threadsafe(SeatMenuGrabState::default); + userdata.insert_if_missing_threadsafe(CursorState::default); + userdata.insert_if_missing_threadsafe(|| ActiveOutput(Mutex::new(output.clone()))); + userdata.insert_if_missing_threadsafe(|| FocusedOutput(Mutex::new(None))); + userdata.insert_if_missing_threadsafe(PointerConstraintHint::default); + userdata.insert_if_missing_threadsafe(|| Mutex::new(CursorImageStatus::default_named())); + + // A lot of clients bind keyboard and pointer unconditionally once on launch.. + // Initial clients might race the compositor on adding periheral and + // end up in a state, where they are not able to receive input. + // Additionally a lot of clients don't handle keyboards/pointer objects being + // removed very well either and we don't want to crash applications, because the + // user is replugging their keyboard or mouse. + // + // So instead of doing the right thing (and initialize these capabilities as matching + // devices appear), we have to surrender to reality and just always expose a keyboard and pointer. + let conf = config.xkb_config(); + seat.add_keyboard( + xkb_config_to_wl(&conf), + (conf.repeat_delay as i32).abs(), + (conf.repeat_rate as i32).abs(), + ) + .or_else(|err| { + warn!( + ?err, + "Failed to load provided xkb config. Trying default...", + ); + seat.add_keyboard( + XkbConfig::default(), + (conf.repeat_delay as i32).abs(), + (conf.repeat_rate as i32).abs(), + ) + }) + .expect("Failed to load xkb configuration files"); + seat.add_pointer(); + seat.add_touch(); + + seat +} + +#[derive(Debug, Copy, Clone)] +pub struct CursorGeometry { + pub geometry: Rectangle, + pub hotspot: Point, +} + +pub trait SeatExt { + fn id(&self) -> usize; + + fn active_output(&self) -> Output; + fn focused_output(&self) -> Option; + fn focused_or_active_output(&self) -> Output { + self.focused_output() + .unwrap_or_else(|| self.active_output()) + } + fn set_active_output(&self, output: &Output); + fn set_focused_output(&self, output: Option<&Output>); + fn devices(&self) -> &Devices; + fn supressed_keys(&self) -> &SupressedKeys; + fn supressed_buttons(&self) -> &SupressedButtons; + fn modifiers_shortcut_queue(&self) -> &ModifiersShortcutQueue; + fn last_modifier_change(&self) -> Option; + fn pointer_constraint_hint(&self) -> Option<(WlSurface, Point)>; + fn set_pointer_constraint_hint(&self, hint: Option<(WlSurface, Point)>); + + fn cursor_geometry( + &self, + loc: impl Into>, + time: Time, + ) -> Option; + fn cursor_image_status(&self) -> CursorImageStatus; + fn set_cursor_image_status(&self, status: CursorImageStatus); +} + +impl SeatExt for Seat { + fn id(&self) -> usize { + self.user_data().get::().unwrap().0 + } + + /// Returns the output that contains the cursor associated with a seat. Note that the window which has keyboard focus + /// may be on a different output. Currently, to get the focused output, use [`Self::focused_output`]. + fn active_output(&self) -> Output { + self.user_data() + .get::() + .map(|x| x.0.lock().unwrap().clone()) + .unwrap() + } + + /// Returns the output which currently has keyboard focus. If no window has keyboard focus (e.g. when there are no windows) + /// the focused output will be the same as the active output. + fn focused_output(&self) -> Option { + if self + .get_keyboard() + .is_some_and(|k| k.current_focus().is_some()) + { + self.user_data() + .get::() + .map(|x| x.0.lock().unwrap().clone())? + } else { + None + } + } + + fn set_active_output(&self, output: &Output) { + *self + .user_data() + .get::() + .unwrap() + .0 + .lock() + .unwrap() = output.clone(); + } + + fn set_focused_output(&self, output: Option<&Output>) { + *self + .user_data() + .get::() + .unwrap() + .0 + .lock() + .unwrap() = output.cloned(); + } + + fn devices(&self) -> &Devices { + self.user_data().get::().unwrap() + } + + fn supressed_keys(&self) -> &SupressedKeys { + self.user_data().get::().unwrap() + } + + fn supressed_buttons(&self) -> &SupressedButtons { + self.user_data().get::().unwrap() + } + + fn modifiers_shortcut_queue(&self) -> &ModifiersShortcutQueue { + self.user_data().get::().unwrap() + } + + fn last_modifier_change(&self) -> Option { + *self + .user_data() + .get::() + .unwrap() + .0 + .lock() + .unwrap() + } + + fn pointer_constraint_hint(&self) -> Option<(WlSurface, Point)> { + let lock = self.user_data().get::().unwrap(); + let mut hint = lock.0.lock().unwrap(); + // Check if alive + if let Some((ref surface, _)) = *hint + && !surface.alive() + { + *hint = None; + } + hint.clone() + } + + fn set_pointer_constraint_hint(&self, hint: Option<(WlSurface, Point)>) { + let lock = self.user_data().get::().unwrap(); + *lock.0.lock().unwrap() = hint; + } + + fn cursor_geometry( + &self, + loc: impl Into>, + time: Time, + ) -> Option { + let location = loc.into().to_i32_round(); + + match self.cursor_image_status() { + CursorImageStatus::Surface(surface) => { + let hotspot = with_states(&surface, |states| { + states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap() + .hotspot + }); + let geo = bbox_from_surface_tree(&surface, (location.x, location.y)); + let buffer_geo = Rectangle::new( + (geo.loc.x, geo.loc.y).into(), + geo.size.to_buffer(1, Transform::Normal), + ); + Some(CursorGeometry { + geometry: buffer_geo, + hotspot: (hotspot.x, hotspot.y).into(), + }) + } + CursorImageStatus::Named(cursor_icon) => { + let seat_userdata = self.user_data(); + seat_userdata.insert_if_missing_threadsafe(CursorState::default); + let state = seat_userdata.get::().unwrap(); + let frame = state + .lock() + .unwrap() + .get_named_cursor(cursor_icon) + .get_image(1, time.as_millis()); + + Some(CursorGeometry { + geometry: Rectangle::new( + location, + (frame.width as i32, frame.height as i32).into(), + ), + hotspot: (frame.xhot as i32, frame.yhot as i32).into(), + }) + } + CursorImageStatus::Hidden => None, + } + } + + fn cursor_image_status(&self) -> CursorImageStatus { + let lock = self.user_data().get::>().unwrap(); + // Reset the cursor if the surface is no longer alive + let mut cursor_status = lock.lock().unwrap(); + if let CursorImageStatus::Surface(ref surface) = *cursor_status + && !surface.alive() + { + *cursor_status = CursorImageStatus::default_named(); + } + cursor_status.clone() + } + + fn set_cursor_image_status(&self, status: CursorImageStatus) { + let cursor_status = self.user_data().get::>().unwrap(); + *cursor_status.lock().unwrap() = status; + } +} diff --git a/src/shell/workspace.rs b/src/shell/workspace.rs new file mode 100644 index 0000000..84a020f --- /dev/null +++ b/src/shell/workspace.rs @@ -0,0 +1,2288 @@ +use crate::backend::render::wayland::SurfaceRenderElement; +use crate::shell::focus::FocusTarget; +use crate::shell::layout::tiling::RestoreTilingState; +use crate::wayland::handlers::xdg_activation::ActivationContext; +use crate::{ + backend::render::{BackdropShader, element::AsGlowRenderer}, + shell::{ + ANIMATION_DURATION, OverviewMode, SeatMoveGrabState, + layout::{ + floating::{FloatingLayout, TiledCorners}, + tiling::TilingLayout, + }, + }, + state::State, + utils::{prelude::*, tween::EaseRectangle}, + wayland::{ + handlers::image_copy_capture::ImageCopySessions, + protocols::{ + toplevel_info::{toplevel_enter_output, toplevel_leave_output}, + workspace::{WorkspaceHandle, WorkspaceUpdateGuard}, + }, + }, +}; +use cosmic_comp_config::AppearanceConfig; +use cosmic_comp_config::workspace::{OutputMatch, PinnedWorkspace}; + +use cosmic::theme::CosmicTheme; +use cosmic_protocols::workspace::v2::server::zcosmic_workspace_handle_v2::TilingState; +use id_tree::Tree; +use indexmap::IndexSet; +use keyframe::{ease, functions::EaseInOutCubic}; +use smallvec::SmallVec; +use smithay::backend::drm::DrmNode; +use smithay::backend::renderer::element::{Kind, NamespacedElement}; +use smithay::output::WeakOutput; +use smithay::utils::user_data::UserDataMap; +use smithay::{ + backend::renderer::{ + element::{ + Element, Id, RenderElement, texture::TextureRenderElement, utils::RescaleRenderElement, + }, + gles::GlesTexture, + glow::GlowRenderer, + utils::{DamageSet, OpaqueRegions}, + }, + desktop::{WindowSurfaceType, layer_map_for_output, space::SpaceElement}, + input::Seat, + output::Output, + reexports::wayland_server::Client, + utils::{Buffer as BufferCoords, IsAlive, Logical, Physical, Point, Rectangle, Scale, Size}, + wayland::xdg_activation::XdgActivationState, +}; +use std::{ + collections::{HashMap, VecDeque}, + sync::atomic::{AtomicBool, Ordering}, + time::{Duration, Instant}, +}; +use wayland_backend::server::ClientId; + +use super::{ + CosmicMappedRenderElement, CosmicSurface, ResizeDirection, ResizeMode, + element::{ + CosmicMapped, CosmicMappedKey, MaximizedState, resize_indicator::ResizeIndicator, + stack::CosmicStackRenderElement, swap_indicator::SwapIndicator, + window::CosmicWindowRenderElement, + }, + focus::{ + FocusStack, FocusStackMut, + target::{KeyboardFocusTarget, PointerFocusTarget, WindowGroup}, + }, + grabs::ResizeEdge, + layout::tiling::{Data, NodeDesc}, +}; + +const FULLSCREEN_ANIMATION_DURATION: Duration = Duration::from_millis(200); + +// For stable workspace id, generate random 24-bit integer, as a hex string +// Must be compared with existing workspaces work uniqueness. +pub fn random_workspace_id() -> String { + let id = rand::random_range(0..(2 << 24)); + format!("{:x}", id) +} + +fn output_match_for_output(output: &Output) -> OutputMatch { + OutputMatch { + name: output.name(), + edid: output.edid().cloned(), + } +} + +// If `disambguate` is true, check that edid *and* connector name match. +// Otherwise, match only edid (if it exists) +fn output_matches(output_match: &OutputMatch, output: &Output, disambiguate: bool) -> bool { + if output_match.edid.as_ref() != output.edid() { + false + } else if disambiguate || output_match.edid.is_none() { + output_match.name == output.name() + } else { + true + } +} + +#[derive(Debug)] +pub struct Workspace { + pub output: Output, + pub tiling_layer: TilingLayout, + pub floating_layer: FloatingLayout, + pub minimized_windows: Vec, + pub tiling_enabled: bool, + pub fullscreen_surfaces: Vec, + pub pinned: bool, + pub id: Option, + pub name: Option, + + pub handle: WorkspaceHandle, + pub focus_stack: FocusStacks, + pub image_copy: ImageCopySessions, + output_stack: VecDeque, + pub(super) backdrop_id: Id, + pub dirty: AtomicBool, +} + +#[derive(Debug)] +pub enum MinimizedWindow { + Fullscreen { + surface: CosmicSurface, + previous: Option, + }, + Floating { + window: CosmicMapped, + previous: FloatingRestoreData, + }, + Tiling { + window: CosmicMapped, + previous: TilingRestoreData, + }, +} + +impl PartialEq for MinimizedWindow { + fn eq(&self, other: &CosmicMapped) -> bool { + self.mapped().is_some_and(|m| m == other) + } +} + +impl MinimizedWindow { + pub fn mapped(&self) -> Option<&CosmicMapped> { + match self { + MinimizedWindow::Floating { window, .. } | MinimizedWindow::Tiling { window, .. } => { + Some(window) + } + _ => None, + } + } + + pub fn mapped_mut(&mut self) -> Option<&mut CosmicMapped> { + match self { + MinimizedWindow::Floating { window, .. } | MinimizedWindow::Tiling { window, .. } => { + Some(window) + } + _ => None, + } + } + + pub fn active_window(&self) -> CosmicSurface { + match self { + MinimizedWindow::Floating { window, .. } | MinimizedWindow::Tiling { window, .. } => { + window.active_window() + } + MinimizedWindow::Fullscreen { surface, .. } => surface.clone(), + } + } + + pub fn windows(&self) -> impl Iterator + '_ { + match self { + MinimizedWindow::Floating { window, .. } | MinimizedWindow::Tiling { window, .. } => { + Box::new(window.windows().map(|(s, _)| s)) + as Box> + } + MinimizedWindow::Fullscreen { surface, .. } => { + Box::new(std::iter::once(surface.clone())) as _ + } + } + } + + pub fn unmaximize( + &mut self, + original_geometry: Rectangle, + original_snapped: Option, + ) { + match self { + MinimizedWindow::Fullscreen { .. } => {} + MinimizedWindow::Tiling { + window, previous, .. + } => { + previous.was_maximized = false; + window.set_maximized(false); + window.configure(); + } + MinimizedWindow::Floating { + window, previous, .. + } => { + previous.geometry = original_geometry; + previous.was_snapped = original_snapped; + window.set_maximized(false); + window.configure(); + } + } + } +} + +#[derive(Debug, Clone)] +pub struct FullscreenSurface { + pub surface: CosmicSurface, + pub previous_state: Option, + pub previous_geometry: Option>, + start_at: Option, + pub ended_at: Option, +} + +impl PartialEq for FullscreenSurface { + fn eq(&self, other: &Self) -> bool { + self.surface == other.surface + } +} + +impl FullscreenSurface { + pub fn is_animating(&self) -> bool { + self.start_at.is_some() || self.ended_at.is_some() + } +} + +impl IsAlive for FullscreenSurface { + fn alive(&self) -> bool { + self.surface.alive() + } +} + +/// LIFO stack of focus targets +#[derive(Debug, Default)] +pub struct FocusStacks(HashMap, IndexSet>); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ManagedLayer { + Fullscreen, + Tiling, + Floating, + Sticky, +} + +#[derive(Debug, Clone)] +pub enum FullscreenRestoreState { + Tiling { + workspace: WorkspaceHandle, + state: TilingRestoreData, + was_stack: bool, + }, + Floating { + workspace: WorkspaceHandle, + state: FloatingRestoreData, + was_stack: bool, + }, + Sticky { + output: WeakOutput, + state: FloatingRestoreData, + was_stack: bool, + }, + Stack { + state: StackRestoreData, + }, +} + +impl FullscreenRestoreState { + pub fn was_maximized(&self) -> bool { + match self { + FullscreenRestoreState::Floating { state, .. } + | FullscreenRestoreState::Sticky { state, .. } => state.was_maximized, + FullscreenRestoreState::Tiling { state, .. } => state.was_maximized, + FullscreenRestoreState::Stack { .. } => false, + } + } + + // Surface was previously a single-window stack + pub fn was_stack(&self) -> bool { + match self { + FullscreenRestoreState::Floating { was_stack, .. } + | FullscreenRestoreState::Sticky { was_stack, .. } => *was_stack, + FullscreenRestoreState::Tiling { was_stack, .. } => *was_stack, + // Stack wasn't removed; surface was removed from the stack + FullscreenRestoreState::Stack { .. } => false, + } + } +} + +#[derive(Debug, Clone)] +pub enum WorkspaceRestoreData { + Fullscreen(Option), + Tiling(TilingRestoreData), + Floating(FloatingRestoreData), + Stack(StackRestoreData), +} + +#[derive(Debug, Clone)] +pub struct FloatingRestoreData { + pub geometry: Rectangle, + pub output_size: Size, + pub was_maximized: bool, + pub was_snapped: Option, +} + +impl FloatingRestoreData { + pub fn position_relative(&self, output_size: Size) -> Point { + if self.output_size != output_size { + Point::from(( + (self.geometry.loc.x as f64 / self.output_size.w as f64 * output_size.w as f64) + .floor() as i32, + (self.geometry.loc.y as f64 / self.output_size.h as f64 * output_size.h as f64) + .floor() as i32, + )) + } else { + self.geometry.loc + } + } +} + +#[derive(Debug, Clone)] +pub struct TilingRestoreData { + pub state: Option, + pub was_maximized: bool, +} + +#[derive(Debug, Clone)] +pub struct StackRestoreData { + pub stack: CosmicMappedKey, + pub idx: usize, +} + +#[derive(Debug, Clone)] +pub struct FullscreenRestoreData { + pub previous_state: FullscreenRestoreState, + pub previous_geometry: Rectangle, +} + +#[derive(Debug, Clone, PartialEq)] +#[allow(clippy::large_enum_variant)] +pub enum FocusResult { + None, + Handled, + Some(KeyboardFocusTarget), +} + +impl FocusResult { + pub fn or_else(self, f: F) -> FocusResult + where + F: FnOnce() -> FocusResult, + { + match self { + FocusResult::None => f(), + x => x, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum MoveResult { + None, + Done, + MoveFurther(KeyboardFocusTarget), + ShiftFocus(KeyboardFocusTarget), +} + +impl MoveResult { + pub fn or_else(self, f: F) -> MoveResult + where + F: FnOnce() -> MoveResult, + { + match self { + MoveResult::None => f(), + x => x, + } + } +} + +impl Workspace { + pub fn new( + handle: WorkspaceHandle, + output: Output, + tiling_enabled: bool, + theme: cosmic::Theme, + appearance: AppearanceConfig, + ) -> Workspace { + let tiling_layer = TilingLayout::new(theme.clone(), appearance, &output); + let floating_layer = FloatingLayout::new(theme, appearance, &output); + let output_match = output_match_for_output(&output); + + Workspace { + output, + tiling_layer, + floating_layer, + tiling_enabled, + minimized_windows: Vec::new(), + fullscreen_surfaces: Vec::new(), + pinned: false, + id: None, + name: None, + handle, + focus_stack: FocusStacks::default(), + image_copy: ImageCopySessions::default(), + output_stack: { + let mut queue = VecDeque::new(); + queue.push_back(output_match); + queue + }, + backdrop_id: Id::new(), + dirty: AtomicBool::new(false), + } + } + + pub fn from_pinned( + pinned: &PinnedWorkspace, + handle: WorkspaceHandle, + output: Output, + theme: cosmic::Theme, + appearance: AppearanceConfig, + ) -> Self { + let tiling_layer = TilingLayout::new(theme.clone(), appearance, &output); + let floating_layer = FloatingLayout::new(theme, appearance, &output); + let output_match = output_match_for_output(&output); + + Workspace { + output, + tiling_layer, + floating_layer, + tiling_enabled: pinned.tiling_enabled, + minimized_windows: Vec::new(), + fullscreen_surfaces: Vec::new(), + pinned: true, + id: pinned.id.clone(), + name: pinned.name.clone(), + handle, + focus_stack: FocusStacks::default(), + image_copy: ImageCopySessions::default(), + output_stack: { + let mut queue = VecDeque::new(); + queue.push_back(pinned.output.clone()); + if output_match != pinned.output { + queue.push_back(output_match); + } + queue + }, + backdrop_id: Id::new(), + dirty: AtomicBool::new(false), + } + } + + pub fn to_pinned(&self) -> Option { + let output = self.explicit_output().clone(); + if self.pinned { + debug_assert!(self.id.is_some()); + Some(PinnedWorkspace { + output: cosmic_comp_config::workspace::OutputMatch { + name: output.name, + edid: output.edid, + }, + tiling_enabled: self.tiling_enabled, + id: self.id.clone(), + name: self.name.clone(), + }) + } else { + None + } + } + + #[profiling::function] + pub fn refresh(&mut self) { + // seems it removes dead windows + // self.fullscreen.take_if(|w| !w.alive()); + self.fullscreen_surfaces.retain(|w| w.alive()); + + self.floating_layer.refresh(); + self.tiling_layer.refresh(); + } + + fn has_activation_token(&self, xdg_activation_state: &XdgActivationState) -> bool { + xdg_activation_state.tokens().any(|(_, data)| { + if let ActivationContext::Workspace(handle) = + data.user_data.get::().unwrap() + { + *handle == self.handle + && data.timestamp.elapsed() < super::ACTIVATION_TOKEN_EXPIRE_TIME + } else { + false + } + }) + } + + // Auto-removal of workspaces is allowed if empty, unless blocked by an + // unused and unexpired activation token, or pinned. + pub fn can_auto_remove(&self, xdg_activation_state: &XdgActivationState) -> bool { + self.is_empty() && !self.has_activation_token(xdg_activation_state) && !self.pinned + } + + /// cleans up any window that is not alive anymore + pub fn refresh_focus_stack(&mut self) { + for (seat, stack) in self.focus_stack.0.iter_mut() { + let fullscreen_surfaces: Vec<&CosmicSurface> = self + .fullscreen_surfaces + .iter() + .filter(|f| f.alive() && f.ended_at.is_none()) + .map(|f| &f.surface) + .collect(); + + // Move grab is treated as focused, so don't change focus to a + // window while grab exists. + let move_grab_state = seat + .user_data() + .get::() + .unwrap() + .lock() + .unwrap(); + let move_mapped = (*move_grab_state) + .as_ref() + .map(|move_grab_state| move_grab_state.element()); + + let mapped = || { + self.floating_layer + .mapped() + .chain(self.tiling_layer.mapped().map(|(w, _)| w)) + .chain(move_mapped.iter()) + }; + stack.retain(|w| match w { + FocusTarget::Fullscreen(s) => fullscreen_surfaces.contains(&s), + FocusTarget::Window(w) => mapped().any(|m| w == m), + }); + } + } + + pub fn animations_going(&self) -> bool { + self.tiling_layer.animations_going() + || self.floating_layer.animations_going() + || self.fullscreen_surfaces.iter().any(|f| f.is_animating()) + || self.dirty.swap(false, Ordering::SeqCst) + } + + pub fn update_animations(&mut self) -> HashMap { + for f in self.fullscreen_surfaces.iter_mut() { + if let Some(start) = f.start_at.as_ref() { + let duration_since = Instant::now().duration_since(*start); + if duration_since > FULLSCREEN_ANIMATION_DURATION { + f.start_at.take(); + self.dirty.store(true, Ordering::SeqCst); + } + } + } + + self.fullscreen_surfaces.retain(|f| { + if let Some(end) = f.ended_at + && Instant::now().duration_since(end) >= FULLSCREEN_ANIMATION_DURATION + { + self.dirty.store(true, Ordering::SeqCst); + return false; + } + true + }); + + let clients = self.tiling_layer.update_animation_state(); + self.floating_layer.update_animation_state(); + clients + } + + pub fn output(&self) -> &Output { + &self.output + } + + /// Output workspace was originally created on, or explicitly moved to by the user + fn explicit_output(&self) -> &OutputMatch { + self.output_stack.front().unwrap() + } + + // Set output the workspace is on + // + // If `explicit` is `true`, the user has explicitly moved the workspace + // to this output, so previous outputs it was on can be forgotten. + pub fn set_output(&mut self, output: &Output, explicit: bool) { + self.tiling_layer.set_output(output); + self.floating_layer.set_output(output); + for mapped in self.mapped() { + for (surface, _) in mapped.windows() { + toplevel_leave_output(&surface, &self.output); + toplevel_enter_output(&surface, output); + } + } + for window in self.minimized_windows.iter() { + for surface in window.windows() { + toplevel_leave_output(&surface, &self.output); + toplevel_enter_output(&surface, output); + } + } + for f in self + .fullscreen_surfaces + .iter() + .filter(|f| f.ended_at.is_none()) + { + toplevel_leave_output(&f.surface, &self.output); + toplevel_enter_output(&f.surface, output); + } + if explicit { + self.output_stack.clear(); + } + if let Some(pos) = self + .output_stack + .iter() + .position(|i| output_matches(i, output, true)) + { + // Matched edid and connector name + self.output_stack.truncate(pos + 1); + } else if let Some(pos) = self + .output_stack + .iter() + .position(|i| output_matches(i, output, false)) + { + // Matched edid but not connector name; truncate entries that don't match edid, + // but keep old entry in case we see two outputs with the same edid. + self.output_stack.truncate(pos + 1); + self.output_stack.push_back(output_match_for_output(output)); + } else { + self.output_stack.push_back(output_match_for_output(output)); + } + self.output = output.clone(); + } + + pub fn prefers_output(&self, output: &Output) -> bool { + // Disambiguate match by connector name if existing output has same edid + let disambiguate = output + .edid() + .is_some_and(|edid| self.output().edid() == Some(edid)); + self.output_stack + .iter() + .any(|i| output_matches(i, output, disambiguate)) + } + + pub fn unmap_element(&mut self, mapped: &CosmicMapped) -> Option { + let was_maximized = if mapped.maximized_state.lock().unwrap().is_some() { + // If surface is maximized then unmaximize it, so it is assigned to only one layer + self.unmaximize_request(mapped) + } else { + None + }; + + self.focus_stack.0.values_mut().for_each(|set| { + set.shift_remove(mapped); + }); + + if let Some(pos) = self.minimized_windows.iter().position(|m| m == mapped) { + let state = self.minimized_windows.remove(pos); + mapped.set_minimized(false); + return Some(match state { + MinimizedWindow::Floating { previous, .. } => { + WorkspaceRestoreData::Floating(previous) + } + MinimizedWindow::Tiling { previous, .. } => WorkspaceRestoreData::Tiling(previous), + MinimizedWindow::Fullscreen { .. } => unreachable!(), + }); + } + + if let Ok(state) = self.tiling_layer.unmap(mapped, None) { + return Some(WorkspaceRestoreData::Tiling(TilingRestoreData { + state, + was_maximized: was_maximized.is_some(), + })); + } + + let was_snapped = *mapped.floating_tiled.lock().unwrap(); + // unmaximize_request might have triggered a `floating_layer.refresh()`, + // which may have already removed a non-alive surface. + if let Some(floating_geometry) = self.floating_layer.unmap(mapped, None).or(was_maximized) { + return Some(WorkspaceRestoreData::Floating(FloatingRestoreData { + geometry: floating_geometry, + output_size: self.output.geometry().size.as_logical(), + was_maximized: was_maximized.is_some(), + was_snapped, + })); + }; + + None + } + + pub fn unmap_surface(&mut self, surface: &S) -> Option<(CosmicSurface, WorkspaceRestoreData)> + where + CosmicSurface: PartialEq, + { + if let Some(idx) = self + .fullscreen_surfaces + .iter() + .position(|f| f.ended_at.is_none() && &f.surface == surface) + { + let (surface, previous_state, previous_geometry) = + self.remove_fullscreen_at(idx).unwrap(); + return Some(( + surface, + WorkspaceRestoreData::Fullscreen(previous_state.zip(previous_geometry).map( + |(previous_state, previous_geometry)| FullscreenRestoreData { + previous_state, + previous_geometry, + }, + )), + )); + } + + if let Some(pos) = self.minimized_windows.iter().position(|m| { + if let MinimizedWindow::Fullscreen { surface: s, .. } = m { + s == surface + } else { + false + } + }) { + let MinimizedWindow::Fullscreen { surface, previous } = + self.minimized_windows.remove(pos) + else { + unreachable!() + }; + + surface.set_minimized(false); + return Some((surface, WorkspaceRestoreData::Fullscreen(previous))); + } + + let mapped = self.element_for_surface(surface)?; + if let Some(stack) = mapped.stack_ref() + && stack.len() > 1 + { + let idx = stack.surfaces().position(|s| &s == surface)?; + return Some(( + stack.remove_idx(idx)?, + WorkspaceRestoreData::Stack(StackRestoreData { + stack: mapped.key(), + idx, + }), + )); + } + + // we know mapped is no stack with more than one element now, + // so we can treat mapped as containing only our surface. + + let mapped = mapped.clone(); + let layer = self.unmap_element(&mapped)?; + Some((mapped.active_window(), layer)) + } + + pub fn fullscreen_geometry_for_surface( + &self, + surface: &CosmicSurface, + ) -> Rectangle { + let bbox = surface.bbox().as_local(); + + let mut full_geo = Rectangle::from_size(self.output.geometry().size.as_local()); + if bbox != full_geo { + if bbox.size.w < full_geo.size.w { + full_geo.loc.x += (full_geo.size.w - bbox.size.w) / 2; + full_geo.size.w = bbox.size.w; + } + if bbox.size.h < full_geo.size.h { + full_geo.loc.y += (full_geo.size.h - bbox.size.h) / 2; + full_geo.size.h = bbox.size.h; + } + } + + full_geo + } + pub fn fullscreen_geometry_for(&self, fullscreen: &FullscreenSurface) -> Rectangle { + self.fullscreen_geometry_for_surface(&fullscreen.surface) + } + + pub fn element_for_surface(&self, surface: &S) -> Option<&CosmicMapped> + where + CosmicSurface: PartialEq, + { + self.floating_layer + .mapped() + .chain(self.tiling_layer.mapped().map(|(w, _)| w)) + .chain(self.minimized_windows.iter().flat_map(|w| w.mapped())) + .find(|e| e.windows().any(|(w, _)| &w == surface)) + } + + pub fn popup_element_under( + &self, + location: Point, + seat: &Seat, + ) -> Option { + if !self.output.geometry().contains(location.to_i32_floor()) { + return None; + } + let location = location.to_local(&self.output); + + let fullscreen_element_under = + |fullscreen: &FullscreenSurface, geometry: Rectangle| { + fullscreen + .surface + .0 + .surface_under( + (location - geometry.loc.to_f64()).as_logical(), + WindowSurfaceType::POPUP | WindowSurfaceType::SUBSURFACE, + ) + .is_some() + .then(|| KeyboardFocusTarget::Fullscreen(fullscreen.surface.clone())) + }; + + let stack = self.focus_stack.get(seat); + let last_focused = stack.last(); + + if let Some(fullscreen) = self.fullscreen_surfaces.iter().find(|f| { + !f.is_animating() + && last_focused + .is_some_and(|t| matches!(t, FocusTarget::Fullscreen(s) if s == &f.surface)) + }) { + let geometry = self.fullscreen_geometry_for(fullscreen); + return fullscreen_element_under(fullscreen, geometry); + } + + self.floating_layer + .popup_element_under(location, seat) + .or_else(|| self.tiling_layer.popup_element_under(location, seat)) + .or_else(|| { + if last_focused.is_none_or(|t| !matches!(t, FocusTarget::Fullscreen(_))) + && let Some(fullscreen) = self.get_fullscreen(seat) + { + let geometry = self.fullscreen_geometry_for(fullscreen); + return fullscreen_element_under(fullscreen, geometry); + } + None + }) + } + + pub fn toplevel_element_under( + &self, + location: Point, + seat: &Seat, + ) -> Option { + if !self.output.geometry().contains(location.to_i32_floor()) { + return None; + } + let location = location.to_local(&self.output); + + let fullscreen_element_under = + |fullscreen: &FullscreenSurface, geometry: Rectangle| { + fullscreen + .surface + .0 + .surface_under( + (location - geometry.loc.to_f64()).as_logical(), + WindowSurfaceType::TOPLEVEL | WindowSurfaceType::SUBSURFACE, + ) + .is_some() + .then(|| KeyboardFocusTarget::Fullscreen(fullscreen.surface.clone())) + }; + + let stack = self.focus_stack.get(seat); + let last_focused = stack.last(); + + if let Some(fullscreen) = self.fullscreen_surfaces.iter().find(|fs| { + !fs.is_animating() + && last_focused + .is_some_and(|t| matches!(t, FocusTarget::Fullscreen(f) if f == &fs.surface)) + }) { + let geometry = self.fullscreen_geometry_for(fullscreen); + return fullscreen_element_under(fullscreen, geometry); + } + + self.floating_layer + .toplevel_element_under(location, seat) + .or_else(|| self.tiling_layer.toplevel_element_under(location, seat)) + .or_else(|| { + if last_focused.is_none_or(|t| !matches!(t, FocusTarget::Fullscreen(_))) + && let Some(fullscreen) = self.get_fullscreen(seat) + { + let geometry = self.fullscreen_geometry_for(fullscreen); + return fullscreen_element_under(fullscreen, geometry); + } + None + }) + } + + pub fn popup_surface_under( + &self, + location: Point, + overview: OverviewMode, + seat: &Seat, + ) -> Option<(PointerFocusTarget, Point)> { + if !self.output.geometry().contains(location.to_i32_floor()) { + return None; + } + let location = location.to_local(&self.output); + + let check_fullscreen = |fullscreen: &FullscreenSurface| { + if !fullscreen.is_animating() { + let geometry = self.fullscreen_geometry_for(fullscreen); + return fullscreen + .surface + .0 + .surface_under( + (location - geometry.loc.to_f64()).as_logical(), + WindowSurfaceType::POPUP | WindowSurfaceType::SUBSURFACE, + ) + .map(|(surface, surface_offset)| { + ( + PointerFocusTarget::WlSurface { + surface, + toplevel: Some(fullscreen.surface.clone().into()), + }, + (geometry.loc + surface_offset.as_local()).to_f64(), + ) + }); + } + None + }; + + let stack = self.focus_stack.get(seat); + let last_focused = stack.last(); + + self.fullscreen_surfaces + .iter() + .find(|f| last_focused.is_some_and(|t| t == &f.surface)) + .and_then(check_fullscreen) + .or_else(|| self.floating_layer.popup_surface_under(location, seat)) + .or_else(|| { + self.tiling_layer + .popup_surface_under(location, overview, seat) + }) + .or_else(|| self.get_fullscreen(seat).and_then(check_fullscreen)) + .map(|(m, p)| (m, p.to_global(&self.output))) + } + + pub fn toplevel_surface_under( + &self, + location: Point, + overview: OverviewMode, + seat: &Seat, + ) -> Option<(PointerFocusTarget, Point)> { + if !self.output.geometry().contains(location.to_i32_floor()) { + return None; + } + let location = location.to_local(&self.output); + + let check_fullscreen = |fullscreen: &FullscreenSurface| { + if !fullscreen.is_animating() { + let geometry = self.fullscreen_geometry_for(fullscreen); + return fullscreen + .surface + .focus_under( + (location - geometry.loc.to_f64()).as_logical(), + WindowSurfaceType::TOPLEVEL | WindowSurfaceType::SUBSURFACE, + ) + .map(|(target, surface_offset)| { + (target, (geometry.loc.to_f64() + surface_offset.as_local())) + }); + } + + None + }; + + let stack = self.focus_stack.get(seat); + let last_focused = stack.last(); + + self.fullscreen_surfaces + .iter() + .find(|f| last_focused.is_some_and(|t| t == &f.surface)) + .and_then(check_fullscreen) + .or_else(|| self.floating_layer.toplevel_surface_under(location, seat)) + .or_else(|| { + self.tiling_layer + .toplevel_surface_under(location, overview, seat) + }) + .or_else(|| self.get_fullscreen(seat).and_then(check_fullscreen)) + .map(|(m, p)| (m, p.to_global(&self.output))) + } + + pub fn update_pointer_position( + &mut self, + location: Option>, + overview: OverviewMode, + ) { + self.floating_layer.update_pointer_position(location); + self.tiling_layer + .update_pointer_position(location, overview); + } + + pub fn element_geometry(&self, elem: &CosmicMapped) -> Option> { + self.floating_layer + .element_geometry(elem) + .or_else(|| self.tiling_layer.element_geometry(elem)) + } + + pub fn recalculate(&mut self) { + self.tiling_layer.recalculate(); + self.floating_layer.recalculate(); + } + + pub fn unmaximize_request(&mut self, elem: &CosmicMapped) -> Option> { + let mut state = elem.maximized_state.lock().unwrap(); + if let Some(state) = state.take() { + if let Some(minimized) = self.minimized_windows.iter_mut().find(|m| *m == elem) { + minimized.unmaximize(state.original_geometry, state.original_snapped); + Some(state.original_geometry) + } else { + match state.original_layer { + ManagedLayer::Tiling if self.tiling_enabled => { + // should still be mapped in tiling + let geo = self.tiling_layer.element_geometry(elem); + self.floating_layer.unmap(elem, geo); + elem.output_enter(&self.output, elem.bbox()); + elem.set_maximized(false); + elem.set_geometry(state.original_geometry.to_global(&self.output)); + elem.configure(); + self.tiling_layer.recalculate(); + geo + } + ManagedLayer::Sticky => unreachable!(), + + x => { + let use_geometry = !matches!(x, ManagedLayer::Tiling); + elem.set_maximized(false); + self.floating_layer.map_internal( + elem.clone(), + use_geometry.then_some(state.original_geometry.loc), + use_geometry.then_some(state.original_geometry.size.as_logical()), + None, + ); + // Re-apply the snap if the window was snapped before maximizing + if let Some(corners) = state.original_snapped { + self.floating_layer.snap_to_corner(elem, &corners); + } + Some(state.original_geometry) + } + } + } + } else { + None + } + } + + pub fn minimize(&mut self, surface: &S, to: Rectangle) -> Option + where + CosmicSurface: PartialEq, + { + if let Some(idx) = self + .fullscreen_surfaces + .iter() + .position(|f| f.ended_at.is_none() && &f.surface == surface) + { + let fullscreen_state = self.fullscreen_surfaces.get(idx)?.clone(); + { + let f = self.fullscreen_surfaces.get_mut(idx)?; + f.previous_geometry = Some(to); + f.ended_at = Some( + Instant::now() + - (FULLSCREEN_ANIMATION_DURATION + - f.start_at + .take() + .map(|earlier| { + Instant::now() + .duration_since(earlier) + .min(FULLSCREEN_ANIMATION_DURATION) + }) + .unwrap_or(FULLSCREEN_ANIMATION_DURATION)), + ); + } + + fullscreen_state.surface.set_minimized(true); + return Some(MinimizedWindow::Fullscreen { + surface: fullscreen_state.surface, + previous: fullscreen_state + .previous_state + .zip(fullscreen_state.previous_geometry) + .map( + |(previous_state, previous_geometry)| FullscreenRestoreData { + previous_state, + previous_geometry, + }, + ), + }); + } + + let mapped = self + .mapped() + .find(|m| m.windows().any(|(ref s, _)| s == surface)) + .cloned()?; + let was_maximized = if let Some(MaximizedState { + original_geometry, + original_layer, + original_snapped, + }) = mapped.maximized_state.lock().unwrap().take() + { + // we need to do this manually instead of calling `self.unmaximize_request` + // to get the correct animation in the tiling case. + match original_layer { + ManagedLayer::Tiling if self.tiling_enabled => { + self.floating_layer.unmap(&mapped, Some(to)); + } + _ => {} + } + mapped.set_geometry(original_geometry.to_global(&self.output)); + mapped.set_maximized(false); + // Restore the snap marker to lett unminimize re-apply it. + if let Some(corners) = original_snapped { + *mapped.floating_tiled.lock().unwrap() = Some(corners); + } + Some(original_geometry) + } else { + None + }; + + mapped.set_minimized(true); + mapped.configure(); + + let was_snapped = *mapped.floating_tiled.lock().unwrap(); + if let Some(geometry) = self.floating_layer.unmap(&mapped, Some(to)) { + return Some(MinimizedWindow::Floating { + window: mapped, + previous: FloatingRestoreData { + geometry: was_maximized.unwrap_or(geometry), + output_size: self.output.geometry().size.as_logical(), + was_maximized: was_maximized.is_some(), + was_snapped, + }, + }); + } + + if let Ok(state) = self + .tiling_layer + .unmap(&mapped, was_maximized.is_none().then_some(to)) + { + return Some(MinimizedWindow::Tiling { + window: mapped, + previous: TilingRestoreData { + state, + was_maximized: was_maximized.is_some(), + }, + }); + } + + unreachable!() + } + + pub fn unminimize( + &mut self, + window: MinimizedWindow, + from: Rectangle, + seat: &Seat, + ) -> Option<( + CosmicSurface, + Option, + Option>, + )> { + match window { + MinimizedWindow::Fullscreen { previous, surface } => { + surface.set_minimized(false); + // focus it so it's the top fullscreen window + self.focus_stack + .get_mut(seat) + .append(FocusTarget::Fullscreen(surface.clone())); + self.fullscreen_surfaces.push(FullscreenSurface { + surface, + previous_state: previous.clone().map(|p| p.previous_state), + previous_geometry: previous.map(|p| p.previous_geometry), + start_at: None, + ended_at: None, + }); + self.dirty.store(true, Ordering::SeqCst); + None + } + MinimizedWindow::Floating { window, previous } => { + let current_output_size = self.output.geometry().size.as_logical(); + let previous_position = previous.position_relative(current_output_size); + + window.set_minimized(false); + self.floating_layer + .remap_minimized(window.clone(), from, previous_position); + + if previous.was_maximized { + let geometry = self.floating_layer.element_geometry(&window).unwrap(); + let mut state = window.maximized_state.lock().unwrap(); + *state = Some(MaximizedState { + original_geometry: geometry, + original_layer: ManagedLayer::Floating, + original_snapped: previous.was_snapped, + }); + std::mem::drop(state); + self.floating_layer.map_maximized(window, geometry, true); + } else if let Some(corners) = previous.was_snapped { + self.floating_layer.snap_to_corner(&window, &corners); + } + + None + } + MinimizedWindow::Tiling { + window, + previous: + TilingRestoreData { + state, + was_maximized, + }, + } => { + window.set_minimized(false); + if self.tiling_enabled { + let focus_stack = self.focus_stack.get(seat); + self.tiling_layer.remap( + window.clone(), + (!was_maximized).then_some(from), + state, + Some(focus_stack.iter()), + ); + if was_maximized { + let previous_geometry = + self.tiling_layer.element_geometry(&window).unwrap(); + let mut state = window.maximized_state.lock().unwrap(); + *state = Some(MaximizedState { + original_geometry: previous_geometry, + original_layer: ManagedLayer::Tiling, + original_snapped: None, + }); + std::mem::drop(state); + self.floating_layer.map_maximized(window, from, true); + } + } else { + self.floating_layer.map(window.clone(), None); + let mut geometry = self.floating_layer.element_geometry(&window).unwrap(); + if let Some(pending_size) = window.pending_size() { + geometry.size = pending_size.as_local(); + } + + if was_maximized { + let mut state = window.maximized_state.lock().unwrap(); + *state = Some(MaximizedState { + original_geometry: geometry, + original_layer: ManagedLayer::Tiling, + original_snapped: None, + }); + std::mem::drop(state); + self.floating_layer.map_maximized(window, from, true); + } else { + // get the right animation + self.floating_layer + .remap_minimized(window.clone(), from, geometry.loc); + } + } + None + } + } + } + + pub fn map_fullscreen<'a>( + &mut self, + window: &CosmicSurface, + seat: impl Into>>, + restore: Option, + previous_geometry: Option>, + ) { + window.set_fullscreen(true); + window.set_geometry(self.output.geometry(), 0); + window.send_configure(); + window.output_enter( + &self.output, + Rectangle::new(Point::new(0, 0), self.output.geometry().size.as_logical()), + ); + + if let Some(seat) = seat.into() { + self.focus_stack.get_mut(seat).append(window.clone()); + } + + self.dirty.store(true, Ordering::SeqCst); + self.fullscreen_surfaces.push(FullscreenSurface { + surface: window.clone(), + previous_state: restore, + previous_geometry, + start_at: Some(Instant::now()), + ended_at: None, + }); + } + + #[must_use] + pub fn take_fullscreen( + &mut self, + surface: &S, + ) -> Option<( + CosmicSurface, + Option, + Option>, + )> + where + CosmicSurface: PartialEq, + { + let idx = self + .fullscreen_surfaces + .iter() + .position(|f| f.ended_at.is_none() && &f.surface == surface)?; + let fs = self.fullscreen_surfaces.remove(idx); + + for focus_stack in self.focus_stack.0.values_mut() { + focus_stack.retain(|t| t != &fs.surface); + } + + Some((fs.surface, fs.previous_state, fs.previous_geometry)) + } + + #[must_use] + pub fn remove_fullscreen_at( + &mut self, + idx: usize, + ) -> Option<( + CosmicSurface, + Option, + Option>, + )> { + // if it doesn't exist we move on. + let surface = self.fullscreen_surfaces.get_mut(idx)?; + // if already being removed, do nothing + if surface.ended_at.is_some() { + return None; + } + + if surface.surface.alive() { + surface.surface.output_leave(&self.output); + surface.surface.set_fullscreen(false); + if let Some(previous_geometry) = surface.previous_geometry.as_ref() { + surface + .surface + .set_geometry(previous_geometry.to_global(&self.output), 0); + } + surface.surface.send_configure(); + } + + for focus_stack in self.focus_stack.0.values_mut() { + focus_stack.retain(|t| t != &surface.surface); + } + + surface.ended_at = Some( + Instant::now() + - (FULLSCREEN_ANIMATION_DURATION + - surface + .start_at + .take() + .map(|earlier| { + Instant::now() + .duration_since(earlier) + .min(FULLSCREEN_ANIMATION_DURATION) + }) + .unwrap_or(FULLSCREEN_ANIMATION_DURATION)), + ); + + Some(( + surface.surface.clone(), + surface.previous_state.clone(), + surface.previous_geometry, + )) + } + + #[must_use] + pub fn remove_fullscreen_surface( + &mut self, + surface: &S, + ) -> Option<( + CosmicSurface, + Option, + Option>, + )> + where + CosmicSurface: PartialEq, + { + let idx = self + .fullscreen_surfaces + .iter() + .position(|f| f.ended_at.is_none() && &f.surface == surface)?; + self.remove_fullscreen_at(idx) + } + + pub fn get_fullscreen(&self, seat: &Seat) -> Option<&FullscreenSurface> { + let stack = self.focus_stack.get(seat); + stack + .iter() + .find_map(|t| { + if let FocusTarget::Fullscreen(s) = t { + self.fullscreen_surfaces + .iter() + .find(|f| f.alive() && f.ended_at.is_none() && &f.surface == s) + } else { + None + } + }) + .or_else(|| { + self.fullscreen_surfaces + .iter() + .rev() + .find(|f| f.alive() && f.ended_at.is_none()) + }) + } + + pub fn get_fullscreen_surfaces(&self) -> impl Iterator { + self.fullscreen_surfaces + .iter() + .filter(|f| f.alive() && f.ended_at.is_none()) + } + + pub fn resize( + &mut self, + focused: &KeyboardFocusTarget, + direction: ResizeDirection, + edge: ResizeEdge, + amount: i32, + ) -> bool { + if matches!(focused, KeyboardFocusTarget::Fullscreen(_)) { + return false; + } + + if !self.floating_layer.resize(focused, direction, edge, amount) { + self.tiling_layer.resize(focused, direction, edge, amount) + } else { + true + } + } + + pub fn toggle_tiling( + &mut self, + seat: &Seat, + workspace_state: &mut WorkspaceUpdateGuard<'_, State>, + ) { + self.set_tiling(!self.tiling_enabled, seat, workspace_state) + } + + pub fn set_tiling( + &mut self, + tiling: bool, + seat: &Seat, + workspace_state: &mut WorkspaceUpdateGuard<'_, State>, + ) { + let mut maximized_windows = Vec::new(); + if tiling { + let floating_windows = self.floating_layer.mapped().cloned().collect::>(); + + for window in floating_windows.iter().filter(|w| w.is_maximized(false)) { + maximized_windows.push((window.clone(), ManagedLayer::Tiling)); + } + + let focus_stack = self.focus_stack.get(seat); + for window in floating_windows.into_iter() { + self.floating_layer.unmap(&window, None); + self.tiling_layer + .map(window, Some(focus_stack.iter()), None) + } + workspace_state.set_workspace_tiling_state(&self.handle, TilingState::TilingEnabled); + self.tiling_enabled = true; + } else { + for window in self + .tiling_layer + .mapped() + .map(|(m, _)| m.clone()) + .collect::>() + .into_iter() + { + if window.is_maximized(false) { + self.unmaximize_request(&window); + maximized_windows.push((window.clone(), ManagedLayer::Floating)); + } + let _ = self.tiling_layer.unmap(&window, None); + self.floating_layer.map(window, None); + } + workspace_state.set_workspace_tiling_state(&self.handle, TilingState::FloatingOnly); + self.tiling_enabled = false; + } + for (window, original_layer) in maximized_windows { + let mut original_geometry = self.element_geometry(&window).unwrap(); + if let Some(pending_size) = window.pending_size() { + original_geometry.size = pending_size.as_local(); + } + let mut state = window.maximized_state.lock().unwrap(); + *state = Some(MaximizedState { + original_geometry, + original_layer, + original_snapped: None, + }); + std::mem::drop(state); + + self.floating_layer + .map_maximized(window, original_geometry, false); + } + } + + pub fn toggle_floating_window(&mut self, seat: &Seat, window: &CosmicMapped) { + if self.tiling_enabled { + if window.is_maximized(false) { + self.unmaximize_request(window); + } + if self.tiling_layer.mapped().any(|(m, _)| m == window) { + let _ = self.tiling_layer.unmap(window, None); + self.floating_layer.map(window.clone(), None); + } else if self.floating_layer.mapped().any(|w| w == window) { + let focus_stack = self.focus_stack.get(seat); + self.floating_layer.unmap(window, None); + self.tiling_layer + .map(window.clone(), Some(focus_stack.iter()), None) + } + } + } + + pub fn toggle_floating_window_focused(&mut self, seat: &Seat) { + if matches!( + seat.get_keyboard().unwrap().current_focus(), + Some(KeyboardFocusTarget::Fullscreen(_)) + ) { + return; + } + let maybe_window = self.focus_stack.get(seat).iter().next().cloned(); + if let Some(FocusTarget::Window(window)) = maybe_window { + self.toggle_floating_window(seat, &window); + } + } + + pub fn mapped(&self) -> impl Iterator { + self.floating_layer + .mapped() + .chain(self.tiling_layer.mapped().map(|(w, _)| w)) + } + + pub fn len(&self) -> usize { + self.floating_layer.mapped().count() + + self.tiling_layer.mapped().count() + + self.minimized_windows.len() + + self + .fullscreen_surfaces + .iter() + .filter(|f| f.ended_at.is_none()) + .count() + } + + pub fn is_empty(&self) -> bool { + self.floating_layer.mapped().next().is_none() + && self.tiling_layer.mapped().next().is_none() + && self.minimized_windows.is_empty() + && self.fullscreen_surfaces.is_empty() + } + + pub fn is_floating(&self, surface: &S) -> bool + where + CosmicSurface: PartialEq, + { + self.floating_layer + .mapped() + .any(|m| m.windows().any(|(ref s, _)| s == surface)) + || self.minimized_windows.iter().any(|m| { + if let MinimizedWindow::Floating { window, .. } = m { + window.windows().any(|(ref s, _)| s == surface) + } else { + false + } + }) + } + + pub fn is_tiled(&self, surface: &S) -> bool + where + CosmicSurface: PartialEq, + { + self.tiling_layer + .mapped() + .any(|(m, _)| m.windows().any(|(ref s, _)| s == surface)) + || self.minimized_windows.iter().any(|m| { + if let MinimizedWindow::Tiling { window, .. } = m { + window.windows().any(|(ref s, _)| s == surface) + } else { + false + } + }) + } + + pub fn node_desc(&self, focus: KeyboardFocusTarget) -> Option { + match focus { + KeyboardFocusTarget::Element(mapped) => { + if mapped.is_maximized(false) { + return None; + } + self.tiling_layer.mapped().find_map(|(m, _)| { + if m == &mapped { + mapped + .tiling_node_id + .lock() + .unwrap() + .clone() + .map(|node_id| NodeDesc { + handle: self.handle, + node: node_id.clone(), + stack_window: if mapped + .stack_ref() + .map(|stack| !stack.whole_stack_focused()) + .unwrap_or(false) + { + Some(mapped.active_window()) + } else { + None + }, + focus_stack: vec![node_id], + }) + } else { + None + } + }) + } + KeyboardFocusTarget::Group(WindowGroup { + node, focus_stack, .. + }) => Some(NodeDesc { + handle: self.handle, + node, + stack_window: None, + focus_stack, + }), + _ => None, + } + } + + #[profiling::function] + pub fn render<'a, R>( + &self, + renderer: &mut R, + last_active_seat: &Seat, + render_focus: bool, + overview: (OverviewMode, Option<(SwapIndicator, Option<&Tree>)>), + resize_indicator: Option<(ResizeMode, ResizeIndicator)>, + indicator_thickness: u8, + theme: &CosmicTheme, + scanout_node: Option, + push: &mut dyn FnMut(WorkspaceRenderElement), + ) where + R: AsGlowRenderer, + R::TextureId: Send + Clone + 'static, + CosmicMappedRenderElement: RenderElement, + CosmicWindowRenderElement: RenderElement, + CosmicStackRenderElement: RenderElement, + WorkspaceRenderElement: RenderElement, + { + let output_scale = self.output.current_scale().fractional_scale(); + let zone = { + let layer_map = layer_map_for_output(&self.output); + layer_map.non_exclusive_zone().as_local() + }; + let focused = self.focus_stack.get(last_active_seat).last().cloned(); + let fullscreen_focused = matches!(focused, Some(FocusTarget::Fullscreen(_))); + + let mut fullscreen_elements = SmallVec::<[WorkspaceRenderElement; 2]>::new_const(); + let mut render_fullscreen = + |fullscreen: &FullscreenSurface, renderer: &mut R, output_scale: f64| { + let fullscreen_geo = self.fullscreen_geometry_for(fullscreen); + let previous_geo = fullscreen + .previous_geometry + .as_ref() + .unwrap_or(&fullscreen_geo); + + let (target_geo, alpha) = match (fullscreen.start_at, fullscreen.ended_at) { + (Some(started), _) => { + let duration = Instant::now().duration_since(started).as_secs_f64() + / FULLSCREEN_ANIMATION_DURATION.as_secs_f64(); + ( + ease( + EaseInOutCubic, + EaseRectangle(*previous_geo), + EaseRectangle(fullscreen_geo), + duration, + ) + .0, + ease(EaseInOutCubic, 0.0, 1.0, duration), + ) + } + (_, Some(ended)) => { + let duration = Instant::now().duration_since(ended).as_secs_f64() + / FULLSCREEN_ANIMATION_DURATION.as_secs_f64(); + ( + ease( + EaseInOutCubic, + EaseRectangle(fullscreen_geo), + EaseRectangle(*previous_geo), + duration, + ) + .0, + ease(EaseInOutCubic, 1.0, 0.0, duration), + ) + } + (None, None) => (fullscreen_geo, 1.0), + }; + + let render_loc = target_geo + .loc + .as_logical() + .to_physical_precise_round(output_scale); + + let fullscreen_geo = fullscreen.surface.0.geometry(); + let scale = Scale { + x: target_geo.size.w as f64 / fullscreen_geo.size.w as f64, + y: target_geo.size.h as f64 / fullscreen_geo.size.h as f64, + }; + + // Only rescale geometry when animating + let animation_rescale = |elem| { + if fullscreen.is_animating() { + RescaleRenderElement::from_element(elem, render_loc, scale).into() + } else { + Into::>::into(elem) + } + }; + + let mut fullscreen_push = |elem: SurfaceRenderElement| { + if fullscreen_focused { + push(animation_rescale(elem.into())) + } else { + fullscreen_elements.push(animation_rescale(elem.into())) + } + }; + fullscreen.surface.push_render_elements( + renderer, + render_loc, + output_scale.into(), + alpha, + Some(true), + scanout_node, + false, + [0, 0, 0, 0], + 0, + &mut fullscreen_push, + None, + ); + }; + + let top_fullscreen = self.get_fullscreen(last_active_seat); + + if let Some(fs) = top_fullscreen { + render_fullscreen(fs, renderer, output_scale) + } + // Also render any animating (entering/exiting) fullscreens + for fs in self.fullscreen_surfaces.iter().filter(|f| f.is_animating()) { + if top_fullscreen.is_none_or(|top| top.surface != fs.surface) { + render_fullscreen(fs, renderer, output_scale); + }; + } + + let any_fullscreen_animating = self + .fullscreen_surfaces + .iter() + .any(|f| f.start_at.is_some() || f.ended_at.is_some()); + if !matches!(focused, Some(FocusTarget::Fullscreen(_))) + || any_fullscreen_animating + || self + .fullscreen_surfaces + .iter() + .all(|f| !f.alive() || f.ended_at.is_some()) + { + // floating surfaces + let alpha = match &overview.0 { + OverviewMode::Started(_, started) => { + (1.0 - (Instant::now().duration_since(*started).as_millis() + / ANIMATION_DURATION.as_millis()) as f32) + .max(0.0) + * 0.4 + + 0.6 + } + OverviewMode::Ended(_, ended) => { + ((Instant::now().duration_since(*ended).as_millis() + / ANIMATION_DURATION.as_millis()) as f32) + * 0.4 + + 0.6 + } + OverviewMode::Active(_) => 0.6, + OverviewMode::None => 1.0, + }; + + self.floating_layer.render( + renderer, + focused.as_ref().and_then(|target| { + if let FocusTarget::Window(mapped) = target { + Some(mapped) + } else { + None + } + }), + resize_indicator.clone(), + indicator_thickness, + alpha, + theme, + scanout_node, + &mut |elem| push(elem.into()), + ); + + let alpha = match &overview.0 { + OverviewMode::Started(_, start) => Some( + (Instant::now().duration_since(*start).as_millis() as f64 / 100.0).min(1.0) + as f32, + ), + OverviewMode::Active(_) => Some(1.0), + OverviewMode::Ended(_, ended) => Some( + 1.0 - (Instant::now().duration_since(*ended).as_millis() as f64 / 100.0) + .min(1.0) as f32, + ), + OverviewMode::None => None, + }; + + //tiling surfaces + self.tiling_layer.render( + renderer, + render_focus.then_some(last_active_seat), + render_focus + .then(|| { + focused.as_ref().and_then(|target| { + if let FocusTarget::Window(mapped) = target { + Some(mapped) + } else { + None + } + }) + }) + .flatten(), + zone, + overview, + resize_indicator, + indicator_thickness, + theme, + scanout_node, + &mut |elem| push(elem.into()), + ); + + if let Some(alpha) = alpha { + push( + Into::>::into(BackdropShader::element( + renderer, + self.backdrop_id.clone(), + Rectangle::from_size(self.output.geometry().size.as_local()), + 0., + alpha * 0.85, + [0.0, 0.0, 0.0], + )) + .into(), + ) + } + } + + for elem in fullscreen_elements { + push(elem); + } + } + + #[profiling::function] + pub fn render_popups<'a, R>( + &self, + renderer: &mut R, + last_active_seat: &Seat, + render_focus: bool, + overview: (OverviewMode, Option<(SwapIndicator, Option<&Tree>)>), + theme: &CosmicTheme, + scanout_node: Option, + push: &mut dyn FnMut(WorkspaceRenderElement), + ) where + R: AsGlowRenderer, + R::TextureId: Send + Clone + 'static, + CosmicMappedRenderElement: RenderElement, + CosmicWindowRenderElement: RenderElement, + CosmicStackRenderElement: RenderElement, + WorkspaceRenderElement: RenderElement, + { + let output_scale = self.output.current_scale().fractional_scale(); + let zone = { + let layer_map = layer_map_for_output(&self.output); + layer_map.non_exclusive_zone().as_local() + }; + + // Render popups for the top (most recently focused) fullscreen + let focus_stack = self.focus_stack.get(last_active_seat); + let top_fullscreen = self.get_fullscreen(last_active_seat); + + if let Some(fullscreen) = top_fullscreen { + let fullscreen_geo = self.fullscreen_geometry_for(fullscreen); + let previous_geo = fullscreen + .previous_geometry + .as_ref() + .unwrap_or(&fullscreen_geo); + + let (target_geo, alpha) = match (fullscreen.start_at, fullscreen.ended_at) { + (Some(started), _) => { + let duration = Instant::now().duration_since(started).as_secs_f64() + / FULLSCREEN_ANIMATION_DURATION.as_secs_f64(); + ( + ease( + EaseInOutCubic, + EaseRectangle(*previous_geo), + EaseRectangle(fullscreen_geo), + duration, + ) + .0, + ease(EaseInOutCubic, 0.0, 1.0, duration), + ) + } + (_, Some(ended)) => { + let duration = Instant::now().duration_since(ended).as_secs_f64() + / FULLSCREEN_ANIMATION_DURATION.as_secs_f64(); + ( + ease( + EaseInOutCubic, + EaseRectangle(fullscreen_geo), + EaseRectangle(*previous_geo), + duration, + ) + .0, + ease(EaseInOutCubic, 1.0, 0.0, duration), + ) + } + (None, None) => (fullscreen_geo, 1.0), + }; + + let render_loc = target_geo + .loc + .as_logical() + .to_physical_precise_round(output_scale); + + fullscreen.surface.push_popup_render_elements( + renderer, + render_loc, + output_scale.into(), + alpha, + scanout_node, + 0, + &mut |elem| push(WorkspaceRenderElement::FullscreenPopup(elem.into())), + ); + } + + let any_fullscreen_animating = self + .fullscreen_surfaces + .iter() + .any(|f| f.start_at.is_some() || f.ended_at.is_some()); + if !matches!(focus_stack.last(), Some(FocusTarget::Fullscreen(_))) + || any_fullscreen_animating + || self + .fullscreen_surfaces + .iter() + .all(|f| !f.alive() || f.ended_at.is_some()) + { + // floating surfaces + let alpha = match &overview.0 { + OverviewMode::Started(_, started) => { + (1.0 - (Instant::now().duration_since(*started).as_millis() + / ANIMATION_DURATION.as_millis()) as f32) + .max(0.0) + * 0.4 + + 0.6 + } + OverviewMode::Ended(_, ended) => { + ((Instant::now().duration_since(*ended).as_millis() + / ANIMATION_DURATION.as_millis()) as f32) + * 0.4 + + 0.6 + } + OverviewMode::Active(_) => 0.6, + OverviewMode::None => 1.0, + }; + + self.floating_layer + .render_popups(renderer, alpha, scanout_node, &mut |elem| push(elem.into())); + + //tiling surfaces + self.tiling_layer.render_popups( + renderer, + render_focus.then_some(last_active_seat), + zone, + overview, + theme, + scanout_node, + &mut |elem| push(elem.into()), + ); + } + } +} + +impl FocusStacks { + pub fn get<'a>(&'a self, seat: &Seat) -> FocusStack<'a> { + FocusStack(self.0.get(seat)) + } + + pub fn get_mut<'a>(&'a mut self, seat: &Seat) -> FocusStackMut<'a> { + FocusStackMut(self.0.entry(seat.clone()).or_default()) + } +} + +pub struct OutputNotMapped; + +pub enum WorkspaceRenderElement +where + R: AsGlowRenderer, + R::TextureId: Send + 'static, +{ + OverrideRedirect(SurfaceRenderElement), + LowerLayerShell(NamespacedElement>), + Fullscreen(RescaleRenderElement>), + FullscreenPopup(CosmicWindowRenderElement), + Window(CosmicMappedRenderElement), + Backdrop(TextureRenderElement), +} + +impl Element for WorkspaceRenderElement +where + R: AsGlowRenderer, + R::TextureId: Send + 'static, +{ + fn id(&self) -> &smithay::backend::renderer::element::Id { + match self { + WorkspaceRenderElement::OverrideRedirect(elem) => elem.id(), + WorkspaceRenderElement::LowerLayerShell(elem) => elem.id(), + WorkspaceRenderElement::Fullscreen(elem) => elem.id(), + WorkspaceRenderElement::FullscreenPopup(elem) => elem.id(), + WorkspaceRenderElement::Window(elem) => elem.id(), + WorkspaceRenderElement::Backdrop(elem) => elem.id(), + } + } + + fn current_commit(&self) -> smithay::backend::renderer::utils::CommitCounter { + match self { + WorkspaceRenderElement::OverrideRedirect(elem) => elem.current_commit(), + WorkspaceRenderElement::LowerLayerShell(elem) => elem.current_commit(), + WorkspaceRenderElement::Fullscreen(elem) => elem.current_commit(), + WorkspaceRenderElement::FullscreenPopup(elem) => elem.current_commit(), + WorkspaceRenderElement::Window(elem) => elem.current_commit(), + WorkspaceRenderElement::Backdrop(elem) => elem.current_commit(), + } + } + + fn src(&self) -> Rectangle { + match self { + WorkspaceRenderElement::OverrideRedirect(elem) => elem.src(), + WorkspaceRenderElement::LowerLayerShell(elem) => elem.src(), + WorkspaceRenderElement::Fullscreen(elem) => elem.src(), + WorkspaceRenderElement::FullscreenPopup(elem) => elem.src(), + WorkspaceRenderElement::Window(elem) => elem.src(), + WorkspaceRenderElement::Backdrop(elem) => elem.src(), + } + } + + fn geometry(&self, scale: Scale) -> Rectangle { + match self { + WorkspaceRenderElement::OverrideRedirect(elem) => elem.geometry(scale), + WorkspaceRenderElement::LowerLayerShell(elem) => elem.geometry(scale), + WorkspaceRenderElement::Fullscreen(elem) => elem.geometry(scale), + WorkspaceRenderElement::FullscreenPopup(elem) => elem.geometry(scale), + WorkspaceRenderElement::Window(elem) => elem.geometry(scale), + WorkspaceRenderElement::Backdrop(elem) => elem.geometry(scale), + } + } + + fn location(&self, scale: Scale) -> Point { + match self { + WorkspaceRenderElement::OverrideRedirect(elem) => elem.location(scale), + WorkspaceRenderElement::LowerLayerShell(elem) => elem.location(scale), + WorkspaceRenderElement::Fullscreen(elem) => elem.location(scale), + WorkspaceRenderElement::FullscreenPopup(elem) => elem.location(scale), + WorkspaceRenderElement::Window(elem) => elem.location(scale), + WorkspaceRenderElement::Backdrop(elem) => elem.location(scale), + } + } + + fn transform(&self) -> smithay::utils::Transform { + match self { + WorkspaceRenderElement::OverrideRedirect(elem) => elem.transform(), + WorkspaceRenderElement::LowerLayerShell(elem) => elem.transform(), + WorkspaceRenderElement::Fullscreen(elem) => elem.transform(), + WorkspaceRenderElement::FullscreenPopup(elem) => elem.transform(), + WorkspaceRenderElement::Window(elem) => elem.transform(), + WorkspaceRenderElement::Backdrop(elem) => elem.transform(), + } + } + + fn damage_since( + &self, + scale: Scale, + commit: Option, + ) -> DamageSet { + match self { + WorkspaceRenderElement::OverrideRedirect(elem) => elem.damage_since(scale, commit), + WorkspaceRenderElement::LowerLayerShell(elem) => elem.damage_since(scale, commit), + WorkspaceRenderElement::Fullscreen(elem) => elem.damage_since(scale, commit), + WorkspaceRenderElement::FullscreenPopup(elem) => elem.damage_since(scale, commit), + WorkspaceRenderElement::Window(elem) => elem.damage_since(scale, commit), + WorkspaceRenderElement::Backdrop(elem) => elem.damage_since(scale, commit), + } + } + + fn opaque_regions(&self, scale: Scale) -> OpaqueRegions { + match self { + WorkspaceRenderElement::OverrideRedirect(elem) => elem.opaque_regions(scale), + WorkspaceRenderElement::LowerLayerShell(elem) => elem.opaque_regions(scale), + WorkspaceRenderElement::Fullscreen(elem) => elem.opaque_regions(scale), + WorkspaceRenderElement::FullscreenPopup(elem) => elem.opaque_regions(scale), + WorkspaceRenderElement::Window(elem) => elem.opaque_regions(scale), + WorkspaceRenderElement::Backdrop(elem) => elem.opaque_regions(scale), + } + } + + fn alpha(&self) -> f32 { + match self { + WorkspaceRenderElement::OverrideRedirect(elem) => elem.alpha(), + WorkspaceRenderElement::LowerLayerShell(elem) => elem.alpha(), + WorkspaceRenderElement::Fullscreen(elem) => elem.alpha(), + WorkspaceRenderElement::FullscreenPopup(elem) => elem.alpha(), + WorkspaceRenderElement::Window(elem) => elem.alpha(), + WorkspaceRenderElement::Backdrop(elem) => elem.alpha(), + } + } + + fn kind(&self) -> Kind { + match self { + WorkspaceRenderElement::OverrideRedirect(elem) => elem.kind(), + WorkspaceRenderElement::LowerLayerShell(elem) => elem.kind(), + WorkspaceRenderElement::Fullscreen(elem) => elem.kind(), + WorkspaceRenderElement::FullscreenPopup(elem) => elem.kind(), + WorkspaceRenderElement::Window(elem) => elem.kind(), + WorkspaceRenderElement::Backdrop(elem) => elem.kind(), + } + } + + fn is_framebuffer_effect(&self) -> bool { + match self { + WorkspaceRenderElement::OverrideRedirect(elem) => elem.is_framebuffer_effect(), + WorkspaceRenderElement::LowerLayerShell(elem) => elem.is_framebuffer_effect(), + WorkspaceRenderElement::Fullscreen(elem) => elem.is_framebuffer_effect(), + WorkspaceRenderElement::FullscreenPopup(elem) => elem.is_framebuffer_effect(), + WorkspaceRenderElement::Window(elem) => elem.is_framebuffer_effect(), + WorkspaceRenderElement::Backdrop(elem) => elem.is_framebuffer_effect(), + } + } +} + +impl RenderElement for WorkspaceRenderElement +where + R: AsGlowRenderer, + R::TextureId: Send + 'static, +{ + fn draw( + &self, + frame: &mut R::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + cache: Option<&UserDataMap>, + ) -> Result<(), R::Error> { + match self { + WorkspaceRenderElement::OverrideRedirect(elem) => { + elem.draw(frame, src, dst, damage, opaque_regions, cache) + } + WorkspaceRenderElement::LowerLayerShell(elem) => { + elem.draw(frame, src, dst, damage, opaque_regions, cache) + } + WorkspaceRenderElement::Fullscreen(elem) => { + elem.draw(frame, src, dst, damage, opaque_regions, cache) + } + WorkspaceRenderElement::FullscreenPopup(elem) => { + elem.draw(frame, src, dst, damage, opaque_regions, cache) + } + WorkspaceRenderElement::Window(elem) => { + elem.draw(frame, src, dst, damage, opaque_regions, cache) + } + WorkspaceRenderElement::Backdrop(elem) => RenderElement::::draw( + elem, + R::glow_frame_mut(frame), + src, + dst, + damage, + opaque_regions, + cache, + ) + .map_err(R::from_gles_error), + } + } + + fn underlying_storage( + &self, + renderer: &mut R, + ) -> Option> { + match self { + WorkspaceRenderElement::OverrideRedirect(elem) => elem.underlying_storage(renderer), + WorkspaceRenderElement::LowerLayerShell(elem) => elem.underlying_storage(renderer), + WorkspaceRenderElement::Fullscreen(elem) => elem.underlying_storage(renderer), + WorkspaceRenderElement::FullscreenPopup(elem) => elem.underlying_storage(renderer), + WorkspaceRenderElement::Window(elem) => elem.underlying_storage(renderer), + WorkspaceRenderElement::Backdrop(elem) => { + elem.underlying_storage(renderer.glow_renderer_mut()) + } + } + } + + fn capture_framebuffer( + &self, + frame: &mut R::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + cache: &UserDataMap, + ) -> Result<(), R::Error> { + match self { + WorkspaceRenderElement::OverrideRedirect(elem) => { + elem.capture_framebuffer(frame, src, dst, cache) + } + WorkspaceRenderElement::LowerLayerShell(elem) => { + elem.capture_framebuffer(frame, src, dst, cache) + } + WorkspaceRenderElement::Fullscreen(elem) => { + elem.capture_framebuffer(frame, src, dst, cache) + } + WorkspaceRenderElement::FullscreenPopup(elem) => { + elem.capture_framebuffer(frame, src, dst, cache) + } + WorkspaceRenderElement::Window(elem) => { + elem.capture_framebuffer(frame, src, dst, cache) + } + WorkspaceRenderElement::Backdrop(elem) => { + RenderElement::::capture_framebuffer( + elem, + R::glow_frame_mut(frame), + src, + dst, + cache, + ) + .map_err(R::from_gles_error) + } + } + } +} + +impl From>> for WorkspaceRenderElement +where + R: AsGlowRenderer, + R::TextureId: Send + 'static, + CosmicMappedRenderElement: RenderElement, +{ + fn from(elem: RescaleRenderElement>) -> Self { + WorkspaceRenderElement::Fullscreen(elem) + } +} + +impl From> for WorkspaceRenderElement +where + R: AsGlowRenderer, + R::TextureId: Send + 'static, + CosmicMappedRenderElement: RenderElement, +{ + fn from(elem: CosmicWindowRenderElement) -> Self { + WorkspaceRenderElement::FullscreenPopup(elem) + } +} + +impl From> for WorkspaceRenderElement +where + R: AsGlowRenderer, + R::TextureId: Send + 'static, + CosmicMappedRenderElement: RenderElement, +{ + fn from(elem: SurfaceRenderElement) -> Self { + WorkspaceRenderElement::OverrideRedirect(elem) + } +} + +impl From>> for WorkspaceRenderElement +where + R: AsGlowRenderer, + R::TextureId: Send + 'static, + CosmicMappedRenderElement: RenderElement, +{ + fn from(elem: NamespacedElement>) -> Self { + WorkspaceRenderElement::LowerLayerShell(elem) + } +} + +impl From> for WorkspaceRenderElement +where + R: AsGlowRenderer, + R::TextureId: Send + 'static, + CosmicMappedRenderElement: RenderElement, +{ + fn from(elem: CosmicMappedRenderElement) -> Self { + WorkspaceRenderElement::Window(elem) + } +} + +impl From> for WorkspaceRenderElement +where + R: AsGlowRenderer, + R::TextureId: Send + 'static, + CosmicMappedRenderElement: RenderElement, +{ + fn from(elem: TextureRenderElement) -> Self { + WorkspaceRenderElement::Backdrop(elem) + } +} diff --git a/src/shell/zoom.rs b/src/shell/zoom.rs new file mode 100644 index 0000000..ef81053 --- /dev/null +++ b/src/shell/zoom.rs @@ -0,0 +1,1099 @@ +use std::{sync::Mutex, time::Instant}; + +use calloop::LoopHandle; +use cosmic::{ + Apply, + iced::{Alignment, Background, Border, Length, alignment::Vertical, widget as iced_widget}, + theme, + widget::{self, icon::Named}, +}; +use cosmic_comp_config::ZoomMovement; +use cosmic_config::ConfigSet; +use keyframe::{ease, functions::Linear}; +use smithay::{ + backend::renderer::ImportMem, + desktop::space::SpaceElement, + input::{ + Seat, + pointer::{ + AxisFrame, ButtonEvent, Focus, GestureHoldBeginEvent, GestureHoldEndEvent, + GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, + GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, + MotionEvent as PointerMotionEvent, PointerTarget, RelativeMotionEvent, + }, + touch::{ + DownEvent, FrameMarker, MotionEvent as TouchMotionEvent, OrientationEvent, ShapeEvent, + TouchTarget, UpEvent, + }, + }, + output::Output, + utils::{FrameExtents, IsAlive, Point, Rectangle, Serial, Size}, +}; +use tracing::error; + +use crate::{ + backend::render::element::AsGlowRenderer, + state::State, + utils::{ + iced::{IcedElement, IcedRenderElement, Program}, + prelude::*, + tween::EasePoint, + }, +}; + +use super::{ + ANIMATION_DURATION, check_grab_preconditions, + focus::target::PointerFocusTarget, + grabs::{ContextMenu, Item, MenuAlignment, MenuGrab}, +}; + +#[derive(Debug, Clone)] +pub struct ZoomState { + pub(super) seat: Seat, + pub(super) show_overlay: bool, + pub(super) increment: u32, + pub(super) movement: ZoomMovement, +} + +#[derive(Debug)] +pub struct OutputZoomState { + pub(super) level: f64, + pub(super) previous_level: Option<(f64, Instant)>, + focal_point: Point, + previous_point: Option<(Point, Instant)>, + element: ZoomElement, +} + +impl OutputZoomState { + pub fn new( + seat: &Seat, + output: &Output, + level: f64, + increment: u32, + movement: ZoomMovement, + loop_handle: LoopHandle<'static, State>, + mut theme: cosmic::Theme, + ) -> OutputZoomState { + theme.transparent = theme.cosmic().frosted_system_interface; + let cursor_position = seat.get_pointer().unwrap().current_location().as_global(); + let focal_point = cursor_position.to_local(output); + let output_geometry = output.geometry().to_f64(); + + let program = ZoomProgram::new(level, movement, increment); + let element = IcedElement::new(program, Size::default(), loop_handle, theme); + let mut size = element.minimum_size(); + size.w = size.w.min(output_geometry.size.w.round() as i32); + element.set_activate(true); + element.resize(size); + element.output_enter(output, Rectangle::new(Point::from((0, 0)), size)); + element.set_additional_scale(level.min(4.)); + + OutputZoomState { + level, + previous_level: None, + focal_point, + previous_point: None, + element, + } + } + + pub fn animating_focal_point(&mut self) -> Point { + if let Some((old_point, start)) = self.previous_point.as_ref() { + let duration_since = Instant::now().duration_since(*start); + if duration_since > ANIMATION_DURATION { + self.previous_point.take(); + return self.focal_point; + } + + let percentage = + duration_since.as_millis() as f32 / ANIMATION_DURATION.as_millis() as f32; + ease( + Linear, + EasePoint(*old_point), + EasePoint(self.focal_point), + percentage, + ) + .0 + } else { + self.focal_point + } + } + + pub fn current_focal_point(&mut self) -> Point { + self.focal_point + } + + pub fn current_level(&self) -> f64 { + self.level + } + + pub fn animating_level(&self) -> f64 { + if let Some((old_level, start)) = self.previous_level.as_ref() { + let percentage = Instant::now().duration_since(*start).as_millis() as f32 + / ANIMATION_DURATION.as_millis() as f32; + + ease(Linear, *old_level, self.level, percentage) + } else { + self.level + } + } + + pub fn is_animating(&self) -> bool { + self.previous_point.is_some() || self.previous_level.is_some() + } + + pub fn refresh(&mut self) -> bool { + if self + .previous_level + .as_ref() + .is_some_and(|(_, start)| Instant::now().duration_since(*start) > ANIMATION_DURATION) + { + self.previous_level.take(); + } + self.element.refresh(); + self.level == 1. && self.previous_level.is_none() + } + + pub fn update(&mut self, level: f64, animate: bool, movement: ZoomMovement, increment: u32) { + self.previous_level = animate.then_some((self.animating_level(), Instant::now())); + self.level = level; + self.element.set_additional_scale(level.min(4.)); + self.element.queue_message(ZoomMessage::Update { + level, + movement, + increment, + }); + } + + fn render( + &mut self, + renderer: &mut R, + output: &Output, + push: &mut dyn FnMut(IcedRenderElement), + ) where + R: AsGlowRenderer + ImportMem, + R::TextureId: Send + Clone + 'static, + { + let size = self.element.current_size().to_f64(); + let output_geo = output.geometry().to_f64(); + let scale = output.current_scale(); + let location = Point::from(( + output_geo.size.w / 2. - size.w / 2., + output_geo.size.h / 4. * 3. - size.h / 2., + )) + .to_physical(scale.fractional_scale()) + .to_i32_round(); + + self.element.push_render_elements( + renderer, + location, + scale.fractional_scale().into(), + 1.0, + self.element + .with_theme(|theme| theme.cosmic().radius_s()) + .map(|x| x.round() as u8), + push, + None, + ) + } +} + +impl ZoomState { + pub fn current_seat(&self) -> Seat { + self.seat.clone() + } + + pub fn current_level(&self, output: &Output) -> f64 { + let output_state = output.user_data().get::>().unwrap(); + output_state.lock().unwrap().current_level() + } + + pub fn animating_level(&self, output: &Output) -> f64 { + let output_state = output.user_data().get::>().unwrap(); + output_state.lock().unwrap().animating_level() + } + + pub fn animating_focal_point(&self, output: Option<&Output>) -> Point { + let active_output = self.seat.active_output(); + let output = output.unwrap_or(&active_output); + let output_state = output.user_data().get::>().unwrap(); + + output_state + .lock() + .unwrap() + .animating_focal_point() + .to_global(output) + } + + pub fn current_focal_point(&self, output: Option<&Output>) -> Point { + let active_output = self.seat.active_output(); + let output = output.unwrap_or(&active_output); + let output_state = output.user_data().get::>().unwrap(); + + output_state + .lock() + .unwrap() + .current_focal_point() + .to_global(output) + } + + pub fn update_focal_point( + &mut self, + output: &Output, + cursor_position: Point, + _original_position: Point, + movement: ZoomMovement, + ) { + let output_geometry = output.geometry().to_f64().to_local(output); + let zoomed_output_geometry = output.zoomed_geometry().unwrap().to_f64().to_local(output); + + let output_state = output.user_data().get::>().unwrap(); + let mut output_state_ref = output_state.lock().unwrap(); + + let level = output_state_ref.current_level(); + + let is_level_change = output_state_ref + .previous_level + .is_some_and(|prev| prev.0 != level); + + // animate level and movement type changes + if is_level_change || self.movement != movement { + output_state_ref.previous_point = Some((output_state_ref.focal_point, Instant::now())); + self.movement = movement; + } + + let cursor_position = cursor_position.to_local(output); + match movement { + ZoomMovement::Continuously => { + // Focal point is the cursor position + output_state_ref.focal_point = cursor_position; + } + ZoomMovement::OnEdge => { + if is_level_change { + // Ensure the cursor doesn't disappear off screen + output_state_ref.focal_point = cursor_position; + return; + } + + // Compute small margin relative to zoomed output to keep cursor within + // (can be user-configurable in the future) + let margin_size = zoomed_output_geometry.size.h * 0.02; + let margins = FrameExtents::new(margin_size, margin_size, margin_size, margin_size); + let inner_rect = zoomed_output_geometry - margins; + + if inner_rect.contains(cursor_position) { + // Do not move if cursor within margins + return; + } + + // Compute dx and dy to move the zoomed output based on cursor distance outside margin(s) + let dx = if cursor_position.x < inner_rect.loc.x { + cursor_position.x - inner_rect.loc.x + } else if cursor_position.x > inner_rect.loc.x + inner_rect.size.w { + cursor_position.x - (inner_rect.loc.x + inner_rect.size.w) + } else { + 0.0 + }; + let dy = if cursor_position.y < inner_rect.loc.y { + cursor_position.y - inner_rect.loc.y + } else if cursor_position.y > inner_rect.loc.y + inner_rect.size.h { + cursor_position.y - (inner_rect.loc.y + inner_rect.size.h) + } else { + 0.0 + }; + + let mut focal_point = output_state_ref.focal_point + Point::new(dx, dy); + + // Clamp the final focal point to output geometry + focal_point.x = focal_point.x.clamp( + output_geometry.loc.x, + output_geometry.loc.x + output_geometry.size.w - 1.0, + ); + focal_point.y = focal_point.y.clamp( + output_geometry.loc.y, + output_geometry.loc.y + output_geometry.size.h - 1.0, + ); + + output_state_ref.focal_point = focal_point; + } + ZoomMovement::Centered => { + let center = (output_geometry.size / 2.).to_point(); + + if level == 1.0 { + // Prevent focal point jumping to top-left corner (0, 0) on zoom out + output_state_ref.focal_point = center; + return; + } + + // Compute translation to keep cursor at center of screen + let mut tx = center.x - cursor_position.x * level; + let mut ty = center.y - cursor_position.y * level; + + // Clamp translation to keep viewport within screen bounds + tx = tx.clamp(output_geometry.size.w * (1.0 - level), 0.0); + ty = ty.clamp(output_geometry.size.h * (1.0 - level), 0.0); + + // Convert translation back to focal point: T = F * (1 - level) + output_state_ref.focal_point = + Point::from((tx / (1.0 - level), ty / (1.0 - level))); + } + } + } + + pub fn surface_under( + &self, + output: &Output, + pos: Point, + ) -> Option<(PointerFocusTarget, Point)> { + let output_geometry = output.geometry(); + let zoomed_output_geometry = output.zoomed_geometry().unwrap().to_f64(); + let local_pos = global_pos_to_screen_space(pos, output); + + let output_state = output.user_data().get::>().unwrap(); + let output_state_ref = output_state.lock().unwrap(); + + let size = output_state_ref.element.current_size().to_f64().as_local(); + let location = Point::::from(( + output_geometry.size.w as f64 / 2. - size.w / 2., + output_geometry.size.h as f64 / 4. * 3. - size.h / 2., + )); + let area = Rectangle::<_, Local>::new(location, size); + + if area.contains(local_pos) { + return Some(( + PointerFocusTarget::ZoomUI(output_state_ref.element.clone().into()), + { + // and vise-versa from screen-space to zoom-space... + let scaled_loc = location.downscale(output_state_ref.level); + let global_loc = Point::::from((scaled_loc.x, scaled_loc.y)) + + zoomed_output_geometry.loc; + + // HACK: We do have the right position now `global_loc`, but smithay calculates + // the relative position for us... Which will be wrong given the cursor movement will + // be scaled, while this element isn't, as it exists in screen-space and not workspace-space. + // So we shift the location relatively to make up for the scaled movement... + let diff = (pos - global_loc).upscale(output_state_ref.level - 1.); + + global_loc - diff + }, + )); + } + + None + } + + pub fn render(renderer: &mut R, output: &Output, push: &mut dyn FnMut(IcedRenderElement)) + where + R: AsGlowRenderer + ImportMem, + R::TextureId: Send + Clone + 'static, + { + let output_state = output.user_data().get::>().unwrap(); + output_state.lock().unwrap().render(renderer, output, push) + } +} + +fn global_pos_to_screen_space( + pos: impl Into>, + output: &Output, +) -> Point { + let pos = pos.into(); + let zoomed_output_geometry = output.zoomed_geometry().unwrap().to_f64(); + let level = output + .user_data() + .get::>() + .unwrap() + .lock() + .unwrap() + .current_level(); + + // lets try to get the global cursor position into screen space + let relative_to_zoom_geo = Point::::from(( + pos.x - zoomed_output_geometry.loc.x, + pos.y - zoomed_output_geometry.loc.y, + )); + relative_to_zoom_geo.upscale(level) +} + +pub type ZoomElement = IcedElement; + +pub struct ZoomProgram { + level: f64, + increments: Vec, + increment_idx: usize, + movement: ZoomMovement, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ZoomMessage { + Decrease, + Increase, + Increment, + More, + Close, + Update { + level: f64, + increment: u32, + movement: ZoomMovement, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum MenuMessage { + ViewContinuously, + ViewOnEdge, + ViewCentered, + OpenSettings, +} + +impl ZoomProgram { + pub fn new(level: f64, movement: ZoomMovement, increment: u32) -> Self { + let mut increments = vec![25, 50, 100, 150, 200]; + if !increments.contains(&increment) { + increments.push(increment); + } + increments.sort(); + let increment_idx = increments.iter().position(|val| *val == increment).unwrap(); + + ZoomProgram { + level, + increments, + increment_idx, + movement, + } + } +} + +impl Program for ZoomProgram { + type Message = ZoomMessage; + + fn view(&self) -> cosmic::Element<'_, Self::Message> { + widget::row::with_children(vec![ + widget::button::icon(Named::new("list-remove-symbolic").size(16).prefer_svg(true)) + .on_press(ZoomMessage::Decrease) + .into(), + widget::text(format!("{}%", (self.level * 100.).round())) + .align_y(Vertical::Center) + .width(Length::Shrink) + .into(), + widget::button::icon(Named::new("list-add-symbolic").size(16).prefer_svg(true)) + .on_press(ZoomMessage::Increase) + .into(), + widget::divider::vertical::default().into(), + widget::button::text(format!("{}%", self.increments[self.increment_idx])) + .trailing_icon(Named::new("pan-down-symbolic").size(16).prefer_svg(true)) + .on_press(ZoomMessage::Increment) + .class(theme::Button::MenuFolder) + .into(), + widget::button::icon(Named::new("view-more-symbolic").size(16).prefer_svg(true)) + .on_press(ZoomMessage::More) + .into(), + widget::divider::vertical::default().into(), + widget::button::icon( + Named::new("window-close-symbolic") + .size(16) + .prefer_svg(true), + ) + .on_press(ZoomMessage::Close) + .into(), + ]) + .spacing(8.) + .height(Length::Fixed(32.)) + .width(Length::Shrink) + .align_y(Alignment::Center) + .apply(widget::container) + .padding(8) + .class(theme::Container::custom(|theme| { + let cosmic = theme.cosmic(); + let component = &cosmic.background(theme.transparent).component; + iced_widget::container::Style { + snap: true, + icon_color: Some(component.on.into()), + text_color: Some(component.on.into()), + background: Some(Background::Color(component.base.into())), + border: Border { + radius: cosmic.radius_s().into(), + width: 1.0, + color: component.divider.into(), + }, + shadow: Default::default(), + } + })) + .into() + } + + fn update( + &mut self, + message: Self::Message, + loop_handle: &LoopHandle<'static, State>, + last_seat: Option<&(Seat, Serial)>, + ) -> cosmic::Task { + match message { + ZoomMessage::Decrease => { + let _ = loop_handle.insert_idle(|state| { + let seat = state.common.shell.read().seats.last_active().clone(); + let increment = + state.common.config.cosmic_conf.accessibility_zoom.increment as f64 / 100.0; + + state.update_zoom(&seat, -increment, true); + }); + } + ZoomMessage::Increase => { + let _ = loop_handle.insert_idle(|state| { + let seat = state.common.shell.read().seats.last_active().clone(); + let increment = + state.common.config.cosmic_conf.accessibility_zoom.increment as f64 / 100.0; + + state.update_zoom(&seat, increment, true); + }); + } + ZoomMessage::More => { + let movement = self.movement; + if let Some((seat, serial)) = last_seat.cloned() { + let _ = loop_handle.insert_idle(move |state| { + if let Some(start_data) = + check_grab_preconditions(&seat, Some(serial), None) + { + let shell = state.common.shell.read(); + let output = seat.active_output(); + + if shell.zoom_state().is_some() { + let location = global_pos_to_screen_space( + start_data.location().as_global(), + &output, + ); + + let output_geometry = output.geometry(); + let output_state = + output.user_data().get::>().unwrap(); + let output_state_ref = output_state.lock().unwrap(); + + let elem_size = + output_state_ref.element.current_size().to_f64().as_local(); + let elem_location = Point::::from(( + output_geometry.size.w as f64 / 2. - elem_size.w / 2., + output_geometry.size.h as f64 / 4. * 3. - elem_size.h / 2., + )); + let position = Point::<_, Local>::from(( + location.x, + elem_location.y + elem_size.h / 2., + )); + let level = output_state_ref.level; + std::mem::drop(output_state_ref); + + let mut theme = state.common.theme.clone(); + theme.transparent = theme.cosmic().frosted_system_interface; + let grab = MenuGrab::new( + start_data, + &seat, + vec![ + Item::new( + crate::fl!("a11y-zoom-move-continuously"), + move |handle| { + let _ = handle.insert_idle(move |state| { + state + .common + .config + .cosmic_conf + .accessibility_zoom + .view_moves = ZoomMovement::Continuously; + if let Err(err) = + state.common.config.cosmic_helper.set( + "accessibility_zoom", + state + .common + .config + .cosmic_conf + .accessibility_zoom, + ) + { + error!( + ?err, + "Failed to update zoom config" + ); + } + state.common.update_config(); + }); + }, + ) + .toggled(movement == ZoomMovement::Continuously), + Item::new( + crate::fl!("a11y-zoom-move-onedge"), + move |handle| { + let _ = handle.insert_idle(move |state| { + state + .common + .config + .cosmic_conf + .accessibility_zoom + .view_moves = ZoomMovement::OnEdge; + if let Err(err) = + state.common.config.cosmic_helper.set( + "accessibility_zoom", + state + .common + .config + .cosmic_conf + .accessibility_zoom, + ) + { + error!( + ?err, + "Failed to update zoom config" + ); + } + state.common.update_config(); + }); + }, + ) + .toggled(movement == ZoomMovement::OnEdge), + Item::new( + crate::fl!("a11y-zoom-move-centered"), + move |handle| { + let _ = handle.insert_idle(move |state| { + state + .common + .config + .cosmic_conf + .accessibility_zoom + .view_moves = ZoomMovement::Centered; + if let Err(err) = + state.common.config.cosmic_helper.set( + "accessibility_zoom", + state + .common + .config + .cosmic_conf + .accessibility_zoom, + ) + { + error!( + ?err, + "Failed to update zoom config" + ); + } + state.common.update_config(); + }); + }, + ) + .toggled(movement == ZoomMovement::Centered), + Item::Separator, + Item::new(crate::fl!("a11y-zoom-settings"), |handle| { + let _ = handle.insert_idle(move |state| { + state.spawn_command( + "cosmic-settings accessibility-magnifier" + .into(), + ); + }); + }), + ] + .into_iter(), + position.to_global(&output).to_i32_round(), + MenuAlignment::horizontally_centered( + (elem_size.h / 2.).round() as u32, + false, + ), + Some(level.min(4.)), + state.common.event_loop_handle.clone(), + theme, + ); + + std::mem::drop(shell); + if grab.is_touch_grab() { + seat.get_touch().unwrap().set_grab(state, grab, serial); + } else { + seat.get_pointer().unwrap().set_grab( + state, + grab, + serial, + Focus::Clear, + ); + } + } + } + }); + } + } + ZoomMessage::Increment => { + if let Some((seat, serial)) = last_seat.cloned() { + let increments = self.increments.clone(); + let _ = loop_handle.insert_idle(move |state| { + if let Some(start_data) = + check_grab_preconditions(&seat, Some(serial), None) + { + let shell = state.common.shell.read(); + let output = seat.active_output(); + + if shell.zoom_state().is_some() { + let location = global_pos_to_screen_space( + start_data.location().as_global(), + &output, + ); + + let output_geometry = output.geometry(); + let output_state = + output.user_data().get::>().unwrap(); + let output_state_ref = output_state.lock().unwrap(); + + let elem_size = + output_state_ref.element.current_size().to_f64().as_local(); + let elem_location = Point::::from(( + output_geometry.size.w as f64 / 2. - elem_size.w / 2., + output_geometry.size.h as f64 / 4. * 3. - elem_size.h / 2., + )); + let position = Point::<_, Local>::from(( + location.x, + elem_location.y + (elem_size.h / 2.), + )); + let level = output_state_ref.level; + std::mem::drop(output_state_ref); + + let mut theme = state.common.theme.clone(); + theme.transparent = theme.cosmic().frosted_system_interface; + let grab = MenuGrab::new( + start_data, + &seat, + increments.into_iter().map(|val| { + Item::new(format!("{}%", val), move |handle| { + let _ = handle.insert_idle(move |state| { + state + .common + .config + .cosmic_conf + .accessibility_zoom + .increment = val; + state.common.update_config(); + if let Err(err) = + state.common.config.cosmic_helper.set( + "accessibility_zoom", + state + .common + .config + .cosmic_conf + .accessibility_zoom, + ) + { + error!(?err, "Failed to update zoom config"); + } + }); + }) + }), + position.to_global(&output).to_i32_round(), + MenuAlignment::PREFER_CENTERED, + Some(level.min(4.)), + state.common.event_loop_handle.clone(), + theme, + ); + + std::mem::drop(shell); + if grab.is_touch_grab() { + seat.get_touch().unwrap().set_grab(state, grab, serial); + } else { + seat.get_pointer().unwrap().set_grab( + state, + grab, + serial, + Focus::Clear, + ); + } + } + } + }); + } + } + ZoomMessage::Close => { + let _ = loop_handle.insert_idle(|state| { + state + .common + .config + .cosmic_conf + .accessibility_zoom + .show_overlay = false; + if let Err(err) = state.common.config.cosmic_helper.set( + "accessibility_zoom", + state.common.config.cosmic_conf.accessibility_zoom, + ) { + error!(?err, "Failed to update zoom config"); + } + state.common.update_config(); + }); + } + ZoomMessage::Update { + level, + increment, + movement, + } => { + self.level = level; + self.movement = movement; + + if let Some(pos) = self.increments.iter().position(|val| *val == increment) { + self.increment_idx = pos; + } else { + let mut increments = vec![25, 50, 100, 150, 200]; + if !increments.contains(&increment) { + increments.push(increment); + } + increments.sort(); + self.increment_idx = + increments.iter().position(|val| *val == increment).unwrap(); + self.increments = increments; + } + } + } + cosmic::Task::none() + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ZoomFocusTarget { + Main(ZoomElement), + Menu(IcedElement), +} + +impl From for ZoomFocusTarget { + fn from(value: ZoomElement) -> Self { + ZoomFocusTarget::Main(value) + } +} + +impl From> for ZoomFocusTarget { + fn from(value: IcedElement) -> Self { + ZoomFocusTarget::Menu(value) + } +} + +impl PointerTarget for ZoomFocusTarget { + fn enter(&self, seat: &Seat, data: &mut State, event: &PointerMotionEvent) { + match self { + ZoomFocusTarget::Main(elem) => PointerTarget::enter(elem, seat, data, event), + ZoomFocusTarget::Menu(elem) => PointerTarget::enter(elem, seat, data, event), + } + } + + fn motion(&self, seat: &Seat, data: &mut State, event: &PointerMotionEvent) { + match self { + ZoomFocusTarget::Main(elem) => PointerTarget::motion(elem, seat, data, event), + ZoomFocusTarget::Menu(elem) => PointerTarget::motion(elem, seat, data, event), + } + } + + fn relative_motion(&self, seat: &Seat, data: &mut State, event: &RelativeMotionEvent) { + match self { + ZoomFocusTarget::Main(elem) => PointerTarget::relative_motion(elem, seat, data, event), + ZoomFocusTarget::Menu(elem) => PointerTarget::relative_motion(elem, seat, data, event), + } + } + + fn button(&self, seat: &Seat, data: &mut State, event: &ButtonEvent) { + match self { + ZoomFocusTarget::Main(elem) => PointerTarget::button(elem, seat, data, event), + ZoomFocusTarget::Menu(elem) => PointerTarget::button(elem, seat, data, event), + } + } + + fn axis(&self, seat: &Seat, data: &mut State, frame: AxisFrame) { + match self { + ZoomFocusTarget::Main(elem) => PointerTarget::axis(elem, seat, data, frame), + ZoomFocusTarget::Menu(elem) => PointerTarget::axis(elem, seat, data, frame), + } + } + + fn frame(&self, seat: &Seat, data: &mut State) { + match self { + ZoomFocusTarget::Main(elem) => PointerTarget::frame(elem, seat, data), + ZoomFocusTarget::Menu(elem) => PointerTarget::frame(elem, seat, data), + } + } + + fn gesture_swipe_begin( + &self, + seat: &Seat, + data: &mut State, + event: &GestureSwipeBeginEvent, + ) { + match self { + ZoomFocusTarget::Main(elem) => { + PointerTarget::gesture_swipe_begin(elem, seat, data, event) + } + ZoomFocusTarget::Menu(elem) => { + PointerTarget::gesture_swipe_begin(elem, seat, data, event) + } + } + } + + fn gesture_swipe_update( + &self, + seat: &Seat, + data: &mut State, + event: &GestureSwipeUpdateEvent, + ) { + match self { + ZoomFocusTarget::Main(elem) => { + PointerTarget::gesture_swipe_update(elem, seat, data, event) + } + ZoomFocusTarget::Menu(elem) => { + PointerTarget::gesture_swipe_update(elem, seat, data, event) + } + } + } + + fn gesture_swipe_end( + &self, + seat: &Seat, + data: &mut State, + event: &GestureSwipeEndEvent, + ) { + match self { + ZoomFocusTarget::Main(elem) => { + PointerTarget::gesture_swipe_end(elem, seat, data, event) + } + ZoomFocusTarget::Menu(elem) => { + PointerTarget::gesture_swipe_end(elem, seat, data, event) + } + } + } + + fn gesture_pinch_begin( + &self, + seat: &Seat, + data: &mut State, + event: &GesturePinchBeginEvent, + ) { + match self { + ZoomFocusTarget::Main(elem) => { + PointerTarget::gesture_pinch_begin(elem, seat, data, event) + } + ZoomFocusTarget::Menu(elem) => { + PointerTarget::gesture_pinch_begin(elem, seat, data, event) + } + } + } + + fn gesture_pinch_update( + &self, + seat: &Seat, + data: &mut State, + event: &GesturePinchUpdateEvent, + ) { + match self { + ZoomFocusTarget::Main(elem) => { + PointerTarget::gesture_pinch_update(elem, seat, data, event) + } + ZoomFocusTarget::Menu(elem) => { + PointerTarget::gesture_pinch_update(elem, seat, data, event) + } + } + } + + fn gesture_pinch_end( + &self, + seat: &Seat, + data: &mut State, + event: &GesturePinchEndEvent, + ) { + match self { + ZoomFocusTarget::Main(elem) => { + PointerTarget::gesture_pinch_end(elem, seat, data, event) + } + ZoomFocusTarget::Menu(elem) => { + PointerTarget::gesture_pinch_end(elem, seat, data, event) + } + } + } + + fn gesture_hold_begin( + &self, + seat: &Seat, + data: &mut State, + event: &GestureHoldBeginEvent, + ) { + match self { + ZoomFocusTarget::Main(elem) => { + PointerTarget::gesture_hold_begin(elem, seat, data, event) + } + ZoomFocusTarget::Menu(elem) => { + PointerTarget::gesture_hold_begin(elem, seat, data, event) + } + } + } + + fn gesture_hold_end(&self, seat: &Seat, data: &mut State, event: &GestureHoldEndEvent) { + match self { + ZoomFocusTarget::Main(elem) => PointerTarget::gesture_hold_end(elem, seat, data, event), + ZoomFocusTarget::Menu(elem) => PointerTarget::gesture_hold_end(elem, seat, data, event), + } + } + + fn leave(&self, seat: &Seat, data: &mut State, serial: Serial, time: u32) { + match self { + ZoomFocusTarget::Main(elem) => PointerTarget::leave(elem, seat, data, serial, time), + ZoomFocusTarget::Menu(elem) => PointerTarget::leave(elem, seat, data, serial, time), + } + } +} + +impl TouchTarget for ZoomFocusTarget { + fn down(&self, seat: &Seat, data: &mut State, event: &DownEvent) { + match self { + ZoomFocusTarget::Main(elem) => TouchTarget::down(elem, seat, data, event), + ZoomFocusTarget::Menu(elem) => TouchTarget::down(elem, seat, data, event), + } + } + + fn up(&self, seat: &Seat, data: &mut State, event: &UpEvent) { + match self { + ZoomFocusTarget::Main(elem) => TouchTarget::up(elem, seat, data, event), + ZoomFocusTarget::Menu(elem) => TouchTarget::up(elem, seat, data, event), + } + } + + fn motion(&self, seat: &Seat, data: &mut State, event: &TouchMotionEvent) { + match self { + ZoomFocusTarget::Main(elem) => TouchTarget::motion(elem, seat, data, event), + ZoomFocusTarget::Menu(elem) => TouchTarget::motion(elem, seat, data, event), + } + } + + fn frame(&self, seat: &Seat, data: &mut State, frame: FrameMarker) { + match self { + ZoomFocusTarget::Main(elem) => TouchTarget::frame(elem, seat, data, frame), + ZoomFocusTarget::Menu(elem) => TouchTarget::frame(elem, seat, data, frame), + } + } + + fn cancel(&self, seat: &Seat, data: &mut State, frame: FrameMarker) { + match self { + ZoomFocusTarget::Main(elem) => TouchTarget::cancel(elem, seat, data, frame), + ZoomFocusTarget::Menu(elem) => TouchTarget::cancel(elem, seat, data, frame), + } + } + + fn shape(&self, seat: &Seat, data: &mut State, event: &ShapeEvent) { + match self { + ZoomFocusTarget::Main(elem) => TouchTarget::shape(elem, seat, data, event), + ZoomFocusTarget::Menu(elem) => TouchTarget::shape(elem, seat, data, event), + } + } + + fn orientation(&self, seat: &Seat, data: &mut State, event: &OrientationEvent) { + match self { + ZoomFocusTarget::Main(elem) => TouchTarget::orientation(elem, seat, data, event), + ZoomFocusTarget::Menu(elem) => TouchTarget::orientation(elem, seat, data, event), + } + } + + fn last_frame(&self, seat: &Seat, data: &mut State) -> Option { + match self { + ZoomFocusTarget::Main(elem) => TouchTarget::last_frame(elem, seat, data), + ZoomFocusTarget::Menu(elem) => TouchTarget::last_frame(elem, seat, data), + } + } +} + +impl IsAlive for ZoomFocusTarget { + fn alive(&self) -> bool { + match self { + ZoomFocusTarget::Main(elem) => elem.alive(), + ZoomFocusTarget::Menu(elem) => elem.alive(), + } + } +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..24ccd9e --- /dev/null +++ b/src/state.rs @@ -0,0 +1,1426 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{ + backend::{ + kms::{KmsGuard, KmsState}, + render::{GlMultiError, RendererRef}, + winit::WinitState, + x11::X11State, + }, + config::{CompOutputConfig, Config, ScreenFilter}, + dbus::DBusState, + input::{PointerFocusState, gestures::GestureState}, + shell::{CosmicSurface, SeatExt, Shell, grabs::SeatMoveGrabState}, + utils::prelude::OutputExt, + wayland::{ + handlers::{data_device::get_dnd_icon, image_copy_capture::SessionHolder}, + protocols::{ + a11y::A11yState, + corner_radius::CornerRadiusState, + drm::WlDrmState, + image_capture_source::CosmicImageCaptureSourceState, + keyboard_layout::KeyboardLayoutState, + output_configuration::OutputConfigurationState, + output_power::OutputPowerState, + overlap_notify::OverlapNotifyState, + toplevel_info::ToplevelInfoState, + toplevel_management::{ManagementCapabilities, ToplevelManagementState}, + workspace::{WorkspaceState, WorkspaceUpdateGuard}, + }, + }, + xwayland::XWaylandState, +}; +use anyhow::Context; +use calloop::RegistrationToken; +use cosmic_comp_config::output::comp::{OutputConfig, OutputState}; +use i18n_embed::{ + DesktopLanguageRequester, + fluent::{FluentLanguageLoader, fluent_language_loader}, +}; +use rust_embed::RustEmbed; +use smithay::{ + backend::{ + allocator::{Fourcc, dmabuf::Dmabuf}, + drm::DrmNode, + renderer::{ + ImportDma, + element::{ + RenderElementState, RenderElementStates, default_primary_scanout_output_compare, + utils::select_dmabuf_feedback, + }, + }, + }, + desktop::{ + PopupManager, layer_map_for_output, + utils::{ + send_dmabuf_feedback_surface_tree, send_frames_surface_tree, + surface_primary_scanout_output, update_surface_primary_scanout_output, + with_surfaces_surface_tree, + }, + }, + input::{SeatState, pointer::CursorImageStatus}, + output::{Output, Scale, WeakOutput}, + reexports::{ + calloop::{LoopHandle, LoopSignal}, + wayland_protocols::xdg::shell::server::xdg_toplevel::WmCapabilities, + wayland_protocols_misc::server_decoration::server::org_kde_kwin_server_decoration_manager::Mode, + wayland_server::{ + Client, DisplayHandle, Resource, + backend::{ClientData, ClientId, DisconnectReason}, + protocol::{wl_shm, wl_surface::WlSurface}, + }, + }, + utils::{Clock, Monotonic, Point}, + wayland::{ + alpha_modifier::AlphaModifierState, + background_effect::BackgroundEffectState, + compositor::{CompositorClientState, CompositorState, SurfaceData}, + cursor_shape::CursorShapeManagerState, + dmabuf::{DmabufFeedback, DmabufGlobal, DmabufState}, + fixes::FixesState, + fractional_scale::{FractionalScaleManagerState, with_fractional_scale}, + idle_inhibit::IdleInhibitManagerState, + idle_notify::IdleNotifierState, + image_capture_source::{OutputCaptureSourceState, ToplevelCaptureSourceState}, + image_copy_capture::ImageCopyCaptureState, + input_method::InputMethodManagerState, + keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitState, + output::OutputManagerState, + pointer_constraints::PointerConstraintsState, + pointer_gestures::PointerGesturesState, + pointer_warp::PointerWarpManager, + presentation::PresentationState, + seat::WaylandFocus, + security_context::{SecurityContext, SecurityContextState}, + selection::{ + data_device::DataDeviceState, + ext_data_control::DataControlState as ExtDataControlState, + primary_selection::PrimarySelectionState, + wlr_data_control::DataControlState as WlrDataControlState, + }, + session_lock::SessionLockManagerState, + shell::{ + kde::decoration::KdeDecorationState, + wlr_layer::WlrLayerShellState, + xdg::{XdgShellState, decoration::XdgDecorationState}, + }, + shm::ShmState, + single_pixel_buffer::SinglePixelBufferState, + tablet_manager::TabletManagerState, + text_input::TextInputManagerState, + viewporter::ViewporterState, + virtual_keyboard::VirtualKeyboardManagerState, + xdg_activation::XdgActivationState, + xdg_foreign::XdgForeignState, + xwayland_keyboard_grab::XWaylandKeyboardGrabState, + xwayland_shell::XWaylandShellState, + }, + xwayland::XWaylandClientData, +}; +use tracing::warn; + +#[cfg(feature = "logind")] +use std::os::fd::OwnedFd; + +use std::{ + cell::RefCell, + cmp::min, + collections::HashSet, + ffi::OsString, + process::{Child, Command}, + sync::{Arc, LazyLock, Once, atomic::AtomicBool}, + time::{Duration, Instant}, +}; + +#[derive(RustEmbed)] +#[folder = "resources/i18n"] +struct Localizations; + +pub static LANG_LOADER: LazyLock = + LazyLock::new(|| fluent_language_loader!()); + +#[macro_export] +macro_rules! fl { + ($message_id:literal) => {{ + i18n_embed_fl::fl!($crate::state::LANG_LOADER, $message_id) + }}; + + ($message_id:literal, $($args:expr),*) => {{ + i18n_embed_fl::fl!($crate::state::LANG_LOADER, $message_id, $($args), *) + }}; +} + +pub struct ClientState { + pub compositor_client_state: CompositorClientState, + pub advertised_drm_node: Option, + pub evlh: LoopHandle<'static, State>, + pub evls: LoopSignal, + pub security_context: Option, +} +unsafe impl Send for ClientState {} +unsafe impl Sync for ClientState {} + +impl ClientState { + /// We treat a client as "sandboxed" if it has a security context for any sandbox engine + /// other than `com.system76.CosmicPanel` + pub fn not_sandboxed(&self) -> bool { + self.security_context + .as_ref() + .is_none_or(|security_context| { + security_context.sandbox_engine.as_deref() == Some("com.system76.CosmicPanel") + }) + } +} + +impl ClientData for ClientState { + fn initialized(&self, _client_id: ClientId) {} + fn disconnected(&self, client_id: ClientId, _reason: DisconnectReason) { + self.evlh.insert_idle(move |state| { + if let BackendData::Kms(kms_state) = &mut state.backend { + for device in kms_state.drm_devices.values_mut() { + if device.inner.active_clients.remove(&client_id) + && !device + .inner + .in_use(kms_state.primary_node.read().unwrap().as_ref()) + { + if let Err(err) = kms_state.refresh_used_devices() { + warn!(?err, "Failed to init devices."); + }; + break; + } + } + } + }); + self.evls.wakeup(); + } +} + +pub fn advertised_node_for_client(client: &Client) -> Option { + // Lets check the global drm-node the client got either through default-feedback or wl_drm + if let Some(normal_client) = client.get_data::() { + return normal_client.advertised_drm_node; + } + // last but not least all xwayland-surfaces should also share a single node + if let Some(xwayland_client) = client.get_data::() { + return xwayland_client.user_data().get::().cloned(); + } + None +} + +pub fn advertised_node_for_surface(w: &WlSurface, dh: &DisplayHandle) -> Option { + let client = dh.get_client(w.id()).ok()?; + advertised_node_for_client(&client) +} + +#[derive(Debug)] +pub enum LastRefresh { + None, + At(Instant), + Scheduled(RegistrationToken), +} + +#[derive(Debug)] +pub struct State { + pub backend: BackendData, + pub common: Common, + pub ready: Once, + pub last_refresh: LastRefresh, + pub kiosk_command: Option, +} +smithay::delegate_dispatch2!(State); + +#[derive(Debug)] +pub struct Common { + pub config: Config, + + pub socket: OsString, + pub display_handle: DisplayHandle, + pub event_loop_handle: LoopHandle<'static, State>, + pub event_loop_signal: LoopSignal, + + pub popups: PopupManager, + pub shell: Arc>, + + pub clock: Clock, + pub startup_done: Arc, + pub should_stop: bool, + pub kiosk_exit_code: Option, + + pub gesture_state: Option, + + pub kiosk_child: Option, + pub theme: cosmic::Theme, + + // wayland state + pub compositor_state: CompositorState, + pub corner_radius_state: CornerRadiusState, + pub data_device_state: DataDeviceState, + pub dmabuf_state: DmabufState, + pub fractional_scale_state: FractionalScaleManagerState, + pub keyboard_shortcuts_inhibit_state: KeyboardShortcutsInhibitState, + pub output_state: OutputManagerState, + pub output_configuration_state: OutputConfigurationState, + pub output_power_state: OutputPowerState, + pub presentation_state: PresentationState, + pub primary_selection_state: PrimarySelectionState, + pub ext_data_control_state: ExtDataControlState, + pub wlr_data_control_state: WlrDataControlState, + pub cosmic_image_capture_source_state: CosmicImageCaptureSourceState, + pub output_capture_source_state: OutputCaptureSourceState, + pub toplevel_capture_source_state: ToplevelCaptureSourceState, + pub image_copy_capture_state: ImageCopyCaptureState, + pub seat_state: SeatState, + pub session_lock_manager_state: SessionLockManagerState, + pub idle_notifier_state: IdleNotifierState, + pub idle_inhibit_manager_state: IdleInhibitManagerState, + pub idle_inhibiting_surfaces: HashSet, + pub shm_state: ShmState, + pub cursor_shape_manager_state: CursorShapeManagerState, + pub wl_drm_state: WlDrmState>, + pub viewporter_state: ViewporterState, + pub kde_decoration_state: KdeDecorationState, + pub xdg_decoration_state: XdgDecorationState, + pub overlap_notify_state: OverlapNotifyState, + pub a11y_state: A11yState, + pub dbus_state: DBusState, + pub keyboard_layout_state: KeyboardLayoutState, + pub background_effect_state: BackgroundEffectState, + + // shell-related wayland state + pub xdg_shell_state: XdgShellState, + pub layer_shell_state: WlrLayerShellState, + pub toplevel_info_state: ToplevelInfoState, + pub toplevel_management_state: ToplevelManagementState, + pub xdg_activation_state: XdgActivationState, + pub xdg_foreign_state: XdgForeignState, + pub workspace_state: WorkspaceState, + pub xwayland_scale: Option, + pub xwayland_state: Option, + pub xwayland_shell_state: XWaylandShellState, + pub pointer_focus_state: Option, + + #[cfg(feature = "logind")] + pub inhibit_lid_fd: Option, + + pub with_xwayland: bool, +} + +#[derive(Debug)] +#[allow(clippy::large_enum_variant)] +pub enum BackendData { + X11(X11State), + Winit(WinitState), + Kms(KmsState), + // TODO + // Wayland(WaylandState), + Unset, +} + +pub enum LockedBackend<'a> { + X11(&'a mut X11State), + Winit(&'a mut WinitState), + Kms(KmsGuard<'a>), +} + +#[derive(Debug, Clone)] +pub struct SurfaceDmabufFeedback { + pub render_feedback: DmabufFeedback, + pub overlay_scanout_feedback: Option, + pub primary_scanout_feedback: Option, +} + +#[derive(Debug)] +struct SurfaceFrameThrottlingState { + last_sent_at: RefCell>, +} +impl Default for SurfaceFrameThrottlingState { + fn default() -> Self { + SurfaceFrameThrottlingState { + last_sent_at: RefCell::new(None), + } + } +} + +impl BackendData { + pub fn kms(&mut self) -> &mut KmsState { + match self { + BackendData::Kms(kms_state) => kms_state, + _ => unreachable!("Called kms in non kms backend"), + } + } + + pub fn x11(&mut self) -> &mut X11State { + match self { + BackendData::X11(x11_state) => x11_state, + _ => unreachable!("Called x11 in non x11 backend"), + } + } + + pub fn winit(&mut self) -> &mut WinitState { + match self { + BackendData::Winit(winit_state) => winit_state, + _ => unreachable!("Called winit in non winit backend"), + } + } + + pub fn schedule_render(&mut self, output: &Output) { + match self { + BackendData::Winit(_) => {} // We cannot do this on the winit backend. + // Winit has a very strict render-loop and skipping frames breaks atleast the wayland winit-backend. + // Swapping with damage (which should be empty on these frames) is likely good enough anyway. + BackendData::X11(state) => state.schedule_render(output), + BackendData::Kms(state) => state.schedule_render(output), + _ => unreachable!("No backend was initialized"), + } + } + + pub fn dmabuf_imported( + &mut self, + client: Option, + global: &DmabufGlobal, + dmabuf: Dmabuf, + ) -> Result, anyhow::Error> { + match self { + BackendData::Kms(state) => { + return state.dmabuf_imported(client, global, dmabuf).map(Some); + } + BackendData::Winit(state) => { + state.backend.renderer().import_dmabuf(&dmabuf, None)?; + } + BackendData::X11(state) => { + state.renderer.import_dmabuf(&dmabuf, None)?; + } + _ => unreachable!("No backend set when importing dmabuf"), + }; + Ok(None) + } + + /// Get an offscreen renderer for screen capture / screenshot rendering + /// + /// `kms_node_cb` callback use used to determine nodes to render with when using kms backend. + /// If this returns `None`, it will attempt to use llvmpipe, then panic if no renderer is + /// found. + pub fn offscreen_renderer, F: FnOnce(&mut KmsState) -> Option>( + &mut self, + kms_node_cb: F, + ) -> Result, GlMultiError> { + match self { + BackendData::Kms(kms) => { + if let Some(nodes) = kms_node_cb(kms) { + let nodes = nodes.into(); + Ok(RendererRef::GlMulti(kms.api.renderer( + &nodes.render_node, + &nodes.target_node, + nodes.copy_format, + )?)) + } else { + Ok(RendererRef::Glow( + kms.software_renderer + .as_mut() + .expect("No Software Rendering"), + )) + } + } + BackendData::Winit(winit) => Ok(RendererRef::Glow(winit.backend.renderer())), + BackendData::X11(x11) => Ok(RendererRef::Glow(&mut x11.renderer)), + _ => unreachable!("No backend set when getting offscreen renderer"), + } + } + + pub fn update_screen_filter(&mut self, screen_filter: &ScreenFilter) -> anyhow::Result<()> { + match self { + BackendData::Kms(state) => state.update_screen_filter(screen_filter), + BackendData::Winit(state) => state.update_screen_filter(screen_filter), + BackendData::X11(state) => state.update_screen_filter(screen_filter), + _ => unreachable!("No backend set when setting screen filters"), + } + } + + pub fn lock(&mut self) -> LockedBackend<'_> { + match self { + BackendData::Kms(state) => LockedBackend::Kms(state.lock_devices()), + BackendData::X11(state) => LockedBackend::X11(state), + BackendData::Winit(state) => LockedBackend::Winit(state), + _ => unreachable!("Tried to lock unset backend"), + } + } +} + +impl LockedBackend<'_> { + pub fn all_outputs(&self) -> Vec { + match self { + LockedBackend::Kms(state) => state.all_outputs(), + LockedBackend::X11(state) => state.all_outputs(), + LockedBackend::Winit(state) => state.all_outputs(), + } + } + + pub fn enable_internal_output( + &self, + output_configuration_state: &mut OutputConfigurationState, + ) { + let outputs = self.all_outputs(); + if let Some(internal) = outputs.iter().find(|o| o.is_internal()) { + let mut config = internal.config_mut(); + if config.enabled == OutputState::Disabled { + // If it was previously mirrored, `read_outputs` will restore that correctly. + // But if we don't have a config for *some* reason or reading it fails, + // we don't want to write out `Disabled` accidentally. + config.enabled = OutputState::Enabled; + output_configuration_state.add_heads(std::iter::once(internal)); + } + } + } + + pub fn disable_internal_output( + &self, + output_configuration_state: &mut OutputConfigurationState, + ) { + let outputs = self.all_outputs(); + if let Some(internal) = outputs.iter().find(|o| o.is_internal()) { + let mut config = internal.config_mut(); + if config.enabled != OutputState::Disabled { + config.enabled = OutputState::Disabled; + output_configuration_state.remove_heads(std::iter::once(internal)); + } + } + } + + pub fn apply_config_for_outputs( + &mut self, + test_only: bool, + loop_handle: &LoopHandle<'static, State>, + screen_filter: &ScreenFilter, + shell: Arc>, + workspace_state: &mut WorkspaceUpdateGuard<'_, State>, + xdg_activation_state: &XdgActivationState, + startup_done: Arc, + clock: &Clock, + ) -> Result<(), anyhow::Error> { + let all_outputs = self.all_outputs(); + + // update outputs, so that `OutputModeSource`s are correct + for output in &all_outputs { + // apply to Output + let final_config = CompOutputConfig( + output + .user_data() + .get::>() + .unwrap() + .borrow(), + ); + + let mode = Some(final_config.output_mode()).filter(|m| match output.current_mode() { + None => true, + Some(c_m) => m.size != c_m.size || m.refresh != c_m.refresh, + }); + let transform = + Some(final_config.transform()).filter(|x| *x != output.current_transform()); + let scale = Some(final_config.0.scale) + .filter(|x| *x != output.current_scale().fractional_scale()); + let location = Some(Point::from(( + final_config.0.position.0 as i32, + final_config.0.position.1 as i32, + ))) + .filter(|x| *x != output.current_location()); + output.change_current_state(mode, transform, scale.map(Scale::Fractional), location); + + output.set_adaptive_sync(final_config.0.vrr); + } + + match self { + LockedBackend::Kms(state) => state.apply_config_for_outputs( + test_only, + loop_handle, + screen_filter, + shell.clone(), + startup_done, + clock, + ), + LockedBackend::Winit(state) => state.apply_config_for_outputs(test_only), + LockedBackend::X11(state) => state.apply_config_for_outputs(test_only), + }?; + + let mut shell_ref = shell.write(); + for output in &all_outputs { + // apply the rest; add / remove outputs + let final_config = output + .user_data() + .get::>() + .unwrap() + .borrow(); + + output.set_mirroring(match &final_config.enabled { + OutputState::Mirroring(conn) => shell_ref + .outputs() + .find(|output| &output.name() == conn) + .cloned(), + _ => None, + }); + + match final_config.enabled { + OutputState::Enabled => shell_ref.workspaces.add_output(output, workspace_state), + _ => { + let shell = &mut *shell_ref; + shell.workspaces.remove_output( + output, + shell.seats.iter(), + workspace_state, + xdg_activation_state, + ) + } + } + + layer_map_for_output(output).arrange(); + } + + // Update layout for changes in resolution, scale, orientation + shell_ref.workspaces.recalculate(); + let active_outputs = shell_ref.outputs().cloned().collect::>(); + std::mem::drop(shell_ref); + + for output in active_outputs { + match self { + LockedBackend::Winit(_) => {} // We cannot do this on the winit backend. + // Winit has a very strict render-loop and skipping frames breaks atleast the wayland winit-backend. + // Swapping with damage (which should be empty on these frames) is likely good enough anyway. + LockedBackend::X11(state) => state.schedule_render(&output), + LockedBackend::Kms(state) => state.schedule_render(&output), + } + } + + loop_handle.insert_idle(move |state| { + state.update_inhibitor_locks(); + state.common.update_xwayland_settings(); + state.common.update_xwayland_primary_output(); + }); + + Ok(()) + } +} + +pub struct KmsNodes { + pub render_node: DrmNode, + pub target_node: DrmNode, + pub copy_format: Fourcc, +} + +impl From for KmsNodes { + fn from(node: DrmNode) -> Self { + KmsNodes { + render_node: node, + target_node: node, + // Ignored if render == target + copy_format: Fourcc::Abgr8888, + } + } +} + +pub fn client_has_no_security_context(client: &Client) -> bool { + client + .get_data::() + .is_none_or(|client_state| client_state.security_context.is_none()) +} + +fn client_not_sandboxed(client: &Client) -> bool { + client + .get_data::() + .is_some_and(|client_state| client_state.not_sandboxed()) +} + +impl State { + pub fn new( + dh: &DisplayHandle, + socket: OsString, + handle: LoopHandle<'static, State>, + signal: LoopSignal, + with_xwayland: bool, + kiosk_command: Option, + ) -> State { + let requested_languages = DesktopLanguageRequester::requested_languages(); + i18n_embed::select(&*LANG_LOADER, &Localizations, &requested_languages) + .with_context(|| "Failed to load languages") + .unwrap(); + + let clock = Clock::new(); + let config = Config::load(&handle); + let compositor_state = CompositorState::new::(dh); + let corner_radius_state = CornerRadiusState::new::(dh); + let data_device_state = DataDeviceState::new::(dh); + let dmabuf_state = DmabufState::new(); + let fractional_scale_state = FractionalScaleManagerState::new::(dh); + let keyboard_shortcuts_inhibit_state = KeyboardShortcutsInhibitState::new::(dh); + let output_state = OutputManagerState::new_with_xdg_output::(dh); + let output_configuration_state = + OutputConfigurationState::new(dh, handle.clone(), client_not_sandboxed); + let output_power_state = OutputPowerState::new::(dh, client_not_sandboxed); + let overlap_notify_state = + OverlapNotifyState::new::(dh, client_has_no_security_context); + let presentation_state = PresentationState::new::(dh, clock.id() as u32); + let primary_selection_state = PrimarySelectionState::new::(dh); + let cosmic_image_capture_source_state = + CosmicImageCaptureSourceState::new::(dh, client_not_sandboxed); + let output_capture_source_state = + OutputCaptureSourceState::new_with_filter::(dh, client_not_sandboxed); + let toplevel_capture_source_state = + ToplevelCaptureSourceState::new_with_filter::(dh, client_not_sandboxed); + let image_copy_capture_state = + ImageCopyCaptureState::new_with_filter::(dh, client_not_sandboxed); + let shm_state = + ShmState::new::(dh, vec![wl_shm::Format::Xbgr8888, wl_shm::Format::Abgr8888]); + let cursor_shape_manager_state = CursorShapeManagerState::new::(dh); + let seat_state = SeatState::::new(); + let viewporter_state = ViewporterState::new::(dh); + let wl_drm_state = WlDrmState::>::default(); + let kde_decoration_state = KdeDecorationState::new::(dh, Mode::Client); + let xdg_decoration_state = XdgDecorationState::new::(dh); + let session_lock_manager_state = + SessionLockManagerState::new::(dh, client_not_sandboxed); + XWaylandKeyboardGrabState::new::(dh); + let xwayland_shell_state = XWaylandShellState::new::(dh); + PointerConstraintsState::new::(dh); + PointerWarpManager::new::(dh); + PointerGesturesState::new::(dh); + TabletManagerState::new::(dh); + SecurityContextState::new::(dh, client_has_no_security_context); + InputMethodManagerState::new::(dh, client_not_sandboxed); + TextInputManagerState::new::(dh); + VirtualKeyboardManagerState::new::(dh, client_not_sandboxed); + AlphaModifierState::new::(dh); + SinglePixelBufferState::new::(dh); + FixesState::new::(dh); + let keyboard_layout_state = KeyboardLayoutState::new::(dh, client_not_sandboxed); + + let background_effect_state = BackgroundEffectState::new::(dh); + + let idle_notifier_state = IdleNotifierState::::new(dh, handle.clone()); + let idle_inhibit_manager_state = IdleInhibitManagerState::new::(dh); + let idle_inhibiting_surfaces = HashSet::new(); + + let ext_data_control_state = ExtDataControlState::new::( + dh, + Some(&primary_selection_state), + client_not_sandboxed, + ); + let wlr_data_control_state = WlrDataControlState::new::( + dh, + Some(&primary_selection_state), + client_not_sandboxed, + ); + + let shell = Arc::new(parking_lot::RwLock::new(Shell::new(&config))); + + let layer_shell_state = + WlrLayerShellState::new_with_filter::(dh, client_not_sandboxed); + let xdg_shell_state = XdgShellState::new_with_capabilities::( + dh, + [ + WmCapabilities::Fullscreen, + WmCapabilities::Maximize, + WmCapabilities::Minimize, + WmCapabilities::WindowMenu, + ], + ); + let xdg_activation_state = XdgActivationState::new::(dh); + let xdg_foreign_state = XdgForeignState::new::(dh); + let toplevel_info_state = ToplevelInfoState::new(dh, client_not_sandboxed); + let toplevel_management_state = ToplevelManagementState::new::( + dh, + vec![ + ManagementCapabilities::Close, + ManagementCapabilities::Activate, + ManagementCapabilities::Maximize, + ManagementCapabilities::Minimize, + ManagementCapabilities::MoveToWorkspace, + ], + client_not_sandboxed, + ); + let workspace_state = WorkspaceState::new(dh, client_not_sandboxed); + + let a11y_state = A11yState::new::(dh, client_not_sandboxed); + + let dbus_state = DBusState::init(&handle); + + State { + common: Common { + config, + socket, + display_handle: dh.clone(), + event_loop_handle: handle, + event_loop_signal: signal, + + popups: PopupManager::default(), + shell, + + clock, + startup_done: Arc::new(AtomicBool::new(false)), + should_stop: false, + kiosk_exit_code: None, + gesture_state: None, + + kiosk_child: None, + theme: cosmic::theme::system_preference(), + + compositor_state, + corner_radius_state, + data_device_state, + dmabuf_state, + fractional_scale_state, + idle_notifier_state, + idle_inhibit_manager_state, + idle_inhibiting_surfaces, + cosmic_image_capture_source_state, + output_capture_source_state, + toplevel_capture_source_state, + image_copy_capture_state, + shm_state, + cursor_shape_manager_state, + seat_state, + session_lock_manager_state, + keyboard_shortcuts_inhibit_state, + output_state, + output_configuration_state, + output_power_state, + overlap_notify_state, + presentation_state, + primary_selection_state, + ext_data_control_state, + wlr_data_control_state, + viewporter_state, + wl_drm_state, + kde_decoration_state, + xdg_decoration_state, + xdg_shell_state, + layer_shell_state, + toplevel_info_state, + toplevel_management_state, + xdg_activation_state, + xdg_foreign_state, + workspace_state, + background_effect_state, + a11y_state, + xwayland_scale: None, + xwayland_state: None, + xwayland_shell_state, + pointer_focus_state: None, + dbus_state, + keyboard_layout_state, + + #[cfg(feature = "logind")] + inhibit_lid_fd: None, + + with_xwayland, + }, + backend: BackendData::Unset, + ready: Once::new(), + last_refresh: LastRefresh::None, + kiosk_command, + } + } + + pub fn new_client_state(&self) -> ClientState { + ClientState { + compositor_client_state: CompositorClientState::default(), + advertised_drm_node: match &self.backend { + BackendData::Kms(kms_state) => *kms_state.primary_node.read().unwrap(), + _ => None, + }, + evlh: self.common.event_loop_handle.clone(), + evls: self.common.event_loop_signal.clone(), + security_context: None, + } + } + + fn update_inhibitor_locks(&mut self) { + #[cfg(feature = "logind")] + { + use smithay::backend::session::Session; + use tracing::{debug, error, warn}; + + let outputs = self.backend.lock().all_outputs(); + let is_active = match &self.backend { + BackendData::Kms(kms) => kms.session.is_active(), + _ => true, + }; + + let should_handle_lid = + is_active && outputs.iter().any(|o| o.is_internal()) && outputs.len() >= 2; + + if should_handle_lid { + if self.common.inhibit_lid_fd.is_none() { + match crate::dbus::logind::inhibit_lid(&self.common) { + Ok(fd) => { + debug!("Inhibiting lid switch"); + self.common.inhibit_lid_fd = Some(fd); + + let backend = self.backend.lock(); + let output = backend + .all_outputs() + .iter() + .find(|o| o.is_internal()) + .cloned(); + let closed = + crate::dbus::logind::lid_closed(&self.common).unwrap_or(false); + + if closed { + backend.disable_internal_output( + &mut self.common.output_configuration_state, + ); + } else { + backend.enable_internal_output( + &mut self.common.output_configuration_state, + ); + } + std::mem::drop(backend); + + if let Err(err) = self.refresh_output_config() { + if !closed { + warn!(?err, "Failed to re-enable internal connector"); + if let Some(output) = output { + output.config_mut().enabled = OutputState::Disabled; + if let Err(err) = self.refresh_output_config() { + error!( + "Unrecoverable output configuration error: {}", + err + ); + } + } + } else { + // Disabling an output should never fail. + error!("Unrecoverable output configuration error: {}", err); + } + } + } + Err(err) => { + error!("Failed to inhibit lid switch: {}", err); + } + } + } + } else if let Some(_fd) = self.common.inhibit_lid_fd.take() { + debug!("Removing inhibitor-lock on lid switch"); + + let backend = self.backend.lock(); + let output = backend + .all_outputs() + .iter() + .find(|o| o.is_internal()) + .cloned(); + backend.enable_internal_output(&mut self.common.output_configuration_state); + std::mem::drop(backend); + + if let Err(err) = self.refresh_output_config() { + warn!(?err, "Failed to re-enable internal connector"); + if let Some(output) = output { + output.config_mut().enabled = OutputState::Disabled; + if let Err(err) = self.refresh_output_config() { + error!("Unrecoverable output configuration error: {}", err); + } + } + } + // drop _fd + } + } + } +} + +fn primary_scanout_output_compare<'a>( + current_output: &'a Output, + current_state: &RenderElementState, + next_output: &'a Output, + next_state: &RenderElementState, +) -> &'a Output { + if !crate::wayland::protocols::output_configuration::head_is_enabled(current_output) { + return next_output; + } + + default_primary_scanout_output_compare(current_output, current_state, next_output, next_state) +} + +impl Common { + #[profiling::function] + pub fn update_primary_output( + &self, + output: &Output, + render_element_states: &RenderElementStates, + ) { + let shell = self.shell.read(); + let processor = |namespace: Option| { + move |surface: &WlSurface, states: &SurfaceData| { + let primary_scanout_output = update_surface_primary_scanout_output( + surface, + output, + states, + namespace, + render_element_states, + primary_scanout_output_compare, + ); + if let Some(output) = primary_scanout_output { + with_fractional_scale(states, |fraction_scale| { + // The 1.0 clamp is a workaround for Chromium + // TODO: remove if Chromium ever gets fixed + fraction_scale.set_preferred_scale( + output.current_scale().fractional_scale().max(1.0), + ); + }); + } + } + }; + + // lock surface + if let Some(session_lock) = shell.session_lock.as_ref() + && let Some(lock_surface) = session_lock.surfaces.get(output) + { + with_surfaces_surface_tree(lock_surface.wl_surface(), processor(None)) + } + + for seat in shell + .seats + .iter() + .filter(|seat| &seat.active_output() == output) + { + let cursor_status = seat.cursor_image_status(); + + // cursor ... + if let CursorImageStatus::Surface(wl_surface) = cursor_status { + with_surfaces_surface_tree(&wl_surface, processor(None)); + } + + // grabs + if let Some(move_grab) = seat.user_data().get::() + && let Some(grab_state) = move_grab.lock().unwrap().as_ref() + { + for (window, _) in grab_state.element().windows() { + window.with_surfaces(processor(None)); + } + } + + if let Some(icon) = get_dnd_icon(seat) { + with_surfaces_surface_tree(&icon.surface, processor(None)); + } + } + + // sticky window + for set in shell.workspaces.sets.values() { + set.sticky_layer.mapped().for_each(|mapped| { + for (window, _) in mapped.windows() { + window.with_surfaces(processor(None)); + } + }); + } + + // normal windows + for space in shell.workspaces.spaces() { + if let Some(fs) = space.get_fullscreen(shell.seats.last_active()) { + fs.surface.with_surfaces(processor(None)); + } + space.mapped().for_each(|mapped| { + for (window, _) in mapped.windows() { + window.with_surfaces(processor(None)); + } + }); + space.minimized_windows.iter().for_each(|m| { + for window in m.windows() { + window.with_surfaces(processor(None)); + } + }) + } + + // OR windows + shell.override_redirect_windows.iter().for_each(|or| { + if let Some(wl_surface) = or.wl_surface() { + with_surfaces_surface_tree(&wl_surface, processor(None)); + } + }); + + // layer surfaces + for o in shell.outputs() { + let namespace = shell.workspaces.active_num(o).1; + let map = smithay::desktop::layer_map_for_output(o); + for layer_surface in map.layers() { + layer_surface.with_surfaces(processor(Some(namespace))); + } + } + } + + #[profiling::function] + pub fn send_dmabuf_feedback( + &self, + output: &Output, + render_element_states: &RenderElementStates, + mut dmabuf_feedback: impl FnMut(DrmNode) -> Option, + ) { + let shell = self.shell.read(); + + if let Some(session_lock) = shell.session_lock.as_ref() + && let Some(lock_surface) = session_lock.surfaces.get(output) + && let Some(feedback) = + advertised_node_for_surface(lock_surface.wl_surface(), &self.display_handle) + .and_then(&mut dmabuf_feedback) + { + send_dmabuf_feedback_surface_tree( + lock_surface.wl_surface(), + output, + surface_primary_scanout_output, + |surface, _| { + select_dmabuf_feedback( + surface, + render_element_states, + &feedback.render_feedback, + feedback + .primary_scanout_feedback + .as_ref() + .unwrap_or(&feedback.render_feedback), + ) + }, + ) + } + + for seat in shell + .seats + .iter() + .filter(|seat| &seat.active_output() == output) + { + let cursor_status = seat.cursor_image_status(); + + if let CursorImageStatus::Surface(wl_surface) = cursor_status + && let Some(feedback) = + advertised_node_for_surface(&wl_surface, &self.display_handle) + .and_then(&mut dmabuf_feedback) + { + send_dmabuf_feedback_surface_tree( + &wl_surface, + output, + surface_primary_scanout_output, + |surface, _| { + select_dmabuf_feedback( + surface, + render_element_states, + &feedback.render_feedback, + feedback + .overlay_scanout_feedback + .as_ref() + .unwrap_or(&feedback.render_feedback), + ) + }, + ); + } + + if let Some(icon) = get_dnd_icon(seat) + && let Some(feedback) = + advertised_node_for_surface(&icon.surface, &self.display_handle) + .and_then(&mut dmabuf_feedback) + { + send_dmabuf_feedback_surface_tree( + &icon.surface, + output, + surface_primary_scanout_output, + |surface, _| { + select_dmabuf_feedback( + surface, + render_element_states, + &feedback.render_feedback, + feedback + .overlay_scanout_feedback + .as_ref() + .unwrap_or(&feedback.render_feedback), + ) + }, + ); + } + + if let Some(move_grab) = seat.user_data().get::() + && let Some(grab_state) = move_grab.lock().unwrap().as_ref() + { + for (window, _) in grab_state.element().windows() { + if let Some(feedback) = window + .wl_surface() + .and_then(|wl_surface| { + advertised_node_for_surface(&wl_surface, &self.display_handle) + }) + .and_then(&mut dmabuf_feedback) + { + window.send_dmabuf_feedback( + output, + &feedback, + render_element_states, + surface_primary_scanout_output, + ); + } + } + } + } + + shell + .workspaces + .sets + .get(output) + .unwrap() + .sticky_layer + .mapped() + .for_each(|mapped| { + for (window, _) in mapped.windows() { + if let Some(feedback) = window + .wl_surface() + .and_then(|wl_surface| { + advertised_node_for_surface(&wl_surface, &self.display_handle) + }) + .and_then(&mut dmabuf_feedback) + { + window.send_dmabuf_feedback( + output, + &feedback, + render_element_states, + surface_primary_scanout_output, + ); + } + } + }); + + if let Some(active) = shell.active_space(output) { + if let Some(fs) = active.get_fullscreen(shell.seats.last_active()) + && let Some(feedback) = fs + .surface + .wl_surface() + .and_then(|wl_surface| { + advertised_node_for_surface(&wl_surface, &self.display_handle) + }) + .and_then(&mut dmabuf_feedback) + { + fs.surface.send_dmabuf_feedback( + output, + &feedback, + render_element_states, + surface_primary_scanout_output, + ); + } + active.mapped().for_each(|mapped| { + for (window, _) in mapped.windows() { + if let Some(feedback) = window + .wl_surface() + .and_then(|wl_surface| { + advertised_node_for_surface(&wl_surface, &self.display_handle) + }) + .and_then(&mut dmabuf_feedback) + { + window.send_dmabuf_feedback( + output, + &feedback, + render_element_states, + surface_primary_scanout_output, + ); + } + } + }); + } + + shell.override_redirect_windows.iter().for_each(|or| { + if let Some(wl_surface) = or.wl_surface() + && let Some(feedback) = + advertised_node_for_surface(&wl_surface, &self.display_handle) + .and_then(&mut dmabuf_feedback) + { + send_dmabuf_feedback_surface_tree( + &wl_surface, + output, + surface_primary_scanout_output, + |surface, _| { + select_dmabuf_feedback( + surface, + render_element_states, + &feedback.render_feedback, + feedback + .overlay_scanout_feedback + .as_ref() + .unwrap_or(&feedback.render_feedback), + ) + }, + ) + } + }); + + let map = smithay::desktop::layer_map_for_output(output); + for layer_surface in map.layers() { + if let Some(feedback) = + advertised_node_for_surface(layer_surface.wl_surface(), &self.display_handle) + .and_then(&mut dmabuf_feedback) + { + layer_surface.send_dmabuf_feedback( + output, + surface_primary_scanout_output, + |surface, _| { + select_dmabuf_feedback( + surface, + render_element_states, + &feedback.render_feedback, + feedback + .overlay_scanout_feedback + .as_ref() + .unwrap_or(&feedback.render_feedback), + ) + }, + ); + } + } + } + + #[profiling::function] + pub fn send_frames(&self, output: &Output, sequence: Option) { + let time = self.clock.now(); + let should_send = |surface: &WlSurface, states: &SurfaceData| { + // Do the standard primary scanout output check. For pointer surfaces it deduplicates + // the frame callbacks across potentially multiple outputs, and for regular windows and + // layer-shell surfaces it avoids sending frame callbacks to invisible surfaces. + let current_primary_output = surface_primary_scanout_output(surface, states); + if current_primary_output.as_ref() != Some(output) { + return None; + } + + let Some(sequence) = sequence else { + return Some(output.clone()); + }; + + // Next, check the throttling status. + let frame_throttling_state = states + .data_map + .get_or_insert(SurfaceFrameThrottlingState::default); + let mut last_sent_at = frame_throttling_state.last_sent_at.borrow_mut(); + + let mut send = true; + + // If we already sent a frame callback to this surface this output refresh + // cycle, don't send one again to prevent empty-damage commit busy loops. + if let Some((last_output, last_sequence)) = &*last_sent_at + && last_output == output + && *last_sequence == sequence + { + send = false; + } + + if send { + *last_sent_at = Some((output.downgrade(), sequence)); + Some(output.clone()) + } else { + None + } + }; + const THROTTLE: Option = Some(Duration::from_millis(995)); + const SCREENCOPY_THROTTLE: Option = Some(Duration::from_nanos(16_666_666)); + + fn throttle(session_holder: &impl SessionHolder) -> Option { + if session_holder.sessions().is_empty() && session_holder.cursor_sessions().is_empty() { + THROTTLE + } else { + SCREENCOPY_THROTTLE + } + } + + let shell = self.shell.read(); + + if let Some(session_lock) = shell.session_lock.as_ref() + && let Some(lock_surface) = session_lock.surfaces.get(output) + { + send_frames_surface_tree(lock_surface.wl_surface(), output, time, None, should_send); + } + + for seat in shell + .seats + .iter() + .filter(|seat| &seat.active_output() == output) + { + let cursor_status = seat.cursor_image_status(); + + if let CursorImageStatus::Surface(wl_surface) = cursor_status { + send_frames_surface_tree( + &wl_surface, + output, + time, + Some(Duration::ZERO), + should_send, + ) + } + + if let Some(move_grab) = seat.user_data().get::() + && let Some(grab_state) = move_grab.lock().unwrap().as_ref() + { + for (window, _) in grab_state.element().windows() { + window.send_frame(output, time, throttle(&window), should_send); + } + } + + if let Some(icon) = get_dnd_icon(seat) { + send_frames_surface_tree( + &icon.surface, + output, + time, + Some(Duration::ZERO), + should_send, + ) + } + } + + shell + .workspaces + .sets + .get(output) + .unwrap() + .sticky_layer + .mapped() + .for_each(|mapped| { + for (window, _) in mapped.windows() { + window.send_frame(output, time, throttle(&window), should_send); + } + }); + + if let Some(active) = shell.active_space(output) { + if let Some(fs) = active.get_fullscreen(shell.seats.last_active()) { + fs.surface + .send_frame(output, time, throttle(&fs.surface), should_send); + } + active.mapped().for_each(|mapped| { + for (window, _) in mapped.windows() { + window.send_frame(output, time, throttle(&window), should_send); + } + }); + + // other (throttled) windows + active.minimized_windows.iter().for_each(|m| { + for window in m.windows() { + window.send_frame(output, time, throttle(&window), |_, _| None); + } + }); + + for space in shell + .workspaces + .spaces_for_output(output) + .filter(|w| w.handle != active.handle) + { + if let Some(fs) = space.get_fullscreen(shell.seats.last_active()) { + let throttle = min(throttle(space), throttle(&fs.surface)); + fs.surface.send_frame(output, time, throttle, |_, _| None); + } + space.mapped().for_each(|mapped| { + for (window, _) in mapped.windows() { + let throttle = min(throttle(space), throttle(&window)); + window.send_frame(output, time, throttle, |_, _| None); + } + }); + space.minimized_windows.iter().for_each(|m| { + for window in m.windows() { + window.send_frame(output, time, throttle(&window), |_, _| None); + } + }) + } + } + + shell.override_redirect_windows.iter().for_each(|or| { + if let Some(wl_surface) = or.wl_surface() { + send_frames_surface_tree(&wl_surface, output, time, THROTTLE, should_send); + } + }); + + let map = smithay::desktop::layer_map_for_output(output); + for layer_surface in map.layers() { + layer_surface.send_frame(output, time, THROTTLE, should_send); + } + } +} diff --git a/src/systemd.rs b/src/systemd.rs new file mode 100644 index 0000000..36720d9 --- /dev/null +++ b/src/systemd.rs @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::state::Common; +use libsystemd::daemon::{NotifyState, booted, notify}; +use std::process::Command; +use tracing::{error, warn}; + +pub fn ready(common: &Common) { + if booted() { + match Command::new("systemctl") + .args(["--user", "import-environment", "WAYLAND_DISPLAY", "DISPLAY"]) + .env("WAYLAND_DISPLAY", &common.socket) + .env( + "DISPLAY", + common + .xwayland_state + .as_ref() + .map(|s| format!(":{}", s.display)) + .unwrap_or_default(), + ) + .status() + { + Ok(x) if x.success() => {} + Ok(x) => warn!( + exit_code = ?x.code(), + "Failed to import WAYLAND_DISPLAY/DISPLAY into systemd", + ), + Err(err) => error!(?err, "Failed to run systemctl although booted with systemd",), + }; + } + + if let Err(err) = notify(false, &[NotifyState::Ready]) { + error!(?err, "Failed to notify systemd"); + } +} diff --git a/src/theme.rs b/src/theme.rs new file mode 100644 index 0000000..d89d89f --- /dev/null +++ b/src/theme.rs @@ -0,0 +1,63 @@ +// insert into the event loop, a watcher for the theme & theme mode for changes + +// update a Arc> in the state on change of the theme and mark all interfaces for a redraw. + +use calloop::LoopHandle; +use cosmic::cosmic_theme::{Theme, ThemeMode, palette}; + +use crate::state::State; + +pub(crate) fn _group_color(theme: &Theme) -> [f32; 3] { + let neutral_8 = theme.palette.neutral_8; + [neutral_8.red, neutral_8.green, neutral_8.blue] +} + +pub(crate) fn active_window_hint(theme: &Theme) -> palette::Srgba { + if let Some(hint) = theme.window_hint { + palette::Srgba::from(hint) + } else { + theme.accent_color() + } +} + +pub fn watch_theme(handle: LoopHandle<'_, State>) -> Result<(), cosmic_config::Error> { + let (ping_tx, ping_rx) = calloop::ping::make_ping().unwrap(); + let config_mode_helper = ThemeMode::config()?; + let config_dark_helper = Theme::dark_config()?; + let config_light_helper = Theme::light_config()?; + + if let Err(e) = handle.insert_source(ping_rx, move |_, _, state| { + let new_theme = cosmic::theme::system_preference(); + let theme = &mut state.common.theme; + + if theme.theme_type != new_theme.theme_type { + *theme = new_theme; + let mut workspace_guard = state.common.workspace_state.update(); + state.common.shell.write().set_theme( + theme.clone(), + &state.common.xdg_activation_state, + &mut workspace_guard, + ); + } + }) { + tracing::error!("{e}"); + }; + + let ping_tx_clone = ping_tx.clone(); + let theme_watcher_mode = config_mode_helper.watch(move |_, _keys| { + ping_tx_clone.ping(); + })?; + let ping_tx_clone = ping_tx.clone(); + let theme_watcher_light = config_light_helper.watch(move |_, _keys| { + ping_tx_clone.ping(); + })?; + let theme_watcher_dark = config_dark_helper.watch(move |_, _keys| { + ping_tx.ping(); + })?; + + std::mem::forget(theme_watcher_dark); + std::mem::forget(theme_watcher_light); + std::mem::forget(theme_watcher_mode); + + Ok(()) +} diff --git a/src/utils/env.rs b/src/utils/env.rs new file mode 100644 index 0000000..87afc12 --- /dev/null +++ b/src/utils/env.rs @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use smithay::backend::drm::{DrmNode, NodeType}; +use tracing::{info, warn}; + +pub fn bool_var(name: &str) -> Option { + let value = std::env::var(name).ok()?.to_lowercase(); + Some(["1", "true", "yes", "y"].contains(&value.as_str())) +} + +#[derive(Debug, Clone, Copy)] +pub enum DeviceIdentifier { + Id { vendor: u32, device: u32 }, + Node(DrmNode), +} + +impl DeviceIdentifier { + pub fn matches(&self, dev_node: &DrmNode) -> bool { + match self { + DeviceIdentifier::Node(id_node) => id_node == dev_node, + DeviceIdentifier::Id { vendor, device } => { + let (major, minor) = (dev_node.major(), dev_node.minor()); + let Some(dev_vendor) = std::fs::read_to_string(format!( + "/sys/dev/char/{}:{}/device/vendor", + major, minor + )) + .ok() + .and_then(|ven| u32::from_str_radix(ven[2..].trim(), 16).ok()) else { + return false; + }; + let Some(dev_device) = std::fs::read_to_string(format!( + "/sys/dev/char/{}:{}/device/device", + major, minor + )) + .ok() + .and_then(|dev| u32::from_str_radix(dev[2..].trim(), 16).ok()) else { + return false; + }; + info!( + "{:x}:{:x} == {:x}:{:x}", + *vendor, *device, dev_vendor, dev_device + ); + dev_vendor == *vendor && dev_device == *device + } + } + } +} + +pub fn dev_var(name: &str) -> Option { + let value = std::env::var(name).ok()?; + try_parse_dev_from_str(&value) +} + +pub fn dev_list_var(name: &str) -> Option> { + let value = std::env::var(name).ok()?; + Some(value.split(',').flat_map(try_parse_dev_from_str).collect()) +} + +fn try_parse_dev_from_str(val: &str) -> Option { + let val = val.trim(); + if val.starts_with("0x") && val.contains(':') { + let (vendor, device) = val.split_once(':').unwrap(); + if !device.starts_with("0x") { + warn!( + "Failed to parse device entry {}, device id doesn't start with '0x'. Skipping", + val + ); + return None; + } + let vendor = u32::from_str_radix(&vendor[2..], 16) + .inspect_err(|err| { + warn!( + "Failed to parse device entry {}, vendor_id is no hex integer: {}. Skipping", + val, err + ); + }) + .ok()?; + let device = u32::from_str_radix(&device[2..], 16) + .inspect_err(|err| { + warn!( + "Failed to parse device entry {}, device_id is no hex integer: {}. Skipping", + val, err + ); + }) + .ok()?; + Some(DeviceIdentifier::Id { vendor, device }) + } else if val.starts_with("pci-") { + let path = std::fs::read_link(format!("/dev/dri/by-path/{}-render", val)) + .inspect_err(|err| { + warn!( + "Failed to parse device entry {}, no known pci path: {}. Skipping", + val, err + ); + }) + .ok()?; + let node = DrmNode::from_path(&path) + .inspect_err(|err| { + warn!( + "Failed to parse device entry {}, failed to get node from path {}: {}", + val, + path.display(), + err + ) + }) + .ok()?; + + let node = node + .node_with_type(NodeType::Render) + .and_then(|res| res.ok()) + .unwrap_or(node); + Some(DeviceIdentifier::Node(node)) + } else if val.contains(':') { + let (major, minor) = val.split_once(':').unwrap(); + let major = str::parse::(major) + .inspect_err(|err| { + warn!( + "Failed to parse device entry {}, major is no integer: {}. Skipping", + val, err + ) + }) + .ok()?; + let minor = str::parse::(minor) + .inspect_err(|err| { + warn!( + "Failed to parse device entry {}, minor is no integer: {}. Skipping", + val, err + ) + }) + .ok()?; + let dev = rustix::fs::makedev(major, minor); + let node = DrmNode::from_dev_id(dev) + .inspect_err(|err| { + warn!( + "Failed to parse device entry {}, failed to get node from dev_t {}: {}", + val, dev, err + ); + }) + .ok()?; + + let node = node + .node_with_type(NodeType::Render) + .and_then(|res| res.ok()) + .unwrap_or(node); + Some(DeviceIdentifier::Node(node)) + } else { + // try to parse as device path + + let path = format!("/dev/dri/{}", val); + let node = DrmNode::from_path(&path) + .inspect_err(|err| { + warn!( + "Failed to parse device entry {}, failed to get node from path {}: {}", + val, path, err + ); + }) + .ok()?; + + let node = node + .node_with_type(NodeType::Render) + .and_then(|res| res.ok()) + .unwrap_or(node); + Some(DeviceIdentifier::Node(node)) + } +} diff --git a/src/utils/geometry.rs b/src/utils/geometry.rs new file mode 100644 index 0000000..c86427e --- /dev/null +++ b/src/utils/geometry.rs @@ -0,0 +1,174 @@ +#![allow(clippy::wrong_self_convention)] + +use std::sync::Mutex; + +use smithay::{ + output::Output, + utils::{Coordinate, Logical, Point, Rectangle, Size}, +}; + +use crate::shell::zoom::OutputZoomState; + +use super::prelude::OutputExt; + +/// Marker type for coordinates in global space +#[derive(Debug)] +pub struct Global; + +/// Marker type for coordinates in workspace local space +#[derive(Debug)] +pub struct Local; + +pub trait PointExt { + fn as_global(self) -> Point; + fn as_local(self) -> Point; +} + +pub trait PointGlobalExt { + fn to_local(self, output: &Output) -> Point; + fn to_zoomed(self, output: &Output) -> Point; + fn as_logical(self) -> Point; +} + +pub trait PointLocalExt { + fn to_global(self, output: &Output) -> Point; + fn as_logical(self) -> Point; +} + +pub trait SizeExt { + fn as_logical(self) -> Size; + fn as_local(self) -> Size; + fn as_global(self) -> Size; +} + +pub trait RectExt { + fn as_global(self) -> Rectangle; + fn as_local(self) -> Rectangle; +} + +pub trait RectGlobalExt { + fn to_local(self, output: &Output) -> Rectangle; + fn as_logical(self) -> Rectangle; +} + +pub trait RectLocalExt { + fn to_global(self, output: &Output) -> Rectangle; + fn as_logical(self) -> Rectangle; +} + +impl PointExt for Point { + fn as_global(self) -> Point { + (self.x, self.y).into() + } + + fn as_local(self) -> Point { + (self.x, self.y).into() + } +} + +impl PointGlobalExt for Point { + fn to_local(self, output: &Output) -> Point { + let point = (self.to_f64() - output.geometry().loc.to_f64()).as_logical(); + (C::from_f64(point.x), C::from_f64(point.y)).into() + } + + fn to_zoomed(self, output: &Output) -> Point { + let zoomed_output_geometry = output.zoomed_geometry().unwrap(); + let level = output + .user_data() + .get::>() + .unwrap() + .lock() + .unwrap() + .current_level(); + let point = (self.to_f64() - zoomed_output_geometry.loc.to_f64()) + .upscale(level) + .as_logical(); + (C::from_f64(point.x), C::from_f64(point.y)).into() + } + + fn as_logical(self) -> Point { + (self.x, self.y).into() + } +} + +impl PointLocalExt for Point { + fn to_global(self, output: &Output) -> Point { + let point = + (self.to_f64().as_logical() + output.geometry().loc.to_f64().as_logical()).as_global(); + (C::from_f64(point.x), C::from_f64(point.y)).into() + } + + fn as_logical(self) -> Point { + (self.x, self.y).into() + } +} + +impl SizeExt for Size { + fn as_logical(self) -> Size { + (self.w, self.h).into() + } + fn as_global(self) -> Size { + self + } + fn as_local(self) -> Size { + (self.w, self.h).into() + } +} + +impl SizeExt for Size { + fn as_logical(self) -> Size { + (self.w, self.h).into() + } + fn as_global(self) -> Size { + (self.w, self.h).into() + } + fn as_local(self) -> Size { + self + } +} + +impl SizeExt for Size { + fn as_logical(self) -> Size { + self + } + fn as_global(self) -> Size { + (self.w, self.h).into() + } + fn as_local(self) -> Size { + (self.w, self.h).into() + } +} + +impl RectExt for Rectangle { + fn as_global(self) -> Rectangle { + Rectangle::new(self.loc.as_global(), (self.size.w, self.size.h).into()) + } + + fn as_local(self) -> Rectangle { + Rectangle::new(self.loc.as_local(), (self.size.w, self.size.h).into()) + } +} + +impl RectGlobalExt for Rectangle { + fn to_local(self, output: &Output) -> Rectangle { + Rectangle::new(self.loc.to_local(output), (self.size.w, self.size.h).into()) + } + + fn as_logical(self) -> Rectangle { + Rectangle::new(self.loc.as_logical(), self.size.as_logical()) + } +} + +impl RectLocalExt for Rectangle { + fn to_global(self, output: &Output) -> Rectangle { + Rectangle::new( + self.loc.to_global(output), + (self.size.w, self.size.h).into(), + ) + } + + fn as_logical(self) -> Rectangle { + Rectangle::new(self.loc.as_logical(), self.size.as_logical()) + } +} diff --git a/src/utils/iced/mod.rs b/src/utils/iced/mod.rs new file mode 100644 index 0000000..2bfe888 --- /dev/null +++ b/src/utils/iced/mod.rs @@ -0,0 +1,1118 @@ +pub mod state; + +use std::{ + collections::{HashMap, HashSet}, + fmt, + hash::{Hash, Hasher}, + sync::{Arc, LazyLock, Mutex, mpsc::Receiver}, +}; + +use cosmic::{ + Theme, + iced::{ + Limits, Point as IcedPoint, Size as IcedSize, Task, + advanced::{graphics::text::font_system, widget::Tree}, + core::{Color, Length, Pixels, clipboard::Null as NullClipboard, id::Id, renderer::Style}, + event::Event, + futures::{FutureExt, StreamExt}, + keyboard::{Event as KeyboardEvent, Modifiers as IcedModifiers}, + mouse::{Button as MouseButton, Cursor, Event as MouseEvent, ScrollDelta}, + runtime::{Action, task::into_stream}, + touch::{Event as TouchEvent, Finger}, + window::Event as WindowEvent, + }, +}; +use iced_tiny_skia::{ + Layer, + graphics::{Viewport, damage}, +}; + +use ordered_float::OrderedFloat; +use smithay::{ + backend::{ + allocator::Fourcc, + input::{ButtonState, KeyState}, + renderer::{ + ImportMem, + element::{ + Kind, + memory::{MemoryRenderBuffer, MemoryRenderBufferRenderElement}, + }, + }, + }, + desktop::space::{RenderZindex, SpaceElement}, + input::{ + Seat, + keyboard::{KeyboardTarget, KeysymHandle, ModifiersState}, + pointer::{ + AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, + GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, + GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, MotionEvent, + PointerTarget, RelativeMotionEvent, + }, + touch::{ + DownEvent, FrameMarker, MotionEvent as TouchMotionEvent, OrientationEvent, ShapeEvent, + TouchTarget, UpEvent, + }, + }, + output::Output, + reexports::calloop::{self, LoopHandle, RegistrationToken, futures::Scheduler}, + render_elements, + utils::{ + Buffer as BufferCoords, IsAlive, Logical, Physical, Point, Rectangle, Scale, Serial, Size, + Transform, + }, +}; + +use crate::{ + backend::render::{ + element::AsGlowRenderer, + wayland::blur_effect::{BlurElement, BlurState}, + }, + utils::iced::state::State, +}; + +static ID: LazyLock = LazyLock::new(|| Id::new("Program")); + +pub struct IcedElement(pub(crate) Arc>>); + +impl fmt::Debug for IcedElement

{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.0, f) + } +} + +// SAFETY: It is not, we need to make sure we never move this into another thread and drop it there, +// as on drop it could trigger RefCells in calloop. +unsafe impl Send for IcedElementInternal

{} + +impl Clone for IcedElement

{ + fn clone(&self) -> Self { + IcedElement(self.0.clone()) + } +} + +impl PartialEq for IcedElement

{ + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} +impl Eq for IcedElement

{} + +impl Hash for IcedElement

{ + fn hash(&self, state: &mut H) { + (Arc::as_ptr(&self.0) as usize).hash(state) + } +} + +// TODO remove this and use our Program trait directly instead +pub trait IcedProgram { + type Message: std::fmt::Debug + Send; + fn update(&mut self, _message: Self::Message) -> Task { + Task::none() + } + fn view(&self) -> cosmic::Element<'_, Self::Message>; + + fn background_color(&self) -> Color { + Color::TRANSPARENT + } + + fn foreground( + &self, + _pixels: &mut tiny_skia::PixmapMut<'_>, + _damage: &[Rectangle], + _scale: f32, + ) { + } +} + +pub trait Program { + type Message: std::fmt::Debug + Send; + fn update( + &mut self, + message: Self::Message, + loop_handle: &LoopHandle<'static, crate::state::State>, + last_seat: Option<&(Seat, Serial)>, + ) -> Task { + let _ = (message, loop_handle, last_seat); + Task::none() + } + fn view(&self) -> cosmic::Element<'_, Self::Message>; + + fn background_color(&self, _theme: &cosmic::Theme) -> Color { + Color::TRANSPARENT + } + + fn foreground( + &self, + pixels: &mut tiny_skia::PixmapMut<'_>, + damage: &[Rectangle], + scale: f32, + theme: &cosmic::Theme, + ) { + let _ = (pixels, damage, scale, theme); + } +} + +// TODO remove this and use our Program trait directly +struct ProgramWrapper { + program: P, + evlh: LoopHandle<'static, crate::state::State>, + last_seat: Arc, Serial)>>>, +} + +impl IcedProgram for ProgramWrapper

{ + type Message =

::Message; + + fn update(&mut self, message: Self::Message) -> Task { + let last_seat = self.last_seat.lock().unwrap(); + self.program.update(message, &self.evlh, last_seat.as_ref()) + } + + fn view(&self) -> cosmic::Element<'_, Self::Message> { + self.program.view() + } +} + +pub(crate) struct IcedElementInternal { + // draw buffer + additional_scale: f64, + outputs: HashSet, + buffers: HashMap, (MemoryRenderBuffer, Option<(Vec, Color)>)>, + pending_realloc: bool, + blur: BlurState, + + // state + size: Size, + last_seat: Arc, Serial)>>>, + cursor_pos: Option>, + touch_map: HashMap, + last_touch_frame: Option, + last_touch_serial: Option, + + // iced + theme: Theme, + renderer: cosmic::Renderer, + state: State>, + + // futures + handle: LoopHandle<'static, crate::state::State>, + scheduler: Scheduler::Message>>, + executor_token: Option, + rx: Receiver::Message>>, +} + +impl Clone for IcedElementInternal

{ + fn clone(&self) -> Self { + let handle = self.handle.clone(); + let (executor, scheduler) = calloop::futures::executor().expect("Out of file descriptors"); + let (tx, rx) = std::sync::mpsc::channel(); + let executor_token = handle + .insert_source(executor, move |message, _, _| { + let _ = tx.send(message); + }) + .ok(); + + if !self.state.is_queue_empty() { + tracing::warn!("Missing force_update call"); + } + let mut renderer = cosmic::Renderer::new(cosmic::font::default(), Pixels(16.0)); + let state = State::new( + ID.clone(), + ProgramWrapper { + program: self.state.program().program.clone(), + evlh: handle.clone(), + last_seat: self.last_seat.clone(), + }, + IcedSize::new(self.size.w as f32, self.size.h as f32), + &mut renderer, + ); + + IcedElementInternal { + additional_scale: self.additional_scale, + outputs: self.outputs.clone(), + buffers: self.buffers.clone(), + pending_realloc: self.pending_realloc, + blur: BlurState::default(), + size: self.size, + last_seat: self.last_seat.clone(), + cursor_pos: self.cursor_pos, + touch_map: self.touch_map.clone(), + last_touch_frame: None, + last_touch_serial: None, + theme: self.theme.clone(), + renderer, + state, + handle, + scheduler, + executor_token, + rx, + } + } +} + +impl fmt::Debug for IcedElementInternal

{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("IcedElementInternal") + .field("additional_scale", &self.additional_scale) + .field( + "outputs", + &self.outputs.iter().map(|o| o.name()).collect::>(), + ) + .field("buffers", &"...") + .field("size", &self.size) + .field("pending_realloc", &self.pending_realloc) + .field("last_seat", &self.last_seat) + .field("cursor_pos", &self.cursor_pos) + .field("touch_map", &self.touch_map) + .field("last_touch_frame", &self.last_touch_frame) + .field("last_touch_serial", &self.last_touch_serial) + .field("theme", &"...") + .field("renderer", &"...") + .field("state", &"...") + .field("handle", &self.handle) + .field("scheduler", &self.scheduler) + .field("executor_token", &self.executor_token) + .field("rx", &self.rx) + .finish() + } +} + +impl Drop for IcedElementInternal

{ + fn drop(&mut self) { + self.handle.remove(self.executor_token.take().unwrap()); + } +} + +impl IcedElement

{ + pub fn new( + program: P, + size: impl Into>, + handle: LoopHandle<'static, crate::state::State>, + theme: cosmic::Theme, + ) -> IcedElement

{ + let size = size.into(); + let last_seat = Arc::new(Mutex::new(None)); + let mut renderer = cosmic::Renderer::new(cosmic::font::default(), Pixels(16.0)); + + let state = State::new( + ID.clone(), + ProgramWrapper { + program, + evlh: handle.clone(), + last_seat: last_seat.clone(), + }, + IcedSize::new(size.w as f32, size.h as f32), + &mut renderer, + ); + + let (executor, scheduler) = calloop::futures::executor().expect("Out of file descriptors"); + let (tx, rx) = std::sync::mpsc::channel(); + let executor_token = handle + .insert_source(executor, move |message, _, _| { + let _ = tx.send(message); + }) + .ok(); + + let mut internal = IcedElementInternal { + additional_scale: 1.0, + outputs: HashSet::new(), + buffers: HashMap::new(), + pending_realloc: false, + blur: BlurState::default(), + size, + cursor_pos: None, + last_seat, + touch_map: HashMap::new(), + last_touch_frame: None, + last_touch_serial: None, + theme, + renderer, + state, + handle, + scheduler, + executor_token, + rx, + }; + internal.update(true); + + IcedElement(Arc::new(Mutex::new(internal))) + } + + pub fn with_program(&self, func: impl FnOnce(&P) -> R) -> R { + let internal = self.0.lock().unwrap(); + func(&internal.state.program().program) + } + + pub fn minimum_size(&self) -> Size { + let internal = self.0.lock().unwrap(); + let mut element = internal.state.program().program.view(); + + let mut tree = Tree::new(element.as_widget()); + let node = element + .as_widget_mut() + .layout( + &mut tree, + &internal.renderer, + &Limits::new(IcedSize::ZERO, IcedSize::INFINITE) + .width(Length::Shrink) + .height(Length::Shrink), + ) + .size(); + Size::from((node.width.ceil() as i32, node.height.ceil() as i32)) + } + + pub fn loop_handle(&self) -> LoopHandle<'static, crate::state::State> { + self.0.lock().unwrap().handle.clone() + } + + pub fn resize(&self, size: Size) { + let mut internal = self.0.lock().unwrap(); + let internal_ref = &mut *internal; + if internal_ref.size == size { + return; + } + + internal_ref.size = size; + internal_ref.pending_realloc = true; + internal_ref.update(true); + } + + pub fn set_additional_scale(&self, scale: f64) { + { + let mut internal = self.0.lock().unwrap(); + let internal_ref = &mut *internal; + if internal_ref.additional_scale == scale { + return; + } + + internal_ref.additional_scale = scale; + } + self.refresh(); + } + + pub fn force_update(&self) { + self.0.lock().unwrap().update(true); + } + + pub fn with_theme(&self, f: impl FnOnce(&Theme) -> R) -> R { + let guard = self.0.lock().unwrap(); + f(&guard.theme) + } + + pub fn set_theme(&self, theme: cosmic::Theme) { + let mut guard = self.0.lock().unwrap(); + guard.theme = theme.clone(); + } + + pub fn force_redraw(&self) { + let mut internal = self.0.lock().unwrap(); + for (_buffer, old_primitives) in internal.buffers.values_mut() { + *old_primitives = None; + } + } + + pub fn current_size(&self) -> Size { + let internal = self.0.lock().unwrap(); + internal + .size + .to_f64() + .upscale(internal.additional_scale) + .to_i32_round() + } + + pub fn queue_message(&self, msg: P::Message) { + self.0.lock().unwrap().state.queue_message(msg); + } +} + +impl IcedElement

{ + pub fn deep_clone(&self) -> Self { + let internal = self.0.lock().unwrap(); + if !internal.state.is_queue_empty() { + self.force_update(); + } + IcedElement(Arc::new(Mutex::new(internal.clone()))) + } +} + +impl IcedElementInternal

{ + #[profiling::function] + fn update(&mut self, force: bool) { + while let Ok(Some(message)) = self.rx.try_recv() { + self.state.queue_message(message); + } + + if self.state.is_queue_empty() && !force { + return; + } + + let cursor = self + .cursor_pos + .map(|p| IcedPoint::new(p.x as f32, p.y as f32)) + .map(Cursor::Available) + .unwrap_or(Cursor::Unavailable); + + let actions = self + .state + .update( + ID.clone(), + IcedSize::new(self.size.w as f32, self.size.h as f32), + cursor, + &mut self.renderer, + &self.theme, + &Style { + scale_factor: 1.0, // TODO: why is this + icon_color: self.theme.cosmic().on_bg_color().into(), + text_color: self.theme.cosmic().on_bg_color().into(), + }, + &mut NullClipboard, + ) + .1; + + if let Some(action) = actions + && let Some(t) = into_stream(action) + { + let _ = self.scheduler.schedule(t.into_future().map(|f| match f.0 { + Some(Action::Output(msg)) => Some(msg), + _ => None, + })); + } + } +} + +impl PointerTarget for IcedElement

{ + fn enter( + &self, + seat: &Seat, + _data: &mut crate::state::State, + event: &MotionEvent, + ) { + let mut internal = self.0.lock().unwrap(); + internal + .state + .queue_event(Event::Mouse(MouseEvent::CursorEntered)); + let event_location = event.location.downscale(internal.additional_scale); + let position = IcedPoint::new(event_location.x as f32, event_location.y as f32); + internal + .state + .queue_event(Event::Mouse(MouseEvent::CursorMoved { position })); + // TODO: Update iced widgets to handle touch using event position, not cursor_pos + internal.cursor_pos = Some(event_location); + *internal.last_seat.lock().unwrap() = Some((seat.clone(), event.serial)); + internal.update(false); + } + + fn motion( + &self, + seat: &Seat, + _data: &mut crate::state::State, + event: &MotionEvent, + ) { + let mut internal = self.0.lock().unwrap(); + let event_location = event.location.downscale(internal.additional_scale); + let position = IcedPoint::new(event_location.x as f32, event_location.y as f32); + internal + .state + .queue_event(Event::Mouse(MouseEvent::CursorMoved { position })); + internal.cursor_pos = Some(event_location); + *internal.last_seat.lock().unwrap() = Some((seat.clone(), event.serial)); + internal.update(false); + } + + fn relative_motion( + &self, + _seat: &Seat, + _data: &mut crate::state::State, + _event: &RelativeMotionEvent, + ) { + } + + fn button( + &self, + seat: &Seat, + _data: &mut crate::state::State, + event: &ButtonEvent, + ) { + let mut internal = self.0.lock().unwrap(); + let button = match event.button { + 0x110 => MouseButton::Left, + 0x111 => MouseButton::Right, + 0x112 => MouseButton::Middle, + x => MouseButton::Other(x as u16), + }; + internal.state.queue_event(Event::Mouse(match event.state { + ButtonState::Pressed => MouseEvent::ButtonPressed(button), + ButtonState::Released => MouseEvent::ButtonReleased(button), + })); + *internal.last_seat.lock().unwrap() = Some((seat.clone(), event.serial)); + internal.update(false); + } + + fn axis( + &self, + _seat: &Seat, + _data: &mut crate::state::State, + frame: AxisFrame, + ) { + let mut internal = self.0.lock().unwrap(); + internal + .state + .queue_event(Event::Mouse(MouseEvent::WheelScrolled { + delta: if let Some(discrete) = frame.v120 { + ScrollDelta::Lines { + x: discrete.0 as f32 / 120., + y: discrete.1 as f32 / 120., + } + } else { + ScrollDelta::Pixels { + x: frame.axis.0 as f32, + y: frame.axis.1 as f32, + } + }, + })); + internal.update(false); + } + + fn frame(&self, _seat: &Seat, _data: &mut crate::state::State) {} + + fn leave( + &self, + _seat: &Seat, + _data: &mut crate::state::State, + _serial: Serial, + _time: u32, + ) { + let mut internal = self.0.lock().unwrap(); + internal + .state + .queue_event(Event::Mouse(MouseEvent::CursorLeft)); + internal.update(false); + } + + fn gesture_swipe_begin( + &self, + _: &Seat, + _: &mut crate::state::State, + _: &GestureSwipeBeginEvent, + ) { + } + fn gesture_swipe_update( + &self, + _: &Seat, + _: &mut crate::state::State, + _: &GestureSwipeUpdateEvent, + ) { + } + fn gesture_swipe_end( + &self, + _: &Seat, + _: &mut crate::state::State, + _: &GestureSwipeEndEvent, + ) { + } + fn gesture_pinch_begin( + &self, + _: &Seat, + _: &mut crate::state::State, + _: &GesturePinchBeginEvent, + ) { + } + fn gesture_pinch_update( + &self, + _: &Seat, + _: &mut crate::state::State, + _: &GesturePinchUpdateEvent, + ) { + } + fn gesture_pinch_end( + &self, + _: &Seat, + _: &mut crate::state::State, + _: &GesturePinchEndEvent, + ) { + } + fn gesture_hold_begin( + &self, + _: &Seat, + _: &mut crate::state::State, + _: &GestureHoldBeginEvent, + ) { + } + fn gesture_hold_end( + &self, + _: &Seat, + _: &mut crate::state::State, + _: &GestureHoldEndEvent, + ) { + } +} + +impl TouchTarget for IcedElement

{ + fn down( + &self, + seat: &Seat, + _data: &mut crate::state::State, + event: &DownEvent, + ) { + let mut internal = self.0.lock().unwrap(); + let id = Finger(i32::from(event.slot) as u64); + let event_location = event.location.downscale(internal.additional_scale); + let position = IcedPoint::new(event_location.x as f32, event_location.y as f32); + internal + .state + .queue_event(Event::Touch(TouchEvent::FingerPressed { id, position })); + internal.touch_map.insert(id, position); + internal.cursor_pos = Some(event_location); + internal.last_touch_serial = Some(event.serial); + *internal.last_seat.lock().unwrap() = Some((seat.clone(), event.serial)); + internal.update(false); + } + + fn up( + &self, + seat: &Seat, + _data: &mut crate::state::State, + event: &UpEvent, + ) { + let mut internal = self.0.lock().unwrap(); + let id = Finger(i32::from(event.slot) as u64); + if let Some(position) = internal.touch_map.remove(&id) { + *internal.last_seat.lock().unwrap() = + Some((seat.clone(), internal.last_touch_serial.unwrap())); + internal + .state + .queue_event(Event::Touch(TouchEvent::FingerLifted { id, position })); + internal.update(false); + } + } + + fn motion( + &self, + seat: &Seat, + _data: &mut crate::state::State, + event: &TouchMotionEvent, + ) { + let mut internal = self.0.lock().unwrap(); + let id = Finger(i32::from(event.slot) as u64); + let event_location = event.location.downscale(internal.additional_scale); + let position = IcedPoint::new(event_location.x as f32, event_location.y as f32); + *internal.last_seat.lock().unwrap() = + Some((seat.clone(), internal.last_touch_serial.unwrap())); + internal + .state + .queue_event(Event::Touch(TouchEvent::FingerMoved { id, position })); + internal.touch_map.insert(id, position); + internal.cursor_pos = Some(event_location); + internal.update(false); + } + + fn frame( + &self, + _seat: &Seat, + _data: &mut crate::state::State, + frame: FrameMarker, + ) { + self.0.lock().unwrap().last_touch_frame = Some(frame); + } + + fn cancel( + &self, + _seat: &Seat, + _data: &mut crate::state::State, + frame: FrameMarker, + ) { + let mut internal = self.0.lock().unwrap(); + internal.last_touch_frame = Some(frame); + for (id, position) in std::mem::take(&mut internal.touch_map) { + internal + .state + .queue_event(Event::Touch(TouchEvent::FingerLost { id, position })); + } + internal.update(false); + } + + fn shape( + &self, + _seat: &Seat, + _data: &mut crate::state::State, + _event: &ShapeEvent, + ) { + } + + fn orientation( + &self, + _seat: &Seat, + _data: &mut crate::state::State, + _event: &OrientationEvent, + ) { + } + + fn last_frame( + &self, + _seat: &Seat, + _data: &mut crate::state::State, + ) -> Option { + self.0.lock().unwrap().last_touch_frame + } +} + +impl KeyboardTarget for IcedElement

{ + fn enter( + &self, + _seat: &Seat, + _data: &mut crate::state::State, + _keys: Vec>, + _serial: Serial, + ) { + // TODO convert keys + } + + fn leave( + &self, + _seat: &Seat, + _data: &mut crate::state::State, + _serial: Serial, + ) { + // TODO remove all held keys + } + + fn key( + &self, + _seat: &Seat, + _data: &mut crate::state::State, + _key: KeysymHandle<'_>, + _state: KeyState, + _serial: Serial, + _time: u32, + ) { + // TODO convert keys + } + + fn modifiers( + &self, + _seat: &Seat, + _data: &mut crate::state::State, + modifiers: ModifiersState, + _serial: Serial, + ) { + let mut internal = self.0.lock().unwrap(); + let mut mods = IcedModifiers::empty(); + if modifiers.shift { + mods.insert(IcedModifiers::SHIFT); + } + if modifiers.alt { + mods.insert(IcedModifiers::ALT); + } + if modifiers.ctrl { + mods.insert(IcedModifiers::CTRL); + } + if modifiers.logo { + mods.insert(IcedModifiers::LOGO); + } + internal + .state + .queue_event(Event::Keyboard(KeyboardEvent::ModifiersChanged(mods))); + internal.update(false); + } +} + +impl IsAlive for IcedElement

{ + fn alive(&self) -> bool { + true + } +} + +impl SpaceElement for IcedElement

{ + fn bbox(&self) -> Rectangle { + let internal = self.0.lock().unwrap(); + Rectangle::from_size( + internal + .size + .to_f64() + .upscale(internal.additional_scale) + .to_i32_round(), + ) + } + + fn is_in_input_region(&self, _point: &Point) -> bool { + true + } + + fn set_activate(&self, activated: bool) { + let mut internal = self.0.lock().unwrap(); + internal.state.queue_event(Event::Window(if activated { + WindowEvent::Focused + } else { + WindowEvent::Unfocused + })); + internal.update(false); + } + + fn output_enter(&self, output: &Output, _overlap: Rectangle) { + let mut internal = self.0.lock().unwrap(); + let scale = output.current_scale().fractional_scale() * internal.additional_scale; + + let internal_size = internal.size; + internal.buffers.entry(OrderedFloat(scale)).or_insert({ + let buffer_size = internal_size + .to_f64() + .to_buffer(scale, Transform::Normal) + .to_i32_round(); + + ( + MemoryRenderBuffer::new(Fourcc::Argb8888, buffer_size, 1, Transform::Normal, None), + None, + ) + }); + + internal.outputs.insert(output.clone()); + std::mem::drop(internal); + self.refresh(); + } + + fn output_leave(&self, output: &Output) { + self.0.lock().unwrap().outputs.remove(output); + self.refresh(); + } + + fn z_index(&self) -> u8 { + // meh, user-provided? + RenderZindex::Shell as u8 + } + + #[profiling::function] + fn refresh(&self) { + let mut internal = self.0.lock().unwrap(); + // makes partial borrows easier + let internal_ref = &mut *internal; + internal_ref.buffers.retain(|scale, _| { + internal_ref.outputs.iter().any(|o| { + o.current_scale().fractional_scale() * internal_ref.additional_scale == **scale + }) + }); + for scale in internal_ref + .outputs + .iter() + .map(|o| { + OrderedFloat(o.current_scale().fractional_scale() * internal_ref.additional_scale) + }) + .filter(|scale| !internal_ref.buffers.contains_key(scale)) + .collect::>() + .into_iter() + { + let buffer_size = internal_ref + .size + .to_f64() + .to_buffer(*scale, Transform::Normal) + .to_i32_round(); + internal_ref.buffers.insert( + scale, + ( + MemoryRenderBuffer::new( + Fourcc::Argb8888, + buffer_size, + 1, + Transform::Normal, + None, + ), + None, + ), + ); + } + internal.update(false); + } +} + +impl IcedElement

{ + pub fn push_render_elements( + &self, + renderer: &mut R, + location: Point, + mut scale: Scale, + alpha: f32, + mut radii: [u8; 4], + push_above: &mut dyn FnMut(IcedRenderElement), + push_below: Option<&mut dyn FnMut(IcedRenderElement)>, + ) where + R: AsGlowRenderer + ImportMem, + R::TextureId: Send + Clone + 'static, + { + let mut internal = self.0.lock().unwrap(); + // makes partial borrows easier + let internal_ref = &mut *internal; + if std::mem::replace(&mut internal_ref.pending_realloc, false) { + for (scale, (buffer, old_primitives)) in internal_ref.buffers.iter_mut() { + let buffer_size = internal_ref + .size + .to_f64() + .to_buffer(**scale, Transform::Normal) + .to_i32_round(); + buffer.render().resize(buffer_size); + *old_primitives = None; + } + } + + scale = scale * internal_ref.additional_scale; + if let Some((buffer, old_layers)) = internal_ref.buffers.get_mut(&OrderedFloat(scale.x)) { + let size: Size = internal_ref + .size + .to_f64() + .to_buffer(scale.x, Transform::Normal) + .to_i32_round(); + if size.w > 0 && size.h > 0 { + let state_ref = &internal_ref.state; + let mut clip_mask = tiny_skia::Mask::new(size.w as u32, size.h as u32).unwrap(); + let theme = &internal_ref.theme; + + _ = buffer.render().draw(|buf| { + let mut pixels = + tiny_skia::PixmapMut::from_bytes(buf, size.w as u32, size.h as u32) + .expect("Failed to create pixel map"); + + let background_color = state_ref.program().program.background_color(theme); + let bounds = IcedSize::new(size.w as u32, size.h as u32); + let viewport = Viewport::with_physical_size(bounds, scale.x); + let scale_x = scale.x as f32; + let current_layers = internal_ref.renderer.layers(); + let mut damage: Vec<_> = old_layers + .as_ref() + .and_then(|(last_primitives, last_color)| { + (last_color == &background_color).then(|| { + damage::diff( + last_primitives, + current_layers, + |_| { + vec![cosmic::iced::Rectangle::new( + cosmic::iced::Point::default(), + viewport.logical_size(), + )] + }, + Layer::damage, + ) + .into_iter() + .filter(|d| { + let width = d.width as u32; + let height = d.height as u32; + + width > 1 && height > 1 + }) + .collect() + }) + }) + .unwrap_or_else(|| { + vec![cosmic::iced::Rectangle::with_size(viewport.logical_size())] + }); + damage = damage::group( + damage, + cosmic::iced::Rectangle::with_size(viewport.logical_size()), + ); + + if !damage.is_empty() { + *old_layers = Some((current_layers.to_vec(), background_color)); + + internal_ref.renderer.draw( + &mut pixels, + &mut clip_mask, + &viewport, + &damage, + background_color, + ); + } + + let damage = damage + .into_iter() + .map(|d| d * scale_x) + .filter_map(|x| x.snap()) + .map(|damage_rect| { + Rectangle::new( + (damage_rect.x as i32, damage_rect.y as i32).into(), + (damage_rect.width as i32, damage_rect.height as i32).into(), + ) + }) + .collect::>(); + state_ref.program().program.foreground( + &mut pixels, + &damage, + scale.x as f32, + theme, + ); + + Result::<_, ()>::Ok(damage) + }); + + // trim the shape cache + { + let mut font_system = font_system().write().unwrap(); + font_system.raw().shape_run_cache.trim(1024); + } + } + + match MemoryRenderBufferRenderElement::from_buffer( + renderer, + location.to_f64(), + buffer, + Some(alpha), + Some(Rectangle::from_size( + size.to_f64() + .to_logical(1., Transform::Normal) + .to_i32_round(), + )), + Some( + internal_ref + .size + .to_f64() + .upscale(internal_ref.additional_scale) + .to_i32_round(), + ), + Kind::Unspecified, + ) { + Ok(buffer) => { + push_above(buffer.into()); + } + Err(err) => tracing::warn!("What? {:?}", err), + } + + if internal_ref.theme.transparent { + for radius in radii.iter_mut() { + *radius = ((*radius as f64) * internal_ref.additional_scale).round() as u8; + } + + match BlurElement::from_state( + renderer, + &mut internal_ref.blur, + Rectangle::new( + location + .to_f64() + .to_logical(scale) + .upscale(internal_ref.additional_scale), + internal_ref + .size + .to_f64() + .upscale(internal_ref.additional_scale) + .to_i32_round(), + ), + scale.x, + radii, + (internal_ref.theme.cosmic().frosted as u8 + 1) as usize, + ) { + Ok(Some(elem)) => { + if let Some(push_below) = push_below { + push_below(elem.into()) + } else { + push_above(elem.into()) + } + } + Ok(None) => {} + Err(err) => tracing::warn!("Blur elem error: {:?}", err), + } + } + } + } +} + +render_elements! { + pub IcedRenderElement where R: ImportMem + AsGlowRenderer, R::TextureId: Send; + UI=MemoryRenderBufferRenderElement, + Blur=BlurElement, +} diff --git a/src/utils/iced/state.rs b/src/utils/iced/state.rs new file mode 100644 index 0000000..2af3a99 --- /dev/null +++ b/src/utils/iced/state.rs @@ -0,0 +1,224 @@ +use super::IcedProgram as Program; +use cosmic::iced::{ + core::{ + self as iced_core, Clipboard, Size, + event::{self, Event}, + mouse, renderer, + widget::operation::{self, Operation}, + }, + runtime::{ + Task, + user_interface::{self, UserInterface}, + }, +}; + +/// 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 cosmic::Renderer, + ) -> Self { + let user_interface = build_user_interface( + id, + &mut program, + user_interface::Cache::default(), + renderer, + bounds, + ); + + 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 cosmic::Renderer, + theme: &cosmic::Theme, + style: &renderer::Style, + clipboard: &mut dyn Clipboard, + ) -> (Vec, Option>) { + let mut user_interface = build_user_interface( + id.clone(), + &mut self.program, + self.cache.take().unwrap(), + renderer, + bounds, + ); + + let mut messages = Vec::new(); + + let (state, 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); + + let task = if messages.is_empty() { + if let cosmic::iced::runtime::user_interface::State::Updated { + mouse_interaction, .. + } = state + { + self.mouse_interaction = mouse_interaction; + } + + user_interface.draw(renderer, theme, style, cursor); + + 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| self.program.update(message)), + ); + + let mut user_interface = + build_user_interface(id, &mut self.program, temp_cache, renderer, bounds); + + if let cosmic::iced::runtime::user_interface::State::Updated { + mouse_interaction, .. + } = state + { + self.mouse_interaction = mouse_interaction; + } + + user_interface.draw(renderer, theme, style, cursor); + + 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 cosmic::Renderer, + operations: impl Iterator>, + bounds: Size, + ) { + let mut user_interface = build_user_interface( + id, + &mut self.program, + self.cache.take().unwrap(), + renderer, + bounds, + ); + + 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 cosmic::Renderer, + size: Size, +) -> UserInterface<'a, P::Message, cosmic::Theme, cosmic::Renderer> { + let view = program.view(); + + UserInterface::build(view, size, cache, renderer) +} diff --git a/src/utils/ids.rs b/src/utils/ids.rs new file mode 100644 index 0000000..1b26752 --- /dev/null +++ b/src/utils/ids.rs @@ -0,0 +1,39 @@ +// TODO: Remove once desktop is back +#![allow(unused)] + +macro_rules! id_gen { + ($func_name:ident, $id_name:ident, $ids_name:ident) => { + static $id_name: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); + static $ids_name: std::sync::LazyLock>> = + std::sync::LazyLock::new(|| std::sync::Mutex::new(std::collections::HashSet::new())); + + fn $func_name() -> usize { + let mut ids = $ids_name.lock().unwrap(); + if ids.len() == usize::MAX { + panic!("Out of ids"); + } + + let id = loop { + let new_id = $id_name.fetch_update( + std::sync::atomic::Ordering::SeqCst, + std::sync::atomic::Ordering::SeqCst, + |mut id| { + while ids.iter().any(|k| *k == id) { + id += 1; + } + id += 1; + Some(id) + }, + ); + if let Ok(id) = new_id { + break id; + } + }; + + ids.insert(id); + id + } + }; +} + +pub(crate) use id_gen; diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..ac1df01 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pub mod env; +mod ids; +pub(crate) use self::ids::id_gen; +pub mod geometry; +pub mod iced; +pub mod prelude; +pub mod quirks; +pub mod rlimit; +pub mod screenshot; +pub mod tween; diff --git a/src/utils/prelude.rs b/src/utils/prelude.rs new file mode 100644 index 0000000..2baae77 --- /dev/null +++ b/src/utils/prelude.rs @@ -0,0 +1,170 @@ +use cosmic_comp_config::output::comp::{AdaptiveSync, OutputConfig, OutputState}; +use smithay::{ + backend::drm::VrrSupport as Support, + output::{Output, WeakOutput}, + utils::Rectangle, +}; + +pub use super::geometry::*; +pub use crate::shell::{SeatExt, Shell, Workspace}; +pub use crate::state::{Common, State}; +pub use crate::wayland::handlers::xdg_shell::popup::update_reactive_popups; +use crate::{config::EdidProduct, shell::zoom::OutputZoomState}; + +use std::{ + cell::{Ref, RefCell, RefMut}, + sync::{ + Mutex, + atomic::{AtomicU8, Ordering}, + }, +}; + +pub trait OutputExt { + fn is_internal(&self) -> bool; + fn geometry(&self) -> Rectangle; + fn zoomed_geometry(&self) -> Option>; + + fn adaptive_sync(&self) -> AdaptiveSync; + fn set_adaptive_sync(&self, vrr: AdaptiveSync); + fn adaptive_sync_support(&self) -> Option; + fn set_adaptive_sync_support(&self, vrr: Option); + fn mirroring(&self) -> Option; + fn set_mirroring(&self, output: Option); + + fn is_enabled(&self) -> bool; + fn config(&self) -> Ref<'_, OutputConfig>; + fn config_mut(&self) -> RefMut<'_, OutputConfig>; + + fn edid(&self) -> Option<&EdidProduct>; +} + +struct Vrr(AtomicU8); +struct VrrSupport(AtomicU8); +struct Mirroring(Mutex>); + +impl OutputExt for Output { + fn is_internal(&self) -> bool { + let name = self.name(); + name.starts_with("eDP-") || name.starts_with("LVDS-") || name.starts_with("DSI-") + } + + fn geometry(&self) -> Rectangle { + Rectangle::new(self.current_location(), { + self.current_transform() + .transform_size( + self.current_mode() + .map(|m| m.size) + .unwrap_or_else(|| (0, 0).into()), + ) + .to_f64() + .to_logical(self.current_scale().fractional_scale()) + .to_i32_round() + }) + .as_global() + } + + fn zoomed_geometry(&self) -> Option> { + let output_geometry = self.geometry(); + + let output_state = self.user_data().get::>()?; + let mut output_state_ref = output_state.lock().unwrap(); + + let focal_point = output_state_ref.current_focal_point().to_global(self); + let mut zoomed_output_geo = output_geometry.to_f64(); + zoomed_output_geo.loc -= focal_point; + zoomed_output_geo = zoomed_output_geo.downscale(output_state_ref.current_level()); + zoomed_output_geo.loc += focal_point; + + Some(zoomed_output_geo.to_i32_round()) + } + + fn adaptive_sync(&self) -> AdaptiveSync { + self.user_data() + .get::() + .map(|vrr| match vrr.0.load(Ordering::SeqCst) { + 2 => AdaptiveSync::Force, + 1 => AdaptiveSync::Enabled, + _ => AdaptiveSync::Disabled, + }) + .unwrap_or(AdaptiveSync::Disabled) + } + fn set_adaptive_sync(&self, vrr: AdaptiveSync) { + let user_data = self.user_data(); + user_data.insert_if_missing_threadsafe(|| Vrr(AtomicU8::new(0))); + user_data.get::().unwrap().0.store( + match vrr { + AdaptiveSync::Disabled => 0, + AdaptiveSync::Enabled => 1, + AdaptiveSync::Force => 2, + }, + Ordering::SeqCst, + ); + } + + fn adaptive_sync_support(&self) -> Option { + self.user_data() + .get::() + .and_then(|vrr| match vrr.0.load(Ordering::SeqCst) { + 0 => None, + 2 => Some(Support::RequiresModeset), + 3 => Some(Support::Supported), + _ => Some(Support::NotSupported), + }) + } + + fn set_adaptive_sync_support(&self, vrr: Option) { + let user_data = self.user_data(); + user_data.insert_if_missing_threadsafe(|| VrrSupport(AtomicU8::new(0))); + user_data.get::().unwrap().0.store( + match vrr { + None => 0, + Some(Support::NotSupported) => 1, + Some(Support::RequiresModeset) => 2, + Some(Support::Supported) => 3, + }, + Ordering::SeqCst, + ); + } + + fn mirroring(&self) -> Option { + self.user_data().get::().and_then(|mirroring| { + mirroring + .0 + .lock() + .unwrap() + .clone() + .and_then(|o| o.upgrade()) + }) + } + fn set_mirroring(&self, output: Option) { + let user_data = self.user_data(); + user_data.insert_if_missing_threadsafe(|| Mirroring(Mutex::new(None))); + *user_data.get::().unwrap().0.lock().unwrap() = + output.map(|output| output.downgrade()); + } + + fn is_enabled(&self) -> bool { + self.user_data() + .get::>() + .map(|conf| conf.borrow().enabled != OutputState::Disabled) + .unwrap_or(false) + } + + fn config(&self) -> Ref<'_, OutputConfig> { + self.user_data() + .get::>() + .unwrap() + .borrow() + } + + fn config_mut(&self) -> RefMut<'_, OutputConfig> { + self.user_data() + .get::>() + .unwrap() + .borrow_mut() + } + + fn edid(&self) -> Option<&EdidProduct> { + self.user_data().get() + } +} diff --git a/src/utils/quirks.rs b/src/utils/quirks.rs new file mode 100644 index 0000000..e0954fd --- /dev/null +++ b/src/utils/quirks.rs @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use smithay::{ + backend::renderer::utils::with_renderer_surface_state, desktop::layer_map_for_output, + output::Output, +}; + +/// Layer shell namespace used by `cosmic-workspaces` +// TODO: Avoid special case, or add protocol to expose required behavior +pub const WORKSPACE_OVERVIEW_NAMESPACE: &str = "cosmic-workspace-overview"; + +/// Check if a workspace overview shell surface is open on the output +pub fn workspace_overview_is_open(output: &Output) -> bool { + layer_map_for_output(output) + .layers() + .filter(|s| s.namespace() == WORKSPACE_OVERVIEW_NAMESPACE) + // Only consider the overview open once it has committed a buffer. The + // surface is inserted into the layer map on its initial (bufferless) + // commit, so checking for the namespace alone hides all toplevels for a + // frame before the overview has anything to draw, briefly flashing the + // bare wallpaper. + .any(|s| { + with_renderer_surface_state(s.wl_surface(), |state| state.buffer().is_some()) + .unwrap_or(false) + }) +} diff --git a/src/utils/rlimit.rs b/src/utils/rlimit.rs new file mode 100644 index 0000000..31f75e0 --- /dev/null +++ b/src/utils/rlimit.rs @@ -0,0 +1,31 @@ +use rustix::process::{Resource, Rlimit, getrlimit, setrlimit}; +use std::sync::atomic::{AtomicU64, Ordering}; + +static OLD_LIMIT: AtomicU64 = AtomicU64::new(0); +static MAX_LIMIT: AtomicU64 = AtomicU64::new(0); + +pub fn increase_nofile_limit() { + let mut limits = getrlimit(Resource::Nofile); + + OLD_LIMIT.store(limits.current.unwrap_or(0), Ordering::SeqCst); + MAX_LIMIT.store(limits.maximum.unwrap_or(0), Ordering::SeqCst); + limits.current = limits.maximum; + + if let Err(err) = setrlimit(Resource::Nofile, limits) { + tracing::warn!("Failed to raise nofile soft limit: {:?}", err); + } +} + +pub fn restore_nofile_limit() { + let current = OLD_LIMIT.load(Ordering::SeqCst); + let maximum = MAX_LIMIT.load(Ordering::SeqCst); + let limits = Rlimit { + current: (current > 0).then_some(current), + maximum: (maximum > 0).then_some(maximum), + }; + + if let Err(err) = setrlimit(Resource::Nofile, limits) { + // Use `eprintln!` instead of `tracing` since this is used in `pre_exec`. + eprintln!("Failed to restore nofile soft limit: {:?}", err); + } +} diff --git a/src/utils/screenshot.rs b/src/utils/screenshot.rs new file mode 100644 index 0000000..f1f1f5f --- /dev/null +++ b/src/utils/screenshot.rs @@ -0,0 +1,115 @@ +use anyhow::Context; +use smithay::{ + backend::{ + allocator::Fourcc, + renderer::{ + ExportMem, ImportAll, Offscreen, Renderer, damage::OutputDamageTracker, + gles::GlesRenderbuffer, + }, + }, + desktop::utils::bbox_from_surface_tree, + utils::{Scale, Transform}, + wayland::seat::WaylandFocus, +}; +use tracing::warn; + +use crate::{ + backend::render::{RendererRef, element::AsGlowRenderer}, + shell::element::CosmicSurface, + state::{State, advertised_node_for_surface}, +}; + +pub fn screenshot_window(state: &mut State, surface: &CosmicSurface) { + fn render_window(renderer: &mut R, window: &CosmicSurface) -> anyhow::Result<()> + where + R: Renderer + ImportAll + Offscreen + ExportMem + AsGlowRenderer, + R::TextureId: Send + Clone + 'static, + R::Error: Send + Sync + 'static, + { + let bbox = bbox_from_surface_tree(&window.wl_surface().unwrap(), (0, 0)); + let mut elements = Vec::new(); + window.push_render_elements( + renderer, + (-bbox.loc.x, -bbox.loc.y).into(), + Scale::from(1.0), + 1.0, + None, + None, + false, + [0; 4], + 0, + &mut |elem| elements.push(elem), + None, + ); + + // TODO: 10-bit + let format = Fourcc::Abgr8888; + let mut render_buffer = Offscreen::::create_buffer( + renderer, + format, + bbox.size.to_buffer(1, Transform::Normal), + )?; + let mut fb = renderer.bind(&mut render_buffer)?; + let mut output_damage_tracker = + OutputDamageTracker::new(bbox.size.to_physical(1), 1.0, Transform::Normal); + output_damage_tracker + .render_output(renderer, &mut fb, 0, &elements, [0.0, 0.0, 0.0, 0.0]) + .map_err(|err| match err { + smithay::backend::renderer::damage::Error::Rendering(err) => err, + smithay::backend::renderer::damage::Error::OutputNoMode(_) => unreachable!(), + })?; + let mapping = renderer.copy_framebuffer( + &fb, + bbox.to_buffer(1, Transform::Normal, &bbox.size), + format, + )?; + let gl_data = renderer.map_texture(&mapping)?; + + if let Ok(Some(path)) = xdg_user::pictures() { + let local_timestamp = jiff::Zoned::now(); + let mut title = window.title(); + title.truncate(227); // 255 - time - png + let name = sanitize_filename::sanitize(format!( + "{}_{}.png", + title, + local_timestamp.strftime("%Y-%m-%d_%H:%M:%S_%4f"), + )); + let file = std::fs::File::create(path.join(name))?; + + let writer = &mut std::io::BufWriter::new(file); + let mut encoder = png::Encoder::new(writer, bbox.size.w as u32, bbox.size.h as u32); + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Eight); + encoder.set_source_gamma(png::ScaledFloat::new(1.0 / 2.2)); // 1.0 / 2.2, unscaled, but rounded + let source_chromaticities = png::SourceChromaticities::new( + // Using unscaled instantiation here + (0.31270, 0.32900), + (0.64000, 0.33000), + (0.30000, 0.60000), + (0.15000, 0.06000), + ); + encoder.set_source_chromaticities(source_chromaticities); + let mut writer = encoder.write_header()?; + writer.write_image_data(gl_data)?; + } + + Ok(()) + } + + if let Some(wl_surface) = surface.wl_surface() { + let res = state + .backend + .offscreen_renderer(|kms| { + advertised_node_for_surface(&wl_surface, &state.common.display_handle) + .or(*kms.primary_node.read().unwrap()) + }) + .with_context(|| "Failed to get renderer for screenshot") + .and_then(|renderer| match renderer { + RendererRef::Glow(renderer) => render_window(renderer, surface), + RendererRef::GlMulti(mut renderer) => render_window(&mut renderer, surface), + }); + if let Err(err) = res { + warn!(?err, "Failed to take screenshot") + } + } +} diff --git a/src/utils/tween.rs b/src/utils/tween.rs new file mode 100644 index 0000000..490fa3e --- /dev/null +++ b/src/utils/tween.rs @@ -0,0 +1,47 @@ +use keyframe::{CanTween, num_traits::Float}; +use smithay::utils::{Coordinate, Point, Rectangle, Size}; + +pub struct EasePoint(pub Point); +pub struct EaseSize(pub Size); +pub struct EaseRectangle(pub Rectangle); + +impl CanTween for EasePoint { + fn ease(from: Self, to: Self, time: impl Float) -> Self { + let x = N::from_f64(CanTween::ease(from.0.x.to_f64(), to.0.x.to_f64(), time).round()); + let y = N::from_f64(CanTween::ease(from.0.y.to_f64(), to.0.y.to_f64(), time).round()); + EasePoint((x, y).into()) + } +} + +impl CanTween for EaseSize { + fn ease(from: Self, to: Self, time: impl Float) -> Self { + let w = N::from_f64(CanTween::ease(from.0.w.to_f64(), to.0.w.to_f64(), time).round()); + let h = N::from_f64(CanTween::ease(from.0.h.to_f64(), to.0.h.to_f64(), time).round()); + EaseSize((w, h).into()) + } +} + +impl CanTween for EaseRectangle { + fn ease(from: Self, to: Self, time: impl Float) -> Self { + EaseRectangle(Rectangle::new( + CanTween::ease(EasePoint(from.0.loc), EasePoint(to.0.loc), time).unwrap(), + CanTween::ease(EaseSize(from.0.size), EaseSize(to.0.size), time).unwrap(), + )) + } +} + +impl EasePoint { + pub fn unwrap(self) -> Point { + self.0 + } +} +impl EaseSize { + pub fn unwrap(self) -> Size { + self.0 + } +} +impl EaseRectangle { + pub fn unwrap(self) -> Rectangle { + self.0 + } +} diff --git a/src/wayland/handlers/a11y.rs b/src/wayland/handlers/a11y.rs new file mode 100644 index 0000000..4e42643 --- /dev/null +++ b/src/wayland/handlers/a11y.rs @@ -0,0 +1,66 @@ +use tracing::warn; + +use crate::{ + config::ColorFilter, + state::State, + wayland::protocols::a11y::{A11yHandler, A11yState, delegate_a11y}, +}; + +impl A11yHandler for State { + fn a11y_state(&mut self) -> &mut A11yState { + &mut self.common.a11y_state + } + + fn request_screen_magnifier(&mut self, enabled: bool) { + let mut shell = self.common.shell.write(); + + if shell + .zoom_state() + .is_some_and(|state| shell.outputs().any(|o| state.current_level(o) != 1.0)) + != enabled + { + let seat = shell.seats.last_active().clone(); + let level = if enabled { + 1.0 + (self.common.config.cosmic_conf.accessibility_zoom.increment as f64 / 100.0) + } else { + 1.0 + }; + let zoom_config = &self.common.config.cosmic_conf.accessibility_zoom; + + shell.trigger_zoom( + &seat, + None, + level, + zoom_config, + true, + &self.common.event_loop_handle, + ); + } + } + + fn request_screen_invert(&mut self, inverted: bool) { + let mut config = self.common.config.dynamic_conf.screen_filter_mut(); + let mut updated = (*config).clone(); + updated.inverted = inverted; + if let Err(err) = self.backend.update_screen_filter(&updated) { + warn!("Failed to apply screen color invert: {}", err); + } else { + *config = updated; + self.common.a11y_state.set_screen_inverted(inverted); + } + } + + fn request_screen_filter(&mut self, filter: Option) { + let mut config = self.common.config.dynamic_conf.screen_filter_mut(); + let mut updated = (*config).clone(); + updated.color_filter = filter; + if let Err(err) = self.backend.update_screen_filter(&updated) { + warn!("Failed to apply screen color filter: {}", err); + } else { + *config = updated; + self.common.a11y_state.set_screen_filter(filter); + } + } +} + +delegate_a11y!(State); diff --git a/src/wayland/handlers/background_effect.rs b/src/wayland/handlers/background_effect.rs new file mode 100644 index 0000000..28682e0 --- /dev/null +++ b/src/wayland/handlers/background_effect.rs @@ -0,0 +1,59 @@ +use smithay::{ + reexports::wayland_server::{DisplayHandle, protocol::wl_surface::WlSurface}, + utils::{Logical, Rectangle}, + wayland::{ + background_effect::{Capability, ExtBackgroundEffectHandler}, + compositor::{Cacheable, RectangleKind, RegionAttributes, with_states}, + }, +}; + +use crate::state::State; + +#[derive(Debug, Clone, Default)] +pub struct ComputedBlurRegionCachedState { + /// Region of the surface that will have its background blurred. + pub blur_region: Option>>, +} + +impl Cacheable for ComputedBlurRegionCachedState { + fn commit(&mut self, _dh: &DisplayHandle) -> Self { + self.clone() + } + + fn merge_into(self, into: &mut Self, _dh: &DisplayHandle) { + *into = self; + } +} + +impl ExtBackgroundEffectHandler for State { + fn capabilities(&self) -> Capability { + Capability::Blur + } + + fn set_blur_region(&mut self, surface: WlSurface, region: RegionAttributes) { + with_states(&surface, |states| { + let mut blur_state = states.cached_state.get::(); + + blur_state.pending().blur_region = Some({ + let (added, subtracted) = region + .rects + .iter() + .cloned() + .partition::, _>(|(op, _)| matches!(op, RectangleKind::Add)); + let added = added.into_iter().map(|(_, rect)| rect).collect::>(); + Rectangle::subtract_rects_many_in_place( + added, + subtracted.into_iter().map(|(_, rect)| rect), + ) + }) + }) + } + + fn unset_blur_region(&mut self, surface: WlSurface) { + with_states(&surface, |states| { + let mut blur_state = states.cached_state.get::(); + + blur_state.pending().blur_region.take(); + }) + } +} diff --git a/src/wayland/handlers/buffer.rs b/src/wayland/handlers/buffer.rs new file mode 100644 index 0000000..184eee4 --- /dev/null +++ b/src/wayland/handlers/buffer.rs @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::state::State; +use smithay::{ + reexports::wayland_server::protocol::wl_buffer::WlBuffer, wayland::buffer::BufferHandler, +}; + +impl BufferHandler for State { + fn buffer_destroyed(&mut self, _buffer: &WlBuffer) {} +} diff --git a/src/wayland/handlers/compositor.rs b/src/wayland/handlers/compositor.rs new file mode 100644 index 0000000..12bc2e5 --- /dev/null +++ b/src/wayland/handlers/compositor.rs @@ -0,0 +1,434 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{shell::grabs::SeatMoveGrabState, state::ClientState, utils::prelude::*}; +use calloop::Interest; +use smithay::{ + backend::renderer::{ + element::{Kind, surface::KindEvaluation}, + utils::{on_commit_buffer_handler, with_renderer_surface_state}, + }, + desktop::{LayerSurface, PopupKind, WindowSurfaceType, layer_map_for_output}, + reexports::wayland_server::{Client, Resource, protocol::wl_surface::WlSurface}, + utils::{Clock, Logical, Monotonic, SERIAL_COUNTER, Size, Time}, + wayland::{ + compositor::{ + BufferAssignment, CompositorClientState, CompositorHandler, CompositorState, + SurfaceAttributes, SurfaceData, TraversalAction, add_blocker, add_post_commit_hook, + add_pre_commit_hook, with_states, with_surface_tree_downward, + }, + dmabuf::get_dmabuf, + drm_syncobj::DrmSyncobjCachedState, + seat::WaylandFocus, + shell::{ + wlr_layer::LayerSurfaceAttributes, + xdg::{ + ToplevelSurface, XdgPopupSurfaceRoleAttributes, XdgToplevelSurfaceRoleAttributes, + }, + }, + }, + xwayland::XWaylandClientData, +}; +use std::{collections::VecDeque, sync::Mutex, time::Duration}; + +fn toplevel_ensure_initial_configure( + toplevel: &ToplevelSurface, + size: Option>, +) -> bool { + // send the initial configure if relevant + let initial_configure_sent = with_states(toplevel.wl_surface(), |states| { + states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap() + .initial_configure_sent + }); + if !initial_configure_sent { + toplevel.with_pending_state(|states| states.size = size); + toplevel.send_configure(); + } + initial_configure_sent +} + +fn xdg_popup_ensure_initial_configure(popup: &PopupKind) { + if let PopupKind::Xdg(popup) = popup { + let initial_configure_sent = with_states(popup.wl_surface(), |states| { + states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap() + .initial_configure_sent + }); + if !initial_configure_sent { + // NOTE: This should never fail as the initial configure is always + // allowed. + popup.send_configure().expect("initial configure failed"); + } + } +} + +fn layer_surface_check_inital_configure(surface: &LayerSurface) -> bool { + // send the initial configure if relevant + with_states(surface.wl_surface(), |states| { + states + .data_map + .get::>() + .unwrap() + .lock() + .unwrap() + .initial_configure_sent + }) +} + +pub fn client_compositor_state(client: &Client) -> &CompositorClientState { + if let Some(state) = client.get_data::() { + return &state.compositor_state; + } + if let Some(state) = client.get_data::() { + return &state.compositor_client_state; + } + panic!("Unknown client data type") +} + +#[derive(Debug)] +struct FrametimeData { + last_commit: Option>, + last_diffs: VecDeque, + estimation: Duration, +} + +impl Default for FrametimeData { + fn default() -> Self { + FrametimeData { + last_commit: None, + last_diffs: VecDeque::with_capacity(100), + estimation: Duration::MAX, + } + } +} + +pub fn frame_time_estimation(clock: &Clock, states: &SurfaceData) -> Option { + let data = states + .data_map + .get::>()? + .lock() + .unwrap(); + if let Some(ref last) = data.last_commit { + // if the time since the last commit is already higher than our estimation, + // there is no reason to not use that as a better "guess" + let diff = Time::elapsed(last, clock.now()); + Some(diff.max(data.estimation)) + } else { + Some(data.estimation) + } +} + +pub fn recursive_frame_time_estimation( + clock: &Clock, + surface: &WlSurface, +) -> Option { + let mut overall_estimate = None; + with_surface_tree_downward( + surface, + (), + |_, _, _| TraversalAction::DoChildren(()), + |_, data, _| { + let surface_estimate = frame_time_estimation(clock, data); + overall_estimate = match (overall_estimate, surface_estimate) { + (x, None) => x, + (None, Some(estimate)) => Some(estimate), + (Some(a), Some(b)) => Some(a.min(b)), + }; + }, + |_, _, _| true, + ); + overall_estimate +} + +pub fn frame_time_filter_fn(states: &SurfaceData) -> Kind { + let clock = Clock::::new(); + const _20_FPS: Duration = Duration::from_nanos(1_000_000_000 / 20); + + if frame_time_estimation(&clock, states).is_some_and(|dur| dur <= _20_FPS) { + Kind::ScanoutCandidate + } else { + Kind::Unspecified + } +} + +pub const FRAME_TIME_FILTER: KindEvaluation = KindEvaluation::Dynamic(frame_time_filter_fn); + +impl CompositorHandler for State { + fn compositor_state(&mut self) -> &mut CompositorState { + &mut self.common.compositor_state + } + + fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState { + client_compositor_state(client) + } + + fn new_surface(&mut self, surface: &WlSurface) { + add_pre_commit_hook::(surface, move |state, _dh, surface| { + let mut acquire_point = None; + let maybe_dmabuf = with_states(surface, |surface_data| { + acquire_point = surface_data + .cached_state + .get::() + .pending() + .acquire_point + .clone(); + surface_data + .cached_state + .get::() + .pending() + .buffer + .as_ref() + .and_then(|assignment| match assignment { + BufferAssignment::NewBuffer(buffer) => get_dmabuf(buffer).ok().cloned(), + _ => None, + }) + }); + if let Some(dmabuf) = maybe_dmabuf { + if let Some(acquire_point) = acquire_point + && let Ok((blocker, source)) = acquire_point.generate_blocker() + { + let client = surface.client().unwrap(); + let res = + state + .common + .event_loop_handle + .insert_source(source, move |_, _, state| { + let dh = state.common.display_handle.clone(); + state + .client_compositor_state(&client) + .blocker_cleared(state, &dh); + Ok(()) + }); + if res.is_ok() { + add_blocker(surface, blocker); + return; + } + } + if let Ok((blocker, source)) = dmabuf.generate_blocker(Interest::READ) { + let client = surface.client().unwrap(); + let res = + state + .common + .event_loop_handle + .insert_source(source, move |_, _, state| { + let dh = state.common.display_handle.clone(); + state + .client_compositor_state(&client) + .blocker_cleared(state, &dh); + Ok(()) + }); + if res.is_ok() { + add_blocker(surface, blocker); + } + } + } + }); + + add_post_commit_hook::(surface, |state, _dh, surface| { + let now = state.common.clock.now(); + with_states(surface, |states| { + let mut data = states + .data_map + .get_or_insert_threadsafe::, _>(Default::default) + .lock() + .unwrap(); + if let Some(ref last) = data.last_commit { + let diff = Time::elapsed(last, now); + data.last_diffs.push_back(diff); + if data.last_diffs.len() > 100 { + data.last_diffs.pop_front(); + } + data.estimation = data + .last_diffs + .iter() + .fold(Duration::ZERO, |acc, new| acc.saturating_add(*new)) + / (data.last_diffs.len() as u32); + } + data.last_commit = Some(now); + }); + }); + } + + fn commit(&mut self, surface: &WlSurface) { + // first load the buffer for various smithay helper functions (which also initializes the RendererSurfaceState) + on_commit_buffer_handler::(surface); + + // and refresh smithays internal state + self.common.on_commit(surface); + + // handle initial configure events and map windows if necessary + let mapped = self.send_initial_configure_and_map(surface); + + let mut shell = self.common.shell.write(); + + // schedule a new render + if let Some(output) = shell.visible_output_for_surface(surface) { + self.backend.schedule_render(output); + } + + if mapped { + return; + } + + if let Some(popup) = self.common.popups.find_popup(surface) { + xdg_popup_ensure_initial_configure(&popup); + // The IME popup need to be repositioned when the size changed + if let PopupKind::InputMethod(_) = popup { + shell.unconstrain_popup(&popup); + } + return; + } + + if with_renderer_surface_state(surface, |state| state.buffer().is_none()).unwrap_or(false) { + // handle null-commits causing weird conflicts: + + // session-lock disallows null commits + + // if it was a move-grab? + let seat = shell.seats.last_active().clone(); + let moved_window = seat + .user_data() + .get::() + .unwrap() + .lock() + .unwrap() + .as_ref() + .and_then(|state| { + state + .element() + .windows() + .any(|(s, _)| { + s.wl_surface() + .as_deref() + .map(|s| s == surface) + .unwrap_or(false) + }) + .then(|| state.element()) + }); + if let Some(window) = moved_window { + if let Some(stack) = window.stack_ref() { + if let Some(i) = stack.surfaces().position(|s| { + s.wl_surface() + .as_deref() + .map(|s| s == surface) + .unwrap_or(false) + }) { + stack.remove_idx(i); + } + } else { + std::mem::drop(shell); + seat.get_pointer() + .unwrap() + .unset_grab(self, SERIAL_COUNTER.next_serial(), 0); + return; + } + } + + // if it was a layer-shell surface? + // ignore, that will affect recompute normally + + // if it was a sticky / floating / tiled window + // we could unmap, I guess? + + // if it was an x11 surface => do nothing, we will get a separate UnmapNotify anyway + } else { + // handle some special cases, like grabs and changing layer surfaces + + // If we would re-position the window inside the grab we would get a weird jittery animation. + // We only want to resize once the client has acknoledged & commited the new size, + // so we need to carefully track the state through different handlers. + if let Some(element) = shell.element_for_surface(surface).cloned() { + crate::shell::layout::floating::ResizeSurfaceGrab::apply_resize_to_location( + element.clone(), + &mut shell, + ); + } + } + + // re-arrange layer-surfaces (commits may change size and positioning) + let layer_output = shell + .outputs() + .find(|o| { + let map = layer_map_for_output(o); + map.layer_for_surface(surface, WindowSurfaceType::ALL) + .is_some() + }) + .cloned(); + + if let Some(output) = layer_output { + let changed = layer_map_for_output(&output).arrange(); + if changed { + shell.workspaces.recalculate(); + } + } + } +} + +impl State { + fn send_initial_configure_and_map(&mut self, surface: &WlSurface) -> bool { + let mut shell = self.common.shell.write(); + + if let Some(pending) = shell + .pending_windows + .iter() + .find(|pending| pending.surface.wl_surface().as_deref() == Some(surface)) + && let Some(toplevel) = pending.surface.0.toplevel() + { + let initial_size = if let Some(output) = pending.fullscreen.as_ref() { + Some(output.geometry().size.as_logical()) + } else if pending.maximized { + let active_output = shell.seats.last_active().active_output(); + let zone = layer_map_for_output(&active_output).non_exclusive_zone(); + Some(zone.size) + } else { + None + }; + if toplevel_ensure_initial_configure(toplevel, initial_size) + && with_renderer_surface_state(surface, |state| state.buffer().is_some()) + .unwrap_or(false) + { + let window = pending.surface.clone(); + window.on_commit(); + let res = shell.map_window( + &window, + &mut self.common.toplevel_info_state, + &mut self.common.workspace_state, + &self.common.event_loop_handle, + ); + if let Some(target) = res { + let seat = shell.seats.last_active().clone(); + std::mem::drop(shell); + Shell::set_focus(self, Some(&target), &seat, None, true); + return true; + } + } + } + + if let Some(layer_surface) = shell + .pending_layers + .iter() + .find(|pending| pending.surface.wl_surface() == surface) + .map(|pending| pending.surface.clone()) + && !layer_surface_check_inital_configure(&layer_surface) + { + // compute initial dimensions by mapping + if let Some(target) = shell.map_layer(&layer_surface) { + let seat = shell.seats.last_active().clone(); + std::mem::drop(shell); + Shell::set_focus(self, Some(&target), &seat, None, false); + } + layer_surface.layer_surface().send_configure(); + return true; + }; + + false + } +} diff --git a/src/wayland/handlers/corner_radius.rs b/src/wayland/handlers/corner_radius.rs new file mode 100644 index 0000000..ded8d92 --- /dev/null +++ b/src/wayland/handlers/corner_radius.rs @@ -0,0 +1,75 @@ +use smithay::utils::{Logical, Rectangle, Size}; +use smithay::wayland::compositor::SurfaceData; + +use crate::wayland::protocols::corner_radius::{ + CacheableCorners, CacheablePadding, CornerRadiusData, CornerRadiusHandler, CornerRadiusState, + delegate_corner_radius, +}; + +use crate::state::State; + +impl CornerRadiusHandler for State { + fn corner_radius_state(&mut self) -> &mut CornerRadiusState { + &mut self.common.corner_radius_state + } + + fn set_corner_radius(&mut self, _data: &CornerRadiusData) {} + + fn unset_corner_radius(&mut self, _data: &CornerRadiusData) {} + + fn set_padding(&mut self, _data: &CornerRadiusData) {} + + fn unset_padding(&mut self, _data: &CornerRadiusData) {} +} + +pub fn surface_corners(states: &SurfaceData, size: Size) -> Option<[u8; 4]> { + let mut guard = states.cached_state.get::(); + + // guard against corner radius being too large, potentially disconnecting the outline + let half_min_dim = u8::try_from(size.w.min(size.h) / 2).unwrap_or(u8::MAX); + let corners = guard.current().0?; + + Some([ + u8::try_from(corners.top_left) + .unwrap_or(u8::MAX) + .min(half_min_dim), + u8::try_from(corners.top_right) + .unwrap_or(u8::MAX) + .min(half_min_dim), + u8::try_from(corners.bottom_right) + .unwrap_or(u8::MAX) + .min(half_min_dim), + u8::try_from(corners.bottom_left) + .unwrap_or(u8::MAX) + .min(half_min_dim), + ]) +} + +pub fn surface_padding(states: &SurfaceData, size: Size) -> Option<[i32; 4]> { + let mut guard = states.cached_state.get::(); + + let padding = guard.current().0?; + + // guard against padding being too large + Some([ + padding.top.min(size.h / 2), + padding.right.min(size.w / 2), + padding.bottom.min(size.h / 2), + padding.left.min(size.w / 2), + ]) +} + +pub fn pad_rect( + mut rect: Rectangle, + padding: &[i32; 4], +) -> Option> { + rect.size.h = rect.size.h.checked_sub(padding[0])?; + rect.loc.x += padding[3]; + rect.size.w = rect.size.w.checked_sub(padding[1])?; + rect.size.h = rect.size.h.checked_sub(padding[2])?; + rect.size.w = rect.size.w.checked_sub(padding[3])?; + rect.loc.y += padding[0]; + Some(rect) +} + +delegate_corner_radius!(State); diff --git a/src/wayland/handlers/data_control/ext.rs b/src/wayland/handlers/data_control/ext.rs new file mode 100644 index 0000000..6fc5530 --- /dev/null +++ b/src/wayland/handlers/data_control/ext.rs @@ -0,0 +1,8 @@ +use crate::state::State; +use smithay::wayland::selection::ext_data_control::{DataControlHandler, DataControlState}; + +impl DataControlHandler for State { + fn data_control_state(&mut self) -> &mut DataControlState { + &mut self.common.ext_data_control_state + } +} diff --git a/src/wayland/handlers/data_control/mod.rs b/src/wayland/handlers/data_control/mod.rs new file mode 100644 index 0000000..e72efaf --- /dev/null +++ b/src/wayland/handlers/data_control/mod.rs @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: GPL-3.0-only + +mod ext; +mod wlr; diff --git a/src/wayland/handlers/data_control/wlr.rs b/src/wayland/handlers/data_control/wlr.rs new file mode 100644 index 0000000..a932429 --- /dev/null +++ b/src/wayland/handlers/data_control/wlr.rs @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::state::State; +use smithay::wayland::selection::wlr_data_control::{DataControlHandler, DataControlState}; + +impl DataControlHandler for State { + fn data_control_state(&mut self) -> &mut DataControlState { + &mut self.common.wlr_data_control_state + } +} diff --git a/src/wayland/handlers/data_device.rs b/src/wayland/handlers/data_device.rs new file mode 100644 index 0000000..51dae6f --- /dev/null +++ b/src/wayland/handlers/data_device.rs @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{state::State, utils::prelude::SeatExt}; +use smithay::{ + input::{ + Seat, + dnd::{DnDGrab, DndGrabHandler, DndTarget, GrabType}, + pointer::{CursorImageStatus, CursorImageSurfaceData, Focus}, + }, + reexports::wayland_server::protocol::wl_surface::WlSurface, + utils::{IsAlive, Logical, Point}, + wayland::{ + compositor::{self, SurfaceAttributes}, + selection::data_device::{DataDeviceHandler, DataDeviceState, WaylandDndGrabHandler}, + }, +}; +use std::sync::Mutex; + +#[derive(Debug, Clone)] +pub struct DnDIcon { + pub surface: WlSurface, + pub offset: Point, +} + +pub fn get_dnd_icon(seat: &Seat) -> Option { + let userdata = seat.user_data(); + userdata + .get::>>() + .and_then(|x| x.lock().unwrap().clone()) + .filter(|icon| icon.surface.alive()) +} + +pub fn on_commit(surface: &WlSurface, seat: &Seat) { + let userdata = seat.user_data(); + + let Some(mut guard) = userdata + .get::>>() + .map(|guard| guard.lock().unwrap()) + else { + return; + }; + + let Some(icon) = guard.as_mut() else { + return; + }; + + if &icon.surface == surface { + compositor::with_states(surface, |states| { + let buffer_delta = states + .cached_state + .get::() + .current() + .buffer_delta + .take() + .unwrap_or_default(); + icon.offset += buffer_delta; + }); + } +} + +impl WaylandDndGrabHandler for State { + fn dnd_requested( + &mut self, + source: S, + icon: Option, + seat: Seat, + serial: smithay::utils::Serial, + type_: GrabType, + ) { + let user_data = seat.user_data(); + user_data.insert_if_missing_threadsafe::>, _>(Default::default); + + let offset = if let CursorImageStatus::Surface(ref surface) = seat.cursor_image_status() { + compositor::with_states(surface, |states| { + let hotspot = states + .data_map + .get::() + .unwrap() + .lock() + .unwrap() + .hotspot; + Point::from((-hotspot.x, -hotspot.y)) + }) + } else { + (0, 0).into() + }; + + *user_data + .get::>>() + .unwrap() + .lock() + .unwrap() = icon.map(|surface| DnDIcon { surface, offset }); + + match type_ { + GrabType::Pointer => { + let pointer = seat.get_pointer().unwrap(); + let start_data = pointer.grab_start_data().unwrap(); + pointer.set_grab( + self, + DnDGrab::new_pointer(&self.common.display_handle, start_data, source, seat), + serial, + Focus::Keep, + ); + } + GrabType::Touch => { + let touch = seat.get_touch().unwrap(); + let start_data = touch.grab_start_data().unwrap(); + touch.set_grab( + self, + DnDGrab::new_touch(&self.common.display_handle, start_data, source, seat), + serial, + ); + } + } + } +} + +impl DndGrabHandler for State { + fn dropped( + &mut self, + _target: Option>, + _validated: bool, + seat: Seat, + _location: Point, + ) { + if let Some(icon) = seat.user_data().get::>>() { + icon.lock().unwrap().take(); + } + } +} + +impl DataDeviceHandler for State { + fn data_device_state(&mut self) -> &mut DataDeviceState { + &mut self.common.data_device_state + } +} diff --git a/src/wayland/handlers/decoration.rs b/src/wayland/handlers/decoration.rs new file mode 100644 index 0000000..90ec585 --- /dev/null +++ b/src/wayland/handlers/decoration.rs @@ -0,0 +1,187 @@ +use std::{cell::RefCell, sync::Mutex}; + +use smithay::{ + desktop::Window, + reexports::{ + wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as XdgMode, + wayland_protocols_misc::server_decoration::server::org_kde_kwin_server_decoration::{ + Mode as KdeMode, OrgKdeKwinServerDecoration, + }, + wayland_server::protocol::wl_surface::WlSurface, + }, + wayland::{ + compositor::with_states, + seat::WaylandFocus, + shell::{ + kde::decoration::{KdeDecorationHandler, KdeDecorationState}, + xdg::{ToplevelSurface, decoration::XdgDecorationHandler}, + }, + }, +}; +use wayland_backend::protocol::WEnum; + +use crate::state::State; + +pub struct PreferredDecorationMode(RefCell>); + +impl PreferredDecorationMode { + pub fn is_unset(window: &Window) -> bool { + window + .user_data() + .get::() + .is_none() + } + + pub fn mode(window: &Window) -> Option { + let user_data = window.user_data(); + user_data.insert_if_missing(|| PreferredDecorationMode(RefCell::new(None))); + *user_data + .get::() + .unwrap() + .0 + .borrow() + } + + pub fn update(window: &Window, update: Option) { + let user_data = window.user_data(); + user_data.insert_if_missing(|| PreferredDecorationMode(RefCell::new(None))); + *user_data + .get::() + .unwrap() + .0 + .borrow_mut() = update; + } +} + +pub type KdeDecorationData = Mutex; +#[derive(Debug, Default)] +pub struct KdeDecorationSurfaceState { + pub mode: Option, + pub objs: Vec, +} + +impl XdgDecorationHandler for State { + fn new_decoration(&mut self, toplevel: ToplevelSurface) { + let shell = self.common.shell.read(); + if let Some(mapped) = shell.element_for_surface(toplevel.wl_surface()) { + let mode = if mapped.is_stack() { + XdgMode::ServerSide + } else { + XdgMode::ClientSide + }; + + if let Some((window, _)) = mapped + .windows() + .find(|(window, _)| window.wl_surface().as_deref() == Some(toplevel.wl_surface())) + && let Some(toplevel) = window.0.toplevel() + { + toplevel.with_pending_state(|state| { + state.decoration_mode = Some(mode); + }); + toplevel.send_configure(); + } + } + } + + fn request_mode(&mut self, toplevel: ToplevelSurface, mode: XdgMode) { + let shell = self.common.shell.read(); + if let Some(mapped) = shell.element_for_surface(toplevel.wl_surface()) { + if let Some((window, _)) = mapped + .windows() + .find(|(window, _)| window.wl_surface().as_deref() == Some(toplevel.wl_surface())) + && let Some(toplevel) = window.0.toplevel() + { + PreferredDecorationMode::update(&window.0, Some(mode)); + toplevel.with_pending_state(|state| { + state.decoration_mode = Some(mode); + }); + toplevel.send_configure(); + } + } else { + toplevel.with_pending_state(|state| state.decoration_mode = Some(mode)); + } + } + + fn unset_mode(&mut self, toplevel: ToplevelSurface) { + let shell = self.common.shell.read(); + if let Some(mapped) = shell.element_for_surface(toplevel.wl_surface()) + && let Some((window, _)) = mapped + .windows() + .find(|(window, _)| window.wl_surface().as_deref() == Some(toplevel.wl_surface())) + && let Some(toplevel) = window.0.toplevel() + { + PreferredDecorationMode::update(&window.0, None); + toplevel.with_pending_state(|state| { + state.decoration_mode = None; + }); + toplevel.send_configure(); + } + } +} + +impl KdeDecorationHandler for State { + fn kde_decoration_state(&self) -> &KdeDecorationState { + &self.common.kde_decoration_state + } + + fn new_decoration(&mut self, surface: &WlSurface, decoration: &OrgKdeKwinServerDecoration) { + let mode = if let Some(mapped) = self.common.shell.read().element_for_surface(surface) { + if mapped.is_stack() { + KdeMode::Server + } else { + KdeMode::Client + } + } else { + KdeMode::Client + }; + + with_states(surface, |states| { + let mut state = states + .data_map + .get_or_insert_threadsafe::(Default::default) + .lock() + .unwrap(); + + state.objs.push(decoration.clone()); + if state.mode.is_none() { + state.mode = Some(mode) + } + }); + + decoration.mode(mode); + } + + fn request_mode( + &mut self, + surface: &WlSurface, + decoration: &OrgKdeKwinServerDecoration, + mode: WEnum, + ) { + if let WEnum::Value(mode) = mode { + with_states(surface, |states| { + states + .data_map + .get_or_insert_threadsafe::(Default::default) + .lock() + .unwrap() + .mode = Some(mode); + }); + decoration.mode(mode); + } + } + + fn release(&mut self, decoration: &OrgKdeKwinServerDecoration, surface: &WlSurface) { + with_states(surface, |states| { + let mut state = states + .data_map + .get_or_insert_threadsafe::(Default::default) + .lock() + .unwrap(); + + state.objs.retain(|obj| obj != decoration); + if state.objs.is_empty() { + state.mode.take(); + } + }); + } +} diff --git a/src/wayland/handlers/dmabuf.rs b/src/wayland/handlers/dmabuf.rs new file mode 100644 index 0000000..4d9732b --- /dev/null +++ b/src/wayland/handlers/dmabuf.rs @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{ + state::{BackendData, State}, + wayland::handlers::compositor::frame_time_filter_fn, +}; +use smithay::{ + backend::{allocator::dmabuf::Dmabuf, renderer::element::Kind}, + desktop::WindowSurfaceType, + reexports::wayland_server::protocol::wl_surface::WlSurface, + wayland::{ + compositor::with_states, + dmabuf::{DmabufFeedback, DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier}, + }, +}; + +impl DmabufHandler for State { + fn dmabuf_state(&mut self) -> &mut DmabufState { + &mut self.common.dmabuf_state + } + + fn dmabuf_imported( + &mut self, + global: &DmabufGlobal, + dmabuf: Dmabuf, + import_notifier: ImportNotifier, + ) { + let client = import_notifier.client(); + match self.backend.dmabuf_imported(client.clone(), global, dmabuf) { + Err(err) => { + tracing::debug!(?err, "dmabuf import failed"); + import_notifier.failed() + } + Ok(_) => { + let _ = import_notifier.successful::(); + } + } + } + + fn new_surface_feedback( + &mut self, + surface: &WlSurface, + global: &DmabufGlobal, + ) -> Option { + let BackendData::Kms(kms) = &self.backend else { + return None; + }; + let shell = self.common.shell.read(); + + let (handle, output) = shell.workspace_for_surface(surface)?; + let is_fullscreen = shell + .workspaces + .space_for_handle(&handle)? + .fullscreen_surfaces + .iter() + .any(|f| { + f.ended_at.is_none() && f.surface.has_surface(surface, WindowSurfaceType::all()) + }); + + let node = kms + .drm_devices + .values() + .find(|device| { + device + .socket + .as_ref() + .map(|s| &s.dmabuf_global == global) + .unwrap_or(false) + })? + .inner + .render_node; + let kms_surface = kms + .drm_devices + .values() + .find_map(|device| device.inner.surfaces.values().find(|s| s.output == output))?; + let feedback = kms_surface.feedback.get(&node)?.clone(); + + Some(with_states(surface, |data| { + if is_fullscreen { + feedback + .primary_scanout_feedback + .unwrap_or(feedback.render_feedback) + } else if frame_time_filter_fn(data) == Kind::ScanoutCandidate { + feedback + .overlay_scanout_feedback + .unwrap_or(feedback.render_feedback) + } else { + feedback.render_feedback + } + })) + } +} diff --git a/src/wayland/handlers/drm.rs b/src/wayland/handlers/drm.rs new file mode 100644 index 0000000..e035bbb --- /dev/null +++ b/src/wayland/handlers/drm.rs @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{ + state::State, + wayland::protocols::drm::{DrmHandler, ImportError, delegate_wl_drm}, +}; +use smithay::{ + backend::{allocator::dmabuf::Dmabuf, drm::DrmNode}, + reexports::wayland_server::protocol::wl_buffer::WlBuffer, + wayland::dmabuf::DmabufGlobal, +}; + +impl DrmHandler> for State { + fn dmabuf_imported( + &mut self, + global: &DmabufGlobal, + dmabuf: Dmabuf, + ) -> Result, ImportError> { + self.backend + .dmabuf_imported(None, global, dmabuf) + .map_err(|_| ImportError::Failed) + } + + fn buffer_created(&mut self, _buffer: WlBuffer, _result: Option) {} +} + +delegate_wl_drm!(State; Option); diff --git a/src/wayland/handlers/drm_lease.rs b/src/wayland/handlers/drm_lease.rs new file mode 100644 index 0000000..d425335 --- /dev/null +++ b/src/wayland/handlers/drm_lease.rs @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::state::State; +use smithay::{ + backend::drm::DrmNode, + wayland::drm_lease::{ + DrmLease, DrmLeaseBuilder, DrmLeaseHandler, DrmLeaseRequest, DrmLeaseState, LeaseRejected, + }, +}; + +impl DrmLeaseHandler for State { + fn drm_lease_state(&mut self, node: DrmNode) -> &mut DrmLeaseState { + self.backend + .kms() + .drm_devices + .get_mut(&node) + .unwrap() + .inner + .leasing_global + .as_mut() + .unwrap() + } + + fn lease_request( + &mut self, + node: DrmNode, + request: DrmLeaseRequest, + ) -> Result { + let kms = self.backend.kms(); + let mut backend = kms + .drm_devices + .get_mut(&node) + .ok_or(LeaseRejected::default())? + .lock(); + let mut renderer = match kms.api.single_renderer(&backend.inner.render_node) { + Ok(renderer) => renderer, + Err(err) => { + tracing::warn!( + ?err, + "Failed to create renderer to disable direct scanout, denying lease" + ); + return Err(LeaseRejected::default()); + } + }; + if let Err(err) = backend.allow_overlay_scanout( + false, + &mut renderer, + &self.common.clock, + &self.common.shell, + ) { + tracing::warn!(?err, "Failed to disable direct scanout"); + return Err(LeaseRejected::default()); + } + + let mut builder = DrmLeaseBuilder::new(backend.drm.device()); + for conn in request.connectors { + if let Some((_, crtc)) = backend + .inner + .leased_connectors + .iter() + .find(|(handle, _)| *handle == conn) + { + builder.add_connector(conn); + builder.add_crtc(*crtc); + let planes = backend + .drm + .device() + .planes(crtc) + .map_err(LeaseRejected::with_cause)?; + let (primary_plane, primary_plane_claim) = planes + .primary + .iter() + .find_map(|plane| { + backend + .drm + .device_mut() + .claim_plane(plane.handle, *crtc) + .map(|claim| (plane, claim)) + }) + .ok_or_else(LeaseRejected::default)?; + builder.add_plane(primary_plane.handle, primary_plane_claim); + if let Some((cursor, claim)) = planes.cursor.into_iter().find_map(|plane| { + backend + .drm + .device_mut() + .claim_plane(plane.handle, *crtc) + .map(|claim| (plane, claim)) + }) { + builder.add_plane(cursor.handle, claim); + } + } else { + tracing::warn!( + ?conn, + "Lease requested for desktop connector, denying request" + ); + return Err(LeaseRejected::default()); + } + } + + Ok(builder) + } + + fn new_active_lease(&mut self, node: DrmNode, lease: DrmLease) { + if let Some(backend) = self.backend.kms().drm_devices.get_mut(&node) { + backend.inner.active_leases.push(lease); + } + // else the backend is gone, drop the lease + } + + fn lease_destroyed(&mut self, node: DrmNode, lease: u32) { + let kms = self.backend.kms(); + if let Some(backend) = kms.drm_devices.get_mut(&node) { + let mut backend = backend.lock(); + backend.inner.active_leases.retain(|l| l.id() != lease); + + if backend.inner.active_leases.is_empty() { + let mut renderer = match kms.api.single_renderer(&backend.inner.render_node) { + Ok(renderer) => renderer, + Err(err) => { + tracing::warn!(?err, "Failed to create renderer to enable direct scanout."); + return; + } + }; + if let Err(err) = backend.allow_overlay_scanout( + true, + &mut renderer, + &self.common.clock, + &self.common.shell, + ) { + tracing::warn!(?err, "Failed to enable direct scanout"); + } + } + } + } +} diff --git a/src/wayland/handlers/drm_syncobj.rs b/src/wayland/handlers/drm_syncobj.rs new file mode 100644 index 0000000..e167cf1 --- /dev/null +++ b/src/wayland/handlers/drm_syncobj.rs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::state::{BackendData, State}; +use smithay::wayland::drm_syncobj::{DrmSyncobjHandler, DrmSyncobjState}; + +impl DrmSyncobjHandler for State { + fn drm_syncobj_state(&mut self) -> Option<&mut DrmSyncobjState> { + let kms = match &mut self.backend { + BackendData::Kms(kms) => kms, + _ => unreachable!(), + }; + kms.syncobj_state.as_mut() + } +} diff --git a/src/wayland/handlers/foreign_toplevel_list.rs b/src/wayland/handlers/foreign_toplevel_list.rs new file mode 100644 index 0000000..4d07525 --- /dev/null +++ b/src/wayland/handlers/foreign_toplevel_list.rs @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{state::State, wayland::protocols::toplevel_info::ToplevelInfoHandler}; +use smithay::wayland::foreign_toplevel_list::{ + ForeignToplevelListHandler, ForeignToplevelListState, +}; + +impl ForeignToplevelListHandler for State { + fn foreign_toplevel_list_state(&mut self) -> &mut ForeignToplevelListState { + &mut self.toplevel_info_state_mut().foreign_toplevel_list + } +} diff --git a/src/wayland/handlers/fractional_scale.rs b/src/wayland/handlers/fractional_scale.rs new file mode 100644 index 0000000..dcc8d7f --- /dev/null +++ b/src/wayland/handlers/fractional_scale.rs @@ -0,0 +1,59 @@ +use crate::{state::State, utils::prelude::SeatExt}; +use smithay::{ + desktop::utils::surface_primary_scanout_output, + reexports::wayland_server::protocol::wl_surface::WlSurface, + wayland::{ + compositor::{get_parent, with_states}, + fractional_scale::{FractionalScaleHandler, with_fractional_scale}, + }, +}; + +impl FractionalScaleHandler for State { + fn new_fractional_scale(&mut self, surface: WlSurface) { + // Here we can set the initial fractional scale + // + // First we look if the surface already has a primary scan-out output, if not + // we test if the surface is a subsurface and try to use the primary scan-out output + // of the root surface. If the root also has no primary scan-out output we just try + // to use the first output of the toplevel. + // If the surface is the root we also try to use the first output of the toplevel. + // + // If all the above tests do not lead to a output we just use the active output + // of the last active seat (which will also be the output a toplevel will + // initially be placed on) + let output = with_states(&surface, |states| { + surface_primary_scanout_output(&surface, states) + }) + .or_else(|| { + let mut root = surface.clone(); + while let Some(parent) = get_parent(&root) { + root = parent; + } + + (root != surface) + .then(|| { + with_states(&root, |states| { + surface_primary_scanout_output(&root, states) + }) + }) + .flatten() + .or_else(|| { + self.common + .shell + .read() + .visible_output_for_surface(&surface) + .cloned() + }) + }) + .unwrap_or_else(|| self.common.shell.read().seats.last_active().active_output()); + + with_states(&surface, |states| { + with_fractional_scale(states, |fractional_scale| { + // The 1.0 clamp is a workaround for Chromium + // TODO: remove if Chromium ever gets fixed + fractional_scale + .set_preferred_scale(output.current_scale().fractional_scale().max(1.0)); + }); + }); + } +} diff --git a/src/wayland/handlers/idle_inhibit.rs b/src/wayland/handlers/idle_inhibit.rs new file mode 100644 index 0000000..26e34bf --- /dev/null +++ b/src/wayland/handlers/idle_inhibit.rs @@ -0,0 +1,18 @@ +use smithay::{ + reexports::wayland_server::protocol::wl_surface::WlSurface, + wayland::idle_inhibit::IdleInhibitHandler, +}; + +use crate::state::State; + +impl IdleInhibitHandler for State { + fn inhibit(&mut self, surface: WlSurface) { + self.common.idle_inhibiting_surfaces.insert(surface); + self.common.idle_notifier_state.set_is_inhibited(true); + } + + fn uninhibit(&mut self, surface: WlSurface) { + self.common.idle_inhibiting_surfaces.remove(&surface); + self.common.refresh_idle_inhibit(); + } +} diff --git a/src/wayland/handlers/idle_notify.rs b/src/wayland/handlers/idle_notify.rs new file mode 100644 index 0000000..9e5f558 --- /dev/null +++ b/src/wayland/handlers/idle_notify.rs @@ -0,0 +1,11 @@ +use smithay::wayland::idle_notify::IdleNotifierHandler; + +use crate::state::State; + +impl IdleNotifierHandler for State { + fn idle_notifier_state( + &mut self, + ) -> &mut smithay::wayland::idle_notify::IdleNotifierState { + &mut self.common.idle_notifier_state + } +} diff --git a/src/wayland/handlers/image_capture_source.rs b/src/wayland/handlers/image_capture_source.rs new file mode 100644 index 0000000..08a8695 --- /dev/null +++ b/src/wayland/handlers/image_capture_source.rs @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{ + state::State, + wayland::protocols::{ + image_capture_source::ImageCaptureSourceKind, toplevel_info::window_from_ext, + }, +}; +use smithay::{ + output::Output, + wayland::{ + foreign_toplevel_list::ForeignToplevelHandle, + image_capture_source::{ + ImageCaptureSource, ImageCaptureSourceHandler, OutputCaptureSourceHandler, + OutputCaptureSourceState, ToplevelCaptureSourceHandler, ToplevelCaptureSourceState, + }, + }, +}; + +impl ImageCaptureSourceHandler for State { + fn source_destroyed(&mut self, _source: ImageCaptureSource) {} +} + +impl OutputCaptureSourceHandler for State { + fn output_capture_source_state(&mut self) -> &mut OutputCaptureSourceState { + &mut self.common.output_capture_source_state + } + + fn output_source_created(&mut self, source: ImageCaptureSource, output: &Output) { + source + .user_data() + .insert_if_missing(|| ImageCaptureSourceKind::Output(output.downgrade())); + } +} + +impl ToplevelCaptureSourceHandler for State { + fn toplevel_capture_source_state(&mut self) -> &mut ToplevelCaptureSourceState { + &mut self.common.toplevel_capture_source_state + } + + fn toplevel_source_created( + &mut self, + source: ImageCaptureSource, + toplevel: ForeignToplevelHandle, + ) { + let data = match window_from_ext(self, toplevel) { + Some(toplevel) => ImageCaptureSourceKind::Toplevel(toplevel.downgrade()), + None => ImageCaptureSourceKind::Destroyed, + }; + source.user_data().insert_if_missing(|| data); + } +} diff --git a/src/wayland/handlers/image_copy_capture/mod.rs b/src/wayland/handlers/image_copy_capture/mod.rs new file mode 100644 index 0000000..85a01c9 --- /dev/null +++ b/src/wayland/handlers/image_copy_capture/mod.rs @@ -0,0 +1,474 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use std::{borrow::Borrow, collections::HashMap, sync::Mutex}; + +use smithay::{ + backend::{ + allocator::{Fourcc, Modifier}, + egl::EGLDevice, + renderer::{ + damage::OutputDamageTracker, + gles::{Capability, GlesRenderer}, + glow::GlowRenderer, + utils::with_renderer_surface_state, + }, + }, + desktop::space::SpaceElement, + input::{Seat, pointer::PointerHandle}, + output::Output, + reexports::wayland_server::protocol::{wl_pointer::WlPointer, wl_shm::Format as ShmFormat}, + utils::{Buffer as BufferCoords, Point, Size, Transform}, + wayland::{ + dmabuf::get_dmabuf, + image_capture_source::ImageCaptureSource, + image_copy_capture::{ + BufferConstraints, CaptureFailureReason, CursorSession, CursorSessionRef, + DmabufConstraints, Frame, FrameRef, ImageCopyCaptureHandler, ImageCopyCaptureState, + Session, SessionRef, + }, + seat::WaylandFocus, + }, +}; + +use crate::{ + shell::{CosmicSurface, CursorGeometry, Shell}, + state::{BackendData, State}, + utils::prelude::{ + OutputExt, PointExt, PointGlobalExt, PointLocalExt, RectExt, RectLocalExt, SeatExt, + }, + wayland::protocols::image_capture_source::ImageCaptureSourceKind, +}; + +mod render; +mod user_data; +pub use self::render::*; +use self::user_data::*; +pub use self::user_data::{FrameHolder, ImageCopySessions, SessionData, SessionHolder}; + +fn default_cursor_size() -> Size { + Size::new(64, 64) +} + +fn seat_for_wl_pointer<'a>(shell: &'a Shell, pointer: &WlPointer) -> Option<&'a Seat> { + let pointer_handle = PointerHandle::::from_resource(pointer)?; + shell + .seats + .iter() + .find(|seat| seat.get_pointer().is_some_and(|p| p == pointer_handle)) +} + +pub fn cursor_capture_constraints(cursor_geometry: Option) -> BufferConstraints { + let size = if let Some(cursor_geometry) = cursor_geometry { + let mut size = cursor_geometry.geometry.size; + // Client shouldn't try to allocate 0x0 buffer + if size == Size::new(0, 0) { + size = Size::new(1, 1); + } + size + } else { + default_cursor_size() + }; + BufferConstraints { + size, + shm: vec![ShmFormat::Argb8888], + dma: None, + } +} + +impl ImageCopyCaptureHandler for State { + fn image_copy_capture_state(&mut self) -> &mut ImageCopyCaptureState { + &mut self.common.image_copy_capture_state + } + + fn capture_constraints(&mut self, source: &ImageCaptureSource) -> Option { + match ImageCaptureSourceKind::from_source(source) { + ImageCaptureSourceKind::Output(weak) => weak + .upgrade() + .and_then(|output| constraints_for_output(&output, &mut self.backend)), + ImageCaptureSourceKind::Workspace(handle) => { + let shell = self.common.shell.read(); + let output = shell.workspaces.space_for_handle(&handle)?.output(); + constraints_for_output(output, &mut self.backend) + } + ImageCaptureSourceKind::Toplevel(window) => { + if let Some(window) = window.upgrade() { + constraints_for_toplevel(&window, &mut self.backend) + } else { + None + } + } + ImageCaptureSourceKind::Destroyed => None, + } + } + + fn cursor_capture_constraints( + &mut self, + _source: &ImageCaptureSource, + pointer: &WlPointer, + ) -> Option { + let shell = self.common.shell.read(); + let seat = seat_for_wl_pointer(&shell, pointer)?; + let cursor_geometry = seat.cursor_geometry((0.0, 0.0), self.common.clock.now()); + Some(cursor_capture_constraints(cursor_geometry)) + } + + fn new_session(&mut self, session: Session) { + match ImageCaptureSourceKind::from_source(&session.source()) { + ImageCaptureSourceKind::Output(weak) => { + let Some(mut output) = weak.upgrade() else { + session.stop(); + return; + }; + + session.user_data().insert_if_missing_threadsafe(|| { + Mutex::new(SessionUserData::new(OutputDamageTracker::from_output( + &output, + ))) + }); + + output.add_session(session); + } + ImageCaptureSourceKind::Workspace(handle) => { + let mut shell = self.common.shell.write(); + let Some(workspace) = shell.workspaces.space_for_handle_mut(&handle) else { + session.stop(); + return; + }; + + session.user_data().insert_if_missing_threadsafe(|| { + Mutex::new(SessionUserData::new(OutputDamageTracker::from_output( + workspace.output(), + ))) + }); + workspace.add_session(session); + } + ImageCaptureSourceKind::Toplevel(toplevel) => { + let Some(mut toplevel) = toplevel.upgrade() else { + session.stop(); + return; + }; + + let size = toplevel.geometry().size.to_physical(1); + session.user_data().insert_if_missing_threadsafe(|| { + Mutex::new(SessionUserData::new(OutputDamageTracker::new( + size, + 1.0, + Transform::Normal, + ))) + }); + toplevel.add_session(session); + } + ImageCaptureSourceKind::Destroyed => { + session.stop(); + } + } + } + + fn new_cursor_session(&mut self, session: CursorSession) { + let (pointer_loc, pointer_size, hotspot) = { + let shell = self.common.shell.read(); + if let Some(seat) = seat_for_wl_pointer(&shell, &session.pointer()) { + let pointer = seat.get_pointer().unwrap(); + let pointer_loc = pointer.current_location().to_i32_round().as_global(); + + let (pointer_size, hotspot) = if let Some(CursorGeometry { geometry, hotspot }) = + seat.cursor_geometry((0.0, 0.0), self.common.clock.now()) + { + (geometry.size, hotspot) + } else { + (default_cursor_size(), Point::from((0, 0))) + }; + + (pointer_loc, pointer_size, hotspot) + } else { + (Point::new(0, 0), default_cursor_size(), Point::from((0, 0))) + } + }; + + session.user_data().insert_if_missing_threadsafe(|| { + Mutex::new(SessionUserData::new(OutputDamageTracker::new( + pointer_size.to_logical(1, Transform::Normal).to_physical(1), + 1.0, + Transform::Normal, + ))) + }); + + match ImageCaptureSourceKind::from_source(&session.source()) { + ImageCaptureSourceKind::Output(weak) => { + let Some(mut output) = weak.upgrade() else { + return; + }; + + if output.geometry().contains(pointer_loc) { + let buffer_pos = pointer_loc + .to_local(&output) + .as_logical() + .to_f64() + .to_buffer( + output.current_scale().fractional_scale(), + output.current_transform(), + &output + .current_mode() + .map(|mode| { + mode.size + .to_f64() + .to_logical(output.current_scale().fractional_scale()) + }) + .unwrap_or(Size::from((0.0, 0.0))), + ) + .to_i32_round(); + session.set_cursor_hotspot(hotspot); + session.set_cursor_pos(Some(buffer_pos)); + } + + output.add_cursor_session(session); + } + ImageCaptureSourceKind::Workspace(handle) => { + let mut shell = self.common.shell.write(); + let Some(workspace) = shell.workspaces.space_for_handle_mut(&handle) else { + return; + }; + + let output = workspace.output().clone(); + if output.geometry().contains(pointer_loc) { + let buffer_pos = pointer_loc + .to_local(&output) + .as_logical() + .to_f64() + .to_buffer( + output.current_scale().fractional_scale(), + output.current_transform(), + &output + .current_mode() + .map(|mode| { + mode.size + .to_f64() + .to_logical(output.current_scale().fractional_scale()) + }) + .unwrap_or(Size::from((0.0, 0.0))), + ) + .to_i32_round(); + session.set_cursor_hotspot(hotspot); + session.set_cursor_pos(Some(buffer_pos)); + } + + workspace.add_cursor_session(session); + } + ImageCaptureSourceKind::Toplevel(toplevel) => { + let Some(mut toplevel) = toplevel.upgrade() else { + return; + }; + + let shell = self.common.shell.read(); + if let Some(element) = shell.element_for_surface(&toplevel) + && element.has_active_window(&toplevel) + && let Some(workspace) = shell.space_for(element) + && let Some(geometry) = workspace.element_geometry(element) + { + let mut surface_geo = element.active_window_geometry().as_local(); + surface_geo.loc += geometry.loc; + let global_geo = surface_geo.to_global(workspace.output()); + if global_geo.contains(pointer_loc) { + let buffer_pos = (pointer_loc - global_geo.loc).as_logical().to_buffer( + 1, + Transform::Normal, + &toplevel.geometry().size, + ); + session.set_cursor_hotspot(hotspot); + session.set_cursor_pos(Some(buffer_pos)); + } + } + + toplevel.add_cursor_session(session); + } + ImageCaptureSourceKind::Destroyed => { + session.stop(); + } + } + } + + fn frame(&mut self, session: &SessionRef, frame: Frame) { + match ImageCaptureSourceKind::from_source(&session.source()) { + ImageCaptureSourceKind::Output(weak) => { + let Some(mut output) = weak.upgrade() else { + return; + }; + + output.add_frame(session.clone(), frame); + self.backend.schedule_render(&output); + } + ImageCaptureSourceKind::Workspace(handle) => { + render_workspace_to_buffer(self, session, frame, handle) + } + ImageCaptureSourceKind::Toplevel(toplevel) => { + let Some(toplevel) = toplevel.upgrade() else { + return; + }; + + render_window_to_buffer(self, session, frame, &toplevel) + } + ImageCaptureSourceKind::Destroyed => { + frame.fail(CaptureFailureReason::Stopped); + } + } + } + + fn cursor_frame(&mut self, session: &CursorSessionRef, frame: Frame) { + if !session.has_cursor() { + frame.success(Transform::Normal, Vec::new(), self.common.clock.now()); + return; + } + + let shell = self.common.shell.read(); + if let Some(seat) = seat_for_wl_pointer(&shell, &session.pointer()).cloned() { + drop(shell); + render_cursor_to_buffer(self, session, frame, &seat); + } + } + + fn frame_aborted(&mut self, frame: FrameRef) { + let shell = self.common.shell.read(); + for mut output in shell.outputs().cloned() { + output.remove_frame(&frame); + } + } + + fn session_destroyed(&mut self, session: SessionRef) { + match ImageCaptureSourceKind::from_source(&session.source()) { + ImageCaptureSourceKind::Output(weak) => { + if let Some(mut output) = weak.upgrade() { + output.remove_session(&session); + } + } + ImageCaptureSourceKind::Workspace(handle) => { + if let Some(workspace) = self + .common + .shell + .write() + .workspaces + .space_for_handle_mut(&handle) + { + workspace.remove_session(&session) + } + } + ImageCaptureSourceKind::Toplevel(toplevel) => { + if let Some(mut toplevel) = toplevel.upgrade() { + toplevel.remove_session(&session); + } + } + ImageCaptureSourceKind::Destroyed => {} + } + } + + fn cursor_session_destroyed(&mut self, session: CursorSessionRef) { + match ImageCaptureSourceKind::from_source(&session.source()) { + ImageCaptureSourceKind::Output(weak) => { + if let Some(mut output) = weak.upgrade() { + output.remove_cursor_session(&session); + } + } + ImageCaptureSourceKind::Workspace(handle) => { + if let Some(workspace) = self + .common + .shell + .write() + .workspaces + .space_for_handle_mut(&handle) + { + workspace.remove_cursor_session(&session) + } + } + ImageCaptureSourceKind::Toplevel(toplevel) => { + if let Some(mut toplevel) = toplevel.upgrade() { + toplevel.remove_cursor_session(&session) + } + } + ImageCaptureSourceKind::Destroyed => {} + } + } +} + +fn constraints_for_output(output: &Output, backend: &mut BackendData) -> Option { + let mode = match output.current_mode() { + Some(mode) => mode.size.to_logical(1).to_buffer(1, Transform::Normal), + None => { + return None; + } + }; + + let mut renderer = backend + .offscreen_renderer(|kms| { + kms.target_node_for_output(output) + .or(*kms.primary_node.read().unwrap()) + }) + .unwrap(); + Some(constraints_for_renderer(mode, renderer.as_mut())) +} + +fn constraints_for_toplevel( + surface: &CosmicSurface, + backend: &mut BackendData, +) -> Option { + let size = surface.geometry().size.to_buffer(1, Transform::Normal); + let wl_surface = surface.wl_surface()?; + + let mut renderer = backend + .offscreen_renderer(|kms| { + let dma_node = with_renderer_surface_state(&wl_surface, |state| { + let buffer = state.buffer()?; + let dmabuf = get_dmabuf(buffer).ok()?; + dmabuf.node() + }) + .flatten(); + + dma_node.or(*kms.primary_node.read().unwrap()) + }) + .unwrap(); + + Some(constraints_for_renderer(size, renderer.as_mut())) +} + +fn constraints_for_renderer( + size: Size, + renderer: &mut GlowRenderer, +) -> BufferConstraints { + let mut constraints = BufferConstraints { + size, + shm: vec![ShmFormat::Abgr8888, ShmFormat::Xbgr8888], + dma: None, + }; + + if (renderer as &dyn Borrow) + .borrow() + .capabilities() + .contains(&Capability::_10Bit) + { + constraints + .shm + .extend([ShmFormat::Abgr2101010, ShmFormat::Xbgr2101010]); + } + + if let Some(node) = EGLDevice::device_for_display(renderer.egl_context().display()) + .ok() + .and_then(|device| device.try_get_render_node().ok().flatten()) + { + constraints.dma = Some(DmabufConstraints { + node, + formats: renderer + .egl_context() + .dmabuf_render_formats() + .iter() + .fold( + HashMap::>::new(), + |mut map, format| { + map.entry(format.code).or_default().push(format.modifier); + map + }, + ) + .into_iter() + .collect::>(), + }); + } + + constraints +} diff --git a/src/wayland/handlers/image_copy_capture/render.rs b/src/wayland/handlers/image_copy_capture/render.rs new file mode 100644 index 0000000..f8b308e --- /dev/null +++ b/src/wayland/handlers/image_copy_capture/render.rs @@ -0,0 +1,967 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use calloop::LoopHandle; +use smithay::{ + backend::{ + allocator::{Buffer, Fourcc, format::get_transparent}, + renderer::{ + BufferType, Color32F, ExportMem, ImportAll, ImportMem, Offscreen, Renderer, + buffer_dimensions, buffer_type, + damage::{Error as DTError, OutputDamageTracker, RenderOutputResult}, + element::{ + RenderElement, UnderlyingStorage, + utils::{Relocate, RelocateRenderElement}, + }, + gles::{GlesError, GlesRenderbuffer}, + sync::SyncPoint, + utils::with_renderer_surface_state, + }, + }, + desktop::space::SpaceElement, + input::Seat, + output::{Output, OutputNoMode}, + reexports::wayland_server::protocol::wl_buffer::WlBuffer, + utils::{ + Buffer as BufferCoords, IsAlive, Logical, Physical, Point, Rectangle, Scale, Size, + Transform, + }, + wayland::{ + dmabuf::get_dmabuf, + image_copy_capture::{CaptureFailureReason, CursorSessionRef, Frame, SessionRef}, + seat::WaylandFocus, + shm::{shm_format_to_fourcc, with_buffer_contents, with_buffer_contents_mut}, + }, +}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use tracing::warn; + +use crate::{ + backend::render::{ + CursorMode, ElementFilter, RendererRef, + cursor::{self, CursorRenderElement}, + element::{AsGlowRenderer, CosmicElement, DamageElement}, + render_workspace, + wayland::SurfaceRenderElement, + }, + shell::{CosmicMappedRenderElement, CosmicSurface, WorkspaceRenderElement}, + state::{Common, KmsNodes, State}, + utils::prelude::{PointExt, PointGlobalExt, RectExt, RectLocalExt, SeatExt}, + wayland::{ + handlers::image_copy_capture::{ + SessionData, SessionUserData, constraints_for_output, constraints_for_toplevel, + }, + protocols::workspace::WorkspaceHandle, + }, +}; + +use super::{ + super::data_device::get_dnd_icon, cursor_capture_constraints, user_data::SessionHolder, +}; + +pub fn render_element_buffers( + renderer: &mut R, + elements: &[E], +) -> Vec +where + R: AsGlowRenderer, + E: RenderElement, +{ + elements + .iter() + .filter_map(|elem| match elem.underlying_storage(renderer) { + Some(UnderlyingStorage::Wayland(buffer)) => Some(buffer.clone()), + Some(UnderlyingStorage::Memory(_)) | None => None, + }) + .collect() +} + +pub struct PendingImageCopyData { + pub frame: Frame, + pub damage: Vec>, + pub sync: SyncPoint, + // Hold reference so `wl_buffer` isn't released and sync point isn't signaled + // until image copy completes. + _buffers: Vec, +} + +impl PendingImageCopyData { + /// Send `success` to image copy frame, once sync point is reached + pub fn send_success_when_ready( + self, + transform: Transform, + loop_handle: &LoopHandle<'static, LoopData>, + presented: impl Into, + ) { + let presented = presented.into(); + if self.sync.is_reached() { + self.frame.success(transform, self.damage, presented); + } else if let Some(fence_fd) = self.sync.export() { + let source = calloop::generic::Generic::new( + fence_fd, + calloop::Interest::READ, + calloop::Mode::OneShot, + ); + let mut data = Some(self); + loop_handle + .insert_source(source, move |_, _, _| { + let data = data.take().unwrap(); + data.frame.success(transform, data.damage, presented); + Ok(calloop::PostAction::Remove) + }) + .expect("Failed to wait on sync point"); + } else { + // Should be able to export fence; but otherwise wait + let _ = self.sync.wait(); + self.frame.success(transform, self.damage, presented); + } + } +} + +pub fn submit_buffer( + frame: Frame, + renderer: &mut R, + offscreen: Option<&mut R::Framebuffer<'_>>, + transform: Transform, + damage: Option<&[Rectangle]>, + mut sync: SyncPoint, + buffers: Vec, +) -> Result, R::Error> +where + R: ExportMem + AsGlowRenderer, +{ + let Some(damage) = damage else { + frame.success( + transform, + None, + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or(Duration::ZERO), + ); + return Ok(None); + }; + + let buffer = frame.buffer(); + let buffer_size = buffer_dimensions(&buffer).unwrap(); + + if let Some(fb) = offscreen { + assert!(matches!(buffer_type(&buffer), Some(BufferType::Shm))); + if let Err(err) = with_buffer_contents_mut(&buffer, |ptr, len, data| { + let offset = data.offset; + let width = data.width; + let height = data.height; + let stride = data.stride; + let format = shm_format_to_fourcc(data.format) + .expect("We should be able to convert all hardcoded shm screencopy formats"); + + // number of bytes per pixel + // TODO: compute from data.format + let pixelsize = 4i32; + + // ensure consistency, the SHM handler of smithay should ensure this + assert!((offset + (height - 1) * stride + width * pixelsize) as usize <= len); + + // ensure rendering is done + renderer.wait(&sync)?; + + let format = get_transparent(format).unwrap_or(format); + let mapping = + renderer.copy_framebuffer(fb, Rectangle::from_size(buffer_size), format)?; + let gl_data = renderer.map_texture(&mapping)?; + assert!((width * height * pixelsize) as usize <= gl_data.len()); + + for i in 0..height { + unsafe { + std::ptr::copy_nonoverlapping::( + gl_data.as_ptr().offset((width * pixelsize * i) as isize), + ptr.offset((offset + stride * i) as isize), + (width * pixelsize) as usize, + ); + } + } + + // We've already waited on the sync point and copied from the texture + // TODO: Don't block in this function, and defer copy until ready? + sync = SyncPoint::signaled(); + + Ok(()) + }) + .map_err(|err| R::from_gles_error(GlesError::BufferAccessError(err))) + .and_then(|x| x) + { + frame.fail(CaptureFailureReason::Unknown); + return Err(err); + } + } + + Ok(Some(PendingImageCopyData { + frame, + damage: damage + .iter() + .map(|rect| { + let logical = rect.to_logical(1); + logical.to_buffer(1, transform.invert(), &buffer_size.to_logical(1, transform)) + }) + .collect(), + sync, + _buffers: buffers, + })) +} + +pub fn render_session( + renderer: &mut R, + session: &SessionData, + frame: Frame, + transform: Transform, + render_fn: F, +) -> Result, DTError> +where + R: AsGlowRenderer, + F: for<'d> FnOnce( + &WlBuffer, + &mut R, + Option<&mut R::Framebuffer<'_>>, + &'d mut OutputDamageTracker, + usize, + Vec>, + ) -> Result< + ( + RenderOutputResult<'d>, + Vec, + ), + DTError, + >, +{ + let mut session_user_data = session.lock().unwrap(); + + let buffer = frame.buffer(); + + let mut age = 1; + if matches!(buffer_type(&buffer), Some(BufferType::Shm)) { + let size = buffer_dimensions(&buffer).ok_or(DTError::OutputNoMode(OutputNoMode))?; + let format = with_buffer_contents(&buffer, |_, _, data| { + shm_format_to_fourcc(data.format) + .expect("We should be able to convert all hardcoded shm screencopy formats") + }) + .map_err(|_| DTError::OutputNoMode(OutputNoMode))?; + + // Re-allocate if context id, size, or format are different + session_user_data + .offscreen + .take_if(|(context_id, renderbuffer)| { + renderer.glow_renderer().context_id() != *context_id + || renderbuffer.size() != size + || renderbuffer.format() != Some(format) + }); + + if session_user_data.offscreen.is_none() { + let renderbuffer = Offscreen::::create_buffer(renderer, format, size) + .map_err(DTError::Rendering)?; + session_user_data.offscreen = + Some((renderer.glow_renderer().context_id(), renderbuffer)); + // If we're allocating a new offscreen buffer, we need to re-render everything + // (or copy the contexts of the shm buffer) + age = 0; + } + } else { + // If for some reason a capture session is used for shm, but then changes to dmabuf capture, + // remove the offscreen buffer. + session_user_data.offscreen = None; + } + + let SessionUserData { dt, offscreen } = &mut *session_user_data; + let mut fb = offscreen + .as_mut() + .map(|(_, tex)| renderer.bind(tex).map_err(DTError::Rendering)) + .transpose()?; + let (result, buffers) = render_fn( + &frame.buffer(), + renderer, + fb.as_mut(), + dt, + age, + frame.damage(), + )?; + + submit_buffer( + frame, + renderer, + fb.as_mut(), + transform, + result.damage.map(|x| x.as_slice()), + result.sync, + buffers, + ) + .map_err(DTError::Rendering) +} + +pub fn render_workspace_to_buffer( + state: &mut State, + session: &SessionRef, + frame: Frame, + handle: WorkspaceHandle, +) { + let shell = state.common.shell.read(); + let Some(workspace) = shell.workspaces.space_for_handle(&handle) else { + return; + }; + + let mut output = workspace.output().clone(); + let idx = shell.workspaces.idx_for_handle(&output, &handle).unwrap(); + std::mem::drop(shell); + + let mode = output + .current_mode() + .map(|mode| mode.size.to_logical(1).to_buffer(1, Transform::Normal)); + + let buffer = frame.buffer(); + let buffer_size = buffer_dimensions(&buffer).unwrap(); + if mode != Some(buffer_size) { + let Some(constraints) = constraints_for_output(&output, &mut state.backend) else { + output.remove_session(session); + return; + }; + session.update_constraints(constraints); + if let Some(data) = session.user_data().get::() { + *data.lock().unwrap() = SessionUserData::new(OutputDamageTracker::from_output(&output)); + } + frame.fail(CaptureFailureReason::BufferConstraints); + return; + } + + fn render_fn<'d, R>( + buffer: &WlBuffer, + renderer: &mut R, + offscreen: Option<&mut R::Framebuffer<'_>>, + dt: &'d mut OutputDamageTracker, + age: usize, + additional_damage: Vec>, + draw_cursor: bool, + common: &mut Common, + output: &Output, + handle: (WorkspaceHandle, usize), + ) -> Result< + ( + RenderOutputResult<'d>, + Vec, + ), + DTError, + > + where + R: AsGlowRenderer, + R::TextureId: Send + Clone + 'static, + CosmicElement: RenderElement, + CosmicMappedRenderElement: RenderElement, + WorkspaceRenderElement: RenderElement, + { + let cursor_mode = if draw_cursor { + CursorMode::All + } else { + CursorMode::None + }; + + let area = output + .current_mode() + .ok_or(DTError::OutputNoMode(OutputNoMode)) + .map( + |mode| { + mode.size + .to_logical(1) + .to_buffer(1, Transform::Normal) + .to_f64() + }, /* TODO: Mode is Buffer..., why is this Physical in the first place */ + )?; + let additional_damage = (!additional_damage.is_empty()).then(|| { + additional_damage + .into_iter() + .map(|rect| { + rect.to_f64() + .to_logical( + output.current_scale().fractional_scale(), + output.current_transform(), + &area, + ) + .to_i32_round() + }) + .collect() + }); + + let (res, elements) = if let Ok(dmabuf) = get_dmabuf(buffer) { + let mut dmabuf = dmabuf.clone(); + let mut fb = renderer.bind(&mut dmabuf).map_err(DTError::Rendering)?; + render_workspace( + None, + renderer, + &mut fb, + dt, + age, + additional_damage, + &common.shell, + None, + common.clock.now(), + output, + None, + handle, + cursor_mode, + ElementFilter::ExcludeWorkspaceOverview, + )? + } else { + let target = offscreen.expect("shm buffers should have an offscreen target"); + render_workspace( + None, + renderer, + target, + dt, + age, + additional_damage, + &common.shell, + None, + common.clock.now(), + output, + None, + handle, + cursor_mode, + ElementFilter::ExcludeWorkspaceOverview, + )? + }; + + let buffers = render_element_buffers(renderer, &elements); + + Ok((res, buffers)) + } + + let draw_cursor = session.draw_cursor(); + let transform = output.current_transform(); + let common = &mut state.common; + + let renderer = match state.backend.offscreen_renderer(|kms| { + let render_node = kms + .target_node_for_output(&output) + .or(*kms.primary_node.read().unwrap())?; + let target_node = get_dmabuf(&buffer) + .ok() + .and_then(|dma| dma.node()) + .unwrap_or(render_node); + + let buffer_format = match buffer_type(&buffer) { + Some(BufferType::Dma) => Some(get_dmabuf(&buffer).unwrap().format().code), + Some(BufferType::Shm) => { + with_buffer_contents(&buffer, |_, _, data| shm_format_to_fourcc(data.format)) + .unwrap() + } + _ => None, + }; + + Some(KmsNodes { + render_node, + target_node, + copy_format: buffer_format.unwrap_or(Fourcc::Abgr8888), + }) + }) { + Ok(renderer) => renderer, + Err(err) => { + warn!(?err, "Couldn't use node for screencopy"); + frame.fail(CaptureFailureReason::Unknown); + return; + } + }; + let result = match renderer { + RendererRef::Glow(renderer) => { + match render_session( + renderer, + session.user_data().get::().unwrap(), + frame, + transform, + |buffer, renderer, offscreen, dt, age, additional_damage| { + render_fn( + buffer, + renderer, + offscreen, + dt, + age, + additional_damage, + draw_cursor, + common, + &output, + (handle, idx), + ) + }, + ) { + Ok(frame) => frame, + Err(err) => { + tracing::warn!(?err, "Failed to render to screencopy buffer"); + None + } + } + } + RendererRef::GlMulti(mut renderer) => { + match render_session( + &mut renderer, + session.user_data().get::().unwrap(), + frame, + transform, + |buffer, renderer, offscreen, dt, age, additional_damage| { + render_fn( + buffer, + renderer, + offscreen, + dt, + age, + additional_damage, + draw_cursor, + common, + &output, + (handle, idx), + ) + }, + ) { + Ok(frame) => frame, + Err(err) => { + tracing::warn!(?err, "Failed to render to screencopy buffer"); + None + } + } + } + }; + + if let Some(pending_image_copy_data) = result { + pending_image_copy_data.send_success_when_ready( + transform, + &common.event_loop_handle, + common.clock.now(), + ); + } +} + +smithay::render_elements! { + pub WindowCaptureElement where R: ImportAll + ImportMem + AsGlowRenderer, R::TextureId: Send; + WaylandElement=SurfaceRenderElement, + CursorElement=RelocateRenderElement>, + DamageElement=DamageElement, +} + +pub fn render_window_to_buffer( + state: &mut State, + session: &SessionRef, + frame: Frame, + toplevel: &CosmicSurface, +) { + if !toplevel.alive() { + toplevel.clone().remove_session(session); + return; + } + + let buffer = frame.buffer(); + let geometry = toplevel.geometry(); + let buffer_size = buffer_dimensions(&buffer).unwrap(); + if buffer_size != geometry.size.to_buffer(1, Transform::Normal) { + let Some(constraints) = constraints_for_toplevel(toplevel, &mut state.backend) else { + toplevel.clone().remove_session(session); + return; + }; + session.update_constraints(constraints); + + if let Some(data) = session.user_data().get::() { + let size = geometry.size.to_physical(1); + *data.lock().unwrap() = + SessionUserData::new(OutputDamageTracker::new(size, 1.0, Transform::Normal)); + } + frame.fail(CaptureFailureReason::BufferConstraints); + return; + } + + fn render_fn<'d, R>( + buffer: &WlBuffer, + renderer: &mut R, + offscreen: Option<&mut R::Framebuffer<'_>>, + dt: &'d mut OutputDamageTracker, + age: usize, + additional_damage: Vec>, + draw_cursor: bool, + common: &mut Common, + toplevel: &CosmicSurface, + geometry: Rectangle, + ) -> Result< + ( + RenderOutputResult<'d>, + Vec, + ), + DTError, + > + where + R: AsGlowRenderer, + R::TextureId: Send + Clone + 'static, + CosmicElement: RenderElement, + CosmicMappedRenderElement: RenderElement, + { + let mut elements: Vec<_> = additional_damage + .into_iter() + .filter_map(|rect| { + let logical_rect = rect.to_logical( + 1, + Transform::Normal, + &geometry.size.to_buffer(1, Transform::Normal), + ); + logical_rect.intersection(Rectangle::from_size(geometry.size)) + }) + .map(DamageElement::new) + .map(WindowCaptureElement::::from) + .collect(); + + let shell = common.shell.read(); + let blur_strength = 9; // TODO + let seat = shell.seats.last_active().clone(); + let pointer = seat.get_pointer().unwrap(); + let pointer_loc = pointer.current_location().to_i32_round().as_global(); + let mut location = None; + if let Some(element) = shell.element_for_surface(toplevel) + && pointer + .current_focus() + .and_then(|f| f.toplevel(&shell)) + .as_ref() + == Some(toplevel) + && element.has_active_window(toplevel) + && let Some(workspace) = shell.space_for(element) + && let Some(geometry) = workspace.element_geometry(element) + { + let mut surface_geo = element.active_window_geometry().as_local(); + surface_geo.loc += geometry.loc; + let global_geo = surface_geo.to_global(workspace.output()); + if global_geo.contains(pointer_loc) { + location = Some((pointer_loc - global_geo.loc).as_logical().to_f64()); + } + }; + std::mem::drop(shell); + + if let Some(location) = location { + if draw_cursor { + cursor::draw_cursor( + renderer, + &seat, + location, + 1.0.into(), + 1.0, + common.clock.now(), + blur_strength, + true, + &mut |elem, hotspot| { + elements.push(WindowCaptureElement::CursorElement( + RelocateRenderElement::from_element( + elem, + Point::from((-hotspot.x, -hotspot.y)), + Relocate::Relative, + ), + )); + }, + ); + } + + // TODO cosmic-workspaces wants to omit, but metadata cursor capture in portal should + // still include dnd surface in window capture buffer? + if draw_cursor && let Some(dnd_icon) = get_dnd_icon(&seat) { + cursor::draw_dnd_icon( + renderer, + &dnd_icon.surface, + (location + dnd_icon.offset.to_f64()).to_i32_round(), + 1.0, + blur_strength, + &mut |elem| { + elements.push( + RelocateRenderElement::from_element( + CursorRenderElement::Surface(elem), + Point::new(0, 0), + Relocate::Relative, + ) + .into(), + ) + }, + ); + } + } + + toplevel.push_render_elements( + renderer, + (-geometry.loc.x, -geometry.loc.y).into(), + Scale::from(1.0), + 1.0, + None, + None, + false, + [0; 4], + blur_strength, + &mut |elem| elements.push(elem.into()), + None, + ); + + let res = if let Ok(dmabuf) = get_dmabuf(buffer) { + let mut dmabuf_clone = dmabuf.clone(); + let mut fb = renderer + .bind(&mut dmabuf_clone) + .map_err(DTError::Rendering)?; + dt.render_output(renderer, &mut fb, age, &elements, Color32F::TRANSPARENT)? + } else { + let fb = offscreen.expect("shm buffer should have an offscreen target"); + dt.render_output(renderer, fb, age, &elements, Color32F::TRANSPARENT)? + }; + + let buffers = render_element_buffers(renderer, &elements); + + Ok((res, buffers)) + } + + let common = &mut state.common; + let draw_cursor = session.draw_cursor(); + + let renderer = match state.backend.offscreen_renderer(|kms| { + get_dmabuf(&buffer) + .ok() + .and_then(|dmabuf| dmabuf.node()) + .or_else(|| { + toplevel + .wl_surface() + .and_then(|wl_surface| { + with_renderer_surface_state(&wl_surface, |state| { + let buffer = state.buffer()?; + let dmabuf = get_dmabuf(buffer).ok()?; + dmabuf.node() + }) + }) + .flatten() + }) + .or(*kms.primary_node.read().unwrap()) + }) { + Ok(renderer) => renderer, + Err(err) => { + warn!(?err, "Couldn't use node for screencopy"); + frame.fail(CaptureFailureReason::Unknown); + return; + } + }; + let result = match renderer { + RendererRef::Glow(renderer) => match render_session( + renderer, + session.user_data().get::().unwrap(), + frame, + Transform::Normal, + |buffer, renderer, offscreen, dt, age, additional_damage| { + render_fn( + buffer, + renderer, + offscreen, + dt, + age, + additional_damage, + draw_cursor, + common, + toplevel, + geometry, + ) + }, + ) { + Ok(frame) => frame, + Err(err) => { + tracing::warn!(?err, "Failed to render to screencopy buffer"); + None + } + }, + RendererRef::GlMulti(mut renderer) => match render_session( + &mut renderer, + session.user_data().get::().unwrap(), + frame, + Transform::Normal, + |buffer, renderer, offscreen, dt, age, additional_damage| { + render_fn( + buffer, + renderer, + offscreen, + dt, + age, + additional_damage, + draw_cursor, + common, + toplevel, + geometry, + ) + }, + ) { + Ok(frame) => frame, + Err(err) => { + tracing::warn!(?err, "Failed to render to screencopy buffer"); + None + } + }, + }; + + if let Some(pending_image_copy_data) = result { + pending_image_copy_data.send_success_when_ready( + Transform::Normal, + &common.event_loop_handle, + common.clock.now(), + ); + } +} + +pub fn render_cursor_to_buffer( + state: &mut State, + session: &CursorSessionRef, + frame: Frame, + seat: &Seat, +) { + let buffer = frame.buffer(); + let cursor_geometry = seat.cursor_geometry((0.0, 0.0), state.common.clock.now()); + let constraints = cursor_capture_constraints(cursor_geometry); + let buffer_size = buffer_dimensions(&buffer).unwrap(); + if buffer_size != constraints.size { + session.update_constraints(constraints.clone()); + if let Some(data) = session.user_data().get::() { + *data.lock().unwrap() = SessionUserData::new(OutputDamageTracker::new( + constraints + .size + .to_logical(1, Transform::Normal) + .to_physical(1), + 1.0, + Transform::Normal, + )); + } + frame.fail(CaptureFailureReason::BufferConstraints); + return; + } + + fn render_fn<'d, R>( + buffer: &WlBuffer, + renderer: &mut R, + offscreen: Option<&mut R::Framebuffer<'_>>, + dt: &'d mut OutputDamageTracker, + age: usize, + additional_damage: Vec>, + common: &mut Common, + seat: &Seat, + ) -> Result< + ( + RenderOutputResult<'d>, + Vec, + ), + DTError, + > + where + R: AsGlowRenderer, + R::TextureId: Send + Clone + 'static, + CosmicElement: RenderElement, + CosmicMappedRenderElement: RenderElement, + { + let mut elements: Vec<_> = additional_damage + .into_iter() + .filter_map(|rect| { + let logical_rect = rect.to_logical(1, Transform::Normal, &Size::from((64, 64))); + logical_rect.intersection(Rectangle::from_size((64, 64).into())) + }) + .map(DamageElement::new) + .map(WindowCaptureElement::from) + .collect(); + + cursor::draw_cursor( + renderer, + seat, + Point::from((0.0, 0.0)), + 1.0.into(), + 1.0, + common.clock.now(), + 0, + true, + &mut |elem, _| { + elements.push( + RelocateRenderElement::from_element(elem, (0, 0), Relocate::Relative).into(), + ) + }, + ); + + let res = if let Ok(dmabuf) = get_dmabuf(buffer) { + let mut dmabuf_clone = dmabuf.clone(); + let mut fb = renderer + .bind(&mut dmabuf_clone) + .map_err(DTError::Rendering)?; + dt.render_output(renderer, &mut fb, age, &elements, [0.0, 0.0, 0.0, 0.0])? + } else { + let fb = offscreen.expect("shm buffers should have offscreen target"); + dt.render_output(renderer, fb, age, &elements, [0.0, 0.0, 0.0, 0.0])? + }; + + let buffers = render_element_buffers(renderer, &elements); + + Ok((res, buffers)) + } + + let common = &mut state.common; + let renderer = match state + .backend + .offscreen_renderer(|kms| *kms.primary_node.read().unwrap()) + { + Ok(renderer) => renderer, + Err(err) => { + warn!(?err, "Couldn't use node for screencopy"); + frame.fail(CaptureFailureReason::Unknown); + return; + } + }; + let result = match renderer { + RendererRef::Glow(renderer) => { + match render_session( + renderer, + session.user_data().get::().unwrap(), + frame, + Transform::Normal, + |buffer, renderer, offscreen, dt, age, additional_damage| { + render_fn( + buffer, + renderer, + offscreen, + dt, + age, + additional_damage, + common, + seat, + ) + }, + ) { + Ok(frame) => frame, + Err(err) => { + tracing::warn!(?err, "Failed to render to screencopy buffer"); + None + } + } + } + RendererRef::GlMulti(mut renderer) => { + match render_session( + &mut renderer, + session.user_data().get::().unwrap(), + frame, + Transform::Normal, + |buffer, renderer, offscreen, dt, age, additional_damage| { + render_fn( + buffer, + renderer, + offscreen, + dt, + age, + additional_damage, + common, + seat, + ) + }, + ) { + Ok(frame) => frame, + Err(err) => { + tracing::warn!(?err, "Failed to render to screencopy buffer"); + None + } + } + } + }; + + if let Some(pending_image_copy_data) = result { + pending_image_copy_data.send_success_when_ready( + Transform::Normal, + &common.event_loop_handle, + common.clock.now(), + ); + } +} diff --git a/src/wayland/handlers/image_copy_capture/user_data.rs b/src/wayland/handlers/image_copy_capture/user_data.rs new file mode 100644 index 0000000..e9f3623 --- /dev/null +++ b/src/wayland/handlers/image_copy_capture/user_data.rs @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use std::{cell::RefCell, sync::Mutex}; + +use smithay::{ + backend::renderer::{ + ContextId, + damage::OutputDamageTracker, + gles::{GlesRenderbuffer, GlesTexture}, + }, + output::Output, + wayland::image_copy_capture::{ + CursorSession, CursorSessionRef, Frame, FrameRef, Session, SessionRef, + }, +}; + +use crate::shell::{CosmicSurface, Workspace}; + +type ImageCopySessionsData = RefCell; +type PendingImageCopyBuffers = Mutex>; + +pub type SessionData = Mutex; + +pub struct SessionUserData { + pub dt: OutputDamageTracker, + pub offscreen: Option<(ContextId, GlesRenderbuffer)>, +} + +impl SessionUserData { + pub fn new(tracker: OutputDamageTracker) -> SessionUserData { + SessionUserData { + dt: tracker, + offscreen: None, + } + } +} + +#[derive(Debug, Default)] +pub struct ImageCopySessions { + sessions: Vec, + cursor_sessions: Vec, +} + +pub trait SessionHolder { + fn add_session(&mut self, session: Session); + fn remove_session(&mut self, session: &SessionRef); + fn sessions(&self) -> Vec; + + fn add_cursor_session(&mut self, session: CursorSession); + fn remove_cursor_session(&mut self, session: &CursorSessionRef); + fn cursor_sessions(&self) -> Vec; +} + +pub trait FrameHolder { + fn add_frame(&mut self, session: SessionRef, frame: Frame); + fn remove_frame(&mut self, frame: &FrameRef); + fn take_pending_frames(&self) -> Vec<(SessionRef, Frame)>; +} + +impl SessionHolder for Output { + fn add_session(&mut self, session: Session) { + self.user_data() + .insert_if_missing(ImageCopySessionsData::default); + self.user_data() + .get::() + .unwrap() + .borrow_mut() + .sessions + .push(session); + } + + fn remove_session(&mut self, session: &SessionRef) { + self.user_data() + .get::() + .unwrap() + .borrow_mut() + .sessions + .retain(|s| s != session); + } + + fn sessions(&self) -> Vec { + self.user_data() + .get::() + .map_or(Vec::new(), |sessions| { + sessions + .borrow() + .sessions + .iter() + .map(|s| (*s).clone()) + .collect() + }) + } + + fn add_cursor_session(&mut self, session: CursorSession) { + self.user_data() + .insert_if_missing(ImageCopySessionsData::default); + self.user_data() + .get::() + .unwrap() + .borrow_mut() + .cursor_sessions + .push(session); + } + + fn remove_cursor_session(&mut self, session: &CursorSessionRef) { + self.user_data() + .get::() + .unwrap() + .borrow_mut() + .cursor_sessions + .retain(|s| s != session); + } + + fn cursor_sessions(&self) -> Vec { + self.user_data() + .get::() + .map_or(Vec::new(), |sessions| { + sessions + .borrow() + .cursor_sessions + .iter() + .map(|s| (*s).clone()) + .collect() + }) + } +} + +impl FrameHolder for Output { + fn add_frame(&mut self, session: SessionRef, frame: Frame) { + self.user_data() + .insert_if_missing_threadsafe(PendingImageCopyBuffers::default); + self.user_data() + .get::() + .unwrap() + .lock() + .unwrap() + .push((session, frame)); + } + fn remove_frame(&mut self, frame: &FrameRef) { + if let Some(pending) = self.user_data().get::() { + pending.lock().unwrap().retain(|(_, f)| f != frame); + } + } + fn take_pending_frames(&self) -> Vec<(SessionRef, Frame)> { + self.user_data() + .get::() + .map(|pending| std::mem::take(&mut *pending.lock().unwrap())) + .unwrap_or_default() + } +} + +impl SessionHolder for Workspace { + fn add_session(&mut self, session: Session) { + self.image_copy.sessions.push(session); + } + + fn remove_session(&mut self, session: &SessionRef) { + self.image_copy.sessions.retain(|s| s != session); + } + fn sessions(&self) -> Vec { + self.image_copy + .sessions + .iter() + .map(|s| (*s).clone()) + .collect() + } + + fn add_cursor_session(&mut self, session: CursorSession) { + self.image_copy.cursor_sessions.push(session); + } + + fn remove_cursor_session(&mut self, session: &CursorSessionRef) { + self.image_copy.cursor_sessions.retain(|s| s != session); + } + fn cursor_sessions(&self) -> Vec { + self.image_copy + .cursor_sessions + .iter() + .map(|s| (*s).clone()) + .collect() + } +} + +impl SessionHolder for CosmicSurface { + fn add_session(&mut self, session: Session) { + self.user_data() + .insert_if_missing(ImageCopySessionsData::default); + self.user_data() + .get::() + .unwrap() + .borrow_mut() + .sessions + .push(session); + } + + fn remove_session(&mut self, session: &SessionRef) { + self.user_data() + .get::() + .unwrap() + .borrow_mut() + .sessions + .retain(|s| s != session); + } + fn sessions(&self) -> Vec { + self.user_data() + .get::() + .map_or(Vec::new(), |sessions| { + sessions + .borrow() + .sessions + .iter() + .map(|s| (*s).clone()) + .collect() + }) + } + + fn add_cursor_session(&mut self, session: CursorSession) { + self.user_data() + .insert_if_missing(ImageCopySessionsData::default); + self.user_data() + .get::() + .unwrap() + .borrow_mut() + .cursor_sessions + .push(session); + } + + fn remove_cursor_session(&mut self, session: &CursorSessionRef) { + self.user_data() + .get::() + .unwrap() + .borrow_mut() + .cursor_sessions + .retain(|s| s != session); + } + + fn cursor_sessions(&self) -> Vec { + self.user_data() + .get::() + .map_or(Vec::new(), |sessions| { + sessions + .borrow() + .cursor_sessions + .iter() + .map(|s| (*s).clone()) + .collect() + }) + } +} diff --git a/src/wayland/handlers/input_method.rs b/src/wayland/handlers/input_method.rs new file mode 100644 index 0000000..081d804 --- /dev/null +++ b/src/wayland/handlers/input_method.rs @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::state::State; +use smithay::{ + desktop::{PopupKind, PopupManager, space::SpaceElement}, + reexports::wayland_server::protocol::wl_surface::WlSurface, + utils::Rectangle, + wayland::input_method::{InputMethodHandler, PopupSurface}, +}; +use tracing::warn; + +impl InputMethodHandler for State { + fn new_popup(&mut self, surface: PopupSurface) { + self.common + .shell + .read() + .unconstrain_popup(&PopupKind::from(surface.clone())); + + if let Err(err) = self.common.popups.track_popup(PopupKind::from(surface)) { + warn!("Failed to track popup: {}", err); + } + } + + fn dismiss_popup(&mut self, surface: PopupSurface) { + if let Some(parent) = surface.get_parent().map(|parent| parent.surface.clone()) { + let _ = PopupManager::dismiss_popup(&parent, &PopupKind::from(surface)); + } + } + + fn parent_geometry(&self, parent: &WlSurface) -> Rectangle { + self.common + .shell + .read() + .element_for_surface(parent) + .map(|e| e.geometry()) + .unwrap_or_default() + } + + fn popup_repositioned(&mut self, popup: PopupSurface) { + self.common.shell.read().unconstrain_popup(&popup.into()); + } +} diff --git a/src/wayland/handlers/keyboard_layout.rs b/src/wayland/handlers/keyboard_layout.rs new file mode 100644 index 0000000..ac1fda8 --- /dev/null +++ b/src/wayland/handlers/keyboard_layout.rs @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::state::State; +use crate::wayland::protocols::keyboard_layout::{KeyboardLayoutHandler, KeyboardLayoutState}; + +impl KeyboardLayoutHandler for State { + fn keyboard_layout_state(&mut self) -> &mut KeyboardLayoutState { + &mut self.common.keyboard_layout_state + } +} diff --git a/src/wayland/handlers/keyboard_shortcuts_inhibit.rs b/src/wayland/handlers/keyboard_shortcuts_inhibit.rs new file mode 100644 index 0000000..dc25d20 --- /dev/null +++ b/src/wayland/handlers/keyboard_shortcuts_inhibit.rs @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::state::State; +use smithay::wayland::keyboard_shortcuts_inhibit::{ + KeyboardShortcutsInhibitHandler, KeyboardShortcutsInhibitState, KeyboardShortcutsInhibitor, +}; + +impl KeyboardShortcutsInhibitHandler for State { + fn keyboard_shortcuts_inhibit_state(&mut self) -> &mut KeyboardShortcutsInhibitState { + &mut self.common.keyboard_shortcuts_inhibit_state + } + + fn new_inhibitor(&mut self, inhibitor: KeyboardShortcutsInhibitor) { + // TODO: Restrict what apps can inhibit shortcuts + inhibitor.activate(); + } +} diff --git a/src/wayland/handlers/layer_shell.rs b/src/wayland/handlers/layer_shell.rs new file mode 100644 index 0000000..9653557 --- /dev/null +++ b/src/wayland/handlers/layer_shell.rs @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{shell::PendingLayer, utils::prelude::*}; +use smithay::{ + desktop::{LayerSurface, PopupKind, WindowSurfaceType, layer_map_for_output}, + output::Output, + reexports::wayland_server::protocol::wl_output::WlOutput, + wayland::shell::{ + wlr_layer::{ + Layer, LayerSurface as WlrLayerSurface, WlrLayerShellHandler, WlrLayerShellState, + }, + xdg::PopupSurface, + }, +}; + +impl WlrLayerShellHandler for State { + fn shell_state(&mut self) -> &mut WlrLayerShellState { + &mut self.common.layer_shell_state + } + + fn new_layer_surface( + &mut self, + surface: WlrLayerSurface, + wl_output: Option, + _layer: Layer, + namespace: String, + ) { + let mut shell = self.common.shell.write(); + let seat = shell.seats.last_active().clone(); + let output = wl_output + .as_ref() + .and_then(Output::from_resource) + .unwrap_or_else(|| seat.active_output()); + shell.pending_layers.push(PendingLayer { + surface: LayerSurface::new(surface, namespace), + output, + seat, + }); + } + + fn new_popup(&mut self, _parent: WlrLayerSurface, popup: PopupSurface) { + self.common + .shell + .read() + .unconstrain_popup(&PopupKind::from(popup.clone())); + + if let Err(err) = popup.send_configure() { + tracing::warn!("Unable to configure popup. {err:?}",); + } else { + self.common + .popups + .track_popup(PopupKind::from(popup)) + .unwrap(); + } + } + + fn layer_destroyed(&mut self, surface: WlrLayerSurface) { + let mut shell = self.common.shell.write(); + let maybe_output = shell + .outputs() + .find(|o| { + let map = layer_map_for_output(o); + map.layer_for_surface(surface.wl_surface(), WindowSurfaceType::TOPLEVEL) + .is_some() + }) + .cloned(); + + if let Some(output) = maybe_output { + { + let mut map = layer_map_for_output(&output); + let layer = map + .layer_for_surface(surface.wl_surface(), WindowSurfaceType::TOPLEVEL) + .unwrap() + .clone(); + map.unmap_layer(&layer); + } + + shell.workspaces.recalculate(); + + self.backend.schedule_render(&output); + } + } +} diff --git a/src/wayland/handlers/mod.rs b/src/wayland/handlers/mod.rs new file mode 100644 index 0000000..651eadc --- /dev/null +++ b/src/wayland/handlers/mod.rs @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pub mod a11y; +pub mod background_effect; +pub mod buffer; +pub mod compositor; +pub mod corner_radius; +pub mod data_control; +pub mod data_device; +pub mod decoration; +pub mod dmabuf; +pub mod drm; +pub mod drm_lease; +pub mod drm_syncobj; +pub mod foreign_toplevel_list; +pub mod fractional_scale; +pub mod idle_inhibit; +pub mod idle_notify; +pub mod image_capture_source; +pub mod image_copy_capture; +pub mod input_method; +pub mod keyboard_layout; +pub mod keyboard_shortcuts_inhibit; +pub mod layer_shell; +pub mod output; +pub mod output_configuration; +pub mod output_power; +pub mod overlap_notify; +pub mod pointer_constraints; +pub mod pointer_warp; +pub mod primary_selection; +pub mod seat; +pub mod security_context; +pub mod selection; +pub mod session_lock; +pub mod shm; +pub mod tablet_manager; +pub mod toplevel_info; +pub mod toplevel_management; +pub mod workspace; +pub mod xdg_activation; +pub mod xdg_foreign; +pub mod xdg_shell; +pub mod xwayland_keyboard_grab; +pub mod xwayland_shell; diff --git a/src/wayland/handlers/output.rs b/src/wayland/handlers/output.rs new file mode 100644 index 0000000..9972d0f --- /dev/null +++ b/src/wayland/handlers/output.rs @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::state::State; +use smithay::wayland::output::OutputHandler; + +impl OutputHandler for State {} diff --git a/src/wayland/handlers/output_configuration.rs b/src/wayland/handlers/output_configuration.rs new file mode 100644 index 0000000..6ab678f --- /dev/null +++ b/src/wayland/handlers/output_configuration.rs @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use cosmic_comp_config::output::comp::{OutputConfig, OutputState, TransformDef}; +use smithay::{output::Output, utils::Point}; +use tracing::{error, warn}; + +use crate::{ + state::State, + utils::prelude::OutputExt, + wayland::protocols::output_configuration::{ + ModeConfiguration, OutputConfiguration, OutputConfigurationHandler, + OutputConfigurationState, delegate_output_configuration, + }, +}; + +use std::cell::RefCell; + +impl OutputConfigurationHandler for State { + fn output_configuration_state(&mut self) -> &mut OutputConfigurationState { + &mut self.common.output_configuration_state + } + + fn test_configuration(&mut self, conf: Vec<(Output, OutputConfiguration)>) -> bool { + self.output_configuration(true, conf) + } + fn apply_configuration(&mut self, conf: Vec<(Output, OutputConfiguration)>) -> bool { + self.output_configuration(false, conf) + } + + fn request_xwayland_primary(&mut self, primary_output: Option) { + for output in self.common.output_configuration_state.outputs() { + output.config_mut().xwayland_primary = + primary_output.as_ref().is_some_and(|o| *o == output); + } + self.common.update_xwayland_primary_output(); + self.common + .config + .write_outputs(self.common.output_configuration_state.outputs()); + } +} + +impl State { + fn output_configuration( + &mut self, + test_only: bool, + mut conf: Vec<(Output, OutputConfiguration)>, + ) -> bool { + if conf + .iter() + .all(|(_, conf)| matches!(conf, OutputConfiguration::Disabled)) + { + return false; // we don't allow the user to accidentally disable all their outputs + } + + // sanitize negative positions + { + let (offset_x, offset_y) = conf.iter().fold((0, 0), |mut offset, (_, conf)| { + if let OutputConfiguration::Enabled { + position: Some(position), + .. + } = conf + { + if position.x.is_negative() { + offset.0 = offset.0.max(position.x.abs()); + } + if position.y.is_negative() { + offset.1 = offset.1.max(position.y.abs()); + } + } + offset + }); + + if offset_x > 0 || offset_y > 0 { + for (output, conf) in conf.iter_mut() { + if let OutputConfiguration::Enabled { position, .. } = conf { + let current_config = output + .user_data() + .get::>() + .unwrap() + .borrow(); + + *position = Some( + position.unwrap_or(Point::from(( + current_config.position.0 as i32, + current_config.position.1 as i32, + ))) + Point::from((offset_x, offset_y)), + ); + } + } + } + } + + let mut backups = Vec::new(); + for (output, conf) in &conf { + { + let mut current_config = output + .user_data() + .get::>() + .unwrap() + .borrow_mut(); + backups.push((output, current_config.clone())); + + if let OutputConfiguration::Enabled { + mirroring, + mode, + scale, + transform, + position, + adaptive_sync, + } = conf + { + match mode { + Some(ModeConfiguration::Mode(mode)) => { + current_config.mode = + ((mode.size.w, mode.size.h), Some(mode.refresh as u32)); + } + Some(ModeConfiguration::Custom { size, refresh }) => { + current_config.mode = ((size.w, size.h), refresh.map(|x| x as u32)); + } + _ => {} + } + if let Some(scale) = scale { + current_config.scale = *scale; + } + if let Some(transform) = transform { + current_config.transform = match transform { + smithay::utils::Transform::Normal => TransformDef::Normal, + smithay::utils::Transform::_90 => TransformDef::_90, + smithay::utils::Transform::_180 => TransformDef::_180, + smithay::utils::Transform::_270 => TransformDef::_270, + smithay::utils::Transform::Flipped => TransformDef::Flipped, + smithay::utils::Transform::Flipped90 => TransformDef::Flipped90, + smithay::utils::Transform::Flipped180 => TransformDef::Flipped180, + smithay::utils::Transform::Flipped270 => TransformDef::Flipped270, + } + } + if let Some(position) = position { + current_config.position = (position.x as u32, position.y as u32); + } + if let Some(vrr) = adaptive_sync { + current_config.vrr = *vrr; + } + if let Some(mirror) = mirroring { + current_config.enabled = OutputState::Mirroring(mirror.name()); + } else { + current_config.enabled = OutputState::Enabled; + } + } else { + current_config.enabled = OutputState::Disabled; + } + } + } + + let mut backend = self.backend.lock(); + let res = backend.apply_config_for_outputs( + test_only, + &self.common.event_loop_handle, + self.common.config.dynamic_conf.screen_filter(), + self.common.shell.clone(), + &mut self.common.workspace_state.update(), + &self.common.xdg_activation_state, + self.common.startup_done.clone(), + &self.common.clock, + ); + if let Err(err) = res { + warn!("Failed to apply config. Resetting: {:?}", err); + for (output, backup) in backups { + { + let mut current_config = output + .user_data() + .get::>() + .unwrap() + .borrow_mut(); + *current_config = backup; + } + } + if !test_only + && let Err(err) = backend.apply_config_for_outputs( + false, + &self.common.event_loop_handle, + self.common.config.dynamic_conf.screen_filter(), + self.common.shell.clone(), + &mut self.common.workspace_state.update(), + &self.common.xdg_activation_state, + self.common.startup_done.clone(), + &self.common.clock, + ) + { + error!("Failed to reset output config: {:?}", err); + } + return false; + } + self.common.refresh(); + + for output in conf + .iter() + .filter(|(_, c)| { + matches!( + c, + OutputConfiguration::Enabled { + mirroring: None, + .. + } + ) + }) + .map(|(o, _)| o) + { + self.common.output_configuration_state.enable_head(output); + } + for output in conf + .iter() + .filter(|(_, c)| { + matches!( + c, + OutputConfiguration::Disabled + | OutputConfiguration::Enabled { + mirroring: Some(_), + .. + } + ) + }) + .map(|(o, _)| o) + { + self.common.output_configuration_state.disable_head(output); + } + self.common + .config + .write_outputs(self.common.output_configuration_state.outputs()); + self.common.event_loop_handle.insert_idle(move |state| { + state.common.output_configuration_state.update(); + }); + + true + } +} + +delegate_output_configuration!(State); diff --git a/src/wayland/handlers/output_power.rs b/src/wayland/handlers/output_power.rs new file mode 100644 index 0000000..2675009 --- /dev/null +++ b/src/wayland/handlers/output_power.rs @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use smithay::output::Output; + +use crate::{ + backend::kms::Surface, + state::{BackendData, State}, + utils::prelude::OutputExt, + wayland::protocols::output_power::{ + OutputPowerHandler, OutputPowerState, delegate_output_power, + }, +}; + +pub fn set_all_surfaces_dpms_on(state: &mut State) { + let mut changed = false; + for surface in kms_surfaces(state) { + if !surface.get_dpms() { + surface.set_dpms(true); + changed = true; + } + } + + if changed { + OutputPowerState::refresh(state); + } +} + +fn kms_surfaces(state: &mut State) -> impl Iterator { + if let BackendData::Kms(kms_state) = &mut state.backend { + Some( + kms_state + .drm_devices + .values_mut() + .flat_map(|device| device.inner.surfaces.values_mut()), + ) + } else { + None + } + .into_iter() + .flatten() +} + +// Get KMS `Surface` for output, and for all outputs mirroring it +fn kms_surfaces_for_output<'a>( + state: &'a mut State, + output: &'a Output, +) -> impl Iterator + 'a { + kms_surfaces(state).filter(move |surface| { + surface.output == *output || surface.output.mirroring().as_ref() == Some(output) + }) +} + +// Get KMS `Surface` for output +fn primary_kms_surface_for_output<'a>( + state: &'a mut State, + output: &Output, +) -> Option<&'a mut Surface> { + kms_surfaces(state).find(|surface| surface.output == *output) +} + +impl OutputPowerHandler for State { + fn output_power_state(&mut self) -> &mut OutputPowerState { + &mut self.common.output_power_state + } + + fn get_dpms(&mut self, output: &Output) -> Option { + let surface = primary_kms_surface_for_output(self, output)?; + Some(surface.get_dpms()) + } + + fn set_dpms(&mut self, output: &Output, on: bool) { + for surface in kms_surfaces_for_output(self, output) { + surface.set_dpms(on); + } + } +} + +delegate_output_power!(State); diff --git a/src/wayland/handlers/overlap_notify.rs b/src/wayland/handlers/overlap_notify.rs new file mode 100644 index 0000000..729d366 --- /dev/null +++ b/src/wayland/handlers/overlap_notify.rs @@ -0,0 +1,55 @@ +use smithay::{ + desktop::{LayerSurface, WindowSurfaceType, layer_map_for_output}, + output::Output, + reexports::wayland_protocols_wlr::layer_shell::v1::server::zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, +}; + +use crate::{ + state::State, + wayland::protocols::overlap_notify::{ + OverlapNotifyHandler, OverlapNotifyState, delegate_overlap_notify, + }, +}; + +impl OverlapNotifyHandler for State { + fn overlap_notify_state(&mut self) -> &mut OverlapNotifyState { + &mut self.common.overlap_notify_state + } + + fn layer_surface_from_resource(&self, resource: ZwlrLayerSurfaceV1) -> Option { + self.common + .layer_shell_state + .layer_surfaces() + .find(|l| l.shell_surface() == &resource) + .and_then(|l| { + let shell = self.common.shell.read(); + let outputs = shell.outputs(); + let ret = outputs.map(|o| layer_map_for_output(o)).find_map(|s| { + s.layer_for_surface(l.wl_surface(), WindowSurfaceType::ALL) + .cloned() + }); + drop(shell); + ret + }) + } + + fn outputs(&self) -> impl Iterator { + let shell = self.common.shell.read(); + shell.outputs().cloned().collect::>().into_iter() + } + + fn active_workspaces( + &self, + ) -> impl Iterator { + let shell = self.common.shell.read(); + shell + .workspaces + .sets + .iter() + .map(|(_, set)| set.workspaces[set.active].handle) + .collect::>() + .into_iter() + } +} + +delegate_overlap_notify!(State); diff --git a/src/wayland/handlers/pointer_constraints.rs b/src/wayland/handlers/pointer_constraints.rs new file mode 100644 index 0000000..9ce5f63 --- /dev/null +++ b/src/wayland/handlers/pointer_constraints.rs @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{shell::CosmicSurface, state::State, utils::prelude::*}; +use smithay::{ + input::pointer::PointerHandle, + reexports::wayland_server::protocol::wl_surface::WlSurface, + utils::{Logical, Point}, + wayland::{pointer_constraints::PointerConstraintsHandler, seat::WaylandFocus}, +}; + +pub use smithay::wayland::pointer_constraints::{PointerConstraintRef, with_pointer_constraint}; + +impl PointerConstraintsHandler for State { + fn new_constraint(&mut self, surface: &WlSurface, pointer: &PointerHandle) { + let seat = self + .common + .shell + .read() + .seats + .iter() + .find(|s| s.get_pointer().as_ref() == Some(pointer)) + .cloned(); + + let (is_under, is_focused, surface_location) = if let Some(seat) = seat { + seat.set_pointer_constraint_hint(None); + let current_output = seat.active_output(); + let position = seat.get_pointer().unwrap().current_location().as_global(); + let shell = self.common.shell.read(); + let under = State::surface_under(position, ¤t_output, &shell); + let mut surface_location = None; + + let is_under = if let Some((target, target_loc)) = under + && let Some(under_surface) = target.wl_surface() + { + if *under_surface == *surface { + surface_location = Some(target_loc); + true + } else { + CosmicSurface::surface_tree_offset(surface, &under_surface).is_some_and( + |offset| { + surface_location = Some(target_loc - offset.to_f64().as_global()); + true + }, + ) + } + } else { + false + }; + + let is_focused = seat + .get_keyboard() + .and_then(|k| k.current_focus()) + .is_some_and(|f| f.has_surface(&shell, surface)); + + (is_under, is_focused, surface_location) + } else { + (false, false, None) + }; + + if is_focused && is_under { + with_pointer_constraint(surface, pointer, |constraint| { + if let Some(constraint) = constraint { + if let Some(region) = constraint.region() { + if let Some(surface_location) = surface_location + && let position = pointer.current_location() + && let point = (position - surface_location.as_logical()).to_i32_floor() + && region.contains(point) + { + constraint.activate(); + } + } else { + constraint.activate(); + } + } + }); + } + } + + fn remove_constraint(&mut self, surface: &WlSurface, pointer: &PointerHandle) { + if with_pointer_constraint(surface, pointer, |constraint| constraint.is_none()) { + let seat = self + .common + .shell + .read() + .seats + .iter() + .find(|s| s.get_pointer().as_ref() == Some(pointer)) + .cloned(); + + if let Some(seat) = seat + && let Some((hint_surface, hint_location)) = seat.pointer_constraint_hint() + && hint_surface == *surface + { + self.apply_cursor_hint(surface, pointer, hint_location); + seat.set_pointer_constraint_hint(None); + } + } + } + + fn cursor_position_hint( + &mut self, + surface: &WlSurface, + pointer: &PointerHandle, + location: Point, + ) { + if with_pointer_constraint(surface, pointer, |constraint| { + constraint.is_some_and(|c| c.is_active()) + }) { + let seat = self + .common + .shell + .read() + .seats + .iter() + .find(|s| s.get_pointer().as_ref() == Some(pointer)) + .cloned(); + + if let Some(seat) = seat { + seat.set_pointer_constraint_hint(Some((surface.clone(), location))); + } + } + } +} diff --git a/src/wayland/handlers/pointer_warp.rs b/src/wayland/handlers/pointer_warp.rs new file mode 100644 index 0000000..971ab58 --- /dev/null +++ b/src/wayland/handlers/pointer_warp.rs @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::state::State; +use smithay::{ + input::pointer::PointerHandle, + reexports::wayland_server::protocol::{wl_pointer::WlPointer, wl_surface::WlSurface}, + utils::{Logical, Point, Serial}, + wayland::pointer_warp::PointerWarpHandler, +}; + +impl PointerWarpHandler for State { + fn warp_pointer( + &mut self, + surface: WlSurface, + pointer: WlPointer, + pos: Point, + serial: Serial, + ) { + let Some(resource_handle) = PointerHandle::::from_resource(&pointer) else { + return; + }; + + let shell = self.common.shell.read(); + + let pointer_handle = shell.seats.iter().find_map(|seat| { + if let Some(pointer_handle) = seat.get_pointer() + && resource_handle == pointer_handle + && pointer_handle.last_enter() == Some(serial) + && let Some(keyboard) = seat.get_keyboard() + && let Some(keyboard_focus) = keyboard.current_focus() + && keyboard_focus.has_surface(&shell, &surface) + { + return Some(pointer_handle); + } + None + }); + + drop(shell); + + if let Some(pointer_handle) = pointer_handle { + self.apply_cursor_hint(&surface, &pointer_handle, pos); + } + } +} diff --git a/src/wayland/handlers/primary_selection.rs b/src/wayland/handlers/primary_selection.rs new file mode 100644 index 0000000..2b63427 --- /dev/null +++ b/src/wayland/handlers/primary_selection.rs @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::state::State; +use smithay::wayland::selection::primary_selection::{ + PrimarySelectionHandler, PrimarySelectionState, +}; + +impl PrimarySelectionHandler for State { + fn primary_selection_state(&mut self) -> &mut PrimarySelectionState { + &mut self.common.primary_selection_state + } +} diff --git a/src/wayland/handlers/seat.rs b/src/wayland/handlers/seat.rs new file mode 100644 index 0000000..10931be --- /dev/null +++ b/src/wayland/handlers/seat.rs @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{ + shell::Devices, + shell::focus::target::{KeyboardFocusTarget, PointerFocusTarget}, + state::State, + utils::prelude::SeatExt, +}; +use smithay::input::{SeatHandler, SeatState, keyboard::LedState, pointer::CursorImageStatus}; + +impl SeatHandler for State { + type KeyboardFocus = KeyboardFocusTarget; + type PointerFocus = PointerFocusTarget; + type TouchFocus = PointerFocusTarget; + + fn seat_state(&mut self) -> &mut SeatState { + &mut self.common.seat_state + } + + fn cursor_image(&mut self, seat: &smithay::input::Seat, image: CursorImageStatus) { + seat.set_cursor_image_status(image); + } + + fn focus_changed( + &mut self, + _seat: &smithay::input::Seat, + _focused: Option<&Self::KeyboardFocus>, + ) { + } + + fn led_state_changed(&mut self, seat: &smithay::input::Seat, led_state: LedState) { + let userdata = seat.user_data(); + let devices = userdata.get::().unwrap(); + devices.update_led_state(led_state); + } +} diff --git a/src/wayland/handlers/security_context.rs b/src/wayland/handlers/security_context.rs new file mode 100644 index 0000000..bcd2685 --- /dev/null +++ b/src/wayland/handlers/security_context.rs @@ -0,0 +1,55 @@ +use crate::state::{ClientState, State}; +use smithay::{ + backend::drm::DrmNode, + wayland::security_context::{ + SecurityContext, SecurityContextHandler, SecurityContextListenerSource, + }, + xwayland::XWaylandClientData, +}; +use std::sync::Arc; +use tracing::warn; + +impl SecurityContextHandler for State { + fn context_created( + &mut self, + source: SecurityContextListenerSource, + security_context: SecurityContext, + ) { + self.common + .event_loop_handle + .insert_source(source, move |client_stream, _, state| { + let client_data = state + .common + .display_handle + .backend_handle() + .get_client_data(security_context.creator_client_id.clone()) + .ok(); + + let new_state = state.new_client_state(); + + let drm_node = client_data + .as_ref() + .and_then(|data| data.downcast_ref::()) + .and_then(|data| data.advertised_drm_node) + .or_else(|| { + client_data + .as_ref() + .and_then(|data| data.downcast_ref::()) + .and_then(|data| data.user_data().get::().cloned()) + }) + .or(new_state.advertised_drm_node); + + if let Err(err) = state.common.display_handle.insert_client( + client_stream, + Arc::new(ClientState { + security_context: Some(security_context.clone()), + advertised_drm_node: drm_node, + ..new_state + }), + ) { + warn!(?err, "Error adding wayland client"); + }; + }) + .expect("Failed to init the wayland socket source."); + } +} diff --git a/src/wayland/handlers/selection.rs b/src/wayland/handlers/selection.rs new file mode 100644 index 0000000..532876a --- /dev/null +++ b/src/wayland/handlers/selection.rs @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::state::State; +use smithay::{ + input::Seat, + wayland::selection::{SelectionHandler, SelectionSource, SelectionTarget}, + xwayland::xwm::XwmId, +}; +use std::os::unix::io::OwnedFd; +use tracing::warn; + +impl SelectionHandler for State { + type SelectionUserData = XwmId; + + fn new_selection( + &mut self, + target: SelectionTarget, + source: Option, + _seat: Seat, + ) { + let Some(xwm_id) = self + .common + .xwayland_state + .as_ref() + .and_then(|xstate| xstate.xwm.as_ref()) + .map(|xwm| xwm.id()) + else { + return; + }; + + let x_has_focus = self.common.has_x_keyboard_focus(xwm_id); + + let xstate = self.common.xwayland_state.as_mut().unwrap(); + let xwm = xstate.xwm.as_mut().unwrap(); + + if let Some(source) = &source { + if x_has_focus { + if let Err(err) = xwm.new_selection(target, Some(source.mime_types())) { + warn!(?err, "Failed to set Xwayland clipboard selection."); + } + } else { + match target { + SelectionTarget::Clipboard => { + xstate.clipboard_selection_dirty = Some(source.mime_types()) + } + SelectionTarget::Primary => { + xstate.primary_selection_dirty = Some(source.mime_types()) + } + }; + } + } else { + if let Err(err) = xwm.new_selection(target, None) { + warn!(?err, "Failed to clear Xwayland selection."); + } + xstate.clipboard_selection_dirty = None; + xstate.primary_selection_dirty = None; + } + } + + fn send_selection( + &mut self, + target: SelectionTarget, + mime_type: String, + fd: OwnedFd, + _seat: Seat, + _user_data: &Self::SelectionUserData, + ) { + if let Some(xwm) = self + .common + .xwayland_state + .as_mut() + .and_then(|xstate| xstate.xwm.as_mut()) + && let Err(err) = xwm.send_selection(target, mime_type, fd) + { + warn!(?err, "Failed to send selection (X11 -> Wayland)."); + } + } +} diff --git a/src/wayland/handlers/session_lock.rs b/src/wayland/handlers/session_lock.rs new file mode 100644 index 0000000..39e1e37 --- /dev/null +++ b/src/wayland/handlers/session_lock.rs @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{shell::SessionLock, state::State, utils::prelude::*}; +use smithay::{ + output::Output, + reexports::wayland_server::{Resource, protocol::wl_output::WlOutput}, + utils::Size, + wayland::session_lock::{ + LockSurface, SessionLockHandler, SessionLockManagerState, SessionLocker, + }, +}; +use std::collections::HashMap; + +impl SessionLockHandler for State { + fn lock_state(&mut self) -> &mut SessionLockManagerState { + &mut self.common.session_lock_manager_state + } + + fn lock(&mut self, locker: SessionLocker) { + let mut shell = self.common.shell.write(); + + // Reject lock if sesion lock exists and is still valid + if let Some(session_lock) = shell.session_lock.as_ref() + && self + .common + .display_handle + .get_client(session_lock.ext_session_lock.id()) + .is_ok() + { + return; + } + + let ext_session_lock = locker.ext_session_lock().clone(); + locker.lock(); + shell.session_lock = Some(SessionLock { + ext_session_lock, + surfaces: HashMap::new(), + }); + + for output in shell.outputs() { + self.backend.schedule_render(output); + } + } + + fn unlock(&mut self) { + let mut shell = self.common.shell.write(); + shell.session_lock = None; + + for output in shell.outputs() { + self.backend.schedule_render(output); + } + } + + fn new_surface(&mut self, lock_surface: LockSurface, wl_output: WlOutput) { + let mut shell = self.common.shell.write(); + if let Some(session_lock) = &mut shell.session_lock + && let Some(output) = Output::from_resource(&wl_output) + { + lock_surface.with_pending_state(|states| { + let size = output.geometry().size; + states.size = Some(Size::from((size.w as u32, size.h as u32))); + }); + lock_surface.send_configure(); + session_lock + .surfaces + .insert(output.clone(), lock_surface.clone()); + } + } +} diff --git a/src/wayland/handlers/shm.rs b/src/wayland/handlers/shm.rs new file mode 100644 index 0000000..7f4fca8 --- /dev/null +++ b/src/wayland/handlers/shm.rs @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::state::State; +use smithay::wayland::shm::{ShmHandler, ShmState}; + +impl ShmHandler for State { + fn shm_state(&self) -> &ShmState { + &self.common.shm_state + } +} diff --git a/src/wayland/handlers/tablet_manager.rs b/src/wayland/handlers/tablet_manager.rs new file mode 100644 index 0000000..60469ba --- /dev/null +++ b/src/wayland/handlers/tablet_manager.rs @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::state::State; +use smithay::{ + backend::input::TabletToolDescriptor, input::pointer::CursorImageStatus, + wayland::tablet_manager::TabletSeatHandler, +}; + +impl TabletSeatHandler for State { + fn tablet_tool_image(&mut self, _tool: &TabletToolDescriptor, _image: CursorImageStatus) { + // TODO display cursor for each tablet tool + } +} diff --git a/src/wayland/handlers/toplevel_info.rs b/src/wayland/handlers/toplevel_info.rs new file mode 100644 index 0000000..bf4b09a --- /dev/null +++ b/src/wayland/handlers/toplevel_info.rs @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use smithay::utils::{Rectangle, user_data::UserDataMap}; + +use crate::{ + shell::CosmicSurface, + state::State, + utils::prelude::Global, + wayland::protocols::toplevel_info::{ + ToplevelInfoHandler, ToplevelInfoState, Window, delegate_toplevel_info, + }, +}; + +impl ToplevelInfoHandler for State { + type Window = CosmicSurface; + + fn toplevel_info_state(&self) -> &ToplevelInfoState { + &self.common.toplevel_info_state + } + fn toplevel_info_state_mut(&mut self) -> &mut ToplevelInfoState { + &mut self.common.toplevel_info_state + } +} + +impl Window for CosmicSurface { + fn title(&self) -> String { + CosmicSurface::title(self) + } + + fn app_id(&self) -> String { + CosmicSurface::app_id(self) + } + + fn is_activated(&self) -> bool { + !self.is_minimized() && CosmicSurface::is_activated(self, true) + } + + fn is_maximized(&self) -> bool { + !self.is_minimized() && CosmicSurface::is_maximized(self, false) + } + + fn is_fullscreen(&self) -> bool { + !self.is_minimized() && CosmicSurface::is_fullscreen(self, false) + } + + fn is_minimized(&self) -> bool { + CosmicSurface::is_minimized(self) + } + + fn is_sticky(&self) -> bool { + CosmicSurface::is_sticky(self) + } + + fn is_resizing(&self) -> bool { + CosmicSurface::is_resizing(self, true).unwrap_or(false) + } + + fn global_geometry(&self) -> Option> { + CosmicSurface::global_geometry(self) + } + + fn user_data(&self) -> &UserDataMap { + CosmicSurface::user_data(self) + } +} + +delegate_toplevel_info!(State, CosmicSurface); diff --git a/src/wayland/handlers/toplevel_management.rs b/src/wayland/handlers/toplevel_management.rs new file mode 100644 index 0000000..3da3995 --- /dev/null +++ b/src/wayland/handlers/toplevel_management.rs @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use smithay::{ + desktop::{WindowSurfaceType, layer_map_for_output}, + input::{Seat, pointer::MotionEvent}, + output::Output, + reexports::wayland_server::DisplayHandle, + utils::{Point, Rectangle, SERIAL_COUNTER, Size}, + wayland::seat::WaylandFocus, +}; + +use crate::{ + shell::{CosmicSurface, Shell, WorkspaceDelta, focus::target::KeyboardFocusTarget}, + utils::prelude::*, + wayland::protocols::{ + toplevel_info::ToplevelInfoHandler, + toplevel_management::{ + ManagementWindow, ToplevelManagementHandler, ToplevelManagementState, + delegate_toplevel_management, toplevel_rectangle_for, + }, + workspace::WorkspaceHandle, + }, +}; + +impl ToplevelManagementHandler for State { + fn toplevel_management_state(&mut self) -> &mut ToplevelManagementState { + &mut self.common.toplevel_management_state + } + + fn activate( + &mut self, + dh: &DisplayHandle, + window: &::Window, + seat: Option>, + ) { + self.unminimize(dh, window); + + let mut shell = self.common.shell.write(); + for output in shell.outputs().cloned().collect::>().iter() { + let maybe = shell + .workspaces + .spaces_for_output(output) + .enumerate() + .find(|(_, w)| { + w.get_fullscreen_surfaces().any(|f| &f.surface == window) + || w.mapped() + .flat_map(|m| m.windows().map(|(s, _)| s)) + .any(|w| &w == window) + }); + + let seat = seat.clone().unwrap_or(shell.seats.last_active().clone()); + let (target, new_pos) = if let Some((idx, workspace)) = maybe { + let handle = workspace.handle; + let new_pos = shell.activate( + output, + idx, + WorkspaceDelta::new_shortcut(), + &mut self.common.workspace_state.update(), + ); + + let workspace = shell.workspaces.space_for_handle_mut(&handle).unwrap(); + if seat + .get_keyboard() + .unwrap() + .current_focus() + .is_some_and(|focus| !focus.windows().any(|w| w == *window)) + && workspace.is_tiled(window) + { + for mapped in workspace + .mapped() + .filter(|m| { + m.maximized_state.lock().unwrap().is_some() + && !m.windows().any(|(ref w, _)| w == window) + }) + .cloned() + .collect::>() + .into_iter() + { + workspace.unmaximize_request(&mapped); + } + } + + let target = if let Some(mapped) = workspace.element_for_surface(window) { + mapped.focus_window(window); + KeyboardFocusTarget::Element(mapped.clone()) + } else { + KeyboardFocusTarget::Fullscreen(window.clone()) + }; + + (target, new_pos.ok()) + // sticky window? + } else if let Some(mapped) = shell + .workspaces + .sets + .get(output) + .unwrap() + .sticky_layer + .mapped() + .find(|m| m.windows().any(|(w, _)| &w == window)) + { + mapped.focus_window(window); + + let output_geo = output.geometry(); + let new_pos = + output_geo.loc + Point::from((output_geo.size.w / 2, output_geo.size.h / 2)); + (KeyboardFocusTarget::Element(mapped.clone()), Some(new_pos)) + } else { + continue; + }; + + std::mem::drop(shell); + + // move pointer to window if it’s on a different monitor/output + if seat.active_output() != *output + && self.common.config.cosmic_conf.cursor_follows_focus + && let Some(new_pos) = new_pos + { + seat.set_active_output(output); + if let Some(ptr) = seat.get_pointer() { + let serial = SERIAL_COUNTER.next_serial(); + ptr.motion( + self, + None, + &MotionEvent { + location: new_pos.to_f64().as_logical(), + serial, + time: self.common.clock.now().as_millis(), + }, + ); + ptr.frame(self); + } + } + + Shell::set_focus(self, Some(&target), &seat, None, false); + return; + } + } + + fn close(&mut self, _dh: &DisplayHandle, window: &::Window) { + window.close(); + } + + fn move_to_workspace( + &mut self, + _dh: &DisplayHandle, + window: &::Window, + to_handle: WorkspaceHandle, + _output: Output, + ) { + let mut shell = self.common.shell.write(); + let seat = shell.seats.last_active().clone(); + let Some(surface) = window.wl_surface() else { + return; + }; + let Some((from_workspace, _)) = shell.workspace_for_surface(&surface) else { + return; + }; + + let res = shell.move_window( + Some(&seat), + window, + &from_workspace, + &to_handle, + false, + None, + &mut self.common.workspace_state.update(), + &self.common.event_loop_handle, + ); + if let Some((target, _)) = res { + std::mem::drop(shell); + Shell::set_focus(self, Some(&target), &seat, None, true); + } + } + + fn fullscreen( + &mut self, + _dh: &DisplayHandle, + window: &::Window, + output: Option, + ) { + let mut shell = self.common.shell.write(); + let seat = shell.seats.last_active().clone(); + let output = output + .or_else(|| { + window + .wl_surface() + .and_then(|surface| shell.visible_output_for_surface(&surface).cloned()) + }) + .unwrap_or_else(|| seat.focused_or_active_output()); + if let Some(target) = + shell.fullscreen_request(window, output, &self.common.event_loop_handle) + { + std::mem::drop(shell); + Shell::set_focus(self, Some(&target), &seat, None, true); + } + } + + fn unfullscreen( + &mut self, + _dh: &DisplayHandle, + window: &::Window, + ) { + let mut shell = self.common.shell.write(); + let _ = shell.unfullscreen_request(window, &self.common.event_loop_handle); + // don't switch focus because of a programmatic action. + // If the toplevel-management client intends to focus the now unfullscreened toplevel, it can send an `activate`-request. + } + + fn maximize(&mut self, _dh: &DisplayHandle, window: &::Window) { + let mut shell = self.common.shell.write(); + if let Some(mapped) = shell.element_for_surface(window).cloned() { + let seat = shell.seats.last_active().clone(); + shell.maximize_request(&mapped, &seat, true, &self.common.event_loop_handle); + } + } + + fn unmaximize(&mut self, _dh: &DisplayHandle, window: &::Window) { + let mut shell = self.common.shell.write(); + if let Some(mapped) = shell.element_for_surface(window).cloned() { + shell.unmaximize_request(&mapped); + } + } + + fn minimize(&mut self, _dh: &DisplayHandle, window: &::Window) { + let mut shell = self.common.shell.write(); + shell.minimize_request(window); + } + + fn unminimize(&mut self, _dh: &DisplayHandle, window: &::Window) { + let mut shell = self.common.shell.write(); + let seat = shell.seats.last_active().clone(); + shell.unminimize_request(window, &seat, &self.common.event_loop_handle); + } + + fn set_sticky(&mut self, _dh: &DisplayHandle, window: &::Window) { + if window.is_sticky() { + return; + } + + let mut shell = self.common.shell.write(); + if let Some(mapped) = shell.element_for_surface(window).cloned() { + let seat = shell.seats.last_active().clone(); + shell.toggle_sticky(&seat, &mapped); + } + } + + fn unset_sticky( + &mut self, + _dh: &DisplayHandle, + window: &::Window, + ) { + if !window.is_sticky() { + return; + } + + let mut shell = self.common.shell.write(); + if let Some(mapped) = shell.element_for_surface(window).cloned() { + let seat = shell.seats.last_active().clone(); + shell.toggle_sticky(&seat, &mapped); + } + } +} + +impl ManagementWindow for CosmicSurface { + fn close(&self) { + CosmicSurface::close(self) + } +} + +pub fn minimize_rectangle(output: &Output, window: &CosmicSurface) -> Rectangle { + toplevel_rectangle_for(window) + .find_map(|(surface, relative)| { + let map = layer_map_for_output(output); + let layer = map.layer_for_surface(&surface, WindowSurfaceType::ALL); + layer.and_then(|s| map.layer_geometry(s)).map(|local| { + Rectangle::new( + Point::from((local.loc.x + relative.loc.x, local.loc.y + relative.loc.y)), + relative.size, + ) + }) + }) + .unwrap_or_else(|| { + let output_size = output.geometry().size; + Rectangle::new( + Point::from(( + (output_size.w / 2) - 100, + output_size.h - (output_size.h / 3) - 50, + )), + Size::from((200, 100)), + ) + }) + .as_local() +} + +delegate_toplevel_management!(State); diff --git a/src/wayland/handlers/workspace.rs b/src/wayland/handlers/workspace.rs new file mode 100644 index 0000000..fc5c835 --- /dev/null +++ b/src/wayland/handlers/workspace.rs @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{ + shell::WorkspaceDelta, + utils::prelude::*, + wayland::protocols::workspace::{ + Request, State as WState, WorkspaceHandler, WorkspaceState, delegate_workspace, + }, +}; +use cosmic_protocols::workspace::v2::server::zcosmic_workspace_handle_v2::TilingState; +use smithay::reexports::wayland_server::DisplayHandle; + +impl WorkspaceHandler for State { + fn workspace_state(&self) -> &WorkspaceState { + &self.common.workspace_state + } + fn workspace_state_mut(&mut self) -> &mut WorkspaceState { + &mut self.common.workspace_state + } + + fn commit_requests(&mut self, _dh: &DisplayHandle, requests: Vec) { + for request in requests.into_iter() { + match request { + Request::Activate(handle) => { + let mut shell = self.common.shell.write(); + let maybe = shell.workspaces.iter().find_map(|(o, set)| { + set.workspaces + .iter() + .position(|w| w.handle == handle) + .map(|i| (o.clone(), i)) + }); + + if let Some((output, idx)) = maybe { + let _ = shell.activate( + &output, + idx, + WorkspaceDelta::new_shortcut(), + &mut self.common.workspace_state.update(), + ); + // TODO: move cursor? + } + } + Request::SetTilingState { workspace, state } => { + let mut shell = self.common.shell.write(); + let seat = shell.seats.last_active().clone(); + if let Some(workspace) = shell.workspaces.space_for_handle_mut(&workspace) { + let mut guard = self.common.workspace_state.update(); + workspace.set_tiling( + !matches!(state.into_result(), Ok(TilingState::FloatingOnly)), + &seat, + &mut guard, + ); + } + } + Request::SetPin { workspace, pinned } => { + let mut shell = self.common.shell.write(); + if let Some(workspace) = shell.workspaces.space_for_handle_mut(&workspace) { + workspace.pinned = pinned; + let mut update = self.common.workspace_state.update(); + if pinned { + if workspace.id.is_none() { + let id = crate::shell::random_workspace_id(); + update + .set_id(&workspace.handle, &id) + .expect("workspace already has id"); + workspace.id = Some(id); + } + update.add_workspace_state(&workspace.handle, WState::Pinned); + // TODO: Also need to update on changing other properties that are saved + shell.workspaces.persist(&self.common.config); + } else { + update.remove_workspace_state(&workspace.handle, WState::Pinned); + shell.workspaces.persist(&self.common.config); + } + } + } + Request::MoveBefore { + workspace, + other_workspace, + axis, + } => { + if axis != 0 { + continue; + } + let mut shell = self.common.shell.write(); + let mut update = self.common.workspace_state.update(); + shell.workspaces.move_workspace( + &workspace, + &other_workspace, + &mut update, + false, + ); + } + Request::MoveAfter { + workspace, + other_workspace, + axis, + } => { + if axis != 0 { + continue; + } + let mut shell = self.common.shell.write(); + let mut update = self.common.workspace_state.update(); + shell.workspaces.move_workspace( + &workspace, + &other_workspace, + &mut update, + true, + ); + } + _ => {} + } + } + } +} + +delegate_workspace!(State); diff --git a/src/wayland/handlers/xdg_activation.rs b/src/wayland/handlers/xdg_activation.rs new file mode 100644 index 0000000..ad8ce0d --- /dev/null +++ b/src/wayland/handlers/xdg_activation.rs @@ -0,0 +1,295 @@ +use crate::shell::WorkspaceDelta; +use crate::shell::focus::target::KeyboardFocusTarget; +use crate::{shell::ActivationKey, state::ClientState, utils::prelude::*}; +use crate::{ + state::State, + wayland::protocols::workspace::{State as WState, WorkspaceHandle}, +}; +use cosmic_comp_config::ActivationPolicy; +use smithay::{ + input::Seat, + reexports::wayland_server::protocol::wl_surface::WlSurface, + wayland::xdg_activation::{ + XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData, + }, +}; +use tracing::{debug, warn}; + +#[derive(Debug, Clone, Copy)] +pub enum ActivationContext { + UrgentOnly, + Workspace(WorkspaceHandle), +} + +// It may happen that we get activation-requests, while all outputs are disabled. +// In these cases we won't be able to determine workspaces for windows and thus +// need to handle the corresponding code paths defensively. + +impl XdgActivationHandler for State { + fn activation_state(&mut self) -> &mut XdgActivationState { + &mut self.common.xdg_activation_state + } + + fn token_created(&mut self, token: XdgActivationToken, data: XdgActivationTokenData) -> bool { + // Privileged clients always get valid tokens + if data + .client_id + .and_then(|client_id| { + self.common + .display_handle + .backend_handle() + .get_client_data(client_id) + .ok() + }) + .and_then(|data| { + data.downcast_ref::() + .map(|data| data.not_sandboxed()) + }) + .unwrap_or(false) + { + let seat = data + .serial + .and_then(|(_, seat)| Seat::from_resource(&seat)) + .unwrap_or_else(|| { + let shell = self.common.shell.read(); + shell.seats.last_active().clone() + }); + let output = seat.active_output(); + let mut shell = self.common.shell.write(); + let Some(workspace) = shell.active_space_mut(&output) else { + debug!(?token, "created urgent token for privileged client"); + data.user_data + .insert_if_missing(move || ActivationContext::UrgentOnly); + return true; + }; + let handle = workspace.handle; + data.user_data + .insert_if_missing(move || ActivationContext::Workspace(handle)); + debug!(?token, "created workspace token for privileged client"); + return true; + }; + + // Tokens without validation aren't allowed to steal focus + let Some((serial, seat)) = data.serial else { + data.user_data + .insert_if_missing(|| ActivationContext::UrgentOnly); + debug!(?token, "created urgent-only token for missing seat/serial"); + return true; + }; + let Some(seat) = Seat::from_resource(&seat) else { + data.user_data + .insert_if_missing(|| ActivationContext::UrgentOnly); + debug!(?token, "created urgent-only token for unknown seat"); + return true; + }; + + // At this point we don't bother with urgent-only tokens. + // If the client provides a bad serial, it should be fixed. + + let keyboard = seat.get_keyboard().unwrap(); + let valid = keyboard + .last_enter() + .map(|last_enter| serial.is_no_older_than(&last_enter)) + .unwrap_or(false); + + if valid { + let output = seat.active_output(); + let mut shell = self.common.shell.write(); + let Some(workspace) = shell.active_space_mut(&output) else { + data.user_data + .insert_if_missing(|| ActivationContext::UrgentOnly); + return true; + }; + let handle = workspace.handle; + data.user_data + .insert_if_missing(move || ActivationContext::Workspace(handle)); + + debug!(?token, "created workspace token"); + } + + valid + } + + fn request_activation( + &mut self, + _token: XdgActivationToken, + token_data: XdgActivationTokenData, + surface: WlSurface, + ) { + let Some(context) = token_data.user_data.get::() else { + return; + }; + + match context { + ActivationContext::UrgentOnly => { + let shell = self.common.shell.write(); + if let Some((workspace, _output)) = shell.workspace_for_surface(&surface) { + let mut workspace_guard = self.common.workspace_state.update(); + workspace_guard.add_workspace_state(&workspace, WState::Urgent); + } + } + ActivationContext::Workspace(_) => { + match self.common.config.cosmic_conf.activation_policy { + ActivationPolicy::Focus => { + self.activate_surface( + &surface, + Some((ActivationKey::Wayland(surface.clone()), *context)), + ); + } + ActivationPolicy::FocusIfActiveWorkspace => { + let shell = self.common.shell.write(); + + let Some((target_workspace, _)) = shell.workspace_for_surface(&surface) + else { + return; + }; + + let seat = shell.seats.last_active().clone(); + let current_output = seat.active_output(); + let current_workspace = shell.active_space(¤t_output).unwrap().handle; + + if target_workspace == current_workspace { + std::mem::drop(shell); + self.activate_surface( + &surface, + Some((ActivationKey::Wayland(surface.clone()), *context)), + ); + } else { + let mut workspace_guard = self.common.workspace_state.update(); + workspace_guard.add_workspace_state(&target_workspace, WState::Urgent); + } + } + ActivationPolicy::Urgent => { + let shell = self.common.shell.write(); + if let Some((workspace, _output)) = shell.workspace_for_surface(&surface) { + let mut workspace_guard = self.common.workspace_state.update(); + workspace_guard.add_workspace_state(&workspace, WState::Urgent); + } + } + } + } + } + } +} + +impl State { + pub fn activate_surface( + &mut self, + surface: &WlSurface, + pending_activation: Option<(ActivationKey, ActivationContext)>, + ) { + let mut shell = self.common.shell.write(); + + let seat = shell.seats.last_active().clone(); + let current_output = seat.active_output(); + + if let Some(element) = shell.element_for_surface(surface).cloned() { + if element.is_minimized() { + shell.unminimize_request(surface, &seat, &self.common.event_loop_handle); + } + + let Some((element_output, element_workspace)) = shell + .space_for(&element) + .map(|w| (w.output.clone(), w.handle)) + else { + return; + }; + let in_current_workspace = + element_workspace == shell.active_space(¤t_output).unwrap().handle; + + if !in_current_workspace { + let Some(idx) = shell + .workspaces + .idx_for_handle(&element_output, &element_workspace) + else { + warn!("Couldn't determine idx for elements workspace?"); + return; + }; + + if let Err(err) = shell.activate( + &element_output, + idx, + WorkspaceDelta::new_shortcut(), + &mut self.common.workspace_state.update(), + ) { + warn!("Failed to activate the workspace: {err:?}"); + } + } + + let current_workspace = shell.active_space_mut(¤t_output).unwrap(); + current_workspace + .floating_layer + .space + .raise_element(&element, true); + if element.is_stack() { + if let Some((window, _)) = element.windows().find(|(window, _)| { + let mut found = false; + window.with_surfaces(|wl_surface, _| { + if wl_surface == surface { + found = true; + } + }); + found + }) { + element.set_active(&window); + } else { + warn!("Failed to find activated window in the stack"); + return; + } + } + + if seat.get_keyboard().unwrap().current_focus() != Some(element.clone().into()) + && current_workspace.is_tiled(surface) + { + for mapped in current_workspace + .mapped() + .filter(|m| m.maximized_state.lock().unwrap().is_some()) + .cloned() + .collect::>() + .into_iter() + { + current_workspace.unmaximize_request(&mapped); + } + } + + std::mem::drop(shell); + Shell::set_focus( + self, + Some(&KeyboardFocusTarget::Element(element.clone())), + &seat, + None, + false, + ); + } else if let Some((workspace, _)) = shell.workspace_for_surface(surface) { + let current_workspace = shell.active_space(¤t_output).unwrap(); + if workspace == current_workspace.handle { + let Some(target) = shell + .workspaces + .space_for_handle(&workspace) + .unwrap() + .get_fullscreen(&seat) + .map(|f| f.surface.clone()) + .map(KeyboardFocusTarget::Fullscreen) + else { + return; + }; + + std::mem::drop(shell); + Shell::set_focus(self, Some(&target), &seat, None, false); + } else { + if let Some(surface) = shell + .workspaces + .space_for_handle(&workspace) + .and_then(|w| w.get_fullscreen(&seat)) + .map(|f| f.surface.clone()) + { + shell.append_focus_stack(surface, &seat) + } + let mut workspace_guard = self.common.workspace_state.update(); + workspace_guard.add_workspace_state(&workspace, WState::Urgent); + } + } else if let Some((activation_key, context)) = pending_activation { + shell.pending_activations.insert(activation_key, context); + }; + } +} diff --git a/src/wayland/handlers/xdg_foreign.rs b/src/wayland/handlers/xdg_foreign.rs new file mode 100644 index 0000000..801e4c2 --- /dev/null +++ b/src/wayland/handlers/xdg_foreign.rs @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::state::State; +use smithay::wayland::xdg_foreign::{XdgForeignHandler, XdgForeignState}; + +impl XdgForeignHandler for State { + fn xdg_foreign_state(&mut self) -> &mut XdgForeignState { + &mut self.common.xdg_foreign_state + } +} diff --git a/src/wayland/handlers/xdg_shell/mod.rs b/src/wayland/handlers/xdg_shell/mod.rs new file mode 100644 index 0000000..b828801 --- /dev/null +++ b/src/wayland/handlers/xdg_shell/mod.rs @@ -0,0 +1,430 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{ + shell::{CosmicSurface, PendingWindow, focus::target::KeyboardFocusTarget, grabs::ReleaseMode}, + utils::prelude::*, +}; +use smithay::desktop::layer_map_for_output; +use smithay::{ + desktop::{ + PopupGrab, PopupKeyboardGrab, PopupKind, PopupPointerGrab, PopupUngrabStrategy, + WindowSurfaceType, find_popup_root_surface, + }, + input::{Seat, pointer::Focus}, + output::Output, + reexports::{ + wayland_protocols::xdg::shell::server::xdg_toplevel, + wayland_server::protocol::{wl_output::WlOutput, wl_seat::WlSeat}, + }, + utils::{Logical, Point, Serial}, + wayland::{ + compositor::with_states, + seat::WaylandFocus, + shell::xdg::{ + PopupSurface, PositionerState, SurfaceCachedState, ToplevelSurface, XdgShellHandler, + XdgShellState, + }, + }, +}; +use std::cell::Cell; +use tracing::warn; + +use super::compositor::client_compositor_state; + +pub mod popup; + +pub type PopupGrabData = Cell>>; + +impl XdgShellHandler for State { + fn xdg_shell_state(&mut self) -> &mut XdgShellState { + &mut self.common.xdg_shell_state + } + + fn new_toplevel(&mut self, surface: ToplevelSurface) { + let mut shell = self.common.shell.write(); + let seat = shell.seats.last_active().clone(); + + if !shell.pending_windows.iter().any(|w| w.surface == surface) { + let surface = CosmicSurface::from(surface); + shell.pending_windows.push(PendingWindow { + surface, + seat, + fullscreen: None, + maximized: false, + sticky: false, + }) + } + // We will position the window after the first commit, when we know its size hints + } + + fn new_popup(&mut self, surface: PopupSurface, positioner: PositionerState) { + surface.with_pending_state(|state| { + state.geometry = positioner.get_geometry(); + state.positioner = positioner; + }); + + if surface.get_parent_surface().is_some() { + // let other shells deal with their popups + self.common + .shell + .read() + .unconstrain_popup(&PopupKind::from(surface.clone())); + + if let Err(err) = surface.send_configure() { + warn!("Unable to configure popup. {err:?}",); + } else { + self.common + .popups + .track_popup(PopupKind::from(surface)) + .unwrap(); + } + } + } + + fn grab(&mut self, surface: PopupSurface, seat: WlSeat, serial: Serial) { + let seat = Seat::from_resource(&seat).unwrap(); + let kind = PopupKind::Xdg(surface); + let maybe_root = find_popup_root_surface(&kind).ok(); + if maybe_root.is_none() { + tracing::warn!("No root surface found for popup grab."); + return; + } + let maybe_root: Option = maybe_root.and_then(|root| { + let shell = self.common.shell.read(); + shell + .element_for_surface(&root) + .map(|r| r.clone().into()) + .or_else(|| { + shell.outputs().find_map(|o| { + layer_map_for_output(o) + .layer_for_surface(&root, WindowSurfaceType::ALL) + .cloned() + .map(KeyboardFocusTarget::LayerSurface) + }) + }) + }); + + if let Some(root) = maybe_root { + let target = root; + let ret = self.common.popups.grab_popup(target, kind, &seat, serial); + match ret { + Ok(mut grab) => { + if let Some(keyboard) = seat.get_keyboard() { + if keyboard.is_grabbed() + && !(keyboard.has_grab(serial) + || keyboard.has_grab(grab.previous_serial().unwrap_or(serial))) + { + grab.ungrab(PopupUngrabStrategy::All); + return; + } + Shell::set_focus( + self, + grab.current_grab().as_ref(), + &seat, + Some(serial), + false, + ); + keyboard.set_grab(self, PopupKeyboardGrab::new(&grab), serial); + } + + if let Some(pointer) = seat.get_pointer() { + if pointer.is_grabbed() + && !(pointer.has_grab(serial) + || pointer.has_grab( + grab.previous_serial().unwrap_or_else(|| grab.serial()), + )) + { + grab.ungrab(PopupUngrabStrategy::All); + return; + } + pointer.set_grab(self, PopupPointerGrab::new(&grab), serial, Focus::Keep); + } + + seat.user_data() + .insert_if_missing(|| PopupGrabData::new(None)); + seat.user_data() + .get::() + .unwrap() + .set(Some(grab)); + } + Err(err) => { + tracing::warn!("Failed to grab popup: {:?}", err); + } + } + } else { + tracing::warn!("No root for grab."); + } + } + + fn reposition_request( + &mut self, + surface: PopupSurface, + positioner: PositionerState, + token: u32, + ) { + surface.with_pending_state(|state| { + let geometry = positioner.get_geometry(); + state.geometry = geometry; + state.positioner = positioner; + }); + + self.common + .shell + .read() + .unconstrain_popup(&PopupKind::from(surface.clone())); + surface.send_repositioned(token); + } + + fn move_request(&mut self, surface: ToplevelSurface, seat: WlSeat, serial: Serial) { + let seat = Seat::from_resource(&seat).unwrap(); + let mut shell = self.common.shell.write(); + if let Some((grab, focus)) = shell.move_request( + surface.wl_surface(), + &seat, + serial, + ReleaseMode::NoMouseButtons, + false, + &self.common.config, + &self.common.event_loop_handle, + true, + ) { + std::mem::drop(shell); + if grab.is_touch_grab() { + seat.get_touch().unwrap().set_grab(self, grab, serial); + } else { + seat.get_pointer() + .unwrap() + .set_grab(self, grab, serial, focus) + } + } + } + + fn resize_request( + &mut self, + surface: ToplevelSurface, + seat: WlSeat, + serial: Serial, + edges: xdg_toplevel::ResizeEdge, + ) { + let seat = Seat::from_resource(&seat).unwrap(); + let mut shell = self.common.shell.write(); + if let Some((grab, focus)) = shell.resize_request( + surface.wl_surface(), + &seat, + serial, + edges.into(), + self.common.config.cosmic_conf.edge_snap_threshold, + true, + ) { + std::mem::drop(shell); + if grab.is_touch_grab() { + seat.get_touch().unwrap().set_grab(self, grab, serial) + } else { + seat.get_pointer() + .unwrap() + .set_grab(self, grab, serial, focus) + } + } + } + + fn minimize_request(&mut self, surface: ToplevelSurface) { + let mut shell = self.common.shell.write(); + shell.minimize_request(surface.wl_surface()) + } + + fn maximize_request(&mut self, surface: ToplevelSurface) { + let mut shell = self.common.shell.write(); + if let Some(mapped) = shell.element_for_surface(surface.wl_surface()).cloned() { + let seat = shell.seats.last_active().clone(); + shell.maximize_request(&mapped, &seat, true, &self.common.event_loop_handle) + } else if let Some(pending) = shell + .pending_windows + .iter_mut() + .find(|pending| pending.surface.wl_surface().as_deref() == Some(surface.wl_surface())) + { + pending.maximized = true; + } + } + + fn unmaximize_request(&mut self, surface: ToplevelSurface) { + let mut shell = self.common.shell.write(); + if let Some(mapped) = shell.element_for_surface(surface.wl_surface()).cloned() { + shell.unmaximize_request(&mapped); + } else if let Some(pending) = shell + .pending_windows + .iter_mut() + .find(|pending| pending.surface.wl_surface().as_deref() == Some(surface.wl_surface())) + { + pending.maximized = false; + } + } + + fn fullscreen_request(&mut self, surface: ToplevelSurface, output: Option) { + let mut shell = self.common.shell.write(); + let seat = shell.seats.last_active().clone(); + let output = output + .as_ref() + .and_then(Output::from_resource) + .or_else(|| { + shell + .visible_output_for_surface(surface.wl_surface()) + .cloned() + }) + .unwrap_or_else(|| seat.focused_or_active_output()); + + match shell.fullscreen_request(&surface, output.clone(), &self.common.event_loop_handle) { + Some(target) => { + std::mem::drop(shell); + Shell::set_focus(self, Some(&target), &seat, None, true); + } + None => { + if let Some(pending) = shell.pending_windows.iter_mut().find(|pending| { + pending.surface.wl_surface().as_deref() == Some(surface.wl_surface()) + }) { + pending.fullscreen = Some(output); + } + } + } + } + + fn unfullscreen_request(&mut self, surface: ToplevelSurface) { + let mut shell = self.common.shell.write(); + let seat = shell.seats.last_active().clone(); + let should_focus = seat + .get_keyboard() + .unwrap() + .current_focus() + .is_some_and(|target| { + if let KeyboardFocusTarget::Fullscreen(s) = target { + s == surface + } else { + false + } + }); + + if let Some(target) = shell.unfullscreen_request(&surface, &self.common.event_loop_handle) { + std::mem::drop(shell); + if should_focus { + Shell::set_focus(self, Some(&target), &seat, None, true); + } + } else if let Some(pending) = shell + .pending_windows + .iter_mut() + .find(|pending| pending.surface.wl_surface().as_deref() == Some(surface.wl_surface())) + { + pending.fullscreen.take(); + } + } + + fn toplevel_destroyed(&mut self, surface: ToplevelSurface) { + for (popup, _) in smithay::desktop::PopupManager::popups_for_surface(surface.wl_surface()) { + if let smithay::desktop::PopupKind::Xdg(ref xdg_popup) = popup { + xdg_popup.send_popup_done(); + } + } + + let (output, clients) = { + let mut shell = self.common.shell.write(); + + for seat in shell.seats.iter() { + if let Some(data) = seat.user_data().get::() { + let mut should_ungrab = false; + let grab = data.take(); + if let Some(ref grab) = grab { + if grab.has_ended() { + should_ungrab = true; + } else if let Some(target) = grab.current_grab() { + if let Some(wl_surface) = target.wl_surface() { + if wl_surface.as_ref() == surface.wl_surface() + || smithay::desktop::PopupManager::popups_for_surface( + surface.wl_surface(), + ) + .any(|(p, _)| p.wl_surface() == wl_surface.as_ref()) + { + should_ungrab = true; + } + } + } + } + if should_ungrab { + if let Some(mut grab) = grab { + grab.ungrab(PopupUngrabStrategy::All); + } + } else { + data.set(grab); + } + } + } + + let seat = shell.seats.last_active().clone(); + + // Clean up pending_windows for surfaces that were never mapped. + shell + .pending_windows + .retain(|pending| pending.surface != surface); + + let output = shell + .visible_output_for_surface(surface.wl_surface()) + .cloned(); + let _ = shell.unmap_surface( + surface.wl_surface(), + &seat, + &mut self.common.toplevel_info_state, + ); + if let Some(output) = output.as_ref() { + shell.refresh_active_space(output); + } + + // animations might be unblocked now + (output, shell.update_animations()) + }; + + { + let dh = self.common.display_handle.clone(); + for client in clients.values() { + client_compositor_state(client).blocker_cleared(self, &dh); + } + } + + if let Some(output) = output.as_ref() { + self.backend.schedule_render(output); + } + } + + fn show_window_menu( + &mut self, + surface: ToplevelSurface, + seat: WlSeat, + serial: Serial, + mut location: Point, + ) { + let seat = Seat::from_resource(&seat).unwrap(); + location -= with_states(surface.wl_surface(), |states| { + states + .cached_state + .get::() + .current() + .geometry + }) + .unwrap_or_default() + .loc; + + let shell = self.common.shell.read(); + let res = shell.menu_request( + true, + surface.wl_surface(), + &seat, + serial, + location, + false, + &self.common.config, + &self.common.event_loop_handle, + ); + if let Some((grab, focus)) = res { + std::mem::drop(shell); + seat.get_pointer() + .unwrap() + .set_grab(self, grab, serial, focus) + } + } +} diff --git a/src/wayland/handlers/xdg_shell/popup.rs b/src/wayland/handlers/xdg_shell/popup.rs new file mode 100644 index 0000000..a3ee2e3 --- /dev/null +++ b/src/wayland/handlers/xdg_shell/popup.rs @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{shell::Shell, utils::prelude::*}; +use smithay::{ + desktop::{ + LayerSurface, PopupKind, PopupManager, WindowSurfaceType, get_popup_toplevel_coords, + layer_map_for_output, space::SpaceElement, utils, + }, + output::Output, + reexports::{ + wayland_protocols::xdg::shell::server::xdg_positioner::ConstraintAdjustment, + wayland_server::protocol::wl_surface::WlSurface, + }, + utils::{Logical, Point, Rectangle}, + wayland::{ + compositor::{get_role, with_states}, + seat::WaylandFocus, + shell::xdg::{PopupCachedState, ToplevelSurface, XDG_POPUP_ROLE, XdgPopupSurfaceData}, + }, +}; +use tracing::warn; + +impl Shell { + pub fn unconstrain_popup(&self, surface: &PopupKind) { + if let Some(parent) = get_popup_toplevel(surface) { + if let Some(elem) = self.element_for_surface(&parent) { + let (mut element_geo, output, is_tiled) = + if let Some(workspace) = self.space_for(elem) { + let Some(elem_geo) = workspace.element_geometry(elem) else { + return; + }; + ( + elem_geo.to_global(workspace.output()), + workspace.output.clone(), + workspace.is_tiled(&elem.active_window()), + ) + } else if let Some((output, set)) = self + .workspaces + .sets + .iter() + .find(|(_, set)| set.sticky_layer.mapped().any(|m| m == elem)) + { + ( + set.sticky_layer + .element_geometry(elem) + .unwrap() + .to_global(output), + output.clone(), + false, + ) + } else { + return; + }; + + let (window, offset) = elem + .windows() + .find(|(w, _)| w.wl_surface().as_deref() == Some(&parent)) + .unwrap(); + let window_geo_offset = window.geometry().loc; + let window_loc: Point = + element_geo.loc + offset.as_global() + window_geo_offset.as_global(); + if is_tiled { + element_geo.loc = (0, 0).into(); + if !unconstrain_xdg_popup_tile(surface, element_geo.as_logical()) { + unconstrain_xdg_popup(surface, window_loc, output.geometry()); + } + } else { + unconstrain_xdg_popup(surface, window_loc, output.geometry()); + } + } else if let Some(output) = self.workspaces.spaces().find_map(|w| { + w.fullscreen_surfaces.iter().find_map(|f| { + (f.surface.wl_surface().as_deref() == Some(&parent)).then_some(w.output()) + }) + }) { + let window_loc = output.geometry().loc; + unconstrain_xdg_popup(surface, window_loc, output.geometry()); + } else if let Some((output, layer_surface)) = self.outputs().find_map(|o| { + let map = layer_map_for_output(o); + map.layer_for_surface(&parent, WindowSurfaceType::ALL) + .map(|l| (o, l.clone())) + }) { + unconstrain_layer_popup(surface, output, &layer_surface) + } + } + } +} + +pub fn update_reactive_popups<'a>( + toplevel: &ToplevelSurface, + loc: Point, + outputs: impl Iterator, +) { + let output_geo = outputs.map(|o| o.geometry()).collect::>(); + for (popup, _) in PopupManager::popups_for_surface(toplevel.wl_surface()) { + match popup { + PopupKind::Xdg(surface) => { + let positioner = with_states(surface.wl_surface(), |states| { + let mut guard = states.cached_state.get::(); + guard + .current() + .last_acked + .map_or_else(Default::default, |configure| configure.state.positioner) + }); + if positioner.reactive { + let anchor_point = loc + positioner.get_anchor_point().as_global(); + if let Some(rect) = output_geo + .iter() + .find(|geo| geo.contains(anchor_point)) + .copied() + { + unconstrain_xdg_popup(&PopupKind::from(surface.clone()), loc, rect); + if let Err(err) = surface.send_configure() { + warn!( + ?err, + "Compositor bug: Unable to re-configure reactive popup", + ); + } + } + } + } + PopupKind::InputMethod(_) => {} + } + } +} + +// Attempt to constraint to tile, without resize. Return `true` if it fits. +fn unconstrain_xdg_popup_tile(surface: &PopupKind, mut rect: Rectangle) -> bool { + rect.loc -= get_popup_toplevel_coords(surface); + position_popup_within_rect(surface, rect.as_global(), true) +} + +fn unconstrain_xdg_popup( + surface: &PopupKind, + window_loc: Point, + mut rect: Rectangle, +) { + rect.loc -= window_loc; + rect.loc -= get_popup_toplevel_coords(surface).as_global(); + position_popup_within_rect(surface, rect, false); +} + +fn unconstrain_layer_popup(surface: &PopupKind, output: &Output, layer_surface: &LayerSurface) { + let map = layer_map_for_output(output); + let layer_geo = map.layer_geometry(layer_surface).unwrap(); + + // the output_rect represented relative to the parents coordinate system + let mut relative = Rectangle::from_size(output.geometry().size).as_logical(); + relative.loc -= layer_geo.loc; + relative.loc -= get_popup_toplevel_coords(surface); + position_popup_within_rect(surface, relative.as_global(), false); +} + +fn position_popup_within_rect( + surface: &PopupKind, + rect: Rectangle, + is_tiled: bool, +) -> bool { + match surface { + PopupKind::Xdg(surface) => { + let rect = rect.as_logical(); + let geometry = surface.with_pending_state(|state| { + let positioner = if is_tiled { + let mut positioner_no_resize = state.positioner; + positioner_no_resize + .constraint_adjustment + .remove(ConstraintAdjustment::ResizeX | ConstraintAdjustment::ResizeY); + positioner_no_resize + } else { + state.positioner + }; + positioner.get_unconstrained_geometry(rect) + }); + surface.with_pending_state(|state| { + state.geometry = geometry; + }); + rect.contains_rect(geometry) + } + PopupKind::InputMethod(popup) => { + let input_rect = popup.text_input_rectangle(); + + // We basically place the IME popup below the input rect. + let mut popup_bbox = utils::bbox_from_surface_tree(popup.wl_surface(), input_rect.loc); + popup_bbox.loc.y += input_rect.size.h; + // tracing::debug!( + // "IME input_rect: {:?}, popup_bbox: {:?}", + // input_rect, + // popup_bbox + // ); + + // Handle the right edge overflow + let popup_right = popup_bbox.loc.x + popup_bbox.size.w; + let rect_right = rect.loc.x + rect.size.w; + popup_bbox.loc.x -= (popup_right - rect_right).max(0); + + // Flip vertically if the bottom edge overflows + let popup_bottom = popup_bbox.loc.y + popup_bbox.size.h; + let rect_bottom = rect.loc.y + rect.size.h; + if popup_bottom > rect_bottom { + popup_bbox.loc.y = input_rect.loc.y - popup_bbox.size.h; + } + + popup.set_location(popup_bbox.loc); + rect.as_logical().contains_rect(popup_bbox) + } + } +} + +pub fn get_popup_toplevel(popup: &PopupKind) -> Option { + let mut parent = match popup { + PopupKind::Xdg(popup) => popup.get_parent_surface()?, + PopupKind::InputMethod(popup) => popup.get_parent()?.surface.clone(), + }; + while get_role(&parent) == Some(XDG_POPUP_ROLE) { + parent = with_states(&parent, |states| { + states + .data_map + .get::() + .unwrap() + .lock() + .unwrap() + .parent + .as_ref() + .cloned() + })?; + } + Some(parent) +} diff --git a/src/wayland/handlers/xwayland_keyboard_grab.rs b/src/wayland/handlers/xwayland_keyboard_grab.rs new file mode 100644 index 0000000..b2b16b7 --- /dev/null +++ b/src/wayland/handlers/xwayland_keyboard_grab.rs @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{shell::focus::target::KeyboardFocusTarget, state::State}; +use smithay::{ + input::Seat, + reexports::wayland_server::{Resource, protocol::wl_surface::WlSurface}, + wayland::xwayland_keyboard_grab::{XWaylandKeyboardGrab, XWaylandKeyboardGrabHandler}, +}; +use std::sync::Mutex; + +#[derive(Default)] +pub struct XWaylandGrabSeatData { + pub grab: Mutex)>>, +} + +impl XWaylandKeyboardGrabHandler for State { + fn grab(&mut self, surface: WlSurface, seat: Seat, grab: XWaylandKeyboardGrab) { + let data = seat + .user_data() + .get_or_insert(XWaylandGrabSeatData::default); + *data.grab.lock().unwrap() = Some((surface, grab)); + } + + fn keyboard_focus_for_xsurface(&self, surface: &WlSurface) -> Option { + let element = self + .common + .shell + .read() + .workspaces + .spaces() + .find_map(|x| x.element_for_surface(surface).cloned())?; + Some(KeyboardFocusTarget::Element(element)) + } +} + +pub trait XWaylandGrabSeat { + fn has_active_xwayland_grab(&self, surface: &WlSurface) -> bool; +} + +impl XWaylandGrabSeat for Seat { + fn has_active_xwayland_grab(&self, surface: &WlSurface) -> bool { + self.user_data() + .get_or_insert(XWaylandGrabSeatData::default) + .grab + .lock() + .unwrap() + .as_ref() + .is_some_and(|(s, g)| g.grab().is_alive() && s == surface) + } +} diff --git a/src/wayland/handlers/xwayland_shell.rs b/src/wayland/handlers/xwayland_shell.rs new file mode 100644 index 0000000..eaa7435 --- /dev/null +++ b/src/wayland/handlers/xwayland_shell.rs @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use crate::state::State; +use smithay::wayland::xwayland_shell::{XWaylandShellHandler, XWaylandShellState}; + +impl XWaylandShellHandler for State { + fn xwayland_shell_state(&mut self) -> &mut XWaylandShellState { + &mut self.common.xwayland_shell_state + } +} diff --git a/src/wayland/mod.rs b/src/wayland/mod.rs new file mode 100644 index 0000000..7426262 --- /dev/null +++ b/src/wayland/mod.rs @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pub mod handlers; +pub mod protocols; diff --git a/src/wayland/protocols/a11y.rs b/src/wayland/protocols/a11y.rs new file mode 100644 index 0000000..52f0b12 --- /dev/null +++ b/src/wayland/protocols/a11y.rs @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use cosmic_protocols::a11y::v1::server::cosmic_a11y_manager_v1; +use smithay::reexports::wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, +}; +use wayland_backend::{protocol::WEnum, server::GlobalId}; + +pub trait A11yHandler { + fn a11y_state(&mut self) -> &mut A11yState; + + fn request_screen_magnifier(&mut self, enabled: bool); + fn request_screen_invert(&mut self, inverted: bool); + fn request_screen_filter(&mut self, filter: Option); +} + +#[derive(Debug)] +pub struct A11yState { + global: GlobalId, + instances: Vec, + + magnifier_state: bool, + screen_inverted: bool, + screen_filter: Option, +} + +struct Unknown; + +fn protocol_to_color_filter( + protocol: WEnum, +) -> Result, Unknown> { + match protocol { + WEnum::Value(cosmic_a11y_manager_v1::Filter::Disabled) => Ok(None), + WEnum::Value(cosmic_a11y_manager_v1::Filter::Greyscale) => Ok(Some(ColorFilter::Greyscale)), + WEnum::Value(cosmic_a11y_manager_v1::Filter::DaltonizeProtanopia) => { + Ok(Some(ColorFilter::Protanopia)) + } + WEnum::Value(cosmic_a11y_manager_v1::Filter::DaltonizeDeuteranopia) => { + Ok(Some(ColorFilter::Deuteranopia)) + } + WEnum::Value(cosmic_a11y_manager_v1::Filter::DaltonizeTritanopia) => { + Ok(Some(ColorFilter::Tritanopia)) + } + WEnum::Unknown(_) | WEnum::Value(_) => Err(Unknown), + } +} + +fn color_filter_to_protocol(filter: Option) -> cosmic_a11y_manager_v1::Filter { + match filter { + None => cosmic_a11y_manager_v1::Filter::Disabled, + Some(ColorFilter::Greyscale) => cosmic_a11y_manager_v1::Filter::Greyscale, + Some(ColorFilter::Protanopia) => cosmic_a11y_manager_v1::Filter::DaltonizeProtanopia, + Some(ColorFilter::Deuteranopia) => cosmic_a11y_manager_v1::Filter::DaltonizeDeuteranopia, + Some(ColorFilter::Tritanopia) => cosmic_a11y_manager_v1::Filter::DaltonizeTritanopia, + } +} + +impl A11yState { + pub fn new(dh: &DisplayHandle, client_filter: F) -> A11yState + where + D: GlobalDispatch + 'static, + F: for<'a> Fn(&'a Client) -> bool + Send + Sync + 'static, + { + let global = dh.create_global::( + 2, + A11yGlobalData { + filter: Box::new(client_filter), + }, + ); + A11yState { + global, + instances: Vec::new(), + + magnifier_state: false, + screen_inverted: false, + screen_filter: None, + } + } + + pub fn global_id(&self) -> GlobalId { + self.global.clone() + } + + pub fn screen_inverted(&self) -> bool { + self.screen_inverted + } + + pub fn set_screen_magnifier(&mut self, enabled: bool) { + self.magnifier_state = enabled; + + for instance in &self.instances { + instance.magnifier(if enabled { + cosmic_a11y_manager_v1::ActiveState::Enabled + } else { + cosmic_a11y_manager_v1::ActiveState::Disabled + }); + } + } + + pub fn set_screen_inverted(&mut self, enabled: bool) { + self.screen_inverted = enabled; + self.send_screen_filter(); + } + + pub fn set_screen_filter(&mut self, state: Option) { + self.screen_filter = state; + self.send_screen_filter(); + } + + fn send_screen_filter(&self) { + for instance in &self.instances { + if instance.version() >= cosmic_a11y_manager_v1::EVT_SCREEN_FILTER_SINCE { + instance.screen_filter( + if self.screen_inverted { + cosmic_a11y_manager_v1::ActiveState::Enabled + } else { + cosmic_a11y_manager_v1::ActiveState::Disabled + }, + color_filter_to_protocol(self.screen_filter), + ); + } + } + } +} + +pub struct A11yGlobalData { + filter: Box Fn(&'a Client) -> bool + Send + Sync>, +} + +impl GlobalDispatch for A11yState +where + D: GlobalDispatch + + Dispatch + + A11yHandler + + 'static, +{ + fn bind( + state: &mut D, + _handle: &DisplayHandle, + _client: &Client, + resource: New, + _global_data: &A11yGlobalData, + data_init: &mut DataInit<'_, D>, + ) { + let instance = data_init.init(resource, ()); + let state = state.a11y_state(); + + instance.magnifier(if state.magnifier_state { + cosmic_a11y_manager_v1::ActiveState::Enabled + } else { + cosmic_a11y_manager_v1::ActiveState::Disabled + }); + + if instance.version() >= cosmic_a11y_manager_v1::EVT_SCREEN_FILTER_SINCE { + instance.screen_filter( + if state.screen_inverted { + cosmic_a11y_manager_v1::ActiveState::Enabled + } else { + cosmic_a11y_manager_v1::ActiveState::Disabled + }, + color_filter_to_protocol(state.screen_filter), + ); + } + + state.instances.push(instance); + } + + fn can_view(client: Client, global_data: &A11yGlobalData) -> bool { + (global_data.filter)(&client) + } +} + +impl Dispatch for A11yState +where + D: Dispatch + A11yHandler + 'static, +{ + fn request( + state: &mut D, + _client: &Client, + _resource: &cosmic_a11y_manager_v1::CosmicA11yManagerV1, + request: ::Request, + _data: &(), + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + cosmic_a11y_manager_v1::Request::SetMagnifier { active } => { + let enabled = active + .into_result() + .unwrap_or(cosmic_a11y_manager_v1::ActiveState::Disabled) + == cosmic_a11y_manager_v1::ActiveState::Enabled; + if enabled != state.a11y_state().magnifier_state { + state.request_screen_magnifier(enabled); + } + } + cosmic_a11y_manager_v1::Request::SetScreenFilter { inverted, filter } => { + let inverted = inverted + .into_result() + .unwrap_or(cosmic_a11y_manager_v1::ActiveState::Disabled) + == cosmic_a11y_manager_v1::ActiveState::Enabled; + let filter = protocol_to_color_filter(filter); + + if inverted != state.a11y_state().screen_inverted { + state.request_screen_invert(inverted); + } + + if let Ok(filter) = filter + && filter != state.a11y_state().screen_filter + { + state.request_screen_filter(filter); + } + } + _ => unreachable!(), + } + } + + fn destroyed( + state: &mut D, + _client: wayland_backend::server::ClientId, + resource: &cosmic_a11y_manager_v1::CosmicA11yManagerV1, + _data: &(), + ) { + state.a11y_state().instances.retain(|i| i != resource); + } +} + +macro_rules! delegate_a11y { + ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { + smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + cosmic_protocols::a11y::v1::server::cosmic_a11y_manager_v1::CosmicA11yManagerV1: $crate::wayland::protocols::a11y::A11yGlobalData + ] => $crate::wayland::protocols::a11y::A11yState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + cosmic_protocols::a11y::v1::server::cosmic_a11y_manager_v1::CosmicA11yManagerV1: () + ] => $crate::wayland::protocols::a11y::A11yState); + }; +} +pub(crate) use delegate_a11y; + +use crate::config::ColorFilter; diff --git a/src/wayland/protocols/corner_radius.rs b/src/wayland/protocols/corner_radius.rs new file mode 100644 index 0000000..51dc115 --- /dev/null +++ b/src/wayland/protocols/corner_radius.rs @@ -0,0 +1,805 @@ +use cosmic_protocols::corner_radius::v1::server::cosmic_corner_radius_layer_v1::{ + self, CosmicCornerRadiusLayerV1, +}; +use cosmic_protocols::corner_radius::v1::server::cosmic_corner_radius_toplevel_v1::CosmicCornerRadiusToplevelV1; +use cosmic_protocols::corner_radius::v1::server::{ + cosmic_corner_radius_manager_v1, cosmic_corner_radius_toplevel_v1, +}; +use smithay::desktop::utils::bbox_from_surface_tree; +use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_popup::XdgPopup; +use smithay::reexports::wayland_protocols_wlr::layer_shell::v1::server::zwlr_layer_surface_v1::ZwlrLayerSurfaceV1; +use smithay::reexports::wayland_server::New; +use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +use smithay::utils::{HookId, Logical, Point, Rectangle}; +use smithay::wayland::compositor::Cacheable; +use smithay::wayland::compositor::add_pre_commit_hook; +use smithay::wayland::compositor::with_states; +use smithay::wayland::shell::wlr_layer::WlrLayerShellHandler; +use smithay::wayland::shell::xdg::{SurfaceCachedState, XdgShellSurfaceUserData}; +use smithay::{ + reexports::{ + wayland_protocols::xdg::shell::server::xdg_toplevel::XdgToplevel, + wayland_server::{Client, Dispatch, DisplayHandle, GlobalDispatch, Resource, Weak}, + }, + wayland::shell::xdg::XdgShellHandler, +}; +use std::sync::Mutex; +use wayland_backend::server::GlobalId; + +type ToplevelHookId = Mutex)>>; +type LayerHookId = Mutex)>>; + +#[derive(Debug)] +pub struct CornerRadiusState { + instances: Vec, + global: GlobalId, +} + +impl CornerRadiusState { + pub fn new(dh: &DisplayHandle) -> CornerRadiusState + where + D: GlobalDispatch + + Dispatch + + Dispatch< + cosmic_corner_radius_toplevel_v1::CosmicCornerRadiusToplevelV1, + CornerRadiusData, + > + Dispatch + + CornerRadiusHandler + + 'static, + { + let global = dh + .create_global::( + 2, + (), + ); + CornerRadiusState { + instances: Vec::new(), + global, + } + } + + pub fn global_id(&self) -> GlobalId { + self.global.clone() + } +} + +pub trait CornerRadiusHandler: XdgShellHandler + WlrLayerShellHandler { + fn corner_radius_state(&mut self) -> &mut CornerRadiusState; + fn set_corner_radius(&mut self, data: &CornerRadiusData); + fn unset_corner_radius(&mut self, data: &CornerRadiusData); + fn set_padding(&mut self, data: &CornerRadiusData); + fn unset_padding(&mut self, data: &CornerRadiusData); +} + +impl GlobalDispatch + for CornerRadiusState +where + D: GlobalDispatch + + Dispatch + + Dispatch + + Dispatch + + CornerRadiusHandler + + 'static, +{ + fn bind( + state: &mut D, + _handle: &DisplayHandle, + _client: &Client, + resource: smithay::reexports::wayland_server::New< + cosmic_corner_radius_manager_v1::CosmicCornerRadiusManagerV1, + >, + _global_data: &(), + data_init: &mut smithay::reexports::wayland_server::DataInit<'_, D>, + ) { + let instance = data_init.init(resource, ()); + state.corner_radius_state().instances.push(instance); + } +} + +impl Dispatch + for CornerRadiusState +where + D: GlobalDispatch + + Dispatch + + Dispatch + + Dispatch + + CornerRadiusHandler + + 'static, +{ + fn request( + state: &mut D, + _client: &Client, + resource: &cosmic_corner_radius_manager_v1::CosmicCornerRadiusManagerV1, + request: ::Request, + _data: &(), + _dhandle: &DisplayHandle, + data_init: &mut smithay::reexports::wayland_server::DataInit<'_, D>, + ) { + match request { + cosmic_corner_radius_manager_v1::Request::Destroy => { + let corner_radius_state = state.corner_radius_state(); + corner_radius_state.instances.retain(|i| i != resource); + } + cosmic_corner_radius_manager_v1::Request::GetCornerRadius { id, toplevel } => { + if let Some(surface) = state.xdg_shell_state().get_toplevel(&toplevel) { + new_xdg( + surface.wl_surface(), + CornerRadiusSurface::Toplevel(surface.xdg_toplevel().downgrade()), + resource, + data_init, + id, + ) + } // TODO: can this fail? + } + cosmic_corner_radius_manager_v1::Request::GetCornerRadiusSurface { id, surface } => { + if let Some(toplevel) = + state + .xdg_shell_state() + .toplevel_surfaces() + .iter() + .find(|toplevel| { + toplevel + .xdg_toplevel() + .data::() + .unwrap() + .xdg_surface() + == &surface + }) + { + new_xdg( + toplevel.wl_surface(), + CornerRadiusSurface::Toplevel(toplevel.xdg_toplevel().downgrade()), + resource, + data_init, + id, + ) + } else if let Some(popup) = + state + .xdg_shell_state() + .popup_surfaces() + .iter() + .find(|popup| { + popup + .xdg_popup() + .data::() + .unwrap() + .xdg_surface() + == &surface + }) + { + new_xdg( + popup.wl_surface(), + CornerRadiusSurface::Popup(popup.xdg_popup().downgrade()), + resource, + data_init, + id, + ) + } else { + resource.post_error( + cosmic_corner_radius_manager_v1::Error::NoRole as u32, + "xdg_surface has no known role object".to_string(), + ); + } + } + cosmic_corner_radius_manager_v1::Request::GetCornerRadiusLayer { id, layer } => { + if let Some(surface) = state + .shell_state() + .layer_surfaces() + .find(|surface| surface.shell_surface() == &layer) + { + let radius_exists = with_states(surface.wl_surface(), |surface_data| { + let hook_id = surface_data + .data_map + .get_or_insert_threadsafe(|| LayerHookId::new(None)); + let guard = hook_id.lock().unwrap(); + guard.as_ref().map(|(_, t)| t.upgrade().is_ok()) + }); + if radius_exists.unwrap_or_default() { + resource.post_error( + cosmic_corner_radius_manager_v1::Error::CornerRadiusExists as u32, + format!( + "{resource:?} CosmicCornerRadiusToplevelV1 object already exists for the surface" + ), + ); + } + let data = Mutex::new(CornerRadiusInternal { + surface: CornerRadiusSurface::Layer(layer.downgrade()), + corners: None, + padding: None, + }); + let obj = data_init.init(id, data); + let obj_downgrade = obj.downgrade(); + + let needs_hook = radius_exists.is_none(); + if needs_hook { + let hook_id = + add_pre_commit_hook::(surface.wl_surface(), layer_radius_hook); + with_states(surface.wl_surface(), |surface_data| { + let hook_ids = surface_data + .data_map + .get_or_insert_threadsafe(|| LayerHookId::new(None)); + let mut guard = hook_ids.lock().unwrap(); + *guard = Some((hook_id, obj_downgrade)); + }); + } + } // TODO: can this fail? + } + _ => unimplemented!(), + } + } + + fn destroyed( + state: &mut D, + _client: wayland_backend::server::ClientId, + resource: &cosmic_corner_radius_manager_v1::CosmicCornerRadiusManagerV1, + _data: &(), + ) { + let corner_radius_state = state.corner_radius_state(); + corner_radius_state.instances.retain(|i| i != resource); + } +} + +fn new_xdg( + wl_surface: &WlSurface, + surface: CornerRadiusSurface, + resource: &cosmic_corner_radius_manager_v1::CosmicCornerRadiusManagerV1, + data_init: &mut smithay::reexports::wayland_server::DataInit<'_, D>, + id: New, +) where + D: GlobalDispatch + + Dispatch + + Dispatch + + CornerRadiusHandler + + 'static, +{ + let radius_exists = with_states(wl_surface, |surface_data| { + let hook_id = surface_data + .data_map + .get_or_insert_threadsafe(|| ToplevelHookId::new(None)); + let guard = hook_id.lock().unwrap(); + guard.as_ref().map(|(_, t)| t.upgrade().is_ok()) + }); + if radius_exists.unwrap_or_default() { + resource.post_error( + cosmic_corner_radius_manager_v1::Error::CornerRadiusExists as u32, + format!( + "{resource:?} CosmicCornerRadiusToplevelV1 object already exists for the surface" + ), + ); + } + let data = Mutex::new(CornerRadiusInternal { + surface, + corners: None, + padding: None, + }); + let obj = data_init.init(id, data); + let obj_downgrade = obj.downgrade(); + + let needs_hook = radius_exists.is_none(); + if needs_hook { + let hook_id = add_pre_commit_hook::(wl_surface, xdg_radius_hook); + with_states(wl_surface, |surface_data| { + let hook_ids = surface_data + .data_map + .get_or_insert_threadsafe(|| ToplevelHookId::new(None)); + let mut guard = hook_ids.lock().unwrap(); + *guard = Some((hook_id, obj_downgrade)); + }); + } +} + +impl + Dispatch + for CornerRadiusState +where + D: GlobalDispatch + + Dispatch + + Dispatch + + CornerRadiusHandler + + 'static, +{ + fn request( + state: &mut D, + _client: &Client, + resource: &cosmic_corner_radius_toplevel_v1::CosmicCornerRadiusToplevelV1, + request: ::Request, + data: &CornerRadiusData, + _dhandle: &DisplayHandle, + _data_init: &mut smithay::reexports::wayland_server::DataInit<'_, D>, + ) { + match request { + cosmic_corner_radius_toplevel_v1::Request::Destroy => { + let mut guard = data.lock().unwrap(); + guard.corners = None; + + let Some(wl_surface) = (match &guard.surface { + CornerRadiusSurface::Toplevel(toplevel) => toplevel + .upgrade() + .ok() + .and_then(|toplevel| state.xdg_shell_state().get_toplevel(&toplevel)) + .map(|toplevel| toplevel.wl_surface().clone()), + CornerRadiusSurface::Popup(popup) => popup + .upgrade() + .ok() + .and_then(|popup| state.xdg_shell_state().get_popup(&popup)) + .map(|popup| popup.wl_surface().clone()), + CornerRadiusSurface::Layer(_) => unreachable!(), + }) else { + return; + }; + + with_states(&wl_surface, |surface_data| { + if let Some(hook_ids_mutex) = surface_data.data_map.get::() { + let mut hook_id = hook_ids_mutex.lock().unwrap(); + *hook_id = None; + } + + let mut cached = surface_data.cached_state.get::(); + let pending = cached.pending(); + *pending = CacheableCorners(None); + }); + drop(guard); + + state.unset_corner_radius(data); + } + cosmic_corner_radius_toplevel_v1::Request::SetRadius { + top_left, + top_right, + bottom_right, + bottom_left, + } => { + let mut guard = data.lock().unwrap(); + guard.set_corner_radius(top_left, top_right, bottom_right, bottom_left); + + let Some(wl_surface) = (match &guard.surface { + CornerRadiusSurface::Toplevel(toplevel) => toplevel + .upgrade() + .ok() + .and_then(|toplevel| state.xdg_shell_state().get_toplevel(&toplevel)) + .map(|toplevel| toplevel.wl_surface().clone()), + CornerRadiusSurface::Popup(popup) => popup + .upgrade() + .ok() + .and_then(|popup| state.xdg_shell_state().get_popup(&popup)) + .map(|popup| popup.wl_surface().clone()), + CornerRadiusSurface::Layer(_) => unreachable!(), + }) else { + resource.post_error( + cosmic_corner_radius_toplevel_v1::Error::ToplevelDestroyed as u32, + format!("{:?} No toplevel found", resource), + ); + return; + }; + with_states(&wl_surface, |s| { + let mut cached = s.cached_state.get::(); + let pending = cached.pending(); + *pending = CacheableCorners(guard.corners); + }); + drop(guard); + + state.set_corner_radius(data); + } + cosmic_corner_radius_toplevel_v1::Request::UnsetRadius => { + let mut guard = data.lock().unwrap(); + guard.corners = None; + + let Some(wl_surface) = (match &guard.surface { + CornerRadiusSurface::Toplevel(toplevel) => toplevel + .upgrade() + .ok() + .and_then(|toplevel| state.xdg_shell_state().get_toplevel(&toplevel)) + .map(|toplevel| toplevel.wl_surface().clone()), + CornerRadiusSurface::Popup(popup) => popup + .upgrade() + .ok() + .and_then(|popup| state.xdg_shell_state().get_popup(&popup)) + .map(|popup| popup.wl_surface().clone()), + CornerRadiusSurface::Layer(_) => unreachable!(), + }) else { + resource.post_error( + cosmic_corner_radius_toplevel_v1::Error::ToplevelDestroyed as u32, + format!("{:?} No toplevel found", resource), + ); + + return; + }; + + with_states(&wl_surface, |s| { + let mut cached = s.cached_state.get::(); + let pending = cached.pending(); + *pending = CacheableCorners(None); + }); + drop(guard); + + state.unset_corner_radius(data); + } + _ => unimplemented!(), + } + } +} + +impl Dispatch + for CornerRadiusState +where + D: GlobalDispatch + + Dispatch + + Dispatch + + CornerRadiusHandler + + 'static, +{ + fn request( + state: &mut D, + _client: &Client, + resource: &cosmic_corner_radius_layer_v1::CosmicCornerRadiusLayerV1, + request: ::Request, + data: &CornerRadiusData, + _dhandle: &DisplayHandle, + _data_init: &mut smithay::reexports::wayland_server::DataInit<'_, D>, + ) { + match request { + cosmic_corner_radius_layer_v1::Request::Destroy => { + let mut guard = data.lock().unwrap(); + guard.corners = None; + + let CornerRadiusSurface::Layer(layer_surface) = &guard.surface else { + unreachable!("corner_radius_layer without layer shell?"); + }; + let Some(layer_surface) = layer_surface.upgrade().ok().and_then(|layer| { + state + .shell_state() + .layer_surfaces() + .find(|s| s.shell_surface() == &layer) + }) else { + resource.post_error( + cosmic_corner_radius_layer_v1::Error::LayerDestroyed as u32, + format!("{:?} No layer found", resource), + ); + return; + }; + + with_states(layer_surface.wl_surface(), |surface_data| { + if let Some(hook_ids_mutex) = surface_data.data_map.get::() { + let mut hook_id = hook_ids_mutex.lock().unwrap(); + *hook_id = None; + } + + let mut cached = surface_data.cached_state.get::(); + let pending = cached.pending(); + *pending = CacheableCorners(None); + + let mut cached = surface_data.cached_state.get::(); + let pending = cached.pending(); + *pending = CacheablePadding(None); + }); + drop(guard); + + state.unset_corner_radius(data); + state.unset_padding(data); + } + cosmic_corner_radius_layer_v1::Request::SetRadius { + top_left, + top_right, + bottom_right, + bottom_left, + } => { + let mut guard = data.lock().unwrap(); + guard.set_corner_radius(top_left, top_right, bottom_right, bottom_left); + + let CornerRadiusSurface::Layer(layer_surface) = &guard.surface else { + unreachable!("corner_radius_layer without layer shell?"); + }; + let Some(layer_surface) = layer_surface.upgrade().ok().and_then(|layer| { + state + .shell_state() + .layer_surfaces() + .find(|s| s.shell_surface() == &layer) + }) else { + resource.post_error( + cosmic_corner_radius_layer_v1::Error::LayerDestroyed as u32, + format!("{:?} No layer found", resource), + ); + return; + }; + + with_states(layer_surface.wl_surface(), |s| { + let mut cached = s.cached_state.get::(); + let pending = cached.pending(); + *pending = CacheableCorners(guard.corners); + }); + drop(guard); + + state.set_corner_radius(data); + } + cosmic_corner_radius_layer_v1::Request::UnsetRadius => { + let mut guard = data.lock().unwrap(); + guard.corners = None; + + let CornerRadiusSurface::Layer(layer_surface) = &guard.surface else { + unreachable!("corner_radius_layer without layer shell?"); + }; + let Some(layer_surface) = layer_surface.upgrade().ok().and_then(|layer| { + state + .shell_state() + .layer_surfaces() + .find(|s| s.shell_surface() == &layer) + }) else { + resource.post_error( + cosmic_corner_radius_layer_v1::Error::LayerDestroyed as u32, + format!("{:?} No layer found", resource), + ); + return; + }; + + with_states(layer_surface.wl_surface(), |s| { + let mut cached = s.cached_state.get::(); + let pending = cached.pending(); + *pending = CacheableCorners(None); + }); + drop(guard); + + state.unset_corner_radius(data); + } + cosmic_corner_radius_layer_v1::Request::SetPadding { + top, + right, + bottom, + left, + } => { + let mut guard = data.lock().unwrap(); + guard.set_padding(top, right, bottom, left); + + let CornerRadiusSurface::Layer(layer_surface) = &guard.surface else { + unreachable!("corner_radius_layer without layer shell?"); + }; + let Some(layer_surface) = layer_surface.upgrade().ok().and_then(|layer| { + state + .shell_state() + .layer_surfaces() + .find(|s| s.shell_surface() == &layer) + }) else { + resource.post_error( + cosmic_corner_radius_layer_v1::Error::LayerDestroyed as u32, + format!("{:?} No layer found", resource), + ); + return; + }; + + with_states(layer_surface.wl_surface(), |s| { + let mut cached = s.cached_state.get::(); + let pending = cached.pending(); + *pending = CacheablePadding(guard.padding); + }); + drop(guard); + + state.set_padding(data); + } + cosmic_corner_radius_layer_v1::Request::UnsetPadding => { + let mut guard = data.lock().unwrap(); + guard.corners = None; + + let CornerRadiusSurface::Layer(layer_surface) = &guard.surface else { + unreachable!("corner_radius_layer without layer shell?"); + }; + let Some(layer_surface) = layer_surface.upgrade().ok().and_then(|layer| { + state + .shell_state() + .layer_surfaces() + .find(|s| s.shell_surface() == &layer) + }) else { + resource.post_error( + cosmic_corner_radius_layer_v1::Error::LayerDestroyed as u32, + format!("{:?} No layer found", resource), + ); + return; + }; + + with_states(layer_surface.wl_surface(), |s| { + let mut cached = s.cached_state.get::(); + let pending = cached.pending(); + *pending = CacheablePadding(None); + }); + drop(guard); + + state.unset_padding(data); + } + _ => unimplemented!(), + } + } +} + +fn xdg_radius_hook(_state: &mut D, _dh: &DisplayHandle, surface: &WlSurface) { + with_states(surface, |surface_data| { + let corners = *surface_data + .cached_state + .get::() + .pending(); + if surface_data + .cached_state + .get::() + .pending() + .geometry + .zip(corners.0.as_ref()) + .is_some_and(|(geo, corners)| { + let half_min_dim = (geo.size.w.min(geo.size.h) / 2) as u32; + corners.top_right > half_min_dim + || corners.top_left > half_min_dim + || corners.bottom_right > half_min_dim + || corners.bottom_left > half_min_dim + }) + && let Some(hook) = surface_data.data_map.get::() + { + let hook_ref = hook.lock().unwrap(); + if let Some((_, obj)) = hook_ref.as_ref() + && let Ok(obj) = obj.upgrade() + { + obj.post_error( + cosmic_corner_radius_toplevel_v1::Error::RadiusTooLarge as u32, + format!("{obj:?} corner radius too large"), + ); + } + } + }); +} + +fn pad_rect( + mut rect: Rectangle, + padding: &Padding, +) -> Option> { + rect.size.h = rect.size.h.checked_sub(padding.top)?; + rect.loc.x += padding.left; + rect.size.w = rect.size.w.checked_sub(padding.left)?; + rect.loc.y += padding.top; + rect.size.h = rect.size.h.checked_sub(padding.bottom)?; + rect.size.w = rect.size.w.checked_sub(padding.right)?; + Some(rect) +} + +fn layer_radius_hook(_state: &mut D, _dh: &DisplayHandle, surface: &WlSurface) { + let bbox = bbox_from_surface_tree(surface, Point::default()); + with_states(surface, |surface_data| { + let corners = *surface_data + .cached_state + .get::() + .pending(); + let padding = *surface_data + .cached_state + .get::() + .pending(); + let empty = Padding::default(); + let Some(padded_box) = pad_rect(bbox, padding.0.as_ref().unwrap_or(&empty)) else { + if let Some(hook) = surface_data.data_map.get::() { + let hook_ref = hook.lock().unwrap(); + if let Some((_, obj)) = hook_ref.as_ref() + && let Ok(obj) = obj.upgrade() + { + obj.post_error( + cosmic_corner_radius_layer_v1::Error::PaddingTooLarge as u32, + format!("{obj:?} padding too large"), + ); + } + } + return; + }; + + if corners.0.as_ref().is_some_and(|corners| { + let half_min_dim = (padded_box.size.w.min(padded_box.size.h) / 2) as u32; + corners.top_right > half_min_dim + || corners.top_left > half_min_dim + || corners.bottom_right > half_min_dim + || corners.bottom_left > half_min_dim + }) && let Some(hook) = surface_data.data_map.get::() + { + let hook_ref = hook.lock().unwrap(); + if let Some((_, obj)) = hook_ref.as_ref() + && let Ok(obj) = obj.upgrade() + { + obj.post_error( + cosmic_corner_radius_layer_v1::Error::RadiusTooLarge as u32, + format!("{obj:?} corner radius too large"), + ); + } + } + }); +} + +pub type CornerRadiusData = Mutex; + +#[derive(Debug)] +pub enum CornerRadiusSurface { + Toplevel(Weak), + Popup(Weak), + Layer(Weak), +} + +#[derive(Debug)] +pub struct CornerRadiusInternal { + pub surface: CornerRadiusSurface, + pub corners: Option, + pub padding: Option, +} + +#[derive(Debug, Copy, Clone)] +pub struct Corners { + pub top_left: u32, + pub top_right: u32, + pub bottom_right: u32, + pub bottom_left: u32, +} + +#[derive(Debug, Copy, Clone, Default)] +pub struct Padding { + pub top: i32, + pub right: i32, + pub bottom: i32, + pub left: i32, +} + +#[derive(Default, Debug, Copy, Clone)] +pub struct CacheableCorners(pub Option); + +impl Cacheable for CacheableCorners { + fn commit(&mut self, _dh: &DisplayHandle) -> Self { + *self + } + fn merge_into(self, into: &mut Self, _dh: &DisplayHandle) { + *into = self; + } +} + +#[derive(Default, Debug, Copy, Clone)] +pub struct CacheablePadding(pub Option); + +impl Cacheable for CacheablePadding { + fn commit(&mut self, _dh: &DisplayHandle) -> Self { + *self + } + fn merge_into(self, into: &mut Self, _dh: &DisplayHandle) { + *into = self; + } +} + +impl CornerRadiusInternal { + fn set_corner_radius( + &mut self, + top_left: u32, + top_right: u32, + bottom_right: u32, + bottom_left: u32, + ) { + let corners = Corners { + top_left, + top_right, + bottom_right, + bottom_left, + }; + self.corners = Some(corners); + } + + fn set_padding(&mut self, top: i32, right: i32, bottom: i32, left: i32) { + let padding = Padding { + top, + right, + bottom, + left, + }; + self.padding = Some(padding); + } +} + +macro_rules! delegate_corner_radius { + ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { + smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + cosmic_protocols::corner_radius::v1::server::cosmic_corner_radius_manager_v1::CosmicCornerRadiusManagerV1: () + ] => $crate::wayland::protocols::corner_radius::CornerRadiusState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + cosmic_protocols::corner_radius::v1::server::cosmic_corner_radius_manager_v1::CosmicCornerRadiusManagerV1: () + ] => $crate::wayland::protocols::corner_radius::CornerRadiusState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + cosmic_protocols::corner_radius::v1::server::cosmic_corner_radius_toplevel_v1::CosmicCornerRadiusToplevelV1: CornerRadiusData + ] => $crate::wayland::protocols::corner_radius::CornerRadiusState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + cosmic_protocols::corner_radius::v1::server::cosmic_corner_radius_layer_v1::CosmicCornerRadiusLayerV1: CornerRadiusData + ] => $crate::wayland::protocols::corner_radius::CornerRadiusState); + }; +} +pub(crate) use delegate_corner_radius; diff --git a/src/wayland/protocols/drm.rs b/src/wayland/protocols/drm.rs new file mode 100644 index 0000000..c86e5ff --- /dev/null +++ b/src/wayland/protocols/drm.rs @@ -0,0 +1,286 @@ +// SPDX-License-Identifier: GPL-3.0-only + +// Re-export only the actual code, and then only use this re-export +// The `generated` module below is just some boilerplate to properly isolate stuff +// and avoid exposing internal details. +// +// You can use all the types from my_protocol as if they went from `wayland_client::protocol`. +pub use generated::wl_drm; + +#[allow(non_snake_case, non_upper_case_globals, non_camel_case_types)] +mod generated { + use smithay::reexports::wayland_server::{self, protocol::*}; + + pub mod __interfaces { + use smithay::reexports::wayland_server::protocol::__interfaces::*; + use wayland_backend; + wayland_scanner::generate_interfaces!("resources/protocols/wayland-drm.xml"); + } + use self::__interfaces::*; + + wayland_scanner::generate_server_code!("resources/protocols/wayland-drm.xml"); +} + +use smithay::{ + backend::allocator::{ + Fourcc, Modifier, + dmabuf::{Dmabuf, DmabufFlags}, + format::FormatSet, + }, + reexports::wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, + backend::GlobalId, protocol::wl_buffer::WlBuffer, + }, + wayland::{ + buffer::BufferHandler, + dmabuf::{DmabufGlobal, DmabufHandler}, + }, +}; +use tracing::trace; + +use std::{convert::TryFrom, path::PathBuf, sync::Arc}; + +pub enum ImportError { + Failed, + InvalidFormat, +} + +pub trait DrmHandler { + fn dmabuf_imported(&mut self, global: &DmabufGlobal, dmabuf: Dmabuf) -> Result; + fn buffer_created(&mut self, buffer: WlBuffer, result: R) { + let _ = (buffer, result); + } +} + +#[derive(Debug, Default)] +pub struct WlDrmState(std::marker::PhantomData); + +/// Data associated with a drm global. +pub struct DrmGlobalData { + filter: Box Fn(&'a Client) -> bool + Send + Sync>, + formats: Arc>, + device_path: PathBuf, + dmabuf_global: DmabufGlobal, +} + +pub struct DrmInstanceData { + formats: Arc>, + dmabuf_global: DmabufGlobal, +} + +impl GlobalDispatch for WlDrmState +where + D: GlobalDispatch + + Dispatch + + BufferHandler + + DmabufHandler + + 'static, + R: 'static, +{ + fn bind( + _state: &mut D, + _dh: &DisplayHandle, + _client: &Client, + resource: New, + global_data: &DrmGlobalData, + data_init: &mut DataInit<'_, D>, + ) { + let data = DrmInstanceData { + formats: global_data.formats.clone(), + dmabuf_global: global_data.dmabuf_global, + }; + let drm_instance = data_init.init(resource, data); + + drm_instance.device(global_data.device_path.to_string_lossy().into_owned()); + if drm_instance.version() >= 2 { + drm_instance.capabilities(wl_drm::Capability::Prime as u32); + } + for format in global_data.formats.iter() { + if let Ok(converted) = wl_drm::Format::try_from(*format as u32) { + drm_instance.format(converted as u32); + } + } + } + + fn can_view(client: Client, global_data: &DrmGlobalData) -> bool { + (global_data.filter)(&client) + } +} + +impl Dispatch for WlDrmState +where + D: GlobalDispatch + + Dispatch + + Dispatch + + BufferHandler + + DrmHandler + + 'static, + R: 'static, +{ + fn request( + state: &mut D, + _client: &Client, + drm: &wl_drm::WlDrm, + request: wl_drm::Request, + data: &DrmInstanceData, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + wl_drm::Request::Authenticate { .. } => drm.authenticated(), + wl_drm::Request::CreateBuffer { .. } => drm.post_error( + wl_drm::Error::InvalidName, + String::from("Flink handles are unsupported, use PRIME"), + ), + wl_drm::Request::CreatePlanarBuffer { .. } => drm.post_error( + wl_drm::Error::InvalidName, + String::from("Flink handles are unsupported, use PRIME"), + ), + wl_drm::Request::CreatePrimeBuffer { + id, + name, + width, + height, + format, + offset0, + stride0, + .. + } => { + let format = match Fourcc::try_from(format) { + Ok(format) => { + if !data.formats.contains(&format) { + drm.post_error( + wl_drm::Error::InvalidFormat, + String::from("Format not advertised by wl_drm"), + ); + return; + } + format + } + Err(_) => { + drm.post_error( + wl_drm::Error::InvalidFormat, + String::from("Format unknown / not advertised by wl_drm"), + ); + return; + } + }; + + if width < 1 || height < 1 { + drm.post_error( + wl_drm::Error::InvalidFormat, + String::from("width or height not positive"), + ); + return; + } + + let mut dma = Dmabuf::builder( + (width, height), + format, + Modifier::Invalid, + DmabufFlags::empty(), + ); + dma.add_plane(name, offset0 as u32, stride0 as u32); + match dma.build() { + Some(dmabuf) => { + match state.dmabuf_imported(&data.dmabuf_global, dmabuf.clone()) { + Ok(result) => { + // import was successful + let buffer = data_init.init(id, dmabuf); + trace!("Created a new validated dma wl_buffer via wl_drm."); + state.buffer_created(buffer, result); + } + + Err(ImportError::InvalidFormat) => { + drm.post_error( + wl_drm::Error::InvalidFormat, + "format and plane combination are not valid", + ); + } + + Err(ImportError::Failed) => { + // Buffer import failed. The protocol documentation heavily implies killing the + // client is the right thing to do here. + drm.post_error(wl_drm::Error::InvalidName, "buffer import failed"); + } + } + } + None => { + // Buffer import failed. The protocol documentation heavily implies killing the + // client is the right thing to do here. + drm.post_error( + wl_drm::Error::InvalidName, + "dmabuf global was destroyed on server", + ); + } + } + } + } + } +} + +impl WlDrmState { + pub fn create_global( + &mut self, + display: &DisplayHandle, + device_path: PathBuf, + formats: FormatSet, + dmabuf_global: &DmabufGlobal, + ) -> GlobalId + where + D: GlobalDispatch + + Dispatch + + BufferHandler + + DmabufHandler + + 'static, + { + self.create_global_with_filter::(display, device_path, formats, dmabuf_global, |_| { + true + }) + } + + pub fn create_global_with_filter( + &mut self, + display: &DisplayHandle, + device_path: PathBuf, + formats: FormatSet, + dmabuf_global: &DmabufGlobal, + client_filter: F, + ) -> GlobalId + where + D: GlobalDispatch + + Dispatch + + BufferHandler + + DmabufHandler + + 'static, + F: for<'a> Fn(&'a Client) -> bool + Send + Sync + 'static, + { + let formats = Arc::new( + formats + .into_iter() + .filter(|f| f.modifier == Modifier::Invalid) + .map(|f| f.code) + .collect(), + ); + let data = DrmGlobalData { + filter: Box::new(client_filter), + formats, + device_path, + dmabuf_global: *dmabuf_global, + }; + + display.create_global::(2, data) + } +} + +macro_rules! delegate_wl_drm { + ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty; $r: ty) => { + smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + $crate::wayland::protocols::drm::wl_drm::WlDrm: $crate::wayland::protocols::drm::DrmGlobalData + ] => $crate::wayland::protocols::drm::WlDrmState<$r>); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + $crate::wayland::protocols::drm::wl_drm::WlDrm: $crate::wayland::protocols::drm::DrmInstanceData + ] => $crate::wayland::protocols::drm::WlDrmState<$r>); + }; +} +pub(crate) use delegate_wl_drm; diff --git a/src/wayland/protocols/image_capture_source.rs b/src/wayland/protocols/image_capture_source.rs new file mode 100644 index 0000000..3a34939 --- /dev/null +++ b/src/wayland/protocols/image_capture_source.rs @@ -0,0 +1,136 @@ +use super::{ + workspace::{WorkspaceHandle, WorkspaceHandler}, +}; +use crate::shell::element::surface::WeakCosmicSurface; +use cosmic_protocols::image_capture_source::v1::server::{ + zcosmic_workspace_image_capture_source_manager_v1::{ + Request as CosmicWorkspaceSourceRequest, ZcosmicWorkspaceImageCaptureSourceManagerV1, + }, +}; +use smithay::reexports::wayland_protocols::ext::image_capture_source::v1::server::{ + ext_image_capture_source_v1::ExtImageCaptureSourceV1, +}; +use smithay::{ + output::WeakOutput, + reexports::wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, + }, + wayland::{image_capture_source::{ImageCaptureSource, ImageCaptureSourceData}, Dispatch2, GlobalDispatch2}, +}; +use wayland_backend::server::GlobalId; + +#[derive(Debug)] +pub struct CosmicImageCaptureSourceState { + workspace_source_global: GlobalId, +} + +pub struct WorkspaceImageCaptureSourceManagerGlobalData { + filter: Box Fn(&'a Client) -> bool + Send + Sync>, +} + +#[derive(Debug, Clone)] +pub enum ImageCaptureSourceKind { + Output(WeakOutput), + Workspace(WorkspaceHandle), + Toplevel(WeakCosmicSurface), + Destroyed, +} + +impl ImageCaptureSourceKind { + pub fn from_source(source: &ImageCaptureSource) -> Self { + // If no user-data, assume source was created for a destroyed output, etc. + source + .user_data() + .get::() + .cloned() + .unwrap_or(Self::Destroyed) + } +} + +impl CosmicImageCaptureSourceState { + pub fn new(display: &DisplayHandle, client_filter: F) -> CosmicImageCaptureSourceState + where + D: GlobalDispatch< + ZcosmicWorkspaceImageCaptureSourceManagerV1, + WorkspaceImageCaptureSourceManagerGlobalData, + > + Dispatch + + Dispatch + + WorkspaceHandler + + 'static, + F: for<'a> Fn(&'a Client) -> bool + Send + Sync + Clone + 'static, + { + CosmicImageCaptureSourceState { + workspace_source_global: display + .create_global::( + 1, + WorkspaceImageCaptureSourceManagerGlobalData { + filter: Box::new(client_filter.clone()), + }, + ), + } + } + + pub fn workspace_source_id(&self) -> &GlobalId { + &self.workspace_source_global + } +} + +pub struct CaptureSourceManagerData; + +impl GlobalDispatch2 + for WorkspaceImageCaptureSourceManagerGlobalData +where + D: Dispatch + + Dispatch + + 'static, +{ + fn bind( + &self, + _state: &mut D, + _handle: &DisplayHandle, + _client: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, CaptureSourceManagerData); + } + + fn can_view(&self, client: &Client) -> bool { + (self.filter)(client) + } +} + +impl Dispatch2 for CaptureSourceManagerData +where + D: Dispatch + WorkspaceHandler + 'static, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + _resource: &ZcosmicWorkspaceImageCaptureSourceManagerV1, + request: ::Request, + _dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + if let CosmicWorkspaceSourceRequest::CreateSource { + source: source_handle, + output, + } = request + { + let data = match state.workspace_state().get_ext_workspace_handle(&output) { + Some(workspace) => ImageCaptureSourceKind::Workspace(workspace), + None => ImageCaptureSourceKind::Destroyed, + }; + let source = ImageCaptureSource::new(); + source.user_data().insert_if_missing(|| data); + let instance = data_init.init( + source_handle, + ImageCaptureSourceData { + source: source.clone(), + }, + ); + source.add_instance(&instance); + } + } +} diff --git a/src/wayland/protocols/keyboard_layout.rs b/src/wayland/protocols/keyboard_layout.rs new file mode 100644 index 0000000..f3a44f0 --- /dev/null +++ b/src/wayland/protocols/keyboard_layout.rs @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use cosmic_protocols::keyboard_layout::v1::server::{ + zcosmic_keyboard_layout_manager_v1::{self, ZcosmicKeyboardLayoutManagerV1}, + zcosmic_keyboard_layout_v1::{self, ZcosmicKeyboardLayoutV1}, +}; +use smithay::{ + input::{ + SeatHandler, + keyboard::{KeyboardHandle, Layout}, + }, + reexports::wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, + }, + wayland::{Dispatch2, GlobalDispatch2}, +}; +use std::mem; +use wayland_backend::server::{ClientId, GlobalId}; + +pub trait KeyboardLayoutHandler { + fn keyboard_layout_state(&mut self) -> &mut KeyboardLayoutState; +} + +#[derive(Debug)] +pub struct KeyboardLayoutState { + pub global: GlobalId, + keyboard_layouts: Vec<(ZcosmicKeyboardLayoutV1, Option)>, +} + +impl KeyboardLayoutState { + pub fn new(dh: &DisplayHandle, client_filter: F) -> Self + where + D: GlobalDispatch + 'static, + F: for<'a> Fn(&'a Client) -> bool + Send + Sync + 'static, + { + let global = dh.create_global::( + 1, + LayoutGlobalData { + filter: Box::new(client_filter), + }, + ); + KeyboardLayoutState { + global, + keyboard_layouts: Vec::new(), + } + } + + pub fn refresh(state: &mut D) + where + D: SeatHandler + KeyboardLayoutHandler + 'static, + { + let mut keyboard_layouts = mem::take(&mut state.keyboard_layout_state().keyboard_layouts); + for (keyboard_layout, last_layout) in &mut keyboard_layouts { + if let Some(data) = keyboard_layout.data::>() + && let Some(handle) = &data.handle + { + let active_layout = handle.with_xkb_state(state, |context| { + context.xkb().lock().unwrap().active_layout() + }); + if *last_layout != Some(active_layout) { + keyboard_layout.group(active_layout.0); + *last_layout = Some(active_layout); + } + } + } + state.keyboard_layout_state().keyboard_layouts = keyboard_layouts; + } +} + +pub struct LayoutGlobalData { + filter: Box Fn(&'a Client) -> bool + Send + Sync>, +} + +struct LayoutManagerData; + +impl GlobalDispatch2 for LayoutGlobalData +where + D: Dispatch + 'static, +{ + fn bind( + &self, + _state: &mut D, + _handle: &DisplayHandle, + _client: &Client, + resource: New, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, LayoutManagerData); + } + + fn can_view(&self, client: &Client) -> bool { + (self.filter)(client) + } +} + +impl Dispatch2 for LayoutManagerData +where + D: Dispatch>, + D: 'static, + D: SeatHandler, + D: KeyboardLayoutHandler, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + _resource: &ZcosmicKeyboardLayoutManagerV1, + request: zcosmic_keyboard_layout_manager_v1::Request, + _dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + zcosmic_keyboard_layout_manager_v1::Request::GetKeyboardLayout { + keyboard_layout, + keyboard, + } => { + let handle = KeyboardHandle::::from_resource(&keyboard); + let active_layout = handle.as_ref().map(|handle| { + handle.with_xkb_state(state, |context| { + context.xkb().lock().unwrap().active_layout() + }) + }); + let keyboard_layout = data_init.init(keyboard_layout, LayoutUserData { handle }); + if let Some(layout) = active_layout { + keyboard_layout.group(layout.0); + } + state + .keyboard_layout_state() + .keyboard_layouts + .push((keyboard_layout, active_layout)); + } + zcosmic_keyboard_layout_manager_v1::Request::Destroy => {} + _ => unreachable!(), + } + } +} + +#[doc(hidden)] +struct LayoutUserData { + handle: Option>, +} + +impl Dispatch2 for LayoutUserData +where + D: 'static, + D: SeatHandler, + D: KeyboardLayoutHandler, +{ + fn request( + &self, + state: &mut D, + _client: &Client, + _resource: &ZcosmicKeyboardLayoutV1, + request: zcosmic_keyboard_layout_v1::Request, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + zcosmic_keyboard_layout_v1::Request::SetGroup { group } => { + if let Some(handle) = self.handle.as_ref() { + handle.with_xkb_state(state, |mut context| { + context.set_layout(Layout(group)); + }); + } + } + zcosmic_keyboard_layout_v1::Request::Destroy => {} + _ => unreachable!(), + } + } + + fn destroyed( + &self, + state: &mut D, + _client: ClientId, + keyboard_layout: &ZcosmicKeyboardLayoutV1, + ) { + let keyboard_layouts = &mut state.keyboard_layout_state().keyboard_layouts; + if let Some(idx) = keyboard_layouts + .iter() + .position(|(x, _)| x == keyboard_layout) + { + keyboard_layouts.remove(idx); + } + } +} diff --git a/src/wayland/protocols/mod.rs b/src/wayland/protocols/mod.rs new file mode 100644 index 0000000..c6c0234 --- /dev/null +++ b/src/wayland/protocols/mod.rs @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pub mod a11y; +pub mod corner_radius; +pub mod drm; +pub mod image_capture_source; +pub mod keyboard_layout; +pub mod output_configuration; +pub mod output_power; +pub mod overlap_notify; +pub mod toplevel_info; +pub mod toplevel_management; +pub mod workspace; diff --git a/src/wayland/protocols/output_configuration/handlers/cosmic.rs b/src/wayland/protocols/output_configuration/handlers/cosmic.rs new file mode 100644 index 0000000..919baed --- /dev/null +++ b/src/wayland/protocols/output_configuration/handlers/cosmic.rs @@ -0,0 +1,315 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use smithay::{ + output::Mode, + reexports::{ + wayland_protocols_wlr::output_management::v1::server::{ + zwlr_output_configuration_head_v1::{self, ZwlrOutputConfigurationHeadV1}, + zwlr_output_configuration_v1::{self, ZwlrOutputConfigurationV1}, + zwlr_output_head_v1::ZwlrOutputHeadV1, + }, + wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, Weak, + }, + }, +}; + +use cosmic_comp_config::output::comp::OutputState as EnabledState; + +use cosmic_protocols::output_management::v1::server::{ + zcosmic_output_configuration_head_v1::{self, ZcosmicOutputConfigurationHeadV1}, + zcosmic_output_configuration_v1::{self, ZcosmicOutputConfigurationV1}, + zcosmic_output_head_v1::{self, ZcosmicOutputHeadV1}, + zcosmic_output_manager_v1::{self, ZcosmicOutputManagerV1}, +}; + +use crate::wayland::protocols::output_configuration::*; + +impl GlobalDispatch + for OutputConfigurationState +where + D: GlobalDispatch + + Dispatch + + Dispatch> + + Dispatch> + + Dispatch> + + OutputConfigurationHandler + + 'static, +{ + fn bind( + _state: &mut D, + _dh: &DisplayHandle, + _client: &Client, + resource: New, + _global_data: &OutputMngrGlobalData, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, ()); + } + + fn can_view(client: Client, global_data: &OutputMngrGlobalData) -> bool { + (global_data.filter)(&client) + } +} + +impl Dispatch for OutputConfigurationState +where + D: GlobalDispatch + + Dispatch + + Dispatch + + Dispatch + + Dispatch + + Dispatch + + GlobalDispatch + + Dispatch + + Dispatch> + + Dispatch> + + Dispatch> + + OutputConfigurationHandler + + 'static, +{ + fn request( + state: &mut D, + _client: &Client, + _obj: &ZcosmicOutputManagerV1, + request: zcosmic_output_manager_v1::Request, + _data: &(), + dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + zcosmic_output_manager_v1::Request::GetHead { extended, head } => { + let inner = state.output_configuration_state(); + if let Some(mngr) = inner + .instances + .iter_mut() + .find(|instance| instance.heads.iter().any(|instance| instance.obj == head)) + { + let head_data = mngr + .heads + .iter_mut() + .find(|instance| instance.obj == head) + .unwrap(); + let obj = data_init.init(extended, head.downgrade()); + head_data.extension_obj = Some(obj); + let output = head_data.output.clone(); + + send_head_to_client::(dh, mngr, &output); + for manager in inner.instances.iter() { + manager.obj.done(inner.serial_counter); + } + } + } + zcosmic_output_manager_v1::Request::GetConfiguration { extended, config } => { + let pending = config.data::().unwrap(); + let obj = data_init.init(extended, config.downgrade()); + pending.lock().unwrap().extension_obj = Some(obj); + } + zcosmic_output_manager_v1::Request::GetConfigurationHead { + extended, + config_head, + } => { + data_init.init(extended, config_head.downgrade()); + } + zcosmic_output_manager_v1::Request::SetXwaylandPrimary { head } => { + let Some(head) = head else { + state.request_xwayland_primary(None); + return; + }; + + let inner = state.output_configuration_state(); + if let Some(head_data) = inner.instances.iter_mut().find_map(|instance| { + instance + .heads + .iter() + .find(|instance| instance.extension_obj.as_ref() == Some(&head)) + }) { + let output = head_data.output.clone(); + if output.config().enabled == EnabledState::Enabled { + state.request_xwayland_primary(Some(output)); + } + } + } + _ => {} + } + } +} + +impl Dispatch, D> for OutputConfigurationState +where + D: OutputConfigurationHandler + 'static, +{ + fn request( + state: &mut D, + _client: &Client, + obj: &ZcosmicOutputHeadV1, + request: zcosmic_output_head_v1::Request, + _data: &Weak, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + if let zcosmic_output_head_v1::Request::Release = request { + let inner = state.output_configuration_state(); + if let Some(head) = inner + .instances + .iter_mut() + .flat_map(|instance| instance.heads.iter_mut()) + .find(|head| head.extension_obj.as_ref().is_some_and(|o| o == obj)) + { + head.extension_obj.take(); + } + } + } +} + +impl Dispatch, D> + for OutputConfigurationState +where + D: GlobalDispatch + + Dispatch + + Dispatch + + Dispatch + + Dispatch + + Dispatch + + GlobalDispatch + + Dispatch + + Dispatch> + + Dispatch> + + Dispatch> + + OutputConfigurationHandler + + 'static, +{ + fn request( + _state: &mut D, + _client: &Client, + extension_obj: &ZcosmicOutputConfigurationV1, + request: zcosmic_output_configuration_v1::Request, + obj: &Weak, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + zcosmic_output_configuration_v1::Request::MirrorHead { + id, + head, + mirroring, + } => { + if let Ok(obj) = obj.upgrade() { + let data = obj.data::().unwrap(); + if let Some(output) = mirroring.data::().unwrap().upgrade() { + let mut pending = data.lock().unwrap(); + if pending.heads.iter().any(|(h, _)| *h == head) { + obj.post_error( + zwlr_output_configuration_v1::Error::AlreadyConfiguredHead, + format!("{:?} was already configured", head), + ); + return; + } + + if pending.heads.iter().any(|(h, c)| { + match c.as_ref() { + Some(c) => { + let conf = c.data::().unwrap(); + match conf.lock().unwrap().mirroring.as_ref() { + Some(mirrored) => { + head.data::().unwrap() == mirrored // we are already a mirror target -> invalid + || *h == mirroring // our target already mirrors -> invalid + } + None => false, + } + } + None => *h == mirroring, // disabled state for our mirror target -> invalid + } + }) { + extension_obj.post_error( + zcosmic_output_configuration_v1::Error::MirroredHeadBusy, + format!("{:?} can't mirror, it is either a mirror target itself or {:?} is not enabled/already mirroring", head, mirroring), + ); + } + + let output_conf = PendingOutputConfiguration::default(); + output_conf.lock().unwrap().mirroring = Some(output.clone()); + let conf_head = data_init.init(id, output_conf); + pending.heads.push((head, Some(conf_head))); + } else { + let output_conf = PendingOutputConfiguration::default(); + data_init.init(id, output_conf); + } + } else { + let output_conf = PendingOutputConfiguration::default(); + data_init.init(id, output_conf); + } + } + zcosmic_output_configuration_v1::Request::Release => { + if let Ok(obj) = obj.upgrade() { + let data = obj.data::().unwrap(); + data.lock().unwrap().extension_obj.take(); + } + } + _ => {} + } + } +} + +impl Dispatch, D> + for OutputConfigurationState +where + D: GlobalDispatch + + Dispatch + + Dispatch + + Dispatch + + Dispatch + + Dispatch + + OutputConfigurationHandler + + 'static, +{ + fn request( + _state: &mut D, + _client: &Client, + _extended_obj: &ZcosmicOutputConfigurationHeadV1, + request: zcosmic_output_configuration_head_v1::Request, + obj: &Weak, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + zcosmic_output_configuration_head_v1::Request::SetScale1000 { scale_1000 } => { + if let Ok(obj) = obj.upgrade() { + let data = obj.data::().unwrap(); + let mut pending = data.lock().unwrap(); + if pending.scale.is_some() { + obj.post_error( + zwlr_output_configuration_head_v1::Error::AlreadySet, + format!("{:?} already had a scale configured", obj), + ); + return; + } + pending.scale = Some((scale_1000 as f64) / 1000.0); + } + } + zcosmic_output_configuration_head_v1::Request::SetAdaptiveSyncExt { state } => { + if let Ok(obj) = obj.upgrade() { + let data = obj.data::().unwrap(); + let mut pending = data.lock().unwrap(); + if pending.adaptive_sync.is_some() { + obj.post_error( + zwlr_output_configuration_head_v1::Error::AlreadySet, + format!("{:?} already had an adaptive_sync state configured", obj), + ); + return; + } + pending.adaptive_sync = match state.into_result() { + Ok(zcosmic_output_head_v1::AdaptiveSyncStateExt::Always) => { + Some(AdaptiveSync::Force) + } + Ok(zcosmic_output_head_v1::AdaptiveSyncStateExt::Automatic) => { + Some(AdaptiveSync::Enabled) + } + _ => Some(AdaptiveSync::Disabled), + }; + } + } + _ => {} + } + } +} diff --git a/src/wayland/protocols/output_configuration/handlers/mod.rs b/src/wayland/protocols/output_configuration/handlers/mod.rs new file mode 100644 index 0000000..8115845 --- /dev/null +++ b/src/wayland/protocols/output_configuration/handlers/mod.rs @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: GPL-3.0-only + +mod cosmic; +mod wlr; diff --git a/src/wayland/protocols/output_configuration/handlers/wlr.rs b/src/wayland/protocols/output_configuration/handlers/wlr.rs new file mode 100644 index 0000000..7ab7a71 --- /dev/null +++ b/src/wayland/protocols/output_configuration/handlers/wlr.rs @@ -0,0 +1,486 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use cosmic_protocols::output_management::v1::server::zcosmic_output_configuration_v1; +use smithay::{ + output::{Mode, Output, WeakOutput}, + reexports::{ + wayland_protocols_wlr::output_management::v1::server::{ + zwlr_output_configuration_head_v1::{self, ZwlrOutputConfigurationHeadV1}, + zwlr_output_configuration_v1::{self, ZwlrOutputConfigurationV1}, + zwlr_output_head_v1::{self, ZwlrOutputHeadV1}, + zwlr_output_manager_v1::{self, ZwlrOutputManagerV1}, + zwlr_output_mode_v1::{self, ZwlrOutputModeV1}, + }, + wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, + backend::ClientId, + }, + }, + utils::{Point, Size}, +}; +use std::convert::TryInto; + +use crate::wayland::protocols::output_configuration::*; + +impl GlobalDispatch for OutputConfigurationState +where + D: GlobalDispatch + + Dispatch + + Dispatch + + Dispatch + + Dispatch + + Dispatch + + OutputConfigurationHandler + + 'static, +{ + fn bind( + state: &mut D, + dh: &DisplayHandle, + _client: &Client, + resource: New, + _global_data: &OutputMngrGlobalData, + data_init: &mut DataInit<'_, D>, + ) { + let mut instance = OutputMngrInstance { + obj: data_init.init(resource, ()), + heads: Vec::new(), + }; + + let mngr_state = state.output_configuration_state(); + for output in &mngr_state.outputs { + send_head_to_client::(dh, &mut instance, output); + } + instance.obj.done(mngr_state.serial_counter); + mngr_state.instances.push(instance); + } + + fn can_view(client: Client, global_data: &OutputMngrGlobalData) -> bool { + (global_data.filter)(&client) + } +} + +impl Dispatch for OutputConfigurationState +where + D: GlobalDispatch + + Dispatch + + Dispatch + + Dispatch + + Dispatch + + Dispatch + + OutputConfigurationHandler + + 'static, +{ + fn request( + state: &mut D, + _client: &Client, + obj: &ZwlrOutputManagerV1, + request: zwlr_output_manager_v1::Request, + _data: &(), + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + zwlr_output_manager_v1::Request::CreateConfiguration { id, serial } => { + let conf = data_init.init( + id, + PendingConfiguration::new(PendingConfigurationInner { + extension_obj: None, + serial, + used: false, + heads: Vec::new(), + }), + ); + + let state = state.output_configuration_state(); + if serial != state.serial_counter { + conf.cancelled(); + } + } + zwlr_output_manager_v1::Request::Stop => { + let state = state.output_configuration_state(); + state.instances.retain(|instance| instance.obj != *obj); + obj.finished(); + } + _ => {} + } + } + + fn destroyed( + state: &mut D, + _client: wayland_backend::server::ClientId, + obj: &ZwlrOutputManagerV1, + _data: &(), + ) { + let state = state.output_configuration_state(); + state.instances.retain(|instance| instance.obj != *obj); + } +} + +impl Dispatch for OutputConfigurationState +where + D: GlobalDispatch + + Dispatch + + Dispatch + + Dispatch + + Dispatch + + Dispatch + + OutputConfigurationHandler + + 'static, +{ + fn request( + state: &mut D, + _client: &Client, + obj: &ZwlrOutputHeadV1, + request: zwlr_output_head_v1::Request, + _data: &WeakOutput, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + if let zwlr_output_head_v1::Request::Release = request { + for instance in &mut state.output_configuration_state().instances { + instance.heads.retain(|h| &h.obj != obj); + } + } + } + + fn destroyed(state: &mut D, _client: ClientId, obj: &ZwlrOutputHeadV1, _data: &WeakOutput) { + for instance in &mut state.output_configuration_state().instances { + instance.heads.retain(|h| &h.obj != obj); + } + } +} + +impl Dispatch for OutputConfigurationState +where + D: GlobalDispatch + + Dispatch + + Dispatch + + Dispatch + + Dispatch + + Dispatch + + OutputConfigurationHandler + + 'static, +{ + fn request( + state: &mut D, + _client: &Client, + obj: &ZwlrOutputModeV1, + request: zwlr_output_mode_v1::Request, + _data: &Mode, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + if let zwlr_output_mode_v1::Request::Release = request { + let state = state.output_configuration_state(); + for instance in &mut state.instances { + for head in &mut instance.heads { + head.modes.retain(|mode| mode != obj) + } + } + } + } +} + +impl Dispatch for OutputConfigurationState +where + D: GlobalDispatch + + Dispatch + + Dispatch + + Dispatch + + Dispatch + + Dispatch + + OutputConfigurationHandler + + 'static, +{ + fn request( + state: &mut D, + _client: &Client, + obj: &ZwlrOutputConfigurationV1, + request: zwlr_output_configuration_v1::Request, + data: &PendingConfiguration, + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + zwlr_output_configuration_v1::Request::EnableHead { id, head } => { + let mut pending = data.lock().unwrap(); + if pending.heads.iter().any(|(h, _)| *h == head) { + obj.post_error( + zwlr_output_configuration_v1::Error::AlreadyConfiguredHead, + format!("{:?} was already configured", head), + ); + return; + } + + let conf_head = data_init.init(id, PendingOutputConfiguration::default()); + pending.heads.push((head, Some(conf_head))); + } + zwlr_output_configuration_v1::Request::DisableHead { head } => { + let mut pending = data.lock().unwrap(); + if pending.heads.iter().any(|(h, _)| *h == head) { + obj.post_error( + zwlr_output_configuration_v1::Error::AlreadyConfiguredHead, + format!("{:?} was already configured", head), + ); + return; + } + + if pending.heads.iter().any(|(_, c)| match c { + Some(conf) => { + let output_conf = conf.data::().unwrap(); + let output = head.data::().unwrap(); + let pending_conf = output_conf.lock().unwrap(); + pending_conf.mirroring.as_ref().is_some_and(|o| o == output) + } + None => false, + }) { + if let Some(extension_obj) = pending.extension_obj.as_ref() { + extension_obj.post_error( + zcosmic_output_configuration_v1::Error::MirroredHeadBusy, + format!("{:?} is disabled and mirrored", head), + ); + } else { + // unreachable? + head.post_error( + zwlr_output_configuration_v1::Error::UnconfiguredHead, + format!("{:?} is disabled and mirrored", head), + ); + } + } + + pending.heads.push((head, None)); + } + x @ zwlr_output_configuration_v1::Request::Apply + | x @ zwlr_output_configuration_v1::Request::Test => { + let mut pending = data.lock().unwrap(); + + if pending.used { + return obj.post_error( + zwlr_output_configuration_v1::Error::AlreadyUsed, + "Configuration object was used already".to_string(), + ); + } + pending.used = true; + + let inner = state.output_configuration_state(); + if pending.serial != inner.serial_counter { + obj.cancelled(); + if let Some(extension_obj) = pending.extension_obj.take() { + extension_obj.finished(); + } + return; + } + + let final_conf = match pending + .heads + .iter_mut() + .map(|(head, conf)| { + let output = match inner + .instances + .iter() + .find_map(|instance| instance.heads.iter().find(|h| h.obj == *head)) + .map(|i| i.output.clone()) + { + Some(o) => o, + None => { + return Err(zwlr_output_configuration_head_v1::Error::InvalidMode); + } + }; + + match conf { + Some(head) => (&mut *head + .data::() + .unwrap() + .lock() + .unwrap()) + .try_into() + .map(|c| (output, c)), + None => Ok((output, OutputConfiguration::Disabled)), + } + }) + .collect::, + zwlr_output_configuration_head_v1::Error, + >>() { + Ok(conf) => conf, + Err(code) => { + return obj.post_error(code, "Incomplete configuration".to_string()); + } + }; + + let configured_outputs = final_conf + .iter() + .map(|(o, _)| o.clone()) + .collect::>(); + + // handle potential races of destroyed heads and modes with cancel instead of a protocol error + // 1. If len doesn't match some outputs aren't configured (technically a protocol error) + // 2. If any configured output isn't in our list anymore, but we passed the len()-test, + // we raced a new head and a destroyed head, so again cancel. + // 3. If the selected mode isn't in our list anymore, we probably already send `finished` for + // the mode, but got no release yet. So again, racy -> cancel. + if configured_outputs.len() != inner.outputs.len() + || configured_outputs + .iter() + .any(|o| !inner.outputs.contains(o)) + || final_conf.iter().any(|(o, c)| match c { + OutputConfiguration::Enabled { + mode: Some(ModeConfiguration::Mode(m)), + .. + } => !o.modes().contains(m), + _ => false, + }) + { + obj.cancelled(); + if let Some(extension_obj) = pending.extension_obj.take() { + extension_obj.finished(); + } + return; + } + + let result = if matches!(x, zwlr_output_configuration_v1::Request::Test) { + state.test_configuration(final_conf) + } else { + state.apply_configuration(final_conf) + }; + + if result { + obj.succeeded(); + } else { + obj.failed(); + } + if let Some(extension_obj) = pending.extension_obj.take() { + extension_obj.finished(); + } + } + zwlr_output_configuration_v1::Request::Destroy => { + let mut pending = data.lock().unwrap(); + if let Some(extension_obj) = pending.extension_obj.take() { + extension_obj.finished(); + } + } + _ => {} + } + } +} + +impl Dispatch + for OutputConfigurationState +where + D: GlobalDispatch + + Dispatch + + Dispatch + + Dispatch + + Dispatch + + Dispatch + + OutputConfigurationHandler + + 'static, +{ + fn request( + _state: &mut D, + _client: &Client, + obj: &ZwlrOutputConfigurationHeadV1, + request: zwlr_output_configuration_head_v1::Request, + data: &PendingOutputConfiguration, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + zwlr_output_configuration_head_v1::Request::SetMode { mode } => { + let mut pending = data.lock().unwrap(); + if pending.mode.is_some() { + obj.post_error( + zwlr_output_configuration_head_v1::Error::AlreadySet, + format!("{:?} already had a mode configured", obj), + ); + return; + } + pending.mode = Some(ModeConfiguration::Mode(mode)); + } + zwlr_output_configuration_head_v1::Request::SetCustomMode { + width, + height, + refresh, + } => { + let mut pending = data.lock().unwrap(); + if pending.mode.is_some() { + obj.post_error( + zwlr_output_configuration_head_v1::Error::AlreadySet, + format!("{:?} already had a mode configured", obj), + ); + return; + } + pending.mode = Some(ModeConfiguration::Custom { + size: Size::from((width, height)), + refresh: if refresh == 0 { None } else { Some(refresh) }, + }); + } + zwlr_output_configuration_head_v1::Request::SetPosition { x, y } => { + let mut pending = data.lock().unwrap(); + if pending.position.is_some() { + obj.post_error( + zwlr_output_configuration_head_v1::Error::AlreadySet, + format!("{:?} already had a position configured", obj), + ); + return; + } + pending.position = Some(Point::from((x, y))); + } + zwlr_output_configuration_head_v1::Request::SetScale { scale } => { + let mut pending = data.lock().unwrap(); + if pending.scale.is_some() { + obj.post_error( + zwlr_output_configuration_head_v1::Error::AlreadySet, + format!("{:?} already had a scale configured", obj), + ); + return; + } + pending.scale = Some(scale); + } + zwlr_output_configuration_head_v1::Request::SetTransform { transform } => { + let mut pending = data.lock().unwrap(); + if pending.transform.is_some() { + obj.post_error( + zwlr_output_configuration_head_v1::Error::AlreadySet, + format!("{:?} already had a transform configured", obj), + ); + return; + } + pending.transform = Some(match transform.into_result() { + Ok(transform) => transform.into(), + Err(err) => { + obj.post_error( + zwlr_output_configuration_head_v1::Error::InvalidTransform, + format!("Invalid transform: {:?}", err), + ); + return; + } + }); + } + zwlr_output_configuration_head_v1::Request::SetAdaptiveSync { state } => { + let mut pending = data.lock().unwrap(); + if pending.adaptive_sync.is_some() { + obj.post_error( + zwlr_output_configuration_head_v1::Error::AlreadySet, + format!("{:?} already had adaptive sync configured", obj), + ); + return; + } + pending.adaptive_sync = Some(match state.into_result() { + Ok(state) => match state { + zwlr_output_head_v1::AdaptiveSyncState::Enabled => AdaptiveSync::Force, + _ => AdaptiveSync::Disabled, + }, + Err(err) => { + obj.post_error( + zwlr_output_configuration_head_v1::Error::InvalidAdaptiveSyncState, + format!("Invalid adaptive sync value: {:?}", err), + ); + return; + } + }); + } + _ => {} + } + } +} diff --git a/src/wayland/protocols/output_configuration/mod.rs b/src/wayland/protocols/output_configuration/mod.rs new file mode 100644 index 0000000..4bd1c0b --- /dev/null +++ b/src/wayland/protocols/output_configuration/mod.rs @@ -0,0 +1,581 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use calloop::{ + LoopHandle, + timer::{TimeoutAction, Timer}, +}; +use cosmic_comp_config::output::comp::AdaptiveSync; +use cosmic_protocols::output_management::v1::server::{ + zcosmic_output_configuration_head_v1::ZcosmicOutputConfigurationHeadV1, + zcosmic_output_configuration_v1::ZcosmicOutputConfigurationV1, + zcosmic_output_head_v1::{self, ZcosmicOutputHeadV1}, + zcosmic_output_manager_v1::ZcosmicOutputManagerV1, +}; +use smithay::{ + backend::drm::VrrSupport, + output::{Mode, Output, WeakOutput}, + reexports::{ + wayland_protocols_wlr::output_management::v1::server::{ + zwlr_output_configuration_head_v1::{self, ZwlrOutputConfigurationHeadV1}, + zwlr_output_configuration_v1::ZwlrOutputConfigurationV1, + zwlr_output_head_v1::{self, ZwlrOutputHeadV1}, + zwlr_output_manager_v1::ZwlrOutputManagerV1, + zwlr_output_mode_v1::ZwlrOutputModeV1, + }, + wayland_server::{ + Client, Dispatch, DisplayHandle, GlobalDispatch, Resource, Weak, backend::GlobalId, + protocol::wl_output::WlOutput, + }, + }, + utils::{Logical, Physical, Point, Size, Transform}, + wayland::output::WlOutputData, +}; +use std::{convert::TryFrom, sync::Mutex, time::Duration}; + +mod handlers; + +pub fn head_is_enabled(output: &Output) -> bool { + output + .user_data() + .get::() + .is_some_and(|inner| inner.lock().unwrap().enabled) +} + +#[derive(Debug)] +pub struct OutputConfigurationState { + outputs: Vec, + removed_outputs: Vec, + instances: Vec, + serial_counter: u32, + global: GlobalId, + extension_global: GlobalId, + dh: DisplayHandle, + event_loop_handle: LoopHandle<'static, D>, + _dispatch: std::marker::PhantomData, +} + +pub trait OutputConfigurationHandler: Sized { + fn output_configuration_state(&mut self) -> &mut OutputConfigurationState; + + fn test_configuration(&mut self, conf: Vec<(Output, OutputConfiguration)>) -> bool; + fn apply_configuration(&mut self, conf: Vec<(Output, OutputConfiguration)>) -> bool; + + fn request_xwayland_primary(&mut self, output: Option); +} + +pub struct OutputMngrGlobalData { + filter: Box Fn(&'a Client) -> bool + Send + Sync>, +} + +#[derive(Debug)] +struct OutputMngrInstance { + obj: ZwlrOutputManagerV1, + heads: Vec, +} + +#[derive(Debug)] +struct OutputHeadInstance { + obj: ZwlrOutputHeadV1, + extension_obj: Option, + output: Output, + modes: Vec, + finished: bool, +} + +#[derive(Debug, Default)] +pub struct PendingConfigurationInner { + extension_obj: Option, + serial: u32, + used: bool, + heads: Vec<(ZwlrOutputHeadV1, Option)>, +} + +pub type PendingConfiguration = Mutex; + +#[derive(Debug, Clone)] +pub enum ModeConfiguration { + Mode(M), + Custom { + size: Size, + refresh: Option, + }, +} + +#[derive(Debug, Default, Clone)] +pub struct PendingOutputConfigurationInner { + mirroring: Option, + mode: Option>, + position: Option>, + transform: Option, + scale: Option, + adaptive_sync: Option, +} +pub type PendingOutputConfiguration = Mutex; + +#[derive(Debug, Clone)] +pub enum OutputConfiguration { + Enabled { + mirroring: Option, + mode: Option>, + position: Option>, + transform: Option, + scale: Option, + adaptive_sync: Option, + }, + Disabled, +} + +impl<'a> TryFrom<&'a mut PendingOutputConfigurationInner> for OutputConfiguration { + type Error = zwlr_output_configuration_head_v1::Error; + fn try_from( + pending: &'a mut PendingOutputConfigurationInner, + ) -> Result { + let mode = match pending.mode.clone() { + Some(ModeConfiguration::Mode(wlr_mode)) => Some(ModeConfiguration::Mode( + wlr_mode + .data::() + .cloned() + .ok_or(zwlr_output_configuration_head_v1::Error::InvalidMode)?, + )), + Some(ModeConfiguration::Custom { size, refresh }) => { + Some(ModeConfiguration::Custom { size, refresh }) + } + None => None, + }; + Ok(OutputConfiguration::Enabled { + mode, + mirroring: pending.mirroring.clone(), + position: pending.position, + transform: pending.transform, + scale: pending.scale, + adaptive_sync: pending.adaptive_sync, + }) + } +} + +struct OutputStateInner { + enabled: bool, + global: Option, +} +type OutputState = Mutex; + +impl OutputConfigurationState +where + D: GlobalDispatch + + GlobalDispatch + + Dispatch + + Dispatch + + Dispatch + + Dispatch + + Dispatch + + GlobalDispatch + + Dispatch + + Dispatch> + + Dispatch> + + Dispatch> + + OutputConfigurationHandler + + 'static, +{ + pub fn new( + dh: &DisplayHandle, + event_loop_handle: LoopHandle<'static, D>, + client_filter: F, + ) -> OutputConfigurationState + where + F: for<'a> Fn(&'a Client) -> bool + Clone + Send + Sync + 'static, + { + let global = dh.create_global::( + 4, + OutputMngrGlobalData { + filter: Box::new(client_filter.clone()), + }, + ); + + let extension_global = dh.create_global::( + 3, + OutputMngrGlobalData { + filter: Box::new(client_filter), + }, + ); + + OutputConfigurationState { + outputs: Vec::new(), + removed_outputs: Vec::new(), + instances: Vec::new(), + serial_counter: 0, + global, + extension_global, + dh: dh.clone(), + event_loop_handle: event_loop_handle.clone(), + _dispatch: std::marker::PhantomData, + } + } + + pub fn global_id(&self) -> GlobalId { + self.global.clone() + } + + pub fn extension_global_id(&self) -> GlobalId { + self.extension_global.clone() + } + + pub fn add_heads<'a>(&mut self, outputs: impl Iterator) { + let new_outputs = outputs + .filter(|o| !self.outputs.contains(o)) + .collect::>(); + + for output in new_outputs { + let added = output.user_data().insert_if_missing(|| { + OutputState::new(OutputStateInner { + enabled: true, + global: None, + }) + }); + if !added { + // If head was previous added, enable it again + let mut inner = output + .user_data() + .get::() + .unwrap() + .lock() + .unwrap(); + inner.enabled = true; + } + self.outputs.push(output.clone()); + } + } + + pub fn remove_heads<'a>(&mut self, outputs: impl Iterator) { + for output in outputs { + if self.outputs.contains(output) { + self.removed_outputs.push(output.clone()); + if let Some(inner) = output.user_data().get::() { + let mut inner = inner.lock().unwrap(); + inner.enabled = false; + output.leave_all(); + if let Some(global) = inner.global.take() { + remove_global_with_timer(&self.dh, &self.event_loop_handle, global); + } + } + } + } + self.outputs.retain(|x| !self.removed_outputs.contains(x)); + } + + pub fn enable_head(&self, output: &Output) { + if let Some(inner) = output.user_data().get::() { + let mut inner = inner.lock().unwrap(); + inner.enabled = true; + } + } + + pub fn disable_head(&self, output: &Output) { + if let Some(inner) = output.user_data().get::() { + let mut inner = inner.lock().unwrap(); + inner.enabled = false; + } + } + + pub fn update(&mut self) { + self.serial_counter += 1; + + for output in std::mem::take(&mut self.removed_outputs).into_iter() { + for instance in &mut self.instances { + let mut removed_heads = Vec::new(); + for head in &mut instance.heads { + if head.output == output { + if head.obj.version() < zwlr_output_head_v1::REQ_RELEASE_SINCE { + removed_heads.push(head.obj.clone()); + } + for mode in &mut head.modes { + mode.finished(); + } + head.obj.finished(); + head.finished = true; + } + } + instance.heads.retain(|h| !removed_heads.contains(&h.obj)) + } + } + + for output in &self.outputs { + { + let state = output.user_data().get::().unwrap(); + let mut inner = state.lock().unwrap(); + if inner.enabled && inner.global.is_none() { + inner.global = Some(output.create_global::(&self.dh)); + } + if !inner.enabled && inner.global.is_some() { + remove_global_with_timer( + &self.dh, + &self.event_loop_handle, + inner.global.take().unwrap(), + ); + } + } + for manager in self.instances.iter_mut() { + send_head_to_client::(&self.dh, manager, output); + } + } + + for manager in self.instances.iter() { + manager.obj.done(self.serial_counter); + } + } + + pub fn outputs(&self) -> impl Iterator { + self.outputs.clone().into_iter() + } +} + +fn send_head_to_client(dh: &DisplayHandle, mngr: &mut OutputMngrInstance, output: &Output) +where + D: GlobalDispatch + + Dispatch + + Dispatch + + Dispatch + + Dispatch + + OutputConfigurationHandler + + 'static, +{ + let instance = match mngr + .heads + .iter_mut() + .filter(|i| !i.finished) + .find(|i| i.output == *output) + { + Some(i) => i, + None => { + if let Ok(client) = dh.get_client(mngr.obj.id()) { + if let Ok(head) = client.create_resource::( + dh, + mngr.obj.version(), + output.downgrade(), + ) { + mngr.obj.head(&head); + let data = OutputHeadInstance { + obj: head, + extension_obj: None, + modes: Vec::new(), + output: output.clone(), + finished: false, + }; + mngr.heads.push(data); + mngr.heads.last_mut().unwrap() + } else { + return; + } + } else { + return; + } + } + }; + + instance.obj.name(output.name()); + instance.obj.description(output.description()); + let physical = output.physical_properties(); + if !(physical.size.w == 0 || physical.size.h == 0) { + instance.obj.physical_size(physical.size.w, physical.size.h); + } + + let inner = output + .user_data() + .get::() + .unwrap() + .lock() + .unwrap(); + + let output_modes = output.modes(); + // remove old modes + instance.modes.retain_mut(|m| { + if !output_modes.contains(m.data::().unwrap()) { + m.finished(); + false + } else { + true + } + }); + + // update other modes + for output_mode in output_modes.into_iter() { + if let Some(mode) = if let Some(wlr_mode) = instance + .modes + .iter() + .find(|mode| *mode.data::().unwrap() == output_mode) + { + Some(wlr_mode) + } else if let Ok(client) = dh.get_client(instance.obj.id()) { + // create the mode if it does not exist yet + if let Ok(mode) = client.create_resource::( + dh, + instance.obj.version(), + output_mode, + ) { + instance.obj.mode(&mode); + mode.size(output_mode.size.w, output_mode.size.h); + mode.refresh(output_mode.refresh); + if output + .preferred_mode() + .map(|p| p == output_mode) + .unwrap_or(false) + { + mode.preferred(); + } + instance.modes.push(mode); + instance.modes.last() + } else { + None + } + } else { + None + } && inner.enabled + && output + .current_mode() + .map(|c| c == output_mode) + .unwrap_or(false) + { + instance.obj.current_mode(mode); + } + } + + instance + .obj + .enabled(if inner.enabled || output.mirroring().is_some() { + 1 + } else { + 0 + }); + if inner.enabled || output.mirroring().is_some() { + let point = output.current_location(); + instance.obj.position(point.x, point.y); + instance.obj.transform(output.current_transform().into()); + + let scale = output.current_scale().fractional_scale(); + instance.obj.scale(scale); + + if instance.obj.version() >= zwlr_output_head_v1::EVT_ADAPTIVE_SYNC_SINCE { + instance + .obj + .adaptive_sync(if output.adaptive_sync() == AdaptiveSync::Disabled { + zwlr_output_head_v1::AdaptiveSyncState::Disabled + } else { + zwlr_output_head_v1::AdaptiveSyncState::Enabled + }); + } + + if let Some(extension_obj) = instance.extension_obj.as_ref() { + extension_obj.scale_1000((scale * 1000.0).round() as i32); + + extension_obj.mirroring(output.mirroring().map(|o| o.name())); + + if extension_obj.version() >= zcosmic_output_head_v1::EVT_ADAPTIVE_SYNC_EXT_SINCE { + extension_obj.adaptive_sync_ext(match output.adaptive_sync() { + AdaptiveSync::Disabled => { + zcosmic_output_head_v1::AdaptiveSyncStateExt::Disabled + } + AdaptiveSync::Enabled => { + zcosmic_output_head_v1::AdaptiveSyncStateExt::Automatic + } + AdaptiveSync::Force => zcosmic_output_head_v1::AdaptiveSyncStateExt::Always, + }); + + extension_obj.adaptive_sync_available( + match output + .adaptive_sync_support() + .unwrap_or(VrrSupport::NotSupported) + { + VrrSupport::NotSupported => { + zcosmic_output_head_v1::AdaptiveSyncAvailability::Unsupported + } + VrrSupport::RequiresModeset => { + zcosmic_output_head_v1::AdaptiveSyncAvailability::RequiresModeset + } + VrrSupport::Supported => { + zcosmic_output_head_v1::AdaptiveSyncAvailability::Supported + } + }, + ); + } + } + } + + if instance.obj.version() >= zwlr_output_head_v1::EVT_MAKE_SINCE { + if physical.make != "Unknown" { + instance.obj.make(physical.make.clone()); + } + if physical.model != "Unknown" { + instance.obj.model(physical.model); + } + if physical.serial_number != "Unknown" { + instance.obj.serial_number(physical.serial_number); + } + } + + if let Some(extension_obj) = instance.extension_obj.as_ref() + && inner.enabled + && extension_obj.version() >= zcosmic_output_head_v1::EVT_XWAYLAND_PRIMARY_SINCE + { + extension_obj.xwayland_primary(output.config().xwayland_primary as u32); + } +} + +fn remove_global_with_timer( + dh: &DisplayHandle, + event_loop_handle: &LoopHandle, + id: GlobalId, +) { + dh.disable_global::(id.clone()); + let source = Timer::from_duration(Duration::from_secs(5)); + let dh = dh.clone(); + let res = event_loop_handle.insert_source(source, move |_, _, _state| { + dh.remove_global::(id.clone()); + TimeoutAction::Drop + }); + if let Err(err) = res { + tracing::error!( + "failed to insert timer source to destroy output global: {}", + err + ); + } +} + +macro_rules! delegate_output_configuration { + ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { + smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_manager_v1::ZwlrOutputManagerV1: $crate::wayland::protocols::output_configuration::OutputMngrGlobalData + ] => $crate::wayland::protocols::output_configuration::OutputConfigurationState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_manager_v1::ZwlrOutputManagerV1: () + ] => $crate::wayland::protocols::output_configuration::OutputConfigurationState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_head_v1::ZwlrOutputHeadV1: smithay::output::WeakOutput + ] => $crate::wayland::protocols::output_configuration::OutputConfigurationState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_mode_v1::ZwlrOutputModeV1: smithay::output::Mode + ] => $crate::wayland::protocols::output_configuration::OutputConfigurationState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_configuration_v1::ZwlrOutputConfigurationV1: $crate::wayland::protocols::output_configuration::PendingConfiguration + ] => $crate::wayland::protocols::output_configuration::OutputConfigurationState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_configuration_head_v1::ZwlrOutputConfigurationHeadV1: $crate::wayland::protocols::output_configuration::PendingOutputConfiguration + ] => $crate::wayland::protocols::output_configuration::OutputConfigurationState); + smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + cosmic_protocols::output_management::v1::server::zcosmic_output_manager_v1::ZcosmicOutputManagerV1: $crate::wayland::protocols::output_configuration::OutputMngrGlobalData + ] => $crate::wayland::protocols::output_configuration::OutputConfigurationState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + cosmic_protocols::output_management::v1::server::zcosmic_output_manager_v1::ZcosmicOutputManagerV1: () + ] => $crate::wayland::protocols::output_configuration::OutputConfigurationState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + cosmic_protocols::output_management::v1::server::zcosmic_output_head_v1::ZcosmicOutputHeadV1: smithay::reexports::wayland_server::Weak + ] => $crate::wayland::protocols::output_configuration::OutputConfigurationState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + cosmic_protocols::output_management::v1::server::zcosmic_output_configuration_v1::ZcosmicOutputConfigurationV1: smithay::reexports::wayland_server::Weak + ] => $crate::wayland::protocols::output_configuration::OutputConfigurationState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + cosmic_protocols::output_management::v1::server::zcosmic_output_configuration_head_v1::ZcosmicOutputConfigurationHeadV1: smithay::reexports::wayland_server::Weak + ] => $crate::wayland::protocols::output_configuration::OutputConfigurationState); + }; +} +pub(crate) use delegate_output_configuration; + +use crate::utils::prelude::OutputExt; diff --git a/src/wayland/protocols/output_power.rs b/src/wayland/protocols/output_power.rs new file mode 100644 index 0000000..96600b8 --- /dev/null +++ b/src/wayland/protocols/output_power.rs @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use smithay::{ + output::{Output, WeakOutput}, + reexports::{ + wayland_protocols_wlr::output_power_management::v1::server::{ + zwlr_output_power_manager_v1::{self, ZwlrOutputPowerManagerV1}, + zwlr_output_power_v1::{self, ZwlrOutputPowerV1}, + }, + wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, + backend::GlobalId, + }, + }, +}; +use std::{collections::HashMap, mem}; +use wayland_backend::{protocol::WEnum, server::ClientId}; + +pub trait OutputPowerHandler { + fn output_power_state(&mut self) -> &mut OutputPowerState; + fn get_dpms(&mut self, output: &Output) -> Option; + fn set_dpms(&mut self, output: &Output, on: bool); +} + +#[derive(Debug)] +pub struct OutputPowerState { + global: GlobalId, + output_powers: HashMap, +} + +impl OutputPowerState { + pub fn new(dh: &DisplayHandle, client_filter: F) -> OutputPowerState + where + D: GlobalDispatch + 'static, + F: for<'a> Fn(&'a Client) -> bool + Clone + Send + Sync + 'static, + { + let global = dh.create_global::( + 1, + OutputPowerManagerGlobalData { + filter: Box::new(client_filter.clone()), + }, + ); + + OutputPowerState { + global, + output_powers: HashMap::new(), + } + } + + pub fn global_id(&self) -> GlobalId { + self.global.clone() + } + + /// Send `mode` events for any output powers where dpms state has changed. + /// + /// This is handled automatically for changes made through the protocol. + pub fn refresh(state: &mut D) { + let mut output_powers = mem::take(&mut state.output_power_state().output_powers); + for (output_power, old_mode) in output_powers.iter_mut() { + let data = output_power.data::().unwrap(); + if let Some(output) = data.output.upgrade() + && let Some(on) = state.get_dpms(&output) + { + let mode = output_power_mode(on); + if mode != *old_mode { + output_power.mode(mode); + *old_mode = mode; + } + } + } + state.output_power_state().output_powers = output_powers; + } +} + +pub struct OutputPowerManagerGlobalData { + filter: Box Fn(&'a Client) -> bool + Send + Sync>, +} + +pub struct OutputPowerData { + output: WeakOutput, +} + +impl GlobalDispatch + for OutputPowerState +where + D: GlobalDispatch + + Dispatch + + 'static, +{ + fn bind( + _state: &mut D, + _dh: &DisplayHandle, + _client: &Client, + resource: New, + _global_data: &OutputPowerManagerGlobalData, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, ()); + } + + fn can_view(client: Client, global_data: &OutputPowerManagerGlobalData) -> bool { + (global_data.filter)(&client) + } +} + +impl Dispatch for OutputPowerState +where + D: GlobalDispatch + + Dispatch + + Dispatch + + OutputPowerHandler + + 'static, +{ + fn request( + state: &mut D, + _client: &Client, + _obj: &ZwlrOutputPowerManagerV1, + request: zwlr_output_power_manager_v1::Request, + _data: &(), + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + zwlr_output_power_manager_v1::Request::GetOutputPower { id, output } => { + let output = Output::from_resource(&output); + let output_power = data_init.init( + id, + OutputPowerData { + output: output.as_ref().map(|o| o.downgrade()).unwrap_or_default(), + }, + ); + if let Some(on) = output.as_ref().and_then(|o| state.get_dpms(o)) { + let mode = output_power_mode(on); + output_power.mode(mode); + state + .output_power_state() + .output_powers + .insert(output_power, mode); + } else { + output_power.failed(); + } + } + zwlr_output_power_manager_v1::Request::Destroy => {} + _ => unreachable!(), + } + } +} + +impl Dispatch for OutputPowerState +where + D: Dispatch + OutputPowerHandler + 'static, +{ + fn request( + state: &mut D, + _client: &Client, + obj: &ZwlrOutputPowerV1, + request: zwlr_output_power_v1::Request, + data: &OutputPowerData, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + zwlr_output_power_v1::Request::SetMode { mode } => { + if let Some(output) = data.output.upgrade() { + let on = match mode { + WEnum::Value(zwlr_output_power_v1::Mode::On) => true, + WEnum::Value(zwlr_output_power_v1::Mode::Off) => false, + _ => { + return; + } + }; + state.set_dpms(&output, on); + if let Some(on) = state.get_dpms(&output) { + let mode = output_power_mode(on); + for (output_power, old_mode) in + state.output_power_state().output_powers.iter_mut() + { + let data = output_power.data::().unwrap(); + if data.output == output && mode != *old_mode { + output_power.mode(mode); + *old_mode = mode; + } + } + } else { + obj.failed(); + state.output_power_state().output_powers.remove(obj); + } + } else { + obj.failed(); + state.output_power_state().output_powers.remove(obj); + } + } + zwlr_output_power_v1::Request::Destroy => {} + _ => unreachable!(), + } + } + + fn destroyed( + state: &mut D, + _client: ClientId, + obj: &ZwlrOutputPowerV1, + _data: &OutputPowerData, + ) { + state.output_power_state().output_powers.remove(obj); + } +} + +fn output_power_mode(on: bool) -> zwlr_output_power_v1::Mode { + if on { + zwlr_output_power_v1::Mode::On + } else { + zwlr_output_power_v1::Mode::Off + } +} + +macro_rules! delegate_output_power { + ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { + smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::output_power_management::v1::server::zwlr_output_power_manager_v1::ZwlrOutputPowerManagerV1: $crate::wayland::protocols::output_power::OutputPowerManagerGlobalData + ] => $crate::wayland::protocols::output_power::OutputPowerState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::output_power_management::v1::server::zwlr_output_power_manager_v1::ZwlrOutputPowerManagerV1: () + ] => $crate::wayland::protocols::output_power::OutputPowerState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::output_power_management::v1::server::zwlr_output_power_v1::ZwlrOutputPowerV1: $crate::wayland::protocols::output_power::OutputPowerData + ] => $crate::wayland::protocols::output_power::OutputPowerState); + }; +} +pub(crate) use delegate_output_power; diff --git a/src/wayland/protocols/overlap_notify.rs b/src/wayland/protocols/overlap_notify.rs new file mode 100644 index 0000000..6bdbd47 --- /dev/null +++ b/src/wayland/protocols/overlap_notify.rs @@ -0,0 +1,483 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use std::{collections::HashMap, sync::Mutex}; + +use cosmic_protocols::{ + overlap_notify::v1::server::{ + zcosmic_overlap_notification_v1::ZcosmicOverlapNotificationV1, + zcosmic_overlap_notify_v1::{self, ZcosmicOverlapNotifyV1}, + }, + toplevel_info::v1::server::{ + zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1, + zcosmic_toplevel_info_v1::ZcosmicToplevelInfoV1, + }, +}; +use smithay::{ + desktop::{LayerSurface, layer_map_for_output}, + output::Output, + reexports::{ + wayland_protocols::ext::foreign_toplevel_list::v1::server::{ + ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1, + ext_foreign_toplevel_list_v1::ExtForeignToplevelListV1, + }, + wayland_protocols_wlr::layer_shell::v1::server::{ + zwlr_layer_shell_v1::Layer as WlrLayer, zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, + }, + wayland_server::{Client, Dispatch, DisplayHandle, GlobalDispatch, Resource, Weak}, + }, + utils::{Logical, Rectangle}, + wayland::{ + foreign_toplevel_list::{ + ForeignToplevelHandle, ForeignToplevelListGlobalData, ForeignToplevelListHandler, + }, + shell::wlr_layer::{ExclusiveZone, Layer}, + }, +}; +use wayland_backend::server::{GlobalId, ObjectId}; + +use crate::utils::prelude::{RectExt, RectGlobalExt, RectLocalExt}; + +use super::{ + toplevel_info::{ + ToplevelHandleState, ToplevelInfoGlobalData, ToplevelInfoHandler, ToplevelState, Window, + }, + workspace::WorkspaceHandle, +}; + +#[derive(Debug)] +pub struct OverlapNotifyState { + instances: Vec, + global: GlobalId, +} + +impl OverlapNotifyState { + pub fn new(dh: &DisplayHandle, client_filter: F) -> OverlapNotifyState + where + D: GlobalDispatch + + Dispatch + + Dispatch + + OverlapNotifyHandler + + 'static, + F: for<'a> Fn(&'a Client) -> bool + Send + Sync + 'static, + { + let global = dh.create_global::( + 1, + OverlapNotifyGlobalData { + filter: Box::new(client_filter), + }, + ); + OverlapNotifyState { + instances: Vec::new(), + global, + } + } + + pub fn global_id(&self) -> GlobalId { + self.global.clone() + } + + #[profiling::function] + pub fn refresh(state: &mut D) + where + D: GlobalDispatch + + GlobalDispatch + + Dispatch + + Dispatch + + Dispatch + + OverlapNotifyHandler + + GlobalDispatch + + Dispatch + + Dispatch> + + ForeignToplevelListHandler + + ToplevelInfoHandler + + 'static, + W: Window + 'static, + { + let active_workspaces: Vec<_> = state.active_workspaces().collect(); + for output in state.outputs() { + let map = layer_map_for_output(&output); + + for layer_surface in map.layers() { + if let Some(data) = layer_surface + .user_data() + .get::() + { + let mut inner = data.lock().unwrap(); + + if inner.has_active_notifications() { + let mut new_snapshot = OverlapSnapshot::default(); + + let layer_geo = map + .layer_geometry(layer_surface) + .unwrap_or_default() + .as_local() + .to_global(&output); + + for window in + state + .toplevel_info_state() + .registered_toplevels() + .filter(|w| { + let state = w + .user_data() + .get::() + .unwrap() + .lock() + .unwrap(); + + w.is_sticky() + || active_workspaces.iter().any(|active_workspace| { + state.in_workspace(active_workspace) + }) + }) + { + if let Some(window_geo) = window.global_geometry() + && let Some(intersection) = layer_geo.intersection(window_geo) + { + // relative to layer location + let region = Rectangle::new( + intersection.loc - layer_geo.loc, + intersection.size, + ) + .as_logical(); + new_snapshot.add_toplevel(window, region); + } + } + + for other_surface in map.layers().filter(|s| *s != layer_surface) { + if other_surface.wl_surface().id() == layer_surface.wl_surface().id() { + continue; + } + let other_geo = map + .layer_geometry(other_surface) + .unwrap_or_default() + .as_local() + .to_global(&output); + if let Some(intersection) = layer_geo.intersection(other_geo) { + // relative to layer location + let region = Rectangle::new( + intersection.loc - layer_geo.loc, + intersection.size, + ) + .as_logical(); + new_snapshot.add_layer(other_surface, region); + } + } + + inner.update_snapshot(new_snapshot); + } + } + } + } + } +} + +pub trait OverlapNotifyHandler: ToplevelInfoHandler { + fn overlap_notify_state(&mut self) -> &mut OverlapNotifyState; + fn layer_surface_from_resource(&self, resource: ZwlrLayerSurfaceV1) -> Option; + fn outputs(&self) -> impl Iterator; + fn active_workspaces(&self) -> impl Iterator; +} + +pub struct OverlapNotifyGlobalData { + filter: Box Fn(&'a Client) -> bool + Send + Sync>, +} + +type LayerOverlapNotificationData = Mutex; + +#[derive(Debug, Default)] +struct LayerOverlapNotificationDataInternal { + active_notifications: Vec>, + last_snapshot: OverlapSnapshot, +} + +impl LayerOverlapNotificationDataInternal { + pub fn has_active_notifications(&mut self) -> bool { + self.active_notifications.retain(|w| w.upgrade().is_ok()); + !self.active_notifications.is_empty() + } + + pub fn add_notification(&mut self, new_notification: ZcosmicOverlapNotificationV1) { + if let Some(client) = new_notification.client() { + for (toplevel, overlap) in &self.last_snapshot.toplevel_overlaps { + if let Some(toplevel) = toplevel + .upgrade() + .ok() + .filter(|handle| handle.client().is_some_and(|c| c == client)) + { + new_notification.toplevel_enter( + &toplevel, + overlap.loc.x, + overlap.loc.y, + overlap.size.w, + overlap.size.h, + ); + } + } + } + for (identifier, namespace, exclusive, layer, overlap) in + self.last_snapshot.layer_overlaps.values() + { + new_notification.layer_enter( + identifier.clone(), + namespace.clone(), + if *exclusive { 1 } else { 0 }, + match layer { + Layer::Background => WlrLayer::Background, + Layer::Bottom => WlrLayer::Bottom, + Layer::Top => WlrLayer::Top, + Layer::Overlay => WlrLayer::Overlay, + }, + overlap.loc.x, + overlap.loc.y, + overlap.size.w, + overlap.size.h, + ); + } + self.active_notifications.push(new_notification.downgrade()); + } + + pub fn update_snapshot(&mut self, new_snapshot: OverlapSnapshot) { + let notifications = self + .active_notifications + .iter() + .flat_map(|w| w.upgrade().ok()) + .collect::>(); + + for toplevel in self.last_snapshot.toplevel_overlaps.keys() { + if !new_snapshot.toplevel_overlaps.contains_key(toplevel) + && let Ok(toplevel) = toplevel.upgrade() + && let Some(client) = toplevel.client() + { + for notification in notifications + .iter() + .filter(|n| n.client().is_some_and(|c| c == client)) + { + notification.toplevel_leave(&toplevel); + } + } + } + for (toplevel, overlap) in &new_snapshot.toplevel_overlaps { + if !self + .last_snapshot + .toplevel_overlaps + .get(toplevel) + .is_some_and(|old_overlap| old_overlap == overlap) + && let Ok(toplevel) = toplevel.upgrade() + && let Some(client) = toplevel.client() + { + for notification in notifications + .iter() + .filter(|n| n.client().is_some_and(|c| c == client)) + { + notification.toplevel_enter( + &toplevel, + overlap.loc.x, + overlap.loc.y, + overlap.size.w, + overlap.size.h, + ); + } + } + } + + for (layer_surface, (identifier, ..)) in &self.last_snapshot.layer_overlaps { + if !new_snapshot.layer_overlaps.contains_key(layer_surface) { + for notification in ¬ifications { + notification.layer_leave(identifier.clone()); + } + } + } + for (layer_surface, (identifier, namespace, exclusive, layer, overlap)) in + &new_snapshot.layer_overlaps + { + if !self + .last_snapshot + .layer_overlaps + .get(layer_surface) + .is_some_and(|(_, _, _, _, old_overlap)| old_overlap == overlap) + { + for notification in ¬ifications { + notification.layer_enter( + identifier.clone(), + namespace.clone(), + if *exclusive { 1 } else { 0 }, + match layer { + Layer::Background => WlrLayer::Background, + Layer::Bottom => WlrLayer::Bottom, + Layer::Top => WlrLayer::Top, + Layer::Overlay => WlrLayer::Overlay, + }, + overlap.loc.x, + overlap.loc.y, + overlap.size.w, + overlap.size.h, + ); + } + } + } + + self.last_snapshot = new_snapshot; + } +} + +#[derive(Debug, Default, Clone)] +struct OverlapSnapshot { + toplevel_overlaps: HashMap, Rectangle>, + layer_overlaps: HashMap)>, +} + +impl OverlapSnapshot { + pub fn add_toplevel(&mut self, window: &impl Window, overlap: Rectangle) { + if let Some(handles) = window + .user_data() + .get::() + .unwrap() + .lock() + .unwrap() + .foreign_handle() + .map(|handle| handle.resources()) + { + for handle in handles.into_iter().map(|h| h.downgrade()) { + self.toplevel_overlaps.insert(handle, overlap); + } + } + } + + pub fn add_layer(&mut self, layer_surface: &LayerSurface, overlap: Rectangle) { + let exclusive = matches!( + layer_surface.cached_state().exclusive_zone, + ExclusiveZone::Exclusive(_) + ); + let layer = layer_surface.layer(); + let id = layer_surface.wl_surface().id(); + let identifier = layer_surface + .user_data() + .get_or_insert(|| Identifier::from(id.clone())); + + self.layer_overlaps.insert( + id, + ( + identifier.0.clone(), + layer_surface.namespace().to_string(), + exclusive, + layer, + overlap, + ), + ); + } +} + +struct Identifier(String); + +impl From for Identifier { + fn from(value: ObjectId) -> Self { + Identifier(value.to_string()) + } +} + +impl GlobalDispatch for OverlapNotifyState +where + D: GlobalDispatch + + Dispatch + + Dispatch + + OverlapNotifyHandler + + 'static, +{ + fn bind( + state: &mut D, + _handle: &DisplayHandle, + _client: &Client, + resource: smithay::reexports::wayland_server::New, + _global_data: &OverlapNotifyGlobalData, + data_init: &mut smithay::reexports::wayland_server::DataInit<'_, D>, + ) { + let instance = data_init.init(resource, ()); + state.overlap_notify_state().instances.push(instance); + } + + fn can_view(client: Client, global_data: &OverlapNotifyGlobalData) -> bool { + (global_data.filter)(&client) + } +} + +impl Dispatch for OverlapNotifyState +where + D: GlobalDispatch + + Dispatch + + Dispatch + + OverlapNotifyHandler + + 'static, +{ + fn request( + state: &mut D, + _client: &Client, + _resource: &ZcosmicOverlapNotifyV1, + request: ::Request, + _data: &(), + _dhandle: &DisplayHandle, + data_init: &mut smithay::reexports::wayland_server::DataInit<'_, D>, + ) { + if let zcosmic_overlap_notify_v1::Request::NotifyOnOverlap { + overlap_notification, + layer_surface, + } = request + { + let notification = data_init.init(overlap_notification, ()); + if let Some(surface) = state.layer_surface_from_resource(layer_surface) { + let mut data = surface + .user_data() + .get_or_insert_threadsafe(LayerOverlapNotificationData::default) + .lock() + .unwrap(); + data.add_notification(notification); + } + } + } + + fn destroyed( + state: &mut D, + _client: wayland_backend::server::ClientId, + resource: &ZcosmicOverlapNotifyV1, + _data: &(), + ) { + let overlap_state = state.overlap_notify_state(); + overlap_state.instances.retain(|i| i != resource); + } +} + +impl Dispatch for OverlapNotifyState +where + D: GlobalDispatch + + Dispatch + + Dispatch + + OverlapNotifyHandler + + 'static, +{ + fn request( + _state: &mut D, + _client: &Client, + _resource: &ZcosmicOverlapNotificationV1, + _request: ::Request, + _data: &(), + _dhandle: &DisplayHandle, + _data_init: &mut smithay::reexports::wayland_server::DataInit<'_, D>, + ) { + {} + } +} + +macro_rules! delegate_overlap_notify { + ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { + smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + cosmic_protocols::overlap_notify::v1::server::zcosmic_overlap_notify_v1::ZcosmicOverlapNotifyV1: $crate::wayland::protocols::overlap_notify::OverlapNotifyGlobalData + ] => $crate::wayland::protocols::overlap_notify::OverlapNotifyState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + cosmic_protocols::overlap_notify::v1::server::zcosmic_overlap_notify_v1::ZcosmicOverlapNotifyV1: () + ] => $crate::wayland::protocols::overlap_notify::OverlapNotifyState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + cosmic_protocols::overlap_notify::v1::server::zcosmic_overlap_notification_v1::ZcosmicOverlapNotificationV1: () + ] => $crate::wayland::protocols::overlap_notify::OverlapNotifyState); + }; +} +pub(crate) use delegate_overlap_notify; diff --git a/src/wayland/protocols/toplevel_info.rs b/src/wayland/protocols/toplevel_info.rs new file mode 100644 index 0000000..2dc7634 --- /dev/null +++ b/src/wayland/protocols/toplevel_info.rs @@ -0,0 +1,718 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use std::{collections::HashSet, sync::Mutex}; + +use smithay::{ + output::Output, + reexports::{ + wayland_protocols::ext::foreign_toplevel_list::v1::server::{ + ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1, + ext_foreign_toplevel_list_v1::ExtForeignToplevelListV1, + }, + wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, Weak, + backend::{ClientId, GlobalId}, + protocol::{wl_output::WlOutput, wl_surface::WlSurface}, + }, + }, + utils::{IsAlive, Logical, Rectangle, user_data::UserDataMap}, + wayland::foreign_toplevel_list::{ + ForeignToplevelHandle, ForeignToplevelListGlobalData, ForeignToplevelListHandler, + ForeignToplevelListState, + }, +}; + +use crate::utils::prelude::{Global, OutputExt, RectGlobalExt}; + +use super::workspace::{WorkspaceHandle, WorkspaceHandler, WorkspaceState}; + +use cosmic_protocols::toplevel_info::v1::server::{ + zcosmic_toplevel_handle_v1::{self, State as States, ZcosmicToplevelHandleV1}, + zcosmic_toplevel_info_v1::{self, ZcosmicToplevelInfoV1}, +}; +use tracing::error; + +pub trait Window: IsAlive + Clone + PartialEq + Send { + fn title(&self) -> String; + fn app_id(&self) -> String; + fn is_activated(&self) -> bool; + fn is_maximized(&self) -> bool; + fn is_fullscreen(&self) -> bool; + fn is_minimized(&self) -> bool; + fn is_sticky(&self) -> bool; + fn is_resizing(&self) -> bool; + fn global_geometry(&self) -> Option>; + fn user_data(&self) -> &UserDataMap; +} + +#[derive(Debug)] +pub struct ToplevelInfoState { + dh: DisplayHandle, + pub(super) toplevels: Vec, + instances: Vec, + dirty: bool, + last_dirty: bool, + pub(in crate::wayland) foreign_toplevel_list: ForeignToplevelListState, + global: GlobalId, + _dispatch_data: std::marker::PhantomData, +} + +pub trait ToplevelInfoHandler: ForeignToplevelListHandler + WorkspaceHandler + Sized { + type Window: Window; + fn toplevel_info_state(&self) -> &ToplevelInfoState; + fn toplevel_info_state_mut(&mut self) -> &mut ToplevelInfoState; +} + +pub struct ToplevelInfoGlobalData { + filter: Box Fn(&'a Client) -> bool + Send + Sync>, +} + +#[derive(Default)] +pub(super) struct ToplevelStateInner { + foreign_handle: Option, + instances: Vec<(Weak, ZcosmicToplevelHandleV1)>, + outputs: Vec, + workspaces: Vec, + pub(super) rectangles: Vec<(Weak, Rectangle)>, +} +pub(super) type ToplevelState = Mutex; + +impl ToplevelStateInner { + fn from_foreign(foreign_handle: ForeignToplevelHandle) -> Mutex { + Mutex::new(ToplevelStateInner { + foreign_handle: Some(foreign_handle), + ..Default::default() + }) + } + + pub fn foreign_handle(&self) -> Option<&ForeignToplevelHandle> { + self.foreign_handle.as_ref() + } + + pub fn in_workspace(&self, handle: &WorkspaceHandle) -> bool { + self.workspaces.contains(handle) + } +} + +pub struct ToplevelHandleStateInner { + outputs: Vec, + geometry: Option>, + wl_outputs: HashSet, + workspaces: Vec, + title: String, + app_id: String, + states: Option>, + pub(super) window: Option, +} +pub type ToplevelHandleState = Mutex>; + +impl ToplevelHandleStateInner { + fn from_window(window: &W) -> ToplevelHandleState { + ToplevelHandleState::new(ToplevelHandleStateInner { + outputs: Vec::new(), + geometry: None, + wl_outputs: HashSet::new(), + workspaces: Vec::new(), + title: String::new(), + app_id: String::new(), + states: None, + window: Some(window.clone()), + }) + } + + fn empty() -> ToplevelHandleState { + ToplevelHandleState::new(ToplevelHandleStateInner { + outputs: Vec::new(), + geometry: None, + wl_outputs: HashSet::new(), + workspaces: Vec::new(), + title: String::new(), + app_id: String::new(), + states: None, + window: None, + }) + } +} + +impl GlobalDispatch + for ToplevelInfoState +where + D: GlobalDispatch + + Dispatch + + Dispatch> + + ToplevelInfoHandler + + 'static, + W: Window + 'static, +{ + fn bind( + state: &mut D, + dh: &DisplayHandle, + _client: &Client, + resource: New, + _global_data: &ToplevelInfoGlobalData, + data_init: &mut DataInit<'_, D>, + ) { + let instance = data_init.init(resource, ()); + for window in &state.toplevel_info_state().toplevels { + send_toplevel_to_client::(dh, state.workspace_state(), &instance, window); + } + state.toplevel_info_state_mut().instances.push(instance); + } + + fn can_view(client: Client, global_data: &ToplevelInfoGlobalData) -> bool { + (global_data.filter)(&client) + } +} + +impl Dispatch for ToplevelInfoState +where + D: GlobalDispatch + + Dispatch + + Dispatch> + + ToplevelInfoHandler + + 'static, + W: Window + 'static, +{ + fn request( + state: &mut D, + _client: &Client, + obj: &ZcosmicToplevelInfoV1, + request: zcosmic_toplevel_info_v1::Request, + _data: &(), + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + zcosmic_toplevel_info_v1::Request::GetCosmicToplevel { + cosmic_toplevel, + foreign_toplevel, + } => { + let toplevel_state = state.toplevel_info_state(); + if let Some(window) = toplevel_state.toplevels.iter().find(|w| { + w.user_data().get::().and_then(|inner| { + inner + .lock() + .unwrap() + .foreign_handle + .as_ref() + .map(|handle| handle.identifier()) + }) == ForeignToplevelHandle::from_resource(&foreign_toplevel) + .map(|handle| handle.identifier()) + }) { + let instance = data_init.init( + cosmic_toplevel, + ToplevelHandleStateInner::from_window(window), + ); + window + .user_data() + .get::() + .unwrap() + .lock() + .unwrap() + .instances + .push((obj.downgrade(), instance)); + } else { + let _ = data_init.init(cosmic_toplevel, ToplevelHandleStateInner::empty()); + error!( + ?foreign_toplevel, + "Toplevel for foreign-toplevel-list not registered for cosmic-toplevel-info." + ); + } + } + zcosmic_toplevel_info_v1::Request::Stop => { + state + .toplevel_info_state_mut() + .instances + .retain(|i| i != obj); + obj.finished(); + } + _ => {} + } + } + + fn destroyed(state: &mut D, _client: ClientId, resource: &ZcosmicToplevelInfoV1, _data: &()) { + state + .toplevel_info_state_mut() + .instances + .retain(|i| i != resource); + } +} + +impl Dispatch, D> for ToplevelInfoState +where + D: GlobalDispatch + + Dispatch + + Dispatch> + + ToplevelInfoHandler + + 'static, + W: Window, +{ + fn request( + _state: &mut D, + _client: &Client, + _obj: &ZcosmicToplevelHandleV1, + request: zcosmic_toplevel_handle_v1::Request, + _data: &ToplevelHandleState, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + if let zcosmic_toplevel_handle_v1::Request::Destroy = request {} + } + + fn destroyed( + state: &mut D, + _client: ClientId, + resource: &ZcosmicToplevelHandleV1, + _data: &ToplevelHandleState, + ) { + for toplevel in &state.toplevel_info_state_mut().toplevels { + if let Some(state) = toplevel.user_data().get::() { + state + .lock() + .unwrap() + .instances + .retain(|(_, i)| i != resource); + } + } + } +} + +pub fn toplevel_enter_output(toplevel: &impl Window, output: &Output) { + if let Some(state) = toplevel.user_data().get::() { + state.lock().unwrap().outputs.push(output.clone()); + } +} + +pub fn toplevel_leave_output(toplevel: &impl Window, output: &Output) { + if let Some(state) = toplevel.user_data().get::() { + state.lock().unwrap().outputs.retain(|o| o != output); + } +} + +pub fn toplevel_enter_workspace(toplevel: &impl Window, workspace: &WorkspaceHandle) { + if let Some(state) = toplevel.user_data().get::() { + state.lock().unwrap().workspaces.push(*workspace); + } +} + +pub fn toplevel_leave_workspace(toplevel: &impl Window, workspace: &WorkspaceHandle) { + if let Some(state) = toplevel.user_data().get::() { + state.lock().unwrap().workspaces.retain(|w| w != workspace); + } +} + +impl ToplevelInfoState +where + D: GlobalDispatch + + GlobalDispatch + + Dispatch + + Dispatch + + Dispatch> + + ForeignToplevelListHandler + + ToplevelInfoHandler + + 'static, + W: Window + 'static, +{ + pub fn new(dh: &DisplayHandle, client_filter: F) -> ToplevelInfoState + where + F: for<'a> Fn(&'a Client) -> bool + Send + Sync + Clone + 'static, + { + let global = dh.create_global::( + 3, + ToplevelInfoGlobalData { + filter: Box::new(client_filter.clone()), + }, + ); + let foreign_toplevel_list = + ForeignToplevelListState::new_with_filter::(dh, client_filter); + ToplevelInfoState { + dh: dh.clone(), + toplevels: Vec::new(), + instances: Vec::new(), + dirty: false, + last_dirty: false, + foreign_toplevel_list, + global, + _dispatch_data: std::marker::PhantomData, + } + } + + pub fn new_toplevel(&mut self, toplevel: &W, workspace_state: &WorkspaceState) { + let toplevel_handle = self + .foreign_toplevel_list + .new_toplevel::(toplevel.title(), toplevel.app_id()); + + if let Some(toplevel_state) = toplevel.user_data().get::() { + let mut toplevel_state = toplevel_state.lock().unwrap(); + toplevel_state.foreign_handle = Some(toplevel_handle); + } else { + toplevel + .user_data() + .insert_if_missing(move || ToplevelStateInner::from_foreign(toplevel_handle)); + } + + for instance in &self.instances { + send_toplevel_to_client::(&self.dh, workspace_state, instance, toplevel); + } + self.toplevels.push(toplevel.clone()); + self.dirty = true; + } + + pub fn remove_toplevel(&mut self, toplevel: &W) { + if let Some(state) = toplevel.user_data().get::() { + let mut state_inner = state.lock().unwrap(); + for (_info, handle) in &state_inner.instances { + // don't send events to stopped instances + if handle.version() < zcosmic_toplevel_info_v1::REQ_GET_COSMIC_TOPLEVEL_SINCE + && self + .instances + .iter() + .any(|i| i.id().same_client_as(&handle.id())) + { + handle.closed(); + } + } + if let Some(handle) = state_inner.foreign_handle.take() { + self.foreign_toplevel_list.remove_toplevel(&handle); + } + *state_inner = Default::default(); + self.dirty = true; + } + self.toplevels.retain(|w| w != toplevel); + } + + pub fn refresh(&mut self, workspace_state: &WorkspaceState) { + let mut dirty = std::mem::replace(&mut self.dirty, false); + + self.toplevels.retain(|window| { + let mut state = window + .user_data() + .get::() + .unwrap() + .lock() + .unwrap(); + state + .rectangles + .retain(|(surface, _)| surface.upgrade().is_ok()); + if window.alive() { + std::mem::drop(state); + for instance in &self.instances { + let changed = send_toplevel_to_client::( + &self.dh, + workspace_state, + instance, + window, + ); + dirty = dirty || changed; + } + true + } else { + for (_info, handle) in &state.instances { + // don't send events to stopped instances + if handle.version() < zcosmic_toplevel_info_v1::REQ_GET_COSMIC_TOPLEVEL_SINCE + && self + .instances + .iter() + .any(|i| i.id().same_client_as(&handle.id())) + { + handle.closed(); + } + } + dirty = true; + false + } + }); + + if !dirty && self.last_dirty { + for instance in &self.instances { + if instance.version() >= zcosmic_toplevel_info_v1::EVT_DONE_SINCE { + instance.done(); + } + } + } + + self.last_dirty = dirty; + } + + pub fn global_id(&self) -> GlobalId { + self.global.clone() + } + + pub fn registered_toplevels(&self) -> impl Iterator { + self.toplevels.iter() + } +} + +fn send_toplevel_to_client( + dh: &DisplayHandle, + workspace_state: &WorkspaceState, + info: &ZcosmicToplevelInfoV1, + window: &W, +) -> bool +where + D: GlobalDispatch + + Dispatch + + Dispatch> + + ToplevelInfoHandler + + 'static, + W: Window + 'static, +{ + let mut state = window + .user_data() + .get::() + .unwrap() + .lock() + .unwrap(); + let (_info, instance) = match state.instances.iter().find(|(i, _)| i == info) { + Some(i) => i, + None => { + if info.version() < zcosmic_toplevel_info_v1::REQ_GET_COSMIC_TOPLEVEL_SINCE { + if let Ok(client) = dh.get_client(info.id()) { + if let Ok(toplevel_handle) = client + .create_resource::( + dh, + info.version(), + ToplevelHandleStateInner::from_window(window), + ) + { + info.toplevel(&toplevel_handle); + state.instances.push((info.downgrade(), toplevel_handle)); + state.instances.last().unwrap() + } else { + return false; + } + } else { + return false; + } + } else { + return false; + } + } + }; + + let mut handle_state = instance + .data::>() + .unwrap() + .lock() + .unwrap(); + let mut changed = false; + + let new_title = (handle_state.title != window.title()).then(|| window.title()); + let new_app_id = (handle_state.app_id != window.app_id()).then(|| window.app_id()); + + let new_states = if handle_state.states.as_ref().is_none_or(|states| { + (states.contains(&States::Maximized) != window.is_maximized()) + || (states.contains(&States::Fullscreen) != window.is_fullscreen()) + || (states.contains(&States::Activated) != window.is_activated()) + || (states.contains(&States::Minimized) != window.is_minimized()) + || (instance.version() >= zcosmic_toplevel_info_v1::REQ_GET_COSMIC_TOPLEVEL_SINCE + && states.contains(&States::Sticky) != window.is_sticky()) + }) { + let mut states = Vec::new(); + if window.is_maximized() { + states.push(States::Maximized); + } + if window.is_fullscreen() { + states.push(States::Fullscreen); + } + if window.is_activated() { + states.push(States::Activated); + } + if window.is_minimized() { + states.push(States::Minimized); + } + if instance.version() >= zcosmic_toplevel_info_v1::REQ_GET_COSMIC_TOPLEVEL_SINCE + && window.is_sticky() + { + states.push(States::Sticky); + } + Some(states) + } else { + None + }; + + let geometry_changed = if !window.is_resizing() { + let geometry = window.global_geometry(); + if handle_state.geometry != geometry { + handle_state.geometry = geometry; + true + } else { + false + } + } else { + false + }; + + let outputs_changed = state.outputs != handle_state.outputs + || handle_state.wl_outputs.iter().any(|o| !o.is_alive()); + + let workspaces_changed = state.workspaces != handle_state.workspaces; + + if new_title.is_none() + && new_app_id.is_none() + && new_states.is_none() + && !geometry_changed + && !outputs_changed + && !workspaces_changed + { + return false; + } + + let foreign_toplevel_handle = state.foreign_handle.as_ref(); + + if let Some(title) = new_title { + handle_state.title = title; + if instance.version() < zcosmic_toplevel_info_v1::REQ_GET_COSMIC_TOPLEVEL_SINCE { + instance.title(handle_state.title.clone()); + } + if let Some(handle) = foreign_toplevel_handle { + handle.send_title(&handle_state.title); + } + changed = true; + } + + if let Some(app_id) = new_app_id { + handle_state.app_id = app_id; + if instance.version() < zcosmic_toplevel_info_v1::REQ_GET_COSMIC_TOPLEVEL_SINCE { + instance.app_id(handle_state.app_id.clone()); + } + if let Some(handle) = foreign_toplevel_handle { + handle.send_app_id(&handle_state.app_id); + } + changed = true; + } + + if let Some(states) = new_states { + handle_state.states = Some(states.clone()); + let states = states + .iter() + .flat_map(|state| (*state as u32).to_ne_bytes()) + .collect::>(); + instance.state(states); + changed = true; + } + + if (outputs_changed || geometry_changed) + && let Ok(client) = dh.get_client(instance.id()) + { + handle_state.outputs = state.outputs.clone(); + + let handle_state = &mut *handle_state; + for output in &handle_state.outputs { + let geometry = handle_state + .geometry + .filter(|_| instance.version() >= zcosmic_toplevel_handle_v1::EVT_GEOMETRY_SINCE) + .filter(|geo| output.geometry().intersection(*geo).is_some()) + .map(|geo| geo.to_local(output)); + for wl_output in output.client_outputs(&client) { + if handle_state.wl_outputs.insert(wl_output.clone()) { + instance.output_enter(&wl_output); + if let Some(geo) = geometry { + instance.geometry(&wl_output, geo.loc.x, geo.loc.y, geo.size.w, geo.size.h); + } + changed = true; + } else if geometry_changed && let Some(geo) = geometry { + instance.geometry(&wl_output, geo.loc.x, geo.loc.y, geo.size.w, geo.size.h); + } + } + } + handle_state.wl_outputs.retain(|wl_output| { + let retain = wl_output.is_alive() + && handle_state + .outputs + .iter() + .any(|output| output.owns(wl_output)); + if !retain { + instance.output_leave(wl_output); + changed = true; + } + retain + }); + } + + if workspaces_changed { + for new_workspace in state + .workspaces + .iter() + .filter(|w| !handle_state.workspaces.contains(w)) + { + for handle in workspace_state.raw_ext_workspace_handles(new_workspace, &instance.id()) { + instance.ext_workspace_enter(handle); + changed = true; + } + } + for old_workspace in handle_state + .workspaces + .iter() + .filter(|w| !state.workspaces.contains(w)) + { + for handle in workspace_state.raw_ext_workspace_handles(old_workspace, &instance.id()) { + instance.ext_workspace_leave(handle); + changed = true; + } + } + handle_state.workspaces = state.workspaces.clone(); + } + + if changed { + if instance.version() < zcosmic_toplevel_info_v1::REQ_GET_COSMIC_TOPLEVEL_SINCE { + instance.done(); + } + if let Some(handle) = foreign_toplevel_handle { + handle.send_done(); + } + } + + changed +} + +pub fn window_from_handle(handle: ZcosmicToplevelHandleV1) -> Option { + handle + .data::>() + .and_then(|state| state.lock().unwrap().window.clone()) +} + +pub fn window_from_ext( + state: &D, + handle: ForeignToplevelHandle, +) -> Option<&W> +where + D: ToplevelInfoHandler, +{ + state.toplevel_info_state().toplevels.iter().find(|w| { + w.user_data().get::().and_then(|inner| { + inner + .lock() + .unwrap() + .foreign_handle + .as_ref() + .map(|handle| handle.identifier()) + }) == Some(handle.identifier()) + }) +} + +pub fn window_from_ext_handle<'a, W: Window + 'static, D>( + state: &'a D, + foreign_toplevel: &ExtForeignToplevelHandleV1, +) -> Option<&'a W> +where + D: ToplevelInfoHandler, +{ + let handle = ForeignToplevelHandle::from_resource(foreign_toplevel)?; + window_from_ext(state, handle) +} + +macro_rules! delegate_toplevel_info { + ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty, $window: ty) => { + smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + cosmic_protocols::toplevel_info::v1::server::zcosmic_toplevel_info_v1::ZcosmicToplevelInfoV1: $crate::wayland::protocols::toplevel_info::ToplevelInfoGlobalData + ] => $crate::wayland::protocols::toplevel_info::ToplevelInfoState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + cosmic_protocols::toplevel_info::v1::server::zcosmic_toplevel_info_v1::ZcosmicToplevelInfoV1: () + ] => $crate::wayland::protocols::toplevel_info::ToplevelInfoState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + cosmic_protocols::toplevel_info::v1::server::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1: $crate::wayland::protocols::toplevel_info::ToplevelHandleState<$window> + ] => $crate::wayland::protocols::toplevel_info::ToplevelInfoState); + }; +} +pub(crate) use delegate_toplevel_info; diff --git a/src/wayland/protocols/toplevel_management.rs b/src/wayland/protocols/toplevel_management.rs new file mode 100644 index 0000000..f252fde --- /dev/null +++ b/src/wayland/protocols/toplevel_management.rs @@ -0,0 +1,302 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use smithay::{ + input::{Seat, SeatHandler}, + output::Output, + reexports::wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, + backend::{ClientId, GlobalId}, + protocol::wl_surface::WlSurface, + }, + utils::{Logical, Rectangle}, +}; + +pub use cosmic_protocols::toplevel_management::v1::server::zcosmic_toplevel_manager_v1::ZcosmicToplelevelManagementCapabilitiesV1 as ManagementCapabilities; +use cosmic_protocols::toplevel_management::v1::server::zcosmic_toplevel_manager_v1::{ + self, ZcosmicToplevelManagerV1, +}; + +use super::{ + toplevel_info::{ToplevelInfoHandler, ToplevelState, Window, window_from_handle}, + workspace::WorkspaceHandle, +}; + +#[derive(Debug)] +pub struct ToplevelManagementState { + instances: Vec, + capabilities: Vec, + global: GlobalId, +} + +pub trait ManagementWindow: Window { + fn close(&self); +} + +#[allow(unused_variables)] +pub trait ToplevelManagementHandler: ToplevelInfoHandler + SeatHandler +where + ::Window: ManagementWindow, +{ + fn toplevel_management_state(&mut self) -> &mut ToplevelManagementState; + + fn activate( + &mut self, + dh: &DisplayHandle, + window: &::Window, + seat: Option>, + ) { + } + fn close(&mut self, dh: &DisplayHandle, window: &::Window) {} + + fn fullscreen( + &mut self, + dh: &DisplayHandle, + window: &::Window, + output: Option, + ) { + } + fn unfullscreen(&mut self, dh: &DisplayHandle, window: &::Window) { + } + fn maximize(&mut self, dh: &DisplayHandle, window: &::Window) {} + fn unmaximize(&mut self, dh: &DisplayHandle, window: &::Window) {} + fn minimize(&mut self, dh: &DisplayHandle, window: &::Window) {} + fn unminimize(&mut self, dh: &DisplayHandle, window: &::Window) {} + fn set_sticky(&mut self, dh: &DisplayHandle, window: &::Window) {} + fn unset_sticky(&mut self, dh: &DisplayHandle, window: &::Window) { + } + fn move_to_workspace( + &mut self, + dh: &DisplayHandle, + window: &::Window, + workspace: WorkspaceHandle, + output: Output, + ) { + } +} + +pub fn toplevel_rectangle_for( + window: &impl ManagementWindow, +) -> impl Iterator)> { + if let Some(state) = window.user_data().get::() { + let mut state = state.lock().unwrap(); + state + .rectangles + .retain(|(surface, _)| surface.upgrade().is_ok()); + Some( + state + .rectangles + .iter() + .map(|(surface, rect)| (surface.upgrade().unwrap(), *rect)) + .collect::>() + .into_iter(), + ) + .into_iter() + .flatten() + } else { + None.into_iter().flatten() + } +} + +pub struct ToplevelManagerGlobalData { + filter: Box Fn(&'a Client) -> bool + Send + Sync>, +} + +impl ToplevelManagementState { + pub fn new( + dh: &DisplayHandle, + capabilities: Vec, + client_filter: F, + ) -> ToplevelManagementState + where + D: GlobalDispatch + + Dispatch + + ToplevelManagementHandler + + 'static, + ::Window: ManagementWindow, + F: for<'a> Fn(&'a Client) -> bool + Send + Sync + 'static, + { + let global = dh.create_global::( + 4, + ToplevelManagerGlobalData { + filter: Box::new(client_filter), + }, + ); + ToplevelManagementState { + capabilities, + instances: Vec::new(), + global, + } + } + + pub fn global_id(&self) -> GlobalId { + self.global.clone() + } +} + +impl GlobalDispatch + for ToplevelManagementState +where + D: GlobalDispatch + + Dispatch + + ToplevelManagementHandler + + 'static, + ::Window: ManagementWindow, +{ + fn bind( + state: &mut D, + _dh: &DisplayHandle, + _client: &Client, + resource: New, + _global_data: &ToplevelManagerGlobalData, + data_init: &mut DataInit<'_, D>, + ) { + let instance = data_init.init(resource, ()); + let capabilities = state + .toplevel_management_state() + .capabilities + .iter() + .flat_map(|cap| (*cap as u32).to_ne_bytes()) + .collect::>(); + + instance.capabilities(capabilities); + state.toplevel_management_state().instances.push(instance); + } + + fn can_view(client: Client, global_data: &ToplevelManagerGlobalData) -> bool { + (global_data.filter)(&client) + } +} + +impl Dispatch for ToplevelManagementState +where + D: GlobalDispatch + + Dispatch + + ToplevelManagementHandler + + 'static, + ::Window: ManagementWindow, +{ + fn request( + state: &mut D, + _client: &Client, + _obj: &ZcosmicToplevelManagerV1, + request: zcosmic_toplevel_manager_v1::Request, + _data: &(), + dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + zcosmic_toplevel_manager_v1::Request::Activate { toplevel, seat } => { + let window = window_from_handle(toplevel).unwrap(); + state.activate(dh, &window, Seat::from_resource(&seat)); + } + zcosmic_toplevel_manager_v1::Request::Close { toplevel } => { + let window = window_from_handle(toplevel).unwrap(); + state.close(dh, &window); + } + zcosmic_toplevel_manager_v1::Request::SetFullscreen { toplevel, output } => { + let window = window_from_handle(toplevel).unwrap(); + state.fullscreen(dh, &window, output.as_ref().and_then(Output::from_resource)) + } + zcosmic_toplevel_manager_v1::Request::UnsetFullscreen { toplevel } => { + let window = window_from_handle(toplevel).unwrap(); + state.unfullscreen(dh, &window); + } + zcosmic_toplevel_manager_v1::Request::SetMaximized { toplevel } => { + let window = window_from_handle(toplevel).unwrap(); + state.maximize(dh, &window); + } + zcosmic_toplevel_manager_v1::Request::UnsetMaximized { toplevel } => { + let window = window_from_handle(toplevel).unwrap(); + state.unmaximize(dh, &window); + } + zcosmic_toplevel_manager_v1::Request::SetMinimized { toplevel } => { + let window = window_from_handle(toplevel).unwrap(); + state.minimize(dh, &window); + } + zcosmic_toplevel_manager_v1::Request::UnsetMinimized { toplevel } => { + let window = window_from_handle(toplevel).unwrap(); + state.unminimize(dh, &window); + } + zcosmic_toplevel_manager_v1::Request::SetSticky { toplevel } => { + let window = window_from_handle(toplevel).unwrap(); + state.set_sticky(dh, &window); + } + zcosmic_toplevel_manager_v1::Request::UnsetSticky { toplevel } => { + let window = window_from_handle(toplevel).unwrap(); + state.unset_sticky(dh, &window); + } + zcosmic_toplevel_manager_v1::Request::SetRectangle { + toplevel, + surface, + x, + y, + width, + height, + } => { + let window = + window_from_handle::<::Window>(toplevel).unwrap(); + if let Some(toplevel_state) = window.user_data().get::() { + let mut toplevel_state = toplevel_state.lock().unwrap(); + toplevel_state + .rectangles + .retain(|(s, _)| s.id() != surface.id()); + if width != 0 || height != 0 { + toplevel_state.rectangles.push(( + surface.downgrade(), + Rectangle::new((x, y).into(), (width, height).into()), + )); + } + } + } + zcosmic_toplevel_manager_v1::Request::MoveToWorkspace { .. } => {} + zcosmic_toplevel_manager_v1::Request::MoveToExtWorkspace { + toplevel, + workspace, + output, + } => { + let window = window_from_handle(toplevel).unwrap(); + if let Some(workspace_handle) = + state.workspace_state().get_ext_workspace_handle(&workspace) + && let Some(output) = Output::from_resource(&output) + { + state.move_to_workspace(dh, &window, workspace_handle, output); + } + } + _ => unreachable!(), + } + } + + fn destroyed(state: &mut D, client: ClientId, resource: &ZcosmicToplevelManagerV1, _data: &()) { + let mng_state = state.toplevel_management_state(); + mng_state.instances.retain(|i| i != resource); + if !mng_state + .instances + .iter() + .any(|i| i.client().map(|c| c.id() == client).unwrap_or(false)) + { + for toplevel in state.toplevel_info_state_mut().toplevels.iter() { + if let Some(toplevel_state) = toplevel.user_data().get::() { + toplevel_state.lock().unwrap().rectangles.retain(|(s, _)| { + s.upgrade() + .ok() + .and_then(|s| s.client()) + .map(|c| c.id() != client) + .unwrap_or(false) + }); + } + } + } + } +} + +macro_rules! delegate_toplevel_management { + ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { + smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + cosmic_protocols::toplevel_management::v1::server::zcosmic_toplevel_manager_v1::ZcosmicToplevelManagerV1: $crate::wayland::protocols::toplevel_management::ToplevelManagerGlobalData + ] => $crate::wayland::protocols::toplevel_management::ToplevelManagementState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + cosmic_protocols::toplevel_management::v1::server::zcosmic_toplevel_manager_v1::ZcosmicToplevelManagerV1: () + ] => $crate::wayland::protocols::toplevel_management::ToplevelManagementState); + }; +} +pub(crate) use delegate_toplevel_management; diff --git a/src/wayland/protocols/workspace/cosmic_v2.rs b/src/wayland/protocols/workspace/cosmic_v2.rs new file mode 100644 index 0000000..42d26b4 --- /dev/null +++ b/src/wayland/protocols/workspace/cosmic_v2.rs @@ -0,0 +1,316 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use cosmic_protocols::workspace::v2::server::{ + zcosmic_workspace_handle_v2::{self, ZcosmicWorkspaceHandleV2}, + zcosmic_workspace_manager_v2::{self, ZcosmicWorkspaceManagerV2}, +}; +use smithay::reexports::{ + wayland_protocols::ext::workspace::v1::server::ext_workspace_handle_v1::ExtWorkspaceHandleV1, + wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, Weak, + }, +}; +use std::sync::Mutex; + +use super::{ + Request, State, Workspace, WorkspaceCapabilities, WorkspaceData, WorkspaceGlobalData, + WorkspaceHandler, WorkspaceManagerData, WorkspaceState, +}; + +#[derive(Default)] +pub struct CosmicWorkspaceV2DataInner { + capabilities: Option, + tiling: Option, + states: Option, +} + +pub struct CosmicWorkspaceV2Data { + workspace: Weak, + inner: Mutex, +} + +impl GlobalDispatch for WorkspaceState +where + D: WorkspaceHandler, +{ + fn bind( + _state: &mut D, + _dh: &DisplayHandle, + _client: &Client, + resource: New, + _global_data: &WorkspaceGlobalData, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, ()); + } + + fn can_view(client: Client, global_data: &WorkspaceGlobalData) -> bool { + (global_data.filter)(&client) + } +} + +impl Dispatch for WorkspaceState +where + D: WorkspaceHandler, +{ + fn request( + state: &mut D, + _client: &Client, + obj: &ZcosmicWorkspaceManagerV2, + request: zcosmic_workspace_manager_v2::Request, + _data: &(), + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + zcosmic_workspace_manager_v2::Request::GetCosmicWorkspace { + cosmic_workspace, + workspace, + } => { + let cosmic_workspace = data_init.init( + cosmic_workspace, + CosmicWorkspaceV2Data { + workspace: workspace.downgrade(), + inner: Mutex::new(CosmicWorkspaceV2DataInner::default()), + }, + ); + if let Some(data) = workspace.data::() { + let mut inner = data.inner.lock().unwrap(); + if inner + .cosmic_v2_handle + .as_ref() + .is_some_and(|x| x.is_alive()) + { + obj.post_error( + zcosmic_workspace_manager_v2::Error::WorkspaceExists, + "zcosmic_workspace_handle_v2 already exists for ext_workspace_handle_v1", + ); + return; + } + inner.cosmic_v2_handle = Some(cosmic_workspace.downgrade()); + if let Some(workspace) = state + .workspace_state() + .groups + .iter() + .flat_map(|g| &g.workspaces) + .find(|w| w.ext_instances.contains(&workspace)) + && let Ok(ext_mngr) = data.manager.upgrade() + { + send_workspace_to_client(&cosmic_workspace, workspace); + ext_mngr.done(); + } + } + } + zcosmic_workspace_manager_v2::Request::Destroy => {} + _ => unreachable!(), + } + } +} + +impl Dispatch for WorkspaceState +where + D: WorkspaceHandler, +{ + fn request( + state: &mut D, + _client: &Client, + _obj: &ZcosmicWorkspaceHandleV2, + request: zcosmic_workspace_handle_v2::Request, + data: &CosmicWorkspaceV2Data, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + let Ok(workspace) = data.workspace.upgrade() else { + return; + }; + match request { + zcosmic_workspace_handle_v2::Request::Rename { name } => { + if let Some(workspace_handle) = + state.workspace_state().get_ext_workspace_handle(&workspace) + && let Ok(manager) = + workspace.data::().unwrap().manager.upgrade() + { + let mut state = manager + .data::() + .unwrap() + .lock() + .unwrap(); + state.requests.push(Request::Rename { + workspace: workspace_handle, + name, + }); + } + } + zcosmic_workspace_handle_v2::Request::SetTilingState { + state: tiling_state, + } => { + if let Some(workspace_handle) = + state.workspace_state().get_ext_workspace_handle(&workspace) + && let Ok(manager) = + workspace.data::().unwrap().manager.upgrade() + { + let mut state = manager + .data::() + .unwrap() + .lock() + .unwrap(); + state.requests.push(Request::SetTilingState { + workspace: workspace_handle, + state: tiling_state, + }); + } + } + zcosmic_workspace_handle_v2::Request::Pin => { + if let Some(workspace_handle) = + state.workspace_state().get_ext_workspace_handle(&workspace) + && let Ok(manager) = + workspace.data::().unwrap().manager.upgrade() + { + let mut state = manager + .data::() + .unwrap() + .lock() + .unwrap(); + state.requests.push(Request::SetPin { + workspace: workspace_handle, + pinned: true, + }); + } + } + zcosmic_workspace_handle_v2::Request::Unpin => { + if let Some(workspace_handle) = + state.workspace_state().get_ext_workspace_handle(&workspace) + && let Ok(manager) = + workspace.data::().unwrap().manager.upgrade() + { + let mut state = manager + .data::() + .unwrap() + .lock() + .unwrap(); + state.requests.push(Request::SetPin { + workspace: workspace_handle, + pinned: false, + }); + } + } + zcosmic_workspace_handle_v2::Request::MoveBefore { + other_workspace, + axis, + } => { + if let Some(workspace_handle) = + state.workspace_state().get_ext_workspace_handle(&workspace) + && let Some(other_workspace) = state + .workspace_state() + .get_ext_workspace_handle(&other_workspace) + && let Ok(manager) = + workspace.data::().unwrap().manager.upgrade() + { + let mut state = manager + .data::() + .unwrap() + .lock() + .unwrap(); + state.requests.push(Request::MoveBefore { + workspace: workspace_handle, + other_workspace, + axis, + }); + } + } + zcosmic_workspace_handle_v2::Request::MoveAfter { + other_workspace, + axis, + } => { + if let Some(workspace_handle) = + state.workspace_state().get_ext_workspace_handle(&workspace) + && let Some(other_workspace) = state + .workspace_state() + .get_ext_workspace_handle(&other_workspace) + && let Ok(manager) = + workspace.data::().unwrap().manager.upgrade() + { + let mut state = manager + .data::() + .unwrap() + .lock() + .unwrap(); + state.requests.push(Request::MoveAfter { + workspace: workspace_handle, + other_workspace, + axis, + }); + } + } + zcosmic_workspace_handle_v2::Request::Destroy => {} + _ => unreachable!(), + } + } +} + +pub fn send_workspace_to_client( + instance: &ZcosmicWorkspaceHandleV2, + workspace: &Workspace, +) -> bool { + let mut changed = false; + + let mut handle_state = instance + .data::() + .unwrap() + .inner + .lock() + .unwrap(); + + let capabilities = workspace + .capabilities + .iter() + .filter_map(|cap| match cap { + WorkspaceCapabilities::Rename => { + Some(zcosmic_workspace_handle_v2::WorkspaceCapabilities::Rename) + } + WorkspaceCapabilities::SetTilingState => { + Some(zcosmic_workspace_handle_v2::WorkspaceCapabilities::SetTilingState) + } + WorkspaceCapabilities::Pin => { + Some(zcosmic_workspace_handle_v2::WorkspaceCapabilities::Pin) + } + WorkspaceCapabilities::Move => { + Some(zcosmic_workspace_handle_v2::WorkspaceCapabilities::Move) + } + _ => None, + }) + .collect::(); + if handle_state.capabilities != Some(capabilities) { + instance.capabilities(capabilities); + handle_state.capabilities = Some(capabilities); + changed = true; + } + + if handle_state + .tiling + .map(|state| state != workspace.tiling) + .unwrap_or(true) + { + instance.tiling_state(workspace.tiling); + handle_state.tiling = Some(workspace.tiling); + changed = true; + } + + if instance.version() >= zcosmic_workspace_handle_v2::EVT_STATE_SINCE { + let states = workspace + .states + .iter() + .filter_map(|state| match state { + State::Pinned => Some(zcosmic_workspace_handle_v2::State::Pinned), + _ => None, + }) + .collect::(); + if handle_state.states != Some(states) { + instance.state(states); + handle_state.states = Some(states); + changed = true; + } + } + + changed +} diff --git a/src/wayland/protocols/workspace/ext.rs b/src/wayland/protocols/workspace/ext.rs new file mode 100644 index 0000000..e7dc131 --- /dev/null +++ b/src/wayland/protocols/workspace/ext.rs @@ -0,0 +1,502 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use cosmic_protocols::workspace::v2::server::zcosmic_workspace_handle_v2::ZcosmicWorkspaceHandleV2; + +use smithay::{ + output::Output, + reexports::{ + wayland_protocols::ext::workspace::v1::server::{ + ext_workspace_group_handle_v1::{self, ExtWorkspaceGroupHandleV1, GroupCapabilities}, + ext_workspace_handle_v1::{self, ExtWorkspaceHandleV1}, + ext_workspace_manager_v1::{self, ExtWorkspaceManagerV1}, + }, + wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, Weak, + backend::ClientId, protocol::wl_output::WlOutput, + }, + }, +}; + +use std::{collections::HashSet, sync::Mutex}; + +use super::{ + Request, State, Workspace, WorkspaceCapabilities, WorkspaceGlobalData, WorkspaceGroup, + WorkspaceGroupHandle, WorkspaceHandler, WorkspaceState, +}; + +#[derive(Debug, Default)] +pub struct WorkspaceManagerDataInner { + pub(super) requests: Vec, +} + +pub type WorkspaceManagerData = Mutex; + +#[derive(Default)] +pub struct WorkspaceGroupDataInner { + outputs: Vec, + wl_outputs: HashSet, + capabilities: Option, + workspace_count: usize, +} + +pub struct WorkspaceGroupData { + inner: Mutex, + pub(super) manager: Weak, +} + +#[derive(Default)] +pub struct WorkspaceDataInner { + pub(super) group: Option, + name: String, + capabilities: Option, + coordinates: Vec, + states: Option, + ext_id: Option, + pub(super) cosmic_v2_handle: Option>, +} + +pub struct WorkspaceData { + pub(super) inner: Mutex, + pub(super) manager: Weak, +} + +impl GlobalDispatch for WorkspaceState +where + D: WorkspaceHandler, +{ + fn bind( + state: &mut D, + dh: &DisplayHandle, + _client: &Client, + resource: New, + _global_data: &WorkspaceGlobalData, + data_init: &mut DataInit<'_, D>, + ) { + let state = state.workspace_state_mut(); + let instance = data_init.init(resource, WorkspaceManagerData::default()); + for group in &mut state.groups { + send_group_to_client::(dh, &instance, group); + } + instance.done(); + state.ext_instances.push(instance); + } + + fn can_view(client: Client, global_data: &WorkspaceGlobalData) -> bool { + (global_data.filter)(&client) + } +} + +impl Dispatch for WorkspaceState +where + D: WorkspaceHandler, +{ + fn request( + state: &mut D, + _client: &Client, + obj: &ExtWorkspaceManagerV1, + request: ext_workspace_manager_v1::Request, + data: &WorkspaceManagerData, + dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + ext_workspace_manager_v1::Request::Commit => { + if state.workspace_state().ext_instances.contains(obj) { + let mut data = data.lock().unwrap(); + state.commit_requests(dh, std::mem::take(&mut data.requests)); + } + } + ext_workspace_manager_v1::Request::Stop => { + state + .workspace_state_mut() + .ext_instances + .retain(|i| i != obj); + // without an instance, the whole send_group_to_client machinery doesn't work + // so there is no way for the whole clients hierachy to get any new events + } + _ => {} + } + } + + fn destroyed( + state: &mut D, + _client: ClientId, + resource: &ExtWorkspaceManagerV1, + _data: &WorkspaceManagerData, + ) { + state + .workspace_state_mut() + .ext_instances + .retain(|i| i != resource); + } +} + +impl Dispatch for WorkspaceState +where + D: WorkspaceHandler, +{ + fn request( + state: &mut D, + _client: &Client, + obj: &ExtWorkspaceGroupHandleV1, + request: ext_workspace_group_handle_v1::Request, + data: &WorkspaceGroupData, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + ext_workspace_group_handle_v1::Request::CreateWorkspace { workspace } => { + if let Some(id) = state + .workspace_state() + .groups + .iter() + .find(|g| g.ext_instances.contains(obj)) + .map(|g| g.id) + && let Ok(manager) = data.manager.upgrade() + { + let mut state = manager + .data::() + .unwrap() + .lock() + .unwrap(); + state.requests.push(Request::Create { + in_group: WorkspaceGroupHandle { id }, + name: workspace, + }); + } + } + ext_workspace_group_handle_v1::Request::Destroy => { + for group in &mut state.workspace_state_mut().groups { + group.ext_instances.retain(|i| i != obj) + } + } + _ => {} + } + } + + fn destroyed( + state: &mut D, + _client: ClientId, + resource: &ExtWorkspaceGroupHandleV1, + _data: &WorkspaceGroupData, + ) { + for group in &mut state.workspace_state_mut().groups { + group.ext_instances.retain(|i| i != resource) + } + } +} + +impl Dispatch for WorkspaceState +where + D: WorkspaceHandler, +{ + fn request( + state: &mut D, + _client: &Client, + obj: &ExtWorkspaceHandleV1, + request: ext_workspace_handle_v1::Request, + data: &WorkspaceData, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + ext_workspace_handle_v1::Request::Activate => { + if let Some(workspace_handle) = + state.workspace_state().get_ext_workspace_handle(obj) + && let Ok(manager) = data.manager.upgrade() + { + let mut state = manager + .data::() + .unwrap() + .lock() + .unwrap(); + state.requests.push(Request::Activate(workspace_handle)); + } + } + ext_workspace_handle_v1::Request::Deactivate => { + if let Some(workspace_handle) = + state.workspace_state().get_ext_workspace_handle(obj) + && let Ok(manager) = data.manager.upgrade() + { + let mut state = manager + .data::() + .unwrap() + .lock() + .unwrap(); + state.requests.push(Request::Deactivate(workspace_handle)); + } + } + ext_workspace_handle_v1::Request::Remove => { + if let Some(workspace_handle) = + state.workspace_state().get_ext_workspace_handle(obj) + && let Ok(manager) = data.manager.upgrade() + { + let mut state = manager + .data::() + .unwrap() + .lock() + .unwrap(); + state.requests.push(Request::Remove(workspace_handle)); + } + } + ext_workspace_handle_v1::Request::Assign { workspace_group } => { + if let Some(workspace_handle) = + state.workspace_state().get_ext_workspace_handle(obj) + && let Some(group_id) = state + .workspace_state() + .groups + .iter() + .find(|g| g.ext_instances.contains(&workspace_group)) + .map(|g| g.id) + && let Ok(manager) = data.manager.upgrade() + { + let mut state = manager + .data::() + .unwrap() + .lock() + .unwrap(); + state.requests.push(Request::Assign { + workspace: workspace_handle, + group: WorkspaceGroupHandle { id: group_id }, + }); + } + } + ext_workspace_handle_v1::Request::Destroy => { + for group in &mut state.workspace_state_mut().groups { + for workspace in &mut group.workspaces { + workspace.ext_instances.retain(|i| i != obj) + } + } + } + _ => {} + } + } + + fn destroyed( + state: &mut D, + _client: ClientId, + resource: &ExtWorkspaceHandleV1, + _data: &WorkspaceData, + ) { + for group in &mut state.workspace_state_mut().groups { + for workspace in &mut group.workspaces { + workspace.ext_instances.retain(|i| i != resource) + } + } + } +} + +pub(super) fn send_group_to_client( + dh: &DisplayHandle, + mngr: &ExtWorkspaceManagerV1, + group: &mut WorkspaceGroup, +) -> bool +where + D: WorkspaceHandler, +{ + let instance = match group + .ext_instances + .iter_mut() + .find(|i| i.data::().unwrap().manager == *mngr) + { + Some(i) => i, + None => { + if let Ok(client) = dh.get_client(mngr.id()) { + if let Ok(handle) = client.create_resource::( + dh, + mngr.version(), + WorkspaceGroupData { + inner: Mutex::new(WorkspaceGroupDataInner::default()), + manager: mngr.downgrade(), + }, + ) { + mngr.workspace_group(&handle); + group.ext_instances.push(handle); + group.ext_instances.last_mut().unwrap() + } else { + return false; + } + } else { + return false; + } + } + }; + + let mut handle_state = instance + .data::() + .unwrap() + .inner + .lock() + .unwrap(); + let mut changed = false; + if let Ok(client) = dh.get_client(instance.id()) { + for output in &group.outputs { + for wl_output in output.client_outputs(&client) { + if handle_state.wl_outputs.insert(wl_output.clone()) { + instance.output_enter(&wl_output); + changed = true; + } + } + } + + handle_state.wl_outputs.retain(|wl_output| { + let retain = + wl_output.is_alive() && group.outputs.iter().any(|output| output.owns(wl_output)); + if !retain { + instance.output_leave(wl_output); + changed = true; + } + retain + }); + + handle_state.outputs = group.outputs.clone(); + } + + if handle_state.capabilities != Some(group.capabilities) { + instance.capabilities(group.capabilities); + handle_state.capabilities = Some(group.capabilities); + changed = true; + } + + if handle_state.workspace_count != group.workspaces.len() { + changed = true; + } + handle_state.workspace_count = group.workspaces.len(); + + for workspace in &mut group.workspaces { + if send_workspace_to_client::(dh, mngr, instance, workspace) { + changed = true; + } + } + + changed +} + +fn send_workspace_to_client( + dh: &DisplayHandle, + mngr: &ExtWorkspaceManagerV1, + group: &ExtWorkspaceGroupHandleV1, + workspace: &mut Workspace, +) -> bool +where + D: WorkspaceHandler, +{ + let instance = match workspace + .ext_instances + .iter_mut() + .find(|i| i.data::().unwrap().manager == *mngr) + { + Some(i) => i, + None => { + if let Ok(client) = dh.get_client(mngr.id()) { + if let Ok(handle) = client.create_resource::( + dh, + mngr.version(), + WorkspaceData { + inner: Mutex::new(WorkspaceDataInner::default()), + manager: mngr.downgrade(), + }, + ) { + mngr.workspace(&handle); + workspace.ext_instances.push(handle); + workspace.ext_instances.last_mut().unwrap() + } else { + return false; + } + } else { + return false; + } + } + }; + let instance = instance.clone(); + + let mut handle_state = instance + .data::() + .unwrap() + .inner + .lock() + .unwrap(); + let mut changed = false; + + if handle_state.group.as_ref() != Some(group) { + if let Some(old_group) = &handle_state.group { + old_group.workspace_leave(&instance); + } + group.workspace_enter(&instance); + handle_state.group = Some(group.clone()); + } + + if handle_state.name != workspace.name { + instance.name(workspace.name.clone()); + handle_state.name = workspace.name.clone(); + changed = true; + } + + if handle_state.coordinates != workspace.coordinates { + let coords = workspace + .coordinates + .iter() + .flat_map(|coord| coord.to_ne_bytes()) + .collect::>(); + instance.coordinates(coords); + handle_state.coordinates = workspace.coordinates.clone(); + changed = true; + } + + let capabilities = workspace + .capabilities + .iter() + .filter_map(|cap| match cap { + WorkspaceCapabilities::Activate => { + Some(ext_workspace_handle_v1::WorkspaceCapabilities::Activate) + } + WorkspaceCapabilities::Deactivate => { + Some(ext_workspace_handle_v1::WorkspaceCapabilities::Deactivate) + } + WorkspaceCapabilities::Remove => { + Some(ext_workspace_handle_v1::WorkspaceCapabilities::Remove) + } + WorkspaceCapabilities::Assign => { + Some(ext_workspace_handle_v1::WorkspaceCapabilities::Assign) + } + _ => None, + }) + .collect::(); + if handle_state.capabilities != Some(capabilities) { + instance.capabilities(capabilities); + handle_state.capabilities = Some(capabilities); + changed = true; + } + + let states = workspace + .states + .iter() + .filter_map(|state| match state { + State::Active => Some(ext_workspace_handle_v1::State::Active), + State::Urgent => Some(ext_workspace_handle_v1::State::Urgent), + State::Hidden => Some(ext_workspace_handle_v1::State::Hidden), + _ => None, + }) + .collect(); + if handle_state.states != Some(states) { + instance.state(states); + handle_state.states = Some(states); + changed = true; + } + + if handle_state.ext_id.is_none() + && let Some(id) = workspace.ext_id.clone() + { + instance.id(id); + } + + if let Some(cosmic_v2_handle) = handle_state + .cosmic_v2_handle + .as_ref() + .and_then(|x| x.upgrade().ok()) + { + changed |= super::cosmic_v2::send_workspace_to_client(&cosmic_v2_handle, workspace); + } + + changed +} diff --git a/src/wayland/protocols/workspace/mod.rs b/src/wayland/protocols/workspace/mod.rs new file mode 100644 index 0000000..e2fbb92 --- /dev/null +++ b/src/wayland/protocols/workspace/mod.rs @@ -0,0 +1,658 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use smithay::{ + output::Output, + reexports::{ + wayland_protocols::ext::workspace::v1::server::{ + ext_workspace_group_handle_v1::{ExtWorkspaceGroupHandleV1, GroupCapabilities}, + ext_workspace_handle_v1::ExtWorkspaceHandleV1, + ext_workspace_manager_v1::ExtWorkspaceManagerV1, + }, + wayland_server::{ + Client, Dispatch, DisplayHandle, GlobalDispatch, Resource, + backend::{GlobalId, ObjectId}, + }, + }, +}; +use wayland_backend::protocol::WEnum; + +use cosmic_protocols::workspace::v2::server::{ + zcosmic_workspace_handle_v2::{self, ZcosmicWorkspaceHandleV2}, + zcosmic_workspace_manager_v2::ZcosmicWorkspaceManagerV2, +}; + +mod cosmic_v2; +pub use cosmic_v2::CosmicWorkspaceV2Data; +mod ext; +pub use ext::{WorkspaceData, WorkspaceGroupData, WorkspaceManagerData}; + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] + pub struct WorkspaceCapabilities: u32 { + const Activate = 1; + const Deactivate = 2; + const Remove = 4; + /// not in legacy cosmic protocol + const Assign = 8; + /// cosmic specific + const Rename = 16; + /// cosmic specific + const SetTilingState = 32; + const Pin = 64; + const Move = 128; + } +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] + pub struct State: u32 { + const Active = 1; + const Urgent = 2; + const Hidden = 4; + /// cosmic specific + const Pinned = 8; + } +} + +#[derive(Debug)] +pub struct WorkspaceState +where + D: WorkspaceHandler, +{ + dh: DisplayHandle, + ext_global: GlobalId, + cosmic_v2_global: GlobalId, + ext_instances: Vec, + groups: Vec, + _marker: std::marker::PhantomData, +} +pub struct WorkspaceUpdateGuard<'a, D>(&'a mut WorkspaceState) +where + D: WorkspaceHandler; + +crate::utils::id_gen!(next_group_id, GROUP_ID, GROUP_IDS); +crate::utils::id_gen!(next_workspace_id, WORKSPACE_ID, WORKSPACE_IDS); + +#[derive(Debug)] +pub struct WorkspaceGroup { + id: usize, + ext_instances: Vec, + workspaces: Vec, + + outputs: Vec, + capabilities: GroupCapabilities, +} + +impl Default for WorkspaceGroup { + fn default() -> Self { + Self { + id: 0, + ext_instances: Vec::new(), + workspaces: Vec::new(), + + outputs: Vec::new(), + capabilities: GroupCapabilities::empty(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct WorkspaceGroupHandle { + id: usize, +} + +#[derive(Debug)] +pub struct Workspace { + id: usize, + ext_instances: Vec, + + name: String, + capabilities: WorkspaceCapabilities, + coordinates: Vec, + states: State, + tiling: zcosmic_workspace_handle_v2::TilingState, + ext_id: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct WorkspaceHandle { + id: usize, +} + +pub trait WorkspaceHandler +where + Self: GlobalDispatch + + Dispatch + + Dispatch + + Dispatch + + GlobalDispatch + + Dispatch + + Dispatch + + Sized + + 'static, +{ + fn workspace_state(&self) -> &WorkspaceState; + fn workspace_state_mut(&mut self) -> &mut WorkspaceState; + fn commit_requests(&mut self, dh: &DisplayHandle, requests: Vec); +} + +pub struct WorkspaceGlobalData { + filter: Box Fn(&'a Client) -> bool + Send + Sync>, +} + +#[derive(Debug)] +pub enum Request { + Activate(WorkspaceHandle), + Deactivate(WorkspaceHandle), + Remove(WorkspaceHandle), + Rename { + workspace: WorkspaceHandle, + name: String, + }, + SetTilingState { + workspace: WorkspaceHandle, + state: WEnum, + }, + Create { + in_group: WorkspaceGroupHandle, + name: String, + }, + Assign { + workspace: WorkspaceHandle, + group: WorkspaceGroupHandle, + }, + SetPin { + workspace: WorkspaceHandle, + pinned: bool, + }, + MoveBefore { + workspace: WorkspaceHandle, + other_workspace: WorkspaceHandle, + axis: u32, + }, + MoveAfter { + workspace: WorkspaceHandle, + other_workspace: WorkspaceHandle, + axis: u32, + }, +} + +impl WorkspaceState +where + D: WorkspaceHandler, +{ + pub fn new(dh: &DisplayHandle, client_filter: F) -> WorkspaceState + where + F: for<'a> Fn(&'a Client) -> bool + Clone + Send + Sync + 'static, + { + let ext_global = dh.create_global::( + 1, + WorkspaceGlobalData { + filter: Box::new(client_filter.clone()), + }, + ); + + let cosmic_v2_global = dh.create_global::( + 2, + WorkspaceGlobalData { + filter: Box::new(client_filter.clone()), + }, + ); + + WorkspaceState { + dh: dh.clone(), + ext_global, + cosmic_v2_global, + ext_instances: Vec::new(), + groups: Vec::new(), + _marker: std::marker::PhantomData, + } + } + + pub fn workspace_belongs_to_group( + &self, + group: &WorkspaceGroupHandle, + workspace: &WorkspaceHandle, + ) -> bool { + if let Some(group) = self.groups.iter().find(|g| g.id == group.id) { + group.workspaces.iter().any(|w| w.id == workspace.id) + } else { + false + } + } + + pub fn group_capabilities(&self, group: &WorkspaceGroupHandle) -> Option { + Some(self.groups.iter().find(|g| g.id == group.id)?.capabilities) + } + + pub fn group_outputs( + &self, + group: &WorkspaceGroupHandle, + ) -> Option> { + self.groups + .iter() + .find(|g| g.id == group.id) + .map(|g| g.outputs.iter()) + } + + pub fn workspace_capabilities( + &self, + workspace: &WorkspaceHandle, + ) -> Option { + self.groups.iter().find_map(|g| { + Some( + g.workspaces + .iter() + .find(|w| w.id == workspace.id)? + .capabilities, + ) + }) + } + + pub fn workspace_name(&self, workspace: &WorkspaceHandle) -> Option<&str> { + self.groups.iter().find_map(|g| { + g.workspaces + .iter() + .find(|w| w.id == workspace.id) + .map(|w| &*w.name) + }) + } + + pub fn workspace_coordinates(&self, workspace: &WorkspaceHandle) -> Option<&[u32]> { + self.groups.iter().find_map(|g| { + g.workspaces + .iter() + .find(|w| w.id == workspace.id) + .map(|w| &*w.coordinates) + }) + } + + pub fn workspace_states(&self, workspace: &WorkspaceHandle) -> Option { + self.groups + .iter() + .find_map(|g| Some(g.workspaces.iter().find(|w| w.id == workspace.id)?.states)) + } + + pub fn workspace_tiling_state( + &self, + workspace: &WorkspaceHandle, + ) -> Option { + self.groups.iter().find_map(|g| { + g.workspaces + .iter() + .find(|w| w.id == workspace.id) + .map(|w| w.tiling) + }) + } + + pub fn raw_ext_workspace_handles<'a>( + &'a self, + workspace: &'a WorkspaceHandle, + client: &'a ObjectId, + ) -> impl Iterator + 'a { + self.groups + .iter() + .find_map(|g| g.workspaces.iter().find(|w| w.id == workspace.id)) + .into_iter() + .flat_map(|w| &w.ext_instances) + .filter(|i| Resource::id(*i).same_client_as(client)) + } + + pub fn update(&mut self) -> WorkspaceUpdateGuard<'_, D> { + WorkspaceUpdateGuard(self) + } + + fn done(&mut self) { + let mut changed = false; + for instance in &self.ext_instances { + for group in &mut self.groups { + if ext::send_group_to_client::(&self.dh, instance, group) { + changed = true; + } + } + } + if changed { + for instance in &self.ext_instances { + instance.done(); + } + } + } + + pub fn get_ext_workspace_handle( + &self, + handle: &ExtWorkspaceHandleV1, + ) -> Option { + self.groups + .iter() + .flat_map(|g| &g.workspaces) + .find(|w| w.ext_instances.contains(handle)) + .map(|w| WorkspaceHandle { id: w.id }) + } + + pub fn ext_global_id(&self) -> GlobalId { + self.ext_global.clone() + } + + pub fn cosmic_v2_global_id(&self) -> GlobalId { + self.cosmic_v2_global.clone() + } +} + +impl WorkspaceUpdateGuard<'_, D> +where + D: WorkspaceHandler, +{ + pub fn create_workspace_group(&mut self) -> WorkspaceGroupHandle { + let id = next_group_id(); + let group = WorkspaceGroup { + id, + ..Default::default() + }; + self.0.groups.push(group); + WorkspaceGroupHandle { id } + } + + pub fn create_workspace( + &mut self, + group: &WorkspaceGroupHandle, + tiling: zcosmic_workspace_handle_v2::TilingState, + ext_id: Option, + ) -> Option { + if let Some(group) = self.0.groups.iter_mut().find(|g| g.id == group.id) { + let id = next_workspace_id(); + let workspace = Workspace { + id, + tiling, + ext_instances: Default::default(), + name: Default::default(), + capabilities: WorkspaceCapabilities::empty(), + coordinates: Default::default(), + states: State::empty(), + ext_id, + }; + group.workspaces.push(workspace); + Some(WorkspaceHandle { id }) + } else { + None + } + } + + pub fn remove_workspace_group(&mut self, group: WorkspaceGroupHandle) { + // "The compositor must remove all workspaces belonging to a workspace group before removing the workspace group." + for workspace in self + .0 + .groups + .iter() + .filter(|g| g.id == group.id) + .flat_map(|g| g.workspaces.iter().map(|w| WorkspaceHandle { id: w.id })) + .collect::>() + .into_iter() + { + self.remove_workspace(workspace); + } + + if let Some(group) = self.0.groups.iter().find(|g| g.id == group.id) { + for instance in &group.ext_instances { + instance.removed(); + } + } + self.0.groups.retain(|g| g.id != group.id); + GROUP_IDS.lock().unwrap().remove(&group.id); + } + + pub fn remove_workspace(&mut self, workspace: WorkspaceHandle) { + for group in &mut self.0.groups { + if let Some(workspace) = group.workspaces.iter().find(|w| w.id == workspace.id) { + for instance in &workspace.ext_instances { + let workspace_data = instance.data::().unwrap(); + if let Some(group_instance) = &workspace_data.inner.lock().unwrap().group { + group_instance.workspace_leave(instance); + } + instance.removed(); + } + } + group.workspaces.retain(|w| w.id != workspace.id); + } + WORKSPACE_IDS.lock().unwrap().remove(&workspace.id); + } + + pub fn move_workspace_to_group( + &mut self, + group_handle: WorkspaceGroupHandle, + workspace_handle: WorkspaceHandle, + ) { + // Get index of new group + let Some(group_idx) = self.0.groups.iter().position(|g| g.id == group_handle.id) else { + // If the new group doesn't exist, we shouldn't remove the workspace from its old group + return; + }; + + // Find old group index, and remove the workspace + let Some(workspace) = self.0.groups.iter_mut().find_map(|group| { + let idx = group + .workspaces + .iter() + .position(|w| w.id == workspace_handle.id)?; + Some(group.workspaces.remove(idx)) + }) else { + // Workspace not found in any group + return; + }; + + // Send `workspace_leave` for ever instance with old group. + // + // `workspace_enter` will be sent in `send_group_to_client`, but if a group is destroyed, + // we need to make sure `workspace_leave` is sent first. + for instance in &workspace.ext_instances { + let workspace_data = instance.data::().unwrap(); + if let Some(group_instance) = workspace_data.inner.lock().unwrap().group.take() { + group_instance.workspace_leave(instance); + } + } + + self.0.groups[group_idx].workspaces.push(workspace); + } + + pub fn workspace_belongs_to_group( + &self, + group: &WorkspaceGroupHandle, + workspace: &WorkspaceHandle, + ) -> bool { + self.0.workspace_belongs_to_group(group, workspace) + } + + pub fn group_capabilities( + &mut self, + group: &WorkspaceGroupHandle, + ) -> Option { + self.0.group_capabilities(group) + } + + pub fn set_group_capabilities( + &mut self, + group: &WorkspaceGroupHandle, + capabilities: GroupCapabilities, + ) { + if let Some(group) = self.0.groups.iter_mut().find(|g| g.id == group.id) { + group.capabilities = capabilities; + } + } + + pub fn group_outputs( + &self, + group: &WorkspaceGroupHandle, + ) -> Option> { + self.0.group_outputs(group) + } + + pub fn add_group_output(&mut self, group: &WorkspaceGroupHandle, output: &Output) { + if let Some(group) = self.0.groups.iter_mut().find(|g| g.id == group.id) { + group.outputs.push(output.clone()) + } + } + + pub fn remove_group_output(&mut self, group: &WorkspaceGroupHandle, output: &Output) { + if let Some(group) = self.0.groups.iter_mut().find(|g| g.id == group.id) { + group.outputs.retain(|o| o != output) + } + } + + pub fn workspace_capabilities( + &self, + workspace: &WorkspaceHandle, + ) -> Option { + self.0.workspace_capabilities(workspace) + } + + pub fn set_workspace_capabilities( + &mut self, + workspace: &WorkspaceHandle, + capabilities: WorkspaceCapabilities, + ) { + if let Some(workspace) = self + .0 + .groups + .iter_mut() + .find_map(|g| g.workspaces.iter_mut().find(|w| w.id == workspace.id)) + { + workspace.capabilities = capabilities; + } + } + + pub fn workspace_name(&self, workspace: &WorkspaceHandle) -> Option<&str> { + self.0.workspace_name(workspace) + } + + pub fn set_workspace_name(&mut self, workspace: &WorkspaceHandle, name: impl Into) { + if let Some(workspace) = self + .0 + .groups + .iter_mut() + .find_map(|g| g.workspaces.iter_mut().find(|w| w.id == workspace.id)) + { + workspace.name = name.into(); + } + } + + pub fn workspace_coordinates(&self, workspace: &WorkspaceHandle) -> Option<&[u32]> { + self.0.workspace_coordinates(workspace) + } + + pub fn set_workspace_coordinates(&mut self, workspace: &WorkspaceHandle, coords: &[u32]) { + if let Some(workspace) = self + .0 + .groups + .iter_mut() + .find_map(|g| g.workspaces.iter_mut().find(|w| w.id == workspace.id)) + { + workspace.coordinates = coords.to_vec(); + } + } + + pub fn workspace_states(&self, workspace: &WorkspaceHandle) -> Option { + self.0.workspace_states(workspace) + } + + pub fn add_workspace_state(&mut self, workspace: &WorkspaceHandle, state: State) { + if let Some(workspace) = self + .0 + .groups + .iter_mut() + .find_map(|g| g.workspaces.iter_mut().find(|w| w.id == workspace.id)) + { + workspace.states.insert(state); + } + } + + pub fn remove_workspace_state(&mut self, workspace: &WorkspaceHandle, state: State) { + if let Some(workspace) = self + .0 + .groups + .iter_mut() + .find_map(|g| g.workspaces.iter_mut().find(|w| w.id == workspace.id)) + { + workspace.states.remove(state); + } + } + + pub fn workspace_tiling_state( + &self, + workspace: &WorkspaceHandle, + ) -> Option { + self.0.workspace_tiling_state(workspace) + } + + pub fn set_workspace_tiling_state( + &mut self, + workspace: &WorkspaceHandle, + state: zcosmic_workspace_handle_v2::TilingState, + ) { + if let Some(workspace) = self + .0 + .groups + .iter_mut() + .find_map(|g| g.workspaces.iter_mut().find(|w| w.id == workspace.id)) + { + workspace.tiling = state; + } + } + + pub fn set_id( + &mut self, + workspace: &WorkspaceHandle, + id: &str, + ) -> Result<(), WorkspaceIdAlreadySetError> { + if let Some(workspace) = self + .0 + .groups + .iter_mut() + .find_map(|g| g.workspaces.iter_mut().find(|w| w.id == workspace.id)) + { + if workspace.ext_id.is_some() { + return Err(WorkspaceIdAlreadySetError); + } + workspace.ext_id = Some(id.to_owned()); + } + Ok(()) + } +} + +impl Drop for WorkspaceUpdateGuard<'_, D> +where + D: WorkspaceHandler, +{ + fn drop(&mut self) { + self.0.done(); + } +} + +#[derive(Clone, Copy, Debug)] +pub struct WorkspaceIdAlreadySetError; + +macro_rules! delegate_workspace { + ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { + smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols::ext::workspace::v1::server::ext_workspace_manager_v1::ExtWorkspaceManagerV1: $crate::wayland::protocols::workspace::WorkspaceGlobalData + ] => $crate::wayland::protocols::workspace::WorkspaceState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols::ext::workspace::v1::server::ext_workspace_manager_v1::ExtWorkspaceManagerV1: $crate::wayland::protocols::workspace::WorkspaceManagerData + ] => $crate::wayland::protocols::workspace::WorkspaceState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols::ext::workspace::v1::server::ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1: $crate::wayland::protocols::workspace::WorkspaceGroupData + ] => $crate::wayland::protocols::workspace::WorkspaceState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols::ext::workspace::v1::server::ext_workspace_handle_v1::ExtWorkspaceHandleV1: $crate::wayland::protocols::workspace::WorkspaceData + ] => $crate::wayland::protocols::workspace::WorkspaceState); + + smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + cosmic_protocols::workspace::v2::server::zcosmic_workspace_manager_v2::ZcosmicWorkspaceManagerV2: $crate::wayland::protocols::workspace::WorkspaceGlobalData + ] => $crate::wayland::protocols::workspace::WorkspaceState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + cosmic_protocols::workspace::v2::server::zcosmic_workspace_manager_v2::ZcosmicWorkspaceManagerV2: () + ] => $crate::wayland::protocols::workspace::WorkspaceState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + cosmic_protocols::workspace::v2::server::zcosmic_workspace_handle_v2::ZcosmicWorkspaceHandleV2: $crate::wayland::protocols::workspace::CosmicWorkspaceV2Data + ] => $crate::wayland::protocols::workspace::WorkspaceState); + }; +} +pub(crate) use delegate_workspace; diff --git a/src/xwayland.rs b/src/xwayland.rs new file mode 100644 index 0000000..83978b6 --- /dev/null +++ b/src/xwayland.rs @@ -0,0 +1,1298 @@ +use std::{ + ffi::OsString, + io::Write, + os::unix::io::OwnedFd, + process::Stdio, + sync::mpsc::{self, Receiver, Sender}, +}; + +use crate::{ + backend::render::cursor::{Cursor, load_cursor_env, load_cursor_theme}, + shell::{ + CosmicSurface, PendingWindow, Shell, focus::target::KeyboardFocusTarget, grabs::ReleaseMode, + }, + state::State, + utils::prelude::*, + wayland::handlers::xdg_activation::ActivationContext, +}; +use cosmic_comp_config::{EavesdroppingKeyboardMode, XwaylandDescaling}; +use smithay::{ + backend::{ + allocator::Fourcc, + drm::DrmNode, + input::{ButtonState, KeyState, Keycode}, + renderer::{ + Bind, Frame, Offscreen, Renderer, + element::{ + Kind, + memory::{MemoryRenderBuffer, MemoryRenderBufferRenderElement}, + }, + pixman::{PixmanError, PixmanRenderer}, + utils::draw_render_elements, + }, + }, + desktop::space::SpaceElement, + input::{keyboard::ModifiersState, pointer::CursorIcon}, + reexports::{wayland_server::Client, x11rb::protocol::xproto::Window as X11Window}, + utils::{ + Buffer as BufferCoords, Logical, Point, Rectangle, SERIAL_COUNTER, Serial, Size, Transform, + }, + wayland::{ + selection::{ + SelectionTarget, + data_device::{ + clear_data_device_selection, current_data_device_selection_userdata, + request_data_device_client_selection, set_data_device_selection, + }, + primary_selection::{ + clear_primary_selection, current_primary_selection_userdata, + request_primary_client_selection, set_primary_selection, + }, + }, + xdg_activation::XdgActivationToken, + }, + xwayland::{ + X11Surface, X11Wm, XWayland, XWaylandClientData, XWaylandEvent, XwmHandler, + xwm::{Reorder, XwmId}, + }, +}; +use tracing::{error, trace, warn}; +use xcursor::parser::Image; +use xkbcommon::xkb::Keysym; + +#[derive(Debug)] +pub struct XWaylandState { + pub client: Client, + pub xwm: Option, + pub display: u32, + pub pressed_keys: Vec, + pub pressed_buttons: Vec, + pub last_modifier_state: Option, + pub clipboard_selection_dirty: Option>, + pub primary_selection_dirty: Option>, + pub xrdb_thread: Sender<(String, u32)>, +} + +fn xrdb_thread(rx: Receiver<(String, u32)>, display: u32) { + while let Ok((cursor_theme, cursor_size)) = rx.recv() { + if let Ok(mut child) = std::process::Command::new("xrdb") + .arg("-merge") + .env("DISPLAY", format!(":{}", display)) + .stdin(Stdio::piped()) + .spawn() + { + let resources = format!( + "Xcursor.theme: {}\nXcursor.size: {}\n", + cursor_theme, cursor_size, + ); + if let Some(mut stdin) = child.stdin.take() + && let Err(err) = stdin.write_all(resources.as_bytes()) + { + warn!("Failed to update xresources: {}", err); + } + match child.wait() { + Ok(code) if code.success() => {} + Ok(code) => { + warn!("xrdb failed with code: {}", code); + } + Err(err) => { + warn!("Failed to wait for child: {}", err); + } + } + } else { + warn!("`xrdb` not found, cannot update Xresources."); + } + } +} + +impl State { + pub fn launch_xwayland(&mut self, render_node: Option) { + if self.common.xwayland_state.is_some() { + return; + } + + let (xwayland, client) = match XWayland::spawn( + &self.common.display_handle, + None, + std::iter::empty::<(OsString, OsString)>(), + std::iter::empty::(), + true, + Stdio::null(), + Stdio::null(), + |user_data| { + if let Some(node) = render_node { + user_data.insert_if_missing_threadsafe(|| node); + } + }, + ) { + Ok((xwayland, client)) => (xwayland, client), + Err(err) => { + error!(?err, "Failed to start Xwayland."); + self.notify_ready(); + return; + } + }; + + match self + .common + .event_loop_handle + .insert_source(xwayland, move |event, _, data| match event { + XWaylandEvent::Ready { + x11_socket, + display_number, + } => { + let (tx, rx) = mpsc::channel(); + std::thread::spawn(move || xrdb_thread(rx, display_number)); + + data.common.xwayland_state = Some(XWaylandState { + client: client.clone(), + xwm: None, + display: display_number, + pressed_keys: Vec::new(), + pressed_buttons: Vec::new(), + last_modifier_state: None, + clipboard_selection_dirty: None, + primary_selection_dirty: None, + xrdb_thread: tx, + }); + + let wm = match X11Wm::start_wm( + data.common.event_loop_handle.clone(), + &data.common.display_handle, + x11_socket, + client.clone(), + ) { + Ok(wm) => wm, + Err(err) => { + error!(?err, "Failed to start Xwayland WM"); + return; + } + }; + + let xwayland_state = data.common.xwayland_state.as_mut().unwrap(); + xwayland_state.xwm = Some(wm); + xwayland_state.reload_cursor(1.); + data.notify_ready(); + + data.common.update_xwayland_settings(); + data.common.update_xwayland_primary_output(); + } + XWaylandEvent::Error => { + if let Some(mut xwayland_state) = data.common.xwayland_state.take() { + xwayland_state.xwm = None; + } + data.notify_ready(); + } + }) { + Ok(_token) => {} + Err(err) => { + error!(?err, "Failed to listen for Xwayland"); + self.notify_ready(); + } + } + } +} + +fn scale_cursor( + scale: f64, + cursor_size: u32, + image: &Image, +) -> Result<(Vec, Size, Point), PixmanError> { + let mut renderer = PixmanRenderer::new()?; + let image_scale = (image.size / cursor_size).max(1); + let pixel_size = (cursor_size as f64 * scale).round() as i32; + let buffer_size = Size::::from((pixel_size, pixel_size)); + let output_size = buffer_size.to_logical(1, Transform::Normal).to_physical(1); + + let image_buffer = MemoryRenderBuffer::from_slice( + &image.pixels_rgba, + Fourcc::Abgr8888, + (image.width as i32, image.height as i32), + image_scale as i32, + Transform::Normal, + None, + ); + let element = MemoryRenderBufferRenderElement::from_buffer( + &mut renderer, + (0., 0.), + &image_buffer, + None, + None, + None, + Kind::Unspecified, + )?; + + let mut buffer = renderer.create_buffer(Fourcc::Abgr8888, buffer_size)?; + let mut fb = renderer.bind(&mut buffer)?; + let mut frame = renderer.render(&mut fb, output_size, Transform::Normal)?; + draw_render_elements( + &mut frame, + scale, + &[element], + &[Rectangle::new((0, 0).into(), output_size)], + )?; + let sync = frame.finish()?; + while sync.wait().is_err() {} + + let len = (buffer_size.w * buffer_size.h * 4) as usize; + let mut data = Vec::with_capacity(len); + assert_eq!(buffer.stride(), (buffer_size.w * 4) as usize); + data.extend_from_slice(unsafe { std::slice::from_raw_parts(buffer.data() as *mut u8, len) }); + + let hotspot = Point::::from((image.xhot as i32, image.yhot as i32)) + .to_f64() + .to_logical( + image_scale as f64, + Transform::Normal, + &Size::from((image.width as f64, image.height as f64)), + ) + .to_buffer( + scale, + Transform::Normal, + &output_size.to_logical(1).to_f64(), + ) + .to_i32_round::(); + Ok(( + data, + Size::from((buffer_size.w as u16, buffer_size.h as u16)), + Point::from((hotspot.x as u16, hotspot.y as u16)), + )) +} + +impl XWaylandState { + pub fn reload_cursor(&mut self, scale: f64) { + if let Some(wm) = self.xwm.as_mut() { + let (theme, size) = load_cursor_theme(); + let cursor = Cursor::load(&theme, CursorIcon::Default, size); + let image = cursor.get_image(scale.ceil() as u32, 0); + + let (pixels_rgba, size, hotspot) = match scale_cursor(scale, size, &image) { + Ok(x) => x, + Err(err) => { + warn!("Failed to scale Xwayland cursor image: {}", err); + ( + image.pixels_rgba, + Size::from((image.width as u16, image.height as u16)), + Point::from((image.xhot as u16, image.yhot as u16)), + ) + } + }; + + if let Err(err) = wm.set_cursor(&pixels_rgba, size, hotspot) { + warn!( + id = ?wm.id(), + ?err, + "Failed to set default cursor for Xwayland WM", + ); + } + } + } +} + +impl Common { + pub fn has_x_keyboard_focus(&self, xwmid: XwmId) -> bool { + let keyboard = self + .shell + .read() + .seats + .last_active() + .get_keyboard() + .unwrap(); + + keyboard + .current_focus() + .is_some_and(|target| target.is_xwm(xwmid)) + } + + fn has_x_pointer_focus(&self, xwmid: XwmId) -> bool { + let pointer = self.shell.read().seats.last_active().get_pointer().unwrap(); + + if let Some(x_client) = self.xwayland_state.as_ref().and_then(|xstate| { + xstate + .xwm + .as_ref() + .is_some_and(|xwm| xwm.id() == xwmid) + .then_some(&xstate.client) + }) { + pointer + .current_focus() + .is_some_and(|target| target.is_client(x_client)) + } else { + false + } + } + + pub fn xwayland_notify_focus_change( + &mut self, + target: Option, + serial: Serial, + ) { + if let Some(xwm_id) = self + .xwayland_state + .as_ref() + .and_then(|xstate| xstate.xwm.as_ref()) + .map(|xwm| xwm.id()) + { + if target + .as_ref() + .is_some_and(|target| matches!(target, KeyboardFocusTarget::LockSurface(_))) + { + self.xwayland_reset_eavesdropping(serial); + return; + } + + if !self.has_x_keyboard_focus(xwm_id) + && target.as_ref().is_some_and(|target| target.is_xwm(xwm_id)) + { + self.xwayland_reset_eavesdropping(serial); + + let xstate = self.xwayland_state.as_mut().unwrap(); + if let Some(mime_types) = xstate.clipboard_selection_dirty.take() + && let Err(err) = xstate + .xwm + .as_mut() + .unwrap() + .new_selection(SelectionTarget::Clipboard, Some(mime_types)) + { + warn!(?err, "Failed to set Xwayland clipboard selection."); + } + if let Some(mime_types) = xstate.primary_selection_dirty.take() + && let Err(err) = xstate + .xwm + .as_mut() + .unwrap() + .new_selection(SelectionTarget::Primary, Some(mime_types)) + { + warn!(?err, "Failed to set Xwayland clipboard selection."); + } + } + } + } + + pub fn xwayland_reset_eavesdropping(&mut self, serial: Serial) { + let seat = self.shell.read().seats.last_active().clone(); + let keyboard = seat.get_keyboard().unwrap(); + let pointer = seat.get_pointer().unwrap(); + + let xstate = self.xwayland_state.as_mut().unwrap(); + xstate.last_modifier_state.take(); + for key in xstate.pressed_keys.drain(..).rev() { + for wl_keyboard in keyboard.client_keyboards(&xstate.client) { + wl_keyboard.key(serial.into(), 0, key.raw() - 8, KeyState::Released.into()); + } + } + for button in xstate.pressed_buttons.drain(..).rev() { + for wl_pointer in pointer.client_pointers(&xstate.client) { + wl_pointer.button(serial.into(), 0, button, ButtonState::Released.into()); + } + } + } + + #[profiling::function] + pub fn xwayland_notify_key_event( + &mut self, + sym: Keysym, + code: Keycode, + state: KeyState, + serial: Serial, + time: u32, + ) { + let config = self.config.cosmic_conf.xwayland_eavesdropping.keyboard; + if config == EavesdroppingKeyboardMode::None { + return; + } + + if self.xwayland_state.as_ref().is_none_or(|xstate| { + xstate + .xwm + .as_ref() + .is_none_or(|xwm| self.has_x_keyboard_focus(xwm.id())) + }) { + return; + } + + let keyboard = self + .shell + .read() + .seats + .last_active() + .get_keyboard() + .unwrap(); + let modifiers = keyboard.modifier_state(); + let is_modifier = sym.is_modifier_key(); + + let xstate = self.xwayland_state.as_mut().unwrap(); + if state == KeyState::Pressed { + match config { + EavesdroppingKeyboardMode::Modifiers => { + if !is_modifier { + return; + } + } + EavesdroppingKeyboardMode::Combinations => { + // don't forward alpha-numeric keys, just because shift is held, but forward shift itself + if !(is_modifier || modifiers.alt || modifiers.ctrl || modifiers.logo) { + return; + } + } + _ => {} + } + + xstate.pressed_keys.push(code); + } else { + let mut removed = false; + xstate.pressed_keys.retain(|c| { + if *c == code { + removed = true; + false + } else { + true + } + }); + + if !removed { + // Don't forward released events, we don't have a record off. + return; + } + } + + tracing::trace!("Forwaring key {} {:?} to xwayland", code.raw() - 8, state); + for wl_keyboard in keyboard.client_keyboards(&xstate.client) { + wl_keyboard.key(serial.into(), time, code.raw() - 8, state.into()); + if xstate.last_modifier_state != Some(modifiers) { + xstate.last_modifier_state = Some(modifiers); + wl_keyboard.modifiers( + serial.into(), + modifiers.serialized.depressed, + modifiers.serialized.latched, + modifiers.serialized.locked, + modifiers.serialized.layout_effective, + ); + } + } + } + + #[profiling::function] + pub fn xwayland_notify_pointer_button_event( + &mut self, + button: u32, + state: ButtonState, + serial: Serial, + time: u32, + ) { + if !self.config.cosmic_conf.xwayland_eavesdropping.pointer { + return; + } + + let pointer = self.shell.read().seats.last_active().get_pointer().unwrap(); + + if self.xwayland_state.as_ref().is_none_or(|xstate| { + xstate + .xwm + .as_ref() + .is_none_or(|xwm| self.has_x_pointer_focus(xwm.id())) + }) { + return; + } + + let xstate = self.xwayland_state.as_mut().unwrap(); + if state == ButtonState::Pressed { + xstate.pressed_buttons.push(button); + } else { + let mut removed = false; + xstate.pressed_buttons.retain(|b| { + if *b == button { + removed = true; + false + } else { + true + } + }); + + if !removed { + // Don't forward released events, we don't have a record off. + // This can happen if `xwayland_reset_eavesdropping` was called in between + return; + } + } + + tracing::trace!("Forwaring ptr button {} {:?} to Xwayland", button, state); + for wl_pointer in pointer.client_pointers(&xstate.client) { + wl_pointer.button(serial.into(), time, button, state.into()); + } + } + + #[profiling::function] + pub fn update_x11_stacking_order(&mut self) { + let shell = self.shell.read(); + let seat = shell.seats.last_active(); + let active_output = seat.active_output(); + + if let Some(xwm) = self + .xwayland_state + .as_mut() + .and_then(|state| state.xwm.as_mut()) + { + // front to back, given that is how the workspace enumerates + let order = shell + .workspaces + .sets + .iter() + .filter(|(output, _)| *output == &active_output) + .chain( + shell + .workspaces + .sets + .iter() + .filter(|(output, _)| *output != &active_output), + ) + .flat_map(|(_, set)| { + set.sticky_layer + .mapped() + .flat_map(|mapped| { + let active = mapped.active_window(); + std::iter::once(active.clone()).chain( + mapped + .is_stack() + .then(move || { + mapped + .windows() + .map(|(s, _)| s) + .filter(move |s| s != &active) + }) + .into_iter() + .flatten(), + ) + }) + .chain( + set.workspaces + .iter() + .enumerate() + .filter(|(i, _)| *i == set.active) + .chain( + set.workspaces + .iter() + .enumerate() + .filter(|(i, _)| *i != set.active), + ) + .flat_map(|(_, workspace)| { + let focus_last = + workspace.focus_stack.get(seat).last().cloned(); + workspace + .get_fullscreen_surfaces() + .filter(|f| { + focus_last.as_ref().is_some_and(|t| t == &f.surface) + }) + .map(|f| f.surface.clone()) + .collect::>() + .into_iter() + .chain(workspace.mapped().flat_map(|mapped| { + let active = mapped.active_window(); + std::iter::once(active.clone()).chain( + mapped + .is_stack() + .then(move || { + mapped + .windows() + .map(|(s, _)| s) + .filter(move |s| s != &active) + }) + .into_iter() + .flatten(), + ) + })) + .chain( + workspace + .get_fullscreen_surfaces() + .filter(|f| { + focus_last + .as_ref() + .is_none_or(|t| t != &f.surface) + }) + .map(|f| f.surface.clone()) + .collect::>() + .into_iter(), + ) + .chain( + workspace + .minimized_windows + .iter() + .flat_map(|m| m.windows()), + ) + }), + ) + }) + .collect::>(); + + // we don't include the popup elements, which contain the OR windows, because we are not supposed to restack them. + // Which is also why we match upwards, to not disturb elements at the top. + // + // But this also means we need to match across all outputs and workspaces at once, to be sure nothing that shouldn't be on top of us is. + if let Err(err) = xwm.update_stacking_order_upwards(order.iter().rev()) { + warn!(wm_id = ?xwm.id(), ?err, "Failed to update Xwm stacking order."); + } + } + } + + pub fn update_xwayland_settings(&mut self) { + let new_scale = match self.config.cosmic_conf.descale_xwayland { + XwaylandDescaling::Disabled => 1., + XwaylandDescaling::Enabled => { + let shell = self.shell.read(); + shell + .outputs() + .map(|o| o.current_scale().integer_scale()) + .max() + .unwrap_or(1) as f64 + } + XwaylandDescaling::Fractional => { + let shell = self.shell.read(); + + if let Some(output) = shell.outputs().find(|o| o.config().xwayland_primary) { + output.current_scale().fractional_scale().max(1f64) + } else { + shell + .outputs() + .map(|o| o.current_scale().fractional_scale()) + .fold(1f64, |acc, val| acc.max(val)) + } + } + }; + let (_, cursor_size) = load_cursor_env(); + + // compare with current scale + if let Some(xwayland) = self.xwayland_state.as_mut() { + let geometries = if Some(new_scale) != self.xwayland_scale { + // backup geometries + let geometries = self + .shell + .read() + .mapped() + .flat_map(|m| m.windows().map(|(s, _)| s)) + .filter_map(|s| s.0.x11_surface().map(|x| (x.clone(), x.geometry()))) + .collect::>(); + + // update xorg dpi + if let Some(xwm) = xwayland.xwm.as_mut() { + let base = 96. * 1024.; + let dpi = new_scale * base; + let fractional = new_scale.fract(); + let integer = (new_scale - fractional).max(1.0); + let unscaled_dpi = base * (1.0 + fractional / integer); + if let Err(err) = xwm.set_xsettings( + [ + ("Xft/DPI".into(), (dpi.round() as i32).into()), + ( + "Gdk/UnscaledDPI".into(), + (unscaled_dpi.round() as i32).into(), + ), + ( + "Gdk/WindowScalingFactor".into(), + (new_scale.floor() as i32).into(), + ), + ( + "Gtk/CursorThemeSize".into(), + ((new_scale * cursor_size as f64).round() as i32).into(), + ), + ] + .into_iter(), + ) { + warn!(wm_id = ?xwm.id(), ?err, "Failed to update XSETTINGS."); + } + } + + Some(geometries) + } else { + None + }; + + if xwayland + .xrdb_thread + .send(( + cosmic::icon_theme::default(), + (new_scale * cursor_size as f64).round() as u32, + )) + .is_err() + { + warn!("xrdb thread died"); + } + + // update cursor + xwayland.reload_cursor(new_scale); + + if let Some(geometries) = geometries { + // update client scale + xwayland + .client + .get_data::() + .unwrap() + .compositor_state + .set_client_scale(new_scale); + + // update wl/xdg_outputs + for output in self.shell.read().outputs() { + output.change_current_state(None, None, None, None); + } + + // update geometries + for (surface, geometry) in geometries.iter() { + if let Err(err) = surface.configure(*geometry) { + warn!(?err, surface = ?surface.window_id(), "Failed to update geometry after scale change"); + } + } + self.update_x11_stacking_order(); + + self.xwayland_scale = Some(new_scale); + } + } + } + + pub fn update_xwayland_primary_output(&mut self) { + let mut xwayland_primary_output = None; + for output in self.output_configuration_state.outputs() { + if output.config().xwayland_primary { + xwayland_primary_output = Some(output); + break; + } + } + + if let Some(xstate) = self.xwayland_state.as_mut() + && let Some(xwm) = xstate.xwm.as_mut() + && let Err(err) = xwm.set_randr_primary_output(xwayland_primary_output.as_ref()) + { + warn!("Failed to set xwayland primary output: {}", err); + return; + }; + + self.output_configuration_state.update(); + } +} + +impl XwmHandler for State { + fn xwm_state(&mut self, _xwm: XwmId) -> &mut X11Wm { + self.common + .xwayland_state + .as_mut() + .and_then(|state| state.xwm.as_mut()) + .unwrap() + } + + fn new_window(&mut self, _xwm: XwmId, _window: X11Surface) {} + fn new_override_redirect_window(&mut self, _xwm: XwmId, _window: X11Surface) {} + fn destroyed_window(&mut self, _xwm: XwmId, _window: X11Surface) {} + + fn map_window_request(&mut self, _xwm: XwmId, window: X11Surface) { + if let Err(err) = window.set_mapped(true) { + warn!(?window, ?err, "Failed to send Xwayland Mapped-Event",); + } + + let mut shell = self.common.shell.write(); + let startup_id = window.startup_id(); + if shell.is_surface_mapped(&window) { + warn!( + ?window, + "Got map_request for already mapped window? Ignoring" + ); + return; + } + + let seat = shell.seats.last_active().clone(); + if let Some(context) = startup_id + .map(XdgActivationToken::from) + .and_then(|token| self.common.xdg_activation_state.data_for_token(&token)) + .and_then(|data| data.user_data.get::()) + { + shell.pending_activations.insert( + crate::shell::ActivationKey::X11(window.window_id()), + *context, + ); + } + let fullscreen = window.is_fullscreen().then(|| seat.active_output()); + let maximized = window.is_maximized(); + if let Some(pending) = shell + .pending_windows + .iter_mut() + .find(|w| w.surface == window) + { + pending.seat = seat; + pending.fullscreen = fullscreen; + pending.maximized = maximized; + } else { + let surface = CosmicSurface::from(window); + shell.pending_windows.push(PendingWindow { + surface, + seat, + fullscreen, + maximized, + sticky: false, + }) + } + } + + fn map_window_notify(&mut self, _xwm: XwmId, surface: X11Surface) { + let mut shell = self.common.shell.write(); + if let Some(window) = shell + .pending_windows + .iter() + .find(|pending| pending.surface.x11_surface() == Some(&surface)) + .map(|pending| pending.surface.clone()) + { + if let std::collections::hash_map::Entry::Vacant(e) = shell + .pending_activations + .entry(crate::shell::ActivationKey::X11(surface.window_id())) + && let Some(startup_id) = window.x11_surface().and_then(|x| x.startup_id()) + && let Some(context) = self + .common + .xdg_activation_state + .data_for_token(&XdgActivationToken::from(startup_id)) + .and_then(|data| data.user_data.get::()) + { + e.insert(*context); + } + let res = shell.map_window( + &window, + &mut self.common.toplevel_info_state, + &mut self.common.workspace_state, + &self.common.event_loop_handle, + ); + if let Some(target) = res { + let seat = shell.seats.last_active().clone(); + std::mem::drop(shell); + Shell::set_focus_on_x11_map(self, &target, &seat, false); + } + } + } + + fn mapped_override_redirect_window(&mut self, _xwm: XwmId, window: X11Surface) { + let mut shell = self.common.shell.write(); + if shell + .override_redirect_windows + .iter() + .any(|or| or == &window) + { + return; + } + shell.map_override_redirect(window) + } + + fn unmapped_window(&mut self, _xwm: XwmId, window: X11Surface) { + let mut shell = self.common.shell.write(); + if window.is_override_redirect() { + shell.override_redirect_windows.retain(|or| or != &window); + } else { + let seat = shell.seats.last_active().clone(); + if let Some(pending) = + shell.unmap_surface(&window, &seat, &mut self.common.toplevel_info_state) + && !shell + .pending_windows + .iter() + .any(|w| w.surface == pending.surface) + { + shell.pending_windows.push(pending); + } + } + + let outputs = if let Some(wl_surface) = window.wl_surface() { + shell + .visible_output_for_surface(&wl_surface) + .into_iter() + .cloned() + .collect::>() + } else { + shell.outputs().cloned().collect::>() + }; + for output in outputs.iter() { + shell.refresh_active_space(output); + } + + for output in outputs.into_iter() { + self.backend.schedule_render(&output); + } + } + + fn configure_request( + &mut self, + _xwm: XwmId, + window: X11Surface, + x: Option, + y: Option, + w: Option, + h: Option, + _reorder: Option, + ) { + // We only allow floating X11 windows to resize themselves. Nothing else + let shell = self.common.shell.read(); + + if window.is_fullscreen() { + let _ = window.configure(None); + return; + } + + if let Some(mapped) = shell + .element_for_surface(&window) + .filter(|mapped| !mapped.is_minimized()) + { + let current_geo = if let Some(workspace) = shell.space_for(mapped) { + workspace + .element_geometry(mapped) + .filter(|_| workspace.is_floating(&window)) + .map(|geo| geo.to_global(workspace.output())) + } else if let Some((output, set)) = shell + .workspaces + .sets + .iter() + .find(|(_, set)| set.sticky_layer.mapped().any(|m| m == mapped)) + { + Some( + set.sticky_layer + .element_geometry(mapped) + .unwrap() + .to_global(output), + ) + } else { + None + }; + + if let Some(current_geo) = current_geo { + let ssd_height = mapped.ssd_height(false).unwrap_or(0); + mapped.set_geometry(Rectangle::new( + current_geo.loc, + ( + w.map(|w| w as i32).unwrap_or(current_geo.size.w), + h.map(|h| h as i32 + ssd_height) + .unwrap_or(current_geo.size.h), + ) + .into(), + )) + } else { + let _ = window.configure(None); // ack and force old state + } + } else { + let mut current_geo = window.geometry(); + if let Some(x) = x { + current_geo.loc.x = x; + } + if let Some(y) = y { + current_geo.loc.y = y; + } + if let Some(w) = w { + current_geo.size.w = w as i32; + } + if let Some(h) = h { + current_geo.size.h = h as i32; + } + // the window is not yet mapped. Lets give it what it wants + let _ = window.configure(current_geo); + } + } + + fn configure_notify( + &mut self, + _xwm: XwmId, + window: X11Surface, + _geometry: Rectangle, + above: Option, + ) { + if window.is_override_redirect() { + let mut shell = self.common.shell.write(); + if let Some(id) = above { + let or_windows = &mut shell.override_redirect_windows; + if let Some(own_pos) = or_windows.iter().position(|or| or == &window) { + let compare_pos = or_windows + .iter() + .position(|or| or.window_id() == id) + .unwrap_or(0); + if compare_pos > own_pos { + let this = or_windows.remove(own_pos); + or_windows.insert(compare_pos, this); + } + } + } + + let geo = window.geometry().as_global(); + for (output, overlap) in shell.outputs().cloned().map(|o| { + let intersection = o.geometry().intersection(geo); + (o, intersection) + }) { + if let Some(overlap) = overlap { + window.output_enter(&output, overlap.as_logical()); + } else { + window.output_leave(&output); + } + } + } + } + + fn resize_request( + &mut self, + _xwm: XwmId, + window: X11Surface, + _button: u32, + resize_edge: smithay::xwayland::xwm::ResizeEdge, + ) { + if let Some(wl_surface) = window.wl_surface() { + let mut shell = self.common.shell.write(); + let seat = shell.seats.last_active().clone(); + if let Some((grab, focus)) = shell.resize_request( + &wl_surface, + &seat, + None, + resize_edge.into(), + self.common.config.cosmic_conf.edge_snap_threshold, + true, + ) { + std::mem::drop(shell); + if grab.is_touch_grab() { + seat.get_touch() + .unwrap() + .set_grab(self, grab, SERIAL_COUNTER.next_serial()) + } else { + seat.get_pointer().unwrap().set_grab( + self, + grab, + SERIAL_COUNTER.next_serial(), + focus, + ) + } + } + } + } + + fn move_request(&mut self, _xwm: XwmId, window: X11Surface, _button: u32) { + if let Some(wl_surface) = window.wl_surface() { + let mut shell = self.common.shell.write(); + let seat = shell.seats.last_active().clone(); + if let Some((grab, focus)) = shell.move_request( + &wl_surface, + &seat, + None, + ReleaseMode::NoMouseButtons, + false, + &self.common.config, + &self.common.event_loop_handle, + true, + ) { + std::mem::drop(shell); + if grab.is_touch_grab() { + seat.get_touch() + .unwrap() + .set_grab(self, grab, SERIAL_COUNTER.next_serial()) + } else { + seat.get_pointer().unwrap().set_grab( + self, + grab, + SERIAL_COUNTER.next_serial(), + focus, + ) + } + } + } + } + + fn maximize_request(&mut self, _xwm: XwmId, window: X11Surface) { + let mut shell = self.common.shell.write(); + if let Some(mapped) = shell.element_for_surface(&window).cloned() { + let seat = shell.seats.last_active().clone(); + shell.maximize_request(&mapped, &seat, true, &self.common.event_loop_handle); + } else if let Some(pending) = shell + .pending_windows + .iter_mut() + .find(|pending| pending.surface.x11_surface() == Some(&window)) + { + pending.maximized = true; + } + } + + fn unmaximize_request(&mut self, _xwm: XwmId, window: X11Surface) { + let mut shell = self.common.shell.write(); + if let Some(mapped) = shell.element_for_surface(&window).cloned() { + shell.unmaximize_request(&mapped); + } else if let Some(pending) = shell + .pending_windows + .iter_mut() + .find(|pending| pending.surface.x11_surface() == Some(&window)) + { + pending.maximized = false; + } + } + + fn minimize_request(&mut self, _xwm: XwmId, window: X11Surface) { + let mut shell = self.common.shell.write(); + shell.minimize_request(&window); + } + + fn unminimize_request(&mut self, _xwm: XwmId, window: X11Surface) { + let mut shell = self.common.shell.write(); + let seat = shell.seats.last_active().clone(); + shell.unminimize_request(&window, &seat, &self.common.event_loop_handle); + } + + fn fullscreen_request(&mut self, _xwm: XwmId, window: X11Surface) { + let mut shell = self.common.shell.write(); + let seat = shell.seats.last_active().clone(); + let output = window + .wl_surface() + .and_then(|surface| shell.visible_output_for_surface(&surface).cloned()) + .unwrap_or_else(|| seat.focused_or_active_output()); + + match shell.fullscreen_request(&window, output.clone(), &self.common.event_loop_handle) { + Some(target) => { + std::mem::drop(shell); + Shell::set_focus(self, Some(&target), &seat, None, true); + } + None => { + if let Some(pending) = shell + .pending_windows + .iter_mut() + .find(|pending| pending.surface.x11_surface() == Some(&window)) + { + pending.fullscreen = Some(output); + } + } + } + } + + fn unfullscreen_request(&mut self, _xwm: XwmId, window: X11Surface) { + let mut shell = self.common.shell.write(); + let seat = shell.seats.last_active().clone(); + let should_focus = seat + .get_keyboard() + .unwrap() + .current_focus() + .is_some_and(|target| { + if let KeyboardFocusTarget::Fullscreen(s) = target { + s == window + } else { + false + } + }); + + if let Some(target) = shell.unfullscreen_request(&window, &self.common.event_loop_handle) { + std::mem::drop(shell); + if should_focus { + Shell::set_focus(self, Some(&target), &seat, None, true); + } + } else if let Some(pending) = shell + .pending_windows + .iter_mut() + .find(|pending| pending.surface.x11_surface() == Some(&window)) + { + pending.fullscreen.take(); + } + } + + fn stick_request(&mut self, _xwm: XwmId, window: X11Surface) { + let mut shell = self.common.shell.write(); + if let Some(pending) = shell + .pending_windows + .iter_mut() + .find(|pending| pending.surface.x11_surface() == Some(&window)) + { + pending.sticky = true; + } + } + + fn unstick_request(&mut self, _xwm: XwmId, window: X11Surface) { + let mut shell = self.common.shell.write(); + if let Some(pending) = shell + .pending_windows + .iter_mut() + .find(|pending| pending.surface.x11_surface() == Some(&window)) + { + pending.sticky = false; + } + } + + fn active_window_request( + &mut self, + _xwm: XwmId, + window: X11Surface, + _timestamp: u32, + _currently_active_window: Option, + ) { + if let Some(surface) = window.wl_surface() { + self.activate_surface(&surface, None); + } + } + + fn send_selection( + &mut self, + _xwm: XwmId, + selection: SelectionTarget, + mime_type: String, + fd: OwnedFd, + ) { + let seat = self.common.shell.read().seats.last_active().clone(); + match selection { + SelectionTarget::Clipboard => { + if let Err(err) = request_data_device_client_selection(&seat, mime_type, fd) { + error!( + ?err, + "Failed to request current wayland clipboard for Xwayland.", + ); + } + } + SelectionTarget::Primary => { + if let Err(err) = request_primary_client_selection(&seat, mime_type, fd) { + error!( + ?err, + "Failed to request current wayland primary selection for Xwayland.", + ); + } + } + } + } + + fn allow_selection_access(&mut self, xwm: XwmId, _selection: SelectionTarget) -> bool { + self.common.has_x_keyboard_focus(xwm) + } + + fn new_selection(&mut self, xwm: XwmId, selection: SelectionTarget, mime_types: Vec) { + trace!(?selection, ?mime_types, "Got Selection from Xwayland",); + + let seat = self.common.shell.read().seats.last_active().clone(); + match selection { + SelectionTarget::Clipboard => { + set_data_device_selection(&self.common.display_handle, &seat, mime_types, xwm) + } + SelectionTarget::Primary => { + set_primary_selection(&self.common.display_handle, &seat, mime_types, xwm) + } + } + } + + fn cleared_selection(&mut self, xwm: XwmId, selection: SelectionTarget) { + let shell = self.common.shell.read(); + for seat in shell.seats.iter() { + match selection { + SelectionTarget::Clipboard => { + if current_data_device_selection_userdata(seat).as_deref() == Some(&xwm) { + clear_data_device_selection(&self.common.display_handle, seat) + } + } + SelectionTarget::Primary => { + if current_primary_selection_userdata(seat).as_deref() == Some(&xwm) { + clear_primary_selection(&self.common.display_handle, seat) + } + } + } + } + } + + fn randr_primary_output_change(&mut self, _xwm: XwmId, output_name: Option) { + for output in self.common.output_configuration_state.outputs() { + output.config_mut().xwayland_primary = + output_name.as_deref().is_some_and(|o| o == output.name()); + } + self.common.output_configuration_state.update(); + self.common.update_xwayland_settings(); + self.common + .config + .write_outputs(self.common.output_configuration_state.outputs()); + } + + fn disconnected(&mut self, _xwm: XwmId) { + let xwayland_state = self.common.xwayland_state.as_mut().unwrap(); + xwayland_state.xwm = None; + } +}