From 5611194ef57cd766223a01932cffa2969a81a983 Mon Sep 17 00:00:00 2001 From: srdusr <99972264+srdusr@users.noreply.github.com> Date: Fri, 17 Jul 2026 21:56:00 +0200 Subject: Write every GTK button style into the stylesheet, all but one commented out The generated file used to hold only the configured style, so it answered "what am I set to" and nothing else. There was no way to see what the other setting looked like without changing the config and restarting, and no way to read the rules in order to borrow from them. Now every style in GTK_BUTTON_STYLES is written out on each generation. The configured one is live; the rest sit below it inside block comments, each labelled with the button_style value that turns it on. The file becomes the menu of what the setting can be, and a place to read or copy real rules from, without any of them applying. The header says plainly that editing this file does nothing, because it is rewritten on every start and every config reload, and points at the two places an edit does survive: button_style in the srdwm config for a whole style, and gtk.css for anything no style covers, since the user's own rules come after the @import and therefore win. A style body containing "*/" would end the comment that hides it early and leave two styles live at once, so a test asserts no body can. The other tests strip every comment and check that the configured style, and only the configured style, survives - which is what a GTK parser would see. Generated the file through this code path and installed it for GTK 3 and GTK 4; backups are at ~/.config/gtk-{3,4}.0/srdwm-buttons.css.bak-20260829-013445. --- crates/srdwm/src/main.rs | 131 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 108 insertions(+), 23 deletions(-) diff --git a/crates/srdwm/src/main.rs b/crates/srdwm/src/main.rs index a0cfc39..6f4cf88 100644 --- a/crates/srdwm/src/main.rs +++ b/crates/srdwm/src/main.rs @@ -155,36 +155,65 @@ const GTK_STYLE_FILE: &str = "srdwm-buttons.css"; /// gives the user's own rules the last word, since later rules win. const GTK_STYLE_IMPORT: &str = "@import url(\"srdwm-buttons.css\");"; +/// The CSS for one button style, without the shared reset in `GTK_BUTTON_BASE`. +/// +/// Every style is generated into the stylesheet on every write. Exactly one is +/// live and the rest are commented out, so the file doubles as the menu of +/// what `button_style` can be: you can see what each one would do, and read +/// the rules rather than guess at them. +const GTK_BUTTON_STYLES: [(&str, &str); 2] = [ + ( + "traditional", + "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\nheaderbar button.titlebutton.close {\n background-image: -gtk-icontheme(\"window-close-symbolic\");\n background-repeat: no-repeat;\n background-position: center;\n background-size: 16px 16px;\n}\n\nheaderbar button.titlebutton.minimize {\n background-image: -gtk-icontheme(\"window-minimize-symbolic\");\n background-repeat: no-repeat;\n background-position: center;\n background-size: 16px 16px;\n}\n\nheaderbar button.titlebutton.maximize {\n background-image: -gtk-icontheme(\"window-maximize-symbolic\");\n background-repeat: no-repeat;\n background-position: center;\n background-size: 16px 16px;\n}\n\nheaderbar 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", + ), + ( + "traffic_lights", + "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", + ), +]; + +/// The reset every style starts from. +/// +/// 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. +const GTK_BUTTON_BASE: &str = "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"; + /// The generated stylesheet body for the configured button style. /// +/// Every style in `GTK_BUTTON_STYLES` is written out. The configured one is +/// live; the others are commented out below it, so the file shows what the +/// alternatives are and what each of them actually does. +/// /// 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"), - ) + let active = if traffic_lights { "traffic_lights" } else { "traditional" }; + let mut out = String::from( + "/* Generated by srdwm from theme.decorations.title_bar.button_style.\n\ + \x20 Rewritten on every start and every config reload, so an edit here does\n\ + \x20 not survive. To change the style, set button_style in your srdwm\n\ + \x20 config. To change something no style covers, write the rule in gtk.css\n\ + \x20 instead: it is yours, srdwm only ever adds the one @import line to it,\n\ + \x20 and rules there come after this import and therefore win.\n\n\ + \x20 Each style below is a complete set of rules. The configured one is\n\ + \x20 live and the rest are commented out, so you can read what any of them\n\ + \x20 would do, or paste one into gtk.css and change it. */\n\n", + ); + out.push_str(GTK_BUTTON_BASE); + for (name, body) in GTK_BUTTON_STYLES { + if name == active { + out.push_str(&format!("/* button_style = \"{name}\" - in use */\n\n{body}\n")); + } else { + // The bodies hold no comments of their own, so wrapping one in a + // block comment cannot terminate early. + out.push_str(&format!("/* button_style = \"{name}\"\n\n{body}*/\n\n")); + } } + out } /// Writes srdwm's own GTK stylesheet and makes sure the user's `gtk.css` @@ -1155,3 +1184,59 @@ fn main() -> Result<(), Box> { log::info!("srdwm shutting down"); Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + /// Strips every `/* ... */` block, so what is left is the CSS a GTK + /// parser would actually apply. + fn uncommented(css: &str) -> String { + let mut out = String::new(); + let mut rest = css; + while let Some(open) = rest.find("/*") { + out.push_str(&rest[..open]); + let after = &rest[open + 2..]; + match after.find("*/") { + Some(close) => rest = &after[close + 2..], + None => return out, + } + } + out.push_str(rest); + out + } + + #[test] + fn every_style_is_written_and_only_the_configured_one_is_live() { + for (traffic_lights, active, inactive) in + [(true, "9999px", "-gtk-icontheme"), (false, "-gtk-icontheme", "9999px")] + { + let css = gtk_button_css(traffic_lights); + // Both styles are in the file, so it reads as the menu of what + // button_style can be... + assert!(css.contains("9999px"), "traffic_lights rules missing"); + assert!(css.contains("-gtk-icontheme"), "traditional rules missing"); + // ...but only the configured one is outside a comment. + let live = uncommented(&css); + assert!(live.contains(active), "configured style is commented out"); + assert!(!live.contains(inactive), "the other style is live too"); + } + } + + #[test] + fn the_reset_applies_whichever_style_is_configured() { + for traffic_lights in [true, false] { + let live = uncommented(>k_button_css(traffic_lights)); + assert!(live.contains(".solid-csd headerbar button.titlebutton")); + } + } + + /// A `*/` inside a commented-out style would end the comment early and + /// leave that style's rules live alongside the configured one. + #[test] + fn no_style_body_can_terminate_the_comment_that_hides_it() { + for (_, body) in GTK_BUTTON_STYLES { + assert!(!body.contains("*/"), "a style body closes a comment"); + } + } +} -- cgit v1.2.3