mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-28 02:03:55 +00:00
343 lines
11 KiB
Zig
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;
|
|
};
|
|
};
|