mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
Merge pull request #2071 from jcollie/resize-overlay
gtk: add resize overlay
This commit is contained in:
@ -531,13 +531,7 @@ pub fn startQuitTimer(self: *App) void {
|
|||||||
|
|
||||||
if (self.config.@"quit-after-last-window-closed-delay") |v| {
|
if (self.config.@"quit-after-last-window-closed-delay") |v| {
|
||||||
// If a delay is configured, set a timeout function to quit after the delay.
|
// If a delay is configured, set a timeout function to quit after the delay.
|
||||||
const ms: u64 = std.math.divTrunc(
|
self.quit_timer = .{ .active = c.g_timeout_add(v.asMilliseconds(), gtkQuitTimerExpired, self) };
|
||||||
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) };
|
|
||||||
} else {
|
} else {
|
||||||
// If no delay is configured, treat it as expired.
|
// If no delay is configured, treat it as expired.
|
||||||
self.quit_timer = .{ .expired = {} };
|
self.quit_timer = .{ .expired = {} };
|
||||||
|
174
src/apprt/gtk/ResizeOverlay.zig
Normal file
174
src/apprt/gtk/ResizeOverlay.zig
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
const ResizeOverlay = @This();
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const c = @import("c.zig");
|
||||||
|
const configpkg = @import("../../config.zig");
|
||||||
|
const Surface = @import("Surface.zig");
|
||||||
|
|
||||||
|
const log = std.log.scoped(.gtk);
|
||||||
|
|
||||||
|
/// Back reference to the surface we belong to
|
||||||
|
surface: ?*Surface = null,
|
||||||
|
|
||||||
|
/// If non-null this is the widget on the overlay that shows the size of the
|
||||||
|
/// surface when it is resized.
|
||||||
|
widget: ?*c.GtkWidget = null,
|
||||||
|
|
||||||
|
/// If non-null this is a timer for dismissing the resize overlay.
|
||||||
|
timer: ?c.guint = null,
|
||||||
|
|
||||||
|
/// If non-null this is a timer for dismissing the resize overlay.
|
||||||
|
idler: ?c.guint = null,
|
||||||
|
|
||||||
|
/// If true, the next resize event will be the first one.
|
||||||
|
first: bool = true,
|
||||||
|
|
||||||
|
/// If we're configured to do so, create a label widget for displaying the size
|
||||||
|
/// of the surface during a resize event.
|
||||||
|
pub fn init(
|
||||||
|
surface: *Surface,
|
||||||
|
config: *configpkg.Config,
|
||||||
|
overlay: *c.GtkOverlay,
|
||||||
|
) ResizeOverlay {
|
||||||
|
// At this point the surface object has been _created_ but not
|
||||||
|
// _initialized_ so we can't use any information from it.
|
||||||
|
|
||||||
|
if (config.@"resize-overlay" == .never) return .{};
|
||||||
|
|
||||||
|
// Create the label that will show the resize information.
|
||||||
|
const widget = c.gtk_label_new("");
|
||||||
|
c.gtk_widget_add_css_class(widget, "view");
|
||||||
|
c.gtk_widget_add_css_class(widget, "size-overlay");
|
||||||
|
c.gtk_widget_add_css_class(widget, "hidden");
|
||||||
|
c.gtk_widget_set_visible(widget, c.FALSE);
|
||||||
|
c.gtk_widget_set_focusable(widget, c.FALSE);
|
||||||
|
c.gtk_widget_set_can_target(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);
|
||||||
|
c.gtk_overlay_add_overlay(overlay, widget);
|
||||||
|
|
||||||
|
return .{ .surface = surface, .widget = widget };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *ResizeOverlay) void {
|
||||||
|
if (self.idler) |idler| {
|
||||||
|
if (c.g_source_remove(idler) == c.FALSE) {
|
||||||
|
log.warn("unable to remove resize overlay idler", .{});
|
||||||
|
}
|
||||||
|
self.idler = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.timer) |timer| {
|
||||||
|
if (c.g_source_remove(timer) == c.FALSE) {
|
||||||
|
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 maybeShowResizeOverlay(self: *ResizeOverlay) void {
|
||||||
|
if (self.widget == null) return;
|
||||||
|
const surface = self.surface orelse return;
|
||||||
|
|
||||||
|
switch (surface.app.config.@"resize-overlay") {
|
||||||
|
.never => return,
|
||||||
|
.always => {},
|
||||||
|
.@"after-first" => if (self.first) {
|
||||||
|
self.first = false;
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
self.first = false;
|
||||||
|
|
||||||
|
// When updating a widget, do so from GTK's thread, but not if there's
|
||||||
|
// already an update queued up. Even though all our function calls ARE
|
||||||
|
// from the main thread, we have to do this to avoid GTK warnings. My
|
||||||
|
// guess is updating a widget in the hierarchy while another widget is
|
||||||
|
// being resized is a bad idea.
|
||||||
|
if (self.idler != null) return;
|
||||||
|
self.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: *ResizeOverlay = @ptrCast(@alignCast(ud));
|
||||||
|
|
||||||
|
// No matter what our idler is complete with this callback
|
||||||
|
self.idler = null;
|
||||||
|
|
||||||
|
const widget = self.widget orelse return c.FALSE;
|
||||||
|
const surface = self.surface orelse return c.FALSE;
|
||||||
|
|
||||||
|
var buf: [32]u8 = undefined;
|
||||||
|
const text = std.fmt.bufPrintZ(
|
||||||
|
&buf,
|
||||||
|
"{d}c ⨯ {d}r",
|
||||||
|
.{
|
||||||
|
surface.core_surface.grid_size.columns,
|
||||||
|
surface.core_surface.grid_size.rows,
|
||||||
|
},
|
||||||
|
) 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, &surface.app.config);
|
||||||
|
|
||||||
|
if (self.timer) |timer| {
|
||||||
|
if (c.g_source_remove(timer) == c.FALSE) {
|
||||||
|
log.warn("unable to remove size overlay timer", .{});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.timer = c.g_timeout_add(
|
||||||
|
surface.app.config.@"resize-overlay-duration".asMilliseconds(),
|
||||||
|
gtkResizeOverlayTimerExpired,
|
||||||
|
@ptrCast(self),
|
||||||
|
);
|
||||||
|
|
||||||
|
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: *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: *ResizeOverlay = @ptrCast(@alignCast(ud));
|
||||||
|
self.timer = null;
|
||||||
|
if (self.widget) |widget| {
|
||||||
|
c.gtk_widget_add_css_class(@ptrCast(widget), "hidden");
|
||||||
|
c.gtk_widget_set_visible(@ptrCast(widget), c.FALSE);
|
||||||
|
}
|
||||||
|
return c.FALSE;
|
||||||
|
}
|
@ -18,6 +18,7 @@ const Split = @import("Split.zig");
|
|||||||
const Tab = @import("Tab.zig");
|
const Tab = @import("Tab.zig");
|
||||||
const Window = @import("Window.zig");
|
const Window = @import("Window.zig");
|
||||||
const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig");
|
const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig");
|
||||||
|
const ResizeOverlay = @import("ResizeOverlay.zig");
|
||||||
const inspector = @import("inspector.zig");
|
const inspector = @import("inspector.zig");
|
||||||
const gtk_key = @import("key.zig");
|
const gtk_key = @import("key.zig");
|
||||||
const c = @import("c.zig");
|
const c = @import("c.zig");
|
||||||
@ -325,6 +326,9 @@ gl_area: *c.GtkGLArea,
|
|||||||
/// If non-null this is the widget on the overlay that shows the URL.
|
/// If non-null this is the widget on the overlay that shows the URL.
|
||||||
url_widget: ?URLWidget = null,
|
url_widget: ?URLWidget = null,
|
||||||
|
|
||||||
|
/// The overlay that shows resizing information.
|
||||||
|
resize_overlay: ResizeOverlay = .{},
|
||||||
|
|
||||||
/// If non-null this is the widget on the overlay which dims the surface when it is unfocused
|
/// If non-null this is the widget on the overlay which dims the surface when it is unfocused
|
||||||
unfocused_widget: ?*c.GtkWidget = null,
|
unfocused_widget: ?*c.GtkWidget = null,
|
||||||
|
|
||||||
@ -492,6 +496,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
|
|||||||
.container = .{ .none = {} },
|
.container = .{ .none = {} },
|
||||||
.overlay = @ptrCast(overlay),
|
.overlay = @ptrCast(overlay),
|
||||||
.gl_area = @ptrCast(gl_area),
|
.gl_area = @ptrCast(gl_area),
|
||||||
|
.resize_overlay = ResizeOverlay.init(self, &app.config, @ptrCast(overlay)),
|
||||||
.title_text = null,
|
.title_text = null,
|
||||||
.core_surface = undefined,
|
.core_surface = undefined,
|
||||||
.font_size = font_size,
|
.font_size = font_size,
|
||||||
@ -597,6 +602,7 @@ pub fn deinit(self: *Surface) void {
|
|||||||
c.gtk_overlay_remove_overlay(self.overlay, widget);
|
c.gtk_overlay_remove_overlay(self.overlay, widget);
|
||||||
self.unfocused_widget = null;
|
self.unfocused_widget = null;
|
||||||
}
|
}
|
||||||
|
self.resize_overlay.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// unref removes the long-held reference to the gl_area and kicks off the
|
// unref removes the long-held reference to the gl_area and kicks off the
|
||||||
@ -1303,6 +1309,8 @@ fn gtkResize(area: *c.GtkGLArea, width: c.gint, height: c.gint, ud: ?*anyopaque)
|
|||||||
log.err("error in size callback err={}", .{err});
|
log.err("error in size callback err={}", .{err});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.resize_overlay.maybeShowResizeOverlay();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,3 +9,15 @@ label.url-overlay:hover {
|
|||||||
label.url-overlay.hidden {
|
label.url-overlay.hidden {
|
||||||
opacity: 0;
|
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;
|
||||||
|
}
|
||||||
|
@ -859,6 +859,74 @@ keybind: Keybinds = .{},
|
|||||||
/// This configuration currently only works with GTK.
|
/// This configuration currently only works with GTK.
|
||||||
@"window-new-tab-position": WindowNewTabPosition = .current,
|
@"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 `after-first`.
|
||||||
|
///
|
||||||
|
/// Changing this value at runtime and reloading the configuration will only
|
||||||
|
/// affect new windows, tabs, and splits.
|
||||||
|
///
|
||||||
|
/// Linux/GTK only.
|
||||||
|
@"resize-overlay": ResizeOverlay = .@"after-first",
|
||||||
|
|
||||||
|
/// 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-duration": Duration = .{ .duration = 750 * std.time.ns_per_ms },
|
||||||
|
|
||||||
// If true, when there are multiple split panes, the mouse selects the pane
|
// 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.
|
// 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
|
// mousing over a split in an unfocused window will now focus that split
|
||||||
@ -3906,6 +3974,24 @@ pub const WindowNewTabPosition = enum {
|
|||||||
end,
|
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
|
/// See grapheme-width-method
|
||||||
pub const GraphemeWidthMethod = enum {
|
pub const GraphemeWidthMethod = enum {
|
||||||
legacy,
|
legacy,
|
||||||
@ -4035,6 +4121,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 {
|
pub const WindowPadding = struct {
|
||||||
|
Reference in New Issue
Block a user