From 18a584b6879cd8aef26479fd711e6ce750810542 Mon Sep 17 00:00:00 2001 From: HMH Date: Fri, 11 Oct 2024 15:51:01 +0200 Subject: [PATCH] Add selection of custom input areas on Wayland. Automatic and correct input mapping on Wayland is difficult/impossible. Thus, on Wayland Weylus now supports the selection of a custom input area. This is done via a pop-up window that is positioned according to the desired area of input. Typically, the different input devices (mouse, touch, pen) can be mapped to the whole workspace (all screens) or only a specific screen. KDE for example offers input mapping of touch input to a specific screen. To Weylus this specific mapping is not know and needs to be specified in a choice menu of the pop-up window. --- lib/linux/xhelper.c | 6 +- src/capturable/mod.rs | 2 + src/gui.rs | 405 +++++++++++++++++++++++++++++- src/input/autopilot_device.rs | 1 + src/input/autopilot_device_win.rs | 13 +- src/input/device.rs | 1 + src/input/uinput_device.rs | 70 ++++-- src/main.rs | 1 + src/protocol.rs | 28 +++ src/web.rs | 3 + src/websocket.rs | 26 +- src/weylus.rs | 5 + ts/lib.ts | 76 +++++- www/static/style.css | 2 +- www/templates/index.html | 5 + 15 files changed, 602 insertions(+), 42 deletions(-) diff --git a/lib/linux/xhelper.c b/lib/linux/xhelper.c index e00613e8..ace02ce5 100644 --- a/lib/linux/xhelper.c +++ b/lib/linux/xhelper.c @@ -21,7 +21,8 @@ int x11_error_handler(Display* disp, XErrorEvent* err) return 0; } -void x11_set_error_handler() { +void x11_set_error_handler() +{ // setting an error handler is required as otherwise xlib may just exit the process, even though // the error was recoverable. XSetErrorHandler(x11_error_handler); @@ -176,7 +177,8 @@ Window* get_client_list(Display* disp, unsigned long* size, Error* err) return client_list; } -int create_capturables(Display* disp, Capturable** capturables, int* num_monitors, int size, Error* err) +int create_capturables( + Display* disp, Capturable** capturables, int* num_monitors, int size, Error* err) { if (size <= 0) return 0; diff --git a/src/capturable/mod.rs b/src/capturable/mod.rs index 078c3f97..34b10045 100644 --- a/src/capturable/mod.rs +++ b/src/capturable/mod.rs @@ -7,6 +7,7 @@ pub mod core_graphics; #[cfg(target_os = "linux")] pub mod pipewire; #[cfg(target_os = "linux")] +#[allow(dead_code)] pub mod remote_desktop_dbus; pub mod testsrc; @@ -37,6 +38,7 @@ where /// VirtualScreen: offset_x, offset_y, width, height for a capturable using a virtual screen. (Windows) pub enum Geometry { Relative(f64, f64, f64, f64), + #[cfg(target_os = "windows")] VirtualScreen(i32, i32, u32, u32, i32, i32), } diff --git a/src/gui.rs b/src/gui.rs index 70eb395c..4e56103a 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -2,11 +2,14 @@ use std::cmp::min; use std::io::Cursor; use std::iter::Iterator; use std::net::{IpAddr, SocketAddr}; +use std::sync::atomic::AtomicBool; +use fltk::app; +use fltk::enums::{FrameType, LabelType}; use fltk::image::PngImage; use fltk::menu::Choice; use std::sync::{mpsc, Arc, Mutex}; -use tracing::{error, info}; +use tracing::{error, info, warn}; use fltk::{ app::{awake_callback, App}, @@ -23,6 +26,7 @@ use fltk::{ use pnet_datalink as datalink; use crate::config::{write_config, Config, ThemeType}; +use crate::protocol::{CustomInputAreas, Rect}; use crate::web::Web2UiMessage::UInputInaccessible; pub fn run(config: &Config, log_receiver: mpsc::Receiver) { @@ -37,6 +41,7 @@ pub fn run(config: &Config, log_receiver: mpsc::Receiver) { .center_screen() .with_label(&format!("Weylus - {}", env!("CARGO_PKG_VERSION"))); wind.set_xclass("weylus"); + wind.set_callback(move |_win| app.quit()); let mut input_access_code = Input::default() .with_pos(130, 30) @@ -378,3 +383,401 @@ pub fn run(config: &Config, log_receiver: mpsc::Receiver) { // this is required to drop the callback and do a graceful shutdown of the web server but_toggle.set_callback(|_| ()); } + +const BORDER: i32 = 30; +static WINCTX: Mutex> = Mutex::new(None); + +struct InputAreaWindowContext { + win: Window, + choice_mouse: Choice, + choice_touch: Choice, + choice_pen: Choice, + workspaces: Vec, +} + +pub fn get_input_area( + no_gui: bool, + output_sender: std::sync::mpsc::Sender, +) { + // If no gui is running there is no event loop and windows can not be created. + // That's why we initialize the fltk app here one the first call. + if no_gui { + static GUI_INITIALIZED: AtomicBool = AtomicBool::new(false); + + if !GUI_INITIALIZED.swap(true, std::sync::atomic::Ordering::Relaxed) { + std::thread::spawn(move || { + let _app = App::default().with_scheme(fltk::app::AppScheme::Gtk); + let mut winctx = create_custom_input_area_window(); + custom_input_area_window_handle_events(&mut winctx.win, output_sender.clone()); + show_overlay_window(&mut winctx); + WINCTX.lock().unwrap().replace(winctx); + loop { + // calling wait_for ensures that the fltk event loop keeps running even if + // there is no window shown + if let Err(err) = app::wait_for(1.0) { + warn!("Error waiting for fltk events: {err}."); + } + } + }); + } else { + fltk::app::awake_callback(move || { + let mut winctx = WINCTX.lock().unwrap(); + let winctx = winctx.as_mut().unwrap(); + custom_input_area_window_handle_events(&mut winctx.win, output_sender.clone()); + show_overlay_window(winctx); + }); + } + } else { + fltk::app::awake_callback(move || { + let mut winctx = WINCTX.lock().unwrap(); + if winctx.is_none() { + winctx.replace(create_custom_input_area_window()); + } + + let winctx = winctx.as_mut().unwrap(); + custom_input_area_window_handle_events(&mut winctx.win, output_sender.clone()); + show_overlay_window(winctx); + }); + } +} + +fn create_custom_input_area_window() -> InputAreaWindowContext { + let mut win = Window::default().with_size(600, 600).center_screen(); + win.make_resizable(true); + win.set_border(false); + win.set_frame(FrameType::FlatBox); + win.set_color(fltk::enums::Color::from_rgb(240, 240, 240)); + let mut frame = Frame::default() + .with_size(win.w() - 2 * BORDER, win.h() - 2 * BORDER) + .center_of_parent() + .with_label( + "Press Enter to submit\ncurrent selection as\ncustom input area,\nEscape to abort.", + ); + frame.set_label_type(LabelType::Normal); + frame.set_label_size(20); + frame.set_color(fltk::enums::Color::Black); + frame.set_frame(FrameType::BorderFrame); + frame.set_label_font(fltk::enums::Font::HelveticaBold); + let width = 200; + let height = 30; + let padding = 10; + let tool_tip = "Some systems may have the input device mapped to a specific screen, this screen has to be selected here. Otherwise input mapping will be wrong. Selecting None disables any mapping."; + let mut choice_mouse = Choice::default() + .with_size(width, height) + .with_pos(padding, 4 * padding) + .center_x(&frame) + .with_id("choice_mouse") + .with_label("Map Mouse from:"); + choice_mouse.set_tooltip(tool_tip); + let mut choice_touch = Choice::default() + .with_size(width, height) + .below_of(&choice_mouse, padding) + .with_id("choice_touch") + .with_label("Map Touch from:"); + choice_touch.set_tooltip(tool_tip); + let mut choice_pen = Choice::default() + .with_size(width, height) + .below_of(&choice_touch, padding) + .with_id("choice_pen") + .with_label("Map Pen from:"); + choice_pen.set_tooltip(tool_tip); + + frame.handle(|frame, event| match event { + fltk::enums::Event::Push => { + if app::event_clicks() { + if let Some(mut win) = frame.window() { + win.fullscreen(!win.fullscreen_active()); + } + true + } else { + false + } + } + _ => false, + }); + win.resize_callback(move |_win, _x, _y, w, h| { + frame.resize(BORDER, BORDER, w - 2 * BORDER, h - 2 * BORDER) + }); + win.end(); + InputAreaWindowContext { + win, + choice_mouse, + choice_touch, + choice_pen, + workspaces: Vec::new(), + } +} + +fn custom_input_area_window_handle_events( + win: &mut Window, + sender: std::sync::mpsc::Sender, +) { + #[derive(Debug)] + enum MouseFlags { + All, + Edge(bool, bool), + Corner(bool, bool), + } + fn get_mouse_flags(win: &Window, x: i32, y: i32) -> MouseFlags { + let dx0 = (win.x() - x).abs(); + let dy0 = (win.y() - y).abs(); + let dx1 = (win.x() + win.w() - x).abs(); + let dy1 = (win.y() + win.h() - y).abs(); + let dx = min(dx0, dx1); + let dy = min(dy0, dy1); + let d = min(dx, dy); + if d <= BORDER { + if dx <= BORDER && dy <= BORDER { + MouseFlags::Corner(dx0 <= dx1, dy0 <= dy1) + } else { + MouseFlags::Edge(dx <= dy, if dx <= dy { dx0 <= dx1 } else { dy0 <= dy1 }) + } + } else { + MouseFlags::All + } + } + fn get_screen_coords_from_event_coords(win: &Window, (x, y): (i32, i32)) -> (i32, i32) { + (x + win.x(), y + win.y()) + } + fn set_cursor( + win: &mut Window, + current_cursor: &mut fltk::enums::Cursor, + flags: Option, + ) { + let cursor = match flags { + Some(MouseFlags::All) => fltk::enums::Cursor::Move, + Some(MouseFlags::Edge(bx, by)) => match (bx, by) { + (true, true) => fltk::enums::Cursor::W, + (true, false) => fltk::enums::Cursor::E, + (false, true) => fltk::enums::Cursor::N, + (false, false) => fltk::enums::Cursor::S, + }, + Some(MouseFlags::Corner(bx, by)) => match (bx, by) { + (true, true) => fltk::enums::Cursor::NWSE, + (true, false) => fltk::enums::Cursor::NESW, + (false, true) => fltk::enums::Cursor::NESW, + (false, false) => fltk::enums::Cursor::NWSE, + }, + None => fltk::enums::Cursor::Default, + }; + if *current_cursor != cursor { + *current_cursor = cursor; + win.set_cursor(cursor); + } + } + + let mut drag_flags = MouseFlags::All; + let mut current_cursor = fltk::enums::Cursor::Default; + let mut x = 0; + let mut y = 0; + let mut win_x_drag_start = 0; + let mut win_y_drag_start = 0; + let mut win_w_drag_start = 0; + let mut win_h_drag_start = 0; + win.handle(move |win, event| { + match event { + fltk::enums::Event::Move => { + let (x, y) = get_screen_coords_from_event_coords(&win, app::event_coords()); + let flags = get_mouse_flags(&win, x, y); + set_cursor(win, &mut current_cursor, Some(flags)); + true + } + fltk::enums::Event::Leave => { + win.set_cursor(fltk::enums::Cursor::Default); + true + } + fltk::enums::Event::Push => { + (x, y) = get_screen_coords_from_event_coords(&win, app::event_coords()); + win_x_drag_start = win.x(); + win_y_drag_start = win.y(); + win_w_drag_start = win.w(); + win_h_drag_start = win.h(); + drag_flags = get_mouse_flags(&win, x, y); + true + } + fltk::enums::Event::Drag => { + if win.opacity() == 1.0 { + win.set_opacity(0.5); + } + let (x_new, y_new) = get_screen_coords_from_event_coords(&win, app::event_coords()); + let dx = x_new - x; + let dy = y_new - y; + match drag_flags { + MouseFlags::All => win.set_pos(win_x_drag_start + dx, win_y_drag_start + dy), + MouseFlags::Edge(bx, by) => match (bx, by) { + (true, true) => win.resize( + win_x_drag_start + dx, + win_y_drag_start, + win_w_drag_start - dx, + win_h_drag_start, + ), + (true, false) => win.resize( + win_x_drag_start, + win_y_drag_start, + win_w_drag_start + dx, + win_h_drag_start, + ), + (false, true) => win.resize( + win_x_drag_start, + win_y_drag_start + dy, + win_w_drag_start, + win_h_drag_start - dy, + ), + (false, false) => win.resize( + win_x_drag_start, + win_y_drag_start, + win_w_drag_start, + win_h_drag_start + dy, + ), + }, + MouseFlags::Corner(bx, by) => match (bx, by) { + (true, true) => win.resize( + win_x_drag_start + dx, + win_y_drag_start + dy, + win_w_drag_start - dx, + win_h_drag_start - dy, + ), + + (true, false) => win.resize( + win_x_drag_start + dx, + win_y_drag_start, + win_w_drag_start - dx, + win_h_drag_start + dy, + ), + (false, true) => win.resize( + win_x_drag_start, + win_y_drag_start + dy, + win_w_drag_start + dx, + win_h_drag_start - dy, + ), + (false, false) => win.resize( + win_x_drag_start, + win_y_drag_start, + win_w_drag_start + dx, + win_h_drag_start + dy, + ), + }, + } + true + } + fltk::enums::Event::Released => { + if win.opacity() != 1.0 { + win.set_opacity(1.0); + } + true + } + fltk::enums::Event::KeyDown => match app::event_key() { + fltk::enums::Key::Enter => { + fn relative_rect(win: &Window, workspace: &Rect) -> Rect { + // clamp rect to workspace and ensure it has non-zero area + let mut rect = crate::protocol::Rect { + x: (win.x() as f64 - workspace.x).min(workspace.w) / workspace.w, + y: (win.y() as f64 - workspace.y).min(workspace.h) / workspace.h, + w: win.w().max(1) as f64 / workspace.w, + h: win.h().max(1) as f64 / workspace.h, + }; + rect.w = rect.w.min(1.0 - rect.x); + rect.h = rect.h.min(1.0 - rect.y); + rect + } + win.set_cursor(fltk::enums::Cursor::Default); + win.hide(); + let mut areas = CustomInputAreas::default(); + let workspaces = WINCTX.lock().unwrap().as_ref().unwrap().workspaces.clone(); + for (name, area) in [ + ("choice_mouse", &mut areas.mouse), + ("choice_touch", &mut areas.touch), + ("choice_pen", &mut areas.pen), + ] { + let c: Choice = fltk::app::widget_from_id(name).unwrap(); + match c.value() { + 0 => (), + v @ 1.. if (v as usize) <= workspaces.len() => { + let workspace = workspaces[v as usize - 1]; + *area = Some(relative_rect(win, &workspace)) + } + v => warn!("Unexpected value in {name}: {v}!"), + } + } + sender.send(areas).unwrap(); + true + } + fltk::enums::Key::Escape => { + win.set_cursor(fltk::enums::Cursor::Default); + win.hide(); + true + } + _ => false, + }, + _ => false, + } + }); +} + +fn show_overlay_window(winctx: &mut InputAreaWindowContext) { + let win = &mut winctx.win; + if win.shown() { + return; + } + let screens = fltk::app::Screen::all_screens(); + winctx.workspaces.clear(); + winctx.workspaces.push(get_full_workspace_rect()); + for screen in &screens { + let fltk::draw::Rect { x, y, w, h } = screen.work_area(); + winctx.workspaces.push(Rect { + x: x as f64, + y: y as f64, + w: w as f64, + h: h as f64, + }); + } + for c in [ + &mut winctx.choice_mouse, + &mut winctx.choice_touch, + &mut winctx.choice_pen, + ] { + let v = c.value(); + c.clear(); + c.add_choice("None"); + c.add_choice("Full Workspace"); + for screen in &screens { + c.add_choice(&format!( + "Screen {n} at {w}x{h}+{x}+{y}", + n = screen.n, + w = screen.w(), + h = screen.h(), + x = screen.x(), + y = screen.y() + )); + } + if v >= 0 && (v as usize) < 2 + screens.len() { + c.set_value(v); + } else { + c.set_value(0); + } + } + if win.fullscreen_active() { + win.set_size(600, 600); + let n = win.screen_num(); + let screen = app::Screen::new(n).unwrap(); + win.set_pos( + screen.x() + (screen.w() - 600) / 2, + screen.y() + (screen.h() - 600) / 2, + ); + } + win.show(); + win.set_on_top(); + win.set_visible_focus(); +} + +pub fn get_full_workspace_rect() -> Rect { + let mut rect = Rect::default(); + for screen in fltk::app::Screen::all_screens() { + let fltk::draw::Rect { x, y, w, h } = screen.work_area(); + rect.x = (x as f64).min(rect.x); + rect.y = (y as f64).min(rect.y); + rect.w = ((x + w) as f64).max(rect.w); + rect.h = ((y + h) as f64).max(rect.h); + } + rect +} diff --git a/src/input/autopilot_device.rs b/src/input/autopilot_device.rs index 0c9ee96f..2746e299 100644 --- a/src/input/autopilot_device.rs +++ b/src/input/autopilot_device.rs @@ -39,6 +39,7 @@ impl InputDevice for AutoPilotDevice { } let (x_rel, y_rel, width_rel, height_rel) = match self.capturable.geometry().unwrap() { Geometry::Relative(x, y, width, height) => (x, y, width, height), + #[cfg(target_os = "windows")] _ => { warn!("Failed to get window geometry, sending no input"); return; diff --git a/src/input/autopilot_device_win.rs b/src/input/autopilot_device_win.rs index 8a2c79ff..d5736081 100644 --- a/src/input/autopilot_device_win.rs +++ b/src/input/autopilot_device_win.rs @@ -45,13 +45,12 @@ impl InputDevice for WindowsInput { warn!("Failed to activate window, sending no input ({})", err); return; } - let (offset_x, offset_y, width, height, left, top) = - match self.capturable.geometry().unwrap() { - Geometry::VirtualScreen(offset_x, offset_y, width, height, left, top) => { - (offset_x, offset_y, width, height, left, top) - } - _ => unreachable!(), - }; + let Geometry::VirtualScreen(offset_x, offset_y, width, height, left, top) = + self.capturable.geometry().unwrap() + else { + unreachable!() + }; + let (x, y) = ( (event.x * width as f64) as i32 + offset_x, (event.y * height as f64) as i32 + offset_y, diff --git a/src/input/device.rs b/src/input/device.rs index 424eebdf..920406ea 100644 --- a/src/input/device.rs +++ b/src/input/device.rs @@ -5,6 +5,7 @@ use crate::protocol::{KeyboardEvent, PointerEvent, WheelEvent}; pub enum InputDeviceType { AutoPilotDevice, UInputDevice, + #[cfg(target_os = "windows")] WindowsInput, } diff --git a/src/input/uinput_device.rs b/src/input/uinput_device.rs index 4a1ea9df..a58f7086 100644 --- a/src/input/uinput_device.rs +++ b/src/input/uinput_device.rs @@ -7,7 +7,7 @@ use crate::capturable::{Capturable, Geometry}; use crate::input::device::{InputDevice, InputDeviceType}; use crate::protocol::{ Button, KeyboardEvent, KeyboardEventType, KeyboardLocation, PointerEvent, PointerEventType, - PointerType, WheelEvent, + PointerType, Rect, WheelEvent, }; use crate::cerror::CError; @@ -36,10 +36,7 @@ pub struct UInputDevice { tool_pen_active: bool, pen_touching: bool, capturable: Box, - x: f64, - y: f64, - width: f64, - height: f64, + geometry: Rect, name_mouse_device: String, name_stylus_device: String, name_touch_device: String, @@ -103,10 +100,7 @@ impl UInputDevice { tool_pen_active: false, pen_touching: false, capturable, - x: 0.0, - y: 0.0, - width: 1.0, - height: 1.0, + geometry: Rect::default(), name_mouse_device: name_mouse, name_touch_device: name_touch, name_stylus_device: name_stylus, @@ -118,12 +112,12 @@ impl UInputDevice { } fn transform_x(&self, x: f64) -> i32 { - let x = (x * self.width + self.x) * ABS_MAX; + let x = (x * self.geometry.w + self.geometry.x) * ABS_MAX; x as i32 } fn transform_y(&self, y: f64) -> i32 { - let y = (y * self.height + self.y) * ABS_MAX; + let y = (y * self.geometry.h + self.geometry.y) * ABS_MAX; y as i32 } @@ -285,20 +279,28 @@ impl InputDevice for UInputDevice { } let (x, y, width, height) = match self.capturable.geometry().unwrap() { Geometry::Relative(x, y, width, height) => (x, y, width, height), - _ => { - warn!("Failed to get window geometry, sending no input"); - return; - } }; - self.x = x; - self.y = y; - self.width = width; - self.height = height; + self.geometry.x = x; + self.geometry.y = y; + self.geometry.w = width; + self.geometry.h = height; match event.pointer_type { PointerType::Touch => { if self.num_touch_mapping_tries < MAX_SCREEN_MAPPING_TRIES { if let Some(x11ctx) = &mut self.x11ctx { - x11ctx.map_input_device_to_entire_screen(&self.name_touch_device, false); + // Mapping input does not work on XWayland as xinput list does not expose + // device names and thus we can not identify the devices created by uinput + if let Ok(session_type) = std::env::var("XDG_SESSION_TYPE") { + if session_type != "wayland" { + x11ctx.map_input_device_to_entire_screen( + &self.name_touch_device, + false, + ); + } + } else { + x11ctx + .map_input_device_to_entire_screen(&self.name_touch_device, false); + } } self.num_touch_mapping_tries += 1; } @@ -440,7 +442,19 @@ impl InputDevice for UInputDevice { PointerType::Pen => { if self.num_stylus_mapping_tries < MAX_SCREEN_MAPPING_TRIES { if let Some(x11ctx) = &mut self.x11ctx { - x11ctx.map_input_device_to_entire_screen(&self.name_stylus_device, true); + // Mapping input does not work on XWayland as xinput list does not expose + // device names and thus we can not identify the devices created by uinput + if let Ok(session_type) = std::env::var("XDG_SESSION_TYPE") { + if session_type != "wayland" { + x11ctx.map_input_device_to_entire_screen( + &self.name_stylus_device, + true, + ); + } + } else { + x11ctx + .map_input_device_to_entire_screen(&self.name_stylus_device, true); + } } self.num_touch_mapping_tries += 1; } @@ -515,7 +529,19 @@ impl InputDevice for UInputDevice { PointerType::Mouse | PointerType::Unknown => { if self.num_mouse_mapping_tries < MAX_SCREEN_MAPPING_TRIES { if let Some(x11ctx) = &mut self.x11ctx { - x11ctx.map_input_device_to_entire_screen(&self.name_mouse_device, false); + // Mapping input does not work on XWayland as xinput list does not expose + // device names and thus we can not identify the devices created by uinput + if let Ok(session_type) = std::env::var("XDG_SESSION_TYPE") { + if session_type != "wayland" { + x11ctx.map_input_device_to_entire_screen( + &self.name_mouse_device, + false, + ); + } + } else { + x11ctx + .map_input_device_to_entire_screen(&self.name_mouse_device, false); + } } self.num_touch_mapping_tries += 1; } diff --git a/src/main.rs b/src/main.rs index 9b7f650c..85112be7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ use clap::CommandFactory; use clap_complete::generate; #[cfg(unix)] use signal_hook::iterator::Signals; +#[cfg(unix)] use signal_hook::{consts::TERM_SIGNALS, low_level::signal_name}; use tracing::{error, info, warn}; diff --git a/src/protocol.rs b/src/protocol.rs index 5229727f..f58c06d4 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -21,6 +21,7 @@ pub enum MessageInbound { Config(ClientConfiguration), PauseVideo, ResumeVideo, + ChooseCustomInputAreas, } #[derive(Serialize, Deserialize, Debug)] @@ -28,10 +29,37 @@ pub enum MessageOutbound { CapturableList(Vec), NewVideo, ConfigOk, + CustomInputAreas(CustomInputAreas), ConfigError(String), Error(String), } +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] +pub struct Rect { + pub x: f64, + pub y: f64, + pub w: f64, + pub h: f64, +} + +impl Default for Rect { + fn default() -> Self { + Self { + x: 0.0, + y: 0.0, + w: 1.0, + h: 1.0, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Default)] +pub struct CustomInputAreas { + pub mouse: Option, + pub touch: Option, + pub pen: Option, +} + #[derive(Serialize, Deserialize, Debug)] pub enum PointerType { #[serde(rename = "")] diff --git a/src/web.rs b/src/web.rs index 5fef7eb7..dc844d3f 100644 --- a/src/web.rs +++ b/src/web.rs @@ -43,6 +43,7 @@ struct IndexTemplateContext { uinput_enabled: bool, capture_cursor_enabled: bool, log_level: String, + enable_custom_input_areas: bool, } fn response_from_str(s: &str, content_type: &str) -> Response> { @@ -126,6 +127,7 @@ async fn serve( uinput_enabled: cfg!(target_os = "linux"), capture_cursor_enabled: cfg!(not(target_os = "windows")), log_level: crate::log::get_log_level().to_string(), + enable_custom_input_areas: context.web_config.enable_custom_input_areas, }; let html = if let Some(path) = context.web_config.custom_index_html.as_ref() { @@ -223,6 +225,7 @@ pub struct WebServerConfig { pub custom_access_html: Option, pub custom_style_css: Option, pub custom_lib_js: Option, + pub enable_custom_input_areas: bool, } struct Context<'a> { diff --git a/src/websocket.rs b/src/websocket.rs index 38be28fe..d877135f 100644 --- a/src/websocket.rs +++ b/src/websocket.rs @@ -7,7 +7,7 @@ use std::sync::{mpsc, Arc}; use std::thread::{spawn, JoinHandle}; use std::time::{Duration, Instant}; use tokio::sync::mpsc::channel; -use tracing::{debug, error, trace, warn}; +use tracing::{error, trace, warn}; use crate::capturable::{get_capturables, Capturable, Recorder}; use crate::input::device::{InputDevice, InputDeviceType}; @@ -61,6 +61,7 @@ pub struct WeylusClientConfig { pub encoder_options: EncoderOptions, #[cfg(target_os = "linux")] pub wayland_support: bool, + pub no_gui: bool, } impl WeylusClientHandler { @@ -119,6 +120,19 @@ impl WeylusClientHandler { MessageInbound::ResumeVideo => { self.video_sender.send(VideoCommands::Resume).unwrap() } + MessageInbound::ChooseCustomInputAreas => { + let (sender, receiver) = std::sync::mpsc::channel(); + crate::gui::get_input_area(self.config.no_gui, sender); + let mut sender = self.sender.clone(); + spawn(move || { + while let Ok(areas) = receiver.recv() { + send_message( + &mut sender, + MessageOutbound::CustomInputAreas(areas), + ); + } + }); + } } } Err(err) => { @@ -307,7 +321,7 @@ fn handle_video( last_frame = next_frame; if frames_passed > 0 { - debug!("Dropped {frames_passed} frame(s)!"); + trace!("Dropped {frames_passed} frame(s)!"); } match receiver.recv_timeout(if paused { EFFECTIVE_INIFINITY } else { timeout }) { @@ -487,7 +501,13 @@ pub fn weylus_websocket_channel( let frame = tokio::select! { _ = semaphore_shutdown.acquire() => break, - frame = fut => frame.unwrap(), + frame = fut => match frame { + Ok(frame) => frame, + Err(err) => { + warn!("Invalid websocket frame: {err}."); + break; + }, + }, }; match frame.opcode { OpCode::Close => break, diff --git a/src/weylus.rs b/src/weylus.rs index 9c4dc6aa..8d2c812c 100644 --- a/src/weylus.rs +++ b/src/weylus.rs @@ -61,11 +61,16 @@ impl Weylus { custom_access_html: config.custom_access_html.clone(), custom_style_css: config.custom_style_css.clone(), custom_lib_js: config.custom_lib_js.clone(), + #[cfg(target_os = "linux")] + enable_custom_input_areas: config.wayland_support, + #[cfg(not(target_os = "linux"))] + enable_custom_input_areas: false, }, WeylusClientConfig { encoder_options, #[cfg(target_os = "linux")] wayland_support: config.wayland_support, + no_gui: config.no_gui, }, ); diff --git a/ts/lib.ts b/ts/lib.ts index ae8a6e5e..e477441a 100644 --- a/ts/lib.ts +++ b/ts/lib.ts @@ -50,6 +50,18 @@ function run(level: string) { function log(level: LogLevel, msg: string) { if (level > log_level) return; + + if (level == LogLevel.TRACE) + console.trace(msg); + else if (level == LogLevel.DEBUG) + console.debug(msg); + else if (level == LogLevel.INFO) + console.info(msg); + else if (level == LogLevel.WARN) + console.warn(msg); + else if (level == LogLevel.ERROR) + console.error(msg); + if (no_log_messages) { no_log_messages = false; document.getElementById("log_section").classList.remove("hide"); @@ -82,6 +94,19 @@ function fresh_canvas() { return canvas; } +class Rect { + x: number; + y: number; + w: number; + h: number; +} + +class CustomInputAreas { + mouse: Rect; + touch: Rect; + pen: Rect; +} + class Settings { webSocket: WebSocket; checks: Map; @@ -94,6 +119,7 @@ class Settings { check_aggressive_seek: HTMLInputElement; client_name_input: HTMLInputElement; visible: boolean; + custom_input_areas: CustomInputAreas; settings: HTMLElement; constructor(webSocket: WebSocket) { @@ -108,10 +134,10 @@ class Settings { this.scale_video_output = this.scale_video_input.nextElementSibling as HTMLOutputElement; this.range_min_pressure = document.getElementById("min_pressure") as HTMLInputElement; this.client_name_input = document.getElementById("client_name") as HTMLInputElement; - this.frame_rate_input.oninput = (e) => { + this.frame_rate_input.oninput = () => { this.frame_rate_output.value = Math.round(frame_rate_scale(this.frame_rate_input.valueAsNumber)).toString(); } - this.scale_video_input.oninput = (e) => { + this.scale_video_input.oninput = () => { let [w, h] = calc_max_video_resolution(this.scale_video_input.valueAsNumber) this.scale_video_output.value = w + "x" + h } @@ -145,13 +171,13 @@ class Settings { this.settings.classList.add("vanish"); } - this.checks.get("stretch").onchange = (e) => { + this.checks.get("stretch").onchange = () => { stretch_video(); this.save_settings(); }; this.check_aggressive_seek = this.checks.get("aggressive_seeking"); - this.check_aggressive_seek.onchange = (e) => { + this.check_aggressive_seek.onchange = () => { this.save_settings(); }; @@ -180,6 +206,10 @@ class Settings { this.toggle_energysaving((e.target as HTMLInputElement).checked); }; + this.checks.get("enable_custom_input_areas").onchange = () => { + this.save_settings(); + }; + this.frame_rate_input.onchange = () => this.save_settings(); this.range_min_pressure.onchange = () => this.save_settings(); @@ -192,6 +222,10 @@ class Settings { this.frame_rate_input.onchange = upd_server_config; document.getElementById("refresh").onclick = () => this.webSocket.send('"GetCapturableList"'); + document.getElementById("custom_input_areas").onclick = () => { + this.webSocket.send('"ChooseCustomInputAreas"'); + this.checks.get("enable_custom_input_areas").checked = true; + }; this.capturable_select.onchange = () => this.send_server_config(); } @@ -218,6 +252,7 @@ class Settings { settings["frame_rate"] = frame_rate_scale(this.frame_rate_input.valueAsNumber).toString(); settings["scale_video"] = this.scale_video_input.value; settings["min_pressure"] = this.range_min_pressure.value; + settings["custom_input_areas"] = this.custom_input_areas; settings["client_name"] = this.client_name_input.value; localStorage.setItem("settings", JSON.stringify(settings)); } @@ -254,6 +289,8 @@ class Settings { if (min_pressure) this.range_min_pressure.value = min_pressure; + this.custom_input_areas = settings["custom_input_areas"]; + if (this.checks.get("lefty").checked) { this.settings.classList.add("lefty"); } @@ -270,6 +307,10 @@ class Settings { this.toggle_energysaving(true); } + if (document.getElementById("custom_input_areas").classList.contains("hide")) { + this.checks.get("enable_custom_input_areas").checked = false; + } + let client_name = settings["client_name"]; if (client_name) this.client_name_input.value = client_name; @@ -381,8 +422,28 @@ class PEvent { btn = 2; this.button = (btn < 0 ? 0 : 1 << btn); this.buttons = event.buttons; - this.x = (event.clientX - targetRect.left) / targetRect.width; - this.y = (event.clientY - targetRect.top) / targetRect.height; + let x_offset = 0; + let y_offset = 0; + let x_scale = 1; + let y_scale = 1; + if (settings.checks.get("enable_custom_input_areas").checked) { + let custom_input_area: Rect = null; + if (event.pointerType == "mouse") { + custom_input_area = settings.custom_input_areas.mouse; + } else if (event.pointerType == "touch") { + custom_input_area = settings.custom_input_areas.touch; + } else if (event.pointerType == "pen") { + custom_input_area = settings.custom_input_areas.pen; + } + if (custom_input_area) { + x_scale = custom_input_area.w; + y_scale = custom_input_area.h; + x_offset = custom_input_area.x; + y_offset = custom_input_area.y; + } + } + this.x = (event.clientX - targetRect.left) / targetRect.width * x_scale + x_offset; + this.y = (event.clientY - targetRect.top) / targetRect.height * y_scale + y_offset; this.movement_x = event.movementX ? event.movementX : 0; this.movement_y = event.movementY ? event.movementY : 0; this.pressure = Math.max(event.pressure, settings.range_min_pressure.valueAsNumber); @@ -820,6 +881,9 @@ function handle_messages( alert(msg["Error"]); else if ("ConfigError" in msg) { onConfigError(msg["ConfigError"]); + } else if ("CustomInputAreas" in msg) { + settings.custom_input_areas = msg["CustomInputAreas"]; + settings.save_settings(); } } diff --git a/www/static/style.css b/www/static/style.css index 5a259b93..49b7e410 100644 --- a/www/static/style.css +++ b/www/static/style.css @@ -151,7 +151,7 @@ form, form input { display: block; margin-top: 0.5em; } -#settings section.hide, section label.hide { +#settings section.hide, section label.hide, section button.hide { display: none !important; } select { diff --git a/www/templates/index.html b/www/templates/index.html index 83d8396d..d628c92e 100644 --- a/www/templates/index.html +++ b/www/templates/index.html @@ -56,6 +56,11 @@

Input

+ +