diff --git a/wake/SharedUI/Graphics/Circle.swift b/wake/SharedUI/Graphics/Circle.swift new file mode 100644 index 0000000..11305dd --- /dev/null +++ b/wake/SharedUI/Graphics/Circle.swift @@ -0,0 +1,99 @@ +import SwiftUI + +/// 一个可配置的圆形视图封装,便于在项目中复用。 +/// 使用示例: +/// CircleView(diameter: 24, color: .orange, stroke: .white, lineWidth: 2) +public struct CircleView: View { + public var diameter: CGFloat + public var color: Color + public var stroke: Color? = nil + public var lineWidth: CGFloat = 1 + + public init( + diameter: CGFloat, + color: Color = .black, + stroke: Color? = nil, + lineWidth: CGFloat = 1 + ) { + self.diameter = diameter + self.color = color + self.stroke = stroke + self.lineWidth = lineWidth + } + + public var body: some View { + Group { + if let stroke = stroke, lineWidth > 0 { + Circle() + .fill(color) + .overlay( + Circle().stroke(stroke, lineWidth: lineWidth) + ) + } else { + Circle().fill(color) + } + } + .frame(width: diameter, height: diameter) + .accessibilityHidden(true) + } +} + +/// 水平渲染多个等尺寸圆形的小组件。 +public struct CircleRow: View { + public var count: Int + public var diameter: CGFloat + public var color: Color + public var spacing: CGFloat + + public init( + count: Int, + diameter: CGFloat, + color: Color = .black, + spacing: CGFloat = 8 + ) { + self.count = count + self.diameter = diameter + self.color = color + self.spacing = spacing + } + + public var body: some View { + HStack(spacing: spacing) { + ForEach(0..<(max(0, count)), id: \.self) { _ in + CircleView(diameter: diameter, color: color) + } + } + .accessibilityElement(children: .ignore) + } +} + +#if DEBUG +struct CircleView_Previews: PreviewProvider { + static var previews: some View { + Group { + CircleView(diameter: 40, color: .black, stroke: .white, lineWidth: 2) + .padding() + .previewDisplayName("Single Circle") + + ZStack { + Color(.black) + HStack { + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(.white) + .shadow(color: Color.black.opacity(0.06), radius: 6, x: 0, y: 2) + .overlay( + HStack { + CircleRow(count: 6, diameter: 10, color: .black, spacing: 6) + } + .padding(.horizontal, 16) + ) + .frame(height: 56) + .padding() + } + } + .previewDisplayName("Circle Row") + } + .previewLayout(.sizeThatFits) + } +} +#endif \ No newline at end of file diff --git a/wake/SharedUI/Graphics/Triangle.swift b/wake/SharedUI/Graphics/Triangle.swift new file mode 100644 index 0000000..3c5c2ae --- /dev/null +++ b/wake/SharedUI/Graphics/Triangle.swift @@ -0,0 +1,202 @@ +import SwiftUI + +/// 三角形方向 +public enum TriangleDirection: Equatable { + case up + case down + case left + case right +} + +/// 一个可插入(inset)的三角形 Shape,支持四个方向。 +struct TriangleShape: InsettableShape { + var direction: TriangleDirection = .up + var insetAmount: CGFloat = 0 + + var animatableData: CGFloat { + get { insetAmount } + set { insetAmount = newValue } + } + + func inset(by amount: CGFloat) -> TriangleShape { + var copy = self + copy.insetAmount += amount + return copy + } + + func path(in rect: CGRect) -> Path { + let r = rect.insetBy(dx: insetAmount, dy: insetAmount) + + // 顶点布局:根据方向决定三点位置 + let p1: CGPoint + let p2: CGPoint + let p3: CGPoint + + switch direction { + case .up: + p1 = CGPoint(x: r.midX, y: r.minY) + p2 = CGPoint(x: r.maxX, y: r.maxY) + p3 = CGPoint(x: r.minX, y: r.maxY) + + case .down: + p1 = CGPoint(x: r.midX, y: r.maxY) + p2 = CGPoint(x: r.minX, y: r.minY) + p3 = CGPoint(x: r.maxX, y: r.minY) + + case .left: + p1 = CGPoint(x: r.minX, y: r.midY) + p2 = CGPoint(x: r.maxX, y: r.minY) + p3 = CGPoint(x: r.maxX, y: r.maxY) + + case .right: + p1 = CGPoint(x: r.maxX, y: r.midY) + p2 = CGPoint(x: r.minX, y: r.maxY) + p3 = CGPoint(x: r.minX, y: r.minY) + } + + var path = Path() + path.move(to: p1) + path.addLine(to: p2) + path.addLine(to: p3) + path.closeSubpath() + return path + } +} + +/// 可配置的三角形视图封装,便于在项目中复用。 +/// 示例: +/// TriangleView(width: 20, height: 18, direction: .up, color: .black, stroke: .white, lineWidth: 2) +public struct TriangleView: View { + public var width: CGFloat + public var height: CGFloat + public var direction: TriangleDirection + public var color: Color + public var stroke: Color? = nil + public var lineWidth: CGFloat = 1 + public var rotation: Angle = .degrees(0) + + public init( + width: CGFloat, + height: CGFloat, + direction: TriangleDirection = .up, + color: Color = .black, + stroke: Color? = nil, + lineWidth: CGFloat = 1, + rotation: Angle = .degrees(0) + ) { + self.width = width + self.height = height + self.direction = direction + self.color = color + self.stroke = stroke + self.lineWidth = lineWidth + self.rotation = rotation + } + + public var body: some View { + let shape = TriangleShape(direction: direction) + Group { + if let stroke = stroke, lineWidth > 0 { + shape.fill(color) + .overlay( + shape.stroke(stroke, lineWidth: lineWidth) + ) + } else { + shape.fill(color) + } + } + .frame(width: width, height: height) + .rotationEffect(rotation) + .accessibilityHidden(true) + } +} + +/// 水平渲染多个等尺寸三角形的小组件。 +public struct TriangleRow: View { + public var count: Int + public var itemSize: CGSize + public var direction: TriangleDirection + public var color: Color + public var spacing: CGFloat + public var rotation: Angle + + public init( + count: Int, + itemSize: CGSize, + direction: TriangleDirection = .up, + color: Color = .black, + spacing: CGFloat = 8, + rotation: Angle = .degrees(0) + ) { + self.count = count + self.itemSize = itemSize + self.direction = direction + self.color = color + self.spacing = spacing + self.rotation = rotation + } + + public var body: some View { + HStack(spacing: spacing) { + ForEach(0..<(max(0, count)), id: \.self) { _ in + TriangleView( + width: itemSize.width, + height: itemSize.height, + direction: direction, + color: color, + rotation: rotation + ) + } + } + .accessibilityElement(children: .ignore) + } +} + +#if DEBUG +struct Triangle_Previews: PreviewProvider { + static var previews: some View { + Group { + // 单个三角形 + TriangleView(width: 24, height: 20, direction: .up, color: .black, stroke: .white, lineWidth: 2) + .padding() + .previewDisplayName("Single Up") + + TriangleView(width: 24, height: 20, direction: .right, color: .orange) + .padding() + .background(Color(.systemGroupedBackground)) + .previewDisplayName("Single Right") + + TriangleView(width: 24, height: 20, direction: .up, color: .blue, rotation: .degrees(132)) + .padding() + .previewDisplayName("Rotated 132") + + // 行展示 + ZStack { + Color(white: 0.97) + HStack { + RoundedRectangle(cornerRadius: 18, style: .continuous) + .fill(.white) + .shadow(color: Color.black.opacity(0.06), radius: 6, x: 0, y: 2) + .overlay( + HStack { + TriangleRow( + count: 6, + itemSize: CGSize(width: 16, height: 14), + direction: .up, + color: .black, + spacing: 6, + rotation: .degrees(180) + ) + } + .padding(.horizontal, 18) + ) + .frame(height: 64) + .padding() + } + } + .previewDisplayName("Row") + } + .previewLayout(.sizeThatFits) + } +} +#endif \ No newline at end of file