diff --git a/README.md b/README.md index 77a1f90..3a8ad2a 100644 --- a/README.md +++ b/README.md @@ -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. +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. ## Translation diff --git a/Sources/Conjure/NotesController.swift b/Sources/Conjure/NotesController.swift index 1590f2a..464a927 100644 --- a/Sources/Conjure/NotesController.swift +++ b/Sources/Conjure/NotesController.swift @@ -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 enum MarkdownHighlighting { @@ -677,6 +686,9 @@ enum MarkdownHighlighting { let size = image.size 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 !(cell is RoundedImageAttachmentCell) { + attachment.attachmentCell = RoundedImageAttachmentCell(imageCell: image) + } } storage.removeAttribute(.strikethroughStyle, range: range) storage.addAttribute(hiddenMarkup, value: false, range: range) diff --git a/TODO.md b/TODO.md index 3427024..da810d3 100644 --- a/TODO.md +++ b/TODO.md @@ -6,6 +6,7 @@ - this should keep window placement invariant of corner window sizes - [x] conjure notes: - [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. - track down the cause and fix it. \ No newline at end of file diff --git a/Tests/ConjureKitTests/NotesMarkupTests.swift b/Tests/ConjureKitTests/NotesMarkupTests.swift index 5ed50f6..2475b59 100644 --- a/Tests/ConjureKitTests/NotesMarkupTests.swift +++ b/Tests/ConjureKitTests/NotesMarkupTests.swift @@ -41,6 +41,68 @@ func noteFormattingHidesSyntaxButPreservesMarkdownAndLiteralCode() throws { #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 func hiddenNoteMarkupKeepsTypingDeletionFormattingAndUndoAtVisibleBoundaries() throws { _ = NSApplication.shared