From 8df39da978807213eb1b11720ac8883c81459ea4 Mon Sep 17 00:00:00 2001 From: Aleksander Date: Sun, 3 Nov 2024 16:03:37 +0100 Subject: [PATCH] WayVR: Modify readme, Various improvements - Add "Quick setup" and images in readme - Implement `click_freeze_time_ms` - Automatically show hidden display if AppClick has been triggered --- contrib/wayvr/README.md | 90 ++++++++ contrib/wayvr/logo_traced.svg | 282 +++++++++++++++++++++++++ contrib/wayvr/watch_wayvr_example.yaml | 195 +++++++++++++++++ contrib/wayvr/wayvr_watch.jpg | Bin 0 -> 41621 bytes src/backend/openvr/mod.rs | 2 +- src/backend/openxr/mod.rs | 2 +- src/backend/wayvr/README.md | 37 ---- src/backend/wayvr/client.rs | 2 +- src/backend/wayvr/display.rs | 21 +- src/backend/wayvr/event_queue.rs | 34 +-- src/backend/wayvr/handle.rs | 2 +- src/backend/wayvr/mod.rs | 23 +- src/config_wayvr.rs | 17 +- src/overlays/keyboard.rs | 9 +- src/overlays/wayvr.rs | 184 ++++++++++------ src/state.rs | 24 ++- 16 files changed, 767 insertions(+), 157 deletions(-) create mode 100644 contrib/wayvr/README.md create mode 100644 contrib/wayvr/logo_traced.svg create mode 100644 contrib/wayvr/watch_wayvr_example.yaml create mode 100644 contrib/wayvr/wayvr_watch.jpg delete mode 100644 src/backend/wayvr/README.md diff --git a/contrib/wayvr/README.md b/contrib/wayvr/README.md new file mode 100644 index 0000000..864bb90 --- /dev/null +++ b/contrib/wayvr/README.md @@ -0,0 +1,90 @@ +

+ +

