diff --git a/catacomb_ipc/src/lib.rs b/catacomb_ipc/src/lib.rs index 8529650..0506dca 100644 --- a/catacomb_ipc/src/lib.rs +++ b/catacomb_ipc/src/lib.rs @@ -141,11 +141,16 @@ pub enum IpcMessage { /// Output power management. Dpms { /// Desired power management state; leave empty to get current state. - state: Option, + state: Option, }, /// Reply for DPMS state request. #[cfg_attr(feature = "clap", clap(skip))] - DpmsReply { state: DpmsState }, + DpmsReply { state: CliToggle }, + /// Set touch cursor visibility. + Cursor { + /// Desired touch cursor visibility. + state: CliToggle, + }, } /// Device orientation. @@ -192,10 +197,10 @@ impl Orientation { } } -/// Output power state. +/// Cli argument that allows enabling or disabling a system. #[cfg_attr(feature = "clap", derive(ValueEnum))] #[derive(Deserialize, Serialize, PartialEq, Eq, Copy, Clone, Debug)] -pub enum DpmsState { +pub enum CliToggle { On, Off, } diff --git a/src/catacomb.rs b/src/catacomb.rs index f270c6b..c4da7ca 100644 --- a/src/catacomb.rs +++ b/src/catacomb.rs @@ -3,7 +3,7 @@ use std::cell::RefCell; use std::sync::Arc; use std::time::{Duration, Instant}; -use std::{cmp, env}; +use std::{cmp, env, mem}; use _decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode; use _server_decoration::server::org_kde_kwin_server_decoration_manager::Mode as ManagerMode; @@ -26,7 +26,7 @@ use smithay::reexports::wayland_server::protocol::wl_output::WlOutput; use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::reexports::wayland_server::{Client, Display, DisplayHandle, Resource}; -use smithay::utils::{Logical, Rectangle, Serial, SERIAL_COUNTER}; +use smithay::utils::{Logical, Point, Rectangle, Serial, SERIAL_COUNTER}; use smithay::wayland::buffer::BufferHandler; use smithay::wayland::compositor; use smithay::wayland::compositor::{CompositorClientState, CompositorHandler, CompositorState}; @@ -114,6 +114,7 @@ pub struct Catacomb { pub key_bindings: Vec, pub touch_state: TouchState, pub frame_pacer: FramePacer, + pub draw_cursor: bool, pub seat_name: String, pub display_on: bool, pub windows: Windows, @@ -137,6 +138,7 @@ pub struct Catacomb { seat_state: SeatState, shm_state: ShmState, + last_cursor_position: Option>, accelerometer_token: RegistrationToken, idle_inhibitors: Vec, last_focus: Option, @@ -318,10 +320,12 @@ impl Catacomb { seat, accelerometer_token: accel_token, display_on: true, + last_cursor_position: Default::default(), idle_inhibitors: Default::default(), key_bindings: Default::default(), ime_override: Default::default(), frame_pacer: Default::default(), + draw_cursor: Default::default(), last_focus: Default::default(), terminated: Default::default(), stalled: Default::default(), @@ -369,15 +373,19 @@ impl Catacomb { let inhibited = inhibitors.any(|surface| self.windows.surface_visible(surface)); self.idle_notifier_state.set_is_inhibited(inhibited); + // Check whether touch cursor should be drawn. + let cursor_position = self.touch_state.position().filter(|_| self.draw_cursor); + let last_cursor_position = mem::replace(&mut self.last_cursor_position, cursor_position); + // Redraw only when there is damage present. - if self.windows.damaged() { + if self.windows.damaged() || last_cursor_position != cursor_position { // Apply pending client updates. if let Some(renderer) = self.backend.renderer() { self.windows.import_buffers(renderer); } // Draw all visible clients. - let rendered = self.backend.render(&mut self.windows); + let rendered = self.backend.render(&mut self.windows, cursor_position); // Update render time prediction. let frame_interval = self.output().frame_interval(); diff --git a/src/drawing.rs b/src/drawing.rs index 0150659..f1c4d6d 100644 --- a/src/drawing.rs +++ b/src/drawing.rs @@ -41,13 +41,19 @@ const DROP_TARGET_RGBA: [u8; 4] = [32, 32, 32, 64]; const GESTURE_NOTCH_PERCENTAGE: f64 = 0.2; /// Gesture handle color with automatic IME control. -const GESTURE_HANDLE_DEFAULT_RGBA: [u8; 3] = [255; 3]; +const GESTURE_HANDLE_DEFAULT_RGB: [u8; 3] = [255; 3]; /// Gesture handle color with IME force-enabled. -const GESTURE_HANDLE_LOCKED_RGBA: [u8; 3] = [42, 117, 42]; +const GESTURE_HANDLE_LOCKED_RGB: [u8; 3] = [42, 117, 42]; /// Gesture handle color with IME force-disabled. -const GESTURE_HANDLE_BLOCKED_RGBA: [u8; 3] = [117, 42, 42]; +const GESTURE_HANDLE_BLOCKED_RGB: [u8; 3] = [117, 42, 42]; + +/// Color of the touch cursor. +const CURSOR_RGBA: [u8; 4] = [86, 33, 33, 192]; + +/// Width and height of the touch cursor texture. +const CURSOR_SIZE: f64 = 32.; /// Cached texture. /// @@ -344,6 +350,7 @@ pub struct Graphics { gesture_handle_default: Option, gesture_handle_blocked: Option, gesture_handle_locked: Option, + cursor: Option, } impl Graphics { @@ -351,7 +358,6 @@ impl Graphics { let active_drop_target = Texture::from_buffer(renderer, 1., &ACTIVE_DROP_TARGET_RGBA, 1, 1, false); let drop_target = Texture::from_buffer(renderer, 1., &DROP_TARGET_RGBA, 1, 1, false); - let urgency_icon = Texture::from_buffer(renderer, 1., &URGENCY_ICON_RGBA, 1, 1, true); Self { @@ -361,6 +367,7 @@ impl Graphics { gesture_handle_default: None, gesture_handle_blocked: None, gesture_handle_locked: None, + cursor: None, } } @@ -372,9 +379,9 @@ impl Graphics { ime_override: Option, ) -> RenderTexture { let (handle, color) = match ime_override { - None => (&mut self.gesture_handle_default, GESTURE_HANDLE_DEFAULT_RGBA), - Some(true) => (&mut self.gesture_handle_locked, GESTURE_HANDLE_LOCKED_RGBA), - Some(false) => (&mut self.gesture_handle_blocked, GESTURE_HANDLE_BLOCKED_RGBA), + None => (&mut self.gesture_handle_default, GESTURE_HANDLE_DEFAULT_RGB), + Some(true) => (&mut self.gesture_handle_locked, GESTURE_HANDLE_LOCKED_RGB), + Some(false) => (&mut self.gesture_handle_blocked, GESTURE_HANDLE_BLOCKED_RGB), }; // Initialize texture or replace it after scale change. @@ -412,6 +419,34 @@ impl Graphics { // SAFETY: The code above ensures the `Option` is `Some`. unsafe { handle.clone().unwrap_unchecked() } } + + /// Get texture for the touch cursor. + pub fn cursor(&mut self, renderer: &mut GlesRenderer, canvas: &Canvas) -> RenderTexture { + let scale = canvas.scale(); + let size = (CURSOR_SIZE * scale).round() as i32; + if self.cursor.as_ref().map_or(true, |cursor| { + cursor.texture.width() != size as u32 || cursor.texture.height() != size as u32 + }) { + // Create a texture with a circle inside it. + let mut buffer = vec![0; (size * size * 4) as usize]; + for x in 0..size { + let x_delta = (size as f64 / 2. - x as f64).floor(); + for y in 0..size { + let y_delta = (size as f64 / 2. - y as f64).floor(); + if x_delta.powi(2) + y_delta.powi(2) <= (size as f64 / 2.).powi(2) { + let offset = (y * size + x) as usize * 4; + buffer[offset..offset + 4].copy_from_slice(&CURSOR_RGBA); + } + } + } + + let texture = Texture::from_buffer(renderer, scale, &buffer, size, size, false); + self.cursor = Some(RenderTexture(Rc::new(texture))); + } + + // SAFETY: The code above ensures the `Option` is `Some`. + unsafe { self.cursor.clone().unwrap_unchecked() } + } } /// Surface data store. diff --git a/src/input.rs b/src/input.rs index ea099d5..3b610ca 100644 --- a/src/input.rs +++ b/src/input.rs @@ -48,7 +48,6 @@ const POINTER_TOUCH_SLOT: Option = Some(0); /// Touch input state. pub struct TouchState { pub user_gestures: Vec, - pub position: Point, last_tap: Option<(Instant, Point)>, pending_single_tap: Option, @@ -58,6 +57,7 @@ pub struct TouchState { tap_surface: Option, active_app_id: Option, velocity: Point, + position: Point, events: Vec, slot: Option, start: TouchStart, @@ -118,6 +118,11 @@ impl TouchState { self.slot.is_some() } + /// Get current touch location. + pub fn position(&self) -> Option> { + self.slot.map(|_| self.position) + } + /// Get the updated active touch action. fn action(&mut self, canvas: &Canvas) -> Option { // Process handle gestures even before completion. @@ -568,6 +573,10 @@ impl Catacomb { /// Handle touch input movement. fn on_touch_motion(&mut self, event: TouchEvent) { + // Always update touch position to ensure accurate cursor location. + self.touch_state.velocity = event.position - self.touch_state.position; + self.touch_state.position = event.position; + // Handle client input. if let Some(input_surface) = &self.touch_state.input_surface { // Convert position to pre-window scaling. @@ -587,7 +596,6 @@ impl Catacomb { return; } - self.touch_state.velocity = event.position - self.touch_state.position; self.update_position(event.position); } diff --git a/src/ipc_server.rs b/src/ipc_server.rs index 3cd829c..235518c 100644 --- a/src/ipc_server.rs +++ b/src/ipc_server.rs @@ -6,7 +6,7 @@ use std::io::{BufRead, BufReader, Write}; use std::os::unix::net::{UnixListener, UnixStream}; use std::path::PathBuf; -use catacomb_ipc::{AppIdMatcher, DpmsState, IpcMessage, Keysym, WindowScale}; +use catacomb_ipc::{AppIdMatcher, CliToggle, IpcMessage, Keysym, WindowScale}; use smithay::reexports::calloop::LoopHandle; use tracing::{error, warn}; @@ -149,12 +149,15 @@ fn handle_message(buffer: &mut String, mut stream: UnixStream, catacomb: &mut Ca }); }, IpcMessage::Dpms { state: Some(state) } => { - catacomb.set_display_status(state == DpmsState::On) + catacomb.set_display_status(state == CliToggle::On) }, IpcMessage::Dpms { state: None } => { - let state = if catacomb.display_on { DpmsState::On } else { DpmsState::Off }; + let state = if catacomb.display_on { CliToggle::On } else { CliToggle::Off }; send_reply(&mut stream, &IpcMessage::DpmsReply { state }); }, + IpcMessage::Cursor { state } => { + catacomb.draw_cursor = state == CliToggle::On; + }, // Ignore IPC replies. IpcMessage::DpmsReply { .. } => (), } diff --git a/src/main.rs b/src/main.rs index c215932..a597676 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ use std::os::unix::process::CommandExt; use std::process::{Command, Stdio}; use std::{env, io, ptr}; -use catacomb_ipc::{DpmsState, IpcMessage}; +use catacomb_ipc::{CliToggle, IpcMessage}; use clap::{Parser, Subcommand}; #[cfg(feature = "profiling")] use profiling::puffin; @@ -58,8 +58,8 @@ pub fn main() { match Options::parse().subcommands { Some(Subcommands::Msg(msg)) => match catacomb_ipc::send_message(&msg) { Err(err) => eprintln!("\x1b[31merror\x1b[0m: {err}"), - Ok(Some(IpcMessage::DpmsReply { state: DpmsState::On })) => println!("on"), - Ok(Some(IpcMessage::DpmsReply { state: DpmsState::Off })) => println!("off"), + Ok(Some(IpcMessage::DpmsReply { state: CliToggle::On })) => println!("on"), + Ok(Some(IpcMessage::DpmsReply { state: CliToggle::Off })) => println!("off"), Ok(_) => (), }, None => udev::run(), diff --git a/src/udev.rs b/src/udev.rs index 0daffdd..b582af9 100644 --- a/src/udev.rs +++ b/src/udev.rs @@ -46,7 +46,7 @@ use smithay::reexports::wayland_protocols::wp::linux_dmabuf as _linux_dmabuf; use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer; use smithay::reexports::wayland_server::protocol::wl_shm; use smithay::reexports::wayland_server::DisplayHandle; -use smithay::utils::{DevPath, DeviceFd, Physical, Rectangle, Size, Transform}; +use smithay::utils::{DevPath, DeviceFd, Logical, Physical, Point, Rectangle, Size, Transform}; use smithay::wayland::dmabuf::{DmabufFeedback, DmabufFeedbackBuilder}; use smithay::wayland::{dmabuf, shm}; use tracing::{debug, error}; @@ -240,13 +240,17 @@ impl Udev { /// Render a frame. /// /// Will return `true` if something was rendered. - pub fn render(&mut self, windows: &mut Windows) -> bool { + pub fn render( + &mut self, + windows: &mut Windows, + cursor_position: Option>, + ) -> bool { let output_device = match &mut self.output_device { Some(output_device) => output_device, None => return false, }; - match output_device.render(&self.event_loop, windows) { + match output_device.render(&self.event_loop, windows, cursor_position) { Ok(rendered) => rendered, Err(err) => { error!("{err}"); @@ -600,13 +604,14 @@ impl OutputDevice { &mut self, event_loop: &LoopHandle<'static, Catacomb>, windows: &mut Windows, + cursor_position: Option>, ) -> Result> { let scale = windows.output().scale(); // Update output mode since we're using static for transforms. self.drm_compositor.set_output_mode_source(windows.canvas().into()); - let textures = windows.textures(&mut self.gles, &mut self.graphics); + let textures = windows.textures(&mut self.gles, &mut self.graphics, cursor_position); let mut frame_result = self.drm_compositor.render_frame(&mut self.gles, textures, CLEAR_COLOR)?; let rendered = !frame_result.is_empty; @@ -631,7 +636,7 @@ impl OutputDevice { return Err(format!("unsupported buffer format: {buffer_type:?}").into()); } - self.copy_framebuffer_shm(windows, region, buffer)? + self.copy_framebuffer_shm(windows, cursor_position, region, buffer)? }; // Wait for OpenGL sync to submit screencopy, frame. @@ -688,6 +693,7 @@ impl OutputDevice { fn copy_framebuffer_shm( &mut self, windows: &mut Windows, + cursor_position: Option>, region: Rectangle, buffer: &WlBuffer, ) -> Result> { @@ -706,7 +712,7 @@ impl OutputDevice { let damage = transform.transform_rect_in(region, &output_size); // Collect textures for rendering. - let textures = windows.textures(&mut self.gles, &mut self.graphics); + let textures = windows.textures(&mut self.gles, &mut self.graphics, cursor_position); // Initialize the buffer to our clear color. let mut frame = self.gles.render(output_size, transform)?; diff --git a/src/windows/mod.rs b/src/windows/mod.rs index 9426a7a..adebb5e 100644 --- a/src/windows/mod.rs +++ b/src/windows/mod.rs @@ -368,6 +368,7 @@ impl Windows { &mut self, renderer: &mut GlesRenderer, graphics: &mut Graphics, + cursor_position: Option>, ) -> &[CatacombElement] { // Clear global damage. self.dirty = false; @@ -398,6 +399,27 @@ impl Windows { ); } + // Render touch location cursor. + if let Some(cursor_position) = cursor_position { + let cursor = graphics.cursor(renderer, &self.canvas); + + // Center texture around touch position. + let mut cursor_position = cursor_position.to_physical(scale).to_i32_round(); + let mut bounds = cursor.geometry(scale.into()); + cursor_position.x -= bounds.size.w / 2; + cursor_position.y -= bounds.size.h / 2; + bounds.loc = cursor_position; + + CatacombElement::add_element( + &mut self.textures, + cursor, + cursor_position, + bounds, + None, + scale, + ); + } + match &mut self.view { View::Workspace => { for layer in self.layers.foreground() {