@momo-kits/native-kits 0.167.1-beta.9 → 0.167.1-duo-search.1-debug
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +36 -0
- package/build.gradle.kts +11 -0
- package/compose/build.gradle.kts +204 -0
- package/compose/build.gradle.kts.backup +204 -0
- package/compose/compose.podspec +47 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +19 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +12 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +7 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +153 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +52 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +124 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +183 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +207 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +39 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +86 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +76 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +76 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +293 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +33 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +833 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +269 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +110 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +378 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +56 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +69 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +157 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +82 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +32 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +338 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +175 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +356 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +113 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +89 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +128 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +209 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +542 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +49 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +76 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +125 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +231 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +189 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +475 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +114 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +266 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +240 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +223 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +264 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +185 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +343 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +108 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +56 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +41 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +84 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +40 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +361 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +108 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +310 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +59 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +87 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +21 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +90 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +323 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +220 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +473 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +122 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +199 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +91 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +552 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +88 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +125 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +207 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +498 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +176 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +182 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +193 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +234 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +188 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +325 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +16 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +18 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +22 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +16 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +188 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +297 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +54 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +50 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +232 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +41 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +52 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +66 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +104 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +48 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +338 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +58 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +137 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +106 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +205 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +393 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +708 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +148 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +35 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +203 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +232 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +98 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +312 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +169 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +408 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +92 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +105 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +74 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +366 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +37 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +363 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +185 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +132 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +127 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +12 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +91 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +9 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +74 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +13 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +55 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +15 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +118 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +40 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +45 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +32 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +38 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +66 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +217 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +86 -0
- package/gradle/libs.versions.toml +69 -0
- package/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/gradle/wrapper/gradle-wrapper.properties +8 -0
- package/gradle.properties +26 -0
- package/gradlew +252 -0
- package/gradlew.bat +94 -0
- package/ios/Application/ApplicationEnvironment.swift +6 -0
- package/ios/Application/Components.swift +1 -3
- package/ios/Application/HeaderRight.swift +32 -14
- package/ios/Application/Layout/ScreenLayout.swift +312 -0
- package/ios/Application/Navigation/BottomTab/BottomTab.swift +67 -10
- package/ios/Application/Navigation/BottomTab/BottomTabBar.swift +5 -2
- package/ios/Application/Navigation/BottomTab/DuoBarCompression.swift +105 -0
- package/ios/Application/Navigation/BottomTab/DuoTabBar.swift +304 -0
- package/ios/Application/Navigation/HeaderUser.swift +4 -4
- package/ios/Application/Navigation/NavigationContainer.swift +48 -42
- package/ios/Application/Navigation/Navigator.swift +122 -29
- package/ios/Application/Navigation/NavigatorManager.swift +13 -6
- package/ios/Application/Navigation/Overplay/BottomSheet.swift +41 -4
- package/ios/Application/Navigation/Overplay/ModalScreen.swift +7 -0
- package/ios/Application/Navigation/component/DuoScrollGradient.swift +54 -0
- package/ios/Application/Navigation/component/Header.swift +11 -6
- package/ios/Application/Navigation/component/HeaderBackground.swift +3 -2
- package/ios/Application/Navigation/component/NavigationFooter.swift +1 -1
- package/ios/Application/Navigation/component/NavigationHeaderRight.swift +93 -9
- package/ios/Application/Navigation/component/SideBar.swift +155 -0
- package/ios/Application/StackScreen.swift +175 -18
- package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +59 -5
- package/ios/Colors+Radius+Spacing/Colors.swift +1 -1
- package/ios/Extensions/Color++.swift +20 -9
- package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
- package/ios/Icon/Icon.swift +14 -22
- package/ios/Icon/Icons.swift +1329 -0
- package/ios/Image/Image.swift +16 -5
- package/ios/Lottie/LottieView.swift +4 -2
- package/ios/Popup/PopupDisplay.swift +84 -67
- package/ios/Popup/PopupLayout.swift +45 -0
- package/ios/Popup/PopupNotify.swift +44 -26
- package/ios/Popup/PopupPromotion.swift +18 -6
- package/ios/Resources/dot_loading.json +558 -0
- package/ios/Resources/lottie_circle_loader.json +1 -0
- package/ios/Template/TrustBanner/TrustBanner.swift +69 -31
- package/ios/Typography/FontScale.swift +3 -3
- package/ios/native-kits.podspec +1 -1
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
- package/ios-demo/README.md +120 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +2 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +1 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +2 -2
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +2 -0
- package/local.properties +8 -0
- package/package.json +1 -1
- package/scripts/gen-ios-icons.mjs +36 -0
- package/settings.gradle.kts +64 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
//
|
|
2
|
+
// ScreenLayout.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// Port of momo-kits RN `useScreenLayout` (Layout/utils.ts) and `Application/device.ts`:
|
|
6
|
+
// window size, safe-area insets, content width, header geometry and the iPhone Duo side bar.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import SwiftUI
|
|
10
|
+
import UIKit
|
|
11
|
+
|
|
12
|
+
public enum SideBarEdge: Equatable {
|
|
13
|
+
case left, right
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public struct SideBarLayout: Equatable {
|
|
17
|
+
public let edge: SideBarEdge
|
|
18
|
+
public let width: CGFloat
|
|
19
|
+
/// Preferred window y of the first control; StackScreen still keeps the column above the bottom bars.
|
|
20
|
+
public let top: CGFloat
|
|
21
|
+
/// Room kept free above the window bottom, clear of a camera at the bottom end of the strip.
|
|
22
|
+
public let bottom: CGFloat
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public struct ScreenLayout: Equatable {
|
|
26
|
+
public let width: CGFloat
|
|
27
|
+
public let height: CGFloat
|
|
28
|
+
public let isIPhoneDuo: Bool
|
|
29
|
+
public let insets: EdgeInsets
|
|
30
|
+
public let insetLeft: CGFloat
|
|
31
|
+
public let insetRight: CGFloat
|
|
32
|
+
public let contentWidth: CGFloat
|
|
33
|
+
public let headerHeight: CGFloat
|
|
34
|
+
/// 0 when the status bar is off the top edge, so the bar row fills the header and keeps the title centered.
|
|
35
|
+
public let headerStatusBarHeight: CGFloat
|
|
36
|
+
/// iPhone Duo moves back and toolkit into a column in the side safe area; nil while they stay in the bar.
|
|
37
|
+
public let sideBar: SideBarLayout?
|
|
38
|
+
|
|
39
|
+
public var headerBarHeight: CGFloat { headerHeight - headerStatusBarHeight }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Measured by momo-kits RN (Layout/utils.ts) on the iOS 27.1 iPhone Duo simulator; keep both in sync.
|
|
43
|
+
enum DuoMetrics {
|
|
44
|
+
// Inner display is ~1.42, outer ~1.45; every slab iPhone is 1.77 or taller.
|
|
45
|
+
static let maxAspectRatio: CGFloat = 1.6
|
|
46
|
+
// Outer display is 466pt tall in landscape, inner 669pt.
|
|
47
|
+
static let innerMinHeight: CGFloat = 600
|
|
48
|
+
static let controlSize: CGFloat = 48
|
|
49
|
+
static let itemPitch: CGFloat = 50
|
|
50
|
+
static let firstItemTop: CGFloat = 170
|
|
51
|
+
static let firstItemTopLandscape: CGFloat = 120
|
|
52
|
+
// Outer display in portrait: the header is 4pt taller than the iPhone floor and its title
|
|
53
|
+
// lines up with the camera punchout (y 24–66 at the top of the strip).
|
|
54
|
+
static let outerHeaderHeight: CGFloat = 76
|
|
55
|
+
static let outerCameraCenterY: CGFloat = 45
|
|
56
|
+
// Outer display in landscape has no status bar in the strip: controls keep this far from
|
|
57
|
+
// both ends unless a reserved region (the camera at either end) asks for more.
|
|
58
|
+
static let edgeInset: CGFloat = 24
|
|
59
|
+
// Widest short side of any single-display iPhone.
|
|
60
|
+
static let iPhoneMaxShortSide: CGFloat = 440
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
enum HeaderMetrics {
|
|
64
|
+
static let barHeight: CGFloat = HEADER_HEIGHT
|
|
65
|
+
static let iPhoneMinHeight: CGFloat = 72
|
|
66
|
+
// A smaller top inset means the status bar is not on the top edge.
|
|
67
|
+
static let statusBarMinInset: CGFloat = 20
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private var isPhoneIdiom: Bool { UIDevice.current.userInterfaceIdiom == .phone }
|
|
71
|
+
|
|
72
|
+
private var keyWindow: UIWindow? {
|
|
73
|
+
UIApplication.shared.connectedScenes
|
|
74
|
+
.compactMap { ($0 as? UIWindowScene)?.keyWindow }
|
|
75
|
+
.first
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
extension ScreenLayout {
|
|
79
|
+
/// Snapshot of the key window; not reactive, so views read `\.screenLayout` instead.
|
|
80
|
+
public static var current: ScreenLayout {
|
|
81
|
+
guard let window = keyWindow else {
|
|
82
|
+
return make(size: UIScreen.main.bounds.size, insets: EdgeInsets())
|
|
83
|
+
}
|
|
84
|
+
let inset = window.safeAreaInsets
|
|
85
|
+
return make(
|
|
86
|
+
size: window.bounds.size,
|
|
87
|
+
insets: EdgeInsets(top: inset.top, leading: inset.left, bottom: inset.bottom, trailing: inset.right)
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
static func make(
|
|
92
|
+
size: CGSize,
|
|
93
|
+
insets: EdgeInsets,
|
|
94
|
+
reservedRegions: [CGRect] = ReservedRegions.current,
|
|
95
|
+
isPhone: Bool = isPhoneIdiom
|
|
96
|
+
) -> ScreenLayout {
|
|
97
|
+
let width = size.width
|
|
98
|
+
let height = size.height
|
|
99
|
+
var (headerHeight, headerStatusBarHeight) = headerGeometry(topInset: insets.top, isPhone: isPhone)
|
|
100
|
+
let isDuo = isPhone && isIPhoneDuoWindow(width: width, height: height, insets: insets)
|
|
101
|
+
guard isDuo else {
|
|
102
|
+
return ScreenLayout(
|
|
103
|
+
width: width, height: height, isIPhoneDuo: false, insets: insets,
|
|
104
|
+
insetLeft: 0, insetRight: 0, contentWidth: width,
|
|
105
|
+
headerHeight: headerHeight, headerStatusBarHeight: headerStatusBarHeight, sideBar: nil
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
var sideBar: SideBarLayout?
|
|
110
|
+
let sideInset = max(insets.leading, insets.trailing)
|
|
111
|
+
// narrower strips cannot host a control 12pt from the inner edge
|
|
112
|
+
if sideInset >= DuoMetrics.controlSize + Spacing.M {
|
|
113
|
+
var top = insets.top + HeaderMetrics.barHeight + Spacing.M
|
|
114
|
+
if insets.top < HeaderMetrics.statusBarMinInset {
|
|
115
|
+
// status bar lives in the strip: start under its cluster, as the native bar does
|
|
116
|
+
if width <= height {
|
|
117
|
+
top = DuoMetrics.firstItemTop
|
|
118
|
+
} else if height >= DuoMetrics.innerMinHeight {
|
|
119
|
+
top = DuoMetrics.firstItemTopLandscape
|
|
120
|
+
} else {
|
|
121
|
+
// outer display in landscape: no status bar in the strip
|
|
122
|
+
top = DuoMetrics.edgeInset
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if height > width && width < DuoMetrics.innerMinHeight {
|
|
126
|
+
// outer display: the row starts below the top so the title centers on the camera
|
|
127
|
+
headerHeight = DuoMetrics.outerHeaderHeight
|
|
128
|
+
headerStatusBarHeight = DuoMetrics.outerCameraCenterY * 2 - DuoMetrics.outerHeaderHeight
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let edge: SideBarEdge = insets.trailing >= insets.leading ? .right : .left
|
|
133
|
+
let stripLeft = edge == .right ? width - sideInset : 0
|
|
134
|
+
// clear of the camera and of the status bar cluster, which grows for calls and Live
|
|
135
|
+
// Activities; region frames already include the system's margin
|
|
136
|
+
let occlusion = stripOcclusion(reservedRegions, stripLeft: stripLeft, stripRight: stripLeft + sideInset, windowHeight: height)
|
|
137
|
+
sideBar = SideBarLayout(
|
|
138
|
+
edge: edge,
|
|
139
|
+
width: sideInset,
|
|
140
|
+
top: max(top, occlusion.top),
|
|
141
|
+
bottom: max(DuoMetrics.edgeInset, occlusion.bottom)
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return ScreenLayout(
|
|
146
|
+
width: width, height: height, isIPhoneDuo: true, insets: insets,
|
|
147
|
+
insetLeft: insets.leading, insetRight: insets.trailing,
|
|
148
|
+
contentWidth: width - insets.leading - insets.trailing,
|
|
149
|
+
headerHeight: headerHeight, headerStatusBarHeight: headerStatusBarHeight, sideBar: sideBar
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private static func headerGeometry(topInset: CGFloat, isPhone: Bool) -> (CGFloat, CGFloat) {
|
|
154
|
+
let height = topInset + HeaderMetrics.barHeight
|
|
155
|
+
guard isPhone else { return (height, topInset) }
|
|
156
|
+
return (
|
|
157
|
+
max(height, HeaderMetrics.iPhoneMinHeight),
|
|
158
|
+
topInset < HeaderMetrics.statusBarMinInset ? 0 : topInset
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// How far system regions reach into a side strip from its top and from its bottom end
|
|
163
|
+
/// (e.g. the camera, which sits at either end depending on the rotation).
|
|
164
|
+
private static func stripOcclusion(
|
|
165
|
+
_ regions: [CGRect],
|
|
166
|
+
stripLeft: CGFloat,
|
|
167
|
+
stripRight: CGFloat,
|
|
168
|
+
windowHeight: CGFloat
|
|
169
|
+
) -> (top: CGFloat, bottom: CGFloat) {
|
|
170
|
+
regions.reduce(into: (top: CGFloat(0), bottom: CGFloat(0))) { occlusion, region in
|
|
171
|
+
guard region.minX < stripRight && region.maxX > stripLeft else { return }
|
|
172
|
+
if region.midY < windowHeight / 2 {
|
|
173
|
+
occlusion.top = max(occlusion.top, region.maxY)
|
|
174
|
+
} else {
|
|
175
|
+
occlusion.bottom = max(occlusion.bottom, windowHeight - region.minY)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Slab iPhones report equal side insets in landscape; iPhone Duo moves the status bar to one side.
|
|
181
|
+
private static func isIPhoneDuoWindow(width: CGFloat, height: CGFloat, insets: EdgeInsets) -> Bool {
|
|
182
|
+
let shortSide = min(width, height)
|
|
183
|
+
let isSquare = shortSide > 0 && max(width, height) / shortSide < DuoMetrics.maxAspectRatio
|
|
184
|
+
return isSquare || insets.leading != insets.trailing
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/// Below the side status bar: in portrait it is about twice as tall as its inset is wide.
|
|
188
|
+
public var duoTopInset: CGFloat {
|
|
189
|
+
max(insets.top, height > width ? max(insets.leading, insets.trailing) * 2 : 0)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/// Port of RN `device.ts` `isIPhoneDuo()`: UIScreen stays on the outer display, so this
|
|
194
|
+
/// still matches when Split View narrows the window. Used by popups and overlays.
|
|
195
|
+
public func isIPhoneDuoDevice(windowSize: CGSize? = nil) -> Bool {
|
|
196
|
+
guard isPhoneIdiom else { return false }
|
|
197
|
+
let screen = UIScreen.main.bounds.size
|
|
198
|
+
let window = windowSize ?? keyWindow?.bounds.size ?? screen
|
|
199
|
+
return max(min(screen.width, screen.height), min(window.width, window.height)) > DuoMetrics.iPhoneMaxShortSide
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// MARK: - Reserved regions
|
|
203
|
+
|
|
204
|
+
/// System occlusion regions (iOS 27.1+) in window coordinates: the iPhone Duo camera, and the
|
|
205
|
+
/// status bar cluster as it grows for a call or a Live Activity. Empty before iOS 27.1.
|
|
206
|
+
enum ReservedRegions {
|
|
207
|
+
// last report of the probe, so `ScreenLayout.current` includes it
|
|
208
|
+
static var current: [CGRect] = []
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/// Invisible probe: reading the regions in layoutSubviews makes UIKit run it again when they
|
|
212
|
+
/// change, so nothing polls. Port of RN `@momo-kits/reserved-regions`.
|
|
213
|
+
private final class ReservedRegionsProbe: UIView {
|
|
214
|
+
var onChange: (() -> Void)?
|
|
215
|
+
private var last: [CGRect]?
|
|
216
|
+
|
|
217
|
+
override init(frame: CGRect) {
|
|
218
|
+
super.init(frame: frame)
|
|
219
|
+
isUserInteractionEnabled = false
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
required init?(coder: NSCoder) { nil }
|
|
223
|
+
|
|
224
|
+
override func layoutSubviews() {
|
|
225
|
+
super.layoutSubviews()
|
|
226
|
+
let regions = occlusionRegions()
|
|
227
|
+
guard regions != last else { return }
|
|
228
|
+
last = regions
|
|
229
|
+
ReservedRegions.current = regions
|
|
230
|
+
// layout pass: publish on the next turn instead of mutating SwiftUI state mid-update
|
|
231
|
+
DispatchQueue.main.async { [weak self] in self?.onChange?() }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
override func didMoveToWindow() {
|
|
235
|
+
super.didMoveToWindow()
|
|
236
|
+
setNeedsLayout()
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// The API is newer than the SDKs hosts may build with, so it is reached through the runtime.
|
|
240
|
+
private func occlusionRegions() -> [CGRect] {
|
|
241
|
+
let regionsOfKind = NSSelectorFromString("reservedRegionsOfKind:")
|
|
242
|
+
guard window != nil, responds(to: regionsOfKind),
|
|
243
|
+
let kindType = NSClassFromString("UIViewReservedRegionKind") as? NSObject.Type,
|
|
244
|
+
let kind = (kindType as AnyObject).perform(NSSelectorFromString("occlusionRegionKind"))?.takeUnretainedValue(),
|
|
245
|
+
let regions = perform(regionsOfKind, with: kind)?.takeUnretainedValue() as? [NSObject]
|
|
246
|
+
else { return [] }
|
|
247
|
+
return regions.compactMap { ($0.value(forKey: "frame") as? NSValue)?.cgRectValue }
|
|
248
|
+
.map { convert($0, to: nil) }
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private struct ReservedRegionsProbeView: UIViewRepresentable {
|
|
253
|
+
let onChange: () -> Void
|
|
254
|
+
|
|
255
|
+
func makeUIView(context: Context) -> ReservedRegionsProbe {
|
|
256
|
+
let probe = ReservedRegionsProbe()
|
|
257
|
+
probe.onChange = onChange
|
|
258
|
+
return probe
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
func updateUIView(_ probe: ReservedRegionsProbe, context: Context) {
|
|
262
|
+
probe.onChange = onChange
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// MARK: - Environment
|
|
267
|
+
|
|
268
|
+
private struct ScreenLayoutKey: EnvironmentKey {
|
|
269
|
+
// Computed so an un-injected subtree (bare UIHostingController) still reads the live window.
|
|
270
|
+
static var defaultValue: ScreenLayout { .current }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
public extension EnvironmentValues {
|
|
274
|
+
var screenLayout: ScreenLayout {
|
|
275
|
+
get { self[ScreenLayoutKey.self] }
|
|
276
|
+
set { self[ScreenLayoutKey.self] = newValue }
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/// Re-reads the window whenever this view's size changes (rotation, fold/unfold, Split View).
|
|
281
|
+
private struct ScreenLayoutReader: ViewModifier {
|
|
282
|
+
@State private var layout: ScreenLayout = .current
|
|
283
|
+
|
|
284
|
+
func body(content: Content) -> some View {
|
|
285
|
+
content
|
|
286
|
+
.environment(\.screenLayout, layout)
|
|
287
|
+
.background(
|
|
288
|
+
GeometryReader { proxy in
|
|
289
|
+
Color.clear
|
|
290
|
+
.onAppear { refresh() }
|
|
291
|
+
.onChange(of: proxy.size) { _ in refresh() }
|
|
292
|
+
.onChange(of: proxy.safeAreaInsets) { _ in refresh() }
|
|
293
|
+
}
|
|
294
|
+
)
|
|
295
|
+
.background(ReservedRegionsProbeView { refresh() }.ignoresSafeArea())
|
|
296
|
+
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
|
|
297
|
+
refresh()
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private func refresh() {
|
|
302
|
+
let next = ScreenLayout.current
|
|
303
|
+
if next != layout { layout = next }
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
public extension View {
|
|
308
|
+
/// Provides `\.screenLayout`; NavigationContainer applies it, a bare UIHostingController root should too.
|
|
309
|
+
func momoScreenLayout() -> some View {
|
|
310
|
+
modifier(ScreenLayoutReader())
|
|
311
|
+
}
|
|
312
|
+
}
|
|
@@ -14,19 +14,61 @@ import SwiftUI
|
|
|
14
14
|
public struct BottomTab: View {
|
|
15
15
|
private let items: [BottomTabItem]
|
|
16
16
|
private let floatingButton: BottomTabFloatingButton?
|
|
17
|
+
private let compressionBehavior: DuoBarCompressionBehavior
|
|
17
18
|
|
|
18
19
|
@EnvironmentObject private var navigator: Navigator
|
|
20
|
+
@Environment(\.screenLayout) private var layout
|
|
19
21
|
@State private var selectedIndex: Int = 0
|
|
22
|
+
@State private var toolbarHeight = DuoBarCompression.defaultToolbarHeight
|
|
23
|
+
@State private var expandedBar: DuoBar?
|
|
24
|
+
@State private var openedBar: DuoBar?
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
/// `compressionBehavior`: on iPhone Duo, when the side bar can't hold both bars, which one
|
|
27
|
+
/// starts compressed; tapping the compressed one swaps them.
|
|
28
|
+
public init(
|
|
29
|
+
items: [BottomTabItem],
|
|
30
|
+
floatingButton: BottomTabFloatingButton? = nil,
|
|
31
|
+
compressionBehavior: DuoBarCompressionBehavior = .toolbar
|
|
32
|
+
) {
|
|
22
33
|
self.items = items
|
|
23
34
|
self.floatingButton = floatingButton
|
|
35
|
+
self.compressionBehavior = compressionBehavior
|
|
24
36
|
}
|
|
25
37
|
|
|
26
38
|
private var bottomInset: CGFloat { safeAreaBottomInset() }
|
|
27
|
-
|
|
39
|
+
// the Duo bars overlay the content or sit in the side strip, so only the home indicator is kept clear
|
|
40
|
+
private var reservedBottom: CGFloat {
|
|
41
|
+
layout.isIPhoneDuo ? layout.insets.bottom : BOTTOM_TAB_BAR_HEIGHT + bottomInset + Spacing.S
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private var compression: DuoBarCompression {
|
|
45
|
+
DuoBarCompression.resolve(
|
|
46
|
+
layout: layout,
|
|
47
|
+
tabCount: items.count,
|
|
48
|
+
hasQr: floatingButton != nil,
|
|
49
|
+
toolbarHeight: toolbarHeight,
|
|
50
|
+
// 'toolbar' compression keeps the tab bar whole until the toolbar is tapped
|
|
51
|
+
expanded: Binding(
|
|
52
|
+
get: { expandedBar ?? (compressionBehavior == .toolbar ? .tabBar : .toolbar) },
|
|
53
|
+
set: { expandedBar = $0 }
|
|
54
|
+
),
|
|
55
|
+
opened: $openedBar,
|
|
56
|
+
setToolbarHeight: { height in
|
|
57
|
+
if toolbarHeight != height { toolbarHeight = height }
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private func duoInsets(_ compression: DuoBarCompression) -> DuoTabBarInsets {
|
|
63
|
+
layout.duoTabBarInsets(
|
|
64
|
+
itemCount: items.count,
|
|
65
|
+
hasFloatingButton: floatingButton != nil,
|
|
66
|
+
tabBarCompressed: compression.tabBarCompressed
|
|
67
|
+
)
|
|
68
|
+
}
|
|
28
69
|
|
|
29
70
|
public var body: some View {
|
|
71
|
+
let compression = compression
|
|
30
72
|
ZStack(alignment: .bottom) {
|
|
31
73
|
// Tab content area, inset above the bar.
|
|
32
74
|
ZStack {
|
|
@@ -38,20 +80,31 @@ public struct BottomTab: View {
|
|
|
38
80
|
}
|
|
39
81
|
}
|
|
40
82
|
.padding(.bottom, reservedBottom)
|
|
83
|
+
.environment(\.duoTabBarInsets, duoInsets(compression))
|
|
84
|
+
.environment(\.duoBarCompression, compression)
|
|
41
85
|
.animation(.easeInOut(duration: 0.2), value: selectedIndex)
|
|
42
86
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
BottomTabBar(
|
|
87
|
+
if layout.isIPhoneDuo {
|
|
88
|
+
DuoTabBar(
|
|
46
89
|
items: items,
|
|
47
90
|
floatingButton: floatingButton,
|
|
48
91
|
selectedIndex: selectedIndex,
|
|
49
|
-
onTabSelected:
|
|
50
|
-
|
|
51
|
-
}
|
|
92
|
+
onTabSelected: selectTab,
|
|
93
|
+
compression: compression
|
|
52
94
|
)
|
|
53
|
-
|
|
54
|
-
|
|
95
|
+
} else {
|
|
96
|
+
// Tab bar + home-indicator filler.
|
|
97
|
+
VStack(spacing: 0) {
|
|
98
|
+
BottomTabBar(
|
|
99
|
+
items: items,
|
|
100
|
+
floatingButton: floatingButton,
|
|
101
|
+
selectedIndex: selectedIndex,
|
|
102
|
+
onTabSelected: selectTab
|
|
103
|
+
)
|
|
104
|
+
Colors.black01
|
|
105
|
+
.frame(height: bottomInset + Spacing.S)
|
|
106
|
+
.ignoresSafeArea(edges: .horizontal)
|
|
107
|
+
}
|
|
55
108
|
}
|
|
56
109
|
}
|
|
57
110
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
@@ -59,6 +112,10 @@ public struct BottomTab: View {
|
|
|
59
112
|
.preference(key: IsBottomTabRootKey.self, value: true)
|
|
60
113
|
}
|
|
61
114
|
|
|
115
|
+
private func selectTab(_ index: Int) {
|
|
116
|
+
if index != selectedIndex { selectedIndex = index }
|
|
117
|
+
}
|
|
118
|
+
|
|
62
119
|
@ViewBuilder
|
|
63
120
|
private func tabScreen(for index: Int) -> some View {
|
|
64
121
|
let item = items[index]
|
|
@@ -65,7 +65,10 @@ struct BottomTabBar: View {
|
|
|
65
65
|
let selectedIndex: Int
|
|
66
66
|
let onTabSelected: (Int) -> Void
|
|
67
67
|
|
|
68
|
-
private var
|
|
68
|
+
@Environment(\.screenLayout) private var layout
|
|
69
|
+
|
|
70
|
+
// the bar sits between the side safe areas
|
|
71
|
+
private var screenWidth: CGFloat { layout.width - layout.insets.leading - layout.insets.trailing }
|
|
69
72
|
private var mid: Int { items.count / 2 }
|
|
70
73
|
|
|
71
74
|
private var tabWidth: CGFloat {
|
|
@@ -95,7 +98,7 @@ struct BottomTabBar: View {
|
|
|
95
98
|
}
|
|
96
99
|
.frame(maxWidth: .infinity)
|
|
97
100
|
.frame(height: BOTTOM_TAB_BAR_HEIGHT)
|
|
98
|
-
.background(Colors.black01)
|
|
101
|
+
.background(Colors.black01.ignoresSafeArea(edges: .horizontal))
|
|
99
102
|
|
|
100
103
|
// Animated selection indicator.
|
|
101
104
|
RoundedRectangle(cornerRadius: 2)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
//
|
|
2
|
+
// DuoBarCompression.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// Port of RN `BottomTab/DuoBarCompression.tsx` (HIG, Designing for iPhone Duo: Vertical controls):
|
|
6
|
+
// keeps the side bar's toolbar and tab bar clear of each other.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import SwiftUI
|
|
10
|
+
|
|
11
|
+
/// iPhone Duo: which side bar bar starts compressed when the strip cannot hold both whole.
|
|
12
|
+
public enum DuoBarCompressionBehavior {
|
|
13
|
+
case toolbar, tabBar
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
enum DuoBar {
|
|
17
|
+
case toolbar, tabBar
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
struct DuoBarCompression {
|
|
21
|
+
var toolbarCompressed = false
|
|
22
|
+
var tabBarCompressed = false
|
|
23
|
+
/// The open rail needs the toolbar button slot too: only Back stays.
|
|
24
|
+
var toolbarHidden = false
|
|
25
|
+
var expandToolbar: () -> Void = {}
|
|
26
|
+
var expandTabBar: () -> Void = {}
|
|
27
|
+
/// A rail opened from the compressed state closes once a tab is picked.
|
|
28
|
+
var onTabSelected: () -> Void = {}
|
|
29
|
+
/// Side bar toolkits report their full height so the tab bar knows what is left.
|
|
30
|
+
var setToolbarHeight: (CGFloat) -> Void = { _ in }
|
|
31
|
+
|
|
32
|
+
/// Space kept between the top and the bottom groups (HIG Vertical controls).
|
|
33
|
+
static let groupGap = Spacing.M
|
|
34
|
+
/// Back, then the right toolkit: help + home capsule.
|
|
35
|
+
static let defaultToolbarHeight = DuoMetrics.controlSize + Spacing.M + 2 * DuoMetrics.itemPitch
|
|
36
|
+
/// Back, then the single button the toolbar folds into.
|
|
37
|
+
private static let compressedToolbarHeight = 2 * DuoMetrics.controlSize + Spacing.M
|
|
38
|
+
/// Back alone, while a rail opened from the compressed state takes the toolbar slot.
|
|
39
|
+
private static let backOnlyHeight = DuoMetrics.controlSize
|
|
40
|
+
|
|
41
|
+
/// - both fit: both stay whole
|
|
42
|
+
/// - one fits whole beside the other compressed: one is compressed, tapping it swaps them
|
|
43
|
+
/// - otherwise both rest compressed; tapping one opens it for a moment (the rail closes
|
|
44
|
+
/// again once a tab is picked)
|
|
45
|
+
static func resolve(
|
|
46
|
+
layout: ScreenLayout,
|
|
47
|
+
tabCount: Int,
|
|
48
|
+
hasQr: Bool,
|
|
49
|
+
toolbarHeight: CGFloat,
|
|
50
|
+
expanded: Binding<DuoBar>,
|
|
51
|
+
opened: Binding<DuoBar?>,
|
|
52
|
+
setToolbarHeight: @escaping (CGFloat) -> Void
|
|
53
|
+
) -> DuoBarCompression {
|
|
54
|
+
guard let sideBar = layout.sideBar else { return DuoBarCompression() }
|
|
55
|
+
let room = layout.height - sideBar.bottom - sideBar.top - groupGap
|
|
56
|
+
let tabBar = duoRailHeight(itemCount: tabCount, hasQr: hasQr)
|
|
57
|
+
let compressedTabBar = duoRailHeight(itemCount: 1, hasQr: hasQr, compressed: true)
|
|
58
|
+
func fits(_ toolbar: CGFloat, _ tabs: CGFloat) -> Bool { toolbar + tabs <= room }
|
|
59
|
+
|
|
60
|
+
if fits(toolbarHeight, tabBar) {
|
|
61
|
+
return DuoBarCompression(setToolbarHeight: setToolbarHeight)
|
|
62
|
+
}
|
|
63
|
+
if fits(compressedToolbarHeight, tabBar) && fits(toolbarHeight, compressedTabBar) {
|
|
64
|
+
return DuoBarCompression(
|
|
65
|
+
toolbarCompressed: expanded.wrappedValue == .tabBar,
|
|
66
|
+
tabBarCompressed: expanded.wrappedValue == .toolbar,
|
|
67
|
+
expandToolbar: { expanded.wrappedValue = .toolbar },
|
|
68
|
+
expandTabBar: { expanded.wrappedValue = .tabBar },
|
|
69
|
+
setToolbarHeight: setToolbarHeight
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let expandToolbar = { opened.wrappedValue = .toolbar }
|
|
74
|
+
let expandTabBar = { opened.wrappedValue = .tabBar }
|
|
75
|
+
if opened.wrappedValue == .tabBar && fits(backOnlyHeight, tabBar) {
|
|
76
|
+
return DuoBarCompression(
|
|
77
|
+
toolbarCompressed: true,
|
|
78
|
+
tabBarCompressed: false,
|
|
79
|
+
toolbarHidden: true,
|
|
80
|
+
expandToolbar: expandToolbar,
|
|
81
|
+
expandTabBar: expandTabBar,
|
|
82
|
+
onTabSelected: { opened.wrappedValue = nil },
|
|
83
|
+
setToolbarHeight: setToolbarHeight
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
return DuoBarCompression(
|
|
87
|
+
toolbarCompressed: opened.wrappedValue != .toolbar,
|
|
88
|
+
tabBarCompressed: true,
|
|
89
|
+
expandToolbar: expandToolbar,
|
|
90
|
+
expandTabBar: expandTabBar,
|
|
91
|
+
setToolbarHeight: setToolbarHeight
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private struct DuoBarCompressionKey: EnvironmentKey {
|
|
97
|
+
static let defaultValue = DuoBarCompression()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
extension EnvironmentValues {
|
|
101
|
+
var duoBarCompression: DuoBarCompression {
|
|
102
|
+
get { self[DuoBarCompressionKey.self] }
|
|
103
|
+
set { self[DuoBarCompressionKey.self] = newValue }
|
|
104
|
+
}
|
|
105
|
+
}
|