Skip to content

Commit

Permalink
gtk: add resize overlay
Browse files Browse the repository at this point in the history
This adds a transient overlay that shows the size of the surface
while you are resizing the window or the surfaces.
  • Loading branch information
jcollie committed Aug 10, 2024
1 parent be88815 commit 301826d
Show file tree
Hide file tree
Showing 4 changed files with 267 additions and 7 deletions.
8 changes: 1 addition & 7 deletions src/apprt/gtk/App.zig
Original file line number Diff line number Diff line change
Expand Up @@ -531,13 +531,7 @@ pub fn startQuitTimer(self: *App) void {

if (self.config.@"quit-after-last-window-closed-delay") |v| {
// If a delay is configured, set a timeout function to quit after the delay.
const ms: u64 = std.math.divTrunc(
u64,
v.duration,
std.time.ns_per_ms,
) catch std.math.maxInt(c.guint);
const t = std.math.cast(c.guint, ms) orelse std.math.maxInt(c.guint);
self.quit_timer = .{ .active = c.g_timeout_add(t, gtkQuitTimerExpired, self) };
self.quit_timer = .{ .active = c.g_timeout_add(v.asMilliseconds(), gtkQuitTimerExpired, self) };
} else {
// If no delay is configured, treat it as expired.
self.quit_timer = .{ .expired = {} };
Expand Down
157 changes: 157 additions & 0 deletions src/apprt/gtk/Surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,19 @@ gl_area: *c.GtkGLArea,
/// If non-null this is the widget on the overlay that shows the URL.
url_widget: ?URLWidget = null,

/// If non-null this is the widget on the overlay that shows the size of the
/// surface when it is resized.
resize_overlay_widget: ?*c.GtkWidget = null,

/// If non-null this is a timer for dismissing the resize overlay.
resize_overlay_timer: ?c.guint = null,

/// If non-null this is a timer for dismissing the resize overlay.
resize_overlay_idler: ?c.guint = null,

/// If true, the next resize event will be the first one.
resize_overlay_first: bool = true,

/// If non-null this is the widget on the overlay which dims the surface when it is unfocused
unfocused_widget: ?*c.GtkWidget = null,

Expand Down Expand Up @@ -456,6 +469,12 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
break :font_size parent.font_size;
};

const resize_overlay_widget = maybeCreateResizeOverlay(&app.config);

if (resize_overlay_widget) |widget| {
c.gtk_overlay_add_overlay(@ptrCast(overlay), @ptrCast(widget));
}

// If the parent has a transient cgroup, then we're creating cgroups
// for each surface if we can. We need to create a child cgroup.
const cgroup_path: ?[]const u8 = cgroup: {
Expand Down Expand Up @@ -500,6 +519,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
.cursor_pos = .{ .x = 0, .y = 0 },
.im_context = im_context,
.cgroup_path = cgroup_path,
.resize_overlay_widget = resize_overlay_widget,
};
errdefer self.* = undefined;

Expand Down Expand Up @@ -597,6 +617,24 @@ pub fn deinit(self: *Surface) void {
c.gtk_overlay_remove_overlay(self.overlay, widget);
self.unfocused_widget = null;
}
if (self.resize_overlay_idler) |idler| {
if (c.g_source_remove(idler) == c.FALSE) {
log.warn("unable to remove resize overlay idler", .{});
}
}
if (self.resize_overlay_timer) |timer| {
if (c.g_source_remove(timer) == c.FALSE) {
log.warn("unable to remove resize overlay timer", .{});
}
}

// This probably _should_ be uncommented, but I get segfaults if I do, and
// none if I dont. ¯\_(ツ)_/¯

// if (self.resize_overlay_widget) |widget| {
// c.gtk_overlay_remove_overlay(self.overlay, widget);
// self.resize_overlay_widget = null;
// }
}

// unref removes the long-held reference to the gl_area and kicks off the
Expand Down Expand Up @@ -1299,6 +1337,8 @@ fn gtkResize(area: *c.GtkGLArea, width: c.gint, height: c.gint, ud: ?*anyopaque)
log.err("error in size callback err={}", .{err});
return;
};

self.maybeShowResizeOverlay();
}
}

Expand Down Expand Up @@ -1974,3 +2014,120 @@ fn translateMods(state: c.GdkModifierType) input.Mods {
if (state & c.GDK_LOCK_MASK != 0) mods.caps_lock = true;
return mods;
}

