diff --git a/.swiftformat b/.swiftformat index 1ebdf18..d350def 100644 --- a/.swiftformat +++ b/.swiftformat @@ -7,7 +7,7 @@ --before-marks --binary-grouping 4,8 --blank-line-after-switch-case multiline-only ---call-site-paren default +--call-site-paren balanced --category-mark "MARK: %c" --class-threshold 0 --closing-paren balanced @@ -41,7 +41,7 @@ --header ignore --hex-grouping 4,8 --hex-literal-case uppercase ---ifdef no-indent +--ifdef indent --import-grouping alpha --indent 4 --indent-case false @@ -73,6 +73,7 @@ --organization-mode visibility --organize-types actor,class,enum,struct --pattern-let hoist +--prefer-synthesized-init-for-internal-structs never --preserve-acronyms --preserve-decls --preserved-property-types Package @@ -110,7 +111,7 @@ --visibility-marks --visibility-order --void-type tuple ---wrap-arguments preserve +--wrap-arguments before-first --wrap-collections preserve --wrap-conditions preserve --wrap-effects preserve @@ -123,5 +124,5 @@ --xcode-indentation disabled --xctest-symbols --yoda-swap always ---disable wrapMultilineStatementBraces ---enable isEmpty,propertyTypes +--disable genericExtensions,redundantProperty,simplifyGenericConstraints,wrapMultilineStatementBraces,wrapPropertyBodies +--enable isEmpty diff --git a/Sources/Luminare/Components/Auxiliary/InfiniteScrollView.swift b/Sources/Luminare/Components/Auxiliary/InfiniteScrollView.swift index b25479e..793d557 100644 --- a/Sources/Luminare/Components/Auxiliary/InfiniteScrollView.swift +++ b/Sources/Luminare/Components/Auxiliary/InfiniteScrollView.swift @@ -37,7 +37,7 @@ public enum InfiniteScrollViewDirection: String, Equatable, Hashable, Identifiab } } - // Stacks the given elements according to the direction + /// Stacks the given elements according to the direction @ViewBuilder func stack(spacing: CGFloat, @ViewBuilder content: @escaping () -> some View) -> some View { switch self { case .horizontal: @@ -47,7 +47,7 @@ public enum InfiniteScrollViewDirection: String, Equatable, Hashable, Identifiab } } - // Gets the length from the given 2D size according to the direction + /// Gets the length from the given 2D size according to the direction func length(of size: CGSize) -> CGFloat { switch self { case .horizontal: @@ -57,7 +57,7 @@ public enum InfiniteScrollViewDirection: String, Equatable, Hashable, Identifiab } } - // Gets the offset from the given 2D point according to the direction + /// Gets the offset from the given 2D point according to the direction func offset(of point: CGPoint) -> CGFloat { switch self { case .horizontal: @@ -67,7 +67,7 @@ public enum InfiniteScrollViewDirection: String, Equatable, Hashable, Identifiab } } - // Forms a point from the given offset according to the direction + /// Forms a point from the given offset according to the direction func point(from offset: CGFloat) -> CGPoint { switch self { case .horizontal: @@ -77,7 +77,7 @@ public enum InfiniteScrollViewDirection: String, Equatable, Hashable, Identifiab } } - // Forms a size from the given length according to the direction + /// Forms a size from the given length according to the direction func size(from length: CGFloat, fallback: CGFloat) -> CGSize { switch self { case .horizontal: @@ -164,31 +164,31 @@ public struct InfiniteScrollView: NSViewRepresentable { } #if DEBUG - init( - debug: Bool, - direction: Direction = .horizontal, - allowsDragging: Bool = true, - size: CGSize, - spacing: CGFloat, - snapping: Bool = true, - wrapping: Bool = true, - initialOffset: CGFloat = .zero, - shouldReset: Binding = .constant(false), - offset: Binding, - page: Binding - ) { - self.debug = debug - self.direction = direction - self.allowsDragging = allowsDragging - self.size = size - self.spacing = spacing - self.snapping = snapping - self.wrapping = wrapping - self.initialOffset = initialOffset - self._shouldReset = shouldReset - self._offset = offset - self._page = page - } + init( + debug: Bool, + direction: Direction = .horizontal, + allowsDragging: Bool = true, + size: CGSize, + spacing: CGFloat, + snapping: Bool = true, + wrapping: Bool = true, + initialOffset: CGFloat = .zero, + shouldReset: Binding = .constant(false), + offset: Binding, + page: Binding + ) { + self.debug = debug + self.direction = direction + self.allowsDragging = allowsDragging + self.size = size + self.spacing = spacing + self.snapping = snapping + self.wrapping = wrapping + self.initialOffset = initialOffset + self._shouldReset = shouldReset + self._offset = offset + self._page = page + } #endif var length: CGFloat { @@ -216,7 +216,7 @@ public struct InfiniteScrollView: NSViewRepresentable { .frame(width: size.width, height: size.height) } - @ViewBuilder private func centerView() -> some View { + private func centerView() -> some View { Color.clear .frame(width: size.width, height: size.height) } @@ -355,10 +355,11 @@ public struct InfiniteScrollView: NSViewRepresentable { case .dragging: // ends dragging draggingStage = .invalid - didEndLiveScroll(.init( - name: NSScrollView.didEndLiveScrollNotification, - object: scrollView - ) + didEndLiveScroll( + .init( + name: NSScrollView.didEndLiveScrollNotification, + object: scrollView + ) ) } case .leftMouseDragged: @@ -399,7 +400,7 @@ public struct InfiniteScrollView: NSViewRepresentable { } } - // Should be called whenever a scroll happens. + /// Should be called whenever a scroll happens. @objc func didLiveScroll(_ notification: Notification) { guard let scrollView = notification.object as? NSScrollView else { return } @@ -448,7 +449,7 @@ public struct InfiniteScrollView: NSViewRepresentable { updateBounds(scrollView.contentView) } - // Should be called whenever a scroll starts. + /// Should be called whenever a scroll starts. @objc func willStartLiveScroll(_ notification: Notification) { guard let scrollView = notification.object as? NSScrollView else { return } @@ -460,7 +461,7 @@ public struct InfiniteScrollView: NSViewRepresentable { updateBounds(scrollView.contentView) } - // Should be called whenever a scroll ends. + /// Should be called whenever a scroll ends. @objc func didEndLiveScroll(_ notification: Notification) { guard let scrollView = notification.object as? NSScrollView else { return } @@ -479,13 +480,13 @@ public struct InfiniteScrollView: NSViewRepresentable { parent.onBoundsChange(clipView.bounds, animate: animate) } - // Accumulates the page for wrapping + /// Accumulates the page for wrapping private func accumulatePage(_ offset: Int) { parent.page += offset pageOrigin = parent.page } - // Overrides the page, not for wrapping + /// Overrides the page, not for wrapping private func overridePage(_ offset: Int) { parent.page = pageOrigin + offset } @@ -499,7 +500,7 @@ public struct InfiniteScrollView: NSViewRepresentable { updateBounds(clipView, animate: animate) } - // Snaps to the nearest available page anchor + /// Snaps to the nearest available page anchor private func snapScrollViewPosition(_ clipView: NSClipView) { let center = parent.direction.offset(of: parent.centerRect.origin) let offset = parent.direction.offset(of: clipView.bounds.origin) @@ -566,66 +567,66 @@ public struct InfiniteScrollView: NSViewRepresentable { #if DEBUG -// MARK: - Preview + // MARK: - Preview -private struct InfiniteScrollPreview: View { - var direction: InfiniteScrollViewDirection = .horizontal - var size: CGSize = .init(width: 500, height: 100) + private struct InfiniteScrollPreview: View { + var direction: InfiniteScrollViewDirection = .horizontal + var size: CGSize = .init(width: 500, height: 100) - @State private var offset: CGFloat = 0 - @State private var page: Int = 0 - @State private var shouldReset: Bool = true - @State private var wrapping: Bool = true + @State private var offset: CGFloat = 0 + @State private var page: Int = 0 + @State private var shouldReset: Bool = true + @State private var wrapping: Bool = true - var body: some View { - InfiniteScrollView( - debug: true, - direction: direction, + var body: some View { + InfiniteScrollView( + debug: true, + direction: direction, - size: size, - spacing: 50, - snapping: true, - wrapping: wrapping, - initialOffset: 0, + size: size, + spacing: 50, + snapping: true, + wrapping: wrapping, + initialOffset: 0, - shouldReset: $shouldReset, - offset: $offset, - page: $page - ) - .frame(width: size.width, height: size.height) - .border(.red) + shouldReset: $shouldReset, + offset: $offset, + page: $page + ) + .frame(width: size.width, height: size.height) + .border(.red) - HStack { - Button("Reset Offset") { - shouldReset = true - } + HStack { + Button("Reset Offset") { + shouldReset = true + } - Button(wrapping ? "Disable Wrapping" : "Enable Wrapping") { - wrapping.toggle() + Button(wrapping ? "Disable Wrapping" : "Enable Wrapping") { + wrapping.toggle() + } } - } - .frame(maxWidth: .infinity) + .frame(maxWidth: .infinity) - HStack { - Text(String(format: "Offset: %.1f", offset)) + HStack { + Text(String(format: "Offset: %.1f", offset)) - Text("Page: \(page)") - .foregroundStyle(.tint) + Text("Page: \(page)") + .foregroundStyle(.tint) + } + .monospaced() + .frame(height: 12) } - .monospaced() - .frame(height: 12) } -} -#Preview { - VStack { - InfiniteScrollPreview() + #Preview { + VStack { + InfiniteScrollPreview() - Divider() + Divider() - InfiniteScrollPreview(direction: .vertical, size: .init(width: 100, height: 500)) + InfiniteScrollPreview(direction: .vertical, size: .init(width: 100, height: 500)) + } + .padding() + .contentTransition(.numericText()) } - .padding() - .contentTransition(.numericText()) -} #endif diff --git a/Sources/Luminare/Components/Auxiliary/Modifiers/LuminareContentSizeModifier.swift b/Sources/Luminare/Components/Auxiliary/Modifiers/LuminareContentSizeModifier.swift index 289a45a..d2fa9a5 100644 --- a/Sources/Luminare/Components/Auxiliary/Modifiers/LuminareContentSizeModifier.swift +++ b/Sources/Luminare/Components/Auxiliary/Modifiers/LuminareContentSizeModifier.swift @@ -23,7 +23,6 @@ public struct LuminareContentSizeModifier: ViewModifier { self.hasFixedHeight = hasFixedHeight } - @ViewBuilder public func body(content: Content) -> some View { if let contentMode { Group { diff --git a/Sources/Luminare/Components/Auxiliary/Modifiers/LuminarePopoverPresenter.swift b/Sources/Luminare/Components/Auxiliary/Modifiers/LuminarePopoverPresenter.swift new file mode 100644 index 0000000..45b9fc8 --- /dev/null +++ b/Sources/Luminare/Components/Auxiliary/Modifiers/LuminarePopoverPresenter.swift @@ -0,0 +1,140 @@ +// +// LuminarePopoverPresenter.swift +// Luminare +// +// Created by Kai Azim on 2026-01-17. +// + +import SwiftUI + +public struct LuminarePopoverPresenter: NSViewRepresentable { + @Binding var isPresented: Bool + let arrowEdge: Edge + let behavior: NSPopover.Behavior + let shouldHideAnchor: Bool? + let shouldAnimate: Bool + let content: () -> Content + + private func closePopover(_ context: Context) { + context.coordinator.isPresented = false + context.coordinator.popover?.close() + } + + public func makeNSView(context _: Context) -> NSView { + NSView() + } + + public func updateNSView(_ nsView: NSView, context: Context) { + if isPresented, context.coordinator.popover == nil, nsView.window != nil { + let popover = NSPopover() + let hostingController = NSHostingController( + rootView: content() + .environment(\.luminareDismiss) { closePopover(context) } + ) + + hostingController.view.layoutSubtreeIfNeeded() + let contentSize = hostingController.view.fittingSize + popover.contentSize = contentSize + popover.behavior = behavior + popover.animates = shouldAnimate + + if let shouldHide = shouldHideAnchor { + popover.setValue(NSNumber(value: shouldHide), forKey: "shouldHideAnchor") + } + + popover.contentViewController = hostingController + popover.delegate = context.coordinator + + let insetFactor: CGFloat = shouldHideAnchor == true ? 4 : 0 + var anchorRect = nsView.bounds.insetBy(dx: insetFactor, dy: insetFactor) + if anchorRect.width < 0 { anchorRect.size.width = 0 } + if anchorRect.height < 0 { anchorRect.size.height = 0 } + + popover.show( + relativeTo: anchorRect, + of: nsView, + preferredEdge: edgeToNSRectEdge(arrowEdge) + ) + context.coordinator.popover = popover + + if let parentWindow = nsView.window { + context.coordinator.startObservingWindow(parentWindow) + } + } else if !isPresented, let popover = context.coordinator.popover { + popover.close() + context.coordinator.popover = nil + } + } + + public func makeCoordinator() -> Coordinator { + Coordinator(isPresented: $isPresented) + } + + public class Coordinator: NSObject, NSPopoverDelegate { + @Binding var isPresented: Bool + var popover: NSPopover? + + init(isPresented: Binding) { + _isPresented = isPresented + super.init() + } + + func startObservingWindow(_ window: NSWindow) { + // Observe when the window loses focus + NotificationCenter.default.addObserver( + forName: NSWindow.didResignKeyNotification, + object: window, + queue: .main + ) { [weak self] _ in + guard let self else { return } + // The parent window is no longer focused, close the popover + DispatchQueue.main.async { + self.isPresented = false + self.popover?.close() + } + } + } + + public func popoverWillClose(_: Notification) { + DispatchQueue.main.async { + self.isPresented = false + } + } + + public func popoverDidClose(_: Notification) { + popover = nil + } + } + + private func edgeToNSRectEdge(_ edge: Edge) -> NSRectEdge { + switch edge { + case .top: .minY + case .leading: .minX + case .bottom: .maxY + case .trailing: .maxX + } + } +} + +public struct LuminarePopoverModifier: ViewModifier { + @Binding var isPresented: Bool + let arrowEdge: Edge + let behavior: NSPopover.Behavior + let shouldHideAnchor: Bool? + let shouldAnimate: Bool + let popoverContent: () -> PopoverContent + + public func body(content: Content) -> some View { + content + .background( + LuminarePopoverPresenter( + isPresented: $isPresented, + arrowEdge: arrowEdge, + behavior: behavior, + shouldHideAnchor: shouldHideAnchor, + shouldAnimate: shouldAnimate, + content: popoverContent + ) + ) + } +} diff --git a/Sources/Luminare/Components/Auxiliary/Modifiers/LuminarePopupModifier.swift b/Sources/Luminare/Components/Auxiliary/Modifiers/LuminarePopupModifier.swift deleted file mode 100644 index 0d94945..0000000 --- a/Sources/Luminare/Components/Auxiliary/Modifiers/LuminarePopupModifier.swift +++ /dev/null @@ -1,468 +0,0 @@ -// -// LuminarePopupModifier.swift -// Luminare -// -// Created by Kai Azim on 2024-08-25. -// - -import SwiftUI - -// MARK: - Popup Modifier - -public struct LuminarePopupModifier: ViewModifier where PopupContent: View { - @Binding private var isPresented: Bool - private let alignment: Alignment - private let material: NSVisualEffectView.Material - - @ViewBuilder private var popupContent: () -> PopupContent - - public init( - isPresented: Binding, - alignment: Alignment = .bottom, - material: NSVisualEffectView.Material = .popover, - @ViewBuilder popupContent: @escaping () -> PopupContent - ) { - self._isPresented = isPresented - self.alignment = alignment - self.material = material - self.popupContent = popupContent - } - - public func body(content: Content) -> some View { - content - .background(LuminarePopup( - isPresented: $isPresented, - alignment: alignment, - material: material, - content: popupContent - )) - } -} - -// MARK: - Popup - -struct LuminarePopup: NSViewRepresentable where Content: View { - @Environment(\.luminarePopupCornerRadii) private var cornerRadii - @Environment(\.luminarePopupPadding) private var padding - @Environment(\.luminareSheetClosesOnDefocus) private var sheetClosesOnDefocus - - @Binding private var isPresented: Bool - private let alignment: Alignment - private let material: NSVisualEffectView.Material - - @ViewBuilder private var content: () -> Content - - init( - isPresented: Binding, - alignment: Alignment = .bottom, - material: NSVisualEffectView.Material = .popover, - @ViewBuilder content: @escaping () -> Content - ) { - self._isPresented = isPresented - self.alignment = alignment - self.material = material - self.content = content - } - - func makeNSView(context _: Context) -> NSView { - .init() - } - - // !!! Referencing `isPresented` in this function is necessary for triggering view update - func updateNSView(_ nsView: NSView, context: Context) { - _ = isPresented - DispatchQueue.main.async { - context.coordinator.setVisible(isPresented, relativeTo: nsView) - } - } - - func makeCoordinator() -> Coordinator { - Coordinator(self) { - content() - } - } - - // MARK: - Coordinator - - @MainActor class Coordinator: NSObject, NSWindowDelegate where InnerContent: View { - private let parent: LuminarePopup - private var content: () -> InnerContent - var panel: LuminarePopupPanel? - - private weak var parentView: NSView? - private let id: UUID = .init() - - init(_ parent: LuminarePopup, content: @escaping () -> InnerContent) { - self.parent = parent - self.content = content - super.init() - } - - // View is optional bevause it is not needed to close the popup - func setVisible( - _ isPresented: Bool, - relativeTo parentView: NSView? = nil - ) { - // If we're going to be closing the window - guard isPresented else { - if let panel { - if let window = panel.parent { - window.removeChildWindow(panel) - } - - panel.close() - } - - return - } - - self.parentView = parentView - guard panel == nil else { return } - - initializePopup() - guard let panel else { return } - - if let window = parentView?.window { - window.addChildWindow(panel, ordered: .above) - } - - panel.makeKeyAndOrderFront(nil) - - if let view = panel.contentView { - updatePosition(for: view.frame.size) - } - - EventMonitorManager.shared.addLocalMonitor( - for: id, - matching: [ - .scrollWheel, - .leftMouseDown, - .rightMouseDown, - .otherMouseDown - ] - ) { [weak self] event in - guard let self else { return event } - if event.window != self.panel { - setVisible(false) - } - return event - } - } - - func windowWillClose(_: Notification) { - Task { @MainActor in - EventMonitorManager.shared.removeMonitor(for: id) - parent.isPresented = false - panel = nil - } - } - - private func initializePopup() { - self.panel = .init( - closesOnDefocus: parent.sheetClosesOnDefocus - ) - guard let panel else { return } - - panel.delegate = self - - let view = NSHostingView( - rootView: LuminarePopupWrappingView( - cornerRadii: parent.cornerRadii, - material: parent.material, - setPanelSize: panel.setSize, - content: content - ) - .environmentObject(panel) - .frame( - maxWidth: .infinity, maxHeight: .infinity, - alignment: parent.alignment.negate - ) - ) - panel.contentView = view - panel.layoutIfNeeded() - panel.center() - - view.postsFrameChangedNotifications = true - NotificationCenter.default.addObserver( - self, - selector: #selector(Coordinator.frameDidChange(_:)), - name: NSView.frameDidChangeNotification, - object: view - ) - } - - private func updatePosition(for size: CGSize) { - guard let panel, let parentView else { return } - guard let window = parentView.window else { return } - - let windowFrame = window.frame - let parentSize = parentView.frame.size - let parentOrigin = parentView.convert( - parentView.frame.origin, to: nil - ) - let globalFrame = CGRect( - origin: .init( - x: windowFrame.origin.x + parentOrigin.x, - y: windowFrame.origin.y + parentOrigin.y - ), - size: parentSize - ) - - let origin: CGPoint = - switch parent.alignment { - case .top: - .init( - x: globalFrame.midX - size.width / 2, - y: globalFrame.maxY + parent.padding - ) - case .leading: - .init( - x: globalFrame.minX - size.width - parent.padding, - y: globalFrame.midY - size.height / 2 - ) - case .bottom: - .init( - x: globalFrame.midX - size.width / 2, - y: globalFrame.minY - size.height - parent.padding - ) - case .trailing: - .init( - x: globalFrame.maxX + parent.padding, - y: globalFrame.midY - size.height / 2 - ) - case .topLeading: - .init( - x: globalFrame.minX - size.width - parent.padding, - y: globalFrame.maxY + parent.padding - ) - case .topTrailing: - .init( - x: globalFrame.maxX + parent.padding, - y: globalFrame.maxY + parent.padding - ) - case .bottomLeading: - .init( - x: globalFrame.minX - size.width - parent.padding, - y: globalFrame.minY - size.height - parent.padding - ) - case .bottomTrailing: - .init( - x: globalFrame.maxX + parent.padding, - y: globalFrame.minY - size.height - parent.padding - ) - case .center: - .init( - x: globalFrame.midX - size.width / 2, - y: globalFrame.midY - size.height / 2 - ) - case .trailingLastTextBaseline: - .init( - x: globalFrame.maxX - size.width + parent.cornerRadii.topTrailing, - y: globalFrame.minY - size.height - parent.padding - ) - default: // Same as leadingLastTextBaseline - .init( - x: globalFrame.minX - parent.cornerRadii.topLeading, - y: globalFrame.minY - size.height - parent.padding - ) - } - - panel.setFrameOrigin(origin) - } - - @objc func frameDidChange(_ notification: Notification) { - guard let view = notification.object as? NSView else { return } - updatePosition(for: view.frame.size) - } - } -} - -// MARK: - Popup Panel - -public class LuminarePopupPanel: NSPanel, ObservableObject { - private let closesOnDefocus: Bool - private let initializedDate: Date = .now - - public init( - closesOnDefocus: Bool = false - ) { - self.closesOnDefocus = closesOnDefocus - - super.init( - contentRect: .zero, - styleMask: [.fullSizeContentView], - backing: .buffered, - defer: false - ) - - collectionBehavior.insert(.fullScreenAuxiliary) - level = .floating - backgroundColor = .clear - ignoresMouseEvents = false - isOpaque = false - hasShadow = true - titlebarAppearsTransparent = true - titleVisibility = .hidden - animationBehavior = .utilityWindow - } - - func setSize(_ size: CGSize) { - let newSize = CGSize( - width: size.width, - height: size.height - ) - let newOrigin = NSPoint( - x: frame.origin.x, - y: frame.origin.y - (size.height - frame.height) - ) - - if Date.now.timeIntervalSince(initializedDate) < 1.0 || (newSize.width >= frame.width && newSize.height >= frame.height) { - setFrame(.init(origin: newOrigin, size: newSize), display: false) - return - } - - NSAnimationContext.runAnimationGroup { context in - context.duration = 0.2 - animator().setFrame(.init(origin: newOrigin, size: newSize), display: false) - } - } - - override public var canBecomeKey: Bool { - true - } - - override public var canBecomeMain: Bool { - false - } - - override public var acceptsFirstResponder: Bool { - true - } - - override public func resignKey() { - if closesOnDefocus { - alphaValue = 0 // Prevents a little flicker in NSVisualEffectView when closing - close() - } - } - - override public func keyDown(with event: NSEvent) { - let kVK_Escape: CGKeyCode = 0x35 - - if event.type == .keyDown, event.keyCode == kVK_Escape { - alphaValue = 0 // Prevents a little flicker in NSVisualEffectView when closing - close() - } else { - super.keyDown(with: event) - } - } -} - -// MARK: - View - -struct LuminarePopupWrappingView: View where Content: View { - private let cornerRadii: RectangleCornerRadii - private let material: NSVisualEffectView.Material - private let setPanelSize: (CGSize) -> () - private let content: () -> Content - - init( - cornerRadii: RectangleCornerRadii, - material: NSVisualEffectView.Material, - setPanelSize: @escaping (CGSize) -> (), - content: @escaping () -> Content - ) { - self.cornerRadii = cornerRadii - self.material = material - self.setPanelSize = setPanelSize - self.content = content - } - - var body: some View { - VStack { - ZStack { - backgroundWindow() - content() - windowBorder() - } - .clipShape(.rect(cornerRadii: cornerRadii)) - .buttonStyle(.luminare) - .fixedSize() - .onGeometryChange(for: CGSize.self, of: \.size, action: setPanelSize) - .frame(minWidth: 12, minHeight: 12, alignment: .top) - - Spacer(minLength: 0) - } - } - - private func backgroundWindow() -> some View { - VisualEffectView( - material: material, - blendingMode: .behindWindow - ) - } - - private func windowBorder() -> some View { - ZStack { - if #unavailable(macOS 26.0) { - UnevenRoundedRectangle(cornerRadii: cornerRadii) - .strokeBorder(Color.white.opacity(0.2), lineWidth: 1) - } - - UnevenRoundedRectangle(cornerRadii: cornerRadii) - .strokeBorder(Color.white.opacity(0.2), lineWidth: 1) - .mask(alignment: .top) { - LinearGradient( - colors: [ - .white, - .clear - ], - startPoint: .top, - endPoint: .bottom - ) - .frame(height: 30) - } - } - } -} - -// MARK: - Preview - -private struct PopupContent: View { - @State private var isExpanded: Bool = false - - var body: some View { - VStack { - Button("Toggle Expansion") { - withAnimation(.smooth(duration: 0.2)) { - isExpanded.toggle() - } - } - .padding() - - if isExpanded { - Text("Expanded Content") - .font(.title) - .padding() - } else { - Text("Normal Content") - .padding() - } - } - } -} - -// Preview as app -@available(macOS 15.0, *) -#Preview { - @Previewable @State var isPresented = false - - Button("Toggle Popup") { - isPresented.toggle() - } - .luminarePopup(isPresented: $isPresented, alignment: .leading) { - PopupContent() - } - .padding() - .frame(width: 500, height: 300) -} diff --git a/Sources/Luminare/Components/Auxiliary/Modifiers/LuminarePopoverModifier.swift b/Sources/Luminare/Components/Auxiliary/Modifiers/LuminareToolTipModifier.swift similarity index 89% rename from Sources/Luminare/Components/Auxiliary/Modifiers/LuminarePopoverModifier.swift rename to Sources/Luminare/Components/Auxiliary/Modifiers/LuminareToolTipModifier.swift index 162ae63..9a3a7e7 100644 --- a/Sources/Luminare/Components/Auxiliary/Modifiers/LuminarePopoverModifier.swift +++ b/Sources/Luminare/Components/Auxiliary/Modifiers/LuminareToolTipModifier.swift @@ -1,5 +1,5 @@ // -// LuminarePopoverModifier.swift +// LuminareToolTipModifier.swift // Luminare // // Created by Kai Azim on 2024-06-02. @@ -7,7 +7,7 @@ import SwiftUI -public enum LuminarePopoverTrigger { +public enum LuminareToolTipTrigger { case hover( showDelay: TimeInterval = 0.5, hideDelay: TimeInterval = 0.5, @@ -27,7 +27,7 @@ public enum LuminarePopoverTrigger { } } -public enum LuminarePopoverShade { +public enum LuminareToolTipShade { case none case styled(_ style: AnyShapeStyle) @@ -47,17 +47,17 @@ public enum LuminarePopoverShade { } } -// MARK: - Popover +// MARK: - Tool Tip -public struct LuminarePopoverModifier: ViewModifier where PopoverContent: View { - public typealias Trigger = LuminarePopoverTrigger - public typealias Shade = LuminarePopoverShade +public struct LuminareToolTipModifier: ViewModifier where ToolTipContent: View { + public typealias Trigger = LuminareToolTipTrigger + public typealias Shade = LuminareToolTipShade // MARK: Environments @Environment(\.luminareAnimationFast) private var animationFast - @Environment(\.luminarePopoverTrigger) private var trigger - @Environment(\.luminarePopoverShade) private var shade + @Environment(\.luminareToolTipTrigger) private var trigger + @Environment(\.luminareToolTipShade) private var shade @Environment(\.luminareCornerRadii) private var cornerRadii // MARK: Fields @@ -66,7 +66,7 @@ public struct LuminarePopoverModifier: ViewModifier where Popove private let arrowEdge: Edge? private let padding: CGFloat - @ViewBuilder private var popoverContent: () -> PopoverContent + @ViewBuilder private var toolTipContent: () -> ToolTipContent @State private var isPopoverPresented: Bool = false @State private var isHovering: Bool = false @@ -81,12 +81,12 @@ public struct LuminarePopoverModifier: ViewModifier where Popove attachmentAnchor: PopoverAttachmentAnchor = .rect(.bounds), arrowEdge: Edge? = nil, padding: CGFloat = 4, - @ViewBuilder popoverContent: @escaping () -> PopoverContent + @ViewBuilder toolTipContent: @escaping () -> ToolTipContent ) { self.attachmentAnchor = attachmentAnchor self.arrowEdge = arrowEdge self.padding = padding - self.popoverContent = popoverContent + self.toolTipContent = toolTipContent } // MARK: Body @@ -139,7 +139,7 @@ public struct LuminarePopoverModifier: ViewModifier where Popove Group { switch trigger { case let .hover(showDelay, hideDelay, throttleDelay): - popoverContent() + toolTipContent() .onHover { isHovering = $0 } .booleanThrottleDebounced( isHovering, @@ -150,7 +150,7 @@ public struct LuminarePopoverModifier: ViewModifier where Popove isPopoverPresented = debouncedValue } case .forceTouch: - popoverContent() + toolTipContent() .opacity(normalizedForceTouchProgress) .scaleEffect(forceTouchRecognized ? 1.1 : 1, anchor: .center) .animation(.bouncy, value: forceTouchRecognized) @@ -211,13 +211,13 @@ private struct PopoverForceTouchPreview: View where Content: Vie var body: some View { badge() - .luminarePopover( + .luminareToolTip( arrowEdge: arrowEdge, padding: padding ) { content(gesture, recognized) } - .luminarePopoverTrigger(.forceTouch { gesture, recognized in + .luminareToolTipTrigger(.forceTouch { gesture, recognized in self.gesture = gesture self.recognized = recognized }) @@ -228,16 +228,16 @@ private struct PopoverForceTouchPreview: View where Content: Vie LuminareSection { LuminareCompose {} label: { Text("Pops to top *on hover*") - .luminarePopover { + .luminareToolTip { Text("Here's to the *crazy* ones.") .padding() } - .luminarePopoverShade(.none) + .luminareToolTipShade(.none) } LuminareCompose {} label: { Text("Pops to trailing with highlight *on hover*") - .luminarePopover { + .luminareToolTip { VStack(alignment: .leading) { Text("The **misfits.** The ~rebels.~") Text("The [troublemakers](https://apple.com).") @@ -248,7 +248,7 @@ private struct PopoverForceTouchPreview: View where Content: Vie LuminareCompose {} label: { Text("Pops from a dot ↗") - .luminarePopover(attachedTo: .topTrailing, arrowEdge: .top) { + .luminareToolTip(attachedTo: .topTrailing, arrowEdge: .top) { VStack(alignment: .leading) { Text("The round pegs in the square holes.") Text("The ones **who see things differently.**") diff --git a/Sources/Luminare/Components/Button Styles/LuminareButtonStyle+Previews.swift b/Sources/Luminare/Components/Button Styles/LuminareButtonStyle+Previews.swift index 7fda2b9..de4ee6c 100644 --- a/Sources/Luminare/Components/Button Styles/LuminareButtonStyle+Previews.swift +++ b/Sources/Luminare/Components/Button Styles/LuminareButtonStyle+Previews.swift @@ -10,103 +10,103 @@ import SwiftUI // MARK: - All #if DEBUG -@available(macOS 15.0, *) -#Preview( - "LuminareButtonStyles", - traits: .sizeThatFitsLayout -) { - VStack { - LuminareSection { - HStack(spacing: 4) { - Button("Prominent") {} - .buttonStyle(.luminare(tinted: true)) - .tint(.purple) - .luminareRoundingBehavior(topLeading: true) - - Button("Prominent") {} - .buttonStyle(.luminare(tinted: true)) - .tint(.teal) - .luminareRoundingBehavior(topTrailing: true) + @available(macOS 15.0, *) + #Preview( + "LuminareButtonStyles", + traits: .sizeThatFitsLayout + ) { + VStack { + LuminareSection { + HStack(spacing: 4) { + Button("Prominent") {} + .buttonStyle(.luminare(tinted: true)) + .tint(.purple) + .luminareRoundingBehavior(topLeading: true) + + Button("Prominent") {} + .buttonStyle(.luminare(tinted: true)) + .tint(.teal) + .luminareRoundingBehavior(topTrailing: true) + } + .frame(height: 40) + + HStack(spacing: 4) { + Button("Normal") {} + .luminareRoundingBehavior(bottomLeading: true) + + Button("Destructive", role: .destructive) {} + .luminareRoundingBehavior(bottomTrailing: true) + } + .frame(height: 40) } - .frame(height: 40) - - HStack(spacing: 4) { - Button("Normal") {} - .luminareRoundingBehavior(bottomLeading: true) - Button("Destructive", role: .destructive) {} - .luminareRoundingBehavior(bottomTrailing: true) + LuminareSection { + HStack { + Button("Plateau") {} + .luminareRoundingBehavior(top: true, bottom: true) + } + .frame(height: 40) } - .frame(height: 40) - } - - LuminareSection { - HStack { - Button("Plateau") {} - .luminareRoundingBehavior(top: true, bottom: true) - } - .frame(height: 40) } + .buttonStyle(.luminare) } - .buttonStyle(.luminare) -} #endif // MARK: - LuminareButtonStyle #if DEBUG -@available(macOS 15.0, *) -#Preview( - "LuminareButtonStyle", - traits: .sizeThatFitsLayout -) { - Button("Click Me!") {} - .buttonStyle(.luminare) - .frame(height: 40) -} + @available(macOS 15.0, *) + #Preview( + "LuminareButtonStyle", + traits: .sizeThatFitsLayout + ) { + Button("Click Me!") {} + .buttonStyle(.luminare) + .frame(height: 40) + } #endif // MARK: - LuminareProminentButtonStyle #if DEBUG -@available(macOS 15.0, *) -#Preview( - "LuminareProminentButtonStyle", - traits: .sizeThatFitsLayout -) { - Button("Click Me!") {} - .buttonStyle(.luminare(tinted: true)) - .frame(height: 40) - - Button("My Role is Destructive", role: .destructive) {} - .buttonStyle(.luminare) - .frame(height: 40) -} + @available(macOS 15.0, *) + #Preview( + "LuminareProminentButtonStyle", + traits: .sizeThatFitsLayout + ) { + Button("Click Me!") {} + .buttonStyle(.luminare(tinted: true)) + .frame(height: 40) + + Button("My Role is Destructive", role: .destructive) {} + .buttonStyle(.luminare) + .frame(height: 40) + } #endif // MARK: - LuminareHoverable #if DEBUG -@available(macOS 15.0, *) -#Preview( - "LuminareHoverable", - traits: .sizeThatFitsLayout -) { - Text("Not Bordered") - .fixedSize() - .modifier(LuminareHoverableModifier()) - .luminareBorderedStates(.none) - - Text("Bordered") - .fixedSize() - .modifier(LuminareHoverableModifier()) - - Text("Bordered, Hovering") - .fixedSize() - .modifier(LuminareHoverableModifier(isHovering: true)) - - Text("Bordered, Pressed") - .fixedSize() - .modifier(LuminareHoverableModifier(isPressed: true)) -} + @available(macOS 15.0, *) + #Preview( + "LuminareHoverable", + traits: .sizeThatFitsLayout + ) { + Text("Not Bordered") + .fixedSize() + .modifier(LuminareHoverableModifier()) + .luminareBorderedStates(.none) + + Text("Bordered") + .fixedSize() + .modifier(LuminareHoverableModifier()) + + Text("Bordered, Hovering") + .fixedSize() + .modifier(LuminareHoverableModifier(isHovering: true)) + + Text("Bordered, Pressed") + .fixedSize() + .modifier(LuminareHoverableModifier(isPressed: true)) + } #endif diff --git a/Sources/Luminare/Components/Color Picker/ColorHueSliderView.swift b/Sources/Luminare/Components/Color Picker/ColorHueSliderView.swift index 1ecfc00..0a7ebce 100644 --- a/Sources/Luminare/Components/Color Picker/ColorHueSliderView.swift +++ b/Sources/Luminare/Components/Color Picker/ColorHueSliderView.swift @@ -25,7 +25,7 @@ struct ColorHueSliderView: View { @State private var selectionCornerRadius: CGFloat = 0 @State private var selectionWidth: CGFloat = 0 - // gradient for the color spectrum slider + /// gradient for the color spectrum slider private let colorSpectrumGradient: Gradient = .init( colors: stride(from: 0.0, through: 1.0, by: 0.01) .map { Color(hue: $0, saturation: 1, brightness: 1) } diff --git a/Sources/Luminare/Components/Color Picker/ColorPickerModalView.swift b/Sources/Luminare/Components/Color Picker/ColorPickerModalView.swift index ee209ea..3888d5d 100644 --- a/Sources/Luminare/Components/Color Picker/ColorPickerModalView.swift +++ b/Sources/Luminare/Components/Color Picker/ColorPickerModalView.swift @@ -123,7 +123,7 @@ struct ColorPickerModalView: View { hasCancel && hasDone } - @ViewBuilder private func rgbInputFields() -> some View { + private func rgbInputFields() -> some View { HStack(alignment: .bottom, spacing: 4) { RGBInputField(value: $redComponent) { Text("Red") @@ -192,7 +192,7 @@ struct ColorPickerModalView: View { } } - @ViewBuilder private func colorPicker() -> some View { + private func colorPicker() -> some View { Button { colorSampler.show { nsColor in if let nsColor { diff --git a/Sources/Luminare/Components/Color Picker/ColorSaturationBrightnessView.swift b/Sources/Luminare/Components/Color Picker/ColorSaturationBrightnessView.swift index f9b23f5..2ec071d 100644 --- a/Sources/Luminare/Components/Color Picker/ColorSaturationBrightnessView.swift +++ b/Sources/Luminare/Components/Color Picker/ColorSaturationBrightnessView.swift @@ -85,7 +85,7 @@ struct ColorSaturationBrightnessView: View { // MARK: Functions - // Update the position of the circle based on user interaction + /// Update the position of the circle based on user interaction private func updateColor(_ location: CGPoint, _ viewSize: CGSize) { let adjustedX = max(0, min(location.x, viewSize.width)) let adjustedY = max(0, min(location.y, viewSize.height)) @@ -104,7 +104,7 @@ struct ColorSaturationBrightnessView: View { } } - // Initialize the position of the circle based on the current color + /// Initialize the position of the circle based on the current color private func updateCirclePositionFromColor(_ viewSize: CGSize) { if selectedColor.saturation <= 0.0001 { circlePosition = CGPoint( diff --git a/Sources/Luminare/Components/Color Picker/RGBInputField.swift b/Sources/Luminare/Components/Color Picker/RGBInputField.swift index 6ad8f02..1def16c 100644 --- a/Sources/Luminare/Components/Color Picker/RGBInputField.swift +++ b/Sources/Luminare/Components/Color Picker/RGBInputField.swift @@ -31,7 +31,7 @@ struct RGBInputField