Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
Implement clear-inspector-v1 protocol and add inspector capability to server
Cargo.lock | 2 +-
Cargo.toml | 6 +-
build.rs | 1 +
protocol/clear-inspector-v1.xml | 52 +++++++++
protocol/river-window-management-v1.xml | 9 ++
src/cursor.rs | 5 +
src/input_manager.rs | 26 +++++
src/inspector.rs | 194 ++++++++++++++++++++++++++++++++
src/layer_shell.rs | 72 ++++++++++++
src/main.rs | 1 +
src/pointer_binding.rs | 12 +-
src/seat.rs | 80 +++++++------
src/server.rs | 34 +++++-
src/window.rs | 35 +++++-
src/window_manager.rs | 12 +-
src/wlroots_log_wrapper.c | 10 ++
src/xkb_bindings.rs | 22 ++--
src/xwayland_window.rs | 74 +++++++++---
start-rust.sh | 18 +--
wrapper.h | 3 +
20 files changed, 582 insertions(+), 86 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index bad00a7..44c8104 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -173,7 +173,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
-name = "clear-river"
+name = "clear-computing-environment-server"
version = "0.1.0"
dependencies = [
"bindgen",
diff --git a/Cargo.toml b/Cargo.toml
index 7c547e2..65d220a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,5 +1,5 @@
[package]
-name = "clear-river"
+name = "clear-computing-environment-server"
version = "0.1.0"
edition = "2021"
@@ -13,6 +13,8 @@ env_logger = "0.11"
[build-dependencies]
bindgen = "0.69"
-cc = "1.0"
pkg-config = "0.3"
+cc = "1.0"
+[profile.release]
+debug = true
diff --git a/build.rs b/build.rs
index da159a9..81003c0 100644
--- a/build.rs
+++ b/build.rs
@@ -82,6 +82,7 @@ fn main() {
("river-input-management-v1.xml", "protocol/river-input-management-v1.xml"),
("river-libinput-config-v1.xml", "protocol/river-libinput-config-v1.xml"),
("river-xkb-config-v1.xml", "protocol/river-xkb-config-v1.xml"),
+ ("clear-inspector-v1.xml", "protocol/clear-inspector-v1.xml"),
];
let mut generated_c_files = Vec::new();
diff --git a/protocol/clear-inspector-v1.xml b/protocol/clear-inspector-v1.xml
new file mode 100644
index 0000000..b321965
--- /dev/null
+++ b/protocol/clear-inspector-v1.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="clear_inspector_v1">
+ <copyright>
+ Copyright © 2026 Antigravity
+ </copyright>
+
+ <interface name="zclear_inspector_v1" version="1">
+ <description summary="query information about client surface states and layouts">
+ This interface allows clear-ui client applications to register their surfaces
+ and publish their internal widget trees and state to the compositor.
+ It also allows inspector tools to request the list of active surfaces and their states.
+ </description>
+
+ <request name="destroy" type="destructor">
+ <description summary="destroy the inspector object"/>
+ </request>
+
+ <request name="register_client">
+ <description summary="register a wl_surface as a clear-ui client window">
+ Associate a client wl_surface with this inspector.
+ </description>
+ <arg name="surface" type="object" interface="wl_surface"/>
+ </request>
+
+ <request name="update_state">
+ <description summary="update the published state of a registered client surface">
+ The state is serialized as a JSON string containing widget layout and state data.
+ </description>
+ <arg name="surface" type="object" interface="wl_surface"/>
+ <arg name="state" type="string" summary="JSON serialized state of the application"/>
+ </request>
+
+ <request name="get_inspected_surfaces">
+ <description summary="request the list of all registered surfaces from the compositor"/>
+ </request>
+
+ <event name="inspected_surface">
+ <description summary="reports a registered surface's state and absolute coordinates"/>
+ <arg name="title" type="string" summary="window title"/>
+ <arg name="app_id" type="string" summary="application ID"/>
+ <arg name="x" type="int" summary="absolute screen X coordinate"/>
+ <arg name="y" type="int" summary="absolute screen Y coordinate"/>
+ <arg name="width" type="int" summary="surface width"/>
+ <arg name="height" type="int" summary="surface height"/>
+ <arg name="state" type="string" summary="JSON state of the application"/>
+ </event>
+
+ <event name="inspected_surface_done">
+ <description summary="indicates all inspected surfaces have been reported"/>
+ </event>
+ </interface>
+</protocol>
diff --git a/protocol/river-window-management-v1.xml b/protocol/river-window-management-v1.xml
index f835971..32d6db3 100644
--- a/protocol/river-window-management-v1.xml
+++ b/protocol/river-window-management-v1.xml
@@ -1121,6 +1121,15 @@
<arg name="max_width" type="int" summary="maximum width"/>
<arg name="max_height" type="int" summary="maximum height"/>
</request>
+
+ <request name="set_opacity" since="4">
+ <description summary="set the window opacity">
+ Set the window opacity, from 0 (fully transparent) to 0xffffffff (fully opaque).
+ This request modifies rendering state and may only be made as part of a
+ render sequence, see the river_window_manager_v1 description.
+ </description>
+ <arg name="opacity" type="uint" summary="opacity value from 0 to 0xffffffff"/>
+ </request>
</interface>
<interface name="river_decoration_v1" version="4">
diff --git a/src/cursor.rs b/src/cursor.rs
index d0db355..ab25112 100644
--- a/src/cursor.rs
+++ b/src/cursor.rs
@@ -478,6 +478,11 @@ impl Cursor {
pub unsafe fn clear_focus(&mut self) {
ffi::wlr_seat_pointer_notify_clear_focus((*self.seat).wlr_seat);
+ ffi::wlr_cursor_set_xcursor(
+ self.wlr_cursor,
+ self.xcursor_manager,
+ b"default\0".as_ptr() as *const _,
+ );
}
}
diff --git a/src/input_manager.rs b/src/input_manager.rs
index 6209664..fff4453 100644
--- a/src/input_manager.rs
+++ b/src/input_manager.rs
@@ -26,6 +26,7 @@ pub struct InputManager {
pub new_input_listener: ffi::wl_listener,
pub new_text_input: ffi::wl_listener,
pub new_input_method: ffi::wl_listener,
+ pub new_virtual_pointer_listener: ffi::wl_listener,
}
pub struct InputManagerObject {
@@ -62,6 +63,10 @@ impl InputManager {
// Create default seat
self.default_seat = Seat::create(server, "default")?;
+ if !(*server).xwayland.is_null() {
+ ffi::wlr_xwayland_set_seat((*server).xwayland, (*self.default_seat).wlr_seat);
+ }
+
self.global = ffi::wl_global_create(
wl_server,
&ffi::river_input_manager_v1_interface,
@@ -89,6 +94,11 @@ impl InputManager {
(*new_input_method_ptr).notify = Some(handle_new_input_method);
wl_signal_add(&mut (*self.input_method_manager).events.input_method, &mut self.new_input_method);
+ // Connect new_virtual_pointer listener
+ let new_virtual_pointer_ptr = &mut self.new_virtual_pointer_listener as *mut ffi::wl_listener as *mut WlListener;
+ (*new_virtual_pointer_ptr).notify = Some(handle_new_virtual_pointer);
+ wl_signal_add(&mut (*self.virtual_pointer_manager).events.new_virtual_pointer, &mut self.new_virtual_pointer_listener);
+
Ok(())
}
@@ -114,6 +124,7 @@ impl InputManager {
wl_listener_remove(&mut self.new_input_listener);
wl_listener_remove(&mut self.new_text_input);
wl_listener_remove(&mut self.new_input_method);
+ wl_listener_remove(&mut self.new_virtual_pointer_listener);
}
}
@@ -330,3 +341,18 @@ unsafe extern "C" fn handle_new_input_method(listener: *mut ffi::wl_listener, da
(*im.default_seat).relay.new_input_method(input_method);
}
}
+
+unsafe extern "C" fn handle_new_virtual_pointer(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
+ let im = &mut *crate::container_of!(listener, InputManager, new_virtual_pointer_listener);
+ let event = data as *mut ffi::wlr_virtual_pointer_v1_new_pointer_event;
+
+ log::info!("new virtual pointer device connected");
+
+ let virtual_pointer = (*event).new_pointer;
+ let wlr_device = &mut (*virtual_pointer).pointer.base as *mut ffi::wlr_input_device;
+
+ let device = crate::input_device::InputDevice::new(im.default_seat, wlr_device, true);
+
+ // Attach device to the default seat
+ (*im.default_seat).attach_device(device);
+}
diff --git a/src/inspector.rs b/src/inspector.rs
new file mode 100644
index 0000000..76a7963
--- /dev/null
+++ b/src/inspector.rs
@@ -0,0 +1,194 @@
+use crate::ffi;
+use crate::server::Server;
+use std::collections::HashMap;
+
+pub struct Inspector {
+ pub server: *mut Server,
+ pub global: *mut ffi::wl_global,
+ pub client_states: HashMap<*mut ffi::wlr_surface, String>,
+ pub resources: Vec<*mut ffi::wl_resource>,
+}
+
+impl Inspector {
+ pub fn new() -> Self {
+ Self {
+ server: std::ptr::null_mut(),
+ global: std::ptr::null_mut(),
+ client_states: HashMap::new(),
+ resources: Vec::new(),
+ }
+ }
+
+ pub unsafe fn init(&mut self, server: *mut Server) -> Result<(), &'static str> {
+ self.server = server;
+ self.client_states = HashMap::new();
+ self.resources = Vec::new();
+
+ self.global = ffi::wl_global_create(
+ (*server).wl_server,
+ &ffi::zclear_inspector_v1_interface,
+ 1,
+ self as *mut Inspector as *mut _,
+ Some(bind),
+ );
+
+ if self.global.is_null() {
+ return Err("Failed to create zclear_inspector_v1 global");
+ }
+
+ log::info!("zclear_inspector_v1 protocol global initialized successfully");
+ Ok(())
+ }
+
+ pub unsafe fn deinit(&mut self) {
+ if !self.global.is_null() {
+ ffi::wl_global_destroy(self.global);
+ self.global = std::ptr::null_mut();
+ }
+ }
+}
+
+unsafe extern "C" fn handle_destroy_resource(resource: *mut ffi::wl_resource) {
+ let inspector = ffi::wl_resource_get_user_data(resource) as *mut Inspector;
+ if !inspector.is_null() {
+ (*inspector).resources.retain(|&r| r != resource);
+ }
+}
+
+unsafe extern "C" fn bind(
+ client: *mut ffi::wl_client,
+ data: *mut std::ffi::c_void,
+ version: u32,
+ id: u32,
+) {
+ let inspector = data as *mut Inspector;
+ if inspector.is_null() {
+ return;
+ }
+
+ let resource = ffi::wl_resource_create(client, &ffi::zclear_inspector_v1_interface, version as i32, id);
+ if resource.is_null() {
+ ffi::wl_client_post_no_memory(client);
+ return;
+ }
+
+ ffi::wl_resource_set_implementation(
+ resource,
+ &INSPECTOR_INTERFACE as *const _ as *const _,
+ inspector as *mut _,
+ Some(handle_destroy_resource),
+ );
+
+ (*inspector).resources.push(resource);
+}
+
+unsafe extern "C" fn inspector_destroy(_client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
+ ffi::wl_resource_destroy(resource);
+}
+
+unsafe extern "C" fn inspector_register_client(
+ _client: *mut ffi::wl_client,
+ resource: *mut ffi::wl_resource,
+ surface_resource: *mut ffi::wl_resource,
+) {
+ let inspector = ffi::wl_resource_get_user_data(resource) as *mut Inspector;
+ if inspector.is_null() {
+ return;
+ }
+ let surface = ffi::wlr_surface_from_resource(surface_resource);
+ if surface.is_null() {
+ return;
+ }
+ (*inspector).client_states.entry(surface).or_insert_with(String::new);
+}
+
+unsafe extern "C" fn inspector_update_state(
+ _client: *mut ffi::wl_client,
+ resource: *mut ffi::wl_resource,
+ surface_resource: *mut ffi::wl_resource,
+ state: *const std::os::raw::c_char,
+) {
+ let inspector = ffi::wl_resource_get_user_data(resource) as *mut Inspector;
+ if inspector.is_null() {
+ return;
+ }
+ let surface = ffi::wlr_surface_from_resource(surface_resource);
+ if surface.is_null() {
+ return;
+ }
+ if !state.is_null() {
+ let state_str = std::ffi::CStr::from_ptr(state).to_string_lossy().into_owned();
+ (*inspector).client_states.insert(surface, state_str);
+ }
+}
+
+unsafe extern "C" fn inspector_get_inspected_surfaces(
+ _client: *mut ffi::wl_client,
+ resource: *mut ffi::wl_resource,
+) {
+ let inspector = ffi::wl_resource_get_user_data(resource) as *mut Inspector;
+ if inspector.is_null() {
+ return;
+ }
+ let server = (*inspector).server;
+ let windows = &(*server).wm.windows;
+
+ for &window in windows.iter() {
+ if window.is_null() {
+ continue;
+ }
+ let root_surface = (*window).root_surface();
+ if root_surface.is_null() {
+ continue;
+ }
+
+ // Retrieve registered UI state JSON or default to empty
+ let state = (*inspector)
+ .client_states
+ .get(&root_surface)
+ .cloned()
+ .unwrap_or_else(|| "{}".to_string());
+
+ let title_ptr = (*window).get_title();
+ let app_id_ptr = (*window).get_app_id();
+
+ let title = if title_ptr.is_null() {
+ std::ffi::CString::new("").unwrap()
+ } else {
+ std::ffi::CStr::from_ptr(title_ptr).to_owned()
+ };
+
+ let app_id = if app_id_ptr.is_null() {
+ std::ffi::CString::new("").unwrap()
+ } else {
+ std::ffi::CStr::from_ptr(app_id_ptr).to_owned()
+ };
+
+ let state_c = std::ffi::CString::new(state).unwrap();
+
+ // Send: inspected_surface(title, app_id, x, y, width, height, state)
+ // Event inspected_surface has index 0
+ ffi::wl_resource_post_event(
+ resource,
+ 0,
+ title.as_ptr(),
+ app_id.as_ptr(),
+ (*window).box_geom.x,
+ (*window).box_geom.y,
+ (*window).box_geom.width,
+ (*window).box_geom.height,
+ state_c.as_ptr(),
+ );
+ }
+
+ // Send: inspected_surface_done()
+ // Event inspected_surface_done has index 1
+ ffi::wl_resource_post_event(resource, 1);
+}
+
+static INSPECTOR_INTERFACE: ffi::zclear_inspector_v1_interface = ffi::zclear_inspector_v1_interface {
+ destroy: Some(inspector_destroy),
+ register_client: Some(inspector_register_client),
+ update_state: Some(inspector_update_state),
+ get_inspected_surfaces: Some(inspector_get_inspected_surfaces),
+};
diff --git a/src/layer_shell.rs b/src/layer_shell.rs
index ff981ae..9a7c5e8 100644
--- a/src/layer_shell.rs
+++ b/src/layer_shell.rs
@@ -296,6 +296,8 @@ pub struct LayerSurface {
pub wlr_layer_surface: *mut ffi::wlr_layer_surface_v1,
pub scene_layer_surface: *mut ffi::wlr_scene_layer_surface_v1,
pub popup_tree: *mut ffi::wlr_scene_tree,
+ pub opacity: f32,
+ pub animation_timer: *mut ffi::wl_event_source,
pub destroy: ffi::wl_listener,
pub map: ffi::wl_listener,
@@ -327,6 +329,8 @@ impl LayerSurface {
wlr_layer_surface,
scene_layer_surface,
popup_tree,
+ opacity: 1.0,
+ animation_timer: std::ptr::null_mut(),
destroy: std::mem::zeroed(),
map: std::mem::zeroed(),
unmap: std::mem::zeroed(),
@@ -382,6 +386,11 @@ unsafe extern "C" fn handle_layer_surface_destroy(listener: *mut ffi::wl_listene
log::debug!("layer surface {:?} destroyed", CStr::from_ptr((*(*layer_surface).wlr_layer_surface).namespace));
+ if !(*layer_surface).animation_timer.is_null() {
+ ffi::wl_event_source_remove((*layer_surface).animation_timer);
+ (*layer_surface).animation_timer = std::ptr::null_mut();
+ }
+
wl_listener_remove(&mut (*layer_surface).destroy);
wl_listener_remove(&mut (*layer_surface).map);
wl_listener_remove(&mut (*layer_surface).unmap);
@@ -399,6 +408,31 @@ unsafe extern "C" fn handle_layer_surface_destroy(listener: *mut ffi::wl_listene
let _ = Box::from_raw(layer_surface);
}
+unsafe extern "C" fn handle_animation_tick(data: *mut std::ffi::c_void) -> std::os::raw::c_int {
+ let layer_surface = data as *mut LayerSurface;
+
+ let new_opacity = ((*layer_surface).opacity + 0.08).min(1.0);
+ (*layer_surface).opacity = new_opacity;
+
+ ffi::river_scene_node_set_opacity(
+ (*(*layer_surface).scene_layer_surface).tree as *mut ffi::wlr_scene_node,
+ new_opacity,
+ );
+
+ if new_opacity >= 1.0 {
+ if !(*layer_surface).animation_timer.is_null() {
+ ffi::wl_event_source_remove((*layer_surface).animation_timer);
+ (*layer_surface).animation_timer = std::ptr::null_mut();
+ }
+ } else {
+ if !(*layer_surface).animation_timer.is_null() {
+ ffi::wl_event_source_timer_update((*layer_surface).animation_timer, 16);
+ }
+ }
+
+ 0
+}
+
unsafe extern "C" fn handle_layer_surface_map(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
let layer_surface = crate::container_of!(listener, LayerSurface, map);
let wlr_layer_surface = (*layer_surface).wlr_layer_surface;
@@ -407,6 +441,32 @@ unsafe extern "C" fn handle_layer_surface_map(listener: *mut ffi::wl_listener, _
let server = (*layer_surface).server;
+ if (*wlr_layer_surface).current.layer == ffi::zwlr_layer_shell_v1_layer_ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY {
+ (*layer_surface).opacity = 0.0;
+ ffi::river_scene_node_set_opacity(
+ (*(*layer_surface).scene_layer_surface).tree as *mut ffi::wlr_scene_node,
+ 0.0,
+ );
+
+ if !(*layer_surface).animation_timer.is_null() {
+ ffi::wl_event_source_remove((*layer_surface).animation_timer);
+ (*layer_surface).animation_timer = std::ptr::null_mut();
+ }
+
+ let event_loop = ffi::wl_display_get_event_loop((*server).wl_server);
+ let timer = ffi::wl_event_loop_add_timer(
+ event_loop,
+ Some(handle_animation_tick),
+ layer_surface as *mut _,
+ );
+ if timer.is_null() {
+ log::error!("Failed to create layer surface animation timer");
+ } else {
+ (*layer_surface).animation_timer = timer;
+ ffi::wl_event_source_timer_update(timer, 16);
+ }
+ }
+
if (*wlr_layer_surface).current.keyboard_interactive == ffi::zwlr_layer_surface_v1_keyboard_interactivity_ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND {
let seats = &mut (*server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
let mut curr = (*seats).next;
@@ -432,6 +492,11 @@ unsafe extern "C" fn handle_layer_surface_unmap(listener: *mut ffi::wl_listener,
log::debug!("layer surface {:?} unmapped", CStr::from_ptr((*wlr_layer_surface).namespace));
+ if !(*layer_surface).animation_timer.is_null() {
+ ffi::wl_event_source_remove((*layer_surface).animation_timer);
+ (*layer_surface).animation_timer = std::ptr::null_mut();
+ }
+
let server = (*layer_surface).server;
let seats = &mut (*server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
@@ -469,6 +534,13 @@ unsafe extern "C" fn handle_layer_surface_commit(listener: *mut ffi::wl_listener
);
}
+ if (*layer_surface).opacity < 1.0 {
+ ffi::river_scene_node_set_opacity(
+ (*(*layer_surface).scene_layer_surface).tree as *mut ffi::wlr_scene_node,
+ (*layer_surface).opacity,
+ );
+ }
+
assert!(!(*wlr_layer_surface).output.is_null());
let server = (*layer_surface).server;
diff --git a/src/main.rs b/src/main.rs
index 5dfdc02..557b7fe 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -41,6 +41,7 @@ pub mod input_popup;
pub mod drag_icon;
pub mod pointer_binding;
pub mod keyboard_group;
+pub mod inspector;
use clap::Parser;
diff --git a/src/pointer_binding.rs b/src/pointer_binding.rs
index cfc2115..4d10910 100644
--- a/src/pointer_binding.rs
+++ b/src/pointer_binding.rs
@@ -12,7 +12,7 @@ pub enum PointerBindingStateChange {
}
pub struct PointerBindingScheduled {
- pub state_change: PointerBindingStateChange,
+ pub state_changes: Vec<PointerBindingStateChange>,
}
pub struct PointerBindingRequested {
@@ -45,7 +45,7 @@ impl PointerBinding {
button,
modifiers,
wm_scheduled: PointerBindingScheduled {
- state_change: PointerBindingStateChange::None,
+ state_changes: Vec::new(),
},
wm_requested: PointerBindingRequested {
enabled: false,
@@ -91,16 +91,12 @@ impl PointerBinding {
}
pub unsafe fn pressed(&mut self) {
- assert!(!self.sent_pressed);
- assert!(matches!(self.wm_scheduled.state_change, PointerBindingStateChange::None));
- self.wm_scheduled.state_change = PointerBindingStateChange::Pressed;
+ self.wm_scheduled.state_changes.push(PointerBindingStateChange::Pressed);
(*(*self.seat).server).wm.dirty_windowing();
}
pub unsafe fn released(&mut self) {
- assert!(self.sent_pressed);
- assert!(matches!(self.wm_scheduled.state_change, PointerBindingStateChange::None));
- self.wm_scheduled.state_change = PointerBindingStateChange::Released;
+ self.wm_scheduled.state_changes.push(PointerBindingStateChange::Released);
(*(*self.seat).server).wm.dirty_windowing();
}
diff --git a/src/seat.rs b/src/seat.rs
index 81b7dd6..9fe702b 100644
--- a/src/seat.rs
+++ b/src/seat.rs
@@ -554,26 +554,30 @@ impl Seat {
while curr != bindings_head {
let next = (*curr).next;
let binding = crate::container_of!(curr, crate::xkb_bindings::XkbBinding, link);
- match (*binding).wm_scheduled.state_change {
- crate::xkb_bindings::XkbBindingStateChange::None => {},
- crate::xkb_bindings::XkbBindingStateChange::Pressed => {
- assert!(!(*binding).sent_pressed);
- (*binding).sent_pressed = true;
- ffi::wl_resource_post_event((*binding).object, 0);
- },
- crate::xkb_bindings::XkbBindingStateChange::StopRepeat => {
- assert!((*binding).sent_pressed);
- if ffi::wl_resource_get_version((*binding).object) >= 2 {
- ffi::wl_resource_post_event((*binding).object, 2);
- }
- },
- crate::xkb_bindings::XkbBindingStateChange::Released => {
- assert!((*binding).sent_pressed);
- (*binding).sent_pressed = false;
- ffi::wl_resource_post_event((*binding).object, 1);
- },
+ for state in (*binding).wm_scheduled.state_changes.drain(..) {
+ match state {
+ crate::xkb_bindings::XkbBindingStateChange::None => {},
+ crate::xkb_bindings::XkbBindingStateChange::Pressed => {
+ if !(*binding).sent_pressed {
+ (*binding).sent_pressed = true;
+ ffi::wl_resource_post_event((*binding).object, 0);
+ }
+ },
+ crate::xkb_bindings::XkbBindingStateChange::StopRepeat => {
+ if (*binding).sent_pressed {
+ if ffi::wl_resource_get_version((*binding).object) >= 2 {
+ ffi::wl_resource_post_event((*binding).object, 2);
+ }
+ }
+ },
+ crate::xkb_bindings::XkbBindingStateChange::Released => {
+ if (*binding).sent_pressed {
+ (*binding).sent_pressed = false;
+ ffi::wl_resource_post_event((*binding).object, 1);
+ }
+ },
+ }
}
- (*binding).wm_scheduled.state_change = crate::xkb_bindings::XkbBindingStateChange::None;
curr = next;
}
@@ -583,20 +587,23 @@ impl Seat {
while curr_ptr != ptr_bindings_head {
let next = (*curr_ptr).next;
let binding = crate::container_of!(curr_ptr, crate::pointer_binding::PointerBinding, link);
- match (*binding).wm_scheduled.state_change {
- crate::pointer_binding::PointerBindingStateChange::None => {}
- crate::pointer_binding::PointerBindingStateChange::Pressed => {
- assert!(!(*binding).sent_pressed);
- (*binding).sent_pressed = true;
- ffi::wl_resource_post_event((*binding).object, 0); // pressed
- }
- crate::pointer_binding::PointerBindingStateChange::Released => {
- assert!((*binding).sent_pressed);
- (*binding).sent_pressed = false;
- ffi::wl_resource_post_event((*binding).object, 1); // released
+ for state in (*binding).wm_scheduled.state_changes.drain(..) {
+ match state {
+ crate::pointer_binding::PointerBindingStateChange::None => {}
+ crate::pointer_binding::PointerBindingStateChange::Pressed => {
+ if !(*binding).sent_pressed {
+ (*binding).sent_pressed = true;
+ ffi::wl_resource_post_event((*binding).object, 0); // pressed
+ }
+ }
+ crate::pointer_binding::PointerBindingStateChange::Released => {
+ if (*binding).sent_pressed {
+ (*binding).sent_pressed = false;
+ ffi::wl_resource_post_event((*binding).object, 1); // released
+ }
+ }
}
}
- (*binding).wm_scheduled.state_change = crate::pointer_binding::PointerBindingStateChange::None;
curr_ptr = next;
}
@@ -780,7 +787,16 @@ unsafe extern "C" fn handle_request_set_cursor(
let event = data as *mut ffi::wlr_seat_pointer_request_set_cursor_event;
let focused_client = ffi::river_wlr_seat_get_pointer_focused_client(seat.wlr_seat);
- if focused_client == (*event).seat_client {
+
+ let event_client = ffi::river_wlr_seat_client_get_client((*event).seat_client);
+ let wm_client = if !(*seat.server).wm.object.is_null() {
+ ffi::wl_resource_get_client((*seat.server).wm.object)
+ } else {
+ std::ptr::null_mut()
+ };
+ let is_wm = !wm_client.is_null() && event_client == wm_client;
+
+ if focused_client == (*event).seat_client || is_wm {
ffi::wlr_cursor_set_surface(
seat.cursor.wlr_cursor,
(*event).surface,
diff --git a/src/server.rs b/src/server.rs
index 0a967f7..f077b72 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -270,6 +270,7 @@ pub struct Server {
pub xkb_config: XkbConfig,
pub idle_inhibit_manager: IdleInhibitManager,
pub lock_manager: LockManager,
+ pub inspector: crate::inspector::Inspector,
// Event listeners
pub renderer_lost: ffi::wl_listener,
@@ -316,15 +317,39 @@ unsafe extern "C" fn handle_request_activate(listener: *mut ffi::wl_listener, _d
}
unsafe extern "C" fn handle_request_set_cursor_shape(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
- let _server = container_of!(listener, Server, request_set_cursor_shape);
+ let server = container_of!(listener, Server, request_set_cursor_shape);
let event = data as *mut ffi::wlr_cursor_shape_manager_v1_request_set_shape_event;
let wlr_seat = (*(*event).seat_client).seat;
let focused_client = ffi::river_wlr_seat_get_pointer_focused_client(wlr_seat);
- if focused_client == (*event).seat_client {
+
+ let event_client = ffi::river_wlr_seat_client_get_client((*event).seat_client);
+ let wm_client = if !(*server).wm.object.is_null() {
+ ffi::wl_resource_get_client((*server).wm.object)
+ } else {
+ std::ptr::null_mut()
+ };
+ let is_wm = !wm_client.is_null() && event_client == wm_client;
+
+ let shape_name = ffi::wlr_cursor_shape_v1_name((*event).shape);
+ let shape_str = if shape_name.is_null() {
+ "unknown".to_string()
+ } else {
+ std::ffi::CStr::from_ptr(shape_name).to_string_lossy().into_owned()
+ };
+
+ log::debug!(
+ "set_cursor_shape: event_client={:?}, wm_client={:?}, is_wm={}, focused={:?}, shape={}",
+ event_client,
+ wm_client,
+ is_wm,
+ focused_client,
+ shape_str
+ );
+
+ if focused_client == (*event).seat_client || is_wm {
let seat = ffi::river_wlr_seat_get_data(wlr_seat) as *mut crate::seat::Seat;
if !seat.is_null() {
- let shape_name = ffi::wlr_cursor_shape_v1_name((*event).shape);
(*seat).cursor.set_xcursor(shape_name);
}
}
@@ -596,6 +621,7 @@ impl Server {
self.xkb_config.init(server_ptr).map_err(|_| "Failed to init xkb_config")?;
self.idle_inhibit_manager.init(server_ptr).map_err(|_| "Failed to init idle_inhibit_manager")?;
self.lock_manager.init(server_ptr).map_err(|_| "Failed to init lock_manager")?;
+ self.inspector.init(server_ptr).map_err(|_| "Failed to init inspector")?;
// Setup listeners
let r_lost = &mut self.renderer_lost as *mut ffi::wl_listener as *mut WlListener;
@@ -670,6 +696,7 @@ impl Server {
self.idle_inhibit_manager.deinit();
self.lock_manager.deinit();
self.layer_shell.deinit();
+ self.inspector.deinit();
ffi::wl_display_destroy(self.wl_server);
}
@@ -685,6 +712,7 @@ impl Default for Server {
// Overwrite SlotMap with a valid SlotMap::new() to avoid UB from null vec pointers
std::ptr::write(&mut (*server.as_mut_ptr()).wm.windows, crate::slotmap::SlotMap::new());
std::ptr::write(&mut (*server.as_mut_ptr()).layer_shell.surfaces, crate::slotmap::SlotMap::new());
+ std::ptr::write(&mut (*server.as_mut_ptr()).inspector, crate::inspector::Inspector::new());
server.assume_init()
}
}
diff --git a/src/window.rs b/src/window.rs
index b53d49e..e08e5ae 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -181,6 +181,7 @@ pub struct WindowRenderingRequested {
pub border: Border,
pub clip: ffi::wlr_box,
pub content_clip: ffi::wlr_box,
+ pub opacity: f32,
}
pub struct Window {
@@ -339,6 +340,7 @@ impl Window {
border: Border { edges: Edges::new(), width: 0, r: 0, g: 0, b: 0, a: 0 },
clip: ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 },
content_clip: ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 },
+ opacity: 1.0f32,
},
box_geom: ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 },
foreign_toplevel_handle: std::ptr::null_mut(),
@@ -624,6 +626,13 @@ impl Window {
ffi::river_wlr_xdg_surface_get_surface(base)
}
}
+ WindowImpl::Xwayland(xwindow) => {
+ if xwindow.is_null() || (*xwindow).xsurface.is_null() {
+ std::ptr::null_mut()
+ } else {
+ (*(*xwindow).xsurface).surface
+ }
+ }
_ => std::ptr::null_mut(),
}
}
@@ -675,6 +684,7 @@ impl Window {
border: Border { edges: Edges::new(), width: 0, r: 0, g: 0, b: 0, a: 0 },
clip: ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 },
content_clip: ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 },
+ opacity: 1.0f32,
};
wl_list_remove(&mut self.node.link as *mut ffi::wl_list as *mut WlList);
@@ -1025,8 +1035,9 @@ impl Window {
}
WindowImpl::Xwayland(xwindow) => {
if !xwindow.is_null() {
- self.rendering_scheduled.width = (*(*xwindow).xsurface).width as u32;
- self.rendering_scheduled.height = (*(*xwindow).xsurface).height as u32;
+ let scale = crate::xwayland_window::XwaylandWindow::get_scale(xwindow);
+ self.rendering_scheduled.width = ((*(*xwindow).xsurface).width as f32 / scale).round() as u32;
+ self.rendering_scheduled.height = ((*(*xwindow).xsurface).height as f32 / scale).round() as u32;
}
}
WindowImpl::Destroying => {}
@@ -1125,6 +1136,7 @@ impl Window {
if enabled {
ffi::river_scene_node_enable_blur(self.surfaces.tree as *mut ffi::wlr_scene_node, true);
+ ffi::river_scene_node_set_opacity(self.tree as *mut ffi::wlr_scene_node, requested.opacity);
}
self.box_geom.width = self.rendering_sent.width as i32;
@@ -1758,6 +1770,23 @@ unsafe extern "C" fn window_set_dimension_bounds(
};
}
+unsafe extern "C" fn window_set_opacity(
+ client: *mut ffi::wl_client,
+ resource: *mut ffi::wl_resource,
+ opacity: u32,
+) {
+ let window = ffi::wl_resource_get_user_data(resource) as *mut Window;
+ if window.is_null() {
+ return;
+ }
+ let server = (*window).server;
+ if !(*server).wm.ensure_rendering() {
+ return;
+ }
+ let opacity_f32 = opacity as f32 / u32::MAX as f32;
+ (*window).rendering_requested.opacity = opacity_f32;
+}
+
// river_window_v1 implementation
static WINDOW_INTERFACE: ffi::river_window_v1_interface = ffi::river_window_v1_interface {
destroy: Some(window_destroy),
@@ -1784,6 +1813,7 @@ static WINDOW_INTERFACE: ffi::river_window_v1_interface = ffi::river_window_v1_i
set_clip_box: Some(window_set_clip_box),
set_content_clip_box: Some(window_set_content_clip_box),
set_dimension_bounds: Some(window_set_dimension_bounds),
+ set_opacity: Some(window_set_opacity),
};
static INERT_WINDOW_INTERFACE: ffi::river_window_v1_interface = ffi::river_window_v1_interface {
@@ -1811,6 +1841,7 @@ static INERT_WINDOW_INTERFACE: ffi::river_window_v1_interface = ffi::river_windo
set_clip_box: None,
set_content_clip_box: None,
set_dimension_bounds: None,
+ set_opacity: None,
};
unsafe extern "C" fn handle_destroy_resource(resource: *mut ffi::wl_resource) {
diff --git a/src/window_manager.rs b/src/window_manager.rs
index 60700c8..160e6d9 100644
--- a/src/window_manager.rs
+++ b/src/window_manager.rs
@@ -243,7 +243,12 @@ impl WindowManager {
curr = next;
}
- assert!(self.sent.output_config.is_null());
+ if !self.sent.output_config.is_null() {
+ log::warn!("sent.output_config was not null in manage_start, destroying old configuration");
+ ffi::wlr_output_configuration_v1_send_failed(self.sent.output_config);
+ ffi::wlr_output_configuration_v1_destroy(self.sent.output_config);
+ self.sent.output_config = std::ptr::null_mut();
+ }
self.sent.output_config = self.scheduled.output_config;
self.scheduled.output_config = std::ptr::null_mut();
@@ -489,8 +494,7 @@ unsafe extern "C" fn dirty_idle_callback(data: *mut std::ffi::c_void) {
WindowManagerState::Idle => {
if (*wm).rendering_scheduled.dirty {
(*wm).render_start();
- } else {
- assert!((*wm).scheduled.dirty || (*wm).scheduled.dirty_lazy);
+ } else if (*wm).scheduled.dirty || (*wm).scheduled.dirty_lazy {
(*wm).scheduled.dirty = true;
(*wm).scheduled.dirty_lazy = false;
(*wm).manage_start();
@@ -720,7 +724,7 @@ unsafe extern "C" fn handle_destroy_wm_resource(resource: *mut ffi::wl_resource)
while curr_b != bindings_head {
let next_b = (*curr_b).next;
let binding = crate::container_of!(curr_b, crate::xkb_bindings::XkbBinding, link);
- (*binding).wm_scheduled.state_change = crate::xkb_bindings::XkbBindingStateChange::None;
+ (*binding).wm_scheduled.state_changes.clear();
curr_b = next_b;
}
diff --git a/src/wlroots_log_wrapper.c b/src/wlroots_log_wrapper.c
index bc4927f..b23f0c9 100644
--- a/src/wlroots_log_wrapper.c
+++ b/src/wlroots_log_wrapper.c
@@ -661,5 +661,15 @@ void river_scene_node_enable_blur(struct wlr_scene_node *node, bool enabled) {
wlr_scene_node_for_each_buffer(node, enable_blur_iterator, &enabled);
}
+static void set_opacity_iterator(struct wlr_scene_buffer *buffer, int sx, int sy, void *user_data) {
+ float opacity = *(float *)user_data;
+ wlr_scene_buffer_set_opacity(buffer, opacity);
+}
+
+void river_scene_node_set_opacity(struct wlr_scene_node *node, float opacity) {
+ wlr_scene_node_for_each_buffer(node, set_opacity_iterator, &opacity);
+}
+
+
diff --git a/src/xkb_bindings.rs b/src/xkb_bindings.rs
index ef82cfd..2444794 100644
--- a/src/xkb_bindings.rs
+++ b/src/xkb_bindings.rs
@@ -313,7 +313,7 @@ unsafe extern "C" fn bindings_get_seat(
}
pub struct XkbBindingScheduled {
- pub state_change: XkbBindingStateChange,
+ pub state_changes: Vec<XkbBindingStateChange>,
}
pub struct XkbBindingRequested {
@@ -347,7 +347,7 @@ impl XkbBinding {
keysym,
modifiers,
wm_scheduled: XkbBindingScheduled {
- state_change: XkbBindingStateChange::None,
+ state_changes: Vec::new(),
},
wm_requested: XkbBindingRequested {
enabled: false,
@@ -389,28 +389,26 @@ impl XkbBinding {
}
pub unsafe fn pressed(&mut self) {
- assert!(!self.sent_pressed);
if (*(*self.seat).server).wm.object.is_null() {
log::warn!("Pressed keybind while window manager is disconnected");
- self.wm_scheduled.state_change = XkbBindingStateChange::None;
return;
}
- assert!(matches!(self.wm_scheduled.state_change, XkbBindingStateChange::None));
- self.wm_scheduled.state_change = XkbBindingStateChange::Pressed;
+ self.wm_scheduled.state_changes.push(XkbBindingStateChange::Pressed);
(*(*self.seat).server).wm.dirty_windowing();
}
pub unsafe fn released(&mut self) {
- self.wm_scheduled.state_change = XkbBindingStateChange::Released;
+ self.wm_scheduled.state_changes.push(XkbBindingStateChange::Released);
(*(*self.seat).server).wm.dirty_windowing();
}
pub unsafe fn stop_repeat(&mut self) {
- if self.sent_pressed {
- if matches!(self.wm_scheduled.state_change, XkbBindingStateChange::None) {
- self.wm_scheduled.state_change = XkbBindingStateChange::StopRepeat;
- (*(*self.seat).server).wm.dirty_windowing();
- }
+ let already_releasing_or_stopping = self.wm_scheduled.state_changes.iter().any(|&s|
+ matches!(s, XkbBindingStateChange::Released | XkbBindingStateChange::StopRepeat)
+ );
+ if self.sent_pressed && !already_releasing_or_stopping {
+ self.wm_scheduled.state_changes.push(XkbBindingStateChange::StopRepeat);
+ (*(*self.seat).server).wm.dirty_windowing();
}
}
diff --git a/src/xwayland_window.rs b/src/xwayland_window.rs
index c1ed65d..8833ae9 100644
--- a/src/xwayland_window.rs
+++ b/src/xwayland_window.rs
@@ -115,11 +115,38 @@ impl XwaylandWindow {
Ok(())
}
+ pub unsafe fn get_scale(xwindow: *mut XwaylandWindow) -> f32 {
+ let window = (*xwindow).window;
+ if window.is_null() {
+ return 1.0;
+ }
+ let server = (*window).server;
+ if server.is_null() {
+ return 1.0;
+ }
+ let wlr_output = (*server).om.max_overlap_output(&(*window).box_geom);
+ if !wlr_output.is_null() {
+ let output = ffi::river_wlr_output_get_data(wlr_output) as *mut crate::output::Output;
+ if !output.is_null() {
+ return (*output).current.scale;
+ }
+ }
+ // Fallback: first output in layout
+ let mut link = (*server).om.outputs.next;
+ if link != &mut (*server).om.outputs as *mut ffi::wl_list {
+ let output = crate::container_of!(link, crate::output::Output, link);
+ return (*output).current.scale;
+ }
+ 1.0
+ }
+
pub unsafe fn configure(&mut self) -> bool {
let window = self.window;
let scheduled = &mut (*window).configure_scheduled;
let sent = &mut (*window).configure_sent;
+ let scale = Self::get_scale(self);
+
if scheduled.width == Some(0) {
scheduled.width = Some((*self.xsurface).width as u32);
}
@@ -127,20 +154,32 @@ impl XwaylandWindow {
scheduled.height = Some((*self.xsurface).height as u32);
}
- let width = scheduled.width.unwrap_or((*self.xsurface).width as u32);
- let height = scheduled.height.unwrap_or((*self.xsurface).height as u32);
+ let phys_width = if let Some(w) = scheduled.width {
+ (w as f32 * scale).round() as u16
+ } else {
+ (*self.xsurface).width
+ };
+
+ let phys_height = if let Some(h) = scheduled.height {
+ (h as f32 * scale).round() as u16
+ } else {
+ (*self.xsurface).height
+ };
+
+ let phys_x = ((*window).box_geom.x as f32 * scale).round() as i16;
+ let phys_y = ((*window).box_geom.y as f32 * scale).round() as i16;
- if (*window).box_geom.x != (*self.xsurface).x as i32
- || (*window).box_geom.y != (*self.xsurface).y as i32
- || width != (*self.xsurface).width as u32
- || height != (*self.xsurface).height as u32
+ if phys_x != (*self.xsurface).x
+ || phys_y != (*self.xsurface).y
+ || phys_width != (*self.xsurface).width
+ || phys_height != (*self.xsurface).height
{
ffi::wlr_xwayland_surface_configure(
self.xsurface,
- (*window).box_geom.x as i16,
- (*window).box_geom.y as i16,
- width as u16,
- height as u16,
+ phys_x,
+ phys_y,
+ phys_width,
+ phys_height,
);
}
@@ -154,6 +193,9 @@ impl XwaylandWindow {
ffi::wlr_xwayland_surface_set_fullscreen(self.xsurface, scheduled.inform_fullscreen);
}
+ let width = scheduled.width.unwrap_or(((*self.xsurface).width as f32 / scale).round() as u32);
+ let height = scheduled.height.unwrap_or(((*self.xsurface).height as f32 / scale).round() as u32);
+
(*window).configure_sent = (*window).configure_scheduled.clone();
(*window).configure_sent.width = Some(width);
(*window).configure_sent.height = Some(height);
@@ -300,14 +342,20 @@ unsafe extern "C" fn handle_request_configure(listener: *mut ffi::wl_listener, d
return;
}
+ let scale = XwaylandWindow::get_scale(xwindow);
+ let phys_x = ((*(*xwindow).window).box_geom.x as f32 * scale).round() as i16;
+ let phys_y = ((*(*xwindow).window).box_geom.y as f32 * scale).round() as i16;
+
ffi::wlr_xwayland_surface_configure(
(*xwindow).xsurface,
- (*(*xwindow).window).box_geom.x as i16,
- (*(*xwindow).window).box_geom.y as i16,
+ phys_x,
+ phys_y,
(*event).width,
(*event).height,
);
- (*(*xwindow).window).set_dimensions((*event).width as u32, (*event).height as u32);
+ let log_width = ((*event).width as f32 / scale).round() as u32;
+ let log_height = ((*event).height as f32 / scale).round() as u32;
+ (*(*xwindow).window).set_dimensions(log_width, log_height);
}
unsafe extern "C" fn handle_set_override_redirect(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
diff --git a/start-rust.sh b/start-rust.sh
index 8d28fff..23c4ff3 100755
--- a/start-rust.sh
+++ b/start-rust.sh
@@ -1,5 +1,5 @@
#!/bin/sh
-# Launch clear-river with clearwm
+# Launch clear-computing-environment-server with ccec
# Usage: ./start-rust.sh [--logging] [--debug]
LOGGING=false
@@ -17,30 +17,30 @@ export XCURSOR_SIZE=24
export XCURSOR_PATH="/home/lsgalante/.local/share/icons:/home/lsgalante/.icons:/usr/share/icons"
export WLR_NO_HARDWARE_CURSORS=1
-# Create the clearwm launch script
+# Create the ccec launch script
DEBUG_FLAG=""
if [ "$DEBUG" = true ]; then
DEBUG_FLAG="WAYLAND_DEBUG=1 "
fi
-cat > /tmp/clearwm-rs-launch-rust.sh << LAUNCH_EOF
+cat > /tmp/ccec-launch-rust.sh << LAUNCH_EOF
#!/bin/sh
-${DEBUG_FLAG}exec /home/lsgalante/.local/bin/clearwm 2>/tmp/clearwm-\${WAYLAND_DISPLAY}.log
+${DEBUG_FLAG}exec /home/lsgalante/.local/bin/ccec 2>/tmp/ccec-\${WAYLAND_DISPLAY}.log
LAUNCH_EOF
-chmod +x /tmp/clearwm-rs-launch-rust.sh
+chmod +x /tmp/ccec-launch-rust.sh
if [ "$LOGGING" = true ] || [ "$DEBUG" = true ]; then
- echo "Starting clear-river with clearwm..."
- echo " Logs: /tmp/river-clearwm.log + /tmp/clearwm-\${WAYLAND_DISPLAY}.log"
+ echo "Starting clear-computing-environment-server with ccec..."
+ echo " Logs: /tmp/river-ccec.log + /tmp/ccec-\${WAYLAND_DISPLAY}.log"
if [ "$DEBUG" = true ]; then
echo " Wayland debug logging enabled (WAYLAND_DEBUG=1)"
fi
fi
-# Resolve script directory to reference target/release/clear-river reliably
+# Resolve script directory to reference target/release/clear-computing-environment-server reliably
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if [ "$DEBUG" = true ]; then
export WAYLAND_DEBUG=1
fi
-exec "$SCRIPT_DIR/target/release/clear-river" -c /tmp/clearwm-rs-launch-rust.sh 2>/tmp/river-clearwm.log
+exec "$SCRIPT_DIR/target/release/clear-computing-environment-server" -c /tmp/ccec-launch-rust.sh 2>/tmp/river-ccec.log
diff --git a/wrapper.h b/wrapper.h
index efe0ca5..756a637 100644
--- a/wrapper.h
+++ b/wrapper.h
@@ -97,6 +97,7 @@
#include "river-input-management-v1-protocol.h"
#include "river-libinput-config-v1-protocol.h"
#include "river-xkb-config-v1-protocol.h"
+#include "clear-inspector-v1-protocol.h"
#include "virtual-keyboard-unstable-v1-protocol.h"
#include <wlr/backend/libinput.h>
@@ -258,4 +259,6 @@ void river_wlr_keyboard_init(struct wlr_keyboard *keyboard, void (*led_update)(s
void river_scene_node_enable_blur(struct wlr_scene_node *node, bool enabled);
+void river_scene_node_set_opacity(struct wlr_scene_node *node, float opacity);
+
#endif // WRAPPER_H