// 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: eller ipc:), 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, 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 = 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) { 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::().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, [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, [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; }