Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions Sources/Internal/Configurables/Local/LocalConfig+Anchored.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//
// LocalConfig+Anchored.swift of MijickPopups
//
// Created by Vidy. Extending MijickPopups with anchored popup support.
//
// Copyright 2024 Mijick. All rights reserved.


import SwiftUI

@MainActor
public class LocalConfigAnchored: LocalConfig { required public init() {}
// MARK: Active Variables
public var popupPadding: EdgeInsets = GlobalConfigContainer.center.popupPadding
public var cornerRadius: CGFloat = GlobalConfigContainer.center.cornerRadius
public var backgroundColor: Color = GlobalConfigContainer.center.backgroundColor
public var overlayColor: Color = GlobalConfigContainer.center.overlayColor
public var isTapOutsideToDismissEnabled: Bool = true

// MARK: Anchored-specific Variables
public var originAnchor: PopupAnchorPoint = .bottom
public var popupAnchor: PopupAnchorPoint = .top
public var offset: CGPoint = .zero
public var tapOutsideBehavior: TapOutsideBehavior = .dismiss
public var edgePadding: CGFloat = 16
public var constrainedEdges: Edge.Set = .horizontal

// MARK: Inactive Variables (inherited from LocalConfig protocol)
public var ignoredSafeAreaEdges: Edge.Set = []
public var heightMode: HeightMode = .auto
public var dragDetents: [DragDetent] = []
public var isDragGestureEnabled: Bool = false
public var dragGestureAreaSize: CGFloat = 0
}

// MARK: - Chained Modifiers
public extension LocalConfigAnchored {
/// Sets the anchor point on the source view (where the popup originates from)
func originAnchor(_ anchor: PopupAnchorPoint) -> Self { self.originAnchor = anchor; return self }

/// Sets the anchor point on the popup itself (which point aligns with origin)
func popupAnchor(_ anchor: PopupAnchorPoint) -> Self { self.popupAnchor = anchor; return self }

/// Sets additional offset from the calculated position
func offset(_ offset: CGPoint) -> Self { self.offset = offset; return self }

/// Sets additional offset from the calculated position
func offset(x: CGFloat = 0, y: CGFloat = 0) -> Self { self.offset = CGPoint(x: x, y: y); return self }

/// Sets behavior when tapping outside the popup
/// - Parameter behavior: The tap outside behavior (.none, .dismiss, .passThrough)
func tapOutsideBehavior(_ behavior: TapOutsideBehavior) -> Self { self.tapOutsideBehavior = behavior; return self }

/// Configures edge padding and which edges to constrain
/// - Parameters:
/// - value: Padding value from screen edges
/// - edges: Which edges to constrain (.horizontal, .vertical, .all, or [] to disable)
func edgePadding(_ value: CGFloat, edges: Edge.Set = .horizontal) -> Self {
self.edgePadding = value
self.constrainedEdges = edges
return self
}
}

