Skip to content

Commit e8092bf

Browse files
authored
fix(image): correct named symbol rendering (#1213)
1 parent 02911a0 commit e8092bf

9 files changed

Lines changed: 226 additions & 46 deletions

File tree

‎.github/actions/uitests/action.yml‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,13 @@ runs:
179179
test_arguments=(-workspace Example.xcworkspace)
180180
if [[ "${{ inputs.test-suite }}" == "InteractionTests" ]]; then
181181
test_arguments+=(-parallel-testing-enabled NO)
182+
elif [[ "${{ inputs.platform }}" == "macos" &&
183+
"$UITEST_ONLY_TESTING" =~ ^[^/]+/[^/]+$ ]]; then
184+
# FIXME:
185+
# Xcode 26.6 can dispatch a macOS suite filter twice. Affected recording
186+
# runs can terminate with SIGTRAP and leave xcresult without Info.plist.
187+
# Swift Testing's .serialized trait does not prevent duplicate dispatch.
188+
test_arguments+=(-parallel-testing-enabled NO)
182189
fi
183190
if [[ -n "$UITEST_ONLY_TESTING" ]]; then
184191
test_arguments+=("-only-testing:$UITEST_ONLY_TESTING")

‎Example/Tests/OpenSwiftUIUITests/View/Image/NamedImageUITests.swift‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@ import Testing
77
@testable import TestingHost
88

99
@MainActor
10-
@Suite(
11-
.snapshots(record: .never, diffTool: diffTool),
12-
.disabled("#817")
13-
)
10+
@Suite(.snapshots(record: .never, diffTool: diffTool))
1411
struct NamedImageUITests {
1512
@Test("Test named image of logo with resizable")
1613
func decorativeLogo() {

‎Example/Tuist/Package.resolved‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Package.resolved‎

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1095,7 +1095,7 @@ extension DisplayList {
10951095
contentOffset: CGSize,
10961096
version: DisplayList.Version
10971097
) -> Bool {
1098-
_openSwiftUIEmptyStub()
1098+
_openSwiftUIUnimplementedWarning()
10991099
return false
11001100
}
11011101
}

‎Sources/OpenSwiftUICore/Render/InterpolatableContent.swift‎

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -182,11 +182,41 @@ private struct InterpolatedDisplayList<Content>: StatefulRule, AsyncAttribute wh
182182
_openSwiftUIUnimplementedWarning()
183183
}
184184
// FIXME
185-
let result: [DisplayList.Item] = list?.items.map {
186-
var item = $0
187-
item.frame.origin = .init(position - containerPosition)
188-
return item
189-
} ?? []
190-
value = DisplayList(result)
185+
let offset = animatedPosition - containerPosition
186+
let contentOrigin: CGPoint
187+
let contentOffset: CGSize
188+
if animatesSize {
189+
contentOrigin = CGPoint(offset)
190+
contentOffset = .zero
191+
} else {
192+
let position = position.roundedToNearestOrUp(toMultipleOf: pixelLength)
193+
contentOrigin = CGPoint(position - containerPosition)
194+
contentOffset = contentOrigin - CGPoint(offset)
195+
}
196+
var result = list ?? DisplayList()
197+
if defersRender {
198+
let effect = DisplayList.Effect.interpolatorRoot(
199+
group,
200+
contentOrigin: contentOrigin,
201+
contentOffset: contentOffset
202+
)
203+
let item = DisplayList.Item(
204+
.effect(effect, result),
205+
frame: .zero,
206+
identity: .none,
207+
version: version
208+
)
209+
result = DisplayList(item)
210+
} else {
211+
_ = group.rewriteDisplayList(
212+
&result,
213+
time: $time,
214+
contentOrigin: contentOrigin,
215+
contentOffset: contentOffset,
216+
version: version
217+
)
218+
}
219+
result.translate(by: offset, version: version)
220+
value = result
191221
}
192222
}

‎Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleRendering.swift‎

