mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-23 12:16:11 +03:00
apprt/gtk-ng: surface input (mouse, keyboard, focus, etc.) (#7986)
This ports back all our event controllers back to the `GhosttySurface`. With this PR, the terminal is now usable again at a very very simple level! This also brings back `winproto` but its still filled with incompatibilities. I just need to bring that back so modifiers worked properly. We'll fix that up in a future PR. This also fixes one undefined memory access in debug modes found by Valgrind.
This commit is contained in:
@ -49,8 +49,20 @@ pub fn getSize(self: *const Self) !apprt.SurfaceSize {
|
||||
}
|
||||
|
||||
pub fn getCursorPos(self: *const Self) !apprt.CursorPos {
|
||||
return self.surface.getCursorPos();
|
||||
}
|
||||
|
||||
pub fn supportsClipboard(
|
||||
self: *const Self,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
) bool {
|
||||
_ = self;
|
||||
return .{ .x = 0, .y = 0 };
|
||||
return switch (clipboard_type) {
|
||||
.standard,
|
||||
.selection,
|
||||
.primary,
|
||||
=> true,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn clipboardRequest(
|
||||
|
@ -3,6 +3,7 @@ const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const builtin = @import("builtin");
|
||||
const adw = @import("adw");
|
||||
const gdk = @import("gdk");
|
||||
const gio = @import("gio");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
@ -20,6 +21,7 @@ const CoreSurface = @import("../../../Surface.zig");
|
||||
|
||||
const adw_version = @import("../adw_version.zig");
|
||||
const gtk_version = @import("../gtk_version.zig");
|
||||
const winprotopkg = @import("../winproto.zig");
|
||||
const ApprtApp = @import("../App.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
const WeakRef = @import("../weak_ref.zig").WeakRef;
|
||||
@ -90,6 +92,9 @@ pub const Application = extern struct {
|
||||
/// The configuration for the application.
|
||||
config: *Config,
|
||||
|
||||
/// State and logic for the underlying windowing protocol.
|
||||
winproto: winprotopkg.App,
|
||||
|
||||
/// The base path of the transient cgroup used to put all surfaces
|
||||
/// into their own cgroup. This is only set if cgroups are enabled
|
||||
/// and initialization was successful.
|
||||
@ -208,6 +213,29 @@ pub const Application = extern struct {
|
||||
break :app_id if (builtin.mode == .Debug) default_id ++ "-debug" else default_id;
|
||||
};
|
||||
|
||||
const display: *gdk.Display = gdk.Display.getDefault() orelse {
|
||||
// I'm unsure of any scenario where this happens. Because we don't
|
||||
// want to litter null checks everywhere, we just exit here.
|
||||
log.warn("gdk display is null, exiting", .{});
|
||||
std.posix.exit(1);
|
||||
};
|
||||
|
||||
// Setup our windowing protocol logic
|
||||
var wp: winprotopkg.App = winprotopkg.App.init(
|
||||
alloc,
|
||||
display,
|
||||
app_id,
|
||||
&config,
|
||||
) catch |err| wp: {
|
||||
// If we fail to detect or setup the windowing protocol
|
||||
// specifies, we fallback to a noop implementation so we can
|
||||
// still launch.
|
||||
log.warn("error initializing windowing protocol err={}", .{err});
|
||||
break :wp .{ .none = .{} };
|
||||
};
|
||||
errdefer wp.deinit(alloc);
|
||||
log.debug("windowing protocol={s}", .{@tagName(wp)});
|
||||
|
||||
// Create our GTK Application which encapsulates our process.
|
||||
log.debug("creating GTK application id={s} single-instance={}", .{
|
||||
app_id,
|
||||
@ -237,6 +265,7 @@ pub const Application = extern struct {
|
||||
.rt_app = rt_app,
|
||||
.core_app = core_app,
|
||||
.config = config_obj,
|
||||
.winproto = wp,
|
||||
};
|
||||
|
||||
return self;
|
||||
@ -251,6 +280,7 @@ pub const Application = extern struct {
|
||||
const alloc = self.allocator();
|
||||
const priv = self.private();
|
||||
priv.config.unref();
|
||||
priv.winproto.deinit(alloc);
|
||||
if (priv.transient_cgroup_base) |base| alloc.free(base);
|
||||
}
|
||||
|
||||
@ -490,6 +520,11 @@ pub const Application = extern struct {
|
||||
return self.private().rt_app;
|
||||
}
|
||||
|
||||
/// Returns the app winproto implementation.
|
||||
pub fn winproto(self: *Self) *winprotopkg.App {
|
||||
return &self.private().winproto;
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Libghostty Callbacks
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
405
src/apprt/gtk-ng/key.zig
Normal file
405
src/apprt/gtk-ng/key.zig
Normal file
@ -0,0 +1,405 @@
|
||||
const std = @import("std");
|
||||
const build_options = @import("build_options");
|
||||
|
||||
const gdk = @import("gdk");
|
||||
const glib = @import("glib");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const input = @import("../../input.zig");
|
||||
const winproto = @import("winproto.zig");
|
||||
|
||||
/// Returns a GTK accelerator string from a trigger.
|
||||
pub fn accelFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u8 {
|
||||
var buf_stream = std.io.fixedBufferStream(buf);
|
||||
const writer = buf_stream.writer();
|
||||
|
||||
// Modifiers
|
||||
if (trigger.mods.shift) try writer.writeAll("<Shift>");
|
||||
if (trigger.mods.ctrl) try writer.writeAll("<Ctrl>");
|
||||
if (trigger.mods.alt) try writer.writeAll("<Alt>");
|
||||
if (trigger.mods.super) try writer.writeAll("<Super>");
|
||||
|
||||
// Write our key
|
||||
if (!try writeTriggerKey(writer, trigger)) return null;
|
||||
|
||||
// We need to make the string null terminated.
|
||||
try writer.writeByte(0);
|
||||
const slice = buf_stream.getWritten();
|
||||
return slice[0 .. slice.len - 1 :0];
|
||||
}
|
||||
|
||||
/// Returns a XDG-compliant shortcuts string from a trigger.
|
||||
/// Spec: https://specifications.freedesktop.org/shortcuts-spec/latest/
|
||||
pub fn xdgShortcutFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u8 {
|
||||
var buf_stream = std.io.fixedBufferStream(buf);
|
||||
const writer = buf_stream.writer();
|
||||
|
||||
// Modifiers
|
||||
if (trigger.mods.shift) try writer.writeAll("SHIFT+");
|
||||
if (trigger.mods.ctrl) try writer.writeAll("CTRL+");
|
||||
if (trigger.mods.alt) try writer.writeAll("ALT+");
|
||||
if (trigger.mods.super) try writer.writeAll("LOGO+");
|
||||
|
||||
// Write our key
|
||||
// NOTE: While the spec specifies that only libxkbcommon keysyms are
|
||||
// expected, using GTK's keysyms should still work as they are identical
|
||||
// to *X11's* keysyms (which I assume is a subset of libxkbcommon's).
|
||||
// I haven't been able to any evidence to back up that assumption but
|
||||
// this works for now
|
||||
if (!try writeTriggerKey(writer, trigger)) return null;
|
||||
|
||||
// We need to make the string null terminated.
|
||||
try writer.writeByte(0);
|
||||
const slice = buf_stream.getWritten();
|
||||
return slice[0 .. slice.len - 1 :0];
|
||||
}
|
||||
|
||||
fn writeTriggerKey(writer: anytype, trigger: input.Binding.Trigger) !bool {
|
||||
switch (trigger.key) {
|
||||
.physical => |k| {
|
||||
const keyval = keyvalFromKey(k) orelse return false;
|
||||
try writer.writeAll(std.mem.span(gdk.keyvalName(keyval) orelse return false));
|
||||
},
|
||||
|
||||
.unicode => |cp| {
|
||||
if (gdk.keyvalName(cp)) |name| {
|
||||
try writer.writeAll(std.mem.span(name));
|
||||
} else {
|
||||
try writer.print("{u}", .{cp});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn translateMods(state: gdk.ModifierType) input.Mods {
|
||||
return .{
|
||||
.shift = state.shift_mask,
|
||||
.ctrl = state.control_mask,
|
||||
.alt = state.alt_mask,
|
||||
.super = state.super_mask,
|
||||
// Lock is dependent on the X settings but we just assume caps lock.
|
||||
.caps_lock = state.lock_mask,
|
||||
};
|
||||
}
|
||||
|
||||
// Get the unshifted unicode value of the keyval. This is used
|
||||
// by the Kitty keyboard protocol.
|
||||
pub fn keyvalUnicodeUnshifted(
|
||||
widget: *gtk.Widget,
|
||||
event: *gdk.KeyEvent,
|
||||
keycode: u32,
|
||||
) u21 {
|
||||
const display = widget.getDisplay();
|
||||
|
||||
// We need to get the currently active keyboard layout so we know
|
||||
// what group to look at.
|
||||
const layout = event.getLayout();
|
||||
|
||||
// Get all the possible keyboard mappings for this keycode. A keycode is the
|
||||
// physical key pressed.
|
||||
var keys: [*]gdk.KeymapKey = undefined;
|
||||
var keyvals: [*]c_uint = undefined;
|
||||
var n_entries: c_int = 0;
|
||||
if (display.mapKeycode(keycode, &keys, &keyvals, &n_entries) == 0) return 0;
|
||||
|
||||
defer glib.free(keys);
|
||||
defer glib.free(keyvals);
|
||||
|
||||
// debugging:
|
||||
// std.log.debug("layout={}", .{layout});
|
||||
// for (0..@intCast(n_entries)) |i| {
|
||||
// std.log.debug("keymap key={} codepoint={x}", .{
|
||||
// keys[i],
|
||||
// gdk.keyvalToUnicode(keyvals[i]),
|
||||
// });
|
||||
// }
|
||||
|
||||
for (0..@intCast(n_entries)) |i| {
|
||||
if (keys[i].f_group == layout and
|
||||
keys[i].f_level == 0)
|
||||
{
|
||||
return std.math.cast(
|
||||
u21,
|
||||
gdk.keyvalToUnicode(keyvals[i]),
|
||||
) orelse 0;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Returns the mods to use a key event from a GTK event.
|
||||
/// This requires a lot of context because the GdkEvent
|
||||
/// doesn't contain enough on its own.
|
||||
pub fn eventMods(
|
||||
event: *gdk.Event,
|
||||
physical_key: input.Key,
|
||||
gtk_mods: gdk.ModifierType,
|
||||
action: input.Action,
|
||||
app_winproto: *winproto.App,
|
||||
) input.Mods {
|
||||
const device = event.getDevice();
|
||||
|
||||
var mods = app_winproto.eventMods(device, gtk_mods);
|
||||
mods.num_lock = if (device) |d| d.getNumLockState() != 0 else false;
|
||||
|
||||
// We use the physical key to determine sided modifiers. As
|
||||
// far as I can tell there's no other way to reliably determine
|
||||
// this.
|
||||
//
|
||||
// We also set the main modifier to true if either side is true,
|
||||
// since on both X11/Wayland, GTK doesn't set the main modifier
|
||||
// if only the modifier key is pressed, but our core logic
|
||||
// relies on it.
|
||||
switch (physical_key) {
|
||||
.shift_left => {
|
||||
mods.shift = action != .release;
|
||||
mods.sides.shift = .left;
|
||||
},
|
||||
|
||||
.shift_right => {
|
||||
mods.shift = action != .release;
|
||||
mods.sides.shift = .right;
|
||||
},
|
||||
|
||||
.control_left => {
|
||||
mods.ctrl = action != .release;
|
||||
mods.sides.ctrl = .left;
|
||||
},
|
||||
|
||||
.control_right => {
|
||||
mods.ctrl = action != .release;
|
||||
mods.sides.ctrl = .right;
|
||||
},
|
||||
|
||||
.alt_left => {
|
||||
mods.alt = action != .release;
|
||||
mods.sides.alt = .left;
|
||||
},
|
||||
|
||||
.alt_right => {
|
||||
mods.alt = action != .release;
|
||||
mods.sides.alt = .right;
|
||||
},
|
||||
|
||||
.meta_left => {
|
||||
mods.super = action != .release;
|
||||
mods.sides.super = .left;
|
||||
},
|
||||
|
||||
.meta_right => {
|
||||
mods.super = action != .release;
|
||||
mods.sides.super = .right;
|
||||
},
|
||||
|
||||
else => {},
|
||||
}
|
||||
|
||||
return mods;
|
||||
}
|
||||
|
||||
/// Returns an input key from a keyval or null if we don't have a mapping.
|
||||
pub fn keyFromKeyval(keyval: c_uint) ?input.Key {
|
||||
for (keymap) |entry| {
|
||||
if (entry[0] == keyval) return entry[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns a keyval from an input key or null if we don't have a mapping.
|
||||
pub fn keyvalFromKey(key: input.Key) ?c_uint {
|
||||
switch (key) {
|
||||
inline else => |key_comptime| {
|
||||
return comptime value: {
|
||||
@setEvalBranchQuota(50_000);
|
||||
for (keymap) |entry| {
|
||||
if (entry[1] == key_comptime) break :value entry[0];
|
||||
}
|
||||
|
||||
break :value null;
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
test "accelFromTrigger" {
|
||||
const testing = std.testing;
|
||||
var buf: [256]u8 = undefined;
|
||||
|
||||
try testing.expectEqualStrings("<Super>q", (try accelFromTrigger(&buf, .{
|
||||
.mods = .{ .super = true },
|
||||
.key = .{ .unicode = 'q' },
|
||||
})).?);
|
||||
|
||||
try testing.expectEqualStrings("<Shift><Ctrl><Alt><Super>backslash", (try accelFromTrigger(&buf, .{
|
||||
.mods = .{ .ctrl = true, .alt = true, .super = true, .shift = true },
|
||||
.key = .{ .unicode = 92 },
|
||||
})).?);
|
||||
}
|
||||
|
||||
test "xdgShortcutFromTrigger" {
|
||||
const testing = std.testing;
|
||||
var buf: [256]u8 = undefined;
|
||||
|
||||
try testing.expectEqualStrings("LOGO+q", (try xdgShortcutFromTrigger(&buf, .{
|
||||
.mods = .{ .super = true },
|
||||
.key = .{ .unicode = 'q' },
|
||||
})).?);
|
||||
|
||||
try testing.expectEqualStrings("SHIFT+CTRL+ALT+LOGO+backslash", (try xdgShortcutFromTrigger(&buf, .{
|
||||
.mods = .{ .ctrl = true, .alt = true, .super = true, .shift = true },
|
||||
.key = .{ .unicode = 92 },
|
||||
})).?);
|
||||
}
|
||||
|
||||
/// A raw entry in the keymap. Our keymap contains mappings between
|
||||
/// GDK keys and our own key enum.
|
||||
const RawEntry = struct { c_uint, input.Key };
|
||||
|
||||
const keymap: []const RawEntry = &.{
|
||||
.{ gdk.KEY_a, .key_a },
|
||||
.{ gdk.KEY_b, .key_b },
|
||||
.{ gdk.KEY_c, .key_c },
|
||||
.{ gdk.KEY_d, .key_d },
|
||||
.{ gdk.KEY_e, .key_e },
|
||||
.{ gdk.KEY_f, .key_f },
|
||||
.{ gdk.KEY_g, .key_g },
|
||||
.{ gdk.KEY_h, .key_h },
|
||||
.{ gdk.KEY_i, .key_i },
|
||||
.{ gdk.KEY_j, .key_j },
|
||||
.{ gdk.KEY_k, .key_k },
|
||||
.{ gdk.KEY_l, .key_l },
|
||||
.{ gdk.KEY_m, .key_m },
|
||||
.{ gdk.KEY_n, .key_n },
|
||||
.{ gdk.KEY_o, .key_o },
|
||||
.{ gdk.KEY_p, .key_p },
|
||||
.{ gdk.KEY_q, .key_q },
|
||||
.{ gdk.KEY_r, .key_r },
|
||||
.{ gdk.KEY_s, .key_s },
|
||||
.{ gdk.KEY_t, .key_t },
|
||||
.{ gdk.KEY_u, .key_u },
|
||||
.{ gdk.KEY_v, .key_v },
|
||||
.{ gdk.KEY_w, .key_w },
|
||||
.{ gdk.KEY_x, .key_x },
|
||||
.{ gdk.KEY_y, .key_y },
|
||||
.{ gdk.KEY_z, .key_z },
|
||||
|
||||
.{ gdk.KEY_0, .digit_0 },
|
||||
.{ gdk.KEY_1, .digit_1 },
|
||||
.{ gdk.KEY_2, .digit_2 },
|
||||
.{ gdk.KEY_3, .digit_3 },
|
||||
.{ gdk.KEY_4, .digit_4 },
|
||||
.{ gdk.KEY_5, .digit_5 },
|
||||
.{ gdk.KEY_6, .digit_6 },
|
||||
.{ gdk.KEY_7, .digit_7 },
|
||||
.{ gdk.KEY_8, .digit_8 },
|
||||
.{ gdk.KEY_9, .digit_9 },
|
||||
|
||||
.{ gdk.KEY_semicolon, .semicolon },
|
||||
.{ gdk.KEY_space, .space },
|
||||
.{ gdk.KEY_apostrophe, .quote },
|
||||
.{ gdk.KEY_comma, .comma },
|
||||
.{ gdk.KEY_grave, .backquote },
|
||||
.{ gdk.KEY_period, .period },
|
||||
.{ gdk.KEY_slash, .slash },
|
||||
.{ gdk.KEY_minus, .minus },
|
||||
.{ gdk.KEY_equal, .equal },
|
||||
.{ gdk.KEY_bracketleft, .bracket_left },
|
||||
.{ gdk.KEY_bracketright, .bracket_right },
|
||||
.{ gdk.KEY_backslash, .backslash },
|
||||
|
||||
.{ gdk.KEY_Up, .arrow_up },
|
||||
.{ gdk.KEY_Down, .arrow_down },
|
||||
.{ gdk.KEY_Right, .arrow_right },
|
||||
.{ gdk.KEY_Left, .arrow_left },
|
||||
.{ gdk.KEY_Home, .home },
|
||||
.{ gdk.KEY_End, .end },
|
||||
.{ gdk.KEY_Insert, .insert },
|
||||
.{ gdk.KEY_Delete, .delete },
|
||||
.{ gdk.KEY_Caps_Lock, .caps_lock },
|
||||
.{ gdk.KEY_Scroll_Lock, .scroll_lock },
|
||||
.{ gdk.KEY_Num_Lock, .num_lock },
|
||||
.{ gdk.KEY_Page_Up, .page_up },
|
||||
.{ gdk.KEY_Page_Down, .page_down },
|
||||
.{ gdk.KEY_Escape, .escape },
|
||||
.{ gdk.KEY_Return, .enter },
|
||||
.{ gdk.KEY_Tab, .tab },
|
||||
.{ gdk.KEY_BackSpace, .backspace },
|
||||
.{ gdk.KEY_Print, .print_screen },
|
||||
.{ gdk.KEY_Pause, .pause },
|
||||
|
||||
.{ gdk.KEY_F1, .f1 },
|
||||
.{ gdk.KEY_F2, .f2 },
|
||||
.{ gdk.KEY_F3, .f3 },
|
||||
.{ gdk.KEY_F4, .f4 },
|
||||
.{ gdk.KEY_F5, .f5 },
|
||||
.{ gdk.KEY_F6, .f6 },
|
||||
.{ gdk.KEY_F7, .f7 },
|
||||
.{ gdk.KEY_F8, .f8 },
|
||||
.{ gdk.KEY_F9, .f9 },
|
||||
.{ gdk.KEY_F10, .f10 },
|
||||
.{ gdk.KEY_F11, .f11 },
|
||||
.{ gdk.KEY_F12, .f12 },
|
||||
.{ gdk.KEY_F13, .f13 },
|
||||
.{ gdk.KEY_F14, .f14 },
|
||||
.{ gdk.KEY_F15, .f15 },
|
||||
.{ gdk.KEY_F16, .f16 },
|
||||
.{ gdk.KEY_F17, .f17 },
|
||||
.{ gdk.KEY_F18, .f18 },
|
||||
.{ gdk.KEY_F19, .f19 },
|
||||
.{ gdk.KEY_F20, .f20 },
|
||||
.{ gdk.KEY_F21, .f21 },
|
||||
.{ gdk.KEY_F22, .f22 },
|
||||
.{ gdk.KEY_F23, .f23 },
|
||||
.{ gdk.KEY_F24, .f24 },
|
||||
.{ gdk.KEY_F25, .f25 },
|
||||
|
||||
.{ gdk.KEY_KP_0, .numpad_0 },
|
||||
.{ gdk.KEY_KP_1, .numpad_1 },
|
||||
.{ gdk.KEY_KP_2, .numpad_2 },
|
||||
.{ gdk.KEY_KP_3, .numpad_3 },
|
||||
.{ gdk.KEY_KP_4, .numpad_4 },
|
||||
.{ gdk.KEY_KP_5, .numpad_5 },
|
||||
.{ gdk.KEY_KP_6, .numpad_6 },
|
||||
.{ gdk.KEY_KP_7, .numpad_7 },
|
||||
.{ gdk.KEY_KP_8, .numpad_8 },
|
||||
.{ gdk.KEY_KP_9, .numpad_9 },
|
||||
.{ gdk.KEY_KP_Decimal, .numpad_decimal },
|
||||
.{ gdk.KEY_KP_Divide, .numpad_divide },
|
||||
.{ gdk.KEY_KP_Multiply, .numpad_multiply },
|
||||
.{ gdk.KEY_KP_Subtract, .numpad_subtract },
|
||||
.{ gdk.KEY_KP_Add, .numpad_add },
|
||||
.{ gdk.KEY_KP_Enter, .numpad_enter },
|
||||
.{ gdk.KEY_KP_Equal, .numpad_equal },
|
||||
|
||||
.{ gdk.KEY_KP_Separator, .numpad_separator },
|
||||
.{ gdk.KEY_KP_Left, .numpad_left },
|
||||
.{ gdk.KEY_KP_Right, .numpad_right },
|
||||
.{ gdk.KEY_KP_Up, .numpad_up },
|
||||
.{ gdk.KEY_KP_Down, .numpad_down },
|
||||
.{ gdk.KEY_KP_Page_Up, .numpad_page_up },
|
||||
.{ gdk.KEY_KP_Page_Down, .numpad_page_down },
|
||||
.{ gdk.KEY_KP_Home, .numpad_home },
|
||||
.{ gdk.KEY_KP_End, .numpad_end },
|
||||
.{ gdk.KEY_KP_Insert, .numpad_insert },
|
||||
.{ gdk.KEY_KP_Delete, .numpad_delete },
|
||||
.{ gdk.KEY_KP_Begin, .numpad_begin },
|
||||
|
||||
.{ gdk.KEY_Copy, .copy },
|
||||
.{ gdk.KEY_Cut, .cut },
|
||||
.{ gdk.KEY_Paste, .paste },
|
||||
|
||||
.{ gdk.KEY_Shift_L, .shift_left },
|
||||
.{ gdk.KEY_Control_L, .control_left },
|
||||
.{ gdk.KEY_Alt_L, .alt_left },
|
||||
.{ gdk.KEY_Super_L, .meta_left },
|
||||
.{ gdk.KEY_Shift_R, .shift_right },
|
||||
.{ gdk.KEY_Control_R, .control_right },
|
||||
.{ gdk.KEY_Alt_R, .alt_right },
|
||||
.{ gdk.KEY_Super_R, .meta_right },
|
||||
|
||||
// TODO: media keys
|
||||
};
|
@ -2,15 +2,12 @@ using Gtk 4.0;
|
||||
using Adw 1;
|
||||
|
||||
template $GhosttySurface: Adw.Bin {
|
||||
// A box isn't strictly necessary right now but there will be more
|
||||
// stuff here in the future. There's still a lot to do with surfaces.
|
||||
Box {
|
||||
orientation: vertical;
|
||||
hexpand: true;
|
||||
|
||||
Overlay {
|
||||
GLArea gl_area {
|
||||
hexpand: true;
|
||||
vexpand: true;
|
||||
focusable: true;
|
||||
focus-on-click: true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
157
src/apprt/gtk-ng/winproto.zig
Normal file
157
src/apprt/gtk-ng/winproto.zig
Normal file
@ -0,0 +1,157 @@
|
||||
const std = @import("std");
|
||||
const build_options = @import("build_options");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const gdk = @import("gdk");
|
||||
|
||||
const Config = @import("../../config.zig").Config;
|
||||
const input = @import("../../input.zig");
|
||||
const key = @import("key.zig");
|
||||
|
||||
// TODO: As we get to these APIs the compiler should tell us
|
||||
const ApprtWindow = void;
|
||||
|
||||
pub const noop = @import("winproto/noop.zig");
|
||||
pub const x11 = @import("winproto/x11.zig");
|
||||
pub const wayland = @import("winproto/wayland.zig");
|
||||
|
||||
pub const Protocol = enum {
|
||||
none,
|
||||
wayland,
|
||||
x11,
|
||||
};
|
||||
|
||||
/// App-state for the underlying windowing protocol. There should be one
|
||||
/// instance of this struct per application.
|
||||
pub const App = union(Protocol) {
|
||||
none: noop.App,
|
||||
wayland: if (build_options.wayland) wayland.App else noop.App,
|
||||
x11: if (build_options.x11) x11.App else noop.App,
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
gdk_display: *gdk.Display,
|
||||
app_id: [:0]const u8,
|
||||
config: *const Config,
|
||||
) !App {
|
||||
inline for (@typeInfo(App).@"union".fields) |field| {
|
||||
if (try field.type.init(
|
||||
alloc,
|
||||
gdk_display,
|
||||
app_id,
|
||||
config,
|
||||
)) |v| {
|
||||
return @unionInit(App, field.name, v);
|
||||
}
|
||||
}
|
||||
|
||||
return .{ .none = .{} };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App, alloc: Allocator) void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| v.deinit(alloc),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eventMods(
|
||||
self: *App,
|
||||
device: ?*gdk.Device,
|
||||
gtk_mods: gdk.ModifierType,
|
||||
) input.Mods {
|
||||
return switch (self.*) {
|
||||
inline else => |*v| v.eventMods(device, gtk_mods),
|
||||
} orelse key.translateMods(gtk_mods);
|
||||
}
|
||||
|
||||
pub fn supportsQuickTerminal(self: App) bool {
|
||||
return switch (self) {
|
||||
inline else => |v| v.supportsQuickTerminal(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Set up necessary support for the quick terminal that must occur
|
||||
/// *before* the window-level winproto object is created.
|
||||
///
|
||||
/// Only has an effect on the Wayland backend, where the gtk4-layer-shell
|
||||
/// library is initialized.
|
||||
pub fn initQuickTerminal(self: *App, apprt_window: *ApprtWindow) !void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| try v.initQuickTerminal(apprt_window),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Per-Window state for the underlying windowing protocol.
|
||||
///
|
||||
/// 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.
|
||||
pub const Window = union(Protocol) {
|
||||
none: noop.Window,
|
||||
wayland: if (build_options.wayland) wayland.Window else noop.Window,
|
||||
x11: if (build_options.x11) x11.Window else noop.Window,
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
app: *App,
|
||||
apprt_window: *ApprtWindow,
|
||||
) !Window {
|
||||
return switch (app.*) {
|
||||
inline else => |*v, tag| {
|
||||
inline for (@typeInfo(Window).@"union".fields) |field| {
|
||||
if (comptime std.mem.eql(
|
||||
u8,
|
||||
field.name,
|
||||
@tagName(tag),
|
||||
)) return @unionInit(
|
||||
Window,
|
||||
field.name,
|
||||
try field.type.init(
|
||||
alloc,
|
||||
v,
|
||||
apprt_window,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Window, alloc: Allocator) void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| v.deinit(alloc),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resizeEvent(self: *Window) !void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| try v.resizeEvent(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn syncAppearance(self: *Window) !void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| try v.syncAppearance(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clientSideDecorationEnabled(self: Window) bool {
|
||||
return switch (self) {
|
||||
inline else => |v| v.clientSideDecorationEnabled(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| try v.addSubprocessEnv(env),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setUrgent(self: *Window, urgent: bool) !void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| try v.setUrgent(urgent),
|
||||
}
|
||||
}
|
||||
};
|
75
src/apprt/gtk-ng/winproto/noop.zig
Normal file
75
src/apprt/gtk-ng/winproto/noop.zig
Normal file
@ -0,0 +1,75 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const gdk = @import("gdk");
|
||||
|
||||
const Config = @import("../../../config.zig").Config;
|
||||
const input = @import("../../../input.zig");
|
||||
const ApprtWindow = void; // TODO: fix
|
||||
|
||||
const log = std.log.scoped(.winproto_noop);
|
||||
|
||||
pub const App = struct {
|
||||
pub fn init(
|
||||
_: Allocator,
|
||||
_: *gdk.Display,
|
||||
_: [:0]const u8,
|
||||
_: *const Config,
|
||||
) !?App {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App, alloc: Allocator) void {
|
||||
_ = self;
|
||||
_ = alloc;
|
||||
}
|
||||
|
||||
pub fn eventMods(
|
||||
_: *App,
|
||||
_: ?*gdk.Device,
|
||||
_: gdk.ModifierType,
|
||||
) ?input.Mods {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn supportsQuickTerminal(_: App) bool {
|
||||
return false;
|
||||
}
|
||||
pub fn initQuickTerminal(_: *App, _: *ApprtWindow) !void {}
|
||||
};
|
||||
|
||||
pub const Window = struct {
|
||||
pub fn init(
|
||||
_: Allocator,
|
||||
_: *App,
|
||||
_: *ApprtWindow,
|
||||
) !Window {
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Window, alloc: Allocator) void {
|
||||
_ = self;
|
||||
_ = alloc;
|
||||
}
|
||||
|
||||
pub fn updateConfigEvent(
|
||||
_: *Window,
|
||||
_: *const ApprtWindow.DerivedConfig,
|
||||
) !void {}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
pub fn addSubprocessEnv(_: *Window, _: *std.process.EnvMap) !void {}
|
||||
|
||||
pub fn setUrgent(_: *Window, _: bool) !void {}
|
||||
};
|
501
src/apprt/gtk-ng/winproto/wayland.zig
Normal file
501
src/apprt/gtk-ng/winproto/wayland.zig
Normal file
@ -0,0 +1,501 @@
|
||||
//! Wayland protocol implementation for the Ghostty GTK apprt.
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const build_options = @import("build_options");
|
||||
|
||||
const gdk = @import("gdk");
|
||||
const gdk_wayland = @import("gdk_wayland");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
const layer_shell = @import("gtk4-layer-shell");
|
||||
const wayland = @import("wayland");
|
||||
|
||||
const Config = @import("../../../config.zig").Config;
|
||||
const input = @import("../../../input.zig");
|
||||
const ApprtWindow = void; // TODO: fix
|
||||
|
||||
const wl = wayland.client.wl;
|
||||
const org = wayland.client.org;
|
||||
const xdg = wayland.client.xdg;
|
||||
|
||||
const log = std.log.scoped(.winproto_wayland);
|
||||
|
||||
/// Wayland state that contains application-wide Wayland objects (e.g. wl_display).
|
||||
pub const App = struct {
|
||||
display: *wl.Display,
|
||||
context: *Context,
|
||||
|
||||
const Context = struct {
|
||||
kde_blur_manager: ?*org.KdeKwinBlurManager = null,
|
||||
|
||||
// FIXME: replace with `zxdg_decoration_v1` once GTK merges
|
||||
// https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398
|
||||
kde_decoration_manager: ?*org.KdeKwinServerDecorationManager = null,
|
||||
|
||||
kde_slide_manager: ?*org.KdeKwinSlideManager = null,
|
||||
|
||||
default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null,
|
||||
|
||||
xdg_activation: ?*xdg.ActivationV1 = null,
|
||||
|
||||
/// Whether the xdg_wm_dialog_v1 protocol is present.
|
||||
///
|
||||
/// If it is present, gtk4-layer-shell < 1.0.4 may crash when the user
|
||||
/// creates a quick terminal, and we need to ensure this fails
|
||||
/// gracefully if this situation occurs.
|
||||
///
|
||||
/// FIXME: This is a temporary workaround - we should remove this when
|
||||
/// all of our supported distros drop support for affected old
|
||||
/// gtk4-layer-shell versions.
|
||||
///
|
||||
/// See https://github.com/wmww/gtk4-layer-shell/issues/50
|
||||
xdg_wm_dialog_present: bool = false,
|
||||
};
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
gdk_display: *gdk.Display,
|
||||
app_id: [:0]const u8,
|
||||
config: *const Config,
|
||||
) !?App {
|
||||
_ = config;
|
||||
_ = app_id;
|
||||
|
||||
const gdk_wayland_display = gobject.ext.cast(
|
||||
gdk_wayland.WaylandDisplay,
|
||||
gdk_display,
|
||||
) orelse return null;
|
||||
|
||||
const display: *wl.Display = @ptrCast(@alignCast(
|
||||
gdk_wayland_display.getWlDisplay() orelse return error.NoWaylandDisplay,
|
||||
));
|
||||
|
||||
// Create our context for our callbacks so we have a stable pointer.
|
||||
// Note: at the time of writing this comment, we don't really need
|
||||
// a stable pointer, but it's too scary that we'd need one in the future
|
||||
// and not have it and corrupt memory or something so let's just do it.
|
||||
const context = try alloc.create(Context);
|
||||
errdefer alloc.destroy(context);
|
||||
context.* = .{};
|
||||
|
||||
// Get our display registry so we can get all the available interfaces
|
||||
// and bind to what we need.
|
||||
const registry = try display.getRegistry();
|
||||
registry.setListener(*Context, registryListener, context);
|
||||
if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
|
||||
|
||||
// Do another round-trip to get the default decoration mode
|
||||
if (context.kde_decoration_manager) |deco_manager| {
|
||||
deco_manager.setListener(*Context, decoManagerListener, context);
|
||||
if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
|
||||
}
|
||||
|
||||
return .{
|
||||
.display = display,
|
||||
.context = context,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App, alloc: Allocator) void {
|
||||
alloc.destroy(self.context);
|
||||
}
|
||||
|
||||
pub fn eventMods(
|
||||
_: *App,
|
||||
_: ?*gdk.Device,
|
||||
_: gdk.ModifierType,
|
||||
) ?input.Mods {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn supportsQuickTerminal(self: App) bool {
|
||||
if (!layer_shell.isSupported()) {
|
||||
log.warn("your compositor does not support the wlr-layer-shell protocol; disabling quick terminal", .{});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (self.context.xdg_wm_dialog_present and layer_shell.getLibraryVersion().order(.{
|
||||
.major = 1,
|
||||
.minor = 0,
|
||||
.patch = 4,
|
||||
}) == .lt) {
|
||||
log.warn("the version of gtk4-layer-shell installed on your system is too old (must be 1.0.4 or newer); disabling quick terminal", .{});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn initQuickTerminal(_: *App, apprt_window: *ApprtWindow) !void {
|
||||
const window = apprt_window.window.as(gtk.Window);
|
||||
|
||||
layer_shell.initForWindow(window);
|
||||
layer_shell.setLayer(window, .top);
|
||||
layer_shell.setNamespace(window, "ghostty-quick-terminal");
|
||||
}
|
||||
|
||||
fn getInterfaceType(comptime field: std.builtin.Type.StructField) ?type {
|
||||
// Globals should be optional pointers
|
||||
const T = switch (@typeInfo(field.type)) {
|
||||
.optional => |o| switch (@typeInfo(o.child)) {
|
||||
.pointer => |v| v.child,
|
||||
else => return null,
|
||||
},
|
||||
else => return null,
|
||||
};
|
||||
|
||||
// Only process Wayland interfaces
|
||||
if (!@hasDecl(T, "interface")) return null;
|
||||
return T;
|
||||
}
|
||||
|
||||
fn registryListener(
|
||||
registry: *wl.Registry,
|
||||
event: wl.Registry.Event,
|
||||
context: *Context,
|
||||
) void {
|
||||
const ctx_fields = @typeInfo(Context).@"struct".fields;
|
||||
|
||||
switch (event) {
|
||||
.global => |v| global: {
|
||||
// We don't actually do anything with this other than checking
|
||||
// for its existence, so we process this separately.
|
||||
if (std.mem.orderZ(u8, v.interface, "xdg_wm_dialog_v1") == .eq)
|
||||
context.xdg_wm_dialog_present = true;
|
||||
|
||||
inline for (ctx_fields) |field| {
|
||||
const T = getInterfaceType(field) orelse continue;
|
||||
|
||||
if (std.mem.orderZ(
|
||||
u8,
|
||||
v.interface,
|
||||
T.interface.name,
|
||||
) != .eq) break :global;
|
||||
|
||||
@field(context, field.name) = registry.bind(
|
||||
v.name,
|
||||
T,
|
||||
T.generated_version,
|
||||
) catch |err| {
|
||||
log.warn(
|
||||
"error binding interface {s} error={}",
|
||||
.{ v.interface, err },
|
||||
);
|
||||
return;
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// This should be a rare occurrence, but in case a global
|
||||
// is suddenly no longer available, we destroy and unset it
|
||||
// as the protocol mandates.
|
||||
.global_remove => |v| remove: {
|
||||
inline for (ctx_fields) |field| {
|
||||
if (getInterfaceType(field) == null) continue;
|
||||
const global = @field(context, field.name) orelse break :remove;
|
||||
if (global.getId() == v.name) {
|
||||
global.destroy();
|
||||
@field(context, field.name) = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn decoManagerListener(
|
||||
_: *org.KdeKwinServerDecorationManager,
|
||||
event: org.KdeKwinServerDecorationManager.Event,
|
||||
context: *Context,
|
||||
) void {
|
||||
switch (event) {
|
||||
.default_mode => |mode| {
|
||||
context.default_deco_mode = @enumFromInt(mode.mode);
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Per-window (wl_surface) state for the Wayland protocol.
|
||||
pub const Window = struct {
|
||||
apprt_window: *ApprtWindow,
|
||||
|
||||
/// The Wayland surface for this window.
|
||||
surface: *wl.Surface,
|
||||
|
||||
/// The context from the app where we can load our Wayland interfaces.
|
||||
app_context: *App.Context,
|
||||
|
||||
/// A token that, when present, indicates that the window is blurred.
|
||||
blur_token: ?*org.KdeKwinBlur = null,
|
||||
|
||||
/// Object that controls the decoration mode (client/server/auto)
|
||||
/// of the window.
|
||||
decoration: ?*org.KdeKwinServerDecoration = null,
|
||||
|
||||
/// Object that controls the slide-in/slide-out animations of the
|
||||
/// quick terminal. Always null for windows other than the quick terminal.
|
||||
slide: ?*org.KdeKwinSlide = null,
|
||||
|
||||
/// Object that, when present, denotes that the window is currently
|
||||
/// requesting attention from the user.
|
||||
activation_token: ?*xdg.ActivationTokenV1 = null,
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
app: *App,
|
||||
apprt_window: *ApprtWindow,
|
||||
) !Window {
|
||||
_ = alloc;
|
||||
|
||||
const gtk_native = apprt_window.window.as(gtk.Native);
|
||||
const gdk_surface = gtk_native.getSurface() orelse return error.NotWaylandSurface;
|
||||
|
||||
// This should never fail, because if we're being called at this point
|
||||
// then we've already asserted that our app state is Wayland.
|
||||
const gdk_wl_surface = gobject.ext.cast(
|
||||
gdk_wayland.WaylandSurface,
|
||||
gdk_surface,
|
||||
) orelse return error.NoWaylandSurface;
|
||||
|
||||
const wl_surface: *wl.Surface = @ptrCast(@alignCast(
|
||||
gdk_wl_surface.getWlSurface() orelse return error.NoWaylandSurface,
|
||||
));
|
||||
|
||||
// Get our decoration object so we can control the
|
||||
// CSD vs SSD status of this surface.
|
||||
const deco: ?*org.KdeKwinServerDecoration = deco: {
|
||||
const mgr = app.context.kde_decoration_manager orelse
|
||||
break :deco null;
|
||||
|
||||
const deco: *org.KdeKwinServerDecoration = mgr.create(
|
||||
wl_surface,
|
||||
) catch |err| {
|
||||
log.warn("could not create decoration object={}", .{err});
|
||||
break :deco null;
|
||||
};
|
||||
|
||||
break :deco deco;
|
||||
};
|
||||
|
||||
if (apprt_window.isQuickTerminal()) {
|
||||
_ = gdk.Surface.signals.enter_monitor.connect(
|
||||
gdk_surface,
|
||||
*ApprtWindow,
|
||||
enteredMonitor,
|
||||
apprt_window,
|
||||
.{},
|
||||
);
|
||||
}
|
||||
|
||||
return .{
|
||||
.apprt_window = apprt_window,
|
||||
.surface = wl_surface,
|
||||
.app_context = app.context,
|
||||
.decoration = deco,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Window, alloc: Allocator) void {
|
||||
_ = alloc;
|
||||
if (self.blur_token) |blur| blur.release();
|
||||
if (self.decoration) |deco| deco.release();
|
||||
if (self.slide) |slide| slide.release();
|
||||
}
|
||||
|
||||
pub fn resizeEvent(_: *Window) !void {}
|
||||
|
||||
pub fn syncAppearance(self: *Window) !void {
|
||||
self.syncBlur() catch |err| {
|
||||
log.err("failed to sync blur={}", .{err});
|
||||
};
|
||||
self.syncDecoration() catch |err| {
|
||||
log.err("failed to sync blur={}", .{err});
|
||||
};
|
||||
|
||||
if (self.apprt_window.isQuickTerminal()) {
|
||||
self.syncQuickTerminal() catch |err| {
|
||||
log.warn("failed to sync quick terminal appearance={}", .{err});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clientSideDecorationEnabled(self: Window) bool {
|
||||
return switch (self.getDecorationMode()) {
|
||||
.Client => true,
|
||||
// If we support SSDs, then we should *not* enable CSDs if we prefer SSDs.
|
||||
// However, if we do not support SSDs (e.g. GNOME) then we should enable
|
||||
// CSDs even if the user prefers SSDs.
|
||||
.Server => if (self.app_context.kde_decoration_manager) |_| false else true,
|
||||
.None => false,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
|
||||
_ = self;
|
||||
_ = env;
|
||||
}
|
||||
|
||||
pub fn setUrgent(self: *Window, urgent: bool) !void {
|
||||
const activation = self.app_context.xdg_activation orelse return;
|
||||
|
||||
// If there already is a token, destroy and unset it
|
||||
if (self.activation_token) |token| token.destroy();
|
||||
|
||||
self.activation_token = if (urgent) token: {
|
||||
const token = try activation.getActivationToken();
|
||||
token.setSurface(self.surface);
|
||||
token.setListener(*Window, onActivationTokenEvent, self);
|
||||
token.commit();
|
||||
break :token token;
|
||||
} else null;
|
||||
}
|
||||
|
||||
/// Update the blur state of the window.
|
||||
fn syncBlur(self: *Window) !void {
|
||||
const manager = self.app_context.kde_blur_manager orelse return;
|
||||
const blur = self.apprt_window.config.background_blur;
|
||||
|
||||
if (self.blur_token) |tok| {
|
||||
// Only release token when transitioning from blurred -> not blurred
|
||||
if (!blur.enabled()) {
|
||||
manager.unset(self.surface);
|
||||
tok.release();
|
||||
self.blur_token = null;
|
||||
}
|
||||
} else {
|
||||
// Only acquire token when transitioning from not blurred -> blurred
|
||||
if (blur.enabled()) {
|
||||
const tok = try manager.create(self.surface);
|
||||
tok.commit();
|
||||
self.blur_token = tok;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn syncDecoration(self: *Window) !void {
|
||||
const deco = self.decoration orelse return;
|
||||
|
||||
// The protocol requests uint instead of enum so we have
|
||||
// to convert it.
|
||||
deco.requestMode(@intCast(@intFromEnum(self.getDecorationMode())));
|
||||
}
|
||||
|
||||
fn getDecorationMode(self: Window) org.KdeKwinServerDecorationManager.Mode {
|
||||
return switch (self.apprt_window.config.window_decoration) {
|
||||
.auto => self.app_context.default_deco_mode orelse .Client,
|
||||
.client => .Client,
|
||||
.server => .Server,
|
||||
.none => .None,
|
||||
};
|
||||
}
|
||||
|
||||
fn syncQuickTerminal(self: *Window) !void {
|
||||
const window = self.apprt_window.window.as(gtk.Window);
|
||||
const config = &self.apprt_window.config;
|
||||
|
||||
layer_shell.setKeyboardMode(
|
||||
window,
|
||||
switch (config.quick_terminal_keyboard_interactivity) {
|
||||
.none => .none,
|
||||
.@"on-demand" => on_demand: {
|
||||
if (layer_shell.getProtocolVersion() < 4) {
|
||||
log.warn("your compositor does not support on-demand keyboard access; falling back to exclusive access", .{});
|
||||
break :on_demand .exclusive;
|
||||
}
|
||||
break :on_demand .on_demand;
|
||||
},
|
||||
.exclusive => .exclusive,
|
||||
},
|
||||
);
|
||||
|
||||
const anchored_edge: ?layer_shell.ShellEdge = switch (config.quick_terminal_position) {
|
||||
.left => .left,
|
||||
.right => .right,
|
||||
.top => .top,
|
||||
.bottom => .bottom,
|
||||
.center => null,
|
||||
};
|
||||
|
||||
for (std.meta.tags(layer_shell.ShellEdge)) |edge| {
|
||||
if (anchored_edge) |anchored| {
|
||||
if (edge == anchored) {
|
||||
layer_shell.setMargin(window, edge, 0);
|
||||
layer_shell.setAnchor(window, edge, true);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Arbitrary margin - could be made customizable?
|
||||
layer_shell.setMargin(window, edge, 20);
|
||||
layer_shell.setAnchor(window, edge, false);
|
||||
}
|
||||
|
||||
if (self.slide) |slide| slide.release();
|
||||
|
||||
self.slide = if (anchored_edge) |anchored| slide: {
|
||||
const mgr = self.app_context.kde_slide_manager orelse break :slide null;
|
||||
|
||||
const slide = mgr.create(self.surface) catch |err| {
|
||||
log.warn("could not create slide object={}", .{err});
|
||||
break :slide null;
|
||||
};
|
||||
|
||||
const slide_location: org.KdeKwinSlide.Location = switch (anchored) {
|
||||
.top => .top,
|
||||
.bottom => .bottom,
|
||||
.left => .left,
|
||||
.right => .right,
|
||||
};
|
||||
|
||||
slide.setLocation(@intCast(@intFromEnum(slide_location)));
|
||||
slide.commit();
|
||||
break :slide slide;
|
||||
} else null;
|
||||
}
|
||||
|
||||
/// Update the size of the quick terminal based on monitor dimensions.
|
||||
fn enteredMonitor(
|
||||
_: *gdk.Surface,
|
||||
monitor: *gdk.Monitor,
|
||||
apprt_window: *ApprtWindow,
|
||||
) callconv(.c) void {
|
||||
const window = apprt_window.window.as(gtk.Window);
|
||||
const config = &apprt_window.config;
|
||||
|
||||
var monitor_size: gdk.Rectangle = undefined;
|
||||
monitor.getGeometry(&monitor_size);
|
||||
|
||||
const dims = config.quick_terminal_size.calculate(
|
||||
config.quick_terminal_position,
|
||||
.{
|
||||
.width = @intCast(monitor_size.f_width),
|
||||
.height = @intCast(monitor_size.f_height),
|
||||
},
|
||||
);
|
||||
|
||||
window.setDefaultSize(@intCast(dims.width), @intCast(dims.height));
|
||||
}
|
||||
|
||||
fn onActivationTokenEvent(
|
||||
token: *xdg.ActivationTokenV1,
|
||||
event: xdg.ActivationTokenV1.Event,
|
||||
self: *Window,
|
||||
) void {
|
||||
const activation = self.app_context.xdg_activation orelse return;
|
||||
const current_token = self.activation_token orelse return;
|
||||
|
||||
if (token.getId() != current_token.getId()) {
|
||||
log.warn("received event for unknown activation token; ignoring", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event) {
|
||||
.done => |done| {
|
||||
activation.activate(done.token, self.surface);
|
||||
token.destroy();
|
||||
self.activation_token = null;
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
507
src/apprt/gtk-ng/winproto/x11.zig
Normal file
507
src/apprt/gtk-ng/winproto/x11.zig
Normal file
@ -0,0 +1,507 @@
|
||||
//! X11 window protocol implementation for the Ghostty GTK apprt.
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const build_options = @import("build_options");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const adw = @import("adw");
|
||||
const gdk = @import("gdk");
|
||||
const gdk_x11 = @import("gdk_x11");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
const xlib = @import("xlib");
|
||||
|
||||
pub const c = @cImport({
|
||||
@cInclude("X11/Xlib.h");
|
||||
@cInclude("X11/Xatom.h");
|
||||
@cInclude("X11/XKBlib.h");
|
||||
});
|
||||
|
||||
const input = @import("../../../input.zig");
|
||||
const Config = @import("../../../config.zig").Config;
|
||||
const ApprtWindow = void; // TODO: fix
|
||||
|
||||
const log = std.log.scoped(.gtk_x11);
|
||||
|
||||
pub const App = struct {
|
||||
display: *xlib.Display,
|
||||
base_event_code: c_int,
|
||||
atoms: Atoms,
|
||||
|
||||
pub fn init(
|
||||
_: Allocator,
|
||||
gdk_display: *gdk.Display,
|
||||
app_id: [:0]const u8,
|
||||
config: *const Config,
|
||||
) !?App {
|
||||
// If the display isn't X11, then we don't need to do anything.
|
||||
const gdk_x11_display = gobject.ext.cast(
|
||||
gdk_x11.X11Display,
|
||||
gdk_display,
|
||||
) orelse return null;
|
||||
|
||||
const xlib_display = gdk_x11_display.getXdisplay();
|
||||
|
||||
const x11_program_name: [:0]const u8 = if (config.@"x11-instance-name") |pn|
|
||||
pn
|
||||
else if (builtin.mode == .Debug)
|
||||
"ghostty-debug"
|
||||
else
|
||||
"ghostty";
|
||||
|
||||
// Set the X11 window class property (WM_CLASS) if are are on an X11
|
||||
// display.
|
||||
//
|
||||
// Note that we also set the program name here using g_set_prgname.
|
||||
// This is how the instance name field for WM_CLASS is derived when
|
||||
// calling gdk_x11_display_set_program_class; there does not seem to be
|
||||
// a way to set it directly. It does not look like this is being set by
|
||||
// our other app initialization routines currently, but since we're
|
||||
// currently deriving its value from x11-instance-name effectively, I
|
||||
// feel like gating it behind an X11 check is better intent.
|
||||
//
|
||||
// This makes the property show up like so when using xprop:
|
||||
//
|
||||
// WM_CLASS(STRING) = "ghostty", "com.mitchellh.ghostty"
|
||||
//
|
||||
// Append "-debug" on both when using the debug build.
|
||||
glib.setPrgname(x11_program_name);
|
||||
gdk_x11.X11Display.setProgramClass(gdk_display, app_id);
|
||||
|
||||
// XKB
|
||||
log.debug("Xkb.init: initializing Xkb", .{});
|
||||
log.debug("Xkb.init: running XkbQueryExtension", .{});
|
||||
var opcode: c_int = 0;
|
||||
var base_event_code: c_int = 0;
|
||||
var base_error_code: c_int = 0;
|
||||
var major = c.XkbMajorVersion;
|
||||
var minor = c.XkbMinorVersion;
|
||||
if (c.XkbQueryExtension(
|
||||
@ptrCast(@alignCast(xlib_display)),
|
||||
&opcode,
|
||||
&base_event_code,
|
||||
&base_error_code,
|
||||
&major,
|
||||
&minor,
|
||||
) == 0) {
|
||||
log.err("Fatal: error initializing Xkb extension: error executing XkbQueryExtension", .{});
|
||||
return error.XkbInitializationError;
|
||||
}
|
||||
|
||||
log.debug("Xkb.init: running XkbSelectEventDetails", .{});
|
||||
if (c.XkbSelectEventDetails(
|
||||
@ptrCast(@alignCast(xlib_display)),
|
||||
c.XkbUseCoreKbd,
|
||||
c.XkbStateNotify,
|
||||
c.XkbModifierStateMask,
|
||||
c.XkbModifierStateMask,
|
||||
) == 0) {
|
||||
log.err("Fatal: error initializing Xkb extension: error executing XkbSelectEventDetails", .{});
|
||||
return error.XkbInitializationError;
|
||||
}
|
||||
|
||||
return .{
|
||||
.display = xlib_display,
|
||||
.base_event_code = base_event_code,
|
||||
.atoms = .init(gdk_x11_display),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App, alloc: Allocator) void {
|
||||
_ = self;
|
||||
_ = alloc;
|
||||
}
|
||||
|
||||
/// Checks for an immediate pending XKB state update event, and returns the
|
||||
/// keyboard state based on if it finds any. This is necessary as the
|
||||
/// standard GTK X11 API (and X11 in general) does not include the current
|
||||
/// key pressed in any modifier state snapshot for that event (e.g. if the
|
||||
/// pressed key is a modifier, that is not necessarily reflected in the
|
||||
/// modifiers).
|
||||
///
|
||||
/// Returns null if there is no event. In this case, the caller should fall
|
||||
/// back to the standard GDK modifier state (this likely means the key
|
||||
/// event did not result in a modifier change).
|
||||
pub fn eventMods(
|
||||
self: App,
|
||||
device: ?*gdk.Device,
|
||||
gtk_mods: gdk.ModifierType,
|
||||
) ?input.Mods {
|
||||
_ = device;
|
||||
_ = gtk_mods;
|
||||
|
||||
// Shoutout to Mozilla for figuring out a clean way to do this, this is
|
||||
// paraphrased from Firefox/Gecko in widget/gtk/nsGtkKeyUtils.cpp.
|
||||
if (c.XEventsQueued(
|
||||
@ptrCast(@alignCast(self.display)),
|
||||
c.QueuedAfterReading,
|
||||
) == 0) return null;
|
||||
|
||||
var nextEvent: c.XEvent = undefined;
|
||||
_ = c.XPeekEvent(@ptrCast(@alignCast(self.display)), &nextEvent);
|
||||
if (nextEvent.type != self.base_event_code) return null;
|
||||
|
||||
const xkb_event: *c.XkbEvent = @ptrCast(&nextEvent);
|
||||
if (xkb_event.any.xkb_type != c.XkbStateNotify) return null;
|
||||
|
||||
const xkb_state_notify_event: *c.XkbStateNotifyEvent = @ptrCast(xkb_event);
|
||||
// Check the state according to XKB masks.
|
||||
const lookup_mods = xkb_state_notify_event.lookup_mods;
|
||||
var mods: input.Mods = .{};
|
||||
|
||||
log.debug("X11: found extra XkbStateNotify event w/lookup_mods: {b}", .{lookup_mods});
|
||||
if (lookup_mods & c.ShiftMask != 0) mods.shift = true;
|
||||
if (lookup_mods & c.ControlMask != 0) mods.ctrl = true;
|
||||
if (lookup_mods & c.Mod1Mask != 0) mods.alt = true;
|
||||
if (lookup_mods & c.Mod4Mask != 0) mods.super = true;
|
||||
if (lookup_mods & c.LockMask != 0) mods.caps_lock = true;
|
||||
|
||||
return mods;
|
||||
}
|
||||
|
||||
pub fn supportsQuickTerminal(_: App) bool {
|
||||
log.warn("quick terminal is not yet supported on X11", .{});
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn initQuickTerminal(_: *App, _: *ApprtWindow) !void {}
|
||||
};
|
||||
|
||||
pub const Window = struct {
|
||||
app: *App,
|
||||
config: *const ApprtWindow.DerivedConfig,
|
||||
gtk_window: *adw.ApplicationWindow,
|
||||
x11_surface: *gdk_x11.X11Surface,
|
||||
|
||||
blur_region: Region = .{},
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
app: *App,
|
||||
apprt_window: *ApprtWindow,
|
||||
) !Window {
|
||||
_ = alloc;
|
||||
|
||||
const surface = apprt_window.window.as(
|
||||
gtk.Native,
|
||||
).getSurface() orelse return error.NotX11Surface;
|
||||
|
||||
const x11_surface = gobject.ext.cast(
|
||||
gdk_x11.X11Surface,
|
||||
surface,
|
||||
) orelse return error.NotX11Surface;
|
||||
|
||||
return .{
|
||||
.app = app,
|
||||
.config = &apprt_window.config,
|
||||
.gtk_window = apprt_window.window,
|
||||
.x11_surface = x11_surface,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Window, alloc: Allocator) void {
|
||||
_ = self;
|
||||
_ = alloc;
|
||||
}
|
||||
|
||||
pub fn resizeEvent(self: *Window) !void {
|
||||
// The blur region must update with window resizes
|
||||
try self.syncBlur();
|
||||
}
|
||||
|
||||
pub fn syncAppearance(self: *Window) !void {
|
||||
// The user could have toggled between CSDs and SSDs,
|
||||
// therefore we need to recalculate the blur region offset.
|
||||
self.blur_region = blur: {
|
||||
// NOTE(pluiedev): CSDs are a f--king mistake.
|
||||
// Please, GNOME, stop this nonsense of making a window ~30% bigger
|
||||
// internally than how they really are just for your shadows and
|
||||
// rounded corners and all that fluff. Please. I beg of you.
|
||||
var x: f64 = 0;
|
||||
var y: f64 = 0;
|
||||
|
||||
self.gtk_window.as(gtk.Native).getSurfaceTransform(&x, &y);
|
||||
|
||||
// Transform surface coordinates to device coordinates.
|
||||
const scale: f64 = @floatFromInt(self.gtk_window.as(gtk.Widget).getScaleFactor());
|
||||
x *= scale;
|
||||
y *= scale;
|
||||
|
||||
break :blur .{
|
||||
.x = @intFromFloat(x),
|
||||
.y = @intFromFloat(y),
|
||||
};
|
||||
};
|
||||
self.syncBlur() catch |err| {
|
||||
log.err("failed to synchronize blur={}", .{err});
|
||||
};
|
||||
self.syncDecorations() catch |err| {
|
||||
log.err("failed to synchronize decorations={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
pub fn clientSideDecorationEnabled(self: Window) bool {
|
||||
return switch (self.config.window_decoration) {
|
||||
.auto, .client => true,
|
||||
.server, .none => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn syncBlur(self: *Window) !void {
|
||||
// FIXME: This doesn't currently factor in rounded corners on Adwaita,
|
||||
// which means that the blur region will grow slightly outside of the
|
||||
// window borders. Unfortunately, actually calculating the rounded
|
||||
// region can be quite complex without having access to existing APIs
|
||||
// (cf. https://github.com/cutefishos/fishui/blob/41d4ba194063a3c7fff4675619b57e6ac0504f06/src/platforms/linux/blurhelper/windowblur.cpp#L134)
|
||||
// and I think it's not really noticeable enough to justify the effort.
|
||||
// (Wayland also has this visual artifact anyway...)
|
||||
|
||||
const gtk_widget = self.gtk_window.as(gtk.Widget);
|
||||
|
||||
// Transform surface coordinates to device coordinates.
|
||||
const scale = self.gtk_window.as(gtk.Widget).getScaleFactor();
|
||||
self.blur_region.width = gtk_widget.getWidth() * scale;
|
||||
self.blur_region.height = gtk_widget.getHeight() * scale;
|
||||
|
||||
const blur = self.config.background_blur;
|
||||
log.debug("set blur={}, window xid={}, region={}", .{
|
||||
blur,
|
||||
self.x11_surface.getXid(),
|
||||
self.blur_region,
|
||||
});
|
||||
|
||||
if (blur.enabled()) {
|
||||
try self.changeProperty(
|
||||
Region,
|
||||
self.app.atoms.kde_blur,
|
||||
c.XA_CARDINAL,
|
||||
._32,
|
||||
.{ .mode = .replace },
|
||||
&self.blur_region,
|
||||
);
|
||||
} else {
|
||||
try self.deleteProperty(self.app.atoms.kde_blur);
|
||||
}
|
||||
}
|
||||
|
||||
fn syncDecorations(self: *Window) !void {
|
||||
var hints: MotifWMHints = .{};
|
||||
|
||||
self.getWindowProperty(
|
||||
MotifWMHints,
|
||||
self.app.atoms.motif_wm_hints,
|
||||
self.app.atoms.motif_wm_hints,
|
||||
._32,
|
||||
.{},
|
||||
&hints,
|
||||
) catch |err| switch (err) {
|
||||
// motif_wm_hints is already initialized, so this is fine
|
||||
error.PropertyNotFound => {},
|
||||
|
||||
error.RequestFailed,
|
||||
error.PropertyTypeMismatch,
|
||||
error.PropertyFormatMismatch,
|
||||
=> return err,
|
||||
};
|
||||
|
||||
hints.flags.decorations = true;
|
||||
hints.decorations.all = switch (self.config.window_decoration) {
|
||||
.server => true,
|
||||
.auto, .client, .none => false,
|
||||
};
|
||||
|
||||
try self.changeProperty(
|
||||
MotifWMHints,
|
||||
self.app.atoms.motif_wm_hints,
|
||||
self.app.atoms.motif_wm_hints,
|
||||
._32,
|
||||
.{ .mode = .replace },
|
||||
&hints,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
|
||||
var buf: [64]u8 = undefined;
|
||||
const window_id = try std.fmt.bufPrint(
|
||||
&buf,
|
||||
"{}",
|
||||
.{self.x11_surface.getXid()},
|
||||
);
|
||||
|
||||
try env.put("WINDOWID", window_id);
|
||||
}
|
||||
|
||||
pub fn setUrgent(self: *Window, urgent: bool) !void {
|
||||
self.x11_surface.setUrgencyHint(@intFromBool(urgent));
|
||||
}
|
||||
|
||||
fn getWindowProperty(
|
||||
self: *Window,
|
||||
comptime T: type,
|
||||
name: c.Atom,
|
||||
typ: c.Atom,
|
||||
comptime format: PropertyFormat,
|
||||
options: struct {
|
||||
offset: c_long = 0,
|
||||
length: c_long = std.math.maxInt(c_long),
|
||||
delete: bool = false,
|
||||
},
|
||||
result: *T,
|
||||
) GetWindowPropertyError!void {
|
||||
// FIXME: Maybe we should switch to libxcb one day.
|
||||
// Sounds like a much better idea than whatever this is
|
||||
var actual_type_return: c.Atom = undefined;
|
||||
var actual_format_return: c_int = undefined;
|
||||
var nitems_return: c_ulong = undefined;
|
||||
var bytes_after_return: c_ulong = undefined;
|
||||
var prop_return: ?format.bufferType() = null;
|
||||
|
||||
const code = c.XGetWindowProperty(
|
||||
@ptrCast(@alignCast(self.app.display)),
|
||||
self.x11_surface.getXid(),
|
||||
name,
|
||||
options.offset,
|
||||
options.length,
|
||||
@intFromBool(options.delete),
|
||||
typ,
|
||||
&actual_type_return,
|
||||
&actual_format_return,
|
||||
&nitems_return,
|
||||
&bytes_after_return,
|
||||
@ptrCast(&prop_return),
|
||||
);
|
||||
if (code != c.Success) return error.RequestFailed;
|
||||
|
||||
if (actual_type_return == c.None) return error.PropertyNotFound;
|
||||
if (typ != actual_type_return) return error.PropertyTypeMismatch;
|
||||
if (@intFromEnum(format) != actual_format_return) return error.PropertyFormatMismatch;
|
||||
|
||||
const data_ptr: *T = @ptrCast(prop_return);
|
||||
result.* = data_ptr.*;
|
||||
_ = c.XFree(prop_return);
|
||||
}
|
||||
|
||||
fn changeProperty(
|
||||
self: *Window,
|
||||
comptime T: type,
|
||||
name: c.Atom,
|
||||
typ: c.Atom,
|
||||
comptime format: PropertyFormat,
|
||||
options: struct {
|
||||
mode: PropertyChangeMode,
|
||||
},
|
||||
value: *T,
|
||||
) X11Error!void {
|
||||
const data: format.bufferType() = @ptrCast(value);
|
||||
|
||||
const status = c.XChangeProperty(
|
||||
@ptrCast(@alignCast(self.app.display)),
|
||||
self.x11_surface.getXid(),
|
||||
name,
|
||||
typ,
|
||||
@intFromEnum(format),
|
||||
@intFromEnum(options.mode),
|
||||
data,
|
||||
@divExact(@sizeOf(T), @sizeOf(format.elemType())),
|
||||
);
|
||||
|
||||
// For some godforsaken reason Xlib alternates between
|
||||
// error values (0 = success) and booleans (1 = success), and they look exactly
|
||||
// the same in the signature (just `int`, since Xlib is written in C89)...
|
||||
if (status == 0) return error.RequestFailed;
|
||||
}
|
||||
|
||||
fn deleteProperty(self: *Window, name: c.Atom) X11Error!void {
|
||||
const status = c.XDeleteProperty(
|
||||
@ptrCast(@alignCast(self.app.display)),
|
||||
self.x11_surface.getXid(),
|
||||
name,
|
||||
);
|
||||
if (status == 0) return error.RequestFailed;
|
||||
}
|
||||
};
|
||||
|
||||
const X11Error = error{
|
||||
RequestFailed,
|
||||
};
|
||||
|
||||
const GetWindowPropertyError = X11Error || error{
|
||||
PropertyNotFound,
|
||||
PropertyTypeMismatch,
|
||||
PropertyFormatMismatch,
|
||||
};
|
||||
|
||||
const Atoms = struct {
|
||||
kde_blur: c.Atom,
|
||||
motif_wm_hints: c.Atom,
|
||||
|
||||
fn init(display: *gdk_x11.X11Display) Atoms {
|
||||
return .{
|
||||
.kde_blur = gdk_x11.x11GetXatomByNameForDisplay(
|
||||
display,
|
||||
"_KDE_NET_WM_BLUR_BEHIND_REGION",
|
||||
),
|
||||
.motif_wm_hints = gdk_x11.x11GetXatomByNameForDisplay(
|
||||
display,
|
||||
"_MOTIF_WM_HINTS",
|
||||
),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const PropertyChangeMode = enum(c_int) {
|
||||
replace = c.PropModeReplace,
|
||||
prepend = c.PropModePrepend,
|
||||
append = c.PropModeAppend,
|
||||
};
|
||||
|
||||
const PropertyFormat = enum(c_int) {
|
||||
_8 = 8,
|
||||
_16 = 16,
|
||||
_32 = 32,
|
||||
|
||||
fn elemType(comptime self: PropertyFormat) type {
|
||||
return switch (self) {
|
||||
._8 => c_char,
|
||||
._16 => c_int,
|
||||
._32 => c_long,
|
||||
};
|
||||
}
|
||||
|
||||
fn bufferType(comptime self: PropertyFormat) type {
|
||||
// The buffer type has to be a multi-pointer to bytes
|
||||
// *aligned to the element type* (very important,
|
||||
// otherwise you'll read garbage!)
|
||||
//
|
||||
// I know this is really ugly. X11 is ugly. I consider it apropos.
|
||||
return [*]align(@alignOf(self.elemType())) u8;
|
||||
}
|
||||
};
|
||||
|
||||
const Region = extern struct {
|
||||
x: c_long = 0,
|
||||
y: c_long = 0,
|
||||
width: c_long = 0,
|
||||
height: c_long = 0,
|
||||
};
|
||||
|
||||
// See Xm/MwmUtil.h, packaged with the Motif Window Manager
|
||||
const MotifWMHints = extern struct {
|
||||
flags: packed struct(c_ulong) {
|
||||
_pad: u1 = 0,
|
||||
decorations: bool = false,
|
||||
|
||||
// We don't really care about the other flags
|
||||
_rest: std.meta.Int(.unsigned, @bitSizeOf(c_ulong) - 2) = 0,
|
||||
} = .{},
|
||||
functions: c_ulong = 0,
|
||||
decorations: packed struct(c_ulong) {
|
||||
all: bool = false,
|
||||
|
||||
// We don't really care about the other flags
|
||||
_rest: std.meta.Int(.unsigned, @bitSizeOf(c_ulong) - 1) = 0,
|
||||
} = .{},
|
||||
input_mode: c_long = 0,
|
||||
status: c_ulong = 0,
|
||||
};
|
@ -310,7 +310,7 @@ fn drainMailbox(
|
||||
// If we have a message we always redraw
|
||||
redraw = true;
|
||||
|
||||
log.debug("mailbox message={}", .{message});
|
||||
log.debug("mailbox message={s}", .{@tagName(message)});
|
||||
switch (message) {
|
||||
.crash => @panic("crash request, crashing intentionally"),
|
||||
.change_config => |config| {
|
||||
|
Reference in New Issue
Block a user