/// If we're configured to do so, create a label widget for displaying the size
/// of the surface during a resize event.
fn maybeCreateResizeOverlay(config: *const configpkg.Config) ?*c.GtkWidget {
if (config.@"resize-overlay" == .never) return null;

const widget = c.gtk_label_new("");

c.gtk_widget_add_css_class(@ptrCast(widget), "view");
c.gtk_widget_add_css_class(@ptrCast(widget), "size-overlay");
c.gtk_widget_add_css_class(@ptrCast(widget), "hidden");
c.gtk_widget_set_visible(@ptrCast(widget), c.FALSE);
c.gtk_widget_set_focusable(@ptrCast(widget), c.FALSE);
c.gtk_widget_set_can_target(@ptrCast(widget), c.FALSE);
c.gtk_label_set_justify(@ptrCast(widget), c.GTK_JUSTIFY_CENTER);
c.gtk_label_set_selectable(@ptrCast(widget), c.FALSE);

setOverlayWidgetPosition(widget, config);

return widget;
}

/// 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.
fn maybeShowResizeOverlay(self: *Surface) void {
if (self.app.config.@"resize-overlay" == .never) return;

if (self.app.config.@"resize-overlay" == .@"after-first" and self.resize_overlay_first) {
self.resize_overlay_first = false;
return;
}

self.resize_overlay_first = false;

// When updating a widget, do so from GTK's thread, but not if there's
// already an update queued up.
if (self.resize_overlay_idler != null) return;
self.resize_overlay_idler = c.g_idle_add(gtkUpdateOverlayWidget, @ptrCast(self));
}

/// Actually update the overlay widget. This should only be called as an idle
/// handler.
fn gtkUpdateOverlayWidget(ud: ?*anyopaque) callconv(.C) c.gboolean {
const self: *Surface = @ptrCast(@alignCast(ud));

if (self.resize_overlay_widget) |widget| {
var buf: [64]u8 = undefined;
const text = std.fmt.bufPrintZ(
&buf,
"{d}c ⨯ {d}r\n{d}px ⨯ {d}px",
.{
self.core_surface.grid_size.columns,
self.core_surface.grid_size.rows,
self.core_surface.screen_size.width,
self.core_surface.screen_size.height,
},
) catch |err| {
log.err("unable to format text: {}", .{err});
return c.FALSE;
};

c.gtk_label_set_text(@ptrCast(widget), text.ptr);
c.gtk_widget_remove_css_class(@ptrCast(widget), "hidden");
c.gtk_widget_set_visible(@ptrCast(widget), 1);

setOverlayWidgetPosition(widget, &self.app.config);

if (self.resize_overlay_timer) |timer| {
if (c.g_source_remove(timer) == c.FALSE) {
log.warn("unable to remove size overlay timer", .{});
}
}
self.resize_overlay_timer = c.g_timeout_add(
self.app.config.@"resize-overlay-delay".asMilliseconds(),
gtkResizeOverlayTimerExpired,
@ptrCast(self),
);
}

self.resize_overlay_idler = null;

return c.FALSE;
}

/// 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.
fn setOverlayWidgetPosition(widget: *c.GtkWidget, config: *const configpkg.Config) void {
c.gtk_widget_set_halign(
@ptrCast(widget),
switch (config.@"resize-overlay-position") {
.center, .@"top-center", .@"bottom-center" => c.GTK_ALIGN_CENTER,
.@"top-left", .@"bottom-left" => c.GTK_ALIGN_START,
.@"top-right", .@"bottom-right" => c.GTK_ALIGN_END,
},
);
c.gtk_widget_set_valign(
@ptrCast(widget),
switch (config.@"resize-overlay-position") {
.center => c.GTK_ALIGN_CENTER,
.@"top-left", .@"top-center", .@"top-right" => c.GTK_ALIGN_START,
.@"bottom-left", .@"bottom-center", .@"bottom-right" => c.GTK_ALIGN_END,
},
);
}

