mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-25 05:06:24 +03:00

Previously, we encoded `ctrl+_` in the CSIu format[1]. This breaks most notably emacs which expects the legacy ambiguous encoding. This commit utilizes the generator from Kitty to generate our control key mappings. We also switch from keycode mapping to key contents mapping which appears to be the correct behavior also compared to other terminals. In the course of doing this, I also found one bug with our fixterms implementation. Fixterms states: "The Shift key should not be considered as a modifier for Unicode characters, because it is most likely used to obtain the character in the first place (e.g. the shift key is often required to obtain the ! symbol)." We were not applying that logic and now do. [1]: https://www.leonerd.org.uk/hacks/fixterms/
1089 lines
38 KiB
Zig
1089 lines
38 KiB
Zig
//! Application runtime implementation that uses GLFW (https://www.glfw.org/).
|
|
//!
|
|
//! This works on macOS and Linux with OpenGL and Metal.
|
|
//! (The above sentence may be out of date).
|
|
|
|
const std = @import("std");
|
|
const builtin = @import("builtin");
|
|
const build_config = @import("../build_config.zig");
|
|
const assert = std.debug.assert;
|
|
const Allocator = std.mem.Allocator;
|
|
const glfw = @import("glfw");
|
|
const macos = @import("macos");
|
|
const objc = @import("objc");
|
|
const input = @import("../input.zig");
|
|
const internal_os = @import("../os/main.zig");
|
|
const renderer = @import("../renderer.zig");
|
|
const terminal = @import("../terminal/main.zig");
|
|
const Renderer = renderer.Renderer;
|
|
const apprt = @import("../apprt.zig");
|
|
const CoreApp = @import("../App.zig");
|
|
const CoreSurface = @import("../Surface.zig");
|
|
const configpkg = @import("../config.zig");
|
|
const Config = @import("../config.zig").Config;
|
|
|
|
// Get native API access on certain platforms so we can do more customization.
|
|
const glfwNative = glfw.Native(.{
|
|
.cocoa = builtin.target.isDarwin(),
|
|
.x11 = builtin.os.tag == .linux,
|
|
});
|
|
|
|
const log = std.log.scoped(.glfw);
|
|
|
|
pub const App = struct {
|
|
app: *CoreApp,
|
|
config: Config,
|
|
|
|
/// Mac-specific state.
|
|
darwin: if (Darwin.enabled) Darwin else void,
|
|
|
|
pub const Options = struct {};
|
|
|
|
pub fn init(core_app: *CoreApp, _: Options) !App {
|
|
if (comptime builtin.target.isDarwin()) {
|
|
log.warn("WARNING WARNING WARNING: GLFW ON MAC HAS BUGS.", .{});
|
|
log.warn("You should use the AppKit-based app instead. The official download", .{});
|
|
log.warn("is properly built and available from GitHub. If you're building from", .{});
|
|
log.warn("source, see the README for details on how to build the AppKit app.", .{});
|
|
}
|
|
|
|
if (!glfw.init(.{})) {
|
|
if (glfw.getError()) |err| {
|
|
log.err("error initializing GLFW err={} msg={s}", .{
|
|
err.error_code,
|
|
err.description,
|
|
});
|
|
return err.error_code;
|
|
}
|
|
|
|
return error.GlfwInitFailedUnknownReason;
|
|
}
|
|
glfw.setErrorCallback(glfwErrorCallback);
|
|
|
|
// Mac-specific state. For example, on Mac we enable window tabbing.
|
|
var darwin = if (Darwin.enabled) try Darwin.init() else {};
|
|
errdefer if (Darwin.enabled) darwin.deinit();
|
|
|
|
// 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});
|
|
}
|
|
}
|
|
|
|
// Queue a single new window that starts on launch
|
|
_ = core_app.mailbox.push(.{
|
|
.new_window = .{},
|
|
}, .{ .forever = {} });
|
|
|
|
// We want the event loop to wake up instantly so we can process our tick.
|
|
glfw.postEmptyEvent();
|
|
|
|
return .{
|
|
.app = core_app,
|
|
.config = config,
|
|
.darwin = darwin,
|
|
};
|
|
}
|
|
|
|
pub fn terminate(self: *App) void {
|
|
self.config.deinit();
|
|
glfw.terminate();
|
|
}
|
|
|
|
/// Run the event loop. This doesn't return until the app exits.
|
|
pub fn run(self: *App) !void {
|
|
while (true) {
|
|
// Wait for any events from the app event loop. wakeup will post
|
|
// an empty event so that this will return.
|
|
//
|
|
// Warning: a known issue on macOS is that this will block while
|
|
// a resize event is actively happening, which will prevent the
|
|
// app tick from happening. I don't know know a way around this
|
|
// but its not a big deal since we don't use glfw for the official
|
|
// mac app, but noting it in case anyone builds for macos using
|
|
// glfw.
|
|
glfw.waitEvents();
|
|
|
|
// Tick the terminal app
|
|
const should_quit = try self.app.tick(self);
|
|
if (should_quit or self.app.surfaces.items.len == 0) {
|
|
for (self.app.surfaces.items) |surface| {
|
|
surface.close(false);
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Wakeup the event loop. This should be able to be called from any thread.
|
|
pub fn wakeup(self: *const App) void {
|
|
_ = self;
|
|
glfw.postEmptyEvent();
|
|
}
|
|
|
|
/// Open the configuration in the system editor.
|
|
pub fn openConfig(self: *App) !void {
|
|
try configpkg.edit.open(self.app.alloc);
|
|
}
|
|
|
|
/// 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.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;
|
|
}
|
|
|
|
/// Toggle the window to fullscreen mode.
|
|
pub fn toggleFullscreen(self: *App, surface: *Surface) void {
|
|
_ = self;
|
|
const win = surface.window;
|
|
|
|
if (surface.isFullscreen()) {
|
|
win.setMonitor(
|
|
null,
|
|
@intCast(surface.monitor_dims.position_x),
|
|
@intCast(surface.monitor_dims.position_y),
|
|
surface.monitor_dims.width,
|
|
surface.monitor_dims.height,
|
|
0,
|
|
);
|
|
return;
|
|
}
|
|
|
|
const monitor = win.getMonitor() orelse monitor: {
|
|
log.warn("window had null monitor, getting primary monitor", .{});
|
|
break :monitor glfw.Monitor.getPrimary() orelse {
|
|
log.warn("window could not get any monitor. will not perform action", .{});
|
|
return;
|
|
};
|
|
};
|
|
|
|
const video_mode = monitor.getVideoMode() orelse {
|
|
log.warn("failed to get video mode. will not perform action", .{});
|
|
return;
|
|
};
|
|
|
|
const position = win.getPos();
|
|
const size = surface.getSize() catch {
|
|
log.warn("failed to get window size. will not perform fullscreen action", .{});
|
|
return;
|
|
};
|
|
|
|
surface.monitor_dims = .{
|
|
.width = size.width,
|
|
.height = size.height,
|
|
.position_x = position.x,
|
|
.position_y = position.y,
|
|
};
|
|
|
|
win.setMonitor(monitor, 0, 0, video_mode.getWidth(), video_mode.getHeight(), 0);
|
|
}
|
|
|
|
/// Create a new window for the app.
|
|
pub fn newWindow(self: *App, parent_: ?*CoreSurface) !void {
|
|
_ = try self.newSurface(parent_);
|
|
}
|
|
|
|
/// Create a new tab in the parent surface.
|
|
fn newTab(self: *App, parent: *CoreSurface) !void {
|
|
if (!Darwin.enabled) {
|
|
log.warn("tabbing is not supported on this platform", .{});
|
|
return;
|
|
}
|
|
|
|
// Create the new window
|
|
const window = try self.newSurface(parent);
|
|
|
|
// Add the new window the parent window
|
|
const parent_win = glfwNative.getCocoaWindow(parent.rt_surface.window).?;
|
|
const other_win = glfwNative.getCocoaWindow(window.window).?;
|
|
const NSWindowOrderingMode = enum(isize) { below = -1, out = 0, above = 1 };
|
|
const nswindow = objc.Object.fromId(parent_win);
|
|
nswindow.msgSend(void, objc.sel("addTabbedWindow:ordered:"), .{
|
|
objc.Object.fromId(other_win),
|
|
NSWindowOrderingMode.above,
|
|
});
|
|
|
|
// Adding a new tab can cause the tab bar to appear which changes
|
|
// our viewport size. We need to call the size callback in order to
|
|
// update values. For example, we need this to set the proper mouse selection
|
|
// point in the grid.
|
|
const size = parent.rt_surface.getSize() catch |err| {
|
|
log.err("error querying window size for size callback on new tab err={}", .{err});
|
|
return;
|
|
};
|
|
parent.sizeCallback(size) catch |err| {
|
|
log.err("error in size callback from new tab err={}", .{err});
|
|
return;
|
|
};
|
|
}
|
|
|
|
fn newSurface(self: *App, parent_: ?*CoreSurface) !*Surface {
|
|
// Grab a surface allocation because we're going to need it.
|
|
var surface = try self.app.alloc.create(Surface);
|
|
errdefer self.app.alloc.destroy(surface);
|
|
|
|
// Create the surface -- because windows are surfaces for glfw.
|
|
try surface.init(self);
|
|
errdefer surface.deinit();
|
|
|
|
// If we have a parent, inherit some properties
|
|
if (self.config.@"window-inherit-font-size") {
|
|
if (parent_) |parent| {
|
|
surface.core_surface.setFontSize(parent.font_size);
|
|
}
|
|
}
|
|
|
|
return surface;
|
|
}
|
|
|
|
/// Close the given surface.
|
|
pub fn closeSurface(self: *App, surface: *Surface) void {
|
|
surface.deinit();
|
|
self.app.alloc.destroy(surface);
|
|
}
|
|
|
|
pub fn redrawSurface(self: *App, surface: *Surface) void {
|
|
_ = self;
|
|
_ = surface;
|
|
|
|
@panic("This should never be called for GLFW.");
|
|
}
|
|
|
|
pub fn redrawInspector(self: *App, surface: *Surface) void {
|
|
_ = self;
|
|
_ = surface;
|
|
|
|
// GLFW doesn't support the inspector
|
|
}
|
|
|
|
fn glfwErrorCallback(code: glfw.ErrorCode, desc: [:0]const u8) void {
|
|
std.log.warn("glfw error={} message={s}", .{ code, desc });
|
|
|
|
// Workaround for: https://github.com/ocornut/imgui/issues/5908
|
|
// If we get an invalid value with "scancode" in the message we assume
|
|
// it is from the glfw key callback that imgui sets and we clear the
|
|
// error so that our future code doesn't crash.
|
|
if (code == glfw.ErrorCode.InvalidValue and
|
|
std.mem.indexOf(u8, desc, "scancode") != null)
|
|
{
|
|
_ = glfw.getError();
|
|
}
|
|
}
|
|
|
|
/// Mac-specific settings. This is only enabled when the target is
|
|
/// Mac and the artifact is a standalone exe. We don't target libs because
|
|
/// the embedded API doesn't do windowing.
|
|
const Darwin = struct {
|
|
const enabled = builtin.target.isDarwin() and build_config.artifact == .exe;
|
|
|
|
tabbing_id: *macos.foundation.String,
|
|
|
|
pub fn init() !Darwin {
|
|
const NSWindow = objc.getClass("NSWindow").?;
|
|
NSWindow.msgSend(void, objc.sel("setAllowsAutomaticWindowTabbing:"), .{true});
|
|
|
|
// Our tabbing ID allows all of our windows to group together
|
|
const tabbing_id = try macos.foundation.String.createWithBytes(
|
|
"com.mitchellh.ghostty.window",
|
|
.utf8,
|
|
false,
|
|
);
|
|
errdefer tabbing_id.release();
|
|
|
|
// Setup our Mac settings
|
|
return .{ .tabbing_id = tabbing_id };
|
|
}
|
|
|
|
pub fn deinit(self: *Darwin) void {
|
|
self.tabbing_id.release();
|
|
self.* = undefined;
|
|
}
|
|
};
|
|
};
|
|
|
|
/// These are used to keep track of the original monitor values so that we can
|
|
/// safely toggle on and off of fullscreen.
|
|
const MonitorDimensions = struct {
|
|
width: u32,
|
|
height: u32,
|
|
position_x: i64,
|
|
position_y: i64,
|
|
};
|
|
|
|
/// Surface represents the drawable surface for glfw. In glfw, a surface
|
|
/// is always a window because that is the only abstraction that glfw exposes.
|
|
///
|
|
/// This means that there is no way for the glfw runtime to support tabs,
|
|
/// splits, etc. without considerable effort. In fact, on Darwin, we do
|
|
/// support tabs because the minimal tabbing interface is a window abstraction,
|
|
/// but this is a bit of a hack. The native Swift runtime should be used instead
|
|
/// which uses real native tabbing.
|
|
///
|
|
/// Other runtimes a surface usually represents the equivalent of a "view"
|
|
/// or "widget" level granularity.
|
|
pub const Surface = struct {
|
|
/// The glfw window handle
|
|
window: glfw.Window,
|
|
|
|
/// The glfw mouse cursor handle.
|
|
cursor: ?glfw.Cursor,
|
|
|
|
/// The app we're part of
|
|
app: *App,
|
|
|
|
/// A core surface
|
|
core_surface: CoreSurface,
|
|
|
|
/// This is the key event that was processed in keyCallback. This is only
|
|
/// non-null if the event was NOT consumed in keyCallback. This lets us
|
|
/// know in charCallback whether we should populate it and call it again.
|
|
/// (GLFW guarantees that charCallback is called after keyCallback).
|
|
key_event: ?input.KeyEvent = null,
|
|
|
|
/// The monitor dimensions so we can toggle fullscreen on and off.
|
|
monitor_dims: MonitorDimensions,
|
|
|
|
pub const Options = struct {};
|
|
|
|
/// Initialize the surface into the given self pointer. This gives a
|
|
/// stable pointer to the destination that can be used for callbacks.
|
|
pub fn init(self: *Surface, app: *App) !void {
|
|
|
|
// Create our window
|
|
const win = glfw.Window.create(
|
|
640,
|
|
480,
|
|
"ghostty",
|
|
if (app.config.fullscreen) glfw.Monitor.getPrimary() else null,
|
|
null,
|
|
Renderer.glfwWindowHints(&app.config),
|
|
) orelse return glfw.mustGetErrorCode();
|
|
errdefer win.destroy();
|
|
|
|
// Get our physical DPI - debug only because we don't have a use for
|
|
// this but the logging of it may be useful
|
|
if (builtin.mode == .Debug) {
|
|
const monitor = win.getMonitor() orelse monitor: {
|
|
log.warn("window had null monitor, getting primary monitor", .{});
|
|
break :monitor glfw.Monitor.getPrimary().?;
|
|
};
|
|
const video_mode = monitor.getVideoMode() orelse return glfw.mustGetErrorCode();
|
|
const physical_size = monitor.getPhysicalSize();
|
|
const physical_x_dpi = @as(f32, @floatFromInt(video_mode.getWidth())) / (@as(f32, @floatFromInt(physical_size.width_mm)) / 25.4);
|
|
const physical_y_dpi = @as(f32, @floatFromInt(video_mode.getHeight())) / (@as(f32, @floatFromInt(physical_size.height_mm)) / 25.4);
|
|
log.debug("physical dpi x={} y={}", .{
|
|
physical_x_dpi,
|
|
physical_y_dpi,
|
|
});
|
|
}
|
|
|
|
// On Mac, enable window tabbing
|
|
if (App.Darwin.enabled) {
|
|
const NSWindowTabbingMode = enum(usize) { automatic = 0, preferred = 1, disallowed = 2 };
|
|
const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(win).?);
|
|
|
|
// Tabbing mode enables tabbing at all
|
|
nswindow.setProperty("tabbingMode", NSWindowTabbingMode.automatic);
|
|
|
|
// All windows within a tab bar must have a matching tabbing ID.
|
|
// The app sets this up for us.
|
|
nswindow.setProperty("tabbingIdentifier", app.darwin.tabbing_id);
|
|
}
|
|
|
|
// Set our callbacks
|
|
win.setUserPointer(&self.core_surface);
|
|
win.setSizeCallback(sizeCallback);
|
|
win.setCharCallback(charCallback);
|
|
win.setKeyCallback(keyCallback);
|
|
win.setFocusCallback(focusCallback);
|
|
win.setRefreshCallback(refreshCallback);
|
|
win.setScrollCallback(scrollCallback);
|
|
win.setCursorPosCallback(cursorPosCallback);
|
|
win.setMouseButtonCallback(mouseButtonCallback);
|
|
win.setDropCallback(dropCallback);
|
|
|
|
const dimensions: MonitorDimensions = dimensions: {
|
|
const pos = win.getPos();
|
|
const size = win.getFramebufferSize();
|
|
break :dimensions .{
|
|
.width = size.width,
|
|
.height = size.height,
|
|
.position_x = pos.x,
|
|
.position_y = pos.y,
|
|
};
|
|
};
|
|
|
|
// Build our result
|
|
self.* = .{
|
|
.app = app,
|
|
.window = win,
|
|
.cursor = null,
|
|
.core_surface = undefined,
|
|
.monitor_dims = dimensions,
|
|
};
|
|
errdefer self.* = undefined;
|
|
|
|
// Initialize our cursor
|
|
try self.setMouseShape(.text);
|
|
|
|
// Add ourselves to the list of surfaces on the app.
|
|
try app.app.addSurface(self);
|
|
errdefer app.app.deleteSurface(self);
|
|
|
|
// Get our new surface config
|
|
var config = try apprt.surface.newConfig(app.app, &app.config);
|
|
defer config.deinit();
|
|
|
|
// Initialize our surface now that we have the stable pointer.
|
|
try self.core_surface.init(
|
|
app.app.alloc,
|
|
&config,
|
|
app.app,
|
|
app,
|
|
self,
|
|
);
|
|
errdefer self.core_surface.deinit();
|
|
}
|
|
|
|
pub fn deinit(self: *Surface) void {
|
|
// Remove ourselves from the list of known surfaces in the app.
|
|
self.app.app.deleteSurface(self);
|
|
|
|
// Clean up our core surface so that all the rendering and IO stop.
|
|
self.core_surface.deinit();
|
|
|
|
var tabgroup_opt: if (App.Darwin.enabled) ?objc.Object else void = undefined;
|
|
if (App.Darwin.enabled) {
|
|
const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(self.window).?);
|
|
const tabgroup = nswindow.getProperty(objc.Object, "tabGroup");
|
|
|
|
// On macOS versions prior to Ventura, we lose window focus on tab close
|
|
// for some reason. We manually fix this by keeping track of the tab
|
|
// group and just selecting the next window.
|
|
if (internal_os.macosVersionAtLeast(13, 0, 0))
|
|
tabgroup_opt = null
|
|
else
|
|
tabgroup_opt = tabgroup;
|
|
|
|
const windows = tabgroup.getProperty(objc.Object, "windows");
|
|
switch (windows.getProperty(usize, "count")) {
|
|
// If we're going down to one window our tab bar is going to be
|
|
// destroyed so unset it so that the later logic doesn't try to
|
|
// use it.
|
|
1 => tabgroup_opt = null,
|
|
|
|
// If our tab bar is visible and we are going down to 1 window,
|
|
// hide the tab bar. The check is "2" because our current window
|
|
// is still present.
|
|
2 => if (tabgroup.getProperty(bool, "tabBarVisible")) {
|
|
nswindow.msgSend(void, objc.sel("toggleTabBar:"), .{nswindow.value});
|
|
},
|
|
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
// We can now safely destroy our windows. We have to do this BEFORE
|
|
// setting up the new focused window below.
|
|
self.window.destroy();
|
|
if (self.cursor) |c| {
|
|
c.destroy();
|
|
self.cursor = null;
|
|
}
|
|
|
|
// If we have a tabgroup set, we want to manually focus the next window.
|
|
// We should NOT have to do this usually, see the comments above.
|
|
if (App.Darwin.enabled) {
|
|
if (tabgroup_opt) |tabgroup| {
|
|
const selected = tabgroup.getProperty(objc.Object, "selectedWindow");
|
|
selected.msgSend(void, objc.sel("makeKeyWindow"), .{});
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Create a new tab in the window containing this surface.
|
|
pub fn newTab(self: *Surface) !void {
|
|
try self.app.newTab(&self.core_surface);
|
|
}
|
|
|
|
/// Checks if the glfw window is in fullscreen.
|
|
pub fn isFullscreen(self: *Surface) bool {
|
|
return self.window.getMonitor() != null;
|
|
}
|
|
|
|
pub fn toggleFullscreen(self: *Surface, _: Config.NonNativeFullscreen) void {
|
|
self.app.toggleFullscreen(self);
|
|
}
|
|
|
|
/// Close this surface.
|
|
pub fn close(self: *Surface, processActive: bool) void {
|
|
_ = processActive;
|
|
self.setShouldClose();
|
|
self.deinit();
|
|
self.app.app.alloc.destroy(self);
|
|
}
|
|
|
|
/// Set the initial window size. This is called exactly once at
|
|
/// surface initialization time. This may be called before "self"
|
|
/// is fully initialized.
|
|
pub fn setInitialWindowSize(self: *const Surface, width: u32, height: u32) !void {
|
|
self.window.setSize(.{ .width = width, .height = height });
|
|
}
|
|
|
|
/// Set the cell size. Unused by GLFW.
|
|
pub fn setCellSize(self: *const Surface, width: u32, height: u32) !void {
|
|
_ = self;
|
|
_ = width;
|
|
_ = height;
|
|
}
|
|
|
|
/// Set the size limits of the window.
|
|
/// Note: this interface is not good, we should redo it if we plan
|
|
/// to use this more. i.e. you can't set max width but no max height,
|
|
/// or no mins.
|
|
pub fn setSizeLimits(self: *Surface, min: apprt.SurfaceSize, max_: ?apprt.SurfaceSize) !void {
|
|
self.window.setSizeLimits(.{
|
|
.width = min.width,
|
|
.height = min.height,
|
|
}, if (max_) |max| .{
|
|
.width = max.width,
|
|
.height = max.height,
|
|
} else .{
|
|
.width = null,
|
|
.height = null,
|
|
});
|
|
}
|
|
|
|
/// Returns the content scale for the created window.
|
|
pub fn getContentScale(self: *const Surface) !apprt.ContentScale {
|
|
const scale = self.window.getContentScale();
|
|
return apprt.ContentScale{ .x = scale.x_scale, .y = scale.y_scale };
|
|
}
|
|
|
|
/// Returns the size of the window in pixels. The pixel size may
|
|
/// not match screen coordinate size but we should be able to convert
|
|
/// back and forth using getContentScale.
|
|
pub fn getSize(self: *const Surface) !apprt.SurfaceSize {
|
|
const size = self.window.getFramebufferSize();
|
|
return apprt.SurfaceSize{ .width = size.width, .height = size.height };
|
|
}
|
|
|
|
/// Returns the cursor position in scaled pixels relative to the
|
|
/// upper-left of the window.
|
|
pub fn getCursorPos(self: *const Surface) !apprt.CursorPos {
|
|
const unscaled_pos = self.window.getCursorPos();
|
|
const pos = try self.cursorPosToPixels(unscaled_pos);
|
|
return apprt.CursorPos{
|
|
.x = @floatCast(pos.xpos),
|
|
.y = @floatCast(pos.ypos),
|
|
};
|
|
}
|
|
|
|
/// Set the flag that notes this window should be closed for the next
|
|
/// iteration of the event loop.
|
|
pub fn setShouldClose(self: *Surface) void {
|
|
self.window.setShouldClose(true);
|
|
}
|
|
|
|
/// Returns true if the window is flagged to close.
|
|
pub fn shouldClose(self: *const Surface) bool {
|
|
return self.window.shouldClose();
|
|
}
|
|
|
|
/// Set the title of the window.
|
|
pub fn setTitle(self: *Surface, slice: [:0]const u8) !void {
|
|
self.window.setTitle(slice.ptr);
|
|
}
|
|
|
|
/// Set the shape of the cursor.
|
|
pub fn setMouseShape(self: *Surface, shape: terminal.MouseShape) !void {
|
|
if ((comptime builtin.target.isDarwin()) and
|
|
!internal_os.macosVersionAtLeast(13, 0, 0))
|
|
{
|
|
// We only set our cursor if we're NOT on Mac, or if we are then the
|
|
// macOS version is >= 13 (Ventura). On prior versions, glfw crashes
|
|
// since we use a tab group.
|
|
return;
|
|
}
|
|
|
|
const new = glfw.Cursor.createStandard(switch (shape) {
|
|
.default => .arrow,
|
|
.text => .ibeam,
|
|
.crosshair => .crosshair,
|
|
.pointer => .pointing_hand,
|
|
.ew_resize => .resize_ew,
|
|
.ns_resize => .resize_ns,
|
|
.nwse_resize => .resize_nwse,
|
|
.nesw_resize => .resize_nesw,
|
|
.all_scroll => .resize_all,
|
|
.not_allowed => .not_allowed,
|
|
else => return, // unsupported, ignore
|
|
}) orelse {
|
|
const err = glfw.mustGetErrorCode();
|
|
log.warn("error creating cursor: {}", .{err});
|
|
return;
|
|
};
|
|
errdefer new.destroy();
|
|
|
|
// Set our cursor before we destroy the old one
|
|
self.window.setCursor(new);
|
|
|
|
if (self.cursor) |c| c.destroy();
|
|
self.cursor = new;
|
|
}
|
|
|
|
/// Set the visibility of the mouse cursor.
|
|
pub fn setMouseVisibility(self: *Surface, visible: bool) void {
|
|
self.window.setInputModeCursor(if (visible) .normal else .hidden);
|
|
}
|
|
|
|
/// Start an async clipboard request.
|
|
pub fn clipboardRequest(
|
|
self: *Surface,
|
|
clipboard_type: apprt.Clipboard,
|
|
state: apprt.ClipboardRequest,
|
|
) !void {
|
|
// GLFW can read clipboards immediately so just do that.
|
|
const str: [:0]const u8 = switch (clipboard_type) {
|
|
.standard => glfw.getClipboardString() orelse return glfw.mustGetErrorCode(),
|
|
.selection, .primary => selection: {
|
|
// Not supported except on Linux
|
|
if (comptime builtin.os.tag != .linux) break :selection "";
|
|
|
|
const raw = glfwNative.getX11SelectionString() orelse
|
|
return glfw.mustGetErrorCode();
|
|
break :selection std.mem.span(raw);
|
|
},
|
|
};
|
|
|
|
// Complete our request. We always allow unsafe because we don't
|
|
// want to deal with user confirmation in this runtime.
|
|
try self.core_surface.completeClipboardRequest(state, str, true);
|
|
}
|
|
|
|
/// Set the clipboard.
|
|
pub fn setClipboardString(
|
|
self: *const Surface,
|
|
val: [:0]const u8,
|
|
clipboard_type: apprt.Clipboard,
|
|
confirm: bool,
|
|
) !void {
|
|
_ = confirm;
|
|
_ = self;
|
|
switch (clipboard_type) {
|
|
.standard => glfw.setClipboardString(val),
|
|
.selection, .primary => {
|
|
// Not supported except on Linux
|
|
if (comptime builtin.os.tag != .linux) return;
|
|
glfwNative.setX11SelectionString(val.ptr);
|
|
},
|
|
}
|
|
}
|
|
|
|
/// The cursor position from glfw directly is in screen coordinates but
|
|
/// all our interface works in pixels.
|
|
fn cursorPosToPixels(self: *const Surface, pos: glfw.Window.CursorPos) !glfw.Window.CursorPos {
|
|
// The cursor position is in screen coordinates but we
|
|
// want it in pixels. we need to get both the size of the
|
|
// window in both to get the ratio to make the conversion.
|
|
const size = self.window.getSize();
|
|
const fb_size = self.window.getFramebufferSize();
|
|
|
|
// If our framebuffer and screen are the same, then there is no scaling
|
|
// happening and we can short-circuit by returning the pos as-is.
|
|
if (fb_size.width == size.width and fb_size.height == size.height)
|
|
return pos;
|
|
|
|
const x_scale = @as(f64, @floatFromInt(fb_size.width)) / @as(f64, @floatFromInt(size.width));
|
|
const y_scale = @as(f64, @floatFromInt(fb_size.height)) / @as(f64, @floatFromInt(size.height));
|
|
return .{
|
|
.xpos = pos.xpos * x_scale,
|
|
.ypos = pos.ypos * y_scale,
|
|
};
|
|
}
|
|
|
|
fn sizeCallback(window: glfw.Window, width: i32, height: i32) void {
|
|
_ = width;
|
|
_ = height;
|
|
|
|
// Get the size. We are given a width/height but this is in screen
|
|
// coordinates and we want raw pixels. The core window uses the content
|
|
// scale to scale appropriately.
|
|
const core_win = window.getUserPointer(CoreSurface) orelse return;
|
|
const size = core_win.rt_surface.getSize() catch |err| {
|
|
log.err("error querying window size for size callback err={}", .{err});
|
|
return;
|
|
};
|
|
|
|
// Call the primary callback.
|
|
core_win.sizeCallback(size) catch |err| {
|
|
log.err("error in size callback err={}", .{err});
|
|
return;
|
|
};
|
|
}
|
|
|
|
fn charCallback(window: glfw.Window, codepoint: u21) void {
|
|
const core_win = window.getUserPointer(CoreSurface) orelse return;
|
|
|
|
// We need a key event in order to process the charcallback. If it
|
|
// isn't set then the key event was consumed.
|
|
var key_event = core_win.rt_surface.key_event orelse return;
|
|
core_win.rt_surface.key_event = null;
|
|
|
|
// Populate the utf8 value for the event
|
|
var buf: [4]u8 = undefined;
|
|
const len = std.unicode.utf8Encode(codepoint, &buf) catch |err| {
|
|
log.err("error encoding codepoint={} err={}", .{ codepoint, err });
|
|
return;
|
|
};
|
|
key_event.utf8 = buf[0..len];
|
|
|
|
// On macOS we need to also disable some modifiers because
|
|
// alt+key consumes the alt.
|
|
if (comptime builtin.target.isDarwin()) {
|
|
// For GLFW, we say we always consume alt because
|
|
// GLFW doesn't have a way to disable the alt key.
|
|
key_event.consumed_mods.alt = true;
|
|
}
|
|
|
|
_ = core_win.keyCallback(key_event) catch |err| {
|
|
log.err("error in key callback err={}", .{err});
|
|
return;
|
|
};
|
|
}
|
|
|
|
fn keyCallback(
|
|
window: glfw.Window,
|
|
glfw_key: glfw.Key,
|
|
scancode: i32,
|
|
glfw_action: glfw.Action,
|
|
glfw_mods: glfw.Mods,
|
|
) void {
|
|
_ = scancode;
|
|
|
|
const core_win = window.getUserPointer(CoreSurface) orelse return;
|
|
|
|
// Convert our glfw types into our input types
|
|
const mods: input.Mods = .{
|
|
.shift = glfw_mods.shift,
|
|
.ctrl = glfw_mods.control,
|
|
.alt = glfw_mods.alt,
|
|
.super = glfw_mods.super,
|
|
};
|
|
const action: input.Action = switch (glfw_action) {
|
|
.release => .release,
|
|
.press => .press,
|
|
.repeat => .repeat,
|
|
};
|
|
const key: input.Key = switch (glfw_key) {
|
|
.a => .a,
|
|
.b => .b,
|
|
.c => .c,
|
|
.d => .d,
|
|
.e => .e,
|
|
.f => .f,
|
|
.g => .g,
|
|
.h => .h,
|
|
.i => .i,
|
|
.j => .j,
|
|
.k => .k,
|
|
.l => .l,
|
|
.m => .m,
|
|
.n => .n,
|
|
.o => .o,
|
|
.p => .p,
|
|
.q => .q,
|
|
.r => .r,
|
|
.s => .s,
|
|
.t => .t,
|
|
.u => .u,
|
|
.v => .v,
|
|
.w => .w,
|
|
.x => .x,
|
|
.y => .y,
|
|
.z => .z,
|
|
.zero => .zero,
|
|
.one => .one,
|
|
.two => .two,
|
|
.three => .three,
|
|
.four => .four,
|
|
.five => .five,
|
|
.six => .six,
|
|
.seven => .seven,
|
|
.eight => .eight,
|
|
.nine => .nine,
|
|
.up => .up,
|
|
.down => .down,
|
|
.right => .right,
|
|
.left => .left,
|
|
.home => .home,
|
|
.end => .end,
|
|
.page_up => .page_up,
|
|
.page_down => .page_down,
|
|
.escape => .escape,
|
|
.F1 => .f1,
|
|
.F2 => .f2,
|
|
.F3 => .f3,
|
|
.F4 => .f4,
|
|
.F5 => .f5,
|
|
.F6 => .f6,
|
|
.F7 => .f7,
|
|
.F8 => .f8,
|
|
.F9 => .f9,
|
|
.F10 => .f10,
|
|
.F11 => .f11,
|
|
.F12 => .f12,
|
|
.F13 => .f13,
|
|
.F14 => .f14,
|
|
.F15 => .f15,
|
|
.F16 => .f16,
|
|
.F17 => .f17,
|
|
.F18 => .f18,
|
|
.F19 => .f19,
|
|
.F20 => .f20,
|
|
.F21 => .f21,
|
|
.F22 => .f22,
|
|
.F23 => .f23,
|
|
.F24 => .f24,
|
|
.F25 => .f25,
|
|
.kp_0 => .kp_0,
|
|
.kp_1 => .kp_1,
|
|
.kp_2 => .kp_2,
|
|
.kp_3 => .kp_3,
|
|
.kp_4 => .kp_4,
|
|
.kp_5 => .kp_5,
|
|
.kp_6 => .kp_6,
|
|
.kp_7 => .kp_7,
|
|
.kp_8 => .kp_8,
|
|
.kp_9 => .kp_9,
|
|
.kp_decimal => .kp_decimal,
|
|
.kp_divide => .kp_divide,
|
|
.kp_multiply => .kp_multiply,
|
|
.kp_subtract => .kp_subtract,
|
|
.kp_add => .kp_add,
|
|
.kp_enter => .kp_enter,
|
|
.kp_equal => .kp_equal,
|
|
.grave_accent => .grave_accent,
|
|
.minus => .minus,
|
|
.equal => .equal,
|
|
.space => .space,
|
|
.semicolon => .semicolon,
|
|
.apostrophe => .apostrophe,
|
|
.comma => .comma,
|
|
.period => .period,
|
|
.slash => .slash,
|
|
.left_bracket => .left_bracket,
|
|
.right_bracket => .right_bracket,
|
|
.backslash => .backslash,
|
|
.enter => .enter,
|
|
.tab => .tab,
|
|
.backspace => .backspace,
|
|
.insert => .insert,
|
|
.delete => .delete,
|
|
.caps_lock => .caps_lock,
|
|
.scroll_lock => .scroll_lock,
|
|
.num_lock => .num_lock,
|
|
.print_screen => .print_screen,
|
|
.pause => .pause,
|
|
.left_shift => .left_shift,
|
|
.left_control => .left_control,
|
|
.left_alt => .left_alt,
|
|
.left_super => .left_super,
|
|
.right_shift => .right_shift,
|
|
.right_control => .right_control,
|
|
.right_alt => .right_alt,
|
|
.right_super => .right_super,
|
|
|
|
.menu,
|
|
.world_1,
|
|
.world_2,
|
|
.unknown,
|
|
=> .invalid,
|
|
};
|
|
|
|
// This is a hack for GLFW. We require our apprts to send both
|
|
// the UTF8 encoding AND the keypress at the same time. Its critical
|
|
// for things like ctrl sequences to work. However, GLFW doesn't
|
|
// provide this information all at once. So we just infer based on
|
|
// the key press. This isn't portable but GLFW is only for testing.
|
|
const utf8 = switch (key) {
|
|
inline else => |k| utf8: {
|
|
if (mods.shift) break :utf8 "";
|
|
const cp = k.codepoint() orelse break :utf8 "";
|
|
const byte = std.math.cast(u8, cp) orelse break :utf8 "";
|
|
break :utf8 &.{byte};
|
|
},
|
|
};
|
|
|
|
const key_event: input.KeyEvent = .{
|
|
.action = action,
|
|
.key = key,
|
|
.physical_key = key,
|
|
.mods = mods,
|
|
.consumed_mods = .{},
|
|
.composing = false,
|
|
.utf8 = utf8,
|
|
.unshifted_codepoint = if (utf8.len > 0) @intCast(utf8[0]) else 0,
|
|
};
|
|
|
|
const effect = core_win.keyCallback(key_event) catch |err| {
|
|
log.err("error in key callback err={}", .{err});
|
|
return;
|
|
};
|
|
|
|
// Surface closed.
|
|
if (effect == .closed) return;
|
|
|
|
// If it wasn't consumed, we set it on our self so that charcallback
|
|
// can make another attempt. Otherwise, we set null so the charcallback
|
|
// is ignored.
|
|
core_win.rt_surface.key_event = null;
|
|
if (effect == .ignored and
|
|
(action == .press or action == .repeat))
|
|
{
|
|
core_win.rt_surface.key_event = key_event;
|
|
}
|
|
}
|
|
|
|
fn focusCallback(window: glfw.Window, focused: bool) void {
|
|
const core_win = window.getUserPointer(CoreSurface) orelse return;
|
|
core_win.focusCallback(focused) catch |err| {
|
|
log.err("error in focus callback err={}", .{err});
|
|
return;
|
|
};
|
|
}
|
|
|
|
fn refreshCallback(window: glfw.Window) void {
|
|
const core_win = window.getUserPointer(CoreSurface) orelse return;
|
|
core_win.refreshCallback() catch |err| {
|
|
log.err("error in refresh callback err={}", .{err});
|
|
return;
|
|
};
|
|
}
|
|
|
|
fn scrollCallback(window: glfw.Window, xoff: f64, yoff: f64) void {
|
|
// Glfw doesn't support any of the scroll mods.
|
|
const scroll_mods: input.ScrollMods = .{};
|
|
|
|
const core_win = window.getUserPointer(CoreSurface) orelse return;
|
|
core_win.scrollCallback(xoff, yoff, scroll_mods) catch |err| {
|
|
log.err("error in scroll callback err={}", .{err});
|
|
return;
|
|
};
|
|
}
|
|
|
|
fn cursorPosCallback(
|
|
window: glfw.Window,
|
|
unscaled_xpos: f64,
|
|
unscaled_ypos: f64,
|
|
) void {
|
|
const core_win = window.getUserPointer(CoreSurface) orelse return;
|
|
|
|
// Convert our unscaled x/y to scaled.
|
|
const pos = core_win.rt_surface.cursorPosToPixels(.{
|
|
.xpos = unscaled_xpos,
|
|
.ypos = unscaled_ypos,
|
|
}) catch |err| {
|
|
log.err(
|
|
"error converting cursor pos to scaled pixels in cursor pos callback err={}",
|
|
.{err},
|
|
);
|
|
return;
|
|
};
|
|
|
|
core_win.cursorPosCallback(.{
|
|
.x = @floatCast(pos.xpos),
|
|
.y = @floatCast(pos.ypos),
|
|
}) catch |err| {
|
|
log.err("error in cursor pos callback err={}", .{err});
|
|
return;
|
|
};
|
|
}
|
|
|
|
fn mouseButtonCallback(
|
|
window: glfw.Window,
|
|
glfw_button: glfw.MouseButton,
|
|
glfw_action: glfw.Action,
|
|
glfw_mods: glfw.Mods,
|
|
) void {
|
|
const core_win = window.getUserPointer(CoreSurface) orelse return;
|
|
|
|
// Convert glfw button to input button
|
|
const mods: input.Mods = .{
|
|
.shift = glfw_mods.shift,
|
|
.ctrl = glfw_mods.control,
|
|
.alt = glfw_mods.alt,
|
|
.super = glfw_mods.super,
|
|
};
|
|
const button: input.MouseButton = switch (glfw_button) {
|
|
.left => .left,
|
|
.right => .right,
|
|
.middle => .middle,
|
|
.four => .four,
|
|
.five => .five,
|
|
.six => .six,
|
|
.seven => .seven,
|
|
.eight => .eight,
|
|
};
|
|
const action: input.MouseButtonState = switch (glfw_action) {
|
|
.press => .press,
|
|
.release => .release,
|
|
else => unreachable,
|
|
};
|
|
|
|
core_win.mouseButtonCallback(action, button, mods) catch |err| {
|
|
log.err("error in scroll callback err={}", .{err});
|
|
return;
|
|
};
|
|
}
|
|
|
|
fn dropCallback(window: glfw.Window, paths: [][*:0]const u8) void {
|
|
const surface = window.getUserPointer(CoreSurface) orelse return;
|
|
|
|
var list = std.ArrayList(u8).init(surface.alloc);
|
|
defer list.deinit();
|
|
|
|
for (paths) |path| {
|
|
const path_slice = std.mem.span(path);
|
|
|
|
// preallocate worst case of escaping every char + space
|
|
list.ensureTotalCapacity(path_slice.len * 2 + 1) catch |err| {
|
|
log.err("error in drop callback err={}", .{err});
|
|
return;
|
|
};
|
|
|
|
const writer = list.writer();
|
|
for (path_slice) |c| {
|
|
if (std.mem.indexOfScalar(u8, "\\ ()[]{}<>\"'`!#$&;|*?\t", c)) |_| {
|
|
writer.print("\\{c}", .{c}) catch unreachable; // memory preallocated
|
|
} else writer.writeByte(c) catch unreachable; // same here
|
|
}
|
|
writer.writeByte(' ') catch unreachable; // separate paths
|
|
|
|
surface.textCallback(list.items) catch |err| {
|
|
log.err("error in drop callback err={}", .{err});
|
|
return;
|
|
};
|
|
|
|
list.clearRetainingCapacity(); // avoid unnecessary reallocations
|
|
}
|
|
}
|
|
};
|