// // SeekBar.swift // // Copyright 2026 Brendan Szymanski // // 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 . // // 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 var playbackState: PlayerPlaybackState? var onSeek: ((Double) -> Void)? public init( value: Binding, range: ClosedRange, 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: 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.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( _ 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)? }