Skip to content

Commit

Permalink
gtk(wayland): implement server-side decorations
Browse files Browse the repository at this point in the history
  • Loading branch information
pluiedev committed Jan 8, 2025
1 parent 7d2b53a commit 7eaed4c
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 38 deletions.
82 changes: 46 additions & 36 deletions src/apprt/gtk/Window.zig
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,6 @@ pub fn init(self: *Window, app: *App) !void {
if (app.config.@"gtk-titlebar") {
const header = HeaderBar.init(self);

// If we are not decorated then we hide the titlebar.
header.setVisible(app.config.@"window-decoration");

{
const btn = c.gtk_menu_button_new();
c.gtk_widget_set_tooltip_text(btn, "Main Menu");
Expand Down Expand Up @@ -212,11 +209,6 @@ pub fn init(self: *Window, app: *App) !void {

_ = c.g_signal_connect_data(gtk_window, "notify::decorated", c.G_CALLBACK(&gtkWindowNotifyDecorated), self, null, c.G_CONNECT_DEFAULT);

// If we are disabling decorations then disable them right away.
if (!app.config.@"window-decoration") {
c.gtk_window_set_decorated(gtk_window, 0);
}

// If Adwaita is enabled and is older than 1.4.0 we don't have the tab overview and so we
// need to stick the headerbar into the content box.
if (!adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) {
Expand Down Expand Up @@ -390,14 +382,40 @@ pub fn init(self: *Window, app: *App) !void {
/// TODO: Many of the initial style settings in `create` could possibly be made
/// reactive by moving them here.
pub fn syncAppearance(self: *Window, config: *const configpkg.Config) !void {
if (config.@"background-opacity" < 1) {
c.gtk_widget_remove_css_class(@ptrCast(self.window), "background");
} else {
c.gtk_widget_add_css_class(@ptrCast(self.window), "background");
}

// Perform protocol-specific config updates
try self.protocol.onConfigUpdate(config);

toggleCssClass(
@ptrCast(self.window),
"background",
config.@"background-opacity" >= 1,
);

// If we are disabling CSDs then disable them right away.
const csd_enabled = self.protocol.clientSideDecorationEnabled();
c.gtk_window_set_decorated(self.window, @intFromBool(csd_enabled));

// If we are not decorated then we hide the titlebar.
if (self.header) |header| {
header.setVisible(config.@"gtk-titlebar" and csd_enabled);
}

// Disable the title buttons (close, maximize, minimize, ...)
// *inside* the tab overview if CSDs are disabled.
// We do spare the search button, though.
// (...Why the heck is there a header bar inside the tab overview to begin with??)
if (self.tab_overview) |tab_overview| {
c.adw_tab_overview_set_show_start_title_buttons(@ptrCast(tab_overview), @intFromBool(csd_enabled));
c.adw_tab_overview_set_show_end_title_buttons(@ptrCast(tab_overview), @intFromBool(csd_enabled));
}
}

fn toggleCssClass(widget: *c.GtkWidget, class: [:0]const u8, v: bool) void {
if (v) {
c.gtk_widget_add_css_class(widget, class);
} else {
c.gtk_widget_remove_css_class(widget, class);
}
}

/// Sets up the GTK actions for the window scope. Actions are how GTK handles
Expand Down Expand Up @@ -526,17 +544,12 @@ pub fn toggleFullscreen(self: *Window) void {

/// Toggle the window decorations for this window.
pub fn toggleWindowDecorations(self: *Window) void {
const old_decorated = c.gtk_window_get_decorated(self.window) == 1;
const new_decorated = !old_decorated;
c.gtk_window_set_decorated(self.window, @intFromBool(new_decorated));

// If we have a titlebar, then we also show/hide it depending on the
// decorated state. GTK tends to consider the titlebar part of the frame
// and hides it with decorations, but libadwaita doesn't. This makes it
// explicit.
if (self.header) |headerbar| {
headerbar.setVisible(new_decorated);
}
self.app.config.@"window-decoration" = switch (self.app.config.@"window-decoration") {
.true => .server,
.server => .false,
.false => .true,
};
self.syncAppearance(&self.app.config) catch {};
}

/// Grabs focus on the currently selected tab.
Expand Down Expand Up @@ -576,17 +589,14 @@ fn gtkWindowNotifyDecorated(
_: *c.GParamSpec,
_: ?*anyopaque,
) callconv(.C) void {
if (c.gtk_window_get_decorated(@ptrCast(object)) == 1) {
c.gtk_widget_remove_css_class(@ptrCast(object), "ssd");
c.gtk_widget_remove_css_class(@ptrCast(object), "no-border-radius");
} else {
// Fix any artifacting that may occur in window corners. The .ssd CSS
// class is defined in the GtkWindow documentation:
// https://docs.gtk.org/gtk4/class.Window.html#css-nodes. A definition
// for .ssd is provided by GTK and Adwaita.
c.gtk_widget_add_css_class(@ptrCast(object), "ssd");
c.gtk_widget_add_css_class(@ptrCast(object), "no-border-radius");
}
const is_decorated = c.gtk_window_get_decorated(@ptrCast(object)) == 1;

// Fix any artifacting that may occur in window corners. The .ssd CSS
// class is defined in the GtkWindow documentation:
// https://docs.gtk.org/gtk4/class.Window.html#css-nodes. A definition
// for .ssd is provided by GTK and Adwaita.
toggleCssClass(@ptrCast(object), "ssd", !is_decorated);
toggleCssClass(@ptrCast(object), "no-border-radius", !is_decorated);
}

// Note: we MUST NOT use the GtkButton parameter because gtkActionNewTab
Expand Down
13 changes: 13 additions & 0 deletions src/apprt/gtk/protocol.zig
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,13 @@ pub const Surface = struct {
pub const DerivedConfig = struct {
blur: Config.BackgroundBlur,
adw_enabled: bool,
window_decoration: Config.WindowDecoration,

pub fn init(config: *const Config) DerivedConfig {
return .{
.blur = config.@"background-blur-radius",
.adw_enabled = adwaita.enabled(config),
.window_decoration = config.@"window-decoration",
};
}
};
Expand All @@ -130,6 +132,17 @@ pub const Surface = struct {
}
}

pub fn clientSideDecorationEnabled(self: Surface) bool {
return switch (self.inner) {
// We can't do SSD anyway if the compositor doesn't support it
.wayland => |wl| if (comptime build_options.x11)
self.derived_config.window_decoration == .true or wl.decoration == null
else
unreachable,
.x11, .none => self.derived_config.window_decoration != .false,
};
}

pub fn onConfigUpdate(self: *Surface, config: *const Config) !void {
self.derived_config = DerivedConfig.init(config);

Expand Down
32 changes: 31 additions & 1 deletion src/apprt/gtk/protocol/wayland.zig
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ const log = std.log.scoped(.gtk_wayland);
/// Wayland state that contains application-wide Wayland objects (e.g. wl_display).
pub const App = struct {
display: *wl.Display,

blur_manager: ?*org.KdeKwinBlurManager = null,
// FIXME: replace with `zxdg_decoration_v1` once GTK merges https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398
decoration_manager: ?*org.KdeKwinServerDecorationManager = null,

pub fn init(common: *protocol.App) !void {
// Check if we're actually on Wayland
Expand Down Expand Up @@ -45,6 +48,7 @@ pub const Surface = struct {

/// A token that, when present, indicates that the window is blurred.
blur_token: ?*org.KdeKwinBlur = null,
decoration: ?*org.KdeKwinServerDecoration = null,

pub fn init(common: *protocol.Surface) void {
const surface = c.gtk_native_get_surface(@ptrCast(common.gtk_window)) orelse return;
Expand All @@ -56,21 +60,31 @@ pub const Surface = struct {
) == 0)
return;

const self: Surface = .{
var self: Surface = .{
.common = common,
.app = &common.app.inner.wayland,
.surface = @ptrCast(c.gdk_wayland_surface_get_wl_surface(surface) orelse return),
};

if (self.app.decoration_manager) |mgr| {
if (mgr.create(self.surface)) |deco| {
self.decoration = deco;
} else |err| {
log.warn("could not create decoration object={}", .{err});
}
}

common.inner = .{ .wayland = self };
}

pub fn deinit(self: Surface) void {
if (self.blur_token) |blur| blur.release();
if (self.decoration) |deco| deco.release();
}

pub fn onConfigUpdate(self: *Surface) !void {
try self.updateBlur();
try self.updateDecoration();
}

fn updateBlur(self: *Surface) !void {
Expand Down Expand Up @@ -98,6 +112,18 @@ pub const Surface = struct {
}
}
}

fn updateDecoration(self: *Surface) !void {
if (self.decoration) |deco| {
const mode: org.KdeKwinServerDecoration.Mode = switch (self.common.derived_config.window_decoration) {
.true => .Client,
.server => .Server,
.false => .None,
};

deco.requestMode(@intCast(@intFromEnum(mode)));
}
}
};

fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, state: *App) void {
Expand All @@ -108,6 +134,10 @@ fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, state: *Ap
state.blur_manager = iface;
return;
}
if (bindInterface(org.KdeKwinServerDecorationManager, registry, global, 1)) |iface| {
state.decoration_manager = iface;
return;
}
},
.global_remove => {},
}
Expand Down
4 changes: 4 additions & 0 deletions src/build/SharedDeps.zig
Original file line number Diff line number Diff line change
Expand Up @@ -452,10 +452,14 @@ pub fn add(
.target = target,
.optimize = optimize,
});

// FIXME: replace with `zxdg_decoration_v1` once GTK merges https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398
scanner.addCustomProtocol(plasma_wayland_protocols.path("src/protocols/blur.xml"));
scanner.addCustomProtocol(plasma_wayland_protocols.path("src/protocols/server-decoration.xml"));

scanner.generate("wl_compositor", 1);
scanner.generate("org_kde_kwin_blur_manager", 1);
scanner.generate("org_kde_kwin_server_decoration_manager", 1);

step.root_module.addImport("wayland", wayland);
step.linkSystemLibrary2("wayland-client", dynamic_link_opts);
Expand Down
23 changes: 22 additions & 1 deletion src/config/Config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1110,7 +1110,7 @@ keybind: Keybinds = .{},
///
/// macOS: To hide the titlebar without removing the native window borders
/// or rounded corners, use `macos-titlebar-style = hidden` instead.
@"window-decoration": bool = true,
@"window-decoration": WindowDecoration = .true,

/// The font that will be used for the application's window and tab titles.
///
Expand Down Expand Up @@ -5780,6 +5780,27 @@ pub const BackgroundBlur = union(enum) {
}
};

/// See window-decoration
pub const WindowDecoration = enum {
true,
server,
false,

pub fn parseCLI(input: ?[]const u8) !WindowDecoration {
const input_ = input orelse {
// Emulate behavior for bools
return .true;
};

return if (cli.args.parseBool(input_)) |b|
if (b) .true else .false
else |_| if (std.mem.eql(u8, input_, "server"))
.server
else
error.InvalidValue;
}
};

/// See theme
pub const Theme = struct {
light: []const u8,
Expand Down

0 comments on commit 7eaed4c

Please sign in to comment.