Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gtk(wayland): implement server-sided decorations #4724

Merged
merged 1 commit into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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