Feedback TF build 79: fuera el swipe entre gráficas (navegación = chip bar; el gesto chocaba con scrub/pan/brush). Brush con gesto unificado decidido en touch-down: cerca de un asa (18pt) redimensiona; en cualquier otro punto (dentro de la ventana o sobre el historial atenuado) desliza la ventana entera manteniendo el ancho — seleccionar ene–mar y arrastrar hasta jun–ago. Con ventanas estrechas las asas solo agarran desde fuera, el interior siempre mueve. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L38583J7AYWCVPevkivscj
This commit is contained in:
@@ -145,9 +145,16 @@ struct ChartRangeBrush: View {
|
||||
@Binding var selection: ClosedRange<Int>
|
||||
|
||||
@State private var dragStartIndices: (lo: Int, hi: Int)?
|
||||
@State private var dragTarget: DragTarget?
|
||||
@State private var lastTickedIndex: Int = -1
|
||||
@State private var showingPickers = false
|
||||
|
||||
/// What a drag manipulates, decided ONCE at touch-down by where it starts.
|
||||
/// One unified gesture instead of per-element gestures: with a narrow
|
||||
/// window (e.g. 2 months) the handles' hit areas used to swallow the whole
|
||||
/// window and moving it was impossible.
|
||||
private enum DragTarget { case lowerHandle, upperHandle, moveWindow }
|
||||
|
||||
private static let labelFormatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.locale = .autoupdatingCurrent
|
||||
@@ -219,7 +226,7 @@ struct ChartRangeBrush: View {
|
||||
.frame(width: max(0, width - xHi))
|
||||
.offset(x: xHi)
|
||||
|
||||
// Window frame — draggable as a whole to slide through history
|
||||
// Window frame
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.stroke(Color.appPrimary.opacity(0.7), lineWidth: 1.4)
|
||||
.background(
|
||||
@@ -227,15 +234,13 @@ struct ChartRangeBrush: View {
|
||||
)
|
||||
.frame(width: max(10, xHi - xLo))
|
||||
.offset(x: xLo)
|
||||
.contentShape(Rectangle())
|
||||
.gesture(moveGesture(width: width))
|
||||
|
||||
// Handles
|
||||
// Handles (visual only — hit-testing lives in the unified gesture)
|
||||
handle(at: xLo)
|
||||
.gesture(handleGesture(isLower: true, width: width))
|
||||
handle(at: xHi)
|
||||
.gesture(handleGesture(isLower: false, width: width))
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.gesture(unifiedGesture(width: width))
|
||||
}
|
||||
|
||||
private func handle(at x: CGFloat) -> some View {
|
||||
@@ -245,9 +250,9 @@ struct ChartRangeBrush: View {
|
||||
.overlay(
|
||||
Capsule().stroke(Color(.systemBackground), lineWidth: 1.5)
|
||||
)
|
||||
.frame(width: 30, height: 44) // generous hit area
|
||||
.contentShape(Rectangle())
|
||||
.offset(x: x - 15)
|
||||
.frame(height: 44)
|
||||
.offset(x: x - 2.5)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
|
||||
// MARK: Geometry helpers
|
||||
@@ -263,22 +268,41 @@ struct ChartRangeBrush: View {
|
||||
return Int((frac * CGFloat(dates.count - 1)).rounded())
|
||||
}
|
||||
|
||||
// MARK: Gestures
|
||||
// MARK: Unified gesture
|
||||
//
|
||||
// Decided at touch-down: grabbing near a handle resizes that edge; grabbing
|
||||
// anywhere else (inside the window OR on the dimmed history) slides the
|
||||
// whole window keeping its width — select Jan–Mar, then swipe to land on
|
||||
// Jun–Aug. Narrow windows stay movable because the edge grip only wins in
|
||||
// its 18pt zone (from outside when the window is too narrow to share).
|
||||
|
||||
private func handleGesture(isLower: Bool, width: CGFloat) -> some Gesture {
|
||||
private func unifiedGesture(width: CGFloat) -> some Gesture {
|
||||
DragGesture(minimumDistance: 1)
|
||||
.onChanged { value in
|
||||
let (lo, hi) = dragStartIndices ?? clampedSelection
|
||||
if dragStartIndices == nil { dragStartIndices = (lo, hi) }
|
||||
let i = index(atX: value.location.x, width: width)
|
||||
if dragStartIndices == nil {
|
||||
dragStartIndices = (lo, hi)
|
||||
dragTarget = target(
|
||||
forX: value.startLocation.x,
|
||||
xLo: xPosition(index: lo, width: width),
|
||||
xHi: xPosition(index: hi, width: width)
|
||||
)
|
||||
}
|
||||
guard let target = dragTarget else { return }
|
||||
var newLo = lo
|
||||
var newHi = hi
|
||||
if isLower {
|
||||
newLo = min(i, hi)
|
||||
} else {
|
||||
newHi = max(i, lo)
|
||||
switch target {
|
||||
case .lowerHandle:
|
||||
newLo = min(index(atX: value.location.x, width: width), hi)
|
||||
case .upperHandle:
|
||||
newHi = max(index(atX: value.location.x, width: width), lo)
|
||||
case .moveWindow:
|
||||
let span = hi - lo
|
||||
let deltaIdx = Int((value.translation.width / max(width, 1) * CGFloat(dates.count - 1)).rounded())
|
||||
newLo = max(0, min(lo + deltaIdx, dates.count - 1 - span))
|
||||
newHi = newLo + span
|
||||
}
|
||||
let ticked = isLower ? newLo : newHi
|
||||
let ticked = (target == .upperHandle) ? newHi : newLo
|
||||
if ticked != lastTickedIndex {
|
||||
lastTickedIndex = ticked
|
||||
ChartHaptics.tick()
|
||||
@@ -287,30 +311,24 @@ struct ChartRangeBrush: View {
|
||||
}
|
||||
.onEnded { _ in
|
||||
dragStartIndices = nil
|
||||
dragTarget = nil
|
||||
lastTickedIndex = -1
|
||||
}
|
||||
}
|
||||
|
||||
/// Dragging the window itself slides it through history keeping its width —
|
||||
/// e.g. fix a 1-month window and replay the portfolio month by month.
|
||||
private func moveGesture(width: CGFloat) -> some Gesture {
|
||||
DragGesture(minimumDistance: 1)
|
||||
.onChanged { value in
|
||||
let (lo, hi) = dragStartIndices ?? clampedSelection
|
||||
if dragStartIndices == nil { dragStartIndices = (lo, hi) }
|
||||
let span = hi - lo
|
||||
let deltaIdx = Int((value.translation.width / max(width, 1) * CGFloat(dates.count - 1)).rounded())
|
||||
let newLo = max(0, min(lo + deltaIdx, dates.count - 1 - span))
|
||||
if newLo != lastTickedIndex {
|
||||
lastTickedIndex = newLo
|
||||
ChartHaptics.tick()
|
||||
}
|
||||
selection = newLo...(newLo + span)
|
||||
}
|
||||
.onEnded { _ in
|
||||
dragStartIndices = nil
|
||||
lastTickedIndex = -1
|
||||
}
|
||||
private func target(forX x: CGFloat, xLo: CGFloat, xHi: CGFloat) -> DragTarget {
|
||||
let grip: CGFloat = 18
|
||||
let windowWidth = xHi - xLo
|
||||
if windowWidth >= grip * 3 {
|
||||
// Wide window: edges grab from either side of the handle.
|
||||
if abs(x - xLo) <= grip { return .lowerHandle }
|
||||
if abs(x - xHi) <= grip { return .upperHandle }
|
||||
} else {
|
||||
// Narrow window: the inside belongs to MOVE; edges only from outside.
|
||||
if x < xLo && xLo - x <= grip { return .lowerHandle }
|
||||
if x > xHi && x - xHi <= grip { return .upperHandle }
|
||||
}
|
||||
return .moveWindow
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user