| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261 |
- //
- // Copyright 2026 Aarav Ravindra Kharade
- //
- import CSystem
- import Foundation
- // MARK: - GTK3 C Bindings via dlopen
- fileprivate let rtlDefault = UnsafeMutableRawPointer(bitPattern: -2) // RTLD_DEFAULT
- fileprivate var gtkHandle: UnsafeMutableRawPointer? = nil
- fileprivate var gdkHandle: UnsafeMutableRawPointer? = nil
- fileprivate func loadGtk() {
- if gtkHandle != nil { return }
- // Load dependency chain in order — inside initramfs, everything lives under /lib/
- let depLibs = [
- "/lib/libglib-2.0.so",
- "/lib/libgobject-2.0.so",
- "/lib/libgio-2.0.so",
- "/lib/libgmodule-2.0.so",
- "/lib/libcairo.so",
- "/lib/libcairo-gobject.so",
- "/lib/libpango-1.0.so",
- "/lib/libpangocairo-1.0.so",
- "/lib/libpangoft2-1.0.so",
- "/lib/libgdk_pixbuf-2.0.so",
- "/lib/libepoxy.so",
- "/lib/libgdk-3.so",
- ]
- for lib in depLibs {
- let h = dlopen(lib, RTLD_NOW | RTLD_GLOBAL)
- if h == nil, let err = dlerror() {
- print("[GtkBackend] warning: \(lib): \(String(cString: err))")
- }
- }
- // Now load GTK itself
- let path = "/lib/libgtk-3.so"
- gtkHandle = dlopen(path, RTLD_NOW | RTLD_GLOBAL)
- if gtkHandle == nil {
- if let err = dlerror() {
- print("[GtkBackend] FATAL: \(path): \(String(cString: err))")
- } else {
- print("[GtkBackend] FATAL: failed to load \(path)")
- }
- } else {
- print("[GtkBackend] GTK3 loaded from \(path)")
- }
- }
- fileprivate func getSym<T>(_ name: String) -> T {
- loadGtk()
- let sym = dlsym(gtkHandle, name)
- if sym == nil {
- fatalError("[GtkBackend] symbol not found: \(name)")
- }
- return unsafeBitCast(sym, to: T.self)
- }
- // Function types
- fileprivate typealias GtkInitType = @convention(c) (UnsafeMutablePointer<Int32>?, UnsafeMutablePointer<UnsafeMutablePointer<UnsafeMutablePointer<CChar>>?>?) -> Void
- fileprivate typealias GtkWindowNewType = @convention(c) (Int32) -> UnsafeMutableRawPointer
- fileprivate typealias GtkWindowSetTitleType = @convention(c) (UnsafeMutableRawPointer, UnsafePointer<CChar>) -> Void
- fileprivate typealias GtkWindowSetDefaultSizeType = @convention(c) (UnsafeMutableRawPointer, Int32, Int32) -> Void
- fileprivate typealias GtkContainerAddType = @convention(c) (UnsafeMutableRawPointer, UnsafeMutableRawPointer) -> Void
- fileprivate typealias GtkBoxNewType = @convention(c) (Int32, Int32) -> UnsafeMutableRawPointer
- fileprivate typealias GtkLabelNewType = @convention(c) (UnsafePointer<CChar>) -> UnsafeMutableRawPointer
- fileprivate typealias GtkLabelSetTextType = @convention(c) (UnsafeMutableRawPointer, UnsafePointer<CChar>) -> Void
- fileprivate typealias GtkButtonNewWithLabelType = @convention(c) (UnsafePointer<CChar>) -> UnsafeMutableRawPointer
- fileprivate typealias GtkButtonSetLabelType = @convention(c) (UnsafeMutableRawPointer, UnsafePointer<CChar>) -> Void
- fileprivate typealias GtkWidgetShowAllType = @convention(c) (UnsafeMutableRawPointer) -> Void
- fileprivate typealias GtkMainType = @convention(c) () -> Void
- fileprivate typealias GtkMainQuitType = @convention(c) () -> Void
- fileprivate typealias GSignalConnectDataType = @convention(c) (UnsafeMutableRawPointer, UnsafePointer<CChar>, UnsafeMutableRawPointer, UnsafeMutableRawPointer?, UnsafeMutableRawPointer?, Int32) -> Int
- fileprivate typealias GtkWidgetOverrideBackgroundColorType = @convention(c) (UnsafeMutableRawPointer, Int32, UnsafeRawPointer?) -> Void
- fileprivate typealias GtkWidgetSetHAlignType = @convention(c) (UnsafeMutableRawPointer, Int32) -> Void
- fileprivate typealias GtkWidgetSetVAlignType = @convention(c) (UnsafeMutableRawPointer, Int32) -> Void
- fileprivate typealias GtkBoxPackStartType = @convention(c) (UnsafeMutableRawPointer, UnsafeMutableRawPointer, Int32, Int32, UInt32) -> Void
- fileprivate typealias GtkWidgetSetMarginTopType = @convention(c) (UnsafeMutableRawPointer, Int32) -> Void
- fileprivate typealias GtkWidgetSetMarginBottomType = @convention(c) (UnsafeMutableRawPointer, Int32) -> Void
- fileprivate let _gtk_init: GtkInitType = getSym("gtk_init")
- fileprivate let _gtk_window_new: GtkWindowNewType = getSym("gtk_window_new")
- fileprivate let _gtk_window_set_title: GtkWindowSetTitleType = getSym("gtk_window_set_title")
- fileprivate let _gtk_window_set_default_size: GtkWindowSetDefaultSizeType = getSym("gtk_window_set_default_size")
- fileprivate let _gtk_container_add: GtkContainerAddType = getSym("gtk_container_add")
- fileprivate let _gtk_box_new: GtkBoxNewType = getSym("gtk_box_new")
- fileprivate let _gtk_label_new: GtkLabelNewType = getSym("gtk_label_new")
- fileprivate let _gtk_label_set_text: GtkLabelSetTextType = getSym("gtk_label_set_text")
- fileprivate let _gtk_button_new_with_label: GtkButtonNewWithLabelType = getSym("gtk_button_new_with_label")
- fileprivate let _gtk_button_set_label: GtkButtonSetLabelType = getSym("gtk_button_set_label")
- fileprivate let _gtk_widget_show_all: GtkWidgetShowAllType = getSym("gtk_widget_show_all")
- fileprivate let _gtk_main: GtkMainType = getSym("gtk_main")
- fileprivate let _gtk_main_quit: GtkMainQuitType = getSym("gtk_main_quit")
- fileprivate let _g_signal_connect_data: GSignalConnectDataType = getSym("g_signal_connect_data")
- fileprivate let _gtk_box_pack_start: GtkBoxPackStartType = getSym("gtk_box_pack_start")
- fileprivate let _gtk_widget_set_margin_top: GtkWidgetSetMarginTopType = getSym("gtk_widget_set_margin_top")
- fileprivate let _gtk_widget_set_margin_bottom: GtkWidgetSetMarginBottomType = getSym("gtk_widget_set_margin_bottom")
- // MARK: - SwiftCrossUI Backend
- public class GtkBackend: BackendFeatures.BaseStubs {
- public struct Widget {
- var node: UnsafeMutableRawPointer?
- }
-
- public struct Window {
- var node: UnsafeMutableRawPointer?
- }
-
- public required init() {}
-
- public var deviceClass: DeviceClass { .desktop }
-
- public static func earlySetup() {
- setenv("GDK_BACKEND", "wayland", 1)
- setenv("WAYLAND_DISPLAY", "wayland-0", 1)
- setenv("XDG_RUNTIME_DIR", "/run", 1)
- loadGtk()
- }
-
- public func runMainLoop(_ callback: @escaping @MainActor () -> Void) {
- _gtk_init(nil, nil)
-
- callback()
-
- _gtk_main()
- }
-
- public nonisolated func runInMainThread(action: @escaping @MainActor () -> Void) {
- Task { @MainActor in
- action()
- }
- }
-
- public func computeRootEnvironment(defaultEnvironment: EnvironmentValues) -> EnvironmentValues {
- return defaultEnvironment
- }
-
- public func setRootEnvironmentChangeHandler(to action: @escaping @Sendable @MainActor () -> Void) {}
-
- // - CoreWindowing -
- public func createWindow(withDefaultSize defaultSize: SIMD2<Int>?, id: String) -> Window {
- let win = _gtk_window_new(0) // GTK_WINDOW_TOPLEVEL
- let w = Int32(defaultSize?.x ?? 1024)
- let h = Int32(defaultSize?.y ?? 768)
- _gtk_window_set_default_size(win, w, h)
- return Window(node: win)
- }
- public func updateWindow(_ window: Window, title: String) {
- if let win = window.node {
- title.withCString { cstr in
- _gtk_window_set_title(win, cstr)
- }
- }
- }
- public func updateWindow(_ window: Window, resizability: WindowResizability) {}
- public func setChild(ofWindow window: Window, to child: Widget) {
- if let win = window.node, let w = child.node {
- _gtk_container_add(win, w)
- }
- }
- public func showWindow(_ window: Window) {
- if let win = window.node {
- _gtk_widget_show_all(win)
- }
- }
-
- // - GenericContainers -
- public func createContainer() -> Widget {
- // We just use a vertical box (GTK_ORIENTATION_VERTICAL = 1)
- let box = _gtk_box_new(1, 0)
- return Widget(node: box)
- }
- public func addChild(_ child: Widget, to container: Widget) {
- if let box = container.node, let w = child.node {
- _gtk_container_add(box, w)
- }
- }
- public func setChildren(_ children: [Widget], of container: Widget) {}
- public func removeChild(_ child: Widget, from container: Widget) {}
- public func removeAllChildren(of container: Widget) {}
- public func insert(_ child: Widget, into container: Widget, at index: Int) {}
- public func swap(childAt firstIndex: Int, withChildAt secondIndex: Int, in container: Widget) {}
- public func setPosition(ofChildAt index: Int, in container: Widget, to position: SIMD2<Int>) {}
- public func remove(childAt index: Int, from container: Widget) {}
-
- // - TextViews -
- public func createTextView() -> Widget {
- let label = _gtk_label_new("")
- return Widget(node: label)
- }
- public func updateTextView(_ textView: Widget, content: String, environment: EnvironmentValues) {
- if let label = textView.node {
- content.withCString { cstr in
- _gtk_label_set_text(label, cstr)
- }
- }
- }
-
- public func size(of text: String, whenDisplayedIn widget: Widget, proposedWidth: Int?, proposedHeight: Int?, environment: EnvironmentValues) -> SIMD2<Int> {
- return SIMD2<Int>(text.count * 8, 16)
- }
-
- // - Buttons -
- public func createSimpleButton() -> Widget {
- let btn = _gtk_button_new_with_label("")
- return Widget(node: btn)
- }
- public func updateSimpleButton(_ button: Widget, label: String, environment: EnvironmentValues, action: @escaping () -> Void) {
- if let btn = button.node {
- label.withCString { cstr in
- _gtk_button_set_label(btn, cstr)
- }
- }
- }
-
- public func createButton(wrapping: Widget) -> Widget {
- let btn = _gtk_button_new_with_label("")
- return Widget(node: btn)
- }
- public func updateButton(_ button: Widget, environment: EnvironmentValues, action: @escaping () -> Void) {}
-
- public func buttonPadding(in environment: EnvironmentValues) -> SIMD2<Int> {
- return SIMD2<Int>(8, 8)
- }
- public func defaultButtonStyle() -> ButtonStyle {
- return .bordered
- }
-
- public var scrollBarWidth: Int { return 10 }
- public var requiresImageUpdateOnScaleFactorChange: Bool { return false }
- public var requiresToggleSwitchSpacer: Bool { return false }
- public var supportedPickerStyles: [BackendPickerStyle] { return [] }
-
- // Windowing extra methods
- public func show(window: Window) {}
- public func setTitle(ofWindow window: Window, to title: String) {}
- public func updateWindow(_ window: Window, environment: EnvironmentValues) {}
- public func size(ofWindow window: Window) -> SIMD2<Int> { return SIMD2<Int>(1024, 768) }
- public func setSize(ofWindow window: Window, to newSize: SIMD2<Int>) {}
- public func setSizeLimits(ofWindow window: Window, minimum minimumSize: SIMD2<Int>, maximum maximumSize: SIMD2<Int>?) {}
- public func isWindowProgrammaticallyResizable(_ window: Window) -> Bool { return false }
- public func setResizeHandler(ofWindow window: Window, to action: @escaping (SIMD2<Int>) -> Void) {}
- public func activate(window: Window) {}
- public func computeWindowEnvironment(window: Window, rootEnvironment: EnvironmentValues) -> EnvironmentValues { return rootEnvironment }
- public func setWindowEnvironmentChangeHandler(of window: Window, to action: @escaping @Sendable @MainActor () -> Void) {}
-
- public var defaultPaddingAmount: Int { return 10 }
-
- public func naturalSize(of widget: Widget) -> SIMD2<Int> { return SIMD2<Int>(0, 0) }
- public func setSize(of widget: Widget, to size: SIMD2<Int>) {}
- public func show(widget: Widget) {}
-
- public var supportsMultipleWindows: Bool { return false }
- public var canOverrideWindowColorScheme: Bool { return false }
- public var restoresWindowFrames: Bool { return false }
- }
|