From 301826dffff3938c2fbeb89bfbb9e8fd96f56886 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 9 Aug 2024 22:58:33 -0500 Subject: [PATCH 1/5] gtk: add resize overlay This adds a transient overlay that shows the size of the surface while you are resizing the window or the surfaces. --- src/apprt/gtk/App.zig | 8 +- src/apprt/gtk/Surface.zig | 157 ++++++++++++++++++++++++++++++++++++++ src/apprt/gtk/style.css | 12 +++ src/config/Config.zig | 97 +++++++++++++++++++++++ 4 files changed, 267 insertions(+), 7 deletions(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 60980ba1d..eb7ce82c4 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -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 = {} }; diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 2a7c7d183..976820874 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -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, @@ -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: { @@ -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; @@ -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 @@ -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(); } } @@ -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; +} diff --git a/src/apprt/gtk/style.css b/src/apprt/gtk/style.css index 4f52015f4..d9ac9abc3 100644 --- a/src/apprt/gtk/style.css +++ b/src/apprt/gtk/style.css @@ -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; +} diff --git a/src/config/Config.zig b/src/config/Config.zig index 379f47bd1..b4afd53da 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -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 @@ -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, @@ -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 { From 8b919df1f5e3195dcb059494e3197e9b2a4df4ee Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 10 Aug 2024 13:20:03 -0500 Subject: [PATCH 2/5] =?UTF-8?q?resize-overlay:=20change=20default=20and=20?= =?UTF-8?q?delay=20=E2=86=92=20duration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change the default to `after-first` and change `resize-overlay-delay` → `resize-overlay-duration`. --- src/apprt/gtk/Surface.zig | 2 +- src/config/Config.zig | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 976820874..114eef19f 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2088,7 +2088,7 @@ fn gtkUpdateOverlayWidget(ud: ?*anyopaque) callconv(.C) c.gboolean { } } self.resize_overlay_timer = c.g_timeout_add( - self.app.config.@"resize-overlay-delay".asMilliseconds(), + self.app.config.@"resize-overlay-duration".asMilliseconds(), gtkResizeOverlayTimerExpired, @ptrCast(self), ); diff --git a/src/config/Config.zig b/src/config/Config.zig index b4afd53da..209139897 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -868,13 +868,13 @@ keybind: Keybinds = .{}, /// is first created, but will show up if the surface is /// subsequently resized. /// -/// The default is `always`. +/// 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 = .always, +@"resize-overlay": ResizeOverlay = .@"after-first", /// If resize overlays are enabled, this controls the position of the overlay. /// The possible options are: @@ -924,7 +924,7 @@ keybind: Keybinds = .{}, /// 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 }, +@"resize-overlay-duration": 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. From b55b3de05bfbe3e47342dffe63482a151bfad8b7 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 10 Aug 2024 14:42:32 -0500 Subject: [PATCH 3/5] resize overlay: move all resize overlay code and data into a struct MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keepin´ the code clean by gathering all of the resize overlay related data and code into a struct. --- src/apprt/gtk/Surface.zig | 245 ++++++++++++++++++++------------------ 1 file changed, 127 insertions(+), 118 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 114eef19f..9cd9f8282 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -325,18 +325,7 @@ 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, +resize_overlay: ResizeOverlay = .{}, /// If non-null this is the widget on the overlay which dims the surface when it is unfocused unfocused_widget: ?*c.GtkWidget = null, @@ -469,11 +458,7 @@ 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)); - } + const resize_overlay = ResizeOverlay.init(self, &app.config, @ptrCast(overlay)); // 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. @@ -519,7 +504,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, + .resize_overlay = resize_overlay, }; errdefer self.* = undefined; @@ -617,24 +602,7 @@ 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; - // } + self.resize_overlay.deinit(); } // unref removes the long-held reference to the gl_area and kicks off the @@ -1338,7 +1306,7 @@ fn gtkResize(area: *c.GtkGLArea, width: c.gint, height: c.gint, ud: ?*anyopaque) return; }; - self.maybeShowResizeOverlay(); + self.resize_overlay.maybeShowResizeOverlay(); } } @@ -2015,61 +1983,102 @@ fn translateMods(state: c.GdkModifierType) input.Mods { 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 ResizeOverlay = struct { + /// Back reference to the surface we belong to + surface: ?*Surface = null, - const widget = c.gtk_label_new(""); + /// 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, - 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); + /// If non-null this is a timer for dismissing the resize overlay. + timer: ?c.guint = null, - setOverlayWidgetPosition(widget, config); + /// If non-null this is a timer for dismissing the resize overlay. + idler: ?c.guint = null, - return widget; -} + /// If true, the next resize event will be the first one. + first: bool = true, -/// 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 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 (self.app.config.@"resize-overlay" == .@"after-first" and self.resize_overlay_first) { - self.resize_overlay_first = false; - return; + if (config.@"resize-overlay" == .never) return .{}; + + 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 }; } - self.resize_overlay_first = false; + pub fn deinit(self: *@This()) void { + if (self.idler) |idler| { + if (c.g_source_remove(idler) == c.FALSE) { + log.warn("unable to remove resize overlay idler", .{}); + } + self.idler = null; + } - // 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)); -} + if (self.timer) |timer| { + if (c.g_source_remove(timer) == c.FALSE) { + log.warn("unable to remove resize overlay timer", .{}); + } + self.timer = null; + } + } -/// 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 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. + pub fn maybeShowResizeOverlay(self: *@This()) void { + if (self.widget == null) return; + const surface = self.surface orelse return; - if (self.resize_overlay_widget) |widget| { - var buf: [64]u8 = undefined; + if (surface.app.config.@"resize-overlay" == .never) return; + + if (surface.app.config.@"resize-overlay" == .@"after-first" and 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. + 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: *@This() = @ptrCast(@alignCast(ud)); + + 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\n{d}px ⨯ {d}px", + "{d}c ⨯ {d}r", .{ - self.core_surface.grid_size.columns, - self.core_surface.grid_size.rows, - self.core_surface.screen_size.width, - self.core_surface.screen_size.height, + surface.core_surface.grid_size.columns, + surface.core_surface.grid_size.rows, }, ) catch |err| { log.err("unable to format text: {}", .{err}); @@ -2080,54 +2089,54 @@ fn gtkUpdateOverlayWidget(ud: ?*anyopaque) callconv(.C) c.gboolean { c.gtk_widget_remove_css_class(@ptrCast(widget), "hidden"); c.gtk_widget_set_visible(@ptrCast(widget), 1); - setOverlayWidgetPosition(widget, &self.app.config); + setOverlayWidgetPosition(widget, &surface.app.config); - if (self.resize_overlay_timer) |timer| { + if (self.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-duration".asMilliseconds(), + self.timer = c.g_timeout_add( + surface.app.config.@"resize-overlay-duration".asMilliseconds(), gtkResizeOverlayTimerExpired, @ptrCast(self), ); + + self.idler = null; + + return c.FALSE; } - 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; + /// 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, + }, + ); } - return c.FALSE; -} + + /// 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: *@This() = @ptrCast(@alignCast(ud)); + if (self.widget) |widget| { + c.gtk_widget_add_css_class(@ptrCast(widget), "hidden"); + c.gtk_widget_set_visible(@ptrCast(widget), c.FALSE); + } + self.timer = null; + return c.FALSE; + } +}; From 4dbd2fb6390d466918e59deeacda7198389d33c5 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 10 Aug 2024 14:59:05 -0500 Subject: [PATCH 4/5] move resize overlay code to a new file to keep the file size down --- src/apprt/gtk/ResizeOverlay.zig | 162 ++++++++++++++++++++++++++++++++ src/apprt/gtk/Surface.zig | 159 +------------------------------ 2 files changed, 163 insertions(+), 158 deletions(-) create mode 100644 src/apprt/gtk/ResizeOverlay.zig diff --git a/src/apprt/gtk/ResizeOverlay.zig b/src/apprt/gtk/ResizeOverlay.zig new file mode 100644 index 000000000..9c4c7a3d5 --- /dev/null +++ b/src/apprt/gtk/ResizeOverlay.zig @@ -0,0 +1,162 @@ +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) @This() { + // 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 .{}; + + 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: *@This()) 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. +pub fn maybeShowResizeOverlay(self: *@This()) void { + if (self.widget == null) return; + const surface = self.surface orelse return; + + if (surface.app.config.@"resize-overlay" == .never) return; + + if (surface.app.config.@"resize-overlay" == .@"after-first" and 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. + 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: *@This() = @ptrCast(@alignCast(ud)); + + 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), + ); + + self.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: *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: *@This() = @ptrCast(@alignCast(ud)); + if (self.widget) |widget| { + c.gtk_widget_add_css_class(@ptrCast(widget), "hidden"); + c.gtk_widget_set_visible(@ptrCast(widget), c.FALSE); + } + self.timer = null; + return c.FALSE; +} diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 9cd9f8282..e35717a4c 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -18,6 +18,7 @@ const Split = @import("Split.zig"); const Tab = @import("Tab.zig"); const Window = @import("Window.zig"); const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig"); +const ResizeOverlay = @import("ResizeOverlay.zig"); const inspector = @import("inspector.zig"); const gtk_key = @import("key.zig"); const c = @import("c.zig"); @@ -1982,161 +1983,3 @@ fn translateMods(state: c.GdkModifierType) input.Mods { if (state & c.GDK_LOCK_MASK != 0) mods.caps_lock = true; return mods; } - -const ResizeOverlay = struct { - /// 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 .{}; - - 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: *@This()) 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. - pub fn maybeShowResizeOverlay(self: *@This()) void { - if (self.widget == null) return; - const surface = self.surface orelse return; - - if (surface.app.config.@"resize-overlay" == .never) return; - - if (surface.app.config.@"resize-overlay" == .@"after-first" and 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. - 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: *@This() = @ptrCast(@alignCast(ud)); - - 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), - ); - - self.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: *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: *@This() = @ptrCast(@alignCast(ud)); - if (self.widget) |widget| { - c.gtk_widget_add_css_class(@ptrCast(widget), "hidden"); - c.gtk_widget_set_visible(@ptrCast(widget), c.FALSE); - } - self.timer = null; - return c.FALSE; - } -}; From 1c88377e97082911eab7094a47608bbeaf59fdc1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 10 Aug 2024 15:06:44 -0700 Subject: [PATCH 5/5] apprt/gtk: small style tweaks for resize overlay --- src/apprt/gtk/ResizeOverlay.zig | 46 +++++++++++++++++++++------------ src/apprt/gtk/Surface.zig | 5 ++-- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/src/apprt/gtk/ResizeOverlay.zig b/src/apprt/gtk/ResizeOverlay.zig index 9c4c7a3d5..c0db30c85 100644 --- a/src/apprt/gtk/ResizeOverlay.zig +++ b/src/apprt/gtk/ResizeOverlay.zig @@ -1,3 +1,5 @@ +const ResizeOverlay = @This(); + const std = @import("std"); const c = @import("c.zig"); const configpkg = @import("../../config.zig"); @@ -23,14 +25,18 @@ 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) @This() { +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"); @@ -39,15 +45,13 @@ pub fn init(surface: *Surface, config: *configpkg.Config, overlay: *c.GtkOverlay 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: *@This()) void { +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", .{}); @@ -66,21 +70,28 @@ pub fn deinit(self: *@This()) void { /// 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. -pub fn maybeShowResizeOverlay(self: *@This()) void { +/// +/// 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; - if (surface.app.config.@"resize-overlay" == .never) return; - - if (surface.app.config.@"resize-overlay" == .@"after-first" and self.first) { - self.first = false; - 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. + // 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)); } @@ -88,7 +99,10 @@ pub fn maybeShowResizeOverlay(self: *@This()) void { /// Actually update the overlay widget. This should only be called as an idle /// handler. fn gtkUpdateOverlayWidget(ud: ?*anyopaque) callconv(.C) c.gboolean { - const self: *@This() = @ptrCast(@alignCast(ud)); + 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; @@ -123,8 +137,6 @@ fn gtkUpdateOverlayWidget(ud: ?*anyopaque) callconv(.C) c.gboolean { @ptrCast(self), ); - self.idler = null; - return c.FALSE; } @@ -152,11 +164,11 @@ fn setOverlayWidgetPosition(widget: *c.GtkWidget, config: *configpkg.Config) voi /// 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: *@This() = @ptrCast(@alignCast(ud)); + 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); } - self.timer = null; return c.FALSE; } diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index e35717a4c..44deb4a1d 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -326,6 +326,7 @@ gl_area: *c.GtkGLArea, /// If non-null this is the widget on the overlay that shows the URL. 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 @@ -459,8 +460,6 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { break :font_size parent.font_size; }; - const resize_overlay = ResizeOverlay.init(self, &app.config, @ptrCast(overlay)); - // 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: { @@ -497,6 +496,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { .container = .{ .none = {} }, .overlay = @ptrCast(overlay), .gl_area = @ptrCast(gl_area), + .resize_overlay = ResizeOverlay.init(self, &app.config, @ptrCast(overlay)), .title_text = null, .core_surface = undefined, .font_size = font_size, @@ -505,7 +505,6 @@ 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 = resize_overlay, }; errdefer self.* = undefined;