ArkUI.swift 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. //
  2. // Copyright 2026 Aarav Ravindra Kharade
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. //
  16. import ark.sys.libc
  17. // ═══════════════════════════════════════════════════════════════════
  18. // ArkUI — Declarative UI Framework for ArkOS
  19. // ═══════════════════════════════════════════════════════════════════
  20. // Provides SwiftUI-inspired view primitives (VStack, HStack, Text,
  21. // Button) with layout modifiers (padding, frame, background, glow,
  22. // outline). All rendering goes through ArkGraphics.
  23. //
  24. // Coordinate system: Cartesian (0,0 = bottom-left).
  25. // ═══════════════════════════════════════════════════════════════════
  26. // MARK: - Core Geometry Types
  27. /// Size in pixels (width × height).
  28. public struct ArkSize {
  29. public var w: Int
  30. public var h: Int
  31. public init(w: Int, h: Int) { self.w = w; self.h = h }
  32. }
  33. /// Point in Cartesian space (0,0 = bottom-left).
  34. public struct ArkPoint {
  35. public var x: Int
  36. public var y: Int
  37. public init(x: Int, y: Int) { self.x = x; self.y = y }
  38. }
  39. /// Rectangle defined by origin (x,y) and size (w,h).
  40. public struct ArkRect {
  41. public var x: Int
  42. public var y: Int
  43. public var w: Int
  44. public var h: Int
  45. public init(x: Int, y: Int, w: Int, h: Int) {
  46. self.x = x; self.y = y; self.w = w; self.h = h
  47. }
  48. /// Returns true if point (px, py) lies within this rectangle.
  49. public func contains(_ px: Int, _ py: Int) -> Bool {
  50. return px >= x && px < x + w && py >= y && py < y + h
  51. }
  52. }
  53. /// RGBA color value.
  54. public struct ArkColor {
  55. public var r: UInt8
  56. public var g: UInt8
  57. public var b: UInt8
  58. public var a: UInt8
  59. public init(r: UInt8, g: UInt8, b: UInt8, a: UInt8) {
  60. self.r = r; self.g = g; self.b = b; self.a = a
  61. }
  62. public static let clear = ArkColor(r: 0, g: 0, b: 0, a: 0)
  63. public static let white = ArkColor(r: 255, g: 255, b: 255, a: 255)
  64. public static let black = ArkColor(r: 0, g: 0, b: 0, a: 255)
  65. }
  66. // MARK: - UI Context
  67. /// Rendering context passed through the view tree during layout and render.
  68. public class ArkUIContext {
  69. public let graphics: ArkGraphics
  70. public let writer: ArkWrite
  71. public let shapes: ArkShapes
  72. public init(graphics: ArkGraphics) {
  73. self.graphics = graphics
  74. self.writer = ArkWrite(graphics: graphics)
  75. self.shapes = ArkShapes(graphics: graphics)
  76. }
  77. }
  78. // MARK: - View Protocol
  79. /// Base protocol for all UI views. Every view must be able to:
  80. /// 1. Report its ideal size given a size proposal.
  81. /// 2. Render itself into a given bounds rectangle.
  82. /// 3. Handle input events at a given point.
  83. public protocol ArkView {
  84. func sizeThatFits(proposal: ArkSize) -> ArkSize
  85. func render(context: ArkUIContext, bounds: ArkRect)
  86. func handleInput(event: ArkInputEvent, point: ArkPoint) -> Bool
  87. }
  88. /// Type-erased wrapper for any ArkView. Enables heterogeneous view storage.
  89. public struct AnyArkView: ArkView {
  90. private let _sizeThatFits: (ArkSize) -> ArkSize
  91. private let _render: (ArkUIContext, ArkRect) -> Void
  92. private let _handleInput: (ArkInputEvent, ArkPoint) -> Bool
  93. public init<V: ArkView>(_ view: V) {
  94. self._sizeThatFits = view.sizeThatFits
  95. self._render = view.render
  96. self._handleInput = view.handleInput
  97. }
  98. public func sizeThatFits(proposal: ArkSize) -> ArkSize { return _sizeThatFits(proposal) }
  99. public func render(context: ArkUIContext, bounds: ArkRect) { _render(context, bounds) }
  100. public func handleInput(event: ArkInputEvent, point: ArkPoint) -> Bool { return _handleInput(event, point) }
  101. }
  102. // MARK: - Content Alignment
  103. /// Alignment options for layout containers and frame views.
  104. public enum ArkAlignment {
  105. case leading, center, trailing, top, bottom
  106. }
  107. // MARK: - Layout Containers
  108. /// Vertical stack layout — arranges children top-to-bottom with spacing.
  109. public struct VStack: ArkView {
  110. var alignment: ArkAlignment = .center
  111. var spacing: Int = 10
  112. var children: [AnyArkView]
  113. public init(alignment: ArkAlignment = .center, spacing: Int = 10, @ArkViewBuilder content: () -> [AnyArkView]) {
  114. self.alignment = alignment
  115. self.spacing = spacing
  116. self.children = content()
  117. }
  118. public func sizeThatFits(proposal: ArkSize) -> ArkSize {
  119. var totalW = 0
  120. var totalH = 0
  121. for child in children {
  122. let size = child.sizeThatFits(proposal: proposal)
  123. if size.w > totalW { totalW = size.w }
  124. totalH += size.h
  125. }
  126. totalH += spacing * max(0, children.count - 1)
  127. return ArkSize(w: totalW, h: totalH)
  128. }
  129. public func render(context: ArkUIContext, bounds: ArkRect) {
  130. let totalH = sizeThatFits(proposal: ArkSize(w: bounds.w, h: bounds.h)).h
  131. // Vertical alignment: determine starting Y position
  132. // In Cartesian coords, top = higher Y values
  133. var currentY: Int
  134. switch alignment {
  135. case .top:
  136. currentY = bounds.y + bounds.h - 1 // Start from top of bounds
  137. case .bottom:
  138. currentY = bounds.y + totalH - 1 // Pack at bottom
  139. default: // .center and horizontal alignments
  140. currentY = bounds.y + bounds.h - (bounds.h - totalH) / 2 - 1
  141. }
  142. for child in children {
  143. let size = child.sizeThatFits(proposal: ArkSize(w: bounds.w, h: bounds.h))
  144. // Horizontal alignment within the VStack
  145. var childX = bounds.x
  146. if alignment == .center {
  147. childX = bounds.x + (bounds.w - size.w) / 2
  148. } else if alignment == .trailing {
  149. childX = bounds.x + bounds.w - size.w
  150. }
  151. // Place child: currentY is the top edge, child renders downward
  152. let childY = currentY - size.h + 1
  153. child.render(context: context, bounds: ArkRect(x: childX, y: childY, w: size.w, h: size.h))
  154. currentY -= size.h + spacing
  155. }
  156. }
  157. public func handleInput(event: ArkInputEvent, point: ArkPoint) -> Bool {
  158. for child in children {
  159. if child.handleInput(event: event, point: point) { return true }
  160. }
  161. return false
  162. }
  163. }
  164. /// Horizontal stack layout — arranges children left-to-right with spacing.
  165. public struct HStack: ArkView {
  166. var alignment: ArkAlignment = .center
  167. var spacing: Int = 10
  168. var children: [AnyArkView]
  169. public init(alignment: ArkAlignment = .center, spacing: Int = 10, @ArkViewBuilder content: () -> [AnyArkView]) {
  170. self.alignment = alignment
  171. self.spacing = spacing
  172. self.children = content()
  173. }
  174. public func sizeThatFits(proposal: ArkSize) -> ArkSize {
  175. var totalW = 0
  176. var totalH = 0
  177. for child in children {
  178. let size = child.sizeThatFits(proposal: proposal)
  179. if size.h > totalH { totalH = size.h }
  180. totalW += size.w
  181. }
  182. totalW += spacing * max(0, children.count - 1)
  183. return ArkSize(w: totalW, h: totalH)
  184. }
  185. public func render(context: ArkUIContext, bounds: ArkRect) {
  186. let totalW = sizeThatFits(proposal: ArkSize(w: bounds.w, h: bounds.h)).w
  187. // Horizontal alignment: determine starting X position
  188. var currentX: Int
  189. switch alignment {
  190. case .trailing:
  191. currentX = bounds.x + bounds.w - totalW
  192. case .leading:
  193. currentX = bounds.x
  194. default: // .center
  195. currentX = bounds.x + (bounds.w - totalW) / 2
  196. }
  197. for child in children {
  198. let size = child.sizeThatFits(proposal: ArkSize(w: bounds.w, h: bounds.h))
  199. // Vertical alignment within the HStack
  200. var childY = bounds.y
  201. if alignment == .center {
  202. childY = bounds.y + (bounds.h - size.h) / 2
  203. } else if alignment == .top {
  204. childY = bounds.y + bounds.h - size.h
  205. }
  206. child.render(context: context, bounds: ArkRect(x: currentX, y: childY, w: size.w, h: size.h))
  207. currentX += size.w + spacing
  208. }
  209. }
  210. public func handleInput(event: ArkInputEvent, point: ArkPoint) -> Bool {
  211. for child in children {
  212. if child.handleInput(event: event, point: point) { return true }
  213. }
  214. return false
  215. }
  216. }
  217. // MARK: - Text Component
  218. /// Renders a text string using the embedded Roboto Bold bitmap font.
  219. public struct Text: ArkView {
  220. public var content: String
  221. public var size: Int = 30
  222. public var color: ArkColor = .white
  223. public init(_ content: String) {
  224. self.content = content
  225. }
  226. /// Computes the exact pixel size of the rendered text using the same
  227. /// bounding-box logic as ArkWrite.drawText, ensuring layout matches rendering.
  228. public func sizeThatFits(proposal: ArkSize) -> ArkSize {
  229. return ArkWrite.measureText(content, size: size)
  230. }
  231. public func render(context: ArkUIContext, bounds: ArkRect) {
  232. context.writer.drawText(content, x: bounds.x, y: bounds.y, size: size, r: color.r, g: color.g, b: color.b)
  233. }
  234. public func handleInput(event: ArkInputEvent, point: ArkPoint) -> Bool { return false }
  235. }
  236. // MARK: - Button Components
  237. /// Simple button that renders a label and triggers an action on click.
  238. /// Stores its rendered bounds for hit testing.
  239. public class Button: ArkView {
  240. var action: () -> Void
  241. var label: AnyArkView
  242. var bg: ArkColor = .clear
  243. var radius: Int = 0
  244. var frame: ArkRect = ArkRect(x: 0, y: 0, w: 0, h: 0)
  245. public init(action: @escaping () -> Void, @ArkViewBuilder label: () -> [AnyArkView]) {
  246. self.action = action
  247. self.label = label()[0]
  248. }
  249. public func sizeThatFits(proposal: ArkSize) -> ArkSize {
  250. return label.sizeThatFits(proposal: proposal)
  251. }
  252. public func render(context: ArkUIContext, bounds: ArkRect) {
  253. // Store bounds for hit testing
  254. self.frame = bounds
  255. if bg.a > 0 {
  256. context.shapes.fillRoundedRect(x: bounds.x, y: bounds.y, w: bounds.w, h: bounds.h, radius: radius, r: bg.r, g: bg.g, b: bg.b)
  257. }
  258. label.render(context: context, bounds: bounds)
  259. }
  260. public func handleInput(event: ArkInputEvent, point: ArkPoint) -> Bool {
  261. // BTN_LEFT press (type=1, code=0x110, value=1)
  262. if event.type == 1 && event.value == 1 {
  263. if frame.contains(point.x, point.y) {
  264. action()
  265. return true
  266. }
  267. }
  268. return false
  269. }
  270. }
  271. /// Stateful button that captures its rendered frame for accurate hit testing.
  272. /// Used in the setup app for the "Get Started" button.
  273. public class StatefulButton: ArkView {
  274. var action: () -> Void
  275. var label: AnyArkView
  276. var frame: ArkRect = ArkRect(x: 0, y: 0, w: 0, h: 0)
  277. public init(action: @escaping () -> Void, label: AnyArkView) {
  278. self.action = action
  279. self.label = label
  280. }
  281. public func sizeThatFits(proposal: ArkSize) -> ArkSize {
  282. return label.sizeThatFits(proposal: proposal)
  283. }
  284. public func render(context: ArkUIContext, bounds: ArkRect) {
  285. self.frame = bounds
  286. label.render(context: context, bounds: bounds)
  287. }
  288. public func handleInput(event: ArkInputEvent, point: ArkPoint) -> Bool {
  289. // BTN_LEFT press (type=1 = EV_KEY, value=1 = press)
  290. if event.type == 1 && event.value == 1 {
  291. if frame.contains(point.x, point.y) {
  292. action()
  293. return true
  294. }
  295. }
  296. return false
  297. }
  298. }
  299. // MARK: - View Modifiers
  300. /// Adds equal padding on all edges around the content view.
  301. /// Correctly reduces the available proposal for the child.
  302. public struct PaddingView: ArkView {
  303. var content: AnyArkView
  304. var edges: Int
  305. public func sizeThatFits(proposal: ArkSize) -> ArkSize {
  306. // Pass reduced proposal to the child so it knows how much space it actually has
  307. let childProposal = ArkSize(w: max(0, proposal.w - edges * 2), h: max(0, proposal.h - edges * 2))
  308. let child = content.sizeThatFits(proposal: childProposal)
  309. return ArkSize(w: child.w + edges * 2, h: child.h + edges * 2)
  310. }
  311. public func render(context: ArkUIContext, bounds: ArkRect) {
  312. let innerBounds = ArkRect(
  313. x: bounds.x + edges,
  314. y: bounds.y + edges,
  315. w: max(0, bounds.w - edges * 2),
  316. h: max(0, bounds.h - edges * 2)
  317. )
  318. content.render(context: context, bounds: innerBounds)
  319. }
  320. public func handleInput(event: ArkInputEvent, point: ArkPoint) -> Bool {
  321. return content.handleInput(event: event, point: point)
  322. }
  323. }
  324. /// Adds a colored glow halo around the content view.
  325. public struct GlowView: ArkView {
  326. var content: AnyArkView
  327. var radius: Int
  328. var color: ArkColor
  329. public func sizeThatFits(proposal: ArkSize) -> ArkSize {
  330. let size = content.sizeThatFits(proposal: ArkSize(w: max(0, proposal.w - radius * 2), h: max(0, proposal.h - radius * 2)))
  331. return ArkSize(w: size.w + radius * 2, h: size.h + radius * 2)
  332. }
  333. public func render(context: ArkUIContext, bounds: ArkRect) {
  334. // Draw glow as a slightly larger rounded rect behind the content
  335. context.shapes.fillRoundedRect(x: bounds.x, y: bounds.y, w: bounds.w, h: bounds.h, radius: radius, r: color.r, g: color.g, b: color.b)
  336. let contentBounds = ArkRect(x: bounds.x + radius, y: bounds.y + radius, w: max(0, bounds.w - radius * 2), h: max(0, bounds.h - radius * 2))
  337. content.render(context: context, bounds: contentBounds)
  338. }
  339. public func handleInput(event: ArkInputEvent, point: ArkPoint) -> Bool {
  340. return content.handleInput(event: event, point: point)
  341. }
  342. }
  343. /// Draws a colored outline border around the content view.
  344. public struct OutlineView: ArkView {
  345. var content: AnyArkView
  346. var width: Int
  347. var color: ArkColor
  348. public func sizeThatFits(proposal: ArkSize) -> ArkSize {
  349. let size = content.sizeThatFits(proposal: ArkSize(w: max(0, proposal.w - width * 2), h: max(0, proposal.h - width * 2)))
  350. return ArkSize(w: size.w + width * 2, h: size.h + width * 2)
  351. }
  352. public func render(context: ArkUIContext, bounds: ArkRect) {
  353. context.shapes.fillRoundedRect(x: bounds.x, y: bounds.y, w: bounds.w, h: bounds.h, radius: width * 2, r: color.r, g: color.g, b: color.b)
  354. let contentBounds = ArkRect(x: bounds.x + width, y: bounds.y + width, w: max(0, bounds.w - width * 2), h: max(0, bounds.h - width * 2))
  355. content.render(context: context, bounds: contentBounds)
  356. }
  357. public func handleInput(event: ArkInputEvent, point: ArkPoint) -> Bool {
  358. return content.handleInput(event: event, point: point)
  359. }
  360. }
  361. /// Fills a colored background behind the content view with optional corner radius.
  362. public struct BackgroundView: ArkView {
  363. var content: AnyArkView
  364. var color: ArkColor
  365. var radius: Int = 0
  366. public func sizeThatFits(proposal: ArkSize) -> ArkSize {
  367. return content.sizeThatFits(proposal: proposal)
  368. }
  369. public func render(context: ArkUIContext, bounds: ArkRect) {
  370. context.shapes.fillRoundedRect(x: bounds.x, y: bounds.y, w: bounds.w, h: bounds.h, radius: radius, r: color.r, g: color.g, b: color.b)
  371. content.render(context: context, bounds: bounds)
  372. }
  373. public func handleInput(event: ArkInputEvent, point: ArkPoint) -> Bool {
  374. return content.handleInput(event: event, point: point)
  375. }
  376. }
  377. /// Constrains the view to a specific width and/or height.
  378. /// Passes the constrained dimensions as the proposal to the child.
  379. public struct FrameView: ArkView {
  380. var content: AnyArkView
  381. var width: Int?
  382. var height: Int?
  383. var alignment: ArkAlignment
  384. public func sizeThatFits(proposal: ArkSize) -> ArkSize {
  385. // Pass constrained dimensions to the child
  386. let constrainedProposal = ArkSize(w: width ?? proposal.w, h: height ?? proposal.h)
  387. let cSize = content.sizeThatFits(proposal: constrainedProposal)
  388. return ArkSize(w: width ?? cSize.w, h: height ?? cSize.h)
  389. }
  390. public func render(context: ArkUIContext, bounds: ArkRect) {
  391. let constrainedProposal = ArkSize(w: bounds.w, h: bounds.h)
  392. let cSize = content.sizeThatFits(proposal: constrainedProposal)
  393. var cx = bounds.x
  394. var cy = bounds.y
  395. switch alignment {
  396. case .center:
  397. cx += (bounds.w - cSize.w) / 2
  398. cy += (bounds.h - cSize.h) / 2
  399. case .leading:
  400. cy += (bounds.h - cSize.h) / 2
  401. case .trailing:
  402. cx += bounds.w - cSize.w
  403. cy += (bounds.h - cSize.h) / 2
  404. case .top:
  405. cx += (bounds.w - cSize.w) / 2
  406. cy += bounds.h - cSize.h
  407. case .bottom:
  408. cx += (bounds.w - cSize.w) / 2
  409. }
  410. content.render(context: context, bounds: ArkRect(x: cx, y: cy, w: cSize.w, h: cSize.h))
  411. }
  412. public func handleInput(event: ArkInputEvent, point: ArkPoint) -> Bool {
  413. return content.handleInput(event: event, point: point)
  414. }
  415. }
  416. // MARK: - Modifier Extension Methods
  417. public extension ArkView {
  418. /// Adds uniform padding around the view.
  419. func padding(_ amount: Int = 10) -> AnyArkView {
  420. return AnyArkView(PaddingView(content: AnyArkView(self), edges: amount))
  421. }
  422. /// Fills a background color behind the view.
  423. func background(_ color: ArkColor, cornerRadius: Int = 0) -> AnyArkView {
  424. return AnyArkView(BackgroundView(content: AnyArkView(self), color: color, radius: cornerRadius))
  425. }
  426. /// Constrains the view to specific dimensions.
  427. func frame(width: Int? = nil, height: Int? = nil, alignment: ArkAlignment = .center) -> AnyArkView {
  428. return AnyArkView(FrameView(content: AnyArkView(self), width: width, height: height, alignment: alignment))
  429. }
  430. /// Sets the text color (only affects Text views).
  431. func foregroundColor(_ color: ArkColor) -> AnyArkView {
  432. if var txt = self as? Text {
  433. txt.color = color
  434. return AnyArkView(txt)
  435. }
  436. return AnyArkView(self)
  437. }
  438. /// Sets the font size (only affects Text views).
  439. func fontSize(_ size: Int) -> AnyArkView {
  440. if var txt = self as? Text {
  441. txt.size = size
  442. return AnyArkView(txt)
  443. }
  444. return AnyArkView(self)
  445. }
  446. /// Adds a colored glow halo around the view.
  447. func glow(radius: Int, color: ArkColor) -> AnyArkView {
  448. return AnyArkView(GlowView(content: AnyArkView(self), radius: radius, color: color))
  449. }
  450. /// Draws a colored outline border around the view.
  451. func outline(width: Int, color: ArkColor) -> AnyArkView {
  452. return AnyArkView(OutlineView(content: AnyArkView(self), width: width, color: color))
  453. }
  454. }
  455. // MARK: - Result Builder
  456. /// Enables declarative view tree construction using Swift result builders.
  457. @resultBuilder
  458. public struct ArkViewBuilder {
  459. public static func buildBlock(_ components: AnyArkView...) -> [AnyArkView] {
  460. return components
  461. }
  462. public static func buildExpression<V: ArkView>(_ expression: V) -> AnyArkView {
  463. return AnyArkView(expression)
  464. }
  465. }