Files
waydock/src/hotspots.rs
claude 558c178ec4
All checks were successful
check / check (push) Successful in 1m39s
Per-skärm-inställningar med skärmväljare, kant-i-kant-marginaler
- outputs{} är nu generiska JSON-överridar: vilket configfält som helst
  kan sättas per skärm och deep-mergas över den globala configen
  (Config::for_output). Docka, autohide, dodge, beteende, hotspots och
  CSS (scopat på window.o-<connector>) läser skärmens effektiva config.
- Settings har skärmväljare överst: klickbar förhandsgranskning av
  skärmarna + deras barer, 'Alla skärmar' (standard) eller enskild
  skärm. Bara ändrade fält sparas som override (json-diff);
  återställ-knapp tar bort skärmens överridar. * = har egna.
- Marginaler: skuggutrymmet läggs inte längre på förankrade sidor så
  0 = kant i kant; minusvärden tillåts (A ner till -64, B/C -256).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 00:25:05 +02:00

186 lines
6.0 KiB
Rust

// Hot corners & hot edges: små osynliga layer-ytor vid skärmens hörn
// och kanter. Pekaren måste vila där i dwell_ms innan actionen körs
// (exec:<kommando> eller ipc:<metod>), och måste lämna ytan innan den
// kan trigga igen.
use std::cell::Cell;
use std::rc::Rc;
use std::time::Duration;
use gtk4 as gtk;
use gtk4::gdk;
use gtk4::glib;
use gtk4::prelude::*;
use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell};
use log::{info, warn};
use crate::config::HotspotAction;
use crate::dock::Ctx;
use crate::wayfire_ipc;
const CORNER_PX: i32 = 6;
const EDGE_PX: i32 = 2;
fn run_action(action: &HotspotAction) {
match action {
HotspotAction::Exec(cmd) => {
info!("hotspot: kör {cmd}");
let cmd = cmd.clone();
if let Err(e) = glib::spawn_command_line_async(&cmd) {
warn!("hotspot-kommando misslyckades: {e}");
}
}
HotspotAction::Ipc(method) => {
info!("hotspot: ipc {method}");
if let Err(e) = wayfire_ipc::call(method, serde_json::Value::Null) {
warn!("hotspot-ipc {method}: {e}");
}
}
}
}
/// En hotspot-yta: `anchors` anger vilka kanter den fästs mot
/// (två = hörn, en/tre = kant), `size` (w, h) där -1 = sträck ut.
fn build_hotspot(
ctx: &Rc<Ctx>,
monitor: &gdk::Monitor,
anchors: &[Edge],
size: (i32, i32),
action: HotspotAction,
dwell_ms: u32,
) -> gtk::ApplicationWindow {
let win = gtk::ApplicationWindow::new(&ctx.app);
win.add_css_class("waydock-hotspot");
win.init_layer_shell();
win.set_namespace(Some("waydock-hotspot"));
win.set_monitor(Some(monitor));
win.set_layer(Layer::Overlay);
win.set_keyboard_mode(KeyboardMode::None);
win.set_exclusive_zone(0);
for edge in anchors {
win.set_anchor(*edge, true);
}
let area = gtk::Box::new(gtk::Orientation::Horizontal, 0);
area.set_size_request(size.0, size.1);
win.set_child(Some(&area));
let armed = Rc::new(Cell::new(true));
let inside = Rc::new(Cell::new(false));
let motion = gtk::EventControllerMotion::new();
{
let armed = armed.clone();
let inside = inside.clone();
motion.connect_enter(move |_, _, _| {
inside.set(true);
if !armed.get() {
return;
}
let armed = armed.clone();
let inside = inside.clone();
let action = action.clone();
glib::timeout_add_local_once(Duration::from_millis(dwell_ms as u64), move || {
if inside.get() && armed.get() {
armed.set(false); // återarmeras när pekaren lämnar
run_action(&action);
}
});
});
}
{
let armed = armed.clone();
let inside = inside.clone();
motion.connect_leave(move |_| {
inside.set(false);
armed.set(true);
});
}
area.add_controller(motion);
win.present();
win
}
thread_local! {
static LAST_SIGNATURE: std::cell::RefCell<String> =
const { std::cell::RefCell::new(String::new()) };
}
/// Bygg om alla hotspot-ytor enligt configen (körs vid start + configbyte).
/// Idempotent: oförändrad hotspot-config + skärmuppsättning → ingen ombyggnad.
pub fn rebuild(ctx: &Rc<Ctx>) {
let display = gdk::Display::default().expect("ingen display");
let monitors = display.monitors();
// signatur = alla skärmars EFFEKTIVA hotspot-config
let mut signature = String::new();
let mut mons: Vec<(String, gdk::Monitor)> = Vec::new();
for i in 0..monitors.n_items() {
let Some(monitor) = monitors.item(i).and_then(|o| o.downcast::<gdk::Monitor>().ok())
else {
continue;
};
let connector = monitor
.connector()
.map(|c| c.to_string())
.unwrap_or_else(|| format!("monitor-{i}"));
let eff = ctx.cfg.borrow().for_output(&connector);
signature += &format!("{connector}:{:?}|", eff.hotspots);
mons.push((connector, monitor));
}
let unchanged = LAST_SIGNATURE.with(|s| *s.borrow() == signature);
if unchanged {
return;
}
LAST_SIGNATURE.with(|s| *s.borrow_mut() = signature);
for win in ctx.hotspot_windows.borrow_mut().drain(..) {
win.destroy();
}
let mut windows = Vec::new();
for (connector, monitor) in &mons {
let h = ctx.cfg.borrow().for_output(connector).hotspots;
let dwell = h.dwell_ms;
let c = h.corners.clone();
let e = h.edges.clone();
let corner_defs: [(&Option<HotspotAction>, [Edge; 2]); 4] = [
(&c.top_left, [Edge::Top, Edge::Left]),
(&c.top_right, [Edge::Top, Edge::Right]),
(&c.bottom_left, [Edge::Bottom, Edge::Left]),
(&c.bottom_right, [Edge::Bottom, Edge::Right]),
];
// kanterna lämnar hörnen fria (hörn-ytor ligger ovanpå ändå)
#[allow(clippy::type_complexity)]
let edge_defs: [(&Option<HotspotAction>, [Edge; 3], (i32, i32)); 4] = [
(&e.top, [Edge::Top, Edge::Left, Edge::Right], (-1, EDGE_PX)),
(&e.bottom, [Edge::Bottom, Edge::Left, Edge::Right], (-1, EDGE_PX)),
(&e.left, [Edge::Left, Edge::Top, Edge::Bottom], (EDGE_PX, -1)),
(&e.right, [Edge::Right, Edge::Top, Edge::Bottom], (EDGE_PX, -1)),
];
for (action, edges) in &corner_defs {
if let Some(a) = action {
windows.push(build_hotspot(
ctx,
monitor,
edges,
(CORNER_PX, CORNER_PX),
a.clone(),
dwell,
));
}
}
for (action, edges, size) in &edge_defs {
if let Some(a) = action {
windows.push(build_hotspot(ctx, monitor, edges, *size, a.clone(), dwell));
}
}
}
if !windows.is_empty() {
info!("{} hotspot-ytor aktiva", windows.len());
}
*ctx.hotspot_windows.borrow_mut() = windows;
}