apprt/gtk-ng: surface progress bar

This commit is contained in:
Mitchell Hashimoto
2025-07-23 16:11:26 -07:00
parent eb3f3fff83
commit 00cce91dc4
4 changed files with 151 additions and 1 deletions

View File

@ -481,6 +481,8 @@ pub const Application = extern struct {
.quit_timer => try Action.quitTimer(self, value),
.progress_report => return Action.progressReport(target, value),
.render => Action.render(self, target),
.set_title => Action.setTitle(target, value),
@ -528,7 +530,6 @@ pub const Application = extern struct {
.check_for_updates,
.undo,
.redo,
.progress_report,
=> {
log.warn("unimplemented action={}", .{action});
return false;
@ -1117,6 +1118,19 @@ const Action = struct {
}
}
pub fn progressReport(
target: apprt.Target,
value: terminal.osc.Command.ProgressReport,
) bool {
return switch (target) {
.app => false,
.surface => |v| surface: {
v.rt_surface.surface.setProgressReport(value);
break :surface true;
},
};
}
pub fn render(_: *Application, target: apprt.Target) void {
switch (target) {
.app => {},

View File

@ -1,4 +1,5 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const adw = @import("adw");
const gdk = @import("gdk");
@ -308,9 +309,13 @@ pub const Surface = extern struct {
/// True when the child has exited.
child_exited: bool = false,
// Progress bar
progress_bar_timer: ?c_uint = null,
// Template binds
child_exited_overlay: *ChildExited,
drop_target: *gtk.DropTarget,
progress_bar_overlay: *gtk.ProgressBar,
pub var offset: c_int = 0;
};
@ -338,6 +343,93 @@ pub const Surface = extern struct {
priv.gl_area.queueRender();
}
/// Set the current progress report state.
pub fn setProgressReport(
self: *Self,
value: terminal.osc.Command.ProgressReport,
) void {
const priv = self.private();
// No matter what, we stop the timer because if we're removing
// then we're done and otherwise we restart it.
if (priv.progress_bar_timer) |timer| {
if (glib.Source.remove(timer) == 0) {
log.warn("unable to remove progress bar timer", .{});
}
priv.progress_bar_timer = null;
}
const progress_bar = priv.progress_bar_overlay;
switch (value.state) {
// Remove the progress bar
.remove => {
progress_bar.as(gtk.Widget).setVisible(@intFromBool(false));
return;
},
// Set the progress bar to a fixed value if one was provided, otherwise pulse.
// Remove the `error` CSS class so that the progress bar shows as normal.
.set => {
progress_bar.as(gtk.Widget).removeCssClass("error");
if (value.progress) |progress| {
progress_bar.setFraction(computeFraction(progress));
} else {
progress_bar.pulse();
}
},
// Set the progress bar to a fixed value if one was provided, otherwise pulse.
// Set the `error` CSS class so that the progress bar shows as an error color.
.@"error" => {
progress_bar.as(gtk.Widget).addCssClass("error");
if (value.progress) |progress| {
progress_bar.setFraction(computeFraction(progress));
} else {
progress_bar.pulse();
}
},
// The state of progress is unknown, so pulse the progress bar to
// indicate that things are still happening.
.indeterminate => {
progress_bar.pulse();
},
// If a progress value was provided, set the progress bar to that value.
// Don't pulse the progress bar as that would indicate that things were
// happening. Otherwise this is mainly used to keep the progress bar on
// screen instead of timing out.
.pause => {
if (value.progress) |progress| {
progress_bar.setFraction(computeFraction(progress));
}
},
}
// Assume all states lead to visibility
assert(value.state != .remove);
progress_bar.as(gtk.Widget).setVisible(@intFromBool(true));
// Start our timer to remove bad actor programs that stall
// the progress bar.
const progress_bar_timeout_seconds = 15;
assert(priv.progress_bar_timer == null);
priv.progress_bar_timer = glib.timeoutAdd(
progress_bar_timeout_seconds * std.time.ms_per_s,
progressBarTimer,
self,
);
}
/// The progress bar hasn't been updated by the TUI recently, remove it.
fn progressBarTimer(ud: ?*anyopaque) callconv(.c) c_int {
const self: *Self = @ptrCast(@alignCast(ud.?));
const priv = self.private();
priv.progress_bar_timer = null;
self.setProgressReport(.{ .state = .remove });
return @intFromBool(glib.SOURCE_REMOVE);
}
/// Key press event (press or release).
///
/// At a high level, we want to construct an `input.KeyEvent` and
@ -873,6 +965,12 @@ pub const Surface = extern struct {
v.unref();
priv.config = null;
}
if (priv.progress_bar_timer) |timer| {
if (glib.Source.remove(timer) == 0) {
log.warn("unable to remove progress bar timer", .{});
}
priv.progress_bar_timer = null;
}
gtk.Widget.disposeTemplate(
self.as(gtk.Widget),
@ -1833,6 +1931,7 @@ pub const Surface = extern struct {
class.bindTemplateChildPrivate("url_left", .{});
class.bindTemplateChildPrivate("url_right", .{});
class.bindTemplateChildPrivate("child_exited_overlay", .{});
class.bindTemplateChildPrivate("progress_bar_overlay", .{});
class.bindTemplateChildPrivate("resize_overlay", .{});
class.bindTemplateChildPrivate("drop_target", .{});
class.bindTemplateChildPrivate("im_context", .{});
@ -2221,3 +2320,16 @@ fn g_value_holds(value_: ?*gobject.Value, g_type: gobject.Type) bool {
}
return false;
}
/// Compute a fraction [0.0, 1.0] from the supplied progress, which is clamped
/// to [0, 100].
fn computeFraction(progress: u8) f64 {
return @as(f64, @floatFromInt(std.math.clamp(progress, 0, 100))) / 100.0;
}
test "computeFraction" {
try std.testing.expectEqual(1.0, computeFraction(100));
try std.testing.expectEqual(1.0, computeFraction(255));
try std.testing.expectEqual(0.0, computeFraction(0));
try std.testing.expectEqual(0.5, computeFraction(50));
}

View File

@ -84,3 +84,12 @@ label.url-overlay.right {
/* after GTK 4.16 is a requirement, switch to the following:
/* background-color: color-mix(in srgb, var(--error-bg-color), transparent 50%); */
}
/*
* Surface
*/
.surface progressbar.error trough progress {
background-color: rgb(192, 28, 40);
/* after GTK 4.16 is a requirement, switch to the following: */
/* background-color: color-mix(in srgb, var(--error-bg-color), transparent); */
}

View File

@ -2,6 +2,10 @@ using Gtk 4.0;
using Adw 1;
template $GhosttySurface: Adw.Bin {
styles [
"surface",
]
notify::config => $notify_config();
notify::mouse-hover-url => $notify_mouse_hover_url();
notify::mouse-hidden => $notify_mouse_hidden();
@ -25,6 +29,17 @@ template $GhosttySurface: Adw.Bin {
use-es: false;
}
[overlay]
ProgressBar progress_bar_overlay {
styles [
"osd",
]
visible: false;
halign: fill;
valign: start;
}
[overlay]
$GhosttySurfaceChildExited child_exited_overlay {
visible: bind template.child-exited;