Skip to content

Commit

Permalink
Add touch cursor option
Browse files Browse the repository at this point in the history
This patch introduces a new touch cursor, which can be controlled using
`catacomb msg cursor on/off`.

The cursor currently only tracks a single touch point, since Catacomb
itself never uses more than that and is rendered as a red circle.

Closes #161.
  • Loading branch information
chrisduerr committed Dec 1, 2024
1 parent 8f5e4fd commit 4f4e30b
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 29 deletions.
13 changes: 9 additions & 4 deletions catacomb_ipc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,16 @@ pub enum IpcMessage {
/// Output power management.
Dpms {
/// Desired power management state; leave empty to get current state.
state: Option<DpmsState>,
state: Option<CliToggle>,
},
/// 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.
Expand Down Expand Up @@ -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,
}
Expand Down
16 changes: 12 additions & 4 deletions src/catacomb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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};
Expand Down Expand Up @@ -114,6 +114,7 @@ pub struct Catacomb {
pub key_bindings: Vec<KeyBinding>,
pub touch_state: TouchState,
pub frame_pacer: FramePacer,
pub draw_cursor: bool,
pub seat_name: String,
pub display_on: bool,
pub windows: Windows,
Expand All @@ -137,6 +138,7 @@ pub struct Catacomb {
seat_state: SeatState<Self>,
shm_state: ShmState,

last_cursor_position: Option<Point<f64, Logical>>,
accelerometer_token: RegistrationToken,
idle_inhibitors: Vec<WlSurface>,
last_focus: Option<WlSurface>,
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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();
Expand Down
49 changes: 42 additions & 7 deletions src/drawing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -344,14 +350,14 @@ pub struct Graphics {
gesture_handle_default: Option<RenderTexture>,
gesture_handle_blocked: Option<RenderTexture>,
gesture_handle_locked: Option<RenderTexture>,
cursor: Option<RenderTexture>,
}

impl Graphics {
pub fn new(renderer: &mut GlesRenderer) -> Self {
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 {
Expand All @@ -361,6 +367,7 @@ impl Graphics {
gesture_handle_default: None,
gesture_handle_blocked: None,
gesture_handle_locked: None,
cursor: None,
}
}

Expand All @@ -372,9 +379,9 @@ impl Graphics {
ime_override: Option<bool>,
) -> 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.
Expand Down Expand Up @@ -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.
Expand Down
12 changes: 10 additions & 2 deletions src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ const POINTER_TOUCH_SLOT: Option<u32> = Some(0);
/// Touch input state.
pub struct TouchState {
pub user_gestures: Vec<GestureBinding>,
pub position: Point<f64, Logical>,

last_tap: Option<(Instant, Point<f64, Logical>)>,
pending_single_tap: Option<RegistrationToken>,
Expand All @@ -58,6 +57,7 @@ pub struct TouchState {
tap_surface: Option<InputSurface>,
active_app_id: Option<String>,
velocity: Point<f64, Logical>,
position: Point<f64, Logical>,
events: Vec<TouchEvent>,
slot: Option<TouchSlot>,
start: TouchStart,
Expand Down Expand Up @@ -118,6 +118,11 @@ impl TouchState {
self.slot.is_some()
}

/// Get current touch location.
pub fn position(&self) -> Option<Point<f64, Logical>> {
self.slot.map(|_| self.position)
}

/// Get the updated active touch action.
fn action(&mut self, canvas: &Canvas) -> Option<TouchAction> {
// Process handle gestures even before completion.
Expand Down Expand Up @@ -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.
Expand All @@ -587,7 +596,6 @@ impl Catacomb {
return;
}

self.touch_state.velocity = event.position - self.touch_state.position;
self.update_position(event.position);
}

Expand Down
9 changes: 6 additions & 3 deletions src/ipc_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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 { .. } => (),
}
Expand Down
6 changes: 3 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(),
Expand Down
18 changes: 12 additions & 6 deletions src/udev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<Point<f64, Logical>>,
) -> 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}");
Expand Down Expand Up @@ -600,13 +604,14 @@ impl OutputDevice {
&mut self,
event_loop: &LoopHandle<'static, Catacomb>,
windows: &mut Windows,
cursor_position: Option<Point<f64, Logical>>,
) -> Result<bool, Box<dyn Error>> {
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;
Expand All @@ -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.
Expand Down Expand Up @@ -688,6 +693,7 @@ impl OutputDevice {
fn copy_framebuffer_shm(
&mut self,
windows: &mut Windows,
cursor_position: Option<Point<f64, Logical>>,
region: Rectangle<i32, Physical>,
buffer: &WlBuffer,
) -> Result<SyncPoint, Box<dyn Error>> {
Expand All @@ -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)?;
Expand Down
22 changes: 22 additions & 0 deletions src/windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ impl Windows {
&mut self,
renderer: &mut GlesRenderer,
graphics: &mut Graphics,
cursor_position: Option<Point<f64, Logical>>,
) -> &[CatacombElement] {
// Clear global damage.
self.dirty = false;
Expand Down Expand Up @@ -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() {
Expand Down

0 comments on commit 4f4e30b

Please sign in to comment.