| @@ -69,6 +69,8 @@ public final class OrgEditor: NSObject { |
| 69 | } |
69 | } |
| 70 | private var isLoading = false |
70 | private var isLoading = false |
| 71 | private var previousSelection = 0 |
71 | private var previousSelection = 0 |
| |
72 | /// The selection shaded block lines last drew. |
| |
73 | private var previousSelectionRange = NSRange(location: 0, length: 0) |
| 72 | /// Text before this offset is styled; after it, only base attributes until the background |
74 | /// Text before this offset is styled; after it, only base attributes until the background |
| 73 | /// passes reach it. Always at a line start. |
75 | /// passes reach it. Always at a line start. |
| 74 | private(set) var styledUpTo = 0 |
76 | private(set) var styledUpTo = 0 |
| @@ -122,6 +124,16 @@ public final class OrgEditor: NSObject { |
| 122 | dispatcher = KeyDispatcher(keymap: KeymapPreset.emacs.keymap) |
124 | dispatcher = KeyDispatcher(keymap: KeymapPreset.emacs.keymap) |
| 123 | super.init() |
125 | super.init() |
| 124 | orgTextView.keyHandler = { [weak self] event in self?.handleKey(event) ?? false } |
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 | orgTextView.onCommandClick = { [weak self] offset in |
137 | orgTextView.onCommandClick = { [weak self] offset in |
| 126 | guard let self, Links.link(at: offset, text: self.document.text, tree: self.document.tree) != nil |
138 | guard let self, Links.link(at: offset, text: self.document.text, tree: self.document.tree) != nil |
| 127 | || Links.timestampDays(at: offset, text: self.document.text) != nil else { return false } |
139 | || Links.timestampDays(at: offset, text: self.document.text) != nil else { return false } |
| @@ -219,7 +231,8 @@ public final class OrgEditor: NSObject { |
| 219 | textView.drawsBackground = true |
231 | textView.drawsBackground = true |
| 220 | textView.backgroundColor = theme.color("background") ?? .textBackgroundColor |
232 | textView.backgroundColor = theme.color("background") ?? .textBackgroundColor |
| 221 | textView.insertionPointColor = theme.color("cursor") ?? .textColor |
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 | if let view = orgTextView { |
236 | if let view = orgTextView { |
| 224 | view.lineNumberFont = NSFont.monospacedDigitSystemFont(ofSize: max(8, theme.font.pointSize - 2), weight: .regular) |
237 | view.lineNumberFont = NSFont.monospacedDigitSystemFont(ofSize: max(8, theme.font.pointSize - 2), weight: .regular) |
| 225 | view.lineNumberColor = theme.color("line-number") ?? .tertiaryLabelColor |
238 | view.lineNumberColor = theme.color("line-number") ?? .tertiaryLabelColor |
| @@ -1309,11 +1322,33 @@ public final class OrgEditor: NSObject { |
| 1309 | return mapped |
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 | /// Keeps the caret out of folded text: moving forward into a fold jumps past it, moving |
1346 | /// Keeps the caret out of folded text: moving forward into a fold jumps past it, moving |
| 1313 | /// back jumps to the end of the heading line. |
1347 | /// back jumps to the end of the heading line. |
| 1314 | func selectionChanged() { |
1348 | func selectionChanged() { |
| 1315 | let selection = textView.selectedRange() |
1349 | let selection = textView.selectedRange() |
| 1316 | defer { previousSelection = selection.location } |
1350 | defer { previousSelection = selection.location } |
| |
1351 | redrawShadedSelection(selection) |
| 1317 | onSelectionChange?(selection) |
1352 | onSelectionChange?(selection) |
| 1318 | revealCaretLine() |
1353 | revealCaretLine() |
| 1319 | orgTextView?.redrawLineNumbers() |
1354 | orgTextView?.redrawLineNumbers() |
| @@ -1435,6 +1470,25 @@ final class ShadedFragment: NSTextLayoutFragment, @unchecked Sendable { |
| 1435 | context.saveGState() |
1470 | context.saveGState() |
| 1436 | context.setFillColor(Theme.blockBackground.cgColor) |
1471 | context.setFillColor(Theme.blockBackground.cgColor) |
| 1437 | context.fill(band.offsetBy(dx: point.x, dy: point.y)) |
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 | context.restoreGState() |
1492 | context.restoreGState() |
| 1439 | super.draw(at: point, in: context) |
1493 | super.draw(at: point, in: context) |
| 1440 | } |
1494 | } |