diff options
| author | srdusr <[email protected]> | 2026-07-26 23:28:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2026-07-26 23:28:00 +0200 |
| commit | 48ccac7ecaeff0aaa503804bdf8e033e273b9862 (patch) | |
| tree | 6d5f1296354955a9051355571490adc12ed805e0 /crates | |
| parent | 492aa5b554ddc389bb2f233c60ea503b3dbda990 (diff) | |
| download | srdwm-48ccac7ecaeff0aaa503804bdf8e033e273b9862.tar.gz srdwm-48ccac7ecaeff0aaa503804bdf8e033e273b9862.zip | |
Give a self-decorating app srdwm's own titlebar look, not just its buttons
Reported live, after the button work landed: "I still don't see our custom
overriding decorations, ie on nemo."
Measured what actually happens, in a nested compositor with a deliberately
garish diagnostic rule: srdwm's stylesheet does reach Nemo's window buttons
- the close button took the diagnostic colour. What it did not reach was
the bar those buttons sit in, so Nemo kept its own header bar in its own
GTK theme's colours next to srdwm's titlebars everywhere else. Two
titlebars, two looks, which is what "not our decorations" was describing.
The stylesheet now also carries srdwm's own titlebar background, text
colour, height and title alignment. A client-side-decorated application
draws its header itself and no protocol reaches that - GTK never
negotiates decoration at all - but its stylesheet does, so the two can at
least be told to look the same.
Verified on screen: with the compositor told one colour and the stylesheet
generated for another, Nemo's own header bar sampled at exactly the
stylesheet's colour. In a real session both come from the same theme value,
so they match.
Deliberately conservative about size. A GNOME app's header bar is also 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,
margin, font-size or spacing is set at all. A test asserts the header bar
rule sets none of those, so a later edit cannot quietly start squeezing
other applications' controls.
Also measured, and worth writing down because it bounds what is possible:
Nemo shows one close button and nothing adds minimize or maximize to it --
not `gtk-decoration-layout` in settings.ini, not the gsettings button-layout
the portal serves, and GTK_CSD=0 does not remove its header bar either. A
client that builds its own header decides what goes in it. Style is
reachable; contents are not.
Diffstat (limited to 'crates')
| -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. |