diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d261324d..86875770 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -90,7 +90,10 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} build-macos: - runs-on: macOS-latest + strategy: + matrix: + os: [macos-latest, macos-13] # -latest if for Apple Silicon, -13 is for Intel + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: actions/cache@v4 @@ -114,13 +117,15 @@ jobs: # run: MACOSX_DEPLOYMENT_TARGET=10.13 cargo bundle --release run: cargo bundle --release - name: Package - run: cd target/release/bundle/osx/ && zip -r macOS.zip Weylus.app + run: | + MACOS_BUILD_NAME=macos-$([ "${{ matrix.os }}" == "macos-latest" ] && echo "arm" || echo "intel") + echo "MACOS_BUILD_NAME=$MACOS_BUILD_NAME" >> $GITHUB_ENV + cd target/release/bundle/osx/ && zip -r ${MACOS_BUILD_NAME}.zip Weylus.app - name: Artifacts uses: actions/upload-artifact@v4 with: - name: ${{ runner.os }} - path: | - target/release/bundle/osx/macOS.zip + name: ${{ env.MACOS_BUILD_NAME }} + path: target/release/bundle/osx/${{ env.MACOS_BUILD_NAME }}.zip - name: ArtifactsDebug if: failure() uses: actions/upload-artifact@v4 diff --git a/deps/download.sh b/deps/download.sh index 28916047..8bd8f58d 100755 --- a/deps/download.sh +++ b/deps/download.sh @@ -3,7 +3,7 @@ set -ex test -d x264 || git clone --depth 1 -b stable https://code.videolan.org/videolan/x264.git x264 -test -d ffmpeg || git clone --depth 1 -b n7.0.2 https://git.ffmpeg.org/ffmpeg.git ffmpeg +test -d ffmpeg || git clone --depth 1 -b n7.1 https://git.ffmpeg.org/ffmpeg.git ffmpeg if [ "$TARGET_OS" == "linux" ]; then test -d nv-codec-headers || git clone --depth 1 https://git.videolan.org/git/ffmpeg/nv-codec-headers.git test -d libva || git clone --depth 1 -b 2.22.0 https://github.com/intel/libva 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/capturable/pipewire.rs b/src/capturable/pipewire.rs index e4dd1ee3..97cc1813 100644 --- a/src/capturable/pipewire.rs +++ b/src/capturable/pipewire.rs @@ -361,7 +361,7 @@ struct CallBackContext { streams: Vec, fd: Option, restore_token: Option, - is_plasma: bool, + has_remote_desktop: bool, failure: bool, } @@ -387,10 +387,10 @@ fn on_create_session_response( .into(); context.lock().unwrap().session = session.clone(); - if context.lock().unwrap().is_plasma { - select_sources(portal, context) - } else { + if context.lock().unwrap().has_remote_desktop { select_devices(portal, context) + } else { + select_sources(portal, context) } } @@ -461,7 +461,8 @@ fn select_sources( // 4: Metadata: The cursor is not part of the screen cast stream, but sent as PipeWire stream metadata. let cursor_mode = if capture_cursor { 2u32 } else { 1u32 }; - if context.lock().unwrap().is_plasma && capture_cursor { + let is_plasma = std::env::var("DESKTOP_SESSION").map_or(false, |s| s.contains("plasma")); + if is_plasma && capture_cursor { // Warn the user if capturing the cursor is tried on kde as this can crash // kwin_wayland and tear down the plasma desktop, see: // https://bugs.kde.org/show_bug.cgi?id=435042 @@ -491,15 +492,15 @@ fn on_select_sources_response( "handle_token".to_string(), Variant(Box::new(format!("weylus{t}"))), ); - let path = if context.lock().unwrap().is_plasma { - OrgFreedesktopPortalScreenCast::start( + let path = if context.lock().unwrap().has_remote_desktop { + OrgFreedesktopPortalRemoteDesktop::start( &portal, context.lock().unwrap().session.clone(), "", args, )? } else { - OrgFreedesktopPortalRemoteDesktop::start( + OrgFreedesktopPortalScreenCast::start( &portal, context.lock().unwrap().session.clone(), "", @@ -527,10 +528,10 @@ fn on_start_response( context.restore_token = Some(t.to_string()); } dbg!(&context.restore_token); - if context.is_plasma { - debug!("Screen Cast Session started"); - } else { + if context.has_remote_desktop { debug!("Remote Desktop Session started"); + } else { + debug!("Screen Cast Session started"); } Ok(()) } @@ -541,7 +542,10 @@ fn request_remote_desktop( let conn = SyncConnection::new_session()?; let portal = get_portal(&conn); - let is_plasma = std::env::var("DESKTOP_SESSION").map_or(false, |s| s.contains("plasma")); + // Disabled for KDE plasma due to https://bugs.kde.org/show_bug.cgi?id=484996 + // List of supported DEs: https://wiki.archlinux.org/title/XDG_Desktop_Portal#List_of_backends_and_interfaces + let has_remote_desktop = + std::env::var("DESKTOP_SESSION").map_or(false, |s| s.contains("gnome")); let context = CallBackContext { capture_cursor, @@ -549,7 +553,7 @@ fn request_remote_desktop( streams: Default::default(), fd: None, restore_token: None, - is_plasma, + has_remote_desktop, failure: false, }; let context = Arc::new(Mutex::new(context)); @@ -565,10 +569,10 @@ fn request_remote_desktop( "handle_token".to_string(), Variant(Box::new(format!("weylus{t2}"))), ); - let path = if is_plasma { - OrgFreedesktopPortalScreenCast::create_session(&portal, args)? - } else { + let path = if has_remote_desktop { OrgFreedesktopPortalRemoteDesktop::create_session(&portal, args)? + } else { + OrgFreedesktopPortalScreenCast::create_session(&portal, args)? }; handle_response(portal, path, context.clone(), on_create_session_response)?; 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 ae8458dc..a6fd9151 100644 --- a/src/input/autopilot_device.rs +++ b/src/input/autopilot_device.rs @@ -49,6 +49,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 51af8869..d74dc732 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, @@ -63,17 +62,12 @@ impl InputDevice for WindowsInput { PointerEventType::DOWN => { POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT | POINTER_FLAG_DOWN } - PointerEventType::MOVE => { - POINTER_FLAG_INRANGE | POINTER_FLAG_UPDATE // POINTER_FLAG_INCONTACT see below "buttons" part + PointerEventType::MOVE | PointerEventType::OVER | PointerEventType::ENTER => { + POINTER_FLAG_INRANGE | POINTER_FLAG_UPDATE } - PointerEventType::UP => { - POINTER_FLAG_INRANGE | POINTER_FLAG_UP - } - PointerEventType::CANCEL => { - POINTER_FLAG_CANCELED | POINTER_FLAG_UP - } - PointerEventType::LEAVE => { - POINTER_FLAG_NONE // anything but POINTER_FLAG_INRANGE + PointerEventType::UP => POINTER_FLAG_UP, + PointerEventType::CANCEL | PointerEventType::LEAVE | PointerEventType::OUT => { + POINTER_FLAG_INRANGE | POINTER_FLAG_UPDATE | POINTER_FLAG_CANCELED } }; let button_change_type = match event.buttons { @@ -162,9 +156,15 @@ impl InputDevice for WindowsInput { InjectSyntheticPointerInput(self.touch_device_handle, m, len as u32); match event.event_type { - PointerEventType::DOWN | PointerEventType::MOVE => {} - - PointerEventType::UP | PointerEventType::CANCEL => { + PointerEventType::DOWN + | PointerEventType::MOVE + | PointerEventType::OVER + | PointerEventType::ENTER => {} + + PointerEventType::UP + | PointerEventType::CANCEL + | PointerEventType::LEAVE + | PointerEventType::OUT => { self.multitouch_map.remove(&event.pointer_id); } @@ -195,7 +195,7 @@ impl InputDevice for WindowsInput { } _ => {} }, - PointerEventType::MOVE => { + PointerEventType::MOVE | PointerEventType::OVER | PointerEventType::ENTER => { unsafe { SetCursorPos(screen_x, screen_y) }; } PointerEventType::UP => match event.button { @@ -210,7 +210,7 @@ impl InputDevice for WindowsInput { } _ => {} }, - PointerEventType::CANCEL => { + PointerEventType::CANCEL | PointerEventType::LEAVE | PointerEventType::OUT => { dw_flags |= MOUSEEVENTF_LEFTUP; }, PointerEventType::ENTER | PointerEventType::LEAVE => { 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 b5d62aa4..80e80230 100644 --- a/src/input/uinput_device.rs +++ b/src/input/uinput_device.rs @@ -1,13 +1,14 @@ use std::cmp::Ordering; use std::ffi::CString; use std::os::raw::{c_char, c_int}; +use std::time::{Duration, Instant}; use crate::capturable::x11::X11Context; 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; @@ -35,11 +36,9 @@ pub struct UInputDevice { touches: [Option; 5], tool_pen_active: bool, pen_touching: bool, + last_pen_event: Instant, capturable: Box, - x: f64, - y: f64, - width: f64, - height: f64, + geometry: Rect, name_mouse_device: String, name_stylus_device: String, name_touch_device: String, @@ -102,11 +101,9 @@ impl UInputDevice { touches: Default::default(), tool_pen_active: false, pen_touching: false, + last_pen_event: Instant::now(), 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 +115,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,25 +282,58 @@ 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; } + + // This is a workaround for browsers that send events when the pen is hovering but + // do not send an event when the pen leaves the hovering range. If the pen is left + // in this state touch rejection may stay active and touch won't work. + // Therefore, we manually remove the pen after a short delay. + if self.tool_pen_active + && !self.pen_touching + && (Instant::now() - self.last_pen_event) > Duration::from_millis(50) + { + self.tool_pen_active = false; + self.send(self.stylus_fd, ET_KEY, EC_KEY_TOUCH, 0); + self.send(self.stylus_fd, ET_KEY, EC_KEY_TOOL_PEN, 0); + self.send(self.stylus_fd, ET_KEY, EC_KEY_TOOL_RUBBER, 0); + self.send(self.stylus_fd, ET_ABSOLUTE, EC_ABSOLUTE_PRESSURE, 0); + self.send( + self.stylus_fd, + ET_MSC, + EC_MSC_TIMESTAMP, + (event.timestamp % (i32::MAX as u64 + 1)) as i32, + ); + self.send(self.stylus_fd, ET_SYNC, EC_SYNC_REPORT, 0); + } match event.event_type { - PointerEventType::DOWN | PointerEventType::MOVE => { + PointerEventType::DOWN + | PointerEventType::MOVE + | PointerEventType::OVER + | PointerEventType::ENTER => { let slot: usize; // check if this event is already assigned to one of our 10 multitouch slots if let Some(s) = self.find_slot(event.pointer_id) { @@ -411,7 +441,10 @@ impl InputDevice for UInputDevice { ); self.send(self.touch_fd, ET_SYNC, EC_SYNC_REPORT, 0); } - PointerEventType::CANCEL | PointerEventType::UP => { + PointerEventType::CANCEL + | PointerEventType::UP + | PointerEventType::LEAVE + | PointerEventType::OUT => { // remove from slot if let Some(slot) = self.find_slot(event.pointer_id) { self.send(self.touch_fd, ET_ABSOLUTE, EC_ABS_MT_SLOT, slot as i32); @@ -435,18 +468,33 @@ impl InputDevice for UInputDevice { self.touches[slot] = None; } } - PointerEventType::ENTER | PointerEventType::LEAVE => () }; } PointerType::Pen => { + self.last_pen_event = Instant::now(); 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; } match event.event_type { - PointerEventType::DOWN | PointerEventType::MOVE => { + PointerEventType::DOWN + | PointerEventType::MOVE + | PointerEventType::OVER + | PointerEventType::ENTER => { if let PointerEventType::DOWN = event.event_type { self.pen_touching = true; self.send(self.stylus_fd, ET_KEY, EC_KEY_TOUCH, 1); @@ -496,7 +544,10 @@ impl InputDevice for UInputDevice { event.tilt_y, ); } - PointerEventType::UP | PointerEventType::CANCEL => { + PointerEventType::UP + | PointerEventType::CANCEL + | PointerEventType::LEAVE + | PointerEventType::OUT => { self.send(self.stylus_fd, ET_KEY, EC_KEY_TOUCH, 0); self.send(self.stylus_fd, ET_KEY, EC_KEY_TOOL_PEN, 0); self.send(self.stylus_fd, ET_KEY, EC_KEY_TOOL_RUBBER, 0); @@ -504,7 +555,6 @@ impl InputDevice for UInputDevice { self.tool_pen_active = false; self.pen_touching = false; } - PointerEventType::ENTER | PointerEventType::LEAVE => () } self.send( self.stylus_fd, @@ -517,12 +567,27 @@ 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; } match event.event_type { - PointerEventType::DOWN | PointerEventType::MOVE => { + PointerEventType::DOWN + | PointerEventType::MOVE + | PointerEventType::OVER + | PointerEventType::ENTER => { if let PointerEventType::DOWN = event.event_type { match event.button { Button::PRIMARY => { @@ -550,7 +615,10 @@ impl InputDevice for UInputDevice { self.transform_y(event.y), ); } - PointerEventType::UP | PointerEventType::CANCEL => match event.button { + PointerEventType::UP + | PointerEventType::CANCEL + | PointerEventType::LEAVE + | PointerEventType::OUT => match event.button { Button::PRIMARY => self.send(self.mouse_fd, ET_KEY, EC_KEY_MOUSE_LEFT, 0), Button::SECONDARY => { self.send(self.mouse_fd, ET_KEY, EC_KEY_MOUSE_RIGHT, 0) @@ -560,7 +628,6 @@ impl InputDevice for UInputDevice { } _ => (), }, - PointerEventType::ENTER | PointerEventType::LEAVE => (), } self.send( self.mouse_fd, 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 591f44fe..1f5eb347 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,11 +29,38 @@ pub enum MessageOutbound { CapturableList(Vec), NewVideo, ConfigOk, + CustomInputAreas(CustomInputAreas), ConfigError(String), Error(String), } -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +#[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, PartialEq)] pub enum PointerType { #[serde(rename = "")] Unknown, @@ -44,7 +72,7 @@ pub enum PointerType { Touch, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] pub enum PointerEventType { #[serde(rename = "pointerdown")] DOWN, @@ -54,10 +82,14 @@ pub enum PointerEventType { CANCEL, #[serde(rename = "pointermove")] MOVE, + #[serde(rename = "pointerover")] + OVER, #[serde(rename = "pointerenter")] ENTER, #[serde(rename = "pointerleave")] LEAVE, + #[serde(rename = "pointerout")] + OUT, } #[derive(Serialize, Deserialize, Debug)] 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 32ff1a0e..df449f18 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,23 @@ class Settings { this.settings.classList.add("vanish"); } - this.checks.get("stretch").onchange = (e) => { + this.checks.get("stretch").onchange = () => { stretch_video(); this.save_settings(); }; + this.checks.get("enable_debug_overlay").onchange = (e) => { + let enabled = (e.target as HTMLInputElement).checked; + if (enabled) { + debug_overlay.classList.remove("hide"); + } else { + debug_overlay.classList.add("hide"); + } + 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 +216,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 +232,9 @@ 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.capturable_select.onchange = () => this.send_server_config(); } @@ -218,6 +261,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 +298,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 +316,15 @@ class Settings { this.toggle_energysaving(true); } + if (this.checks.get("enable_debug_overlay").checked) { + debug_overlay.classList.remove("hide"); + } + + + 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; @@ -345,6 +400,8 @@ class Settings { } let settings: Settings; +let debug_overlay: HTMLElement; +let last_pointer_data: Object; class PEvent { event_type: string; @@ -381,8 +438,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); @@ -623,8 +700,10 @@ class PointerHandler { video.onpointerup = (e) => this.onEvent(e, "pointerup"); video.onpointercancel = (e) => this.onEvent(e, "pointercancel"); video.onpointermove = (e) => this.onEvent(e, "pointermove"); - video.onpointerenter = (e) => this.onEvent(e, "pointerenter"); + video.onpointerout = (e) => this.onEvent(e, "pointerout"); video.onpointerleave = (e) => this.onEvent(e, "pointerleave"); + video.onpointerenter = (e) => this.onEvent(e, "pointerenter"); + video.onpointerover = (e) => this.onEvent(e, "pointerover"); let painter: Painter; if (!settings.checks.get("energysaving").checked) @@ -635,11 +714,19 @@ class PointerHandler { canvas.onpointerup = (e) => { this.onEvent(e, "pointerup"); painter.onstop(e); }; canvas.onpointercancel = (e) => { this.onEvent(e, "pointercancel"); painter.onstop(e); }; canvas.onpointermove = (e) => { this.onEvent(e, "pointermove"); painter.onmove(e); }; + canvas.onpointerout = (e) => { this.onEvent(e, "pointerout"); painter.onstop(e); }; + canvas.onpointerleave = (e) => { this.onEvent(e, "pointerleave"); painter.onstop(e); }; + canvas.onpointerenter = (e) => { this.onEvent(e, "pointerenter"); painter.onmove(e); }; + canvas.onpointerover = (e) => { this.onEvent(e, "pointerover"); painter.onmove(e); }; } else { canvas.onpointerdown = (e) => this.onEvent(e, "pointerdown"); canvas.onpointerup = (e) => this.onEvent(e, "pointerup"); canvas.onpointercancel = (e) => this.onEvent(e, "pointercancel"); canvas.onpointermove = (e) => this.onEvent(e, "pointermove"); + canvas.onpointerout = (e) => this.onEvent(e, "pointerout"); + canvas.onpointerleave = (e) => this.onEvent(e, "pointerleave"); + canvas.onpointerenter = (e) => this.onEvent(e, "pointerenter"); + canvas.onpointerover = (e) => this.onEvent(e, "pointerover"); } canvas.onpointerenter = (e) => this.onEvent(e, "pointerenter"); canvas.onpointerleave = (e) => this.onEvent(e, "pointerleave"); @@ -658,6 +745,65 @@ class PointerHandler { } onEvent(event: PointerEvent, event_type: string) { + if (settings.checks.get("enable_debug_overlay").checked) { + let props = [ + "altKey", + "altitudeAngle", + "azimuthAngle", + "button", + "buttons", + "clientX", + "clientY", + "ctrlKey", + "height", + "isPrimary", + "metaKey", + "movementX", + "movementY", + "offsetX", + "offsetY", + "pageX", + "pageY", + "pointerId", + "pointerType", + "pressure", + "screenX", + "screenY", + "shiftKey", + "tangentialPressure", + "tiltX", + "tiltY", + "timeStamp", + "twist", + "type", + "width", + "x", + "y", + ]; + if (!last_pointer_data) { + last_pointer_data = {}; + for (let prop of props) { + let span_id = `prop_${prop}_span`; + let span = document.getElementById(span_id); + span = document.createElement("span"); + span.id = span_id; + debug_overlay.appendChild(span); + debug_overlay.appendChild(document.createElement("br")); + } + } + for (let prop of props) { + let span_id = `prop_${prop}_span`; + let span = document.getElementById(span_id); + let v = event[prop]; + span.textContent = `${prop}: ${v}`; + if (last_pointer_data[prop] == v) { + span.classList.remove("updated"); + } else { + span.classList.add("updated"); + last_pointer_data[prop] = v; + } + } + } if (this.pointerTypes.includes(event.pointerType)) { let rect = (event.target as HTMLElement).getBoundingClientRect(); const events = event_type === "pointermove" && typeof event.getCoalescedEvents === 'function' ? event.getCoalescedEvents() : [event]; @@ -824,6 +970,10 @@ 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.checks.get("enable_custom_input_areas").checked = true; + settings.save_settings(); } } @@ -884,6 +1034,7 @@ function init() { ); webSocket.binaryType = "arraybuffer"; + debug_overlay = document.getElementById("debug_overlay"); settings = new Settings(webSocket); let video = document.getElementById("video") as HTMLVideoElement; @@ -894,6 +1045,11 @@ function init() { event.stopPropagation(); return false; }; + canvas.oncontextmenu = function(event) { + event.preventDefault(); + event.stopPropagation(); + return false; + }; let toggle_fullscreen_btn = document.getElementById("fullscreen") as HTMLButtonElement; diff --git a/weylus_tls.sh b/weylus_tls.sh index a2d20e71..9b66a84e 100755 --- a/weylus_tls.sh +++ b/weylus_tls.sh @@ -4,7 +4,6 @@ function die { # cleanup to ensure restarting this script doesn't fail because # of ports that are still in use kill $(jobs -p) > /dev/null 2>&1 - rm -f index_tls.html exit $1 } @@ -49,28 +48,17 @@ fi # cleanup on CTRL+C trap die SIGINT -# The TLS proxy will be set up as follows: -# Proxy all incoming traffic from ports 1701 and 9001 to 1702 and -# 9002 on which the actual instance of Weylus is running. -# -# This means the websocket port that Weylus encodes into the -# index.html is the unencrypted port 9002 which is changed to the -# encrypted version on port 9001 by specifiying a custom index html. -$WEYLUS --print-index-html | sed 's/{{websocket_port}}/9001/' > index_tls.html +# The TLS proxy will be set up as follows: Proxy all incoming traffic from +# port 1701 to 1702 on which the actual instance of Weylus is running. # start Weylus listening only on the local interface -$WEYLUS --custom-index-html index_tls.html \ - --bind-address 127.0.0.1 \ - --web-port 1702 \ - --websocket-port 9002 \ +$WEYLUS --bind-address "127.0.0.1" \ + --web-port "1702" \ --access-code "$ACCESS_CODE" \ --no-gui & # start the proxy -hitch --frontend=[0.0.0.0]:1701 --backend=[127.0.0.1]:1702 \ - --daemon=off --tls-protos="TLSv1.2 TLSv1.3" weylus.pem & - -hitch --frontend=[0.0.0.0]:9001 --backend=[127.0.0.1]:9002 \ - --daemon=off --tls-protos="TLSv1.2 TLSv1.3" weylus.pem & +hitch --frontend="[0.0.0.0]:1701" --backend="[127.0.0.1]:1702" \ + --daemon=off --tls-protos="TLSv1.2 TLSv1.3" "weylus.pem" & wait diff --git a/www/static/style.css b/www/static/style.css index 5a259b93..41d48cbc 100644 --- a/www/static/style.css +++ b/www/static/style.css @@ -1,37 +1,15 @@ @media (prefers-color-scheme: dark) { - body, html { - background: #202020; - } - #settings { - color: #ddd; - background: #303030; - } - #handle { - background: #303030; - color: #ddd; - } - form { - background: #303030; - color: #ddd; - border: 0.5em solid #252525; + :root { + --color: #ddd; + --background-color-1: #303030; + --background-color-2: #202020; } } @media (prefers-color-scheme: light) { - body, html { - background: #fff; - } - #settings { - color: #111; - background: #eee; - } - #handle { - background: #eee; - color: #111; - } - form { - background: #eee; - color: #111; - border: 0.5em solid #f7f7f7; + :root { + --color: #111; + --background-color-1: #eee; + --background-color-2: #fff; } } main { @@ -50,6 +28,8 @@ body, html, main { padding: 0; overflow: hidden; display: flex; + color: var(--color); + background: var(--background-color-2); } video { max-width: 100%; @@ -76,10 +56,6 @@ input[type='text'] { input[type='range']::-webkit-slider-runnable-track, input[type="range"]::-moz-range-track { background-color: #00aaff; } -form, form input { - font-size: 1.4em; - display: grid; -} .container { width: 100%; height: 100%; @@ -87,6 +63,10 @@ form, form input { justify-content: center; align-items: center; } +#settings, #handle, #debug_overlay { + color: var(--color); + background: var(--background-color-1); +} #settings_scroll { overflow: auto; width: 100%; @@ -151,7 +131,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, #debug_overlay.hide { display: none !important; } select { @@ -205,3 +185,25 @@ label input:disabled + span { -moz-user-select: text; -ms-user-select: text; } +#debug_overlay { + --padding: 3px; + position: absolute; + top:0; + bottom: 0; + left: 0; + right: 0; + margin: auto; + width: fit-content; + height: fit-content; + z-index: 1; + pointer-events: none; + opacity: 75%; + padding: var(--padding); + border: 2px solid var(--color); + white-space: pre; + font-size: small; + text-align: center; +} +#debug_overlay span.updated { + color: darkgreen; +} diff --git a/www/templates/index.html b/www/templates/index.html index 83d8396d..17ab93a9 100644 --- a/www/templates/index.html +++ b/www/templates/index.html @@ -18,6 +18,7 @@
+
@@ -56,6 +57,11 @@

Input

+ +