Lines changed: 154 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -265,17 +265,161 @@ package struct _ShapeStyle_RenderedShape {
265265
styles: ShapeStyle.Pack,
266266
layers: inout ShapeStyle.RenderedLayers
267267
) {
268-
// TODO: RBSymbolAnimator/RBSymbolUpdate
269-
// We use a plain implementation for now
270-
_openSwiftUIUnimplementedWarning()
271-
let style = styles[name, 0]
272-
layers.beginLayer(
273-
id: .styled(name, 0),
274-
style: style,
275-
shape: &self
268+
#if canImport(Darwin)
269+
func level(for style: ORBSymbolStyle) -> Int? {
270+
switch style {
271+
case .primary: 0
272+
case .secondary: 1
273+
case .tertiary: 2
274+
case .quaternary: 3
275+
case .quinary: 4
276+
default: nil
277+
}
278+
}
279+
280+
let slice = styles[name]
281+
var flags: ORBSymbolUpdateFlags
282+
switch orientation {
283+
case .up: flags = []
284+
case .upMirrored: flags = [.flipX]
285+
case .down: flags = [.flipX, .flipY]
286+
case .downMirrored: flags = [.flipY]
287+
case .left: flags = [.swapAxes]
288+
case .leftMirrored: flags = [.swapAxes, .flipX]
289+
case .right: flags = [.swapAxes, .flipX, .flipY]
290+
case .rightMirrored: flags = [.swapAxes, .flipY]
291+
}
292+
flags.insert(.mergeLayers)
293+
if !glyph.preservesVectorRepresentation {
294+
flags.insert(.applyVerticalOffset)
295+
}
296+
var position = frame.origin
297+
var size = frame.size
298+
let update = glyph.animator.beginUpdate(
299+
with: .unspecified,
300+
position: &position,
301+
size: &size,
302+
flags: flags
276303
)
277-
render(style: style)
278-
layers.endLayer(shape: &self)
304+
defer { glyph.animator.end(update) }
305+
let nextTime = update.nextTime
306+
var imageScale = 1.0
307+
var imageOpacity: Float = 1.0
308+
if !glyph.preservesVectorRepresentation,
309+
!slice.isEmpty,
310+
let image = update.templateImage(scale: &imageScale, opacity: &imageOpacity) {
311+
shape = .image(GraphicsImage(
312+
contents: .cgImage(image),
313+
scale: imageScale,
314+
unrotatedPixelSize: CGSize(width: image.width, height: image.height),
315+
orientation: orientation,
316+
isTemplate: true
317+
))
318+
let style = slice[0].applyingOpacity(imageOpacity)
319+
layers.beginLayer(id: .styled(name, 0), style: style, shape: &self)
320+
render(style: style)
321+
layers.endLayer(shape: &self)
322+
return
323+
}
324+
325+
let groupCount = update.groupCount
326+
// Style-mask bit 12 disables the single-layer path. Its meaning is not known.
327+
let isSingleLayer = update.styleMask & (1 << 12) == 0
328+
&& groupCount == 1 && update.groupSize(group: 0) == 1
329+
if !isSingleLayer, slice.allColors {
330+
let list = ORBDisplayList()
331+
update.draw(in: list.state) { symbolStyle, colorName in
332+
let color: Color.Resolved?
333+
if let level = level(for: symbolStyle), !slice.isEmpty {
334+
color = slice[min(level, slice.count - 1)].color
335+
} else if symbolStyle == .namedColor {
336+
color = colorName.flatMap { styles[$0] }
337+
} else {
338+
color = nil
339+
}
340+
guard let color else { return .null }
341+
return ORBColor(red: color.red, green: color.green, blue: color.blue, alpha: color.opacity)
342+
}
343+
layers.beginLayer(id: .unstyled, style: nil, shape: &self)
344+
item = makeDrawingItem(
345+
from: list.moveContents(),
346+
offset: CGSize(item.frame.origin),
347+
flags: []
348+
)
349+
if nextTime.isFinite {
350+
layerNeedes.insert(.drawingGroup)
351+
}
352+
layers.endLayer(shape: &self)
353+
return
354+
}
355+
356+
var drawingList: ORBDisplayList?
357+
for group in 0 ..< groupCount {
358+
for index in 0 ..< update.groupSize(group: group) {
359+
let layer = ORBSymbolUpdateLayer(update: update, group: group, index: index)
360+
let opacity = layer.drawingOpacity
361+
guard opacity > 0 else { continue }
362+
let style: ShapeStyle.Pack.Style
363+
let id: ShapeStyle.LayerID
364+
if let level = level(for: layer.style), !slice.isEmpty {
365+
style = slice[min(level, slice.count - 1)].applyingOpacity(opacity)
366+
id = .styled(name, UInt16(level))
367+
} else if layer.style == .namedColor, let colorName = layer.colorName {
368+
let color: Color.Resolved
369+
if let resolved = styles[colorName] {
370+
color = resolved
371+
} else if let proposed = layer.defaultColor {
372+
color = Color.Resolved(failableCGColor: proposed) ?? .clear
373+
} else {
374+
continue
375+
}
376+
style = ShapeStyle.Pack.Style(.color(color)).applyingOpacity(opacity)
377+
id = .named(colorName)
378+
} else {
379+
continue
380+
}
381+
layers.beginLayer(id: id, style: style, shape: &self)
382+
var transform = CGAffineTransform()
383+
let path = glyph.preservesVectorRepresentation
384+
? ORBPath.null
385+
: layer.path(transform: &transform, flags: [.includeClips])
386+
if !path.isNull {
387+
// Layer paths belong to the update; the shape takes an owned copy.
388+
shape = .path(
389+
Path(box: Path.PathBox(takingPath: path.copy(transform: &transform))),
390+
FillStyle()
391+
)
392+
} else {
393+
let list: ORBDisplayList
394+
if let drawingList {
395+
list = drawingList
396+
} else {
397+
list = ORBDisplayList()
398+
drawingList = list
399+
}
400+
layer.draw(
401+
in: list.state,
402+
color: .white,
403+
opacity: 1,
404+
blendMode: .init(rawValue: 0),
405+
flags: [.includeClips]
406+
)
407+
shape = .alphaMask(makeDrawingItem(
408+
from: list.moveContents(),
409+
offset: .zero,
410+
flags: [.alphaOnly]
411+
))
412+
if nextTime.isFinite {
413+
layerNeedes.insert(.drawingGroup)
414+
}
415+
}
416+
render(style: style)
417+
layers.endLayer(shape: &self)
418+
}
419+
}
420+
#else
421+
_openSwiftUIPlatformUnimplementedFailure()
422+
#endif
279423
}
280424

281425
private mutating func renderUnstyledImage(

‎Sources/OpenSwiftUICore/View/Image/GraphicsImage.swift‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ package struct ResolvedVectorGlyph: Equatable {
201201
#endif
202202
animator.variableValue = variableValue
203203
animator.flipsRightToLeft = flipsRightToLeft
204-
animator.renderingMode = context.effectiveSymbolRenderingMode?.rbRenderingMode ?? 255
204+
animator.renderingMode = context.effectiveSymbolRenderingMode?.rbRenderingMode ?? .unspecified
205205
let direction = context.environment.layoutDirection
206206
let version = animator.version
207207
let options = context.options
@@ -237,9 +237,9 @@ package struct ResolvedVectorGlyph: Equatable {
237237
}
238238

239239
package var resolvedRenderingMode: SymbolRenderingMode.Storage? {
240-
#if OPENSWIFTUI_LINK_COREUI && OPENSWIFTUI_RENDERBOX
240+
#if OPENSWIFTUI_LINK_COREUI
241241
let rbMode = animator.renderingMode
242-
guard rbMode == 0 else {
242+
guard rbMode == .preferred else {
243243
return SymbolRenderingMode(rbRenderingMode: rbMode)?.storage
244244
}
245245
guard let glyph = animator.glyph else {

‎Sources/OpenSwiftUICore/View/Image/SymbolRenderingMode.swift‎

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
// Status: Complete
77
// ID: FE3FF33C1D9A704A22DF5519034B23F2 (SwiftUICore)
88

9+
package import OpenRenderBoxShims
10+
911
// MARK: - SymbolRenderingMode
1012

1113
/// A symbol rendering mode.
@@ -101,29 +103,29 @@ public struct SymbolRenderingMode: Sendable {
101103
// MARK: - SymbolRenderingMode + RB Rendering Mode
102104

103105
extension SymbolRenderingMode {
104-
package var rbRenderingMode: UInt32 {
106+
package var rbRenderingMode: ORBSymbolRenderingMode {
105107
switch storage {
106-
case .monochrome: 1
107-
case .multicolor: 2
108-
case .hierarchical: 3
109-
case .palette: 4
110-
case .preferred: 0
111-
case .hierarchicalUnlessSlashed: 128
112-
case .hierarchicalSlashBadge: 129
113-
case .paletteSlashBadge: 130
108+
case .monochrome: .monochrome
109+
case .multicolor: .multicolor
110+
case .hierarchical: .hierarchical
111+
case .palette: .palette
112+
case .preferred: .preferred
113+
case .hierarchicalUnlessSlashed: .hierarchicalUnlessSlashed
114+
case .hierarchicalSlashBadge: .hierarchicalSlashBadge
115+
case .paletteSlashBadge: .paletteSlashBadge
114116
}
115117
}
116118

117-
package init?(rbRenderingMode mode: UInt32) {
119+
package init?(rbRenderingMode mode: ORBSymbolRenderingMode) {
118120
switch mode {
119-
case 0: self = .preferred
120-
case 1: self = .monochrome
121-
case 2: self = .multicolor
122-
case 3: self = .hierarchical
123-
case 4: self = .palette
124-
case 128: self = .hierarchicalUnlessSlashed
125-
case 129: self = .hierarchicalSlashBadge
126-
case 130: self = .paletteSlashBadge
121+
case .preferred: self = .preferred
122+
case .monochrome: self = .monochrome
123+
case .multicolor: self = .multicolor
124+
case .hierarchical: self = .hierarchical
125+
case .palette: self = .palette
126+
case .hierarchicalUnlessSlashed: self = .hierarchicalUnlessSlashed
127+
case .hierarchicalSlashBadge: self = .hierarchicalSlashBadge
128+
case .paletteSlashBadge: self = .paletteSlashBadge
127129
default: return nil
128130
}
129131
}

0 commit comments

Comments
 (0)