diff options
| author | srdusr <[email protected]> | 2025-11-07 14:04:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2025-11-07 14:04:00 +0200 |
| commit | ea78f94027ecd2c690904a6d7729e9e3cc190a20 (patch) | |
| tree | fe622c0cd12a2fe077501a26f0077c51d41b4209 /crates/platform/src/ipc | |
| parent | a8991f65602abc5ecee740c443c58fa96ecd15e1 (diff) | |
| download | srdwm-ea78f94027ecd2c690904a6d7729e9e3cc190a20.tar.gz srdwm-ea78f94027ecd2c690904a6d7729e9e3cc190a20.zip | |
Make tiling's master/stack ratio live, add settings readback everywhere
Investigated the "tiling needs a lot of work" report directly. The
MasterStackLayout algorithm itself was already correct; the real gap was
that dragging or resizing a tiled window did nothing durable (raw
geometry that the next arrange_workspace silently discarded), and
master_ratio/master_count had no live path at all (config-file only).
A resize-drag on the shared master/stack boundary now live-adjusts
TilingConfig::master_ratio and re-arranges the group immediately; srd set
master_ratio/master_count do the same for a keybind or script. Found and
fixed a real bug while building this: start_resize's own focus_window
call re-stacks its target in self.order before the ratio-drag decision
used to be made, silently misclassifying real master-column grabs.
Fixed by deciding ratio-drag status (and freezing the membership
snapshot it depends on) before that raise happens, applying
MasterStackLayout directly against the frozen snapshot rather than
re-deriving membership from the by-then-reordered live order. Live-
verified in a nested compositor, not just unit-tested.
Also closes the readback gaps flagged directly by the AGS peer session:
border_width/border_color/corner_radius/decoration_mode/gap_inner/
gap_outer/master_ratio/master_count were all live-settable via srd set
with no way to read the current value back, and pin_input had no
readback at all. SettingsResponse now reports all of them; a new
pinned_inputs query (srd pinned inputs) lists every currently pinned
pid/window.
Diffstat (limited to 'crates/platform/src/ipc')
| -rw-r--r-- | crates/platform/src/ipc/dispatch.rs | 45 | ||||
| -rw-r--r-- | crates/platform/src/ipc/tests.rs | 86 | ||||
| -rw-r--r-- | crates/platform/src/ipc/types.rs | 48 |
3 files changed, 179 insertions, 0 deletions
diff --git a/crates/platform/src/ipc/dispatch.rs b/crates/platform/src/ipc/dispatch.rs index 918b7e0..3dcca57 100644 --- a/crates/platform/src/ipc/dispatch.rs +++ b/crates/platform/src/ipc/dispatch.rs @@ -28,6 +28,14 @@ pub(crate) fn handle_request(line: &[u8], wm: &std::rc::Rc<std::cell::RefCell<Wi reading_mode: wm.color_filter == srdwm_core::ColorFilter::ReadingMode, phone_mode: wm.phone_mode, multi_cursor: wm.multi_cursor_enabled, + border_width: wm.theme.default_border_width, + border_color: srdwm_core::format_hex_color(wm.theme.default_border_color), + corner_radius: wm.theme.default_corner_radius, + decoration_mode_server: wm.theme.default_decorated, + gap_inner: wm.tiling.gap_inner, + gap_outer: wm.tiling.gap_outer, + master_ratio: wm.tiling.master_ratio, + master_count: wm.tiling.master_count, }; (serde_json::to_vec(&settings).unwrap_or_default(), false) } @@ -127,6 +135,14 @@ pub(crate) fn handle_request(line: &[u8], wm: &std::rc::Rc<std::cell::RefCell<Wi wm.borrow_mut().request_pin_input(pid as i32, id); (ok(), true) } + // `{"cmd":"pinned_inputs"}` - every pid currently pinned and which + // window, read from `WindowManager::all_pinned_windows` (the + // backend's own confirmation that a `pin_input` request was + // genuinely applied, not the one-shot request queue itself). + "pinned_inputs" => { + let pinned: Vec<PinnedInputInfo> = wm.borrow().all_pinned_windows().map(|(pid, id)| PinnedInputInfo { pid, id }).collect(); + (serde_json::to_vec(&PinnedInputsResponse { pinned }).unwrap_or_default(), false) + } // `{"cmd":"create_fake_monitor","name":<string>,"width":<u32>, // "height":<u32>}` - a fully virtual `wl_output` with no real // hardware behind it, applied by whichever backend owns real @@ -498,6 +514,35 @@ fn handle_set(req: &serde_json::Value, wm: &std::rc::Rc<std::cell::RefCell<Windo wm.borrow_mut().tiling.gap_outer = v as u32; (ok(), true) } + // `srd set master_ratio <0.0..1.0>` / `srd set master_count <n>` -- + // the two `TilingConfig` fields a resize-drag on the master/stack + // boundary (`WindowManager::adjust_master_ratio_for_drag`) already + // live-adjusts interactively; this is the same thing for a + // keybinding or script instead of a mouse drag, e.g. dwm's + // `mod+h`/`mod+l` grow/shrink-master or `mod+i`/`mod+d` add/remove- + // a-master-window conventions. Unlike `gap_inner`/`gap_outer` + // above, which accept "takes effect whenever the workspace next + // re-arranges anyway" (a cosmetic preference under no time + // pressure), this re-arranges the current workspace immediately -- + // a keybind pressed to grow the master column is expected to show + // the result at once, the same instant feedback the drag path + // already gives. + "master_ratio" => { + let Some(v) = value.and_then(|v| v.as_f64()) else { return (err("master_ratio needs a numeric value"), false) }; + let mut wm = wm.borrow_mut(); + wm.tiling.master_ratio = (v as f32).clamp(0.1, 0.9); + let current = wm.current_workspace(); + wm.arrange_workspace(current); + (ok(), true) + } + "master_count" => { + let Some(v) = value.and_then(|v| v.as_u64()) else { return (err("master_count needs a numeric value"), false) }; + let mut wm = wm.borrow_mut(); + wm.tiling.master_count = (v as usize).max(1); + let current = wm.current_workspace(); + wm.arrange_workspace(current); + (ok(), true) + } "shadows" => { let Some(v) = value.and_then(|v| v.as_bool()) else { return (err("shadows needs a boolean value"), false) }; wm.borrow_mut().shadows_enabled = v; diff --git a/crates/platform/src/ipc/tests.rs b/crates/platform/src/ipc/tests.rs index 109a9ec..13ab4c2 100644 --- a/crates/platform/src/ipc/tests.rs +++ b/crates/platform/src/ipc/tests.rs @@ -735,3 +735,89 @@ fn a_oneshot_clients_request_still_closes_the_connection_as_before() { Err(e) => assert_eq!(e.kind(), ErrorKind::WouldBlock), } } + +#[test] +fn settings_reports_the_readback_fields_flagged_as_missing() { + // Confirms the whole batch at once rather than one test per field -- + // these were all added together for the same reason (a settings + // panel could set any of them blind but never read the current value + // back). + let dir = tempfile::tempdir().unwrap(); + let mut server = IpcServer::bind_in(dir.path(), "test").unwrap(); + let wm = Rc::new(RefCell::new(WindowManager::new())); + + let mut client = UnixStream::connect(&server.path).unwrap(); + client.write_all(b"{\"cmd\":\"settings\"}\n").unwrap(); + server.poll(&wm); + let line = read_line(&mut std::io::BufReader::new(client)); + let parsed: serde_json::Value = serde_json::from_str(&line).unwrap(); + for field in ["border_width", "border_color", "corner_radius", "decoration_mode_server", "gap_inner", "gap_outer", "master_ratio", "master_count"] { + assert!(parsed.get(field).is_some(), "settings response is missing '{field}'"); + } + assert_eq!(parsed["border_color"].as_str().unwrap().chars().next(), Some('#'), "border_color must be a hex string, matching what srd set border_color itself accepts"); +} + +#[test] +fn set_master_ratio_is_reflected_immediately_by_settings() { + let dir = tempfile::tempdir().unwrap(); + let mut server = IpcServer::bind_in(dir.path(), "test").unwrap(); + let wm = Rc::new(RefCell::new(WindowManager::new())); + wm.borrow_mut().set_monitors(vec![srdwm_core::Monitor::new(0, "eDP-1", srdwm_core::Rect::new(0, 0, 1920, 1080))]); + + let mut set_client = UnixStream::connect(&server.path).unwrap(); + set_client.write_all(b"{\"cmd\":\"set\",\"key\":\"master_ratio\",\"value\":0.7}\n").unwrap(); + server.poll(&wm); + let _ = read_line(&mut std::io::BufReader::new(set_client)); + + let mut settings_client = UnixStream::connect(&server.path).unwrap(); + settings_client.write_all(b"{\"cmd\":\"settings\"}\n").unwrap(); + server.poll(&wm); + let line = read_line(&mut std::io::BufReader::new(settings_client)); + assert!(line.contains(r#""master_ratio":0.7"#)); +} + +#[test] +fn set_master_ratio_clamps_to_a_sane_range() { + let dir = tempfile::tempdir().unwrap(); + let mut server = IpcServer::bind_in(dir.path(), "test").unwrap(); + let wm = Rc::new(RefCell::new(WindowManager::new())); + + let mut client = UnixStream::connect(&server.path).unwrap(); + client.write_all(b"{\"cmd\":\"set\",\"key\":\"master_ratio\",\"value\":1.5}\n").unwrap(); + server.poll(&wm); + let _ = read_line(&mut std::io::BufReader::new(client)); + + assert_eq!(wm.borrow().tiling.master_ratio, 0.9, "a value past the sane range must clamp, not be accepted verbatim"); +} + +#[test] +fn pinned_inputs_lists_nothing_before_any_pin_is_applied() { + let dir = tempfile::tempdir().unwrap(); + let mut server = IpcServer::bind_in(dir.path(), "test").unwrap(); + let wm = Rc::new(RefCell::new(WindowManager::new())); + + let mut client = UnixStream::connect(&server.path).unwrap(); + client.write_all(b"{\"cmd\":\"pinned_inputs\"}\n").unwrap(); + server.poll(&wm); + let line = read_line(&mut std::io::BufReader::new(client)); + assert_eq!(line.trim_end(), r#"{"pinned":[]}"#); +} + +#[test] +fn pinned_inputs_reports_a_pin_once_the_backend_has_applied_it() { + // `pin_input` only ever queues a *request* - `WindowManager:: + // set_pinned_window` is the backend's own confirmation that it was + // genuinely applied, called directly here to simulate that (the real + // caller is `CompState::set_virtual_pointer_pin` in the wayland + // crate, unreachable from a platform-crate test). + let dir = tempfile::tempdir().unwrap(); + let mut server = IpcServer::bind_in(dir.path(), "test").unwrap(); + let wm = Rc::new(RefCell::new(WindowManager::new())); + wm.borrow_mut().set_pinned_window(12345, Some(7)); + + let mut client = UnixStream::connect(&server.path).unwrap(); + client.write_all(b"{\"cmd\":\"pinned_inputs\"}\n").unwrap(); + server.poll(&wm); + let line = read_line(&mut std::io::BufReader::new(client)); + assert_eq!(line.trim_end(), r#"{"pinned":[{"pid":12345,"id":7}]}"#); +} diff --git a/crates/platform/src/ipc/types.rs b/crates/platform/src/ipc/types.rs index df11592..24e3148 100644 --- a/crates/platform/src/ipc/types.rs +++ b/crates/platform/src/ipc/types.rs @@ -127,6 +127,26 @@ pub(crate) struct WorkspacesResponse { pub(crate) workspaces: Vec<WorkspaceInfo>, } +/// One `pid`/pinned-window pair - `WindowManager::all_pinned_windows`'s +/// own doc comment. `id` matches the plain `WindowId` every other +/// dispatch already reads/writes, not a separate type. +#[derive(Serialize)] +pub(crate) struct PinnedInputInfo { + pub(crate) pid: i32, + pub(crate) id: WindowId, +} + +/// `"pinned_inputs"`'s one-shot reply - every pid Multi-cursor Phase 2 +/// (`srd dispatch pin input`) currently has pinned to a window, and which +/// one. Added because pinning had no readback at all: a caller could ask +/// to pin a window blind, but never confirm the pin actually took, or +/// list what's pinned right now without already knowing which pids to +/// ask about. +#[derive(Serialize)] +pub(crate) struct PinnedInputsResponse { + pub(crate) pinned: Vec<PinnedInputInfo>, +} + /// `"settings"`'s one-shot reply - the live-settable toggles `"set"` /// accepts, so a migrated toggle script (night-light, reading-mode, /// hypr-performance-profile) can read current state back instead of @@ -150,6 +170,34 @@ pub(crate) struct SettingsResponse { pub(crate) phone_mode: bool, /// `WindowManager::multi_cursor_enabled`'s own doc comment. pub(crate) multi_cursor: bool, + /// The theme/tiling values `srd set` can already change live + /// (`border_width`, `border_color`, `corner_radius`, `decoration_ + /// mode`, `gap_inner`, `gap_outer`, `master_ratio`, `master_count`) + /// had no way to read the *current* value back at all - a settings + /// panel could set any of these blind, but not honestly show its own + /// control's starting position, or confirm a set actually took. + /// Flagged directly by the AGS peer session as the common shape behind + /// several separate gaps at once: "a control whose value cannot be + /// read back is a control that lies on every restart." + pub(crate) border_width: u32, + /// `#rrggbb`, matching the exact string shape `srd set border_color` + /// itself accepts (`srdwm_core::parse_hex_color`'s own format) - a + /// caller can feed this straight back into another `set` unchanged. + pub(crate) border_color: String, + pub(crate) corner_radius: u32, + /// `true` when new windows default to a server-drawn titlebar + /// (`general.decoration_mode`/`srd set decoration_mode`'s own "server" + /// value), `false` for "client" (CSD-only default). + pub(crate) decoration_mode_server: bool, + pub(crate) gap_inner: u32, + pub(crate) gap_outer: u32, + /// `TilingConfig::master_ratio`/`master_count` - see `WindowManager:: + /// adjust_master_ratio_for_drag`'s own doc comment for the live + /// interactive path (a resize-drag on the master/stack boundary) that + /// also mutates this, in addition to `srd set master_ratio`/ + /// `master_count`. + pub(crate) master_ratio: f32, + pub(crate) master_count: usize, } /// `"keyboard_layout"`'s one-shot reply shape - the active XKB layout's |