diff options
| author | srdusr <[email protected]> | 2026-07-02 09:26:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2026-07-02 09:26:00 +0200 |
| commit | 4c6d059359537e03f677e83d345f52fae29ae746 (patch) | |
| tree | d880028cee3ecf2c196f76e35cb99e8cd6a5a7c9 /crates | |
| parent | 695d8f5204731376aa60ad6c759d361b903e81dd (diff) | |
| download | srdwm-4c6d059359537e03f677e83d345f52fae29ae746.tar.gz srdwm-4c6d059359537e03f677e83d345f52fae29ae746.zip | |
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.
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/srdwm/src/main.rs | 93 |
1 files changed, 93 insertions, 0 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}'"); |