Files
ghostty/src/apprt/gtk/class/key_state_overlay.zig
2025-12-23 11:06:10 -08:00

343 lines
11 KiB
Zig

const std = @import("std");
const adw = @import("adw");
const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");
const ext = @import("../ext.zig");
const gresource = @import("../build/gresource.zig");
const Application = @import("application.zig").Application;
const Common = @import("../class.zig").Common;
const log = std.log.scoped(.gtk_ghostty_key_state_overlay);
/// An overlay that displays the current key table stack and pending key sequence.
/// This helps users understand what key bindings are active and what keys they've
/// pressed in a multi-key sequence.
pub const KeyStateOverlay = extern struct {
const Self = @This();
parent_instance: Parent,
pub const Parent = adw.Bin;
pub const getGObjectType = gobject.ext.defineClass(Self, .{
.name = "GhosttyKeyStateOverlay",
.instanceInit = &init,
.classInit = &Class.init,
.parent_class = &Class.parent,
.private = .{ .Type = Private, .offset = &Private.offset },
});
pub const properties = struct {
pub const tables = struct {
pub const name = "tables";
const impl = gobject.ext.defineProperty(
name,
Self,
?*ext.StringList,
.{
.accessor = gobject.ext.typedAccessor(
Self,
?*ext.StringList,
.{
.getter = getTables,
.getter_transfer = .none,
.setter = setTables,
.setter_transfer = .full,
},
),
},
);
};
pub const @"has-tables" = struct {
pub const name = "has-tables";
const impl = gobject.ext.defineProperty(
name,
Self,
bool,
.{
.default = false,
.accessor = gobject.ext.typedAccessor(
Self,
bool,
.{ .getter = getHasTables },
),
},
);
};
pub const sequence = struct {
pub const name = "sequence";
const impl = gobject.ext.defineProperty(
name,
Self,
?*ext.StringList,
.{
.accessor = gobject.ext.typedAccessor(
Self,
?*ext.StringList,
.{
.getter = getSequence,
.getter_transfer = .none,
.setter = setSequence,
.setter_transfer = .full,
},
),
},
);
};
pub const @"has-sequence" = struct {
pub const name = "has-sequence";
const impl = gobject.ext.defineProperty(
name,
Self,
bool,
.{
.default = false,
.accessor = gobject.ext.typedAccessor(
Self,
bool,
.{ .getter = getHasSequence },
),
},
);
};
pub const @"valign-target" = struct {
pub const name = "valign-target";
const impl = gobject.ext.defineProperty(
name,
Self,
gtk.Align,
.{
.default = .end,
.accessor = C.privateShallowFieldAccessor("valign_target"),
},
);
};
};
const Private = struct {
/// The key table stack.
tables: ?*ext.StringList = null,
/// The key sequence.
sequence: ?*ext.StringList = null,
/// Target vertical alignment for the overlay.
valign_target: gtk.Align = .end,
pub var offset: c_int = 0;
};
fn init(self: *Self, _: *Class) callconv(.c) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
}
fn getTables(self: *Self) ?*ext.StringList {
return self.private().tables;
}
fn getSequence(self: *Self) ?*ext.StringList {
return self.private().sequence;
}
fn setTables(self: *Self, value: ?*ext.StringList) void {
const priv = self.private();
if (priv.tables) |old| {
old.destroy();
priv.tables = null;
}
if (value) |v| {
priv.tables = v;
}
self.as(gobject.Object).notifyByPspec(properties.tables.impl.param_spec);
self.as(gobject.Object).notifyByPspec(properties.@"has-tables".impl.param_spec);
}
fn setSequence(self: *Self, value: ?*ext.StringList) void {
const priv = self.private();
if (priv.sequence) |old| {
old.destroy();
priv.sequence = null;
}
if (value) |v| {
priv.sequence = v;
}
self.as(gobject.Object).notifyByPspec(properties.sequence.impl.param_spec);
self.as(gobject.Object).notifyByPspec(properties.@"has-sequence".impl.param_spec);
}
fn getHasTables(self: *Self) bool {
const v = self.private().tables orelse return false;
return v.strings.len > 0;
}
fn getHasSequence(self: *Self) bool {
const v = self.private().sequence orelse return false;
return v.strings.len > 0;
}
fn closureShowChevron(
_: *Self,
has_tables: bool,
has_sequence: bool,
) callconv(.c) c_int {
return if (has_tables and has_sequence) 1 else 0;
}
fn closureHasState(
_: *Self,
has_tables: bool,
has_sequence: bool,
) callconv(.c) c_int {
return if (has_tables or has_sequence) 1 else 0;
}
fn closureTablesText(
_: *Self,
tables: ?*ext.StringList,
) callconv(.c) ?[*:0]const u8 {
const list = tables orelse return null;
if (list.strings.len == 0) return null;
var buf: std.Io.Writer.Allocating = .init(Application.default().allocator());
defer buf.deinit();
for (list.strings, 0..) |s, i| {
if (i > 0) buf.writer.writeAll(" > ") catch return null;
buf.writer.writeAll(s) catch return null;
}
return glib.ext.dupeZ(u8, buf.written());
}
fn closureSequenceText(
_: *Self,
sequence: ?*ext.StringList,
) callconv(.c) ?[*:0]const u8 {
const list = sequence orelse return null;
if (list.strings.len == 0) return null;
var buf: std.Io.Writer.Allocating = .init(Application.default().allocator());
defer buf.deinit();
for (list.strings, 0..) |s, i| {
if (i > 0) buf.writer.writeAll(" ") catch return null;
buf.writer.writeAll(s) catch return null;
}
return glib.ext.dupeZ(u8, buf.written());
}
//---------------------------------------------------------------
// Template callbacks
fn onDragEnd(
_: *gtk.GestureDrag,
_: f64,
offset_y: f64,
self: *Self,
) callconv(.c) void {
// Key state overlay only moves between top-center and bottom-center.
// Horizontal alignment is always center.
const priv = self.private();
const widget = self.as(gtk.Widget);
const parent = widget.getParent() orelse return;
const parent_height: f64 = @floatFromInt(parent.getAllocatedHeight());
const self_height: f64 = @floatFromInt(widget.getAllocatedHeight());
const self_y: f64 = if (priv.valign_target == .start) 0 else parent_height - self_height;
const new_y = self_y + offset_y + (self_height / 2);
const new_valign: gtk.Align = if (new_y > parent_height / 2) .end else .start;
if (new_valign != priv.valign_target) {
priv.valign_target = new_valign;
self.as(gobject.Object).notifyByPspec(properties.@"valign-target".impl.param_spec);
self.as(gtk.Widget).queueResize();
}
}
//---------------------------------------------------------------
// Virtual methods
fn dispose(self: *Self) callconv(.c) void {
gtk.Widget.disposeTemplate(
self.as(gtk.Widget),
getGObjectType(),
);
gobject.Object.virtual_methods.dispose.call(
Class.parent,
self.as(Parent),
);
}
fn finalize(self: *Self) callconv(.c) void {
const priv = self.private();
if (priv.tables) |v| {
v.destroy();
}
if (priv.sequence) |v| {
v.destroy();
}
gobject.Object.virtual_methods.finalize.call(
Class.parent,
self.as(Parent),
);
}
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
pub const unref = C.unref;
const private = C.private;
pub const Class = extern struct {
parent_class: Parent.Class,
var parent: *Parent.Class = undefined;
pub const Instance = Self;
fn init(class: *Class) callconv(.c) void {
gtk.Widget.Class.setTemplateFromResource(
class.as(gtk.Widget.Class),
comptime gresource.blueprint(.{
.major = 1,
.minor = 2,
.name = "key-state-overlay",
}),
);
// Template Callbacks
class.bindTemplateCallback("on_drag_end", &onDragEnd);
class.bindTemplateCallback("show_chevron", &closureShowChevron);
class.bindTemplateCallback("has_state", &closureHasState);
class.bindTemplateCallback("tables_text", &closureTablesText);
class.bindTemplateCallback("sequence_text", &closureSequenceText);
// Properties
gobject.ext.registerProperties(class, &.{
properties.tables.impl,
properties.@"has-tables".impl,
properties.sequence.impl,
properties.@"has-sequence".impl,
properties.@"valign-target".impl,
});
// Virtual methods
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
gobject.Object.virtual_methods.finalize.implement(class, &finalize);
}
pub const as = C.Class.as;
pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
pub const bindTemplateCallback = C.Class.bindTemplateCallback;
};
};