GtkBackend.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. //
  2. // Copyright 2026 Aarav Ravindra Kharade
  3. //
  4. import CSystem
  5. import Foundation
  6. // MARK: - GTK3 C Bindings via dlopen
  7. fileprivate let rtlDefault = UnsafeMutableRawPointer(bitPattern: -2) // RTLD_DEFAULT
  8. fileprivate var gtkHandle: UnsafeMutableRawPointer? = nil
  9. fileprivate var gdkHandle: UnsafeMutableRawPointer? = nil
  10. fileprivate func loadGtk() {
  11. if gtkHandle != nil { return }
  12. // Load dependency chain in order — inside initramfs, everything lives under /lib/
  13. let depLibs = [
  14. "/lib/libglib-2.0.so",
  15. "/lib/libgobject-2.0.so",
  16. "/lib/libgio-2.0.so",
  17. "/lib/libgmodule-2.0.so",
  18. "/lib/libcairo.so",
  19. "/lib/libcairo-gobject.so",
  20. "/lib/libpango-1.0.so",
  21. "/lib/libpangocairo-1.0.so",
  22. "/lib/libpangoft2-1.0.so",
  23. "/lib/libgdk_pixbuf-2.0.so",
  24. "/lib/libepoxy.so",
  25. "/lib/libgdk-3.so",
  26. ]
  27. for lib in depLibs {
  28. let h = dlopen(lib, RTLD_NOW | RTLD_GLOBAL)
  29. if h == nil, let err = dlerror() {
  30. print("[GtkBackend] warning: \(lib): \(String(cString: err))")
  31. }
  32. }
  33. // Now load GTK itself
  34. let path = "/lib/libgtk-3.so"
  35. gtkHandle = dlopen(path, RTLD_NOW | RTLD_GLOBAL)
  36. if gtkHandle == nil {
  37. if let err = dlerror() {
  38. print("[GtkBackend] FATAL: \(path): \(String(cString: err))")
  39. } else {
  40. print("[GtkBackend] FATAL: failed to load \(path)")
  41. }
  42. } else {
  43. print("[GtkBackend] GTK3 loaded from \(path)")
  44. }
  45. }
  46. fileprivate func getSym<T>(_ name: String) -> T {
  47. loadGtk()
  48. let sym = dlsym(gtkHandle, name)
  49. if sym == nil {
  50. fatalError("[GtkBackend] symbol not found: \(name)")
  51. }
  52. return unsafeBitCast(sym, to: T.self)
  53. }
  54. // Function types
  55. fileprivate typealias GtkInitType = @convention(c) (UnsafeMutablePointer<Int32>?, UnsafeMutablePointer<UnsafeMutablePointer<UnsafeMutablePointer<CChar>>?>?) -> Void
  56. fileprivate typealias GtkWindowNewType = @convention(c) (Int32) -> UnsafeMutableRawPointer
  57. fileprivate typealias GtkWindowSetTitleType = @convention(c) (UnsafeMutableRawPointer, UnsafePointer<CChar>) -> Void
  58. fileprivate typealias GtkWindowSetDefaultSizeType = @convention(c) (UnsafeMutableRawPointer, Int32, Int32) -> Void
  59. fileprivate typealias GtkContainerAddType = @convention(c) (UnsafeMutableRawPointer, UnsafeMutableRawPointer) -> Void
  60. fileprivate typealias GtkBoxNewType = @convention(c) (Int32, Int32) -> UnsafeMutableRawPointer
  61. fileprivate typealias GtkLabelNewType = @convention(c) (UnsafePointer<CChar>) -> UnsafeMutableRawPointer
  62. fileprivate typealias GtkLabelSetTextType = @convention(c) (UnsafeMutableRawPointer, UnsafePointer<CChar>) -> Void
  63. fileprivate typealias GtkButtonNewWithLabelType = @convention(c) (UnsafePointer<CChar>) -> UnsafeMutableRawPointer
  64. fileprivate typealias GtkButtonSetLabelType = @convention(c) (UnsafeMutableRawPointer, UnsafePointer<CChar>) -> Void
  65. fileprivate typealias GtkWidgetShowAllType = @convention(c) (UnsafeMutableRawPointer) -> Void
  66. fileprivate typealias GtkMainType = @convention(c) () -> Void
  67. fileprivate typealias GtkMainQuitType = @convention(c) () -> Void
  68. fileprivate typealias GSignalConnectDataType = @convention(c) (UnsafeMutableRawPointer, UnsafePointer<CChar>, UnsafeMutableRawPointer, UnsafeMutableRawPointer?, UnsafeMutableRawPointer?, Int32) -> Int
  69. fileprivate typealias GtkWidgetOverrideBackgroundColorType = @convention(c) (UnsafeMutableRawPointer, Int32, UnsafeRawPointer?) -> Void
  70. fileprivate typealias GtkWidgetSetHAlignType = @convention(c) (UnsafeMutableRawPointer, Int32) -> Void
  71. fileprivate typealias GtkWidgetSetVAlignType = @convention(c) (UnsafeMutableRawPointer, Int32) -> Void
  72. fileprivate typealias GtkBoxPackStartType = @convention(c) (UnsafeMutableRawPointer, UnsafeMutableRawPointer, Int32, Int32, UInt32) -> Void
  73. fileprivate typealias GtkWidgetSetMarginTopType = @convention(c) (UnsafeMutableRawPointer, Int32) -> Void
  74. fileprivate typealias GtkWidgetSetMarginBottomType = @convention(c) (UnsafeMutableRawPointer, Int32) -> Void
  75. fileprivate let _gtk_init: GtkInitType = getSym("gtk_init")
  76. fileprivate let _gtk_window_new: GtkWindowNewType = getSym("gtk_window_new")
  77. fileprivate let _gtk_window_set_title: GtkWindowSetTitleType = getSym("gtk_window_set_title")
  78. fileprivate let _gtk_window_set_default_size: GtkWindowSetDefaultSizeType = getSym("gtk_window_set_default_size")
  79. fileprivate let _gtk_container_add: GtkContainerAddType = getSym("gtk_container_add")
  80. fileprivate let _gtk_box_new: GtkBoxNewType = getSym("gtk_box_new")
  81. fileprivate let _gtk_label_new: GtkLabelNewType = getSym("gtk_label_new")
  82. fileprivate let _gtk_label_set_text: GtkLabelSetTextType = getSym("gtk_label_set_text")
  83. fileprivate let _gtk_button_new_with_label: GtkButtonNewWithLabelType = getSym("gtk_button_new_with_label")
  84. fileprivate let _gtk_button_set_label: GtkButtonSetLabelType = getSym("gtk_button_set_label")
  85. fileprivate let _gtk_widget_show_all: GtkWidgetShowAllType = getSym("gtk_widget_show_all")
  86. fileprivate let _gtk_main: GtkMainType = getSym("gtk_main")
  87. fileprivate let _gtk_main_quit: GtkMainQuitType = getSym("gtk_main_quit")
  88. fileprivate let _g_signal_connect_data: GSignalConnectDataType = getSym("g_signal_connect_data")
  89. fileprivate let _gtk_box_pack_start: GtkBoxPackStartType = getSym("gtk_box_pack_start")
  90. fileprivate let _gtk_widget_set_margin_top: GtkWidgetSetMarginTopType = getSym("gtk_widget_set_margin_top")
  91. fileprivate let _gtk_widget_set_margin_bottom: GtkWidgetSetMarginBottomType = getSym("gtk_widget_set_margin_bottom")
  92. // MARK: - SwiftCrossUI Backend
  93. public class GtkBackend: BackendFeatures.BaseStubs {
  94. public struct Widget {
  95. var node: UnsafeMutableRawPointer?
  96. }
  97. public struct Window {
  98. var node: UnsafeMutableRawPointer?
  99. }
  100. public required init() {}
  101. public var deviceClass: DeviceClass { .desktop }
  102. public static func earlySetup() {
  103. setenv("GDK_BACKEND", "wayland", 1)
  104. setenv("WAYLAND_DISPLAY", "wayland-0", 1)
  105. setenv("XDG_RUNTIME_DIR", "/run", 1)
  106. loadGtk()
  107. }
  108. public func runMainLoop(_ callback: @escaping @MainActor () -> Void) {
  109. _gtk_init(nil, nil)
  110. callback()
  111. _gtk_main()
  112. }
  113. public nonisolated func runInMainThread(action: @escaping @MainActor () -> Void) {
  114. Task { @MainActor in
  115. action()
  116. }
  117. }
  118. public func computeRootEnvironment(defaultEnvironment: EnvironmentValues) -> EnvironmentValues {
  119. return defaultEnvironment
  120. }
  121. public func setRootEnvironmentChangeHandler(to action: @escaping @Sendable @MainActor () -> Void) {}
  122. // - CoreWindowing -
  123. public func createWindow(withDefaultSize defaultSize: SIMD2<Int>?, id: String) -> Window {
  124. let win = _gtk_window_new(0) // GTK_WINDOW_TOPLEVEL
  125. let w = Int32(defaultSize?.x ?? 1024)
  126. let h = Int32(defaultSize?.y ?? 768)
  127. _gtk_window_set_default_size(win, w, h)
  128. return Window(node: win)
  129. }
  130. public func updateWindow(_ window: Window, title: String) {
  131. if let win = window.node {
  132. title.withCString { cstr in
  133. _gtk_window_set_title(win, cstr)
  134. }
  135. }
  136. }
  137. public func updateWindow(_ window: Window, resizability: WindowResizability) {}
  138. public func setChild(ofWindow window: Window, to child: Widget) {
  139. if let win = window.node, let w = child.node {
  140. _gtk_container_add(win, w)
  141. }
  142. }
  143. public func showWindow(_ window: Window) {
  144. if let win = window.node {
  145. _gtk_widget_show_all(win)
  146. }
  147. }
  148. // - GenericContainers -
  149. public func createContainer() -> Widget {
  150. // We just use a vertical box (GTK_ORIENTATION_VERTICAL = 1)
  151. let box = _gtk_box_new(1, 0)
  152. return Widget(node: box)
  153. }
  154. public func addChild(_ child: Widget, to container: Widget) {
  155. if let box = container.node, let w = child.node {
  156. _gtk_container_add(box, w)
  157. }
  158. }
  159. public func setChildren(_ children: [Widget], of container: Widget) {}
  160. public func removeChild(_ child: Widget, from container: Widget) {}
  161. public func removeAllChildren(of container: Widget) {}
  162. public func insert(_ child: Widget, into container: Widget, at index: Int) {}
  163. public func swap(childAt firstIndex: Int, withChildAt secondIndex: Int, in container: Widget) {}
  164. public func setPosition(ofChildAt index: Int, in container: Widget, to position: SIMD2<Int>) {}
  165. public func remove(childAt index: Int, from container: Widget) {}
  166. // - TextViews -
  167. public func createTextView() -> Widget {
  168. let label = _gtk_label_new("")
  169. return Widget(node: label)
  170. }
  171. public func updateTextView(_ textView: Widget, content: String, environment: EnvironmentValues) {
  172. if let label = textView.node {
  173. content.withCString { cstr in
  174. _gtk_label_set_text(label, cstr)
  175. }
  176. }
  177. }
  178. public func size(of text: String, whenDisplayedIn widget: Widget, proposedWidth: Int?, proposedHeight: Int?, environment: EnvironmentValues) -> SIMD2<Int> {
  179. return SIMD2<Int>(text.count * 8, 16)
  180. }
  181. // - Buttons -
  182. public func createSimpleButton() -> Widget {
  183. let btn = _gtk_button_new_with_label("")
  184. return Widget(node: btn)
  185. }
  186. public func updateSimpleButton(_ button: Widget, label: String, environment: EnvironmentValues, action: @escaping () -> Void) {
  187. if let btn = button.node {
  188. label.withCString { cstr in
  189. _gtk_button_set_label(btn, cstr)
  190. }
  191. }
  192. }
  193. public func createButton(wrapping: Widget) -> Widget {
  194. let btn = _gtk_button_new_with_label("")
  195. return Widget(node: btn)
  196. }
  197. public func updateButton(_ button: Widget, environment: EnvironmentValues, action: @escaping () -> Void) {}
  198. public func buttonPadding(in environment: EnvironmentValues) -> SIMD2<Int> {
  199. return SIMD2<Int>(8, 8)
  200. }
  201. public func defaultButtonStyle() -> ButtonStyle {
  202. return .bordered
  203. }
  204. public var scrollBarWidth: Int { return 10 }
  205. public var requiresImageUpdateOnScaleFactorChange: Bool { return false }
  206. public var requiresToggleSwitchSpacer: Bool { return false }
  207. public var supportedPickerStyles: [BackendPickerStyle] { return [] }
  208. // Windowing extra methods
  209. public func show(window: Window) {}
  210. public func setTitle(ofWindow window: Window, to title: String) {}
  211. public func updateWindow(_ window: Window, environment: EnvironmentValues) {}
  212. public func size(ofWindow window: Window) -> SIMD2<Int> { return SIMD2<Int>(1024, 768) }
  213. public func setSize(ofWindow window: Window, to newSize: SIMD2<Int>) {}
  214. public func setSizeLimits(ofWindow window: Window, minimum minimumSize: SIMD2<Int>, maximum maximumSize: SIMD2<Int>?) {}
  215. public func isWindowProgrammaticallyResizable(_ window: Window) -> Bool { return false }
  216. public func setResizeHandler(ofWindow window: Window, to action: @escaping (SIMD2<Int>) -> Void) {}
  217. public func activate(window: Window) {}
  218. public func computeWindowEnvironment(window: Window, rootEnvironment: EnvironmentValues) -> EnvironmentValues { return rootEnvironment }
  219. public func setWindowEnvironmentChangeHandler(of window: Window, to action: @escaping @Sendable @MainActor () -> Void) {}
  220. public var defaultPaddingAmount: Int { return 10 }
  221. public func naturalSize(of widget: Widget) -> SIMD2<Int> { return SIMD2<Int>(0, 0) }
  222. public func setSize(of widget: Widget, to size: SIMD2<Int>) {}
  223. public func show(widget: Widget) {}
  224. public var supportsMultipleWindows: Bool { return false }
  225. public var canOverrideWindowColorScheme: Bool { return false }
  226. public var restoresWindowFrames: Bool { return false }
  227. }