Per-skärm-inställningar med skärmväljare, kant-i-kant-marginaler
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>
This commit is contained in:
2026-08-30 00:25:05 +02:00
parent 04d999642c
commit 558c178ec4
8 changed files with 609 additions and 167 deletions

View File

@@ -35,6 +35,13 @@ Alla ändringar i filen slår igenom **live** — ingen omstart behövs.
Högerklick på tom yta → **Inställningar…** Ändringar skrivs direkt till
JSON-filen och appliceras live. GUI:t och handredigering kan blandas fritt.
Överst finns **skärmväljaren** med en klickbar förhandsgranskning av
skärmarna och deras barer. "Alla skärmar" (standard) redigerar den
globala configen; välj en skärm för att ge just den egna inställningar —
bara fälten du ändrar sparas som override (`outputs` i JSON:en), resten
följer globala. `*` i förhandsgranskningen = skärmen har egna
inställningar; "Återställ skärmen till globalt" tar bort dem.
## Configreferens (`config.json`)
```jsonc
@@ -42,9 +49,11 @@ JSON-filen och appliceras live. GUI:t och handredigering kan blandas fritt.
"position": "bottom", // bottom | top | left | right
"alignment": "center", // center | start | end — var längs kanten ön ligger
"expand": false, // true = täck hela kanten (panelen fyller bredden/höjden)
"margin": 8, // A: px från kanten baren ligger mot
"margin": 8, // A: px från kanten baren ligger mot (minus = utanför)
"margin_start": 0, // B: marginal i början av kanten (vänster/topp)
"margin_end": 0, // C: marginal i slutet av kanten (höger/botten)
// skuggutrymme läggs aldrig på förankrade sidor,
// så 0 = verkligen kant i kant
"layer": "top", // top | overlay | bottom (always-on-top = overlay)
"avoid_windows": false, // true = reservera plats (exclusive zone)
@@ -81,11 +90,16 @@ JSON-filen och appliceras live. GUI:t och handredigering kan blandas fritt.
"pins": ["firefox", "org.kde.konsole", "org.kde.dolphin"],
"special_buttons": { "show_desktop": true, "overview": true },
"outputs": { // per-skärm-överstyrning (nyckel = connector)
"outputs": { // per-skärm-överridar (nyckel = connector).
// VILKET configfält som helst kan läggas här och gäller då bara den
// skärmen (deep-merge över globala). Extranycklar: "enabled" och
// "only_this_output" (visa bara fönster på den här skärmen).
"DP-1": {
"enabled": true,
"only_this_output": true, // visa bara fönster på den här skärmen
"pins": ["steam", "discord"] // null/utelämnad = ärver globala pins
"only_this_output": true,
"position": "left",
"pins": ["steam", "discord"],
"appearance": { "accent": "#5fb878" }
}
},

View File

@@ -197,13 +197,10 @@ impl Default for Behavior {
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct OutputConfig {
pub enabled: Option<bool>,
pub only_this_output: Option<bool>,
pub pins: Option<Vec<String>>,
}
// Per-skärm-överridar: en partiell Config i JSON-form som deep-mergas
// över den globala. Extranycklarna "enabled" och "only_this_output" är
// skärm-specifika och läses separat (de finns inte i Config).
pub type OutputOverride = serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
@@ -316,7 +313,7 @@ pub struct Config {
pub context_menu: Vec<MenuItem>,
pub pins: Vec<String>,
pub special_buttons: SpecialButtons,
pub outputs: HashMap<String, OutputConfig>,
pub outputs: HashMap<String, OutputOverride>,
pub autohide: Autohide,
pub hotspots: Hotspots,
pub log: LogConfig,
@@ -356,26 +353,94 @@ impl Default for Config {
}
}
/// Deep-merge: värden i `patch` skrivs in i `base` (objekt rekursivt,
/// allt annat ersätts).
pub fn json_merge(base: &mut serde_json::Value, patch: &serde_json::Value) {
if let (Some(base_map), Some(patch_map)) = (base.as_object_mut(), patch.as_object()) {
for (k, v) in patch_map {
match base_map.get_mut(k) {
Some(slot) if slot.is_object() && v.is_object() => json_merge(slot, v),
_ => {
base_map.insert(k.clone(), v.clone());
}
}
}
} else {
*base = patch.clone();
}
}
/// Minimal diff: objektet med bara de löv som skiljer `after` från `before`.
/// Null om inget skiljer.
pub fn json_diff(before: &serde_json::Value, after: &serde_json::Value) -> serde_json::Value {
use serde_json::Value;
if before == after {
return Value::Null;
}
if let (Some(b), Some(a)) = (before.as_object(), after.as_object()) {
let mut out = serde_json::Map::new();
for (k, av) in a {
match b.get(k) {
Some(bv) => {
let d = json_diff(bv, av);
if !d.is_null() {
out.insert(k.clone(), d);
}
}
None => {
out.insert(k.clone(), av.clone());
}
}
}
if out.is_empty() {
Value::Null
} else {
Value::Object(out)
}
} else {
after.clone()
}
}
impl Config {
/// Effektiva pins för en viss skärm (output-överstyrning eller globala).
pub fn pins_for(&self, output: &str) -> &[String] {
self.outputs
.get(output)
.and_then(|o| o.pins.as_deref())
.unwrap_or(&self.pins)
/// Effektiv config för en skärm: global + per-skärm-överridar mergade.
pub fn for_output(&self, output: &str) -> Config {
match self.outputs.get(output) {
None => self.clone(),
Some(over) => {
let mut base = serde_json::to_value(self).expect("config serialiserbar");
json_merge(&mut base, over);
match serde_json::from_value::<Config>(base) {
Ok(mut cfg) => {
// undvik rekursiva överridar
cfg.outputs = self.outputs.clone();
cfg
}
Err(e) => {
warn!("trasig override för {output} ({e}) — kör globalt");
self.clone()
}
}
}
}
}
/// Effektiva pins för en viss skärm.
pub fn pins_for(&self, output: &str) -> Vec<String> {
self.for_output(output).pins
}
pub fn output_enabled(&self, output: &str) -> bool {
self.outputs
.get(output)
.and_then(|o| o.enabled)
.and_then(|o| o["enabled"].as_bool())
.unwrap_or(true)
}
pub fn only_this_output(&self, output: &str) -> bool {
self.outputs
.get(output)
.and_then(|o| o.only_this_output)
.and_then(|o| o["only_this_output"].as_bool())
.unwrap_or(false)
}
}

View File

@@ -16,8 +16,8 @@ use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell};
use log::{debug, info};
use crate::config::{
Alignment, AutohideMode, ClickAction, GroupPopup, Layer as CfgLayer, MiddleClick,
Position, ScrollAction, SharedConfig,
Alignment, AutohideMode, ClickAction, Config, GroupPopup, Layer as CfgLayer,
MiddleClick, Position, ScrollAction, SharedConfig,
};
use crate::icons::{self, AppMeta};
use crate::menu;
@@ -90,8 +90,7 @@ fn side_edges(pos: &Position) -> (Edge, Edge) {
}
}
fn configure_layer(win: &gtk::ApplicationWindow, ctx: &Ctx, monitor: &gdk::Monitor) {
let cfg = ctx.cfg.borrow();
fn configure_layer(win: &gtk::ApplicationWindow, cfg: &Config, monitor: &gdk::Monitor) {
win.init_layer_shell();
win.set_namespace(Some("waydock"));
win.set_monitor(Some(monitor));
@@ -167,9 +166,10 @@ pub fn rebuild_all(ctx: &Rc<Ctx>) {
}
fn build_dock(ctx: &Rc<Ctx>, monitor: &gdk::Monitor, connector: String) -> Rc<Dock> {
let cfg = ctx.cfg.borrow();
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);
@@ -199,17 +199,46 @@ fn build_dock(ctx: &Rc<Ctx>, monitor: &gdk::Monitor, connector: String) -> Rc<Do
}
panel.append(&items_box);
// yttre marginal = plats för skuggan att rita
// 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
};
outer.set_margin_top(pad);
outer.set_margin_bottom(pad);
outer.set_margin_start(pad);
outer.set_margin_end(pad);
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
@@ -229,9 +258,8 @@ fn build_dock(ctx: &Rc<Ctx>, monitor: &gdk::Monitor, connector: String) -> Rc<Do
revealer.set_child(Some(&outer));
win.set_child(Some(&revealer));
let autohide_enabled = cfg.autohide.enabled;
drop(cfg);
configure_layer(&win, ctx, monitor);
configure_layer(&win, &cfg, monitor);
menu::setup_dock_actions(ctx, &win);
let dock = Rc::new(Dock {
@@ -289,7 +317,7 @@ fn build_dock(ctx: &Rc<Ctx>, monitor: &gdk::Monitor, connector: String) -> Rc<Do
/// 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 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();
@@ -359,7 +387,7 @@ fn set_dock_hidden(dock: &Rc<Dock>, hidden: bool) {
}
fn should_hide(ctx: &Ctx, dock: &Dock) -> bool {
let cfg = ctx.cfg.borrow();
let cfg = ctx.cfg.borrow().for_output(&dock.connector);
if !cfg.autohide.enabled || dock.hover.get() {
return false;
}
@@ -383,7 +411,7 @@ pub fn update_autohide(ctx: &Rc<Ctx>) {
}
fn schedule_hide(ctx: &Rc<Ctx>, dock: &Rc<Dock>) {
let delay = ctx.cfg.borrow().autohide.hide_delay_ms;
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();
@@ -397,18 +425,24 @@ fn schedule_hide(ctx: &Rc<Ctx>, dock: &Rc<Dock>) {
/// 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;
}
// 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 };
let position = ctx.cfg.borrow().position.clone();
let margin = ctx.cfg.borrow().margin;
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()))
@@ -542,7 +576,11 @@ fn special_button(icon_name: &str, tooltip: &str) -> gtk::Button {
}
fn add_special_buttons(ctx: &Rc<Ctx>, dock: &Rc<Dock>) {
let specials = ctx.cfg.borrow().special_buttons.clone();
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)");
@@ -562,12 +600,11 @@ fn add_special_buttons(ctx: &Rc<Ctx>, dock: &Rc<Dock>) {
}
fn build_item_widget(ctx: &Rc<Ctx>, connector: &str, item: Rc<Item>) -> gtk::Button {
let cfg = ctx.cfg.borrow();
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;
drop(cfg);
let btn = gtk::Button::new();
btn.add_css_class("dock-item");
@@ -639,8 +676,8 @@ fn build_item_widget(ctx: &Rc<Ctx>, connector: &str, item: Rc<Item>) -> gtk::But
gesture.set_state(gtk::EventSequenceState::Claimed);
let live = live_item(&ctx, &conn2, &item);
match gesture.current_button() {
1 => primary_action(&ctx, &live, &btn2),
2 => middle_action(&ctx, &live),
1 => primary_action(&ctx, &conn2, &live, &btn2),
2 => middle_action(&ctx, &conn2, &live),
3 => menu::popup_item_menu(&ctx, &live, &btn2),
_ => {}
}
@@ -649,9 +686,7 @@ fn build_item_widget(ctx: &Rc<Ctx>, connector: &str, item: Rc<Item>) -> gtk::But
btn.add_controller(click);
// hover-popup för grupperade appar (om valt i configen)
if item.windows.len() > 1
&& ctx.cfg.borrow().behavior.group_popup == GroupPopup::Hover
{
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();
@@ -682,7 +717,7 @@ fn build_item_widget(ctx: &Rc<Ctx>, connector: &str, item: Rc<Item>) -> gtk::But
let item = item.clone();
let conn2 = connector.to_string();
scroll.connect_scroll(move |_, _, dy| {
if ctx.cfg.borrow().behavior.scroll == ScrollAction::CycleWindows {
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
@@ -696,28 +731,25 @@ fn build_item_widget(ctx: &Rc<Ctx>, connector: &str, item: Rc<Item>) -> gtk::But
btn
}
fn primary_action(ctx: &Rc<Ctx>, item: &Rc<Item>, anchor: &gtk::Button) {
fn primary_action(ctx: &Rc<Ctx>, connector: &str, item: &Rc<Item>, anchor: &gtk::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];
let click = ctx.cfg.borrow().behavior.click.clone();
if w.activated && !w.minimized && click == ClickAction::FocusOrMinimize {
if w.activated && !w.minimized && behavior.click == ClickAction::FocusOrMinimize {
service.set_minimized(w, true);
} else {
service.activate(w);
}
}
_ => {
let (click, popup) = {
let b = &ctx.cfg.borrow().behavior;
(b.click.clone(), b.group_popup.clone())
};
let (click, popup) = (behavior.click.clone(), behavior.group_popup.clone());
if click == ClickAction::Cycle || popup == GroupPopup::Hover {
cycle_windows(ctx, item, true);
} else {
@@ -727,8 +759,8 @@ fn primary_action(ctx: &Rc<Ctx>, item: &Rc<Item>, anchor: &gtk::Button) {
}
}
fn middle_action(ctx: &Rc<Ctx>, item: &Rc<Item>) {
let action = ctx.cfg.borrow().behavior.middle_click.clone();
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 => {

View File

@@ -109,10 +109,25 @@ thread_local! {
/// Bygg om alla hotspot-ytor enligt configen (körs vid start + configbyte).
/// Idempotent: oförändrad hotspot-config + skärmuppsättning → ingen ombyggnad.
pub fn rebuild(ctx: &Rc<Ctx>) {
let n_monitors = gdk::Display::default()
.map(|d| d.monitors().n_items())
.unwrap_or(0);
let signature = format!("{:?}|{n_monitors}", ctx.cfg.borrow().hotspots);
let display = gdk::Display::default().expect("ingen display");
let monitors = display.monitors();
// signatur = alla skärmars EFFEKTIVA hotspot-config
let mut signature = String::new();
let mut mons: Vec<(String, gdk::Monitor)> = 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}"));
let eff = ctx.cfg.borrow().for_output(&connector);
signature += &format!("{connector}:{:?}|", eff.hotspots);
mons.push((connector, monitor));
}
let unchanged = LAST_SIGNATURE.with(|s| *s.borrow() == signature);
if unchanged {
return;
@@ -123,41 +138,33 @@ pub fn rebuild(ctx: &Rc<Ctx>) {
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 (connector, monitor) in &mons {
let h = ctx.cfg.borrow().for_output(connector).hotspots;
let dwell = h.dwell_ms;
let c = h.corners.clone();
let e = h.edges.clone();
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å)
#[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)),
];
for (action, edges) in &corner_defs {
if let Some(a) = action {
windows.push(build_hotspot(
ctx,
&monitor,
monitor,
edges,
(CORNER_PX, CORNER_PX),
a.clone(),
@@ -167,7 +174,7 @@ pub fn rebuild(ctx: &Rc<Ctx>) {
}
for (action, edges, size) in &edge_defs {
if let Some(a) = action {
windows.push(build_hotspot(ctx, &monitor, edges, *size, a.clone(), dwell));
windows.push(build_hotspot(ctx, monitor, edges, *size, a.clone(), dwell));
}
}
}

View File

@@ -113,7 +113,7 @@ fn layout_signature(cfg: &config::Config) -> String {
cfg.avoid_windows,
cfg.appearance.shadow.enabled,
cfg.appearance.shadow.blur,
cfg.outputs.keys().collect::<Vec<_>>(),
serde_json::to_string(&cfg.outputs).unwrap_or_default(),
) + &format!("|{:?}", cfg.autohide)
}

View File

@@ -123,24 +123,27 @@ fn toggle_pin(ctx: &Rc<Ctx>, connector: &str) {
};
{
let mut cfg = ctx.cfg.borrow_mut();
// skriv i output-egna pins om skärmen har egna, annars i de globala
// skriv i skärmens pin-override om den har en, annars i de globala
let has_own = cfg
.outputs
.get(connector)
.map(|o| o.pins.is_some())
.map(|o| o["pins"].is_array())
.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
let mutate = |pins: &mut Vec<String>| {
if pinned {
pins.retain(|p| p.to_lowercase() != key);
} else if !pins.iter().any(|p| p.to_lowercase() == key) {
pins.push(key.clone());
}
};
if pinned {
pins.retain(|p| p.to_lowercase() != key);
} else if !pins.iter().any(|p| p.to_lowercase() == key) {
pins.push(key.clone());
if has_own {
let over = cfg.outputs.get_mut(connector).expect("has_own kollades nyss");
let mut pins: Vec<String> =
serde_json::from_value(over["pins"].clone()).unwrap_or_default();
mutate(&mut pins);
over["pins"] = serde_json::to_value(pins).expect("strängar serialiserbara");
} else {
mutate(&mut cfg.pins);
}
if let Err(e) = config::save(&ctx.cfg_path, &cfg) {
warn!("kunde inte spara config: {e}");

View File

@@ -24,14 +24,58 @@ thread_local! {
const { std::cell::RefCell::new(None) };
static ILLUSTRATION: std::cell::RefCell<Option<glib::WeakRef<gtk::DrawingArea>>> =
const { std::cell::RefCell::new(None) };
static PREVIEW: std::cell::RefCell<Option<glib::WeakRef<gtk::DrawingArea>>> =
const { std::cell::RefCell::new(None) };
/// None = "Alla skärmar" (globala configen); Some = en enskild skärm
static SCOPE: std::cell::RefCell<Option<String>> =
const { std::cell::RefCell::new(None) };
}
fn current_scope() -> Option<String> {
SCOPE.with(|s| s.borrow().clone())
}
/// Configen som settings-sidorna ska visa: global eller skärmens effektiva.
fn scoped_config(ctx: &Rc<Ctx>) -> config::Config {
match current_scope() {
None => ctx.cfg.borrow().clone(),
Some(conn) => ctx.cfg.borrow().for_output(&conn),
}
}
/// Uppdatera config + applicera + spara (debouncat 300 ms).
/// Med en skärm vald skrivs bara de ändrade fälten in som override för
/// den skärmen; "Alla skärmar" redigerar den globala configen.
fn commit(ctx: &Rc<Ctx>, mutate: impl FnOnce(&mut config::Config)) {
mutate(&mut ctx.cfg.borrow_mut());
match current_scope() {
None => mutate(&mut ctx.cfg.borrow_mut()),
Some(conn) => {
let mut eff = ctx.cfg.borrow().for_output(&conn);
let before = serde_json::to_value(&eff).expect("config serialiserbar");
mutate(&mut eff);
let after = serde_json::to_value(&eff).expect("config serialiserbar");
let mut patch = config::json_diff(&before, &after);
if let Some(obj) = patch.as_object_mut() {
obj.remove("outputs"); // aldrig rekursiva överridar
}
if !patch.is_null() {
let mut cfg = ctx.cfg.borrow_mut();
let entry = cfg
.outputs
.entry(conn)
.or_insert_with(|| serde_json::json!({}));
config::json_merge(entry, &patch);
}
}
}
if let Some(cb) = ctx.on_config_changed.borrow().as_ref() {
cb();
}
PREVIEW.with(|i| {
if let Some(da) = i.borrow().as_ref().and_then(|w| w.upgrade()) {
da.queue_draw();
}
});
ILLUSTRATION.with(|i| {
if let Some(da) = i.borrow().as_ref().and_then(|w| w.upgrade()) {
da.queue_draw();
@@ -221,7 +265,7 @@ fn hotspot_entry(
fn build_appearance_page(ctx: &Rc<Ctx>) -> gtk::ScrolledWindow {
let (scroll, list) = page();
let a = ctx.cfg.borrow().appearance.clone();
let a = scoped_config(ctx).appearance;
heading(&list, "Panel");
row(&list, "Panelfärg", &color_button(ctx, &a.background, |c, v| c.appearance.background = v));
@@ -267,7 +311,7 @@ fn margin_illustration(ctx: &Rc<Ctx>) -> gtk::DrawingArea {
da.set_halign(gtk::Align::Center);
let ctx2 = ctx.clone();
da.set_draw_func(move |_, cr, w, h| {
let cfg = ctx2.cfg.borrow();
let cfg = scoped_config(&ctx2);
let (w, h) = (w as f64, h as f64);
// "skärmen"
@@ -381,7 +425,7 @@ fn margin_illustration(ctx: &Rc<Ctx>) -> gtk::DrawingArea {
fn build_layout_page(ctx: &Rc<Ctx>) -> gtk::ScrolledWindow {
let (scroll, list) = page();
let cfg = ctx.cfg.borrow().clone();
let cfg = scoped_config(ctx);
heading(&list, "Placering");
let pos_idx = match cfg.position {
@@ -427,9 +471,9 @@ fn build_layout_page(ctx: &Rc<Ctx>) -> gtk::ScrolledWindow {
heading(&list, "Marginaler");
list.append(&margin_illustration(ctx));
row(&list, "A — Avstånd från kanten", &spin(ctx, cfg.margin as f64, (0.0, 128.0), 1.0, |c, v| c.margin = v as i32));
row(&list, "B — Marginal start", &spin(ctx, cfg.margin_start as f64, (0.0, 512.0), 1.0, |c, v| c.margin_start = v as i32));
row(&list, "C — Marginal slut", &spin(ctx, cfg.margin_end as f64, (0.0, 512.0), 1.0, |c, v| c.margin_end = v as i32));
row(&list, "A — Avstånd från kanten", &spin(ctx, cfg.margin as f64, (-64.0, 256.0), 1.0, |c, v| c.margin = v as i32));
row(&list, "B — Marginal start", &spin(ctx, cfg.margin_start as f64, (-256.0, 1024.0), 1.0, |c, v| c.margin_start = v as i32));
row(&list, "C — Marginal slut", &spin(ctx, cfg.margin_end as f64, (-256.0, 1024.0), 1.0, |c, v| c.margin_end = v as i32));
let note = gtk::Label::new(Some(
"A = avstånd från kanten baren ligger mot.\nB/C = marginal i början/slutet av kanten — indrag när baren\ntäcker kanten, annars avstånd från vald sida (vid Start/Slut).",
));
@@ -461,8 +505,9 @@ fn build_layout_page(ctx: &Rc<Ctx>) -> gtk::ScrolledWindow {
fn build_behavior_page(ctx: &Rc<Ctx>) -> gtk::ScrolledWindow {
let (scroll, list) = page();
let b = ctx.cfg.borrow().behavior.clone();
let sp = ctx.cfg.borrow().special_buttons.clone();
let scoped = scoped_config(ctx);
let b = scoped.behavior.clone();
let sp = scoped.special_buttons.clone();
heading(&list, "Klick");
let click_idx = match b.click {
@@ -534,7 +579,7 @@ fn build_behavior_page(ctx: &Rc<Ctx>) -> gtk::ScrolledWindow {
("Stäng", MenuItem::Close),
];
for (label, mi) in entries {
let present = ctx.cfg.borrow().context_menu.contains(&mi);
let present = scoped.context_menu.contains(&mi);
let mi2 = mi.clone();
row(&list, label, &switch(ctx, present, move |c, v| {
if v {
@@ -561,7 +606,7 @@ fn build_pins_page(ctx: &Rc<Ctx>) -> gtk::ScrolledWindow {
while let Some(c) = pins_box.first_child() {
pins_box.remove(&c);
}
let pins = ctx.cfg.borrow().pins.clone();
let pins = scoped_config(ctx).pins.clone();
for (i, pin) in pins.iter().enumerate() {
let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 6);
let name = crate::icons::lookup_pin(pin)
@@ -592,7 +637,9 @@ fn build_pins_page(ctx: &Rc<Ctx>) -> gtk::ScrolledWindow {
let pb3 = pins_box.clone();
remove.connect_clicked(move |_| {
commit(&ctx3, |c| {
c.pins.remove(i);
if i < c.pins.len() {
c.pins.remove(i);
}
});
rebuild_rows(&ctx3, &pb3);
});
@@ -643,7 +690,7 @@ fn build_pins_page(ctx: &Rc<Ctx>) -> gtk::ScrolledWindow {
fn build_hotspots_page(ctx: &Rc<Ctx>) -> gtk::ScrolledWindow {
let (scroll, list) = page();
let h = ctx.cfg.borrow().hotspots.clone();
let h = scoped_config(ctx).hotspots.clone();
heading(&list, "Hot corners");
row(&list, "Uppe vänster", &hotspot_entry(ctx, &h.corners.top_left, |c, v| c.hotspots.corners.top_left = v));
@@ -668,6 +715,142 @@ fn build_hotspots_page(ctx: &Rc<Ctx>) -> gtk::ScrolledWindow {
scroll
}
/// Skärmarna som gdk ser: (connector, logisk geometri).
fn monitor_list() -> Vec<(String, gdk::Rectangle)> {
let mut out = Vec::new();
let Some(display) = gdk::Display::default() else {
return out;
};
let monitors = display.monitors();
for i in 0..monitors.n_items() {
let Some(m) = monitors.item(i).and_then(|o| o.downcast::<gdk::Monitor>().ok()) else {
continue;
};
let name = m
.connector()
.map(|c| c.to_string())
.unwrap_or_else(|| format!("monitor-{i}"));
out.push((name, m.geometry()));
}
out
}
/// Normalisera skärmrektanglarna till förhandsgranskningens yta.
fn preview_layout(mons: &[(String, gdk::Rectangle)], w: f64, h: f64)
-> Vec<(String, (f64, f64, f64, f64))> {
if mons.is_empty() {
return Vec::new();
}
let min_x = mons.iter().map(|(_, g)| g.x()).min().unwrap_or(0) as f64;
let min_y = mons.iter().map(|(_, g)| g.y()).min().unwrap_or(0) as f64;
let max_x = mons.iter().map(|(_, g)| g.x() + g.width()).max().unwrap_or(1) as f64;
let max_y = mons.iter().map(|(_, g)| g.y() + g.height()).max().unwrap_or(1) as f64;
let scale = ((w - 16.0) / (max_x - min_x)).min((h - 16.0) / (max_y - min_y));
let off_x = (w - (max_x - min_x) * scale) / 2.0;
let off_y = (h - (max_y - min_y) * scale) / 2.0;
mons.iter()
.map(|(name, g)| {
(
name.clone(),
(
off_x + (g.x() as f64 - min_x) * scale,
off_y + (g.y() as f64 - min_y) * scale,
g.width() as f64 * scale,
g.height() as f64 * scale,
),
)
})
.collect()
}
type RefreshSlot = Rc<std::cell::RefCell<Option<Rc<dyn Fn()>>>>;
fn build_preview(ctx: &Rc<Ctx>, refresh: RefreshSlot) -> gtk::DrawingArea {
let da = gtk::DrawingArea::new();
da.set_content_width(400);
da.set_content_height(96);
da.set_halign(gtk::Align::Center);
let ctx2 = ctx.clone();
da.set_draw_func(move |_, cr, w, h| {
let scope = current_scope();
let mons = monitor_list();
for (name, (x, y, mw, mh)) in preview_layout(&mons, w as f64, h as f64) {
let eff = ctx2.cfg.borrow().for_output(&name);
// skärmen
cr.set_source_rgba(0.15, 0.15, 0.17, 1.0);
cr.rectangle(x, y, mw, mh);
let _ = cr.fill();
let selected = scope.is_none() || scope.as_deref() == Some(name.as_str());
if selected {
cr.set_source_rgba(0.79, 0.33, 0.36, 1.0);
cr.set_line_width(2.0);
} else {
cr.set_source_rgba(0.45, 0.45, 0.5, 1.0);
cr.set_line_width(1.0);
}
cr.rectangle(x, y, mw, mh);
let _ = cr.stroke();
// barens mini-representation
let horizontal = matches!(eff.position, Position::Top | Position::Bottom);
let full = if horizontal { mw - 8.0 } else { mh - 8.0 };
let len = if eff.expand { full } else { full * 0.45 };
let along = match (eff.expand, &eff.alignment) {
(true, _) | (false, Alignment::Center) => (full - len) / 2.0 + 4.0,
(false, Alignment::Start) => 4.0,
(false, Alignment::End) => full - len + 4.0,
};
cr.set_source_rgba(0.9, 0.9, 0.92, 0.95);
let t = 3.0;
match eff.position {
Position::Bottom => cr.rectangle(x + along, y + mh - 6.0, len, t),
Position::Top => cr.rectangle(x + along, y + 3.0, len, t),
Position::Left => cr.rectangle(x + 3.0, y + along, t, len),
Position::Right => cr.rectangle(x + mw - 6.0, y + along, t, len),
}
let _ = cr.fill();
// namn + markering om skärmen har egna inställningar
cr.set_source_rgba(0.85, 0.85, 0.88, 1.0);
cr.set_font_size(10.0);
cr.move_to(x + 5.0, y + 14.0);
let has_override = ctx2.cfg.borrow().outputs.contains_key(&name);
let text = if has_override { format!("{name} *") } else { name.clone() };
let _ = cr.show_text(&text);
}
});
// klick väljer skärm (utanför = alla)
let click = gtk::GestureClick::new();
{
let da2 = da.clone();
click.connect_pressed(move |_, _, px, py| {
let mons = monitor_list();
let rects = preview_layout(&mons, da2.width() as f64, da2.height() as f64);
let hit = rects
.iter()
.find(|(_, (x, y, w, h))| px >= *x && px <= x + w && py >= *y && py <= y + h)
.map(|(n, _)| n.clone());
SCOPE.with(|s| *s.borrow_mut() = hit);
if let Some(f) = refresh.borrow().as_ref() {
f();
}
});
}
da.add_controller(click);
PREVIEW.with(|p| *p.borrow_mut() = Some(da.downgrade()));
da
}
fn build_notebook(ctx: &Rc<Ctx>) -> gtk::Notebook {
let notebook = gtk::Notebook::new();
notebook.set_vexpand(true);
notebook.append_page(&build_appearance_page(ctx), Some(&gtk::Label::new(Some("Utseende"))));
notebook.append_page(&build_layout_page(ctx), Some(&gtk::Label::new(Some("Layout"))));
notebook.append_page(&build_behavior_page(ctx), Some(&gtk::Label::new(Some("Beteende"))));
notebook.append_page(&build_pins_page(ctx), Some(&gtk::Label::new(Some("Pins"))));
notebook.append_page(&build_hotspots_page(ctx), Some(&gtk::Label::new(Some("Hotspots"))));
notebook
}
/// Öppna settings-fönstret (återanvänder ett redan öppet).
pub fn open(ctx: &Rc<Ctx>) {
let existing = OPEN_WINDOW.with(|w| w.borrow().as_ref().and_then(|w| w.upgrade()));
@@ -675,24 +858,136 @@ pub fn open(ctx: &Rc<Ctx>) {
win.present();
return;
}
SCOPE.with(|s| *s.borrow_mut() = None);
let win = gtk::Window::new();
win.set_title(Some("waydock — inställningar"));
win.set_icon_name(Some("waydock"));
win.set_default_size(520, 620);
win.set_default_size(560, 760);
let notebook = gtk::Notebook::new();
notebook.append_page(&build_appearance_page(ctx), Some(&gtk::Label::new(Some("Utseende"))));
notebook.append_page(&build_layout_page(ctx), Some(&gtk::Label::new(Some("Layout"))));
notebook.append_page(&build_behavior_page(ctx), Some(&gtk::Label::new(Some("Beteende"))));
notebook.append_page(&build_pins_page(ctx), Some(&gtk::Label::new(Some("Pins"))));
notebook.append_page(&build_hotspots_page(ctx), Some(&gtk::Label::new(Some("Hotspots"))));
win.set_child(Some(&notebook));
let root = gtk::Box::new(gtk::Orientation::Vertical, 6);
root.set_margin_top(10);
root.set_margin_bottom(4);
root.set_margin_start(10);
root.set_margin_end(10);
let refresh_slot: RefreshSlot = Rc::new(std::cell::RefCell::new(None));
// väljarrad: "Alla skärmar" + en toggle per skärm + återställ-knapp
let scope_row = gtk::Box::new(gtk::Orientation::Horizontal, 6);
scope_row.set_halign(gtk::Align::Center);
let updating = Rc::new(Cell::new(false));
let mut scope_values: Vec<Option<String>> = vec![None];
for (name, _) in monitor_list() {
scope_values.push(Some(name));
}
#[allow(clippy::type_complexity)]
let toggles: Rc<std::cell::RefCell<Vec<(Option<String>, gtk::ToggleButton)>>> =
Rc::new(std::cell::RefCell::new(Vec::new()));
for value in &scope_values {
let label = match value {
None => "Alla skärmar".to_string(),
Some(n) => n.clone(),
};
let btn = gtk::ToggleButton::with_label(&label);
btn.set_active(value.is_none());
{
let value = value.clone();
let updating = updating.clone();
let refresh_slot = refresh_slot.clone();
btn.connect_toggled(move |b| {
if updating.get() {
return;
}
if b.is_active() {
SCOPE.with(|s| *s.borrow_mut() = value.clone());
if let Some(f) = refresh_slot.borrow().as_ref() {
f();
}
} else if current_scope() == value {
// hindra av-toggling av den aktiva
updating.set(true);
b.set_active(true);
updating.set(false);
}
});
}
scope_row.append(&btn);
toggles.borrow_mut().push((value.clone(), btn));
}
let reset_btn = gtk::Button::with_label("Återställ skärmen till globalt");
reset_btn.add_css_class("destructive-action");
reset_btn.set_halign(gtk::Align::Center);
reset_btn.set_visible(false);
{
let ctx2 = ctx.clone();
let refresh_slot = refresh_slot.clone();
reset_btn.connect_clicked(move |_| {
if let Some(conn) = current_scope() {
commit(&ctx2, |_| {}); // no-op: säkrar att ev. debounce inte skriver gammalt
ctx2.cfg.borrow_mut().outputs.remove(&conn);
if let Err(e) = config::save(&ctx2.cfg_path, &ctx2.cfg.borrow()) {
warn!("kunde inte spara config: {e}");
}
if let Some(cb) = ctx2.on_config_changed.borrow().as_ref() {
cb();
}
if let Some(f) = refresh_slot.borrow().as_ref() {
f();
}
}
});
}
let preview = build_preview(ctx, refresh_slot.clone());
let hint = gtk::Label::new(Some(
"Klicka på en skärm för att ställa in just den (utanför = alla).\n\
* = skärmen har egna inställningar. Som standard gäller allt alla skärmar.",
));
hint.add_css_class("dim-label");
hint.set_halign(gtk::Align::Center);
hint.set_justify(gtk::Justification::Center);
let holder = gtk::Box::new(gtk::Orientation::Vertical, 0);
holder.set_vexpand(true);
holder.append(&build_notebook(ctx));
// refresh: uppdatera togglar/preview/reset + bygg om sidorna
{
let ctx2 = ctx.clone();
let holder2 = holder.clone();
let toggles2 = toggles.clone();
let updating2 = updating.clone();
let reset2 = reset_btn.clone();
let refresh: Rc<dyn Fn()> = Rc::new(move || {
let scope = current_scope();
updating2.set(true);
for (value, btn) in toggles2.borrow().iter() {
btn.set_active(*value == scope);
}
updating2.set(false);
reset2.set_visible(scope.is_some());
while let Some(c) = holder2.first_child() {
holder2.remove(&c);
}
holder2.append(&build_notebook(&ctx2));
PREVIEW.with(|p| {
if let Some(da) = p.borrow().as_ref().and_then(|w| w.upgrade()) {
da.queue_draw();
}
});
});
*refresh_slot.borrow_mut() = Some(refresh);
}
root.append(&scope_row);
root.append(&preview);
root.append(&hint);
root.append(&reset_btn);
root.append(&holder);
win.set_child(Some(&root));
OPEN_WINDOW.with(|w| *w.borrow_mut() = Some(win.downgrade()));
win.present();
}
// hindra att Cell-importen städas bort av misstag när sidor ändras
#[allow(unused)]
fn _keep(_: Cell<u8>) {}

View File

@@ -1,12 +1,22 @@
// Config → CSS. All theming (färger, transparens, skuggor, accent,
// storlekar, animationer) genereras här och laddas i en CssProvider som
// byts ut vid live-omladdning.
// byts ut vid live-omladdning. Skärmar med per-skärm-överridar får egna
// CSS-block scopade på dockfönstrets output-klass (o-<connector>).
use gtk4 as gtk;
use gtk4::gdk;
use crate::config::{Config, IndicatorStyle};
/// CSS-klass för en viss skärms dockfönster.
pub fn output_class(connector: &str) -> String {
let safe: String = connector
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
.collect();
format!("o-{safe}")
}
/// "#rrggbb" + alpha → "rgba(r, g, b, a)". Trasig färg → grå.
fn rgba(hex: &str, alpha: f64) -> String {
let h = hex.trim_start_matches('#');
@@ -22,7 +32,9 @@ fn rgba(hex: &str, alpha: f64) -> String {
format!("rgba({r}, {g}, {b}, {alpha:.3})")
}
pub fn build_css(cfg: &Config) -> String {
/// De utseende-styrda reglerna, med valfritt selektor-prefix
/// ("" globalt, "window.o-DP-1 " för en enskild skärm).
fn css_body(cfg: &Config, p: &str) -> String {
let a = &cfg.appearance;
let bg = rgba(&a.background, a.opacity);
let border = rgba(&a.border.color, a.border.opacity);
@@ -52,18 +64,7 @@ pub fn build_css(cfg: &Config) -> String {
format!(
r#"
window.waydock,
window.waydock-sensor,
window.waydock-hotspot {{
background: transparent;
}}
window.waydock-sensor box,
window.waydock-hotspot box {{
background-color: rgba(0, 0, 0, 0.004);
}}
.dock-panel {{
{p}.dock-panel {{
background-color: {bg};
border-radius: {radius}px;
border: {bwidth}px solid {border};
@@ -71,7 +72,7 @@ window.waydock-hotspot box {{
{shadow}
}}
.dock-item {{
{p}.dock-item {{
background: transparent;
border: none;
border-radius: {item_radius}px;
@@ -82,33 +83,28 @@ window.waydock-hotspot box {{
{transition}
}}
.dock-item:hover {{
{p}.dock-item:hover {{
background-color: {accent_soft};
}}
.dock-item:hover image {{
{p}.dock-item:hover image {{
{hover_zoom}
}}
.dock-item image {{
{p}.dock-item image {{
{transition}
}}
.dock-item.urgent {{
{p}.dock-item.urgent {{
background-color: {accent_soft};
animation: waydock-pulse 900ms ease-in-out infinite alternate;
}}
@keyframes waydock-pulse {{
from {{ background-color: transparent; }}
to {{ background-color: {accent_soft}; }}
}}
.indicator-box {{
{p}.indicator-box {{
min-height: {ind_h}px;
}}
.indicator {{
{p}.indicator {{
background-color: {accent};
border-radius: {ind_radius}px;
min-width: {ind_w}px;
@@ -116,11 +112,11 @@ window.waydock-hotspot box {{
margin: 0 1px;
}}
.indicator.inactive {{
{p}.indicator.inactive {{
background-color: {accent_dim};
}}
.group-badge {{
{p}.group-badge {{
background-color: {accent};
color: #ffffff;
border-radius: 8px;
@@ -131,24 +127,25 @@ window.waydock-hotspot box {{
padding: 0 2px;
}}
popover.waydock-menu contents {{
{p}popover.waydock-menu contents {{
background-color: {menu_bg};
border-radius: 10px;
border: 1px solid {border};
}}
popover.waydock-menu modelbutton,
popover.waydock-menu button.flat {{
{p}popover.waydock-menu modelbutton,
{p}popover.waydock-menu button.flat {{
color: #eeeeee;
border-radius: 6px;
padding: 6px 10px;
}}
popover.waydock-menu modelbutton:hover,
popover.waydock-menu button.flat:hover {{
{p}popover.waydock-menu modelbutton:hover,
{p}popover.waydock-menu button.flat:hover {{
background-color: {accent_soft};
}}
"#,
p = p,
bg = bg,
radius = a.corner_radius,
bwidth = a.border.width,
@@ -170,6 +167,35 @@ popover.waydock-menu button.flat:hover {{
)
}
pub fn build_css(cfg: &Config) -> String {
let mut css = String::from(
r#"
window.waydock,
window.waydock-sensor,
window.waydock-hotspot {
background: transparent;
}
window.waydock-sensor box,
window.waydock-hotspot box {
background-color: rgba(0, 0, 0, 0.004);
}
/* utan "to" animeras tillbaka till regelns egen bakgrund (accenten) */
@keyframes waydock-pulse {
from { background-color: transparent; }
}
"#,
);
css += &css_body(cfg, "");
// skärmar med överridar får egna, mer specifika block
for name in cfg.outputs.keys() {
let eff = cfg.for_output(name);
css += &css_body(&eff, &format!("window.{} ", output_class(name)));
}
css
}
/// Ladda (eller byt ut) den globala CSS-providern.
pub fn apply(cfg: &Config, previous: Option<&gtk::CssProvider>) -> gtk::CssProvider {
let display = gdk::Display::default().expect("ingen display");