+ +**WayVR acts as a bridge between Wayland applications and wlx-overlay-s panels, allowing you to display your applications within a VR environment. Internally, WayVR utilizes Smithay to run a Wayland compositor.** + +# >> Quick setup << + +#### Configure your applications list + +Go to `src/res/wayvr.yaml` to configure your desired application list. This configuration file represents all currently available WayVR options. Feel free to adjust it to your liking. + +#### Add WayVR Launcher to your watch + +Copy `watch_wayvr_example.yaml` to `~/.config/wlxoverlay/watch.yaml`. This file contains pre-configured **WayVRLauncher** and **WayVRDisplayList** widget types. By default, the _default_catalog_ is used. + +That's it; you're all set! + +###### _Make sure you have `wayvr` feature enabled in Cargo.toml (enabled by default)_ + +![alt text](wayvr_watch.jpg) + +# Overview + +### Features + +- Display Wayland applications without GPU overhead (zero-copy via dma-buf) +- Mouse and keyboard input, with precision scrolling support +- Tested on AMD and Nvidia + +### Supported software + +- Basically all Qt applications (they work out of the box) +- Most XWayland applications via `cage` + +### XWayland + +WayVR does not have native XWayland support. You can run X11 applications (or these who require DISPLAY set) by wrapping them in a `cage` program, like so: + +```yaml +- name: "Xeyes" + target_display: "Disp1" + exec: "cage" + args: "xeyes -- -fg blue" +``` + +instead of: + +```yaml +- name: "Xeyes" + target_display: "Disp1" + exec: "xeyes" + args: "-fg blue" +``` + +in `wayvr.yaml` configuration file, in your desired catalog. + +### Launching external apps inside WayVR + +To launch your app externally: + +```sh +DISPLAY= WAYLAND_DISPLAY=wayland-$(cat $XDG_RUNTIME_DIR/wayvr.disp) yourapp +``` + +or (in the most cases): + +``` +DISPLAY= WAYLAND_DISPLAY=wayland-20 yourapp +``` + +Setting `DISPLAY` to an empty string forces various apps to use Wayland instead of X11. + +# Troubleshooting + +### My application doesn't launch but others do! + +Even though some applications support Wayland, some still check for the `DISPLAY` environment variable and an available X11 server, throwing an error. This can also be fixed by running `cage` on top of them. + +### Image corruption + +dma-buf textures may display various graphical glitches due to unsupported dma-buf tiling modifiers between GLES<->Vulkan on Radeon RDNA3 graphics cards. Current situation: https://gitlab.freedesktop.org/mesa/mesa/-/issues/11629). Nvidia should work out of the box, without any isues. Alternatively, you can run wlx-overlay-s with `LIBGL_ALWAYS_SOFTWARE=1` to mitigate that (only the Smithay compositor will run in software renderer mode, wlx will still be accelerated). + +### Floating windows + +Context menus are not functional in most cases yet, including drag & drop support. + +### Forced window shadows in GTK + +GNOME still insists on rendering client-side decorations instead of server-side ones. This results in all GTK applications looking odd due to additional window shadows. [Fix here, "Client-side decorations"](https://wiki.archlinux.org/title/GTK) diff --git a/contrib/wayvr/logo_traced.svg b/contrib/wayvr/logo_traced.svg new file mode 100644 index 0000000..115a78e --- /dev/null +++ b/contrib/wayvr/logo_traced.svg @@ -0,0 +1,282 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + WayVR logo + + + + + + + + + + + WayVR logo + + + + + diff --git a/contrib/wayvr/watch_wayvr_example.yaml b/contrib/wayvr/watch_wayvr_example.yaml new file mode 100644 index 0000000..10f56ef --- /dev/null +++ b/contrib/wayvr/watch_wayvr_example.yaml @@ -0,0 +1,195 @@ +# looking to make changes? +# drop me in ~/.config/wlxoverlay/watch.yaml +# + +width: 0.115 + +size: [400, 272] + +elements: + # background panel + - type: Panel + rect: [0, 30, 400, 130] + corner_radius: 20 + bg_color: "#24273a" + + - type: Button + rect: [2, 162, 26, 36] + corner_radius: 4 + font_size: 15 + bg_color: "#c6a0f6" + fg_color: "#24273a" + text: "C" + click_up: # destroy if exists, otherwise create + - type: Window + target: settings + action: ShowUi # only triggers if not exists + - type: Window + target: settings + action: Destroy # only triggers if exists since before current frame + + # Keyboard button + - type: Button + rect: [32, 162, 60, 36] + corner_radius: 4 + font_size: 15 + fg_color: "#24273a" + bg_color: "#a6da95" + text: Kbd + click_up: + - type: Overlay + target: "kbd" + action: ToggleVisible + long_click_up: + - type: Overlay + target: "kbd" + action: Reset + right_up: + - type: Overlay + target: "kbd" + action: ToggleImmovable + middle_up: + - type: Overlay + target: "kbd" + action: ToggleInteraction + scroll_up: + - type: Overlay + target: "kbd" + action: + Opacity: { delta: 0.025 } + scroll_down: + - type: Overlay + target: "kbd" + action: + Opacity: { delta: -0.025 } + + # bottom row, of keyboard + overlays + - type: OverlayList + rect: [94, 160, 306, 40] + corner_radius: 4 + font_size: 15 + fg_color: "#cad3f5" + bg_color: "#1e2030" + layout: Horizontal + click_up: ToggleVisible + long_click_up: Reset + right_up: ToggleImmovable + middle_up: ToggleInteraction + scroll_up: + Opacity: { delta: 0.025 } + scroll_down: + Opacity: { delta: -0.025 } + + - type: WayVRLauncher + rect: [0, 200, 400, 36] + corner_radius: 4 + font_size: 15 + fg_color: "#24273a" + bg_color: "#e590c4" + catalog_name: "default_catalog" + + - type: WayVRDisplayList + rect: [0, 236, 400, 36] + corner_radius: 4 + font_size: 15 + fg_color: "#24273a" + bg_color: "#ca68a4" + + # local clock + - type: Label + rect: [19, 90, 200, 50] + corner_radius: 4 + font_size: 46 # Use 32 for 12-hour time + fg_color: "#cad3f5" + source: Clock + format: "%H:%M" # 23:59 + #format: "%I:%M %p" # 11:59 PM + + # local date + - type: Label + rect: [20, 117, 200, 20] + corner_radius: 4 + font_size: 14 + fg_color: "#cad3f5" + source: Clock + format: "%x" # local date representation + + # local day-of-week + - type: Label + rect: [20, 137, 200, 50] + corner_radius: 4 + font_size: 14 + fg_color: "#cad3f5" + source: Clock + format: "%A" # Tuesday + #format: "%a" # Tue + + # alt clock 1 + - type: Label + rect: [210, 90, 200, 50] + corner_radius: 4 + font_size: 24 # Use 18 for 12-hour time + fg_color: "#8bd5ca" + source: Clock + timezone: "Asia/Tokyo" # change TZ1 here + format: "%H:%M" # 23:59 + #format: "%I:%M %p" # 11:59 PM + - type: Label + rect: [210, 60, 200, 50] + corner_radius: 4 + font_size: 14 + fg_color: "#8bd5ca" + source: Static + text: "Tokyo" # change TZ1 label here + + # alt clock 2 + - type: Label + rect: [210, 150, 200, 50] + corner_radius: 4 + font_size: 24 # Use 18 for 12-hour time + fg_color: "#b7bdf8" + source: Clock + timezone: "America/Chicago" # change TZ2 here + format: "%H:%M" # 23:59 + #format: "%I:%M %p" # 11:59 PM + - type: Label + rect: [210, 120, 200, 50] + corner_radius: 4 + font_size: 14 + fg_color: "#b7bdf8" + source: Static + text: "Chicago" # change TZ2 label here + + # batteries + - type: BatteryList + rect: [0, 5, 400, 30] + corner_radius: 4 + font_size: 16 + fg_color: "#8bd5ca" + fg_color_low: "#B06060" + fg_color_charging: "#6080A0" + num_devices: 9 + layout: Horizontal + low_threshold: 33 + + # volume buttons + - type: Button + rect: [315, 52, 70, 32] + corner_radius: 4 + font_size: 13 + fg_color: "#cad3f5" + bg_color: "#5b6078" + text: "Vol +" + click_down: + - type: Exec + command: ["pactl", "set-sink-volume", "@DEFAULT_SINK@", "+5%"] + - type: Button + rect: [315, 116, 70, 32] + corner_radius: 4 + font_size: 13 + fg_color: "#cad3f5" + bg_color: "#5b6078" + text: "Vol -" + click_down: + - type: Exec + command: ["pactl", "set-sink-volume", "@DEFAULT_SINK@", "-5%"] diff --git a/contrib/wayvr/wayvr_watch.jpg b/contrib/wayvr/wayvr_watch.jpg new file mode 100644 index 0000000000000000000000000000000000000000..43814eaff070e36eb91e527ad0fa26690e7e2417 GIT binary patch literal 41621 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

