Files
Outpost/Vendor/swift-markdown-engine/Sources/MarkdownEngine/TextView/NativeTextView/InvertedIBeamCursor.swift
T
Puranjay Savar Mattas 6e1ffb96fd chore: vendor swift-markdown-engine as plain tracked source
Convert from git submodule to plain vendored copy. No push access to
upstream nodes-app/swift-markdown-engine meant our local fix commit
(05c1720) was stranded and unreachable from any remote on fresh
clones/CI. Vendoring as plain files folds it into normal repo history
instead.
2026-08-20 13:26:13 +01:00

99 lines
4.9 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// InvertedIBeamCursor.swift
// MarkdownEngine
//
// Created by Luca Chen on 30.07.26.
//
// The system I-beam is one appearance-independent glyph — a dark core inside a
// light halo — and macOS inverts it against the EDITOR's backdrop, not against
// the run under the pointer. Over a span that inverts (dark ink on a light
// block) both of the glyph's colors are then wrong: in a dark editor the
// inverted core is the block's own color and the pointer vanishes inside it.
//
// So recolor, don't redraw: the live `NSCursor.iBeam` image is re-tinted core →
// the span's ink, halo → the span's block. Deriving from the live image keeps
// the system's shape and the user's pointer size (Accessibility ▸ Pointer)
// instead of hand-drawing a cursor that ignores both.
//
import AppKit
enum InvertedIBeamCursor {
private static var cache: [Key: NSCursor] = [:]
private struct Key: Hashable {
let ink: String
let halo: String
}
/// I-beam whose core is `ink` and whose halo is `block`. Cached: recoloring
/// walks every representation's pixels (~65k across 1x10x), which is fine
/// once per color pair and far too slow per mouse-move.
static func cursor(ink: NSColor, block: NSColor) -> NSCursor? {
let key = Key(ink: descriptor(ink), halo: descriptor(block))
if let cached = cache[key] { return cached }
let system = NSCursor.iBeam
guard let recolored = recolor(system.image, ink: ink, block: block) else { return nil }
let cursor = NSCursor(image: recolored, hotSpot: system.hotSpot)
cache[key] = cursor
return cursor
}
/// Identity for the cache key — two dynamic colors that resolve the same
/// today must share an entry, so key on the resolved components.
private static func descriptor(_ color: NSColor) -> String {
guard let rgb = color.usingColorSpace(.deviceRGB) else { return color.description }
return "\(rgb.redComponent),\(rgb.greenComponent),\(rgb.blueComponent),\(rgb.alphaComponent)"
}
private static func recolor(_ image: NSImage, ink: NSColor, block: NSColor) -> NSImage? {
guard let inkRGB = ink.usingColorSpace(.deviceRGB),
let blockRGB = block.usingColorSpace(.deviceRGB) else { return nil }
let inkBytes = bytes(of: inkRGB), blockBytes = bytes(of: blockRGB)
let out = NSImage(size: image.size)
var added = false
for case let source as NSBitmapImageRep in image.representations {
let width = source.pixelsWide, height = source.pixelsHigh
// Redraw into a known format first (straight alpha, 8-bit RGBA) —
// the system reps' own layout is not guaranteed, and per-pixel
// NSColor round-trips cost 1.3 s across the 1x10x reps.
guard let target = NSBitmapImageRep(
bitmapDataPlanes: nil, pixelsWide: width, pixelsHigh: height,
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false,
colorSpaceName: .deviceRGB, bytesPerRow: width * 4, bitsPerPixel: 32
), let context = NSGraphicsContext(bitmapImageRep: target) else { continue }
target.size = source.size
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current = context
source.draw(in: NSRect(x: 0, y: 0, width: width, height: height))
NSGraphicsContext.restoreGraphicsState()
guard let data = target.bitmapData else { continue }
for pixel in stride(from: 0, to: width * height * 4, by: 4) {
// CoreGraphics only fills a PREMULTIPLIED buffer, so undo that
// before judging the color and redo it when writing back.
let alpha = Double(data[pixel + 3]) / 255
guard alpha > 0 else { continue } // transparent: leave it
// Split on the glyph's own luminance: the dark core carries the
// shape, the light halo carries it against a dark page.
let luminance = (0.299 * Double(data[pixel])
+ 0.587 * Double(data[pixel + 1])
+ 0.114 * Double(data[pixel + 2])) / (255 * alpha)
let replacement = luminance < 0.5 ? inkBytes : blockBytes
data[pixel] = UInt8(clamping: Int(Double(replacement.0) * alpha))
data[pixel + 1] = UInt8(clamping: Int(Double(replacement.1) * alpha))
data[pixel + 2] = UInt8(clamping: Int(Double(replacement.2) * alpha))
}
out.addRepresentation(target)
added = true
}
return added ? out : nil
}
private static func bytes(of color: NSColor) -> (UInt8, UInt8, UInt8) {
(UInt8(clamping: Int(color.redComponent * 255)),
UInt8(clamping: Int(color.greenComponent * 255)),
UInt8(clamping: Int(color.blueComponent * 255)))
}
}