srdusr
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--crates/srdwm/src/main.rs131
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(&gtk_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");
+ }
+ }
+}