core: report cursor postion to the app runtimes

This commit is contained in:
Jeffrey C. Ollie
2025-01-18 15:50:37 -06:00
parent 72d085525b
commit e469699278
10 changed files with 133 additions and 0 deletions

View File

@ -597,6 +597,7 @@ typedef enum {
GHOSTTY_ACTION_COLOR_CHANGE,
GHOSTTY_ACTION_RELOAD_CONFIG,
GHOSTTY_ACTION_CONFIG_CHANGE,
GHOSTTY_ACTION_REPORT_CURSOR_POSITION,
} ghostty_action_tag_e;
typedef union {

View File

@ -71,6 +71,10 @@ renderer_thread: renderer.Thread,
/// The actual thread
renderer_thr: std.Thread,
/// Cursor state. Updated at most once per frame so it may not be accurate 100%
/// of the time.
cursor: Cursor,
/// Mouse state.
mouse: Mouse,
@ -150,6 +154,37 @@ pub const InputEffect = enum {
closed,
};
/// Cursor state for the surface.
const Cursor = struct {
x: terminal.size.CellCountInt = 0,
y: terminal.size.CellCountInt = 0,
pub fn reportCursorPosition(self: *Cursor, x: terminal.size.CellCountInt, y: terminal.size.CellCountInt) void {
const surface: *Surface = @alignCast(@fieldParentPtr("cursor", self));
// Defer updating the stored cursor position until after the apprt has been
// informed of the new position.
defer {
self.x = x;
self.y = y;
}
surface.rt_app.performAction(
.{ .surface = surface },
.report_cursor_position,
.{
.x = x,
.y = y,
},
) catch |err| {
log.warn(
"failed to notify surface of cursor position err={}",
.{err},
);
};
}
};
/// Mouse state for the surface.
const Mouse = struct {
/// The last tracked mouse button state by button.
@ -496,6 +531,7 @@ pub fn init(
.terminal = &self.io.terminal,
},
.renderer_thr = undefined,
.cursor = .{},
.mouse = .{},
.keyboard = .{},
.io = undefined,
@ -943,6 +979,8 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
.present_surface => try self.presentSurface(),
.password_input => |v| try self.passwordInput(v),
.report_cursor_position => |v| self.cursor.reportCursorPosition(v.x, v.y),
}
}

View File

@ -226,6 +226,10 @@ pub const Action = union(Key) {
/// for changes.
config_change: ConfigChange,
/// Report the location of the cursor. This will happen at most once per
/// frame, and only if the cursor has moved since the last update.
report_cursor_position: CursorPositionReport,
/// Sync with: ghostty_action_tag_e
pub const Key = enum(c_int) {
quit,
@ -266,6 +270,7 @@ pub const Action = union(Key) {
color_change,
reload_config,
config_change,
report_cursor_position,
};
/// Sync with: ghostty_action_u
@ -549,3 +554,9 @@ pub const ConfigChange = struct {
};
}
};
/// Used to report the location of the cursor.
pub const CursorPositionReport = extern struct {
x: terminal.size.CellCountInt,
y: terminal.size.CellCountInt,
};

View File

@ -238,6 +238,7 @@ pub const App = struct {
.pwd,
.config_change,
.toggle_maximize,
.report_cursor_position,
=> log.info("unimplemented action={}", .{action}),
}
}

View File

@ -535,6 +535,7 @@ pub fn performAction(
.toggle_split_zoom => self.toggleSplitZoom(target),
.toggle_window_decorations => self.toggleWindowDecorations(target),
.quit_timer => self.quitTimer(value),
.report_cursor_position => self.reportCursorPosition(target, value),
// Unimplemented
.close_all_windows,
@ -1898,6 +1899,17 @@ pub fn refreshContextMenu(_: *App, window: ?*c.GtkWindow, has_selection: bool) v
c.g_simple_action_set_enabled(action, if (has_selection) 1 else 0);
}
fn reportCursorPosition(
_: *const App,
target: apprt.Target,
value: apprt.action.CursorPositionReport,
) void {
switch (target) {
.app => {},
.surface => |v| v.rt_surface.reportCursorPosition(value),
}
}
fn isValidAppId(app_id: [:0]const u8) bool {
if (app_id.len > 255 or app_id.len == 0) return false;
if (app_id[0] == '.') return false;

View File

@ -2189,3 +2189,8 @@ fn g_value_holds(value_: ?*c.GValue, g_type: c.GType) bool {
}
return false;
}
pub fn reportCursorPosition(self: *Surface, position: apprt.action.CursorPositionReport) void {
_ = self;
log.debug("cursor position: {d}×{d}", .{ position.x, position.y });
}

