From ee1813595e32e76255c94f6810bb4a0f2f11e8e0 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Sat, 22 Feb 2025 16:45:06 -0500 Subject: [PATCH] Widget Editor Refinements (#1793) --- .../InteractionBarEditorView+Logic.swift | 111 ++++++++++++++++-- .../InteractionBarEditorView+Views.swift | 25 +++- .../InteractionBarEditorView.swift | 18 ++- 3 files changed, 136 insertions(+), 18 deletions(-) diff --git a/Mlem/App/Views/Root/Tabs/Settings/InteractionBarEditor/InteractionBarEditorView+Logic.swift b/Mlem/App/Views/Root/Tabs/Settings/InteractionBarEditor/InteractionBarEditorView+Logic.swift index 14a62bfab..71f58a2bf 100644 --- a/Mlem/App/Views/Root/Tabs/Settings/InteractionBarEditor/InteractionBarEditorView+Logic.swift +++ b/Mlem/App/Views/Root/Tabs/Settings/InteractionBarEditor/InteractionBarEditorView+Logic.swift @@ -114,6 +114,11 @@ extension InteractionBarEditorView { if barPickedUpItem == nil { HapticManager.main.play(haptic: .firmInfo, priority: .low) barPickedUpItem = (item, index) + if let trayItem = trayItems.first(where: { $0.item == item.item }) { + withAnimation(.easeOut(duration: barAnimationDuration)) { + trayItem.hide() + } + } } dragLocation = gesture.location dragTranslation = gesture.translation @@ -126,7 +131,7 @@ extension InteractionBarEditorView { func trayItemDragGesture(trayItem: TrayItem) -> some Gesture { DragGesture(minimumDistance: 0, coordinateSpace: .named("editor")) .onChanged { gesture in - if trayPickedUpItem == nil, !barItems.contains(where: { $0.item == trayItem.item }) { + if trayPickedUpItem == nil { HapticManager.main.play(haptic: .firmInfo, priority: .low) trayPickedUpItem = trayItem } @@ -151,11 +156,17 @@ extension InteractionBarEditorView { guard case let .bar(targetIndex) = dropLocation else { return } addToBar(trayPickedUpItem, at: targetIndex) } else if let barPickedUpItem { + if let trayItem = trayItems.first(where: { $0.item == barPickedUpItem.barItem.item }) { + withAnimation(.easeOut(duration: barAnimationDuration)) { + trayItem.show() + } + } + switch dropLocation { case let .bar(targetIndex): moveOnBar(barItem: barPickedUpItem.barItem, from: barPickedUpItem.index, to: targetIndex) case .tray: - removeFromBar(barItem: barPickedUpItem.barItem) + removeFromBar(barItem: barPickedUpItem.barItem, barItemIndex: barPickedUpItem.index) } } } @@ -163,17 +174,30 @@ extension InteractionBarEditorView { // MARK: - State Updates func addToBar(_ trayItem: TrayItem, at index: Int) { - guard allowNewItemInsertion, - !barItems.contains(where: { $0.item == trayItem.item }) else { - assertionFailure(!allowNewItemInsertion ? "Item insertion disabled" : "Item already in bar") + guard allowNewItemInsertion else { + assertionFailure("Item insertion disabled") return } HapticManager.main.play(haptic: .firmInfo, priority: .high) let newItem: BarItem = .init(item: trayItem.item, expanded: false, visible: true) + barItems.insert(newItem, at: index) + + // gently fade the tray item back in trayItem.hide() + withAnimation(.easeOut(duration: trayItemDuration)) { + trayItem.show() + } + + // recompute infoStackAlignment with actual barItems, since these animations all play nice + withAnimation(.easeInOut(duration: barAnimationDuration)) { + infoStackAlignment = computeInfoStackAlignment( + infoStackIndex: infoStackIndex(), + totalItems: barItems.count + ) + } updateConfiguration() } @@ -193,6 +217,47 @@ extension InteractionBarEditorView { barItems.insert(newItem, at: targetIndex) } + // recompute infoStackAlignment with projected info stack location + let infoStackIndex = infoStackIndex() + let newInfoStackAlignment: Alignment? + if barItem.item == nil { + // if moving info stack itself, can compute alignment based on whether moving to beginning or end + if targetIndex == 0 { + newInfoStackAlignment = barItems.count == 1 ? .center : .leading + } else if targetIndex == barItems.count - 1 { + newInfoStackAlignment = .trailing + } else { + newInfoStackAlignment = .center + } + } else { + if sourceIndex < infoStackIndex { + if targetIndex > infoStackIndex { + // moving widget from left to right of info stack, projected infostack index is current - 1 + newInfoStackAlignment = computeInfoStackAlignment( + infoStackIndex: infoStackIndex - 1, + totalItems: barItems.count + ) + } else { + // widget not moving "over" the info stack, no change + newInfoStackAlignment = nil + } + } else { + if targetIndex < infoStackIndex { + // moving widget from right to left of info stack, projected infostack index is current + 1 + newInfoStackAlignment = computeInfoStackAlignment( + infoStackIndex: infoStackIndex + 1, + totalItems: barItems.count + ) + } else { + // widget not moving "over" the info stack, no change + newInfoStackAlignment = nil + } + } + } + if let newInfoStackAlignment { + withAnimation(.easeInOut(duration: barAnimationDuration)) { infoStackAlignment = newInfoStackAlignment } + } + // wait for animation to complete, then remove original item from barItems DispatchQueue.main.asyncAfter(deadline: .now() + barAnimationDuration) { barItems.removeAll(where: { $0 == barItem }) @@ -200,18 +265,30 @@ extension InteractionBarEditorView { } } - func removeFromBar(barItem: BarItem) { + func removeFromBar(barItem: BarItem, barItemIndex: Int) { // no removing the info stack guard let item = barItem.item else { return } - let trayItem = trayItems.first(where: { $0.item == item }) HapticManager.main.play(haptic: .firmInfo, priority: .high) + // recompute infoStackAlignment with projected info stack location + let infoStackIndex = infoStackIndex() + let newInfoStackAlignment: Alignment + if barItemIndex < infoStackIndex { + // removing item to the left of info stack: shift info stack left + newInfoStackAlignment = computeInfoStackAlignment(infoStackIndex: infoStackIndex - 1, totalItems: barItems.count - 1) + } else { + // removing item to the right of info stack: info stack index unchanged, but barItems.count still decreases + newInfoStackAlignment = computeInfoStackAlignment(infoStackIndex: infoStackIndex, totalItems: barItems.count - 1) + } + // smoothly animate away barItem.hide() withAnimation(.easeInOut(duration: barAnimationDuration)) { - trayItem?.show() barItem.collapse() + if newInfoStackAlignment != infoStackAlignment { + infoStackAlignment = newInfoStackAlignment + } } // wait for animation to complete, then remove from barItems @@ -243,4 +320,22 @@ extension InteractionBarEditorView { } return palette.tertiary } + + func infoStackIndex() -> Int { + guard let ret = barItems.firstIndex(where: { $0.item == nil }) else { + assertionFailure("could not find infoStack index") + return 0 + } + return ret + } +} + +func computeInfoStackAlignment(infoStackIndex: Int, totalItems: Int) -> Alignment { + if infoStackIndex == 0 { + return totalItems == 1 ? .center : .leading + } else if infoStackIndex == totalItems - 1 { + return .trailing + } else { + return .center + } } diff --git a/Mlem/App/Views/Root/Tabs/Settings/InteractionBarEditor/InteractionBarEditorView+Views.swift b/Mlem/App/Views/Root/Tabs/Settings/InteractionBarEditor/InteractionBarEditorView+Views.swift index 7c4fe33cc..a739ba2bd 100644 --- a/Mlem/App/Views/Root/Tabs/Settings/InteractionBarEditor/InteractionBarEditorView+Views.swift +++ b/Mlem/App/Views/Root/Tabs/Settings/InteractionBarEditor/InteractionBarEditorView+Views.swift @@ -96,7 +96,7 @@ extension InteractionBarEditorView { } if dropLocation?.index == barItems.count, - barPickedUpIndex != barItems.count { + barPickedUpIndex != barItems.count - 1 { dropIndicator(index: barItems.count) } } @@ -168,10 +168,23 @@ extension InteractionBarEditorView { .geometryGroup() .offset(trayPickedUpItem == trayItem ? dragTranslation : .zero) .background { - Capsule() - .fill(trayItemOutlineColor(trayItem).opacity(0.2)) - .stroke(trayItemOutlineColor(trayItem)) - .background(palette.secondaryGroupedBackground, in: .capsule) + Group { + switch trayItem.item { + case let .action(action): + InteractionBarActionLabelView(action.appearance) + case let .counter(counter): + InteractionBarCounterLabelView(counter.appearance) + .fixedSize() + } + } + .opacity(0.2) + .padding(Constants.main.standardSpacing) + .background { + Capsule() + .fill(trayItemOutlineColor(trayItem).opacity(0.2)) + .stroke(trayItemOutlineColor(trayItem)) + .background(palette.secondaryGroupedBackground, in: .capsule) + } } .gesture(trayItemDragGesture(trayItem: trayItem)) .zIndex(trayPickedUpItem == trayItem ? 2 : 0) @@ -344,7 +357,7 @@ extension InteractionBarEditorView { } } .foregroundStyle(palette.secondary) - .frame(maxWidth: .infinity) + .frame(maxWidth: .infinity, alignment: infoStackAlignment) } @ViewBuilder diff --git a/Mlem/App/Views/Root/Tabs/Settings/InteractionBarEditor/InteractionBarEditorView.swift b/Mlem/App/Views/Root/Tabs/Settings/InteractionBarEditor/InteractionBarEditorView.swift index 9c1127218..9d4cbb0de 100644 --- a/Mlem/App/Views/Root/Tabs/Settings/InteractionBarEditor/InteractionBarEditorView.swift +++ b/Mlem/App/Views/Root/Tabs/Settings/InteractionBarEditor/InteractionBarEditorView.swift @@ -29,11 +29,14 @@ struct InteractionBarEditorView: Vie @State var dragLocation: CGPoint = .zero @State var dragTranslation: CGSize = .zero + @State var infoStackAlignment: Alignment + @State var showingApplyToAllConfirmation: Bool = false let onSet: (Configuration) -> Void let barAnimationDuration: CGFloat = 0.15 + let trayItemDuration: CGFloat = 0.5 @ScaledMetric(relativeTo: .body) var baseInfoCapsuleHeight: CGFloat = 22 var infoCapsuleHeight: CGFloat { baseInfoCapsuleHeight + Constants.main.doubleSpacing } @@ -44,10 +47,17 @@ struct InteractionBarEditorView: Vie self.configuration = configuration self.onSet = onSet let configurationItems: [Configuration.Item?] = configuration.leading + [nil] + configuration.trailing - self._barItems = .init(wrappedValue: configurationItems.map { item in - .init(item: item, expanded: true, visible: true) - }) self.configurationType = configuration is PostBarConfiguration ? .post : .comment + + let newBarItems: [BarItem] = configurationItems.map { .init(item: $0, expanded: true, visible: true) } + let newInfoStackIndex = newBarItems.firstIndex(where: { $0.item == nil }) + assert(newInfoStackIndex != nil, "could not find infoStack index") + + self._barItems = .init(wrappedValue: newBarItems) + self._infoStackAlignment = .init(wrappedValue: computeInfoStackAlignment( + infoStackIndex: newInfoStackIndex ?? 0, + totalItems: newBarItems.count) + ) } init(setting: WritableKeyPath) { @@ -78,7 +88,7 @@ struct InteractionBarEditorView: Vie let configurationItems: [Configuration.Item?] = configuration.leading + [nil] + configuration.trailing trayItems = Configuration.Item.allCases .filter { configuration.availableWidgets.contains($0) } - .map { TrayItem(item: $0, visible: !configurationItems.contains($0)) } + .map { TrayItem(item: $0, visible: true) } } .frame(maxWidth: .infinity) .padding(Constants.main.standardSpacing)