-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∓mOr78MDTtP5(`Y;mg3=ibcqf%-my!@5| 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 Urn70h)^9m=xlOus`Y$>E*(+=-^8f$< literal 0 HcmV?d00001 diff --git a/src/backend/openvr/mod.rs b/src/backend/openvr/mod.rs index 2c1b868..95e01a5 100644 --- a/src/backend/openvr/mod.rs +++ b/src/backend/openvr/mod.rs @@ -346,7 +346,7 @@ pub fn openvr_run(running: Arc, show_by_default: bool) -> Result<(), #[cfg(feature = "wayvr")] if let Some(wayvr) = &state.wayvr { - wayvr.borrow_mut().tick_finish()?; + wayvr.borrow_mut().state.tick_finish()?; } // chaperone diff --git a/src/backend/openxr/mod.rs b/src/backend/openxr/mod.rs index f2cd591..d6e04cd 100644 --- a/src/backend/openxr/mod.rs +++ b/src/backend/openxr/mod.rs @@ -402,7 +402,7 @@ pub fn openxr_run(running: Arc, show_by_default: bool) -> Result<(), #[cfg(feature = "wayvr")] if let Some(wayvr) = &app_state.wayvr { - wayvr.borrow_mut().tick_finish()?; + wayvr.borrow_mut().state.tick_finish()?; } command_buffer.build_and_execute_now()?; diff --git a/src/backend/wayvr/README.md b/src/backend/wayvr/README.md deleted file mode 100644 index fd72057..0000000 --- a/src/backend/wayvr/README.md +++ /dev/null @@ -1,37 +0,0 @@ -**WayVR acts as a bridge between Wayland applications and wlx-overlay-s panels, allowing you to display your applications within a VR environment. Internally, WayVR utilizes Smithay to run a Wayland compositor.** - -# Features - -- Display Wayland applications without GPU overhead (zero-copy via dma-buf) -- Mouse input -- Precision scrolling support -- XWayland "support" via `cage` - -# Supported hardware - -### Confirmed working GPUs - -- Navi 32 family: AMD Radeon RX 7800 XT **\*** -- Navi 23 family: AMD Radeon RX 6600 XT -- Navi 21 family: AMD Radeon Pro W6800, AMD Radeon RX 6800 XT -- Nvidia GTX 16 Series -- _Your GPU here? (Let us know!)_ - -**\*** - With dmabuf modifier mitigation (probably Mesa bug) - -# Supported software - -- Basically all Qt applications (they work out of the box) -- Most XWayland applications via `cage` - -# Known issues - -- Context menus are not functional in most cases yet - -- Due to unknown circumstances, dma-buf textures may display various graphical glitches due to invalid dma-buf tiling modifier. Please report your GPU model when filing an issue. Alternatively, you can run wlx-overlay-s with `LIBGL_ALWAYS_SOFTWARE=1` to mitigate that (only the Smithay compositor will run in software renderer mode, wlx will still be accelerated). - -- ~~Potential data race in the rendering pipeline - A texture could be displayed during the clear-and-blit process in the compositor, causing minor artifacts (no fence sync support yet).~~ happens on all overlays on a simulated Monado driver - -- Even though some applications support Wayland, some still check for the `DISPLAY` environment variable and an available X11 server, throwing an error. This can be fixed by running `cage`. - -- GNOME still insists on rendering client-side decorations instead of server-side ones. This results in all GTK applications looking odd due to additional window shadows. [Fix here, "Client-side decorations"](https://wiki.archlinux.org/title/GTK) diff --git a/src/backend/wayvr/client.rs b/src/backend/wayvr/client.rs index 6bb5888..3493e99 100644 --- a/src/backend/wayvr/client.rs +++ b/src/backend/wayvr/client.rs @@ -201,7 +201,7 @@ fn export_display_number(display_num: u32) -> anyhow::Result<()> { .map(PathBuf::from) .unwrap_or_else(|_| PathBuf::from("/tmp")); path.push("wayvr.disp"); - std::fs::write(path, format!("{}\n", display_num)).unwrap(); + std::fs::write(path, format!("{}\n", display_num))?; Ok(()) } diff --git a/src/backend/wayvr/display.rs b/src/backend/wayvr/display.rs index 986cd0d..912f97e 100644 --- a/src/backend/wayvr/display.rs +++ b/src/backend/wayvr/display.rs @@ -19,7 +19,7 @@ use crate::{backend::overlay::OverlayID, gen_id}; use super::{ client::WayVRManager, comp::send_frames_surface_tree, egl_data, event_queue::SyncEventQueue, - process, smithay_wrapper, window, + process, smithay_wrapper, time, window, }; fn generate_auth_key() -> String { @@ -54,6 +54,7 @@ pub struct Display { wm: Rc>, pub displayed_windows: Vec, wayland_env: super::WaylandEnv, + last_pressed_time_ms: u64, // Render data stuff gles_texture: GlesTexture, // TODO: drop texture @@ -121,6 +122,7 @@ impl Display { primary, overlay_id: None, tasks: SyncEventQueue::new(), + last_pressed_time_ms: 0, }) } @@ -250,7 +252,18 @@ impl Display { } } - pub fn send_mouse_move(&self, manager: &mut WayVRManager, x: u32, y: u32) { + pub fn send_mouse_move( + &self, + config: &super::Config, + manager: &mut WayVRManager, + x: u32, + y: u32, + ) { + let current_ms = time::get_millis(); + if self.last_pressed_time_ms + config.click_freeze_time_ms as u64 > current_ms { + return; + } + if let Some(window_handle) = self.get_hovered_window(x, y) { let wm = self.wm.borrow(); if let Some(window) = wm.windows.get(&window_handle) { @@ -283,10 +296,12 @@ impl Display { } } - pub fn send_mouse_down(&self, manager: &mut WayVRManager, index: super::MouseIndex) { + pub fn send_mouse_down(&mut self, manager: &mut WayVRManager, index: super::MouseIndex) { // Change keyboard focus to pressed window let loc = manager.seat_pointer.current_location(); + self.last_pressed_time_ms = time::get_millis(); + if let Some(window_handle) = self.get_hovered_window(loc.x.max(0.0) as u32, loc.y.max(0.0) as u32) { diff --git a/src/backend/wayvr/event_queue.rs b/src/backend/wayvr/event_queue.rs index 40a940b..6f6df55 100644 --- a/src/backend/wayvr/event_queue.rs +++ b/src/backend/wayvr/event_queue.rs @@ -3,30 +3,30 @@ use std::{cell::RefCell, collections::VecDeque, rc::Rc}; struct Data { - queue: VecDeque, + queue: VecDeque, } #[derive(Clone)] pub struct SyncEventQueue { - data: Rc>>, + data: Rc>>, } impl SyncEventQueue { - pub fn new() -> Self { - Self { - data: Rc::new(RefCell::new(Data { - queue: Default::default(), - })), - } - } + pub fn new() -> Self { + Self { + data: Rc::new(RefCell::new(Data { + queue: Default::default(), + })), + } + } - pub fn send(&self, message: DataType) { - let mut data = self.data.borrow_mut(); - data.queue.push_back(message); - } + pub fn send(&self, message: DataType) { + let mut data = self.data.borrow_mut(); + data.queue.push_back(message); + } - pub fn read(&self) -> Option { - let mut data = self.data.borrow_mut(); - data.queue.pop_front() - } + pub fn read(&self) -> Option { + let mut data = self.data.borrow_mut(); + data.queue.pop_front() + } } diff --git a/src/backend/wayvr/handle.rs b/src/backend/wayvr/handle.rs index 2ca7989..2947e3e 100644 --- a/src/backend/wayvr/handle.rs +++ b/src/backend/wayvr/handle.rs @@ -20,7 +20,7 @@ macro_rules! gen_id { } //ThingHandle - #[derive(Default, Clone, Copy, PartialEq)] + #[derive(Default, Clone, Copy, PartialEq, Hash, Eq)] pub struct $handle_name { idx: u32, generation: u64, diff --git a/src/backend/wayvr/mod.rs b/src/backend/wayvr/mod.rs index 485343a..c2dcf90 100644 --- a/src/backend/wayvr/mod.rs +++ b/src/backend/wayvr/mod.rs @@ -65,6 +65,12 @@ pub enum WayVRTask { ProcessTerminationRequest(process::ProcessHandle), } +pub struct Config { + pub click_freeze_time_ms: u32, + pub keyboard_repeat_delay_ms: u32, + pub keyboard_repeat_rate: u32, +} + #[allow(dead_code)] pub struct WayVR { time_start: u64, @@ -74,6 +80,7 @@ pub struct WayVR { wm: Rc>, egl_data: Rc, pub processes: process::ProcessVec, + config: Config, tasks: SyncEventQueue, } @@ -89,7 +96,7 @@ pub enum TickResult { } impl WayVR { - pub fn new() -> anyhow::Result { + pub fn new(config: Config) -> anyhow::Result { log::info!("Initializing WayVR"); let display: wayland_server::Display = wayland_server::Display::new()?; let dh = display.handle(); @@ -100,8 +107,11 @@ impl WayVR { let data_device = DataDeviceState::new::(&dh); let mut seat = seat_state.new_wl_seat(&dh, "wayvr"); - // TODO: Keyboard repeat delay and rate? - let seat_keyboard = seat.add_keyboard(Default::default(), 100, 100)?; + let seat_keyboard = seat.add_keyboard( + Default::default(), + config.keyboard_repeat_delay_ms as i32, + config.keyboard_repeat_rate as i32, + )?; let seat_pointer = seat.add_pointer(); let tasks = SyncEventQueue::new(); @@ -131,6 +141,7 @@ impl WayVR { egl_data: Rc::new(egl_data), wm: Rc::new(RefCell::new(window::WindowManager::new())), tasks, + config, }) } @@ -222,7 +233,7 @@ impl WayVR { } } else { log::error!( - "WayVR window creation failed: Unexpected process PID {}. It wasn't registered before.", + "WayVR window creation failed: Unexpected process ID {}. It wasn't registered before.", client.pid ); } @@ -255,12 +266,12 @@ impl WayVR { pub fn send_mouse_move(&mut self, display: display::DisplayHandle, x: u32, y: u32) { if let Some(display) = self.displays.get(&display) { - display.send_mouse_move(&mut self.manager, x, y); + display.send_mouse_move(&self.config, &mut self.manager, x, y); } } pub fn send_mouse_down(&mut self, display: display::DisplayHandle, index: MouseIndex) { - if let Some(display) = self.displays.get(&display) { + if let Some(display) = self.displays.get_mut(&display) { display.send_mouse_down(&mut self.manager, index); } } diff --git a/src/config_wayvr.rs b/src/config_wayvr.rs index 9847619..ef11843 100644 --- a/src/config_wayvr.rs +++ b/src/config_wayvr.rs @@ -17,7 +17,7 @@ use crate::{ wayvr, }, config::{load_known_yaml, ConfigType}, - overlays::wayvr::WayVRAction, + overlays::wayvr::{WayVRAction, WayVRState}, }; // Flat version of RelativeTo @@ -106,10 +106,19 @@ impl WayVRConfig { None } + pub fn get_wayvr_config(config: &crate::config::GeneralConfig) -> wayvr::Config { + wayvr::Config { + click_freeze_time_ms: config.click_freeze_time_ms, + keyboard_repeat_delay_ms: 200, + keyboard_repeat_rate: 50, + } + } + pub fn post_load( &self, + config: &crate::config::GeneralConfig, tasks: &mut TaskContainer, - ) -> anyhow::Result>>> { + ) -> anyhow::Result>>> { let primary_count = self .displays .iter() @@ -137,7 +146,9 @@ impl WayVRConfig { if self.run_compositor_at_start { // Start Wayland server instantly - Ok(Some(Rc::new(RefCell::new(wayvr::WayVR::new()?)))) + Ok(Some(Rc::new(RefCell::new(WayVRState::new( + Self::get_wayvr_config(config), + )?)))) } else { // Lazy-init WayVR later if the user requested Ok(None) diff --git a/src/overlays/keyboard.rs b/src/overlays/keyboard.rs index e1eb769..61a6e08 100644 --- a/src/overlays/keyboard.rs +++ b/src/overlays/keyboard.rs @@ -32,12 +32,6 @@ const AUTO_RELEASE_MODS: [KeyModifier; 5] = [SHIFT, CTRL, ALT, SUPER, META]; pub const KEYBOARD_NAME: &str = "kbd"; fn send_key(app: &mut AppState, key: VirtualKey, down: bool) { - log::info!( - "Sending key {:?} to {:?} (down: {})", - key, - app.keyboard_focus, - down - ); match app.keyboard_focus { KeyboardFocus::PhysicalScreen => { app.hid_provider.send_key(key, down); @@ -46,7 +40,7 @@ fn send_key(app: &mut AppState, key: VirtualKey, down: bool) { { #[cfg(feature = "wayvr")] if let Some(wayvr) = &app.wayvr { - wayvr.borrow_mut().send_key(key as u32, down); + wayvr.borrow_mut().state.send_key(key as u32, down); } } } @@ -241,7 +235,6 @@ fn key_press( app.hid_provider.set_modifiers(data.modifiers); - send_key(app, *vk, true); *pressed = true; } diff --git a/src/overlays/wayvr.rs b/src/overlays/wayvr.rs index 7b25a04..d757f55 100644 --- a/src/overlays/wayvr.rs +++ b/src/overlays/wayvr.rs @@ -1,6 +1,6 @@ use glam::{vec3a, Affine2, Vec3, Vec3A}; use serde::Deserialize; -use std::{cell::RefCell, rc::Rc, sync::Arc}; +use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc}; use vulkano::image::SubresourceLayout; use wlx_capture::frame::{DmabufFrame, FourCC, FrameFormat, FramePlane}; @@ -8,7 +8,10 @@ use crate::{ backend::{ common::OverlayContainer, input::{self, InteractionHandler}, - overlay::{ui_transform, OverlayData, OverlayRenderer, OverlayState, SplitOverlayBackend}, + overlay::{ + ui_transform, OverlayData, OverlayID, OverlayRenderer, OverlayState, + SplitOverlayBackend, + }, wayvr::{self, display, WayVR}, }, graphics::WlxGraphics, @@ -16,13 +19,13 @@ use crate::{ }; pub struct WayVRContext { - wayvr: Rc>, + wayvr: Rc>, display: wayvr::display::DisplayHandle, } impl WayVRContext { pub fn new( - wvr: Rc>, + wvr: Rc>, display: wayvr::display::DisplayHandle, ) -> anyhow::Result { Ok(Self { @@ -32,6 +35,20 @@ impl WayVRContext { } } +pub struct WayVRState { + pub display_handle_map: HashMap, + pub state: WayVR, +} + +impl WayVRState { + pub fn new(config: wayvr::Config) -> anyhow::Result { + Ok(Self { + display_handle_map: Default::default(), + state: WayVR::new(config)?, + }) + } +} + pub struct WayVRInteractionHandler { context: Rc>, mouse_transform: Affine2, @@ -54,7 +71,7 @@ impl InteractionHandler for WayVRInteractionHandler { ) -> Option { let ctx = self.context.borrow(); - let mut wayvr = ctx.wayvr.borrow_mut(); + let wayvr = &mut ctx.wayvr.borrow_mut().state; if let Some(disp) = wayvr.displays.get(&ctx.display) { let pos = self.mouse_transform.transform_point2(hit.uv); let x = ((pos.x * disp.width as f32) as i32).max(0); @@ -82,7 +99,7 @@ impl InteractionHandler for WayVRInteractionHandler { } } { let ctx = self.context.borrow(); - let mut wayvr = ctx.wayvr.borrow_mut(); + let wayvr = &mut ctx.wayvr.borrow_mut().state; if pressed { wayvr.send_mouse_down(ctx.display, index); } else { @@ -93,7 +110,10 @@ impl InteractionHandler for WayVRInteractionHandler { fn on_scroll(&mut self, _app: &mut state::AppState, _hit: &input::PointerHit, delta: f32) { let ctx = self.context.borrow(); - ctx.wayvr.borrow_mut().send_mouse_scroll(ctx.display, delta); + ctx.wayvr + .borrow_mut() + .state + .send_mouse_scroll(ctx.display, delta); } } @@ -107,7 +127,7 @@ pub struct WayVRRenderer { impl WayVRRenderer { pub fn new( app: &mut state::AppState, - wvr: Rc>, + wvr: Rc>, display: wayvr::display::DisplayHandle, ) -> anyhow::Result { Ok(Self { @@ -121,7 +141,7 @@ impl WayVRRenderer { fn get_or_create_display( app: &mut AppState, - wayvr: &mut WayVR, + wayvr: &mut WayVRState, disp_name: &str, ) -> anyhow::Result<(display::DisplayHandle, Option>)> where @@ -129,57 +149,62 @@ where { let created_overlay: Option>; - let disp_handle = if let Some(disp) = WayVR::get_display_by_name(&wayvr.displays, disp_name) { - created_overlay = None; - disp - } else { - let conf_display = app - .session - .wayvr_config - .get_display(disp_name) - .ok_or(anyhow::anyhow!( - "Cannot find display named \"{}\"", - disp_name - ))? - .clone(); - - let disp_handle = wayvr.create_display( - conf_display.width, - conf_display.height, - disp_name, - conf_display.primary.unwrap_or(false), - )?; - - let mut overlay = create_wayvr_display_overlay::( - app, - conf_display.width, - conf_display.height, - disp_handle, - conf_display.scale.unwrap_or(1.0), - )?; - - if let Some(attach_to) = &conf_display.attach_to { - overlay.state.relative_to = attach_to.get_relative_to(); - } + let disp_handle = + if let Some(disp) = WayVR::get_display_by_name(&wayvr.state.displays, disp_name) { + created_overlay = None; + disp + } else { + let conf_display = app + .session + .wayvr_config + .get_display(disp_name) + .ok_or(anyhow::anyhow!( + "Cannot find display named \"{}\"", + disp_name + ))? + .clone(); + + let disp_handle = wayvr.state.create_display( + conf_display.width, + conf_display.height, + disp_name, + conf_display.primary.unwrap_or(false), + )?; + + let mut overlay = create_wayvr_display_overlay::( + app, + conf_display.width, + conf_display.height, + disp_handle, + conf_display.scale.unwrap_or(1.0), + )?; + + wayvr + .display_handle_map + .insert(disp_handle, overlay.state.id); + + if let Some(attach_to) = &conf_display.attach_to { + overlay.state.relative_to = attach_to.get_relative_to(); + } - if let Some(rot) = &conf_display.rotation { - overlay.state.spawn_rotation = glam::Quat::from_axis_angle( - Vec3::from_slice(&rot.axis), - f32::to_radians(rot.angle), - ); - } + if let Some(rot) = &conf_display.rotation { + overlay.state.spawn_rotation = glam::Quat::from_axis_angle( + Vec3::from_slice(&rot.axis), + f32::to_radians(rot.angle), + ); + } - if let Some(pos) = &conf_display.pos { - overlay.state.spawn_point = Vec3A::from_slice(pos); - } + if let Some(pos) = &conf_display.pos { + overlay.state.spawn_point = Vec3A::from_slice(pos); + } - let display = wayvr.displays.get_mut(&disp_handle).unwrap(); // Never fails - display.overlay_id = Some(overlay.state.id); + let display = wayvr.state.displays.get_mut(&disp_handle).unwrap(); // Never fails + display.overlay_id = Some(overlay.state.id); - created_overlay = Some(overlay); + created_overlay = Some(overlay); - disp_handle - }; + disp_handle + }; Ok((disp_handle, created_overlay)) } @@ -189,7 +214,7 @@ where O: Default, { if let Some(wayvr) = app.wayvr.clone() { - let res = wayvr.borrow_mut().tick_events()?; + let res = wayvr.borrow_mut().state.tick_events()?; for result in res { match result { @@ -214,9 +239,9 @@ where let (disp_handle, created_overlay) = get_or_create_display::(app, &mut wayvr, &disp_name)?; - wayvr.add_external_process(disp_handle, req.pid); + wayvr.state.add_external_process(disp_handle, req.pid); - wayvr.manager.add_client(wayvr::client::WayVRClient { + wayvr.state.manager.add_client(wayvr::client::WayVRClient { client: req.client, display_handle: disp_handle, pid: req.pid, @@ -245,7 +270,7 @@ impl WayVRRenderer { let ctx = self.context.borrow_mut(); let wayvr = ctx.wayvr.borrow_mut(); - if let Some(disp) = wayvr.displays.get(&ctx.display) { + if let Some(disp) = wayvr.state.displays.get(&ctx.display) { let frame = DmabufFrame { format: FrameFormat { width: disp.width, @@ -293,14 +318,14 @@ impl OverlayRenderer for WayVRRenderer { fn pause(&mut self, _app: &mut state::AppState) -> anyhow::Result<()> { let ctx = self.context.borrow_mut(); - let mut wayvr = ctx.wayvr.borrow_mut(); + let wayvr = &mut ctx.wayvr.borrow_mut().state; wayvr.set_display_visible(ctx.display, false); Ok(()) } fn resume(&mut self, _app: &mut state::AppState) -> anyhow::Result<()> { let ctx = self.context.borrow_mut(); - let mut wayvr = ctx.wayvr.borrow_mut(); + let wayvr = &mut ctx.wayvr.borrow_mut().state; wayvr.set_display_visible(ctx.display, true); Ok(()) } @@ -309,9 +334,10 @@ impl OverlayRenderer for WayVRRenderer { let ctx = self.context.borrow(); let mut wayvr = ctx.wayvr.borrow_mut(); - wayvr.tick_display(ctx.display)?; + wayvr.state.tick_display(ctx.display)?; let dmabuf_data = wayvr + .state .get_dmabuf_data(ctx.display) .ok_or(anyhow::anyhow!("Failed to fetch dmabuf data"))? .clone(); @@ -392,8 +418,24 @@ pub enum WayVRAction { }, } +fn show_display(wayvr: &mut WayVRState, overlays: &mut OverlayContainer, display_name: &str) +where + O: Default, +{ + if let Some(display) = WayVR::get_display_by_name(&wayvr.state.displays, display_name) { + if let Some(overlay_id) = wayvr.display_handle_map.get(&display) { + if let Some(overlay) = overlays.mut_by_id(*overlay_id) { + overlay.state.want_visible = true; + } + } + + wayvr.state.set_display_visible(display, true); + } +} + fn action_app_click( app: &mut AppState, + overlays: &mut OverlayContainer, catalog_name: &Arc, app_name: &Arc, ) -> anyhow::Result>> @@ -437,13 +479,19 @@ where // Terminate existing process if required if let Some(process_handle) = - wayvr.process_query(disp_handle, &app_entry.exec, &args_vec, &env_vec) + wayvr + .state + .process_query(disp_handle, &app_entry.exec, &args_vec, &env_vec) { // Terminate process - wayvr.terminate_process(process_handle); + wayvr.state.terminate_process(process_handle); } else { // Spawn process - wayvr.spawn_process(disp_handle, &app_entry.exec, &args_vec, &env_vec)?; + wayvr + .state + .spawn_process(disp_handle, &app_entry.exec, &args_vec, &env_vec)?; + + show_display(&mut wayvr, overlays, &app_entry.target_display.as_str()); } Ok(created_overlay) } else { @@ -463,8 +511,8 @@ where let wayvr = app.get_wayvr()?; let mut wayvr = wayvr.borrow_mut(); - if let Some(handle) = WayVR::get_display_by_name(&wayvr.displays, display_name) { - if let Some(display) = wayvr.displays.get_mut(&handle) { + if let Some(handle) = WayVR::get_display_by_name(&wayvr.state.displays, display_name) { + if let Some(display) = wayvr.state.displays.get_mut(&handle) { if let Some(overlay_id) = display.overlay_id { if let Some(overlay) = overlays.mut_by_id(overlay_id) { match action { @@ -495,7 +543,7 @@ where catalog_name, app_name, } => { - match action_app_click(app, catalog_name, app_name) { + match action_app_click(app, overlays, catalog_name, app_name) { Ok(res) => { if let Some(created_overlay) = res { overlays.add(created_overlay); diff --git a/src/state.rs b/src/state.rs index e789590..de8698f 100644 --- a/src/state.rs +++ b/src/state.rs @@ -8,13 +8,11 @@ use std::{io::Cursor, sync::Arc}; use vulkano::image::view::ImageView; #[cfg(feature = "wayvr")] -use std::{cell::RefCell, rc::Rc}; - -#[cfg(feature = "wayvr")] -use crate::backend::wayvr::WayVR; - -#[cfg(feature = "wayvr")] -use crate::config_wayvr::{self, WayVRConfig}; +use { + crate::config_wayvr::{self, WayVRConfig}, + crate::overlays::wayvr::WayVRState, + std::{cell::RefCell, rc::Rc}, +}; use crate::{ backend::{input::InputState, overlay::OverlayID, task::TaskContainer}, @@ -52,7 +50,7 @@ pub struct AppState { pub keyboard_focus: KeyboardFocus, #[cfg(feature = "wayvr")] - pub wayvr: Option>>, // Dynamically created if requested + pub wayvr: Option>>, // Dynamically created if requested } impl AppState { @@ -100,7 +98,9 @@ impl AppState { let session = AppSession::load(); #[cfg(feature = "wayvr")] - let wayvr = session.wayvr_config.post_load(&mut tasks)?; + let wayvr = session + .wayvr_config + .post_load(&session.config, &mut tasks)?; Ok(AppState { fc: FontCache::new(session.config.primary_font.clone())?, @@ -122,11 +122,13 @@ impl AppState { #[cfg(feature = "wayvr")] #[allow(dead_code)] - pub fn get_wayvr(&mut self) -> anyhow::Result>> { + pub fn get_wayvr(&mut self) -> anyhow::Result>> { if let Some(wvr) = &self.wayvr { Ok(wvr.clone()) } else { - let wayvr = Rc::new(RefCell::new(WayVR::new()?)); + let wayvr = Rc::new(RefCell::new(WayVRState::new( + WayVRConfig::get_wayvr_config(&self.session.config), + )?)); self.wayvr = Some(wayvr.clone()); Ok(wayvr) }