4*16 widget pool grid for button macro
This commit is contained in:
1 parent
2c2088e4b1
commit
bb44b2067e
1 file changed
+246
-147
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
|
||||||
@available(iOS 13.0, *)
|
@available(iOS 13.0, *)
|
||||||
private extension View {
|
private extension View {
|
||||||
@@ -449,7 +450,7 @@ struct WidgetPickerView: View {
|
|||||||
case error
|
case error
|
||||||
}
|
}
|
||||||
|
|
||||||
private let maxPoolSlots = 16
|
private let maxPoolSlots = 64
|
||||||
private let buttonMacroMinimumDuration: Double = 30
|
private let buttonMacroMinimumDuration: Double = 30
|
||||||
private let buttonMacroTimedMinimumDuration: Double = 50
|
private let buttonMacroTimedMinimumDuration: Double = 50
|
||||||
private let buttonMacroMaximumDuration: Double = 2000
|
private let buttonMacroMaximumDuration: Double = 2000
|
||||||
@@ -505,6 +506,8 @@ struct WidgetPickerView: View {
|
|||||||
@SwiftUI.State private var triggerIntervalManualValueText: String = ""
|
@SwiftUI.State private var triggerIntervalManualValueText: String = ""
|
||||||
@SwiftUI.State private var suppressKeyboardDrivenLayoutAnimation = false
|
@SwiftUI.State private var suppressKeyboardDrivenLayoutAnimation = false
|
||||||
@SwiftUI.State private var poolGridInteractionResetToken: Int = 0
|
@SwiftUI.State private var poolGridInteractionResetToken: Int = 0
|
||||||
|
@SwiftUI.State private var poolAutoScrollTargetItemID: UUID? = nil
|
||||||
|
@SwiftUI.State private var poolAutoScrollRequestToken: Int = 0
|
||||||
|
|
||||||
private static func restoredTab(from availableTabs: [WidgetPickerTab]) -> WidgetPickerTab {
|
private static func restoredTab(from availableTabs: [WidgetPickerTab]) -> WidgetPickerTab {
|
||||||
if let persistedIdentifier = UserDefaults.standard.string(forKey: lastSelectedTabDefaultsKey),
|
if let persistedIdentifier = UserDefaults.standard.string(forKey: lastSelectedTabDefaultsKey),
|
||||||
@@ -1156,6 +1159,7 @@ struct WidgetPickerView: View {
|
|||||||
let tabFontSize = isPadPortrait ? 12.0 : metrics.tabFontSize
|
let tabFontSize = isPadPortrait ? 12.0 : metrics.tabFontSize
|
||||||
let tabHorizontalPadding = isPadPortrait ? 10.0 : metrics.tabHorizontalPadding
|
let tabHorizontalPadding = isPadPortrait ? 10.0 : metrics.tabHorizontalPadding
|
||||||
let tabHeight = isPadPortrait ? 30.0 : metrics.tabHeight
|
let tabHeight = isPadPortrait ? 30.0 : metrics.tabHeight
|
||||||
|
let tabMinWidth = isPadPortrait ? 88.0 : (metrics.isPhone ? 43 : 50)
|
||||||
|
|
||||||
return HStack(spacing: tabSpacing) {
|
return HStack(spacing: tabSpacing) {
|
||||||
ForEach(availableTabs) { tab in
|
ForEach(availableTabs) { tab in
|
||||||
@@ -1166,6 +1170,7 @@ struct WidgetPickerView: View {
|
|||||||
.font(.system(size: tabFontSize, weight: .bold, design: .rounded))
|
.font(.system(size: tabFontSize, weight: .bold, design: .rounded))
|
||||||
.foregroundColor(selectedTab == tab ? Color.white : Color.black.opacity(0.56))
|
.foregroundColor(selectedTab == tab ? Color.white : Color.black.opacity(0.56))
|
||||||
.padding(.horizontal, tabHorizontalPadding)
|
.padding(.horizontal, tabHorizontalPadding)
|
||||||
|
.frame(minWidth: tabMinWidth)
|
||||||
.frame(height: tabHeight)
|
.frame(height: tabHeight)
|
||||||
.background(
|
.background(
|
||||||
Capsule()
|
Capsule()
|
||||||
@@ -1231,7 +1236,9 @@ struct WidgetPickerView: View {
|
|||||||
items: poolItems,
|
items: poolItems,
|
||||||
onItemTap: { handlePoolItemTap($0) },
|
onItemTap: { handlePoolItemTap($0) },
|
||||||
spacing: metrics.poolGridSpacing,
|
spacing: metrics.poolGridSpacing,
|
||||||
contentInset: metrics.poolGridInset
|
contentInset: metrics.poolGridInset,
|
||||||
|
scrollTargetItemID: poolAutoScrollTargetItemID,
|
||||||
|
scrollRequestToken: poolAutoScrollRequestToken
|
||||||
)
|
)
|
||||||
.id(poolGridInteractionResetToken)
|
.id(poolGridInteractionResetToken)
|
||||||
.aspectRatio(1, contentMode: .fit)
|
.aspectRatio(1, contentMode: .fit)
|
||||||
@@ -1349,6 +1356,7 @@ struct WidgetPickerView: View {
|
|||||||
.font(.system(size: customFontSize ?? metrics.tipFontSize, weight: .semibold, design: .rounded))
|
.font(.system(size: customFontSize ?? metrics.tipFontSize, weight: .semibold, design: .rounded))
|
||||||
.foregroundColor(tipMessageColor)
|
.foregroundColor(tipMessageColor)
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
|
.minimumScaleFactor(0.5)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
@@ -1423,6 +1431,8 @@ struct WidgetPickerView: View {
|
|||||||
rebalanceTriggerIntervalItems()
|
rebalanceTriggerIntervalItems()
|
||||||
}
|
}
|
||||||
normalizeLeadingFunctionalItemPriorityIfNeeded()
|
normalizeLeadingFunctionalItemPriorityIfNeeded()
|
||||||
|
poolAutoScrollTargetItemID = item.id
|
||||||
|
poolAutoScrollRequestToken += 1
|
||||||
updateTipMessageForCurrentPoolState()
|
updateTipMessageForCurrentPoolState()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1832,8 +1842,8 @@ struct WidgetPickerView: View {
|
|||||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
showButtonMacroSheet = false
|
showButtonMacroSheet = false
|
||||||
editingButtonMacroItemID = nil
|
// editingButtonMacroItemID = nil
|
||||||
poolGridInteractionResetToken += 1
|
// poolGridInteractionResetToken += 1
|
||||||
NSLog("WidgetPickerView dismissButtonMacroSheet resetTokenAfter=%d", poolGridInteractionResetToken)
|
NSLog("WidgetPickerView dismissButtonMacroSheet resetTokenAfter=%d", poolGridInteractionResetToken)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2034,8 +2044,8 @@ struct WidgetPickerView: View {
|
|||||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
showTriggerIntervalSheet = false
|
showTriggerIntervalSheet = false
|
||||||
editingTriggerIntervalItemID = nil
|
// editingTriggerIntervalItemID = nil
|
||||||
poolGridInteractionResetToken += 1
|
// poolGridInteractionResetToken += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2514,6 +2524,106 @@ struct WidgetPickerView: View {
|
|||||||
}
|
}
|
||||||
// .widgetPickerIgnoreKeyboardSafeAreaWhenAvailable()
|
// .widgetPickerIgnoreKeyboardSafeAreaWhenAvailable()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var triggerIntervalSheet: some View {
|
||||||
|
let isPhone = UIDevice.current.userInterfaceIdiom == .phone
|
||||||
|
let contentSpacing: CGFloat = isPhone ? 10 : 16
|
||||||
|
let sectionSpacing: CGFloat = isPhone ? 7 : 10
|
||||||
|
let fieldHeight: CGFloat = isPhone ? 30 : 38
|
||||||
|
let segmentedControlHeight: CGFloat = isPhone ? 36 : 38
|
||||||
|
let actionSpacing: CGFloat = isPhone ? 8 : 12
|
||||||
|
let actionHeight: CGFloat = isPhone ? 34 : 44
|
||||||
|
let actionFontSize: CGFloat = isPhone ? 13 : 15
|
||||||
|
let cardPadding: CGFloat = isPhone ? 12 : 18
|
||||||
|
let cardMaxWidth: CGFloat = isPhone ? 320 : 420
|
||||||
|
let cardCornerRadius: CGFloat = isPhone ? 22 : 28
|
||||||
|
let horizontalInset: CGFloat = isPhone ? 14 : 24
|
||||||
|
|
||||||
|
return ZStack {
|
||||||
|
Color.black.opacity(0.28)
|
||||||
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: contentSpacing) {
|
||||||
|
VStack(alignment: .leading, spacing: sectionSpacing) {
|
||||||
|
createInteractiveControlSection(
|
||||||
|
title: "",
|
||||||
|
controlHeight: segmentedControlHeight
|
||||||
|
) {
|
||||||
|
Picker("", selection: triggerIntervalEditModeBinding) {
|
||||||
|
ForEach(TriggerIntervalEditMode.allCases) { mode in
|
||||||
|
Text(mode.title).tag(mode.rawValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheetSegmentedControlStyle(height: segmentedControlHeight)
|
||||||
|
}
|
||||||
|
.zIndex(30)
|
||||||
|
|
||||||
|
createInteractiveControlSection(
|
||||||
|
title: triggerIntervalTitleText,
|
||||||
|
controlHeight: fieldHeight
|
||||||
|
) {
|
||||||
|
Slider(
|
||||||
|
value: $triggerIntervalEditorValue,
|
||||||
|
in: triggerIntervalMinimumDuration...triggerIntervalMaximumDuration,
|
||||||
|
step: 1
|
||||||
|
)
|
||||||
|
.environment(\.colorScheme, .light)
|
||||||
|
.frame(height: fieldHeight)
|
||||||
|
.disabled(manualTriggerIntervalValue != nil)
|
||||||
|
.opacity(manualTriggerIntervalValue == nil ? 1.0 : 0.45)
|
||||||
|
}
|
||||||
|
.zIndex(20)
|
||||||
|
|
||||||
|
createWidgetSection(title: LocalizationHelper.localizedString(forKey: "Manually enter trigger interval in milliseconds")) {
|
||||||
|
InteractiveTextField(
|
||||||
|
placeholder: "",
|
||||||
|
text: $triggerIntervalManualValueText
|
||||||
|
)
|
||||||
|
.frame(height: fieldHeight)
|
||||||
|
}
|
||||||
|
.zIndex(40)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: actionSpacing) {
|
||||||
|
PoolActionChip(
|
||||||
|
title: LocalizationHelper.localizedString(forKey: "Cancel"),
|
||||||
|
isPrimary: false,
|
||||||
|
height: actionHeight,
|
||||||
|
fontSize: actionFontSize,
|
||||||
|
cornerRadius: 14
|
||||||
|
) {
|
||||||
|
dismissTriggerIntervalSheet()
|
||||||
|
}
|
||||||
|
|
||||||
|
PoolActionChip(
|
||||||
|
title: LocalizationHelper.localizedString(forKey: "Confirm"),
|
||||||
|
isPrimary: true,
|
||||||
|
height: actionHeight,
|
||||||
|
fontSize: actionFontSize,
|
||||||
|
cornerRadius: 14
|
||||||
|
) {
|
||||||
|
applyTriggerIntervalChanges()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, isPhone ? 2 : 0)
|
||||||
|
.zIndex(0)
|
||||||
|
}
|
||||||
|
.padding(cardPadding)
|
||||||
|
.frame(maxWidth: cardMaxWidth)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: cardCornerRadius, style: .continuous)
|
||||||
|
.fill(Color(red: 0.94, green: 0.97, blue: 0.98).opacity(0.98))
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: cardCornerRadius, style: .continuous)
|
||||||
|
.stroke(Color.white.opacity(0.96), lineWidth: 1.1)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.shadow(color: Color.black.opacity(0.12), radius: 24, x: 0, y: 12)
|
||||||
|
.padding(.horizontal, horizontalInset)
|
||||||
|
}
|
||||||
|
// .widgetPickerIgnoreKeyboardSafeAreaWhenAvailable()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
private var buttonMacroSheet: some View {
|
private var buttonMacroSheet: some View {
|
||||||
@@ -2618,104 +2728,6 @@ struct WidgetPickerView: View {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
private var triggerIntervalSheet: some View {
|
|
||||||
let isPhone = UIDevice.current.userInterfaceIdiom == .phone
|
|
||||||
let contentSpacing: CGFloat = isPhone ? 10 : 16
|
|
||||||
let sectionSpacing: CGFloat = isPhone ? 7 : 10
|
|
||||||
let fieldHeight: CGFloat = isPhone ? 30 : 38
|
|
||||||
let segmentedControlHeight: CGFloat = isPhone ? 36 : 38
|
|
||||||
let actionSpacing: CGFloat = isPhone ? 8 : 12
|
|
||||||
let actionHeight: CGFloat = isPhone ? 34 : 44
|
|
||||||
let actionFontSize: CGFloat = isPhone ? 13 : 15
|
|
||||||
let cardPadding: CGFloat = isPhone ? 12 : 18
|
|
||||||
let cardMaxWidth: CGFloat = isPhone ? 320 : 420
|
|
||||||
let cardCornerRadius: CGFloat = isPhone ? 22 : 28
|
|
||||||
let horizontalInset: CGFloat = isPhone ? 14 : 24
|
|
||||||
|
|
||||||
return ZStack {
|
|
||||||
Color.black.opacity(0.28)
|
|
||||||
.edgesIgnoringSafeArea(.all)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: contentSpacing) {
|
|
||||||
VStack(alignment: .leading, spacing: sectionSpacing) {
|
|
||||||
createInteractiveControlSection(
|
|
||||||
title: "",
|
|
||||||
controlHeight: segmentedControlHeight
|
|
||||||
) {
|
|
||||||
Picker("", selection: triggerIntervalEditModeBinding) {
|
|
||||||
ForEach(TriggerIntervalEditMode.allCases) { mode in
|
|
||||||
Text(mode.title).tag(mode.rawValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.sheetSegmentedControlStyle(height: segmentedControlHeight)
|
|
||||||
}
|
|
||||||
.zIndex(30)
|
|
||||||
|
|
||||||
createInteractiveControlSection(
|
|
||||||
title: triggerIntervalTitleText,
|
|
||||||
controlHeight: fieldHeight
|
|
||||||
) {
|
|
||||||
Slider(
|
|
||||||
value: $triggerIntervalEditorValue,
|
|
||||||
in: triggerIntervalMinimumDuration...triggerIntervalMaximumDuration,
|
|
||||||
step: 1
|
|
||||||
)
|
|
||||||
.environment(\.colorScheme, .light)
|
|
||||||
.frame(height: fieldHeight)
|
|
||||||
.disabled(manualTriggerIntervalValue != nil)
|
|
||||||
.opacity(manualTriggerIntervalValue == nil ? 1.0 : 0.45)
|
|
||||||
}
|
|
||||||
.zIndex(20)
|
|
||||||
|
|
||||||
createWidgetSection(title: LocalizationHelper.localizedString(forKey: "Trigger interval")) {
|
|
||||||
InteractiveTextField(
|
|
||||||
placeholder: LocalizationHelper.localizedString(forKey: "Manually enter trigger interval in milliseconds"),
|
|
||||||
text: $triggerIntervalManualValueText
|
|
||||||
)
|
|
||||||
.frame(height: fieldHeight)
|
|
||||||
}
|
|
||||||
.zIndex(40)
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack(spacing: actionSpacing) {
|
|
||||||
PoolActionChip(
|
|
||||||
title: LocalizationHelper.localizedString(forKey: "Cancel"),
|
|
||||||
isPrimary: false,
|
|
||||||
height: actionHeight,
|
|
||||||
fontSize: actionFontSize,
|
|
||||||
cornerRadius: 14
|
|
||||||
) {
|
|
||||||
dismissTriggerIntervalSheet()
|
|
||||||
}
|
|
||||||
|
|
||||||
PoolActionChip(
|
|
||||||
title: LocalizationHelper.localizedString(forKey: "Confirm"),
|
|
||||||
isPrimary: true,
|
|
||||||
height: actionHeight,
|
|
||||||
fontSize: actionFontSize,
|
|
||||||
cornerRadius: 14
|
|
||||||
) {
|
|
||||||
applyTriggerIntervalChanges()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.top, isPhone ? 2 : 0)
|
|
||||||
.zIndex(0)
|
|
||||||
}
|
|
||||||
.padding(cardPadding)
|
|
||||||
.frame(maxWidth: cardMaxWidth)
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: cardCornerRadius, style: .continuous)
|
|
||||||
.fill(Color(red: 0.94, green: 0.97, blue: 0.98).opacity(0.98))
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: cardCornerRadius, style: .continuous)
|
|
||||||
.stroke(Color.white.opacity(0.96), lineWidth: 1.1)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.shadow(color: Color.black.opacity(0.12), radius: 24, x: 0, y: 12)
|
|
||||||
.padding(.horizontal, horizontalInset)
|
|
||||||
}
|
|
||||||
// .widgetPickerIgnoreKeyboardSafeAreaWhenAvailable()
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
private func createInteractiveControlContainer<Content: View>(
|
private func createInteractiveControlContainer<Content: View>(
|
||||||
@@ -3826,8 +3838,12 @@ struct WidgetPoolGridView: View {
|
|||||||
let onItemTap: (WidgetPoolItem) -> Void
|
let onItemTap: (WidgetPoolItem) -> Void
|
||||||
let spacing: CGFloat
|
let spacing: CGFloat
|
||||||
let contentInset: CGFloat
|
let contentInset: CGFloat
|
||||||
|
let scrollTargetItemID: UUID?
|
||||||
|
let scrollRequestToken: Int
|
||||||
|
|
||||||
private let columns = 4
|
private let columns = 4
|
||||||
|
private let visibleRows = 4
|
||||||
|
@SwiftUI.State private var lastHandledScrollRequestToken: Int = -1
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { proxy in
|
GeometryReader { proxy in
|
||||||
@@ -3835,12 +3851,16 @@ struct WidgetPoolGridView: View {
|
|||||||
let height = proxy.size.height
|
let height = proxy.size.height
|
||||||
let availableSide = max(min(width, height) - contentInset * 2, 1)
|
let availableSide = max(min(width, height) - contentInset * 2, 1)
|
||||||
let cellSize = (availableSide - CGFloat(columns - 1) * spacing) / CGFloat(columns)
|
let cellSize = (availableSide - CGFloat(columns - 1) * spacing) / CGFloat(columns)
|
||||||
let gridSide = cellSize * CGFloat(columns) + spacing * CGFloat(columns - 1)
|
|
||||||
let gridOriginX = (width - gridSide) * 0.5
|
|
||||||
let gridOriginY = (height - gridSide) * 0.5
|
|
||||||
let placements = makePlacements(for: items, columns: columns)
|
let placements = makePlacements(for: items, columns: columns)
|
||||||
|
let totalRows = max(visibleRows, (placements.map(\.row).max() ?? -1) + 1)
|
||||||
|
let gridWidth = cellSize * CGFloat(columns) + spacing * CGFloat(columns - 1)
|
||||||
|
let viewportGridHeight = cellSize * CGFloat(visibleRows) + spacing * CGFloat(visibleRows - 1)
|
||||||
|
let contentGridHeight = cellSize * CGFloat(totalRows) + spacing * CGFloat(max(totalRows - 1, 0))
|
||||||
|
let viewportHeight = viewportGridHeight + contentInset * 2
|
||||||
|
let contentHeight = contentGridHeight + contentInset * 2
|
||||||
|
let gridOriginX = (width - gridWidth) * 0.5
|
||||||
|
|
||||||
ZStack(alignment: .topLeading) {
|
ZStack {
|
||||||
RoundedRectangle(cornerRadius: 24, style: .continuous)
|
RoundedRectangle(cornerRadius: 24, style: .continuous)
|
||||||
.fill(
|
.fill(
|
||||||
LinearGradient(
|
LinearGradient(
|
||||||
@@ -3861,52 +3881,131 @@ struct WidgetPoolGridView: View {
|
|||||||
NSLog("WidgetPickerView pool grid background tapped")
|
NSLog("WidgetPickerView pool grid background tapped")
|
||||||
}
|
}
|
||||||
|
|
||||||
ForEach(0..<columns, id: \.self) { column in
|
Group {
|
||||||
ForEach(0..<columns, id: \.self) { row in
|
if #available(iOS 14.0, *) {
|
||||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
poolScrollViewWithAutoScroll(
|
||||||
.fill(Color.white.opacity(0.16))
|
width: width,
|
||||||
.overlay(
|
cellSize: cellSize,
|
||||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
contentHeight: contentHeight,
|
||||||
.stroke(Color.white.opacity(0.25), style: StrokeStyle(lineWidth: 1, dash: [4, 5]))
|
contentInset: contentInset,
|
||||||
|
totalRows: totalRows,
|
||||||
|
gridOriginX: gridOriginX,
|
||||||
|
placements: placements,
|
||||||
|
showsIndicators: totalRows > visibleRows
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ScrollView(.vertical, showsIndicators: totalRows > visibleRows) {
|
||||||
|
poolGridContent(
|
||||||
|
width: width,
|
||||||
|
cellSize: cellSize,
|
||||||
|
contentHeight: contentHeight,
|
||||||
|
contentInset: contentInset,
|
||||||
|
totalRows: totalRows,
|
||||||
|
gridOriginX: gridOriginX,
|
||||||
|
placements: placements
|
||||||
)
|
)
|
||||||
.frame(width: cellSize, height: cellSize)
|
}
|
||||||
.position(
|
|
||||||
x: gridOriginX + CGFloat(column) * (cellSize + spacing) + cellSize * 0.5,
|
|
||||||
y: gridOriginY + CGFloat(row) * (cellSize + spacing) + cellSize * 0.5
|
|
||||||
)
|
|
||||||
.allowsHitTesting(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.frame(height: viewportHeight)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
|
||||||
|
}
|
||||||
|
.frame(height: viewportHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ForEach(placements) { placement in
|
@available(iOS 14.0, *)
|
||||||
poolItemView(for: placement.item, cellSize: cellSize)
|
private func poolScrollViewWithAutoScroll(
|
||||||
.frame(
|
width: CGFloat,
|
||||||
width: cellSize * CGFloat(placement.item.span) + spacing * CGFloat(placement.item.span - 1),
|
cellSize: CGFloat,
|
||||||
height: cellSize
|
contentHeight: CGFloat,
|
||||||
)
|
contentInset: CGFloat,
|
||||||
.contentShape(Rectangle())
|
totalRows: Int,
|
||||||
.onTapGesture {
|
gridOriginX: CGFloat,
|
||||||
NSLog(
|
placements: [WidgetPoolPlacement],
|
||||||
"WidgetPickerView pool item tapped cmd=%@ id=%@ row=%d column=%d span=%d",
|
showsIndicators: Bool
|
||||||
placement.item.cmd,
|
) -> some View {
|
||||||
placement.item.id.uuidString,
|
ScrollViewReader { scrollProxy in
|
||||||
placement.row,
|
ScrollView(.vertical, showsIndicators: showsIndicators) {
|
||||||
placement.column,
|
poolGridContent(
|
||||||
placement.item.span
|
width: width,
|
||||||
)
|
cellSize: cellSize,
|
||||||
onItemTap(placement.item)
|
contentHeight: contentHeight,
|
||||||
}
|
contentInset: contentInset,
|
||||||
.position(
|
totalRows: totalRows,
|
||||||
x: gridOriginX + CGFloat(placement.column) * (cellSize + spacing)
|
gridOriginX: gridOriginX,
|
||||||
+ (cellSize * CGFloat(placement.item.span) + spacing * CGFloat(placement.item.span - 1)) * 0.5,
|
placements: placements
|
||||||
y: gridOriginY + CGFloat(placement.row) * (cellSize + spacing) + cellSize * 0.5
|
)
|
||||||
)
|
}
|
||||||
.zIndex(1)
|
.onReceive(Just(scrollRequestToken)) { requestToken in
|
||||||
|
guard requestToken != lastHandledScrollRequestToken else { return }
|
||||||
|
lastHandledScrollRequestToken = requestToken
|
||||||
|
guard let scrollTargetItemID else { return }
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
scrollProxy.scrollTo(scrollTargetItemID, anchor: .bottom)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func poolGridContent(
|
||||||
|
width: CGFloat,
|
||||||
|
cellSize: CGFloat,
|
||||||
|
contentHeight: CGFloat,
|
||||||
|
contentInset: CGFloat,
|
||||||
|
totalRows: Int,
|
||||||
|
gridOriginX: CGFloat,
|
||||||
|
placements: [WidgetPoolPlacement]
|
||||||
|
) -> some View {
|
||||||
|
ZStack(alignment: .topLeading) {
|
||||||
|
ForEach(0..<columns, id: \.self) { column in
|
||||||
|
ForEach(0..<totalRows, id: \.self) { row in
|
||||||
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||||
|
.fill(Color.white.opacity(0.16))
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||||
|
.stroke(Color.white.opacity(0.25), style: StrokeStyle(lineWidth: 1, dash: [4, 5]))
|
||||||
|
)
|
||||||
|
.frame(width: cellSize, height: cellSize)
|
||||||
|
.position(
|
||||||
|
x: gridOriginX + CGFloat(column) * (cellSize + spacing) + cellSize * 0.5,
|
||||||
|
y: contentInset + CGFloat(row) * (cellSize + spacing) + cellSize * 0.5
|
||||||
|
)
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ForEach(placements) { placement in
|
||||||
|
poolItemView(for: placement.item, cellSize: cellSize)
|
||||||
|
.frame(
|
||||||
|
width: cellSize * CGFloat(placement.item.span) + spacing * CGFloat(placement.item.span - 1),
|
||||||
|
height: cellSize
|
||||||
|
)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.id(placement.item.id)
|
||||||
|
.onTapGesture {
|
||||||
|
NSLog(
|
||||||
|
"WidgetPickerView pool item tapped cmd=%@ id=%@ row=%d column=%d span=%d",
|
||||||
|
placement.item.cmd,
|
||||||
|
placement.item.id.uuidString,
|
||||||
|
placement.row,
|
||||||
|
placement.column,
|
||||||
|
placement.item.span
|
||||||
|
)
|
||||||
|
onItemTap(placement.item)
|
||||||
|
}
|
||||||
|
.position(
|
||||||
|
x: gridOriginX + CGFloat(placement.column) * (cellSize + spacing)
|
||||||
|
+ (cellSize * CGFloat(placement.item.span) + spacing * CGFloat(placement.item.span - 1)) * 0.5,
|
||||||
|
y: contentInset + CGFloat(placement.row) * (cellSize + spacing) + cellSize * 0.5
|
||||||
|
)
|
||||||
|
.zIndex(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: width, height: contentHeight, alignment: .topLeading)
|
||||||
|
}
|
||||||
|
|
||||||
private func poolItemView(for item: WidgetPoolItem, cellSize: CGFloat) -> some View {
|
private func poolItemView(for item: WidgetPoolItem, cellSize: CGFloat) -> some View {
|
||||||
Group {
|
Group {
|
||||||
if item.visualKind == .gamepadButton {
|
if item.visualKind == .gamepadButton {
|
||||||
@@ -3929,7 +4028,7 @@ struct WidgetPoolGridView: View {
|
|||||||
Text(item.displayCmd)
|
Text(item.displayCmd)
|
||||||
.font(.system(size: max(11, cellSize * 0.18), weight: .bold, design: .rounded))
|
.font(.system(size: max(11, cellSize * 0.18), weight: .bold, design: .rounded))
|
||||||
.foregroundColor(Color.black.opacity(0.70))
|
.foregroundColor(Color.black.opacity(0.70))
|
||||||
.minimumScaleFactor(0.6)
|
.minimumScaleFactor(0.25)
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.padding(.horizontal, 6)
|
.padding(.horizontal, 6)
|
||||||
@@ -3955,7 +4054,7 @@ struct WidgetPoolGridView: View {
|
|||||||
Text(item.displayCmd)
|
Text(item.displayCmd)
|
||||||
.font(.system(size: max(11, cellSize * 0.18), weight: .bold, design: .rounded))
|
.font(.system(size: max(11, cellSize * 0.18), weight: .bold, design: .rounded))
|
||||||
.foregroundColor(Color.black.opacity(0.70))
|
.foregroundColor(Color.black.opacity(0.70))
|
||||||
.minimumScaleFactor(0.6)
|
.minimumScaleFactor(0.25)
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 8)
|
||||||
|
|||||||
Reference in new issue
Block a user