145 lines
4.4 KiB
Swift
145 lines
4.4 KiB
Swift
//
|
|
// SeekBar.swift
|
|
//
|
|
// Copyright 2026 Brendan Szymanski <hello@bscubed.dev>
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
//
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
//
|
|
|
|
import Adwaita
|
|
import CAdw
|
|
import LuminateCore
|
|
|
|
/// A seekable progress slider wrapping `GtkScale`.
|
|
///
|
|
/// Use for media playback position scrubbing. Emits the new value
|
|
/// via the `onSeek` callback when the user drags the slider.
|
|
///
|
|
/// ```swift
|
|
/// SeekBar(value: $position, range: 0...duration, onSeek: { pos in
|
|
/// player.seek(to: pos)
|
|
/// })
|
|
/// ```
|
|
public struct SeekBar: Widget {
|
|
|
|
#if exposeGeneratedAppearUpdateFunctions
|
|
public var updateFunctions: [(ViewStorage, WidgetData, Bool) -> Void] = []
|
|
public var appearFunctions: [(ViewStorage, WidgetData) -> Void] = []
|
|
#else
|
|
var updateFunctions: [(ViewStorage, WidgetData, Bool) -> Void] = []
|
|
var appearFunctions: [(ViewStorage, WidgetData) -> Void] = []
|
|
#endif
|
|
|
|
@Binding var value: Double
|
|
var range: ClosedRange<Double>
|
|
var playbackState: PlayerPlaybackState?
|
|
var onSeek: ((Double) -> Void)?
|
|
|
|
public init(
|
|
value: Binding<Double>,
|
|
range: ClosedRange<Double>,
|
|
playbackState: PlayerPlaybackState? = nil,
|
|
onSeek: ((Double) -> Void)? = nil
|
|
) {
|
|
self._value = value
|
|
self.range = range
|
|
self.playbackState = playbackState
|
|
self.onSeek = onSeek
|
|
}
|
|
|
|
// MARK: - Widget
|
|
|
|
public func container<Data>(data: WidgetData, type: Data.Type) -> ViewStorage
|
|
where Data: ViewRenderData {
|
|
let widgetPtr = gtk_scale_new_with_range(
|
|
.GTK_ORIENTATION_HORIZONTAL,
|
|
range.lowerBound,
|
|
range.upperBound,
|
|
1.0
|
|
)
|
|
let scale = widgetPtr.map(OpaquePointer.init)
|
|
gtk_scale_set_draw_value(scale?.cast(), 0)
|
|
gtk_widget_set_hexpand(scale?.cast(), 1)
|
|
gtk_range_set_show_fill_level(scale?.cast(), 1)
|
|
gtk_range_set_restrict_to_fill_level(scale?.cast(), 0)
|
|
|
|
// Register scale for direct C-level updates during playback
|
|
if let scale {
|
|
playbackState?.seekScale = scale
|
|
}
|
|
|
|
let storage = ViewStorage(scale)
|
|
|
|
let context = SeekBarContext()
|
|
context.onSeek = onSeek
|
|
let binding = _value
|
|
context.setValue = { binding.wrappedValue = $0 }
|
|
storage.fields["context"] = context
|
|
|
|
let callback: @convention(c) (
|
|
OpaquePointer?,
|
|
GtkScrollType,
|
|
Double,
|
|
UnsafeMutableRawPointer?
|
|
) -> Int32 = { _, _, val, userData in
|
|
let ctx = Unmanaged<SeekBarContext>.fromOpaque(userData!).takeUnretainedValue()
|
|
ctx.setValue?(val)
|
|
ctx.onSeek?(val)
|
|
return 0
|
|
}
|
|
g_signal_connect_data(
|
|
scale.map(UnsafeMutableRawPointer.init),
|
|
"change-value",
|
|
unsafeBitCast(callback, to: GCallback.self),
|
|
Unmanaged.passUnretained(context).toOpaque(),
|
|
nil,
|
|
GConnectFlags(rawValue: 1)
|
|
)
|
|
|
|
for function in appearFunctions {
|
|
function(storage, data)
|
|
}
|
|
return storage
|
|
}
|
|
|
|
public func update<Data>(
|
|
_ storage: ViewStorage,
|
|
data: WidgetData,
|
|
updateProperties: Bool,
|
|
type: Data.Type
|
|
) where Data: ViewRenderData {
|
|
storage.modify { widget in
|
|
let currentValue = gtk_range_get_value(widget?.cast())
|
|
if abs(currentValue - value) > 0.5 {
|
|
gtk_range_set_value(widget?.cast(), value)
|
|
}
|
|
}
|
|
for function in updateFunctions {
|
|
function(storage, data, updateProperties)
|
|
}
|
|
if updateProperties {
|
|
storage.previousState = self
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - Context
|
|
|
|
private class SeekBarContext {
|
|
var setValue: ((Double) -> Void)?
|
|
var onSeek: ((Double) -> Void)?
|
|
}
|