diff options
| -rw-r--r-- | crates/srdwm/src/main.rs | 101 |
1 files changed, 95 insertions, 6 deletions
diff --git a/crates/srdwm/src/main.rs b/crates/srdwm/src/main.rs index 5e5d5c5..d53c76e 100644 --- a/crates/srdwm/src/main.rs +++ b/crates/srdwm/src/main.rs @@ -199,8 +199,37 @@ const GTK_BUTTON_BASE: &str = "headerbar button.titlebutton,\n.solid-csd headerb /// 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 active = if traffic_lights { "traffic_lights" } else { "traditional" }; +/// The look of srdwm's own titlebar, for a client that draws its own. +/// +/// A client-side-decorated application draws its header itself, and no +/// compositor protocol reaches that - GTK never negotiates decoration at +/// all. So an app like Nemo keeps its own header bar whatever srdwm is +/// configured to do, and the desktop ends up with two different-looking +/// titlebars: srdwm's on every ordinary window, the app's own on that one. +/// +/// This is the part that can be closed. The stylesheet gives a GTK header +/// bar the same background, text colour, height and title alignment srdwm +/// draws for itself, so the two read as one style even though two different +/// pieces of software draw them. +/// +/// Deliberately not set: anything that would change what fits in the bar. A +/// GNOME app's header bar is its whole toolbar - search, menus, view +/// switches - so `min-height` can only raise the floor to srdwm's own +/// height, never cap it, and no padding or spacing is touched. +pub(crate) struct TitlebarLook { + pub(crate) traffic_lights: bool, + pub(crate) background: (u8, u8, u8), + pub(crate) foreground: (u8, u8, u8), + pub(crate) height: u32, + pub(crate) title_centered: bool, +} + +fn rgb_css((r, g, b): (u8, u8, u8)) -> String { + format!("rgb({r}, {g}, {b})") +} + +fn gtk_titlebar_css(look: &TitlebarLook) -> String { + let active = if look.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\ @@ -212,6 +241,18 @@ fn gtk_button_css(traffic_lights: bool) -> String { \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(&format!( + "/* srdwm's own titlebar look, for header bars this compositor cannot\n draw itself. Only colour, height and title alignment: padding and\n spacing are left alone, because a header bar is also some apps' whole\n toolbar. */\n\nheaderbar,\nheaderbar.titlebar {{\n background-image: none;\n background-color: {bg};\n color: {fg};\n min-height: {h}px;\n}}\n\nheaderbar .title,\nheaderbar .subtitle {{\n color: {fg};\n}}\n\n", + bg = rgb_css(look.background), + fg = rgb_css(look.foreground), + h = look.height, + )); + // GTK centres a header bar's title by default, so only the off case + // needs saying - and it is said by pinning the title to the start, + // which is what srdwm's own left-aligned titlebar looks like. + if !look.title_centered { + out.push_str("headerbar .title {\n margin-left: 0;\n}\n\n"); + } out.push_str(GTK_BUTTON_BASE); for (name, body) in GTK_BUTTON_STYLES { if name == active { @@ -248,8 +289,17 @@ fn publish_gtk_stylesheet(wm: &Rc<RefCell<WindowManager>>, nested: bool) { return; } 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); + let look = { + let wm = wm.borrow(); + TitlebarLook { + traffic_lights: wm.theme.traffic_light_buttons, + background: wm.theme.titlebar_bg, + foreground: wm.theme.titlebar_fg_focused, + height: srdwm_core::TITLEBAR_HEIGHT, + title_centered: wm.theme.title_centered, + } + }; + let css = gtk_titlebar_css(&look); for version in ["gtk-3.0", "gtk-4.0"] { let dir = std::path::Path::new(&home).join(".config").join(version); if !dir.is_dir() { @@ -1201,6 +1251,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { mod tests { use super::*; + fn look(traffic_lights: bool) -> TitlebarLook { + TitlebarLook { traffic_lights, background: (46, 52, 64), foreground: (236, 239, 244), height: 32, title_centered: true } + } + /// Strips every `/* ... */` block, so what is left is the CSS a GTK /// parser would actually apply. fn uncommented(css: &str) -> String { @@ -1223,7 +1277,7 @@ mod tests { for (traffic_lights, active, inactive) in [(true, "9999px", "-gtk-icontheme"), (false, "-gtk-icontheme", "9999px")] { - let css = gtk_button_css(traffic_lights); + let css = gtk_titlebar_css(&look(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"); @@ -1238,11 +1292,46 @@ mod tests { #[test] fn the_reset_applies_whichever_style_is_configured() { for traffic_lights in [true, false] { - let live = uncommented(>k_button_css(traffic_lights)); + let live = uncommented(>k_titlebar_css(&look(traffic_lights))); assert!(live.contains(".solid-csd headerbar button.titlebutton")); } } + /// The whole point of the titlebar block: a client that draws its own + /// header must be able to end up looking like srdwm's. + #[test] + fn the_stylesheet_carries_srdwms_own_titlebar_colours_and_height() { + let mut l = look(false); + l.background = (10, 20, 30); + l.foreground = (200, 210, 220); + l.height = 44; + let live = uncommented(>k_titlebar_css(&l)); + assert!(live.contains("background-color: rgb(10, 20, 30)")); + assert!(live.contains("color: rgb(200, 210, 220)")); + assert!(live.contains("min-height: 44px")); + } + + /// A header bar is some applications' entire toolbar, so the stylesheet + /// must never cap its height or touch what fits inside it. + #[test] + fn the_titlebar_block_never_caps_a_header_bars_size() { + for centered in [true, false] { + let mut l = look(false); + l.title_centered = centered; + let live = uncommented(>k_titlebar_css(&l)); + // Only the header bar's own rule: the button reset further down + // sets `padding: 0` on a titlebutton, which is correct there. + let start = live.find("headerbar,\nheaderbar.titlebar {").expect("no header bar rule"); + let block = &live[start..start + live[start..].find('}').expect("unterminated rule")]; + // Declarations, not substrings: "min-height" legitimately + // contains "height". + for property in ["height", "max-height", "padding", "font-size", "margin"] { + let set = block.lines().any(|line| line.trim_start().starts_with(&format!("{property}:"))); + assert!(!set, "the header bar rule sets {property}"); + } + } + } + /// srdwm-x is the window manager of the X display it runs on, not a /// client of another compositor, so a set `DISPLAY` must not make it /// look nested and stop it publishing the user's own settings. |