WayVR: Layouting system, state changing feedback, process userdata, various IPC changes
WayVR: - Layouting system (tiled and stacked) IPC: - Implemented routes: `WvrDisplaySetWindowLayout`, `WvrDisplayWindowList`, `WvrWindowSetVisible`, `WvrProcessGet`, - Packet broadcasting - State change feedback to the client (displays/windows/processes)
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -4582,7 +4582,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "wayvr_ipc"
|
name = "wayvr_ipc"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/olekolek1000/wayvr-ipc.git?rev=fe2e8be04c7b86adcf972be7ebe46961bd881f35#fe2e8be04c7b86adcf972be7ebe46961bd881f35"
|
source = "git+https://github.com/olekolek1000/wayvr-ipc.git?rev=3c411d09ba1bba2609288e29739c0f1ec736b012#3c411d09ba1bba2609288e29739c0f1ec736b012"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ wayland-client = { version = "0.31.6", optional = true }
|
|||||||
wayland-egl = { version = "0.32.4", optional = true }
|
wayland-egl = { version = "0.32.4", optional = true }
|
||||||
interprocess = { version = "2.2.2", optional = true }
|
interprocess = { version = "2.2.2", optional = true }
|
||||||
bytes = { version = "1.9.0", optional = true }
|
bytes = { version = "1.9.0", optional = true }
|
||||||
wayvr_ipc = { git = "https://github.com/olekolek1000/wayvr-ipc.git", rev = "fe2e8be04c7b86adcf972be7ebe46961bd881f35", default-features = false, optional = true }
|
wayvr_ipc = { git = "https://github.com/olekolek1000/wayvr-ipc.git", rev = "3c411d09ba1bba2609288e29739c0f1ec736b012", default-features = false, optional = true }
|
||||||
################################
|
################################
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
|||||||
@@ -338,7 +338,11 @@ pub fn openvr_run(running: Arc<AtomicBool>, show_by_default: bool) -> Result<(),
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "wayvr")]
|
#[cfg(feature = "wayvr")]
|
||||||
crate::overlays::wayvr::tick_events::<OpenVrOverlayData>(&mut state, &mut overlays)?;
|
if let Err(e) =
|
||||||
|
crate::overlays::wayvr::tick_events::<OpenVrOverlayData>(&mut state, &mut overlays)
|
||||||
|
{
|
||||||
|
log::error!("WayVR tick_events failed: {:?}", e);
|
||||||
|
}
|
||||||
|
|
||||||
log::trace!("Rendering frame");
|
log::trace!("Rendering frame");
|
||||||
|
|
||||||
|
|||||||
@@ -386,7 +386,11 @@ pub fn openxr_run(running: Arc<AtomicBool>, show_by_default: bool) -> Result<(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "wayvr")]
|
#[cfg(feature = "wayvr")]
|
||||||
crate::overlays::wayvr::tick_events::<OpenXrOverlayData>(&mut app_state, &mut overlays)?;
|
if let Err(e) =
|
||||||
|
crate::overlays::wayvr::tick_events::<OpenXrOverlayData>(&mut app_state, &mut overlays)
|
||||||
|
{
|
||||||
|
log::error!("WayVR tick_events failed: {:?}", e);
|
||||||
|
}
|
||||||
|
|
||||||
for o in overlays.iter_mut() {
|
for o in overlays.iter_mut() {
|
||||||
if !o.state.want_visible {
|
if !o.state.want_visible {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ fn generate_auth_key() -> String {
|
|||||||
pub struct DisplayWindow {
|
pub struct DisplayWindow {
|
||||||
pub window_handle: window::WindowHandle,
|
pub window_handle: window::WindowHandle,
|
||||||
pub toplevel: ToplevelSurface,
|
pub toplevel: ToplevelSurface,
|
||||||
process_handle: process::ProcessHandle,
|
pub process_handle: process::ProcessHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SpawnProcessResult {
|
pub struct SpawnProcessResult {
|
||||||
@@ -57,6 +57,7 @@ pub struct Display {
|
|||||||
pub height: u16,
|
pub height: u16,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub visible: bool,
|
pub visible: bool,
|
||||||
|
pub layout: packet_server::WvrDisplayWindowLayout,
|
||||||
pub overlay_id: Option<OverlayID>,
|
pub overlay_id: Option<OverlayID>,
|
||||||
pub wants_redraw: bool,
|
pub wants_redraw: bool,
|
||||||
pub primary: bool,
|
pub primary: bool,
|
||||||
@@ -84,29 +85,31 @@ impl Drop for Display {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct DisplayInitParams<'a> {
|
||||||
|
pub wm: Rc<RefCell<window::WindowManager>>,
|
||||||
|
pub renderer: &'a mut GlesRenderer,
|
||||||
|
pub egl_data: Rc<egl_data::EGLData>,
|
||||||
|
pub wayland_env: super::WaylandEnv,
|
||||||
|
pub width: u16,
|
||||||
|
pub height: u16,
|
||||||
|
pub name: &'a str,
|
||||||
|
pub primary: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl Display {
|
impl Display {
|
||||||
pub fn new(
|
pub fn new(params: DisplayInitParams) -> anyhow::Result<Self> {
|
||||||
wm: Rc<RefCell<window::WindowManager>>,
|
if params.width > MAX_DISPLAY_SIZE {
|
||||||
renderer: &mut GlesRenderer,
|
|
||||||
egl_data: Rc<egl_data::EGLData>,
|
|
||||||
wayland_env: super::WaylandEnv,
|
|
||||||
width: u16,
|
|
||||||
height: u16,
|
|
||||||
name: &str,
|
|
||||||
primary: bool,
|
|
||||||
) -> anyhow::Result<Self> {
|
|
||||||
if width > MAX_DISPLAY_SIZE {
|
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"display width ({}) is larger than {}",
|
"display width ({}) is larger than {}",
|
||||||
width,
|
params.width,
|
||||||
MAX_DISPLAY_SIZE
|
MAX_DISPLAY_SIZE
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if height > MAX_DISPLAY_SIZE {
|
if params.height > MAX_DISPLAY_SIZE {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"display height ({}) is larger than {}",
|
"display height ({}) is larger than {}",
|
||||||
height,
|
params.height,
|
||||||
MAX_DISPLAY_SIZE
|
MAX_DISPLAY_SIZE
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -114,42 +117,44 @@ impl Display {
|
|||||||
let tex_format = ffi::RGBA;
|
let tex_format = ffi::RGBA;
|
||||||
let internal_format = ffi::RGBA8;
|
let internal_format = ffi::RGBA8;
|
||||||
|
|
||||||
let tex_id = renderer.with_context(|gl| {
|
let tex_id = params.renderer.with_context(|gl| {
|
||||||
smithay_wrapper::create_framebuffer_texture(
|
smithay_wrapper::create_framebuffer_texture(
|
||||||
gl,
|
gl,
|
||||||
width as u32,
|
params.width as u32,
|
||||||
height as u32,
|
params.height as u32,
|
||||||
tex_format,
|
tex_format,
|
||||||
internal_format,
|
internal_format,
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let egl_image = egl_data.create_egl_image(tex_id)?;
|
let egl_image = params.egl_data.create_egl_image(tex_id)?;
|
||||||
let dmabuf_data = egl_data.create_dmabuf_data(&egl_image)?;
|
let dmabuf_data = params.egl_data.create_dmabuf_data(&egl_image)?;
|
||||||
|
|
||||||
let opaque = false;
|
let opaque = false;
|
||||||
let size = (width as i32, height as i32).into();
|
let size = (params.width as i32, params.height as i32).into();
|
||||||
let gles_texture =
|
let gles_texture = unsafe {
|
||||||
unsafe { GlesTexture::from_raw(renderer, Some(tex_format), opaque, tex_id, size) };
|
GlesTexture::from_raw(params.renderer, Some(tex_format), opaque, tex_id, size)
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
wm,
|
egl_data: params.egl_data,
|
||||||
width,
|
width: params.width,
|
||||||
height,
|
height: params.height,
|
||||||
name: String::from(name),
|
name: String::from(params.name),
|
||||||
|
primary: params.primary,
|
||||||
|
wayland_env: params.wayland_env,
|
||||||
|
wm: params.wm,
|
||||||
displayed_windows: Vec::new(),
|
displayed_windows: Vec::new(),
|
||||||
wants_redraw: true,
|
|
||||||
egl_data,
|
|
||||||
dmabuf_data,
|
dmabuf_data,
|
||||||
egl_image,
|
egl_image,
|
||||||
gles_texture,
|
gles_texture,
|
||||||
wayland_env,
|
|
||||||
visible: true,
|
|
||||||
primary,
|
|
||||||
overlay_id: None,
|
|
||||||
tasks: SyncEventQueue::new(),
|
|
||||||
last_pressed_time_ms: 0,
|
last_pressed_time_ms: 0,
|
||||||
no_windows_since: None,
|
no_windows_since: None,
|
||||||
|
overlay_id: None,
|
||||||
|
tasks: SyncEventQueue::new(),
|
||||||
|
visible: true,
|
||||||
|
wants_redraw: true,
|
||||||
|
layout: packet_server::WvrDisplayWindowLayout::Tiling,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,19 +183,62 @@ impl Display {
|
|||||||
self.reposition_windows();
|
self.reposition_windows();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reposition_windows(&mut self) {
|
pub fn reposition_windows(&mut self) {
|
||||||
let window_count = self.displayed_windows.len();
|
let window_count = self.displayed_windows.len();
|
||||||
|
|
||||||
for (i, win) in self.displayed_windows.iter_mut().enumerate() {
|
match &self.layout {
|
||||||
if let Some(window) = self.wm.borrow_mut().windows.get_mut(&win.window_handle) {
|
packet_server::WvrDisplayWindowLayout::Tiling => {
|
||||||
let d_cur = i as f32 / window_count as f32;
|
let mut i = 0;
|
||||||
let d_next = (i + 1) as f32 / window_count as f32;
|
for win in self.displayed_windows.iter_mut() {
|
||||||
|
if let Some(window) = self.wm.borrow_mut().windows.get_mut(&win.window_handle) {
|
||||||
|
if !window.visible {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let d_cur = i as f32 / window_count as f32;
|
||||||
|
let d_next = (i + 1) as f32 / window_count as f32;
|
||||||
|
|
||||||
let left = (d_cur * self.width as f32) as i32;
|
let left = (d_cur * self.width as f32) as i32;
|
||||||
let right = (d_next * self.width as f32) as i32;
|
let right = (d_next * self.width as f32) as i32;
|
||||||
|
|
||||||
window.set_pos(left, 0);
|
window.set_pos(left, 0);
|
||||||
window.set_size((right - left) as u32, self.height as u32);
|
window.set_size((right - left) as u32, self.height as u32);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
packet_server::WvrDisplayWindowLayout::Stacking(opts) => {
|
||||||
|
let do_margins = |margins: &packet_server::Margins, window: &mut window::Window| {
|
||||||
|
let top = margins.top as i32;
|
||||||
|
let bottom = self.height as i32 - margins.bottom as i32;
|
||||||
|
let left = margins.left as i32;
|
||||||
|
let right = self.width as i32 - margins.right as i32;
|
||||||
|
let width = right - left;
|
||||||
|
let height = bottom - top;
|
||||||
|
if width < 0 || height < 0 {
|
||||||
|
return; // wrong parameters, do nothing!
|
||||||
|
}
|
||||||
|
|
||||||
|
window.set_pos(left, top);
|
||||||
|
window.set_size(width as u32, height as u32);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut i = 0;
|
||||||
|
for win in self.displayed_windows.iter_mut() {
|
||||||
|
if let Some(window) = self.wm.borrow_mut().windows.get_mut(&win.window_handle) {
|
||||||
|
if !window.visible {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
do_margins(
|
||||||
|
if i == 0 {
|
||||||
|
&opts.margins_first
|
||||||
|
} else {
|
||||||
|
&opts.margins_rest
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,6 +265,7 @@ impl Display {
|
|||||||
while let Some(task) = self.tasks.read() {
|
while let Some(task) = self.tasks.read() {
|
||||||
match task {
|
match task {
|
||||||
DisplayTask::ProcessCleanup(process_handle) => {
|
DisplayTask::ProcessCleanup(process_handle) => {
|
||||||
|
let count = self.displayed_windows.len();
|
||||||
self.displayed_windows
|
self.displayed_windows
|
||||||
.retain(|win| win.process_handle != process_handle);
|
.retain(|win| win.process_handle != process_handle);
|
||||||
log::info!(
|
log::info!(
|
||||||
@@ -226,6 +275,12 @@ impl Display {
|
|||||||
);
|
);
|
||||||
self.no_windows_since = Some(get_millis());
|
self.no_windows_since = Some(get_millis());
|
||||||
|
|
||||||
|
if count != self.displayed_windows.len() {
|
||||||
|
signals.send(WayVRSignal::BroadcastStateChanged(
|
||||||
|
packet_server::WvrStateChanged::WindowRemoved,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
self.reposition_windows();
|
self.reposition_windows();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,6 +300,9 @@ impl Display {
|
|||||||
.flat_map(|display_window| {
|
.flat_map(|display_window| {
|
||||||
let wm = self.wm.borrow_mut();
|
let wm = self.wm.borrow_mut();
|
||||||
if let Some(window) = wm.windows.get(&display_window.window_handle) {
|
if let Some(window) = wm.windows.get(&display_window.window_handle) {
|
||||||
|
if !window.visible {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
render_elements_from_surface_tree(
|
render_elements_from_surface_tree(
|
||||||
renderer,
|
renderer,
|
||||||
display_window.toplevel.wl_surface(),
|
display_window.toplevel.wl_surface(),
|
||||||
@@ -284,8 +342,12 @@ impl Display {
|
|||||||
fn get_hovered_window(&self, cursor_x: u32, cursor_y: u32) -> Option<window::WindowHandle> {
|
fn get_hovered_window(&self, cursor_x: u32, cursor_y: u32) -> Option<window::WindowHandle> {
|
||||||
let wm = self.wm.borrow();
|
let wm = self.wm.borrow();
|
||||||
|
|
||||||
for cell in self.displayed_windows.iter() {
|
for cell in self.displayed_windows.iter().rev() {
|
||||||
if let Some(window) = wm.windows.get(&cell.window_handle) {
|
if let Some(window) = wm.windows.get(&cell.window_handle) {
|
||||||
|
if !window.visible {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (cursor_x as i32) >= window.pos_x
|
if (cursor_x as i32) >= window.pos_x
|
||||||
&& (cursor_x as i32) < window.pos_x + window.size_x as i32
|
&& (cursor_x as i32) < window.pos_x + window.size_x as i32
|
||||||
&& (cursor_y as i32) >= window.pos_y
|
&& (cursor_y as i32) >= window.pos_y
|
||||||
@@ -298,15 +360,30 @@ impl Display {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn trigger_rerender(&mut self) {
|
||||||
|
self.wants_redraw = true;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_visible(&mut self, visible: bool) {
|
pub fn set_visible(&mut self, visible: bool) {
|
||||||
log::info!("Display \"{}\" visible: {}", self.name.as_str(), visible);
|
log::info!("Display \"{}\" visible: {}", self.name.as_str(), visible);
|
||||||
if self.visible != visible {
|
if self.visible == visible {
|
||||||
self.visible = visible;
|
return;
|
||||||
if visible {
|
|
||||||
self.wants_redraw = true;
|
|
||||||
self.no_windows_since = None;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
self.visible = visible;
|
||||||
|
if visible {
|
||||||
|
self.no_windows_since = None;
|
||||||
|
self.trigger_rerender();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_layout(&mut self, layout: packet_server::WvrDisplayWindowLayout) {
|
||||||
|
log::info!("Display \"{}\" layout: {:?}", self.name.as_str(), layout);
|
||||||
|
if self.layout == layout {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.layout = layout;
|
||||||
|
self.trigger_rerender();
|
||||||
|
self.reposition_windows();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_mouse_move(
|
pub fn send_mouse_move(
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ mod smithay_wrapper;
|
|||||||
mod time;
|
mod time;
|
||||||
mod window;
|
mod window;
|
||||||
use comp::Application;
|
use comp::Application;
|
||||||
use display::DisplayVec;
|
use display::{DisplayInitParams, DisplayVec};
|
||||||
use event_queue::SyncEventQueue;
|
use event_queue::SyncEventQueue;
|
||||||
use process::ProcessVec;
|
use process::ProcessVec;
|
||||||
use server_ipc::WayVRServer;
|
use server_ipc::WayVRServer;
|
||||||
@@ -32,9 +32,13 @@ use smithay::{
|
|||||||
shm::ShmState,
|
shm::ShmState,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use std::{cell::RefCell, collections::HashSet, rc::Rc};
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
rc::Rc,
|
||||||
|
};
|
||||||
use time::get_millis;
|
use time::get_millis;
|
||||||
use wayvr_ipc::packet_client;
|
use wayvr_ipc::{packet_client, packet_server};
|
||||||
|
|
||||||
const STR_INVALID_HANDLE_DISP: &str = "Invalid display handle";
|
const STR_INVALID_HANDLE_DISP: &str = "Invalid display handle";
|
||||||
const STR_INVALID_HANDLE_PROCESS: &str = "Invalid process handle";
|
const STR_INVALID_HANDLE_PROCESS: &str = "Invalid process handle";
|
||||||
@@ -76,6 +80,11 @@ pub enum WayVRTask {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum WayVRSignal {
|
pub enum WayVRSignal {
|
||||||
DisplayVisibility(display::DisplayHandle, bool),
|
DisplayVisibility(display::DisplayHandle, bool),
|
||||||
|
DisplayWindowLayout(
|
||||||
|
display::DisplayHandle,
|
||||||
|
packet_server::WvrDisplayWindowLayout,
|
||||||
|
),
|
||||||
|
BroadcastStateChanged(packet_server::WvrStateChanged),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
@@ -102,7 +111,7 @@ pub struct WayVRState {
|
|||||||
|
|
||||||
pub struct WayVR {
|
pub struct WayVR {
|
||||||
pub state: WayVRState,
|
pub state: WayVRState,
|
||||||
ipc_server: WayVRServer,
|
pub ipc_server: WayVRServer,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum MouseIndex {
|
pub enum MouseIndex {
|
||||||
@@ -302,13 +311,13 @@ impl WayVR {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
for (p_handle, disp_handle) in to_remove {
|
for (p_handle, disp_handle) in &to_remove {
|
||||||
self.state.processes.remove(&p_handle);
|
self.state.processes.remove(p_handle);
|
||||||
|
|
||||||
if let Some(display) = self.state.displays.get_mut(&disp_handle) {
|
if let Some(display) = self.state.displays.get_mut(disp_handle) {
|
||||||
display
|
display
|
||||||
.tasks
|
.tasks
|
||||||
.send(display::DisplayTask::ProcessCleanup(p_handle));
|
.send(display::DisplayTask::ProcessCleanup(*p_handle));
|
||||||
display.wants_redraw = true;
|
display.wants_redraw = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -317,6 +326,12 @@ impl WayVR {
|
|||||||
display.tick(&self.state.config, &handle, &mut self.state.signals);
|
display.tick(&self.state.config, &handle, &mut self.state.signals);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if !to_remove.is_empty() {
|
||||||
|
self.state.signals.send(WayVRSignal::BroadcastStateChanged(
|
||||||
|
packet_server::WvrStateChanged::ProcessRemoved,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
while let Some(task) = self.state.tasks.read() {
|
while let Some(task) = self.state.tasks.read() {
|
||||||
match task {
|
match task {
|
||||||
WayVRTask::NewExternalProcess(req) => {
|
WayVRTask::NewExternalProcess(req) => {
|
||||||
@@ -329,15 +344,22 @@ impl WayVR {
|
|||||||
// Attach newly created toplevel surfaces to displays
|
// Attach newly created toplevel surfaces to displays
|
||||||
for client in &self.state.manager.clients {
|
for client in &self.state.manager.clients {
|
||||||
if client.client.id() == client_id {
|
if client.client.id() == client_id {
|
||||||
let window_handle = self.state.wm.borrow_mut().create_window(&toplevel);
|
|
||||||
|
|
||||||
if let Some(process_handle) =
|
if let Some(process_handle) =
|
||||||
process::find_by_pid(&self.state.processes, client.pid)
|
process::find_by_pid(&self.state.processes, client.pid)
|
||||||
{
|
{
|
||||||
|
let window_handle = self
|
||||||
|
.state
|
||||||
|
.wm
|
||||||
|
.borrow_mut()
|
||||||
|
.create_window(client.display_handle, &toplevel);
|
||||||
|
|
||||||
if let Some(display) =
|
if let Some(display) =
|
||||||
self.state.displays.get_mut(&client.display_handle)
|
self.state.displays.get_mut(&client.display_handle)
|
||||||
{
|
{
|
||||||
display.add_window(window_handle, process_handle, &toplevel);
|
display.add_window(window_handle, process_handle, &toplevel);
|
||||||
|
self.state.signals.send(WayVRSignal::BroadcastStateChanged(
|
||||||
|
packet_server::WvrStateChanged::WindowCreated,
|
||||||
|
));
|
||||||
} else {
|
} else {
|
||||||
// This shouldn't happen, scream if it does
|
// This shouldn't happen, scream if it does
|
||||||
log::error!("Could not attach window handle into display");
|
log::error!("Could not attach window handle into display");
|
||||||
@@ -456,6 +478,16 @@ impl WayVRState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_display_layout(
|
||||||
|
&mut self,
|
||||||
|
display: display::DisplayHandle,
|
||||||
|
layout: packet_server::WvrDisplayWindowLayout,
|
||||||
|
) {
|
||||||
|
if let Some(display) = self.displays.get_mut(&display) {
|
||||||
|
display.set_layout(layout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_dmabuf_data(&self, display: display::DisplayHandle) -> Option<egl_data::DMAbufData> {
|
pub fn get_dmabuf_data(&self, display: display::DisplayHandle) -> Option<egl_data::DMAbufData> {
|
||||||
self.displays
|
self.displays
|
||||||
.get(&display)
|
.get(&display)
|
||||||
@@ -469,17 +501,24 @@ impl WayVRState {
|
|||||||
name: &str,
|
name: &str,
|
||||||
primary: bool,
|
primary: bool,
|
||||||
) -> anyhow::Result<display::DisplayHandle> {
|
) -> anyhow::Result<display::DisplayHandle> {
|
||||||
let display = display::Display::new(
|
let display = display::Display::new(DisplayInitParams {
|
||||||
self.wm.clone(),
|
wm: self.wm.clone(),
|
||||||
&mut self.manager.state.gles_renderer,
|
egl_data: self.egl_data.clone(),
|
||||||
self.egl_data.clone(),
|
renderer: &mut self.manager.state.gles_renderer,
|
||||||
self.manager.wayland_env.clone(),
|
wayland_env: self.manager.wayland_env.clone(),
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
name,
|
name,
|
||||||
primary,
|
primary,
|
||||||
)?;
|
})?;
|
||||||
Ok(self.displays.add(display))
|
|
||||||
|
let handle = self.displays.add(display);
|
||||||
|
|
||||||
|
self.signals.send(WayVRSignal::BroadcastStateChanged(
|
||||||
|
packet_server::WvrStateChanged::DisplayCreated,
|
||||||
|
));
|
||||||
|
|
||||||
|
Ok(handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn destroy_display(&mut self, handle: display::DisplayHandle) -> anyhow::Result<()> {
|
pub fn destroy_display(&mut self, handle: display::DisplayHandle) -> anyhow::Result<()> {
|
||||||
@@ -519,6 +558,10 @@ impl WayVRState {
|
|||||||
|
|
||||||
self.displays.remove(&handle);
|
self.displays.remove(&handle);
|
||||||
|
|
||||||
|
self.signals.send(WayVRSignal::BroadcastStateChanged(
|
||||||
|
packet_server::WvrStateChanged::DisplayRemoved,
|
||||||
|
));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,6 +627,7 @@ impl WayVRState {
|
|||||||
exec_path: &str,
|
exec_path: &str,
|
||||||
args: &[&str],
|
args: &[&str],
|
||||||
env: &[(&str, &str)],
|
env: &[(&str, &str)],
|
||||||
|
userdata: HashMap<String, String>,
|
||||||
) -> anyhow::Result<process::ProcessHandle> {
|
) -> anyhow::Result<process::ProcessHandle> {
|
||||||
let display = self
|
let display = self
|
||||||
.displays
|
.displays
|
||||||
@@ -591,18 +635,26 @@ impl WayVRState {
|
|||||||
.ok_or(anyhow::anyhow!(STR_INVALID_HANDLE_DISP))?;
|
.ok_or(anyhow::anyhow!(STR_INVALID_HANDLE_DISP))?;
|
||||||
|
|
||||||
let res = display.spawn_process(exec_path, args, env)?;
|
let res = display.spawn_process(exec_path, args, env)?;
|
||||||
Ok(self
|
|
||||||
|
let handle = self
|
||||||
.processes
|
.processes
|
||||||
.add(process::Process::Managed(process::WayVRProcess {
|
.add(process::Process::Managed(process::WayVRProcess {
|
||||||
auth_key: res.auth_key,
|
auth_key: res.auth_key,
|
||||||
child: res.child,
|
child: res.child,
|
||||||
display_handle,
|
display_handle,
|
||||||
exec_path: String::from(exec_path),
|
exec_path: String::from(exec_path),
|
||||||
|
userdata,
|
||||||
args: args.iter().map(|x| String::from(*x)).collect(),
|
args: args.iter().map(|x| String::from(*x)).collect(),
|
||||||
env: env
|
env: env
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(a, b)| (String::from(*a), String::from(*b)))
|
.map(|(a, b)| (String::from(*a), String::from(*b)))
|
||||||
.collect(),
|
.collect(),
|
||||||
})))
|
}));
|
||||||
|
|
||||||
|
self.signals.send(WayVRSignal::BroadcastStateChanged(
|
||||||
|
packet_server::WvrStateChanged::ProcessCreated,
|
||||||
|
));
|
||||||
|
|
||||||
|
Ok(handle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use wayvr_ipc::packet_server;
|
use wayvr_ipc::packet_server;
|
||||||
|
|
||||||
use crate::gen_id;
|
use crate::gen_id;
|
||||||
@@ -13,6 +15,8 @@ pub struct WayVRProcess {
|
|||||||
pub exec_path: String,
|
pub exec_path: String,
|
||||||
pub args: Vec<String>,
|
pub args: Vec<String>,
|
||||||
pub env: Vec<(String, String)>,
|
pub env: Vec<(String, String)>,
|
||||||
|
|
||||||
|
pub userdata: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -60,11 +64,13 @@ impl Process {
|
|||||||
match self {
|
match self {
|
||||||
Process::Managed(p) => packet_server::WvrProcess {
|
Process::Managed(p) => packet_server::WvrProcess {
|
||||||
name: p.get_name().unwrap_or(String::from("unknown")),
|
name: p.get_name().unwrap_or(String::from("unknown")),
|
||||||
|
userdata: p.userdata.clone(),
|
||||||
display_handle: p.display_handle.as_packet(),
|
display_handle: p.display_handle.as_packet(),
|
||||||
handle: handle.as_packet(),
|
handle: handle.as_packet(),
|
||||||
},
|
},
|
||||||
Process::External(p) => packet_server::WvrProcess {
|
Process::External(p) => packet_server::WvrProcess {
|
||||||
name: p.get_name().unwrap_or(String::from("unknown")),
|
name: p.get_name().unwrap_or(String::from("unknown")),
|
||||||
|
userdata: Default::default(),
|
||||||
display_handle: p.display_handle.as_packet(),
|
display_handle: p.display_handle.as_packet(),
|
||||||
handle: handle.as_packet(),
|
handle: handle.as_packet(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use super::{display, process, TickTask, WayVRSignal};
|
use super::{display, process, window, TickTask, WayVRSignal};
|
||||||
use bytes::BufMut;
|
use bytes::BufMut;
|
||||||
use interprocess::local_socket::{self, traits::Listener, ToNsName};
|
use interprocess::local_socket::{self, traits::Listener, ToNsName};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
@@ -236,6 +236,97 @@ impl Connection {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_wvr_display_set_window_layout(
|
||||||
|
&mut self,
|
||||||
|
params: &mut TickParams,
|
||||||
|
handle: packet_server::WvrDisplayHandle,
|
||||||
|
layout: packet_server::WvrDisplayWindowLayout,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
params.state.signals.send(WayVRSignal::DisplayWindowLayout(
|
||||||
|
display::DisplayHandle::from_packet(handle),
|
||||||
|
layout,
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_wvr_display_window_list(
|
||||||
|
&mut self,
|
||||||
|
params: &mut TickParams,
|
||||||
|
serial: ipc::Serial,
|
||||||
|
display_handle: packet_server::WvrDisplayHandle,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let mut send = |list: Option<packet_server::WvrWindowList>| -> anyhow::Result<()> {
|
||||||
|
send_packet(
|
||||||
|
&mut self.conn,
|
||||||
|
&ipc::data_encode(&PacketServer::WvrDisplayWindowListResponse(serial, list)),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(display) = params
|
||||||
|
.state
|
||||||
|
.displays
|
||||||
|
.get(&display::DisplayHandle::from_packet(display_handle.clone()))
|
||||||
|
else {
|
||||||
|
return send(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
send(Some(packet_server::WvrWindowList {
|
||||||
|
list: display
|
||||||
|
.displayed_windows
|
||||||
|
.iter()
|
||||||
|
.filter_map(|disp_win| {
|
||||||
|
params
|
||||||
|
.state
|
||||||
|
.wm
|
||||||
|
.borrow_mut()
|
||||||
|
.windows
|
||||||
|
.get(&disp_win.window_handle)
|
||||||
|
.map(|win| packet_server::WvrWindow {
|
||||||
|
handle: window::WindowHandle::as_packet(&disp_win.window_handle),
|
||||||
|
process_handle: process::ProcessHandle::as_packet(
|
||||||
|
&disp_win.process_handle,
|
||||||
|
),
|
||||||
|
pos_x: win.pos_x,
|
||||||
|
pos_y: win.pos_y,
|
||||||
|
size_x: win.size_x,
|
||||||
|
size_y: win.size_y,
|
||||||
|
visible: win.visible,
|
||||||
|
display_handle: display_handle.clone(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_wvr_window_set_visible(
|
||||||
|
&mut self,
|
||||||
|
params: &mut TickParams,
|
||||||
|
handle: packet_server::WvrWindowHandle,
|
||||||
|
visible: bool,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let mut to_resize = None;
|
||||||
|
|
||||||
|
if let Some(window) = params
|
||||||
|
.state
|
||||||
|
.wm
|
||||||
|
.borrow_mut()
|
||||||
|
.windows
|
||||||
|
.get_mut(&window::WindowHandle::from_packet(handle))
|
||||||
|
{
|
||||||
|
window.visible = visible;
|
||||||
|
to_resize = Some(window.display_handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(to_resize) = to_resize {
|
||||||
|
if let Some(display) = params.state.displays.get_mut(&to_resize) {
|
||||||
|
display.reposition_windows();
|
||||||
|
display.trigger_rerender();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_wvr_process_launch(
|
fn handle_wvr_process_launch(
|
||||||
&mut self,
|
&mut self,
|
||||||
params: &mut TickParams,
|
params: &mut TickParams,
|
||||||
@@ -250,6 +341,7 @@ impl Connection {
|
|||||||
&packet_params.exec,
|
&packet_params.exec,
|
||||||
&args_vec,
|
&args_vec,
|
||||||
&env_vec,
|
&env_vec,
|
||||||
|
packet_params.userdata,
|
||||||
);
|
);
|
||||||
|
|
||||||
let res = res.map(|r| r.as_packet()).map_err(|e| e.to_string());
|
let res = res.map(|r| r.as_packet()).map_err(|e| e.to_string());
|
||||||
@@ -262,7 +354,7 @@ impl Connection {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_wvr_process_get(
|
fn handle_wvr_display_get(
|
||||||
&mut self,
|
&mut self,
|
||||||
params: &TickParams,
|
params: &TickParams,
|
||||||
serial: ipc::Serial,
|
serial: ipc::Serial,
|
||||||
@@ -332,6 +424,27 @@ impl Connection {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_wvr_process_get(
|
||||||
|
&mut self,
|
||||||
|
params: &TickParams,
|
||||||
|
serial: ipc::Serial,
|
||||||
|
process_handle: packet_server::WvrProcessHandle,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let native_handle = &process::ProcessHandle::from_packet(process_handle.clone());
|
||||||
|
let process = params
|
||||||
|
.state
|
||||||
|
.processes
|
||||||
|
.get(native_handle)
|
||||||
|
.map(|process| process.to_packet(*native_handle));
|
||||||
|
|
||||||
|
send_packet(
|
||||||
|
&mut self.conn,
|
||||||
|
&ipc::data_encode(&PacketServer::WvrProcessGetResponse(serial, process)),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_wlx_haptics(
|
fn handle_wlx_haptics(
|
||||||
&mut self,
|
&mut self,
|
||||||
params: &mut TickParams,
|
params: &mut TickParams,
|
||||||
@@ -362,7 +475,7 @@ impl Connection {
|
|||||||
self.handle_wvr_display_list(params, serial)?;
|
self.handle_wvr_display_list(params, serial)?;
|
||||||
}
|
}
|
||||||
PacketClient::WvrDisplayGet(serial, display_handle) => {
|
PacketClient::WvrDisplayGet(serial, display_handle) => {
|
||||||
self.handle_wvr_process_get(params, serial, display_handle)?;
|
self.handle_wvr_display_get(params, serial, display_handle)?;
|
||||||
}
|
}
|
||||||
PacketClient::WvrDisplayRemove(serial, display_handle) => {
|
PacketClient::WvrDisplayRemove(serial, display_handle) => {
|
||||||
self.handle_wvr_display_remove(params, serial, display_handle)?;
|
self.handle_wvr_display_remove(params, serial, display_handle)?;
|
||||||
@@ -370,6 +483,18 @@ impl Connection {
|
|||||||
PacketClient::WvrDisplaySetVisible(display_handle, visible) => {
|
PacketClient::WvrDisplaySetVisible(display_handle, visible) => {
|
||||||
self.handle_wvr_display_set_visible(params, display_handle, visible)?;
|
self.handle_wvr_display_set_visible(params, display_handle, visible)?;
|
||||||
}
|
}
|
||||||
|
PacketClient::WvrDisplaySetWindowLayout(display_handle, layout) => {
|
||||||
|
self.handle_wvr_display_set_window_layout(params, display_handle, layout)?;
|
||||||
|
}
|
||||||
|
PacketClient::WvrDisplayWindowList(serial, display_handle) => {
|
||||||
|
self.handle_wvr_display_window_list(params, serial, display_handle)?;
|
||||||
|
}
|
||||||
|
PacketClient::WvrWindowSetVisible(window_handle, visible) => {
|
||||||
|
self.handle_wvr_window_set_visible(params, window_handle, visible)?;
|
||||||
|
}
|
||||||
|
PacketClient::WvrProcessGet(serial, process_handle) => {
|
||||||
|
self.handle_wvr_process_get(params, serial, process_handle)?;
|
||||||
|
}
|
||||||
PacketClient::WvrProcessList(serial) => {
|
PacketClient::WvrProcessList(serial) => {
|
||||||
self.handle_wvr_process_list(params, serial)?;
|
self.handle_wvr_process_list(params, serial)?;
|
||||||
}
|
}
|
||||||
@@ -507,4 +632,11 @@ impl WayVRServer {
|
|||||||
self.tick_connections(params);
|
self.tick_connections(params);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn broadcast(&mut self, packet: packet_server::PacketServer) -> anyhow::Result<()> {
|
||||||
|
for connection in &mut self.connections {
|
||||||
|
send_packet(&mut connection.conn, &ipc::data_encode(&packet))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,31 @@
|
|||||||
use smithay::wayland::shell::xdg::ToplevelSurface;
|
use smithay::wayland::shell::xdg::ToplevelSurface;
|
||||||
|
use wayvr_ipc::packet_server;
|
||||||
|
|
||||||
use crate::gen_id;
|
use crate::gen_id;
|
||||||
|
|
||||||
|
use super::display;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Window {
|
pub struct Window {
|
||||||
pub pos_x: i32,
|
pub pos_x: i32,
|
||||||
pub pos_y: i32,
|
pub pos_y: i32,
|
||||||
pub size_x: u32,
|
pub size_x: u32,
|
||||||
pub size_y: u32,
|
pub size_y: u32,
|
||||||
|
pub visible: bool,
|
||||||
pub toplevel: ToplevelSurface,
|
pub toplevel: ToplevelSurface,
|
||||||
|
pub display_handle: display::DisplayHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Window {
|
impl Window {
|
||||||
pub fn new(toplevel: &ToplevelSurface) -> Self {
|
pub fn new(display_handle: display::DisplayHandle, toplevel: &ToplevelSurface) -> Self {
|
||||||
Self {
|
Self {
|
||||||
pos_x: 0,
|
pos_x: 0,
|
||||||
pos_y: 0,
|
pos_y: 0,
|
||||||
size_x: 0,
|
size_x: 0,
|
||||||
size_y: 0,
|
size_y: 0,
|
||||||
|
visible: true,
|
||||||
toplevel: toplevel.clone(),
|
toplevel: toplevel.clone(),
|
||||||
|
display_handle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,9 +70,29 @@ impl WindowManager {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_window(&mut self, toplevel: &ToplevelSurface) -> WindowHandle {
|
pub fn create_window(
|
||||||
self.windows.add(Window::new(toplevel))
|
&mut self,
|
||||||
|
display_handle: display::DisplayHandle,
|
||||||
|
toplevel: &ToplevelSurface,
|
||||||
|
) -> WindowHandle {
|
||||||
|
self.windows.add(Window::new(display_handle, toplevel))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gen_id!(WindowVec, Window, WindowCell, WindowHandle);
|
gen_id!(WindowVec, Window, WindowCell, WindowHandle);
|
||||||
|
|
||||||
|
impl WindowHandle {
|
||||||
|
pub fn from_packet(handle: packet_server::WvrWindowHandle) -> Self {
|
||||||
|
Self {
|
||||||
|
generation: handle.generation,
|
||||||
|
idx: handle.idx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_packet(&self) -> packet_server::WvrWindowHandle {
|
||||||
|
packet_server::WvrWindowHandle {
|
||||||
|
idx: self.idx,
|
||||||
|
generation: self.generation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use glam::{vec3a, Affine2, Vec3, Vec3A};
|
use glam::{vec3a, Affine2, Vec3, Vec3A};
|
||||||
use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc};
|
use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc};
|
||||||
use vulkano::image::SubresourceLayout;
|
use vulkano::image::SubresourceLayout;
|
||||||
|
use wayvr_ipc::packet_server;
|
||||||
use wlx_capture::frame::{DmabufFrame, FourCC, FrameFormat, FramePlane};
|
use wlx_capture::frame::{DmabufFrame, FourCC, FrameFormat, FramePlane};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -309,12 +310,17 @@ where
|
|||||||
None => vec![],
|
None => vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut userdata = HashMap::new();
|
||||||
|
userdata.insert(String::from("type"), String::from("dashboard"));
|
||||||
|
|
||||||
// Start dashboard specified in the WayVR config
|
// Start dashboard specified in the WayVR config
|
||||||
let _process_handle_unused =
|
let _process_handle_unused = wayvr.data.state.spawn_process(
|
||||||
wayvr
|
disp_handle,
|
||||||
.data
|
&conf_dash.exec,
|
||||||
.state
|
&args_vec,
|
||||||
.spawn_process(disp_handle, &conf_dash.exec, &args_vec, &env_vec)?;
|
&env_vec,
|
||||||
|
userdata,
|
||||||
|
)?;
|
||||||
|
|
||||||
wayvr.dashboard_executed = true;
|
wayvr.dashboard_executed = true;
|
||||||
|
|
||||||
@@ -434,6 +440,15 @@ where
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
wayvr::WayVRSignal::DisplayWindowLayout(display_handle, layout) => {
|
||||||
|
wayvr.data.state.set_display_layout(display_handle, layout);
|
||||||
|
}
|
||||||
|
wayvr::WayVRSignal::BroadcastStateChanged(packet) => {
|
||||||
|
wayvr
|
||||||
|
.data
|
||||||
|
.ipc_server
|
||||||
|
.broadcast(packet_server::PacketServer::WvrStateChanged(packet))?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -734,10 +749,13 @@ where
|
|||||||
wayvr.data.terminate_process(process_handle);
|
wayvr.data.terminate_process(process_handle);
|
||||||
} else {
|
} else {
|
||||||
// Spawn process
|
// Spawn process
|
||||||
wayvr
|
wayvr.data.state.spawn_process(
|
||||||
.data
|
disp_handle,
|
||||||
.state
|
&app_entry.exec,
|
||||||
.spawn_process(disp_handle, &app_entry.exec, &args_vec, &env_vec)?;
|
&args_vec,
|
||||||
|
&env_vec,
|
||||||
|
Default::default(),
|
||||||
|
)?;
|
||||||
|
|
||||||
show_display::<O>(&mut wayvr, overlays, app_entry.target_display.as_str());
|
show_display::<O>(&mut wayvr, overlays, app_entry.target_display.as_str());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user