mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 16:26:08 +03:00
Merge pull request #68 from mitchellh/window-abs
Enable alternate windowing runtimes
This commit is contained in:
@ -203,7 +203,7 @@ fn setQuit(self: *App) !void {
|
|||||||
|
|
||||||
// Mark that all our windows should close
|
// Mark that all our windows should close
|
||||||
for (self.windows.items) |window| {
|
for (self.windows.items) |window| {
|
||||||
window.window.setShouldClose(true);
|
window.window.setShouldClose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
675
src/Window.zig
675
src/Window.zig
File diff suppressed because it is too large
Load Diff
27
src/apprt.zig
Normal file
27
src/apprt.zig
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
//! "apprt" is the "application runtime" package. This abstracts the
|
||||||
|
//! application runtime and lifecycle management such as creating windows,
|
||||||
|
//! getting user input (mouse/keyboard), etc.
|
||||||
|
//!
|
||||||
|
//! This enables compile-time interfaces to be built to swap out the underlying
|
||||||
|
//! application runtime. For example: glfw, pure macOS Cocoa, GTK+, browser, etc.
|
||||||
|
//!
|
||||||
|
//! The goal is to have different implementations share as much of the core
|
||||||
|
//! logic as possible, and to only reach out to platform-specific implementation
|
||||||
|
//! code when absolutely necessary.
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
|
pub usingnamespace @import("apprt/structs.zig");
|
||||||
|
pub const glfw = @import("apprt/glfw.zig");
|
||||||
|
pub const Window = @import("apprt/Window.zig");
|
||||||
|
|
||||||
|
/// The implementation to use for the app runtime. This is comptime chosen
|
||||||
|
/// so that every build has exactly one application runtime implementation.
|
||||||
|
/// Note: it is very rare to use Runtime directly; most usage will use
|
||||||
|
/// Window or something.
|
||||||
|
pub const runtime = switch (builtin.os.tag) {
|
||||||
|
else => glfw,
|
||||||
|
};
|
||||||
|
|
||||||
|
test {
|
||||||
|
@import("std").testing.refAllDecls(@This());
|
||||||
|
}
|
531
src/apprt/glfw.zig
Normal file
531
src/apprt/glfw.zig
Normal file
@ -0,0 +1,531 @@
|
|||||||
|
//! 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 assert = std.debug.assert;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const trace = @import("tracy").trace;
|
||||||
|
const glfw = @import("glfw");
|
||||||
|
const objc = @import("objc");
|
||||||
|
const App = @import("../App.zig");
|
||||||
|
const input = @import("../input.zig");
|
||||||
|
const internal_os = @import("../os/main.zig");
|
||||||
|
const renderer = @import("../renderer.zig");
|
||||||
|
const Renderer = renderer.Renderer;
|
||||||
|
const apprt = @import("../apprt.zig");
|
||||||
|
const CoreWindow = @import("../Window.zig");
|
||||||
|
|
||||||
|
// Get native API access on certain platforms so we can do more customization.
|
||||||
|
const glfwNative = glfw.Native(.{
|
||||||
|
.cocoa = builtin.target.isDarwin(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const log = std.log.scoped(.glfw);
|
||||||
|
|
||||||
|
pub const Window = struct {
|
||||||
|
/// The glfw window handle
|
||||||
|
window: glfw.Window,
|
||||||
|
|
||||||
|
/// The glfw mouse cursor handle.
|
||||||
|
cursor: glfw.Cursor,
|
||||||
|
|
||||||
|
pub fn init(app: *const App, core_win: *CoreWindow) !Window {
|
||||||
|
// Create our window
|
||||||
|
const win = try glfw.Window.create(
|
||||||
|
640,
|
||||||
|
480,
|
||||||
|
"ghostty",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
Renderer.glfwWindowHints(),
|
||||||
|
);
|
||||||
|
errdefer win.destroy();
|
||||||
|
|
||||||
|
if (builtin.mode == .Debug) {
|
||||||
|
// Get our physical DPI - debug only because we don't have a use for
|
||||||
|
// this but the logging of it may be useful
|
||||||
|
const monitor = win.getMonitor() orelse monitor: {
|
||||||
|
log.warn("window had null monitor, getting primary monitor", .{});
|
||||||
|
break :monitor glfw.Monitor.getPrimary().?;
|
||||||
|
};
|
||||||
|
const physical_size = monitor.getPhysicalSize();
|
||||||
|
const video_mode = try monitor.getVideoMode();
|
||||||
|
const physical_x_dpi = @intToFloat(f32, video_mode.getWidth()) / (@intToFloat(f32, physical_size.width_mm) / 25.4);
|
||||||
|
const physical_y_dpi = @intToFloat(f32, video_mode.getHeight()) / (@intToFloat(f32, physical_size.height_mm) / 25.4);
|
||||||
|
log.debug("physical dpi x={} y={}", .{
|
||||||
|
physical_x_dpi,
|
||||||
|
physical_y_dpi,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// On Mac, enable tabbing
|
||||||
|
if (comptime builtin.target.isDarwin()) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the cursor
|
||||||
|
const cursor = try glfw.Cursor.createStandard(.ibeam);
|
||||||
|
errdefer cursor.destroy();
|
||||||
|
if ((comptime !builtin.target.isDarwin()) or 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.
|
||||||
|
try win.setCursor(cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set our callbacks
|
||||||
|
win.setUserPointer(core_win);
|
||||||
|
win.setSizeCallback(sizeCallback);
|
||||||
|
win.setCharCallback(charCallback);
|
||||||
|
win.setKeyCallback(keyCallback);
|
||||||
|
win.setFocusCallback(focusCallback);
|
||||||
|
win.setRefreshCallback(refreshCallback);
|
||||||
|
win.setScrollCallback(scrollCallback);
|
||||||
|
win.setCursorPosCallback(cursorPosCallback);
|
||||||
|
win.setMouseButtonCallback(mouseButtonCallback);
|
||||||
|
|
||||||
|
// Build our result
|
||||||
|
return Window{
|
||||||
|
.window = win,
|
||||||
|
.cursor = cursor,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Window) void {
|
||||||
|
var tabgroup_opt: if (builtin.target.isDarwin()) ?objc.Object else void = undefined;
|
||||||
|
if (comptime builtin.target.isDarwin()) {
|
||||||
|
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();
|
||||||
|
self.cursor.destroy();
|
||||||
|
|
||||||
|
// 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 (comptime builtin.target.isDarwin()) {
|
||||||
|
if (tabgroup_opt) |tabgroup| {
|
||||||
|
const selected = tabgroup.getProperty(objc.Object, "selectedWindow");
|
||||||
|
selected.msgSend(void, objc.sel("makeKeyWindow"), .{});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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: *Window, min: apprt.WindowSize, max_: ?apprt.WindowSize) !void {
|
||||||
|
try 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 Window) !apprt.ContentScale {
|
||||||
|
const scale = try 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 Window) !apprt.WindowSize {
|
||||||
|
const size = self.window.getFramebufferSize() catch |err| err: {
|
||||||
|
log.err("error querying window size in pixels, will use screen size err={}", .{err});
|
||||||
|
break :err try self.window.getSize();
|
||||||
|
};
|
||||||
|
|
||||||
|
return apprt.WindowSize{ .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 Window) !apprt.CursorPos {
|
||||||
|
const unscaled_pos = try self.window.getCursorPos();
|
||||||
|
const pos = try self.cursorPosToPixels(unscaled_pos);
|
||||||
|
return apprt.CursorPos{
|
||||||
|
.x = @floatCast(f32, pos.xpos),
|
||||||
|
.y = @floatCast(f32, pos.ypos),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the flag that notes this window should be closed for the next
|
||||||
|
/// iteration of the event loop.
|
||||||
|
pub fn setShouldClose(self: *Window) void {
|
||||||
|
self.window.setShouldClose(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the window is flagged to close.
|
||||||
|
pub fn shouldClose(self: *const Window) bool {
|
||||||
|
return self.window.shouldClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the title of the window.
|
||||||
|
pub fn setTitle(self: *Window, slice: [:0]const u8) !void {
|
||||||
|
try self.window.setTitle(slice.ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the clipboard. The windowing system is responsible for allocating
|
||||||
|
/// a buffer as necessary. This should be a stable pointer until the next
|
||||||
|
/// time getClipboardString is called.
|
||||||
|
pub fn getClipboardString(self: *const Window) ![:0]const u8 {
|
||||||
|
_ = self;
|
||||||
|
return try glfw.getClipboardString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the clipboard.
|
||||||
|
pub fn setClipboardString(self: *const Window, val: [:0]const u8) !void {
|
||||||
|
_ = self;
|
||||||
|
try glfw.setClipboardString(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The cursor position from glfw directly is in screen coordinates but
|
||||||
|
/// all our interface works in pixels.
|
||||||
|
fn cursorPosToPixels(self: *const Window, 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 = try self.window.getSize();
|
||||||
|
const fb_size = try 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 = @intToFloat(f64, fb_size.width) / @intToFloat(f64, size.width);
|
||||||
|
const y_scale = @intToFloat(f64, fb_size.height) / @intToFloat(f64, 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(CoreWindow) orelse return;
|
||||||
|
const size = core_win.window.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 tracy = trace(@src());
|
||||||
|
defer tracy.end();
|
||||||
|
|
||||||
|
const core_win = window.getUserPointer(CoreWindow) orelse return;
|
||||||
|
core_win.charCallback(codepoint) catch |err| {
|
||||||
|
log.err("error in char 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 tracy = trace(@src());
|
||||||
|
defer tracy.end();
|
||||||
|
|
||||||
|
// Convert our glfw types into our input types
|
||||||
|
const mods = @bitCast(input.Mods, glfw_mods);
|
||||||
|
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 => .three,
|
||||||
|
.three => .four,
|
||||||
|
.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,
|
||||||
|
};
|
||||||
|
|
||||||
|
const core_win = window.getUserPointer(CoreWindow) orelse return;
|
||||||
|
core_win.keyCallback(action, key, mods) catch |err| {
|
||||||
|
log.err("error in key callback err={}", .{err});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focusCallback(window: glfw.Window, focused: bool) void {
|
||||||
|
const tracy = trace(@src());
|
||||||
|
defer tracy.end();
|
||||||
|
|
||||||
|
const core_win = window.getUserPointer(CoreWindow) orelse return;
|
||||||
|
core_win.focusCallback(focused) catch |err| {
|
||||||
|
log.err("error in focus callback err={}", .{err});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refreshCallback(window: glfw.Window) void {
|
||||||
|
const tracy = trace(@src());
|
||||||
|
defer tracy.end();
|
||||||
|
|
||||||
|
const core_win = window.getUserPointer(CoreWindow) 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 {
|
||||||
|
const tracy = trace(@src());
|
||||||
|
defer tracy.end();
|
||||||
|
|
||||||
|
const core_win = window.getUserPointer(CoreWindow) orelse return;
|
||||||
|
core_win.scrollCallback(xoff, yoff) catch |err| {
|
||||||
|
log.err("error in scroll callback err={}", .{err});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursorPosCallback(
|
||||||
|
window: glfw.Window,
|
||||||
|
unscaled_xpos: f64,
|
||||||
|
unscaled_ypos: f64,
|
||||||
|
) void {
|
||||||
|
const tracy = trace(@src());
|
||||||
|
defer tracy.end();
|
||||||
|
|
||||||
|
const core_win = window.getUserPointer(CoreWindow) orelse return;
|
||||||
|
|
||||||
|
// Convert our unscaled x/y to scaled.
|
||||||
|
const pos = core_win.window.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(f32, pos.xpos),
|
||||||
|
.y = @floatCast(f32, 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 tracy = trace(@src());
|
||||||
|
defer tracy.end();
|
||||||
|
|
||||||
|
const core_win = window.getUserPointer(CoreWindow) orelse return;
|
||||||
|
|
||||||
|
// Convert glfw button to input button
|
||||||
|
const mods = @bitCast(input.Mods, glfw_mods);
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
19
src/apprt/structs.zig
Normal file
19
src/apprt/structs.zig
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/// ContentScale is the ratio between the current DPI and the platform's
|
||||||
|
/// default DPI. This is used to determine how much certain rendered elements
|
||||||
|
/// need to be scaled up or down.
|
||||||
|
pub const ContentScale = struct {
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The size of the window in pixels.
|
||||||
|
pub const WindowSize = struct {
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The position of the cursor in pixels.
|
||||||
|
pub const CursorPos = struct {
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
};
|
@ -13,6 +13,13 @@ pub const Mods = packed struct {
|
|||||||
_padding: u2 = 0,
|
_padding: u2 = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// The action associated with an input event.
|
||||||
|
pub const Action = enum {
|
||||||
|
release,
|
||||||
|
press,
|
||||||
|
repeat,
|
||||||
|
};
|
||||||
|
|
||||||
/// The set of keys that can map to keybindings. These have no fixed enum
|
/// The set of keys that can map to keybindings. These have no fixed enum
|
||||||
/// values because we map platform-specific keys to this set. Note that
|
/// values because we map platform-specific keys to this set. Note that
|
||||||
/// this only needs to accomodate what maps to a key. If a key is not bound
|
/// this only needs to accomodate what maps to a key. If a key is not bound
|
||||||
@ -61,10 +68,19 @@ pub const Key = enum {
|
|||||||
eight,
|
eight,
|
||||||
nine,
|
nine,
|
||||||
|
|
||||||
// other
|
// puncuation
|
||||||
|
semicolon,
|
||||||
|
space,
|
||||||
|
apostrophe,
|
||||||
|
comma,
|
||||||
grave_accent, // `
|
grave_accent, // `
|
||||||
|
period,
|
||||||
|
slash,
|
||||||
minus,
|
minus,
|
||||||
equal,
|
equal,
|
||||||
|
left_bracket, // [
|
||||||
|
right_bracket, // ]
|
||||||
|
backslash, // /
|
||||||
|
|
||||||
// control
|
// control
|
||||||
up,
|
up,
|
||||||
@ -73,10 +89,21 @@ pub const Key = enum {
|
|||||||
left,
|
left,
|
||||||
home,
|
home,
|
||||||
end,
|
end,
|
||||||
|
insert,
|
||||||
|
delete,
|
||||||
|
caps_lock,
|
||||||
|
scroll_lock,
|
||||||
|
num_lock,
|
||||||
page_up,
|
page_up,
|
||||||
page_down,
|
page_down,
|
||||||
escape,
|
escape,
|
||||||
|
enter,
|
||||||
|
tab,
|
||||||
|
backspace,
|
||||||
|
print_screen,
|
||||||
|
pause,
|
||||||
|
|
||||||
|
// function keys
|
||||||
f1,
|
f1,
|
||||||
f2,
|
f2,
|
||||||
f3,
|
f3,
|
||||||
@ -89,6 +116,48 @@ pub const Key = enum {
|
|||||||
f10,
|
f10,
|
||||||
f11,
|
f11,
|
||||||
f12,
|
f12,
|
||||||
|
f13,
|
||||||
|
f14,
|
||||||
|
f15,
|
||||||
|
f16,
|
||||||
|
f17,
|
||||||
|
f18,
|
||||||
|
f19,
|
||||||
|
f20,
|
||||||
|
f21,
|
||||||
|
f22,
|
||||||
|
f23,
|
||||||
|
f24,
|
||||||
|
f25,
|
||||||
|
|
||||||
|
// keypad
|
||||||
|
kp_0,
|
||||||
|
kp_1,
|
||||||
|
kp_2,
|
||||||
|
kp_3,
|
||||||
|
kp_4,
|
||||||
|
kp_5,
|
||||||
|
kp_6,
|
||||||
|
kp_7,
|
||||||
|
kp_8,
|
||||||
|
kp_9,
|
||||||
|
kp_decimal,
|
||||||
|
kp_divide,
|
||||||
|
kp_multiply,
|
||||||
|
kp_subtract,
|
||||||
|
kp_add,
|
||||||
|
kp_enter,
|
||||||
|
kp_equal,
|
||||||
|
|
||||||
|
// modifiers
|
||||||
|
left_shift,
|
||||||
|
left_control,
|
||||||
|
left_alt,
|
||||||
|
left_super,
|
||||||
|
right_shift,
|
||||||
|
right_control,
|
||||||
|
right_alt,
|
||||||
|
right_super,
|
||||||
|
|
||||||
// To support more keys (there are obviously more!) add them here
|
// To support more keys (there are obviously more!) add them here
|
||||||
// and ensure the mapping is up to date in the Window key handler.
|
// and ensure the mapping is up to date in the Window key handler.
|
||||||
|
@ -10,6 +10,7 @@ const glfw = @import("glfw");
|
|||||||
const objc = @import("objc");
|
const objc = @import("objc");
|
||||||
const macos = @import("macos");
|
const macos = @import("macos");
|
||||||
const imgui = @import("imgui");
|
const imgui = @import("imgui");
|
||||||
|
const apprt = @import("../apprt.zig");
|
||||||
const font = @import("../font/main.zig");
|
const font = @import("../font/main.zig");
|
||||||
const terminal = @import("../terminal/main.zig");
|
const terminal = @import("../terminal/main.zig");
|
||||||
const renderer = @import("../renderer.zig");
|
const renderer = @import("../renderer.zig");
|
||||||
@ -124,7 +125,7 @@ const GPUCellMode = enum(u8) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// Returns the hints that we want for this
|
/// Returns the hints that we want for this
|
||||||
pub fn windowHints() glfw.Window.Hints {
|
pub fn glfwWindowHints() glfw.Window.Hints {
|
||||||
return .{
|
return .{
|
||||||
.client_api = .no_api,
|
.client_api = .no_api,
|
||||||
// .cocoa_graphics_switching = builtin.os.tag == .macos,
|
// .cocoa_graphics_switching = builtin.os.tag == .macos,
|
||||||
@ -134,8 +135,8 @@ pub fn windowHints() glfw.Window.Hints {
|
|||||||
|
|
||||||
/// This is called early right after window creation to setup our
|
/// This is called early right after window creation to setup our
|
||||||
/// window surface as necessary.
|
/// window surface as necessary.
|
||||||
pub fn windowInit(window: glfw.Window) !void {
|
pub fn windowInit(win: apprt.runtime.Window) !void {
|
||||||
_ = window;
|
_ = win;
|
||||||
|
|
||||||
// We don't do anything else here because we want to set everything
|
// We don't do anything else here because we want to set everything
|
||||||
// else up during actual initialization.
|
// else up during actual initialization.
|
||||||
@ -303,9 +304,12 @@ pub fn deinit(self: *Metal) void {
|
|||||||
|
|
||||||
/// This is called just prior to spinning up the renderer thread for
|
/// This is called just prior to spinning up the renderer thread for
|
||||||
/// final main thread setup requirements.
|
/// final main thread setup requirements.
|
||||||
pub fn finalizeWindowInit(self: *const Metal, window: glfw.Window) !void {
|
pub fn finalizeWindowInit(self: *const Metal, win: apprt.runtime.Window) !void {
|
||||||
// Set our window backing layer to be our swapchain
|
// Set our window backing layer to be our swapchain
|
||||||
const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(window).?);
|
const nswindow = switch (apprt.runtime) {
|
||||||
|
apprt.glfw => objc.Object.fromId(glfwNative.getCocoaWindow(win.window).?),
|
||||||
|
else => @compileError("unsupported apprt for metal"),
|
||||||
|
};
|
||||||
const contentView = objc.Object.fromId(nswindow.getProperty(?*anyopaque, "contentView").?);
|
const contentView = objc.Object.fromId(nswindow.getProperty(?*anyopaque, "contentView").?);
|
||||||
contentView.setProperty("layer", self.swapchain.value);
|
contentView.setProperty("layer", self.swapchain.value);
|
||||||
contentView.setProperty("wantsLayer", true);
|
contentView.setProperty("wantsLayer", true);
|
||||||
@ -319,11 +323,11 @@ pub fn finalizeWindowInit(self: *const Metal, window: glfw.Window) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// This is called if this renderer runs DevMode.
|
/// This is called if this renderer runs DevMode.
|
||||||
pub fn initDevMode(self: *const Metal, window: glfw.Window) !void {
|
pub fn initDevMode(self: *const Metal, win: apprt.runtime.Window) !void {
|
||||||
if (DevMode.enabled) {
|
if (DevMode.enabled) {
|
||||||
// Initialize for our window
|
// Initialize for our window
|
||||||
assert(imgui.ImplGlfw.initForOther(
|
assert(imgui.ImplGlfw.initForOther(
|
||||||
@ptrCast(*imgui.ImplGlfw.GLFWWindow, window.handle),
|
@ptrCast(*imgui.ImplGlfw.GLFWWindow, win.window.handle),
|
||||||
true,
|
true,
|
||||||
));
|
));
|
||||||
assert(imgui.ImplMetal.init(self.device.value));
|
assert(imgui.ImplMetal.init(self.device.value));
|
||||||
@ -341,9 +345,9 @@ pub fn deinitDevMode(self: *const Metal) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Callback called by renderer.Thread when it begins.
|
/// Callback called by renderer.Thread when it begins.
|
||||||
pub fn threadEnter(self: *const Metal, window: glfw.Window) !void {
|
pub fn threadEnter(self: *const Metal, win: apprt.runtime.Window) !void {
|
||||||
_ = self;
|
_ = self;
|
||||||
_ = window;
|
_ = win;
|
||||||
|
|
||||||
// Metal requires no per-thread state.
|
// Metal requires no per-thread state.
|
||||||
}
|
}
|
||||||
@ -425,10 +429,10 @@ pub fn setFontSize(self: *Metal, size: font.face.DesiredSize) !void {
|
|||||||
/// The primary render callback that is completely thread-safe.
|
/// The primary render callback that is completely thread-safe.
|
||||||
pub fn render(
|
pub fn render(
|
||||||
self: *Metal,
|
self: *Metal,
|
||||||
window: glfw.Window,
|
win: apprt.runtime.Window,
|
||||||
state: *renderer.State,
|
state: *renderer.State,
|
||||||
) !void {
|
) !void {
|
||||||
_ = window;
|
_ = win;
|
||||||
|
|
||||||
// Data we extract out of the critical area.
|
// Data we extract out of the critical area.
|
||||||
const Critical = struct {
|
const Critical = struct {
|
||||||
|
@ -7,6 +7,7 @@ const glfw = @import("glfw");
|
|||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
const apprt = @import("../apprt.zig");
|
||||||
const font = @import("../font/main.zig");
|
const font = @import("../font/main.zig");
|
||||||
const imgui = @import("imgui");
|
const imgui = @import("imgui");
|
||||||
const renderer = @import("../renderer.zig");
|
const renderer = @import("../renderer.zig");
|
||||||
@ -350,7 +351,7 @@ fn resetCellsLRU(self: *OpenGL) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the hints that we want for this
|
/// Returns the hints that we want for this
|
||||||
pub fn windowHints() glfw.Window.Hints {
|
pub fn glfwWindowHints() glfw.Window.Hints {
|
||||||
return .{
|
return .{
|
||||||
.context_version_major = 3,
|
.context_version_major = 3,
|
||||||
.context_version_minor = 3,
|
.context_version_minor = 3,
|
||||||
@ -363,10 +364,10 @@ pub fn windowHints() glfw.Window.Hints {
|
|||||||
|
|
||||||
/// This is called early right after window creation to setup our
|
/// This is called early right after window creation to setup our
|
||||||
/// window surface as necessary.
|
/// window surface as necessary.
|
||||||
pub fn windowInit(window: glfw.Window) !void {
|
pub fn windowInit(win: apprt.runtime.Window) !void {
|
||||||
// Treat this like a thread entry
|
// Treat this like a thread entry
|
||||||
const self: OpenGL = undefined;
|
const self: OpenGL = undefined;
|
||||||
try self.threadEnter(window);
|
try self.threadEnter(win);
|
||||||
|
|
||||||
// Blending for text
|
// Blending for text
|
||||||
try gl.enable(gl.c.GL_BLEND);
|
try gl.enable(gl.c.GL_BLEND);
|
||||||
@ -380,40 +381,23 @@ pub fn windowInit(window: glfw.Window) !void {
|
|||||||
// log.debug("OpenGL extension available name={s}", .{ext});
|
// log.debug("OpenGL extension available name={s}", .{ext});
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
if (builtin.mode == .Debug) {
|
|
||||||
// Get our physical DPI - debug only because we don't have a use for
|
|
||||||
// this but the logging of it may be useful
|
|
||||||
const monitor = window.getMonitor() orelse monitor: {
|
|
||||||
log.warn("window had null monitor, getting primary monitor", .{});
|
|
||||||
break :monitor glfw.Monitor.getPrimary().?;
|
|
||||||
};
|
|
||||||
const physical_size = monitor.getPhysicalSize();
|
|
||||||
const video_mode = try monitor.getVideoMode();
|
|
||||||
const physical_x_dpi = @intToFloat(f32, video_mode.getWidth()) / (@intToFloat(f32, physical_size.width_mm) / 25.4);
|
|
||||||
const physical_y_dpi = @intToFloat(f32, video_mode.getHeight()) / (@intToFloat(f32, physical_size.height_mm) / 25.4);
|
|
||||||
log.debug("physical dpi x={} y={}", .{
|
|
||||||
physical_x_dpi,
|
|
||||||
physical_y_dpi,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This is called just prior to spinning up the renderer thread for
|
/// This is called just prior to spinning up the renderer thread for
|
||||||
/// final main thread setup requirements.
|
/// final main thread setup requirements.
|
||||||
pub fn finalizeWindowInit(self: *const OpenGL, window: glfw.Window) !void {
|
pub fn finalizeWindowInit(self: *const OpenGL, win: apprt.runtime.Window) !void {
|
||||||
_ = self;
|
_ = self;
|
||||||
_ = window;
|
_ = win;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This is called if this renderer runs DevMode.
|
/// This is called if this renderer runs DevMode.
|
||||||
pub fn initDevMode(self: *const OpenGL, window: glfw.Window) !void {
|
pub fn initDevMode(self: *const OpenGL, win: apprt.runtime.Window) !void {
|
||||||
_ = self;
|
_ = self;
|
||||||
|
|
||||||
if (DevMode.enabled) {
|
if (DevMode.enabled) {
|
||||||
// Initialize for our window
|
// Initialize for our window
|
||||||
assert(imgui.ImplGlfw.initForOpenGL(
|
assert(imgui.ImplGlfw.initForOpenGL(
|
||||||
@ptrCast(*imgui.ImplGlfw.GLFWWindow, window.handle),
|
@ptrCast(*imgui.ImplGlfw.GLFWWindow, win.window.handle),
|
||||||
true,
|
true,
|
||||||
));
|
));
|
||||||
assert(imgui.ImplOpenGL3.init("#version 330 core"));
|
assert(imgui.ImplOpenGL3.init("#version 330 core"));
|
||||||
@ -431,7 +415,7 @@ pub fn deinitDevMode(self: *const OpenGL) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Callback called by renderer.Thread when it begins.
|
/// Callback called by renderer.Thread when it begins.
|
||||||
pub fn threadEnter(self: *const OpenGL, window: glfw.Window) !void {
|
pub fn threadEnter(self: *const OpenGL, win: apprt.runtime.Window) !void {
|
||||||
_ = self;
|
_ = self;
|
||||||
|
|
||||||
// We need to make the OpenGL context current. OpenGL requires
|
// We need to make the OpenGL context current. OpenGL requires
|
||||||
@ -439,7 +423,7 @@ pub fn threadEnter(self: *const OpenGL, window: glfw.Window) !void {
|
|||||||
// ensures that the context switches over to our thread. Important:
|
// ensures that the context switches over to our thread. Important:
|
||||||
// the prior thread MUST have detached the context prior to calling
|
// the prior thread MUST have detached the context prior to calling
|
||||||
// this entrypoint.
|
// this entrypoint.
|
||||||
try glfw.makeContextCurrent(window);
|
try glfw.makeContextCurrent(win.window);
|
||||||
errdefer glfw.makeContextCurrent(null) catch |err|
|
errdefer glfw.makeContextCurrent(null) catch |err|
|
||||||
log.warn("failed to cleanup OpenGL context err={}", .{err});
|
log.warn("failed to cleanup OpenGL context err={}", .{err});
|
||||||
try glfw.swapInterval(1);
|
try glfw.swapInterval(1);
|
||||||
@ -541,7 +525,7 @@ fn resetFontMetrics(
|
|||||||
/// The primary render callback that is completely thread-safe.
|
/// The primary render callback that is completely thread-safe.
|
||||||
pub fn render(
|
pub fn render(
|
||||||
self: *OpenGL,
|
self: *OpenGL,
|
||||||
window: glfw.Window,
|
win: apprt.runtime.Window,
|
||||||
state: *renderer.State,
|
state: *renderer.State,
|
||||||
) !void {
|
) !void {
|
||||||
// Data we extract out of the critical area.
|
// Data we extract out of the critical area.
|
||||||
@ -657,7 +641,7 @@ pub fn render(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Swap our window buffers
|
// Swap our window buffers
|
||||||
try window.swapBuffers();
|
try win.window.swapBuffers();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// rebuildCells rebuilds all the GPU cells from our CPU state. This is a
|
/// rebuildCells rebuilds all the GPU cells from our CPU state. This is a
|
||||||
|
@ -4,9 +4,9 @@ pub const Thread = @This();
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
const glfw = @import("glfw");
|
|
||||||
const libuv = @import("libuv");
|
const libuv = @import("libuv");
|
||||||
const renderer = @import("../renderer.zig");
|
const renderer = @import("../renderer.zig");
|
||||||
|
const apprt = @import("../apprt.zig");
|
||||||
const BlockingQueue = @import("../blocking_queue.zig").BlockingQueue;
|
const BlockingQueue = @import("../blocking_queue.zig").BlockingQueue;
|
||||||
const tracy = @import("tracy");
|
const tracy = @import("tracy");
|
||||||
const trace = tracy.trace;
|
const trace = tracy.trace;
|
||||||
@ -37,8 +37,8 @@ render_h: libuv.Timer,
|
|||||||
/// The timer used for cursor blinking
|
/// The timer used for cursor blinking
|
||||||
cursor_h: libuv.Timer,
|
cursor_h: libuv.Timer,
|
||||||
|
|
||||||
/// The windo we're rendering to.
|
/// The window we're rendering to.
|
||||||
window: glfw.Window,
|
window: apprt.runtime.Window,
|
||||||
|
|
||||||
/// The underlying renderer implementation.
|
/// The underlying renderer implementation.
|
||||||
renderer: *renderer.Renderer,
|
renderer: *renderer.Renderer,
|
||||||
@ -55,7 +55,7 @@ mailbox: *Mailbox,
|
|||||||
/// is up to the caller to start the thread with the threadMain entrypoint.
|
/// is up to the caller to start the thread with the threadMain entrypoint.
|
||||||
pub fn init(
|
pub fn init(
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
window: glfw.Window,
|
win: apprt.runtime.Window,
|
||||||
renderer_impl: *renderer.Renderer,
|
renderer_impl: *renderer.Renderer,
|
||||||
state: *renderer.State,
|
state: *renderer.State,
|
||||||
) !Thread {
|
) !Thread {
|
||||||
@ -120,7 +120,7 @@ pub fn init(
|
|||||||
.stop = stop_h,
|
.stop = stop_h,
|
||||||
.render_h = render_h,
|
.render_h = render_h,
|
||||||
.cursor_h = cursor_timer,
|
.cursor_h = cursor_timer,
|
||||||
.window = window,
|
.window = win,
|
||||||
.renderer = renderer_impl,
|
.renderer = renderer_impl,
|
||||||
.state = state,
|
.state = state,
|
||||||
.mailbox = mailbox,
|
.mailbox = mailbox,
|
||||||
|
Reference in New Issue
Block a user