mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 00:06:09 +03:00
Merge pull request #474 from mitchellh/gtk-refactor
GTK refactor, ability to drag tab out to new window
This commit is contained in:
1801
src/apprt/gtk.zig
1801
src/apprt/gtk.zig
File diff suppressed because it is too large
Load Diff
298
src/apprt/gtk/App.zig
Normal file
298
src/apprt/gtk/App.zig
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
/// App is the entrypoint for the application. This is called after all
|
||||||
|
/// of the runtime-agnostic initialization is complete and we're ready
|
||||||
|
/// to start.
|
||||||
|
///
|
||||||
|
/// There is only ever one App instance per process. This is because most
|
||||||
|
/// application frameworks also have this restriction so it simplifies
|
||||||
|
/// the assumptions.
|
||||||
|
///
|
||||||
|
/// In GTK, the App contains the primary GApplication and GMainContext
|
||||||
|
/// (event loop) along with any global app state.
|
||||||
|
const App = @This();
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
const glfw = @import("glfw");
|
||||||
|
const configpkg = @import("../../config.zig");
|
||||||
|
const Config = configpkg.Config;
|
||||||
|
const CoreApp = @import("../../App.zig");
|
||||||
|
const CoreSurface = @import("../../Surface.zig");
|
||||||
|
|
||||||
|
const Surface = @import("Surface.zig");
|
||||||
|
const Window = @import("Window.zig");
|
||||||
|
const c = @import("c.zig");
|
||||||
|
|
||||||
|
const log = std.log.scoped(.gtk);
|
||||||
|
|
||||||
|
pub const Options = struct {};
|
||||||
|
|
||||||
|
core_app: *CoreApp,
|
||||||
|
config: Config,
|
||||||
|
|
||||||
|
app: *c.GtkApplication,
|
||||||
|
ctx: *c.GMainContext,
|
||||||
|
|
||||||
|
/// The "none" cursor. We use one that is shared across the entire app.
|
||||||
|
cursor_none: ?*c.GdkCursor,
|
||||||
|
|
||||||
|
/// This is set to false when the main loop should exit.
|
||||||
|
running: bool = true,
|
||||||
|
|
||||||
|
pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||||
|
_ = opts;
|
||||||
|
|
||||||
|
// This is super weird, but we still use GLFW with GTK only so that
|
||||||
|
// we can tap into their folklore logic to get screen DPI. If we can
|
||||||
|
// figure out a reliable way to determine this ourselves, we can get
|
||||||
|
// rid of this dep.
|
||||||
|
if (!glfw.init(.{})) return error.GlfwInitFailed;
|
||||||
|
|
||||||
|
// Load our configuration
|
||||||
|
var config = try Config.load(core_app.alloc);
|
||||||
|
errdefer config.deinit();
|
||||||
|
|
||||||
|
// If we had configuration errors, then log them.
|
||||||
|
if (!config._errors.empty()) {
|
||||||
|
for (config._errors.list.items) |err| {
|
||||||
|
log.warn("configuration error: {s}", .{err.message});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The "none" cursor is used for hiding the cursor
|
||||||
|
const cursor_none = c.gdk_cursor_new_from_name("none", null);
|
||||||
|
errdefer if (cursor_none) |cursor| c.g_object_unref(cursor);
|
||||||
|
|
||||||
|
// Our uniqueness ID is based on whether we're in a debug mode or not.
|
||||||
|
// In debug mode we want to be separate so we can develop Ghostty in
|
||||||
|
// Ghostty.
|
||||||
|
const uniqueness_id: ?[*c]const u8 = uniqueness_id: {
|
||||||
|
if (!config.@"gtk-single-instance") break :uniqueness_id null;
|
||||||
|
|
||||||
|
break :uniqueness_id "com.mitchellh.ghostty" ++ if (builtin.mode == .Debug) "-debug" else "";
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create our GTK Application which encapsulates our process.
|
||||||
|
const app = @as(?*c.GtkApplication, @ptrCast(c.gtk_application_new(
|
||||||
|
uniqueness_id orelse null,
|
||||||
|
c.G_APPLICATION_DEFAULT_FLAGS,
|
||||||
|
))) orelse return error.GtkInitFailed;
|
||||||
|
errdefer c.g_object_unref(app);
|
||||||
|
_ = c.g_signal_connect_data(
|
||||||
|
app,
|
||||||
|
"activate",
|
||||||
|
c.G_CALLBACK(&activate),
|
||||||
|
core_app,
|
||||||
|
null,
|
||||||
|
c.G_CONNECT_DEFAULT,
|
||||||
|
);
|
||||||
|
|
||||||
|
// We don't use g_application_run, we want to manually control the
|
||||||
|
// loop so we have to do the same things the run function does:
|
||||||
|
// https://github.com/GNOME/glib/blob/a8e8b742e7926e33eb635a8edceac74cf239d6ed/gio/gapplication.c#L2533
|
||||||
|
const ctx = c.g_main_context_default() orelse return error.GtkContextFailed;
|
||||||
|
if (c.g_main_context_acquire(ctx) == 0) return error.GtkContextAcquireFailed;
|
||||||
|
errdefer c.g_main_context_release(ctx);
|
||||||
|
|
||||||
|
const gapp = @as(*c.GApplication, @ptrCast(app));
|
||||||
|
var err_: ?*c.GError = null;
|
||||||
|
if (c.g_application_register(
|
||||||
|
gapp,
|
||||||
|
null,
|
||||||
|
@ptrCast(&err_),
|
||||||
|
) == 0) {
|
||||||
|
if (err_) |err| {
|
||||||
|
log.warn("error registering application: {s}", .{err.message});
|
||||||
|
c.g_error_free(err);
|
||||||
|
}
|
||||||
|
return error.GtkApplicationRegisterFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This just calls the "activate" signal but its part of the normal
|
||||||
|
// startup routine so we just call it:
|
||||||
|
// https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302
|
||||||
|
c.g_application_activate(gapp);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.core_app = core_app,
|
||||||
|
.app = app,
|
||||||
|
.config = config,
|
||||||
|
.ctx = ctx,
|
||||||
|
.cursor_none = cursor_none,
|
||||||
|
|
||||||
|
// If we are NOT the primary instance, then we never want to run.
|
||||||
|
// This means that another instance of the GTK app is running and
|
||||||
|
// our "activate" call above will open a window.
|
||||||
|
.running = c.g_application_get_is_remote(gapp) == 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terminate the application. The application will not be restarted after
|
||||||
|
// this so all global state can be cleaned up.
|
||||||
|
pub fn terminate(self: *App) void {
|
||||||
|
c.g_settings_sync();
|
||||||
|
while (c.g_main_context_iteration(self.ctx, 0) != 0) {}
|
||||||
|
c.g_main_context_release(self.ctx);
|
||||||
|
c.g_object_unref(self.app);
|
||||||
|
|
||||||
|
if (self.cursor_none) |cursor| c.g_object_unref(cursor);
|
||||||
|
|
||||||
|
self.config.deinit();
|
||||||
|
|
||||||
|
glfw.terminate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reload the configuration. This should return the new configuration.
|
||||||
|
/// The old value can be freed immediately at this point assuming a
|
||||||
|
/// successful return.
|
||||||
|
///
|
||||||
|
/// The returned pointer value is only valid for a stable self pointer.
|
||||||
|
pub fn reloadConfig(self: *App) !?*const Config {
|
||||||
|
// Load our configuration
|
||||||
|
var config = try Config.load(self.core_app.alloc);
|
||||||
|
errdefer config.deinit();
|
||||||
|
|
||||||
|
// Update the existing config, be sure to clean up the old one.
|
||||||
|
self.config.deinit();
|
||||||
|
self.config = config;
|
||||||
|
|
||||||
|
return &self.config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called by CoreApp to wake up the event loop.
|
||||||
|
pub fn wakeup(self: App) void {
|
||||||
|
_ = self;
|
||||||
|
c.g_main_context_wakeup(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the event loop. This doesn't return until the app exits.
|
||||||
|
pub fn run(self: *App) !void {
|
||||||
|
while (self.running) {
|
||||||
|
_ = c.g_main_context_iteration(self.ctx, 1);
|
||||||
|
|
||||||
|
// Tick the terminal app
|
||||||
|
const should_quit = try self.core_app.tick(self);
|
||||||
|
if (should_quit) self.quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close the given surface.
|
||||||
|
pub fn redrawSurface(self: *App, surface: *Surface) void {
|
||||||
|
_ = self;
|
||||||
|
surface.redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called by CoreApp to create a new window with a new surface.
|
||||||
|
pub fn newWindow(self: *App, parent_: ?*CoreSurface) !void {
|
||||||
|
const alloc = self.core_app.alloc;
|
||||||
|
|
||||||
|
// Allocate a fixed pointer for our window. We try to minimize
|
||||||
|
// allocations but windows and other GUI requirements are so minimal
|
||||||
|
// compared to the steady-state terminal operation so we use heap
|
||||||
|
// allocation for this.
|
||||||
|
//
|
||||||
|
// The allocation is owned by the GtkWindow created. It will be
|
||||||
|
// freed when the window is closed.
|
||||||
|
var window = try Window.create(alloc, self);
|
||||||
|
|
||||||
|
// Add our initial tab
|
||||||
|
try window.newTab(parent_);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quit(self: *App) void {
|
||||||
|
// If we have no toplevel windows, then we're done.
|
||||||
|
const list = c.gtk_window_list_toplevels();
|
||||||
|
if (list == null) {
|
||||||
|
self.running = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
c.g_list_free(list);
|
||||||
|
|
||||||
|
// If the app says we don't need to confirm, then we can quit now.
|
||||||
|
if (!self.core_app.needsConfirmQuit()) {
|
||||||
|
self.quitNow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have windows, then we want to confirm that we want to exit.
|
||||||
|
const alert = c.gtk_message_dialog_new(
|
||||||
|
null,
|
||||||
|
c.GTK_DIALOG_MODAL,
|
||||||
|
c.GTK_MESSAGE_QUESTION,
|
||||||
|
c.GTK_BUTTONS_YES_NO,
|
||||||
|
"Quit Ghostty?",
|
||||||
|
);
|
||||||
|
c.gtk_message_dialog_format_secondary_text(
|
||||||
|
@ptrCast(alert),
|
||||||
|
"All active terminal sessions will be terminated.",
|
||||||
|
);
|
||||||
|
|
||||||
|
// We want the "yes" to appear destructive.
|
||||||
|
const yes_widget = c.gtk_dialog_get_widget_for_response(
|
||||||
|
@ptrCast(alert),
|
||||||
|
c.GTK_RESPONSE_YES,
|
||||||
|
);
|
||||||
|
c.gtk_widget_add_css_class(yes_widget, "destructive-action");
|
||||||
|
|
||||||
|
// We want the "no" to be the default action
|
||||||
|
c.gtk_dialog_set_default_response(
|
||||||
|
@ptrCast(alert),
|
||||||
|
c.GTK_RESPONSE_NO,
|
||||||
|
);
|
||||||
|
|
||||||
|
_ = c.g_signal_connect_data(
|
||||||
|
alert,
|
||||||
|
"response",
|
||||||
|
c.G_CALLBACK(>kQuitConfirmation),
|
||||||
|
self,
|
||||||
|
null,
|
||||||
|
c.G_CONNECT_DEFAULT,
|
||||||
|
);
|
||||||
|
|
||||||
|
c.gtk_widget_show(alert);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This immediately destroys all windows, forcing the application to quit.
|
||||||
|
fn quitNow(self: *App) void {
|
||||||
|
_ = self;
|
||||||
|
const list = c.gtk_window_list_toplevels();
|
||||||
|
defer c.g_list_free(list);
|
||||||
|
c.g_list_foreach(list, struct {
|
||||||
|
fn callback(data: c.gpointer, _: c.gpointer) callconv(.C) void {
|
||||||
|
const ptr = data orelse return;
|
||||||
|
const widget: *c.GtkWidget = @ptrCast(@alignCast(ptr));
|
||||||
|
const window: *c.GtkWindow = @ptrCast(widget);
|
||||||
|
c.gtk_window_destroy(window);
|
||||||
|
}
|
||||||
|
}.callback, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gtkQuitConfirmation(
|
||||||
|
alert: *c.GtkMessageDialog,
|
||||||
|
response: c.gint,
|
||||||
|
ud: ?*anyopaque,
|
||||||
|
) callconv(.C) void {
|
||||||
|
const self: *App = @ptrCast(@alignCast(ud orelse return));
|
||||||
|
|
||||||
|
// Close the alert window
|
||||||
|
c.gtk_window_destroy(@ptrCast(alert));
|
||||||
|
|
||||||
|
// If we didn't confirm then we're done
|
||||||
|
if (response != c.GTK_RESPONSE_YES) return;
|
||||||
|
|
||||||
|
// Force close all open windows
|
||||||
|
self.quitNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is called by the "activate" signal. This is sent on program
|
||||||
|
/// startup and also when a secondary instance launches and requests
|
||||||
|
/// a new window.
|
||||||
|
fn activate(app: *c.GtkApplication, ud: ?*anyopaque) callconv(.C) void {
|
||||||
|
_ = app;
|
||||||
|
|
||||||
|
const core_app: *CoreApp = @ptrCast(@alignCast(ud orelse return));
|
||||||
|
|
||||||
|
// Queue a new window
|
||||||
|
_ = core_app.mailbox.push(.{
|
||||||
|
.new_window = .{},
|
||||||
|
}, .{ .forever = {} });
|
||||||
|
}
|
1140
src/apprt/gtk/Surface.zig
Normal file
1140
src/apprt/gtk/Surface.zig
Normal file
File diff suppressed because it is too large
Load Diff
460
src/apprt/gtk/Window.zig
Normal file
460
src/apprt/gtk/Window.zig
Normal file
@ -0,0 +1,460 @@
|
|||||||
|
/// A Window is a single, real GTK window.
|
||||||
|
const Window = @This();
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const configpkg = @import("../../config.zig");
|
||||||
|
const font = @import("../../font/main.zig");
|
||||||
|
const CoreSurface = @import("../../Surface.zig");
|
||||||
|
|
||||||
|
const App = @import("App.zig");
|
||||||
|
const Surface = @import("Surface.zig");
|
||||||
|
const c = @import("c.zig");
|
||||||
|
|
||||||
|
const log = std.log.scoped(.gtk);
|
||||||
|
|
||||||
|
const GL_AREA_SURFACE = "gl_area_surface";
|
||||||
|
|
||||||
|
app: *App,
|
||||||
|
|
||||||
|
/// Our window
|
||||||
|
window: *c.GtkWindow,
|
||||||
|
|
||||||
|
/// The notebook (tab grouping) for this window.
|
||||||
|
notebook: *c.GtkNotebook,
|
||||||
|
|
||||||
|
/// The resources directory for the icon (if any). We need to retain a
|
||||||
|
/// pointer to this because GTK can use it at any time.
|
||||||
|
icon_search_dir: ?[:0]const u8 = null,
|
||||||
|
|
||||||
|
pub fn create(alloc: Allocator, app: *App) !*Window {
|
||||||
|
// Allocate a fixed pointer for our window. We try to minimize
|
||||||
|
// allocations but windows and other GUI requirements are so minimal
|
||||||
|
// compared to the steady-state terminal operation so we use heap
|
||||||
|
// allocation for this.
|
||||||
|
//
|
||||||
|
// The allocation is owned by the GtkWindow created. It will be
|
||||||
|
// freed when the window is closed.
|
||||||
|
var window = try alloc.create(Window);
|
||||||
|
errdefer alloc.destroy(window);
|
||||||
|
try window.init(app);
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(self: *Window, app: *App) !void {
|
||||||
|
// Set up our own state
|
||||||
|
self.* = .{
|
||||||
|
.app = app,
|
||||||
|
.window = undefined,
|
||||||
|
.notebook = undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the window
|
||||||
|
const window = c.gtk_application_window_new(app.app);
|
||||||
|
const gtk_window: *c.GtkWindow = @ptrCast(window);
|
||||||
|
errdefer c.gtk_window_destroy(gtk_window);
|
||||||
|
self.window = gtk_window;
|
||||||
|
c.gtk_window_set_title(gtk_window, "Ghostty");
|
||||||
|
c.gtk_window_set_default_size(gtk_window, 1000, 600);
|
||||||
|
|
||||||
|
// If we don't have the icon then we'll try to add our resources dir
|
||||||
|
// to the search path and see if we can find it there.
|
||||||
|
const icon_name = "com.mitchellh.ghostty";
|
||||||
|
const icon_theme = c.gtk_icon_theme_get_for_display(c.gtk_widget_get_display(window));
|
||||||
|
if (c.gtk_icon_theme_has_icon(icon_theme, icon_name) == 0) icon: {
|
||||||
|
const base = self.app.core_app.resources_dir orelse {
|
||||||
|
log.info("gtk app missing Ghostty icon and no resources dir detected", .{});
|
||||||
|
log.info("gtk app will not have Ghostty icon", .{});
|
||||||
|
break :icon;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Note that this method for adding the icon search path is
|
||||||
|
// a fallback mechanism. The recommended mechanism is the
|
||||||
|
// Freedesktop Icon Theme Specification. We distribute a ".desktop"
|
||||||
|
// file in zig-out/share that should be installed to the proper
|
||||||
|
// place.
|
||||||
|
const dir = try std.fmt.allocPrintZ(app.core_app.alloc, "{s}/icons", .{base});
|
||||||
|
self.icon_search_dir = dir;
|
||||||
|
c.gtk_icon_theme_add_search_path(icon_theme, dir.ptr);
|
||||||
|
if (c.gtk_icon_theme_has_icon(icon_theme, icon_name) == 0) {
|
||||||
|
log.warn("Ghostty icon for gtk app not found", .{});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.gtk_window_set_icon_name(gtk_window, icon_name);
|
||||||
|
|
||||||
|
// Apply background opacity if we have it
|
||||||
|
if (app.config.@"background-opacity" < 1) {
|
||||||
|
c.gtk_widget_set_opacity(@ptrCast(window), app.config.@"background-opacity");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide window decoration if configured. This has to happen before
|
||||||
|
// `gtk_widget_show`.
|
||||||
|
if (!app.config.@"window-decoration") {
|
||||||
|
c.gtk_window_set_decorated(gtk_window, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a notebook to hold our tabs.
|
||||||
|
const notebook_widget = c.gtk_notebook_new();
|
||||||
|
const notebook: *c.GtkNotebook = @ptrCast(notebook_widget);
|
||||||
|
self.notebook = notebook;
|
||||||
|
c.gtk_notebook_set_tab_pos(notebook, c.GTK_POS_TOP);
|
||||||
|
c.gtk_notebook_set_scrollable(notebook, 1);
|
||||||
|
c.gtk_notebook_set_show_tabs(notebook, 0);
|
||||||
|
c.gtk_notebook_set_show_border(notebook, 0);
|
||||||
|
|
||||||
|
// This is important so the notebook expands to fit available space.
|
||||||
|
// Otherwise, it will be zero/zero in the box below.
|
||||||
|
c.gtk_widget_set_vexpand(notebook_widget, 1);
|
||||||
|
c.gtk_widget_set_hexpand(notebook_widget, 1);
|
||||||
|
|
||||||
|
// Create our add button for new tabs
|
||||||
|
const notebook_add_btn = c.gtk_button_new_from_icon_name("list-add-symbolic");
|
||||||
|
c.gtk_notebook_set_action_widget(notebook, notebook_add_btn, c.GTK_PACK_END);
|
||||||
|
|
||||||
|
// Create our box which will hold our widgets.
|
||||||
|
const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0);
|
||||||
|
|
||||||
|
// In debug we show a warning. This is a really common issue where
|
||||||
|
// people build from source in debug and performance is really bad.
|
||||||
|
if (builtin.mode == .Debug) {
|
||||||
|
const warning = c.gtk_label_new("⚠️ You're running a debug build of Ghostty! Performance will be degraded.");
|
||||||
|
c.gtk_widget_set_margin_top(warning, 10);
|
||||||
|
c.gtk_widget_set_margin_bottom(warning, 10);
|
||||||
|
c.gtk_box_append(@ptrCast(box), warning);
|
||||||
|
}
|
||||||
|
c.gtk_box_append(@ptrCast(box), notebook_widget);
|
||||||
|
|
||||||
|
// All of our events
|
||||||
|
_ = c.g_signal_connect_data(window, "close-request", c.G_CALLBACK(>kCloseRequest), self, null, c.G_CONNECT_DEFAULT);
|
||||||
|
_ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT);
|
||||||
|
_ = c.g_signal_connect_data(notebook_add_btn, "clicked", c.G_CALLBACK(>kTabAddClick), self, null, c.G_CONNECT_DEFAULT);
|
||||||
|
_ = c.g_signal_connect_data(notebook, "page-added", c.G_CALLBACK(>kPageAdded), self, null, c.G_CONNECT_DEFAULT);
|
||||||
|
_ = c.g_signal_connect_data(notebook, "page-removed", c.G_CALLBACK(>kPageRemoved), self, null, c.G_CONNECT_DEFAULT);
|
||||||
|
_ = c.g_signal_connect_data(notebook, "switch-page", c.G_CALLBACK(>kSwitchPage), self, null, c.G_CONNECT_DEFAULT);
|
||||||
|
_ = c.g_signal_connect_data(notebook, "create-window", c.G_CALLBACK(>kNotebookCreateWindow), self, null, c.G_CONNECT_DEFAULT);
|
||||||
|
|
||||||
|
// The box is our main child
|
||||||
|
c.gtk_window_set_child(gtk_window, box);
|
||||||
|
|
||||||
|
// Show the window
|
||||||
|
c.gtk_widget_show(window);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Window) void {
|
||||||
|
if (self.icon_search_dir) |ptr| self.app.core_app.alloc.free(ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a new tab to this window.
|
||||||
|
pub fn newTab(self: *Window, parent_: ?*CoreSurface) !void {
|
||||||
|
// Grab a surface allocation we'll need it later.
|
||||||
|
var surface = try self.app.core_app.alloc.create(Surface);
|
||||||
|
errdefer self.app.core_app.alloc.destroy(surface);
|
||||||
|
|
||||||
|
// Inherit the parent's font size if we are configured to.
|
||||||
|
const font_size: ?font.face.DesiredSize = font_size: {
|
||||||
|
if (!self.app.config.@"window-inherit-font-size") break :font_size null;
|
||||||
|
const parent = parent_ orelse break :font_size null;
|
||||||
|
break :font_size parent.font_size;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build our tab label
|
||||||
|
const label_box_widget = c.gtk_box_new(c.GTK_ORIENTATION_HORIZONTAL, 0);
|
||||||
|
const label_box = @as(*c.GtkBox, @ptrCast(label_box_widget));
|
||||||
|
const label_text = c.gtk_label_new("Ghostty");
|
||||||
|
c.gtk_box_append(label_box, label_text);
|
||||||
|
|
||||||
|
const label_close_widget = c.gtk_button_new_from_icon_name("window-close");
|
||||||
|
const label_close = @as(*c.GtkButton, @ptrCast(label_close_widget));
|
||||||
|
c.gtk_button_set_has_frame(label_close, 0);
|
||||||
|
c.gtk_box_append(label_box, label_close_widget);
|
||||||
|
_ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(>kTabCloseClick), surface, null, c.G_CONNECT_DEFAULT);
|
||||||
|
|
||||||
|
// Initialize the GtkGLArea and attach it to our surface.
|
||||||
|
// The surface starts in the "unrealized" state because we have to
|
||||||
|
// wait for the "realize" callback from GTK to know that the OpenGL
|
||||||
|
// context is ready. See Surface docs for more info.
|
||||||
|
const gl_area = c.gtk_gl_area_new();
|
||||||
|
try surface.init(self.app, .{
|
||||||
|
.window = self,
|
||||||
|
.gl_area = @ptrCast(gl_area),
|
||||||
|
.title_label = @ptrCast(label_text),
|
||||||
|
.font_size = font_size,
|
||||||
|
});
|
||||||
|
errdefer surface.deinit();
|
||||||
|
const page_idx = c.gtk_notebook_append_page(self.notebook, gl_area, label_box_widget);
|
||||||
|
if (page_idx < 0) {
|
||||||
|
log.warn("failed to add surface to notebook", .{});
|
||||||
|
return error.GtkAppendPageFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab settings
|
||||||
|
c.gtk_notebook_set_tab_reorderable(self.notebook, gl_area, 1);
|
||||||
|
c.gtk_notebook_set_tab_detachable(self.notebook, gl_area, 1);
|
||||||
|
|
||||||
|
// If we have multiple tabs, show the tab bar.
|
||||||
|
if (c.gtk_notebook_get_n_pages(self.notebook) > 1) {
|
||||||
|
c.gtk_notebook_set_show_tabs(self.notebook, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the userdata of the close button so it points to this page.
|
||||||
|
c.g_object_set_data(@ptrCast(gl_area), GL_AREA_SURFACE, surface);
|
||||||
|
|
||||||
|
// Switch to the new tab
|
||||||
|
c.gtk_notebook_set_current_page(self.notebook, page_idx);
|
||||||
|
|
||||||
|
// We need to grab focus after it is added to the window. When
|
||||||
|
// creating a window we want to always focus on the widget.
|
||||||
|
const widget = @as(*c.GtkWidget, @ptrCast(gl_area));
|
||||||
|
_ = c.gtk_widget_grab_focus(widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close the tab for the given notebook page. This will automatically
|
||||||
|
/// handle closing the window if there are no more tabs.
|
||||||
|
fn closeTab(self: *Window, page: *c.GtkNotebookPage) void {
|
||||||
|
// Remove the page
|
||||||
|
const page_idx = getNotebookPageIndex(page);
|
||||||
|
c.gtk_notebook_remove_page(self.notebook, page_idx);
|
||||||
|
|
||||||
|
const remaining = c.gtk_notebook_get_n_pages(self.notebook);
|
||||||
|
switch (remaining) {
|
||||||
|
// If we have no more tabs we close the window
|
||||||
|
0 => c.gtk_window_destroy(self.window),
|
||||||
|
|
||||||
|
// If we have one more tab we hide the tab bar
|
||||||
|
1 => c.gtk_notebook_set_show_tabs(self.notebook, 0),
|
||||||
|
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have remaining tabs, we need to make sure we grab focus.
|
||||||
|
if (remaining > 0) self.focusCurrentTab();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close the surface. This surface must be definitely part of this window.
|
||||||
|
pub fn closeSurface(self: *Window, surface: *Surface) void {
|
||||||
|
assert(surface.window == self);
|
||||||
|
|
||||||
|
const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(surface.gl_area)) orelse return;
|
||||||
|
self.closeTab(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Go to the previous tab for a surface.
|
||||||
|
pub fn gotoPreviousTab(self: *Window, surface: *Surface) void {
|
||||||
|
const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(surface.gl_area)) orelse return;
|
||||||
|
const page_idx = getNotebookPageIndex(page);
|
||||||
|
|
||||||
|
// The next index is the previous or we wrap around.
|
||||||
|
const next_idx = if (page_idx > 0) page_idx - 1 else next_idx: {
|
||||||
|
const max = c.gtk_notebook_get_n_pages(self.notebook);
|
||||||
|
break :next_idx max -| 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Do nothing if we have one tab
|
||||||
|
if (next_idx == page_idx) return;
|
||||||
|
|
||||||
|
c.gtk_notebook_set_current_page(self.notebook, next_idx);
|
||||||
|
self.focusCurrentTab();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Go to the next tab for a surface.
|
||||||
|
pub fn gotoNextTab(self: *Window, surface: *Surface) void {
|
||||||
|
const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(surface.gl_area)) orelse return;
|
||||||
|
const page_idx = getNotebookPageIndex(page);
|
||||||
|
const max = c.gtk_notebook_get_n_pages(self.notebook) -| 1;
|
||||||
|
const next_idx = if (page_idx < max) page_idx + 1 else 0;
|
||||||
|
if (next_idx == page_idx) return;
|
||||||
|
|
||||||
|
c.gtk_notebook_set_current_page(self.notebook, next_idx);
|
||||||
|
self.focusCurrentTab();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Go to the specific tab index.
|
||||||
|
pub fn gotoTab(self: *Window, n: usize) void {
|
||||||
|
if (n == 0) return;
|
||||||
|
const max = c.gtk_notebook_get_n_pages(self.notebook);
|
||||||
|
const page_idx = std.math.cast(c_int, n - 1) orelse return;
|
||||||
|
if (page_idx < max) {
|
||||||
|
c.gtk_notebook_set_current_page(self.notebook, page_idx);
|
||||||
|
self.focusCurrentTab();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggle fullscreen for this window.
|
||||||
|
pub fn toggleFullscreen(self: *Window, _: configpkg.NonNativeFullscreen) void {
|
||||||
|
const is_fullscreen = c.gtk_window_is_fullscreen(self.window);
|
||||||
|
if (is_fullscreen == 0) {
|
||||||
|
c.gtk_window_fullscreen(self.window);
|
||||||
|
} else {
|
||||||
|
c.gtk_window_unfullscreen(self.window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Grabs focus on the currently selected tab.
|
||||||
|
fn focusCurrentTab(self: *Window) void {
|
||||||
|
const page_idx = c.gtk_notebook_get_current_page(self.notebook);
|
||||||
|
const widget = c.gtk_notebook_get_nth_page(self.notebook, page_idx);
|
||||||
|
_ = c.gtk_widget_grab_focus(widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gtkTabAddClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
|
||||||
|
const self = userdataSelf(ud.?);
|
||||||
|
const parent = self.app.core_app.focusedSurface();
|
||||||
|
self.newTab(parent) catch |err| {
|
||||||
|
log.warn("error adding new tab: {}", .{err});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
|
||||||
|
const surface: *Surface = @ptrCast(@alignCast(ud));
|
||||||
|
surface.core_surface.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gtkPageAdded(
|
||||||
|
_: *c.GtkNotebook,
|
||||||
|
child: *c.GtkWidget,
|
||||||
|
_: c.guint,
|
||||||
|
ud: ?*anyopaque,
|
||||||
|
) callconv(.C) void {
|
||||||
|
const self = userdataSelf(ud.?);
|
||||||
|
_ = self;
|
||||||
|
_ = child;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gtkPageRemoved(
|
||||||
|
_: *c.GtkNotebook,
|
||||||
|
_: *c.GtkWidget,
|
||||||
|
_: c.guint,
|
||||||
|
ud: ?*anyopaque,
|
||||||
|
) callconv(.C) void {
|
||||||
|
const self = userdataSelf(ud.?);
|
||||||
|
|
||||||
|
// Hide the tab bar if we only have one tab after removal
|
||||||
|
const remaining = c.gtk_notebook_get_n_pages(self.notebook);
|
||||||
|
if (remaining == 1) {
|
||||||
|
c.gtk_notebook_set_show_tabs(self.notebook, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gtkSwitchPage(_: *c.GtkNotebook, page: *c.GtkWidget, _: usize, ud: ?*anyopaque) callconv(.C) void {
|
||||||
|
const self = userdataSelf(ud.?);
|
||||||
|
const gtk_label_box = @as(*c.GtkWidget, @ptrCast(c.gtk_notebook_get_tab_label(self.notebook, page)));
|
||||||
|
const gtk_label = @as(*c.GtkLabel, @ptrCast(c.gtk_widget_get_first_child(gtk_label_box)));
|
||||||
|
const label_text = c.gtk_label_get_text(gtk_label);
|
||||||
|
c.gtk_window_set_title(self.window, label_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gtkNotebookCreateWindow(
|
||||||
|
_: *c.GtkNotebook,
|
||||||
|
page: *c.GtkWidget,
|
||||||
|
ud: ?*anyopaque,
|
||||||
|
) callconv(.C) ?*c.GtkNotebook {
|
||||||
|
// The surface for the page is stored in the widget data.
|
||||||
|
const surface: *Surface = @ptrCast(@alignCast(
|
||||||
|
c.g_object_get_data(@ptrCast(page), GL_AREA_SURFACE) orelse return null,
|
||||||
|
));
|
||||||
|
|
||||||
|
const self = userdataSelf(ud.?);
|
||||||
|
const alloc = self.app.core_app.alloc;
|
||||||
|
|
||||||
|
// Create a new window
|
||||||
|
const window = Window.create(alloc, self.app) catch |err| {
|
||||||
|
log.warn("error creating new window error={}", .{err});
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// We need to update our surface to point to the new window so that
|
||||||
|
// events such as new tab go to the right window.
|
||||||
|
surface.window = window;
|
||||||
|
|
||||||
|
return window.notebook;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gtkCloseRequest(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
|
||||||
|
_ = v;
|
||||||
|
log.debug("window close request", .{});
|
||||||
|
const self = userdataSelf(ud.?);
|
||||||
|
|
||||||
|
// If none of our surfaces need confirmation, we can just exit.
|
||||||
|
for (self.app.core_app.surfaces.items) |surface| {
|
||||||
|
if (surface.window == self) {
|
||||||
|
if (surface.core_surface.needsConfirmQuit()) break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.gtk_window_destroy(self.window);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup our basic message
|
||||||
|
const alert = c.gtk_message_dialog_new(
|
||||||
|
self.window,
|
||||||
|
c.GTK_DIALOG_MODAL,
|
||||||
|
c.GTK_MESSAGE_QUESTION,
|
||||||
|
c.GTK_BUTTONS_YES_NO,
|
||||||
|
"Close this window?",
|
||||||
|
);
|
||||||
|
c.gtk_message_dialog_format_secondary_text(
|
||||||
|
@ptrCast(alert),
|
||||||
|
"All terminal sessions in this window will be terminated.",
|
||||||
|
);
|
||||||
|
|
||||||
|
// We want the "yes" to appear destructive.
|
||||||
|
const yes_widget = c.gtk_dialog_get_widget_for_response(
|
||||||
|
@ptrCast(alert),
|
||||||
|
c.GTK_RESPONSE_YES,
|
||||||
|
);
|
||||||
|
c.gtk_widget_add_css_class(yes_widget, "destructive-action");
|
||||||
|
|
||||||
|
// We want the "no" to be the default action
|
||||||
|
c.gtk_dialog_set_default_response(
|
||||||
|
@ptrCast(alert),
|
||||||
|
c.GTK_RESPONSE_NO,
|
||||||
|
);
|
||||||
|
|
||||||
|
_ = c.g_signal_connect_data(alert, "response", c.G_CALLBACK(>kCloseConfirmation), self, null, c.G_CONNECT_DEFAULT);
|
||||||
|
|
||||||
|
c.gtk_widget_show(alert);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gtkCloseConfirmation(
|
||||||
|
alert: *c.GtkMessageDialog,
|
||||||
|
response: c.gint,
|
||||||
|
ud: ?*anyopaque,
|
||||||
|
) callconv(.C) void {
|
||||||
|
c.gtk_window_destroy(@ptrCast(alert));
|
||||||
|
if (response == c.GTK_RESPONSE_YES) {
|
||||||
|
const self = userdataSelf(ud.?);
|
||||||
|
c.gtk_window_destroy(self.window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// "destroy" signal for the window
|
||||||
|
fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
|
||||||
|
_ = v;
|
||||||
|
log.debug("window destroy", .{});
|
||||||
|
|
||||||
|
const self = userdataSelf(ud.?);
|
||||||
|
const alloc = self.app.core_app.alloc;
|
||||||
|
self.deinit();
|
||||||
|
alloc.destroy(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getNotebookPageIndex(page: *c.GtkNotebookPage) c_int {
|
||||||
|
var value: c.GValue = std.mem.zeroes(c.GValue);
|
||||||
|
defer c.g_value_unset(&value);
|
||||||
|
_ = c.g_value_init(&value, c.G_TYPE_INT);
|
||||||
|
c.g_object_get_property(
|
||||||
|
@ptrCast(@alignCast(page)),
|
||||||
|
"position",
|
||||||
|
&value,
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.g_value_get_int(&value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn userdataSelf(ud: *anyopaque) *Window {
|
||||||
|
return @ptrCast(@alignCast(ud));
|
||||||
|
}
|
11
src/apprt/gtk/c.zig
Normal file
11
src/apprt/gtk/c.zig
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
const c = @cImport({
|
||||||
|
@cInclude("gtk/gtk.h");
|
||||||
|
});
|
||||||
|
|
||||||
|
pub usingnamespace c;
|
||||||
|
|
||||||
|
/// Compatibility with gobject < 2.74
|
||||||
|
pub usingnamespace if (@hasDecl(c, "G_CONNECT_DEFAULT")) struct {} else struct {
|
||||||
|
pub const G_CONNECT_DEFAULT = 0;
|
||||||
|
pub const G_APPLICATION_DEFAULT_FLAGS = c.G_APPLICATION_FLAGS_NONE;
|
||||||
|
};
|
@ -72,12 +72,7 @@ gl_cells_size: usize = 0,
|
|||||||
gl_cells_written: usize = 0,
|
gl_cells_written: usize = 0,
|
||||||
|
|
||||||
/// Shader program for cell rendering.
|
/// Shader program for cell rendering.
|
||||||
program: gl.Program,
|
gl_state: ?GLState = null,
|
||||||
vao: gl.VertexArray,
|
|
||||||
ebo: gl.Buffer,
|
|
||||||
vbo: gl.Buffer,
|
|
||||||
texture: gl.Texture,
|
|
||||||
texture_color: gl.Texture,
|
|
||||||
|
|
||||||
/// The font structures.
|
/// The font structures.
|
||||||
font_group: *font.GroupCache,
|
font_group: *font.GroupCache,
|
||||||
@ -110,6 +105,8 @@ const SetScreenSize = struct {
|
|||||||
size: renderer.ScreenSize,
|
size: renderer.ScreenSize,
|
||||||
|
|
||||||
fn apply(self: SetScreenSize, r: *const OpenGL) !void {
|
fn apply(self: SetScreenSize, r: *const OpenGL) !void {
|
||||||
|
const gl_state = r.gl_state orelse return error.OpenGLUninitialized;
|
||||||
|
|
||||||
// Apply our padding
|
// Apply our padding
|
||||||
const padding = r.padding.explicit.add(if (r.padding.balance)
|
const padding = r.padding.explicit.add(if (r.padding.balance)
|
||||||
renderer.Padding.balanced(self.size, r.gridSize(self.size), r.cell_size)
|
renderer.Padding.balanced(self.size, r.gridSize(self.size), r.cell_size)
|
||||||
@ -135,7 +132,7 @@ const SetScreenSize = struct {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Update the projection uniform within our shader
|
// Update the projection uniform within our shader
|
||||||
try r.program.setUniform(
|
try gl_state.program.setUniform(
|
||||||
"projection",
|
"projection",
|
||||||
|
|
||||||
// 2D orthographic projection with the full w/h
|
// 2D orthographic projection with the full w/h
|
||||||
@ -153,18 +150,20 @@ const SetFontSize = struct {
|
|||||||
metrics: font.face.Metrics,
|
metrics: font.face.Metrics,
|
||||||
|
|
||||||
fn apply(self: SetFontSize, r: *const OpenGL) !void {
|
fn apply(self: SetFontSize, r: *const OpenGL) !void {
|
||||||
try r.program.setUniform(
|
const gl_state = r.gl_state orelse return error.OpenGLUninitialized;
|
||||||
|
|
||||||
|
try gl_state.program.setUniform(
|
||||||
"cell_size",
|
"cell_size",
|
||||||
@Vector(2, f32){
|
@Vector(2, f32){
|
||||||
@floatFromInt(self.metrics.cell_width),
|
@floatFromInt(self.metrics.cell_width),
|
||||||
@floatFromInt(self.metrics.cell_height),
|
@floatFromInt(self.metrics.cell_height),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
try r.program.setUniform(
|
try gl_state.program.setUniform(
|
||||||
"strikethrough_position",
|
"strikethrough_position",
|
||||||
@as(f32, @floatFromInt(self.metrics.strikethrough_position)),
|
@as(f32, @floatFromInt(self.metrics.strikethrough_position)),
|
||||||
);
|
);
|
||||||
try r.program.setUniform(
|
try gl_state.program.setUniform(
|
||||||
"strikethrough_thickness",
|
"strikethrough_thickness",
|
||||||
@as(f32, @floatFromInt(self.metrics.strikethrough_thickness)),
|
@as(f32, @floatFromInt(self.metrics.strikethrough_thickness)),
|
||||||
);
|
);
|
||||||
@ -295,12 +294,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL {
|
|||||||
});
|
});
|
||||||
errdefer shaper.deinit();
|
errdefer shaper.deinit();
|
||||||
|
|
||||||
// Create our shader
|
|
||||||
const program = try gl.Program.createVF(
|
|
||||||
@embedFile("shaders/cell.v.glsl"),
|
|
||||||
@embedFile("shaders/cell.f.glsl"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Setup our font metrics uniform
|
// Setup our font metrics uniform
|
||||||
const metrics = try resetFontMetrics(
|
const metrics = try resetFontMetrics(
|
||||||
alloc,
|
alloc,
|
||||||
@ -308,109 +301,8 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL {
|
|||||||
options.config.font_thicken,
|
options.config.font_thicken,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set our cell dimensions
|
var gl_state = try GLState.init(options.font_group);
|
||||||
const pbind = try program.use();
|
errdefer gl_state.deinit();
|
||||||
defer pbind.unbind();
|
|
||||||
|
|
||||||
// Set all of our texture indexes
|
|
||||||
try program.setUniform("text", 0);
|
|
||||||
try program.setUniform("text_color", 1);
|
|
||||||
|
|
||||||
// Setup our VAO
|
|
||||||
const vao = try gl.VertexArray.create();
|
|
||||||
errdefer vao.destroy();
|
|
||||||
try vao.bind();
|
|
||||||
defer gl.VertexArray.unbind() catch null;
|
|
||||||
|
|
||||||
// Element buffer (EBO)
|
|
||||||
const ebo = try gl.Buffer.create();
|
|
||||||
errdefer ebo.destroy();
|
|
||||||
var ebobind = try ebo.bind(.ElementArrayBuffer);
|
|
||||||
defer ebobind.unbind();
|
|
||||||
try ebobind.setData([6]u8{
|
|
||||||
0, 1, 3, // Top-left triangle
|
|
||||||
1, 2, 3, // Bottom-right triangle
|
|
||||||
}, .StaticDraw);
|
|
||||||
|
|
||||||
// Vertex buffer (VBO)
|
|
||||||
const vbo = try gl.Buffer.create();
|
|
||||||
errdefer vbo.destroy();
|
|
||||||
var vbobind = try vbo.bind(.ArrayBuffer);
|
|
||||||
defer vbobind.unbind();
|
|
||||||
var offset: usize = 0;
|
|
||||||
try vbobind.attributeAdvanced(0, 2, gl.c.GL_UNSIGNED_SHORT, false, @sizeOf(GPUCell), offset);
|
|
||||||
offset += 2 * @sizeOf(u16);
|
|
||||||
try vbobind.attributeAdvanced(1, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(GPUCell), offset);
|
|
||||||
offset += 2 * @sizeOf(u32);
|
|
||||||
try vbobind.attributeAdvanced(2, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(GPUCell), offset);
|
|
||||||
offset += 2 * @sizeOf(u32);
|
|
||||||
try vbobind.attributeAdvanced(3, 2, gl.c.GL_INT, false, @sizeOf(GPUCell), offset);
|
|
||||||
offset += 2 * @sizeOf(i32);
|
|
||||||
try vbobind.attributeAdvanced(4, 4, gl.c.GL_UNSIGNED_BYTE, false, @sizeOf(GPUCell), offset);
|
|
||||||
offset += 4 * @sizeOf(u8);
|
|
||||||
try vbobind.attributeAdvanced(5, 4, gl.c.GL_UNSIGNED_BYTE, false, @sizeOf(GPUCell), offset);
|
|
||||||
offset += 4 * @sizeOf(u8);
|
|
||||||
try vbobind.attributeIAdvanced(6, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(GPUCell), offset);
|
|
||||||
offset += 1 * @sizeOf(u8);
|
|
||||||
try vbobind.attributeIAdvanced(7, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(GPUCell), offset);
|
|
||||||
try vbobind.enableAttribArray(0);
|
|
||||||
try vbobind.enableAttribArray(1);
|
|
||||||
try vbobind.enableAttribArray(2);
|
|
||||||
try vbobind.enableAttribArray(3);
|
|
||||||
try vbobind.enableAttribArray(4);
|
|
||||||
try vbobind.enableAttribArray(5);
|
|
||||||
try vbobind.enableAttribArray(6);
|
|
||||||
try vbobind.enableAttribArray(7);
|
|
||||||
try vbobind.attributeDivisor(0, 1);
|
|
||||||
try vbobind.attributeDivisor(1, 1);
|
|
||||||
try vbobind.attributeDivisor(2, 1);
|
|
||||||
try vbobind.attributeDivisor(3, 1);
|
|
||||||
try vbobind.attributeDivisor(4, 1);
|
|
||||||
try vbobind.attributeDivisor(5, 1);
|
|
||||||
try vbobind.attributeDivisor(6, 1);
|
|
||||||
try vbobind.attributeDivisor(7, 1);
|
|
||||||
|
|
||||||
// Build our texture
|
|
||||||
const tex = try gl.Texture.create();
|
|
||||||
errdefer tex.destroy();
|
|
||||||
{
|
|
||||||
const texbind = try tex.bind(.@"2D");
|
|
||||||
try texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE);
|
|
||||||
try texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE);
|
|
||||||
try texbind.parameter(.MinFilter, gl.c.GL_LINEAR);
|
|
||||||
try texbind.parameter(.MagFilter, gl.c.GL_LINEAR);
|
|
||||||
try texbind.image2D(
|
|
||||||
0,
|
|
||||||
.Red,
|
|
||||||
@intCast(options.font_group.atlas_greyscale.size),
|
|
||||||
@intCast(options.font_group.atlas_greyscale.size),
|
|
||||||
0,
|
|
||||||
.Red,
|
|
||||||
.UnsignedByte,
|
|
||||||
options.font_group.atlas_greyscale.data.ptr,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build our color texture
|
|
||||||
const tex_color = try gl.Texture.create();
|
|
||||||
errdefer tex_color.destroy();
|
|
||||||
{
|
|
||||||
const texbind = try tex_color.bind(.@"2D");
|
|
||||||
try texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE);
|
|
||||||
try texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE);
|
|
||||||
try texbind.parameter(.MinFilter, gl.c.GL_LINEAR);
|
|
||||||
try texbind.parameter(.MagFilter, gl.c.GL_LINEAR);
|
|
||||||
try texbind.image2D(
|
|
||||||
0,
|
|
||||||
.RGBA,
|
|
||||||
@intCast(options.font_group.atlas_color.size),
|
|
||||||
@intCast(options.font_group.atlas_color.size),
|
|
||||||
0,
|
|
||||||
.BGRA,
|
|
||||||
.UnsignedByte,
|
|
||||||
options.font_group.atlas_color.data.ptr,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return OpenGL{
|
return OpenGL{
|
||||||
.alloc = alloc,
|
.alloc = alloc,
|
||||||
@ -420,12 +312,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL {
|
|||||||
.cells_lru = CellsLRU.init(0),
|
.cells_lru = CellsLRU.init(0),
|
||||||
.cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height },
|
.cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height },
|
||||||
.screen_size = null,
|
.screen_size = null,
|
||||||
.program = program,
|
.gl_state = gl_state,
|
||||||
.vao = vao,
|
|
||||||
.ebo = ebo,
|
|
||||||
.vbo = vbo,
|
|
||||||
.texture = tex,
|
|
||||||
.texture_color = tex_color,
|
|
||||||
.font_group = options.font_group,
|
.font_group = options.font_group,
|
||||||
.font_shaper = shaper,
|
.font_shaper = shaper,
|
||||||
.draw_background = options.config.background,
|
.draw_background = options.config.background,
|
||||||
@ -440,12 +327,7 @@ pub fn deinit(self: *OpenGL) void {
|
|||||||
self.font_shaper.deinit();
|
self.font_shaper.deinit();
|
||||||
self.alloc.free(self.font_shaper.cell_buf);
|
self.alloc.free(self.font_shaper.cell_buf);
|
||||||
|
|
||||||
self.texture.destroy();
|
if (self.gl_state) |*v| v.deinit();
|
||||||
self.texture_color.destroy();
|
|
||||||
self.vbo.destroy();
|
|
||||||
self.ebo.destroy();
|
|
||||||
self.vao.destroy();
|
|
||||||
self.program.destroy();
|
|
||||||
|
|
||||||
self.resetCellsLRU();
|
self.resetCellsLRU();
|
||||||
self.cells_lru.deinit(self.alloc);
|
self.cells_lru.deinit(self.alloc);
|
||||||
@ -508,13 +390,6 @@ pub fn surfaceInit(surface: *apprt.Surface) !void {
|
|||||||
apprt.glfw => try self.threadEnter(surface),
|
apprt.glfw => try self.threadEnter(surface),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blending for text. We use GL_ONE here because we should be using
|
|
||||||
// premultiplied alpha for all our colors in our fragment shaders.
|
|
||||||
// This avoids having a blurry border where transparency is expected on
|
|
||||||
// pixels.
|
|
||||||
try gl.enable(gl.c.GL_BLEND);
|
|
||||||
try gl.blendFunc(gl.c.GL_ONE, gl.c.GL_ONE_MINUS_SRC_ALPHA);
|
|
||||||
|
|
||||||
// These are very noisy so this is commented, but easy to uncomment
|
// These are very noisy so this is commented, but easy to uncomment
|
||||||
// whenever we need to check the OpenGL extension list
|
// whenever we need to check the OpenGL extension list
|
||||||
// if (builtin.mode == .Debug) {
|
// if (builtin.mode == .Debug) {
|
||||||
@ -538,6 +413,54 @@ pub fn finalizeSurfaceInit(self: *const OpenGL, surface: *apprt.Surface) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Called when the OpenGL context is made invalid, so we need to free
|
||||||
|
/// all previous resources and stop rendering.
|
||||||
|
pub fn displayUnrealized(self: *OpenGL) void {
|
||||||
|
if (single_threaded_draw) self.draw_mutex.lock();
|
||||||
|
defer if (single_threaded_draw) self.draw_mutex.unlock();
|
||||||
|
|
||||||
|
if (self.gl_state) |*v| {
|
||||||
|
v.deinit();
|
||||||
|
self.gl_state = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when the OpenGL is ready to be initialized.
|
||||||
|
pub fn displayRealize(self: *OpenGL) !void {
|
||||||
|
if (single_threaded_draw) self.draw_mutex.lock();
|
||||||
|
defer if (single_threaded_draw) self.draw_mutex.unlock();
|
||||||
|
|
||||||
|
// Reset our GPU uniforms
|
||||||
|
const metrics = try resetFontMetrics(
|
||||||
|
self.alloc,
|
||||||
|
self.font_group,
|
||||||
|
self.config.font_thicken,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Make our new state
|
||||||
|
var gl_state = try GLState.init(self.font_group);
|
||||||
|
errdefer gl_state.deinit();
|
||||||
|
|
||||||
|
// Unrealize if we have to
|
||||||
|
if (self.gl_state) |*v| v.deinit();
|
||||||
|
|
||||||
|
// Set our new state
|
||||||
|
self.gl_state = gl_state;
|
||||||
|
|
||||||
|
// Make sure we invalidate all the fields so that we
|
||||||
|
// reflush everything
|
||||||
|
self.gl_cells_size = 0;
|
||||||
|
self.gl_cells_written = 0;
|
||||||
|
self.font_group.atlas_greyscale.modified = true;
|
||||||
|
self.font_group.atlas_color.modified = true;
|
||||||
|
|
||||||
|
// We need to reset our uniforms
|
||||||
|
if (self.screen_size) |size| {
|
||||||
|
self.deferred_screen_size = .{ .size = size };
|
||||||
|
}
|
||||||
|
self.deferred_font_size = .{ .metrics = metrics };
|
||||||
|
}
|
||||||
|
|
||||||
/// Callback called by renderer.Thread when it begins.
|
/// Callback called by renderer.Thread when it begins.
|
||||||
pub fn threadEnter(self: *const OpenGL, surface: *apprt.Surface) !void {
|
pub fn threadEnter(self: *const OpenGL, surface: *apprt.Surface) !void {
|
||||||
_ = self;
|
_ = self;
|
||||||
@ -1429,11 +1352,13 @@ pub fn setScreenSize(
|
|||||||
|
|
||||||
/// Updates the font texture atlas if it is dirty.
|
/// Updates the font texture atlas if it is dirty.
|
||||||
fn flushAtlas(self: *OpenGL) !void {
|
fn flushAtlas(self: *OpenGL) !void {
|
||||||
|
const gl_state = self.gl_state orelse return;
|
||||||
|
|
||||||
{
|
{
|
||||||
const atlas = &self.font_group.atlas_greyscale;
|
const atlas = &self.font_group.atlas_greyscale;
|
||||||
if (atlas.modified) {
|
if (atlas.modified) {
|
||||||
atlas.modified = false;
|
atlas.modified = false;
|
||||||
var texbind = try self.texture.bind(.@"2D");
|
var texbind = try gl_state.texture.bind(.@"2D");
|
||||||
defer texbind.unbind();
|
defer texbind.unbind();
|
||||||
|
|
||||||
if (atlas.resized) {
|
if (atlas.resized) {
|
||||||
@ -1467,7 +1392,7 @@ fn flushAtlas(self: *OpenGL) !void {
|
|||||||
const atlas = &self.font_group.atlas_color;
|
const atlas = &self.font_group.atlas_color;
|
||||||
if (atlas.modified) {
|
if (atlas.modified) {
|
||||||
atlas.modified = false;
|
atlas.modified = false;
|
||||||
var texbind = try self.texture_color.bind(.@"2D");
|
var texbind = try gl_state.texture_color.bind(.@"2D");
|
||||||
defer texbind.unbind();
|
defer texbind.unbind();
|
||||||
|
|
||||||
if (atlas.resized) {
|
if (atlas.resized) {
|
||||||
@ -1507,6 +1432,7 @@ pub fn draw(self: *OpenGL) !void {
|
|||||||
// If we're in single-threaded more we grab a lock since we use shared data.
|
// If we're in single-threaded more we grab a lock since we use shared data.
|
||||||
if (single_threaded_draw) self.draw_mutex.lock();
|
if (single_threaded_draw) self.draw_mutex.lock();
|
||||||
defer if (single_threaded_draw) self.draw_mutex.unlock();
|
defer if (single_threaded_draw) self.draw_mutex.unlock();
|
||||||
|
const gl_state = self.gl_state orelse return;
|
||||||
|
|
||||||
// If we have no cells to render, then we render nothing.
|
// If we have no cells to render, then we render nothing.
|
||||||
if (self.cells.items.len == 0) return;
|
if (self.cells.items.len == 0) return;
|
||||||
@ -1525,28 +1451,28 @@ pub fn draw(self: *OpenGL) !void {
|
|||||||
gl.clear(gl.c.GL_COLOR_BUFFER_BIT);
|
gl.clear(gl.c.GL_COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
// Setup our VAO
|
// Setup our VAO
|
||||||
try self.vao.bind();
|
try gl_state.vao.bind();
|
||||||
defer gl.VertexArray.unbind() catch null;
|
defer gl.VertexArray.unbind() catch null;
|
||||||
|
|
||||||
// Bind EBO
|
// Bind EBO
|
||||||
var ebobind = try self.ebo.bind(.ElementArrayBuffer);
|
var ebobind = try gl_state.ebo.bind(.ElementArrayBuffer);
|
||||||
defer ebobind.unbind();
|
defer ebobind.unbind();
|
||||||
|
|
||||||
// Bind VBO and set data
|
// Bind VBO and set data
|
||||||
var binding = try self.vbo.bind(.ArrayBuffer);
|
var binding = try gl_state.vbo.bind(.ArrayBuffer);
|
||||||
defer binding.unbind();
|
defer binding.unbind();
|
||||||
|
|
||||||
// Bind our textures
|
// Bind our textures
|
||||||
try gl.Texture.active(gl.c.GL_TEXTURE0);
|
try gl.Texture.active(gl.c.GL_TEXTURE0);
|
||||||
var texbind = try self.texture.bind(.@"2D");
|
var texbind = try gl_state.texture.bind(.@"2D");
|
||||||
defer texbind.unbind();
|
defer texbind.unbind();
|
||||||
|
|
||||||
try gl.Texture.active(gl.c.GL_TEXTURE1);
|
try gl.Texture.active(gl.c.GL_TEXTURE1);
|
||||||
var texbind1 = try self.texture_color.bind(.@"2D");
|
var texbind1 = try gl_state.texture_color.bind(.@"2D");
|
||||||
defer texbind1.unbind();
|
defer texbind1.unbind();
|
||||||
|
|
||||||
// Pick our shader to use
|
// Pick our shader to use
|
||||||
const pbind = try self.program.use();
|
const pbind = try gl_state.program.use();
|
||||||
defer pbind.unbind();
|
defer pbind.unbind();
|
||||||
|
|
||||||
// If we have deferred operations, run them.
|
// If we have deferred operations, run them.
|
||||||
@ -1596,7 +1522,7 @@ fn drawCells(
|
|||||||
// If we have data to write to the GPU, send it.
|
// If we have data to write to the GPU, send it.
|
||||||
if (self.gl_cells_written < cells.items.len) {
|
if (self.gl_cells_written < cells.items.len) {
|
||||||
const data = cells.items[self.gl_cells_written..];
|
const data = cells.items[self.gl_cells_written..];
|
||||||
//log.info("sending {} cells to GPU", .{data.len});
|
// log.info("sending {} cells to GPU", .{data.len});
|
||||||
try binding.setSubData(self.gl_cells_written * @sizeOf(GPUCell), data);
|
try binding.setSubData(self.gl_cells_written * @sizeOf(GPUCell), data);
|
||||||
|
|
||||||
self.gl_cells_written += data.len;
|
self.gl_cells_written += data.len;
|
||||||
@ -1611,3 +1537,152 @@ fn drawCells(
|
|||||||
cells.items.len,
|
cells.items.len,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The OpenGL objects that are associated with a renderer. This makes it
|
||||||
|
/// easy to create/destroy these as a set in situations i.e. where the
|
||||||
|
/// OpenGL context is replaced.
|
||||||
|
const GLState = struct {
|
||||||
|
program: gl.Program,
|
||||||
|
vao: gl.VertexArray,
|
||||||
|
ebo: gl.Buffer,
|
||||||
|
vbo: gl.Buffer,
|
||||||
|
texture: gl.Texture,
|
||||||
|
texture_color: gl.Texture,
|
||||||
|
|
||||||
|
pub fn init(font_group: *font.GroupCache) !GLState {
|
||||||
|
// Blending for text. We use GL_ONE here because we should be using
|
||||||
|
// premultiplied alpha for all our colors in our fragment shaders.
|
||||||
|
// This avoids having a blurry border where transparency is expected on
|
||||||
|
// pixels.
|
||||||
|
try gl.enable(gl.c.GL_BLEND);
|
||||||
|
try gl.blendFunc(gl.c.GL_ONE, gl.c.GL_ONE_MINUS_SRC_ALPHA);
|
||||||
|
|
||||||
|
// Shader
|
||||||
|
const program = try gl.Program.createVF(
|
||||||
|
@embedFile("shaders/cell.v.glsl"),
|
||||||
|
@embedFile("shaders/cell.f.glsl"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set our cell dimensions
|
||||||
|
const pbind = try program.use();
|
||||||
|
defer pbind.unbind();
|
||||||
|
|
||||||
|
// Set all of our texture indexes
|
||||||
|
try program.setUniform("text", 0);
|
||||||
|
try program.setUniform("text_color", 1);
|
||||||
|
|
||||||
|
// Setup our VAO
|
||||||
|
const vao = try gl.VertexArray.create();
|
||||||
|
errdefer vao.destroy();
|
||||||
|
try vao.bind();
|
||||||
|
defer gl.VertexArray.unbind() catch null;
|
||||||
|
|
||||||
|
// Element buffer (EBO)
|
||||||
|
const ebo = try gl.Buffer.create();
|
||||||
|
errdefer ebo.destroy();
|
||||||
|
var ebobind = try ebo.bind(.ElementArrayBuffer);
|
||||||
|
defer ebobind.unbind();
|
||||||
|
try ebobind.setData([6]u8{
|
||||||
|
0, 1, 3, // Top-left triangle
|
||||||
|
1, 2, 3, // Bottom-right triangle
|
||||||
|
}, .StaticDraw);
|
||||||
|
|
||||||
|
// Vertex buffer (VBO)
|
||||||
|
const vbo = try gl.Buffer.create();
|
||||||
|
errdefer vbo.destroy();
|
||||||
|
var vbobind = try vbo.bind(.ArrayBuffer);
|
||||||
|
defer vbobind.unbind();
|
||||||
|
var offset: usize = 0;
|
||||||
|
try vbobind.attributeAdvanced(0, 2, gl.c.GL_UNSIGNED_SHORT, false, @sizeOf(GPUCell), offset);
|
||||||
|
offset += 2 * @sizeOf(u16);
|
||||||
|
try vbobind.attributeAdvanced(1, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(GPUCell), offset);
|
||||||
|
offset += 2 * @sizeOf(u32);
|
||||||
|
try vbobind.attributeAdvanced(2, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(GPUCell), offset);
|
||||||
|
offset += 2 * @sizeOf(u32);
|
||||||
|
try vbobind.attributeAdvanced(3, 2, gl.c.GL_INT, false, @sizeOf(GPUCell), offset);
|
||||||
|
offset += 2 * @sizeOf(i32);
|
||||||
|
try vbobind.attributeAdvanced(4, 4, gl.c.GL_UNSIGNED_BYTE, false, @sizeOf(GPUCell), offset);
|
||||||
|
offset += 4 * @sizeOf(u8);
|
||||||
|
try vbobind.attributeAdvanced(5, 4, gl.c.GL_UNSIGNED_BYTE, false, @sizeOf(GPUCell), offset);
|
||||||
|
offset += 4 * @sizeOf(u8);
|
||||||
|
try vbobind.attributeIAdvanced(6, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(GPUCell), offset);
|
||||||
|
offset += 1 * @sizeOf(u8);
|
||||||
|
try vbobind.attributeIAdvanced(7, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(GPUCell), offset);
|
||||||
|
try vbobind.enableAttribArray(0);
|
||||||
|
try vbobind.enableAttribArray(1);
|
||||||
|
try vbobind.enableAttribArray(2);
|
||||||
|
try vbobind.enableAttribArray(3);
|
||||||
|
try vbobind.enableAttribArray(4);
|
||||||
|
try vbobind.enableAttribArray(5);
|
||||||
|
try vbobind.enableAttribArray(6);
|
||||||
|
try vbobind.enableAttribArray(7);
|
||||||
|
try vbobind.attributeDivisor(0, 1);
|
||||||
|
try vbobind.attributeDivisor(1, 1);
|
||||||
|
try vbobind.attributeDivisor(2, 1);
|
||||||
|
try vbobind.attributeDivisor(3, 1);
|
||||||
|
try vbobind.attributeDivisor(4, 1);
|
||||||
|
try vbobind.attributeDivisor(5, 1);
|
||||||
|
try vbobind.attributeDivisor(6, 1);
|
||||||
|
try vbobind.attributeDivisor(7, 1);
|
||||||
|
|
||||||
|
// Build our texture
|
||||||
|
const tex = try gl.Texture.create();
|
||||||
|
errdefer tex.destroy();
|
||||||
|
{
|
||||||
|
const texbind = try tex.bind(.@"2D");
|
||||||
|
try texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE);
|
||||||
|
try texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE);
|
||||||
|
try texbind.parameter(.MinFilter, gl.c.GL_LINEAR);
|
||||||
|
try texbind.parameter(.MagFilter, gl.c.GL_LINEAR);
|
||||||
|
try texbind.image2D(
|
||||||
|
0,
|
||||||
|
.Red,
|
||||||
|
@intCast(font_group.atlas_greyscale.size),
|
||||||
|
@intCast(font_group.atlas_greyscale.size),
|
||||||
|
0,
|
||||||
|
.Red,
|
||||||
|
.UnsignedByte,
|
||||||
|
font_group.atlas_greyscale.data.ptr,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build our color texture
|
||||||
|
const tex_color = try gl.Texture.create();
|
||||||
|
errdefer tex_color.destroy();
|
||||||
|
{
|
||||||
|
const texbind = try tex_color.bind(.@"2D");
|
||||||
|
try texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE);
|
||||||
|
try texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE);
|
||||||
|
try texbind.parameter(.MinFilter, gl.c.GL_LINEAR);
|
||||||
|
try texbind.parameter(.MagFilter, gl.c.GL_LINEAR);
|
||||||
|
try texbind.image2D(
|
||||||
|
0,
|
||||||
|
.RGBA,
|
||||||
|
@intCast(font_group.atlas_color.size),
|
||||||
|
@intCast(font_group.atlas_color.size),
|
||||||
|
0,
|
||||||
|
.BGRA,
|
||||||
|
.UnsignedByte,
|
||||||
|
font_group.atlas_color.data.ptr,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.program = program,
|
||||||
|
.vao = vao,
|
||||||
|
.ebo = ebo,
|
||||||
|
.vbo = vbo,
|
||||||
|
.texture = tex,
|
||||||
|
.texture_color = tex_color,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *GLState) void {
|
||||||
|
self.texture.destroy();
|
||||||
|
self.texture_color.destroy();
|
||||||
|
self.vbo.destroy();
|
||||||
|
self.ebo.destroy();
|
||||||
|
self.vao.destroy();
|
||||||
|
self.program.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Reference in New Issue
Block a user