style: match Raycast notes window

This commit is contained in:
Valdemar Lorenzen
2026-10-19 15:26:42 +02:00
parent 1c3ecb232b
commit 999d44226d
7 changed files with 332 additions and 73 deletions
@@ -250,7 +250,7 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
selectionAccessibilityLabel = "Notes"
currentChoiceAccessibilityLabel = "Current note"
show(title: "Browse Notes", relativeTo: anchor, available: [],
searchAccessibilityLabel: "Search notes",
searchAccessibilityLabel: "Search notes", belowAnchor: true,
selectionChoices: notes.map {
SelectionChoice(id: .note($0.id), title: $0.title, searchText: FuzzySearchText($0.title))
}, selectionName: "notes")
@@ -258,7 +258,7 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
func showNotesActions(relativeTo anchor: NSView) {
show(title: "Notes", relativeTo: anchor, available: [12, 13],
searchAccessibilityLabel: "Search notes actions")
searchAccessibilityLabel: "Search notes actions", belowAnchor: true)
}
private func show(title: String, relativeTo anchor: NSView, available: [Int], emoji: Bool = false,
+20 -9
View File
@@ -6,7 +6,6 @@ import ApplicationServices
@MainActor
final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSearchFieldDelegate, NSTableViewDataSource, NSTableViewDelegate {
private let searchField = NSSearchField()
private let commandTitle = NSTextField(labelWithString: "Conjure Notes")
private let searchSymbol = IconView(font: .systemFont(ofSize: 20))
private let commandBack = NSButton(title: "", target: nil, action: nil)
private let translationHeader = NSStackView()
@@ -265,9 +264,6 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
attributes: [.foregroundColor: SunsetPalette.secondaryText, .font: searchFont]
)
searchField.textColor = SunsetPalette.text
commandTitle.font = searchFont
commandTitle.textColor = SunsetPalette.text
commandTitle.isHidden = true
searchField.isBordered = false
searchField.drawsBackground = false
searchField.focusRingType = .none
@@ -294,7 +290,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
commandBack.heightAnchor.constraint(equalToConstant: IconStyle.applicationSize(for: searchFont)),
])
translationHeader.isHidden = true
let header = NSStackView(views: [searchSymbol, commandBack, searchField, commandTitle, translationHeader])
let header = NSStackView(views: [searchSymbol, commandBack, searchField, translationHeader])
header.orientation = .horizontal
header.alignment = .centerY
header.spacing = 12
@@ -897,6 +893,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
activeCommand = command
history.recordLaunch(of: Application(command: command))
let previousQuery = searchField.stringValue
let previousFrame = window?.frame
let previousPlaceholder = searchField.placeholderAttributedString
let close: () -> Void = { [weak self, weak controller] in
guard let self, let controller else { return }
@@ -907,7 +904,12 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
self.emojiPicker = nil
self.notes = nil
self.searchField.isHidden = false
self.commandTitle.isHidden = true
self.launcherViews.first?.isHidden = false
self.launcherViews.first?.setAccessibilityHidden(false)
if command == .notes, let previousFrame {
self.window?.setFrame(previousFrame, display: true)
if let material = content as? NSVisualEffectView { roundMaterialCorners(material, radius: 24) }
}
self.activeCommand = nil
self.commandBack.isHidden = true
self.commandBack.target = nil
@@ -943,8 +945,15 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
)
searchField.setAccessibilityLabel(accessibilityLabel)
if command == .notes {
searchField.isHidden = true
commandTitle.isHidden = false
launcherViews.first?.isHidden = true
launcherViews.first?.setAccessibilityHidden(true)
if var frame = window?.frame {
frame.origin.x += (frame.width - 570) / 2
frame.origin.y = frame.maxY - 404
frame.size = NSSize(width: 570, height: 404)
window?.setFrame(frame, display: true)
}
if let material = content as? NSVisualEffectView { roundMaterialCorners(material, radius: 14) }
}
for view in launcherViews.dropFirst() {
view.isHidden = true
@@ -959,7 +968,9 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
NSLayoutConstraint.activate([
controller.view.leadingAnchor.constraint(equalTo: content.leadingAnchor),
controller.view.trailingAnchor.constraint(equalTo: content.trailingAnchor),
controller.view.topAnchor.constraint(equalTo: command == .notes ? commandTitle.bottomAnchor : searchField.bottomAnchor, constant: 16),
command == .notes
? controller.view.topAnchor.constraint(equalTo: content.topAnchor)
: controller.view.topAnchor.constraint(equalTo: searchField.bottomAnchor, constant: 16),
controller.view.bottomAnchor.constraint(equalTo: content.bottomAnchor),
])
if let notes { notes.focusEditor() }
+219 -43
View File
@@ -2,6 +2,21 @@ import AppKit
import ConjureKit
import Highlighter
@MainActor
enum NotesAppearance {
static let background = NSColor(white: 0.035, alpha: 1)
static let text = NSColor(white: 0.94, alpha: 1)
static let muted = NSColor(white: 0.58, alpha: 1)
static let accent = NSColor(srgbRed: 1, green: 0.36, blue: 0.39, alpha: 1)
static let font = NSFont.systemFont(ofSize: 17)
static let paragraph: NSParagraphStyle = {
let style = NSMutableParagraphStyle()
style.lineSpacing = 5
style.paragraphSpacing = 8
return style
}()
}
@MainActor
final class NotesController: NSViewController, NSTextViewDelegate {
private let browseButton = NSButton(title: "Browse Notes", target: nil, action: nil)
@@ -9,6 +24,8 @@ final class NotesController: NSViewController, NSTextViewDelegate {
private let actions = ApplicationActionsController()
private let editor = NotesTextView()
private let status = NSTextField(labelWithString: "")
private let noteTitle = NSTextField(labelWithString: "Untitled Note")
private let formatting = NSStackView()
private let defaults: UserDefaults
private var store: NotesStore?
private var selectedID: UUID?
@@ -32,32 +49,84 @@ final class NotesController: NSViewController, NSTextViewDelegate {
let content = NotesView()
content.commandShortcut = { [weak self] in self?.handleCommandKey($0) ?? false }
view = content
content.wantsLayer = true
content.layer?.backgroundColor = NotesAppearance.background.cgColor
let close = iconButton("circle.fill", label: "Close notes", action: #selector(closeWindow))
close.contentTintColor = NotesAppearance.accent
close.isBordered = false
close.image = close.image?.withSymbolConfiguration(.init(paletteColors: [NotesAppearance.accent]))
let lights = NSStackView(views: [close])
lights.spacing = 8
for _ in 0..<2 {
let dot = NSView()
dot.wantsLayer = true
dot.layer?.backgroundColor = NSColor(white: 0.15, alpha: 1).cgColor
dot.layer?.cornerRadius = 6
dot.setAccessibilityElement(false)
NSLayoutConstraint.activate([dot.widthAnchor.constraint(equalToConstant: 12), dot.heightAnchor.constraint(equalToConstant: 12)])
lights.addArrangedSubview(dot)
}
browseButton.target = self
browseButton.action = #selector(browseNotes)
browseButton.setAccessibilityLabel("Browse notes")
browseButton.toolTip = "Browse notes (Command-P)"
configureIcon(browseButton, symbol: "note.text", label: "Browse notes (⌘P)")
browseButton.keyEquivalent = "p"
actionsButton.target = self
actionsButton.action = #selector(showActions)
actionsButton.setAccessibilityLabel("Notes actions")
actionsButton.toolTip = "Notes actions (Command-K)"
configureIcon(actionsButton, symbol: "command", label: "Notes actions (⌘K)")
let create = iconButton("plus", label: "New note (⌘N)", action: #selector(newNote))
let controls = NSStackView(views: [actionsButton, browseButton, create])
controls.spacing = 2
noteTitle.font = .systemFont(ofSize: 13)
noteTitle.textColor = NotesAppearance.muted
noteTitle.alignment = .center
noteTitle.lineBreakMode = .byTruncatingTail
noteTitle.setAccessibilityLabel("Note title")
let header = NSView()
for control in [lights, noteTitle, controls] {
control.translatesAutoresizingMaskIntoConstraints = false
header.addSubview(control)
control.centerYAnchor.constraint(equalTo: header.centerYAnchor).isActive = true
}
NSLayoutConstraint.activate([
lights.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: 10),
controls.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -8),
noteTitle.centerXAnchor.constraint(equalTo: header.centerXAnchor),
noteTitle.leadingAnchor.constraint(greaterThanOrEqualTo: lights.trailingAnchor, constant: 16),
noteTitle.trailingAnchor.constraint(lessThanOrEqualTo: controls.leadingAnchor, constant: -16),
])
actions.closed = { [weak self] in self?.focusEditor() }
actions.selectNote = { [weak self] in self?.switchNote($0) }
actions.newNote = { [weak self] in self?.newNote() }
actions.browseNotes = { [weak self] in self?.browseNotes() }
let pill = ActionPill(primaryButton: browseButton, actionsButton: actionsButton, primaryCommand: "p")
let footer = NSStackView(views: [status, NSView(), pill])
footer.orientation = .horizontal
let tools = [
("textformat.size", "Heading"), ("bold", "Bold"), ("italic", "Italic"),
("strikethrough", "Strikethrough"), ("chevron.left.forwardslash.chevron.right", "Inline code"),
("link", "Link"), ("curlybraces.square", "Code block"), ("text.quote", "Quote"),
("list.number", "Ordered list"), ("list.bullet", "Bullet list"), ("checklist", "Checklist"),
]
for (index, tool) in tools.enumerated() {
let button = iconButton(tool.0, label: tool.1, action: #selector(formatNote(_:)))
button.tag = index
formatting.addArrangedSubview(button)
}
formatting.spacing = 8
let toggle = iconButton("xmark.circle.fill", label: "Hide formatting", action: #selector(toggleFormatting(_:)))
let separator = NSBox()
separator.boxType = .separator
let footer = NSStackView(views: [formatting, NSView(), separator, toggle])
footer.alignment = .centerY
footer.spacing = 12
footer.spacing = 8
separator.heightAnchor.constraint(equalToConstant: 22).isActive = true
editor.delegate = self
editor.isRichText = true
editor.importsGraphics = true
editor.allowsUndo = true
editor.drawsBackground = false
editor.textColor = SunsetPalette.text
editor.insertionPointColor = SunsetPalette.accent
editor.font = .systemFont(ofSize: 14)
editor.textColor = NotesAppearance.text
editor.insertionPointColor = NotesAppearance.accent
editor.selectedTextAttributes = [.backgroundColor: NotesAppearance.accent.withAlphaComponent(0.25)]
editor.font = NotesAppearance.font
editor.defaultParagraphStyle = NotesAppearance.paragraph
editor.isAutomaticQuoteSubstitutionEnabled = false
editor.isAutomaticDashSubstitutionEnabled = false
editor.isAutomaticTextReplacementEnabled = false
@@ -65,39 +134,49 @@ final class NotesController: NSViewController, NSTextViewDelegate {
editor.isHorizontallyResizable = false
editor.autoresizingMask = [.width]
editor.textContainer?.widthTracksTextView = true
editor.textContainerInset = NSSize(width: 10, height: 10)
editor.textContainerInset = NSSize(width: 22, height: 8)
editor.setAccessibilityLabel("Note editor")
let scroll = NSScrollView()
scroll.documentView = editor
scroll.hasVerticalScroller = true
scroll.autohidesScrollers = true
scroll.drawsBackground = false
scroll.wantsLayer = true
scroll.layer?.cornerRadius = 8
scroll.layer?.borderWidth = 0.5
scroll.layer?.borderColor = SunsetPalette.secondaryText.withAlphaComponent(0.22).cgColor
scroll.scrollerStyle = .overlay
status.font = .systemFont(ofSize: 12)
status.textColor = SunsetPalette.secondaryText
status.textColor = NotesAppearance.muted
status.setAccessibilityLabel("Notes save status")
status.lineBreakMode = .byTruncatingTail
status.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
for control in [scroll, footer] {
status.isHidden = true
let body = NSStackView(views: [scroll, status, footer])
body.orientation = .vertical
body.alignment = .leading
body.spacing = 4
for control in [header, body] {
control.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(control)
}
NSLayoutConstraint.activate([
scroll.topAnchor.constraint(equalTo: view.topAnchor),
scroll.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
scroll.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
scroll.bottomAnchor.constraint(equalTo: footer.topAnchor, constant: -8),
footer.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
footer.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8),
footer.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8),
footer.heightAnchor.constraint(equalToConstant: 36),
header.topAnchor.constraint(equalTo: view.topAnchor),
header.leadingAnchor.constraint(equalTo: view.leadingAnchor),
header.trailingAnchor.constraint(equalTo: view.trailingAnchor),
header.heightAnchor.constraint(equalToConstant: 38),
body.topAnchor.constraint(equalTo: header.bottomAnchor, constant: 4),
body.leadingAnchor.constraint(equalTo: view.leadingAnchor),
body.trailingAnchor.constraint(equalTo: view.trailingAnchor),
body.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -4),
scroll.widthAnchor.constraint(equalTo: body.widthAnchor),
footer.leadingAnchor.constraint(equalTo: body.leadingAnchor, constant: 24),
footer.trailingAnchor.constraint(equalTo: body.trailingAnchor, constant: -6),
status.leadingAnchor.constraint(equalTo: body.leadingAnchor, constant: 28),
status.trailingAnchor.constraint(equalTo: body.trailingAnchor, constant: -12),
scroll.heightAnchor.constraint(greaterThanOrEqualToConstant: 80),
footer.heightAnchor.constraint(equalToConstant: 32),
])
guard let store else {
editor.isEditable = false
for control in [browseButton, actionsButton] { control.isEnabled = false }
for control in [browseButton, actionsButton, create] { control.isEnabled = false }
formatting.subviews.compactMap { $0 as? NSButton }.forEach { $0.isEnabled = false }
showError(loadError!)
return
}
@@ -111,6 +190,94 @@ final class NotesController: NSViewController, NSTextViewDelegate {
}
}
private func configureIcon(_ button: NSButton, symbol: String, label: String) {
button.image = NSImage(systemSymbolName: symbol, accessibilityDescription: nil)?
.withSymbolConfiguration(.init(pointSize: 16, weight: .regular))
button.imagePosition = .imageOnly
button.bezelStyle = .accessoryBarAction
button.showsBorderOnlyWhileMouseInside = true
button.contentTintColor = NotesAppearance.muted
button.toolTip = label
button.setAccessibilityLabel(label)
NSLayoutConstraint.activate([button.widthAnchor.constraint(equalToConstant: 28), button.heightAnchor.constraint(equalToConstant: 28)])
}
private func iconButton(_ symbol: String, label: String, action: Selector) -> NSButton {
let button = NSButton(title: "", target: self, action: action)
configureIcon(button, symbol: symbol, label: label)
return button
}
@objc private func closeWindow() {
guard save() else { return }
view.window?.performClose(nil)
}
@objc private func toggleFormatting(_ sender: NSButton) {
formatting.isHidden.toggle()
sender.image = NSImage(systemSymbolName: formatting.isHidden ? "textformat" : "xmark.circle.fill", accessibilityDescription: nil)
sender.toolTip = formatting.isHidden ? "Show formatting" : "Hide formatting"
sender.setAccessibilityLabel(sender.toolTip)
focusEditor()
}
@objc private func formatNote(_ sender: NSButton) {
guard editor.isEditable else { return }
let contents = editor.markdownContents
let source = contents.string as NSString
var selection = editor.markdownInsertionRange(for: editor.selectedRange())
if sender.tag == 6 {
selection = source.lineRange(for: selection)
if selection.length > 0, source.character(at: NSMaxRange(selection) - 1) == 10 { selection.length -= 1 }
}
let wrappers = [1: ("**", "**"), 2: ("*", "*"), 3: ("~~", "~~"), 4: ("`", "`"),
5: ("[", "](url)"), 6: ("```\n", "\n```")]
if let (open, close) = wrappers[sender.tag] {
let before = selection.location - open.utf16.count
let after = NSMaxRange(selection)
if before >= 0, after + close.utf16.count <= source.length,
source.substring(with: NSRange(location: before, length: open.utf16.count)) == open,
source.substring(with: NSRange(location: after, length: close.utf16.count)) == close {
let body = contents.attributedSubstring(from: selection)
selection = NSRange(location: before, length: selection.length + open.utf16.count + close.utf16.count)
replaceMarkdown(body, range: selection, selection: NSRange(location: before, length: body.length))
} else {
let body = NSMutableAttributedString(string: open, attributes: editor.typingAttributes)
body.append(contents.attributedSubstring(from: selection))
body.append(NSAttributedString(string: close, attributes: editor.typingAttributes))
replaceMarkdown(body, range: selection,
selection: NSRange(location: selection.location + open.utf16.count, length: selection.length))
}
} else {
let prefixes = [0: "# ", 7: "> ", 8: "1. ", 9: "- ", 10: "- [ ] "]
guard let prefix = prefixes[sender.tag] else { return }
let length = selection.length > 0 && source.character(at: NSMaxRange(selection) - 1) == 10
? selection.length - 1 : selection.length
let lines = source.lineRange(for: NSRange(location: selection.location, length: length))
let body = NSMutableAttributedString(attributedString: contents.attributedSubstring(from: lines))
let rows = body.string.components(separatedBy: "\n")
var offset = body.length, firstDelta = 0
for (index, line) in rows.enumerated().reversed() {
offset -= line.utf16.count + (index < rows.count - 1 ? 1 : 0)
if index == rows.count - 1, index > 0, line.isEmpty { continue }
let indent = String(line.prefix { $0 == " " || $0 == "\t" })
let content = String(line.dropFirst(indent.count))
let item = Self.listMatch(in: content as NSString)
let kind = item.map { $0.range(at: 6).location != NSNotFound ? 10 : $0.range(at: 3).location != NSNotFound ? 8 : 9 }
let removing = kind == sender.tag || (item == nil && content.hasPrefix(prefix))
let oldPrefix = item?.range.length ?? (removing ? prefix.utf16.count : 0)
let newPrefix = removing ? "" : sender.tag == 8 ? "\(index + 1). " : prefix
body.replaceCharacters(in: NSRange(location: offset + indent.utf16.count, length: oldPrefix),
with: NSAttributedString(string: newPrefix, attributes: editor.typingAttributes))
if index == 0 { firstDelta = newPrefix.utf16.count - oldPrefix }
}
let start = max(lines.location, selection.location + firstDelta)
let end = max(start, NSMaxRange(selection) + body.length - lines.length)
replaceMarkdown(body, range: lines, selection: NSRange(location: start, length: end - start))
}
focusEditor()
}
func focusEditor() { view.window?.makeFirstResponder(editor) }
func textDidChange(_ notification: Notification) {
@@ -164,6 +331,10 @@ final class NotesController: NSViewController, NSTextViewDelegate {
}
private func replaceListMarkdown(_ replacement: String, range: NSRange, selection: NSRange) {
replaceMarkdown(NSAttributedString(string: replacement, attributes: editor.typingAttributes), range: range, selection: selection)
}
private func replaceMarkdown(_ replacement: NSAttributedString, range: NSRange, selection: NSRange) {
editor.undoManager?.beginUndoGrouping()
// Native text undo restores its diff boundary, not the user's selection.
setSelectionWithUndo(editor.selectedRange())
@@ -246,8 +417,8 @@ final class NotesController: NSViewController, NSTextViewDelegate {
do {
try store.update(id: selectedID, contents: editor.markdownContents)
isDirty = false
status.stringValue = "Saved locally · Markdown · Paste or drop images"
status.textColor = SunsetPalette.secondaryText
noteTitle.stringValue = store.notes.first { $0.id == selectedID }?.title ?? "Untitled Note"
status.isHidden = true
return true
} catch {
showError(error)
@@ -312,12 +483,13 @@ final class NotesController: NSViewController, NSTextViewDelegate {
editor.setSelectedRange(NSRange(location: editor.string.utf16.count, length: 0))
MarkdownHighlighting.apply(to: editor)
editor.refreshTasks()
status.stringValue = "Saved locally · Markdown · Paste or drop images"
status.textColor = SunsetPalette.secondaryText
noteTitle.stringValue = note.title
status.isHidden = true
}
private func showError(_ error: Error) {
status.isHidden = false
status.stringValue = "Couldn’t save or load notes: \(error.localizedDescription)"
status.toolTip = status.stringValue
status.textColor = .systemRed
@@ -353,16 +525,17 @@ private final class NotesView: NSView {
enum MarkdownHighlighting {
// ponytail: whole-note highlighting; incremental parsing if large notes make typing slow.
private static let rules: [(NSRegularExpression, [NSAttributedString.Key: Any])] = [
(try! NSRegularExpression(pattern: "(?m)^#{1,6} .+$"), [.font: NSFont.systemFont(ofSize: 17, weight: .semibold), .foregroundColor: SunsetPalette.accent]),
(try! NSRegularExpression(pattern: "\\*\\*[^\\n]+?\\*\\*|__[^\\n]+?__"), [.font: NSFont.boldSystemFont(ofSize: 14)]),
(try! NSRegularExpression(pattern: "(?<!\\*)\\*(?!\\*)[^*\\n]+\\*(?!\\*)|(?<!_)_(?!_)[^_\\n]+_(?!_)"), [.font: NSFontManager.shared.convert(.systemFont(ofSize: 14), toHaveTrait: .italicFontMask)]),
(try! NSRegularExpression(pattern: "!?\\[[^\\]\\n]+\\]\\([^\\n)]+\\)|(?m)^\\s*(?:[-+*]|[0-9]+\\.)\\s"), [.foregroundColor: SunsetPalette.accent]),
(try! NSRegularExpression(pattern: "`[^`\\n]+`"), [.font: NSFont.monospacedSystemFont(ofSize: 14, weight: .regular), .foregroundColor: NSColor.systemGreen]),
(try! NSRegularExpression(pattern: "(?m)^#{1,6} .+$"), [.font: NSFont.systemFont(ofSize: 23, weight: .semibold)]),
(try! NSRegularExpression(pattern: "\\*\\*[^\\n]+?\\*\\*|__[^\\n]+?__"), [.font: NSFont.boldSystemFont(ofSize: 17)]),
(try! NSRegularExpression(pattern: "(?<!\\*)\\*(?!\\*)[^*\\n]+\\*(?!\\*)|(?<!_)_(?!_)[^_\\n]+_(?!_)"), [.font: NSFontManager.shared.convert(NotesAppearance.font, toHaveTrait: .italicFontMask)]),
(try! NSRegularExpression(pattern: "!?\\[[^\\]\\n]+\\]\\([^\\n)]+\\)|(?m)^\\s*(?:[-+*]|[0-9]+\\.)\\s"), [.foregroundColor: NotesAppearance.accent]),
(try! NSRegularExpression(pattern: "~~[^\\n]+?~~"), [.strikethroughStyle: NSUnderlineStyle.single.rawValue]),
(try! NSRegularExpression(pattern: "`[^`\\n]+`"), [.font: NSFont.monospacedSystemFont(ofSize: 17, weight: .regular), .foregroundColor: NSColor.systemGreen]),
]
private static let highlighter: Highlighter = {
guard let value = Highlighter() else { preconditionFailure("Missing bundled syntax highlighter") }
value.setTheme("atom-one-dark", withFont: "Menlo-Regular", ofSize: 14)
value.setTheme("atom-one-dark", withFont: "Menlo-Regular", ofSize: 17)
return value
}()
private static let openingFence = try! NSRegularExpression(pattern: "(?m)^ {0,3}(`{3,}|~{3,})([^\\r\\n]*)\\r?\\n")
@@ -401,7 +574,9 @@ enum MarkdownHighlighting {
let scale = min(1, 460 / max(size.width, 1), 240 / max(size.height, 1))
if scale < 1 { image.size = NSSize(width: size.width * scale, height: size.height * scale) }
}
storage.addAttributes([.font: NSFont.systemFont(ofSize: 14), .foregroundColor: SunsetPalette.text], range: range)
storage.removeAttribute(.strikethroughStyle, range: range)
storage.addAttributes([.font: NotesAppearance.font, .foregroundColor: NotesAppearance.text,
.paragraphStyle: NotesAppearance.paragraph], range: range)
let blocks = codeBlocks(in: text)
for (expression, attributes) in rules {
for match in expression.matches(in: text, range: range) {
@@ -410,9 +585,9 @@ enum MarkdownHighlighting {
}
}
for block in blocks {
storage.addAttributes([.font: NSFont.monospacedSystemFont(ofSize: 14, weight: .regular),
.foregroundColor: SunsetPalette.secondaryText], range: block.range)
storage.addAttribute(.foregroundColor, value: SunsetPalette.text, range: block.body)
storage.addAttributes([.font: NSFont.monospacedSystemFont(ofSize: 17, weight: .regular),
.foregroundColor: NotesAppearance.muted], range: block.range)
storage.addAttribute(.foregroundColor, value: NotesAppearance.text, range: block.body)
let code = (text as NSString).substring(with: block.body)
guard let highlighted = highlighter.highlight(code, as: block.language),
highlighted.string == code else { continue }
@@ -423,6 +598,7 @@ enum MarkdownHighlighting {
}
}
}
editor.typingAttributes = [.font: NSFont.systemFont(ofSize: 14), .foregroundColor: SunsetPalette.text]
editor.typingAttributes = [.font: NotesAppearance.font, .foregroundColor: NotesAppearance.text,
.paragraphStyle: NotesAppearance.paragraph]
}
}
+14 -8
View File
@@ -18,19 +18,19 @@ private final class ListAttachment: NSTextAttachment {
super.init(data: nil, ofType: nil)
let cell = ListAttachmentCell(textCell: isTask ? "" : (source.length == 1 ? "•" : source.string))
cell.attributedStringValue = NSAttributedString(string: cell.stringValue,
attributes: [.font: NSFont.systemFont(ofSize: 14), .foregroundColor: SunsetPalette.accent])
attributes: [.font: NotesAppearance.font, .foregroundColor: NotesAppearance.accent])
cell.alignment = .right
attachmentCell = cell
bounds = NSRect(x: 0, y: -3, width: 18, height: 17)
bounds = NSRect(x: 0, y: -4, width: 22, height: 21)
}
required init?(coder: NSCoder) { fatalError("List attachments are serialized as Markdown") }
}
private final class ListAttachmentCell: NSTextAttachmentCell {
nonisolated(unsafe) var columnWidth: CGFloat = 18
nonisolated override func cellSize() -> NSSize { NSSize(width: columnWidth, height: 17) }
nonisolated override func cellBaselineOffset() -> NSPoint { NSPoint(x: 0, y: -3) }
nonisolated(unsafe) var columnWidth: CGFloat = 22
nonisolated override func cellSize() -> NSSize { NSSize(width: columnWidth, height: 21) }
nonisolated override func cellBaselineOffset() -> NSPoint { NSPoint(x: 0, y: -4) }
override func draw(withFrame cellFrame: NSRect, in controlView: NSView?) {
guard !stringValue.isEmpty else { return }
let label = attributedStringValue
@@ -202,6 +202,10 @@ final class NotesTextView: NSTextView, @MainActor NSLayoutManagerDelegate {
replaceSource(NSAttributedString(string: text, attributes: textTypingAttributes), range: range, semantic: true)
}
func replaceMarkdown(_ contents: NSAttributedString, range: NSRange) {
replaceSource(contents, range: range, semantic: true)
}
func markdownInsertionRange(for rendered: NSRange) -> NSRange {
guard rendered.length == 0 else { return markdownRange(for: rendered) }
let body = listWhitespaceBoundary(from: rendered.location, forward: true)
@@ -506,7 +510,7 @@ final class NotesTextView: NSTextView, @MainActor NSLayoutManagerDelegate {
while checkboxes.count < taskIndices.count {
let checkbox = NSButton(checkboxWithTitle: "", target: self, action: #selector(toggleTask(_:)))
checkbox.controlSize = .small
checkbox.contentTintColor = SunsetPalette.accent
checkbox.contentTintColor = NotesAppearance.accent
checkbox.sizeToFit()
checkboxes.append(checkbox)
addSubview(checkbox)
@@ -534,8 +538,8 @@ final class NotesTextView: NSTextView, @MainActor NSLayoutManagerDelegate {
layoutManager.delegate = self
storage.beginEditing()
Self.restoreListLayout(in: storage)
let markerFont = NSFont.systemFont(ofSize: 14)
let column = ceil(tasks.reduce(CGFloat(18)) { width, task in
let markerFont = NotesAppearance.font
let column = ceil(tasks.reduce(CGFloat(22)) { width, task in
guard !task.attachment.isTask else { return width }
return max(width, (task.attachment.marker as NSString).size(withAttributes: [.font: markerFont]).width + 4)
})
@@ -552,6 +556,8 @@ final class NotesTextView: NSTextView, @MainActor NSLayoutManagerDelegate {
}
// Keep indentation as native whitespace; null paragraph-leading glyphs lose first-line layout.
style.firstLineHeadIndent = 0
style.lineSpacing = NotesAppearance.paragraph.lineSpacing
style.paragraphSpacing = NotesAppearance.paragraph.paragraphSpacing
style.headIndent = CGFloat(depth) * space + column
style.tabStops = []
style.defaultTabInterval = 4 * space