// MARK: - Public Type Alias
public typealias AnchoredPopupConfig = LocalConfigAnchored
8 changes: 7 additions & 1 deletion Sources/Internal/Containers/PopupStack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ extension PopupStack {
// MARK: Modify
extension PopupStack { enum StackOperation {
case insertPopup(AnyPopup)
case removeLastPopup, removePopup(AnyPopup), removeAllPopupsOfType(any Popup.Type), removeAllPopupsWithID(String), removeAllPopups
case removeLastPopup, removePopup(AnyPopup), removeAllPopupsOfType(any Popup.Type), removeAllPopupsWithID(String), removeAllPopups, removeAllPopupsExcluding([String])
}}
extension PopupStack {
func modify(_ operation: StackOperation) { Task {
Expand Down Expand Up @@ -67,6 +67,7 @@ private extension PopupStack {
case .removeAllPopupsOfType(let popupType): await removedAllPopupsOfType(popupType)
case .removeAllPopupsWithID(let id): await removedAllPopupsWithID(id)
case .removeAllPopups: await removedAllPopups()
case .removeAllPopupsExcluding(let excludedIDs): await removedAllPopupsExcluding(excludedIDs)
}}
nonisolated func getNewPriority(_ newPopups: [AnyPopup]) async -> StackPriority {
await priority.reshuffled(newPopups)
Expand Down Expand Up @@ -100,6 +101,11 @@ private extension PopupStack {
nonisolated func removedAllPopups() async -> [AnyPopup] {
[]
}
nonisolated func removedAllPopupsExcluding(_ excludedIDs: [String]) async -> [AnyPopup] {
await popups.filter { popup in
excludedIDs.contains { popup.id.isSameType(as: $0) }
}
}
}


Expand Down
1 change: 1 addition & 0 deletions Sources/Internal/Extensions/EdgeInsets++.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ extension EdgeInsets {
case .top: top
case .center: 0
case .bottom: bottom
case .anchored: 0
}}
}
2 changes: 2 additions & 0 deletions Sources/Internal/Models/AnyPopup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ extension AnyPopup {
func updatedEnvironmentObject(_ environmentObject: some ObservableObject) -> AnyPopup { updated { $0._body = .init(_body.environmentObject(environmentObject)) }}
func updatedKeyboardDismissal(_ shouldDismiss: Bool) -> AnyPopup { updated { $0.shouldDismissKeyboardOnPopupToggle = shouldDismiss }}
func startDismissTimerIfNeeded(_ popupStack: PopupStack) -> AnyPopup { updated { $0._dismissTimer?.schedule { popupStack.modify(.removePopup(self)) }}}
func updatedAnchorID(_ anchorID: String) -> AnyPopup { updated { $0.config.anchorID = anchorID }}
func updatedAnchorFrame(_ frame: CGRect) -> AnyPopup { updated { $0.config.anchorFrame = frame }}
}
private extension AnyPopup {
func updated(_ customBuilder: (inout AnyPopup) -> ()) -> AnyPopup {
Expand Down
31 changes: 31 additions & 0 deletions Sources/Internal/Models/AnyPopupConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,27 @@ struct AnyPopupConfig: LocalConfig, Sendable { init() {}
var isTapOutsideToDismissEnabled: Bool = false
var isDragGestureEnabled: Bool = false
var dragGestureAreaSize: CGFloat = 0

// MARK: Anchored-specific
var anchorID: String? = nil // ID for AnchorRegistry lookup
var anchorFrame: CGRect? = nil // Static anchor frame (alternative to anchorID)
/// Returns the anchor frame - uses static frame if set, otherwise fetches from registry
func getAnchorFrame() -> CGRect {
if let staticFrame = anchorFrame {
return staticFrame
} else if let anchorID = anchorID {
return AnchorRegistry.frame(forKey: anchorID)
}
return .zero
}
var originAnchor: PopupAnchorPoint = .bottom
var popupAnchor: PopupAnchorPoint = .top
var anchorOffset: CGPoint = .zero
var tapOutsideBehavior: TapOutsideBehavior = .dismiss
// MARK: Transition
var transition: AnyTransition? = nil
var edgePadding: CGFloat = 16
var constrainedEdges: Edge.Set = .horizontal
}

// MARK: Initialize
Expand All @@ -42,5 +63,15 @@ extension AnyPopupConfig {
self.isTapOutsideToDismissEnabled = config.isTapOutsideToDismissEnabled
self.isDragGestureEnabled = config.isDragGestureEnabled
self.dragGestureAreaSize = config.dragGestureAreaSize

// Anchored-specific properties
if let anchoredConfig = config as? LocalConfigAnchored {
self.originAnchor = anchoredConfig.originAnchor
self.popupAnchor = anchoredConfig.popupAnchor
self.anchorOffset = anchoredConfig.offset
self.tapOutsideBehavior = anchoredConfig.tapOutsideBehavior
self.edgePadding = anchoredConfig.edgePadding
self.constrainedEdges = anchoredConfig.constrainedEdges
}
}
}
6 changes: 4 additions & 2 deletions Sources/Internal/Models/StackPriority.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ struct StackPriority: Equatable, Sendable {
var top: CGFloat { values[0] }
var center: CGFloat { values[1] }
var bottom: CGFloat { values[2] }
var overlay: CGFloat { 1 }
var anchored: CGFloat { values[3] }
var overlay: CGFloat { (values.min() ?? 0) - 1 }

private var values: [CGFloat] = [0, 0, 0]
private var values: [CGFloat] = [0, 0, 0, 0]
}

// MARK: Reshuffled
Expand All @@ -26,6 +27,7 @@ extension StackPriority {
case .top: reshuffled(0)
case .center: reshuffled(1)
case .bottom: reshuffled(2)
case .anchored: reshuffled(3)
default: self
}}
}
Expand Down
19 changes: 19 additions & 0 deletions Sources/Internal/Models/TapOutsideBehavior.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// TapOutsideBehavior.swift of MijickPopups
//
// Created by Vidy. Extending MijickPopups with anchored popup support.
//
// Copyright 2024 Mijick. All rights reserved.


import Foundation

/// Defines behavior when tapping outside an AnchoredPopup
public enum TapOutsideBehavior {
/// Does not dismiss, does not pass through (blocks tap)
case none
/// Dismisses popup, does not pass through
case dismiss
/// Passes through to underlying views, does not dismiss
case passThrough
}
Loading