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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ The CodeMirror decoration and statistics interaction patterns are adapted from [

`DisplayList.description` prints identity effects, empty property effects, and platform effects with the same empty `(effect …)` form, while `minimalDescription` encodes them differently. Because the source text has already discarded that distinction, the converter renders an empty effect as identity and calls out the ambiguity in the interface.

### Mask input subtrees

SwiftUI prints the mask input DisplayList recursively in `description`, but prints only `M`
for the mask effect in `minimalDescription`. The items that follow `M` inside `E` are the masked
content, not the mask input. The converter follows this format. For example, identities repeated
only inside two mask inputs will not appear in the compact output or its item statistics.

The `M` source mapping covers the full `(mask …)` expression, including its input subtree.
Inspect that source range to check mask identities. Reverse conversion uses `(mask *)`, because
the compact encoding does not contain the input tree. Shared links also retain only the compact
encoding, so use the full description when investigating repeated identities.

## Local development

Prerequisites:
Expand Down
2 changes: 1 addition & 1 deletion Sources/DisplayListDescription/EncodingReference.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public struct DisplayListEncodingReference: Equatable, Sendable {
.init(id: "effect.opacity", token: "O", name: "Opacity", detail: "An opacity effect; the numeric value is omitted.", category: .effect),
.init(id: "effect.blend-mode", token: "B", name: "Blend mode", detail: "A blend-mode effect. B is context-dependent: inside E it is not backdrop content.", category: .effect),
.init(id: "effect.clip", token: "C", name: "Clip", detail: "A clipping path and style. C is context-dependent: inside E it is not color content.", category: .effect),
.init(id: "effect.mask", token: "M", name: "Mask", detail: "A mask effect.", category: .effect),
.init(id: "effect.mask", token: "M", name: "Mask", detail: "The mask input subtree and its item identities are omitted. Following items in E are the masked content. Inspect the full source to check mask identities.", category: .effect),
.init(id: "effect.transform", token: "T", name: "Transform", detail: "A geometry transform. T is context-dependent: inside E it is not text content.", category: .effect),
.init(id: "effect.filter", token: "F", name: "Graphics filter", detail: "Any graphics filter; the specific filter parameters are omitted.", category: .effect),
.init(id: "effect.animation", token: "AN", name: "Animation", detail: "An animation effect.", category: .effect),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,58 @@ final class DisplayListDescriptionConverterTests: XCTestCase {
XCTAssertTrue(result.usedEncodingIDs.contains("effect.view"))
}

func testMaskInputsAreOmittedButRemainInSourceMappings() throws {
// The full printer can place options before or after the mask input items.
for optionsFirst in [true, false] {
let options = "(options ClipOptions(rawValue: 0))"
func maskedItem(identity: Int, maskIdentity: Int, contentIdentity: Int) -> String {
"""
(item #:identity \(identity) #:version 1
(effect
(mask
\(optionsFirst ? options : "")
(item #:identity \(maskIdentity) #:version 1
(effect
(item #:identity 815 #:version 1
(effect #:blend-mode normal
(item #:identity 814 #:version 1 (content-seed 1) (shape *))))
(item #:identity 816 #:version 1
(effect (transform *)
(item #:identity 817 #:version 1
(effect (transform *)
(item #:identity 819 #:version 1
(effect #:blend-mode normal
(item #:identity 818 #:version 1 (content-seed 1) (shape *))))))))))
\(optionsFirst ? "" : options))
(item #:identity \(contentIdentity) #:version 1 (content-seed 1) (color *))))
"""
}
let description = """
(display-list
\(maskedItem(identity: 823, maskIdentity: 820, contentIdentity: 821))
\(maskedItem(identity: 834, maskIdentity: 831, contentIdentity: 832)))
"""

let result = try DisplayListDescriptionConverter.convert(description)

XCTAssertEqual(result.minimalDescription, "(DL(I:823(E M(I:821 C)))(I:834(E M(I:832 C))))")
XCTAssertEqual(result.spans.filter { $0.encodingID == "structure.item" }.count, 4)
let masks = result.spans.filter { $0.encodingID == "effect.mask" }
XCTAssertEqual(masks.count, 2)
for mask in masks {
let source = utf16Slice(description, mask.sourceStart, mask.sourceEnd)
XCTAssertTrue(source.hasPrefix("(mask"))
XCTAssertTrue(source.contains(options))
for identity in 814...819 {
XCTAssertTrue(source.contains("#:identity \(identity) "))
}
XCTAssertFalse(source.contains("(color"))
XCTAssertEqual(utf16Slice(result.minimalDescription, mask.outputStart, mask.outputEnd), "M")
}
XCTAssertEqual(Set(masks.map(\.occurrenceID)).count, 2)
}
}

func testConvertsStates() throws {
let description = """
(display-list
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,21 @@ final class DisplayListMinimalDescriptionConverterTests: XCTestCase {
)
}

func testReconstructsMaskInputAsUnknownAndPreservesMaskedContent() throws {
let minimalDescription = "(DL(I:823(E M(I:821 C)))(I:834(E M(I:832 C))))"

let result = try DisplayListMinimalDescriptionConverter.convert(minimalDescription)

XCTAssertEqual(result.description.components(separatedBy: "(mask *)").count - 1, 2)
XCTAssertEqual(result.description.components(separatedBy: "(item ").count - 1, 4)
XCTAssertTrue(result.description.contains("#:identity 821 "))
XCTAssertTrue(result.description.contains("#:identity 832 "))
XCTAssertEqual(
try DisplayListDescriptionConverter.convert(result.description).minimalDescription,
minimalDescription
)
}

func testRejectsNonMinimalDescriptionRoot() {
XCTAssertThrowsError(try DisplayListMinimalDescriptionConverter.convert("(display-list)")) { error in
XCTAssertEqual(
Expand Down
Loading