From a201b78fe47d474779044f2d3c5667b243c43c87 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 15 Jan 2025 08:21:46 -0600 Subject: [PATCH] gtk: menu start work on top menu --- src/apprt/gtk/Surface.zig | 12 +- src/apprt/gtk/Window.zig | 362 ++++++++++-------- src/apprt/gtk/gresource.zig | 19 +- src/apprt/gtk/menu.zig | 90 +++-- ...enu-surface.ui => menu-surface-context.ui} | 0 ...menu-window.ui => menu-window-titlebar.ui} | 0 src/apprt/gtk/ui/menu-window-top.ui | 106 +++++ 7 files changed, 383 insertions(+), 206 deletions(-) rename src/apprt/gtk/ui/{menu-surface.ui => menu-surface-context.ui} (100%) rename src/apprt/gtk/ui/{menu-window.ui => menu-window-titlebar.ui} (100%) create mode 100644 src/apprt/gtk/ui/menu-window-top.ui diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 51c09bfc5e..4031cf8982 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -381,7 +381,7 @@ im_len: u7 = 0, cgroup_path: ?[]const u8 = null, /// Our context menu. -menu: Menu(Surface), +context_menu: Menu(Surface, .context, .popover_menu), /// Configuration used for initializing the surface. We have to copy some /// data since initialization is delayed with GTK (on realize). @@ -567,13 +567,13 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { .cursor_pos = .{ .x = 0, .y = 0 }, .im_context = im_context, .cgroup_path = cgroup_path, - .menu = undefined, + .context_menu = undefined, }; errdefer self.* = undefined; // initialize the context menu - self.menu.init(); - self.menu.setParent(overlay); + self.context_menu.init(); + self.context_menu.setParent(overlay); // Set our default mouse shape try self.setMouseShape(.text); @@ -1431,7 +1431,7 @@ fn gtkMouseDown( // word and returns false. We can use this to handle the context menu // opening under normal scenarios. if (!consumed and button == .right) { - self.menu.popupAt(x, y); + self.context_menu.popupAt(x, y); } } @@ -1984,7 +1984,7 @@ pub fn dimSurface(self: *Surface) void { // Don't dim surface if context menu is open. // This means we got unfocused due to it opening. - if (self.menu.isVisible()) return; + if (self.context_menu.isVisible()) return; if (self.unfocused_widget != null) return; self.unfocused_widget = c.gtk_drawing_area_new(); diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index a89381cc53..cea6dd7cf1 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -48,8 +48,14 @@ tab_overview: ?*c.GtkWidget, /// can be either c.GtkNotebook or c.AdwTabView. notebook: Notebook, +/// The "top" menu that appears at the top of a window. +top_menu: Menu(Window, .top, .popover_menu_bar), + +/// Revealer for showing/hiding top menu. +top_menu_revealer: *c.GtkRevealer, + /// The "main" menu that is attached to a button in the headerbar. -menu: Menu(Window), +titlebar_menu: Menu(Window, .titlebar, .popover_menu), /// The libadwaita widget for receiving toast send requests. If libadwaita is /// not used, this is null and unused. @@ -75,6 +81,20 @@ pub fn create(alloc: Allocator, app: *App) !*Window { return window; } +pub const Flavor = enum { + gtk, + adw, + adw130, + adw140, +}; + +pub inline fn flavor(self: *const Window) Flavor { + if (adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) return .adw140; + if (adwaita.versionAtLeast(1, 3, 0) and adwaita.enabled(&self.app.config)) return .adw130; + if (adwaita.versionAtLeast(0, 0, 0) and adwaita.enabled(&self.app.config)) return .adw; + return .gtk; +} + pub fn init(self: *Window, app: *App) !void { // Set up our own state self.* = .{ @@ -82,23 +102,26 @@ pub fn init(self: *Window, app: *App) !void { .window = undefined, .headerbar = undefined, .tab_overview = null, + .toast_overlay = null, .notebook = undefined, - .menu = undefined, - .toast_overlay = undefined, + .top_menu = undefined, + .top_menu_revealer = undefined, + .titlebar_menu = undefined, .winproto = .none, }; // Create the window - const window: *c.GtkWidget = window: { - if ((comptime adwaita.versionAtLeast(0, 0, 0)) and adwaita.enabled(&self.app.config)) { + const window: *c.GtkWidget = switch (self.flavor()) { + .adw, .adw130, .adw140 => window: { const window = c.adw_application_window_new(app.app); c.gtk_widget_add_css_class(@ptrCast(window), "adw"); break :window window; - } else { + }, + .gtk => window: { const window = c.gtk_application_window_new(app.app); c.gtk_widget_add_css_class(@ptrCast(window), "gtk"); break :window window; - } + }, }; errdefer c.gtk_window_destroy(@ptrCast(window)); @@ -125,36 +148,45 @@ pub fn init(self: *Window, app: *App) !void { // Create our box which will hold our widgets in the main content area. const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0); - // Set up the menu - self.menu.init(); + // Set up the menus + self.top_menu.init(); + self.titlebar_menu.init(); + + self.top_menu_revealer = @ptrCast(@alignCast(c.gtk_revealer_new())); + c.gtk_revealer_set_child(self.top_menu_revealer, self.top_menu.asWidget()); + c.gtk_revealer_set_transition_type(self.top_menu_revealer, c.GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN); + c.gtk_revealer_set_reveal_child(self.top_menu_revealer, 1); // Setup our notebook self.notebook.init(); - // If we are using Adwaita, then we can support the tab overview. - self.tab_overview = if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.enabled(&self.app.config) and adwaita.versionAtLeast(1, 4, 0)) overview: { - const tab_overview = c.adw_tab_overview_new(); - c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw.tab_view); - c.adw_tab_overview_set_enable_new_tab(@ptrCast(tab_overview), 1); - _ = c.g_signal_connect_data( - tab_overview, - "create-tab", - c.G_CALLBACK(>kNewTabFromOverview), - self, - null, - c.G_CONNECT_DEFAULT, - ); - _ = c.g_signal_connect_data( - tab_overview, - "notify::open", - c.G_CALLBACK(&adwTabOverviewOpen), - self, - null, - c.G_CONNECT_DEFAULT, - ); + self.tab_overview = switch (self.flavor()) { + .adw140 => overview: { + // If we are using Adwaita 1.4.0, then we can support the tab overview. + const tab_overview = c.adw_tab_overview_new(); + c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw.tab_view); + c.adw_tab_overview_set_enable_new_tab(@ptrCast(tab_overview), 1); + _ = c.g_signal_connect_data( + tab_overview, + "create-tab", + c.G_CALLBACK(>kNewTabFromOverview), + self, + null, + c.G_CONNECT_DEFAULT, + ); + _ = c.g_signal_connect_data( + tab_overview, + "notify::open", + c.G_CALLBACK(&adwTabOverviewOpen), + self, + null, + c.G_CONNECT_DEFAULT, + ); - break :overview tab_overview; - } else null; + break :overview tab_overview; + }, + .adw, .adw130, .gtk => null, + }; // gtk-titlebar can be used to disable the header bar (but keep the window // manager's decorations). We create this no matter if we are decorated or @@ -165,7 +197,7 @@ pub fn init(self: *Window, app: *App) !void { const btn = c.gtk_menu_button_new(); c.gtk_widget_set_tooltip_text(btn, "Main Menu"); c.gtk_menu_button_set_icon_name(@ptrCast(btn), "open-menu-symbolic"); - c.gtk_menu_button_set_popover(@ptrCast(btn), self.menu.asWidget()); + c.gtk_menu_button_set_popover(@ptrCast(btn), self.titlebar_menu.asWidget()); _ = c.g_signal_connect_data( btn, "notify::active", @@ -179,34 +211,35 @@ pub fn init(self: *Window, app: *App) !void { // If we're using an AdwWindow then we can support the tab overview. if (self.tab_overview) |tab_overview| { - if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable; - assert(self.app.config.@"gtk-adwaita" and adwaita.versionAtLeast(1, 4, 0)); - const btn = switch (app.config.@"gtk-tabs-location") { - .top, .bottom, .left, .right => btn: { - const btn = c.gtk_toggle_button_new(); - c.gtk_widget_set_tooltip_text(btn, "View Open Tabs"); - c.gtk_button_set_icon_name(@ptrCast(btn), "view-grid-symbolic"); - _ = c.g_object_bind_property( - btn, - "active", - tab_overview, - "open", - c.G_BINDING_BIDIRECTIONAL | c.G_BINDING_SYNC_CREATE, - ); - - break :btn btn; - }, - - .hidden => btn: { - const btn = c.adw_tab_button_new(); - c.adw_tab_button_set_view(@ptrCast(btn), self.notebook.adw.tab_view); - c.gtk_actionable_set_action_name(@ptrCast(btn), "overview.open"); - break :btn btn; + switch (self.flavor()) { + .adw140 => { + const btn = switch (app.config.@"gtk-tabs-location") { + .top, .bottom, .left, .right => btn: { + const btn = c.gtk_toggle_button_new(); + c.gtk_widget_set_tooltip_text(btn, "View Open Tabs"); + c.gtk_button_set_icon_name(@ptrCast(btn), "view-grid-symbolic"); + _ = c.g_object_bind_property( + btn, + "active", + tab_overview, + "open", + c.G_BINDING_BIDIRECTIONAL | c.G_BINDING_SYNC_CREATE, + ); + break :btn btn; + }, + .hidden => btn: { + const btn = c.adw_tab_button_new(); + c.adw_tab_button_set_view(@ptrCast(btn), self.notebook.adw.tab_view); + c.gtk_actionable_set_action_name(@ptrCast(btn), "overview.open"); + break :btn btn; + }, + }; + + c.gtk_widget_set_focus_on_click(btn, c.FALSE); + self.headerbar.packEnd(btn); }, - }; - - c.gtk_widget_set_focus_on_click(btn, c.FALSE); - self.headerbar.packEnd(btn); + .gtk, .adw, .adw130 => {}, + } } { @@ -221,9 +254,17 @@ pub fn init(self: *Window, app: *App) !void { _ = c.g_signal_connect_data(gtk_window, "notify::fullscreened", c.G_CALLBACK(>kWindowNotifyFullscreened), self, null, c.G_CONNECT_DEFAULT); // If Adwaita is enabled and is older than 1.4.0 we don't have the tab overview and so we - // need to stick the headerbar into the content box. - if (!adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) { - c.gtk_box_append(@ptrCast(box), self.headerbar.asWidget()); + // need to stick the headerbar into the outer content box. + switch (self.flavor()) { + .adw140 => {}, + .adw, .adw130 => { + c.gtk_box_append(@ptrCast(box), self.headerbar.asWidget()); + c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(self.top_menu_revealer))); + }, + .gtk => { + c.gtk_window_set_titlebar(gtk_window, self.headerbar.asWidget()); + c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(self.top_menu_revealer))); + }, } // In debug we show a warning and apply the 'devel' class to the window. @@ -231,18 +272,18 @@ pub fn init(self: *Window, app: *App) !void { if (comptime std.debug.runtime_safety) { const warning_box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0); const warning_text = "⚠️ You're running a debug build of Ghostty! Performance will be degraded."; - if ((comptime adwaita.versionAtLeast(1, 3, 0)) and - adwaita.enabled(&app.config) and - adwaita.versionAtLeast(1, 3, 0)) - { - const banner = c.adw_banner_new(warning_text); - c.adw_banner_set_revealed(@ptrCast(banner), 1); - c.gtk_box_append(@ptrCast(warning_box), @ptrCast(banner)); - } else { - const warning = c.gtk_label_new(warning_text); - c.gtk_widget_set_margin_top(warning, 10); - c.gtk_widget_set_margin_bottom(warning, 10); - c.gtk_box_append(@ptrCast(warning_box), warning); + switch (self.flavor()) { + .adw130, .adw140 => { + const banner = c.adw_banner_new(warning_text); + c.adw_banner_set_revealed(@ptrCast(banner), 1); + c.gtk_box_append(@ptrCast(warning_box), @ptrCast(banner)); + }, + .adw, .gtk => { + const warning = c.gtk_label_new(warning_text); + c.gtk_widget_set_margin_top(warning, 10); + c.gtk_widget_set_margin_bottom(warning, 10); + c.gtk_box_append(@ptrCast(warning_box), warning); + }, } c.gtk_widget_add_css_class(@ptrCast(gtk_window), "devel"); c.gtk_widget_add_css_class(@ptrCast(warning_box), "background"); @@ -250,24 +291,30 @@ pub fn init(self: *Window, app: *App) !void { } // Setup our toast overlay if we have one - self.toast_overlay = if (adwaita.enabled(&self.app.config)) toast: { - const toast_overlay = c.adw_toast_overlay_new(); - c.adw_toast_overlay_set_child( - @ptrCast(toast_overlay), - @ptrCast(@alignCast(self.notebook.asWidget())), - ); - c.gtk_box_append(@ptrCast(box), toast_overlay); - break :toast toast_overlay; - } else toast: { - c.gtk_box_append(@ptrCast(box), self.notebook.asWidget()); - break :toast null; + self.toast_overlay = switch (self.flavor()) { + .adw, .adw130, .adw140 => overlay: { + const toast_overlay = c.adw_toast_overlay_new(); + c.adw_toast_overlay_set_child( + @ptrCast(toast_overlay), + @ptrCast(@alignCast(self.notebook.asWidget())), + ); + c.gtk_box_append(@ptrCast(box), toast_overlay); + break :overlay toast_overlay; + }, + .gtk => overlay: { + c.gtk_box_append(@ptrCast(box), self.notebook.asWidget()); + break :overlay null; + }, }; // If we have a tab overview then we can set it on our notebook. if (self.tab_overview) |tab_overview| { - if (comptime !adwaita.versionAtLeast(1, 3, 0)) unreachable; - assert(self.notebook == .adw); - c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw.tab_view); + switch (self.flavor()) { + .adw130, .adw140 => { + c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw.tab_view); + }, + .adw, .gtk => {}, + } } // If we want the window to be maximized, we do that here. @@ -290,87 +337,86 @@ pub fn init(self: *Window, app: *App) !void { _ = c.g_signal_connect_data(ec_key_press, "key-pressed", c.G_CALLBACK(>kKeyPressed), self, null, c.G_CONNECT_DEFAULT); // Our actions for the menu - initActions(self); + self.initActions(); - if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) { - const toolbar_view: *c.AdwToolbarView = @ptrCast(c.adw_toolbar_view_new()); + switch (self.flavor()) { + .adw140 => { + const toolbar_view: *c.AdwToolbarView = @ptrCast(c.adw_toolbar_view_new()); - c.adw_toolbar_view_add_top_bar(toolbar_view, self.headerbar.asWidget()); + const top_box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0); + c.gtk_box_append(@ptrCast(top_box), self.headerbar.asWidget()); + c.gtk_box_append(@ptrCast(top_box), @ptrCast(@alignCast(self.top_menu_revealer))); - if (self.app.config.@"gtk-tabs-location" != .hidden) { - const tab_bar = c.adw_tab_bar_new(); - c.adw_tab_bar_set_view(tab_bar, self.notebook.adw.tab_view); + c.adw_toolbar_view_add_top_bar(toolbar_view, top_box); - if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0); - - const tab_bar_widget: *c.GtkWidget = @ptrCast(@alignCast(tab_bar)); - switch (self.app.config.@"gtk-tabs-location") { - // left and right are not supported in libadwaita. - .top, .left, .right => c.adw_toolbar_view_add_top_bar(toolbar_view, tab_bar_widget), - .bottom => c.adw_toolbar_view_add_bottom_bar(toolbar_view, tab_bar_widget), - .hidden => unreachable, - } - } - c.adw_toolbar_view_set_content(toolbar_view, box); - - const toolbar_style: c.AdwToolbarStyle = switch (self.app.config.@"adw-toolbar-style") { - .flat => c.ADW_TOOLBAR_FLAT, - .raised => c.ADW_TOOLBAR_RAISED, - .@"raised-border" => c.ADW_TOOLBAR_RAISED_BORDER, - }; - c.adw_toolbar_view_set_top_bar_style(toolbar_view, toolbar_style); - c.adw_toolbar_view_set_bottom_bar_style(toolbar_view, toolbar_style); - - // Set our application window content. - c.adw_tab_overview_set_child( - @ptrCast(self.tab_overview), - @ptrCast(@alignCast(toolbar_view)), - ); - c.adw_application_window_set_content( - @ptrCast(gtk_window), - @ptrCast(@alignCast(self.tab_overview)), - ); - } else tab_bar: { - switch (self.notebook) { - .adw => |*adw| if (comptime adwaita.versionAtLeast(0, 0, 0)) { - if (app.config.@"gtk-tabs-location" == .hidden) break :tab_bar; - // In earlier adwaita versions, we need to add the tabbar manually since we do not use - // an AdwToolbarView. - const tab_bar: *c.AdwTabBar = c.adw_tab_bar_new().?; - c.gtk_widget_add_css_class(@ptrCast(@alignCast(tab_bar)), "inline"); - switch (app.config.@"gtk-tabs-location") { - .top, - .left, - .right, - => c.gtk_box_prepend( - @ptrCast(box), - @ptrCast(@alignCast(tab_bar)), - ), - - .bottom => c.gtk_box_append( - @ptrCast(box), - @ptrCast(@alignCast(tab_bar)), - ), - .hidden => unreachable, - } - c.adw_tab_bar_set_view(tab_bar, adw.tab_view); + if (self.app.config.@"gtk-tabs-location" != .hidden) { + const tab_bar = c.adw_tab_bar_new(); + c.adw_tab_bar_set_view(tab_bar, self.notebook.adw.tab_view); if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0); - }, - .gtk => {}, - } + const tab_bar_widget: *c.GtkWidget = @ptrCast(@alignCast(tab_bar)); + switch (self.app.config.@"gtk-tabs-location") { + // left and right are not supported in libadwaita. + .top, .left, .right => c.adw_toolbar_view_add_top_bar(toolbar_view, tab_bar_widget), + .bottom => c.adw_toolbar_view_add_bottom_bar(toolbar_view, tab_bar_widget), + .hidden => unreachable, + } + } + c.adw_toolbar_view_set_content(toolbar_view, box); + + const toolbar_style: c.AdwToolbarStyle = switch (self.app.config.@"adw-toolbar-style") { + .flat => c.ADW_TOOLBAR_FLAT, + .raised => c.ADW_TOOLBAR_RAISED, + .@"raised-border" => c.ADW_TOOLBAR_RAISED_BORDER, + }; + c.adw_toolbar_view_set_top_bar_style(toolbar_view, toolbar_style); + c.adw_toolbar_view_set_bottom_bar_style(toolbar_view, toolbar_style); + + // Set our application window content. + c.adw_tab_overview_set_child( + @ptrCast(self.tab_overview), + @ptrCast(@alignCast(toolbar_view)), + ); + c.adw_application_window_set_content( + @ptrCast(gtk_window), + @ptrCast(@alignCast(self.tab_overview.?)), + ); + }, + .adw, .adw130 => brk: { + if (app.config.@"gtk-tabs-location" == .hidden) break :brk; + + // In earlier adwaita versions, we need to add the tabbar manually since we do not use + // an AdwToolbarView. + const tab_bar: *c.AdwTabBar = c.adw_tab_bar_new().?; + c.gtk_widget_add_css_class(@ptrCast(@alignCast(tab_bar)), "inline"); + switch (app.config.@"gtk-tabs-location") { + .top, + .left, + .right, + => c.gtk_box_prepend( + @ptrCast(box), + @ptrCast(@alignCast(tab_bar)), + ), + + .bottom => c.gtk_box_append( + @ptrCast(box), + @ptrCast(@alignCast(tab_bar)), + ), + .hidden => unreachable, + } + c.adw_tab_bar_set_view(tab_bar, self.notebook.adw.tab_view); - // The box is our main child - if (!adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) { + if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0); c.adw_application_window_set_content( @ptrCast(gtk_window), box, ); - } else { - c.gtk_window_set_titlebar(gtk_window, self.headerbar.asWidget()); + }, + + .gtk => { c.gtk_window_set_child(gtk_window, box); - } + }, } // Show the window @@ -1083,7 +1129,7 @@ fn gtkMenuActivate( const active = c.gtk_menu_button_get_active(btn) != 0; const self = userdataSelf(ud orelse return); if (active) { - self.menu.refresh(); + self.titlebar_menu.refresh(); } else { self.focusCurrentTab(); } diff --git a/src/apprt/gtk/gresource.zig b/src/apprt/gtk/gresource.zig index 6935619b86..0b46a4e4f9 100644 --- a/src/apprt/gtk/gresource.zig +++ b/src/apprt/gtk/gresource.zig @@ -53,11 +53,6 @@ const icons = [_]struct { }, }; -const builder_files = [_][]const u8{ - "menu-window.ui", - "menu-surface.ui", -}; - pub const gresource_xml = comptimeGenerateGResourceXML(); fn comptimeGenerateGResourceXML() []const u8 { @@ -98,14 +93,6 @@ fn writeGResourceXML(writer: anytype) !void { .{ icon.alias, icon.source }, ); } - try writer.writeAll( - \\ - \\ - \\ - ); - for (builder_files) |builder_file| { - try writer.print(" src/apprt/gtk/ui/{s}\n", .{ builder_file, builder_file }); - } try writer.writeAll( \\ \\ @@ -114,7 +101,7 @@ fn writeGResourceXML(writer: anytype) !void { } pub const dependencies = deps: { - const total = css_files.len + icons.len + builder_files.len; + const total = css_files.len + icons.len; var deps: [total][]const u8 = undefined; var index: usize = 0; for (css_files) |css_file| { @@ -125,9 +112,5 @@ pub const dependencies = deps: { deps[index] = std.fmt.comptimePrint("images/icons/icon_{s}.png", .{icon.source}); index += 1; } - for (builder_files) |builder_file| { - deps[index] = std.fmt.comptimePrint("src/apprt/gtk/ui/{s}", .{builder_file}); - index += 1; - } break :deps deps; }; diff --git a/src/apprt/gtk/menu.zig b/src/apprt/gtk/menu.zig index f4293e29d9..4d0ff06739 100644 --- a/src/apprt/gtk/menu.zig +++ b/src/apprt/gtk/menu.zig @@ -8,30 +8,58 @@ const Surface = @import("Surface.zig"); const log = std.log.scoped(.gtk_menu); -pub fn Menu(comptime T: type) type { +pub fn Menu( + comptime T: type, + variant: enum { top, titlebar, context }, + style: enum { popover_menu, popover_menu_bar }, +) type { return struct { + const Self = @This(); + const MenuWidget = switch (style) { + .popover_menu => c.GtkPopoverMenu, + .popover_menu_bar => c.GtkPopoverMenuBar, + }; + parent: *T, - popover: *c.GtkPopover, + menu_widget: *MenuWidget, - pub fn init(self: *Menu(T)) void { + pub fn init(self: *Self) void { const name = switch (T) { Window => "window", Surface => "surface", else => unreachable, }; - const parent: *T = @alignCast(@fieldParentPtr("menu", self)); + const parent: *T = @alignCast(@fieldParentPtr(@tagName(variant) ++ "_menu", self)); - const builder = c.gtk_builder_new_from_resource("/com/mitchellh/ghostty/ui/menu-" ++ name ++ ".ui"); + const data = @embedFile("ui/menu-" ++ name ++ "-" ++ @tagName(variant) ++ ".ui"); + const builder = c.gtk_builder_new_from_string(data.ptr, @intCast(data.len)); defer c.g_object_unref(@ptrCast(builder)); - const menu: *c.GMenuModel = @ptrCast(@alignCast(c.gtk_builder_get_object(builder, "menu"))); - const popover: *c.GtkPopover = @ptrCast(@alignCast(c.gtk_popover_menu_new_from_model(menu))); - c.gtk_popover_menu_set_flags(@ptrCast(@alignCast(popover)), c.GTK_POPOVER_MENU_NESTED); + const menu_model: *c.GMenuModel = @ptrCast(@alignCast(c.gtk_builder_get_object(builder, "menu"))); + + const menu_widget: *MenuWidget = switch (style) { + .popover_menu => brk: { + const menu_widget: *MenuWidget = @ptrCast(@alignCast(c.gtk_popover_menu_new_from_model(menu_model))); + c.gtk_popover_menu_set_flags(menu_widget, c.GTK_POPOVER_MENU_NESTED); + _ = c.g_signal_connect_data( + @ptrCast(@alignCast(menu_widget)), + "closed", + c.G_CALLBACK(>kRefocusTerm), + self, + null, + c.G_CONNECT_DEFAULT, + ); + break :brk menu_widget; + }, + .popover_menu_bar => brk: { + break :brk @ptrCast(@alignCast(c.gtk_popover_menu_bar_new_from_model(menu_model))); + }, + }; _ = c.g_signal_connect_data( - popover, - "closed", - c.G_CALLBACK(>kRefocusTerm), + @ptrCast(@alignCast(menu_widget)), + "show", + c.G_CALLBACK(>kShow), self, null, c.G_CONNECT_DEFAULT, @@ -39,27 +67,31 @@ pub fn Menu(comptime T: type) type { self.* = .{ .parent = parent, - .popover = popover, + .menu_widget = menu_widget, }; } - pub fn setParent(self: *const Menu(T), widget: *c.GtkWidget) void { + pub fn setParent(self: *const Self, widget: *c.GtkWidget) void { c.gtk_widget_set_parent(self.asWidget(), widget); } - pub fn asPopover(self: *const Menu(T)) *c.GtkPopover { - return self.popover; + pub fn asPopover(self: *const Self) *c.GtkPopover { + return @ptrCast(@alignCast(self.menu_widget)); } - pub fn asWidget(self: *const Menu(T)) *c.GtkWidget { - return @ptrCast(@alignCast(self.popover)); + pub fn asWidget(self: *const Self) *c.GtkWidget { + return @ptrCast(@alignCast(self.menu_widget)); } - pub fn isVisible(self: *const Menu(T)) bool { + pub fn isVisible(self: *const Self) bool { return c.gtk_widget_get_visible(self.asWidget()) != 0; } - pub fn refresh(self: *const Menu(T)) void { + pub fn setVisible(self: *const Self, visible: bool) void { + return c.gtk_widget_set_visible(self.asWidget(), @intFromBool(visible)); + } + + pub fn refresh(self: *const Self) void { const window: *Window, const has_selection: bool = switch (T) { Window => window: { const core_surface = self.parent.actionSurface() orelse break :window .{ self.parent, false }; @@ -81,20 +113,30 @@ pub fn Menu(comptime T: type) type { c.g_simple_action_set_enabled(action, @intFromBool(has_selection)); } - pub fn popupAt(self: *const Menu(T), x: f64, y: f64) void { + pub fn popupAt(self: *const Self, x: f64, y: f64) void { const rect: c.GdkRectangle = .{ .x = @intFromFloat(x), .y = @intFromFloat(y), .width = 1, .height = 1, }; - c.gtk_popover_set_pointing_to(self.popover, &rect); + c.gtk_popover_set_pointing_to(self.asPopover(), &rect); self.refresh(); - c.gtk_popover_popup(self.popover); + c.gtk_popover_popup(self.asPopover()); } - fn gtkRefocusTerm(_: *c.GtkPopover, _: *c.GVariant, ud: ?*anyopaque) callconv(.C) bool { - const self: *Menu(T) = @ptrCast(@alignCast(ud orelse return false)); + fn gtkShow(_: *MenuWidget, _: *c.GVariant, _: ?*anyopaque) callconv(.C) bool { + // const self: *Self = @ptrCast(@alignCast(ud orelse return false)); + + log.info("show!!!", .{}); + + return true; + } + + fn gtkRefocusTerm(_: *MenuWidget, _: *c.GVariant, ud: ?*anyopaque) callconv(.C) bool { + const self: *Self = @ptrCast(@alignCast(ud orelse return false)); + + log.info("closed!!!", .{}); const window: *Window = switch (T) { Window => self.parent, diff --git a/src/apprt/gtk/ui/menu-surface.ui b/src/apprt/gtk/ui/menu-surface-context.ui similarity index 100% rename from src/apprt/gtk/ui/menu-surface.ui rename to src/apprt/gtk/ui/menu-surface-context.ui diff --git a/src/apprt/gtk/ui/menu-window.ui b/src/apprt/gtk/ui/menu-window-titlebar.ui similarity index 100% rename from src/apprt/gtk/ui/menu-window.ui rename to src/apprt/gtk/ui/menu-window-titlebar.ui diff --git a/src/apprt/gtk/ui/menu-window-top.ui b/src/apprt/gtk/ui/menu-window-top.ui new file mode 100644 index 0000000000..f7f9ea4c3d --- /dev/null +++ b/src/apprt/gtk/ui/menu-window-top.ui @@ -0,0 +1,106 @@ + + + + + + _File +
+ + New Window + win.new-window + + + Close Window + win.close + +
+
+ + New Tab + win.new-tab + + + Close Tab + win.close-tab + +
+
+ + Split +
+ + Split Up + win.split-up + + + Split Down + win.split-down + + + Split Left + win.split-left + + + Split Right + win.split-right + +
+
+
+
+ + Quit + app.quit + +
+
+ + _Edit +
+ + Copy + win.copy + + + Paste + win.paste + +
+
+ + Clear + win.clear + + + Reset + win.reset + +
+
+ + Open Configuration + app.open-config + + + Reload Configuration + app.reload-config + +
+
+ + _Help +
+ + Terminal Inspector + win.toggle-inspector + +
+
+ + About Ghostty + win.about + +
+
+
+