Compare commits

...

9 commits
0.7.0 ... main

Author SHA1 Message Date
Shibo Lyu
9a8b486f55
Create FUNDING.yml 2024-12-24 14:18:08 +08:00
Shibo Lyu
481f59cf41 fix: macos build
fixes #4
2024-12-20 14:58:23 +08:00
Shibo Lyu
7042102108 fix: build on non-macOS. 2024-12-18 16:49:24 +08:00
Shibo Lyu
a9672b8a33 feat: supports Swift 6 2024-12-18 16:11:02 +08:00
Shibo Lyu
54861801e6 chore: convert to preview macro 2024-12-18 16:06:40 +08:00
Shibo Lyu
6b33bcdbe4 fix(macos): remove monitor after use 2024-12-18 15:49:02 +08:00
infinitepower18
246b20d079 Disable spring animation for scroll zoom due to performance issue 2024-08-14 22:24:08 +01:00
infinitepower18
672ec51d3e only allow when hovered 2024-08-13 21:02:07 +01:00
Ahnaf Mahmud
ea5a5354fb
Allow zoom using scroll wheel 2024-08-13 20:30:21 +01:00
7 changed files with 93 additions and 50 deletions

2
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,2 @@
github: [laosb]
buy_me_a_coffee: laosb

View file

@ -26,5 +26,6 @@ let package = Package(
.target(
name: "CropImage",
dependencies: [])
]
],
swiftLanguageVersions: [.version("6"), .v5]
)

View file

