From 4e2781fdec96f1784f02da94de990bfbff0d090a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 26 Sep 2024 10:35:31 -0700 Subject: [PATCH] apprt/gtk --- src/apprt/action.zig | 23 ++-- src/apprt/gtk/App.zig | 255 ++++++++++++++++++++++++++++++++++++-- src/apprt/gtk/Split.zig | 17 +-- src/apprt/gtk/Surface.zig | 90 +------------- src/apprt/gtk/Window.zig | 2 +- 5 files changed, 279 insertions(+), 108 deletions(-) diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 35bdcb3c2..edeb02d7a 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -2,6 +2,14 @@ const std = @import("std"); const assert = std.debug.assert; const CoreSurface = @import("../Surface.zig"); +/// The target for an action. This is generally the thing that had focus +/// while the action was made but the concept of "focus" is not guaranteed +/// since actions can also be triggered by timers, scripts, etc. +pub const Target = union(enum) { + app, + surface: *CoreSurface, +}; + /// The possible actions an apprt has to react to. Actions are one-way /// messages that are sent to the app runtime to trigger some behavior. /// @@ -69,7 +77,7 @@ pub const Action = union(enum) { /// after the configured delay. This can be cancelled by sending /// another quit_timer action with "stop". Multiple "starts" shouldn't /// happen and can be ignored or cause a restart it isn't that important. - quit_timer: enum { start, stop }, + quit_timer: QuitTimer, /// Set the secure input functionality on or off. "Secure input" means /// that the user is currently at some sort of prompt where they may be @@ -92,14 +100,6 @@ pub const Action = union(enum) { } }; -/// The target for an action. This is generally the thing that had focus -/// while the action was made but the concept of "focus" is not guaranteed -/// since actions can also be triggered by timers, scripts, etc. -pub const Target = union(enum) { - app, - surface: *CoreSurface, -}; - // This is made extern (c_int) to make interop easier with our embedded // runtime. The small size cost doesn't make a difference in our union. pub const SplitDirection = enum(c_int) { @@ -165,6 +165,11 @@ pub const Inspector = enum(c_int) { hide, }; +pub const QuitTimer = enum(c_int) { + start, + stop, +}; + /// The desktop notification to show. pub const DesktopNotification = struct { title: [:0]const u8, diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 73c25ec87..e5a51d66d 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -27,6 +27,7 @@ const Surface = @import("Surface.zig"); const Window = @import("Window.zig"); const ConfigErrorsWindow = @import("ConfigErrorsWindow.zig"); const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig"); +const Split = @import("Split.zig"); const c = @import("c.zig").c; const inspector = @import("inspector.zig"); const key = @import("key.zig"); @@ -341,9 +342,249 @@ pub fn terminate(self: *App) void { self.config.deinit(); } -/// Open the configuration in the system editor. -pub fn openConfig(self: *App) !void { - try configpkg.edit.open(self.core_app.alloc); +/// Perform a given action. +pub fn performAction( + self: *App, + target: apprt.Target, + comptime action: apprt.Action.Key, + value: apprt.Action.Value(action), +) !void { + switch (action) { + .new_window => _ = try self.newWindow(switch (target) { + .app => null, + .surface => |v| v, + }), + .toggle_fullscreen => self.toggleFullscreen(target, value), + + .new_tab => try self.newTab(target), + .goto_tab => self.gotoTab(target, value), + .new_split => try self.newSplit(target, value), + .resize_split => self.resizeSplit(target, value), + .equalize_splits => self.equalizeSplits(target), + .goto_split => self.gotoSplit(target, value), + .open_config => try configpkg.edit.open(self.core_app.alloc), + .inspector => self.controlInspector(target, value), + .desktop_notification => self.showDesktopNotification(target, value), + .present_terminal => self.presentTerminal(target), + .toggle_window_decorations => self.toggleWindowDecorations(target), + .quit_timer => self.quitTimer(value), + + // Unimplemented + .close_all_windows, + .toggle_split_zoom, + .secure_input, + => log.warn("unimplemented action={}", .{action}), + } +} + +fn newTab(_: *App, target: apprt.Target) !void { + switch (target) { + .app => {}, + .surface => |v| { + const window = v.rt_surface.container.window() orelse { + log.info( + "new_tab invalid for container={s}", + .{@tagName(v.rt_surface.container)}, + ); + return; + }; + + try window.newTab(v); + }, + } +} + +fn gotoTab(_: *App, target: apprt.Target, tab: apprt.action.GotoTab) void { + switch (target) { + .app => {}, + .surface => |v| { + const window = v.rt_surface.container.window() orelse { + log.info( + "gotoTab invalid for container={s}", + .{@tagName(v.rt_surface.container)}, + ); + return; + }; + + switch (tab) { + .previous => window.gotoPreviousTab(v.rt_surface), + .next => window.gotoNextTab(v.rt_surface), + .last => window.gotoLastTab(), + else => window.gotoTab(@intCast(@intFromEnum(tab))), + } + }, + } +} + +fn newSplit( + self: *App, + target: apprt.Target, + direction: apprt.action.SplitDirection, +) !void { + switch (target) { + .app => {}, + .surface => |v| { + const alloc = self.core_app.alloc; + _ = try Split.create(alloc, v.rt_surface, direction); + }, + } +} + +fn equalizeSplits(_: *App, target: apprt.Target) void { + switch (target) { + .app => {}, + .surface => |v| { + const tab = v.rt_surface.container.tab() orelse return; + const top_split = switch (tab.elem) { + .split => |s| s, + else => return, + }; + _ = top_split.equalize(); + }, + } +} + +fn gotoSplit( + _: *const App, + target: apprt.Target, + direction: apprt.action.GotoSplit, +) void { + switch (target) { + .app => {}, + .surface => |v| { + const s = v.rt_surface.container.split() orelse return; + const map = s.directionMap(switch (v.rt_surface.container) { + .split_tl => .top_left, + .split_br => .bottom_right, + .none, .tab_ => unreachable, + }); + const surface_ = map.get(direction) orelse return; + if (surface_) |surface| surface.grabFocus(); + }, + } +} + +fn resizeSplit( + _: *const App, + target: apprt.Target, + resize: apprt.action.ResizeSplit, +) void { + switch (target) { + .app => {}, + .surface => |v| { + const s = v.rt_surface.container.firstSplitWithOrientation( + Split.Orientation.fromResizeDirection(resize.direction), + ) orelse return; + s.moveDivider(resize.direction, resize.amount); + }, + } +} + +fn presentTerminal( + _: *const App, + target: apprt.Target, +) void { + switch (target) { + .app => {}, + .surface => |v| v.rt_surface.present(), + } +} + +fn controlInspector( + _: *const App, + target: apprt.Target, + mode: apprt.action.Inspector, +) void { + const surface: *Surface = switch (target) { + .app => return, + .surface => |v| v.rt_surface, + }; + + surface.controlInspector(mode); +} + +fn toggleFullscreen( + _: *App, + target: apprt.Target, + _: apprt.action.Fullscreen, +) void { + switch (target) { + .app => {}, + .surface => |v| { + const window = v.rt_surface.container.window() orelse { + log.info( + "toggleFullscreen invalid for container={s}", + .{@tagName(v.rt_surface.container)}, + ); + return; + }; + + window.toggleFullscreen(); + }, + } +} + +fn toggleWindowDecorations( + _: *App, + target: apprt.Target, +) void { + switch (target) { + .app => {}, + .surface => |v| { + const window = v.rt_surface.container.window() orelse { + log.info( + "toggleFullscreen invalid for container={s}", + .{@tagName(v.rt_surface.container)}, + ); + return; + }; + + window.toggleWindowDecorations(); + }, + } +} + +fn quitTimer(self: *App, mode: apprt.action.QuitTimer) void { + switch (mode) { + .start => self.startQuitTimer(), + .stop => self.stopQuitTimer(), + } +} + +fn showDesktopNotification( + self: *App, + target: apprt.Target, + n: apprt.action.DesktopNotification, +) void { + // Set a default title if we don't already have one + const t = switch (n.title.len) { + 0 => "Ghostty", + else => n.title, + }; + + const notification = c.g_notification_new(t.ptr); + defer c.g_object_unref(notification); + c.g_notification_set_body(notification, n.body.ptr); + + const icon = c.g_themed_icon_new("com.mitchellh.ghostty"); + defer c.g_object_unref(icon); + c.g_notification_set_icon(notification, icon); + + const pointer = c.g_variant_new_uint64(switch (target) { + .app => 0, + .surface => |v| @intFromPtr(v), + }); + c.g_notification_set_default_action_and_target_value( + notification, + "app.present-surface", + pointer, + ); + + const g_app: *c.GApplication = @ptrCast(self.app); + + // We set the notification ID to the body content. If the content is the + // same, this notification may replace a previous notification + c.g_application_send_notification(g_app, n.body.ptr, notification); } /// Reload the configuration. This should return the new configuration. @@ -565,9 +806,9 @@ pub fn gtkQuitTimerExpired(ud: ?*anyopaque) callconv(.C) c.gboolean { } /// This will get called when there are no more open surfaces. -pub fn startQuitTimer(self: *App) void { +fn startQuitTimer(self: *App) void { // Cancel any previous timer. - self.cancelQuitTimer(); + self.stopQuitTimer(); // This is a no-op unless we are configured to quit after last window is closed. if (!self.config.@"quit-after-last-window-closed") return; @@ -582,7 +823,7 @@ pub fn startQuitTimer(self: *App) void { } /// This will get called when a new surface gets opened. -pub fn cancelQuitTimer(self: *App) void { +fn stopQuitTimer(self: *App) void { switch (self.quit_timer) { .off => {}, .expired => self.quit_timer = .{ .off = {} }, @@ -608,7 +849,7 @@ pub fn redrawInspector(self: *App, surface: *Surface) void { } /// Called by CoreApp to create a new window with a new surface. -pub fn newWindow(self: *App, parent_: ?*CoreSurface) !void { +fn newWindow(self: *App, parent_: ?*CoreSurface) !void { const alloc = self.core_app.alloc; // Allocate a fixed pointer for our window. We try to minimize diff --git a/src/apprt/gtk/Split.zig b/src/apprt/gtk/Split.zig index 105646c7c..7a3645d1b 100644 --- a/src/apprt/gtk/Split.zig +++ b/src/apprt/gtk/Split.zig @@ -7,7 +7,6 @@ const Allocator = std.mem.Allocator; const assert = std.debug.assert; const apprt = @import("../../apprt.zig"); const font = @import("../../font/main.zig"); -const input = @import("../../input.zig"); const CoreSurface = @import("../../Surface.zig"); const Surface = @import("Surface.zig"); @@ -21,14 +20,14 @@ pub const Orientation = enum { horizontal, vertical, - pub fn fromDirection(direction: apprt.SplitDirection) Orientation { + pub fn fromDirection(direction: apprt.action.SplitDirection) Orientation { return switch (direction) { .right => .horizontal, .down => .vertical, }; } - pub fn fromResizeDirection(direction: input.SplitResizeDirection) Orientation { + pub fn fromResizeDirection(direction: apprt.action.ResizeSplit.Direction) Orientation { return switch (direction) { .up, .down => .vertical, .left, .right => .horizontal, @@ -58,7 +57,7 @@ bottom_right: Surface.Container.Elem, pub fn create( alloc: Allocator, sibling: *Surface, - direction: apprt.SplitDirection, + direction: apprt.action.SplitDirection, ) !*Split { var split = try alloc.create(Split); errdefer alloc.destroy(split); @@ -69,7 +68,7 @@ pub fn create( pub fn init( self: *Split, sibling: *Surface, - direction: apprt.SplitDirection, + direction: apprt.action.SplitDirection, ) !void { // Create the new child surface for the other direction. const alloc = sibling.app.core_app.alloc; @@ -164,7 +163,11 @@ fn removeChild( } /// Move the divider in the given direction by the given amount. -pub fn moveDivider(self: *Split, direction: input.SplitResizeDirection, amount: u16) void { +pub fn moveDivider( + self: *Split, + direction: apprt.action.ResizeSplit.Direction, + amount: u16, +) void { const min_pos = 10; const pos = c.gtk_paned_get_position(self.paned); @@ -263,7 +266,7 @@ fn updateChildren(self: *const Split) void { /// A mapping of direction to the element (if any) in that direction. pub const DirectionMap = std.EnumMap( - input.SplitFocusDirection, + apprt.action.GotoSplit, ?*Surface, ); diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index caa4653f0..657b6abf4 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -688,7 +688,10 @@ pub fn close(self: *Surface, processActive: bool) void { c.gtk_widget_show(alert); } -pub fn controlInspector(self: *Surface, mode: input.InspectorMode) void { +pub fn controlInspector( + self: *Surface, + mode: apprt.action.Inspector, +) void { const show = switch (mode) { .toggle => self.inspector == null, .show => true, @@ -715,30 +718,6 @@ pub fn controlInspector(self: *Surface, mode: input.InspectorMode) void { }; } -pub fn toggleFullscreen(self: *Surface, mac_non_native: configpkg.NonNativeFullscreen) void { - const window = self.container.window() orelse { - log.info( - "toggleFullscreen invalid for container={s}", - .{@tagName(self.container)}, - ); - return; - }; - - window.toggleFullscreen(mac_non_native); -} - -pub fn toggleWindowDecorations(self: *Surface) void { - const window = self.container.window() orelse { - log.info( - "toggleWindowDecorations invalid for container={s}", - .{@tagName(self.container)}, - ); - return; - }; - - window.toggleWindowDecorations(); -} - pub fn getTitleLabel(self: *Surface) ?*c.GtkWidget { switch (self.title) { .none => return null, @@ -749,64 +728,6 @@ pub fn getTitleLabel(self: *Surface) ?*c.GtkWidget { } } -pub fn newSplit(self: *Surface, direction: apprt.SplitDirection) !void { - const alloc = self.app.core_app.alloc; - _ = try Split.create(alloc, self, direction); -} - -pub fn gotoSplit(self: *const Surface, direction: input.SplitFocusDirection) void { - const s = self.container.split() orelse return; - const map = s.directionMap(switch (self.container) { - .split_tl => .top_left, - .split_br => .bottom_right, - .none, .tab_ => unreachable, - }); - const surface_ = map.get(direction) orelse return; - if (surface_) |surface| surface.grabFocus(); -} - -pub fn resizeSplit(self: *const Surface, direction: input.SplitResizeDirection, amount: u16) void { - const s = self.container.firstSplitWithOrientation( - Split.Orientation.fromResizeDirection(direction), - ) orelse return; - s.moveDivider(direction, amount); -} - -pub fn equalizeSplits(self: *const Surface) void { - const tab = self.container.tab() orelse return; - const top_split = switch (tab.elem) { - .split => |s| s, - else => return, - }; - _ = top_split.equalize(); -} - -pub fn newTab(self: *Surface) !void { - const window = self.container.window() orelse { - log.info("surface cannot create new tab when not attached to a window", .{}); - return; - }; - - try window.newTab(&self.core_surface); -} - -pub fn gotoTab(self: *Surface, tab: apprt.GotoTab) void { - const window = self.container.window() orelse { - log.info( - "gotoTab invalid for container={s}", - .{@tagName(self.container)}, - ); - return; - }; - - switch (tab) { - .previous => window.gotoPreviousTab(self), - .next => window.gotoNextTab(self), - .last => window.gotoLastTab(), - else => window.gotoTab(@intCast(@intFromEnum(tab))), - } -} - pub fn setShouldClose(self: *Surface) void { _ = self; } @@ -1975,7 +1896,7 @@ fn translateMods(state: c.GdkModifierType) input.Mods { return mods; } -pub fn presentSurface(self: *Surface) void { +pub fn present(self: *Surface) void { if (self.container.window()) |window| { if (self.container.tab()) |tab| { if (window.notebook.getTabPosition(tab)) |position| @@ -1983,5 +1904,6 @@ pub fn presentSurface(self: *Surface) void { } c.gtk_window_present(window.window); } + self.grabFocus(); } diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index b6f896592..80bbd0944 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -459,7 +459,7 @@ pub fn gotoTab(self: *Window, n: usize) void { } /// Toggle fullscreen for this window. -pub fn toggleFullscreen(self: *Window, _: configpkg.NonNativeFullscreen) void { +pub fn toggleFullscreen(self: *Window) void { const is_fullscreen = c.gtk_window_is_fullscreen(self.window); if (is_fullscreen == 0) { c.gtk_window_fullscreen(self.window);