diff options
| -rw-r--r-- | crates/srdwm/src/main.rs | 131 |
1 files 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<dyn std::error::Error>> { 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"); + } + } +} |