/// If this fires, it means that the delay period has expired and the resize
/// overlay widget should be hidden.
fn gtkResizeOverlayTimerExpired(ud: ?*anyopaque) callconv(.C) c.gboolean {
const self: *Surface = @ptrCast(@alignCast(ud));
if (self.resize_overlay_widget) |widget| {
c.gtk_widget_add_css_class(@ptrCast(widget), "hidden");
c.gtk_widget_set_visible(@ptrCast(widget), c.FALSE);
self.resize_overlay_timer = null;
}
return c.FALSE;
}
12 changes: 12 additions & 0 deletions src/apprt/gtk/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,15 @@ label.url-overlay:hover {
label.url-overlay.hidden {
opacity: 0;
}

label.size-overlay {
padding: 4px 8px 4px 8px;
border-radius: 6px 6px 6px 6px;
outline-style: solid;
outline-width: 1px;
outline-color: #555555;
}

label.size-overlay.hidden {
opacity: 0;
}
97 changes: 97 additions & 0 deletions src/config/Config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,74 @@ keybind: Keybinds = .{},
/// This configuration currently only works with GTK.
@"window-new-tab-position": WindowNewTabPosition = .current,

/// This controls when resize overlays are shown. Resize overlays are a
/// transient popup that shows the size of the terminal while the surfaces are
/// being resized. The possible options are:
///
/// * `always` - Always show resize overlays.
/// * `never` - Never show resize overlays.
/// * `after-first` - The resize overlay will not appear when the surface
/// is first created, but will show up if the surface is
/// subsequently resized.
///
/// The default is `always`.
///
/// Changing this value at runtime and reloading the configuration will only
/// affect new windows, tabs, and splits.
///
/// Linux/GTK only.
@"resize-overlay": ResizeOverlay = .always,

/// If resize overlays are enabled, this controls the position of the overlay.
/// The possible options are:
///
/// * `center`
/// * `top-left`
/// * `top-center`
/// * `top-right`
/// * `bottom-left`
/// * `bottom-center`
/// * `bottom-right`
///
/// The default is `center`.
///
/// Linux/GTK only.
@"resize-overlay-position": ResizeOverlayPosition = .center,

/// If resize overlays are enabled, this controls how long the overlay is
/// visible on the screen before it is hidden. The default is ¾ of a second or
/// 750 ms.
///
/// The duration is specified as a series of numbers followed by time units.
/// Whitespace is allowed between numbers and units. Each number and unit will
/// be added together to form the total duration.
///
/// The allowed time units are as follows:
///
/// * `y` - 365 SI days, or 8760 hours, or 31536000 seconds. No adjustments
/// are made for leap years or leap seconds.
/// * `d` - one SI day, or 86400 seconds.
/// * `h` - one hour, or 3600 seconds.
/// * `m` - one minute, or 60 seconds.
/// * `s` - one second.
/// * `ms` - one millisecond, or 0.001 second.
/// * `us` or `µs` - one microsecond, or 0.000001 second.
/// * `ns` - one nanosecond, or 0.000000001 second.
///
/// Examples:
/// * `1h30m`
/// * `45s`
///
/// Units can be repeated and will be added together. This means that
/// `1h1h` is equivalent to `2h`. This is confusing and should be avoided.
/// A future update may disallow this.
///
/// The maximum value is `584y 49w 23h 34m 33s 709ms 551µs 615ns`. Any
/// value larger than this will be clamped to the maximum value.
///
/// Linux/GTK only.
@"resize-overlay-delay": Duration = .{ .duration = 750 * std.time.ns_per_ms },

// If true, when there are multiple split panes, the mouse selects the pane
// that is focused. This only applies to the currently focused window; i.e.
// mousing over a split in an unfocused window will now focus that split
Expand Down Expand Up @@ -3901,6 +3969,24 @@ pub const WindowNewTabPosition = enum {
end,
};

/// See resize-overlay
pub const ResizeOverlay = enum {
always,
never,
@"after-first",
};

/// See resize-overlay-position
pub const ResizeOverlayPosition = enum {
center,
@"top-left",
@"top-center",
@"top-right",
@"bottom-left",
@"bottom-center",
@"bottom-right",
};

/// See grapheme-width-method
pub const GraphemeWidthMethod = enum {
legacy,
Expand Down Expand Up @@ -4030,6 +4116,17 @@ pub const Duration = struct {
}
}
}

/// Convenience function to convert to milliseconds since many OS and
/// library timing functions operate on that timescale.
pub fn asMilliseconds(self: @This()) c_uint {
const ms: u64 = std.math.divTrunc(
u64,
self.duration,
std.time.ns_per_ms,
) catch std.math.maxInt(c_uint);
return std.math.cast(c_uint, ms) orelse std.math.maxInt(c_uint);
}
};

pub const WindowPadding = struct {
Expand Down

0 comments on commit 301826d

Please sign in to comment.