Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-native.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ concurrency:

jobs:
native:
uses: SimonCropp/PlatformHeads/.github/workflows/build-native.yml@0.3.0
uses: SimonCropp/PlatformHeads/.github/workflows/build-native.yml@0.4.0
permissions:
contents: write
pull-requests: write
Expand Down
17 changes: 12 additions & 5 deletions claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,8 +265,8 @@ the comment there about not caching "nothing staged" asks for.
grid. The cost is that none of them exists in `deview_capture`, which makes no window — hence
`PixelTests.ContextMenu` being skipped there, and the scroller taking its strip out of the
renderer only when a window exists. AppKit's own loops run inside the pump, so the managed loop
waits them out. The scroller is a `PaneScroller`, which follows a drag of its knob as ordinary
events for that reason, where AppKit's tracking loop left the panes still until the knob was let
waits them out. The scroller is PlatformHeadsMac's `KnobScroller`, which follows a drag of its
knob as ordinary events for that reason, where AppKit's tracking loop left the panes still until the knob was let
go. A live resize is the same kind of loop and still draws the rows sliced for the old size:
doing better takes a frame callback the C ABI does not have. Keys and clicks are queued in
`Runtime` and handed over one a poll, as the WinForms head does and for its reason. The footer
Expand Down Expand Up @@ -615,10 +615,17 @@ the comment there about not caching "nothing staged" asks for.
**Native shim (`native/`), used by the Mac and Linux heads only:**
- The build and packaging of the heads is PlatformHeads' (`D:\Code\PlatformHeads`,
github.com/SimonCropp/PlatformHeads), which BuildMonitor shares: it was worked out here and
copied there, and fixes made here did not reach the copy. Five pins name one tag of it and move
copied there, and fixes made here did not reach the copy. Six pins name one tag of it and move
together: the `uses:` of `.github/workflows/build-native.yml`, the `GIT_TAG` in
`native/CMakeLists.txt`, and `PlatformHeads`, `PlatformHeads.Core` and
`PlatformHeads.WindowsForms` in `Directory.Packages.props`. The workflow is its
`native/CMakeLists.txt`, `PlatformHeads`, `PlatformHeads.Core` and
`PlatformHeads.WindowsForms` in `Directory.Packages.props`, and the `PlatformHeads` package
in `native/swift/Package.swift`. So is the native runtime under both heads' drawing: the C++
one `deview.cpp` calls as `ph::` (the window, GLFW's input as it happened, which frames are
built and drawn, the renderer, the fonts and fontconfig, and the label helpers) and the Swift
`PlatformHeadsMac` the AppKit head imports (`HeadWindow`, `EventPump`, `TerminateGuard`,
`NapGuard`, `KnobScroller`, `ScrollerTravel`, `WheelNotches`, `ToolTipRegions`, `StringBlob`,
`Capture`, `Clipboard` and `WindowPlacement`). Each was worked out here and copied to
BuildMonitor; a fix to any of them goes there now. The workflow is its
reusable `build-native`. Its CMake module pins raylib and Dear ImGui, carries the raylib 6.0
option workarounds and links the library, and `native/CMakeLists.txt` names only the picture
formats, the source file and the two libraries the viewer links beyond raylib. Its package's
Expand Down
2 changes: 1 addition & 1 deletion native/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ include(FetchContent)
FetchContent_Declare(
platformheads
GIT_REPOSITORY https://github.com/SimonCropp/PlatformHeads.git
GIT_TAG 0.3.0
GIT_TAG 0.4.0
GIT_SHALLOW ON)
FetchContent_MakeAvailable(platformheads)
include(${platformheads_SOURCE_DIR}/cmake/PlatformHeads.cmake)
Expand Down
2,104 changes: 186 additions & 1,918 deletions native/src/deview.cpp

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion native/swift/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,21 @@ let package = Package(
products: [
.library(name: "diffengine_viewer", type: .dynamic, targets: ["Deview"])
],
dependencies: [
// What every AppKit head does underneath its drawing, shared with BuildMonitor. The sixth
// pin of PlatformHeads, moved with the other five.
.package(url: "https://github.com/SimonCropp/PlatformHeads", exact: "0.4.0")
],
targets: [
// Exists only to import the ABI. Swift does not guarantee struct layout, so the structs
// have to come from the C header rather than being redeclared here.
.target(name: "CDeview"),
.target(
name: "Deview",
dependencies: ["CDeview"],
dependencies: [
"CDeview",
.product(name: "PlatformHeadsMac", package: "PlatformHeads")
],
linkerSettings: [
.linkedFramework("AppKit"),
.linkedFramework("CoreText"),
Expand Down
62 changes: 13 additions & 49 deletions native/swift/Sources/Deview/Exports.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import AppKit
import CDeview
import CoreGraphics
import Foundation
import ImageIO
import PlatformHeadsMac

/// The eleven entry points of native/include/deview.h, implemented over AppKit and Core Text.
///
Expand Down Expand Up @@ -105,12 +104,8 @@ public func deviewSetClipboard(_ text: UnsafePointer<CChar>?) {
return
}

// Cleared first: NSPasteboard keeps whatever types were declared before, so writing a string
// over an image would otherwise leave both on the board and paste the wrong one.
autoreleasepool {
let board = NSPasteboard.general
board.clearContents()
board.setString(String(cString: text), forType: .string)
Clipboard.set(String(cString: text))
}
}

Expand Down Expand Up @@ -153,13 +148,8 @@ public func deviewShutdown() {
}
}

/// Renders into a bitmap of this side's own making rather than asking the view for one.
///
/// `bitmapImageRepForCachingDisplay` would inherit the window's backing scale, which is 2 on a
/// Retina machine and 1 elsewhere, so a committed baseline would only ever match on the kind of
/// display that produced it. Everything that varies is pinned here instead: scale, colour space,
/// and the six font smoothing and subpixel switches. No window is needed, which also means the
/// snapshot tests do not need a window server.
/// Renders into a bitmap of this side's own making rather than asking the view for one, with
/// everything that varies pinned, so a committed baseline matches on any display: see `Capture`.
@_cdecl("deview_capture")
public func deviewCapture(
_ screen: UnsafePointer<DeviewScreen>?,
Expand All @@ -176,44 +166,18 @@ private func capture(
_ width: Int32,
_ height: Int32,
_ pngPath: UnsafePointer<CChar>?) -> Int32 {
guard let screen,
let pngPath,
let renderer = Runtime.shared.renderer,
let space = CGColorSpace(name: CGColorSpace.sRGB),
let context = CGContext(
data: nil,
width: Int(width),
height: Int(height),
bitsPerComponent: 8,
bytesPerRow: 0,
space: space,
bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue)
else {
guard let screen, let pngPath, let renderer = Runtime.shared.renderer else {
return 0
}

context.setAllowsFontSmoothing(false)
context.setShouldSmoothFonts(false)
context.setAllowsFontSubpixelPositioning(false)
context.setShouldSubpixelPositionFonts(false)
context.setAllowsFontSubpixelQuantization(false)
context.setShouldSubpixelQuantizeFonts(false)

renderer.draw(
Frame.decode(screen),
in: context,
size: CGSize(width: CGFloat(width), height: CGFloat(height)),
capturing: true)

guard let image = context.makeImage() else {
return 0
}

let url = URL(fileURLWithPath: String(cString: pngPath)) as CFURL
guard let destination = CGImageDestinationCreateWithURL(url, "public.png" as CFString, 1, nil) else {
return 0
let frame = Frame.decode(screen)
let written = Capture.png(width: width, height: height, path: String(cString: pngPath)) { context in
_ = renderer.draw(
frame,
in: context,
size: CGSize(width: CGFloat(width), height: CGFloat(height)),
capturing: true)
}

CGImageDestinationAddImage(destination, image, nil)
return CGImageDestinationFinalize(destination) ? 1 : 0
return written ? 1 : 0
}
15 changes: 3 additions & 12 deletions native/swift/Sources/Deview/Frame.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import CDeview
import Foundation
import PlatformHeadsMac

/// One frame, decoded out of the flat description the managed side hands over.
///
Expand Down Expand Up @@ -196,18 +197,8 @@ struct Frame: Equatable {
return pane
}

/// Every offset is validated against the blob rather than trusted. This is the boundary the
/// managed side reaches across, and a bad length here would be a read past the end of it.
/// Every offset is validated against the blob rather than trusted: see `StringBlob`.
private static func string(_ screen: DeviewScreen, _ offset: Int32, _ length: Int32) -> String {
guard length > 0,
offset >= 0,
let base = screen.strings,
Int(offset) + Int(length) <= Int(screen.stringsLength)
else {
return ""
}

let buffer = UnsafeBufferPointer(start: base + Int(offset), count: Int(length))
return String(decoding: buffer, as: UTF8.self)
StringBlob(screen.strings, length: screen.stringsLength).text(offset: offset, length: length)
}
}
Loading
Loading