From 1e2cb836f49cad02d0f0291ba9feb73943f08852 Mon Sep 17 00:00:00 2001 From: "Pedro M. de Echanove Pasquin" Date: Tue, 26 May 2026 22:22:18 +0200 Subject: [PATCH] ltk: convert physical sizes to logical for overlays and input regions on HiDPI Overlay `set_size` and input regions were handed physical (layout-space) pixels straight to layer-shell and `wl_region`, both of which expect logical coordinates. They only coincide at scale 1, so on a scale-2 output every overlay requested a surface twice its intended size and the input region covered the wrong area. `apply_input_region` now takes the surface scale and divides each `Rect` down to logical before adding it to the region; the four draw paths (software and gles, full and partial) forward their scale. `reconcile_overlays` converts `OverlaySpec::size` to logical for both `set_size` and the initial `OverlayConfig` (0 = fill survives the divide), and seeds the new surface's `scale_factor` from the parent so the first configure allocates a HiDPI buffer instead of rendering at scale 1 until `scale_factor_changed` lands a frame or two later. --- src/draw/chrome.rs | 10 +++++++++- src/draw/gles.rs | 4 ++-- src/draw/software.rs | 4 ++-- src/event_loop/overlays_reconcile.rs | 24 +++++++++++++++++++++--- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/draw/chrome.rs b/src/draw/chrome.rs index d6f70be..3e03891 100644 --- a/src/draw/chrome.rs +++ b/src/draw/chrome.rs @@ -84,15 +84,23 @@ pub( crate ) fn apply_input_region( wl_surface: &WlSurface, compositor: &CompositorState, input_region: Option<&[Rect]>, + scale: u32, ) { if let Some( regions ) = input_region { if let Ok( region ) = Region::new( compositor ) { + // `Rect`s are physical (layout space); `wl_region` is logical. + let s = scale.max( 1 ) as f32; for r in regions { - region.add( r.x as i32, r.y as i32, r.width as i32, r.height as i32 ); + region.add( + ( r.x / s ).round() as i32, + ( r.y / s ).round() as i32, + ( r.width / s ).round() as i32, + ( r.height / s ).round() as i32, + ); } wl_surface.set_input_region( Some( region.wl_region() ) ); } diff --git a/src/draw/gles.rs b/src/draw/gles.rs index 0938993..a5bb6dc 100644 --- a/src/draw/gles.rs +++ b/src/draw/gles.rs @@ -141,7 +141,7 @@ pub( crate ) fn draw_surface_full_gpu( ss.content_dirty = false; let wl_surface = ss.surface.wl_surface(); - apply_input_region( wl_surface, compositor, input_region ); + apply_input_region( wl_surface, compositor, input_region, scale ); // Frame callback request must precede the implicit commit done by // `swap_buffers_with_damage`, so the compositor can attach it to this // frame and not the previous one. @@ -241,7 +241,7 @@ pub( crate ) fn draw_surface_partial_gpu( ss.content_dirty = false; let wl_surface = ss.surface.wl_surface(); - apply_input_region( wl_surface, compositor, input_region ); + apply_input_region( wl_surface, compositor, input_region, scale ); request_frame( wl_surface ); // Convert top-left dirty rects to EGL bottom-left coords for the // swap-with-damage call. diff --git a/src/draw/software.rs b/src/draw/software.rs index 6ea9ee7..893b7b8 100644 --- a/src/draw/software.rs +++ b/src/draw/software.rs @@ -167,7 +167,7 @@ pub( crate ) fn draw_surface_full( } } - apply_input_region( wl_surface, compositor, input_region ); + apply_input_region( wl_surface, compositor, input_region, scale ); // Request a frame callback BEFORE commit so the compositor schedules it // against this exact frame; sets `frame_pending` so the run loop won't // try to draw this surface again until the callback fires. @@ -282,7 +282,7 @@ pub( crate ) fn draw_surface_partial( wl_surface.damage_buffer( r.x as i32, r.y as i32, r.width as i32, r.height as i32 ); } - apply_input_region( wl_surface, compositor, input_region ); + apply_input_region( wl_surface, compositor, input_region, scale ); request_frame( wl_surface ); wl_surface.commit(); ss.frame_pending = true; diff --git a/src/event_loop/overlays_reconcile.rs b/src/event_loop/overlays_reconcile.rs index 670c4c5..c5628da 100644 --- a/src/event_loop/overlays_reconcile.rs +++ b/src/event_loop/overlays_reconcile.rs @@ -99,7 +99,19 @@ pub( super ) fn reconcile_overlays( data: &mut AppData ) SurfaceKind::Window( ref w ) => Some( w.xdg_surface().clone() ), _ => None, }; - let parent_scale = data.main.scale_factor.max( 1 ) as f32; + let parent_scale_i = data.main.scale_factor.max( 1 ); + let parent_scale = parent_scale_i as f32; + // `OverlaySpec::size` is physical pixels (the app's layout space); the + // layer-shell `set_size` is logical. They only coincide at scale 1, so + // convert here — without it a scale-2 overlay requests a surface twice + // its intended size. `0 = fill` survives the divide. + let to_logical_size = move | ( w, h ): ( u32, u32 ) | -> ( u32, u32 ) + { + ( + ( w as f32 / parent_scale ).round() as u32, + ( h as f32 / parent_scale ).round() as u32, + ) + }; // Snapshot the previous-frame anchor lookup table so we can resolve // `anchor_widget_id` → `Rect` without holding a borrow on `data.main` // across the overlay-mut loop below. @@ -122,7 +134,8 @@ pub( super ) fn reconcile_overlays( data: &mut AppData ) { if let SurfaceKind::Layer( ref layer_surface ) = ss.surface { - layer_surface.set_size( spec.size.0, spec.size.1 ); + let ( lw, lh ) = to_logical_size( spec.size ); + layer_surface.set_size( lw, lh ); layer_surface.commit(); ss.last_requested_size = spec.size; } @@ -272,7 +285,7 @@ pub( super ) fn reconcile_overlays( data: &mut AppData ) layer: spec.layer.to_wlr_layer(), exclusive_zone: spec.exclusive_zone, anchor: spec.anchor, - size: spec.size, + size: to_logical_size( spec.size ), keyboard_exclusive: spec.keyboard_exclusive, namespace: "ltk-overlay", }; @@ -282,6 +295,11 @@ pub( super ) fn reconcile_overlays( data: &mut AppData ) surface.materialize( cs, layer_shell, qh, output ); } let mut ss = SurfaceState::::new( surface, 0.0, String::new() ); + // Inherit the parent's scale so the first configure allocates a + // HiDPI buffer and lays out at the right size from frame one, + // instead of rendering at scale 1 until `scale_factor_changed` + // lands a frame or two later. + ss.scale_factor = parent_scale_i; ss.last_requested_size = spec.size; ss.layer_anchor = Some( spec.anchor ); overlays_m.insert( spec.id, ss );