waydock v0.1: panel, pins, fönstergrupper, menyer, settings-GUI, live-config
Some checks failed
check / check (push) Failing after 52s
Some checks failed
check / check (push) Failing after 52s
M0–M5 ur doc/plan.md: layer-shell-dock per skärm med transparent #222222-ö, pins + körande appar (gruppering, badges, indikatorer), klick/skroll/mittklick, konfigurerbar högerklicksmeny, tomyta-meny, settings-GUI som redigerar JSON-configen live, filewatcher, Wayfire-IPC-stubbar (scale, flytta-till-skärm), CI + release-flöde. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
402
src/dock.rs
Normal file
402
src/dock.rs
Normal file
@@ -0,0 +1,402 @@
|
||||
// Dockfönstren: ett layer-shell-fönster per skärm. Bygger ikonraden av
|
||||
// pins + körande fönstergrupper och kopplar klick/skroll/menyer.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
|
||||
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::{
|
||||
ClickAction, 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<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: std::cell::Cell<u64>,
|
||||
}
|
||||
|
||||
pub struct Dock {
|
||||
pub window: gtk::ApplicationWindow,
|
||||
pub panel: gtk::Box,
|
||||
pub connector: String,
|
||||
}
|
||||
|
||||
/// 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 configure_layer(win: >k::ApplicationWindow, ctx: &Ctx, monitor: &gdk::Monitor) {
|
||||
let cfg = ctx.cfg.borrow();
|
||||
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 (main_edge, side_a, side_b) = match cfg.position {
|
||||
Position::Bottom => (Edge::Bottom, Edge::Left, Edge::Right),
|
||||
Position::Top => (Edge::Top, Edge::Left, Edge::Right),
|
||||
Position::Left => (Edge::Left, Edge::Top, Edge::Bottom),
|
||||
Position::Right => (Edge::Right, Edge::Top, Edge::Bottom),
|
||||
};
|
||||
win.set_anchor(main_edge, true);
|
||||
win.set_margin(main_edge, cfg.margin);
|
||||
if !cfg.centered {
|
||||
// full bredd/höjd längs kanten
|
||||
win.set_anchor(side_a, true);
|
||||
win.set_anchor(side_b, true);
|
||||
}
|
||||
if cfg.avoid_windows {
|
||||
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(..) {
|
||||
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);
|
||||
}
|
||||
|
||||
fn build_dock(ctx: &Rc<Ctx>, monitor: &gdk::Monitor, connector: String) -> Dock {
|
||||
let cfg = ctx.cfg.borrow();
|
||||
let win = gtk::ApplicationWindow::new(&ctx.app);
|
||||
win.add_css_class("waydock");
|
||||
|
||||
let orient = orientation(&cfg.position);
|
||||
let panel = gtk::Box::new(orient, 0);
|
||||
panel.add_css_class("dock-panel");
|
||||
panel.set_halign(gtk::Align::Center);
|
||||
panel.set_valign(gtk::Align::Center);
|
||||
|
||||
// yttre marginal = plats för skuggan att rita
|
||||
let outer = gtk::Box::new(orient, 0);
|
||||
let pad = if cfg.appearance.shadow.enabled {
|
||||
cfg.appearance.shadow.blur as i32
|
||||
} else {
|
||||
0
|
||||
};
|
||||
outer.set_margin_top(pad);
|
||||
outer.set_margin_bottom(pad);
|
||||
outer.set_margin_start(pad);
|
||||
outer.set_margin_end(pad);
|
||||
outer.append(&panel);
|
||||
win.set_child(Some(&outer));
|
||||
drop(cfg);
|
||||
|
||||
configure_layer(&win, ctx, monitor);
|
||||
menu::setup_dock_actions(ctx, &win);
|
||||
|
||||
// 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);
|
||||
|
||||
win.present();
|
||||
info!("dock uppe på {connector}");
|
||||
Dock { window: win, panel, connector }
|
||||
}
|
||||
|
||||
/// 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.panel.first_child() {
|
||||
dock.panel.remove(&child);
|
||||
}
|
||||
add_special_buttons(ctx, dock);
|
||||
for item in build_items(ctx, &dock.connector, &snapshot) {
|
||||
let widget = build_item_widget(ctx, Rc::new(item));
|
||||
dock.panel.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: &Dock) {
|
||||
let cfg = ctx.cfg.borrow();
|
||||
let specials = cfg.special_buttons.clone();
|
||||
let icon_size = cfg.appearance.icon_size;
|
||||
drop(cfg);
|
||||
let _ = icon_size;
|
||||
|
||||
if specials.overview && wayfire_ipc::available() {
|
||||
let btn = special_button("view-grid-symbolic", "Översikt (scale)");
|
||||
btn.connect_clicked(|_| wayfire_ipc::toggle_scale());
|
||||
dock.panel.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.panel.append(&btn);
|
||||
}
|
||||
}
|
||||
|
||||
fn build_item_widget(ctx: &Rc<Ctx>, item: Rc<Item>) -> gtk::Button {
|
||||
let cfg = ctx.cfg.borrow();
|
||||
let icon_size = cfg.appearance.icon_size;
|
||||
let max_dots = cfg.appearance.indicator.max as usize;
|
||||
let tooltips = cfg.behavior.tooltips;
|
||||
drop(cfg);
|
||||
|
||||
let btn = gtk::Button::new();
|
||||
btn.add_css_class("dock-item");
|
||||
btn.set_has_frame(false);
|
||||
|
||||
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 {
|
||||
let tip = match item.windows.len() {
|
||||
0 => item.meta.display_name.clone(),
|
||||
1 => {
|
||||
let t = &item.windows[0].title;
|
||||
if t.is_empty() { item.meta.display_name.clone() } else { t.clone() }
|
||||
}
|
||||
n => format!("{} — {n} fönster", item.meta.display_name),
|
||||
};
|
||||
btn.set_tooltip_text(Some(&tip));
|
||||
}
|
||||
|
||||
// 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();
|
||||
click.connect_released(move |gesture, _, _, _| {
|
||||
gesture.set_state(gtk::EventSequenceState::Claimed);
|
||||
match gesture.current_button() {
|
||||
1 => primary_action(&ctx, &item, &btn2),
|
||||
2 => middle_action(&ctx, &item),
|
||||
3 => menu::popup_item_menu(&ctx, &item, &btn2),
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
btn.add_controller(click);
|
||||
|
||||
// skroll: växla mellan gruppens fönster
|
||||
let scroll = gtk::EventControllerScroll::new(gtk::EventControllerScrollFlags::VERTICAL);
|
||||
{
|
||||
let ctx = ctx.clone();
|
||||
let item = item.clone();
|
||||
scroll.connect_scroll(move |_, _, dy| {
|
||||
if ctx.cfg.borrow().behavior.scroll == ScrollAction::CycleWindows {
|
||||
cycle_windows(&ctx, &item, dy > 0.0);
|
||||
glib::Propagation::Stop
|
||||
} else {
|
||||
glib::Propagation::Proceed
|
||||
}
|
||||
});
|
||||
}
|
||||
btn.add_controller(scroll);
|
||||
|
||||
btn
|
||||
}
|
||||
|
||||
fn primary_action(ctx: &Rc<Ctx>, item: &Rc<Item>, anchor: >k::Button) {
|
||||
let service = ctx.service.borrow();
|
||||
let Some(service) = service.as_ref() else {
|
||||
icons::launch(&item.meta);
|
||||
return;
|
||||
};
|
||||
match item.windows.len() {
|
||||
0 => icons::launch(&item.meta),
|
||||
1 => {
|
||||
let w = &item.windows[0];
|
||||
let click = ctx.cfg.borrow().behavior.click.clone();
|
||||
if w.activated && !w.minimized && click == ClickAction::FocusOrMinimize {
|
||||
service.set_minimized(w, true);
|
||||
} else {
|
||||
service.activate(w);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let click = ctx.cfg.borrow().behavior.click.clone();
|
||||
if click == ClickAction::Cycle {
|
||||
cycle_windows(ctx, item, true);
|
||||
} else {
|
||||
menu::popup_group_list(ctx, item, anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn middle_action(ctx: &Rc<Ctx>, item: &Rc<Item>) {
|
||||
let action = ctx.cfg.borrow().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]);
|
||||
}
|
||||
Reference in New Issue
Block a user