| @@ -69,6 +69,8 @@ public final class OrgEditor: NSObject { |
| 69 | 69 | } |
| 70 | 70 | private var isLoading = false |
| 71 | 71 | private var previousSelection = 0 |
| 72 | /// The selection shaded block lines last drew. |
| 73 | private var previousSelectionRange = NSRange(location: 0, length: 0) |
| 72 | 74 | /// Text before this offset is styled; after it, only base attributes until the background |
| 73 | 75 | /// passes reach it. Always at a line start. |
| 74 | 76 | private(set) var styledUpTo = 0 |
| @@ -122,6 +124,16 @@ public final class OrgEditor: NSObject { |
| 122 | 124 | dispatcher = KeyDispatcher(keymap: KeymapPreset.emacs.keymap) |
| 123 | 125 | super.init() |
| 124 | 126 | orgTextView.keyHandler = { [weak self] event in self?.handleKey(event) ?? false } |
| 127 | orgTextView.onFocusChange = { [weak self] in |
| 128 | guard let self else { return } |
| 129 | let id = ObjectIdentifier(self.textLayoutManager) |
| 130 | if let window = self.textView.window, window.isKeyWindow, window.firstResponder === self.textView { |
| 131 | Theme.focusedLayouts.insert(id) |
| 132 | } else { |
| 133 | Theme.focusedLayouts.remove(id) |
| 134 | } |
| 135 | self.redrawShaded([self.textView.selectedRange()]) |
| 136 | } |
| 125 | 137 | orgTextView.onCommandClick = { [weak self] offset in |
| 126 | 138 | guard let self, Links.link(at: offset, text: self.document.text, tree: self.document.tree) != nil |
| 127 | 139 | || Links.timestampDays(at: offset, text: self.document.text) != nil else { return false } |
| @@ -219,7 +231,8 @@ public final class OrgEditor: NSObject { |
| 219 | 231 | textView.drawsBackground = true |
| 220 | 232 | textView.backgroundColor = theme.color("background") ?? .textBackgroundColor |
| 221 | 233 | textView.insertionPointColor = theme.color("cursor") ?? .textColor |
| 222 | | textView.selectedTextAttributes = [.backgroundColor: theme.color("selection") ?? .selectedTextBackgroundColor] |
| 234 | Theme.selection = theme.color("selection") ?? .selectedTextBackgroundColor |
| 235 | textView.selectedTextAttributes = [.backgroundColor: Theme.selection] |
| 223 | 236 | if let view = orgTextView { |
| 224 | 237 | view.lineNumberFont = NSFont.monospacedDigitSystemFont(ofSize: max(8, theme.font.pointSize - 2), weight: .regular) |
| 225 | 238 | view.lineNumberColor = theme.color("line-number") ?? .tertiaryLabelColor |
| @@ -1309,11 +1322,33 @@ public final class OrgEditor: NSObject { |
| 1309 | 1322 | return mapped |
| 1310 | 1323 | } |
| 1311 | 1324 | |
| 1325 | /// Shaded block lines the old or new selection covers are drawn again, as they draw the |
| 1326 | /// selection over their band. |
| 1327 | private func redrawShadedSelection(_ selection: NSRange) { |
| 1328 | defer { previousSelectionRange = selection } |
| 1329 | redrawShaded([previousSelectionRange, selection]) |
| 1330 | } |
| 1331 | |
| 1332 | private func redrawShaded(_ ranges: [NSRange]) { |
| 1333 | guard let manager = textLayoutManager.textContentManager else { return } |
| 1334 | for old in ranges where old.length > 0 { |
| 1335 | for block in shaded.all where block.overlaps(old.location..<NSMaxRange(old)) { |
| 1336 | let lower = max(block.lowerBound, old.location) |
| 1337 | let upper = min(block.upperBound, NSMaxRange(old)) |
| 1338 | guard let start = manager.location(manager.documentRange.location, offsetBy: lower), |
| 1339 | let end = manager.location(start, offsetBy: upper - lower), |
| 1340 | let range = NSTextRange(location: start, end: end) else { continue } |
| 1341 | textLayoutManager.invalidateLayout(for: range) |
| 1342 | } |
| 1343 | } |
| 1344 | } |
| 1345 | |
| 1312 | 1346 | /// Keeps the caret out of folded text: moving forward into a fold jumps past it, moving |
| 1313 | 1347 | /// back jumps to the end of the heading line. |
| 1314 | 1348 | func selectionChanged() { |
| 1315 | 1349 | let selection = textView.selectedRange() |
| 1316 | 1350 | defer { previousSelection = selection.location } |
| 1351 | redrawShadedSelection(selection) |
| 1317 | 1352 | onSelectionChange?(selection) |
| 1318 | 1353 | revealCaretLine() |
| 1319 | 1354 | orgTextView?.redrawLineNumbers() |
| @@ -1435,6 +1470,25 @@ final class ShadedFragment: NSTextLayoutFragment, @unchecked Sendable { |
| 1435 | 1470 | context.saveGState() |
| 1436 | 1471 | context.setFillColor(Theme.blockBackground.cgColor) |
| 1437 | 1472 | context.fill(band.offsetBy(dx: point.x, dy: point.y)) |
| 1473 | // The band covers the view's selection highlight, so the selected text is drawn on it, |
| 1474 | // in the color the view uses: the theme's when focused, the system's muted one when not. |
| 1475 | if let manager = textLayoutManager { |
| 1476 | let active = Theme.focusedLayouts.contains(ObjectIdentifier(manager)) |
| 1477 | context.setFillColor((active ? Theme.selection : NSColor.unemphasizedSelectedTextBackgroundColor).cgColor) |
| 1478 | let single = textLineFragments.count == 1 |
| 1479 | for selection in manager.textSelections { |
| 1480 | for range in selection.textRanges where !range.isEmpty { |
| 1481 | guard let part = range.intersection(rangeInElement), !part.isEmpty else { continue } |
| 1482 | manager.enumerateTextSegments(in: part, type: .selection, options: [.rangeNotRequired]) { _, frame, _, _ in |
| 1483 | var rect = frame.offsetBy(dx: point.x - self.layoutFragmentFrame.minX, dy: point.y - self.layoutFragmentFrame.minY) |
| 1484 | // A one-line fragment's selection covers its band, leaving no gap to the next line. |
| 1485 | if single { rect = CGRect(x: rect.minX, y: point.y, width: rect.width, height: self.band.height) } |
| 1486 | context.fill(rect) |
| 1487 | return true |
| 1488 | } |
| 1489 | } |
| 1490 | } |
| 1491 | } |
| 1438 | 1492 | context.restoreGState() |
| 1439 | 1493 | super.draw(at: point, in: context) |
| 1440 | 1494 | } |