waydock v0.1: panel, pins, fönstergrupper, menyer, settings-GUI, live-config
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:
2026-08-29 23:24:29 +02:00
commit bc2fa65558
21 changed files with 4579 additions and 0 deletions

316
src/menu.rs Normal file
View File

@@ -0,0 +1,316 @@
// Kontextmenyer: höger­klick på app-ikon (konfigurerbar via
// config.context_menu), grupp-popup med fönsterlistan, samt panelmenyn
// på tom yta (Inställningar…, Ladda om, Avsluta).
use std::rc::Rc;
use gtk4 as gtk;
use gtk4::gdk;
use gtk4::gio;
use gtk4::glib;
use gtk4::prelude::*;
use log::warn;
use crate::config::{self, MenuItem};
use crate::dock::{Ctx, Item};
use crate::icons;
use crate::settings;
use crate::toplevel::ToplevelInfo;
use crate::wayfire_ipc;
/// Målet för den senast öppnade ikon-menyn (actions läser härifrån).
pub struct MenuTarget {
pub key: String,
pub windows: Vec<ToplevelInfo>,
pub pinned: bool,
pub connector: String,
}
fn with_service(ctx: &Ctx, f: impl Fn(&crate::toplevel::ToplevelService, &ToplevelInfo)) {
let target = ctx.menu_target.borrow();
let service = ctx.service.borrow();
if let (Some(t), Some(s)) = (target.as_ref(), service.as_ref()) {
for w in &t.windows {
f(s, w);
}
}
}
/// Registrera action-gruppen "dock" på ett dockfönster.
pub fn setup_dock_actions(ctx: &Rc<Ctx>, win: &gtk::ApplicationWindow) {
let group = gio::SimpleActionGroup::new();
let minimize = gio::SimpleAction::new("minimize", None);
{
let ctx = ctx.clone();
minimize.connect_activate(move |_, _| {
let all_min = ctx
.menu_target
.borrow()
.as_ref()
.map(|t| t.windows.iter().all(|w| w.minimized))
.unwrap_or(false);
with_service(&ctx, |s, w| s.set_minimized(w, !all_min));
});
}
group.add_action(&minimize);
let maximize = gio::SimpleAction::new("maximize", None);
{
let ctx = ctx.clone();
maximize.connect_activate(move |_, _| {
let all_max = ctx
.menu_target
.borrow()
.as_ref()
.map(|t| t.windows.iter().all(|w| w.maximized))
.unwrap_or(false);
with_service(&ctx, |s, w| s.set_maximized(w, !all_max));
});
}
group.add_action(&maximize);
let close = gio::SimpleAction::new("close", None);
{
let ctx = ctx.clone();
close.connect_activate(move |_, _| with_service(&ctx, |s, w| s.close(w)));
}
group.add_action(&close);
let pin = gio::SimpleAction::new("pin", None);
{
let ctx = ctx.clone();
pin.connect_activate(move |_, _| {
let connector = ctx
.menu_target
.borrow()
.as_ref()
.map(|t| t.connector.clone())
.unwrap_or_default();
toggle_pin(&ctx, &connector);
});
}
group.add_action(&pin);
let move_to = gio::SimpleAction::new("move-to-output", Some(glib::VariantTy::STRING));
{
let ctx = ctx.clone();
move_to.connect_activate(move |_, param| {
let Some(output) = param.and_then(|v| v.get::<String>()) else { return };
let target = ctx.menu_target.borrow();
let Some(t) = target.as_ref() else { return };
for w in &t.windows {
match wayfire_ipc::find_view_id(&w.app_id, &w.title) {
Some(id) => wayfire_ipc::move_view_to_output(id, &output),
None => warn!("hittade ingen wayfire-vy för {}", w.app_id),
}
}
});
}
group.add_action(&move_to);
win.insert_action_group("dock", Some(&group));
}
fn toggle_pin(ctx: &Rc<Ctx>, connector: &str) {
let Some((key, pinned)) = ctx
.menu_target
.borrow()
.as_ref()
.map(|t| (t.key.clone(), t.pinned))
else {
return;
};
{
let mut cfg = ctx.cfg.borrow_mut();
// skriv i output-egna pins om skärmen har egna, annars i de globala
let has_own = cfg
.outputs
.get(connector)
.map(|o| o.pins.is_some())
.unwrap_or(false);
let pins: &mut Vec<String> = if has_own {
cfg.outputs
.get_mut(connector)
.and_then(|o| o.pins.as_mut())
.expect("has_own kollades nyss")
} else {
&mut cfg.pins
};
if pinned {
pins.retain(|p| p.to_lowercase() != key);
} else if !pins.iter().any(|p| p.to_lowercase() == key) {
pins.push(key.clone());
}
if let Err(e) = config::save(&ctx.cfg_path, &cfg) {
warn!("kunde inte spara config: {e}");
}
}
crate::dock::refresh_items(ctx);
}
/// Popover som städar bort sig själv när den stängs.
fn autodispose(popover: &impl IsA<gtk::Popover>) {
let p = popover.as_ref().clone();
p.connect_closed(|p| {
let p = p.clone();
glib::idle_add_local_once(move || p.unparent());
});
}
/// Högerklicksmenyn på en app-ikon.
pub fn popup_item_menu(ctx: &Rc<Ctx>, item: &Rc<Item>, anchor: &gtk::Button) {
let cfg = ctx.cfg.borrow();
let has_windows = !item.windows.is_empty();
let all_min = has_windows && item.windows.iter().all(|w| w.minimized);
let all_max = has_windows && item.windows.iter().all(|w| w.maximized);
let menu = gio::Menu::new();
for entry in &cfg.context_menu {
match entry {
MenuItem::Minimize if has_windows => {
menu.append(
Some(if all_min { "Återställ" } else { "Minimera" }),
Some("dock.minimize"),
);
}
MenuItem::Maximize if has_windows => {
menu.append(
Some(if all_max { "Avmaximera" } else { "Maximera" }),
Some("dock.maximize"),
);
}
MenuItem::MoveToOutput if has_windows && wayfire_ipc::available() => {
let sub = gio::Menu::new();
let docks = ctx.docks.borrow();
for dock in docks.iter() {
let mi = gio::MenuItem::new(Some(&dock.connector), None);
mi.set_action_and_target_value(
Some("dock.move-to-output"),
Some(&dock.connector.to_variant()),
);
sub.append_item(&mi);
}
menu.append_submenu(Some("Flytta till skärm"), &sub);
}
MenuItem::Pin => {
menu.append(
Some(if item.pinned { "Släpp pin" } else { "Pinna" }),
Some("dock.pin"),
);
}
MenuItem::Close if has_windows => {
menu.append(Some("Stäng"), Some("dock.close"));
}
_ => {}
}
}
drop(cfg);
// hitta skärmens connector via dockan som äger knappen
let connector = ctx
.docks
.borrow()
.iter()
.find(|d| anchor.ancestor(gtk::ApplicationWindow::static_type())
.map(|w| w == *d.window.upcast_ref::<gtk::Widget>())
.unwrap_or(false))
.map(|d| d.connector.clone())
.unwrap_or_default();
*ctx.menu_target.borrow_mut() = Some(MenuTarget {
key: item.meta.key.clone(),
windows: item.windows.clone(),
pinned: item.pinned,
connector,
});
let popover = gtk::PopoverMenu::from_model(Some(&menu));
popover.add_css_class("waydock-menu");
popover.set_parent(anchor);
autodispose(&popover);
popover.popup();
}
/// Vänsterklick på grupp med flera fönster: lista fönstren.
pub fn popup_group_list(ctx: &Rc<Ctx>, item: &Rc<Item>, anchor: &gtk::Button) {
let popover = gtk::Popover::new();
popover.add_css_class("waydock-menu");
let list = gtk::Box::new(gtk::Orientation::Vertical, 2);
for w in &item.windows {
let row = gtk::Button::new();
row.add_css_class("flat");
let title = if w.title.is_empty() { item.meta.display_name.clone() } else { w.title.clone() };
let label = gtk::Label::new(Some(&title));
label.set_ellipsize(gtk4::pango::EllipsizeMode::End);
label.set_max_width_chars(40);
label.set_halign(gtk::Align::Start);
row.set_child(Some(&label));
let ctx2 = ctx.clone();
let w2 = w.clone();
let pop2 = popover.clone();
row.connect_clicked(move |_| {
if let Some(s) = ctx2.service.borrow().as_ref() {
s.activate(&w2);
}
pop2.popdown();
});
list.append(&row);
}
popover.set_child(Some(&list));
popover.set_parent(anchor);
autodispose(&popover);
popover.popup();
}
/// Högerklick på tom panelyta.
pub fn popup_panel_menu(ctx: &Rc<Ctx>, panel: &gtk::Box, x: f64, y: f64) {
let menu = gio::Menu::new();
menu.append(Some("Inställningar…"), Some("panel.settings"));
menu.append(Some("Ladda om config"), Some("panel.reload"));
menu.append(Some("Avsluta waydock"), Some("panel.quit"));
let group = gio::SimpleActionGroup::new();
let open_settings = gio::SimpleAction::new("settings", None);
{
let ctx = ctx.clone();
open_settings.connect_activate(move |_, _| settings::open(&ctx));
}
group.add_action(&open_settings);
let reload = gio::SimpleAction::new("reload", None);
{
let ctx = ctx.clone();
reload.connect_activate(move |_, _| {
let fresh = config::load_or_create(&ctx.cfg_path);
*ctx.cfg.borrow_mut() = fresh;
if let Some(cb) = ctx.on_config_changed.borrow().as_ref() {
cb();
}
});
}
group.add_action(&reload);
let quit = gio::SimpleAction::new("quit", None);
{
let ctx = ctx.clone();
quit.connect_activate(move |_, _| ctx.app.quit());
}
group.add_action(&quit);
panel.insert_action_group("panel", Some(&group));
let popover = gtk::PopoverMenu::from_model(Some(&menu));
popover.add_css_class("waydock-menu");
popover.set_parent(panel);
popover.set_pointing_to(Some(&gdk::Rectangle::new(x as i32, y as i32, 1, 1)));
autodispose(&popover);
popover.popup();
}
/// Starta en app från menyn (används inte ännu — behålls för settings-GUI:t).
#[allow(dead_code)]
pub fn launch_item(item: &Item) {
icons::launch(&item.meta);
}