All checks were successful
check / check (push) Successful in 1m39s
- 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>
796 lines
27 KiB
Rust
796 lines
27 KiB
Rust
// Dockfönstren: ett layer-shell-fönster per skärm. Bygger ikonraden av
|
|
// 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::{Cell, RefCell};
|
|
use std::collections::HashSet;
|
|
use std::path::PathBuf;
|
|
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::{debug, info};
|
|
|
|
use crate::config::{
|
|
Alignment, AutohideMode, ClickAction, Config, GroupPopup, Layer as CfgLayer,
|
|
MiddleClick, Position, ScrollAction, SharedConfig,
|
|
};
|
|
use crate::icons::{self, AppMeta};
|
|
use crate::menu;
|
|
use crate::toplevel::{ToplevelInfo, ToplevelService};
|
|
use crate::wayfire_ipc;
|
|
|
|
/// Allt delat som dockarna behöver nå.
|
|
pub struct Ctx {
|
|
pub cfg: SharedConfig,
|
|
pub cfg_path: PathBuf,
|
|
pub service: RefCell<Option<Rc<ToplevelService>>>,
|
|
pub docks: RefCell<Vec<Rc<Dock>>>,
|
|
/// målet för den öppna kontextmenyn (menu.rs läser)
|
|
pub menu_target: RefCell<Option<menu::MenuTarget>>,
|
|
pub app: gtk::Application,
|
|
/// körs när configen ändrats (sätts av main; applicerar CSS/rebuild)
|
|
pub on_config_changed: RefCell<Option<Rc<dyn Fn()>>>,
|
|
/// generationsräknare för settings-GUI:ts debouncade sparningar
|
|
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 window: gtk::ApplicationWindow,
|
|
pub items_box: gtk::Box,
|
|
pub revealer: gtk::Revealer,
|
|
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.
|
|
pub struct Item {
|
|
pub meta: AppMeta,
|
|
pub windows: Vec<ToplevelInfo>,
|
|
pub pinned: bool,
|
|
}
|
|
|
|
fn orientation(pos: &Position) -> gtk::Orientation {
|
|
match pos {
|
|
Position::Left | Position::Right => gtk::Orientation::Vertical,
|
|
_ => gtk::Orientation::Horizontal,
|
|
}
|
|
}
|
|
|
|
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, cfg: &Config, monitor: &gdk::Monitor) {
|
|
win.init_layer_shell();
|
|
win.set_namespace(Some("waydock"));
|
|
win.set_monitor(Some(monitor));
|
|
win.set_layer(match cfg.layer {
|
|
CfgLayer::Top => Layer::Top,
|
|
CfgLayer::Overlay => Layer::Overlay,
|
|
CfgLayer::Bottom => Layer::Bottom,
|
|
});
|
|
win.set_keyboard_mode(KeyboardMode::None);
|
|
|
|
let edge = main_edge(&cfg.position);
|
|
let (side_start, side_end) = side_edges(&cfg.position);
|
|
win.set_anchor(edge, true);
|
|
win.set_margin(edge, cfg.margin);
|
|
if cfg.expand {
|
|
// täck hela kanten; start/slut-marginalerna drar in baren
|
|
win.set_anchor(side_start, true);
|
|
win.set_anchor(side_end, true);
|
|
win.set_margin(side_start, cfg.margin_start);
|
|
win.set_margin(side_end, cfg.margin_end);
|
|
} else {
|
|
// ö: förankra bara i vald ände (ingen sidoförankring = centrerad)
|
|
match cfg.alignment {
|
|
Alignment::Center => {}
|
|
Alignment::Start => {
|
|
win.set_anchor(side_start, true);
|
|
win.set_margin(side_start, cfg.margin_start);
|
|
}
|
|
Alignment::End => {
|
|
win.set_anchor(side_end, true);
|
|
win.set_margin(side_end, cfg.margin_end);
|
|
}
|
|
}
|
|
}
|
|
if cfg.avoid_windows && !cfg.autohide.enabled {
|
|
win.auto_exclusive_zone_enable();
|
|
} else {
|
|
win.set_exclusive_zone(0);
|
|
}
|
|
}
|
|
|
|
/// Bygg om alla dockfönster från grunden (vid start, configbyte, skärmbyte).
|
|
pub fn rebuild_all(ctx: &Rc<Ctx>) {
|
|
for dock in ctx.docks.borrow_mut().drain(..) {
|
|
if let Some(sensor) = dock.sensor.borrow_mut().take() {
|
|
sensor.destroy();
|
|
}
|
|
dock.window.destroy();
|
|
}
|
|
|
|
let display = gdk::Display::default().expect("ingen display");
|
|
let monitors = display.monitors();
|
|
let mut docks = 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}"));
|
|
if !ctx.cfg.borrow().output_enabled(&connector) {
|
|
debug!("skärm {connector} avstängd i configen");
|
|
continue;
|
|
}
|
|
docks.push(build_dock(ctx, &monitor, connector));
|
|
}
|
|
*ctx.docks.borrow_mut() = docks;
|
|
refresh_items(ctx);
|
|
recompute_dodge(ctx);
|
|
update_autohide(ctx);
|
|
}
|
|
|
|
fn build_dock(ctx: &Rc<Ctx>, monitor: &gdk::Monitor, connector: String) -> Rc<Dock> {
|
|
let cfg = ctx.cfg.borrow().for_output(&connector);
|
|
let win = gtk::ApplicationWindow::new(&ctx.app);
|
|
win.add_css_class("waydock");
|
|
win.add_css_class(&crate::style::output_class(&connector));
|
|
|
|
let orient = orientation(&cfg.position);
|
|
let panel = gtk::Box::new(orient, 0);
|
|
panel.add_css_class("dock-panel");
|
|
let items_align = match cfg.alignment {
|
|
Alignment::Center => gtk::Align::Center,
|
|
Alignment::Start => gtk::Align::Start,
|
|
Alignment::End => gtk::Align::End,
|
|
};
|
|
let items_box = gtk::Box::new(orient, 0);
|
|
if cfg.expand {
|
|
// panelbakgrunden täcker kanten; ikonerna läggs enligt alignment
|
|
panel.set_halign(gtk::Align::Fill);
|
|
panel.set_valign(gtk::Align::Fill);
|
|
if orient == gtk::Orientation::Horizontal {
|
|
panel.set_hexpand(true);
|
|
items_box.set_hexpand(true);
|
|
items_box.set_halign(items_align);
|
|
} else {
|
|
panel.set_vexpand(true);
|
|
items_box.set_vexpand(true);
|
|
items_box.set_valign(items_align);
|
|
}
|
|
} else {
|
|
panel.set_halign(gtk::Align::Center);
|
|
panel.set_valign(gtk::Align::Center);
|
|
}
|
|
panel.append(&items_box);
|
|
|
|
// yttre marginal = plats för skuggan att rita — men INTE på sidor som
|
|
// är förankrade mot skärmkanten, så att margin = 0 verkligen når kanten
|
|
let outer = gtk::Box::new(orient, 0);
|
|
let pad = if cfg.appearance.shadow.enabled {
|
|
cfg.appearance.shadow.blur as i32
|
|
} else {
|
|
0
|
|
};
|
|
let anchored_start = cfg.expand || cfg.alignment == Alignment::Start;
|
|
let anchored_end = cfg.expand || cfg.alignment == Alignment::End;
|
|
let (top, bottom, start, end) = match cfg.position {
|
|
Position::Bottom => (
|
|
pad,
|
|
0,
|
|
if anchored_start { 0 } else { pad },
|
|
if anchored_end { 0 } else { pad },
|
|
),
|
|
Position::Top => (
|
|
0,
|
|
pad,
|
|
if anchored_start { 0 } else { pad },
|
|
if anchored_end { 0 } else { pad },
|
|
),
|
|
Position::Left => (
|
|
if anchored_start { 0 } else { pad },
|
|
if anchored_end { 0 } else { pad },
|
|
0,
|
|
pad,
|
|
),
|
|
Position::Right => (
|
|
if anchored_start { 0 } else { pad },
|
|
if anchored_end { 0 } else { pad },
|
|
pad,
|
|
0,
|
|
),
|
|
};
|
|
outer.set_margin_top(top);
|
|
outer.set_margin_bottom(bottom);
|
|
outer.set_margin_start(start);
|
|
outer.set_margin_end(end);
|
|
outer.append(&panel);
|
|
|
|
// 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;
|
|
|
|
configure_layer(&win, &cfg, monitor);
|
|
menu::setup_dock_actions(ctx, &win);
|
|
|
|
let dock = Rc::new(Dock {
|
|
window: win.clone(),
|
|
items_box: items_box.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
|
|
let empty_click = gtk::GestureClick::new();
|
|
empty_click.set_button(3);
|
|
{
|
|
let ctx = ctx.clone();
|
|
let panel2 = panel.clone();
|
|
empty_click.connect_pressed(move |gesture, _, x, y| {
|
|
gesture.set_state(gtk::EventSequenceState::Claimed);
|
|
menu::popup_panel_menu(&ctx, &panel2, x, y);
|
|
});
|
|
}
|
|
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();
|
|
info!("dock uppe på {}", dock.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().for_output(&dock.connector);
|
|
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().for_output(&dock.connector);
|
|
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().for_output(&dock.connector).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>) {
|
|
// hämta bara geometri om någon dock alls kör dodge
|
|
let any_dodge = ctx.docks.borrow().iter().any(|d| {
|
|
let c = ctx.cfg.borrow().for_output(&d.connector);
|
|
c.autohide.enabled && c.autohide.mode == AutohideMode::Dodge
|
|
});
|
|
if !any_dodge {
|
|
return;
|
|
}
|
|
let Some(views) = wayfire_ipc::list_views() else { return };
|
|
let Some(outputs) = wayfire_ipc::list_outputs() else { return };
|
|
|
|
for dock in ctx.docks.borrow().iter() {
|
|
let eff = ctx.cfg.borrow().for_output(&dock.connector);
|
|
if !eff.autohide.enabled || eff.autohide.mode != AutohideMode::Dodge {
|
|
dock.dodge_overlap.set(false);
|
|
continue;
|
|
}
|
|
let (position, margin) = (eff.position.clone(), eff.margin);
|
|
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);
|
|
}
|
|
|
|
/// Färska fönster för en ikon — slås upp vid klick-/tooltip-tillfället så
|
|
/// att closures aldrig agerar på inaktuellt state.
|
|
pub fn fresh_windows(ctx: &Ctx, connector: &str, key: &str) -> Vec<ToplevelInfo> {
|
|
let service = ctx.service.borrow();
|
|
let Some(service) = service.as_ref() else {
|
|
return Vec::new();
|
|
};
|
|
let only_here = ctx.cfg.borrow().only_this_output(connector);
|
|
service
|
|
.snapshot()
|
|
.into_iter()
|
|
.filter(|t| {
|
|
(!only_here || t.outputs.iter().any(|o| o == connector))
|
|
&& icons::lookup(&t.app_id).key == key
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Bygg en färsk kopia av en ikon vid interaktionstillfället.
|
|
fn live_item(ctx: &Ctx, connector: &str, item: &Item) -> Rc<Item> {
|
|
Rc::new(Item {
|
|
meta: item.meta.clone(),
|
|
windows: fresh_windows(ctx, connector, &item.meta.key),
|
|
pinned: item.pinned,
|
|
})
|
|
}
|
|
|
|
/// Gruppera snapshot → ikonrad för en viss skärm.
|
|
fn build_items(ctx: &Ctx, connector: &str, snapshot: &[ToplevelInfo]) -> Vec<Item> {
|
|
let cfg = ctx.cfg.borrow();
|
|
let only_here = cfg.only_this_output(connector);
|
|
let group = cfg.behavior.group_apps;
|
|
|
|
let mut items: Vec<Item> = cfg
|
|
.pins_for(connector)
|
|
.iter()
|
|
.filter_map(|pin| icons::lookup_pin(pin))
|
|
.map(|meta| Item { meta, windows: Vec::new(), pinned: true })
|
|
.collect();
|
|
|
|
for t in snapshot {
|
|
if only_here && !t.outputs.iter().any(|o| o == connector) {
|
|
continue;
|
|
}
|
|
let meta = icons::lookup(&t.app_id);
|
|
let existing = items
|
|
.iter_mut()
|
|
.find(|i| i.meta.key == meta.key && (group || i.windows.is_empty()));
|
|
match existing {
|
|
Some(item) => item.windows.push(t.clone()),
|
|
None => items.push(Item { meta, windows: vec![t.clone()], pinned: false }),
|
|
}
|
|
}
|
|
items
|
|
}
|
|
|
|
/// Uppdatera ikonraderna i alla dockor (körs vid varje fönsterhändelse).
|
|
pub fn refresh_items(ctx: &Rc<Ctx>) {
|
|
let snapshot = ctx
|
|
.service
|
|
.borrow()
|
|
.as_ref()
|
|
.map(|s| s.snapshot())
|
|
.unwrap_or_default();
|
|
|
|
for dock in ctx.docks.borrow().iter() {
|
|
while let Some(child) = dock.items_box.first_child() {
|
|
dock.items_box.remove(&child);
|
|
}
|
|
add_special_buttons(ctx, dock);
|
|
for item in build_items(ctx, &dock.connector, &snapshot) {
|
|
let widget = build_item_widget(ctx, &dock.connector, Rc::new(item));
|
|
dock.items_box.append(&widget);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn special_button(icon_name: &str, tooltip: &str) -> gtk::Button {
|
|
let btn = gtk::Button::new();
|
|
btn.add_css_class("dock-item");
|
|
btn.set_has_frame(false);
|
|
let img = gtk::Image::from_icon_name(icon_name);
|
|
img.set_pixel_size(24);
|
|
btn.set_child(Some(&img));
|
|
btn.set_tooltip_text(Some(tooltip));
|
|
btn
|
|
}
|
|
|
|
fn add_special_buttons(ctx: &Rc<Ctx>, dock: &Rc<Dock>) {
|
|
let specials = ctx
|
|
.cfg
|
|
.borrow()
|
|
.for_output(&dock.connector)
|
|
.special_buttons;
|
|
|
|
if specials.overview && wayfire_ipc::available() {
|
|
let btn = special_button("view-grid-symbolic", "Översikt (scale)");
|
|
btn.connect_clicked(|_| wayfire_ipc::toggle_scale());
|
|
dock.items_box.append(&btn);
|
|
}
|
|
if specials.show_desktop {
|
|
let btn = special_button("user-desktop-symbolic", "Visa skrivbordet");
|
|
let ctx = ctx.clone();
|
|
btn.connect_clicked(move |_| {
|
|
if let Some(service) = ctx.service.borrow().as_ref() {
|
|
service.toggle_show_desktop();
|
|
}
|
|
});
|
|
dock.items_box.append(&btn);
|
|
}
|
|
}
|
|
|
|
fn build_item_widget(ctx: &Rc<Ctx>, connector: &str, item: Rc<Item>) -> gtk::Button {
|
|
let cfg = ctx.cfg.borrow().for_output(connector);
|
|
let icon_size = cfg.appearance.icon_size;
|
|
let max_dots = cfg.appearance.indicator.max as usize;
|
|
let tooltips = cfg.behavior.tooltips;
|
|
let urgent_pulse = cfg.behavior.urgent_pulse;
|
|
|
|
let btn = gtk::Button::new();
|
|
btn.add_css_class("dock-item");
|
|
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 overlay = gtk::Overlay::new();
|
|
overlay.set_child(Some(&icons::image_for(&item.meta, icon_size)));
|
|
|
|
// siffer-badge för grupper med flera fönster
|
|
if item.windows.len() > 1 {
|
|
let badge = gtk::Label::new(Some(&item.windows.len().to_string()));
|
|
badge.add_css_class("group-badge");
|
|
badge.set_halign(gtk::Align::End);
|
|
badge.set_valign(gtk::Align::Start);
|
|
overlay.add_overlay(&badge);
|
|
}
|
|
vbox.append(&overlay);
|
|
|
|
// indikatorprickar
|
|
let ind_box = gtk::Box::new(gtk::Orientation::Horizontal, 2);
|
|
ind_box.add_css_class("indicator-box");
|
|
ind_box.set_halign(gtk::Align::Center);
|
|
let any_active = item.windows.iter().any(|w| w.activated);
|
|
for _ in 0..item.windows.len().min(max_dots) {
|
|
let dot = gtk::Box::new(gtk::Orientation::Horizontal, 0);
|
|
dot.add_css_class("indicator");
|
|
if !any_active {
|
|
dot.add_css_class("inactive");
|
|
}
|
|
ind_box.append(&dot);
|
|
}
|
|
vbox.append(&ind_box);
|
|
btn.set_child(Some(&vbox));
|
|
|
|
if tooltips {
|
|
btn.set_has_tooltip(true);
|
|
let ctx2 = ctx.clone();
|
|
let item2 = item.clone();
|
|
let conn2 = connector.to_string();
|
|
btn.connect_query_tooltip(move |_, _, _, _, tooltip| {
|
|
let windows = fresh_windows(&ctx2, &conn2, &item2.meta.key);
|
|
let tip = match windows.len() {
|
|
0 => item2.meta.display_name.clone(),
|
|
1 => {
|
|
let t = &windows[0].title;
|
|
if t.is_empty() { item2.meta.display_name.clone() } else { t.clone() }
|
|
}
|
|
n => format!("{} — {n} fönster", item2.meta.display_name),
|
|
};
|
|
tooltip.set_text(Some(&tip));
|
|
true
|
|
});
|
|
}
|
|
|
|
// klick: vänster/mitt/höger
|
|
let click = gtk::GestureClick::new();
|
|
click.set_button(0);
|
|
{
|
|
let ctx = ctx.clone();
|
|
let item = item.clone();
|
|
let btn2 = btn.clone();
|
|
let conn2 = connector.to_string();
|
|
click.connect_released(move |gesture, _, _, _| {
|
|
gesture.set_state(gtk::EventSequenceState::Claimed);
|
|
let live = live_item(&ctx, &conn2, &item);
|
|
match gesture.current_button() {
|
|
1 => primary_action(&ctx, &conn2, &live, &btn2),
|
|
2 => middle_action(&ctx, &conn2, &live),
|
|
3 => menu::popup_item_menu(&ctx, &live, &btn2),
|
|
_ => {}
|
|
}
|
|
});
|
|
}
|
|
btn.add_controller(click);
|
|
|
|
// hover-popup för grupperade appar (om valt i configen)
|
|
if item.windows.len() > 1 && cfg.behavior.group_popup == GroupPopup::Hover {
|
|
let hover_ctl = gtk::EventControllerMotion::new();
|
|
let ctx2 = ctx.clone();
|
|
let item2 = item.clone();
|
|
let btn2 = btn.clone();
|
|
let conn2 = connector.to_string();
|
|
hover_ctl.connect_enter(move |ctl, _, _| {
|
|
let ctx3 = ctx2.clone();
|
|
let item3 = item2.clone();
|
|
let btn3 = btn2.clone();
|
|
let conn3 = conn2.clone();
|
|
let ctl2 = ctl.clone();
|
|
glib::timeout_add_local_once(Duration::from_millis(350), move || {
|
|
if ctl2.contains_pointer() {
|
|
let live = live_item(&ctx3, &conn3, &item3);
|
|
if live.windows.len() > 1 {
|
|
menu::popup_group_list(&ctx3, &live, &btn3);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
btn.add_controller(hover_ctl);
|
|
}
|
|
|
|
// skroll: växla mellan gruppens fönster
|
|
let scroll = gtk::EventControllerScroll::new(gtk::EventControllerScrollFlags::VERTICAL);
|
|
{
|
|
let ctx = ctx.clone();
|
|
let item = item.clone();
|
|
let conn2 = connector.to_string();
|
|
scroll.connect_scroll(move |_, _, dy| {
|
|
if ctx.cfg.borrow().for_output(&conn2).behavior.scroll == ScrollAction::CycleWindows {
|
|
let live = live_item(&ctx, &conn2, &item);
|
|
cycle_windows(&ctx, &live, dy > 0.0);
|
|
glib::Propagation::Stop
|
|
} else {
|
|
glib::Propagation::Proceed
|
|
}
|
|
});
|
|
}
|
|
btn.add_controller(scroll);
|
|
|
|
btn
|
|
}
|
|
|
|
fn primary_action(ctx: &Rc<Ctx>, connector: &str, item: &Rc<Item>, anchor: >k::Button) {
|
|
let service = ctx.service.borrow();
|
|
let Some(service) = service.as_ref() else {
|
|
icons::launch(&item.meta);
|
|
return;
|
|
};
|
|
let behavior = ctx.cfg.borrow().for_output(connector).behavior;
|
|
match item.windows.len() {
|
|
0 => icons::launch(&item.meta),
|
|
1 => {
|
|
let w = &item.windows[0];
|
|
if w.activated && !w.minimized && behavior.click == ClickAction::FocusOrMinimize {
|
|
service.set_minimized(w, true);
|
|
} else {
|
|
service.activate(w);
|
|
}
|
|
}
|
|
_ => {
|
|
let (click, popup) = (behavior.click.clone(), behavior.group_popup.clone());
|
|
if click == ClickAction::Cycle || popup == GroupPopup::Hover {
|
|
cycle_windows(ctx, item, true);
|
|
} else {
|
|
menu::popup_group_list(ctx, item, anchor);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn middle_action(ctx: &Rc<Ctx>, connector: &str, item: &Rc<Item>) {
|
|
let action = ctx.cfg.borrow().for_output(connector).behavior.middle_click.clone();
|
|
match action {
|
|
MiddleClick::LaunchNew => icons::launch(&item.meta),
|
|
MiddleClick::Close => {
|
|
if let Some(service) = ctx.service.borrow().as_ref() {
|
|
for w in &item.windows {
|
|
service.close(w);
|
|
}
|
|
}
|
|
}
|
|
MiddleClick::None => {}
|
|
}
|
|
}
|
|
|
|
pub fn cycle_windows(ctx: &Rc<Ctx>, item: &Item, forward: bool) {
|
|
if item.windows.is_empty() {
|
|
return;
|
|
}
|
|
let service = ctx.service.borrow();
|
|
let Some(service) = service.as_ref() else { return };
|
|
let idx = item.windows.iter().position(|w| w.activated);
|
|
let next = match idx {
|
|
Some(i) => {
|
|
if forward {
|
|
(i + 1) % item.windows.len()
|
|
} else {
|
|
(i + item.windows.len() - 1) % item.windows.len()
|
|
}
|
|
}
|
|
None => 0,
|
|
};
|
|
service.activate(&item.windows[next]);
|
|
}
|