mirror-ghostty/src/apprt/gtk/ResizeOverlay.zig

207 lines
6.5 KiB
Zig

const ResizeOverlay = @This();
const std = @import("std");
const glib = @import("glib");
const gtk = @import("gtk");
const configpkg = @import("../../config.zig");
const Surface = @import("Surface.zig");
const log = std.log.scoped(.gtk);
/// local copy of configuration data
const DerivedConfig = struct {
resize_overlay: configpkg.Config.ResizeOverlay,
resize_overlay_position: configpkg.Config.ResizeOverlayPosition,
resize_overlay_duration: configpkg.Config.Duration,
pub fn init(config: *const configpkg.Config) DerivedConfig {
return .{
.resize_overlay = config.@"resize-overlay",
.resize_overlay_position = config.@"resize-overlay-position",
.resize_overlay_duration = config.@"resize-overlay-duration",
};
}
};
/// the surface that we are attached to
surface: *Surface,
/// a copy of the configuration that we need to operate
config: DerivedConfig,
/// If non-null this is the widget on the overlay that shows the size of the
/// surface when it is resized.
label: ?*gtk.Label = null,
/// If non-null this is a timer for dismissing the resize overlay.
timer: ?c_uint = null,
/// If non-null this is a timer for dismissing the resize overlay.
idler: ?c_uint = null,
/// If true, the next resize event will be the first one.
first: bool = true,
/// Initialize the ResizeOverlay. This doesn't do anything more than save a
/// pointer to the surface that we are a part of as all of the widget creation
/// is done later.
pub fn init(self: *ResizeOverlay, surface: *Surface, config: *const configpkg.Config) void {
self.* = .{
.surface = surface,
.config = .init(config),
};
}
pub fn updateConfig(self: *ResizeOverlay, config: *const configpkg.Config) void {
self.config = .init(config);
}
/// De-initialize the ResizeOverlay. This removes any pending idlers/timers that
/// may not have fired yet.
pub fn deinit(self: *ResizeOverlay) void {
if (self.idler) |idler| {
if (glib.Source.remove(idler) == 0) {
log.warn("unable to remove resize overlay idler", .{});
}
self.idler = null;
}
if (self.timer) |timer| {
if (glib.Source.remove(timer) == 0) {
log.warn("unable to remove resize overlay timer", .{});
}
self.timer = null;
}
}
/// If we're configured to do so, update the text in the resize overlay widget
/// and make it visible. Schedule a timer to hide the widget after the delay
/// expires.
///
/// If we're not configured to show the overlay, do nothing.
pub fn maybeShow(self: *ResizeOverlay) void {
switch (self.config.resize_overlay) {
.never => return,
.always => {},
.@"after-first" => if (self.first) {
self.first = false;
return;
},
}
self.first = false;
// When updating a widget, wait until GTK is "idle", i.e. not in the middle
// of doing any other updates. Since we are called in the middle of resizing
// GTK is doing a lot of work rearranging all of the widgets. Not doing this
// results in a lot of warnings from GTK and _horrible_ flickering of the
// resize overlay.
if (self.idler != null) return;
self.idler = glib.idleAdd(gtkUpdate, self);
}
/// Actually update the overlay widget. This should only be called from a GTK
/// idle handler.
fn gtkUpdate(ud: ?*anyopaque) callconv(.c) c_int {
const self: *ResizeOverlay = @ptrCast(@alignCast(ud orelse return 0));
// No matter what our idler is complete with this callback
self.idler = null;
const grid_size = self.surface.core_surface.size.grid();
var buf: [32]u8 = undefined;
const text = std.fmt.bufPrintZ(
&buf,
"{d} x {d}",
.{
grid_size.columns,
grid_size.rows,
},
) catch |err| {
log.err("unable to format text: {}", .{err});
return 0;
};
if (self.label) |label| {
// The resize overlay widget already exists, just update it.
label.setText(text.ptr);
setPosition(label, &self.config);
show(label);
} else {
// Create the resize overlay widget.
const label = gtk.Label.new(text.ptr);
label.setJustify(gtk.Justification.center);
label.setSelectable(0);
setPosition(label, &self.config);
const widget = label.as(gtk.Widget);
widget.addCssClass("view");
widget.addCssClass("size-overlay");
widget.setFocusable(0);
widget.setCanTarget(0);
const overlay: *gtk.Overlay = @ptrCast(@alignCast(self.surface.overlay));
overlay.addOverlay(widget);
self.label = label;
}
if (self.timer) |timer| {
if (glib.Source.remove(timer) == 0) {
log.warn("unable to remove size overlay timer", .{});
}
}
self.timer = glib.timeoutAdd(
self.surface.app.config.@"resize-overlay-duration".asMilliseconds(),
gtkTimerExpired,
self,
);
return 0;
}
// This should only be called from a GTK idle handler or timer.
fn show(label: *gtk.Label) void {
const widget = label.as(gtk.Widget);
widget.removeCssClass("hidden");
}
// This should only be called from a GTK idle handler or timer.
fn hide(label: *gtk.Label) void {
const widget = label.as(gtk.Widget);
widget.addCssClass("hidden");
}
/// Update the position of the resize overlay widget. It might seem excessive to
/// do this often, but it should make hot config reloading of the position work.
/// This should only be called from a GTK idle handler.
fn setPosition(label: *gtk.Label, config: *DerivedConfig) void {
const widget = label.as(gtk.Widget);
widget.setHalign(
switch (config.resize_overlay_position) {
.center, .@"top-center", .@"bottom-center" => gtk.Align.center,
.@"top-left", .@"bottom-left" => gtk.Align.start,
.@"top-right", .@"bottom-right" => gtk.Align.end,
},
);
widget.setValign(
switch (config.resize_overlay_position) {
.center => gtk.Align.center,
.@"top-left", .@"top-center", .@"top-right" => gtk.Align.start,
.@"bottom-left", .@"bottom-center", .@"bottom-right" => gtk.Align.end,
},
);
}
/// If this fires, it means that the delay period has expired and the resize
/// overlay widget should be hidden.
fn gtkTimerExpired(ud: ?*anyopaque) callconv(.c) c_int {
const self: *ResizeOverlay = @ptrCast(@alignCast(ud orelse return 0));
self.timer = null;
if (self.label) |label| hide(label);
return 0;
}