|
| 1 | +// Two-arm test: |
| 2 | +// 1. Does AppKit's continuous spell-check pass WRITE .spellingState to the |
| 3 | +// NSTextLayoutManager on macOS 15.7.7? We read renderingAttributes back. |
| 4 | +// 2. If we MANUALLY write .spellingState via addRenderingAttribute, does |
| 5 | +// the underline appear? This is the viability test for EdgeMark's |
| 6 | +// Option A (manual driver) regardless of question 1. |
| 7 | +import AppKit |
| 8 | + |
| 9 | +let app = NSApplication.shared |
| 10 | +app.setActivationPolicy(.accessory) |
| 11 | + |
| 12 | +func analyze(_ rep: NSBitmapImageRep, label: String) { |
| 13 | + var red = 0 |
| 14 | + for y in 0..<rep.pixelsHigh { |
| 15 | + for x in 0..<rep.pixelsWide { |
| 16 | + guard let c = rep.colorAt(x: x, y: y)?.usingColorSpace(.sRGB) else { continue } |
| 17 | + let r = c.redComponent, g = c.greenComponent, b = c.blueComponent |
| 18 | + if r > 0.55 && g < 0.5 && b < 0.5 { red += 1 } |
| 19 | + } |
| 20 | + } |
| 21 | + print("\(label): red=\(red)") |
| 22 | +} |
| 23 | + |
| 24 | +func countSystemSpellingStates(_ tlm: NSTextLayoutManager, label: String) { |
| 25 | + let documentRange = tlm.documentRange |
| 26 | + var states: [(NSRange, Int)] = [] |
| 27 | + tlm.enumerateRenderingAttributes(from: documentRange.location, reverse: false) { (tlm2, attrs, range) -> Bool in |
| 28 | + if let s = attrs[.spellingState] as? Int { |
| 29 | + let loc = tlm2.offset(from: documentRange.location, to: range.location) |
| 30 | + let len = tlm2.offset(from: range.location, to: range.endLocation) |
| 31 | + let nsRange = NSRange(location: loc, length: len) |
| 32 | + states.append((nsRange, s)) |
| 33 | + } |
| 34 | + return true |
| 35 | + } |
| 36 | + print("\(label): system-wrote .spellingState entries = \(states.count)") |
| 37 | + for (r, v) in states { print(" range=\(r.location)+\(r.length) value=\(v)") } |
| 38 | +} |
| 39 | + |
| 40 | +func run(label: String, insertManual: Bool) { |
| 41 | + let storage = NSTextContentStorage() |
| 42 | + let tlm = NSTextLayoutManager() |
| 43 | + let container = NSTextContainer(size: CGSize(width: 400, height: 200)) |
| 44 | + tlm.textContainer = container |
| 45 | + storage.addTextLayoutManager(tlm) |
| 46 | + |
| 47 | + let tv = NSTextView(frame: NSRect(x: 0, y: 0, width: 400, height: 200), textContainer: container) |
| 48 | + tv.isEditable = true |
| 49 | + tv.isContinuousSpellCheckingEnabled = true |
| 50 | + tv.backgroundColor = .white |
| 51 | + tv.textColor = .black |
| 52 | + tv.font = NSFont.systemFont(ofSize: 16) |
| 53 | + |
| 54 | + let window = NSWindow(contentRect: NSRect(x: 120, y: 120, width: 400, height: 200), |
| 55 | + styleMask: [.titled], backing: .buffered, defer: false) |
| 56 | + window.contentView = tv |
| 57 | + window.makeKeyAndOrderFront(nil) |
| 58 | + window.makeFirstResponder(tv) |
| 59 | + tv.insertText("helllo wrold misspeled", replacementRange: NSRange(location: 0, length: 0)) |
| 60 | + |
| 61 | + // Give continuous spell-check time to run. |
| 62 | + RunLoop.current.run(until: Date().addingTimeInterval(3.0)) |
| 63 | + tv.needsDisplay = true |
| 64 | + tv.displayIfNeeded() |
| 65 | + RunLoop.current.run(until: Date().addingTimeInterval(0.5)) |
| 66 | + |
| 67 | + // Question 1: did the system write .spellingState? |
| 68 | + countSystemSpellingStates(tlm, label: "\(label) [after system pass]") |
| 69 | + |
| 70 | + // Question 2: manually write .spellingState on the first misspelled word. |
| 71 | + if insertManual { |
| 72 | + let docRange = tlm.documentRange |
| 73 | + // "helllo" is at offset 0, length 6. |
| 74 | + let endLoc = tlm.location(docRange.location, offsetBy: 6)! |
| 75 | + let manualRange = NSTextRange(location: docRange.location, end: endLoc)! |
| 76 | + tlm.addRenderingAttribute(.spellingState, value: 1, for: manualRange) |
| 77 | + print("\(label): manually wrote .spellingState:1 on 0..<6") |
| 78 | + } |
| 79 | + RunLoop.current.run(until: Date().addingTimeInterval(0.5)) |
| 80 | + tv.needsDisplay = true |
| 81 | + tv.displayIfNeeded() |
| 82 | + RunLoop.current.run(until: Date().addingTimeInterval(0.5)) |
| 83 | + |
| 84 | + guard let rep = tv.bitmapImageRepForCachingDisplay(in: tv.bounds) else { |
| 85 | + print("\(label): no bitmap") |
| 86 | + window.orderOut(nil) |
| 87 | + return |
| 88 | + } |
| 89 | + tv.cacheDisplay(in: tv.bounds, to: rep) |
| 90 | + analyze(rep, label: label) |
| 91 | + if let png = rep.representation(using: .png, properties: [:]) { |
| 92 | + try? png.write(to: URL(fileURLWithPath: "/tmp/spellcheck_\(label).png")) |
| 93 | + } |
| 94 | + window.orderOut(nil) |
| 95 | +} |
| 96 | + |
| 97 | +// Arm A: system pass only (question 1 only) |
| 98 | +run(label: "A_systemOnly", insertManual: false) |
| 99 | +// Arm B: system pass + manual write (questions 1 + 2) |
| 100 | +run(label: "B_withManual", insertManual: true) |
0 commit comments