Hermes Agent 36967370fd SwiftBricks: Bricks JSON UI framework native Swift/SwiftUI implementation
- Core: Schema (Codable), Store, EventBus, RPC, Engine, I18n
- Controls: Text, Title1-6, Label, Input, Textarea, Number, Date, Button, Select
  VBox, HBox, ScrollPanel, DynamicColumn, Tabular, Form, InlineForm,
  TabView, Menu, PopupWindow, Html, Image, UrlWidget
- Renderer: BricksView (main entry), ControlRenderer (recursive dispatch)
- 30+ widget types, 9 bind actiontypes
- Form validation (required/minlength/maxlength/min/max/email/number/pattern)
- i18n with multi-language JSON files
- 18 tests covering schema parsing, store, i18n, events
2026-06-21 12:12:03 +08:00

168 lines
5.5 KiB
Swift

import SwiftUI
// MARK: - VBox Control
struct VBoxControl: View {
let schema: ControlSchema
@ObservedObject var engine: BricksEngine
var body: some View {
VStack(alignment: .leading, spacing: spacing) {
if let subwidgets = schema.subwidgets {
SubWidgetsView(subwidgets: subwidgets, engine: engine)
}
}
.padding(padding)
.frame(maxWidth: .infinity, alignment: .leading)
.background(backgroundColor)
.cornerRadius(cornerRadius)
.overlay(
RoundedRectangle(cornerRadius: cornerRadius)
.stroke(borderColor, lineWidth: borderWidth)
)
.contentShape(Rectangle())
.onTapGesture {
Task {
await engine.triggerEvent(widgetId: schema.effectiveId, event: "click")
}
}
}
private var spacing: CGFloat { CGFloat(schema.options.spacing ?? 8) }
private var padding: CGFloat { parsePadding(schema.options.padding) }
private var cornerRadius: CGFloat {
(schema.options.css?.contains("card") ?? false) ? 8 : 0
}
private var borderWidth: CGFloat {
(schema.options.css?.contains("card") ?? false) ? 1 : 0
}
private var borderColor: Color {
(schema.options.css?.contains("card") ?? false) ? Color.secondary.opacity(0.2) : .clear
}
private var backgroundColor: Color {
if let bg = schema.options.bgcolor {
return Color.fromCSS(bg)
}
if schema.options.css?.contains("card") ?? false {
return Color.secondary.opacity(0.05)
}
return .clear
}
private func parsePadding(_ padding: String?) -> CGFloat {
guard let p = padding else { return 0 }
if let val = Double(p.replacingOccurrences(of: "px", with: "")) {
return CGFloat(val)
}
return 8
}
}
// MARK: - HBox Control
struct HBoxControl: View {
let schema: ControlSchema
@ObservedObject var engine: BricksEngine
var body: some View {
HStack(alignment: verticalAlignment, spacing: spacing) {
if let subwidgets = schema.subwidgets {
SubWidgetsView(subwidgets: subwidgets, engine: engine)
}
}
.padding(padding)
.frame(maxWidth: .infinity, alignment: .leading)
.background(backgroundColor)
.cornerRadius(8)
}
private var spacing: CGFloat { CGFloat(schema.options.spacing ?? 8) }
private var padding: CGFloat { parsePadding(schema.options.padding) }
private var verticalAlignment: VerticalAlignment {
switch schema.options.alignItems {
case "center": return .center
case "flex-end", "end": return .bottom
default: return .top
}
}
private var backgroundColor: Color {
if let bg = schema.options.bgcolor { return Color.fromCSS(bg) }
return .clear
}
private func parsePadding(_ padding: String?) -> CGFloat {
guard let p = padding else { return 0 }
if let val = Double(p.replacingOccurrences(of: "px", with: "")) { return CGFloat(val) }
return 8
}
}
// MARK: - ScrollPanel Control
struct ScrollPanelControl: View {
let schema: ControlSchema
@ObservedObject var engine: BricksEngine
var body: some View {
if schema.widgettype == "HScrollPanel" {
ScrollView(.horizontal, showsIndicators: true) {
HStack(alignment: .top, spacing: spacing) {
if let subwidgets = schema.subwidgets {
SubWidgetsView(subwidgets: subwidgets, engine: engine)
}
}
.padding(padding)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
ScrollView(.vertical, showsIndicators: true) {
VStack(alignment: .leading, spacing: spacing) {
if let subwidgets = schema.subwidgets {
SubWidgetsView(subwidgets: subwidgets, engine: engine)
}
}
.padding(padding)
.frame(maxWidth: .infinity, alignment: .leading)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
private var spacing: CGFloat { CGFloat(schema.options.spacing ?? 8) }
private var padding: CGFloat {
if let p = schema.options.padding, let val = Double(p.replacingOccurrences(of: "px", with: "")) {
return CGFloat(val)
}
return 8
}
}
// MARK: - DynamicColumn Control
struct DynamicColumnControl: View {
let schema: ControlSchema
@ObservedObject var engine: BricksEngine
private let columns = [GridItem(.adaptive(minimum: 200), spacing: 12)]
var body: some View {
ScrollView {
LazyVGrid(columns: resolvedColumns, spacing: gap) {
if let subwidgets = schema.subwidgets {
ForEach(Array(subwidgets.enumerated()), id: \.offset) { _, sub in
ControlRenderer(schema: sub, engine: engine)
.frame(maxWidth: .infinity)
}
}
}
.padding(8)
}
}
private var gap: CGFloat { CGFloat(schema.options.col_cgap ?? 1) * 12 }
private var resolvedColumns: [GridItem] {
let minWidth = CGFloat(schema.options.col_cwidth ?? 20) * 8
return [GridItem(.adaptive(minimum: minWidth), spacing: gap)]
}
}