fix: center progress thumb on pointer while seeking

This commit is contained in:
2026-10-22 12:37:01 +02:00
parent 19a1de625b
commit 40e2ea45a0
3 changed files with 6 additions and 12 deletions
+2 -2
View File
@@ -201,9 +201,9 @@ struct CastPlaybackCheck {
_ = NSApplication.shared _ = NSApplication.shared
NSApp.setActivationPolicy(.prohibited) NSApp.setActivationPolicy(.prohibited)
require(castVolumeSliderFeedbackCheck(), "Native slider keeps user position during tracking and applies receiver volume when idle") require(castVolumeSliderFeedbackCheck(), "Native slider keeps user position during tracking and applies receiver volume when idle")
require(playbackSliderHitCheck(), "Progress slider seeks beside the thumb and preserves cursor alignment throughout dragging") require(playbackSliderHitCheck(), "Progress thumb centers on the cursor for clicks and drags, clamped to track endpoints")
if CommandLine.arguments.count == 2, CommandLine.arguments[1] == "--slider-regression" { if CommandLine.arguments.count == 2, CommandLine.arguments[1] == "--slider-regression" {
print("PASS: adjacent seeking, thumb and track drag alignment, and volume tracking feedback") print("PASS: centered progress clicks and drags, endpoint clamping, and volume tracking feedback")
return return
} }
let source = URL(fileURLWithPath: CommandLine.arguments[1]) let source = URL(fileURLWithPath: CommandLine.arguments[1])
+1 -1
View File
@@ -23,7 +23,7 @@ To run without installing, use `make run`. The built `.build/terebi.app` is self
- **Local files:** use **⌘O**, drag files onto the player, or choose Finder's **Open With**. Multiple files form a queue in natural filename order, with automatic advance. Use **View → Show Queue** to play, reorder or remove items; **+** or dropping files onto the sidebar appends them. The queue lasts only for the session; opening other media replaces it. - **Local files:** use **⌘O**, drag files onto the player, or choose Finder's **Open With**. Multiple files form a queue in natural filename order, with automatic advance. Use **View → Show Queue** to play, reorder or remove items; **+** or dropping files onto the sidebar appends them. The queue lasts only for the session; opening other media replaces it.
- **Streams:** use **Open Stream…** (**⇧⌘O**) or drop a supported link. YouTube supports individual videos, not playlists or channels. Miruro links must use `https://barelystarted.miruro.tv/watch/<opaque-id>/<slug>?ep=<episode>` with an explicit positive episode number; arbitrary streaming sites are not supported. - **Streams:** use **Open Stream…** (**⇧⌘O**) or drop a supported link. YouTube supports individual videos, not playlists or channels. Miruro links must use `https://barelystarted.miruro.tv/watch/<opaque-id>/<slug>?ep=<episode>` with an explicit positive episode number; arbitrary streaming sites are not supported.
- **Seeking:** click the progress track to seek, including immediately beside the visible thumb. Grabbing the thumb preserves the cursor's position within it throughout the drag; dragging from the track first seeks to the cursor. - **Seeking:** click or drag the progress track or thumb to seek. The thumb centers on the cursor, clamped to the track endpoints.
- **Miruro metadata:** titles and opening/ending intervals load independently of playback when available. Small rounded gaps mark interval boundaries in the slider, without visible labels; accessibility descriptions retain the interval names and times. Missing/invalid metadata leaves playback usable. Timings may differ between cuts. No automatic skipping is performed. - **Miruro metadata:** titles and opening/ending intervals load independently of playback when available. Small rounded gaps mark interval boundaries in the slider, without visible labels; accessibility descriptions retain the interval names and times. Missing/invalid metadata leaves playback usable. Timings may differ between cuts. No automatic skipping is performed.
- **Resume:** videos resume their saved position; finished videos restart from the beginning. Live and audio-only media do not save progress. - **Resume:** videos resume their saved position; finished videos restart from the beginning. Live and audio-only media do not save progress.
- **Compact mode:** press **P** for a floating player; double-click it to restore. Drag noninteractive areas to move the window. Use the titlebar pin to keep the normal window on top. - **Compact mode:** press **P** for a floating player; double-click it to restore. Drag noninteractive areas to move the window. Use the titlebar pin to keep the normal window on top.
+3 -9
View File
@@ -2008,17 +2008,14 @@ private struct PlaybackSlider: NSViewRepresentable {
let width = bar.width - cell.knobThickness let width = bar.width - cell.knobThickness
guard width > 0, maxValue > minValue else { return } guard width > 0, maxValue > minValue else { return }
let point = convert(event.locationInWindow, from: nil) let point = convert(event.locationInWindow, from: nil)
let knob = cell.visibleKnobRect(cell.knobRect(flipped: isFlipped))
let grabbed = NSBezierPath(roundedRect: knob, xRadius: 1, yRadius: 1).contains(point)
let offset = grabbed ? point.x - knob.midX : 0
func seek(_ x: CGFloat) { func seek(_ x: CGFloat) {
let fraction = min(1, max(0, (x - offset - start) / width)) let fraction = min(1, max(0, (x - start) / width))
let value = minValue + fraction * (maxValue - minValue) let value = minValue + fraction * (maxValue - minValue)
guard value != doubleValue else { return } guard value != doubleValue else { return }
doubleValue = value doubleValue = value
if let action { sendAction(action, to: target) } if let action { sendAction(action, to: target) }
} }
if !grabbed { seek(point.x) } seek(point.x)
while let next = window?.nextEvent(matching: [.leftMouseDragged, .leftMouseUp]) { while let next = window?.nextEvent(matching: [.leftMouseDragged, .leftMouseUp]) {
let nextPoint = convert(next.locationInWindow, from: nil) let nextPoint = convert(next.locationInWindow, from: nil)
if next.type == .leftMouseDragged || nextPoint.x != point.x { seek(nextPoint.x) } if next.type == .leftMouseDragged || nextPoint.x != point.x { seek(nextPoint.x) }
@@ -2130,12 +2127,9 @@ func playbackSliderHitCheck() -> Bool {
} }
slider.mouseDown(with: down) slider.mouseDown(with: down)
let movedKnob = cell.knobRect(flipped: slider.isFlipped) let movedKnob = cell.knobRect(flipped: slider.isFlipped)
let expectedMovement = movement + (abs(offset) <= 1 ? 0 : offset) let expectedMovement = movement + offset
let expectedX = min(right, max(left, knob.midX + expectedMovement)) let expectedX = min(right, max(left, knob.midX + expectedMovement))
guard abs(movedKnob.midX - expectedX) < 0.5 else { return false } guard abs(movedKnob.midX - expectedX) < 0.5 else { return false }
if abs(offset) <= 1 && movement == 0 {
guard slider.doubleValue == initial else { return false }
}
} }
return true return true
} }