srdusr
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--crates/srdwm/src/main.rs101
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(&gtk_button_css(traffic_lights));
+ let live = uncommented(&gtk_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(&gtk_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(&gtk_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.