fix: match progress slider dragging to visible thumb
The 3-point progress thumb was drawn inside larger native knob bounds. Override those bounds for progress only so adjacent clicks seek; retain native volume geometry and tracking. Native click regression reproduced before the fix and passes afterward.
This commit is contained in:
@@ -201,8 +201,9 @@ struct CastPlaybackCheck {
|
||||
_ = NSApplication.shared
|
||||
NSApp.setActivationPolicy(.prohibited)
|
||||
require(castVolumeSliderFeedbackCheck(), "Native slider keeps user position during tracking and applies receiver volume when idle")
|
||||
require(playbackSliderHitCheck(), "Progress slider seeks beside the visible thumb and grabs only the thumb")
|
||||
if CommandLine.arguments.count == 2, CommandLine.arguments[1] == "--slider-regression" {
|
||||
print("PASS: native volume slider tracking preserves user input and idle receiver feedback")
|
||||
print("PASS: progress thumb hit bounds, adjacent seeking, and volume tracking feedback")
|
||||
return
|
||||
}
|
||||
let source = URL(fileURLWithPath: CommandLine.arguments[1])
|
||||
|
||||
@@ -23,6 +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.
|
||||
- **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. Grab the thumb itself to drag without first jumping to the click position.
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
@@ -2005,6 +2005,11 @@ private struct PlaybackSlider: NSViewRepresentable {
|
||||
var volume = false
|
||||
var bufferedRange: ClosedRange<Double>?
|
||||
var intervals: [MiruroInterval] = []
|
||||
override func knobRect(flipped: Bool) -> NSRect {
|
||||
let rect = super.knobRect(flipped: flipped)
|
||||
guard !volume else { return rect }
|
||||
return NSRect(x: rect.midX - 1.5, y: rect.midY - 7.5, width: 3, height: 15)
|
||||
}
|
||||
override func drawBar(inside rect: NSRect, flipped: Bool) {
|
||||
let bar = NSRect(x: rect.minX + knobThickness / 2, y: rect.midY - 1.5,
|
||||
width: max(0, rect.width - knobThickness), height: 3)
|
||||
@@ -2046,9 +2051,9 @@ private struct PlaybackSlider: NSViewRepresentable {
|
||||
}
|
||||
}
|
||||
override func drawKnob(_ rect: NSRect) {
|
||||
let width: CGFloat = volume ? 16 : 3
|
||||
let height: CGFloat = volume ? 12 : 15
|
||||
let knob = NSRect(x: rect.midX - width / 2, y: rect.midY - height / 2, width: width, height: height)
|
||||
let knob = volume
|
||||
? NSRect(x: rect.midX - 8, y: rect.midY - 6, width: 16, height: 12)
|
||||
: rect
|
||||
let color = isEnabled ? (volume ? SunsetPalette.text : SunsetPalette.secondaryText)
|
||||
: SunsetPalette.disabledThumb
|
||||
color.setFill()
|
||||
@@ -2060,6 +2065,38 @@ private struct PlaybackSlider: NSViewRepresentable {
|
||||
#if PLAYER_CHECK
|
||||
@MainActor
|
||||
func castVolumeSliderFeedbackCheck() -> Bool { PlaybackSlider.Slider.feedbackCheck() }
|
||||
@MainActor
|
||||
func playbackSliderHitCheck() -> Bool {
|
||||
let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 320, height: 80),
|
||||
styleMask: [.borderless], backing: .buffered, defer: false)
|
||||
let slider = PlaybackSlider.Slider(frame: NSRect(x: 20, y: 20, width: 280, height: 24))
|
||||
slider.cell = PlaybackSlider.Cell()
|
||||
slider.minValue = 0
|
||||
slider.maxValue = 100
|
||||
slider.isContinuous = true
|
||||
window.contentView!.addSubview(slider)
|
||||
window.display()
|
||||
for offset: CGFloat in [-4, -2, -1, 0, 1, 2, 4] {
|
||||
slider.doubleValue = 50
|
||||
let cell = slider.cell as! NSSliderCell
|
||||
let knob = cell.knobRect(flipped: slider.isFlipped)
|
||||
let point = slider.convert(NSPoint(x: knob.midX + offset, y: knob.midY), to: nil)
|
||||
let up = NSEvent.mouseEvent(with: .leftMouseUp, location: point, modifierFlags: [],
|
||||
timestamp: 0, windowNumber: window.windowNumber, context: nil,
|
||||
eventNumber: 1, clickCount: 1, pressure: 0)!
|
||||
let down = NSEvent.mouseEvent(with: .leftMouseDown, location: point, modifierFlags: [],
|
||||
timestamp: 0, windowNumber: window.windowNumber, context: nil,
|
||||
eventNumber: 0, clickCount: 1, pressure: 1)!
|
||||
NSApp.postEvent(up, atStart: true)
|
||||
slider.mouseDown(with: down)
|
||||
if abs(offset) <= 1 {
|
||||
guard slider.doubleValue == 50 else { return false }
|
||||
} else {
|
||||
guard (slider.doubleValue - 50) * offset > 0 else { return false }
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
|
||||
Reference in New Issue
Block a user