srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/srdwm/src/main.rs93
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}'");