mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 08:21:35 +00:00
fix(linux): keep Quick Chat on screen across DPI and widget resizes (#114271)
Two geometry defects found while stress-testing the companion. Positioning mixed coordinate spaces. `work_area()` is physical pixels of the monitor Quick Chat is moving to, but `inner_size()` is physical pixels at the scale of the monitor it is on now. A 640pt window on a 2x display reports 1280px, so invoking it on a 1x 1920px display centred using 1280 and landed it 320px left of centre. Re-express the window size in the target monitor's scale first; equal scales give a ratio of 1, so single-monitor setups are untouched to the pixel. Widget growth resized without re-anchoring. `quickchat_set_expanded` resizes and then repositions, but the widget path only resized, keeping the old top edge. Growing from 360pt to 440pt in a 440pt work area left the bottom 80pt off-screen and unreachable. `resize_window_if_needed` now reports whether it actually changed the height so the caller re-anchors only on a real resize, rather than moving the window when nothing changed.
This commit is contained in:
committed by
GitHub
parent
95f56b84cb
commit
730b341f3f
@@ -478,7 +478,28 @@ fn ensure_quickchat_window(app: &AppHandle) -> Result<WebviewWindow, String> {
|
||||
Ok(window)
|
||||
}
|
||||
|
||||
fn position_quickchat(app: &AppHandle, window: &Window) -> Result<(), String> {
|
||||
/// Re-express a window's physical size in a target monitor's physical pixels.
|
||||
///
|
||||
/// `inner_size()` reports physical pixels at the scale of the monitor the
|
||||
/// window is on *now*, while `work_area()` is physical pixels of the monitor we
|
||||
/// are about to move to. Comparing them directly misplaces Quick Chat across a
|
||||
/// mixed-DPI boundary: a 640pt window on a 2x display reports 1280px, so
|
||||
/// centring it on a 1x display uses double its real width. Scales that are
|
||||
/// equal (the single-monitor case) give a ratio of 1 and change nothing.
|
||||
pub(crate) fn quickchat_target_size(
|
||||
window_physical: (f64, f64),
|
||||
window_scale: f64,
|
||||
monitor_scale: f64,
|
||||
) -> (f64, f64) {
|
||||
let usable = |scale: f64| scale.is_finite() && scale > 0.0;
|
||||
if !usable(window_scale) || !usable(monitor_scale) {
|
||||
return window_physical;
|
||||
}
|
||||
let ratio = monitor_scale / window_scale;
|
||||
(window_physical.0 * ratio, window_physical.1 * ratio)
|
||||
}
|
||||
|
||||
pub(crate) fn position_quickchat(app: &AppHandle, window: &Window) -> Result<(), String> {
|
||||
let monitor = app
|
||||
.cursor_position()
|
||||
.ok()
|
||||
@@ -490,10 +511,18 @@ fn position_quickchat(app: &AppHandle, window: &Window) -> Result<(), String> {
|
||||
let window_size = window
|
||||
.inner_size()
|
||||
.map_err(|error| format!("Could not read Quick Chat size: {error}"))?;
|
||||
let window_scale = window
|
||||
.scale_factor()
|
||||
.map_err(|error| format!("Could not read Quick Chat scale: {error}"))?;
|
||||
let target_size = quickchat_target_size(
|
||||
(window_size.width as f64, window_size.height as f64),
|
||||
window_scale,
|
||||
monitor.scale_factor(),
|
||||
);
|
||||
let (x, y) = quickchat_position(
|
||||
(work_area.position.x as f64, work_area.position.y as f64),
|
||||
(work_area.size.width as f64, work_area.size.height as f64),
|
||||
(window_size.width as f64, window_size.height as f64),
|
||||
target_size,
|
||||
);
|
||||
window
|
||||
.set_position(PhysicalPosition::new(x.round() as i32, y.round() as i32))
|
||||
@@ -981,4 +1010,64 @@ mod tests {
|
||||
state.set_shortcut_registered(false);
|
||||
assert!(!state.matches_shortcut(&custom));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_size_is_identity_within_one_monitor() {
|
||||
// The single-monitor case must not move by even a pixel.
|
||||
assert_eq!(
|
||||
quickchat_target_size((1280.0, 720.0), 2.0, 2.0),
|
||||
(1280.0, 720.0)
|
||||
);
|
||||
assert_eq!(
|
||||
quickchat_target_size((640.0, 360.0), 1.0, 1.0),
|
||||
(640.0, 360.0)
|
||||
);
|
||||
assert_eq!(
|
||||
quickchat_target_size((960.0, 540.0), 1.5, 1.5),
|
||||
(960.0, 540.0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_size_rescales_across_a_dpi_boundary() {
|
||||
// A 640pt window on a 2x display reports 1280px; on a 1x display it is
|
||||
// really 640px wide, and centring with 1280 would sit it far left.
|
||||
assert_eq!(
|
||||
quickchat_target_size((1280.0, 720.0), 2.0, 1.0),
|
||||
(640.0, 360.0)
|
||||
);
|
||||
// And the reverse direction.
|
||||
assert_eq!(
|
||||
quickchat_target_size((640.0, 360.0), 1.0, 2.0),
|
||||
(1280.0, 720.0)
|
||||
);
|
||||
// Fractional scales, as Windows and some Linux compositors report.
|
||||
assert_eq!(
|
||||
quickchat_target_size((1200.0, 600.0), 2.0, 1.5),
|
||||
(900.0, 450.0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_size_falls_back_on_unusable_scales() {
|
||||
for scale in [0.0, -1.0, f64::NAN, f64::INFINITY] {
|
||||
assert_eq!(
|
||||
quickchat_target_size((800.0, 600.0), scale, 2.0),
|
||||
(800.0, 600.0)
|
||||
);
|
||||
assert_eq!(
|
||||
quickchat_target_size((800.0, 600.0), 2.0, scale),
|
||||
(800.0, 600.0)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rescaled_window_centers_on_the_target_monitor() {
|
||||
// End to end: a 2x-scaled 640pt-wide window invoked on a 1x 1920px
|
||||
// monitor. Without the rescale it centers using 1280 and lands at 320.
|
||||
let target = quickchat_target_size((1280.0, 720.0), 2.0, 1.0);
|
||||
let (x, _) = quickchat_position((0.0, 0.0), (1920.0, 1080.0), target);
|
||||
assert_eq!(x, 640.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::gateway_ws::GatewayClient;
|
||||
use crate::quickchat::{QuickChatState, QUICKCHAT_LABEL};
|
||||
use crate::quickchat::{position_quickchat, QuickChatState, QUICKCHAT_LABEL};
|
||||
use serde::Deserialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::HashSet;
|
||||
@@ -60,7 +60,9 @@ fn quickchat_window_height(has_widgets: bool, expanded: bool) -> f64 {
|
||||
}
|
||||
}
|
||||
|
||||
fn resize_window_if_needed(window: &Window, height: f64) -> Result<(), String> {
|
||||
/// Returns whether the window was actually resized, so the caller can re-anchor
|
||||
/// it only when its height really changed.
|
||||
fn resize_window_if_needed(window: &Window, height: f64) -> Result<bool, String> {
|
||||
let scale = window
|
||||
.scale_factor()
|
||||
.map_err(|error| format!("Could not read Quick Chat scale: {error}"))?;
|
||||
@@ -69,11 +71,12 @@ fn resize_window_if_needed(window: &Window, height: f64) -> Result<(), String> {
|
||||
.map_err(|error| format!("Could not read Quick Chat size: {error}"))?;
|
||||
let current_height = f64::from(current.height) / scale;
|
||||
if (current_height - height).abs() <= 0.5 {
|
||||
return Ok(());
|
||||
return Ok(false);
|
||||
}
|
||||
window
|
||||
.set_size(LogicalSize::new(QUICKCHAT_WIDTH, height))
|
||||
.map_err(|error| format!("Could not resize Quick Chat for widgets: {error}"))
|
||||
.map_err(|error| format!("Could not resize Quick Chat for widgets: {error}"))?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
impl QuickChatWidgetState {
|
||||
@@ -488,11 +491,21 @@ impl QuickChatWidgetState {
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Err(error) =
|
||||
resize_window_if_needed(&window, quickchat_window_height(has_widgets, expanded))
|
||||
{
|
||||
cleanup_created(&created);
|
||||
return Err(error);
|
||||
match resize_window_if_needed(&window, quickchat_window_height(has_widgets, expanded)) {
|
||||
// Growing for widgets has to re-anchor the window the same way the
|
||||
// expand path does. Resizing alone keeps the old top edge, so a
|
||||
// taller window can hang off the bottom of the work area.
|
||||
Ok(true) => {
|
||||
if let Err(error) = position_quickchat(window.app_handle(), &window) {
|
||||
cleanup_created(&created);
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(error) => {
|
||||
cleanup_created(&created);
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
|
||||
*self.views.lock().map_err(|_| {
|
||||
|
||||
Reference in New Issue
Block a user