fix(linux): reposition notifications for multi-monitor and arbitrary resolutions

This commit is contained in:
katR 2026-09-06 18:14:01 +05:30
parent efd7d65bd6
commit 80f489e2fa
2 changed files with 109 additions and 24 deletions

View file

@ -181,10 +181,32 @@ pub fn show_shadowplay_toast(title: &str, subtitle: &str, icon: ToastIcon) {
(accent_hex, accent_rgb)
};
fn get_active_gdk_monitor(display: &gdk::Display) -> Option<gdk::Monitor> {
if let Ok(out) = std::process::Command::new("hyprctl").args(["monitors", "-j"]).output() {
if let Ok(v) = serde_json::from_slice::<Vec<serde_json::Value>>(&out.stdout) {
let focused = v.iter().find(|m| m["focused"].as_bool().unwrap_or(false))
.or_else(|| v.first());
if let Some(m) = focused {
let fx = m["x"].as_i64().unwrap_or(0) as i32;
let fy = m["y"].as_i64().unwrap_or(0) as i32;
for i in 0..display.n_monitors() {
if let Some(mon) = display.monitor(i) {
let geom = mon.geometry();
if geom.x() == fx && geom.y() == fy {
return Some(mon);
}
}
}
}
}
}
display.primary_monitor().or_else(|| display.monitor(0))
}
app.connect_activate(move |app| {
let window = ApplicationWindow::builder()
.application(app)
.default_width(340)
.default_width(360)
.default_height(64)
.build();
@ -193,26 +215,31 @@ pub fn show_shadowplay_toast(title: &str, subtitle: &str, icon: ToastIcon) {
#[cfg(not(target_os = "linux"))]
let layer_shell_ok = false;
let active_monitor = gdk::Display::default().and_then(|d| get_active_gdk_monitor(&d));
if layer_shell_ok {
window.init_layer_shell();
window.set_layer(Layer::Overlay);
window.set_namespace("scythe-notification");
#[cfg(target_os = "linux")]
if let Some(ref mon) = active_monitor {
gtk_layer_shell::LayerShell::set_monitor(&window, mon);
}
window.set_anchor(gtk_layer_shell::Edge::Top, true);
window.set_anchor(gtk_layer_shell::Edge::Right, true);
window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 24);
window.set_layer_shell_margin(gtk_layer_shell::Edge::Right, 24);
window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 28);
window.set_layer_shell_margin(gtk_layer_shell::Edge::Right, 28);
window.set_keyboard_interactivity(false);
} else {
window.set_decorated(false);
window.set_keep_above(true);
window.set_skip_taskbar_hint(true);
window.set_accept_focus(false);
if let Some(display) = gdk::Display::default()
&& let Some(mon) = display.primary_monitor().or_else(|| display.monitor(0)) {
let geom = mon.geometry();
let x = geom.x() + geom.width() - 340 - 24;
let y = geom.y() + 24;
window.move_(x, y);
if let Some(ref mon) = active_monitor {
let geom = mon.geometry();
let x = geom.x() + geom.width() - 360 - 28;
let y = geom.y() + 28;
window.move_(x, y);
}
}
@ -406,8 +433,8 @@ pub fn show_shadowplay_toast(title: &str, subtitle: &str, icon: ToastIcon) {
hbox.pack_start(&vbox, true, true, 0);
let fixed = Fixed::new();
fixed.set_size_request(340, 64);
fixed.put(&hbox, 340, 4);
fixed.set_size_request(360, 64);
fixed.put(&hbox, 360, 4);
window.add(&fixed);
window.show_all();
@ -426,16 +453,16 @@ pub fn show_shadowplay_toast(title: &str, subtitle: &str, icon: ToastIcon) {
let slide_x = if elapsed < 0.35 {
let t = (elapsed / 0.35).min(1.0);
let ease = 1.0 - (1.0 - t).powi(3);
(1.0 - ease) * 340.0
(1.0 - ease) * 360.0
} else if elapsed < 2.40 {
0.0
} else {
let t = ((elapsed - 2.40) / 0.40).min(1.0);
let ease = t.powi(3);
ease * 340.0
ease * 360.0
};
let target_x = (slide_x + 10.0).round() as i32;
let target_x = (slide_x + 14.0).round() as i32;
fixed_clone.move_(&hbox_clone, target_x, 4);
window_clone.queue_draw();
gtk::glib::ControlFlow::Continue

View file

@ -2030,7 +2030,7 @@ impl ScytheOverlayApp {
let card_w = 320.0;
let card_h = 56.0;
let toast_rect = egui::Rect::from_min_size(
egui::pos2(screen_w - card_w - 24.0 + slide_x, 24.0),
egui::pos2(screen_w - card_w - 28.0 + slide_x, 28.0),
egui::vec2(card_w, card_h),
);
@ -2287,22 +2287,79 @@ pub fn apply_windows_transparency(title: &str) {
}
}
#[cfg(not(target_os = "windows"))]
pub fn query_focused_monitor_rect() -> (f32, f32, f32, f32) {
// 1. hyprctl monitors -j (Hyprland/Wayland)
if let Ok(out) = std::process::Command::new("hyprctl").args(["monitors", "-j"]).output() {
if let Ok(v) = serde_json::from_slice::<Vec<serde_json::Value>>(&out.stdout) {
let focused = v.iter().find(|m| m["focused"].as_bool().unwrap_or(false))
.or_else(|| v.first());
if let Some(m) = focused {
let x = m["x"].as_f64().unwrap_or(0.0) as f32;
let y = m["y"].as_f64().unwrap_or(0.0) as f32;
let w = m["width"].as_f64().unwrap_or(1920.0) as f32;
let h = m["height"].as_f64().unwrap_or(1080.0) as f32;
let scale = m["scale"].as_f64().unwrap_or(1.0) as f32;
let lw = if scale > 0.0 { w / scale } else { w };
let lh = if scale > 0.0 { h / scale } else { h };
let lx = if scale > 0.0 { x / scale } else { x };
let ly = if scale > 0.0 { y / scale } else { y };
if lw > 320.0 && lh > 200.0 {
return (lx, ly, lw, lh);
}
}
}
}
// 2. xrandr fallback (X11 / XWayland)
if let Ok(out) = std::process::Command::new("xrandr").output() {
if let Ok(text) = std::str::from_utf8(&out.stdout) {
for line in text.lines() {
if line.contains(" connected") {
for part in line.split_whitespace() {
if part.contains('x') && part.contains('+') {
let parts: Vec<&str> = part.split('+').collect();
if parts.len() >= 3 {
let dims: Vec<&str> = parts[0].split('x').collect();
if dims.len() == 2 {
if let (Ok(w), Ok(h), Ok(x), Ok(y)) = (
dims[0].parse::<f32>(),
dims[1].parse::<f32>(),
parts[1].parse::<f32>(),
parts[2].parse::<f32>(),
) {
if w > 320.0 && h > 200.0 {
return (x, y, w, h);
}
}
}
}
}
}
}
}
}
}
(0.0, 0.0, 1920.0, 1080.0)
}
pub fn run_egui_overlay() {
#[cfg(target_os = "windows")]
let (screen_w, screen_h): (f32, f32) = unsafe {
let (screen_x, screen_y, screen_w, screen_h): (f32, f32, f32, f32) = unsafe {
use windows::Win32::UI::WindowsAndMessaging::{GetSystemMetrics, SM_CXSCREEN, SM_CYSCREEN};
let w = GetSystemMetrics(SM_CXSCREEN) as f32;
let h = GetSystemMetrics(SM_CYSCREEN) as f32;
if w > 100.0 && h > 100.0 { (w, h) } else { (1920.0, 1080.0) }
let sw = if w > 100.0 { w } else { 1920.0 };
let sh = if h > 100.0 { h } else { 1080.0 };
(0.0, 0.0, sw, sh)
};
#[cfg(not(target_os = "windows"))]
let (screen_w, screen_h): (f32, f32) = (1920.0, 1080.0);
let (screen_x, screen_y, screen_w, screen_h) = query_focused_monitor_rect();
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_title("Scythe")
.with_app_id("scythe-overlay")
.with_position([0.0, 0.0])
.with_position([screen_x, screen_y])
.with_inner_size([screen_w, screen_h])
.with_maximized(false)
.with_resizable(false)
@ -2474,16 +2531,17 @@ pub fn run_egui_toast(title: &str, subtitle: &str, icon: crate::overlay::ToastIc
}
#[cfg(target_os = "windows")]
let screen_w: f32 = unsafe {
let (mon_x, screen_w): (f32, f32) = unsafe {
use windows::Win32::UI::WindowsAndMessaging::{GetSystemMetrics, SM_CXSCREEN};
let w = GetSystemMetrics(SM_CXSCREEN) as f32;
if w > 100.0 { w } else { 1920.0 }
let sw = if w > 100.0 { w } else { 1920.0 };
(0.0, sw)
};
#[cfg(not(target_os = "windows"))]
let screen_w: f32 = 1920.0;
let (mon_x, _mon_y, screen_w, _screen_h) = query_focused_monitor_rect();
let pos_x = (screen_w - toast_w - 24.0).max(10.0);
let pos_y = 24.0;
let pos_x = (mon_x + screen_w - toast_w - 28.0).max(10.0);
let pos_y = 28.0;
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()