diff --git a/src/server/mod.rs b/src/server/mod.rs index 28e4a34..1e10520 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -6,7 +6,7 @@ pub(crate) mod selection; mod tests; use self::event::*; -use crate::xstate::{Decorations, WindowDims, WmHints, WmName, WmNormalHints}; +use crate::xstate::{Decorations, MoveResizeDirection, WindowDims, WmHints, WmName, WmNormalHints}; use crate::{X11Selection, XConnection}; use clientside::MyWorld; use hecs::{Entity, World}; @@ -49,7 +49,7 @@ use wayland_protocols::{ xdg_popup::XdgPopup, xdg_positioner::{Anchor, Gravity, XdgPositioner}, xdg_surface::XdgSurface, - xdg_toplevel::XdgToplevel, + xdg_toplevel::{self, XdgToplevel}, xdg_wm_base::XdgWmBase, }, xdg_output::zv1::server::zxdg_output_manager_v1::ZxdgOutputManagerV1, @@ -1116,6 +1116,47 @@ impl InnerServerState { data.toplevel._move(&last_click_data.0, last_click_data.1); } + pub fn resize_window(&mut self, window: x::Window, direction: MoveResizeDirection) { + let Some(data) = self + .windows + .get(&window) + .copied() + .and_then(|e| self.world.entity(e).ok()) + else { + warn!("Requested resize of unknown window {window:?}"); + return; + }; + + let Some(last_click_data) = data.get::<&LastClickSerial>() else { + warn!("Requested resize of window {window:?} but we don't have a click serial for it"); + return; + }; + + let role = data.get::<&SurfaceRole>(); + let Some(SurfaceRole::Toplevel(Some(data))) = role.as_deref() else { + warn!("Requested resize of non toplevel {window:?} ({role:?})"); + return; + }; + + let edge = match direction { + MoveResizeDirection::SizeTopLeft => xdg_toplevel::ResizeEdge::TopLeft, + MoveResizeDirection::SizeTop => xdg_toplevel::ResizeEdge::Top, + MoveResizeDirection::SizeTopRight => xdg_toplevel::ResizeEdge::TopRight, + MoveResizeDirection::SizeRight => xdg_toplevel::ResizeEdge::Right, + MoveResizeDirection::SizeBottomRight => xdg_toplevel::ResizeEdge::BottomRight, + MoveResizeDirection::SizeBottom => xdg_toplevel::ResizeEdge::Bottom, + MoveResizeDirection::SizeBottomLeft => xdg_toplevel::ResizeEdge::BottomLeft, + MoveResizeDirection::SizeLeft => xdg_toplevel::ResizeEdge::Left, + MoveResizeDirection::MoveKeyboard + | MoveResizeDirection::SizeKeyboard + | MoveResizeDirection::Move + | MoveResizeDirection::Cancel => unreachable!(), + }; + + data.toplevel + .resize(&last_click_data.0, last_click_data.1, edge); + } + pub fn destroy_window(&mut self, window: x::Window) { if let Some(id) = self.windows.remove(&window) { self.world.remove::<(x::Window, WindowData)>(id).unwrap(); diff --git a/src/xstate/mod.rs b/src/xstate/mod.rs index c6a2b50..823a310 100644 --- a/src/xstate/mod.rs +++ b/src/xstate/mod.rs @@ -471,68 +471,87 @@ impl XState { } )); } - xcb::Event::X(x::Event::ClientMessage(e)) => match e.r#type() { - x if x == self.atoms.wl_surface_id => { - panic!(concat!( + xcb::Event::X(x::Event::ClientMessage(e)) => { + match e.r#type() { + x if x == self.atoms.wl_surface_id => { + panic!(concat!( "Xserver should be using WL_SURFACE_SERIAL, not WL_SURFACE_ID\n", "Your Xwayland is likely too old, it should be version 23.1 or greater." )); - } - x if x == self.atoms.wl_surface_serial => { - let x::ClientMessageData::Data32(data) = e.data() else { - unreachable!(); - }; - server_state.set_window_serial(e.window(), [data[0], data[1]]); - } - x if x == self.atoms.net_wm_state => { - let x::ClientMessageData::Data32(data) = e.data() else { - unreachable!(); - }; - let Ok(action) = SetState::try_from(data[0]) else { - warn!("unknown action for _NET_WM_STATE: {}", data[0]); - continue; - }; - let prop1 = unsafe { x::Atom::new(data[1]) }; - let prop2 = unsafe { x::Atom::new(data[2]) }; + } + x if x == self.atoms.wl_surface_serial => { + let x::ClientMessageData::Data32(data) = e.data() else { + unreachable!(); + }; + server_state.set_window_serial(e.window(), [data[0], data[1]]); + } + x if x == self.atoms.net_wm_state => { + let x::ClientMessageData::Data32(data) = e.data() else { + unreachable!(); + }; + let Ok(action) = SetState::try_from(data[0]) else { + warn!("unknown action for _NET_WM_STATE: {}", data[0]); + continue; + }; + let prop1 = unsafe { x::Atom::new(data[1]) }; + let prop2 = unsafe { x::Atom::new(data[2]) }; - trace!("_NET_WM_STATE ({action:?}) props: {prop1:?} {prop2:?}"); + trace!("_NET_WM_STATE ({action:?}) props: {prop1:?} {prop2:?}"); - for prop in [prop1, prop2] { - match prop { - x if x == self.atoms.wm_fullscreen => { - server_state.set_fullscreen(e.window(), action); + for prop in [prop1, prop2] { + match prop { + x if x == self.atoms.wm_fullscreen => { + server_state.set_fullscreen(e.window(), action); + } + _ => {} } - _ => {} } } - } - x if x == self.atoms.active_win => { - server_state.activate_window(e.window()); - } - x if x == self.atoms.moveresize => { - let x::ClientMessageData::Data32(data) = e.data() else { - unreachable!(); - }; - - let (_x_root, _y_root) = (data[0], data[1]); - let Ok(direction) = MoveResizeDirection::try_from(data[2]) else { - warn!("unknown direction for _NET_WM_MOVERESIZE: {}", data[2]); - continue; - }; - let button = data[3]; - // XXX: This can technically be driven by keyboard events and other mouse buttons as well, - // but I haven't found an application that does this yet. We'll cross that bridge when we get to it. - if button != 1 { - warn!("Attempted move/resize of {:?} with non left click button ({button})", e.window()); - continue; + x if x == self.atoms.active_win => { + server_state.activate_window(e.window()); } + x if x == self.atoms.moveresize => { + let x::ClientMessageData::Data32(data) = e.data() else { + unreachable!(); + }; - if matches!(direction, MoveResizeDirection::Move) { - server_state.move_window(e.window()); + let (_x_root, _y_root) = (data[0], data[1]); + let Ok(direction) = MoveResizeDirection::try_from(data[2]) else { + warn!("unknown direction for _NET_WM_MOVERESIZE: {}", data[2]); + continue; + }; + let button = data[3]; + // XXX: This can technically be driven by keyboard events and other mouse buttons as well, + // but I haven't found an application that does this yet. We'll cross that bridge when we get to it. + if button != 1 { + warn!("Attempted move/resize of {:?} with non left click button ({button})", e.window()); + continue; + } + + match direction { + MoveResizeDirection::Move => { + server_state.move_window(e.window()); + } + MoveResizeDirection::SizeTopLeft + | MoveResizeDirection::SizeTop + | MoveResizeDirection::SizeTopRight + | MoveResizeDirection::SizeRight + | MoveResizeDirection::SizeBottomRight + | MoveResizeDirection::SizeBottom + | MoveResizeDirection::SizeBottomLeft + | MoveResizeDirection::SizeLeft => { + server_state.resize_window(e.window(), direction); + } + MoveResizeDirection::SizeKeyboard + | MoveResizeDirection::MoveKeyboard + | MoveResizeDirection::Cancel => { + warn!("Unimplemented window move/resize action: {direction:?} ({:?})", e.window()); + } + } } + t => warn!("unrecognized message: {t:?}"), } - t => warn!("unrecognized message: {t:?}"), - }, + } xcb::Event::X(x::Event::MappingNotify(_)) => {} xcb::Event::RandR(xcb::randr::Event::Notify(e)) if matches!(e.u(), xcb::randr::NotifyData::Rc(_)) => diff --git a/tests/integration.rs b/tests/integration.rs index 71b0291..95ab070 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -2053,3 +2053,33 @@ fn client_init_move() { let data = f.testwl.get_surface_data(surface).unwrap(); assert!(data.moving); } + +#[test] +fn client_init_resize() { + let mut f = Fixture::new(); + let mut connection = Connection::new(&f.display); + + let win_toplevel = connection.new_window(connection.root, 0, 0, 20, 20, false); + let surface = f.map_as_toplevel(&mut connection, win_toplevel); + f.testwl.move_pointer_to(surface, 10., 10.); + let ptr = f.testwl.pointer(); + ptr.motion(10, 10.0, 10.0); + ptr.frame(); + ptr.button(10, 20, BTN_LEFT, wl_pointer::ButtonState::Pressed); + ptr.frame(); + f.testwl.dispatch(); + + connection.send_client_message(&x::ClientMessageEvent::new( + win_toplevel, + connection.atoms.moveresize, + x::ClientMessageData::Data32([0, 0, MoveResizeDirection::SizeBottomRight.into(), 1, 0]), + )); + + f.wait_and_dispatch(); + let data = f.testwl.get_surface_data(surface).unwrap(); + assert!( + matches!(data.resizing, Some(xdg_toplevel::ResizeEdge::BottomRight)), + "Got wrong resizing edge: {:?}", + data.resizing + ); +} diff --git a/testwl/src/lib.rs b/testwl/src/lib.rs index 481dcc6..93e131e 100644 --- a/testwl/src/lib.rs +++ b/testwl/src/lib.rs @@ -111,6 +111,7 @@ pub struct SurfaceData { pub fractional: Option, pub viewport: Option, pub moving: bool, + pub resizing: Option, } impl SurfaceData { @@ -1546,6 +1547,17 @@ impl Dispatch for State { let data = state.surfaces.get_mut(surface_id).unwrap(); data.moving = true; } + xdg_toplevel::Request::Resize { + seat: _, + serial: _, + edges, + } => { + let data = state.surfaces.get_mut(surface_id).unwrap(); + let WEnum::Value(edge) = edges else { + unreachable!() + }; + data.resizing = Some(edge); + } other => todo!("unhandled request {other:?}"), } } @@ -1868,6 +1880,7 @@ impl Dispatch for State { fractional: None, viewport: None, moving: false, + resizing: None, }, ); state.last_surface_id = Some(SurfaceId(id));