From 26735f0e052b1ebfbaad9b7b0bda1207103b7ac8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 29 Jun 2024 11:23:14 -0500 Subject: [PATCH] apprt: convert selection info to a single struct and C API --- include/ghostty.h | 10 +- .../Sources/Ghostty/SurfaceView_AppKit.swift | 15 ++- src/Surface.zig | 108 ++++++++---------- src/apprt/embedded.zig | 49 ++++---- src/apprt/structs.zig | 15 +++ 5 files changed, 102 insertions(+), 95 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 1deea1864..e3a9e4223 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -373,6 +373,13 @@ typedef struct { const char* message; } ghostty_error_s; +typedef struct { + double tl_px_x; + double tl_px_y; + uint32_t offset_start; + uint32_t offset_len; +} ghostty_selection_s; + typedef struct { void* nsview; } ghostty_platform_macos_s; @@ -557,8 +564,7 @@ uintptr_t ghostty_surface_selection(ghostty_surface_t, char*, uintptr_t); #ifdef __APPLE__ void ghostty_surface_set_display_id(ghostty_surface_t, uint32_t); void* ghostty_surface_quicklook_font(ghostty_surface_t); -void ghostty_surface_selection_range(ghostty_surface_t, uint32_t*, uint32_t*); -void ghostty_surface_selection_point(ghostty_surface_t, double*, double*); +bool ghostty_surface_selection_info(ghostty_surface_t, ghostty_selection_s*); #endif ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t); diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index a95538729..9a28dc5cf 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -935,10 +935,9 @@ extension Ghostty.SurfaceView: NSTextInputClient { // Get our range from the Ghostty API. There is a race condition between getting the // range and actually using it since our selection may change but there isn't a good // way I can think of to solve this for AppKit. - var start: UInt32 = 0; - var len: UInt32 = 0; - ghostty_surface_selection_range(surface, &start, &len); - return NSRange(location: Int(start), length: Int(len)) + var sel: ghostty_selection_s = ghostty_selection_s(); + guard ghostty_surface_selection_info(surface, &sel) else { return NSRange() } + return NSRange(location: Int(sel.offset_start), length: Int(sel.offset_len)) } func setMarkedText(_ string: Any, selectedRange: NSRange, replacementRange: NSRange) { @@ -1017,7 +1016,13 @@ extension Ghostty.SurfaceView: NSTextInputClient { // point right now. I'm sure I'm missing something fundamental... if range.length > 0 && range != self.selectedRange() { // QuickLook - ghostty_surface_selection_point(surface, &x, &y) + var sel: ghostty_selection_s = ghostty_selection_s(); + if ghostty_surface_selection_info(surface, &sel) { + x = sel.tl_px_x; + y = sel.tl_px_y; + } else { + ghostty_surface_ime_point(surface, &x, &y) + } } else { ghostty_surface_ime_point(surface, &x, &y) } diff --git a/src/Surface.zig b/src/Surface.zig index c0c011936..a4557be78 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -849,26 +849,23 @@ pub fn selectionString(self: *Surface, alloc: Allocator) !?[]const u8 { }); } -/// This returns the selection range offset from the beginning of the -/// viewport. If the selection is not entirely within the viewport then -/// this will return null. +/// Return the apprt selection metadata used by apprt's for implementing +/// things like contextual information on right click and so on. /// -/// This is a oddly specific function that is used with macOS to enable -/// NSTextInputClient to work properly for features such as the IME Emoji -/// keyboard and QuickLook amongst other things. -pub fn selectionRange(self: *Surface) ?struct { - start: u32, - len: u32, -} { +/// This only returns non-null if the selection is fully contained within +/// the viewport. The use case for this function at the time of authoring +/// it is for apprt's to implement right-click contextual menus and +/// those only make sense for selections fully contained within the +/// viewport. We don't handle the case where you right click a word-wrapped +/// word at the end of the viewport yet. +pub fn selectionInfo(self: *const Surface) ?apprt.Selection { self.renderer_state.mutex.lock(); defer self.renderer_state.mutex.unlock(); - - // Get the TL/BR pins for the selection const sel = self.io.terminal.screen.selection orelse return null; + + // Get the TL/BR pins for the selection and convert to viewport. const tl = sel.topLeft(&self.io.terminal.screen); const br = sel.bottomRight(&self.io.terminal.screen); - - // Convert the pins to coordinates (x,y) const tl_pt = self.io.terminal.screen.pages.pointFromPin(.viewport, tl) orelse return null; const br_pt = self.io.terminal.screen.pages.pointFromPin(.viewport, br) orelse return null; const tl_coord = tl_pt.coord(); @@ -878,7 +875,41 @@ pub fn selectionRange(self: *Surface) ?struct { const start = tl_coord.y * self.io.terminal.screen.pages.cols + tl_coord.x; const end = br_coord.y * self.io.terminal.screen.pages.cols + br_coord.x; - return .{ .start = start, .len = end - start }; + // Our sizes are all scaled so we need to send the unscaled values back. + const content_scale = self.rt_surface.getContentScale() catch .{ .x = 1, .y = 1 }; + + const x: f64 = x: { + // Simple x * cell width gives the top-left corner + var x: f64 = @floatFromInt(tl_coord.x * self.cell_size.width); + + // We want the midpoint + x += @as(f64, @floatFromInt(self.cell_size.width)) / 2; + + // And scale it + x /= content_scale.x; + + break :x x; + }; + + const y: f64 = y: { + // Simple x * cell width gives the top-left corner + var y: f64 = @floatFromInt(tl_coord.y * self.cell_size.height); + + // We want the bottom + y += @floatFromInt(self.cell_size.height); + + // And scale it + y /= content_scale.y; + + break :y y; + }; + + return .{ + .tl_x_px = x, + .tl_y_px = y, + .offset_start = start, + .offset_len = end - start, + }; } /// Returns the pwd of the terminal, if any. This is always copied because @@ -933,53 +964,6 @@ pub fn imePoint(self: *const Surface) apprt.IMEPos { return .{ .x = x, .y = y }; } -/// Returns the x/y coordinate of where the selection top-left is. This is -/// used currently only by macOS to render the QuickLook highlight in the -/// proper location. -pub fn selectionPoint(self: *const Surface) ?apprt.IMEPos { - self.renderer_state.mutex.lock(); - defer self.renderer_state.mutex.unlock(); - - // Get the top-left coordinate of the selection in the viewport. - const sel = self.io.terminal.screen.selection orelse return null; - const tl_pt = self.io.terminal.screen.pages.pointFromPin( - .viewport, - sel.topLeft(&self.io.terminal.screen), - ) orelse return null; - const tl_coord = tl_pt.coord(); - - // Our sizes are all scaled so we need to send the unscaled values back. - const content_scale = self.rt_surface.getContentScale() catch .{ .x = 1, .y = 1 }; - - const x: f64 = x: { - // Simple x * cell width gives the top-left corner - var x: f64 = @floatFromInt(tl_coord.x * self.cell_size.width); - - // We want the midpoint - x += @as(f64, @floatFromInt(self.cell_size.width)) / 2; - - // And scale it - x /= content_scale.x; - - break :x x; - }; - - const y: f64 = y: { - // Simple x * cell width gives the top-left corner - var y: f64 = @floatFromInt(tl_coord.y * self.cell_size.height); - - // We want the bottom - y += @floatFromInt(self.cell_size.height); - - // And scale it - y /= content_scale.y; - - break :y y; - }; - - return .{ .x = x, .y = y }; -} - fn clipboardWrite(self: *const Surface, data: []const u8, loc: apprt.Clipboard) !void { if (self.config.clipboard_write == .deny) { log.info("application attempted to write clipboard, but 'clipboard-write' is set to deny", .{}); diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 98a96345b..34a82046e 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1376,6 +1376,13 @@ pub const CAPI = struct { } }; + const Selection = extern struct { + tl_x_px: f64, + tl_y_px: f64, + offset_start: u32, + offset_len: u32, + }; + /// Create a new app. export fn ghostty_app_new( opts: *const apprt.runtime.App.Options, @@ -1803,34 +1810,24 @@ pub const CAPI = struct { return copy; } - /// This returns the start and length of the current selection range - /// in viewport coordinates. If the selection is not visible in the - /// viewport completely then this will return 0,0. This rather odd - /// detail is due to the current usage of this in the macOS app where - /// selections are only meaningful if they're in the viewport. We can - /// change this behavior if we have something useful to do with the - /// selection range outside of the viewport in the future. - export fn ghostty_surface_selection_range( + /// This returns the selection metadata for the current selection. + /// This will return false if there is no selection or the + /// selection is not fully contained in the viewport (since the + /// metadata is all about that). + export fn ghostty_surface_selection_info( ptr: *Surface, - start: *u32, - len: *u32, - ) void { - start.* = 0; - len.* = 0; + info: *Selection, + ) bool { + const sel = ptr.core_surface.selectionInfo() orelse + return false; - const range = ptr.core_surface.selectionRange() orelse return; - start.* = range.start; - len.* = range.len; - } - - export fn ghostty_surface_selection_point( - ptr: *Surface, - x: *f64, - y: *f64, - ) void { - const point = ptr.core_surface.selectionPoint() orelse return; - x.* = point.x; - y.* = point.y; + info.* = .{ + .tl_x_px = sel.tl_x_px, + .tl_y_px = sel.tl_y_px, + .offset_start = sel.offset_start, + .offset_len = sel.offset_len, + }; + return true; } export fn ghostty_inspector_metal_init(ptr: *Inspector, device: objc.c.id) bool { diff --git a/src/apprt/structs.zig b/src/apprt/structs.zig index 2a4583103..1982cc497 100644 --- a/src/apprt/structs.zig +++ b/src/apprt/structs.zig @@ -74,3 +74,18 @@ pub const ColorScheme = enum(u2) { light = 0, dark = 1, }; + +/// Selection information +pub const Selection = struct { + /// Top-left point of the selection in the viewport in scaled + /// window pixels. (0,0) is the top-left of the window. + tl_x_px: f64, + tl_y_px: f64, + + /// The offset of the selection start in cells from the top-left + /// of the viewport. + /// + /// This is a strange metric but its used by macOS. + offset_start: u32, + offset_len: u32, +};