feat: round embedded note images

This commit is contained in:
Valdemar Lorenzen
2026-10-20 12:36:35 +02:00
parent 794d927df1
commit f6d9d04b74
4 changed files with 78 additions and 1 deletions
+2
View File
@@ -179,6 +179,8 @@ Typing, pasting, and Return at the visible start of an item use the position aft
Fenced code uses [HighlighterSwift](https://github.com/smittytone/HighlighterSwift) (Highlight.js) for language-aware token colors. Add a language after the opening backticks or tildes, for example `py`, `python`, `swift`, `js`, or `json`. Unlabeled fences use automatic detection; unknown languages remain plain monospace. Highlighting runs entirely offline. Fenced code uses [HighlighterSwift](https://github.com/smittytone/HighlighterSwift) (Highlight.js) for language-aware token colors. Add a language after the opening backticks or tildes, for example `py`, `python`, `swift`, `js`, or `json`. Unlabeled fences use automatic detection; unknown languages remain plain monospace. Highlighting runs entirely offline.
Embedded images display with 8-point rounded corners. Rounding only clips their rendering in Notes; saving and copying retain the original image data.
Notes autosave locally as RTFD packages, including embedded images, under `~/Library/Application Support/Conjure/Notes`. No account, network access, or cloud sync is involved. Save failures appear below the editor and prevent switching, closing, or quitting until saving succeeds, preserving the unsaved buffer. The Close button and Command-W dismiss Conjure, including any open popup; leaving Notes or switching destinations removes its popup completely and restores the launcher’s original dimensions. Notes autosave locally as RTFD packages, including embedded images, under `~/Library/Application Support/Conjure/Notes`. No account, network access, or cloud sync is involved. Save failures appear below the editor and prevent switching, closing, or quitting until saving succeeds, preserving the unsaved buffer. The Close button and Command-W dismiss Conjure, including any open popup; leaving Notes or switching destinations removes its popup completely and restores the launcher’s original dimensions.
## Translation ## Translation
+12
View File
@@ -619,6 +619,15 @@ private final class NotesView: NSView {
} }
} }
private final class RoundedImageAttachmentCell: NSTextAttachmentCell {
override func draw(withFrame cellFrame: NSRect, in controlView: NSView?) {
NSGraphicsContext.saveGraphicsState()
defer { NSGraphicsContext.restoreGraphicsState() }
NSBezierPath(roundedRect: cellFrame, xRadius: 8, yRadius: 8).addClip()
super.draw(withFrame: cellFrame, in: controlView)
}
}
@MainActor @MainActor
enum MarkdownHighlighting { enum MarkdownHighlighting {
@@ -677,6 +686,9 @@ enum MarkdownHighlighting {
let size = image.size let size = image.size
let scale = min(1, 460 / max(size.width, 1), 240 / max(size.height, 1)) 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) } if scale < 1 { image.size = NSSize(width: size.width * scale, height: size.height * scale) }
if !(cell is RoundedImageAttachmentCell) {
attachment.attachmentCell = RoundedImageAttachmentCell(imageCell: image)
}
} }
storage.removeAttribute(.strikethroughStyle, range: range) storage.removeAttribute(.strikethroughStyle, range: range)
storage.addAttribute(hiddenMarkup, value: false, range: range) storage.addAttribute(hiddenMarkup, value: false, range: range)
+2 -1
View File
@@ -6,6 +6,7 @@
- this should keep window placement invariant of corner window sizes - this should keep window placement invariant of corner window sizes
- [x] conjure notes: - [x] conjure notes:
- [x] add action to delete a note (currently selected note) - [x] add action to delete a note (currently selected note)
- [ ] make images have rounded corners - [x] make images have rounded corners
- [ ] bug: text flashes when inserting characters, which is visually disturbing.
- [ ] the guidelines for centering the application when dragged are blurry on some external monitors. - [ ] the guidelines for centering the application when dragged are blurry on some external monitors.
- track down the cause and fix it. - track down the cause and fix it.
@@ -41,6 +41,68 @@ func noteFormattingHidesSyntaxButPreservesMarkdownAndLiteralCode() throws {
#expect(reloaded.contents.string == source) #expect(reloaded.contents.string == source)
} }
@Test @MainActor
func noteImagesRenderRoundedWithoutChangingSavedImageData() throws {
_ = NSApplication.shared
let image = NSImage(size: NSSize(width: 80, height: 48), flipped: false) { rect in
NSColor.red.setFill()
rect.fill()
return true
}
let tiff = try #require(image.tiffRepresentation)
let bitmap = try #require(NSBitmapImageRep(data: tiff))
let png = try #require(bitmap.representation(using: .png, properties: [:]))
let pasteboard = NSPasteboard.withUniqueName()
defer { pasteboard.releaseGlobally() }
pasteboard.setData(png, forType: .png)
let editor = NotesTextView(frame: NSRect(x: 0, y: 0, width: 200, height: 120))
editor.isRichText = true
editor.importsGraphics = true
#expect(editor.readSelection(from: pasteboard))
func checkRoundedDrawing() throws {
MarkdownHighlighting.apply(to: editor)
let layout = try #require(editor.layoutManager)
layout.ensureLayout(for: try #require(editor.textContainer))
let output = try #require(NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: 200, pixelsHigh: 120,
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false,
colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0))
let context = try #require(NSGraphicsContext(bitmapImageRep: output))
NSGraphicsContext.saveGraphicsState()
defer { NSGraphicsContext.restoreGraphicsState() }
NSGraphicsContext.current = context
context.cgContext.clear(CGRect(x: 0, y: 0, width: 200, height: 120))
layout.drawGlyphs(forGlyphRange: layout.glyphRange(forCharacterRange: NSRange(location: 0, length: 1),
actualCharacterRange: nil), at: .zero)
var left = 200, right = -1, top = 120, bottom = -1
for y in 0..<120 {
for x in 0..<200 where (output.colorAt(x: x, y: y)?.alphaComponent ?? 0) > 0.5 {
left = min(left, x); right = max(right, x)
top = min(top, y); bottom = max(bottom, y)
}
}
try #require(right > left && bottom > top)
for x in [left, right] {
for y in [top, bottom] {
#expect(try #require(output.colorAt(x: x, y: y)).alphaComponent < 0.1)
}
}
let center = try #require(output.colorAt(x: (left + right) / 2, y: (top + bottom) / 2)?.usingColorSpace(.deviceRGB))
#expect(center.redComponent > 0.9 && center.greenComponent < 0.1 && center.alphaComponent > 0.9)
}
try checkRoundedDrawing()
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at: directory) }
let store = try NotesStore(directory: directory)
let id = try store.create()
try store.update(id: id, contents: editor.markdownContents)
let reloaded = try #require(NotesStore(directory: directory).notes.first)
let attachment = try #require(reloaded.contents.attribute(.attachment, at: 0, effectiveRange: nil) as? NSTextAttachment)
#expect(attachment.fileWrapper?.regularFileContents == png)
editor.setMarkdownContents(reloaded.contents)
try checkRoundedDrawing()
}
@Test @MainActor @Test @MainActor
func hiddenNoteMarkupKeepsTypingDeletionFormattingAndUndoAtVisibleBoundaries() throws { func hiddenNoteMarkupKeepsTypingDeletionFormattingAndUndoAtVisibleBoundaries() throws {
_ = NSApplication.shared _ = NSApplication.shared