From 4c6d059359537e03f677e83d345f52fae29ae746 Mon Sep 17 00:00:00 2001 From: srdusr <99972264+srdusr@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:26:00 +0200 Subject: Generate the GTK button stylesheet from button_style Asked for after the previous entry declined to write GTK CSS. The objection was to clobbering a file full of hand-written work, not to the feature, and splitting ownership solves both. srdwm owns srdwm-buttons.css, generated from theme.decorations.title_bar.button_style and rewritten on every start and config reload, and adds exactly one @import line to the user's gtk.css if it is missing. Nothing else in that file is ever touched. The import goes first because CSS only permits @import ahead of other rules, which also leaves the user's own rules last and therefore able to override the generated style. The generated CSS sets a background per button rather than un-hiding a child image, for the reason the earlier hand-written attempt found by screenshot: WhiteSur paints the control as the button's own background-image from a compiled gresource, so clearing that background leaves a blank button rather than revealing a glyph. Verified end to end on a real GTK app, both directions, through the generated file: traffic_lights gives Nemo coloured dots, traditional gives a dash, a square and an X, each matching srdwm's own titlebar directly above it. Also verified the import is added once and not duplicated, that switching style rewrites only the generated file, and that a home without GTK config directories has nothing written to it. 527 tests pass, clippy clean. --- crates/srdwm/src/main.rs | 93 ++++++++++++++++++++++++++++++++++++++++++++++++ docs/DEFAULTS.md | 25 ++++++++++++- docs/TODO.md | 33 +++++++++++++++++ 3 files changed, 150 insertions(+), 1 deletion(-) diff --git a/crates/srdwm/src/main.rs b/crates/srdwm/src/main.rs index 3f4fe34..7670b99 100644 --- a/crates/srdwm/src/main.rs +++ b/crates/srdwm/src/main.rs @@ -126,6 +126,95 @@ const CONFIG_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_secs /// not an error worth interrupting startup for. The compositor's own /// titlebars are already correct either way; this only brings the /// self-decorating clients into line. +/// The stylesheet srdwm owns and rewrites, named so it is obvious in a +/// directory listing which file is generated and which is the user's. +const GTK_STYLE_FILE: &str = "srdwm-buttons.css"; + +/// The one line srdwm adds to the user's own `gtk.css`. First in the file, +/// because CSS `@import` is only valid before other rules - which also +/// gives the user's own rules the last word, since later rules win. +const GTK_STYLE_IMPORT: &str = "@import url(\"srdwm-buttons.css\");"; + +/// The generated stylesheet body for the configured button style. +/// +/// Only `button_style` matters here. Which *side* the buttons sit on is not +/// a CSS question - that is the desktop's button-layout preference, which +/// `publish_gtk_button_layout` handles. +fn gtk_button_css(traffic_lights: bool) -> String { + let header = "/* Generated by srdwm. Do not edit: rewritten on every start and every\n config reload, from theme.decorations.title_bar.button_style.\n\n Imported from gtk.css, which is yours - srdwm only ever adds the one\n @import line to it and never touches anything else in that file. Rules\n you write there come after this import and therefore win. */\n\n"; + // WhiteSur, and themes like it, paint the control as the button's own + // `background-image` from a compiled gresource rather than as a child + // `image` widget. Clearing that background is what leaves a button with + // nothing drawn in it at all, so each style sets a background of its + // own rather than relying on un-hiding something underneath. + let base = "headerbar button.titlebutton,\n.solid-csd headerbar button.titlebutton {\n background-image: none;\n background-color: transparent;\n border: none;\n box-shadow: none;\n padding: 0;\n}\n\n"; + if traffic_lights { + format!( + "{header}{base}headerbar button.titlebutton {{\n min-width: 13px;\n min-height: 13px;\n border-radius: 9999px;\n}}\n\nheaderbar button.titlebutton > image {{\n opacity: 0;\n}}\n\nheaderbar button.titlebutton.close {{\n background-image: radial-gradient(circle at 32% 28%, #ffb3ad 0%, #ff5f57 42%, #dd4b43 100%);\n}}\n\nheaderbar button.titlebutton.minimize {{\n background-image: radial-gradient(circle at 32% 28%, #ffe2a8 0%, #ffbd2e 42%, #dba024 100%);\n}}\n\nheaderbar button.titlebutton.maximize {{\n background-image: radial-gradient(circle at 32% 28%, #9df0a6 0%, #28c840 42%, #1f9f34 100%);\n}}\n\nheaderbar button.titlebutton:hover {{\n filter: brightness(1.12);\n}}\n\nheaderbar button.titlebutton:active {{\n filter: brightness(0.9);\n}}\n" + ) + } else { + let icon = |class: &str, name: &str| { + format!( + "headerbar button.titlebutton.{class} {{\n background-image: -gtk-icontheme(\"{name}\");\n background-repeat: no-repeat;\n background-position: center;\n background-size: 16px 16px;\n}}\n\n" + ) + }; + format!( + "{header}{base}headerbar button.titlebutton {{\n min-width: 24px;\n min-height: 24px;\n border-radius: 4px;\n}}\n\nheaderbar button.titlebutton > image {{\n opacity: 1;\n}}\n\n{}{}{}headerbar button.titlebutton:hover {{\n background-color: alpha(currentColor, 0.14);\n}}\n\nheaderbar button.titlebutton:active {{\n background-color: alpha(currentColor, 0.22);\n}}\n", + icon("close", "window-close-symbolic"), + icon("minimize", "window-minimize-symbolic"), + icon("maximize", "window-maximize-symbolic"), + ) + } +} + +/// Writes srdwm's own GTK stylesheet and makes sure the user's `gtk.css` +/// imports it, for both GTK 3 and GTK 4. +/// +/// A GTK app draws its own window buttons from its GTK theme, and no +/// compositor protocol reaches those (GTK never negotiates decoration at +/// all). A user stylesheet does, because it loads after the theme - so +/// this is the only channel by which `button_style` can govern a +/// self-decorating app, and without it "set the style once" stops at +/// srdwm's own titlebars. +/// +/// srdwm writes only its own file. The user's `gtk.css` gets one `@import` +/// line added if it is missing, and is otherwise never touched: it is +/// theirs, it routinely holds hand-written work, and silently rewriting it +/// would destroy customisation this code cannot understand. Their rules +/// come after the import and therefore still win. +/// +/// Best-effort: a missing directory or an unwritable file is logged at +/// debug and skipped. srdwm's own titlebars are already correct regardless. +fn publish_gtk_stylesheet(wm: &Rc>) { + let Ok(home) = std::env::var("HOME") else { return }; + let traffic_lights = wm.borrow().theme.traffic_light_buttons; + let css = gtk_button_css(traffic_lights); + for version in ["gtk-3.0", "gtk-4.0"] { + let dir = std::path::Path::new(&home).join(".config").join(version); + if !dir.is_dir() { + continue; + } + if let Err(e) = std::fs::write(dir.join(GTK_STYLE_FILE), &css) { + log::debug!("couldn't write {version}/{GTK_STYLE_FILE} ({e}); GTK keeps its own button style"); + continue; + } + let user_css = dir.join("gtk.css"); + let existing = std::fs::read_to_string(&user_css).unwrap_or_default(); + if existing.contains(GTK_STYLE_IMPORT) { + continue; + } + // Prepended, not appended: `@import` is only valid before any other + // rule, and putting it first is also what leaves the user's own + // rules able to override these. + let merged = format!("{GTK_STYLE_IMPORT}\n\n{existing}"); + if let Err(e) = std::fs::write(&user_css, merged) { + log::debug!("couldn't add the srdwm import to {version}/gtk.css ({e})"); + } else { + log::info!("added {GTK_STYLE_IMPORT} to {version}/gtk.css"); + } + } +} + fn publish_gtk_button_layout(wm: &Rc>) { let layout = if wm.borrow().theme.buttons_left { "close,minimize,maximize:" } else { ":minimize,maximize,close" }; match std::process::Command::new("gsettings") @@ -758,6 +847,7 @@ fn main() -> Result<(), Box> { // `button_side` into the theme - publishing before it would broadcast // the built-in default rather than the user's choice. publish_gtk_button_layout(&wm); + publish_gtk_stylesheet(&wm); apply_default_layout(&engine, &wm); let running = engine.running_flag(); // `general.config_reload_on_write` - on by default. A programmable @@ -868,6 +958,7 @@ fn main() -> Result<(), Box> { apply_general_settings(&engine, &wm); publish_keybindings(&engine, &wm); publish_gtk_button_layout(&wm); + publish_gtk_stylesheet(&wm); // After `apply_general_settings`, which rebuilds the // theme from the config file - see // `WindowManager::live_settings` for why a hand-made @@ -892,6 +983,7 @@ fn main() -> Result<(), Box> { apply_general_settings(&engine, &wm); publish_keybindings(&engine, &wm); publish_gtk_button_layout(&wm); + publish_gtk_stylesheet(&wm); srdwm_platform::replay_live_settings(&wm); // After the reload, so a handler edited in the config since // startup is the one that runs. @@ -911,6 +1003,7 @@ fn main() -> Result<(), Box> { apply_general_settings(&engine, &wm); publish_keybindings(&engine, &wm); publish_gtk_button_layout(&wm); + publish_gtk_stylesheet(&wm); srdwm_platform::replay_live_settings(&wm); } else if !engine.dispatch_keybinding(&combo) { log::debug!("no binding for '{combo}'"); diff --git a/docs/DEFAULTS.md b/docs/DEFAULTS.md index 217075b..014c6f5 100644 --- a/docs/DEFAULTS.md +++ b/docs/DEFAULTS.md @@ -1052,7 +1052,30 @@ setting: buttons moved from x=25/49/73 (left) to x=817/841/865 (right), landing in the same place and the same order as srdwm's own titlebar buttons directly above them. -### Button style: a user CSS override +### Button style: srdwm generates the CSS + +srdwm writes `~/.config/gtk-3.0/srdwm-buttons.css` (and the GTK 4 copy) from +`theme.decorations.title_bar.button_style`, and rewrites it on every start +and every config reload. It adds one line to your own `gtk.css`: + +```css +@import url("srdwm-buttons.css"); +``` + +That line is added once if missing and nothing else in the file is ever +touched. The import sits first because CSS only allows `@import` before +other rules - which also means your own rules come after it and win, so +anything you write in `gtk.css` still overrides the generated style. + +Verified end to end on a real GTK app, both ways: with `button_style = +"traffic_lights"` Nemo's own header draws coloured dots, and with +`"traditional"` it draws a dash, a square and an X - in both cases matching +srdwm's own titlebar directly above it. + +srdwm writes only its own file, and skips a GTK version whose config +directory does not exist rather than creating one. + +### Doing it by hand instead srdwm's `button_style` draws srdwm's own titlebar. A GTK app draws its own buttons from its GTK theme, and no compositor setting reaches those - but diff --git a/docs/TODO.md b/docs/TODO.md index 74eed81..18e551a 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,5 +1,38 @@ # TODO / planned features - master checklist +## srdwm now generates the GTK button stylesheet too (2026-08-28) + +The previous entry recorded a deliberate decision *not* to write GTK CSS, +because the user's `gtk.css` holds hand-written work a compositor cannot +safely rewrite. Asked to build it anyway, which is reasonable - the +objection was to clobbering their file, not to the feature. + +The split solves both: srdwm owns `srdwm-buttons.css`, generated from +`button_style` and rewritten on every start and reload, and adds exactly one +`@import` line to `gtk.css` if it is missing. Nothing else in that file is +ever touched. The import is first because CSS only permits `@import` ahead +of other rules, which conveniently also leaves the user's own rules last and +therefore winning. + +The generated CSS sets a background per button rather than un-hiding a child +`image`, for the reason the previous attempt found the hard way: WhiteSur +paints the control as the button's own `background-image` from a compiled +gresource, so clearing the background leaves a blank button. + +Verified end to end on a real GTK app, both directions, through the +generated file rather than a hand-written one: `traffic_lights` gives Nemo +coloured dots, `traditional` gives it a dash, a square and an X, each time +matching srdwm's own titlebar directly above. Also verified that the import +is added once and not duplicated on a second run, that switching the style +rewrites only the generated file, and that a home directory without GTK +config directories has nothing written to it at all. + +Left in place: `gtk-traffic-lights.css`, the user's original hand-written +override, kept as reference. `gtk.css` is now the import plus a comment +saying what owns what. + +277 core tests, 527 total, clippy clean. + ## Firefox and Nemo's traffic lights: the compositor was never drawing them (2026-08-28) Reported as still using traffic lights after the button *side* was fixed. -- cgit v1.2.3