@ -19,7 +19,7 @@ public struct CropImageView<Controls: View, CutHole: View>: View {
/// - scale: The scale binding of the image.
/// - rotation: The rotation binding of the image.
/// - crop: An async function to trigger crop action. Result will be delivered via ``onCrop``.
public typealias ControlClosure<Controls> = (
public typealias ControlClosure = (
_ offset: Binding<CGSize>,
_ scale: Binding<CGFloat>,
_ rotation: Binding<Angle>,
@ -30,7 +30,7 @@ public struct CropImageView<Controls: View, CutHole: View>: View {
///
/// - Parameters:
/// - targetSize: The size of the cut hole.
public typealias CutHoleClosure<CutHole> = (_ targetSize: CGSize) -> CutHole
public typealias CutHoleClosure = (_ targetSize: CGSize) -> CutHole
/// Errors that could happen during the cropping process.
public enum CropError: Error {
@ -66,8 +66,8 @@ public struct CropImageView<Controls: View, CutHole: View>: View {
///
/// The error should be a ``CropError``.
public var onCrop: (Result<PlatformImage, Error>) -> Void
var controls: ControlClosure<Controls>
var cutHole: CutHoleClosure<CutHole>
var controls: ControlClosure
var cutHole: CutHoleClosure
/// Create a ``CropImageView`` with a custom controls view and a custom cut hole.
public init(
image: PlatformImage,
@ -75,8 +75,8 @@ public struct CropImageView<Controls: View, CutHole: View>: View {
targetScale: CGFloat = 1,
fulfillTargetFrame: Bool = true,
onCrop: @escaping (Result<PlatformImage, Error>) -> Void,
@ViewBuilder controls: @escaping ControlClosure<Controls>,
@ViewBuilder cutHole: @escaping CutHoleClosure<CutHole>
@ViewBuilder controls: @escaping ControlClosure,
@ViewBuilder cutHole: @escaping CutHoleClosure
) {
self.image = image
self.targetSize = targetSize
@ -92,7 +92,7 @@ public struct CropImageView<Controls: View, CutHole: View>: View {
targetScale: CGFloat = 1,
fulfillTargetFrame: Bool = true,
onCrop: @escaping (Result<PlatformImage, Error>) -> Void,
@ViewBuilder controls: @escaping ControlClosure<Controls>
@ViewBuilder controls: @escaping ControlClosure
) where CutHole == DefaultCutHoleView {
self.image = image
self.targetSize = targetSize
@ -225,15 +225,15 @@ public struct CropImageView<Controls: View, CutHole: View>: View {
}
}
struct CropImageView_Previews: PreviewProvider {
#Preview {
struct PreviewView: View {
@State private var targetSize: CGSize = .init(width: 100, height: 100)
@State private var result: Result<PlatformImage, Error>? = nil
var body: some View {
VStack {
CropImageView(
image: .init(contentsOfFile: "/Users/laosb/Downloads/png.png")!,
image: .previewImage,
targetSize: targetSize
) {
result = $0
@ -262,18 +262,16 @@ struct CropImageView_Previews: PreviewProvider {
}
} header: { Text("Result") }
}
#if os(macOS)
#if os(macOS)
.formStyle(.grouped)
#endif
#endif
}
}
}
static var previews: some View {
PreviewView()
#if os(macOS)
.frame(width: 500)
.frame(minHeight: 600)
#endif
}
return PreviewView()
#if os(macOS)
.frame(width: 500)
.frame(minHeight: 600)
#endif
}

View file

@ -44,20 +44,18 @@ struct DefaultCutHoleShape: Shape {
}
}
struct DefaultCutHoleShape_Previews: PreviewProvider {
static var previews: some View {
VStack {
DefaultCutHoleShape(size: .init(width: 100, height: 100))
.fill(style: FillStyle(eoFill: true))
.foregroundColor(.black.opacity(0.6))
}
.previewDisplayName("Default")
VStack {
DefaultCutHoleShape(size: .init(width: 100, height: 100), isCircular: true)
.fill(style: FillStyle(eoFill: true))
.foregroundColor(.black.opacity(0.6))
}
.previewDisplayName("Circular")
#Preview("Default") {
VStack {
DefaultCutHoleShape(size: .init(width: 100, height: 100))
.fill(style: FillStyle(eoFill: true))
.foregroundColor(.black.opacity(0.6))
}
}
#Preview("Circular") {
VStack {
DefaultCutHoleShape(size: .init(width: 100, height: 100), isCircular: true)
.fill(style: FillStyle(eoFill: true))
.foregroundColor(.black.opacity(0.6))
}
}

View file

@ -61,11 +61,10 @@ public struct DefaultCutHoleView: View {
}
}
struct DefaultCutHoleView_Previews: PreviewProvider {
static var previews: some View {
DefaultCutHoleView(targetSize: .init(width: 100, height: 100))
.previewDisplayName("Default")
DefaultCutHoleView(targetSize: .init(width: 100, height: 100), isCircular: true)
.previewDisplayName("Circular")
}
#Preview("Default") {
DefaultCutHoleView(targetSize: .init(width: 100, height: 100))
}
#Preview("Circular") {
DefaultCutHoleView(targetSize: .init(width: 100, height: 100), isCircular: true)
}

View file

@ -13,10 +13,17 @@ import AppKit
///
/// On macOS, it's `NSImage` and on iOS/visionOS it's `UIImage`.
public typealias PlatformImage = NSImage
extension PlatformImage {
@MainActor static let previewImage: PlatformImage = .init(contentsOf: URL(string: "file:///System/Library/Desktop%20Pictures/Hello%20Metallic%20Blue.heic")!)!
}
#else
import UIKit
/// The image object type, aliased to each platform.
///
/// On macOS, it's `NSImage` and on iOS/visionOS it's `UIImage`.
public typealias PlatformImage = UIImage
extension PlatformImage {
// This doesn't really work, but at least passes build.
static let previewImage: PlatformImage = .init(contentsOfFile: "/System/Library/Desktop Pictures/Hello Metallic Blue.heic")!
}
#endif

View file

@ -25,6 +25,11 @@ struct UnderlyingImageView: View {
@State private var tempOffset: CGSize = .zero
@State private var tempScale: CGFloat = 1
@State private var tempRotation: Angle = .zero
@State private var scrolling: Bool = false
#if os(macOS)
@State private var hovering: Bool = false
@State private var scrollMonitor: Any?
#endif
// When rotated odd multiples of 90 degrees, we need to switch width and height of the image in calculations.
var isRotatedOddMultiplesOf90Deg: Bool {
@ -66,9 +71,15 @@ struct UnderlyingImageView: View {
clampedOffset.height = clampedOffset.height.clamped(to: yOffsetBounds(at: clampedScale))
if clampedScale != scale || clampedOffset != offset {
withAnimation(.interactiveSpring()) {
if scrolling {
scale = clampedScale
offset = clampedOffset
scrolling = false
} else {
withAnimation(.interactiveSpring()) {
scale = clampedScale
offset = clampedOffset
}
}
}
}
@ -81,6 +92,24 @@ struct UnderlyingImageView: View {
scale = min(widthScale, heightScale)
}
#if os(macOS)
private func setupScrollMonitor() {
scrollMonitor = NSEvent.addLocalMonitorForEvents(matching: .scrollWheel) { event in
if hovering {
scrolling = true
scale = scale + event.scrollingDeltaY / 1000
}
return event
}
}
private func removeScrollMonitor() {
if let scrollMonitor {
NSEvent.removeMonitor(scrollMonitor)
}
}
#endif
var imageView: Image {
#if os(macOS)
Image(nsImage: image)
@ -94,6 +123,14 @@ struct UnderlyingImageView: View {
.gesture(dragGesture)
.gesture(magnificationgesture)
.gesture(rotationGesture)
#if os(macOS)
.onAppear {
setupScrollMonitor()
}
.onDisappear {
removeScrollMonitor()
}
#endif
}
var dragGesture: some Gesture {
@ -148,21 +185,24 @@ struct UnderlyingImageView: View {
.onChange(of: rotation) { _ in
adjustToFulfillTargetFrame()
}
#if os(macOS)
.onHover { hovering = $0 }
#endif
}
}
struct MoveAndScalableImageView_Previews: PreviewProvider {
#Preview {
struct PreviewView: View {
@State private var offset: CGSize = .zero
@State private var scale: CGFloat = 1
@State private var rotation: Angle = .zero
var body: some View {
UnderlyingImageView(
offset: $offset,
scale: $scale,
rotation: $rotation,
image: .init(contentsOfFile: "/Users/laosb/Downloads/png.png")!,
image: .previewImage,
viewSize: .init(width: 200, height: 100),
targetSize: .init(width: 100, height: 100),
fulfillTargetFrame: true
@ -170,8 +210,6 @@ struct MoveAndScalableImageView_Previews: PreviewProvider {
.frame(width: 200, height: 100)
}
}
static var previews: some View {
PreviewView()
}
return PreviewView()
}