Skip to content

Commit

Permalink
gtk(wayland): implement server-sided decorations (#4724)
Browse files Browse the repository at this point in the history
Fixes #4630 fully unless there exists an X11 API I haven't found yet :p 

~~Depends on first commit of #4723, which is duplicated here for now~~
  • Loading branch information
mitchellh authored Jan 14, 2025
2 parents d1fd22a + 4e0d9b1 commit 3cdb9a7
Show file tree
Hide file tree
Showing 11 changed files with 293 additions and 75 deletions.
24 changes: 20 additions & 4 deletions macos/Sources/Ghostty/Ghostty.Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,14 @@ extension Ghostty {
}

var windowDecorations: Bool {
guard let config = self.config else { return true }
var v = false;
let defaultValue = true
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
let key = "window-decoration"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
return v;
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
guard let ptr = v else { return defaultValue }
let str = String(cString: ptr)
return WindowDecoration(rawValue: str)?.enabled() ?? defaultValue
}

var windowTheme: String? {
Expand Down Expand Up @@ -554,4 +557,17 @@ extension Ghostty.Config {
}
}
}

enum WindowDecoration: String {
case none
case client
case server

func enabled() -> Bool {
switch self {
case .client, .server: return true
case .none: return false
}
}
}
}
2 changes: 1 addition & 1 deletion src/apprt/gtk/App.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1881,7 +1881,7 @@ fn initContextMenu(self: *App) void {
c.g_menu_append(section, "Terminal Inspector", "win.toggle_inspector");
}

if (!self.config.@"window-decoration") {
if (!self.config.@"window-decoration".isCSD()) {
const section = c.g_menu_new();
defer c.g_object_unref(section);
const submenu = c.g_menu_new();
Expand Down
8 changes: 3 additions & 5 deletions src/apprt/gtk/Surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1384,11 +1384,9 @@ fn gtkResize(area: *c.GtkGLArea, width: c.gint, height: c.gint, ud: ?*anyopaque)
};

if (self.container.window()) |window| {
if (window.winproto) |*winproto| {
winproto.resizeEvent() catch |err| {
log.warn("failed to notify window protocol of resize={}", .{err});
};
}
window.winproto.resizeEvent() catch |err| {
log.warn("failed to notify window protocol of resize={}", .{err});
};
}

self.resize_overlay.maybeShow();
Expand Down
102 changes: 65 additions & 37 deletions src/apprt/gtk/Window.zig
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ toast_overlay: ?*c.GtkWidget,
adw_tab_overview_focus_timer: ?c.guint = null,

/// State and logic for windowing protocol for a window.
winproto: ?winproto.Window,
winproto: winproto.Window,

pub fn create(alloc: Allocator, app: *App) !*Window {
// Allocate a fixed pointer for our window. We try to minimize
Expand All @@ -83,7 +83,7 @@ pub fn init(self: *Window, app: *App) !void {
.notebook = undefined,
.context_menu = undefined,
.toast_overlay = undefined,
.winproto = null,
.winproto = .none,
};

// Create the window
Expand Down Expand Up @@ -207,11 +207,6 @@ pub fn init(self: *Window, app: *App) !void {
_ = c.g_signal_connect_data(gtk_window, "notify::maximized", c.G_CALLBACK(&gtkWindowNotifyMaximized), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(gtk_window, "notify::fullscreened", c.G_CALLBACK(&gtkWindowNotifyFullscreened), 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 @@ -379,7 +374,11 @@ pub fn updateConfig(
self: *Window,
config: *const configpkg.Config,
) !void {
if (self.winproto) |*v| try v.updateConfigEvent(config);
self.winproto.updateConfigEvent(config) catch |err| {
// We want to continue attempting to make the other config
// changes necessary so we just log the error and continue.
log.warn("failed to update window protocol config error={}", .{err});
};

// We always resync our appearance whenever the config changes.
try self.syncAppearance(config);
Expand All @@ -391,16 +390,52 @@ pub fn updateConfig(
/// 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");
self.winproto.syncAppearance() catch |err| {
log.warn("failed to sync winproto appearance error={}", .{err});
};

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

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

// If we are not decorated then we hide the titlebar.
self.headerbar.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.
if ((comptime adwaita.versionAtLeast(0, 0, 0)) and
adwaita.enabled(&self.app.config))
{
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),
);
}
}
}

// Window protocol specific appearance updates
if (self.winproto) |*v| v.syncAppearance() catch |err| {
log.warn("failed to sync window protocol appearance error={}", .{err});
};
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 @@ -440,7 +475,7 @@ fn initActions(self: *Window) void {
pub fn deinit(self: *Window) void {
c.gtk_widget_unparent(@ptrCast(self.context_menu));

if (self.winproto) |*v| v.deinit(self.app.core_app.alloc);
self.winproto.deinit(self.app.core_app.alloc);

if (self.adw_tab_overview_focus_timer) |timer| {
_ = c.g_source_remove(timer);
Expand Down Expand Up @@ -548,15 +583,11 @@ 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.
self.headerbar.setVisible(new_decorated);
self.app.config.@"window-decoration" = switch (self.app.config.@"window-decoration") {
.client, .server => .none,
.none => .server,
};
self.updateConfig(&self.app.config) catch {};
}

/// Grabs focus on the currently selected tab.
Expand Down Expand Up @@ -623,17 +654,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);
}

fn gtkWindowNotifyFullscreened(
Expand Down
3 changes: 0 additions & 3 deletions src/apprt/gtk/headerbar.zig
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ pub const HeaderBar = union(enum) {
} else {
HeaderBarGtk.init(self);
}

if (!window.app.config.@"gtk-titlebar" or !window.app.config.@"window-decoration")
self.setVisible(false);
}

pub fn setVisible(self: HeaderBar, visible: bool) void {
Expand Down
8 changes: 7 additions & 1 deletion src/apprt/gtk/winproto.zig
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ pub const App = union(Protocol) {

/// Per-Window state for the underlying windowing protocol.
///
/// In both X and Wayland, the terminology used is "Surface" and this is
/// In Wayland, the terminology used is "Surface" and for it, this is
/// really "Surface"-specific state. But Ghostty uses the term "Surface"
/// heavily to mean something completely different, so we use "Window" here
/// to better match what it generally maps to in the Ghostty codebase.
Expand Down Expand Up @@ -125,4 +125,10 @@ pub const Window = union(Protocol) {
inline else => |*v| try v.syncAppearance(),
}
}

pub fn clientSideDecorationEnabled(self: Window) bool {
return switch (self) {
inline else => |v| v.clientSideDecorationEnabled(),
};
}
};
8 changes: 8 additions & 0 deletions src/apprt/gtk/winproto/noop.zig
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,12 @@ pub const Window = struct {
pub fn resizeEvent(_: *Window) !void {}

pub fn syncAppearance(_: *Window) !void {}

/// This returns true if CSD is enabled for this window. This
/// should be the actual present state of the window, not the
/// desired state.
pub fn clientSideDecorationEnabled(self: Window) bool {
_ = self;
return true;
}
};
Loading

0 comments on commit 3cdb9a7

Please sign in to comment.