Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Sources/OpenUsage/Support/MenuBarIcon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ enum MenuBarIcon {
let renderer = ImageRenderer(
// Smaller inset than the provider default so the brand gauge keeps its prior menu-bar size
// (its art already carries ~8% margin inside the source viewBox).
content: ProviderIconShape(pathData: mark.path, inset: 0.08)
content: ProviderIconShape(mark: mark, inset: 0.08)
.fill(Color.black)
.frame(width: side, height: side)
)
Expand Down
4 changes: 2 additions & 2 deletions Sources/OpenUsage/Support/MenuBarStripRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ private struct MenuBarPrivacyLabel: View {
// The same mark and inset as `MenuBarIcon` (the art carries its own margin), sized to the
// strip's glyph box so the swap keeps the provider-glyph scale.
if let mark = ProviderMarks.mark(for: "openusage") {
ProviderIconShape(pathData: mark.path, inset: 0.08)
ProviderIconShape(mark: mark, inset: 0.08)
.fill(Color.black)
.frame(width: 16, height: 16)
}
Expand Down Expand Up @@ -199,7 +199,7 @@ private struct MenuBarTextStrip: View {
@ViewBuilder
private func glyph(_ icon: IconSource) -> some View {
if let mark = ProviderMarks.mark(for: icon.providerID) {
ProviderIconShape(pathData: mark.path, inset: 0.04)
ProviderIconShape(mark: mark, inset: 0.04)
.fill(Color.black)
.frame(width: Self.glyphSide, height: Self.glyphSide)
} else {
Expand Down
19 changes: 14 additions & 5 deletions Sources/OpenUsage/Support/ProviderIconShape.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ struct ProviderIcon: View {

var body: some View {
if let mark = ProviderMarks.mark(for: source.providerID) {
ProviderIconShape(pathData: mark.path, inset: inset)
ProviderIconShape(mark: mark, inset: inset)
.fill(Theme.iconGray)
} else {
Image(systemName: ProviderMarks.symbolFallback(for: source.providerID))
Expand All @@ -38,13 +38,13 @@ struct ProviderIcon: View {
/// run edge-to-edge (Devin, Grok). Fitting the real path bounds gives every provider mark the same
/// optical weight, then a single shared `inset` adds consistent breathing room so none touch the edge.
struct ProviderIconShape: Shape {
let pathData: String
let mark: ProviderMark
/// Fraction of the frame kept as margin on every side, so normalized marks have uniform padding.
var inset: CGFloat = 0.14

func path(in rect: CGRect) -> Path {
let raw = SVGPath.parse(pathData)
let bounds = raw.cgPath.boundingBoxOfPath
let raw = mark.parsedPath
let bounds = mark.bounds
guard bounds.width > 0, bounds.height > 0 else { return raw }
let target = rect.insetBy(dx: rect.width * inset, dy: rect.height * inset)
let scale = min(target.width / bounds.width, target.height / bounds.height)
Expand All @@ -58,8 +58,17 @@ struct ProviderIconShape: Shape {

/// A provider vector mark: the combined SVG path data. `ProviderIconShape` normalizes by the path's
/// true bounding box, so the source `viewBox` isn't needed.
struct ProviderMark: Hashable {
struct ProviderMark {
let path: String
let parsedPath: Path
let bounds: CGRect

init(path: String) {
self.path = path
let parsedPath = SVGPath.parse(path)
self.parsedPath = parsedPath
self.bounds = parsedPath.cgPath.boundingBoxOfPath
}
}

/// Loads copied provider SVGs from the bundle and extracts their path data (cached).
Expand Down
20 changes: 15 additions & 5 deletions Sources/OpenUsage/Views/CustomizeProviderDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ struct CustomizeProviderDetailView: View {
let providerID: String
let reorderSpaceName: String
@Binding var reorderLift: ReorderLift?
let rowFrames: [String: CGRect]
let frameStore: ReorderFrameStore

@State private var activeMetricID: String?
@AppStorage(DensitySetting.key) private var density = DensitySetting.regular
Expand Down Expand Up @@ -134,7 +134,12 @@ struct CustomizeProviderDetailView: View {
reorderLift?.location = value.location
let divider = expandedDividerID(for: providerID)
let ordered = reorderTargetIDs(for: providerID)
guard let target = reorderTarget(at: value.location, in: rowFrames, excluding: id, orderedIDs: ordered),
guard let target = reorderTarget(
at: value.location,
in: frameStore.frames,
excluding: id,
orderedIDs: ordered
),
let next = LayoutStore.reordered(ordered, dragged: id, target: target) else { return }
withAnimation(Motion.spring) {
_ = layout.applyMetricDividerOrder(next, dragged: id, dividerID: divider, in: providerID)
Expand All @@ -147,9 +152,9 @@ struct CustomizeProviderDetailView: View {
}

/// The metric a drag started on, by hit-testing the drag start against the grip frames
/// ("grip:<metric>" entries in `rowFrames`). Nil when the drag didn't start on a grip.
/// ("grip:<metric>" entries in the frame store). Nil when the drag didn't start on a grip.
private func metricID(at point: CGPoint) -> String? {
for (key, frame) in rowFrames {
for (key, frame) in frameStore.frames {
guard key.hasPrefix("grip:"), frame.insetBy(dx: 0, dy: -2).contains(point) else { continue }
return String(key.dropFirst("grip:".count))
}
Expand All @@ -166,7 +171,12 @@ struct CustomizeProviderDetailView: View {

private func makeLift(metricID: String, value: DragGesture.Value) -> ReorderLift? {
let title = layout.customizeDetail(for: providerID)?.metrics.first { $0.id == metricID }?.title ?? ""
return ReorderLift.make(id: metricID, payload: .customizeMetric(title: title), value: value, frames: rowFrames)
return ReorderLift.make(
id: metricID,
payload: .customizeMetric(title: title),
value: value,
frames: frameStore.frames
)
}
}

Expand Down
6 changes: 3 additions & 3 deletions Sources/OpenUsage/Views/CustomizeProviderListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ struct CustomizeProviderListView: View {
@Environment(AppContainer.self) private var container
let reorderSpaceName: String
@Binding var reorderLift: ReorderLift?
let rowFrames: [String: CGRect]
let frameStore: ReorderFrameStore

@State private var activeProviderID: String?
@AppStorage(DensitySetting.key) private var density = DensitySetting.regular
Expand Down Expand Up @@ -63,7 +63,7 @@ struct CustomizeProviderListView: View {
reorderDragGesture(
id: row.id,
coordinateSpaceName: reorderSpaceName,
rowFrames: rowFrames,
frameStore: frameStore,
active: $activeProviderID,
lift: $reorderLift,
makeLift: { makeProviderLift(for: row, value: $0) },
Expand All @@ -79,7 +79,7 @@ struct CustomizeProviderListView: View {
id: row.id,
payload: .customizeProviderRow(provider: row.provider, isEnabled: row.isEnabled, metricCount: row.metricCount),
value: value,
frames: rowFrames
frames: frameStore.frames
)
}
}
8 changes: 4 additions & 4 deletions Sources/OpenUsage/Views/CustomizeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ struct CustomizeView: View {
let reorderSpaceName: String
@Binding var reorderLift: ReorderLift?

@State private var rowFrames: [String: CGRect] = [:]
@State private var frameStore = ReorderFrameStore()

var body: some View {
PopoverScrollView {
Expand All @@ -24,7 +24,7 @@ struct CustomizeView: View {
.padding(.vertical, 12)
.frame(maxWidth: .infinity)
}
.onPreferenceChange(ReorderFramePreferenceKey.self) { rowFrames = $0 }
.onPreferenceChange(ReorderFramePreferenceKey.self) { frameStore.frames = $0 }
// The transient star/denial pill floats above the Customize content — the same capsule style
// as the dashboard's "Copied to clipboard" share pill. Green for a successful star/unstar,
// orange for the per-provider cap denial.
Expand Down Expand Up @@ -56,14 +56,14 @@ struct CustomizeView: View {
providerID: id,
reorderSpaceName: reorderSpaceName,
reorderLift: $reorderLift,
rowFrames: rowFrames
frameStore: frameStore
)
.transition(.move(edge: .trailing))
} else {
CustomizeProviderListView(
reorderSpaceName: reorderSpaceName,
reorderLift: $reorderLift,
rowFrames: rowFrames
frameStore: frameStore
)
.transition(.move(edge: .leading))
}
Expand Down
17 changes: 14 additions & 3 deletions Sources/OpenUsage/Views/ReorderGeometry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,19 @@ struct ReorderFramePreferenceKey: PreferenceKey {
}
}

/// Keeps continuously changing scroll geometry out of SwiftUI state. The preference updates on every
/// scroll frame; storing it in `@State` invalidates the whole widget list even though only drag gestures
/// need the latest values.
final class ReorderFrameStore {
var frames: [String: CGRect] = [:]
}

extension View {
func reorderFrame(id: String, in coordinateSpace: CoordinateSpace, yOutset: CGFloat = 0) -> some View {
func reorderFrame(
id: String,
in coordinateSpace: CoordinateSpace,
yOutset: CGFloat = 0
) -> some View {
background(
GeometryReader { proxy in
Color.clear.preference(
Expand All @@ -151,7 +162,7 @@ extension View {
func reorderDragGesture(
id: String,
coordinateSpaceName: String,
rowFrames: [String: CGRect],
frameStore: ReorderFrameStore,
active: Binding<String?>,
lift: Binding<ReorderLift?>,
makeLift: @escaping (DragGesture.Value) -> ReorderLift?,
Expand All @@ -167,7 +178,7 @@ func reorderDragGesture(
lift.wrappedValue?.location = value.location
guard let target = reorderTarget(
at: value.location,
in: rowFrames,
in: frameStore.frames,
excluding: id,
orderedIDs: orderedIDs()
) else { return }
Expand Down
20 changes: 11 additions & 9 deletions Sources/OpenUsage/Views/WidgetGroupedListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ struct WidgetGroupedListView: View {
let reorderSpaceName: String
@Binding var reorderLift: ReorderLift?

@State private var rowFrames: [String: CGRect] = [:]
@State private var frameStore = ReorderFrameStore()
@State private var activeProviderID: String?
@State private var activeMetricID: String?
@AppStorage(DensitySetting.key) private var density = DensitySetting.regular
Expand All @@ -31,7 +31,7 @@ struct WidgetGroupedListView: View {
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.onPreferenceChange(ReorderFramePreferenceKey.self) { rowFrames = $0 }
.onPreferenceChange(ReorderFramePreferenceKey.self) { frameStore.frames = $0 }
.animation(Motion.spring, value: layout.displayGroups.map(\.provider.id))
}

Expand Down Expand Up @@ -200,7 +200,10 @@ struct WidgetGroupedListView: View {
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.reorderFrame(id: expandedDividerID(for: providerID), in: .named(reorderSpaceName))
.reorderFrame(
id: expandedDividerID(for: providerID),
in: .named(reorderSpaceName)
)
.accessibilityLabel(isExpanded ? "Show less" : "Show more")
}

Expand All @@ -224,15 +227,14 @@ struct WidgetGroupedListView: View {

private func row(_ descriptor: WidgetDescriptor, data: WidgetData, in providerID: String,
condensedTop: Bool) -> some View {
let isActive = activeMetricID == descriptor.id
return WidgetRowView(
data: data,
onToggleResetDisplay: { dataStore.resetDisplayMode.toggle() },
onToggleMeterStyle: { dataStore.meterStyle.toggle() },
condensedTop: condensedTop
)
.contentShape(Rectangle())
.opacity(isActive ? 0 : 1)
.opacity(activeMetricID == descriptor.id ? 0 : 1)
.highPriorityGesture(metricDragGesture(for: descriptor, providerID: providerID))
.contextMenu { rowMenu(descriptor, providerID: providerID) }
.reorderFrame(id: descriptor.id, in: .named(reorderSpaceName))
Expand Down Expand Up @@ -280,7 +282,7 @@ struct WidgetGroupedListView: View {
reorderDragGesture(
id: group.provider.id,
coordinateSpaceName: reorderSpaceName,
rowFrames: rowFrames,
frameStore: frameStore,
active: $activeProviderID,
lift: $reorderLift,
makeLift: { makeProviderLift(for: group, value: $0) },
Expand All @@ -293,7 +295,7 @@ struct WidgetGroupedListView: View {
reorderDragGesture(
id: descriptor.id,
coordinateSpaceName: reorderSpaceName,
rowFrames: rowFrames,
frameStore: frameStore,
active: $activeMetricID,
lift: $reorderLift,
makeLift: { makeMetricLift(for: descriptor, value: $0) },
Expand Down Expand Up @@ -346,7 +348,7 @@ struct WidgetGroupedListView: View {
rows: rows
),
value: value,
frames: rowFrames
frames: frameStore.frames
)
}

Expand All @@ -355,7 +357,7 @@ struct WidgetGroupedListView: View {
id: descriptor.id,
payload: .dashboardMetric(data: dataStore.data(for: descriptor)),
value: value,
frames: rowFrames
frames: frameStore.frames
)
}
}