mirror of
https://github.com/nestriness/nestri.git
synced 2026-11-03 22:22:39 +02:00
fix(nescope): a window cannot ask for more than the output has (#438)
Phase 0 of decision 0086. Cause (inferred from the code, not observed live): the client's window size resizes the box's output, but a Proton game under XWayland re-asserts its launch size with a ConfigureRequest, which configure_request honoured. The window ends up larger than the output; the pointer lives on the output (clamp_cursor), so the cut-off part of the window is unreachable. The client sends relative motion only, so no client-side mapping is involved. Fix: configure_request clamps a non-override-redirect X11 window's requested size to the current output size (and keeps it inside the output). The output keeps following the client. Pure fn clamp_to_output, unit-tested incl. width-only, height-only and multiple windows. Risk: a game that ignores the configure and keeps drawing at its own size will still be cropped. Wayland toplevels are unchanged (they already get the output size). Live proof is still pending: pointer reach on a real Proton window after a resize has not been exercised.
This commit is contained in:
@@ -537,14 +537,24 @@ impl XwmHandler for CalloopData {
|
||||
h: Option<u32>,
|
||||
_reorder: Option<Reorder>,
|
||||
) {
|
||||
// Honor size requests within reason but always keep position at (0,0).
|
||||
let mut geo = window.geometry();
|
||||
if let Some(w) = w {
|
||||
geo.size.w = w as i32;
|
||||
// Honor size requests, but never past the output: a window larger
|
||||
// than it is cut off, and the pointer cannot reach the cut part. The
|
||||
// output keeps following the client's window instead.
|
||||
if window.is_override_redirect() {
|
||||
return;
|
||||
}
|
||||
if let Some(h) = h {
|
||||
geo.size.h = h as i32;
|
||||
let (req_w, req_h) = (w, h);
|
||||
let cur = window.geometry();
|
||||
let (x, y, w, h) = crate::state::clamp_to_output(
|
||||
(cur.loc.x, cur.loc.y, cur.size.w, cur.size.h),
|
||||
(w, h),
|
||||
(self.state.width as i32, self.state.height as i32),
|
||||
);
|
||||
let asked = (req_w, req_h);
|
||||
if req_w.is_some_and(|v| v as i32 != w) || req_h.is_some_and(|v| v as i32 != h) {
|
||||
self.state.note_clamp(window.window_id(), asked, (w, h));
|
||||
}
|
||||
let geo = Rectangle::new((x, y).into(), (w, h).into());
|
||||
let _ = window.configure(geo);
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +91,43 @@ impl WinKey {
|
||||
}
|
||||
}
|
||||
|
||||
/// A window's geometry after it asked for `want` (either side may be absent),
|
||||
/// held inside an `output` of its own size: no larger, and moved back in if
|
||||
/// the request would put part of it outside. Pure so it can be tested.
|
||||
pub fn clamp_to_output(
|
||||
cur: (i32, i32, i32, i32),
|
||||
want: (Option<u32>, Option<u32>),
|
||||
output: (i32, i32),
|
||||
) -> (i32, i32, i32, i32) {
|
||||
let side = |req: Option<u32>, cur: i32, max: i32| {
|
||||
let v = req.map_or(cur, |v| v.min(i32::MAX as u32) as i32);
|
||||
v.clamp(1, max.max(1))
|
||||
};
|
||||
let w = side(want.0, cur.2, output.0);
|
||||
let h = side(want.1, cur.3, output.1);
|
||||
let x = cur.0.clamp(0, (output.0 - w).max(0));
|
||||
let y = cur.1.clamp(0, (output.1 - h).max(0));
|
||||
(x, y, w, h)
|
||||
}
|
||||
|
||||
impl NescopeState {
|
||||
/// Say a request was clamped. A window that keeps asking is fighting the
|
||||
/// configure: warn once when the run passes 50 inside 5 seconds.
|
||||
pub fn note_clamp(&mut self, id: u32, asked: (Option<u32>, Option<u32>), got: (i32, i32)) {
|
||||
let now = std::time::Instant::now();
|
||||
let run = self.clamp_runs.entry(id).or_insert((now, 0));
|
||||
if now.duration_since(run.0) > std::time::Duration::from_secs(5) {
|
||||
*run = (now, 0);
|
||||
}
|
||||
run.1 += 1;
|
||||
let n = run.1;
|
||||
tracing::debug!(window = id, ?asked, clamped = ?got, repeats = n, "configure_request clamped");
|
||||
if n == 50 {
|
||||
tracing::warn!(window = id, ?asked, clamped = ?got, "window keeps asking past the output: configure ping-pong?");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CalloopData {
|
||||
pub state: NescopeState,
|
||||
pub display: Display<NescopeState>,
|
||||
@@ -239,6 +276,8 @@ pub struct NescopeState {
|
||||
/// counted for, ref(d-0036): a client's window can shrink the output
|
||||
/// below it, never grow it past.
|
||||
pub launch_size: (u32, u32),
|
||||
/// Per X11 window: when its run of clamped requests began and how many.
|
||||
pub clamp_runs: std::collections::HashMap<u32, (std::time::Instant, u32)>,
|
||||
pub fps: u32,
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────
|
||||
@@ -376,6 +415,7 @@ impl NescopeState {
|
||||
width,
|
||||
height,
|
||||
launch_size: (width, height),
|
||||
clamp_runs: std::collections::HashMap::new(),
|
||||
fps,
|
||||
loop_handle,
|
||||
game_launched: false,
|
||||
@@ -1213,3 +1253,43 @@ mod fit_output_tests {
|
||||
assert_eq!(fit_output((10, 10), (1920, 1080)), (320, 320));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod clamp_to_output_tests {
|
||||
use super::clamp_to_output;
|
||||
|
||||
const OUT: (i32, i32) = (1280, 720);
|
||||
|
||||
#[test]
|
||||
fn a_window_that_fits_is_left_alone() {
|
||||
assert_eq!(clamp_to_output((0, 0, 800, 600), (Some(1000), Some(700)), OUT), (0, 0, 1000, 700));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_launch_size_game_is_held_to_the_shrunk_output() {
|
||||
assert_eq!(clamp_to_output((0, 0, 1280, 720), (Some(1920), Some(1080)), OUT), (0, 0, 1280, 720));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_width_only_request_leaves_the_height() {
|
||||
assert_eq!(clamp_to_output((0, 0, 1280, 500), (Some(1920), None), OUT), (0, 0, 1280, 500));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_height_only_request_leaves_the_width() {
|
||||
assert_eq!(clamp_to_output((0, 0, 640, 720), (None, Some(2000)), OUT), (0, 0, 640, 720));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_window_pushed_past_the_edge_is_moved_back() {
|
||||
assert_eq!(clamp_to_output((900, 500, 400, 300), (None, None), OUT), (880, 420, 400, 300));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_window_is_clamped_on_its_own() {
|
||||
let a = clamp_to_output((0, 0, 1920, 1080), (None, None), OUT);
|
||||
let b = clamp_to_output((100, 100, 300, 200), (Some(5000), None), OUT);
|
||||
assert_eq!(a, (0, 0, 1280, 720));
|
||||
assert_eq!(b, (0, 100, 1280, 200));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user