mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-20 10:46:07 +03:00
apprt/gtk-ng: very basic no-input surface (#7985)
This ports over the `new_window` functionality and shows a single no-input, no-tab, no-split surface. The surface renders (e.g. the cursor blinks) and it is a full Ghostty surface underneath so the shell prompt shows up and everything. However, the surface doesn't respond to input. I'm going to put this PR up in this state so that it isn't too much all at once. This work also required some core libghostty improvements that just didn't fit the model being built here. They're extremely minimal, however (basically going from struct fields to struct decls so we can do some logic). A couple new Valgrind suppressions had to be added to deal with GLAreas. These suppressions were necessary before we ever hooked up our renderers so they're caused by GTK itself. Only two, though! Other than that, Ghostty runs **Valgrind clean**. <img width="1726" height="714" alt="2025-07-18-131732_hyprshot" src="https://github.com/user-attachments/assets/9c8bfe86-705c-4173-916b-df2b9b54dbfd" />
This commit is contained in:
@ -729,6 +729,7 @@ typedef enum {
|
||||
GHOSTTY_ACTION_RESET_WINDOW_SIZE,
|
||||
GHOSTTY_ACTION_INITIAL_SIZE,
|
||||
GHOSTTY_ACTION_CELL_SIZE,
|
||||
GHOSTTY_ACTION_RENDER,
|
||||
GHOSTTY_ACTION_INSPECTOR,
|
||||
GHOSTTY_ACTION_SHOW_GTK_INSPECTOR,
|
||||
GHOSTTY_ACTION_RENDER_INSPECTOR,
|
||||
|
49
src/App.zig
49
src/App.zig
@ -154,7 +154,7 @@ pub fn tick(self: *App, rt_app: *apprt.App) !void {
|
||||
pub fn updateConfig(self: *App, rt_app: *apprt.App, config: *const Config) !void {
|
||||
// Go through and update all of the surface configurations.
|
||||
for (self.surfaces.items) |surface| {
|
||||
try surface.core_surface.handleMessage(.{ .change_config = config });
|
||||
try surface.core().handleMessage(.{ .change_config = config });
|
||||
}
|
||||
|
||||
// Apply our conditional state. If we fail to apply the conditional state
|
||||
@ -190,7 +190,7 @@ pub fn addSurface(
|
||||
// Since we have non-zero surfaces, we can cancel the quit timer.
|
||||
// It is up to the apprt if there is a quit timer at all and if it
|
||||
// should be canceled.
|
||||
_ = rt_surface.app.performAction(
|
||||
_ = rt_surface.rtApp().performAction(
|
||||
.app,
|
||||
.quit_timer,
|
||||
.stop,
|
||||
@ -207,7 +207,7 @@ pub fn deleteSurface(self: *App, rt_surface: *apprt.Surface) void {
|
||||
// just let focused surface be but the allocator was reusing addresses
|
||||
// after free and giving false positives, so we must clear it.
|
||||
if (self.focused_surface) |focused| {
|
||||
if (focused == &rt_surface.core_surface) {
|
||||
if (focused == rt_surface.core()) {
|
||||
self.focused_surface = null;
|
||||
}
|
||||
}
|
||||
@ -224,7 +224,7 @@ pub fn deleteSurface(self: *App, rt_surface: *apprt.Surface) void {
|
||||
|
||||
// If we have no surfaces, we can start the quit timer. It is up to the
|
||||
// apprt to determine if this is necessary.
|
||||
if (self.surfaces.items.len == 0) _ = rt_surface.app.performAction(
|
||||
if (self.surfaces.items.len == 0) _ = rt_surface.rtApp().performAction(
|
||||
.app,
|
||||
.quit_timer,
|
||||
.start,
|
||||
@ -245,7 +245,7 @@ pub fn focusedSurface(self: *const App) ?*Surface {
|
||||
/// the apprt to call this.
|
||||
pub fn needsConfirmQuit(self: *const App) bool {
|
||||
for (self.surfaces.items) |v| {
|
||||
if (v.core_surface.needsConfirmQuit()) return true;
|
||||
if (v.core().needsConfirmQuit()) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -260,7 +260,7 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
|
||||
.new_window => |msg| try self.newWindow(rt_app, msg),
|
||||
.close => |surface| self.closeSurface(surface),
|
||||
.surface_message => |msg| try self.surfaceMessage(msg.surface, msg.message),
|
||||
.redraw_surface => |surface| self.redrawSurface(rt_app, surface),
|
||||
.redraw_surface => |surface| try self.redrawSurface(rt_app, surface),
|
||||
.redraw_inspector => |surface| self.redrawInspector(rt_app, surface),
|
||||
|
||||
// If we're quitting, then we set the quit flag and stop
|
||||
@ -286,13 +286,30 @@ pub fn focusSurface(self: *App, surface: *Surface) void {
|
||||
self.focused_surface = surface;
|
||||
}
|
||||
|
||||
fn redrawSurface(self: *App, rt_app: *apprt.App, surface: *apprt.Surface) void {
|
||||
if (!self.hasSurface(&surface.core_surface)) return;
|
||||
rt_app.redrawSurface(surface);
|
||||
fn redrawSurface(
|
||||
self: *App,
|
||||
rt_app: *apprt.App,
|
||||
surface: *apprt.Surface,
|
||||
) !void {
|
||||
if (!self.hasRtSurface(surface)) return;
|
||||
|
||||
// TODO: Remove this in a separate PR. We should transition to
|
||||
// the `render` apprt action completely. This is only to make
|
||||
// our initial gtk-ng work touch less things.
|
||||
if (@hasDecl(apprt.App, "redrawSurface")) {
|
||||
rt_app.redrawSurface(surface);
|
||||
return;
|
||||
}
|
||||
|
||||
_ = try rt_app.performAction(
|
||||
.{ .surface = surface.core() },
|
||||
.render,
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
fn redrawInspector(self: *App, rt_app: *apprt.App, surface: *apprt.Surface) void {
|
||||
if (!self.hasSurface(&surface.core_surface)) return;
|
||||
if (!self.hasRtSurface(surface)) return;
|
||||
rt_app.redrawInspector(surface);
|
||||
}
|
||||
|
||||
@ -482,7 +499,7 @@ pub fn performAllAction(
|
||||
// Surface-scoped actions are performed on all surfaces. Errors
|
||||
// are logged but processing continues.
|
||||
.surface => for (self.surfaces.items) |surface| {
|
||||
_ = surface.core_surface.performBindingAction(action) catch |err| {
|
||||
_ = surface.core().performBindingAction(action) catch |err| {
|
||||
log.warn("error performing binding action on surface ptr={X} err={}", .{
|
||||
@intFromPtr(surface),
|
||||
err,
|
||||
@ -507,7 +524,15 @@ fn surfaceMessage(self: *App, surface: *Surface, msg: apprt.surface.Message) !vo
|
||||
|
||||
fn hasSurface(self: *const App, surface: *const Surface) bool {
|
||||
for (self.surfaces.items) |v| {
|
||||
if (&v.core_surface == surface) return true;
|
||||
if (v.core() == surface) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
fn hasRtSurface(self: *const App, surface: *apprt.Surface) bool {
|
||||
for (self.surfaces.items) |v| {
|
||||
if (v == surface) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -1738,7 +1738,10 @@ pub fn selectionInfo(self: *const Surface) ?apprt.Selection {
|
||||
/// Returns the pwd of the terminal, if any. This is always copied because
|
||||
/// the pwd can change at any point from termio. If we are calling from the IO
|
||||
/// thread you should just check the terminal directly.
|
||||
pub fn pwd(self: *const Surface, alloc: Allocator) !?[]const u8 {
|
||||
pub fn pwd(
|
||||
self: *const Surface,
|
||||
alloc: Allocator,
|
||||
) Allocator.Error!?[]const u8 {
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
const terminal_pwd = self.io.terminal.getPwd() orelse return null;
|
||||
|
@ -162,6 +162,11 @@ pub const Action = union(Key) {
|
||||
/// The cell size has changed to the given dimensions in pixels.
|
||||
cell_size: CellSize,
|
||||
|
||||
/// The target should be re-rendered. This usually has a specific
|
||||
/// surface target but if the app is targeted then all active
|
||||
/// surfaces should be redrawn.
|
||||
render,
|
||||
|
||||
/// Control whether the inspector is shown or hidden.
|
||||
inspector: Inspector,
|
||||
|
||||
@ -203,9 +208,16 @@ pub const Action = union(Key) {
|
||||
open_config,
|
||||
|
||||
/// Called when there are no more surfaces and the app should quit
|
||||
/// 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.
|
||||
/// after the configured delay.
|
||||
///
|
||||
/// Despite the name, this is the notification that libghostty sends
|
||||
/// when there are no more surfaces regardless of if the configuration
|
||||
/// wants to quit after close, has any delay set, etc. It's up to the
|
||||
/// apprt to implement the proper logic based on the config.
|
||||
///
|
||||
/// 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: QuitTimer,
|
||||
|
||||
/// Set the window floating state. A floating window is one that is
|
||||
@ -304,6 +316,7 @@ pub const Action = union(Key) {
|
||||
reset_window_size,
|
||||
initial_size,
|
||||
cell_size,
|
||||
render,
|
||||
inspector,
|
||||
show_gtk_inspector,
|
||||
render_inspector,
|
||||
|
@ -601,6 +601,14 @@ pub const Surface = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn core(self: *Surface) *CoreSurface {
|
||||
return &self.core_surface;
|
||||
}
|
||||
|
||||
pub fn rtApp(self: *const Surface) *App {
|
||||
return self.app;
|
||||
}
|
||||
|
||||
pub fn close(self: *const Surface, process_alive: bool) void {
|
||||
const func = self.app.opts.close_surface orelse {
|
||||
log.info("runtime embedder does not support closing a surface", .{});
|
||||
|
@ -19,6 +19,11 @@ const adw_version = @import("adw_version.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
/// This is detected by the Renderer, in which case it sends a `redraw_surface`
|
||||
/// message so that we can call `drawFrame` ourselves from the app thread,
|
||||
/// because GTK's `GLArea` does not support drawing from a different thread.
|
||||
pub const must_draw_from_app_thread = true;
|
||||
|
||||
/// The GObject Application instance
|
||||
app: *Application,
|
||||
|
||||
@ -48,6 +53,11 @@ pub fn terminate(self: *App) void {
|
||||
self.app.deinit();
|
||||
}
|
||||
|
||||
/// Called by CoreApp to wake up the event loop.
|
||||
pub fn wakeup(self: *App) void {
|
||||
self.app.wakeup();
|
||||
}
|
||||
|
||||
pub fn performAction(
|
||||
self: *App,
|
||||
target: apprt.Target,
|
||||
@ -69,12 +79,6 @@ pub fn performIpc(
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Close the given surface.
|
||||
pub fn redrawSurface(self: *App, surface: *Surface) void {
|
||||
_ = self;
|
||||
_ = surface;
|
||||
}
|
||||
|
||||
/// Redraw the inspector for the given surface.
|
||||
pub fn redrawInspector(self: *App, surface: *Surface) void {
|
||||
_ = self;
|
||||
|
@ -1,41 +1,60 @@
|
||||
const Surface = @This();
|
||||
const Self = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const apprt = @import("../../apprt.zig");
|
||||
const CoreSurface = @import("../../Surface.zig");
|
||||
const ApprtApp = @import("App.zig");
|
||||
const Application = @import("class/application.zig").Application;
|
||||
const Surface = @import("class/surface.zig").Surface;
|
||||
|
||||
core_surface: CoreSurface,
|
||||
/// The GObject Surface
|
||||
surface: *Surface,
|
||||
|
||||
pub fn deinit(self: *Surface) void {
|
||||
pub fn deinit(self: *Self) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
pub fn close(self: *Surface, process_active: bool) void {
|
||||
pub fn core(self: *Self) *CoreSurface {
|
||||
// This asserts the non-optional because libghostty should only
|
||||
// be calling this for initialized surfaces.
|
||||
return self.surface.core().?;
|
||||
}
|
||||
|
||||
pub fn rtApp(self: *Self) *ApprtApp {
|
||||
_ = self;
|
||||
return Application.default().rt();
|
||||
}
|
||||
|
||||
pub fn close(self: *Self, process_active: bool) void {
|
||||
_ = self;
|
||||
_ = process_active;
|
||||
}
|
||||
|
||||
pub fn shouldClose(self: *Surface) bool {
|
||||
pub fn shouldClose(self: *Self) bool {
|
||||
_ = self;
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn getTitle(self: *Surface) ?[:0]const u8 {
|
||||
pub fn getTitle(self: *Self) ?[:0]const u8 {
|
||||
_ = self;
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn getContentScale(self: *const Surface) !apprt.ContentScale {
|
||||
_ = self;
|
||||
return .{ .x = 1, .y = 1 };
|
||||
pub fn getContentScale(self: *const Self) !apprt.ContentScale {
|
||||
return self.surface.getContentScale();
|
||||
}
|
||||
|
||||
pub fn getCursorPos(self: *const Surface) !apprt.CursorPos {
|
||||
pub fn getSize(self: *const Self) !apprt.SurfaceSize {
|
||||
return self.surface.getSize();
|
||||
}
|
||||
|
||||
pub fn getCursorPos(self: *const Self) !apprt.CursorPos {
|
||||
_ = self;
|
||||
return .{ .x = 0, .y = 0 };
|
||||
}
|
||||
|
||||
pub fn clipboardRequest(
|
||||
self: *Surface,
|
||||
self: *Self,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
state: apprt.ClipboardRequest,
|
||||
) !void {
|
||||
@ -45,7 +64,7 @@ pub fn clipboardRequest(
|
||||
}
|
||||
|
||||
pub fn setClipboardString(
|
||||
self: *Surface,
|
||||
self: *Self,
|
||||
val: [:0]const u8,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
confirm: bool,
|
||||
@ -55,3 +74,7 @@ pub fn setClipboardString(
|
||||
_ = clipboard_type;
|
||||
_ = confirm;
|
||||
}
|
||||
|
||||
pub fn defaultTermioEnv(self: *Self) !std.process.EnvMap {
|
||||
return try self.surface.defaultTermioEnv();
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ pub const icon_sizes: []const comptime_int = &.{ 16, 32, 128, 256, 512, 1024 };
|
||||
/// These will be asserted to exist at runtime.
|
||||
pub const blueprints: []const Blueprint = &.{
|
||||
.{ .major = 1, .minor = 2, .name = "config-errors-dialog" },
|
||||
.{ .major = 1, .minor = 2, .name = "surface" },
|
||||
.{ .major = 1, .minor = 5, .name = "config-errors-dialog" },
|
||||
.{ .major = 1, .minor = 5, .name = "window" },
|
||||
};
|
||||
|
@ -3,10 +3,12 @@
|
||||
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
pub const Application = @import("class/application.zig").Application;
|
||||
pub const Window = @import("class/window.zig").Window;
|
||||
pub const Config = @import("class/config.zig").Config;
|
||||
pub const Surface = @import("class/surface.zig").Surface;
|
||||
|
||||
/// Unrefs the given GObject on the next event loop tick.
|
||||
///
|
||||
@ -60,6 +62,30 @@ pub fn Common(
|
||||
);
|
||||
}
|
||||
}).private else {};
|
||||
|
||||
/// Common class functions.
|
||||
pub const Class = struct {
|
||||
pub fn as(class: *Self.Class, comptime T: type) *T {
|
||||
return gobject.ext.as(T, class);
|
||||
}
|
||||
|
||||
/// Bind a template child to a private entry in the class.
|
||||
pub const bindTemplateChildPrivate = if (Private) |P| (struct {
|
||||
pub fn bindTemplateChildPrivate(
|
||||
class: *Self.Class,
|
||||
comptime name: [:0]const u8,
|
||||
comptime options: gtk.ext.BindTemplateChildOptions,
|
||||
) void {
|
||||
gtk.ext.impl_helpers.bindTemplateChildPrivate(
|
||||
class,
|
||||
name,
|
||||
P,
|
||||
P.offset,
|
||||
options,
|
||||
);
|
||||
}
|
||||
}).bindTemplateChildPrivate else {};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@ const configpkg = @import("../../../config.zig");
|
||||
const internal_os = @import("../../../os/main.zig");
|
||||
const xev = @import("../../../global.zig").xev;
|
||||
const CoreConfig = configpkg.Config;
|
||||
const CoreSurface = @import("../../../Surface.zig");
|
||||
|
||||
const adw_version = @import("../adw_version.zig");
|
||||
const gtk_version = @import("../gtk_version.zig");
|
||||
@ -58,6 +59,25 @@ pub const Application = extern struct {
|
||||
.private = .{ .Type = Private, .offset = &Private.offset },
|
||||
});
|
||||
|
||||
pub const properties = struct {
|
||||
pub const config = struct {
|
||||
pub const name = "config";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
"config",
|
||||
Self,
|
||||
?*Config,
|
||||
.{
|
||||
.nick = "Config",
|
||||
.blurb = "The current active configuration for the application.",
|
||||
.default = null,
|
||||
.accessor = .{
|
||||
.getter = Self.getPropConfig,
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const Private = struct {
|
||||
/// The apprt App. This is annoying that we need this it'd be
|
||||
/// nicer to just make THIS the apprt app but the current libghostty
|
||||
@ -88,6 +108,17 @@ pub const Application = extern struct {
|
||||
pub var offset: c_int = 0;
|
||||
};
|
||||
|
||||
/// Get this application as the default, allowing access to its
|
||||
/// properties globally.
|
||||
///
|
||||
/// This asserts that there is a default application and that the
|
||||
/// default application is a GhosttyApplication. The program would have
|
||||
/// to be in a very bad state for this to be violated.
|
||||
pub fn default() *Self {
|
||||
const app = gio.Application.getDefault().?;
|
||||
return gobject.ext.cast(Self, app).?;
|
||||
}
|
||||
|
||||
/// Creates a new Application instance.
|
||||
///
|
||||
/// This does a lot more work than a typical class instantiation,
|
||||
@ -121,14 +152,14 @@ pub const Application = extern struct {
|
||||
// the error in the diagnostics so it can be shown to the user.
|
||||
// We can still load a default which only fails for OOM, allowing
|
||||
// us to startup.
|
||||
var default: CoreConfig = try .default(alloc);
|
||||
errdefer default.deinit();
|
||||
try default.addDiagnosticFmt(
|
||||
var def: CoreConfig = try .default(alloc);
|
||||
errdefer def.deinit();
|
||||
try def.addDiagnosticFmt(
|
||||
"error loading user configuration: {}",
|
||||
.{err},
|
||||
);
|
||||
|
||||
break :err default;
|
||||
break :err def;
|
||||
};
|
||||
defer config.deinit();
|
||||
|
||||
@ -223,6 +254,13 @@ pub const Application = extern struct {
|
||||
if (priv.transient_cgroup_base) |base| alloc.free(base);
|
||||
}
|
||||
|
||||
/// The global allocator that all other classes should use by
|
||||
/// calling `Application.default().allocator()`. Zig code should prefer
|
||||
/// this wherever possible so we get leak detection in debug/tests.
|
||||
pub fn allocator(self: *Self) std.mem.Allocator {
|
||||
return self.private().core_app.alloc;
|
||||
}
|
||||
|
||||
/// Run the application. This is a replacement for `gio.Application.run`
|
||||
/// because we want more tight control over our event loop so we can
|
||||
/// integrate it with libghostty.
|
||||
@ -332,9 +370,20 @@ pub const Application = extern struct {
|
||||
value.config,
|
||||
),
|
||||
|
||||
.new_window => try Action.newWindow(
|
||||
self,
|
||||
switch (target) {
|
||||
.app => null,
|
||||
.surface => |v| v,
|
||||
},
|
||||
),
|
||||
|
||||
.quit_timer => try Action.quitTimer(self, value),
|
||||
|
||||
.render => Action.render(self, target),
|
||||
|
||||
// Unimplemented
|
||||
.quit,
|
||||
.new_window,
|
||||
.close_window,
|
||||
.toggle_maximize,
|
||||
.toggle_fullscreen,
|
||||
@ -362,7 +411,6 @@ pub const Application = extern struct {
|
||||
.toggle_tab_overview,
|
||||
.toggle_split_zoom,
|
||||
.toggle_window_decorations,
|
||||
.quit_timer,
|
||||
.prompt_title,
|
||||
.toggle_quick_terminal,
|
||||
.secure_input,
|
||||
@ -410,6 +458,46 @@ pub const Application = extern struct {
|
||||
try priv.core_app.updateConfig(priv.rt_app, &config);
|
||||
}
|
||||
|
||||
/// Returns the configuration for this application.
|
||||
///
|
||||
/// The reference count is increased.
|
||||
pub fn getConfig(self: *Self) *Config {
|
||||
var value = gobject.ext.Value.zero;
|
||||
gobject.Object.getProperty(
|
||||
self.as(gobject.Object),
|
||||
properties.config.name,
|
||||
&value,
|
||||
);
|
||||
|
||||
const obj = value.getObject().?;
|
||||
return gobject.ext.cast(Config, obj).?;
|
||||
}
|
||||
|
||||
fn getPropConfig(self: *Self) *Config {
|
||||
// Property return must not increase reference count since
|
||||
// the gobject getter handles this automatically.
|
||||
return self.private().config;
|
||||
}
|
||||
|
||||
/// Returns the core app associated with this application. This is
|
||||
/// not a reference-counted type so you should not store this.
|
||||
pub fn core(self: *Self) *CoreApp {
|
||||
return self.private().core_app;
|
||||
}
|
||||
|
||||
/// Returns the apprt application associated with this application.
|
||||
pub fn rt(self: *Self) *ApprtApp {
|
||||
return self.private().rt_app;
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Libghostty Callbacks
|
||||
|
||||
pub fn wakeup(self: *Self) void {
|
||||
_ = self;
|
||||
glib.MainContext.wakeup(null);
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Virtual Methods
|
||||
|
||||
@ -421,6 +509,9 @@ pub const Application = extern struct {
|
||||
self.as(Parent),
|
||||
);
|
||||
|
||||
// Set ourselves as the default application.
|
||||
gio.Application.setDefault(self.as(gio.Application));
|
||||
|
||||
// Setup our event loop
|
||||
self.startupXev();
|
||||
|
||||
@ -581,14 +672,17 @@ pub const Application = extern struct {
|
||||
fn activate(self: *Self) callconv(.C) void {
|
||||
log.debug("activate", .{});
|
||||
|
||||
// Queue a new window
|
||||
const priv = self.private();
|
||||
_ = priv.core_app.mailbox.push(.{
|
||||
.new_window = .{},
|
||||
}, .{ .forever = {} });
|
||||
|
||||
// Call the parent activate method.
|
||||
gio.Application.virtual_methods.activate.call(
|
||||
Class.parent,
|
||||
self.as(Parent),
|
||||
);
|
||||
|
||||
// const win = Window.new(self);
|
||||
// gtk.Window.present(win.as(gtk.Window));
|
||||
}
|
||||
|
||||
fn dispose(self: *Self) callconv(.C) void {
|
||||
@ -697,10 +791,6 @@ pub const Application = extern struct {
|
||||
//----------------------------------------------------------------
|
||||
// Boilerplate/Noise
|
||||
|
||||
fn allocator(self: *Self) std.mem.Allocator {
|
||||
return self.private().core_app.alloc;
|
||||
}
|
||||
|
||||
const C = Common(Self, Private);
|
||||
pub const as = C.as;
|
||||
pub const ref = C.ref;
|
||||
@ -729,6 +819,11 @@ pub const Application = extern struct {
|
||||
}
|
||||
}
|
||||
|
||||
// Properties
|
||||
gobject.ext.registerProperties(class, &.{
|
||||
properties.config.impl,
|
||||
});
|
||||
|
||||
// Virtual methods
|
||||
gio.Application.virtual_methods.activate.implement(class, &activate);
|
||||
gio.Application.virtual_methods.startup.implement(class, &startup);
|
||||
@ -765,6 +860,38 @@ const Action = struct {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn newWindow(
|
||||
self: *Application,
|
||||
parent: ?*CoreSurface,
|
||||
) !void {
|
||||
_ = parent;
|
||||
|
||||
const win = Window.new(self);
|
||||
gtk.Window.present(win.as(gtk.Window));
|
||||
}
|
||||
|
||||
pub fn quitTimer(
|
||||
self: *Application,
|
||||
mode: apprt.action.QuitTimer,
|
||||
) !void {
|
||||
// TODO: An actual quit timer implementation. For now, we immediately
|
||||
// quit on no windows regardless of the config.
|
||||
switch (mode) {
|
||||
.start => {
|
||||
self.private().running = false;
|
||||
},
|
||||
|
||||
.stop => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(_: *Application, target: apprt.Target) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| v.rt_surface.surface.redraw(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// This sets various GTK-related environment variables as necessary
|
||||
|
521
src/apprt/gtk-ng/class/surface.zig
Normal file
521
src/apprt/gtk-ng/class/surface.zig
Normal file
@ -0,0 +1,521 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const adw = @import("adw");
|
||||
const gdk = @import("gdk");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const apprt = @import("../../../apprt.zig");
|
||||
const internal_os = @import("../../../os/main.zig");
|
||||
const renderer = @import("../../../renderer.zig");
|
||||
const CoreSurface = @import("../../../Surface.zig");
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
const adw_version = @import("../adw_version.zig");
|
||||
const ApprtSurface = @import("../Surface.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
const Application = @import("application.zig").Application;
|
||||
const Config = @import("config.zig").Config;
|
||||
|
||||
const log = std.log.scoped(.gtk_ghostty_surface);
|
||||
|
||||
pub const Surface = extern struct {
|
||||
const Self = @This();
|
||||
parent_instance: Parent,
|
||||
pub const Parent = adw.Bin;
|
||||
pub const getGObjectType = gobject.ext.defineClass(Self, .{
|
||||
.name = "GhosttySurface",
|
||||
.instanceInit = &init,
|
||||
.classInit = &Class.init,
|
||||
.parent_class = &Class.parent,
|
||||
.private = .{ .Type = Private, .offset = &Private.offset },
|
||||
});
|
||||
|
||||
pub const properties = struct {
|
||||
pub const config = struct {
|
||||
pub const name = "config";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
?*Config,
|
||||
.{
|
||||
.nick = "Config",
|
||||
.blurb = "The configuration that this surface is using.",
|
||||
.default = null,
|
||||
.accessor = gobject.ext.privateFieldAccessor(
|
||||
Self,
|
||||
Private,
|
||||
&Private.offset,
|
||||
"config",
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const Private = struct {
|
||||
/// The configuration that this surface is using.
|
||||
config: ?*Config = null,
|
||||
|
||||
/// The GLAarea that renders the actual surface. This is a binding
|
||||
/// to the template so it doesn't have to be unrefed manually.
|
||||
gl_area: *gtk.GLArea,
|
||||
|
||||
/// The apprt Surface.
|
||||
rt_surface: ApprtSurface,
|
||||
|
||||
/// The core surface backing this GTK surface. This starts out
|
||||
/// null because it can't be initialized until there is an available
|
||||
/// GLArea that is realized.
|
||||
//
|
||||
// NOTE(mitchellh): This is a limitation we should definitely remove
|
||||
// at some point by modifying our OpenGL renderer for GTK to
|
||||
// start in an unrealized state. There are other benefits to being
|
||||
// able to initialize the surface early so we should aim for that,
|
||||
// eventually.
|
||||
core_surface: ?*CoreSurface = null,
|
||||
|
||||
/// Cached metrics for libghostty callbacks
|
||||
size: apprt.SurfaceSize,
|
||||
|
||||
pub var offset: c_int = 0;
|
||||
};
|
||||
|
||||
pub fn new() *Self {
|
||||
return gobject.ext.newInstance(Self, .{});
|
||||
}
|
||||
|
||||
pub fn core(self: *Self) ?*CoreSurface {
|
||||
const priv = self.private();
|
||||
return priv.core_surface;
|
||||
}
|
||||
|
||||
pub fn rt(self: *Self) *ApprtSurface {
|
||||
const priv = self.private();
|
||||
return &priv.rt_surface;
|
||||
}
|
||||
|
||||
/// Force the surface to redraw itself. Ghostty often will only redraw
|
||||
/// the terminal in reaction to internal changes. If there are external
|
||||
/// events that invalidate the surface, such as the widget moving parents,
|
||||
/// then we should force a redraw.
|
||||
pub fn redraw(self: *Self) void {
|
||||
const priv = self.private();
|
||||
priv.gl_area.queueRender();
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Libghostty Callbacks
|
||||
|
||||
pub fn getContentScale(self: *Self) apprt.ContentScale {
|
||||
const priv = self.private();
|
||||
const gl_area = priv.gl_area;
|
||||
|
||||
const gtk_scale: f32 = scale: {
|
||||
const widget = gl_area.as(gtk.Widget);
|
||||
// Future: detect GTK version 4.12+ and use gdk_surface_get_scale so we
|
||||
// can support fractional scaling.
|
||||
const scale = widget.getScaleFactor();
|
||||
if (scale <= 0) {
|
||||
log.warn("gtk_widget_get_scale_factor returned a non-positive number: {}", .{scale});
|
||||
break :scale 1.0;
|
||||
}
|
||||
break :scale @floatFromInt(scale);
|
||||
};
|
||||
|
||||
// Also scale using font-specific DPI, which is often exposed to the user
|
||||
// via DE accessibility settings (see https://docs.gtk.org/gtk4/class.Settings.html).
|
||||
const xft_dpi_scale = xft_scale: {
|
||||
// gtk-xft-dpi is font DPI multiplied by 1024. See
|
||||
// https://docs.gtk.org/gtk4/property.Settings.gtk-xft-dpi.html
|
||||
const settings = gtk.Settings.getDefault() orelse break :xft_scale 1.0;
|
||||
var value = std.mem.zeroes(gobject.Value);
|
||||
defer value.unset();
|
||||
_ = value.init(gobject.ext.typeFor(c_int));
|
||||
settings.as(gobject.Object).getProperty("gtk-xft-dpi", &value);
|
||||
const gtk_xft_dpi = value.getInt();
|
||||
|
||||
// Use a value of 1.0 for the XFT DPI scale if the setting is <= 0
|
||||
// See:
|
||||
// https://gitlab.gnome.org/GNOME/libadwaita/-/commit/a7738a4d269bfdf4d8d5429ca73ccdd9b2450421
|
||||
// https://gitlab.gnome.org/GNOME/libadwaita/-/commit/9759d3fd81129608dd78116001928f2aed974ead
|
||||
if (gtk_xft_dpi <= 0) {
|
||||
log.warn("gtk-xft-dpi was not set, using default value", .{});
|
||||
break :xft_scale 1.0;
|
||||
}
|
||||
|
||||
// As noted above gtk-xft-dpi is multiplied by 1024, so we divide by
|
||||
// 1024, then divide by the default value (96) to derive a scale. Note
|
||||
// gtk-xft-dpi can be fractional, so we use floating point math here.
|
||||
const xft_dpi: f32 = @as(f32, @floatFromInt(gtk_xft_dpi)) / 1024.0;
|
||||
break :xft_scale xft_dpi / 96.0;
|
||||
};
|
||||
|
||||
const scale = gtk_scale * xft_dpi_scale;
|
||||
return .{ .x = scale, .y = scale };
|
||||
}
|
||||
|
||||
pub fn getSize(self: *Self) apprt.SurfaceSize {
|
||||
return self.private().size;
|
||||
}
|
||||
|
||||
pub fn defaultTermioEnv(self: *Self) !std.process.EnvMap {
|
||||
_ = self;
|
||||
|
||||
const alloc = Application.default().allocator();
|
||||
var env = try internal_os.getEnvMap(alloc);
|
||||
errdefer env.deinit();
|
||||
|
||||
// Don't leak these GTK environment variables to child processes.
|
||||
env.remove("GDK_DEBUG");
|
||||
env.remove("GDK_DISABLE");
|
||||
env.remove("GSK_RENDERER");
|
||||
|
||||
// Remove some environment variables that are set when Ghostty is launched
|
||||
// from a `.desktop` file, by D-Bus activation, or systemd.
|
||||
env.remove("GIO_LAUNCHED_DESKTOP_FILE");
|
||||
env.remove("GIO_LAUNCHED_DESKTOP_FILE_PID");
|
||||
env.remove("DBUS_STARTER_ADDRESS");
|
||||
env.remove("DBUS_STARTER_BUS_TYPE");
|
||||
env.remove("INVOCATION_ID");
|
||||
env.remove("JOURNAL_STREAM");
|
||||
env.remove("NOTIFY_SOCKET");
|
||||
|
||||
// Unset environment varies set by snaps if we're running in a snap.
|
||||
// This allows Ghostty to further launch additional snaps.
|
||||
if (env.get("SNAP")) |_| {
|
||||
env.remove("SNAP");
|
||||
env.remove("DRIRC_CONFIGDIR");
|
||||
env.remove("__EGL_EXTERNAL_PLATFORM_CONFIG_DIRS");
|
||||
env.remove("__EGL_VENDOR_LIBRARY_DIRS");
|
||||
env.remove("LD_LIBRARY_PATH");
|
||||
env.remove("LIBGL_DRIVERS_PATH");
|
||||
env.remove("LIBVA_DRIVERS_PATH");
|
||||
env.remove("VK_LAYER_PATH");
|
||||
env.remove("XLOCALEDIR");
|
||||
env.remove("GDK_PIXBUF_MODULEDIR");
|
||||
env.remove("GDK_PIXBUF_MODULE_FILE");
|
||||
env.remove("GTK_PATH");
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Virtual Methods
|
||||
|
||||
fn init(self: *Self, _: *Class) callconv(.C) void {
|
||||
gtk.Widget.initTemplate(self.as(gtk.Widget));
|
||||
|
||||
const priv = self.private();
|
||||
|
||||
// Initialize some private fields so they aren't undefined
|
||||
priv.rt_surface = .{ .surface = self };
|
||||
priv.size = .{
|
||||
// Funky numbers on purpose so they stand out if for some reason
|
||||
// our size doesn't get properly set.
|
||||
.width = 111,
|
||||
.height = 111,
|
||||
};
|
||||
|
||||
// If our configuration is null then we get the configuration
|
||||
// from the application.
|
||||
if (priv.config == null) {
|
||||
const app = Application.default();
|
||||
priv.config = app.getConfig();
|
||||
}
|
||||
|
||||
// Initialize our GLArea. We could do a lot of this in
|
||||
// the Blueprint file but I think its cleaner to separate
|
||||
// the "UI" part of the blueprint file from the internal logic/config
|
||||
// part.
|
||||
const gl_area = priv.gl_area;
|
||||
gl_area.setRequiredVersion(
|
||||
renderer.OpenGL.MIN_VERSION_MAJOR,
|
||||
renderer.OpenGL.MIN_VERSION_MINOR,
|
||||
);
|
||||
gl_area.setHasStencilBuffer(0);
|
||||
gl_area.setHasDepthBuffer(0);
|
||||
gl_area.setUseEs(0);
|
||||
_ = gtk.Widget.signals.realize.connect(
|
||||
gl_area,
|
||||
*Self,
|
||||
glareaRealize,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.Widget.signals.unrealize.connect(
|
||||
gl_area,
|
||||
*Self,
|
||||
glareaUnrealize,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.GLArea.signals.render.connect(
|
||||
gl_area,
|
||||
*Self,
|
||||
glareaRender,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.GLArea.signals.resize.connect(
|
||||
gl_area,
|
||||
*Self,
|
||||
glareaResize,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
}
|
||||
|
||||
fn dispose(self: *Self) callconv(.C) void {
|
||||
const priv = self.private();
|
||||
if (priv.config) |v| {
|
||||
v.unref();
|
||||
priv.config = null;
|
||||
}
|
||||
|
||||
gtk.Widget.disposeTemplate(
|
||||
self.as(gtk.Widget),
|
||||
getGObjectType(),
|
||||
);
|
||||
|
||||
gobject.Object.virtual_methods.dispose.call(
|
||||
Class.parent,
|
||||
self.as(Parent),
|
||||
);
|
||||
}
|
||||
|
||||
fn finalize(self: *Self) callconv(.C) void {
|
||||
const priv = self.private();
|
||||
if (priv.core_surface) |v| {
|
||||
priv.core_surface = null;
|
||||
|
||||
// Remove ourselves from the list of known surfaces in the app.
|
||||
// We do this before deinit in case a callback triggers
|
||||
// searching for this surface.
|
||||
Application.default().core().deleteSurface(self.rt());
|
||||
|
||||
// Deinit the surface
|
||||
v.deinit();
|
||||
const alloc = Application.default().allocator();
|
||||
alloc.destroy(v);
|
||||
}
|
||||
|
||||
gobject.Object.virtual_methods.finalize.call(
|
||||
Class.parent,
|
||||
self.as(Parent),
|
||||
);
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Signal Handlers
|
||||
|
||||
fn glareaRealize(
|
||||
_: *gtk.GLArea,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
log.debug("realize", .{});
|
||||
|
||||
self.realizeSurface() catch |err| {
|
||||
log.warn("surface failed to realize err={}", .{err});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
fn glareaUnrealize(
|
||||
gl_area: *gtk.GLArea,
|
||||
self: *Self,
|
||||
) callconv(.c) void {
|
||||
log.debug("unrealize", .{});
|
||||
|
||||
// Get our surface. If we don't have one, there's no work we
|
||||
// need to do here.
|
||||
const priv = self.private();
|
||||
const surface = priv.core_surface orelse return;
|
||||
|
||||
// There is no guarantee that our GLArea context is current
|
||||
// when unrealize is emitted, so we need to make it current.
|
||||
gl_area.makeCurrent();
|
||||
if (gl_area.getError()) |err| {
|
||||
// I don't know a scenario this can happen, but it means
|
||||
// we probably leaked memory because displayUnrealized
|
||||
// below frees resources that aren't specifically OpenGL
|
||||
// related. I didn't make the OpenGL renderer handle this
|
||||
// scenario because I don't know if its even possible
|
||||
// under valid circumstances, so let's log.
|
||||
log.warn(
|
||||
"gl_area_make_current failed in unrealize msg={s}",
|
||||
.{err.f_message orelse "(no message)"},
|
||||
);
|
||||
log.warn("OpenGL resources and memory likely leaked", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
surface.renderer.displayUnrealized();
|
||||
}
|
||||
|
||||
fn glareaRender(
|
||||
_: *gtk.GLArea,
|
||||
_: *gdk.GLContext,
|
||||
self: *Self,
|
||||
) callconv(.c) c_int {
|
||||
// If we don't have a surface then we failed to initialize for
|
||||
// some reason and there's nothing to draw to the GLArea.
|
||||
const priv = self.private();
|
||||
const surface = priv.core_surface orelse return 1;
|
||||
|
||||
surface.renderer.drawFrame(true) catch |err| {
|
||||
log.warn("failed to draw frame err={}", .{err});
|
||||
return 0;
|
||||
};
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
fn glareaResize(
|
||||
gl_area: *gtk.GLArea,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
self: *Surface,
|
||||
) callconv(.c) void {
|
||||
// Some debug output to help understand what GTK is telling us.
|
||||
{
|
||||
const widget = gl_area.as(gtk.Widget);
|
||||
const scale_factor = widget.getScaleFactor();
|
||||
const window_scale_factor = scale: {
|
||||
const root = widget.getRoot() orelse break :scale 0;
|
||||
const gtk_native = root.as(gtk.Native);
|
||||
const gdk_surface = gtk_native.getSurface() orelse break :scale 0;
|
||||
break :scale gdk_surface.getScaleFactor();
|
||||
};
|
||||
|
||||
log.debug("gl resize width={} height={} scale={} window_scale={}", .{
|
||||
width,
|
||||
height,
|
||||
scale_factor,
|
||||
window_scale_factor,
|
||||
});
|
||||
}
|
||||
|
||||
// Store our cached size
|
||||
const priv = self.private();
|
||||
priv.size = .{
|
||||
.width = @intCast(width),
|
||||
.height = @intCast(height),
|
||||
};
|
||||
|
||||
// If our surface is realize, we send callbacks.
|
||||
if (priv.core_surface) |surface| {
|
||||
// We also update the content scale because there is no signal for
|
||||
// content scale change and it seems to trigger a resize event.
|
||||
surface.contentScaleCallback(self.getContentScale()) catch |err| {
|
||||
log.warn("error in content scale callback err={}", .{err});
|
||||
};
|
||||
|
||||
surface.sizeCallback(priv.size) catch |err| {
|
||||
log.warn("error in size callback err={}", .{err});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const RealizeError = Allocator.Error || error{
|
||||
GLAreaError,
|
||||
RendererError,
|
||||
SurfaceError,
|
||||
};
|
||||
|
||||
fn realizeSurface(self: *Self) RealizeError!void {
|
||||
const priv = self.private();
|
||||
const gl_area = priv.gl_area;
|
||||
|
||||
// We need to make the context current so we can call GL functions.
|
||||
// This is required for all surface operations.
|
||||
gl_area.makeCurrent();
|
||||
if (gl_area.getError()) |err| {
|
||||
log.warn("failed to make GL context current: {s}", .{err.f_message orelse "(no message)"});
|
||||
log.warn("this error is usually due to a driver or gtk bug", .{});
|
||||
log.warn("this is a common cause of this issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/4950", .{});
|
||||
return error.GLAreaError;
|
||||
}
|
||||
|
||||
// If we already have an initialized surface then we just notify.
|
||||
if (priv.core_surface) |v| {
|
||||
v.renderer.displayRealized() catch |err| {
|
||||
log.warn("core displayRealized failed err={}", .{err});
|
||||
return error.RendererError;
|
||||
};
|
||||
self.redraw();
|
||||
return;
|
||||
}
|
||||
|
||||
// Make our pointer to store our surface
|
||||
const app = Application.default();
|
||||
const alloc = app.allocator();
|
||||
const surface = try alloc.create(CoreSurface);
|
||||
errdefer alloc.destroy(surface);
|
||||
|
||||
// Add ourselves to the list of surfaces on the app.
|
||||
try app.core().addSurface(self.rt());
|
||||
errdefer app.core().deleteSurface(self.rt());
|
||||
|
||||
// Initialize our surface configuration.
|
||||
var config = try apprt.surface.newConfig(
|
||||
app.core(),
|
||||
priv.config.?.get(),
|
||||
);
|
||||
defer config.deinit();
|
||||
|
||||
// Initialize the surface
|
||||
surface.init(
|
||||
alloc,
|
||||
&config,
|
||||
app.core(),
|
||||
app.rt(),
|
||||
&priv.rt_surface,
|
||||
) catch |err| {
|
||||
log.warn("failed to initialize surface err={}", .{err});
|
||||
return error.SurfaceError;
|
||||
};
|
||||
errdefer surface.deinit();
|
||||
|
||||
// Store it!
|
||||
priv.core_surface = surface;
|
||||
}
|
||||
|
||||
const C = Common(Self, Private);
|
||||
pub const as = C.as;
|
||||
pub const ref = C.ref;
|
||||
pub const unref = C.unref;
|
||||
const private = C.private;
|
||||
|
||||
pub const Class = extern struct {
|
||||
parent_class: Parent.Class,
|
||||
var parent: *Parent.Class = undefined;
|
||||
pub const Instance = Self;
|
||||
|
||||
fn init(class: *Class) callconv(.C) void {
|
||||
gtk.Widget.Class.setTemplateFromResource(
|
||||
class.as(gtk.Widget.Class),
|
||||
comptime gresource.blueprint(.{
|
||||
.major = 1,
|
||||
.minor = 2,
|
||||
.name = "surface",
|
||||
}),
|
||||
);
|
||||
|
||||
// Bindings
|
||||
class.bindTemplateChildPrivate("gl_area", .{});
|
||||
|
||||
// Properties
|
||||
gobject.ext.registerProperties(class, &.{
|
||||
properties.config.impl,
|
||||
});
|
||||
|
||||
// Virtual methods
|
||||
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
|
||||
gobject.Object.virtual_methods.finalize.implement(class, &finalize);
|
||||
}
|
||||
|
||||
pub const as = C.Class.as;
|
||||
pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
|
||||
};
|
||||
};
|
@ -6,6 +6,7 @@ const gtk = @import("gtk");
|
||||
const gresource = @import("../build/gresource.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
const Application = @import("application.zig").Application;
|
||||
const Surface = @import("surface.zig").Surface;
|
||||
|
||||
const log = std.log.scoped(.gtk_ghostty_window);
|
||||
|
||||
@ -58,6 +59,8 @@ pub const Window = extern struct {
|
||||
pub const Instance = Self;
|
||||
|
||||
fn init(class: *Class) callconv(.C) void {
|
||||
gobject.ext.ensureType(Surface);
|
||||
|
||||
gtk.Widget.Class.setTemplateFromResource(
|
||||
class.as(gtk.Widget.Class),
|
||||
comptime gresource.blueprint(.{
|
||||
|
16
src/apprt/gtk-ng/ui/1.2/surface.blp
Normal file
16
src/apprt/gtk-ng/ui/1.2/surface.blp
Normal file
@ -0,0 +1,16 @@
|
||||
using Gtk 4.0;
|
||||
using Adw 1;
|
||||
|
||||
template $GhosttySurface: Adw.Bin {
|
||||
// A box isn't strictly necessary right now but there will be more
|
||||
// stuff here in the future. There's still a lot to do with surfaces.
|
||||
Box {
|
||||
orientation: vertical;
|
||||
hexpand: true;
|
||||
|
||||
GLArea gl_area {
|
||||
hexpand: true;
|
||||
vexpand: true;
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,5 @@ using Gtk 4.0;
|
||||
using Adw 1;
|
||||
|
||||
template $GhosttyWindow: Adw.ApplicationWindow {
|
||||
content: Label {
|
||||
label: "Hello, Ghostty!";
|
||||
};
|
||||
content: $GhosttySurface {};
|
||||
}
|
||||
|
@ -524,6 +524,7 @@ pub fn performAction(
|
||||
.open_url => self.openUrl(value),
|
||||
.show_child_exited => return try self.showChildExited(target, value),
|
||||
.progress_report => return try self.handleProgressReport(target, value),
|
||||
.render => self.render(target),
|
||||
|
||||
// Unimplemented
|
||||
.close_all_windows,
|
||||
@ -881,6 +882,13 @@ fn handleProgressReport(_: *App, target: apprt.Target, value: terminal.osc.Comma
|
||||
}
|
||||
}
|
||||
|
||||
fn render(_: *App, target: apprt.Target) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| v.rt_surface.redraw(),
|
||||
}
|
||||
}
|
||||
|
||||
fn quitTimer(self: *App, mode: apprt.action.QuitTimer) void {
|
||||
switch (mode) {
|
||||
.start => self.startQuitTimer(),
|
||||
@ -1479,12 +1487,6 @@ fn stopQuitTimer(self: *App) void {
|
||||
}
|
||||
}
|
||||
|
||||
/// Close the given surface.
|
||||
pub fn redrawSurface(self: *App, surface: *Surface) void {
|
||||
_ = self;
|
||||
surface.redraw();
|
||||
}
|
||||
|
||||
/// Redraw the inspector for the given surface.
|
||||
pub fn redrawInspector(self: *App, surface: *Surface) void {
|
||||
_ = self;
|
||||
|
@ -781,6 +781,14 @@ pub fn deinit(self: *Surface) void {
|
||||
self.resize_overlay.deinit();
|
||||
}
|
||||
|
||||
pub fn core(self: *Surface) *CoreSurface {
|
||||
return &self.core_surface;
|
||||
}
|
||||
|
||||
pub fn rtApp(self: *const Surface) *App {
|
||||
return self.app;
|
||||
}
|
||||
|
||||
/// Update our local copy of any configuration that we use.
|
||||
pub fn updateConfig(self: *Surface, config: *const configpkg.Config) !void {
|
||||
self.resize_overlay.updateConfig(config);
|
||||
|
@ -1,3 +1,6 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const apprt = @import("../apprt.zig");
|
||||
const App = @import("../App.zig");
|
||||
const Surface = @import("../Surface.zig");
|
||||
@ -133,7 +136,10 @@ pub const Mailbox = struct {
|
||||
/// Returns a new config for a surface for the given app that should be
|
||||
/// used for any new surfaces. The resulting config should be deinitialized
|
||||
/// after the surface is initialized.
|
||||
pub fn newConfig(app: *const App, config: *const Config) !Config {
|
||||
pub fn newConfig(
|
||||
app: *const App,
|
||||
config: *const Config,
|
||||
) Allocator.Error!Config {
|
||||
// Create a shallow clone
|
||||
var copy = config.shallowClone(app.alloc);
|
||||
|
||||
|
@ -165,7 +165,9 @@ pub fn surfaceInit(surface: *apprt.Surface) !void {
|
||||
else => @compileError("unsupported app runtime for OpenGL"),
|
||||
|
||||
// GTK uses global OpenGL context so we load from null.
|
||||
apprt.gtk => try prepareContext(null),
|
||||
apprt.gtk,
|
||||
apprt.gtk_ng,
|
||||
=> try prepareContext(null),
|
||||
|
||||
apprt.embedded => {
|
||||
// TODO(mitchellh): this does nothing today to allow libghostty
|
||||
@ -199,7 +201,7 @@ pub fn threadEnter(self: *const OpenGL, surface: *apprt.Surface) !void {
|
||||
switch (apprt.runtime) {
|
||||
else => @compileError("unsupported app runtime for OpenGL"),
|
||||
|
||||
apprt.gtk => {
|
||||
apprt.gtk, apprt.gtk_ng => {
|
||||
// GTK doesn't support threaded OpenGL operations as far as I can
|
||||
// tell, so we use the renderer thread to setup all the state
|
||||
// but then do the actual draws and texture syncs and all that
|
||||
@ -221,7 +223,7 @@ pub fn threadExit(self: *const OpenGL) void {
|
||||
switch (apprt.runtime) {
|
||||
else => @compileError("unsupported app runtime for OpenGL"),
|
||||
|
||||
apprt.gtk => {
|
||||
apprt.gtk, apprt.gtk_ng => {
|
||||
// We don't need to do any unloading for GTK because we may
|
||||
// be sharing the global bindings with other windows.
|
||||
},
|
||||
@ -236,7 +238,7 @@ pub fn displayRealized(self: *const OpenGL) void {
|
||||
_ = self;
|
||||
|
||||
switch (apprt.runtime) {
|
||||
apprt.gtk => prepareContext(null) catch |err| {
|
||||
apprt.gtk, apprt.gtk_ng => prepareContext(null) catch |err| {
|
||||
log.warn(
|
||||
"Error preparing GL context in displayRealized, err={}",
|
||||
.{err},
|
||||
|
@ -13,6 +13,39 @@
|
||||
# You must gracefully exit Ghostty (do not SIGINT) by closing all windows
|
||||
# and quitting. Otherwise, we leave a number of GTK resources around.
|
||||
|
||||
{
|
||||
GSK Renderer GPU Stuff
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: possible
|
||||
...
|
||||
fun:gsk_gpu_image_toggle_ref_texture
|
||||
fun:gsk_gl_image_new_for_texture
|
||||
fun:gsk_gl_frame_upload_texture
|
||||
fun:gsk_gpu_frame_do_upload_texture
|
||||
fun:gsk_gpu_lookup_texture
|
||||
...
|
||||
fun:gsk_gpu_node_processor_add_first_node
|
||||
fun:gsk_gpu_node_processor_process
|
||||
fun:gsk_gpu_frame_render
|
||||
fun:gsk_gpu_renderer_render
|
||||
fun:gsk_renderer_render
|
||||
fun:gtk_widget_render
|
||||
fun:surface_render
|
||||
...
|
||||
}
|
||||
|
||||
{
|
||||
GTK Shader Selector
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: possible
|
||||
...
|
||||
fun:_ZL29si_init_shader_selector_asyncPvS_i
|
||||
fun:util_queue_thread_func
|
||||
fun:impl_thrd_routine
|
||||
fun:start_thread
|
||||
fun:clone
|
||||
}
|
||||
|
||||
# Weird gtk_tooltip_init leak I can't figure out
|
||||
{
|
||||
Non-builder tooltip create
|
||||
|
Reference in New Issue
Block a user