View File

@ -81,6 +81,12 @@ pub const Message = union(enum) {
/// The terminal has reported a change in the working directory.
pwd_change: WriteReq,
/// Report cursor position
report_cursor_position: struct {
x: terminal.size.CellCountInt,
y: terminal.size.CellCountInt,
},
pub const ReportTitleStyle = enum {
csi_21_t,

View File

@ -25,6 +25,7 @@ const graphics = macos.graphics;
const fgMode = @import("cell.zig").fgMode;
const isCovering = @import("cell.zig").isCovering;
const shadertoy = @import("shadertoy.zig");
const CursorPosition = @import("cursor_position.zig").CursorPosition;
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
@ -161,6 +162,9 @@ health: std.atomic.Value(Health) = .{ .raw = .healthy },
/// Our GPU state
gpu_state: GPUState,
/// The cursor position as of the last frame update
cursor_position: CursorPosition(Metal) = .{},
/// State we need for the GPU that is shared between all frames.
pub const GPUState = struct {
// The count of buffers we use for double/triple buffering. If
@ -991,6 +995,10 @@ pub fn updateFrame(
cursor_style: ?renderer.CursorStyle,
color_palette: terminal.color.Palette,
viewport_pin: terminal.Pin,
cursor_position: struct {
x: terminal.size.CellCountInt,
y: terminal.size.CellCountInt,
},
/// If true, rebuild the full screen.
full_rebuild: bool,
@ -1154,6 +1162,10 @@ pub fn updateFrame(
.color_palette = state.terminal.color_palette.colors,
.viewport_pin = viewport_pin,
.full_rebuild = full_rebuild,
.cursor_position = .{
.x = state.terminal.screen.cursor.x,
.y = state.terminal.screen.cursor.y,
},
};
};
defer {
@ -1240,6 +1252,8 @@ pub fn updateFrame(
}
}
}
self.cursor_position.update(critical.cursor_position.x, critical.cursor_position.y);
}
/// Draw the frame to the screen.

View File

@ -30,6 +30,7 @@ const custom = @import("opengl/custom.zig");
const Image = gl_image.Image;
const ImageMap = gl_image.ImageMap;
const ImagePlacementList = std.ArrayListUnmanaged(gl_image.Placement);
const CursorPosition = @import("cursor_position.zig").CursorPosition;
const log = std.log.scoped(.grid);
@ -146,6 +147,9 @@ image_bg_end: u32 = 0,
image_text_end: u32 = 0,
image_virtual: bool = false,
/// The cursor position as of the last frame update.
cursor_position: CursorPosition(OpenGL) = .{},
/// Deferred OpenGL operation to update the screen size.
const SetScreenSize = struct {
size: renderer.Size,
@ -700,6 +704,10 @@ pub fn updateFrame(
screen_type: terminal.ScreenType,
mouse: renderer.State.Mouse,
preedit: ?renderer.State.Preedit,
cursor_position: struct {
x: terminal.size.CellCountInt,
y: terminal.size.CellCountInt,
},
cursor_style: ?renderer.CursorStyle,
color_palette: terminal.color.Palette,
};
@ -861,6 +869,10 @@ pub fn updateFrame(
.screen_type = state.terminal.active_screen,
.mouse = state.mouse,
.preedit = preedit,
.cursor_position = .{
.x = state.terminal.screen.cursor.x,
.y = state.terminal.screen.cursor.y,
},
.cursor_style = cursor_style,
.color_palette = state.terminal.color_palette.colors,
};
@ -893,6 +905,8 @@ pub fn updateFrame(
// CoreText this triggers off-thread cleanup logic.
self.font_shaper.endFrame();
}
self.cursor_position.update(critical.cursor_position.x, critical.cursor_position.y);
}
/// This goes through the Kitty graphic placements and accumulates the

View File

@ -0,0 +1,31 @@
const terminal = @import("../terminal/main.zig");
pub fn CursorPosition(comptime T: type) type {
return struct {
x: terminal.size.CellCountInt = 0,
y: terminal.size.CellCountInt = 0,
pub fn update(
self: *CursorPosition(T),
x: terminal.size.CellCountInt,
y: terminal.size.CellCountInt,
) void {
const renderer: *T = @alignCast(@fieldParentPtr("cursor_position", self));
if (self.x != x or self.y != y) {
_ = renderer.surface_mailbox.push(
.{
.report_cursor_position = .{
.x = x,
.y = y,
},
},
.{ .instant = {} },
);
}
self.x = x;
self.y = y;
}
};
}