Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
Update and align interface features
src/input.rs | 149 +++++++++++++++++++++++++++++++++++++------------
src/ipc.rs | 96 +++++++++++++++++++++++++++-----
src/status_server.rs | 68 ++++++++++++-----------
src/types.rs | 5 ++
src/wayland.rs | 154 +++++++++++++++++++++++++++++++++++++++++++++++++--
5 files changed, 388 insertions(+), 84 deletions(-)
diff --git a/src/input.rs b/src/input.rs
index 0e85dd8..1e5aeed 100644
--- a/src/input.rs
+++ b/src/input.rs
@@ -28,6 +28,16 @@ pub fn update_pointer_coords(dx: i32, dy: i32) {
POINTER_Y.store(current_y, Ordering::SeqCst);
}
+pub fn set_pointer_coords(x: i32, y: i32) {
+ let screen_w = SCREEN_WIDTH.load(Ordering::SeqCst);
+ let screen_h = SCREEN_HEIGHT.load(Ordering::SeqCst);
+ let current_x = x.clamp(0, screen_w);
+ let current_y = y.clamp(0, screen_h);
+
+ POINTER_X.store(current_x, Ordering::SeqCst);
+ POINTER_Y.store(current_y, Ordering::SeqCst);
+}
+
// IOCTL and Event constants
const UI_DEV_CREATE: libc::c_ulong = 0x5501;
const UI_DEV_SETUP: libc::c_ulong = 0x405C5503;
@@ -131,7 +141,8 @@ const ABS_MT_SLOT: u16 = 0x2f;
#[derive(Debug, Clone)]
pub enum InputDaemonMsg {
UpdateConfig(InertialConfig, bool),
- SimulateMove { dx: i32, dy: i32 },
+ SimulateMoveTo { x: i32, y: i32 },
+ SimulateMoveBy { dx: i32, dy: i32 },
SimulateButton { button: u16, press: bool },
SimulateKey { keycode: u16, press: bool },
SimulateClick { button: u16 },
@@ -150,7 +161,7 @@ enum CoordinatorMsg {
InternalReleaseKey { keycode: u16 },
}
-fn setup_uinput() -> std::io::Result<std::fs::File> {
+fn setup_uinput_mouse() -> std::io::Result<std::fs::File> {
let file = OpenOptions::new()
.write(true)
.custom_flags(libc::O_NONBLOCK)
@@ -174,6 +185,59 @@ fn setup_uinput() -> std::io::Result<std::fs::File> {
if libc::ioctl(fd, UI_SET_RELBIT, REL_HWHEEL as libc::c_int) < 0 {
return Err(std::io::Error::last_os_error());
}
+
+ if libc::ioctl(fd, UI_SET_EVBIT, EV_KEY as libc::c_int) < 0 {
+ return Err(std::io::Error::last_os_error());
+ }
+ for key in 1..=511 {
+ if libc::ioctl(fd, UI_SET_KEYBIT, key as libc::c_int) < 0 {
+ return Err(std::io::Error::last_os_error());
+ }
+ }
+ for btn in 272..=276 {
+ if libc::ioctl(fd, UI_SET_KEYBIT, btn as libc::c_int) < 0 {
+ return Err(std::io::Error::last_os_error());
+ }
+ }
+ }
+
+ let mut setup = UinputSetup {
+ id: InputId {
+ bustype: 0x0006, // BUS_VIRTUAL
+ vendor: 0x1234,
+ product: 0x5678,
+ version: 1,
+ },
+ name: [0; 80],
+ ff_effects_max: 0,
+ };
+
+ let name_bytes = b"Clear Virtual Mouse";
+ setup.name[..name_bytes.len()].copy_from_slice(name_bytes);
+
+ unsafe {
+ let setup_ptr = &setup as *const UinputSetup as *const libc::c_void;
+ if libc::ioctl(fd, UI_DEV_SETUP, setup_ptr) < 0 {
+ return Err(std::io::Error::last_os_error());
+ }
+ if libc::ioctl(fd, UI_DEV_CREATE) < 0 {
+ return Err(std::io::Error::last_os_error());
+ }
+ }
+
+ println!("[input-subsystem] Successfully created virtual uinput mouse device.");
+ Ok(file)
+}
+
+fn setup_uinput_abs() -> std::io::Result<std::fs::File> {
+ let file = OpenOptions::new()
+ .write(true)
+ .custom_flags(libc::O_NONBLOCK)
+ .open("/dev/uinput")?;
+
+ let fd = file.as_raw_fd();
+
+ unsafe {
if libc::ioctl(fd, UI_SET_EVBIT, EV_ABS as libc::c_int) < 0 {
return Err(std::io::Error::last_os_error());
}
@@ -218,11 +282,6 @@ fn setup_uinput() -> std::io::Result<std::fs::File> {
if libc::ioctl(fd, UI_SET_EVBIT, EV_KEY as libc::c_int) < 0 {
return Err(std::io::Error::last_os_error());
}
- for key in 1..=511 {
- if libc::ioctl(fd, UI_SET_KEYBIT, key as libc::c_int) < 0 {
- return Err(std::io::Error::last_os_error());
- }
- }
for btn in 272..=276 {
if libc::ioctl(fd, UI_SET_KEYBIT, btn as libc::c_int) < 0 {
return Err(std::io::Error::last_os_error());
@@ -234,14 +293,14 @@ fn setup_uinput() -> std::io::Result<std::fs::File> {
id: InputId {
bustype: 0x0006, // BUS_VIRTUAL
vendor: 0x1234,
- product: 0x5678,
+ product: 0x5679,
version: 1,
},
name: [0; 80],
ff_effects_max: 0,
};
- let name_bytes = b"Clear Virtual Mouse";
+ let name_bytes = b"Clear Virtual Absolute Pointer";
setup.name[..name_bytes.len()].copy_from_slice(name_bytes);
unsafe {
@@ -254,7 +313,7 @@ fn setup_uinput() -> std::io::Result<std::fs::File> {
}
}
- println!("[input-subsystem] Successfully created virtual uinput device.");
+ println!("[input-subsystem] Successfully created virtual uinput absolute pointer device.");
Ok(file)
}
@@ -283,6 +342,20 @@ fn write_mouse_absolute(file: &mut std::fs::File, x: i32, y: i32) -> std::io::Re
Ok(())
}
+fn write_mouse_relative(file: &mut std::fs::File, dx: i32, dy: i32) -> std::io::Result<()> {
+ if dx == 0 && dy == 0 {
+ return Ok(());
+ }
+ if dx != 0 {
+ write_raw_event(file, EV_REL, REL_X, dx)?;
+ }
+ if dy != 0 {
+ write_raw_event(file, EV_REL, REL_Y, dy)?;
+ }
+ write_raw_event(file, EV_SYN, SYN_REPORT, 0)?;
+ Ok(())
+}
+
fn write_scroll(file: &mut std::fs::File, dwx: i32, dwy: i32) -> std::io::Result<()> {
if dwx != 0 {
write_raw_event(file, EV_REL, REL_HWHEEL, dwx)?;
@@ -548,10 +621,18 @@ pub fn run_input_daemon(
let res: Result<(), Box<dyn std::error::Error>> = rt.block_on(async move {
println!("[input-subsystem] Starting Clear Input Subsystem...");
- let mut uinput_file = match setup_uinput() {
+ let mut uinput_mouse_file = match setup_uinput_mouse() {
+ Ok(f) => f,
+ Err(e) => {
+ eprintln!("[input-subsystem] FATAL: Could not initialize uinput mouse: {}.", e);
+ return Err(Box::new(e) as Box<dyn std::error::Error>);
+ }
+ };
+
+ let mut uinput_abs_file = match setup_uinput_abs() {
Ok(f) => f,
Err(e) => {
- eprintln!("[input-subsystem] FATAL: Could not initialize /dev/uinput: {}.", e);
+ eprintln!("[input-subsystem] FATAL: Could not initialize uinput absolute pointer: {}.", e);
return Err(Box::new(e) as Box<dyn std::error::Error>);
}
};
@@ -678,25 +759,29 @@ pub fn run_input_daemon(
trigger_tap_to_click_ipc(tap, &ipc_tx, pipe_write);
}
}
- InputDaemonMsg::SimulateMove { dx, dy } => {
- update_pointer_coords(dx, dy);
+ InputDaemonMsg::SimulateMoveTo { x, y } => {
+ set_pointer_coords(x, y);
let px = POINTER_X.load(Ordering::SeqCst);
let py = POINTER_Y.load(Ordering::SeqCst);
- let _ = write_mouse_absolute(&mut uinput_file, px, py);
+ let _ = write_mouse_absolute(&mut uinput_abs_file, px, py);
+ }
+ InputDaemonMsg::SimulateMoveBy { dx, dy } => {
+ update_pointer_coords(dx, dy);
+ let _ = write_mouse_relative(&mut uinput_mouse_file, dx, dy);
}
InputDaemonMsg::SimulateButton { button, press } => {
let val = if press { 1 } else { 0 };
- let _ = write_raw_event(&mut uinput_file, EV_KEY, button, val);
- let _ = write_raw_event(&mut uinput_file, EV_SYN, SYN_REPORT, 0);
+ let _ = write_raw_event(&mut uinput_mouse_file, EV_KEY, button, val);
+ let _ = write_raw_event(&mut uinput_mouse_file, EV_SYN, SYN_REPORT, 0);
}
InputDaemonMsg::SimulateKey { keycode, press } => {
let val = if press { 1 } else { 0 };
- let _ = write_raw_event(&mut uinput_file, EV_KEY, keycode, val);
- let _ = write_raw_event(&mut uinput_file, EV_SYN, SYN_REPORT, 0);
+ let _ = write_raw_event(&mut uinput_mouse_file, EV_KEY, keycode, val);
+ let _ = write_raw_event(&mut uinput_mouse_file, EV_SYN, SYN_REPORT, 0);
}
InputDaemonMsg::SimulateClick { button } => {
- let _ = write_raw_event(&mut uinput_file, EV_KEY, button, 1);
- let _ = write_raw_event(&mut uinput_file, EV_SYN, SYN_REPORT, 0);
+ let _ = write_raw_event(&mut uinput_mouse_file, EV_KEY, button, 1);
+ let _ = write_raw_event(&mut uinput_mouse_file, EV_SYN, SYN_REPORT, 0);
let tx_clone = tx.clone();
tokio::spawn(async move {
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
@@ -704,8 +789,8 @@ pub fn run_input_daemon(
});
}
InputDaemonMsg::SimulateKeyPress { keycode } => {
- let _ = write_raw_event(&mut uinput_file, EV_KEY, keycode, 1);
- let _ = write_raw_event(&mut uinput_file, EV_SYN, SYN_REPORT, 0);
+ let _ = write_raw_event(&mut uinput_mouse_file, EV_KEY, keycode, 1);
+ let _ = write_raw_event(&mut uinput_mouse_file, EV_SYN, SYN_REPORT, 0);
let tx_clone = tx.clone();
tokio::spawn(async move {
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
@@ -715,12 +800,12 @@ pub fn run_input_daemon(
}
}
CoordinatorMsg::InternalReleaseButton { button } => {
- let _ = write_raw_event(&mut uinput_file, EV_KEY, button, 0);
- let _ = write_raw_event(&mut uinput_file, EV_SYN, SYN_REPORT, 0);
+ let _ = write_raw_event(&mut uinput_mouse_file, EV_KEY, button, 0);
+ let _ = write_raw_event(&mut uinput_mouse_file, EV_SYN, SYN_REPORT, 0);
}
CoordinatorMsg::InternalReleaseKey { keycode } => {
- let _ = write_raw_event(&mut uinput_file, EV_KEY, keycode, 0);
- let _ = write_raw_event(&mut uinput_file, EV_SYN, SYN_REPORT, 0);
+ let _ = write_raw_event(&mut uinput_mouse_file, EV_KEY, keycode, 0);
+ let _ = write_raw_event(&mut uinput_mouse_file, EV_SYN, SYN_REPORT, 0);
}
CoordinatorMsg::PhysicalMove { dx, dy, timestamp } => {
update_pointer_coords(dx, dy);
@@ -897,9 +982,7 @@ pub fn run_input_daemon(
if steps_x != 0 || steps_y != 0 {
update_pointer_coords(steps_x, steps_y);
- let px = POINTER_X.load(Ordering::SeqCst);
- let py = POINTER_Y.load(Ordering::SeqCst);
- let _ = write_mouse_absolute(&mut uinput_file, px, py);
+ let _ = write_mouse_relative(&mut uinput_mouse_file, steps_x, steps_y);
}
}
}
@@ -925,9 +1008,7 @@ pub fn run_input_daemon(
if steps_x != 0 || steps_y != 0 {
update_pointer_coords(steps_x, steps_y);
- let px = POINTER_X.load(Ordering::SeqCst);
- let py = POINTER_Y.load(Ordering::SeqCst);
- let _ = write_mouse_absolute(&mut uinput_file, px, py);
+ let _ = write_mouse_relative(&mut uinput_mouse_file, steps_x, steps_y);
}
}
}
@@ -964,7 +1045,7 @@ pub fn run_input_daemon(
state.accum_scroll_y -= steps_y as f32;
if steps_x != 0 || steps_y != 0 {
- let _ = write_scroll(&mut uinput_file, steps_x, steps_y);
+ let _ = write_scroll(&mut uinput_mouse_file, steps_x, steps_y);
}
}
}
diff --git a/src/ipc.rs b/src/ipc.rs
index 8580dca..982403f 100644
--- a/src/ipc.rs
+++ b/src/ipc.rs
@@ -311,6 +311,58 @@ pub fn handle_ipc_command(cmd: &str, state: &mut WindowManager) -> String {
"apply-mode-sharing" => {
handle_apply_mode_sharing_command(rest, state);
}
+ "mode-next-shared" => {
+ let cycle = [
+ TilingMode::Cascade,
+ TilingMode::Grid,
+ TilingMode::Fullscreen,
+ TilingMode::Floating,
+ ];
+ let focused_id = state
+ .seats
+ .iter()
+ .find(|s| !s.removed)
+ .and_then(|s| s.focused_window_id);
+ if let Some(fid) = focused_id {
+ let notifications_enable = state.notifications_enable;
+ let current_mode = state.get_window(fid).map(|w| w.tiling_mode);
+ if let Some(old_mode) = current_mode {
+ let next = cycle
+ .iter()
+ .position(|m| *m == old_mode)
+ .map(|i| cycle[(i + 1) % cycle.len()])
+ .unwrap_or(TilingMode::Cascade);
+
+ let active_tags = state.active_tags;
+
+ let mut updated_count = 0;
+ for win in &mut state.windows {
+ if !win.closed
+ && win.app_id.as_deref() != Some("clear-status-interface")
+ && (win.tags & active_tags) != 0
+ && win.tiling_mode == old_mode
+ {
+ win.tiling_mode = next;
+ win.mode_locked = true;
+ updated_count += 1;
+ }
+ }
+
+ if notifications_enable && updated_count > 0 {
+ crate::config::show_notification(
+ "ccec",
+ &format!(
+ "Tiling mode set to {} for all {} windows on active tag",
+ next.as_str(),
+ old_mode.as_str()
+ ),
+ );
+ }
+ state.needs_render = true;
+ state.needs_status_update = true;
+ }
+ }
+ }
"bind" => {
handle_bind_command(rest, state);
}
@@ -398,12 +450,8 @@ pub fn handle_ipc_command(cmd: &str, state: &mut WindowManager) -> String {
let parts: Vec<&str> = rest.split_whitespace().collect();
if parts.len() == 2 {
if let (Ok(target_x), Ok(target_y)) = (parts[0].parse::<i32>(), parts[1].parse::<i32>()) {
- let cur_x = crate::input::POINTER_X.load(std::sync::atomic::Ordering::SeqCst);
- let cur_y = crate::input::POINTER_Y.load(std::sync::atomic::Ordering::SeqCst);
- let dx = target_x - cur_x;
- let dy = target_y - cur_y;
if let Some(ref controller) = state.input_controller {
- let _ = controller.send(crate::input::InputDaemonMsg::SimulateMove { dx, dy });
+ let _ = controller.send(crate::input::InputDaemonMsg::SimulateMoveTo { x: target_x, y: target_y });
}
} else {
reply = "error: invalid coordinates\n".to_string();
@@ -417,7 +465,7 @@ pub fn handle_ipc_command(cmd: &str, state: &mut WindowManager) -> String {
if parts.len() == 2 {
if let (Ok(dx), Ok(dy)) = (parts[0].parse::<i32>(), parts[1].parse::<i32>()) {
if let Some(ref controller) = state.input_controller {
- let _ = controller.send(crate::input::InputDaemonMsg::SimulateMove { dx, dy });
+ let _ = controller.send(crate::input::InputDaemonMsg::SimulateMoveBy { dx, dy });
}
} else {
reply = "error: invalid deltas\n".to_string();
@@ -696,14 +744,22 @@ fn handle_mode_command(rest: &str, state: &mut WindowManager) {
}
let mode = parse_tiling_mode(mode_str);
- state.mode_rules.push(ModeRule {
- mode,
- app_id_pattern,
- title_pattern,
- single_instance,
- tag,
- circular: false,
- });
+ if let Some(existing) = state.mode_rules.iter_mut().find(|r| {
+ r.app_id_pattern == app_id_pattern && r.title_pattern == title_pattern
+ }) {
+ existing.mode = mode;
+ existing.single_instance = single_instance;
+ existing.tag = tag;
+ } else {
+ state.mode_rules.push(ModeRule {
+ mode,
+ app_id_pattern,
+ title_pattern,
+ single_instance,
+ tag,
+ circular: false,
+ });
+ }
}
/// Handle "set-mode <mode>" command — set the focused window's tiling mode
@@ -1210,6 +1266,18 @@ mod tests {
assert_eq!(state.mode_rules[0].app_id_pattern, "ghostty");
}
+ #[test]
+ fn test_ipc_mode_rule_update() {
+ let mut state = WindowManager::default();
+ handle_ipc_command("mode fullscreen clear-system-interface", &mut state);
+ assert_eq!(state.mode_rules.len(), 1);
+ assert_eq!(state.mode_rules[0].mode, TilingMode::Fullscreen);
+
+ handle_ipc_command("mode cascade clear-system-interface", &mut state);
+ assert_eq!(state.mode_rules.len(), 1);
+ assert_eq!(state.mode_rules[0].mode, TilingMode::Cascade);
+ }
+
#[test]
fn test_ipc_mode_rule_with_single() {
let mut state = WindowManager::default();
diff --git a/src/status_server.rs b/src/status_server.rs
index 8b7fc67..6e79e1a 100644
--- a/src/status_server.rs
+++ b/src/status_server.rs
@@ -107,6 +107,7 @@ fn status_server_main(rx: mpsc::Receiver<StatusUpdate>) {
loop {
let mut activity = false;
+ let mut has_new_update = false;
// Accept new connections (non-blocking)
for _ in 0..5 {
@@ -154,6 +155,7 @@ fn status_server_main(rx: mpsc::Receiver<StatusUpdate>) {
Ok(update) => {
latest = Some(update);
activity = true;
+ has_new_update = true;
}
Err(mpsc::TryRecvError::Empty) => break,
Err(mpsc::TryRecvError::Disconnected) => {
@@ -164,41 +166,43 @@ fn status_server_main(rx: mpsc::Receiver<StatusUpdate>) {
}
}
- // If we got an update, push it to all clients
- if let Some(ref update) = latest {
- let mut dead_clients = Vec::new();
-
- for (i, client) in clients.iter_mut().enumerate() {
- let msg = format_for_subscription(client.subscription, update);
- match client
- .stream
- .write_all(msg.as_bytes())
- .and_then(|_| client.stream.write_all(b"\n"))
- {
- Ok(_) => {}
- Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
- // Client not ready to receive — skip for now
- }
- Err(ref e) if e.kind() == std::io::ErrorKind::BrokenPipe => {
- eprintln!(
- "[status] client {:?} disconnected (broken pipe)",
- client.subscription
- );
- dead_clients.push(i);
- }
- Err(e) => {
- eprintln!(
- "[status] write error to client {:?}: {}",
- client.subscription, e
- );
- dead_clients.push(i);
+ // If we got a new update, push it to all clients
+ if has_new_update {
+ if let Some(ref update) = latest {
+ let mut dead_clients = Vec::new();
+
+ for (i, client) in clients.iter_mut().enumerate() {
+ let msg = format_for_subscription(client.subscription, update);
+ match client
+ .stream
+ .write_all(msg.as_bytes())
+ .and_then(|_| client.stream.write_all(b"\n"))
+ {
+ Ok(_) => {}
+ Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
+ // Client not ready to receive — skip for now
+ }
+ Err(ref e) if e.kind() == std::io::ErrorKind::BrokenPipe => {
+ eprintln!(
+ "[status] client {:?} disconnected (broken pipe)",
+ client.subscription
+ );
+ dead_clients.push(i);
+ }
+ Err(e) => {
+ eprintln!(
+ "[status] write error to client {:?}: {}",
+ client.subscription, e
+ );
+ dead_clients.push(i);
+ }
}
}
- }
- // Remove dead clients (iterate in reverse to preserve indices)
- for i in dead_clients.into_iter().rev() {
- clients.remove(i);
+ // Remove dead clients (iterate in reverse to preserve indices)
+ for i in dead_clients.into_iter().rev() {
+ clients.remove(i);
+ }
}
}
diff --git a/src/types.rs b/src/types.rs
index 0d89512..6b2a585 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -40,6 +40,7 @@ pub enum Action {
Fullscreen,
LayoutNext,
ModeNext,
+ ModeNextShared,
Reload,
Restart,
View1,
@@ -525,6 +526,8 @@ pub fn parse_action(s: &str) -> Action {
Action::LayoutNext
} else if s == "mode-next" {
Action::ModeNext
+ } else if s == "mode-next-shared" {
+ Action::ModeNextShared
} else if s == "reload" {
Action::Reload
} else if s == "restart" {
@@ -676,6 +679,8 @@ mod tests {
assert_eq!(parse_action("move"), Action::Move);
assert_eq!(parse_action("resize"), Action::Resize);
assert_eq!(parse_action("layout-next"), Action::LayoutNext);
+ assert_eq!(parse_action("mode-next"), Action::ModeNext);
+ assert_eq!(parse_action("mode-next-shared"), Action::ModeNextShared);
assert_eq!(parse_action("reload"), Action::Reload);
assert_eq!(parse_action("restart"), Action::Restart);
assert_eq!(parse_action("spawn"), Action::Spawn);
diff --git a/src/wayland.rs b/src/wayland.rs
index 67380e4..1c83e74 100644
--- a/src/wayland.rs
+++ b/src/wayland.rs
@@ -1562,12 +1562,97 @@ impl Dispatch<RiverWindowV1, ()> for AppState {
);
}
- river_window_v1::Event::PointerMoveRequested { .. } => {
- // Will handle pointer ops later
+ river_window_v1::Event::PointerMoveRequested { seat } => {
+ if let Some(_sid) = state.seat_id_for_proxy(&seat) {
+ let mut window_found = false;
+ let mut needs_render = false;
+ if let Some(win) = state.wm.get_window_mut(wid) {
+ if win.tiling_mode != TilingMode::Fullscreen && win.tiling_mode != TilingMode::Popup {
+ if win.tiling_mode != TilingMode::Floating {
+ win.tiling_mode = TilingMode::Floating;
+ needs_render = true;
+ }
+ win.mode_locked = true;
+ window_found = true;
+ }
+ }
+ if window_found {
+ if needs_render {
+ state.wm.needs_render = true;
+ state.wm.needs_focus = true;
+ state.wm.needs_status_update = true;
+ }
+ seat.op_start_pointer();
+ if let Some(win) = state.wm.get_window_mut(wid) {
+ win.anim_x = None;
+ win.anim_y = None;
+ win.anim_w = None;
+ win.anim_h = None;
+ win.anim_opacity = None;
+ state.active_pointer_op = Some(PointerOp {
+ window_id: wid,
+ op_type: PointerOpType::Move,
+ start_x: win.x,
+ start_y: win.y,
+ start_width: win.width,
+ start_height: win.height,
+ });
+ }
+ }
+ }
}
- river_window_v1::Event::PointerResizeRequested { .. } => {
- // Will handle pointer ops later
+ river_window_v1::Event::PointerResizeRequested { seat, edges } => {
+ if let Some(_sid) = state.seat_id_for_proxy(&seat) {
+ let mut window_found = false;
+ let mut needs_render = false;
+ if let Some(win) = state.wm.get_window_mut(wid) {
+ if win.tiling_mode != TilingMode::Fullscreen && win.tiling_mode != TilingMode::Popup {
+ if win.tiling_mode != TilingMode::Floating {
+ win.tiling_mode = TilingMode::Floating;
+ needs_render = true;
+ }
+ win.mode_locked = true;
+ window_found = true;
+ }
+ }
+ if window_found {
+ if needs_render {
+ state.wm.needs_render = true;
+ state.wm.needs_focus = true;
+ state.wm.needs_status_update = true;
+ }
+ seat.op_start_pointer();
+ if let Some(win) = state.wm.get_window_mut(wid) {
+ win.anim_x = None;
+ win.anim_y = None;
+ win.anim_w = None;
+ win.anim_h = None;
+ win.anim_opacity = None;
+
+ let edges_u32: u32 = edges.into();
+ let op_type = match edges_u32 {
+ 1 => PointerOpType::ResizeTop,
+ 2 => PointerOpType::ResizeBottom,
+ 4 => PointerOpType::ResizeLeft,
+ 8 => PointerOpType::ResizeRight,
+ 5 => PointerOpType::ResizeTopLeft,
+ 9 => PointerOpType::ResizeTopRight,
+ 6 => PointerOpType::ResizeBottomLeft,
+ 10 => PointerOpType::ResizeBottomRight,
+ _ => PointerOpType::Resize,
+ };
+ state.active_pointer_op = Some(PointerOp {
+ window_id: wid,
+ op_type,
+ start_x: win.x,
+ start_y: win.y,
+ start_width: win.width,
+ start_height: win.height,
+ });
+ }
+ }
+ }
}
_ => {}
@@ -2532,6 +2617,67 @@ fn execute_action(state: &mut AppState, seat_id: u64, action: &crate::types::Act
}
}
}
+ Action::ModeNextShared => {
+ let cycle = [
+ TilingMode::Cascade,
+ TilingMode::Grid,
+ TilingMode::Fullscreen,
+ TilingMode::Floating,
+ ];
+ let focused_id = state
+ .wm
+ .seats
+ .iter()
+ .find(|s| !s.removed)
+ .and_then(|s| s.focused_window_id);
+ if let Some(fid) = focused_id {
+ let notifications_enable = state.wm.notifications_enable;
+ let current_mode = state.wm.get_window(fid).map(|w| w.tiling_mode);
+ if let Some(old_mode) = current_mode {
+ let next = cycle
+ .iter()
+ .position(|m| *m == old_mode)
+ .map(|i| cycle[(i + 1) % cycle.len()])
+ .unwrap_or(TilingMode::Cascade);
+
+ let active_tags = state.wm.active_tags;
+
+ let mut updated_count = 0;
+ for win in &mut state.wm.windows {
+ if !win.closed
+ && win.app_id.as_deref() != Some("clear-status-interface")
+ && (win.tags & active_tags) != 0
+ && win.tiling_mode == old_mode
+ {
+ win.tiling_mode = next;
+ win.mode_locked = true;
+ updated_count += 1;
+ }
+ }
+
+ eprintln!(
+ "mode-next-shared: updated {} windows (current tag, mode {}) -> {}",
+ updated_count,
+ old_mode.as_str(),
+ next.as_str()
+ );
+
+ if notifications_enable && updated_count > 0 {
+ crate::config::show_notification(
+ "ccec",
+ &format!(
+ "Tiling mode set to {} for all {} windows on active tag",
+ next.as_str(),
+ old_mode.as_str()
+ ),
+ );
+ }
+
+ state.wm.needs_render = true;
+ state.wm.needs_status_update = true;
+ }
+ }
+ }
Action::Reload => {
crate::restart::wm_restart();
}