diff options
| -rw-r--r-- | crates/srdwm/src/main.rs | 93 | ||||
| -rw-r--r-- | docs/DEFAULTS.md | 25 | ||||
| -rw-r--r-- | docs/TODO.md | 33 |
3 files changed, 150 insertions, 1 deletions
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<RefCell<WindowManager>>) { + 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<RefCell<WindowManager>>) { 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<dyn std::error::Error>> { // `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<dyn std::error::Error>> { 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<dyn std::error::Error>> { 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<dyn std::error::Error>> { 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. |