srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/srdwm/src/main.rs68
-rw-r--r--crates/wayland/src/protocols.rs1
-rw-r--r--crates/wayland/src/protocols/kde_decoration.rs74
-rw-r--r--crates/wayland/src/state/mod.rs6
-rw-r--r--crates/wayland/src/udev/platform.rs8
-rw-r--r--crates/wayland/src/winit/connect.rs8
6 files changed, 153 insertions, 12 deletions
diff --git a/crates/srdwm/src/main.rs b/crates/srdwm/src/main.rs
index d53c76e..4a68006 100644
--- a/crates/srdwm/src/main.rs
+++ b/crates/srdwm/src/main.rs
@@ -164,23 +164,44 @@ 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`.
+/// The selectors a toolkit puts its window buttons under.
///
-/// 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.
+/// GTK3 and GTK4 disagree, and a stylesheet that names only one silently
+/// does nothing for half the applications on the desktop. Measured, not
+/// assumed: a diagnostic rule that turned the buttons a flat colour
+/// reached Nemo (GTK3) through `headerbar button.titlebutton` and
+/// gnome-calculator (GTK4) through `windowcontrols button`, and neither
+/// selector reached the other. `headerbar` itself works in both, so the
+/// titlebar block above needs no such split.
+///
+/// Both are always written. A toolkit ignores a selector whose node it
+/// does not have, so naming both costs a few lines and covers both.
+const GTK_BUTTON_SELECTORS: [&str; 2] = ["headerbar button.titlebutton", "windowcontrols button"];
+
+/// The CSS for one button style, as a template over `{sel}` - one of
+/// `GTK_BUTTON_SELECTORS`.
+///
+/// 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",
+ "{sel} {\n min-width: 24px;\n min-height: 24px;\n border-radius: 4px;\n}\n\n{sel} > image {\n opacity: 1;\n}\n\n{sel}.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\n{sel}.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\n{sel}.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\n{sel}:hover {\n background-color: alpha(currentColor, 0.14);\n}\n\n{sel}: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",
+ "{sel} {\n min-width: 13px;\n min-height: 13px;\n border-radius: 9999px;\n}\n\n{sel} > image {\n opacity: 0;\n}\n\n{sel}.close {\n background-image: radial-gradient(circle at 32% 28%, #ffb3ad 0%, #ff5f57 42%, #dd4b43 100%);\n}\n\n{sel}.minimize {\n background-image: radial-gradient(circle at 32% 28%, #ffe2a8 0%, #ffbd2e 42%, #dba024 100%);\n}\n\n{sel}.maximize {\n background-image: radial-gradient(circle at 32% 28%, #9df0a6 0%, #28c840 42%, #1f9f34 100%);\n}\n\n{sel}:hover {\n filter: brightness(1.12);\n}\n\n{sel}:active {\n filter: brightness(0.9);\n}\n",
),
];
+/// One style's rules, written out once for every selector a toolkit might
+/// use.
+fn button_style_rules(template: &str) -> String {
+ GTK_BUTTON_SELECTORS.iter().map(|sel| template.replace("{sel}", sel)).collect()
+}
+
/// The reset every style starts from.
///
/// WhiteSur, and themes like it, paint the control as the button's own
@@ -188,7 +209,14 @@ const GTK_BUTTON_STYLES: [(&str, &str); 2] = [
/// `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";
+fn gtk_button_base() -> String {
+ let mut selectors: Vec<String> = Vec::new();
+ for sel in GTK_BUTTON_SELECTORS {
+ selectors.push(sel.to_string());
+ selectors.push(format!(".solid-csd {sel}"));
+ }
+ format!("{} {{\n background-image: none;\n background-color: transparent;\n border: none;\n box-shadow: none;\n padding: 0;\n}}\n\n", selectors.join(",\n"))
+}
/// The generated stylesheet body for the configured button style.
///
@@ -253,8 +281,9 @@ fn gtk_titlebar_css(look: &TitlebarLook) -> String {
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 {
+ out.push_str(&gtk_button_base());
+ for (name, template) in GTK_BUTTON_STYLES {
+ let body = button_style_rules(template);
if name == active {
out.push_str(&format!("/* button_style = \"{name}\" - in use */\n\n{body}\n"));
} else {
@@ -1294,6 +1323,7 @@ mod tests {
for traffic_lights in [true, false] {
let live = uncommented(&gtk_titlebar_css(&look(traffic_lights)));
assert!(live.contains(".solid-csd headerbar button.titlebutton"));
+ assert!(live.contains(".solid-csd windowcontrols button"));
}
}
@@ -1332,6 +1362,20 @@ mod tests {
}
}
+ /// GTK3 and GTK4 put their window buttons under different selectors,
+ /// and a stylesheet naming only one silently does nothing for half the
+ /// applications on the desktop.
+ #[test]
+ fn every_style_is_written_for_both_toolkits() {
+ for traffic_lights in [true, false] {
+ let live = uncommented(&gtk_titlebar_css(&look(traffic_lights)));
+ for sel in GTK_BUTTON_SELECTORS {
+ assert!(live.contains(&format!("{sel}.close")), "no close rule for {sel}");
+ assert!(live.contains(&format!("{sel}:hover")), "no hover rule for {sel}");
+ }
+ }
+ }
+
/// 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.
@@ -1346,8 +1390,8 @@ mod tests {
/// 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");
+ for (_, template) in GTK_BUTTON_STYLES {
+ assert!(!template.contains("*/"), "a style body closes a comment");
}
}
}
diff --git a/crates/wayland/src/protocols.rs b/crates/wayland/src/protocols.rs
index 664e889..bd8307a 100644
--- a/crates/wayland/src/protocols.rs
+++ b/crates/wayland/src/protocols.rs
@@ -23,6 +23,7 @@ mod misc;
mod seat;
mod selection;
mod xdg_activation;
+mod kde_decoration;
mod xdg_decoration;
mod xdg_shell;
diff --git a/crates/wayland/src/protocols/kde_decoration.rs b/crates/wayland/src/protocols/kde_decoration.rs
new file mode 100644
index 0000000..2448a39
--- /dev/null
+++ b/crates/wayland/src/protocols/kde_decoration.rs
@@ -0,0 +1,74 @@
+//! `org_kde_kwin_server_decoration`: KDE's older decoration protocol, and
+//! the only decoration protocol GTK actually speaks.
+//!
+//! GTK has never implemented `xdg-decoration`. It does implement this one:
+//! `org_kde_kwin_server_decoration_manager` is present in libgtk-4 on this
+//! machine (confirmed by reading the library's own symbol strings), and it
+//! is how a KDE session gets GTK applications to stop drawing their own
+//! frame. A compositor that advertises only `xdg-decoration` is invisible
+//! to those clients, which is why "set the decoration once and every
+//! application follows" stopped at srdwm's own titlebars.
+//!
+//! Both protocols are advertised, and both answer with the same policy
+//! (`theme.default_decorated`, `theme.force_server_side`), so a client is
+//! told the same thing whichever one it asks through.
+//!
+//! The protocol's own warning applies here: a client may ignore the mode
+//! the compositor suggests and ask for its own. That is honoured the same
+//! way `xdg_decoration.rs` honours it, and for the same reason - a client
+//! that draws its own titlebar regardless (Firefox with its system-titlebar
+//! setting off) would otherwise get srdwm's row on top of its own.
+
+use smithay::reexports::wayland_protocols_misc::server_decoration::server::org_kde_kwin_server_decoration::{
+ Mode, OrgKdeKwinServerDecoration,
+};
+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
+use smithay::reexports::wayland_server::WEnum;
+use smithay::wayland::shell::kde::decoration::{KdeDecorationHandler, KdeDecorationState};
+
+use crate::state::CompState;
+
+impl KdeDecorationHandler for CompState {
+ fn kde_decoration_state(&self) -> &KdeDecorationState {
+ &self.kde_decoration_state
+ }
+
+ /// Tells a client, the moment it asks, which mode this compositor
+ /// wants - the same answer `XdgDecorationHandler::new_decoration`
+ /// gives through the other protocol.
+ ///
+ /// The manager's own default mode (set once, at startup) is what a
+ /// client sees before it creates a decoration object at all; this is
+ /// what it sees afterward, and it has to agree, or a client that reads
+ /// both ends up with two different answers.
+ fn new_decoration(&mut self, surface: &WlSurface, decoration: &OrgKdeKwinServerDecoration) {
+ let server = self.wm.borrow().theme.default_decorated;
+ decoration.mode(if server { Mode::Server } else { Mode::Client });
+ self.set_decorated_from_mode(surface, server);
+ }
+
+ /// Honours what the client asked for, unless `force_server_side` says
+ /// otherwise - identical policy to the xdg-decoration path, and the
+ /// mode is echoed back either way because the protocol requires the
+ /// compositor to confirm what it decided.
+ fn request_mode(&mut self, surface: &WlSurface, decoration: &OrgKdeKwinServerDecoration, mode: WEnum<Mode>) {
+ let WEnum::Value(requested) = mode else { return };
+ let forced = self.wm.borrow().theme.force_server_side;
+ // `Mode::None` means no decoration at all, which for srdwm's
+ // purposes is a client saying it wants nothing drawn around it --
+ // treated as client-side, the same as `Mode::Client`, rather than
+ // as a request for a titlebar.
+ let server = forced || requested == Mode::Server;
+ let granted = if server { Mode::Server } else { requested };
+ decoration.mode(granted);
+ self.set_decorated_from_mode(surface, server);
+ }
+
+ /// The client is going away, or has dropped its decoration object.
+ /// Nothing to undo: `remove_window` already clears everything keyed on
+ /// this surface, and a surface with no decoration object keeps
+ /// whatever mode it last negotiated, exactly as before.
+ fn release(&mut self, _decoration: &OrgKdeKwinServerDecoration, _surface: &WlSurface) {}
+}
+
+smithay::delegate_kde_decoration!(CompState);
diff --git a/crates/wayland/src/state/mod.rs b/crates/wayland/src/state/mod.rs
index 64990c3..6382ee3 100644
--- a/crates/wayland/src/state/mod.rs
+++ b/crates/wayland/src/state/mod.rs
@@ -173,6 +173,12 @@ pub(crate) struct CompState {
pub(crate) compositor_state: CompositorState,
pub(crate) xdg_shell_state: XdgShellState,
pub(crate) _xdg_decoration_state: XdgDecorationState,
+ /// KDE's older decoration protocol - see `protocols/kde_decoration.rs`
+ /// for why it is advertised alongside `xdg-decoration` rather than
+ /// instead of it. Read by `KdeDecorationHandler`, so unlike the
+ /// xdg-decoration state above this one is not an underscore-prefixed
+ /// keep-alive.
+ pub(crate) kde_decoration_state: smithay::wayland::shell::kde::decoration::KdeDecorationState,
pub(crate) shm_state: ShmState,
/// `zwp_linux_dmabuf_v1` - see `protocols.rs`'s `DmabufHandler` impl.
/// Without this global, no client can hand the compositor a GPU buffer
diff --git a/crates/wayland/src/udev/platform.rs b/crates/wayland/src/udev/platform.rs
index d3c091c..45def0a 100644
--- a/crates/wayland/src/udev/platform.rs
+++ b/crates/wayland/src/udev/platform.rs
@@ -178,6 +178,14 @@ impl UdevPlatform {
compositor_state,
xdg_shell_state,
_xdg_decoration_state: xdg_decoration_state,
+ // Default mode advertised before a client creates a decoration
+ // object at all - see `protocols/kde_decoration.rs`. Server,
+ // so a GTK application that reads only this protocol is told
+ // srdwm decorates, which is the whole point of advertising it.
+ kde_decoration_state: smithay::wayland::shell::kde::decoration::KdeDecorationState::new::<CompState>(
+ &display_handle,
+ smithay::reexports::wayland_protocols_misc::server_decoration::server::org_kde_kwin_server_decoration_manager::Mode::Server,
+ ),
shm_state,
dmabuf_state,
xdg_activation_state: XdgActivationState::new::<CompState>(&display_handle),
diff --git a/crates/wayland/src/winit/connect.rs b/crates/wayland/src/winit/connect.rs
index 1f2f876..e320cb8 100644
--- a/crates/wayland/src/winit/connect.rs
+++ b/crates/wayland/src/winit/connect.rs
@@ -91,6 +91,14 @@ impl WaylandPlatform {
compositor_state,
xdg_shell_state,
_xdg_decoration_state: xdg_decoration_state,
+ // Default mode advertised before a client creates a decoration
+ // object at all - see `protocols/kde_decoration.rs`. Server,
+ // so a GTK application that reads only this protocol is told
+ // srdwm decorates, which is the whole point of advertising it.
+ kde_decoration_state: smithay::wayland::shell::kde::decoration::KdeDecorationState::new::<CompState>(
+ &dh,
+ smithay::reexports::wayland_protocols_misc::server_decoration::server::org_kde_kwin_server_decoration_manager::Mode::Server,
+ ),
shm_state,
dmabuf_state,
xdg_activation_state: XdgActivationState::new::<CompState>(&dh),