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:
304
src/dock.rs
304
src/dock.rs
@@ -1,9 +1,12 @@
|
|||||||
// Dockfönstren: ett layer-shell-fönster per skärm. Bygger ikonraden av
|
// Dockfönstren: ett layer-shell-fönster per skärm. Bygger ikonraden av
|
||||||
// pins + körande fönstergrupper och kopplar klick/skroll/menyer.
|
// pins + körande fönstergrupper, kopplar klick/skroll/menyer och sköter
|
||||||
|
// autohide (alltid-göm eller dodge via Wayfire-IPC-geometri).
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::cell::{Cell, RefCell};
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use gtk4 as gtk;
|
use gtk4 as gtk;
|
||||||
use gtk4::gdk;
|
use gtk4::gdk;
|
||||||
@@ -13,7 +16,8 @@ use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell};
|
|||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
|
|
||||||
use crate::config::{
|
use crate::config::{
|
||||||
ClickAction, Layer as CfgLayer, MiddleClick, Position, ScrollAction, SharedConfig,
|
AutohideMode, ClickAction, Layer as CfgLayer, MiddleClick, Position, ScrollAction,
|
||||||
|
SharedConfig,
|
||||||
};
|
};
|
||||||
use crate::icons::{self, AppMeta};
|
use crate::icons::{self, AppMeta};
|
||||||
use crate::menu;
|
use crate::menu;
|
||||||
@@ -25,20 +29,35 @@ pub struct Ctx {
|
|||||||
pub cfg: SharedConfig,
|
pub cfg: SharedConfig,
|
||||||
pub cfg_path: PathBuf,
|
pub cfg_path: PathBuf,
|
||||||
pub service: RefCell<Option<Rc<ToplevelService>>>,
|
pub service: RefCell<Option<Rc<ToplevelService>>>,
|
||||||
pub docks: RefCell<Vec<Dock>>,
|
pub docks: RefCell<Vec<Rc<Dock>>>,
|
||||||
/// målet för den öppna kontextmenyn (menu.rs läser)
|
/// målet för den öppna kontextmenyn (menu.rs läser)
|
||||||
pub menu_target: RefCell<Option<menu::MenuTarget>>,
|
pub menu_target: RefCell<Option<menu::MenuTarget>>,
|
||||||
pub app: gtk::Application,
|
pub app: gtk::Application,
|
||||||
/// körs när configen ändrats (sätts av main; applicerar CSS/rebuild)
|
/// körs när configen ändrats (sätts av main; applicerar CSS/rebuild)
|
||||||
pub on_config_changed: RefCell<Option<Rc<dyn Fn()>>>,
|
pub on_config_changed: RefCell<Option<Rc<dyn Fn()>>>,
|
||||||
/// generationsräknare för settings-GUI:ts debouncade sparningar
|
/// generationsräknare för settings-GUI:ts debouncade sparningar
|
||||||
pub save_generation: std::cell::Cell<u64>,
|
pub save_generation: Cell<u64>,
|
||||||
|
/// app-nycklar vars fönster kräver uppmärksamhet (via Wayfire-events)
|
||||||
|
pub urgent: RefCell<HashSet<String>>,
|
||||||
|
/// hot corner/edge-fönster (hotspots.rs äger innehållet)
|
||||||
|
pub hotspot_windows: RefCell<Vec<gtk::ApplicationWindow>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Dock {
|
pub struct Dock {
|
||||||
pub window: gtk::ApplicationWindow,
|
pub window: gtk::ApplicationWindow,
|
||||||
pub panel: gtk::Box,
|
pub panel: gtk::Box,
|
||||||
|
pub revealer: gtk::Revealer,
|
||||||
pub connector: String,
|
pub connector: String,
|
||||||
|
/// pekaren är över dockan just nu
|
||||||
|
pub hover: Cell<bool>,
|
||||||
|
/// dockan är undan-gömd av autohide
|
||||||
|
pub hidden: Cell<bool>,
|
||||||
|
/// något fönster överlappar dockans kantzon (dodge)
|
||||||
|
pub dodge_overlap: Cell<bool>,
|
||||||
|
/// generationsräknare för fördröjd göm-timer
|
||||||
|
hide_generation: Cell<u64>,
|
||||||
|
/// sensorremsan som väcker dockan när den är gömd
|
||||||
|
sensor: RefCell<Option<gtk::ApplicationWindow>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// En ikon i docken: en pin och/eller en grupp körande fönster.
|
/// En ikon i docken: en pin och/eller en grupp körande fönster.
|
||||||
@@ -55,6 +74,22 @@ fn orientation(pos: &Position) -> gtk::Orientation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn main_edge(pos: &Position) -> Edge {
|
||||||
|
match pos {
|
||||||
|
Position::Bottom => Edge::Bottom,
|
||||||
|
Position::Top => Edge::Top,
|
||||||
|
Position::Left => Edge::Left,
|
||||||
|
Position::Right => Edge::Right,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn side_edges(pos: &Position) -> (Edge, Edge) {
|
||||||
|
match pos {
|
||||||
|
Position::Bottom | Position::Top => (Edge::Left, Edge::Right),
|
||||||
|
_ => (Edge::Top, Edge::Bottom),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn configure_layer(win: >k::ApplicationWindow, ctx: &Ctx, monitor: &gdk::Monitor) {
|
fn configure_layer(win: >k::ApplicationWindow, ctx: &Ctx, monitor: &gdk::Monitor) {
|
||||||
let cfg = ctx.cfg.borrow();
|
let cfg = ctx.cfg.borrow();
|
||||||
win.init_layer_shell();
|
win.init_layer_shell();
|
||||||
@@ -67,20 +102,15 @@ fn configure_layer(win: >k::ApplicationWindow, ctx: &Ctx, monitor: &gdk::Monit
|
|||||||
});
|
});
|
||||||
win.set_keyboard_mode(KeyboardMode::None);
|
win.set_keyboard_mode(KeyboardMode::None);
|
||||||
|
|
||||||
let (main_edge, side_a, side_b) = match cfg.position {
|
let edge = main_edge(&cfg.position);
|
||||||
Position::Bottom => (Edge::Bottom, Edge::Left, Edge::Right),
|
let (side_a, side_b) = side_edges(&cfg.position);
|
||||||
Position::Top => (Edge::Top, Edge::Left, Edge::Right),
|
win.set_anchor(edge, true);
|
||||||
Position::Left => (Edge::Left, Edge::Top, Edge::Bottom),
|
win.set_margin(edge, cfg.margin);
|
||||||
Position::Right => (Edge::Right, Edge::Top, Edge::Bottom),
|
|
||||||
};
|
|
||||||
win.set_anchor(main_edge, true);
|
|
||||||
win.set_margin(main_edge, cfg.margin);
|
|
||||||
if !cfg.centered {
|
if !cfg.centered {
|
||||||
// full bredd/höjd längs kanten
|
|
||||||
win.set_anchor(side_a, true);
|
win.set_anchor(side_a, true);
|
||||||
win.set_anchor(side_b, true);
|
win.set_anchor(side_b, true);
|
||||||
}
|
}
|
||||||
if cfg.avoid_windows {
|
if cfg.avoid_windows && !cfg.autohide.enabled {
|
||||||
win.auto_exclusive_zone_enable();
|
win.auto_exclusive_zone_enable();
|
||||||
} else {
|
} else {
|
||||||
win.set_exclusive_zone(0);
|
win.set_exclusive_zone(0);
|
||||||
@@ -90,6 +120,9 @@ fn configure_layer(win: >k::ApplicationWindow, ctx: &Ctx, monitor: &gdk::Monit
|
|||||||
/// Bygg om alla dockfönster från grunden (vid start, configbyte, skärmbyte).
|
/// Bygg om alla dockfönster från grunden (vid start, configbyte, skärmbyte).
|
||||||
pub fn rebuild_all(ctx: &Rc<Ctx>) {
|
pub fn rebuild_all(ctx: &Rc<Ctx>) {
|
||||||
for dock in ctx.docks.borrow_mut().drain(..) {
|
for dock in ctx.docks.borrow_mut().drain(..) {
|
||||||
|
if let Some(sensor) = dock.sensor.borrow_mut().take() {
|
||||||
|
sensor.destroy();
|
||||||
|
}
|
||||||
dock.window.destroy();
|
dock.window.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,9 +146,11 @@ pub fn rebuild_all(ctx: &Rc<Ctx>) {
|
|||||||
}
|
}
|
||||||
*ctx.docks.borrow_mut() = docks;
|
*ctx.docks.borrow_mut() = docks;
|
||||||
refresh_items(ctx);
|
refresh_items(ctx);
|
||||||
|
recompute_dodge(ctx);
|
||||||
|
update_autohide(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_dock(ctx: &Rc<Ctx>, monitor: &gdk::Monitor, connector: String) -> Dock {
|
fn build_dock(ctx: &Rc<Ctx>, monitor: &gdk::Monitor, connector: String) -> Rc<Dock> {
|
||||||
let cfg = ctx.cfg.borrow();
|
let cfg = ctx.cfg.borrow();
|
||||||
let win = gtk::ApplicationWindow::new(&ctx.app);
|
let win = gtk::ApplicationWindow::new(&ctx.app);
|
||||||
win.add_css_class("waydock");
|
win.add_css_class("waydock");
|
||||||
@@ -138,12 +173,41 @@ fn build_dock(ctx: &Rc<Ctx>, monitor: &gdk::Monitor, connector: String) -> Dock
|
|||||||
outer.set_margin_start(pad);
|
outer.set_margin_start(pad);
|
||||||
outer.set_margin_end(pad);
|
outer.set_margin_end(pad);
|
||||||
outer.append(&panel);
|
outer.append(&panel);
|
||||||
win.set_child(Some(&outer));
|
|
||||||
|
// revealern ger autohide-animationen
|
||||||
|
let revealer = gtk::Revealer::new();
|
||||||
|
revealer.set_transition_type(match cfg.position {
|
||||||
|
Position::Bottom => gtk::RevealerTransitionType::SlideUp,
|
||||||
|
Position::Top => gtk::RevealerTransitionType::SlideDown,
|
||||||
|
Position::Left => gtk::RevealerTransitionType::SlideRight,
|
||||||
|
Position::Right => gtk::RevealerTransitionType::SlideLeft,
|
||||||
|
});
|
||||||
|
revealer.set_transition_duration(if cfg.appearance.animations {
|
||||||
|
cfg.autohide.animation_ms
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
});
|
||||||
|
revealer.set_reveal_child(true);
|
||||||
|
revealer.set_child(Some(&outer));
|
||||||
|
win.set_child(Some(&revealer));
|
||||||
|
let autohide_enabled = cfg.autohide.enabled;
|
||||||
drop(cfg);
|
drop(cfg);
|
||||||
|
|
||||||
configure_layer(&win, ctx, monitor);
|
configure_layer(&win, ctx, monitor);
|
||||||
menu::setup_dock_actions(ctx, &win);
|
menu::setup_dock_actions(ctx, &win);
|
||||||
|
|
||||||
|
let dock = Rc::new(Dock {
|
||||||
|
window: win.clone(),
|
||||||
|
panel: panel.clone(),
|
||||||
|
revealer,
|
||||||
|
connector,
|
||||||
|
hover: Cell::new(false),
|
||||||
|
hidden: Cell::new(false),
|
||||||
|
dodge_overlap: Cell::new(false),
|
||||||
|
hide_generation: Cell::new(0),
|
||||||
|
sensor: RefCell::new(None),
|
||||||
|
});
|
||||||
|
|
||||||
// högerklick på tom panelyta → panelmenyn
|
// högerklick på tom panelyta → panelmenyn
|
||||||
let empty_click = gtk::GestureClick::new();
|
let empty_click = gtk::GestureClick::new();
|
||||||
empty_click.set_button(3);
|
empty_click.set_button(3);
|
||||||
@@ -157,9 +221,198 @@ fn build_dock(ctx: &Rc<Ctx>, monitor: &gdk::Monitor, connector: String) -> Dock
|
|||||||
}
|
}
|
||||||
panel.add_controller(empty_click);
|
panel.add_controller(empty_click);
|
||||||
|
|
||||||
|
// hover-spårning för autohide
|
||||||
|
let motion = gtk::EventControllerMotion::new();
|
||||||
|
{
|
||||||
|
let dock2 = dock.clone();
|
||||||
|
motion.connect_enter(move |_, _, _| {
|
||||||
|
dock2.hover.set(true);
|
||||||
|
dock2.hide_generation.set(dock2.hide_generation.get() + 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let ctx = ctx.clone();
|
||||||
|
let dock2 = dock.clone();
|
||||||
|
motion.connect_leave(move |_| {
|
||||||
|
dock2.hover.set(false);
|
||||||
|
schedule_hide(&ctx, &dock2);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
win.add_controller(motion);
|
||||||
|
|
||||||
|
if autohide_enabled {
|
||||||
|
build_sensor(ctx, &dock, monitor);
|
||||||
|
}
|
||||||
|
|
||||||
win.present();
|
win.present();
|
||||||
info!("dock uppe på {connector}");
|
info!("dock uppe på {}", dock.connector);
|
||||||
Dock { window: win, panel, connector }
|
dock
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Den tunna remsan vid skärmkanten som väcker en gömd dock.
|
||||||
|
fn build_sensor(ctx: &Rc<Ctx>, dock: &Rc<Dock>, monitor: &gdk::Monitor) {
|
||||||
|
let cfg = ctx.cfg.borrow();
|
||||||
|
let sensor = gtk::ApplicationWindow::new(&ctx.app);
|
||||||
|
sensor.add_css_class("waydock-sensor");
|
||||||
|
sensor.init_layer_shell();
|
||||||
|
sensor.set_namespace(Some("waydock-sensor"));
|
||||||
|
sensor.set_monitor(Some(monitor));
|
||||||
|
sensor.set_layer(Layer::Overlay);
|
||||||
|
sensor.set_keyboard_mode(KeyboardMode::None);
|
||||||
|
sensor.set_exclusive_zone(0);
|
||||||
|
|
||||||
|
let edge = main_edge(&cfg.position);
|
||||||
|
let (side_a, side_b) = side_edges(&cfg.position);
|
||||||
|
sensor.set_anchor(edge, true);
|
||||||
|
sensor.set_anchor(side_a, true);
|
||||||
|
sensor.set_anchor(side_b, true);
|
||||||
|
|
||||||
|
let strip = gtk::Box::new(gtk::Orientation::Horizontal, 0);
|
||||||
|
let px = cfg.autohide.reveal_px.max(1);
|
||||||
|
match cfg.position {
|
||||||
|
Position::Bottom | Position::Top => strip.set_size_request(-1, px),
|
||||||
|
_ => strip.set_size_request(px, -1),
|
||||||
|
}
|
||||||
|
drop(cfg);
|
||||||
|
sensor.set_child(Some(&strip));
|
||||||
|
|
||||||
|
let motion = gtk::EventControllerMotion::new();
|
||||||
|
{
|
||||||
|
let ctx = ctx.clone();
|
||||||
|
let dock2 = dock.clone();
|
||||||
|
motion.connect_enter(move |_, _, _| {
|
||||||
|
if dock2.hidden.get() {
|
||||||
|
set_dock_hidden(&dock2, false);
|
||||||
|
// om pekaren aldrig når dockan: göm igen efter en stund
|
||||||
|
let ctx2 = ctx.clone();
|
||||||
|
let dock3 = dock2.clone();
|
||||||
|
glib::timeout_add_local_once(Duration::from_millis(1500), move || {
|
||||||
|
if !dock3.hover.get() {
|
||||||
|
update_autohide(&ctx2);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
strip.add_controller(motion);
|
||||||
|
|
||||||
|
sensor.present();
|
||||||
|
*dock.sensor.borrow_mut() = Some(sensor);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_dock_hidden(dock: &Rc<Dock>, hidden: bool) {
|
||||||
|
if dock.hidden.get() == hidden {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dock.hidden.set(hidden);
|
||||||
|
if hidden {
|
||||||
|
let duration = dock.revealer.transition_duration();
|
||||||
|
dock.revealer.set_reveal_child(false);
|
||||||
|
let dock2 = dock.clone();
|
||||||
|
glib::timeout_add_local_once(Duration::from_millis(duration as u64 + 30), move || {
|
||||||
|
if dock2.hidden.get() {
|
||||||
|
dock2.window.set_visible(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dock.window.set_visible(true);
|
||||||
|
dock.revealer.set_reveal_child(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_hide(ctx: &Ctx, dock: &Dock) -> bool {
|
||||||
|
let cfg = ctx.cfg.borrow();
|
||||||
|
if !cfg.autohide.enabled || dock.hover.get() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
match cfg.autohide.mode {
|
||||||
|
AutohideMode::Always => true,
|
||||||
|
AutohideMode::Dodge => {
|
||||||
|
if wayfire_ipc::available() {
|
||||||
|
dock.dodge_overlap.get()
|
||||||
|
} else {
|
||||||
|
true // ingen geometri att gå på — bete dig som always
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applicera göm/visa-läget på alla dockor.
|
||||||
|
pub fn update_autohide(ctx: &Rc<Ctx>) {
|
||||||
|
for dock in ctx.docks.borrow().iter() {
|
||||||
|
set_dock_hidden(dock, should_hide(ctx, dock));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schedule_hide(ctx: &Rc<Ctx>, dock: &Rc<Dock>) {
|
||||||
|
let delay = ctx.cfg.borrow().autohide.hide_delay_ms;
|
||||||
|
let generation = dock.hide_generation.get() + 1;
|
||||||
|
dock.hide_generation.set(generation);
|
||||||
|
let ctx = ctx.clone();
|
||||||
|
let dock = dock.clone();
|
||||||
|
glib::timeout_add_local_once(Duration::from_millis(delay as u64), move || {
|
||||||
|
if dock.hide_generation.get() == generation && should_hide(&ctx, &dock) {
|
||||||
|
set_dock_hidden(&dock, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Räkna om dodge-överlapp för alla dockor via Wayfire-IPC-geometri.
|
||||||
|
pub fn recompute_dodge(ctx: &Rc<Ctx>) {
|
||||||
|
{
|
||||||
|
let cfg = ctx.cfg.borrow();
|
||||||
|
if !cfg.autohide.enabled || cfg.autohide.mode != AutohideMode::Dodge {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Some(views) = wayfire_ipc::list_views() else { return };
|
||||||
|
let Some(outputs) = wayfire_ipc::list_outputs() else { return };
|
||||||
|
let position = ctx.cfg.borrow().position.clone();
|
||||||
|
let margin = ctx.cfg.borrow().margin;
|
||||||
|
|
||||||
|
for dock in ctx.docks.borrow().iter() {
|
||||||
|
let Some(output) = outputs
|
||||||
|
.iter()
|
||||||
|
.find(|o| o["name"].as_str() == Some(dock.connector.as_str()))
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let ow = output["geometry"]["width"].as_f64().unwrap_or(0.0);
|
||||||
|
let oh = output["geometry"]["height"].as_f64().unwrap_or(0.0);
|
||||||
|
// zonen dockan upptar vid sin kant (fönstrets storlek + marginal)
|
||||||
|
let depth = match position {
|
||||||
|
Position::Bottom | Position::Top => dock.window.height().max(40) + margin,
|
||||||
|
_ => dock.window.width().max(40) + margin,
|
||||||
|
} as f64;
|
||||||
|
|
||||||
|
let overlap = views.iter().any(|v| {
|
||||||
|
if v["role"].as_str() != Some("toplevel")
|
||||||
|
|| !v["mapped"].as_bool().unwrap_or(false)
|
||||||
|
|| v["minimized"].as_bool().unwrap_or(false)
|
||||||
|
|| v["output-name"].as_str() != Some(dock.connector.as_str())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let g = &v["geometry"];
|
||||||
|
let (x, y) = (g["x"].as_f64().unwrap_or(0.0), g["y"].as_f64().unwrap_or(0.0));
|
||||||
|
let (w, h) = (
|
||||||
|
g["width"].as_f64().unwrap_or(0.0),
|
||||||
|
g["height"].as_f64().unwrap_or(0.0),
|
||||||
|
);
|
||||||
|
// utanför aktuell arbetsyta → ingen kollision
|
||||||
|
if x + w <= 0.0 || y + h <= 0.0 || x >= ow || y >= oh {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
match position {
|
||||||
|
Position::Bottom => y + h >= oh - depth,
|
||||||
|
Position::Top => y <= depth,
|
||||||
|
Position::Left => x <= depth,
|
||||||
|
Position::Right => x + w >= ow - depth,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dock.dodge_overlap.set(overlap);
|
||||||
|
}
|
||||||
|
update_autohide(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gruppera snapshot → ikonrad för en viss skärm.
|
/// Gruppera snapshot → ikonrad för en viss skärm.
|
||||||
@@ -223,12 +476,8 @@ fn special_button(icon_name: &str, tooltip: &str) -> gtk::Button {
|
|||||||
btn
|
btn
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_special_buttons(ctx: &Rc<Ctx>, dock: &Dock) {
|
fn add_special_buttons(ctx: &Rc<Ctx>, dock: &Rc<Dock>) {
|
||||||
let cfg = ctx.cfg.borrow();
|
let specials = ctx.cfg.borrow().special_buttons.clone();
|
||||||
let specials = cfg.special_buttons.clone();
|
|
||||||
let icon_size = cfg.appearance.icon_size;
|
|
||||||
drop(cfg);
|
|
||||||
let _ = icon_size;
|
|
||||||
|
|
||||||
if specials.overview && wayfire_ipc::available() {
|
if specials.overview && wayfire_ipc::available() {
|
||||||
let btn = special_button("view-grid-symbolic", "Översikt (scale)");
|
let btn = special_button("view-grid-symbolic", "Översikt (scale)");
|
||||||
@@ -252,12 +501,17 @@ fn build_item_widget(ctx: &Rc<Ctx>, item: Rc<Item>) -> gtk::Button {
|
|||||||
let icon_size = cfg.appearance.icon_size;
|
let icon_size = cfg.appearance.icon_size;
|
||||||
let max_dots = cfg.appearance.indicator.max as usize;
|
let max_dots = cfg.appearance.indicator.max as usize;
|
||||||
let tooltips = cfg.behavior.tooltips;
|
let tooltips = cfg.behavior.tooltips;
|
||||||
|
let urgent_pulse = cfg.behavior.urgent_pulse;
|
||||||
drop(cfg);
|
drop(cfg);
|
||||||
|
|
||||||
let btn = gtk::Button::new();
|
let btn = gtk::Button::new();
|
||||||
btn.add_css_class("dock-item");
|
btn.add_css_class("dock-item");
|
||||||
btn.set_has_frame(false);
|
btn.set_has_frame(false);
|
||||||
|
|
||||||
|
if urgent_pulse && ctx.urgent.borrow().contains(&item.meta.key) {
|
||||||
|
btn.add_css_class("urgent");
|
||||||
|
}
|
||||||
|
|
||||||
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 2);
|
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 2);
|
||||||
let overlay = gtk::Overlay::new();
|
let overlay = gtk::Overlay::new();
|
||||||
overlay.set_child(Some(&icons::image_for(&item.meta, icon_size)));
|
overlay.set_child(Some(&icons::image_for(&item.meta, icon_size)));
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
75
src/main.rs
75
src/main.rs
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
mod dock;
|
mod dock;
|
||||||
|
mod hotspots;
|
||||||
mod icons;
|
mod icons;
|
||||||
mod menu;
|
mod menu;
|
||||||
mod settings;
|
mod settings;
|
||||||
@@ -110,7 +111,7 @@ fn layout_signature(cfg: &config::Config) -> String {
|
|||||||
cfg.appearance.shadow.enabled,
|
cfg.appearance.shadow.enabled,
|
||||||
cfg.appearance.shadow.blur,
|
cfg.appearance.shadow.blur,
|
||||||
cfg.outputs.keys().collect::<Vec<_>>(),
|
cfg.outputs.keys().collect::<Vec<_>>(),
|
||||||
)
|
) + &format!("|{:?}", cfg.autohide)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> glib::ExitCode {
|
fn main() -> glib::ExitCode {
|
||||||
@@ -149,6 +150,8 @@ fn activate(app: >k::Application, cfg_path: PathBuf) {
|
|||||||
app: app.clone(),
|
app: app.clone(),
|
||||||
on_config_changed: RefCell::new(None),
|
on_config_changed: RefCell::new(None),
|
||||||
save_generation: Cell::new(0),
|
save_generation: Cell::new(0),
|
||||||
|
urgent: RefCell::new(Default::default()),
|
||||||
|
hotspot_windows: RefCell::new(Vec::new()),
|
||||||
});
|
});
|
||||||
|
|
||||||
// CSS + rebuild/refresh-logik
|
// CSS + rebuild/refresh-logik
|
||||||
@@ -170,6 +173,7 @@ fn activate(app: >k::Application, cfg_path: PathBuf) {
|
|||||||
} else {
|
} else {
|
||||||
dock::refresh_items(&ctx2);
|
dock::refresh_items(&ctx2);
|
||||||
}
|
}
|
||||||
|
hotspots::rebuild(&ctx2);
|
||||||
});
|
});
|
||||||
*ctx.on_config_changed.borrow_mut() = Some(apply);
|
*ctx.on_config_changed.borrow_mut() = Some(apply);
|
||||||
}
|
}
|
||||||
@@ -186,8 +190,10 @@ fn activate(app: >k::Application, cfg_path: PathBuf) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !wayfire_ipc::available() {
|
if wayfire_ipc::available() {
|
||||||
warn!("WAYFIRE_SOCKET saknas — översikt/flytta-till-skärm/dodge avstängda (aktivera ipc-pluginen i wayfire.ini)");
|
subscribe_wayfire_events(&ctx);
|
||||||
|
} else {
|
||||||
|
warn!("Wayfire-IPC saknas — översikt/flytta-till-skärm/dodge avstängda (aktivera ipc-pluginen i wayfire.ini)");
|
||||||
}
|
}
|
||||||
|
|
||||||
// första uppritningen
|
// första uppritningen
|
||||||
@@ -230,3 +236,66 @@ fn activate(app: >k::Application, cfg_path: PathBuf) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Händelsestyrd dodge + urgency via Wayfires eventström.
|
||||||
|
fn subscribe_wayfire_events(ctx: &Rc<dock::Ctx>) {
|
||||||
|
let ctx2 = ctx.clone();
|
||||||
|
let pending = Rc::new(Cell::new(0u64));
|
||||||
|
let result = wayfire_ipc::subscribe(Rc::new(move |ev| {
|
||||||
|
let name = ev["event"].as_str().unwrap_or("");
|
||||||
|
match name {
|
||||||
|
"view-hints-changed" => {
|
||||||
|
let view = &ev["view"];
|
||||||
|
let app_id = view["app-id"].as_str().unwrap_or("");
|
||||||
|
if app_id.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let key = icons::lookup(app_id).key;
|
||||||
|
let wants = ev["hints"]["demands-attention"].as_bool()
|
||||||
|
.or_else(|| view["demands-attention"].as_bool())
|
||||||
|
.unwrap_or(false);
|
||||||
|
let changed = if wants {
|
||||||
|
ctx2.urgent.borrow_mut().insert(key)
|
||||||
|
} else {
|
||||||
|
ctx2.urgent.borrow_mut().remove(&key)
|
||||||
|
};
|
||||||
|
if changed {
|
||||||
|
dock::refresh_items(&ctx2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"view-focused" => {
|
||||||
|
let app_id = ev["view"]["app-id"].as_str().unwrap_or("");
|
||||||
|
if !app_id.is_empty() {
|
||||||
|
let key = icons::lookup(app_id).key;
|
||||||
|
if ctx2.urgent.borrow_mut().remove(&key) {
|
||||||
|
dock::refresh_items(&ctx2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
schedule_dodge(&ctx2, &pending);
|
||||||
|
}
|
||||||
|
n if n.starts_with("view-") || n.starts_with("output-") => {
|
||||||
|
schedule_dodge(&ctx2, &pending);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
if let Err(e) = result {
|
||||||
|
warn!("kunde inte prenumerera på wayfire-events: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Debounca dodge-omräkningen (events kommer i skurar).
|
||||||
|
fn schedule_dodge(ctx: &Rc<dock::Ctx>, pending: &Rc<Cell<u64>>) {
|
||||||
|
if !ctx.cfg.borrow().autohide.enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let generation = pending.get() + 1;
|
||||||
|
pending.set(generation);
|
||||||
|
let ctx = ctx.clone();
|
||||||
|
let pending = pending.clone();
|
||||||
|
glib::timeout_add_local_once(Duration::from_millis(120), move || {
|
||||||
|
if pending.get() == generation {
|
||||||
|
dock::recompute_dodge(&ctx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -52,10 +52,17 @@ pub fn build_css(cfg: &Config) -> String {
|
|||||||
|
|
||||||
format!(
|
format!(
|
||||||
r#"
|
r#"
|
||||||
window.waydock {{
|
window.waydock,
|
||||||
|
window.waydock-sensor,
|
||||||
|
window.waydock-hotspot {{
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
window.waydock-sensor box,
|
||||||
|
window.waydock-hotspot box {{
|
||||||
|
background-color: rgba(0, 0, 0, 0.004);
|
||||||
|
}}
|
||||||
|
|
||||||
.dock-panel {{
|
.dock-panel {{
|
||||||
background-color: {bg};
|
background-color: {bg};
|
||||||
border-radius: {radius}px;
|
border-radius: {radius}px;
|
||||||
|
|||||||
@@ -15,11 +15,22 @@ pub fn socket_path() -> Option<String> {
|
|||||||
if let Ok(p) = std::env::var("WAYFIRE_SOCKET") {
|
if let Ok(p) = std::env::var("WAYFIRE_SOCKET") {
|
||||||
return Some(p);
|
return Some(p);
|
||||||
}
|
}
|
||||||
// ipc-pluginen aktiverad efter sessionsstart → env saknas men
|
// ipc-pluginen aktiverad efter sessionsstart → env saknas; leta upp
|
||||||
// socketen ligger på standardvägen /tmp/wayfire-<display>.socket
|
// socketen (t.ex. $XDG_RUNTIME_DIR/wayfire-wayland-1-.socket)
|
||||||
let display = std::env::var("WAYLAND_DISPLAY").unwrap_or_else(|_| "wayland-1".into());
|
let mut dirs = vec!["/tmp".to_string()];
|
||||||
let fallback = format!("/tmp/wayfire-{display}.socket");
|
if let Ok(runtime) = std::env::var("XDG_RUNTIME_DIR") {
|
||||||
std::path::Path::new(&fallback).exists().then_some(fallback)
|
dirs.insert(0, runtime);
|
||||||
|
}
|
||||||
|
for dir in dirs {
|
||||||
|
let Ok(entries) = std::fs::read_dir(&dir) else { continue };
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
if name.starts_with("wayfire-") && name.ends_with(".socket") {
|
||||||
|
return Some(entry.path().to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn available() -> bool {
|
pub fn available() -> bool {
|
||||||
@@ -108,6 +119,65 @@ pub fn find_view_id(app_id: &str, title: &str) -> Option<i64> {
|
|||||||
matches.first().and_then(|v| v["id"].as_i64())
|
matches.first().and_then(|v| v["id"].as_i64())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prenumerera på Wayfires händelseström (view-mapped, view-geometry-changed,
|
||||||
|
/// view-hints-changed …). Callbacken körs på GTK-tråden för varje event.
|
||||||
|
/// Strömmen lever så länge processen; returnerar Err om IPC saknas.
|
||||||
|
pub fn subscribe(on_event: std::rc::Rc<dyn Fn(&Value)>) -> Result<(), String> {
|
||||||
|
use std::os::fd::AsRawFd;
|
||||||
|
|
||||||
|
let path = socket_path().ok_or("WAYFIRE_SOCKET saknas")?;
|
||||||
|
let mut stream = UnixStream::connect(&path).map_err(|e| format!("connect: {e}"))?;
|
||||||
|
|
||||||
|
// registrera prenumerationen synkront (svaret är {"result":"ok"})
|
||||||
|
let payload = serde_json::to_vec(&json!({ "method": "window-rules/events/watch" }))
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let len = (payload.len() as u32).to_le_bytes();
|
||||||
|
stream.write_all(&len).map_err(|e| format!("write: {e}"))?;
|
||||||
|
stream.write_all(&payload).map_err(|e| format!("write: {e}"))?;
|
||||||
|
stream
|
||||||
|
.set_nonblocking(true)
|
||||||
|
.map_err(|e| format!("nonblocking: {e}"))?;
|
||||||
|
|
||||||
|
let fd = stream.as_raw_fd();
|
||||||
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
|
let mut first = true;
|
||||||
|
gtk4::glib::source::unix_fd_add_local(fd, gtk4::glib::IOCondition::IN, move |_, _| {
|
||||||
|
// stream ägs av closuren — håll den vid liv
|
||||||
|
let mut chunk = [0u8; 8192];
|
||||||
|
loop {
|
||||||
|
match (&stream).read(&mut chunk) {
|
||||||
|
Ok(0) => {
|
||||||
|
warn!("wayfire-ipc: händelseströmmen stängdes");
|
||||||
|
return gtk4::glib::ControlFlow::Break;
|
||||||
|
}
|
||||||
|
Ok(n) => buf.extend_from_slice(&chunk[..n]),
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
|
||||||
|
Err(e) => {
|
||||||
|
warn!("wayfire-ipc: läsfel {e}");
|
||||||
|
return gtk4::glib::ControlFlow::Break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// plocka kompletta ramar ur bufferten
|
||||||
|
while buf.len() >= 4 {
|
||||||
|
let n = u32::from_le_bytes(buf[0..4].try_into().unwrap()) as usize;
|
||||||
|
if buf.len() < 4 + n {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let frame: Vec<u8> = buf.drain(0..4 + n).skip(4).collect();
|
||||||
|
if let Ok(v) = serde_json::from_slice::<Value>(&frame) {
|
||||||
|
if first {
|
||||||
|
first = false; // svaret på watch-anropet
|
||||||
|
} else {
|
||||||
|
on_event(&v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gtk4::glib::ControlFlow::Continue
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Flytta en vy till en annan output (namn t.ex. "DP-1").
|
/// Flytta en vy till en annan output (namn t.ex. "DP-1").
|
||||||
pub fn move_view_to_output(view_id: i64, output_name: &str) {
|
pub fn move_view_to_output(view_id: i64, output_name: &str) {
|
||||||
let Some(outputs) = list_outputs() else {
|
let Some(outputs) = list_outputs() else {
|
||||||
|
|||||||
Reference in New Issue
Block a user