diff --git a/Sources/OversizeUI/Controls/HUD/HUD.swift b/Sources/OversizeUI/Controls/HUD/HUD.swift index eff6dc4..ad1248c 100644 --- a/Sources/OversizeUI/Controls/HUD/HUD.swift +++ b/Sources/OversizeUI/Controls/HUD/HUD.swift @@ -14,8 +14,12 @@ public struct HUD: View where Title: View, Icon: View { private let isAutoHide: Bool @Binding private var isPresented: Bool + #if os(macOS) + @State private var offset: CGFloat = 200 + #else + @State private var offset: CGFloat = -200 + #endif - @State private var bottomOffset: CGFloat = 0 @State private var opacity: CGFloat = 0 // MARK: Initializers @@ -41,7 +45,12 @@ public struct HUD: View where Title: View, Icon: View { if let text { Text(text) .body(.medium) - .foregroundColor(.onSurfaceHighEmphasis) + #if os(macOS) + .foregroundColor(Color.onPrimaryHighEmphasis) + #else + .foregroundColor(Color.onSurfaceHighEmphasis) + + #endif } else if let title { title @@ -50,26 +59,41 @@ public struct HUD: View where Title: View, Icon: View { .padding(.leading, icon == nil ? .medium : .small) .padding(.trailing, .medium) .padding(.vertical, .xSmall) - .background( - Capsule() - .foregroundColor(Color.surfacePrimary) - .shadowElevaton(.z2) - ) - .padding(.small) - .opacity(opacity) - .offset(y: bottomOffset) - .onChange(of: isPresented, perform: { present in - if present { - presentAnimated() - } else { - dismissAnimated() + #if os(macOS) + .background( + RoundedRectangle(cornerRadius: .small, style: .continuous) + .foregroundColor(Color.onBackgroundHighEmphasis) + .shadowElevaton(.z2) + ) + #else + .background( + Capsule() + .foregroundColor(Color.surfacePrimary) + .shadowElevaton(.z2) + ) + #endif + .padding(.small) + .opacity(opacity) + .offset(y: offset) + .onChange(of: isPresented) { present in + if present { + if offset == 0 { + dismissAnimated() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + presentAnimated() + } + } else { + presentAnimated() + } + } else { + dismissAnimated() + } } - }) } private func presentAnimated() { withAnimation { - bottomOffset = 0 + offset = 0 opacity = 1 } if isAutoHide { @@ -83,7 +107,11 @@ public struct HUD: View where Title: View, Icon: View { private func dismissAnimated() { withAnimation { - bottomOffset = -200 + #if os(macOS) + offset = 200 + #else + offset = -200 + #endif opacity = 0 } } @@ -132,6 +160,42 @@ public extension HUD where Icon == EmptyView { } } +#if os(macOS) +@MainActor +public extension View { + func hud(_ text: String, autoHide: Bool = true, isPresented: Binding) -> some View { + overlay(alignment: .bottomTrailing) { + HUD(text, autoHide: autoHide, isPresented: isPresented) + } + } + + func hud(_ text: String, isPresented: Binding, @ViewBuilder icon: () -> some View) -> some View { + overlay(alignment: .bottomTrailing) { + HUD(text, isPresented: isPresented, icon: icon) + } + } + + func hud(isPresented: Binding, @ViewBuilder title: () -> some View) -> some View { + overlay(alignment: .bottomTrailing) { + HUD(isPresented: isPresented, title: title) + } + } + + func hud(isPresented: Binding, @ViewBuilder title: () -> some View, @ViewBuilder icon: () -> some View) -> some View { + overlay(alignment: .bottomTrailing) { + HUD(isPresented: isPresented, title: title, icon: icon) + } + } + + func hudLoader(_ text: String = "Loading", isPresented: Binding) -> some View { + overlay(alignment: .bottomTrailing) { + HUD(text, autoHide: false, isPresented: isPresented) { + ProgressView() + } + } + } +} +#else public extension View { func hud(_ text: String, isPresented: Binding) -> some View { overlay(alignment: .top) { @@ -165,3 +229,4 @@ public extension View { } } } +#endif diff --git a/Sources/OversizeUI/Controls/PageView/Page.swift b/Sources/OversizeUI/Controls/PageView/Page.swift index b176af8..f5025e5 100644 --- a/Sources/OversizeUI/Controls/PageView/Page.swift +++ b/Sources/OversizeUI/Controls/PageView/Page.swift @@ -4,6 +4,8 @@ import SwiftUI public struct Page: View where Content: View, Header: View, LeadingBar: View, TrailingBar: View, TopToolbar: View, TitleLabel: View { + @Environment(\.platform) var platform + public typealias ScrollAction = (_ offset: CGPoint, _ headerVisibleRatio: CGFloat) -> Void private let title: String? @@ -103,7 +105,7 @@ public struct Page CGFloat) -> some View + computeValue: @Sendable @escaping (ViewDimensions) -> CGFloat) -> some View { if isActive { alignmentGuide(alignment, computeValue: computeValue) @@ -33,7 +33,7 @@ extension View { @ViewBuilder @inlinable func alignmentGuide(_ alignment: VerticalAlignment, isActive: Bool, - computeValue: @escaping (ViewDimensions) -> CGFloat) -> some View + computeValue: @Sendable @escaping (ViewDimensions) -> CGFloat) -> some View { if isActive { alignmentGuide(alignment, computeValue: computeValue) diff --git a/Sources/OversizeUI/Controls/Stacks/LeadingVStack.swift b/Sources/OversizeUI/Controls/Stacks/LeadingVStack.swift new file mode 100644 index 0000000..f2a9293 --- /dev/null +++ b/Sources/OversizeUI/Controls/Stacks/LeadingVStack.swift @@ -0,0 +1,102 @@ +// +// Copyright © 2024 Alexander Romanov +// LeadingVStack.swift, created on 28.10.2024 +// + +import SwiftUI + +public struct LeadingVStack: View { + private let spacing: Space + private let content: Content + + public init(spacing: Space = .zero, @ViewBuilder content: () -> Content) { + self.spacing = spacing + self.content = content() + } + + public var body: some View { + VStack(alignment: .leading, spacing: spacing.rawValue) { + content + } + } +} + +public struct TrailingVStack: View { + private let spacing: Space + private let content: Content + + public init(spacing: Space = .zero, @ViewBuilder content: () -> Content) { + self.spacing = spacing + self.content = content() + } + + public var body: some View { + VStack(alignment: .trailing, spacing: spacing.rawValue) { + content + } + } +} + +public struct CenterVStack: View { + private let spacing: Space + private let content: Content + + public init(spacing: Space = .zero, @ViewBuilder content: () -> Content) { + self.spacing = spacing + self.content = content() + } + + public var body: some View { + VStack(alignment: .center, spacing: spacing.rawValue) { + content + } + } +} + +public struct LeadingLazyVStack: View { + private let spacing: Space + private let content: Content + + public init(spacing: Space = .zero, @ViewBuilder content: () -> Content) { + self.spacing = spacing + self.content = content() + } + + public var body: some View { + LazyVStack(alignment: .leading, spacing: spacing.rawValue) { + content + } + } +} + +public struct TrailingLazyVStack: View { + private let spacing: Space + private let content: Content + + public init(spacing: Space = .zero, @ViewBuilder content: () -> Content) { + self.spacing = spacing + self.content = content() + } + + public var body: some View { + LazyVStack(alignment: .trailing, spacing: spacing.rawValue) { + content + } + } +} + +public struct CenterLazyVStack: View { + private let spacing: Space + private let content: Content + + public init(spacing: Space = .zero, @ViewBuilder content: () -> Content) { + self.spacing = spacing + self.content = content() + } + + public var body: some View { + LazyVStack(alignment: .center, spacing: spacing.rawValue) { + content + } + } +} diff --git a/Sources/OversizeUI/Controls/Surface/Surface.swift b/Sources/OversizeUI/Controls/Surface/Surface.swift index cd13ba9..9fade58 100644 --- a/Sources/OversizeUI/Controls/Surface/Surface.swift +++ b/Sources/OversizeUI/Controls/Surface/Surface.swift @@ -36,6 +36,8 @@ public struct Surface: View { private let forceContentInsets: EdgeSpaceInsets? private var isSurfaceClipped: Bool = false + @State var isHover = false + public init( action: (() -> Void)? = nil, @ViewBuilder label: () -> Label @@ -58,8 +60,12 @@ public struct Surface: View { action?() } label: { surface + .contentShape(Rectangle()) } .buttonStyle(SurfaceButtonStyle()) + .onHover { hover in + isHover = hover + } } private var surface: some View { @@ -68,14 +74,33 @@ public struct Surface: View { .padding(.bottom, forceContentInsets?.bottom ?? contentInsets.bottom) .padding(.leading, forceContentInsets?.leading ?? contentInsets.leading) .padding(.trailing, forceContentInsets?.trailing ?? contentInsets.trailing) - .background( + .background { + #if os(macOS) + ZStack { + RoundedRectangle( + cornerRadius: surfaceRadius, + style: .continuous + ) + .fill(surfaceBackgroundColor) + .shadowElevaton(elevation) + + if isHover { + RoundedRectangle( + cornerRadius: surfaceRadius, + style: .continuous + ) + .fill(Color.onSurfaceDisabled.opacity(0.04)) + } + } + #else RoundedRectangle( cornerRadius: surfaceRadius, style: .continuous ) .fill(surfaceBackgroundColor) .shadowElevaton(elevation) - ) + #endif + } .overlay( RoundedRectangle( cornerRadius: surfaceRadius, @@ -171,7 +196,11 @@ public struct SurfaceButtonStyle: ButtonStyle { public func makeBody(configuration: Self.Configuration) -> some View { configuration.label + #if os(macOS) + .scaleEffect(configuration.isPressed ? 0.98 : 1) + #else .scaleEffect(configuration.isPressed ? 0.96 : 1) + #endif } } diff --git a/Sources/OversizeUI/Controls/Toast/Snackbar.swift b/Sources/OversizeUI/Controls/Toast/Snackbar.swift index ffa46d4..3e04082 100644 --- a/Sources/OversizeUI/Controls/Toast/Snackbar.swift +++ b/Sources/OversizeUI/Controls/Toast/Snackbar.swift @@ -55,7 +55,6 @@ public struct Snackbar: View where Label: View, Actions: View { } } } - .padding(.leading, .medium) .padding(.trailing, .xSmall) .padding(.vertical, .xSmall) diff --git a/Sources/OversizeUI/Controls/URLField/URLField.swift b/Sources/OversizeUI/Controls/URLField/URLField.swift index 341c6de..491b50f 100644 --- a/Sources/OversizeUI/Controls/URLField/URLField.swift +++ b/Sources/OversizeUI/Controls/URLField/URLField.swift @@ -20,6 +20,7 @@ public struct URLField: View { public init(_ title: String = "URL", url: Binding) { self.title = title _url = url + _urlString = .init(initialValue: url.wrappedValue?.absoluteString ?? "") } public var body: some View { @@ -38,7 +39,7 @@ public struct URLField: View { if state { textFieldHelper = .none - } else if let url = URL(string: urlString), NSWorkspace.shared.urlForApplication(toOpen: url) != nil { + } else if let url = URL(string: urlString) { // , NSWorkspace.shared.urlForApplication(toOpen: url) != nil { textFieldHelper = .none self.url = url } else { @@ -56,7 +57,7 @@ public struct URLField: View { textFieldHelper = .errorText } #else - if let url = URL(string: urlString), NSWorkspace.shared.urlForApplication(toOpen: url) != nil { + if let url = URL(string: urlString) { // , NSWorkspace.shared.urlForApplication(toOpen: url) != nil { textFieldHelper = .none self.url = url } else { @@ -71,6 +72,11 @@ public struct URLField: View { .textContentType(.URL) .autocorrectionDisabled() .fieldHelper(.constant("Invalid URL"), style: $textFieldHelper) + .onChange(of: url) { newValue in + if let newValue, newValue.absoluteString != urlString { + urlString = newValue.absoluteString + } + } } } #endif diff --git a/Sources/OversizeUI/Core/Appearance/ThemeSettings.swift b/Sources/OversizeUI/Core/Appearance/ThemeSettings.swift index d710d9d..2003066 100644 --- a/Sources/OversizeUI/Core/Appearance/ThemeSettings.swift +++ b/Sources/OversizeUI/Core/Appearance/ThemeSettings.swift @@ -39,19 +39,19 @@ public class ThemeSettings: ObservableObject, @unchecked Sendable { @AppStorage(ThemeSettingsNames.borderApp) public var borderApp: Bool = false @AppStorage(ThemeSettingsNames.borderButtons) public var borderButtons: Bool = false #if os(macOS) + @AppStorage(ThemeSettingsNames.borderSize) public var borderSize: Double = 1 @AppStorage(ThemeSettingsNames.borderTextFields) public var borderTextFields: Bool = true @AppStorage(ThemeSettingsNames.borderSurface) public var borderSurface: Bool = true + @AppStorage(ThemeSettingsNames.radius) public var radius: Double = 4 #else @AppStorage(ThemeSettingsNames.borderSurface) public var borderSurface: Bool = false @AppStorage(ThemeSettingsNames.borderTextFields) public var borderTextFields: Bool = false + @AppStorage(ThemeSettingsNames.borderSize) public var borderSize: Double = 0.5 + @AppStorage(ThemeSettingsNames.radius) public var radius: Double = 8 #endif @AppStorage(ThemeSettingsNames.borderControls) public var borderControls: Bool = false - @AppStorage(ThemeSettingsNames.borderSize) public var borderSize: Double = 0.5 - - @AppStorage(ThemeSettingsNames.radius) public var radius: Double = 8 - @AppStorage(ThemeSettingsNames.theme) public var theme: Int = 0 public let themes: [Theme] = [ diff --git a/Sources/OversizeUI/Core/Typography.swift b/Sources/OversizeUI/Core/Typography.swift index 2d3dc1f..1310577 100644 --- a/Sources/OversizeUI/Core/Typography.swift +++ b/Sources/OversizeUI/Core/Typography.swift @@ -41,6 +41,20 @@ public struct Typography: ViewModifier { .frame(minHeight: lineHeight) } + #if os(macOS) + private var lineHeight: CGFloat { + switch fontStyle { + case .largeTitle: return 32 + case .title: return 28 + case .title2: return 24 + case .title3: return 20 + case .headline: return 16 + case .subheadline: return 20 + case .body, .callout, .footnote, .caption, .caption2: return 16 + @unknown default: return 16 + } + } + #else private var lineHeight: CGFloat { switch fontStyle { case .largeTitle: return 44 @@ -53,6 +67,8 @@ public struct Typography: ViewModifier { } } + #endif + private var fontDesign: Font.Design { switch fontStyle { case .largeTitle, .title, .title2, .title3, .headline, .subheadline: diff --git a/Sources/OversizeUI/Deprecated/ButtonLegacy.swift b/Sources/OversizeUI/Deprecated/ButtonLegacy.swift index de2ee23..5e1cf5b 100644 --- a/Sources/OversizeUI/Deprecated/ButtonLegacy.swift +++ b/Sources/OversizeUI/Deprecated/ButtonLegacy.swift @@ -277,23 +277,22 @@ public struct ButtonStyleExtended: ButtonStyle { public extension Button { /// Changes the appearance of the button - - @available(*, deprecated, message: "Use native buttonStyle", renamed: "buttonStyle") + @MainActor @available(*, deprecated, message: "Use native buttonStyle", renamed: "buttonStyle") func style(_ style: LegacyButtonType) -> some View { buttonStyle(ButtonStyleExtended(style: style)) } - @available(*, deprecated, message: "Use native buttonStyle", renamed: "buttonStyle") + @MainActor @available(*, deprecated, message: "Use native buttonStyle", renamed: "buttonStyle") func style(_ style: LegacyButtonType, size: ButtonSize) -> some View { buttonStyle(ButtonStyleExtended(style: style, size: size)) } - @available(*, deprecated, message: "Use native buttonStyle", renamed: "buttonStyle") + @MainActor @available(*, deprecated, message: "Use native buttonStyle", renamed: "buttonStyle") func style(_ style: LegacyButtonType, size: ButtonSize, shadow: Bool) -> some View { buttonStyle(ButtonStyleExtended(style: style, size: size, shadow: shadow)) } - @available(*, deprecated, message: "Use native buttonStyle", renamed: "buttonStyle") + @MainActor @available(*, deprecated, message: "Use native buttonStyle", renamed: "buttonStyle") func style(_ style: LegacyButtonType, size: ButtonSize, rounded: ButtonRounded, width: ButtonWidth = .standart, shadow: Bool) -> some View { buttonStyle(ButtonStyleExtended(style: style, size: size, rounded: rounded, width: width, shadow: shadow)) } diff --git a/Sources/OversizeUI/Extensions/View/View+Cursor.swift b/Sources/OversizeUI/Extensions/View/View+Cursor.swift new file mode 100644 index 0000000..fe624f3 --- /dev/null +++ b/Sources/OversizeUI/Extensions/View/View+Cursor.swift @@ -0,0 +1,20 @@ +// +// Copyright © 2024 Alexander Romanov +// View+Cursor.swift, created on 23.10.2024 +// + +import SwiftUI + +#if os(macOS) +public extension View { + func cursor(_ cursor: NSCursor) -> some View { + onHover { inside in + if inside { + cursor.push() + } else { + NSCursor.pop() + } + } + } +} +#endif