M6: autohide/dodge, hot corners/edges, Wayfire-eventström, urgency
Some checks failed
check / check (push) Failing after 13s
Some checks failed
check / check (push) Failing after 13s
Autohide med Revealer-animation + sensorremsa vid kanten; dodge räknar fönsteröverlapp via Wayfire-IPC-geometri, helt händelsestyrt via window-rules/events/watch (ingen polling). Hotspot-ytor för hörn/kanter med dwell + exec/ipc-actions. Urgency-puls via view-hints-changed. IPC-socketen hittas även när pluginen aktiverats efter sessionsstart. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
162
src/hotspots.rs
Normal file
162
src/hotspots.rs
Normal file
@@ -0,0 +1,162 @@
|
||||
// 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
|
||||
}
|
||||
|
||||
/// Bygg om alla hotspot-ytor enligt configen (körs vid start + configbyte).
|
||||
pub fn rebuild(ctx: &Rc<Ctx>) {
|
||||
for win in ctx.hotspot_windows.borrow_mut().drain(..) {
|
||||
win.destroy();
|
||||
}
|
||||
|
||||
let cfg = ctx.cfg.borrow();
|
||||
let dwell = cfg.hotspots.dwell_ms;
|
||||
let c = cfg.hotspots.corners.clone();
|
||||
let e = cfg.hotspots.edges.clone();
|
||||
drop(cfg);
|
||||
|
||||
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å — overlay
|
||||
// med senare mappade ytor hamnar överst, så det kvittar i praktiken)
|
||||
#[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)),
|
||||
];
|
||||
|
||||
let display = gdk::Display::default().expect("ingen display");
|
||||
let monitors = display.monitors();
|
||||
let mut windows = 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;
|
||||
};
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user