// 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>>, pub docks: RefCell>>, /// målet för den öppna kontextmenyn (menu.rs läser) pub menu_target: RefCell>, pub app: gtk::Application, /// körs när configen ändrats (sätts av main; applicerar CSS/rebuild) pub on_config_changed: RefCell>>, /// generationsräknare för settings-GUI:ts debouncade sparningar pub save_generation: Cell, /// app-nycklar vars fönster kräver uppmärksamhet (via Wayfire-events) pub urgent: RefCell>, /// hot corner/edge-fönster (hotspots.rs äger innehållet) pub hotspot_windows: RefCell>, } 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, /// dockan är undan-gömd av autohide pub hidden: Cell, /// något fönster överlappar dockans kantzon (dodge) pub dodge_overlap: Cell, /// generationsräknare för fördröjd göm-timer hide_generation: Cell, /// sensorremsan som väcker dockan när den är gömd sensor: RefCell>, } /// En ikon i docken: en pin och/eller en grupp körande fönster. pub struct Item { pub meta: AppMeta, pub windows: Vec, 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) { 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::().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, monitor: &gdk::Monitor, connector: String) -> Rc { 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, dock: &Rc, 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, 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) { for dock in ctx.docks.borrow().iter() { set_dock_hidden(dock, should_hide(ctx, dock)); } } fn schedule_hide(ctx: &Rc, dock: &Rc) { 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) { // 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 { 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 { 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 { let cfg = ctx.cfg.borrow(); let only_here = cfg.only_this_output(connector); let group = cfg.behavior.group_apps; let mut items: Vec = 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) { 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, dock: &Rc) { 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, connector: &str, item: Rc) -> 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, connector: &str, item: &Rc, 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, connector: &str, item: &Rc) { 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, 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]); }