diff --git a/.github/actions/uitests/action.yml b/.github/actions/uitests/action.yml index 2f9e77a7d..302fcf65d 100644 --- a/.github/actions/uitests/action.yml +++ b/.github/actions/uitests/action.yml @@ -179,6 +179,13 @@ runs: test_arguments=(-workspace Example.xcworkspace) if [[ "${{ inputs.test-suite }}" == "InteractionTests" ]]; then test_arguments+=(-parallel-testing-enabled NO) + elif [[ "${{ inputs.platform }}" == "macos" && + "$UITEST_ONLY_TESTING" =~ ^[^/]+/[^/]+$ ]]; then + # FIXME: + # Xcode 26.6 can dispatch a macOS suite filter twice. Affected recording + # runs can terminate with SIGTRAP and leave xcresult without Info.plist. + # Swift Testing's .serialized trait does not prevent duplicate dispatch. + test_arguments+=(-parallel-testing-enabled NO) fi if [[ -n "$UITEST_ONLY_TESTING" ]]; then test_arguments+=("-only-testing:$UITEST_ONLY_TESTING") diff --git a/Example/Tests/OpenSwiftUIUITests/View/Image/NamedImageUITests.swift b/Example/Tests/OpenSwiftUIUITests/View/Image/NamedImageUITests.swift index 54d8c98ff..8a92e084e 100644 --- a/Example/Tests/OpenSwiftUIUITests/View/Image/NamedImageUITests.swift +++ b/Example/Tests/OpenSwiftUIUITests/View/Image/NamedImageUITests.swift @@ -7,10 +7,7 @@ import Testing @testable import TestingHost @MainActor -@Suite( - .snapshots(record: .never, diffTool: diffTool), - .disabled("#817") -) +@Suite(.snapshots(record: .never, diffTool: diffTool)) struct NamedImageUITests { @Test("Test named image of logo with resizable") func decorativeLogo() { diff --git a/Example/Tuist/Package.resolved b/Example/Tuist/Package.resolved index 17467ec4d..c0be68774 100644 --- a/Example/Tuist/Package.resolved +++ b/Example/Tuist/Package.resolved @@ -25,7 +25,7 @@ "location" : "https://github.com/OpenSwiftUIProject/OpenCoreGraphics", "state" : { "branch" : "main", - "revision" : "1d4657f665921ba1f147eef930997b5c3bd02082" + "revision" : "0d7a0d0d9e236cbf9dea2f10a5b0b88c8789d9e4" } }, { diff --git a/Package.resolved b/Package.resolved index b011b9735..9f844a409 100644 --- a/Package.resolved +++ b/Package.resolved @@ -7,7 +7,7 @@ "location" : "https://github.com/OpenSwiftUIProject/DarwinPrivateFrameworks.git", "state" : { "branch" : "main", - "revision" : "a256dadaa7531c32b706894b52f75696f2bf0bc0" + "revision" : "3690622baedc4daf3cf9f9e32b38ede607fc7f11" } }, { @@ -25,7 +25,7 @@ "location" : "https://github.com/OpenSwiftUIProject/OpenCoreGraphics", "state" : { "branch" : "main", - "revision" : "1d4657f665921ba1f147eef930997b5c3bd02082" + "revision" : "0d7a0d0d9e236cbf9dea2f10a5b0b88c8789d9e4" } }, { @@ -43,7 +43,7 @@ "location" : "https://github.com/OpenSwiftUIProject/OpenRenderBox", "state" : { "branch" : "main", - "revision" : "e5ad62adf14b6fd3546a4a6f5c87f4ece1b893c0" + "revision" : "f9a39649347134aa1801ff6ce973506a5b3a9ee0" } }, { diff --git a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift index 0b5e35d4d..510440d0c 100644 --- a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift +++ b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift @@ -1095,7 +1095,7 @@ extension DisplayList { contentOffset: CGSize, version: DisplayList.Version ) -> Bool { - _openSwiftUIEmptyStub() + _openSwiftUIUnimplementedWarning() return false } } diff --git a/Sources/OpenSwiftUICore/Render/InterpolatableContent.swift b/Sources/OpenSwiftUICore/Render/InterpolatableContent.swift index 9e702d246..35ae064f9 100644 --- a/Sources/OpenSwiftUICore/Render/InterpolatableContent.swift +++ b/Sources/OpenSwiftUICore/Render/InterpolatableContent.swift @@ -182,11 +182,41 @@ private struct InterpolatedDisplayList: StatefulRule, AsyncAttribute wh _openSwiftUIUnimplementedWarning() } // FIXME - let result: [DisplayList.Item] = list?.items.map { - var item = $0 - item.frame.origin = .init(position - containerPosition) - return item - } ?? [] - value = DisplayList(result) + let offset = animatedPosition - containerPosition + let contentOrigin: CGPoint + let contentOffset: CGSize + if animatesSize { + contentOrigin = CGPoint(offset) + contentOffset = .zero + } else { + let position = position.roundedToNearestOrUp(toMultipleOf: pixelLength) + contentOrigin = CGPoint(position - containerPosition) + contentOffset = contentOrigin - CGPoint(offset) + } + var result = list ?? DisplayList() + if defersRender { + let effect = DisplayList.Effect.interpolatorRoot( + group, + contentOrigin: contentOrigin, + contentOffset: contentOffset + ) + let item = DisplayList.Item( + .effect(effect, result), + frame: .zero, + identity: .none, + version: version + ) + result = DisplayList(item) + } else { + _ = group.rewriteDisplayList( + &result, + time: $time, + contentOrigin: contentOrigin, + contentOffset: contentOffset, + version: version + ) + } + result.translate(by: offset, version: version) + value = result } } diff --git a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleRendering.swift b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleRendering.swift index 4c141bf76..69ca8a818 100644 --- a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleRendering.swift +++ b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleRendering.swift @@ -265,17 +265,161 @@ package struct _ShapeStyle_RenderedShape { styles: ShapeStyle.Pack, layers: inout ShapeStyle.RenderedLayers ) { - // TODO: RBSymbolAnimator/RBSymbolUpdate - // We use a plain implementation for now - _openSwiftUIUnimplementedWarning() - let style = styles[name, 0] - layers.beginLayer( - id: .styled(name, 0), - style: style, - shape: &self + #if canImport(Darwin) + func level(for style: ORBSymbolStyle) -> Int? { + switch style { + case .primary: 0 + case .secondary: 1 + case .tertiary: 2 + case .quaternary: 3 + case .quinary: 4 + default: nil + } + } + + let slice = styles[name] + var flags: ORBSymbolUpdateFlags + switch orientation { + case .up: flags = [] + case .upMirrored: flags = [.flipX] + case .down: flags = [.flipX, .flipY] + case .downMirrored: flags = [.flipY] + case .left: flags = [.swapAxes] + case .leftMirrored: flags = [.swapAxes, .flipX] + case .right: flags = [.swapAxes, .flipX, .flipY] + case .rightMirrored: flags = [.swapAxes, .flipY] + } + flags.insert(.mergeLayers) + if !glyph.preservesVectorRepresentation { + flags.insert(.applyVerticalOffset) + } + var position = frame.origin + var size = frame.size + let update = glyph.animator.beginUpdate( + with: .unspecified, + position: &position, + size: &size, + flags: flags ) - render(style: style) - layers.endLayer(shape: &self) + defer { glyph.animator.end(update) } + let nextTime = update.nextTime + var imageScale = 1.0 + var imageOpacity: Float = 1.0 + if !glyph.preservesVectorRepresentation, + !slice.isEmpty, + let image = update.templateImage(scale: &imageScale, opacity: &imageOpacity) { + shape = .image(GraphicsImage( + contents: .cgImage(image), + scale: imageScale, + unrotatedPixelSize: CGSize(width: image.width, height: image.height), + orientation: orientation, + isTemplate: true + )) + let style = slice[0].applyingOpacity(imageOpacity) + layers.beginLayer(id: .styled(name, 0), style: style, shape: &self) + render(style: style) + layers.endLayer(shape: &self) + return + } + + let groupCount = update.groupCount + // Style-mask bit 12 disables the single-layer path. Its meaning is not known. + let isSingleLayer = update.styleMask & (1 << 12) == 0 + && groupCount == 1 && update.groupSize(group: 0) == 1 + if !isSingleLayer, slice.allColors { + let list = ORBDisplayList() + update.draw(in: list.state) { symbolStyle, colorName in + let color: Color.Resolved? + if let level = level(for: symbolStyle), !slice.isEmpty { + color = slice[min(level, slice.count - 1)].color + } else if symbolStyle == .namedColor { + color = colorName.flatMap { styles[$0] } + } else { + color = nil + } + guard let color else { return .null } + return ORBColor(red: color.red, green: color.green, blue: color.blue, alpha: color.opacity) + } + layers.beginLayer(id: .unstyled, style: nil, shape: &self) + item = makeDrawingItem( + from: list.moveContents(), + offset: CGSize(item.frame.origin), + flags: [] + ) + if nextTime.isFinite { + layerNeedes.insert(.drawingGroup) + } + layers.endLayer(shape: &self) + return + } + + var drawingList: ORBDisplayList? + for group in 0 ..< groupCount { + for index in 0 ..< update.groupSize(group: group) { + let layer = ORBSymbolUpdateLayer(update: update, group: group, index: index) + let opacity = layer.drawingOpacity + guard opacity > 0 else { continue } + let style: ShapeStyle.Pack.Style + let id: ShapeStyle.LayerID + if let level = level(for: layer.style), !slice.isEmpty { + style = slice[min(level, slice.count - 1)].applyingOpacity(opacity) + id = .styled(name, UInt16(level)) + } else if layer.style == .namedColor, let colorName = layer.colorName { + let color: Color.Resolved + if let resolved = styles[colorName] { + color = resolved + } else if let proposed = layer.defaultColor { + color = Color.Resolved(failableCGColor: proposed) ?? .clear + } else { + continue + } + style = ShapeStyle.Pack.Style(.color(color)).applyingOpacity(opacity) + id = .named(colorName) + } else { + continue + } + layers.beginLayer(id: id, style: style, shape: &self) + var transform = CGAffineTransform() + let path = glyph.preservesVectorRepresentation + ? ORBPath.null + : layer.path(transform: &transform, flags: [.includeClips]) + if !path.isNull { + // Layer paths belong to the update; the shape takes an owned copy. + shape = .path( + Path(box: Path.PathBox(takingPath: path.copy(transform: &transform))), + FillStyle() + ) + } else { + let list: ORBDisplayList + if let drawingList { + list = drawingList + } else { + list = ORBDisplayList() + drawingList = list + } + layer.draw( + in: list.state, + color: .white, + opacity: 1, + blendMode: .init(rawValue: 0), + flags: [.includeClips] + ) + shape = .alphaMask(makeDrawingItem( + from: list.moveContents(), + offset: .zero, + flags: [.alphaOnly] + )) + if nextTime.isFinite { + layerNeedes.insert(.drawingGroup) + } + } + render(style: style) + layers.endLayer(shape: &self) + } + } + #else + _openSwiftUIPlatformUnimplementedFailure() + #endif } private mutating func renderUnstyledImage( diff --git a/Sources/OpenSwiftUICore/View/Image/GraphicsImage.swift b/Sources/OpenSwiftUICore/View/Image/GraphicsImage.swift index 38b02cc4b..2a534e433 100644 --- a/Sources/OpenSwiftUICore/View/Image/GraphicsImage.swift +++ b/Sources/OpenSwiftUICore/View/Image/GraphicsImage.swift @@ -201,7 +201,7 @@ package struct ResolvedVectorGlyph: Equatable { #endif animator.variableValue = variableValue animator.flipsRightToLeft = flipsRightToLeft - animator.renderingMode = context.effectiveSymbolRenderingMode?.rbRenderingMode ?? 255 + animator.renderingMode = context.effectiveSymbolRenderingMode?.rbRenderingMode ?? .unspecified let direction = context.environment.layoutDirection let version = animator.version let options = context.options @@ -237,9 +237,9 @@ package struct ResolvedVectorGlyph: Equatable { } package var resolvedRenderingMode: SymbolRenderingMode.Storage? { - #if OPENSWIFTUI_LINK_COREUI && OPENSWIFTUI_RENDERBOX + #if OPENSWIFTUI_LINK_COREUI let rbMode = animator.renderingMode - guard rbMode == 0 else { + guard rbMode == .preferred else { return SymbolRenderingMode(rbRenderingMode: rbMode)?.storage } guard let glyph = animator.glyph else { diff --git a/Sources/OpenSwiftUICore/View/Image/SymbolRenderingMode.swift b/Sources/OpenSwiftUICore/View/Image/SymbolRenderingMode.swift index 6b97e0029..4fc859ffd 100644 --- a/Sources/OpenSwiftUICore/View/Image/SymbolRenderingMode.swift +++ b/Sources/OpenSwiftUICore/View/Image/SymbolRenderingMode.swift @@ -6,6 +6,8 @@ // Status: Complete // ID: FE3FF33C1D9A704A22DF5519034B23F2 (SwiftUICore) +package import OpenRenderBoxShims + // MARK: - SymbolRenderingMode /// A symbol rendering mode. @@ -101,29 +103,29 @@ public struct SymbolRenderingMode: Sendable { // MARK: - SymbolRenderingMode + RB Rendering Mode extension SymbolRenderingMode { - package var rbRenderingMode: UInt32 { + package var rbRenderingMode: ORBSymbolRenderingMode { switch storage { - case .monochrome: 1 - case .multicolor: 2 - case .hierarchical: 3 - case .palette: 4 - case .preferred: 0 - case .hierarchicalUnlessSlashed: 128 - case .hierarchicalSlashBadge: 129 - case .paletteSlashBadge: 130 + case .monochrome: .monochrome + case .multicolor: .multicolor + case .hierarchical: .hierarchical + case .palette: .palette + case .preferred: .preferred + case .hierarchicalUnlessSlashed: .hierarchicalUnlessSlashed + case .hierarchicalSlashBadge: .hierarchicalSlashBadge + case .paletteSlashBadge: .paletteSlashBadge } } - package init?(rbRenderingMode mode: UInt32) { + package init?(rbRenderingMode mode: ORBSymbolRenderingMode) { switch mode { - case 0: self = .preferred - case 1: self = .monochrome - case 2: self = .multicolor - case 3: self = .hierarchical - case 4: self = .palette - case 128: self = .hierarchicalUnlessSlashed - case 129: self = .hierarchicalSlashBadge - case 130: self = .paletteSlashBadge + case .preferred: self = .preferred + case .monochrome: self = .monochrome + case .multicolor: self = .multicolor + case .hierarchical: self = .hierarchical + case .palette: self = .palette + case .hierarchicalUnlessSlashed: self = .hierarchicalUnlessSlashed + case .hierarchicalSlashBadge: self = .hierarchicalSlashBadge + case .paletteSlashBadge: self = .paletteSlashBadge default: return nil } }