From d5f27245d4784e5c4897e59ef99b47dc34a5b3fe Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 18 Jun 2024 17:43:39 -0400 Subject: [PATCH] macos: hacky API to get a CTFont for QuickLook --- include/ghostty.h | 1 + .../Sources/Ghostty/SurfaceView_AppKit.swift | 37 ++++++++++++++++++- src/apprt/embedded.zig | 32 ++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index d49d02386..8aef893c9 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -556,6 +556,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); #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 ed8d52c43..2abf20d9c 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -1,4 +1,5 @@ import SwiftUI +import CoreText import UserNotifications import GhosttyKit @@ -929,7 +930,13 @@ extension Ghostty.SurfaceView: NSTextInputClient { } func selectedRange() -> NSRange { - return NSRange() + guard let surface = self.surface else { return NSRange() } + guard ghostty_surface_has_selection(surface) else { return NSRange() } + + // If we have a selection, we just return a non-empty range. The actual + // values are meaningless but the non-emptiness of it tells AppKit we + // have a selection. + return NSRange(location: 0, length: 1) } func setMarkedText(_ string: Any, selectedRange: NSRange, replacementRange: NSRange) { @@ -954,7 +961,33 @@ extension Ghostty.SurfaceView: NSTextInputClient { } func attributedSubstring(forProposedRange range: NSRange, actualRange: NSRangePointer?) -> NSAttributedString? { - return nil + // We ignore the proposed range and always return the selection from + // this (if we have one). This enables features like QuickLook. I don't + // know if this breaks anything else... + guard let surface = self.surface else { return nil } + guard ghostty_surface_has_selection(surface) else { return nil } + + // Get our selection. We cap it at 1MB for the purpose of this. This is + // arbitrary. If this is a good reason to increase it I'm happy to. + let v = String(unsafeUninitializedCapacity: 1000000) { + Int(ghostty_surface_selection(surface, $0.baseAddress, UInt($0.count))) + } + + // If we can get a font then we use the font. This should always work + // since we always have a primary font. The only scenario this doesn't + // work is if someone is using a non-CoreText build which would be + // unofficial. + var attributes: [ NSAttributedString.Key : Any ] = [:]; + if let fontRaw = ghostty_surface_quicklook_font(surface) { + // Memory management here is wonky: ghostty_surface_quicklook_font + // will create a copy of a CTFont, Swift will auto-retain the + // unretained value passed into the dict, so we release the original. + let font = Unmanaged.fromOpaque(fontRaw) + attributes[.font] = font.takeUnretainedValue() + font.release() + } + + return .init(string: v, attributes: attributes) } func characterIndex(for point: NSPoint) -> Int { diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 6fbd8ef14..392c3a90b 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -10,6 +10,7 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const objc = @import("objc"); const apprt = @import("../apprt.zig"); +const font = @import("../font/main.zig"); const input = @import("../input.zig"); const renderer = @import("../renderer.zig"); const terminal = @import("../terminal/main.zig"); @@ -1771,6 +1772,37 @@ pub const CAPI = struct { surface.renderer_thread.wakeup.notify() catch {}; } + /// This returns a CTFontRef that should be used for quicklook + /// highlighted text. This is always the primary font in use + /// regardless of the selected text. If coretext is not in use + /// then this will return nothing. + export fn ghostty_surface_quicklook_font(ptr: *Surface) ?*anyopaque { + // For non-CoreText we just return null. + if (comptime font.options.backend != .coretext) { + return null; + } + + // Get the shared font grid. We acquire a read lock to + // read the font face. It should not be deffered since + // we're loading the primary face. + const grid = ptr.core_surface.renderer.font_grid; + grid.lock.lockShared(); + defer grid.lock.unlockShared(); + + const collection = &grid.resolver.collection; + const face = collection.getFace(.{}) catch return null; + + // The font is not the right size by default so we need + // to set it to our configured window size. + const copy = face.font.copyWithAttributes( + ptr.app.config.@"font-size", + null, + null, + ) catch return null; + + return copy; + } + export fn ghostty_inspector_metal_init(ptr: *Inspector, device: objc.c.id) bool { return ptr.initMetal(objc.Object.fromId(device)); }