diff --git a/Examples/ExampleApp/.gitignore b/Examples/ExampleApp/.gitignore new file mode 100644 index 0000000..425bd96 --- /dev/null +++ b/Examples/ExampleApp/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc + +.shaft \ No newline at end of file diff --git a/Examples/ExampleApp/.swift-format b/Examples/ExampleApp/.swift-format new file mode 100644 index 0000000..16921f7 --- /dev/null +++ b/Examples/ExampleApp/.swift-format @@ -0,0 +1,11 @@ +{ + "version": 1, + "lineLength": 100, + "indentation": { + "spaces": 4 + }, + "maximumBlankLines": 1, + "respectsExistingLineBreaks": true, + "lineBreakBeforeControlFlowKeywords": false, + "lineBreakBeforeEachArgument": true +} \ No newline at end of file diff --git a/Examples/ExampleApp/Package.resolved b/Examples/ExampleApp/Package.resolved new file mode 100644 index 0000000..ccec1a1 --- /dev/null +++ b/Examples/ExampleApp/Package.resolved @@ -0,0 +1,104 @@ +{ + "pins" : [ + { + "identity" : "rainbow", + "kind" : "remoteSourceControl", + "location" : "https://github.com/onevcat/Rainbow", + "state" : { + "revision" : "16da5c62dd737258c6df2e8c430f8a3202f655a7", + "version" : "4.2.0" + } + }, + { + "identity" : "shaft", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ShaftUI/Shaft", + "state" : { + "branch" : "main", + "revision" : "42b227b4903ce3fc78049d449210f7c0ce90a8cc" + } + }, + { + "identity" : "splash", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ShaftUI/Splash", + "state" : { + "branch" : "master", + "revision" : "ed08785980b61de9b98306434410ce7fc10572ea" + } + }, + { + "identity" : "swift-cmark", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-cmark.git", + "state" : { + "branch" : "gfm", + "revision" : "e02dcabc65781626a7e46b22fa275923bbe6c2f6" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ShaftUI/swift-collections", + "state" : { + "revision" : "52a1f698d5faa632df0e1219b1bbffa07cf65260", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-markdown", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-markdown.git", + "state" : { + "branch" : "main", + "revision" : "0186e808140db5eb5ba9b052e86fbe15b88f7975" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax.git", + "state" : { + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" + } + }, + { + "identity" : "swiftmath", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ShaftUI/SwiftMath", + "state" : { + "revision" : "29039462bcd88b9469041f2678b892d0dd7a4c6f", + "version" : "3.4.0" + } + }, + { + "identity" : "swiftreload", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ShaftUI/SwiftReload.git", + "state" : { + "branch" : "main", + "revision" : "cce18342fe98213eeb98ca58ca2779ecd14e3762" + } + }, + { + "identity" : "swiftsdl3", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ShaftUI/SwiftSDL3", + "state" : { + "branch" : "main", + "revision" : "d33d998856d263f19f7fc8c5f893fbb82e6564dc" + } + }, + { + "identity" : "yams", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/Yams.git", + "state" : { + "revision" : "b4b8042411dc7bbb696300a34a4bf3ba1b7ad19b", + "version" : "5.3.1" + } + } + ], + "version" : 2 +} diff --git a/Examples/ExampleApp/Package.swift b/Examples/ExampleApp/Package.swift new file mode 100644 index 0000000..2c6da7d --- /dev/null +++ b/Examples/ExampleApp/Package.swift @@ -0,0 +1,53 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "ExampleApp", + + platforms: [ + .macOS(.v14), + .iOS(.v13), + .tvOS(.v13), + ], + + dependencies: [ + .package( + url: "https://github.com/ShaftUI/Shaft", + branch: "main" + ), + .package(url: "https://github.com/ShaftUI/SwiftReload.git", branch: "main"), + .package(path: "../../"), // nativeapi-swift + ], + + targets: [ + .executableTarget( + name: "ExampleApp", + dependencies: [ + "SwiftReload", + .product(name: "Shaft", package: "Shaft"), + .product(name: "ShaftSetup", package: "Shaft"), + .product(name: "NativeAPI", package: "nativeapi-swift"), + ], + swiftSettings: [ + .interoperabilityMode(.Cxx), // This is necessary to use the Skia renderer + .unsafeFlags( + ["-Xfrontend", "-enable-private-imports"], + .when(configuration: .debug) + ), + .unsafeFlags( + ["-Xfrontend", "-enable-implicit-dynamic"], + .when(configuration: .debug) + ), + ], + linkerSettings: [ + .unsafeFlags( + ["-Xlinker", "--export-dynamic"], + .when(platforms: [.linux, .android], configuration: .debug) + ) + ] + ) + ], + cxxLanguageStandard: .cxx17 // This is necessary to use the Skia renderer +) diff --git a/Examples/ExampleApp/README.md b/Examples/ExampleApp/README.md new file mode 100644 index 0000000..ff3a7cc --- /dev/null +++ b/Examples/ExampleApp/README.md @@ -0,0 +1,5 @@ +# ExampleApp + +```sh +swift run +``` diff --git a/Examples/ExampleApp/Sources/DisplayManagerDemo.swift b/Examples/ExampleApp/Sources/DisplayManagerDemo.swift new file mode 100644 index 0000000..129dfda --- /dev/null +++ b/Examples/ExampleApp/Sources/DisplayManagerDemo.swift @@ -0,0 +1,305 @@ +import Foundation +import NativeAPI +import Observation +import Shaft + +@Observable +class DisplayManagerState { + var displays: [Display] = [] + var cursorPosition: Point = Point(x: 0, y: 0) + var currentWindow: Window? + var lastUpdate: Date = Date() +} + +final class DisplayManagerDemo: StatefulWidget { + func createState() -> DisplayManagerDemoState { + DisplayManagerDemoState() + } +} + +final class DisplayManagerDemoState: State { + let state = DisplayManagerState() + var updateTimer: Foundation.Timer? + + override func initState() { + super.initState() + + // Start update timer + updateTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { + [weak self] _ in + self?.updateDisplayInfo() + } + + // Initial load + self.updateDisplayInfo() + } + + override func dispose() { + updateTimer?.invalidate() + super.dispose() + } + + private func updateDisplayInfo() { + state.displays = DisplayManager.shared.getAll() + state.cursorPosition = DisplayManager.shared.getCursorPosition() + state.currentWindow = WindowManager.shared.getCurrent() + state.lastUpdate = Date() + } + + override func build(context: BuildContext) -> Widget { + Column(crossAxisAlignment: .stretch, spacing: 16) { + // Header + buildHeader() + + // Content area with displays and cursor info + Expanded { + Row(spacing: 16) { + // Displays list (2/3 width) + Expanded(flex: 2) { + buildDisplaysList() + } + + // Cursor and current window info (1/3 width) + Expanded(flex: 1) { + Column(spacing: 16) { + buildCursorInfo() + buildCurrentWindowInfo() + } + } + } + } + } + .padding(.all(16)) + } + + private func buildHeader() -> Widget { + Row(mainAxisAlignment: .spaceBetween) { + Column(crossAxisAlignment: .start, spacing: 4) { + Text("Display Manager") + .textStyle( + TextStyle( + fontSize: 24, + fontWeight: .bold + ) + ) + Text( + "\(state.displays.count) display\(state.displays.count == 1 ? "" : "s") detected" + ) + .textStyle( + TextStyle( + color: Color(0xFF66_6666), + fontSize: 14 + ) + ) + } + + Button { + self.updateDisplayInfo() + } child: { + Text("Refresh") + } + } + .padding(EdgeInsets.all(16)) + .boxDecoration(color: Color(0xFFFF_FFFF), borderRadius: .circular(8)) + } + + private func buildDisplaysList() -> Widget { + Column(crossAxisAlignment: .stretch, spacing: 8) { + SectionHeader("Displays") + + if state.displays.isEmpty { + Expanded { + Text("No displays found") + .textStyle( + TextStyle( + color: Color(0xFF99_9999), + fontSize: 14 + ) + ) + .center() + } + } else { + Expanded { + ListView { + for display in state.displays { + buildDisplayCard(display) + .padding(EdgeInsets.only(bottom: 8)) + } + } + } + } + } + .padding(EdgeInsets.all(16)) + .boxDecoration(color: Color(0xFFFF_FFFF), borderRadius: .circular(8)) + } + + private func buildDisplayCard(_ display: Display) -> Widget { + Column(crossAxisAlignment: .stretch, spacing: 12) { + // Display name and primary indicator + Row(mainAxisAlignment: .spaceBetween) { + Text(display.name) + .textStyle( + TextStyle( + fontSize: 16, + fontWeight: .bold + ) + ) + if display.isPrimary { + Text("PRIMARY") + .textStyle( + TextStyle( + color: Color(0xFFFF_FFFF), + fontSize: 10, + fontWeight: .w600 + ) + ) + .padding(EdgeInsets.symmetric(vertical: 4, horizontal: 8)) + .boxDecoration(color: Color(0xFF4C_AF50), borderRadius: .circular(4)) + } + + // Display ID + PropertyRow(label: "ID", value: display.id) + + HorizontalDivider() + + // Resolution and geometry + SectionHeader("Geometry") + Column(spacing: 4) { + PropertyRow( + label: "Resolution", + value: SharedHelpers.formatSize(display.size) + ) + PropertyRow( + label: "Position", + value: SharedHelpers.formatPoint(display.position) + ) + PropertyRow( + label: "Work Area Size", + value: SharedHelpers.formatSize( + Size( + width: display.workArea.width, + height: display.workArea.height + ) + ) + ) + PropertyRow( + label: "Work Area Position", + value: "(\(Int(display.workArea.left)), \(Int(display.workArea.top)))" + ) + } + + HorizontalDivider() + + // Hardware properties + SectionHeader("Hardware") + Column(spacing: 4) { + PropertyRow( + label: "Scale Factor", + value: String(format: "%.2f×", display.scaleFactor) + ) + PropertyRow( + label: "Refresh Rate", + value: "\(display.refreshRate) Hz" + ) + PropertyRow( + label: "Bit Depth", + value: "\(display.bitDepth) bit" + ) + PropertyRow( + label: "Orientation", + value: orientationString(display.orientation) + ) + } + } + } + .padding(EdgeInsets.all(12)) + .boxDecoration( + color: display.isPrimary ? Color(0xFFE8_F5E9) : Color(0xFFF5_F5F5), + borderRadius: .circular(8) + ) + } + + private func buildCursorInfo() -> Widget { + Column(crossAxisAlignment: .stretch, spacing: 8) { + SectionHeader("Cursor Position") + + Column(spacing: 8) { + InfoCard( + title: "X", + value: String(format: "%.1f", state.cursorPosition.x) + ) + InfoCard( + title: "Y", + value: String(format: "%.1f", state.cursorPosition.y) + ) + } + + Text("Updates in real-time") + .textStyle( + TextStyle( + color: Color(0xFF99_9999), + fontSize: 11 + ) + ) + .padding(EdgeInsets.only(top: 4)) + } + .padding(EdgeInsets.all(16)) + .boxDecoration(color: Color(0xFFFF_FFFF), borderRadius: .circular(8)) + } + + private func buildCurrentWindowInfo() -> Widget { + Column(crossAxisAlignment: .stretch, spacing: 8) { + SectionHeader("Current Window") + + if let window = state.currentWindow { + Column(spacing: 8) { + PropertyRow( + label: "ID", + value: String(window.id) + ) + PropertyRow( + label: "Title", + value: window.title.isEmpty ? "(No title)" : window.title + ) + PropertyRow( + label: "Size", + value: SharedHelpers.formatSize(window.size) + ) + PropertyRow( + label: "Position", + value: SharedHelpers.formatPoint(window.position) + ) + PropertyRow( + label: "Focused", + value: SharedHelpers.formatBool(window.isFocused) + ) + } + } else { + Text("No window focused") + .textStyle( + TextStyle( + color: Color(0xFF99_9999), + fontSize: 13 + ) + ) + .padding(EdgeInsets.symmetric(vertical: 16)) + .center() + } + } + .padding(EdgeInsets.all(16)) + .boxDecoration(color: Color(0xFFFF_FFFF), borderRadius: .circular(8)) + } + + private func orientationString(_ orientation: DisplayOrientation) -> String { + switch orientation { + case .portrait: + return "Portrait" + case .landscape: + return "Landscape" + case .portraitFlipped: + return "Portrait (Flipped)" + case .landscapeFlipped: + return "Landscape (Flipped)" + } + } +} diff --git a/Examples/ExampleApp/Sources/MenuDemo.swift b/Examples/ExampleApp/Sources/MenuDemo.swift new file mode 100644 index 0000000..136c3ac --- /dev/null +++ b/Examples/ExampleApp/Sources/MenuDemo.swift @@ -0,0 +1,367 @@ +import Foundation +import NativeAPI +import Observation +import Shaft + +@Observable +class MenuState { + var itemCount: Int = 0 + var checkboxState: Bool = false + var radioSelection: String = "Option 1" + var eventHistory: [String] = [] + + func addEvent(_ message: String) { + let timestamp = SharedHelpers.formatTimestamp() + eventHistory.insert("[\(timestamp)] \(message)", at: 0) + if eventHistory.count > 20 { + eventHistory.removeLast() + } + } +} + +final class MenuDemo: StatefulWidget { + func createState() -> MenuDemoState { + MenuDemoState() + } +} + +final class MenuDemoState: State { + let state = MenuState() + var menu: Menu? + var menuItems: [MenuItem] = [] + + // Menu item references for state management + var checkboxItem: MenuItem? + var radio1: MenuItem? + var radio2: MenuItem? + var radio3: MenuItem? + var submenu: Menu? + var submenuItem: MenuItem? + + override func initState() { + super.initState() + setupMenu() + state.addEvent("Menu system initialized") + } + + override func dispose() { + menu?.dispose() + for item in menuItems { + item.dispose() + } + super.dispose() + } + + private func setupMenu() { + menu = Menu() + guard let menu = menu else { return } + + // Listen to menu events + _ = menu.onOpened { [weak self] event in + self?.state.addEvent("Menu opened (ID: \(event.menuId))") + } + + _ = menu.onClosed { [weak self] event in + self?.state.addEvent("Menu closed (ID: \(event.menuId))") + } + + // 1. Normal menu item + let normalItem = MenuItem("Normal Menu Item", type: .normal) + _ = normalItem.onClicked { [weak self] event in + self?.state.addEvent("Normal item clicked (ID: \(event.menuItemId))") + } + menu.addItem(normalItem) + menuItems.append(normalItem) + + // 2. Separator + menu.addSeparator() + + // 3. Checkbox item + checkboxItem = MenuItem("Checkbox Item", type: .checkbox) + checkboxItem?.state = .unchecked + _ = checkboxItem?.onClicked { [weak self] event in + guard let self = self else { return } + self.state.checkboxState.toggle() + self.checkboxItem?.state = self.state.checkboxState ? .checked : .unchecked + self.state.addEvent("Checkbox clicked - State: \(self.state.checkboxState)") + } + if let checkboxItem = checkboxItem { + menu.addItem(checkboxItem) + menuItems.append(checkboxItem) + } + + // 4. Radio items + radio1 = MenuItem("Radio Option 1", type: .radio) + radio1?.radioGroup = 1 + radio1?.state = .checked + _ = radio1?.onClicked { [weak self] event in + guard let self = self else { return } + self.state.radioSelection = "Option 1" + self.radio1?.state = .checked + self.radio2?.state = .unchecked + self.radio3?.state = .unchecked + self.state.addEvent("Radio Option 1 selected") + } + if let radio1 = radio1 { + menu.addItem(radio1) + menuItems.append(radio1) + } + + radio2 = MenuItem("Radio Option 2", type: .radio) + radio2?.radioGroup = 1 + radio2?.state = .unchecked + _ = radio2?.onClicked { [weak self] event in + guard let self = self else { return } + self.state.radioSelection = "Option 2" + self.radio1?.state = .unchecked + self.radio2?.state = .checked + self.radio3?.state = .unchecked + self.state.addEvent("Radio Option 2 selected") + } + if let radio2 = radio2 { + menu.addItem(radio2) + menuItems.append(radio2) + } + + radio3 = MenuItem("Radio Option 3", type: .radio) + radio3?.radioGroup = 1 + radio3?.state = .unchecked + _ = radio3?.onClicked { [weak self] event in + guard let self = self else { return } + self.state.radioSelection = "Option 3" + self.radio1?.state = .unchecked + self.radio2?.state = .unchecked + self.radio3?.state = .checked + self.state.addEvent("Radio Option 3 selected") + } + if let radio3 = radio3 { + menu.addItem(radio3) + menuItems.append(radio3) + } + + // 5. Separator + menu.addSeparator() + + // 6. Menu item with tooltip + let tooltipItem = MenuItem("Item with Tooltip", type: .normal) + tooltipItem.tooltip = "This is a helpful tooltip" + _ = tooltipItem.onClicked { [weak self] event in + self?.state.addEvent("Tooltip item clicked") + } + menu.addItem(tooltipItem) + menuItems.append(tooltipItem) + + // 7. Submenu + submenu = Menu() + submenuItem = MenuItem("Submenu", type: .submenu) + + if let submenu = submenu, let submenuItem = submenuItem { + let subItem1 = MenuItem("Submenu Item 1") + _ = subItem1.onClicked { [weak self] event in + self?.state.addEvent("Submenu Item 1 clicked") + } + submenu.addItem(subItem1) + + let subItem2 = MenuItem("Submenu Item 2") + _ = subItem2.onClicked { [weak self] event in + self?.state.addEvent("Submenu Item 2 clicked") + } + submenu.addItem(subItem2) + + submenuItem.submenu = submenu + menu.addItem(submenuItem) + menuItems.append(submenuItem) + } + + state.itemCount = menu.itemCount + } + + private func showMenu() { + menu?.open(PositioningStrategy.cursorPosition()) + state.addEvent("Menu opened at cursor position") + } + + private func showMenuAt(x: Double, y: Double) { + menu?.open(PositioningStrategy.absolute(Point(x: x, y: y))) + state.addEvent("Menu opened at (\(Int(x)), \(Int(y)))") + } + + private func addNewItem() { + guard let menu = menu else { return } + let newItem = MenuItem("New Item \(menuItems.count + 1)") + _ = newItem.onClicked { [weak self] event in + self?.state.addEvent("New item clicked") + } + menu.addItem(newItem) + menuItems.append(newItem) + state.itemCount = menu.itemCount + state.addEvent("Added new menu item") + } + + private func removeFirstItem() { + guard let menu = menu, !menuItems.isEmpty else { return } + let item = menuItems.removeFirst() + let _ = menu.removeItem(item) + state.itemCount = menu.itemCount + state.addEvent("Removed first menu item") + } + + private func setCheckboxMixed() { + checkboxItem?.state = .mixed + state.addEvent("Checkbox state set to Mixed") + } + + override func build(context: BuildContext) -> Widget { + Column(crossAxisAlignment: .stretch, spacing: 16) { + // Header + buildHeader() + + // Content area + Expanded { + Row(spacing: 16) { + // Demo area and controls (2/3 width) + Expanded(flex: 2) { + Column(spacing: 16) { + buildDemoArea() + buildControlsArea() + } + } + + // Event history (1/3 width) + Expanded(flex: 1) { + buildEventHistory() + } + } + } + } + .padding(.all(16)) + } + + private func buildHeader() -> Widget { + Row(mainAxisAlignment: .spaceBetween) { + Column(crossAxisAlignment: .start, spacing: 4) { + Text("Menu System") + .textStyle( + TextStyle( + fontSize: 24, + fontWeight: .bold + ) + ) + Text("\(state.itemCount) menu items") + .textStyle( + TextStyle( + color: Color(0xFF66_6666), + fontSize: 14 + ) + ) + } + } + .padding(EdgeInsets.all(16)) + .boxDecoration(color: Color(0xFFFF_FFFF), borderRadius: .circular(8)) + } + + private func buildDemoArea() -> Widget { + Column(crossAxisAlignment: .stretch, spacing: 12) { + SectionHeader("Demo Area") + + Text("Click the button to show the context menu") + .textStyle( + TextStyle( + color: Color(0xFF66_6666), + fontSize: 13 + ) + ) + + Button { + self.showMenu() + } child: { + Text("Show Menu at Cursor") + } + .padding(EdgeInsets.symmetric(vertical: 8)) + + Row(spacing: 8) { + Button { + self.showMenuAt(x: 100, y: 100) + } child: { + Text("Show at (100, 100)") + } + + Button { + self.showMenuAt(x: 300, y: 200) + } child: { + Text("Show at (300, 200)") + } + } + + HorizontalDivider() + + // Menu state display + Column(spacing: 4) { + PropertyRow( + label: "Item Count", + value: "\(state.itemCount)" + ) + PropertyRow( + label: "Checkbox State", + value: "\(state.checkboxState)" + ) + PropertyRow( + label: "Radio Selection", + value: state.radioSelection + ) + } + } + .padding(EdgeInsets.all(16)) + .boxDecoration(color: Color(0xFFFF_FFFF), borderRadius: .circular(8)) + } + + private func buildControlsArea() -> Widget { + Column(crossAxisAlignment: .stretch, spacing: 12) { + SectionHeader("Controls") + + Text("Manipulate menu items dynamically") + .textStyle( + TextStyle( + color: Color(0xFF66_6666), + fontSize: 13 + ) + ) + + Row(spacing: 8) { + Button { + self.addNewItem() + } child: { + Text("Add Item") + } + + Button { + self.removeFirstItem() + } child: { + Text("Remove First") + } + } + + Row(spacing: 8) { + Button { + self.setCheckboxMixed() + } child: { + Text("Set Checkbox Mixed") + } + } + } + .padding(EdgeInsets.all(16)) + .boxDecoration(color: Color(0xFFFF_FFFF), borderRadius: .circular(8)) + } + + private func buildEventHistory() -> Widget { + Column(crossAxisAlignment: .stretch, spacing: 8) { + SectionHeader("Event History") + + Expanded { + EventHistoryView(events: state.eventHistory) + } + } + .padding(EdgeInsets.all(16)) + .boxDecoration(color: Color(0xFFFF_FFFF), borderRadius: .circular(8)) + } +} diff --git a/Examples/ExampleApp/Sources/SharedComponents.swift b/Examples/ExampleApp/Sources/SharedComponents.swift new file mode 100644 index 0000000..ee9a425 --- /dev/null +++ b/Examples/ExampleApp/Sources/SharedComponents.swift @@ -0,0 +1,166 @@ +import Foundation +import NativeAPI +import Observation +import Shaft + +// MARK: - Reusable UI Components + +/// A card widget that displays key-value information +final class InfoCard: StatelessWidget { + init(title: String, value: String) { + self.title = title + self.value = value + } + + let title: String + let value: String + + func build(context: BuildContext) -> Widget { + Column(crossAxisAlignment: .start, spacing: 4) { + Text(title) + .textStyle( + TextStyle( + color: Color(0xFF66_6666), + fontSize: 12 + ) + ) + Text(value) + .textStyle( + TextStyle( + color: Color(0xFF00_0000), + fontSize: 14, + fontWeight: .w600 + ) + ) + } + .padding(EdgeInsets.all(12)) + .boxDecoration(color: Color(0xFFF5_F5F5), borderRadius: .circular(6)) + } +} + +/// A row that displays a property name and value +final class PropertyRow: StatelessWidget { + init(label: String, value: String) { + self.label = label + self.value = value + } + + let label: String + let value: String + + func build(context: BuildContext) -> Widget { + Row(mainAxisAlignment: .spaceBetween) { + Text(label) + .textStyle( + TextStyle( + color: Color(0xFF66_6666), + fontSize: 13 + ) + ) + Text(value) + .textStyle( + TextStyle( + color: Color(0xFF00_0000), + fontSize: 13, + fontWeight: .w500 + ) + ) + } + .padding(EdgeInsets.symmetric(vertical: 8)) + } +} + +/// A styled section header +final class SectionHeader: StatelessWidget { + init(_ title: String) { + self.title = title + } + + let title: String + + func build(context: BuildContext) -> Widget { + Text(title) + .textStyle( + TextStyle( + color: Color(0xFF00_0000), + fontSize: 16, + fontWeight: .bold + ) + ) + .padding(EdgeInsets.only(top: 16, bottom: 8)) + } +} + +/// A view that displays an event history log +final class EventHistoryView: StatelessWidget { + init(events: [String], maxEvents: Int = 20) { + self.events = events + self.maxEvents = maxEvents + } + + let events: [String] + let maxEvents: Int + + func build(context: BuildContext) -> Widget { + if events.isEmpty { + return Text("No events yet") + .textStyle( + TextStyle( + color: Color(0xFF99_9999), + fontSize: 13 + ) + ) + .center() + .constrained(height: 100.0) + .boxDecoration(color: Color(0xFFF5_F5F5), borderRadius: .circular(6)) + } + + return ListView { + for event in events.prefix(maxEvents) { + Text(event) + .textStyle( + TextStyle( + color: Color(0xFF33_3333), + fontFamily: "monospace", + fontSize: 11, + ) + ) + .padding(EdgeInsets.all(8)) + .boxDecoration(color: Color(0xFFFF_FFFF), borderRadius: .circular(4)) + .padding(EdgeInsets.only(bottom: 4)) + } + } + .padding(EdgeInsets.all(12)) + .boxDecoration(color: Color(0xFFF5_F5F5), borderRadius: .circular(6)) + } +} + +// MARK: - Formatting Helpers + +extension SharedHelpers { + /// Format a size as "width × height" + static func formatSize(_ size: NativeAPI.Size) -> String { + return "\(Int(size.width)) × \(Int(size.height))" + } + + /// Format a point as "(x, y)" + static func formatPoint(_ point: NativeAPI.Point) -> String { + return "(\(Int(point.x)), \(Int(point.y)))" + } + + /// Format a timestamp for event logs + static func formatTimestamp(_ date: Date = Date()) -> String { + let formatter = DateFormatter() + formatter.dateFormat = "HH:mm:ss" + return formatter.string(from: date) + } + + /// Format a boolean as "Yes" or "No" + static func formatBool(_ value: Bool) -> String { + return value ? "Yes" : "No" + } +} + +enum SharedHelpers { + // Namespace for helper functions +} diff --git a/Examples/ExampleApp/Sources/WindowManagerDemo.swift b/Examples/ExampleApp/Sources/WindowManagerDemo.swift new file mode 100644 index 0000000..21b11c3 --- /dev/null +++ b/Examples/ExampleApp/Sources/WindowManagerDemo.swift @@ -0,0 +1,293 @@ +import Foundation +import NativeAPI +import Observation +import Shaft + +@Observable +class WindowManagerState { + var windows: [Window] = [] + var eventHistory: [String] = [] + var selectedWindowId: Int? + var lastUpdate: Date = Date() + + func addEvent(_ message: String) { + let timestamp = SharedHelpers.formatTimestamp() + eventHistory.insert("[\(timestamp)] \(message)", at: 0) + if eventHistory.count > 20 { + eventHistory.removeLast() + } + } +} + +final class WindowManagerDemo: StatefulWidget { + func createState() -> WindowManagerDemoState { + WindowManagerDemoState() + } +} + +final class WindowManagerDemoState: State { + let state = WindowManagerState() + var eventCallbackId: Int? + var updateTimer: Foundation.Timer? + + override func initState() { + super.initState() + + // Register for window events + eventCallbackId = WindowManager.shared.registerEventCallback { [weak self] event in + guard let self = self else { return } + self.handleWindowEvent(event) + } + + // Start update timer + updateTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { + [weak self] _ in + self?.updateWindows() + } + + // Initial load + updateWindows() + state.addEvent("Window Manager initialized") + } + + override func dispose() { + if let callbackId = eventCallbackId { + WindowManager.shared.unregisterEventCallback(callbackId) + } + updateTimer?.invalidate() + super.dispose() + } + + private func handleWindowEvent(_ event: WindowEvent) { + switch event.type { + case .created: + state.addEvent("Window created (ID: \(event.windowId))") + case .closed: + state.addEvent("Window closed (ID: \(event.windowId))") + case .focused: + state.addEvent("Window focused (ID: \(event.windowId))") + case .blurred: + state.addEvent("Window blurred (ID: \(event.windowId))") + case .minimized: + state.addEvent("Window minimized (ID: \(event.windowId))") + case .maximized: + state.addEvent("Window maximized (ID: \(event.windowId))") + case .restored: + state.addEvent("Window restored (ID: \(event.windowId))") + case .moved(let position): + state.addEvent( + "Window moved to \(SharedHelpers.formatPoint(position)) (ID: \(event.windowId))" + ) + case .resized(let size): + state.addEvent( + "Window resized to \(SharedHelpers.formatSize(size)) (ID: \(event.windowId))" + ) + } + updateWindows() + } + + private func updateWindows() { + let windowList = WindowManager.shared.getAll() + state.windows = windowList.windows + state.lastUpdate = Date() + } + + override func build(context: BuildContext) -> Widget { + Column(crossAxisAlignment: .stretch, spacing: 16) { + // Header + buildHeader() + + // Content area with windows list and event history + Expanded { + Row(spacing: 16) { + // Windows list (2/3 width) + Expanded(flex: 2) { + buildWindowsList() + } + + // Event history (1/3 width) + Expanded(flex: 1) { + buildEventHistory() + } + } + } + } + .padding(.all(16)) + } + + private func buildHeader() -> Widget { + Row(mainAxisAlignment: .spaceBetween) { + Column(crossAxisAlignment: .start, spacing: 4) { + Text("Window Manager") + .textStyle( + TextStyle( + fontSize: 24, + fontWeight: .bold + ) + ) + Text("\(state.windows.count) window\(state.windows.count == 1 ? "" : "s") detected") + .textStyle( + TextStyle( + color: Color(0xFF66_6666), + fontSize: 14 + ) + ) + } + + Button { + self.updateWindows() + self.state.addEvent("Manual refresh triggered") + } child: { + Text("Refresh") + } + } + .padding(EdgeInsets.all(16)) + .boxDecoration(color: Color(0xFFFF_FFFF), borderRadius: .circular(8)) + } + + private func buildWindowsList() -> Widget { + Column(crossAxisAlignment: .stretch, spacing: 8) { + SectionHeader("Windows") + + if state.windows.isEmpty { + Expanded { + Text("No windows found") + .textStyle( + TextStyle( + color: Color(0xFF99_9999), + fontSize: 14 + ) + ) + .center() + } + } else { + Expanded { + ListView { + for window in state.windows { + buildWindowCard(window) + .padding(EdgeInsets.only(bottom: 8)) + } + } + } + } + } + .padding(EdgeInsets.all(16)) + .boxDecoration(color: Color(0xFFFF_FFFF), borderRadius: .circular(8)) + } + + private func buildWindowCard(_ window: Window) -> Widget { + let isSelected = state.selectedWindowId == window.id + + return GestureDetector( + onTap: { [self] in + state.selectedWindowId = window.id + state.addEvent("Selected window \(window.id)") + } + ) { + Column(crossAxisAlignment: .stretch, spacing: 8) { + // Title and ID + Row(mainAxisAlignment: .spaceBetween) { + Text(window.title.isEmpty ? "Window \(window.id)" : window.title) + .textStyle( + TextStyle( + fontSize: 16, + fontWeight: .bold + ) + ) + Text("#\(window.id)") + .textStyle( + TextStyle( + color: Color(0xFF99_9999), + fontSize: 12 + ) + ) + } + + // State indicators + Row(spacing: 8) { + if window.isFocused { + buildBadge("Focused", Color(0xFF4C_AF50)) + } + if window.isMinimized { + buildBadge("Minimized", Color(0xFFFF_9800)) + } + if window.isMaximized { + buildBadge("Maximized", Color(0xFF21_96F3)) + } + if window.isFullscreen { + buildBadge("Fullscreen", Color(0xFF9C_27B0)) + } + if !window.isVisible { + buildBadge("Hidden", Color(0xFF75_7575)) + } + } + + HorizontalDivider() + + // Geometry info + Column(spacing: 4) { + PropertyRow( + label: "Size", + value: SharedHelpers.formatSize(window.size) + ) + PropertyRow( + label: "Position", + value: SharedHelpers.formatPoint(window.position) + ) + PropertyRow( + label: "Opacity", + value: String(format: "%.2f", window.opacity) + ) + } + + HorizontalDivider() + + // Properties + Column(spacing: 4) { + PropertyRow( + label: "Resizable", + value: SharedHelpers.formatBool(window.isResizable) + ) + PropertyRow( + label: "Movable", + value: SharedHelpers.formatBool(window.isMovable) + ) + PropertyRow( + label: "Always on Top", + value: SharedHelpers.formatBool(window.isAlwaysOnTop) + ) + } + } + } + .padding(EdgeInsets.all(12)) + .boxDecoration( + color: isSelected ? Color(0xFFE3_F2FD) : Color(0xFFF5_F5F5), + borderRadius: .circular(8) + ) + } + + private func buildBadge(_ text: String, _ color: Color) -> Widget { + Text(text) + .textStyle( + TextStyle( + color: Color(0xFFFF_FFFF), + fontSize: 10, + fontWeight: .w600 + ) + ) + .padding(EdgeInsets.symmetric(vertical: 4, horizontal: 8)) + .boxDecoration(color: color, borderRadius: .circular(4)) + } + + private func buildEventHistory() -> Widget { + Column(crossAxisAlignment: .stretch, spacing: 8) { + SectionHeader("Event History") + + Expanded { + EventHistoryView(events: state.eventHistory) + } + } + .padding(EdgeInsets.all(16)) + .boxDecoration(color: Color(0xFFFF_FFFF), borderRadius: .circular(8)) + } +} diff --git a/Examples/ExampleApp/Sources/main.swift b/Examples/ExampleApp/Sources/main.swift new file mode 100644 index 0000000..67b5ca4 --- /dev/null +++ b/Examples/ExampleApp/Sources/main.swift @@ -0,0 +1,103 @@ +import Foundation +import NativeAPI +import Observation +import Shaft +import ShaftSetup + +// Use the default backend +ShaftSetup.useDefault() + +// Enable hot reloading +#if DEBUG && !os(Windows) + import SwiftReload + LocalSwiftReloader(onReload: backend.scheduleReassemble).start() +#endif + +runApp( + NativeAPIDemo() +) + +final class NativeAPIDemo: StatefulWidget { + func createState() -> NativeAPIDemoState { + NativeAPIDemoState() + } +} + +final class NativeAPIDemoState: State { + let pageByTitle: [String: Widget] = [ + "Window Manager": WindowManagerDemo(), + "Display Manager": DisplayManagerDemo(), + "Menu System": MenuDemo(), + ] + + lazy var selectedPage = ValueNotifier("Window Manager") + + override func initState() { + super.initState() + updateTitle() + selectedPage.addListener(self, callback: handleSelectedPageChanged) + } + + override func dispose() { + selectedPage.removeListener(self) + super.dispose() + } + + private func handleSelectedPageChanged() { + updateTitle() + } + + private func updateTitle() { + View.maybeOf(context)?.title = "NativeAPI Demo - \(selectedPage.wrappedValue)" + } + + override func build(context: BuildContext) -> Widget { + NavigationSplitView { + // Sidebar navigation + FixedListView(selection: selectedPage) { + Section { + Text("Features") + .textStyle( + TextStyle( + color: Color(0xFF66_6666), + fontSize: 14, + fontWeight: .bold, + ) + ) + .padding(.all(12)) + } content: { + MenuTile("Window Manager") + MenuTile("Display Manager") + MenuTile("Menu System") + } + } + } detail: { + // Detail view showing selected demo + let page = pageByTitle[selectedPage.wrappedValue] + page + ?? Text("Under construction") + .center() + .padding(EdgeInsets.all(20)) + } + } +} + +final class MenuTile: StatelessWidget { + init(_ title: String) { + self.title = title + } + + let title: String + + func build(context: BuildContext) -> Widget { + ListTile(title) { + Text(title) + .textStyle( + TextStyle( + color: Color(0xFF00_0000), + fontSize: 14, + ) + ) + } + } +}