From 5bde68f825083f3906b00c87a15a0ba5908d3593 Mon Sep 17 00:00:00 2001 From: Lilith Crook Date: Mon, 19 Jan 2026 21:09:28 -0700 Subject: [PATCH 1/7] feat(platform): add Win32 and Cocoa native platform backends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement native GUI backends for Windows and macOS: Win32 (1,780 lines): - FFI declarations for 35+ Win32 API functions - Window class registration and message loop - Custom layout manager (Win32 has no auto-layout) - All NativeApp, NativeWidgetBuilder, NativeLayout trait methods - 22 unit tests covering structures, constants, string conversion Cocoa (1,519 lines): - Objective-C runtime bridge (objc_msgSend, selectors, etc.) - ObjC helper struct for message passing - NSApplication lifecycle and NSWindow management - NSStackView-based layout with Auto Layout integration - 32 unit tests covering NSRect, NSPoint, NSSize, constants Also includes: - GTK4 reference implementation (690 lines) - Shared types and traits in mod.sigil ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/platform/native/cocoa.sigil | 1519 ++++++++++++++++++++++++++ src/platform/native/gtk4.sigil | 689 ++++++++++++ src/platform/native/mod.sigil | 508 +++++++++ src/platform/native/win32.sigil | 1780 +++++++++++++++++++++++++++++++ 4 files changed, 4496 insertions(+) create mode 100644 src/platform/native/cocoa.sigil create mode 100644 src/platform/native/gtk4.sigil create mode 100644 src/platform/native/mod.sigil create mode 100644 src/platform/native/win32.sigil diff --git a/src/platform/native/cocoa.sigil b/src/platform/native/cocoa.sigil new file mode 100644 index 0000000..59817fc --- /dev/null +++ b/src/platform/native/cocoa.sigil @@ -0,0 +1,1519 @@ +// Cocoa Platform Module +// macOS native GUI backend via AppKit/Cocoa API + +use std::collections::HashMap; +use crate::core::vdom::{VNode, Patch}; +use crate::core::events::{Event, EventType}; +use crate::platform::{Platform, DomElement, FetchOptions, FetchResponse, Storage, StorageType}; +use super::{NativeWidget, WidgetType, NativeApp, NativeWidgetBuilder, NativeLayout, Orientation, Align}; + +// ============================================================================ +// Cocoa FFI Declarations +// ============================================================================ + +#[cfg(target_os = "macos")] +#[link("AppKit", framework)] +#[link("Foundation", framework)] +#[link("objc")] +extern "C" { + // Objective-C runtime + rite objc_getClass(name: *c_char) โ†’ *void; + rite objc_allocateClassPair(superclass: *void, name: *c_char, extraBytes: usize) โ†’ *void; + rite objc_registerClassPair(cls: *void); + rite class_addMethod(cls: *void, name: *void, imp: *void, types: *c_char) โ†’ bool; + rite object_getClass(obj: *void) โ†’ *void; + + // Selectors + rite sel_registerName(name: *c_char) โ†’ *void; + rite sel_getName(sel: *void) โ†’ *c_char; + + // Message sending + rite objc_msgSend(obj: *void, sel: *void, ...) โ†’ *void; + rite objc_msgSend_stret(ret: *void, obj: *void, sel: *void, ...); + rite objc_msgSend_fpret(obj: *void, sel: *void, ...) โ†’ f64; + + // Property access + rite objc_getAssociatedObject(obj: *void, key: *void) โ†’ *void; + rite objc_setAssociatedObject(obj: *void, key: *void, value: *void, policy: u32); + + // Memory + rite malloc(size: usize) โ†’ *void; + rite free(ptr: *void); + + // Core Foundation + rite CFStringCreateWithCString(allocator: *void, cStr: *c_char, encoding: u32) โ†’ *void; + rite CFRelease(cf: *void); +} + +// ============================================================================ +// Objective-C Constants +// ============================================================================ + +#[cfg(target_os = "macos")] +const OBJC_ASSOCIATION_RETAIN_NONATOMIC: u32 = 1; +const kCFStringEncodingUTF8: u32 = 0x08000100; + +// NSWindow style masks +const NSWindowStyleMaskTitled: u64 = 1 << 0; +const NSWindowStyleMaskClosable: u64 = 1 << 1; +const NSWindowStyleMaskMiniaturizable: u64 = 1 << 2; +const NSWindowStyleMaskResizable: u64 = 1 << 3; +const NSWindowStyleMaskFullSizeContentView: u64 = 1 << 15; + +// NSBackingStoreType +const NSBackingStoreBuffered: u64 = 2; + +// NSStackView orientation +const NSUserInterfaceLayoutOrientationHorizontal: i64 = 0; +const NSUserInterfaceLayoutOrientationVertical: i64 = 1; + +// NSStackView distribution +const NSStackViewDistributionFillEqually: i64 = 1; + +// NSProgressIndicator style +const NSProgressIndicatorStyleBar: u64 = 0; +const NSProgressIndicatorStyleSpinning: u64 = 1; + +// NSButton types +const NSButtonTypeMomentaryPushIn: u64 = 7; +const NSButtonTypeSwitch: u64 = 3; + +// NSTextAlignment +const NSTextAlignmentLeft: u64 = 0; +const NSTextAlignmentCenter: u64 = 1; +const NSTextAlignmentRight: u64 = 2; + +// Layout priorities +const NSLayoutPriorityRequired: f32 = 1000.0; +const NSLayoutPriorityDefaultHigh: f32 = 750.0; +const NSLayoutPriorityDefaultLow: f32 = 250.0; + +// ============================================================================ +// NSRect Structure +// ============================================================================ + +#[cfg(target_os = "macos")] +#[repr(C)] +sigil NSPoint { + x: f64, + y: f64 +} + +#[cfg(target_os = "macos")] +#[repr(C)] +sigil NSSize { + width: f64, + height: f64 +} + +#[cfg(target_os = "macos")] +#[repr(C)] +sigil NSRect { + origin: NSPoint, + size: NSSize +} + +โŠข NSRect { + rite new(x: f64, y: f64, width: f64, height: f64) โ†’ This! { + NSRect { + origin: NSPoint { x, y }, + size: NSSize { width, height } + } + } + + rite zero() โ†’ This! { + NSRect::new(0.0, 0.0, 0.0, 0.0) + } +} + +// ============================================================================ +// Objective-C Runtime Helpers +// ============================================================================ + +#[cfg(target_os = "macos")] +sigil ObjC; + +โŠข ObjC { + /// Get class by name + rite class(name: &str) โ†’ *void! { + unsafe { + โ‰” cname! = name.as_ptr() as *c_char; + objc_getClass(cname) + } + } + + /// Get selector by name + rite sel(name: &str) โ†’ *void! { + unsafe { + โ‰” cname! = name.as_ptr() as *c_char; + sel_registerName(cname) + } + } + + /// Create NSString from Rust string + rite string(s: &str) โ†’ *void! { + unsafe { + โ‰” cls = ObjC::class("NSString\0"); + โ‰” sel_alloc = ObjC::sel("alloc\0"); + โ‰” sel_init = ObjC::sel("initWithUTF8String:\0"); + + โ‰” obj = objc_msgSend(cls, sel_alloc); + objc_msgSend(obj, sel_init, s.as_ptr() as *c_char) + } + } + + /// Send message with no args, returns id + rite msg0(obj: *void, sel_name: &str) โ†’ *void! { + unsafe { + โ‰” sel = ObjC::sel(sel_name); + objc_msgSend(obj, sel) + } + } + + /// Send message with one pointer arg + rite msg1(obj: *void, sel_name: &str, arg1: *void) โ†’ *void! { + unsafe { + โ‰” sel = ObjC::sel(sel_name); + objc_msgSend(obj, sel, arg1) + } + } + + /// Send message with one i64 arg + rite msg1_i64(obj: *void, sel_name: &str, arg1: i64) โ†’ *void! { + unsafe { + โ‰” sel = ObjC::sel(sel_name); + objc_msgSend(obj, sel, arg1) + } + } + + /// Send message with one f64 arg + rite msg1_f64(obj: *void, sel_name: &str, arg1: f64) โ†’ *void! { + unsafe { + โ‰” sel = ObjC::sel(sel_name); + objc_msgSend(obj, sel, arg1) + } + } + + /// Send message with one bool arg + rite msg1_bool(obj: *void, sel_name: &str, arg1: bool) โ†’ *void! { + unsafe { + โ‰” sel = ObjC::sel(sel_name); + objc_msgSend(obj, sel, arg1 as i32) + } + } + + /// Send message with two pointer args + rite msg2(obj: *void, sel_name: &str, arg1: *void, arg2: *void) โ†’ *void! { + unsafe { + โ‰” sel = ObjC::sel(sel_name); + objc_msgSend(obj, sel, arg1, arg2) + } + } + + /// Alloc + init pattern + rite alloc_init(class_name: &str) โ†’ *void! { + unsafe { + โ‰” cls = ObjC::class(class_name); + โ‰” obj = ObjC::msg0(cls, "alloc\0"); + ObjC::msg0(obj, "init\0") + } + } +} + +// ============================================================================ +// Layout Info for Auto Layout +// ============================================================================ + +#[cfg(target_os = "macos")] +sigil LayoutInfo { + hexpand: bool, + vexpand: bool, + halign: Align, + valign: Align, + margin_top: i32, + margin_bottom: i32, + margin_start: i32, + margin_end: i32, + orientation: Orientation +} + +โŠข LayoutInfo { + rite default() โ†’ This! { + LayoutInfo { + hexpand: โŠฅ, + vexpand: โŠฅ, + halign: Align::Fill, + valign: Align::Fill, + margin_top: 0, + margin_bottom: 0, + margin_start: 0, + margin_end: 0, + orientation: Orientation::Vertical + } + } +} + +// ============================================================================ +// Cocoa Platform Implementation +// ============================================================================ + +/// Global platform instance pointer for target-action callbacks +#[cfg(target_os = "macos")] +static vary PLATFORM_INSTANCE: *void = 0 as *void; + +/// Target-action callback handler +#[cfg(target_os = "macos")] +extern "C" rite action_handler(this: *void, sel: *void, sender: *void) { + unsafe { + โއ PLATFORM_INSTANCE as usize != 0 { + โ‰” platform = &*(PLATFORM_INSTANCE as *CocoaPlatform); + platform.handle_action(sender); + } + } +} + +/// Cocoa Platform for macOS native GUI +#[cfg(target_os = "macos")] +โ˜‰ sigil CocoaPlatform { + initialized: bool!, + app: *void!, + main_window: *void!, + event_handlers: HashMap!, + layout_info: HashMap!, + next_handler_id: u64!, + widget_to_handler: HashMap!, + timer_map: HashMap!, + action_target: *void! +} + +#[cfg(target_os = "macos")] +โŠข CocoaPlatform { + โ˜‰ rite new() โ†’ This! { + CocoaPlatform { + initialized: โŠฅ, + app: 0 as *void, + main_window: 0 as *void, + event_handlers: HashMap::new(), + layout_info: HashMap::new(), + next_handler_id: 1000, + widget_to_handler: HashMap::new(), + timer_map: HashMap::new(), + action_target: 0 as *void + } + } + + /// Handle target-action callback + rite handle_action(&this, sender: *void) { + โއ let Some(handler_id) = this.widget_to_handler.get(&(sender as usize)) { + โއ let Some(callback) = this.event_handlers.get(handler_id) { + callback(); + } + } + } + + /// Get next handler ID + rite get_next_handler_id(&vary this) โ†’ u64! { + โ‰” id! = this.next_handler_id; + this.next_handler_id = this.next_handler_id + 1; + id + } + + /// Create the action target object + rite create_action_target(&vary this) โ†’ *void! { + unsafe { + // Create a custom class for handling actions + โ‰” superclass = ObjC::class("NSObject\0"); + โ‰” cls = objc_allocateClassPair(superclass, "QliphothActionTarget\0".as_ptr() as *c_char, 0); + + โއ cls as usize != 0 { + // Add the action: method + โ‰” sel = ObjC::sel("action:\0"); + โ‰” types = "v@:@\0".as_ptr() as *c_char; // void, self, SEL, id + class_addMethod(cls, sel, action_handler as *void, types); + objc_registerClassPair(cls); + } + + // Alloc and init an instance + โ‰” target_cls = ObjC::class("QliphothActionTarget\0"); + โއ target_cls as usize == 0 { + // Class already registered, use it + target_cls = ObjC::class("QliphothActionTarget\0"); + } + โ‰” target = ObjC::msg0(target_cls, "alloc\0"); + ObjC::msg0(target, "init\0") + } + } +} + +#[cfg(target_os = "macos")] +โŠข CocoaPlatform : NativeApp { + โ˜‰ rite init(&vary this) โ†’ bool! { + โއ this.initialized { + ret โŠค; + } + + unsafe { + // Set global instance + PLATFORM_INSTANCE = this as *void; + + // Get shared application + โ‰” app_class = ObjC::class("NSApplication\0"); + this.app = ObjC::msg0(app_class, "sharedApplication\0"); + + // Set activation policy (regular app with dock icon) + โ‰” sel = ObjC::sel("setActivationPolicy:\0"); + objc_msgSend(this.app, sel, 0_i64); // NSApplicationActivationPolicyRegular + + // Create action target + this.action_target = this.create_action_target(); + + this.initialized = โŠค; + } + โŠค + } + + โ˜‰ rite run(&this) { + unsafe { + // Activate the app + ObjC::msg1_bool(this.app, "activateIgnoringOtherApps:\0", โŠค); + // Run the event loop + ObjC::msg0(this.app, "run\0"); + } + } + + โ˜‰ rite quit(&this) { + unsafe { + ObjC::msg1(this.app, "terminate:\0", 0 as *void); + } + } + + โ˜‰ rite create_window(&vary this, title: !String, width: !i32, height: !i32) โ†’ NativeWidget! { + unsafe { + // Create window frame + โ‰” frame! = NSRect::new(100.0, 100.0, width as f64, height as f64); + + // Create window: [[NSWindow alloc] initWithContentRect:styleMask:backing:defer:] + โ‰” window_class = ObjC::class("NSWindow\0"); + โ‰” window = ObjC::msg0(window_class, "alloc\0"); + + โ‰” style_mask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | + NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable; + + โ‰” sel = ObjC::sel("initWithContentRect:styleMask:backing:defer:\0"); + window = objc_msgSend(window, sel, frame, style_mask, NSBackingStoreBuffered, โŠฅ as i32); + + โއ window as usize != 0 { + // Set title + โ‰” ns_title = ObjC::string(&title); + ObjC::msg1(window, "setTitle:\0", ns_title); + + // Center window + ObjC::msg0(window, "center\0"); + + // Store as main window + this.main_window = window; + this.layout_info.insert(window as usize, LayoutInfo::default()); + + NativeWidget::new(window as usize, WidgetType::Window) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite show(&this, widget: &NativeWidget) { + unsafe { + โŒฅ widget.widget_type { + WidgetType::Window => { + ObjC::msg1(widget.handle as *void, "makeKeyAndOrderFront:\0", 0 as *void); + } + _ => { + ObjC::msg1_bool(widget.handle as *void, "setHidden:\0", โŠฅ); + } + } + } + } + + โ˜‰ rite hide(&this, widget: &NativeWidget) { + unsafe { + โŒฅ widget.widget_type { + WidgetType::Window => { + ObjC::msg1(widget.handle as *void, "orderOut:\0", 0 as *void); + } + _ => { + ObjC::msg1_bool(widget.handle as *void, "setHidden:\0", โŠค); + } + } + } + } + + โ˜‰ rite set_property(&this, widget: &NativeWidget, name: &str, value: &str) { + unsafe { + โŒฅ (widget.widget_type, name) { + (WidgetType::Label, "text") | (_, "text") | (WidgetType::Button, "label") => { + โ‰” ns_value = ObjC::string(value); + โŒฅ widget.widget_type { + WidgetType::Button => { + ObjC::msg1(widget.handle as *void, "setTitle:\0", ns_value); + } + _ => { + ObjC::msg1(widget.handle as *void, "setStringValue:\0", ns_value); + } + } + } + (WidgetType::ProgressBar, "fraction") => { + โ‰” fraction! = value.parse::().unwrap_or(0.0); + ObjC::msg1_f64(widget.handle as *void, "setDoubleValue:\0", fraction * 100.0); + } + (WidgetType::Scale, "value") => { + โ‰” val! = value.parse::().unwrap_or(0.0); + ObjC::msg1_f64(widget.handle as *void, "setDoubleValue:\0", val); + } + (WidgetType::CheckButton, "active") | (WidgetType::Switch, "active") => { + โ‰” checked! = value == "true" || value == "1"; + ObjC::msg1_i64(widget.handle as *void, "setState:\0", โއ checked { 1 } โމ { 0 }); + } + (_, "visible") => { + โ‰” visible! = value == "true" || value == "1"; + ObjC::msg1_bool(widget.handle as *void, "setHidden:\0", ยฌvisible); + } + (_, "sensitive") | (_, "enabled") => { + โ‰” enabled! = value == "true" || value == "1"; + ObjC::msg1_bool(widget.handle as *void, "setEnabled:\0", enabled); + } + _ => {} + } + } + } + + โ˜‰ rite connect(&vary this, widget: &NativeWidget, signal: &str, handler_id: u64) { + unsafe { + // Set target and action for controls + ObjC::msg1(widget.handle as *void, "setTarget:\0", this.action_target); + โ‰” action_sel = ObjC::sel("action:\0"); + ObjC::msg1(widget.handle as *void, "setAction:\0", action_sel); + + // Map widget to handler + this.widget_to_handler.insert(widget.handle, handler_id); + } + } + + โ˜‰ rite disconnect(&vary this, widget: &NativeWidget, handler_id: u64) { + unsafe { + ObjC::msg1(widget.handle as *void, "setTarget:\0", 0 as *void); + ObjC::msg1(widget.handle as *void, "setAction:\0", 0 as *void); + } + this.widget_to_handler.remove(&widget.handle); + this.event_handlers.remove(&handler_id); + } +} + +#[cfg(target_os = "macos")] +โŠข CocoaPlatform : NativeWidgetBuilder { + โ˜‰ rite create_box(&vary this, orientation: Orientation) โ†’ NativeWidget! { + unsafe { + โ‰” stack = ObjC::alloc_init("NSStackView\0"); + + โއ stack as usize != 0 { + // Set orientation + โ‰” ns_orientation! = โŒฅ orientation { + Orientation::Horizontal => NSUserInterfaceLayoutOrientationHorizontal, + Orientation::Vertical => NSUserInterfaceLayoutOrientationVertical + }; + ObjC::msg1_i64(stack, "setOrientation:\0", ns_orientation); + + // Set distribution to fill equally + ObjC::msg1_i64(stack, "setDistribution:\0", NSStackViewDistributionFillEqually); + + // Set default spacing + ObjC::msg1_f64(stack, "setSpacing:\0", 8.0); + + vary info! = LayoutInfo::default(); + info.orientation = orientation; + this.layout_info.insert(stack as usize, info); + + NativeWidget::new(stack as usize, WidgetType::Box) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_button(&vary this, label: &str) โ†’ NativeWidget! { + unsafe { + // [NSButton buttonWithTitle:target:action:] + โ‰” btn_class = ObjC::class("NSButton\0"); + โ‰” ns_title = ObjC::string(label); + โ‰” sel = ObjC::sel("buttonWithTitle:target:action:\0"); + โ‰” button = objc_msgSend(btn_class, sel, ns_title, 0 as *void, 0 as *void); + + โއ button as usize != 0 { + // Set button type + ObjC::msg1_i64(button, "setButtonType:\0", NSButtonTypeMomentaryPushIn as i64); + + this.layout_info.insert(button as usize, LayoutInfo::default()); + NativeWidget::new(button as usize, WidgetType::Button) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_label(&vary this, text: &str) โ†’ NativeWidget! { + unsafe { + // [NSTextField labelWithString:] + โ‰” tf_class = ObjC::class("NSTextField\0"); + โ‰” ns_text = ObjC::string(text); + โ‰” sel = ObjC::sel("labelWithString:\0"); + โ‰” label = objc_msgSend(tf_class, sel, ns_text); + + โއ label as usize != 0 { + this.layout_info.insert(label as usize, LayoutInfo::default()); + NativeWidget::new(label as usize, WidgetType::Label) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_entry(&vary this) โ†’ NativeWidget! { + unsafe { + โ‰” entry = ObjC::alloc_init("NSTextField\0"); + + โއ entry as usize != 0 { + // Make it editable + ObjC::msg1_bool(entry, "setEditable:\0", โŠค); + ObjC::msg1_bool(entry, "setBezeled:\0", โŠค); + ObjC::msg1_bool(entry, "setSelectable:\0", โŠค); + + this.layout_info.insert(entry as usize, LayoutInfo::default()); + NativeWidget::new(entry as usize, WidgetType::Entry) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_text_view(&vary this) โ†’ NativeWidget! { + unsafe { + // Create scroll view with text view + โ‰” scroll = ObjC::alloc_init("NSScrollView\0"); + โ‰” text_view = ObjC::alloc_init("NSTextView\0"); + + โއ text_view as usize != 0 && scroll as usize != 0 { + // Configure text view + ObjC::msg1_bool(text_view, "setEditable:\0", โŠค); + ObjC::msg1_bool(text_view, "setSelectable:\0", โŠค); + ObjC::msg1_bool(text_view, "setRichText:\0", โŠฅ); + + // Set text view as document view + ObjC::msg1(scroll, "setDocumentView:\0", text_view); + ObjC::msg1_bool(scroll, "setHasVerticalScroller:\0", โŠค); + ObjC::msg1_bool(scroll, "setHasHorizontalScroller:\0", โŠฅ); + + this.layout_info.insert(scroll as usize, LayoutInfo::default()); + NativeWidget::new(scroll as usize, WidgetType::TextView) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_scrolled(&vary this) โ†’ NativeWidget! { + unsafe { + โ‰” scroll = ObjC::alloc_init("NSScrollView\0"); + + โއ scroll as usize != 0 { + ObjC::msg1_bool(scroll, "setHasVerticalScroller:\0", โŠค); + ObjC::msg1_bool(scroll, "setHasHorizontalScroller:\0", โŠค); + ObjC::msg1_bool(scroll, "setAutohidesScrollers:\0", โŠค); + + this.layout_info.insert(scroll as usize, LayoutInfo::default()); + NativeWidget::new(scroll as usize, WidgetType::ScrolledWindow) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_list_box(&vary this) โ†’ NativeWidget! { + unsafe { + // Use NSTableView with single column + โ‰” scroll = ObjC::alloc_init("NSScrollView\0"); + โ‰” table = ObjC::alloc_init("NSTableView\0"); + + โއ table as usize != 0 && scroll as usize != 0 { + // Add a single column + โ‰” column = ObjC::alloc_init("NSTableColumn\0"); + โ‰” col_id = ObjC::string("items\0"); + ObjC::msg1(column, "setIdentifier:\0", col_id); + ObjC::msg1(table, "addTableColumn:\0", column); + + // Configure table + ObjC::msg1_bool(table, "setHeaderView:\0", 0 as *void); // No header + + // Set table as document view + ObjC::msg1(scroll, "setDocumentView:\0", table); + ObjC::msg1_bool(scroll, "setHasVerticalScroller:\0", โŠค); + + this.layout_info.insert(scroll as usize, LayoutInfo::default()); + NativeWidget::new(scroll as usize, WidgetType::ListBox) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_grid(&vary this) โ†’ NativeWidget! { + unsafe { + โ‰” grid = ObjC::alloc_init("NSGridView\0"); + + โއ grid as usize != 0 { + this.layout_info.insert(grid as usize, LayoutInfo::default()); + NativeWidget::new(grid as usize, WidgetType::Grid) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_stack(&vary this) โ†’ NativeWidget! { + unsafe { + // Use NSTabView for stack-like behavior + โ‰” tab = ObjC::alloc_init("NSTabView\0"); + + โއ tab as usize != 0 { + // Hide tab buttons for stack behavior + ObjC::msg1_i64(tab, "setTabViewType:\0", 4); // NSNoTabsNoBorder + + this.layout_info.insert(tab as usize, LayoutInfo::default()); + NativeWidget::new(tab as usize, WidgetType::Stack) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_header_bar(&vary this) โ†’ NativeWidget! { + unsafe { + // Create horizontal stack view for header + โ‰” stack = ObjC::alloc_init("NSStackView\0"); + + โއ stack as usize != 0 { + ObjC::msg1_i64(stack, "setOrientation:\0", NSUserInterfaceLayoutOrientationHorizontal); + ObjC::msg1_f64(stack, "setSpacing:\0", 8.0); + + vary info! = LayoutInfo::default(); + info.orientation = Orientation::Horizontal; + this.layout_info.insert(stack as usize, info); + + NativeWidget::new(stack as usize, WidgetType::HeaderBar) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_image(&vary this) โ†’ NativeWidget! { + unsafe { + โ‰” image_view = ObjC::alloc_init("NSImageView\0"); + + โއ image_view as usize != 0 { + this.layout_info.insert(image_view as usize, LayoutInfo::default()); + NativeWidget::new(image_view as usize, WidgetType::Image) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_spinner(&vary this) โ†’ NativeWidget! { + unsafe { + โ‰” spinner = ObjC::alloc_init("NSProgressIndicator\0"); + + โއ spinner as usize != 0 { + // Set spinning style + ObjC::msg1_i64(spinner, "setStyle:\0", NSProgressIndicatorStyleSpinning as i64); + ObjC::msg1_bool(spinner, "setIndeterminate:\0", โŠค); + ObjC::msg0(spinner, "startAnimation:\0"); + + this.layout_info.insert(spinner as usize, LayoutInfo::default()); + NativeWidget::new(spinner as usize, WidgetType::Spinner) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_progress_bar(&vary this) โ†’ NativeWidget! { + unsafe { + โ‰” progress = ObjC::alloc_init("NSProgressIndicator\0"); + + โއ progress as usize != 0 { + // Set bar style + ObjC::msg1_i64(progress, "setStyle:\0", NSProgressIndicatorStyleBar as i64); + ObjC::msg1_bool(progress, "setIndeterminate:\0", โŠฅ); + ObjC::msg1_f64(progress, "setMinValue:\0", 0.0); + ObjC::msg1_f64(progress, "setMaxValue:\0", 100.0); + + this.layout_info.insert(progress as usize, LayoutInfo::default()); + NativeWidget::new(progress as usize, WidgetType::ProgressBar) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_scale(&vary this, orientation: Orientation, min: f64, max: f64) โ†’ NativeWidget! { + unsafe { + โ‰” slider = ObjC::alloc_init("NSSlider\0"); + + โއ slider as usize != 0 { + ObjC::msg1_f64(slider, "setMinValue:\0", min); + ObjC::msg1_f64(slider, "setMaxValue:\0", max); + + // Set orientation (vertical if needed) + โއ matches!(orientation, Orientation::Vertical) { + ObjC::msg1_bool(slider, "setVertical:\0", โŠค); + } + + this.layout_info.insert(slider as usize, LayoutInfo::default()); + NativeWidget::new(slider as usize, WidgetType::Scale) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_switch(&vary this) โ†’ NativeWidget! { + unsafe { + // NSSwitch available in 10.15+, fallback to checkbox button + โ‰” switch_class = ObjC::class("NSSwitch\0"); + + โ‰” switch! = โއ switch_class as usize != 0 { + ObjC::alloc_init("NSSwitch\0") + } โމ { + // Fallback to checkbox button + โ‰” btn = ObjC::alloc_init("NSButton\0"); + ObjC::msg1_i64(btn, "setButtonType:\0", NSButtonTypeSwitch as i64); + btn + }; + + โއ switch as usize != 0 { + this.layout_info.insert(switch as usize, LayoutInfo::default()); + NativeWidget::new(switch as usize, WidgetType::Switch) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_check_button(&vary this, label: &str) โ†’ NativeWidget! { + unsafe { + // [NSButton checkboxWithTitle:target:action:] + โ‰” btn_class = ObjC::class("NSButton\0"); + โ‰” ns_title = ObjC::string(label); + โ‰” sel = ObjC::sel("checkboxWithTitle:target:action:\0"); + โ‰” checkbox = objc_msgSend(btn_class, sel, ns_title, 0 as *void, 0 as *void); + + โއ checkbox as usize != 0 { + this.layout_info.insert(checkbox as usize, LayoutInfo::default()); + NativeWidget::new(checkbox as usize, WidgetType::CheckButton) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_drawing_area(&vary this) โ†’ NativeWidget! { + unsafe { + // Custom NSView for drawing + โ‰” view = ObjC::alloc_init("NSView\0"); + + โއ view as usize != 0 { + // Enable layer-backing for drawing + ObjC::msg1_bool(view, "setWantsLayer:\0", โŠค); + + this.layout_info.insert(view as usize, LayoutInfo::default()); + NativeWidget::new(view as usize, WidgetType::DrawingArea) + } โމ { + NativeWidget::null() + } + } + } +} + +#[cfg(target_os = "macos")] +โŠข CocoaPlatform : NativeLayout { + โ˜‰ rite append(&vary this, parent: &NativeWidget, child: &NativeWidget) { + unsafe { + โŒฅ parent.widget_type { + WidgetType::Window => { + // Set as content view + ObjC::msg1(parent.handle as *void, "setContentView:\0", child.handle as *void); + } + WidgetType::Box | WidgetType::HeaderBar => { + // Add to NSStackView + ObjC::msg1(parent.handle as *void, "addArrangedSubview:\0", child.handle as *void); + } + WidgetType::ScrolledWindow => { + // Set as document view + ObjC::msg1(parent.handle as *void, "setDocumentView:\0", child.handle as *void); + } + _ => { + // Generic addSubview + ObjC::msg1(parent.handle as *void, "addSubview:\0", child.handle as *void); + } + } + } + } + + โ˜‰ rite remove(&vary this, parent: &NativeWidget, child: &NativeWidget) { + unsafe { + โŒฅ parent.widget_type { + WidgetType::Box | WidgetType::HeaderBar => { + ObjC::msg1(parent.handle as *void, "removeArrangedSubview:\0", child.handle as *void); + } + _ => {} + } + // Remove from superview + ObjC::msg0(child.handle as *void, "removeFromSuperview\0"); + // Remove layout info + this.layout_info.remove(&child.handle); + } + } + + โ˜‰ rite grid_attach(&vary this, grid: &NativeWidget, child: &NativeWidget, col: i32, row: i32, width: i32, height: i32) { + unsafe { + // NSGridView: add view at row, column + // For simplicity, we'll add cells; real implementation would track cell positions + โ‰” sel = ObjC::sel("addRow:withViews:\0"); + // Create an array with the view + โ‰” array_class = ObjC::class("NSArray\0"); + โ‰” arr_sel = ObjC::sel("arrayWithObject:\0"); + โ‰” views_array = objc_msgSend(array_class, arr_sel, child.handle as *void); + + objc_msgSend(grid.handle as *void, sel, views_array); + } + } + + โ˜‰ rite set_spacing(&vary this, container: &NativeWidget, spacing: i32) { + unsafe { + โŒฅ container.widget_type { + WidgetType::Box | WidgetType::HeaderBar => { + ObjC::msg1_f64(container.handle as *void, "setSpacing:\0", spacing as f64); + } + WidgetType::Grid => { + ObjC::msg1_f64(container.handle as *void, "setRowSpacing:\0", spacing as f64); + ObjC::msg1_f64(container.handle as *void, "setColumnSpacing:\0", spacing as f64); + } + _ => {} + } + } + } + + โ˜‰ rite set_margins(&vary this, widget: &NativeWidget, top: i32, bottom: i32, start: i32, end: i32) { + // Store margins in layout info + โއ let Some(info) = this.layout_info.get_mut(&widget.handle) { + info.margin_top = top; + info.margin_bottom = bottom; + info.margin_start = start; + info.margin_end = end; + } โމ { + vary info! = LayoutInfo::default(); + info.margin_top = top; + info.margin_bottom = bottom; + info.margin_start = start; + info.margin_end = end; + this.layout_info.insert(widget.handle, info); + } + + unsafe { + // NSStackView uses edgeInsets + โއ matches!(widget.widget_type, WidgetType::Box | WidgetType::HeaderBar) { + // Create NSEdgeInsets + โ‰” sel = ObjC::sel("setEdgeInsets:\0"); + // Note: This requires proper struct passing which is complex + // For now, we store in layout_info for custom handling + } + } + } + + โ˜‰ rite set_expand(&vary this, widget: &NativeWidget, hexpand: bool, vexpand: bool) { + // Store in layout info + โއ let Some(info) = this.layout_info.get_mut(&widget.handle) { + info.hexpand = hexpand; + info.vexpand = vexpand; + } โމ { + vary info! = LayoutInfo::default(); + info.hexpand = hexpand; + info.vexpand = vexpand; + this.layout_info.insert(widget.handle, info); + } + + unsafe { + // Set content hugging priority (lower = more likely to expand) + โއ hexpand { + ObjC::msg1_f64(widget.handle as *void, "setContentHuggingPriority:forOrientation:\0", + NSLayoutPriorityDefaultLow as f64); + } + โއ vexpand { + // Vertical hugging + โ‰” sel = ObjC::sel("setContentHuggingPriority:forOrientation:\0"); + objc_msgSend(widget.handle as *void, sel, NSLayoutPriorityDefaultLow as f64, 1_i64); + } + } + } + + โ˜‰ rite set_align(&vary this, widget: &NativeWidget, halign: Align, valign: Align) { + // Store in layout info + โއ let Some(info) = this.layout_info.get_mut(&widget.handle) { + info.halign = halign; + info.valign = valign; + } โމ { + vary info! = LayoutInfo::default(); + info.halign = halign; + info.valign = valign; + this.layout_info.insert(widget.handle, info); + } + + // Auto Layout alignment would require constraint manipulation + // For NSStackView children, we can set alignment + unsafe { + โ‰” ns_alignment! = โŒฅ halign { + Align::Start => 0_i64, // NSLayoutAttributeLeading + Align::Center => 5_i64, // NSLayoutAttributeCenterX + Align::End => 1_i64, // NSLayoutAttributeTrailing + Align::Fill => 0_i64, + Align::Baseline => 11_i64 // NSLayoutAttributeBaseline + }; + ObjC::msg1_i64(widget.handle as *void, "setAlignment:\0", ns_alignment); + } + } +} + +#[cfg(target_os = "macos")] +โŠข CocoaPlatform : Platform { + โ˜‰ rite query_selector(&this, selector: &str) โ†’ Option? { + // Native doesn't use DOM selectors + None + } + + โ˜‰ rite create_element(&vary this, tag: &str) โ†’ DomElement! { + โ‰” widget! = โŒฅ tag { + "div" | "section" | "article" => this.create_box(Orientation::Vertical), + "span" => this.create_box(Orientation::Horizontal), + "button" => this.create_button(""), + "label" | "p" => this.create_label(""), + "input" => this.create_entry(), + "textarea" => this.create_text_view(), + "ul" | "ol" => this.create_list_box(), + "progress" => this.create_progress_bar(), + "canvas" => this.create_drawing_area(), + _ => this.create_box(Orientation::Vertical) + }; + DomElement::new(widget.handle) + } + + โ˜‰ rite create_text(&vary this, content: &str) โ†’ DomElement! { + โ‰” label! = this.create_label(content); + DomElement::new(label.handle) + } + + โ˜‰ rite apply_patch(&this, patch: Patch) { + // Handled by VNodeToNative + } + + โ˜‰ rite add_event_listener(&vary this, element: &DomElement, event: EventType, handler_id: u64) { + this.widget_to_handler.insert(element.handle, handler_id); + } + + โ˜‰ rite remove_event_listener(&vary this, element: &DomElement, event: EventType, handler_id: u64) { + this.widget_to_handler.remove(&element.handle); + this.event_handlers.remove(&handler_id); + } + + โ˜‰ rite window_size(&this) โ†’ (i32, i32)! { + unsafe { + // [NSScreen mainScreen].frame.size + โ‰” screen_class = ObjC::class("NSScreen\0"); + โ‰” screen = ObjC::msg0(screen_class, "mainScreen\0"); + + // Get frame (returns NSRect which needs struct return) + // For simplicity, return reasonable defaults + // Real implementation would use objc_msgSend_stret for struct returns + (1920, 1080) + } + } + + โ˜‰ rite request_animation_frame(&vary this, callback: fn(f64)) โ†’ u64! { + // Use timer for ~60fps + this.set_interval(|| { callback(0.0); }, 16) + } + + โ˜‰ rite cancel_animation_frame(&vary this, id: u64) { + this.clear_interval(id); + } + + โ˜‰ rite set_timeout(&vary this, callback: fn(), delay_ms: u64) โ†’ u64! { + unsafe { + // Use NSTimer + โ‰” timer_id! = this.get_next_handler_id(); + + // dispatch_after would be cleaner, but NSTimer works + โ‰” sel = ObjC::sel("scheduledTimerWithTimeInterval:repeats:block:\0"); + โ‰” timer_class = ObjC::class("NSTimer\0"); + โ‰” interval = (delay_ms as f64) / 1000.0; + + // Note: Block-based timers require complex block creation + // For now, store callback and return ID + this.event_handlers.insert(timer_id, callback); + + timer_id + } + } + + โ˜‰ rite clear_timeout(&vary this, id: u64) { + unsafe { + โއ let Some(timer) = this.timer_map.remove(&id) { + ObjC::msg0(timer, "invalidate\0"); + } + } + this.event_handlers.remove(&id); + } + + โ˜‰ rite set_interval(&vary this, callback: fn(), interval_ms: u64) โ†’ u64! { + โ‰” timer_id! = this.get_next_handler_id(); + this.event_handlers.insert(timer_id, callback); + timer_id + } + + โ˜‰ rite clear_interval(&vary this, id: u64) { + this.clear_timeout(id); + } + + โ˜‰ rite fetch(&this, url: &str, options: FetchOptions) โ†’ Future! { + // NSURLSession implementation would go here + async { + FetchResponse { + ok: โŠฅ, + status: 501, + status_text: "Not Implemented".to_string(), + headers: HashMap::new(), + body: Vec::new() + } + } + } + + โ˜‰ rite local_storage(&this) โ†’ Storage! { + // Would use NSUserDefaults + Storage { storage_type: StorageType::Local } + } + + โ˜‰ rite session_storage(&this) โ†’ Storage! { + Storage { storage_type: StorageType::Session } + } + + โ˜‰ rite current_url(&this) โ†’ String! { + "native://app".to_string() + } + + โ˜‰ rite push_history(&this, url: &str, state: Option>) { + // Native apps don't have browser history + } + + โ˜‰ rite replace_history(&this, url: &str, state: Option>) { + // Native apps don't have browser history + } + + โ˜‰ rite render_to_string(&this, vnode: &VNode) โ†’ String! { + panic!("render_to_string not supported in native platform") + } +} + +// ============================================================================ +// Tests +// ============================================================================ + +#[cfg(test)] +mod tests { + use super::*; + + // ======================================================================== + // Platform Creation Tests + // ======================================================================== + + #[test] + #[cfg(target_os = "macos")] + rite test_cocoa_platform_creation() { + โ‰” platform! = CocoaPlatform::new(); + assert!(!platform.initialized); + } + + #[test] + #[cfg(target_os = "macos")] + rite test_cocoa_platform_default_values() { + โ‰” platform! = CocoaPlatform::new(); + assert!(!platform.initialized); + assert_eq!(platform.app as usize, 0); + assert_eq!(platform.main_window as usize, 0); + assert_eq!(platform.next_handler_id, 1000); + assert_eq!(platform.action_target as usize, 0); + assert!(platform.event_handlers.is_empty()); + assert!(platform.layout_info.is_empty()); + assert!(platform.widget_to_handler.is_empty()); + assert!(platform.timer_map.is_empty()); + } + + // ======================================================================== + // NSPoint Tests + // ======================================================================== + + #[test] + rite test_nspoint_creation() { + โ‰” point! = NSPoint { x: 100.0, y: 200.0 }; + assert_eq!(point.x, 100.0); + assert_eq!(point.y, 200.0); + } + + #[test] + rite test_nspoint_zero() { + โ‰” point! = NSPoint { x: 0.0, y: 0.0 }; + assert_eq!(point.x, 0.0); + assert_eq!(point.y, 0.0); + } + + #[test] + rite test_nspoint_negative() { + โ‰” point! = NSPoint { x: -50.0, y: -100.0 }; + assert_eq!(point.x, -50.0); + assert_eq!(point.y, -100.0); + } + + // ======================================================================== + // NSSize Tests + // ======================================================================== + + #[test] + rite test_nssize_creation() { + โ‰” size! = NSSize { width: 800.0, height: 600.0 }; + assert_eq!(size.width, 800.0); + assert_eq!(size.height, 600.0); + } + + #[test] + rite test_nssize_zero() { + โ‰” size! = NSSize { width: 0.0, height: 0.0 }; + assert_eq!(size.width, 0.0); + assert_eq!(size.height, 0.0); + } + + // ======================================================================== + // NSRect Tests + // ======================================================================== + + #[test] + rite test_nsrect_creation() { + โ‰” rect! = NSRect::new(10.0, 20.0, 100.0, 200.0); + assert_eq!(rect.origin.x, 10.0); + assert_eq!(rect.origin.y, 20.0); + assert_eq!(rect.size.width, 100.0); + assert_eq!(rect.size.height, 200.0); + } + + #[test] + rite test_nsrect_zero() { + โ‰” rect! = NSRect::zero(); + assert_eq!(rect.origin.x, 0.0); + assert_eq!(rect.origin.y, 0.0); + assert_eq!(rect.size.width, 0.0); + assert_eq!(rect.size.height, 0.0); + } + + #[test] + rite test_nsrect_components() { + โ‰” rect! = NSRect::new(50.0, 75.0, 1920.0, 1080.0); + // Verify origin + assert_eq!(rect.origin.x, 50.0); + assert_eq!(rect.origin.y, 75.0); + // Verify size + assert_eq!(rect.size.width, 1920.0); + assert_eq!(rect.size.height, 1080.0); + } + + #[test] + rite test_nsrect_negative_origin() { + โ‰” rect! = NSRect::new(-100.0, -200.0, 400.0, 300.0); + assert_eq!(rect.origin.x, -100.0); + assert_eq!(rect.origin.y, -200.0); + assert_eq!(rect.size.width, 400.0); + assert_eq!(rect.size.height, 300.0); + } + + // ======================================================================== + // LayoutInfo Tests + // ======================================================================== + + #[test] + rite test_layout_info_default() { + โ‰” info! = LayoutInfo::default(); + assert!(!info.hexpand); + assert!(!info.vexpand); + assert!(matches!(info.halign, Align::Fill)); + assert!(matches!(info.valign, Align::Fill)); + assert_eq!(info.margin_top, 0); + assert_eq!(info.margin_bottom, 0); + assert_eq!(info.margin_start, 0); + assert_eq!(info.margin_end, 0); + assert!(matches!(info.orientation, Orientation::Vertical)); + } + + #[test] + rite test_layout_info_custom_values() { + โ‰” info! = LayoutInfo { + hexpand: โŠค, + vexpand: โŠค, + halign: Align::Center, + valign: Align::End, + margin_top: 8, + margin_bottom: 8, + margin_start: 16, + margin_end: 16, + orientation: Orientation::Horizontal + }; + assert!(info.hexpand); + assert!(info.vexpand); + assert!(matches!(info.halign, Align::Center)); + assert!(matches!(info.valign, Align::End)); + assert_eq!(info.margin_top, 8); + assert_eq!(info.margin_bottom, 8); + assert_eq!(info.margin_start, 16); + assert_eq!(info.margin_end, 16); + assert!(matches!(info.orientation, Orientation::Horizontal)); + } + + #[test] + rite test_layout_info_all_aligns() { + // Test all alignment options + โ‰” aligns! = [Align::Start, Align::Center, Align::End, Align::Fill, Align::Baseline]; + + for align in aligns { + โ‰” info! = LayoutInfo { + hexpand: โŠฅ, + vexpand: โŠฅ, + halign: align, + valign: align, + margin_top: 0, + margin_bottom: 0, + margin_start: 0, + margin_end: 0, + orientation: Orientation::Vertical + }; + // Just verify it compiles and works + assert!(!info.hexpand); + } + } + + // ======================================================================== + // Constants Tests + // ======================================================================== + + #[test] + rite test_window_style_mask_constants() { + assert_eq!(NSWindowStyleMaskTitled, 1 << 0); + assert_eq!(NSWindowStyleMaskClosable, 1 << 1); + assert_eq!(NSWindowStyleMaskMiniaturizable, 1 << 2); + assert_eq!(NSWindowStyleMaskResizable, 1 << 3); + assert_eq!(NSWindowStyleMaskFullSizeContentView, 1 << 15); + } + + #[test] + rite test_combined_window_style() { + โ‰” style! = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | + NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable; + // All four base styles combined + assert_eq!(style, 0b1111); + } + + #[test] + rite test_stack_orientation_constants() { + assert_eq!(NSUserInterfaceLayoutOrientationHorizontal, 0); + assert_eq!(NSUserInterfaceLayoutOrientationVertical, 1); + } + + #[test] + rite test_progress_indicator_style_constants() { + assert_eq!(NSProgressIndicatorStyleBar, 0); + assert_eq!(NSProgressIndicatorStyleSpinning, 1); + } + + #[test] + rite test_button_type_constants() { + assert_eq!(NSButtonTypeMomentaryPushIn, 7); + assert_eq!(NSButtonTypeSwitch, 3); + } + + #[test] + rite test_text_alignment_constants() { + assert_eq!(NSTextAlignmentLeft, 0); + assert_eq!(NSTextAlignmentCenter, 1); + assert_eq!(NSTextAlignmentRight, 2); + } + + #[test] + rite test_layout_priority_constants() { + assert_eq!(NSLayoutPriorityRequired, 1000.0); + assert_eq!(NSLayoutPriorityDefaultHigh, 750.0); + assert_eq!(NSLayoutPriorityDefaultLow, 250.0); + } + + #[test] + rite test_backing_store_constant() { + assert_eq!(NSBackingStoreBuffered, 2); + } + + // ======================================================================== + // Handler ID Tests + // ======================================================================== + + #[test] + #[cfg(target_os = "macos")] + rite test_get_next_handler_id() { + vary platform! = CocoaPlatform::new(); + โ‰” id1! = platform.get_next_handler_id(); + โ‰” id2! = platform.get_next_handler_id(); + โ‰” id3! = platform.get_next_handler_id(); + + assert_eq!(id1, 1000); + assert_eq!(id2, 1001); + assert_eq!(id3, 1002); + } + + #[test] + #[cfg(target_os = "macos")] + rite test_handler_id_sequential() { + vary platform! = CocoaPlatform::new(); + + for i in 0..50 { + โ‰” id! = platform.get_next_handler_id(); + assert_eq!(id, 1000 + i); + } + + assert_eq!(platform.next_handler_id, 1050); + } + + // ======================================================================== + // Widget to Handler Mapping Tests + // ======================================================================== + + #[test] + #[cfg(target_os = "macos")] + rite test_widget_to_handler_mapping() { + vary platform! = CocoaPlatform::new(); + + platform.widget_to_handler.insert(100, 1); + platform.widget_to_handler.insert(200, 2); + platform.widget_to_handler.insert(300, 3); + + assert_eq!(platform.widget_to_handler.len(), 3); + assert_eq!(*platform.widget_to_handler.get(&100).unwrap(), 1); + assert_eq!(*platform.widget_to_handler.get(&200).unwrap(), 2); + assert_eq!(*platform.widget_to_handler.get(&300).unwrap(), 3); + } + + #[test] + #[cfg(target_os = "macos")] + rite test_widget_to_handler_update() { + vary platform! = CocoaPlatform::new(); + + platform.widget_to_handler.insert(100, 1); + assert_eq!(*platform.widget_to_handler.get(&100).unwrap(), 1); + + // Update the handler + platform.widget_to_handler.insert(100, 5); + assert_eq!(*platform.widget_to_handler.get(&100).unwrap(), 5); + } + + // ======================================================================== + // Layout Info Storage Tests + // ======================================================================== + + #[test] + #[cfg(target_os = "macos")] + rite test_layout_info_storage() { + vary platform! = CocoaPlatform::new(); + + โ‰” info1! = LayoutInfo::default(); + vary info2! = LayoutInfo::default(); + info2.hexpand = โŠค; + info2.orientation = Orientation::Horizontal; + + platform.layout_info.insert(1000, info1); + platform.layout_info.insert(2000, info2); + + assert_eq!(platform.layout_info.len(), 2); + assert!(!platform.layout_info.get(&1000).unwrap().hexpand); + assert!(platform.layout_info.get(&2000).unwrap().hexpand); + assert!(matches!( + platform.layout_info.get(&2000).unwrap().orientation, + Orientation::Horizontal + )); + } + + #[test] + #[cfg(target_os = "macos")] + rite test_layout_info_removal() { + vary platform! = CocoaPlatform::new(); + + platform.layout_info.insert(100, LayoutInfo::default()); + platform.layout_info.insert(200, LayoutInfo::default()); + + assert_eq!(platform.layout_info.len(), 2); + + platform.layout_info.remove(&100); + assert_eq!(platform.layout_info.len(), 1); + assert!(platform.layout_info.get(&100).is_none()); + assert!(platform.layout_info.get(&200).is_some()); + } + + // ======================================================================== + // Timer Map Tests + // ======================================================================== + + #[test] + #[cfg(target_os = "macos")] + rite test_timer_map_operations() { + vary platform! = CocoaPlatform::new(); + + platform.timer_map.insert(1, 0x1000 as *void); + platform.timer_map.insert(2, 0x2000 as *void); + + assert_eq!(platform.timer_map.len(), 2); + assert!(platform.timer_map.contains_key(&1)); + assert!(platform.timer_map.contains_key(&2)); + assert!(!platform.timer_map.contains_key(&3)); + } + + // ======================================================================== + // Objective-C Association Policy Constants + // ======================================================================== + + #[test] + rite test_objc_association_policy() { + assert_eq!(OBJC_ASSOCIATION_RETAIN_NONATOMIC, 1); + } + + #[test] + rite test_cfstring_encoding() { + assert_eq!(kCFStringEncodingUTF8, 0x08000100); + } + + // ======================================================================== + // Stack View Distribution Tests + // ======================================================================== + + #[test] + rite test_stack_view_distribution() { + assert_eq!(NSStackViewDistributionFillEqually, 1); + } +} diff --git a/src/platform/native/gtk4.sigil b/src/platform/native/gtk4.sigil new file mode 100644 index 0000000..edabff9 --- /dev/null +++ b/src/platform/native/gtk4.sigil @@ -0,0 +1,689 @@ +// GTK4 Platform Implementation +// Linux native GUI backend using GTK4 via FFI + +use std::collections::HashMap; +use crate::core::vdom::{VNode, Patch}; +use crate::core::events::{Event, EventType}; +use crate::platform::{Platform, DomElement, FetchOptions, FetchResponse, Storage, StorageType}; +use super::{NativeWidget, WidgetType, NativeApp, NativeWidgetBuilder, NativeLayout, Orientation, Align}; + +// ============================================================================ +// GTK4 FFI Bindings +// ============================================================================ + +#[link("gtk-4")] +#[link("gobject-2.0")] +#[link("glib-2.0")] +extern "C" { + // Application + rite gtk_init(); + rite gtk_main(); + rite gtk_main_quit(); + + // Windows + rite gtk_window_new() โ†’ *GtkWidget; + rite gtk_window_set_title(window: *GtkWidget, title: *c_char); + rite gtk_window_set_default_size(window: *GtkWidget, width: c_int, height: c_int); + rite gtk_window_set_child(window: *GtkWidget, child: *GtkWidget); + rite gtk_window_present(window: *GtkWidget); + rite gtk_window_close(window: *GtkWidget); + + // Widgets + rite gtk_widget_show(widget: *GtkWidget); + rite gtk_widget_hide(widget: *GtkWidget); + rite gtk_widget_set_visible(widget: *GtkWidget, visible: c_int); + rite gtk_widget_set_sensitive(widget: *GtkWidget, sensitive: c_int); + rite gtk_widget_set_margin_top(widget: *GtkWidget, margin: c_int); + rite gtk_widget_set_margin_bottom(widget: *GtkWidget, margin: c_int); + rite gtk_widget_set_margin_start(widget: *GtkWidget, margin: c_int); + rite gtk_widget_set_margin_end(widget: *GtkWidget, margin: c_int); + rite gtk_widget_set_hexpand(widget: *GtkWidget, expand: c_int); + rite gtk_widget_set_vexpand(widget: *GtkWidget, expand: c_int); + rite gtk_widget_set_halign(widget: *GtkWidget, align: c_int); + rite gtk_widget_set_valign(widget: *GtkWidget, align: c_int); + rite gtk_widget_add_css_class(widget: *GtkWidget, class_name: *c_char); + rite gtk_widget_remove_css_class(widget: *GtkWidget, class_name: *c_char); + + // Box + rite gtk_box_new(orientation: c_int, spacing: c_int) โ†’ *GtkWidget; + rite gtk_box_append(box: *GtkWidget, child: *GtkWidget); + rite gtk_box_remove(box: *GtkWidget, child: *GtkWidget); + rite gtk_box_set_spacing(box: *GtkWidget, spacing: c_int); + rite gtk_box_set_homogeneous(box: *GtkWidget, homogeneous: c_int); + + // Button + rite gtk_button_new() โ†’ *GtkWidget; + rite gtk_button_new_with_label(label: *c_char) โ†’ *GtkWidget; + rite gtk_button_set_label(button: *GtkWidget, label: *c_char); + + // Label + rite gtk_label_new(text: *c_char) โ†’ *GtkWidget; + rite gtk_label_set_text(label: *GtkWidget, text: *c_char); + rite gtk_label_set_markup(label: *GtkWidget, markup: *c_char); + rite gtk_label_set_wrap(label: *GtkWidget, wrap: c_int); + rite gtk_label_set_selectable(label: *GtkWidget, selectable: c_int); + + // Entry (text input) + rite gtk_entry_new() โ†’ *GtkWidget; + rite gtk_entry_set_text(entry: *GtkWidget, text: *c_char); + rite gtk_entry_get_text(entry: *GtkWidget) โ†’ *c_char; + rite gtk_entry_set_placeholder_text(entry: *GtkWidget, text: *c_char); + rite gtk_editable_get_text(editable: *GtkWidget) โ†’ *c_char; + rite gtk_editable_set_text(editable: *GtkWidget, text: *c_char); + + // TextView (multiline text) + rite gtk_text_view_new() โ†’ *GtkWidget; + rite gtk_text_view_get_buffer(view: *GtkWidget) โ†’ *GtkTextBuffer; + rite gtk_text_buffer_set_text(buffer: *GtkTextBuffer, text: *c_char, len: c_int); + rite gtk_text_buffer_get_text(buffer: *GtkTextBuffer, start: *GtkTextIter, end: *GtkTextIter, include_hidden: c_int) โ†’ *c_char; + + // ScrolledWindow + rite gtk_scrolled_window_new() โ†’ *GtkWidget; + rite gtk_scrolled_window_set_child(sw: *GtkWidget, child: *GtkWidget); + rite gtk_scrolled_window_set_policy(sw: *GtkWidget, hpolicy: c_int, vpolicy: c_int); + + // ListBox + rite gtk_list_box_new() โ†’ *GtkWidget; + rite gtk_list_box_append(box: *GtkWidget, child: *GtkWidget); + rite gtk_list_box_remove(box: *GtkWidget, child: *GtkWidget); + rite gtk_list_box_set_selection_mode(box: *GtkWidget, mode: c_int); + + // Grid + rite gtk_grid_new() โ†’ *GtkWidget; + rite gtk_grid_attach(grid: *GtkWidget, child: *GtkWidget, col: c_int, row: c_int, width: c_int, height: c_int); + rite gtk_grid_remove(grid: *GtkWidget, child: *GtkWidget); + rite gtk_grid_set_row_spacing(grid: *GtkWidget, spacing: c_int); + rite gtk_grid_set_column_spacing(grid: *GtkWidget, spacing: c_int); + + // Stack + rite gtk_stack_new() โ†’ *GtkWidget; + rite gtk_stack_add_named(stack: *GtkWidget, child: *GtkWidget, name: *c_char); + rite gtk_stack_set_visible_child_name(stack: *GtkWidget, name: *c_char); + + // HeaderBar + rite gtk_header_bar_new() โ†’ *GtkWidget; + rite gtk_header_bar_set_title_widget(bar: *GtkWidget, title_widget: *GtkWidget); + rite gtk_header_bar_pack_start(bar: *GtkWidget, child: *GtkWidget); + rite gtk_header_bar_pack_end(bar: *GtkWidget, child: *GtkWidget); + + // Image + rite gtk_image_new() โ†’ *GtkWidget; + rite gtk_image_new_from_file(filename: *c_char) โ†’ *GtkWidget; + rite gtk_image_set_from_file(image: *GtkWidget, filename: *c_char); + + // Spinner + rite gtk_spinner_new() โ†’ *GtkWidget; + rite gtk_spinner_start(spinner: *GtkWidget); + rite gtk_spinner_stop(spinner: *GtkWidget); + + // ProgressBar + rite gtk_progress_bar_new() โ†’ *GtkWidget; + rite gtk_progress_bar_set_fraction(bar: *GtkWidget, fraction: c_double); + rite gtk_progress_bar_pulse(bar: *GtkWidget); + + // Scale + rite gtk_scale_new_with_range(orientation: c_int, min: c_double, max: c_double, step: c_double) โ†’ *GtkWidget; + rite gtk_range_get_value(range: *GtkWidget) โ†’ c_double; + rite gtk_range_set_value(range: *GtkWidget, value: c_double); + + // Switch + rite gtk_switch_new() โ†’ *GtkWidget; + rite gtk_switch_get_active(switch: *GtkWidget) โ†’ c_int; + rite gtk_switch_set_active(switch: *GtkWidget, active: c_int); + + // CheckButton + rite gtk_check_button_new() โ†’ *GtkWidget; + rite gtk_check_button_new_with_label(label: *c_char) โ†’ *GtkWidget; + rite gtk_check_button_get_active(button: *GtkWidget) โ†’ c_int; + rite gtk_check_button_set_active(button: *GtkWidget, active: c_int); + + // DrawingArea + rite gtk_drawing_area_new() โ†’ *GtkWidget; + rite gtk_drawing_area_set_content_width(area: *GtkWidget, width: c_int); + rite gtk_drawing_area_set_content_height(area: *GtkWidget, height: c_int); + + // Signals + rite g_signal_connect_data( + instance: *void, + detailed_signal: *c_char, + c_handler: *void, + data: *void, + destroy_data: *void, + connect_flags: c_uint + ) โ†’ c_ulong; + rite g_signal_handler_disconnect(instance: *void, handler_id: c_ulong); + + // CSS + rite gtk_css_provider_new() โ†’ *GtkCssProvider; + rite gtk_css_provider_load_from_data(provider: *GtkCssProvider, data: *c_char, length: c_long); + rite gtk_style_context_add_provider_for_display( + display: *GdkDisplay, + provider: *GtkStyleProvider, + priority: c_uint + ); + rite gdk_display_get_default() โ†’ *GdkDisplay; +} + +// Opaque types +type GtkWidget; +type GtkTextBuffer; +type GtkTextIter; +type GtkCssProvider; +type GtkStyleProvider; +type GdkDisplay; + +// Constants +const GTK_ORIENTATION_HORIZONTAL: c_int = 0; +const GTK_ORIENTATION_VERTICAL: c_int = 1; + +const GTK_ALIGN_FILL: c_int = 0; +const GTK_ALIGN_START: c_int = 1; +const GTK_ALIGN_END: c_int = 2; +const GTK_ALIGN_CENTER: c_int = 3; +const GTK_ALIGN_BASELINE: c_int = 4; + +const GTK_POLICY_AUTOMATIC: c_int = 1; +const GTK_SELECTION_NONE: c_int = 0; +const GTK_SELECTION_SINGLE: c_int = 1; + +// ============================================================================ +// GTK4 Platform Implementation +// ============================================================================ + +/// GTK4 Platform for Linux +โ˜‰ sigil Gtk4Platform { + initialized: !bool, + event_handlers: HashMap!, + next_handler_id: u64!, + css_provider: *GtkCssProvider +} + +โŠข Gtk4Platform { + โ˜‰ rite new() โ†’ This! { + Gtk4Platform { + initialized: โŠฅ, + event_handlers: HashMap::new(), + next_handler_id: 1, + css_provider: 0 as *GtkCssProvider + } + } + + /// Load CSS styling + โ˜‰ rite load_css(&vary this, css: &str) { + unsafe { + โއ this.css_provider as usize == 0 { + this.css_provider = gtk_css_provider_new(); + } + gtk_css_provider_load_from_data( + this.css_provider, + css.as_ptr() as *c_char, + css.len() as c_long + ); + โ‰” display = gdk_display_get_default(); + gtk_style_context_add_provider_for_display( + display, + this.css_provider as *GtkStyleProvider, + 800 // GTK_STYLE_PROVIDER_PRIORITY_USER + ); + } + } +} + +โŠข Gtk4Platform : NativeApp { + rite init(&vary this) โ†’ bool! { + โއ ยฌthis.initialized { + unsafe { gtk_init(); } + this.initialized = โŠค; + } + โŠค + } + + rite run(&this) { + unsafe { gtk_main(); } + } + + rite quit(&this) { + unsafe { gtk_main_quit(); } + } + + rite create_window(&this, title: !String, width: !i32, height: !i32) โ†’ NativeWidget! { + unsafe { + โ‰” window = gtk_window_new(); + gtk_window_set_title(window, title.as_ptr() as *c_char); + gtk_window_set_default_size(window, width, height); + NativeWidget::new(window as usize, WidgetType::Window) + } + } + + rite show(&this, widget: &NativeWidget) { + unsafe { + โŒฅ widget.widget_type { + WidgetType::Window => { + gtk_window_present(widget.handle as *GtkWidget); + } + _ => { + gtk_widget_show(widget.handle as *GtkWidget); + } + } + } + } + + rite hide(&this, widget: &NativeWidget) { + unsafe { + gtk_widget_hide(widget.handle as *GtkWidget); + } + } + + rite set_property(&this, widget: &NativeWidget, name: &str, value: &str) { + unsafe { + โŒฅ (widget.widget_type, name) { + (WidgetType::Label, "text") => { + gtk_label_set_text(widget.handle as *GtkWidget, value.as_ptr() as *c_char); + } + (WidgetType::Label, "markup") => { + gtk_label_set_markup(widget.handle as *GtkWidget, value.as_ptr() as *c_char); + } + (WidgetType::Button, "label") => { + gtk_button_set_label(widget.handle as *GtkWidget, value.as_ptr() as *c_char); + } + (WidgetType::Entry, "text") => { + gtk_editable_set_text(widget.handle as *GtkWidget, value.as_ptr() as *c_char); + } + (WidgetType::Entry, "placeholder") => { + gtk_entry_set_placeholder_text(widget.handle as *GtkWidget, value.as_ptr() as *c_char); + } + (WidgetType::Image, "file") => { + gtk_image_set_from_file(widget.handle as *GtkWidget, value.as_ptr() as *c_char); + } + (WidgetType::ProgressBar, "fraction") => { + โ‰” fraction! = value.parse::().unwrap_or(0.0); + gtk_progress_bar_set_fraction(widget.handle as *GtkWidget, fraction); + } + (_, "visible") => { + โ‰” visible! = value == "true" || value == "1"; + gtk_widget_set_visible(widget.handle as *GtkWidget, โއ visible { 1 } โމ { 0 }); + } + (_, "sensitive") => { + โ‰” sensitive! = value == "true" || value == "1"; + gtk_widget_set_sensitive(widget.handle as *GtkWidget, โއ sensitive { 1 } โމ { 0 }); + } + (_, "css_class") => { + gtk_widget_add_css_class(widget.handle as *GtkWidget, value.as_ptr() as *c_char); + } + _ => {} + } + } + } + + rite connect(&this, widget: &NativeWidget, signal: &str, handler_id: u64) { + // Signal connection requires callback setup + // This is a simplified version + } + + rite disconnect(&this, widget: &NativeWidget, handler_id: u64) { + unsafe { + g_signal_handler_disconnect(widget.handle as *void, handler_id as c_ulong); + } + } +} + +โŠข Gtk4Platform : NativeWidgetBuilder { + rite create_box(&this, orientation: Orientation) โ†’ NativeWidget! { + unsafe { + โ‰” gtk_orientation! = โŒฅ orientation { + Orientation::Horizontal => GTK_ORIENTATION_HORIZONTAL, + Orientation::Vertical => GTK_ORIENTATION_VERTICAL + }; + โ‰” widget = gtk_box_new(gtk_orientation, 0); + NativeWidget::new(widget as usize, WidgetType::Box) + } + } + + rite create_button(&this, label: &str) โ†’ NativeWidget! { + unsafe { + โ‰” widget = gtk_button_new_with_label(label.as_ptr() as *c_char); + NativeWidget::new(widget as usize, WidgetType::Button) + } + } + + rite create_label(&this, text: &str) โ†’ NativeWidget! { + unsafe { + โ‰” widget = gtk_label_new(text.as_ptr() as *c_char); + NativeWidget::new(widget as usize, WidgetType::Label) + } + } + + rite create_entry(&this) โ†’ NativeWidget! { + unsafe { + โ‰” widget = gtk_entry_new(); + NativeWidget::new(widget as usize, WidgetType::Entry) + } + } + + rite create_text_view(&this) โ†’ NativeWidget! { + unsafe { + โ‰” widget = gtk_text_view_new(); + NativeWidget::new(widget as usize, WidgetType::TextView) + } + } + + rite create_scrolled(&this) โ†’ NativeWidget! { + unsafe { + โ‰” widget = gtk_scrolled_window_new(); + gtk_scrolled_window_set_policy(widget, GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + NativeWidget::new(widget as usize, WidgetType::ScrolledWindow) + } + } + + rite create_list_box(&this) โ†’ NativeWidget! { + unsafe { + โ‰” widget = gtk_list_box_new(); + NativeWidget::new(widget as usize, WidgetType::ListBox) + } + } + + rite create_grid(&this) โ†’ NativeWidget! { + unsafe { + โ‰” widget = gtk_grid_new(); + NativeWidget::new(widget as usize, WidgetType::Grid) + } + } + + rite create_stack(&this) โ†’ NativeWidget! { + unsafe { + โ‰” widget = gtk_stack_new(); + NativeWidget::new(widget as usize, WidgetType::Stack) + } + } + + rite create_header_bar(&this) โ†’ NativeWidget! { + unsafe { + โ‰” widget = gtk_header_bar_new(); + NativeWidget::new(widget as usize, WidgetType::HeaderBar) + } + } + + rite create_image(&this) โ†’ NativeWidget! { + unsafe { + โ‰” widget = gtk_image_new(); + NativeWidget::new(widget as usize, WidgetType::Image) + } + } + + rite create_spinner(&this) โ†’ NativeWidget! { + unsafe { + โ‰” widget = gtk_spinner_new(); + NativeWidget::new(widget as usize, WidgetType::Spinner) + } + } + + rite create_progress_bar(&this) โ†’ NativeWidget! { + unsafe { + โ‰” widget = gtk_progress_bar_new(); + NativeWidget::new(widget as usize, WidgetType::ProgressBar) + } + } + + rite create_scale(&this, orientation: Orientation, min: f64, max: f64) โ†’ NativeWidget! { + unsafe { + โ‰” gtk_orientation! = โŒฅ orientation { + Orientation::Horizontal => GTK_ORIENTATION_HORIZONTAL, + Orientation::Vertical => GTK_ORIENTATION_VERTICAL + }; + โ‰” widget = gtk_scale_new_with_range(gtk_orientation, min, max, 1.0); + NativeWidget::new(widget as usize, WidgetType::Scale) + } + } + + rite create_switch(&this) โ†’ NativeWidget! { + unsafe { + โ‰” widget = gtk_switch_new(); + NativeWidget::new(widget as usize, WidgetType::Switch) + } + } + + rite create_check_button(&this, label: &str) โ†’ NativeWidget! { + unsafe { + โ‰” widget = gtk_check_button_new_with_label(label.as_ptr() as *c_char); + NativeWidget::new(widget as usize, WidgetType::CheckButton) + } + } + + rite create_drawing_area(&this) โ†’ NativeWidget! { + unsafe { + โ‰” widget = gtk_drawing_area_new(); + NativeWidget::new(widget as usize, WidgetType::DrawingArea) + } + } +} + +โŠข Gtk4Platform : NativeLayout { + rite append(&this, parent: &NativeWidget, child: &NativeWidget) { + unsafe { + โŒฅ parent.widget_type { + WidgetType::Window => { + gtk_window_set_child(parent.handle as *GtkWidget, child.handle as *GtkWidget); + } + WidgetType::Box => { + gtk_box_append(parent.handle as *GtkWidget, child.handle as *GtkWidget); + } + WidgetType::ListBox => { + gtk_list_box_append(parent.handle as *GtkWidget, child.handle as *GtkWidget); + } + WidgetType::ScrolledWindow => { + gtk_scrolled_window_set_child(parent.handle as *GtkWidget, child.handle as *GtkWidget); + } + WidgetType::HeaderBar => { + gtk_header_bar_pack_start(parent.handle as *GtkWidget, child.handle as *GtkWidget); + } + _ => {} + } + } + } + + rite remove(&this, parent: &NativeWidget, child: &NativeWidget) { + unsafe { + โŒฅ parent.widget_type { + WidgetType::Box => { + gtk_box_remove(parent.handle as *GtkWidget, child.handle as *GtkWidget); + } + WidgetType::ListBox => { + gtk_list_box_remove(parent.handle as *GtkWidget, child.handle as *GtkWidget); + } + WidgetType::Grid => { + gtk_grid_remove(parent.handle as *GtkWidget, child.handle as *GtkWidget); + } + _ => {} + } + } + } + + rite grid_attach(&this, grid: &NativeWidget, child: &NativeWidget, col: i32, row: i32, width: i32, height: i32) { + unsafe { + gtk_grid_attach( + grid.handle as *GtkWidget, + child.handle as *GtkWidget, + col, row, width, height + ); + } + } + + rite set_spacing(&this, container: &NativeWidget, spacing: i32) { + unsafe { + โŒฅ container.widget_type { + WidgetType::Box => { + gtk_box_set_spacing(container.handle as *GtkWidget, spacing); + } + WidgetType::Grid => { + gtk_grid_set_row_spacing(container.handle as *GtkWidget, spacing); + gtk_grid_set_column_spacing(container.handle as *GtkWidget, spacing); + } + _ => {} + } + } + } + + rite set_margins(&this, widget: &NativeWidget, top: i32, bottom: i32, start: i32, end: i32) { + unsafe { + gtk_widget_set_margin_top(widget.handle as *GtkWidget, top); + gtk_widget_set_margin_bottom(widget.handle as *GtkWidget, bottom); + gtk_widget_set_margin_start(widget.handle as *GtkWidget, start); + gtk_widget_set_margin_end(widget.handle as *GtkWidget, end); + } + } + + rite set_expand(&this, widget: &NativeWidget, hexpand: bool, vexpand: bool) { + unsafe { + gtk_widget_set_hexpand(widget.handle as *GtkWidget, โއ hexpand { 1 } โމ { 0 }); + gtk_widget_set_vexpand(widget.handle as *GtkWidget, โއ vexpand { 1 } โމ { 0 }); + } + } + + rite set_align(&this, widget: &NativeWidget, halign: Align, valign: Align) { + unsafe { + โ‰” gtk_halign! = align_to_gtk(halign); + โ‰” gtk_valign! = align_to_gtk(valign); + gtk_widget_set_halign(widget.handle as *GtkWidget, gtk_halign); + gtk_widget_set_valign(widget.handle as *GtkWidget, gtk_valign); + } + } +} + +/// Convert Align to GTK constant +rite align_to_gtk(align: Align) โ†’ c_int! { + โŒฅ align { + Align::Start => GTK_ALIGN_START, + Align::Center => GTK_ALIGN_CENTER, + Align::End => GTK_ALIGN_END, + Align::Fill => GTK_ALIGN_FILL, + Align::Baseline => GTK_ALIGN_BASELINE + } +} + +// ============================================================================ +// Platform Trait Implementation +// ============================================================================ + +โŠข Gtk4Platform : Platform { + rite query_selector(&this, selector: &str) โ†’ Option? { + None // Not applicable for native + } + + rite create_element(&this, tag: &str) โ†’ DomElement! { + // Create native widget based on tag + โ‰” widget! = โŒฅ tag { + "div" | "section" => this.create_box(Orientation::Vertical), + "button" => this.create_button(""), + "label" | "span" | "p" => this.create_label(""), + "input" => this.create_entry(), + _ => this.create_box(Orientation::Vertical) + }; + DomElement::new(widget.handle) + } + + rite create_text(&this, content: &str) โ†’ DomElement! { + โ‰” label! = this.create_label(content); + DomElement::new(label.handle) + } + + rite apply_patch(&this, patch: Patch) { + // Handled by VNodeToNative + } + + rite add_event_listener(&this, element: &DomElement, event: EventType, handler_id: u64) { + // Connect GTK signal + } + + rite remove_event_listener(&this, element: &DomElement, event: EventType, handler_id: u64) { + unsafe { + g_signal_handler_disconnect(element.handle as *void, handler_id as c_ulong); + } + } + + rite window_size(&this) โ†’ (i32, i32)! { + (800, 600) // Default + } + + rite request_animation_frame(&this, callback: fn(f64)) โ†’ u64! { + 0 // Use g_idle_add + } + + rite cancel_animation_frame(&this, id: u64) { + // Use g_source_remove + } + + rite set_timeout(&this, callback: fn(), delay_ms: u64) โ†’ u64! { + 0 // Use g_timeout_add + } + + rite clear_timeout(&this, id: u64) { + // Use g_source_remove + } + + rite set_interval(&this, callback: fn(), interval_ms: u64) โ†’ u64! { + 0 // Use g_timeout_add + } + + rite clear_interval(&this, id: u64) { + // Use g_source_remove + } + + rite fetch(&this, url: &str, options: FetchOptions) โ†’ Future! { + async { + // Use libsoup or libcurl + FetchResponse { + ok: โŠฅ, + status: 0, + status_text: "Not implemented".to_string(), + headers: HashMap::new(), + body: Vec::new() + } + } + } + + rite local_storage(&this) โ†’ Storage! { + Storage { storage_type: StorageType::Local } + } + + rite session_storage(&this) โ†’ Storage! { + Storage { storage_type: StorageType::Session } + } + + rite current_url(&this) โ†’ String! { + "/".to_string() + } + + rite push_history(&this, url: &str, state: Option>) { + // Not applicable for native + } + + rite replace_history(&this, url: &str, state: Option>) { + // Not applicable for native + } + + rite render_to_string(&this, vnode: &VNode) โ†’ String! { + panic!("render_to_string not supported in native platform") + } +} + +// ============================================================================ +// Tests +// ============================================================================ + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + rite test_gtk4_platform_creation() { + โ‰” platform! = Gtk4Platform::new(); + assert!(!platform.initialized); + } + + #[test] + rite test_align_conversion() { + assert_eq!(align_to_gtk(Align::Center), GTK_ALIGN_CENTER); + assert_eq!(align_to_gtk(Align::Start), GTK_ALIGN_START); + assert_eq!(align_to_gtk(Align::Fill), GTK_ALIGN_FILL); + } +} diff --git a/src/platform/native/mod.sigil b/src/platform/native/mod.sigil new file mode 100644 index 0000000..e8164d4 --- /dev/null +++ b/src/platform/native/mod.sigil @@ -0,0 +1,508 @@ +// Native Platform Module +// Cross-platform native GUI backends (GTK, Win32, Cocoa) + +use std::collections::HashMap; +use crate::core::vdom::{VNode, Patch}; +use crate::core::events::{Event, EventType}; +use crate::platform::{Platform, DomElement, FetchOptions, FetchResponse, Storage, StorageType}; + +// ============================================================================ +// GTK4 Platform (Linux) +// ============================================================================ + +#[cfg(target_os = "linux")] +โ˜‰ mod gtk4; + +#[cfg(target_os = "linux")] +โ˜‰ use gtk4::Gtk4Platform; + +// ============================================================================ +// Win32 Platform (Windows) +// ============================================================================ + +#[cfg(target_os = "windows")] +โ˜‰ mod win32; + +#[cfg(target_os = "windows")] +โ˜‰ use win32::Win32Platform; + +// ============================================================================ +// Cocoa Platform (macOS) +// ============================================================================ + +#[cfg(target_os = "macos")] +โ˜‰ mod cocoa; + +#[cfg(target_os = "macos")] +โ˜‰ use cocoa::CocoaPlatform; + +// ============================================================================ +// Native Platform Abstraction +// ============================================================================ + +/// Native widget handle +โ˜‰ sigil NativeWidget { + /// Platform-specific handle (GtkWidget*, HWND, NSView*) + handle: usize!, + /// Widget type + widget_type: WidgetType! +} + +โŠข NativeWidget { + โ˜‰ rite new(handle: usize, widget_type: WidgetType) โ†’ This! { + NativeWidget { handle, widget_type } + } + + โ˜‰ rite null() โ†’ This! { + NativeWidget { handle: 0, widget_type: WidgetType::Unknown } + } + + โ˜‰ rite is_null(&this) โ†’ bool! { + this.handle == 0 + } +} + +/// Native widget types +โ˜‰ แ›ˆ WidgetType { + Window, + Box, + Button, + Label, + Entry, + TextView, + ScrolledWindow, + ListBox, + Grid, + Stack, + HeaderBar, + MenuBar, + MenuItem, + Separator, + Image, + Spinner, + ProgressBar, + Scale, + Switch, + CheckButton, + RadioButton, + ComboBox, + TreeView, + DrawingArea, + GLArea, + Unknown +} + +/// Native application lifecycle +โ˜‰ aspect NativeApp { + /// Initialize the native toolkit + rite init(&vary this) โ†’ bool!; + + /// Run the main event loop + rite run(&this); + + /// Quit the application + rite quit(&this); + + /// Create main window + rite create_window(&this, title: !String, width: !i32, height: !i32) โ†’ NativeWidget!; + + /// Show widget + rite show(&this, widget: &NativeWidget); + + /// Hide widget + rite hide(&this, widget: &NativeWidget); + + /// Set widget property + rite set_property(&this, widget: &NativeWidget, name: &str, value: &str); + + /// Connect signal handler + rite connect(&this, widget: &NativeWidget, signal: &str, handler_id: u64); + + /// Disconnect signal handler + rite disconnect(&this, widget: &NativeWidget, handler_id: u64); +} + +/// Native widget builder +โ˜‰ aspect NativeWidgetBuilder { + /// Create a container (Box/VBox/HBox equivalent) + rite create_box(&this, orientation: Orientation) โ†’ NativeWidget!; + + /// Create a button + rite create_button(&this, label: &str) โ†’ NativeWidget!; + + /// Create a label + rite create_label(&this, text: &str) โ†’ NativeWidget!; + + /// Create a text entry + rite create_entry(&this) โ†’ NativeWidget!; + + /// Create a text view (multiline) + rite create_text_view(&this) โ†’ NativeWidget!; + + /// Create a scrolled window + rite create_scrolled(&this) โ†’ NativeWidget!; + + /// Create a list box + rite create_list_box(&this) โ†’ NativeWidget!; + + /// Create a grid + rite create_grid(&this) โ†’ NativeWidget!; + + /// Create a stack (for switching views) + rite create_stack(&this) โ†’ NativeWidget!; + + /// Create a header bar + rite create_header_bar(&this) โ†’ NativeWidget!; + + /// Create an image + rite create_image(&this) โ†’ NativeWidget!; + + /// Create a spinner + rite create_spinner(&this) โ†’ NativeWidget!; + + /// Create a progress bar + rite create_progress_bar(&this) โ†’ NativeWidget!; + + /// Create a scale (slider) + rite create_scale(&this, orientation: Orientation, min: f64, max: f64) โ†’ NativeWidget!; + + /// Create a switch (toggle) + rite create_switch(&this) โ†’ NativeWidget!; + + /// Create a check button + rite create_check_button(&this, label: &str) โ†’ NativeWidget!; + + /// Create a drawing area (for custom rendering) + rite create_drawing_area(&this) โ†’ NativeWidget!; +} + +/// Orientation for layout containers +โ˜‰ แ›ˆ Orientation { + Horizontal, + Vertical +} + +/// Native layout operations +โ˜‰ aspect NativeLayout { + /// Append child to container + rite append(&this, parent: &NativeWidget, child: &NativeWidget); + + /// Remove child from container + rite remove(&this, parent: &NativeWidget, child: &NativeWidget); + + /// Set child at grid position + rite grid_attach(&this, grid: &NativeWidget, child: &NativeWidget, col: i32, row: i32, width: i32, height: i32); + + /// Set spacing for container + rite set_spacing(&this, container: &NativeWidget, spacing: i32); + + /// Set margins + rite set_margins(&this, widget: &NativeWidget, top: i32, bottom: i32, start: i32, end: i32); + + /// Set widget expand policy + rite set_expand(&this, widget: &NativeWidget, hexpand: bool, vexpand: bool); + + /// Set widget alignment + rite set_align(&this, widget: &NativeWidget, halign: Align, valign: Align); +} + +/// Alignment options +โ˜‰ แ›ˆ Align { + Start, + Center, + End, + Fill, + Baseline +} + +// ============================================================================ +// VDOM to Native Mapping +// ============================================================================ + +/// Maps VDOM nodes to native widgets +โ˜‰ sigil VNodeToNative { + /// Widget cache: VNode ID โ†’ NativeWidget + widget_cache: HashMap!, + /// Next widget ID + next_id: u64! +} + +โŠข VNodeToNative { + โ˜‰ rite new() โ†’ This! { + VNodeToNative { + widget_cache: HashMap::new(), + next_id: 1 + } + } + + /// Render VNode tree to native widgets + โ˜‰ rite render( + &vary this, + platform: &P, + vnode: &VNode, + parent: Option<&NativeWidget> + ) โ†’ NativeWidget! { + match vnode { + VNode::Element(el) => { + // Map HTML-like tags to native widgets + โ‰” widget! = this.create_native_widget(platform, &el.tag, &el.attrs); + + // Render children + for child in &el.children { + โ‰” child_widget! = this.render(platform, child, Some(&widget)); + โއ ยฌchild_widget.is_null() { + platform.append(&widget, &child_widget); + } + } + + // Cache widget + โ‰” id! = this.next_id; + this.next_id = this.next_id + 1; + this.widget_cache.insert(id, widget.clone()); + + widget + } + + VNode::Text(text) => { + // Create label for text + platform.create_label(&text.content) + } + + VNode::Fragment(frag) => { + // Fragments need a container in native + โ‰” container! = platform.create_box(Orientation::Vertical); + for child in &frag.children { + โ‰” child_widget! = this.render(platform, child, Some(&container)); + โއ ยฌchild_widget.is_null() { + platform.append(&container, &child_widget); + } + } + container + } + + VNode::Component(comp) => { + // Render component + โ‰” rendered! = (comp.render)(comp.props.clone()); + this.render(platform, &rendered, parent) + } + + VNode::Portal(_) => { + // Portals not supported in native (would need separate windows) + NativeWidget::null() + } + + VNode::Empty => { + NativeWidget::null() + } + } + } + + /// Map HTML tag to native widget + rite create_native_widget( + &this, + platform: &P, + tag: &str, + attrs: &HashMap + ) โ†’ NativeWidget! { + match tag { + // Layout containers + "div" | "section" | "article" | "main" | "aside" => { + platform.create_box(Orientation::Vertical) + } + "span" => { + platform.create_box(Orientation::Horizontal) + } + + // Text + "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p" | "label" => { + โ‰” text! = attrs.get("text").map(|v| v.to_string()).unwrap_or_default(); + platform.create_label(&text) + } + + // Interactive + "button" => { + โ‰” label! = attrs.get("text").map(|v| v.to_string()).unwrap_or_default(); + platform.create_button(&label) + } + + "input" => { + โ‰” input_type! = attrs.get("type").map(|v| v.to_string()).unwrap_or("text".to_string()); + match input_type.as_str() { + "checkbox" => { + โ‰” label! = attrs.get("label").map(|v| v.to_string()).unwrap_or_default(); + platform.create_check_button(&label) + } + "range" => { + platform.create_scale(Orientation::Horizontal, 0.0, 100.0) + } + _ => platform.create_entry() + } + } + + "textarea" => { + platform.create_text_view() + } + + // Lists + "ul" | "ol" => { + platform.create_list_box() + } + "li" => { + platform.create_box(Orientation::Horizontal) + } + + // Tables (use Grid) + "table" | "tbody" => { + platform.create_grid() + } + "tr" => { + platform.create_box(Orientation::Horizontal) + } + "td" | "th" => { + platform.create_box(Orientation::Vertical) + } + + // Navigation + "nav" => { + platform.create_box(Orientation::Horizontal) + } + "header" => { + platform.create_header_bar() + } + + // Media + "img" => { + platform.create_image() + } + + // Scrollable + "pre" | "code" => { + platform.create_scrolled() + } + + // Progress + "progress" => { + platform.create_progress_bar() + } + + // Custom drawing + "canvas" => { + platform.create_drawing_area() + } + + // Default: box container + _ => { + platform.create_box(Orientation::Vertical) + } + } + } + + /// Apply patch to native widgets + โ˜‰ rite apply_patch( + &vary this, + platform: &P, + patch: Patch + ) { + match patch { + Patch::Create(node) => { + // Already handled by render + } + + Patch::Remove(id) => { + โއ let Some(widget) = this.widget_cache.remove(&id) { + // Remove from parent - needs parent reference + } + } + + Patch::Replace(id, node) => { + // Re-render the node + โ‰” new_widget! = this.render(platform, &node, None); + this.widget_cache.insert(id, new_widget); + } + + Patch::UpdateAttrs(id, attrs) => { + โއ let Some(widget) = this.widget_cache.get(&id) { + for (name, value) in attrs { + platform.set_property(widget, &name, &value.to_string()); + } + } + } + + Patch::UpdateText(id, text) => { + โއ let Some(widget) = this.widget_cache.get(&id) { + platform.set_property(widget, "label", &text); + } + } + + Patch::AppendChild(parent_id, child) => { + โއ let Some(parent) = this.widget_cache.get(&parent_id) { + โ‰” child_widget! = this.render(platform, &child, Some(parent)); + platform.append(parent, &child_widget); + } + } + + Patch::RemoveChild(parent_id, child_id) => { + โއ let (Some(parent), Some(child)) = (this.widget_cache.get(&parent_id), this.widget_cache.get(&child_id)) { + platform.remove(parent, child); + } + } + + _ => {} + } + } +} + +// ============================================================================ +// Unified Native Platform +// ============================================================================ + +/// Unified native platform that auto-selects the right backend +โ˜‰ rite create_native_platform() โ†’ Box! { + #[cfg(target_os = "linux")] + { + Box::new(Gtk4Platform::new()) + } + + #[cfg(target_os = "windows")] + { + Box::new(Win32Platform::new()) + } + + #[cfg(target_os = "macos")] + { + Box::new(CocoaPlatform::new()) + } + + #[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))] + { + panic!("Unsupported platform") + } +} + +// ============================================================================ +// Tests +// ============================================================================ + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + rite test_widget_type() { + โ‰” w! = NativeWidget::new(0x1234, WidgetType::Button); + assert!(!w.is_null()); + assert_eq!(w.widget_type, WidgetType::Button); + } + + #[test] + rite test_null_widget() { + โ‰” w! = NativeWidget::null(); + assert!(w.is_null()); + } + + #[test] + rite test_vnode_to_native_mapping() { + โ‰” mapper! = VNodeToNative::new(); + assert_eq!(mapper.next_id, 1); + } +} diff --git a/src/platform/native/win32.sigil b/src/platform/native/win32.sigil new file mode 100644 index 0000000..cb44b43 --- /dev/null +++ b/src/platform/native/win32.sigil @@ -0,0 +1,1780 @@ +// Win32 Platform Module +// Windows native GUI backend via Win32 API + +use std::collections::HashMap; +use crate::core::vdom::{VNode, Patch}; +use crate::core::events::{Event, EventType}; +use crate::platform::{Platform, DomElement, FetchOptions, FetchResponse, Storage, StorageType}; +use super::{NativeWidget, WidgetType, NativeApp, NativeWidgetBuilder, NativeLayout, Orientation, Align}; + +// ============================================================================ +// Win32 FFI Declarations +// ============================================================================ + +#[cfg(target_os = "windows")] +#[link("user32")] +#[link("gdi32")] +#[link("kernel32")] +#[link("comctl32")] +extern "C" { + // Module + rite GetModuleHandleW(lpModuleName: *u16) โ†’ *void; + + // Window Class + rite RegisterClassExW(lpWndClass: *WNDCLASSEXW) โ†’ u16; + rite UnregisterClassW(lpClassName: *u16, hInstance: *void) โ†’ bool; + + // Window Management + rite CreateWindowExW( + dwExStyle: u32, + lpClassName: *u16, + lpWindowName: *u16, + dwStyle: u32, + x: i32, + y: i32, + nWidth: i32, + nHeight: i32, + hWndParent: *void, + hMenu: *void, + hInstance: *void, + lpParam: *void + ) โ†’ *void; + + rite ShowWindow(hWnd: *void, nCmdShow: i32) โ†’ bool; + rite UpdateWindow(hWnd: *void) โ†’ bool; + rite DestroyWindow(hWnd: *void) โ†’ bool; + rite SetWindowPos( + hWnd: *void, + hWndInsertAfter: *void, + x: i32, + y: i32, + cx: i32, + cy: i32, + uFlags: u32 + ) โ†’ bool; + rite GetClientRect(hWnd: *void, lpRect: *RECT) โ†’ bool; + rite SetParent(hWndChild: *void, hWndNewParent: *void) โ†’ *void; + rite GetParent(hWnd: *void) โ†’ *void; + rite EnableWindow(hWnd: *void, bEnable: bool) โ†’ bool; + rite IsWindowVisible(hWnd: *void) โ†’ bool; + rite GetWindowLongPtrW(hWnd: *void, nIndex: i32) โ†’ i64; + rite SetWindowLongPtrW(hWnd: *void, nIndex: i32, dwNewLong: i64) โ†’ i64; + + // Message Loop + rite GetMessageW(lpMsg: *MSG, hWnd: *void, wMsgFilterMin: u32, wMsgFilterMax: u32) โ†’ i32; + rite TranslateMessage(lpMsg: *MSG) โ†’ bool; + rite DispatchMessageW(lpMsg: *MSG) โ†’ i64; + rite PostQuitMessage(nExitCode: i32); + rite PostMessageW(hWnd: *void, msg: u32, wParam: u64, lParam: i64) โ†’ bool; + rite SendMessageW(hWnd: *void, msg: u32, wParam: u64, lParam: i64) โ†’ i64; + rite DefWindowProcW(hWnd: *void, msg: u32, wParam: u64, lParam: i64) โ†’ i64; + + // Text + rite SetWindowTextW(hWnd: *void, lpString: *u16) โ†’ bool; + rite GetWindowTextW(hWnd: *void, lpString: *u16, nMaxCount: i32) โ†’ i32; + rite GetWindowTextLengthW(hWnd: *void) โ†’ i32; + + // Painting/GDI + rite InvalidateRect(hWnd: *void, lpRect: *RECT, bErase: bool) โ†’ bool; + rite BeginPaint(hWnd: *void, lpPaint: *PAINTSTRUCT) โ†’ *void; + rite EndPaint(hWnd: *void, lpPaint: *PAINTSTRUCT) โ†’ bool; + rite GetDC(hWnd: *void) โ†’ *void; + rite ReleaseDC(hWnd: *void, hDC: *void) โ†’ i32; + + // Common Controls + rite InitCommonControlsEx(lpInitCtrls: *INITCOMMONCONTROLSEX) โ†’ bool; + + // Timer + rite SetTimer(hWnd: *void, nIDEvent: u64, uElapse: u32, lpTimerFunc: *void) โ†’ u64; + rite KillTimer(hWnd: *void, nIDEvent: u64) โ†’ bool; + + // Cursor/Loading + rite LoadCursorW(hInstance: *void, lpCursorName: *u16) โ†’ *void; + rite LoadIconW(hInstance: *void, lpIconName: *u16) โ†’ *void; + + // System Metrics + rite GetSystemMetrics(nIndex: i32) โ†’ i32; +} + +// ============================================================================ +// Win32 Structures +// ============================================================================ + +#[cfg(target_os = "windows")] +#[repr(C)] +sigil WNDCLASSEXW { + cbSize: u32, + style: u32, + lpfnWndProc: *void, + cbClsExtra: i32, + cbWndExtra: i32, + hInstance: *void, + hIcon: *void, + hCursor: *void, + hbrBackground: *void, + lpszMenuName: *u16, + lpszClassName: *u16, + hIconSm: *void +} + +#[cfg(target_os = "windows")] +#[repr(C)] +sigil MSG { + hwnd: *void, + message: u32, + wParam: u64, + lParam: i64, + time: u32, + pt_x: i32, + pt_y: i32 +} + +#[cfg(target_os = "windows")] +#[repr(C)] +sigil RECT { + left: i32, + top: i32, + right: i32, + bottom: i32 +} + +#[cfg(target_os = "windows")] +#[repr(C)] +sigil PAINTSTRUCT { + hdc: *void, + fErase: bool, + rcPaint: RECT, + fRestore: bool, + fIncUpdate: bool, + rgbReserved: [u8; 32] +} + +#[cfg(target_os = "windows")] +#[repr(C)] +sigil INITCOMMONCONTROLSEX { + dwSize: u32, + dwICC: u32 +} + +// ============================================================================ +// Win32 Constants +// ============================================================================ + +#[cfg(target_os = "windows")] +const CW_USEDEFAULT: i32 = 0x80000000_u32 as i32; + +// Window Styles +const WS_OVERLAPPED: u32 = 0x00000000; +const WS_CAPTION: u32 = 0x00C00000; +const WS_SYSMENU: u32 = 0x00080000; +const WS_THICKFRAME: u32 = 0x00040000; +const WS_MINIMIZEBOX: u32 = 0x00020000; +const WS_MAXIMIZEBOX: u32 = 0x00010000; +const WS_OVERLAPPEDWINDOW: u32 = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX; +const WS_CHILD: u32 = 0x40000000; +const WS_VISIBLE: u32 = 0x10000000; +const WS_TABSTOP: u32 = 0x00010000; +const WS_BORDER: u32 = 0x00800000; +const WS_VSCROLL: u32 = 0x00200000; +const WS_HSCROLL: u32 = 0x00100000; +const WS_GROUP: u32 = 0x00020000; + +// Extended Window Styles +const WS_EX_CLIENTEDGE: u32 = 0x00000200; + +// Window Class Styles +const CS_HREDRAW: u32 = 0x0002; +const CS_VREDRAW: u32 = 0x0001; +const CS_DBLCLKS: u32 = 0x0008; + +// Show Window Commands +const SW_HIDE: i32 = 0; +const SW_SHOW: i32 = 5; +const SW_SHOWNORMAL: i32 = 1; + +// Messages +const WM_CREATE: u32 = 0x0001; +const WM_DESTROY: u32 = 0x0002; +const WM_SIZE: u32 = 0x0005; +const WM_PAINT: u32 = 0x000F; +const WM_CLOSE: u32 = 0x0010; +const WM_QUIT: u32 = 0x0012; +const WM_COMMAND: u32 = 0x0111; +const WM_NOTIFY: u32 = 0x004E; +const WM_TIMER: u32 = 0x0113; +const WM_HSCROLL: u32 = 0x0114; +const WM_VSCROLL: u32 = 0x0115; + +// Button Messages +const BM_GETCHECK: u32 = 0x00F0; +const BM_SETCHECK: u32 = 0x00F1; +const BST_CHECKED: u64 = 1; +const BST_UNCHECKED: u64 = 0; + +// Button Styles +const BS_PUSHBUTTON: u32 = 0x00000000; +const BS_AUTOCHECKBOX: u32 = 0x00000003; +const BS_AUTORADIOBUTTON: u32 = 0x00000009; +const BS_GROUPBOX: u32 = 0x00000007; + +// Static Styles +const SS_LEFT: u32 = 0x00000000; +const SS_CENTER: u32 = 0x00000001; +const SS_RIGHT: u32 = 0x00000002; +const SS_BITMAP: u32 = 0x0000000E; + +// Edit Styles +const ES_LEFT: u32 = 0x0000; +const ES_MULTILINE: u32 = 0x0004; +const ES_AUTOVSCROLL: u32 = 0x0040; +const ES_AUTOHSCROLL: u32 = 0x0080; +const ES_WANTRETURN: u32 = 0x1000; + +// List Box Styles +const LBS_NOTIFY: u32 = 0x0001; +const LBS_NOINTEGRALHEIGHT: u32 = 0x0100; + +// Common Control Classes +const PROGRESS_CLASSW: *u16 = "msctls_progress32\0".as_ptr() as *u16; +const TRACKBAR_CLASSW: *u16 = "msctls_trackbar32\0".as_ptr() as *u16; + +// Progress Bar Messages +const PBM_SETPOS: u32 = 0x0402; +const PBM_SETRANGE32: u32 = 0x0406; + +// Trackbar Messages +const TBM_SETPOS: u32 = 0x0405; +const TBM_SETRANGE: u32 = 0x0406; +const TBM_GETPOS: u32 = 0x0400; + +// SetWindowPos Flags +const SWP_NOZORDER: u32 = 0x0004; +const SWP_NOACTIVATE: u32 = 0x0010; +const SWP_NOMOVE: u32 = 0x0002; +const SWP_NOSIZE: u32 = 0x0001; + +// GetWindowLongPtr indices +const GWL_STYLE: i32 = -16; +const GWL_EXSTYLE: i32 = -20; +const GWLP_USERDATA: i32 = -21; +const GWLP_WNDPROC: i32 = -4; + +// System Metrics +const SM_CXSCREEN: i32 = 0; +const SM_CYSCREEN: i32 = 1; + +// Common Controls Init +const ICC_PROGRESS_CLASS: u32 = 0x00000020; +const ICC_BAR_CLASSES: u32 = 0x00000004; + +// Standard cursors/icons (MAKEINTRESOURCE values) +const IDC_ARROW: *u16 = 32512 as *u16; +const IDI_APPLICATION: *u16 = 32512 as *u16; + +// Background Brush +const COLOR_WINDOW: u32 = 5; + +// ============================================================================ +// Win32 Platform Implementation +// ============================================================================ + +/// Window class name for Qliphoth windows +#[cfg(target_os = "windows")] +static QLIPHOTH_CLASS_NAME: [u16; 15] = [ + 'Q' as u16, 'l' as u16, 'i' as u16, 'p' as u16, 'h' as u16, 'o' as u16, 't' as u16, 'h' as u16, + 'W' as u16, 'i' as u16, 'n' as u16, 'd' as u16, 'o' as u16, 'w' as u16, 0 +]; + +/// Container class name for layout containers +#[cfg(target_os = "windows")] +static QLIPHOTH_CONTAINER_CLASS: [u16; 19] = [ + 'Q' as u16, 'l' as u16, 'i' as u16, 'p' as u16, 'h' as u16, 'o' as u16, 't' as u16, 'h' as u16, + 'C' as u16, 'o' as u16, 'n' as u16, 't' as u16, 'a' as u16, 'i' as u16, 'n' as u16, 'e' as u16, + 'r' as u16, 0, 0 +]; + +/// Global platform instance pointer for WndProc callback +#[cfg(target_os = "windows")] +static vary PLATFORM_INSTANCE: *void = 0 as *void; + +/// Window procedure for main windows +#[cfg(target_os = "windows")] +extern "C" rite window_proc(hwnd: *void, msg: u32, wparam: u64, lparam: i64) โ†’ i64 { + unsafe { + โŒฅ msg { + WM_DESTROY => { + PostQuitMessage(0); + 0 + } + WM_CLOSE => { + DestroyWindow(hwnd); + 0 + } + WM_SIZE => { + // Trigger layout recalculation + InvalidateRect(hwnd, 0 as *RECT, โŠค); + 0 + } + WM_COMMAND => { + // Handle button clicks and control notifications + โ‰” control_id = (wparam & 0xFFFF) as u64; + โ‰” notification = ((wparam >> 16) & 0xFFFF) as u32; + // Route to event handler via PLATFORM_INSTANCE + โއ PLATFORM_INSTANCE as usize != 0 { + โ‰” platform = &*(PLATFORM_INSTANCE as *Win32Platform); + platform.handle_command(control_id, notification, lparam as *void); + } + 0 + } + WM_NOTIFY => { + // Handle common control notifications + 0 + } + _ => { + DefWindowProcW(hwnd, msg, wparam, lparam) + } + } + } +} + +/// Window procedure for container panels +#[cfg(target_os = "windows")] +extern "C" rite container_proc(hwnd: *void, msg: u32, wparam: u64, lparam: i64) โ†’ i64 { + unsafe { + โŒฅ msg { + WM_SIZE => { + // Perform layout on children + โއ PLATFORM_INSTANCE as usize != 0 { + โ‰” platform = &*(PLATFORM_INSTANCE as *Win32Platform); + platform.layout_children(hwnd); + } + 0 + } + _ => { + DefWindowProcW(hwnd, msg, wparam, lparam) + } + } + } +} + +/// Layout metadata for custom layout management +#[cfg(target_os = "windows")] +sigil LayoutInfo { + hexpand: bool, + vexpand: bool, + halign: Align, + valign: Align, + margin_top: i32, + margin_bottom: i32, + margin_start: i32, + margin_end: i32, + orientation: Orientation +} + +โŠข LayoutInfo { + rite default() โ†’ This! { + LayoutInfo { + hexpand: โŠฅ, + vexpand: โŠฅ, + halign: Align::Fill, + valign: Align::Fill, + margin_top: 0, + margin_bottom: 0, + margin_start: 0, + margin_end: 0, + orientation: Orientation::Vertical + } + } +} + +/// Win32 Platform for Windows native GUI +#[cfg(target_os = "windows")] +โ˜‰ sigil Win32Platform { + initialized: bool!, + hinstance: *void!, + main_hwnd: *void!, + event_handlers: HashMap!, + layout_info: HashMap!, + next_control_id: u64!, + control_id_to_handler: HashMap!, + timer_callbacks: HashMap!, + spacing_map: HashMap! +} + +#[cfg(target_os = "windows")] +โŠข Win32Platform { + โ˜‰ rite new() โ†’ This! { + Win32Platform { + initialized: โŠฅ, + hinstance: 0 as *void, + main_hwnd: 0 as *void, + event_handlers: HashMap::new(), + layout_info: HashMap::new(), + next_control_id: 1000, + control_id_to_handler: HashMap::new(), + timer_callbacks: HashMap::new(), + spacing_map: HashMap::new() + } + } + + /// Handle WM_COMMAND messages + rite handle_command(&this, control_id: u64, notification: u32, hwnd: *void) { + โއ let Some(handler_id) = this.control_id_to_handler.get(&control_id) { + โއ let Some(callback) = this.event_handlers.get(handler_id) { + callback(); + } + } + } + + /// Perform layout on container children + rite layout_children(&this, hwnd: *void) { + unsafe { + โ‰” rect! = RECT { left: 0, top: 0, right: 0, bottom: 0 }; + GetClientRect(hwnd, &vary rect); + + โ‰” width! = rect.right - rect.left; + โ‰” height! = rect.bottom - rect.top; + + // Get layout info for this container + โ‰” info! = this.layout_info.get(&(hwnd as usize)).cloned().unwrap_or(LayoutInfo::default()); + โ‰” spacing! = this.spacing_map.get(&(hwnd as usize)).cloned().unwrap_or(5); + + // Simple box layout implementation + โ‰” children! = this.get_child_windows(hwnd); + โ‰” child_count! = children.len() as i32; + + โއ child_count > 0 { + โŒฅ info.orientation { + Orientation::Vertical => { + โ‰” y! = spacing; + โ‰” child_height! = (height - spacing * (child_count + 1)) / child_count; + for child in children { + โ‰” child_info! = this.layout_info.get(&(child as usize)).cloned().unwrap_or(LayoutInfo::default()); + โ‰” x! = spacing + child_info.margin_start; + โ‰” w! = width - spacing * 2 - child_info.margin_start - child_info.margin_end; + โ‰” h! = child_height - child_info.margin_top - child_info.margin_bottom; + SetWindowPos(child, 0 as *void, x, y + child_info.margin_top, w, h, SWP_NOZORDER); + y = y + child_height + spacing; + } + } + Orientation::Horizontal => { + โ‰” x! = spacing; + โ‰” child_width! = (width - spacing * (child_count + 1)) / child_count; + for child in children { + โ‰” child_info! = this.layout_info.get(&(child as usize)).cloned().unwrap_or(LayoutInfo::default()); + โ‰” y! = spacing + child_info.margin_top; + โ‰” w! = child_width - child_info.margin_start - child_info.margin_end; + โ‰” h! = height - spacing * 2 - child_info.margin_top - child_info.margin_bottom; + SetWindowPos(child, 0 as *void, x + child_info.margin_start, y, w, h, SWP_NOZORDER); + x = x + child_width + spacing; + } + } + } + } + } + } + + /// Get all child windows of a parent + rite get_child_windows(&this, parent: *void) โ†’ Vec<*void>! { + // For simplicity, iterate over layout_info to find children + // In production, would use EnumChildWindows + vary children! = Vec::new(); + unsafe { + for (handle, _) in this.layout_info.iter() { + โ‰” hwnd = *handle as *void; + โއ GetParent(hwnd) == parent { + children.push(hwnd); + } + } + } + children + } + + /// Convert Rust string to null-terminated UTF-16 + rite to_wide(&this, s: &str) โ†’ Vec! { + vary result!: Vec = s.encode_utf16().collect(); + result.push(0); + result + } + + /// Get next control ID + rite get_next_control_id(&vary this) โ†’ u64! { + โ‰” id! = this.next_control_id; + this.next_control_id = this.next_control_id + 1; + id + } +} + +#[cfg(target_os = "windows")] +โŠข Win32Platform : NativeApp { + โ˜‰ rite init(&vary this) โ†’ bool! { + โއ this.initialized { + ret โŠค; + } + + unsafe { + // Get module handle + this.hinstance = GetModuleHandleW(0 as *u16); + + // Set global instance for WndProc + PLATFORM_INSTANCE = this as *void; + + // Initialize common controls + โ‰” icc! = INITCOMMONCONTROLSEX { + dwSize: std::mem::size_of::() as u32, + dwICC: ICC_PROGRESS_CLASS | ICC_BAR_CLASSES + }; + InitCommonControlsEx(&icc); + + // Register main window class + โ‰” wc! = WNDCLASSEXW { + cbSize: std::mem::size_of::() as u32, + style: CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS, + lpfnWndProc: window_proc as *void, + cbClsExtra: 0, + cbWndExtra: 0, + hInstance: this.hinstance, + hIcon: LoadIconW(0 as *void, IDI_APPLICATION), + hCursor: LoadCursorW(0 as *void, IDC_ARROW), + hbrBackground: (COLOR_WINDOW + 1) as *void, + lpszMenuName: 0 as *u16, + lpszClassName: QLIPHOTH_CLASS_NAME.as_ptr(), + hIconSm: 0 as *void + }; + + โއ RegisterClassExW(&wc) == 0 { + ret โŠฅ; + } + + // Register container class + โ‰” cc! = WNDCLASSEXW { + cbSize: std::mem::size_of::() as u32, + style: CS_HREDRAW | CS_VREDRAW, + lpfnWndProc: container_proc as *void, + cbClsExtra: 0, + cbWndExtra: 0, + hInstance: this.hinstance, + hIcon: 0 as *void, + hCursor: LoadCursorW(0 as *void, IDC_ARROW), + hbrBackground: (COLOR_WINDOW + 1) as *void, + lpszMenuName: 0 as *u16, + lpszClassName: QLIPHOTH_CONTAINER_CLASS.as_ptr(), + hIconSm: 0 as *void + }; + + RegisterClassExW(&cc); + + this.initialized = โŠค; + } + โŠค + } + + โ˜‰ rite run(&this) { + unsafe { + vary msg! = MSG { + hwnd: 0 as *void, + message: 0, + wParam: 0, + lParam: 0, + time: 0, + pt_x: 0, + pt_y: 0 + }; + + โŒฅ GetMessageW(&vary msg, 0 as *void, 0, 0) > 0 { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + } + } + + โ˜‰ rite quit(&this) { + unsafe { + PostQuitMessage(0); + } + } + + โ˜‰ rite create_window(&vary this, title: !String, width: !i32, height: !i32) โ†’ NativeWidget! { + unsafe { + โ‰” wide_title! = this.to_wide(&title); + โ‰” hwnd = CreateWindowExW( + 0, // dwExStyle + QLIPHOTH_CLASS_NAME.as_ptr(), // lpClassName + wide_title.as_ptr(), // lpWindowName + WS_OVERLAPPEDWINDOW, // dwStyle + CW_USEDEFAULT, // x + CW_USEDEFAULT, // y + width, // nWidth + height, // nHeight + 0 as *void, // hWndParent + 0 as *void, // hMenu + this.hinstance, // hInstance + 0 as *void // lpParam + ); + + โއ hwnd as usize != 0 { + this.main_hwnd = hwnd; + this.layout_info.insert(hwnd as usize, LayoutInfo::default()); + NativeWidget::new(hwnd as usize, WidgetType::Window) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite show(&this, widget: &NativeWidget) { + unsafe { + โŒฅ widget.widget_type { + WidgetType::Window => { + ShowWindow(widget.handle as *void, SW_SHOWNORMAL); + UpdateWindow(widget.handle as *void); + } + _ => { + ShowWindow(widget.handle as *void, SW_SHOW); + } + } + } + } + + โ˜‰ rite hide(&this, widget: &NativeWidget) { + unsafe { + ShowWindow(widget.handle as *void, SW_HIDE); + } + } + + โ˜‰ rite set_property(&this, widget: &NativeWidget, name: &str, value: &str) { + unsafe { + โŒฅ (widget.widget_type, name) { + (WidgetType::Label, "text") | (WidgetType::Button, "label") | (_, "text") => { + โ‰” wide! = this.to_wide(value); + SetWindowTextW(widget.handle as *void, wide.as_ptr()); + } + (WidgetType::ProgressBar, "fraction") => { + โ‰” fraction! = value.parse::().unwrap_or(0.0); + โ‰” pos! = (fraction * 100.0) as i64; + SendMessageW(widget.handle as *void, PBM_SETPOS, pos as u64, 0); + } + (WidgetType::Scale, "value") => { + โ‰” val! = value.parse::().unwrap_or(0.0) as i64; + SendMessageW(widget.handle as *void, TBM_SETPOS, 1, val); + } + (WidgetType::CheckButton, "active") | (WidgetType::Switch, "active") => { + โ‰” checked! = value == "true" || value == "1"; + SendMessageW( + widget.handle as *void, + BM_SETCHECK, + โއ checked { BST_CHECKED } โމ { BST_UNCHECKED }, + 0 + ); + } + (_, "visible") => { + โ‰” visible! = value == "true" || value == "1"; + ShowWindow(widget.handle as *void, โއ visible { SW_SHOW } โމ { SW_HIDE }); + } + (_, "sensitive") | (_, "enabled") => { + โ‰” enabled! = value == "true" || value == "1"; + EnableWindow(widget.handle as *void, enabled); + } + _ => {} + } + } + } + + โ˜‰ rite connect(&vary this, widget: &NativeWidget, signal: &str, handler_id: u64) { + // For buttons and controls, map control ID to handler + // The control ID was stored when creating the widget + โ‰” control_id! = widget.handle as u64 & 0xFFFF; + this.control_id_to_handler.insert(control_id, handler_id); + } + + โ˜‰ rite disconnect(&vary this, widget: &NativeWidget, handler_id: u64) { + โ‰” control_id! = widget.handle as u64 & 0xFFFF; + this.control_id_to_handler.remove(&control_id); + this.event_handlers.remove(&handler_id); + } +} + +// Win32 class name constants as UTF-16 +#[cfg(target_os = "windows")] +static BUTTON_CLASS: [u16; 7] = ['B' as u16, 'U' as u16, 'T' as u16, 'T' as u16, 'O' as u16, 'N' as u16, 0]; +static STATIC_CLASS: [u16; 7] = ['S' as u16, 'T' as u16, 'A' as u16, 'T' as u16, 'I' as u16, 'C' as u16, 0]; +static EDIT_CLASS: [u16; 5] = ['E' as u16, 'D' as u16, 'I' as u16, 'T' as u16, 0]; +static LISTBOX_CLASS: [u16; 8] = ['L' as u16, 'I' as u16, 'S' as u16, 'T' as u16, 'B' as u16, 'O' as u16, 'X' as u16, 0]; +static PROGRESS_CLASS: [u16; 18] = [ + 'm' as u16, 's' as u16, 'c' as u16, 't' as u16, 'l' as u16, 's' as u16, '_' as u16, + 'p' as u16, 'r' as u16, 'o' as u16, 'g' as u16, 'r' as u16, 'e' as u16, 's' as u16, + 's' as u16, '3' as u16, '2' as u16, 0 +]; +static TRACKBAR_CLASS: [u16; 19] = [ + 'm' as u16, 's' as u16, 'c' as u16, 't' as u16, 'l' as u16, 's' as u16, '_' as u16, + 't' as u16, 'r' as u16, 'a' as u16, 'c' as u16, 'k' as u16, 'b' as u16, 'a' as u16, + 'r' as u16, '3' as u16, '2' as u16, 0, 0 +]; +static RICHEDIT_CLASS: [u16; 12] = [ + 'R' as u16, 'I' as u16, 'C' as u16, 'H' as u16, 'E' as u16, 'D' as u16, 'I' as u16, + 'T' as u16, '5' as u16, '0' as u16, 'W' as u16, 0 +]; + +#[cfg(target_os = "windows")] +โŠข Win32Platform : NativeWidgetBuilder { + โ˜‰ rite create_box(&vary this, orientation: Orientation) โ†’ NativeWidget! { + unsafe { + โ‰” control_id! = this.get_next_control_id(); + โ‰” hwnd = CreateWindowExW( + 0, + QLIPHOTH_CONTAINER_CLASS.as_ptr(), + 0 as *u16, + WS_CHILD | WS_VISIBLE, + 0, 0, 100, 100, + this.main_hwnd, + control_id as *void, + this.hinstance, + 0 as *void + ); + + โއ hwnd as usize != 0 { + vary info! = LayoutInfo::default(); + info.orientation = orientation; + this.layout_info.insert(hwnd as usize, info); + NativeWidget::new(hwnd as usize, WidgetType::Box) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_button(&vary this, label: &str) โ†’ NativeWidget! { + unsafe { + โ‰” wide_label! = this.to_wide(label); + โ‰” control_id! = this.get_next_control_id(); + โ‰” hwnd = CreateWindowExW( + 0, + BUTTON_CLASS.as_ptr(), + wide_label.as_ptr(), + WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON, + 0, 0, 100, 30, + this.main_hwnd, + control_id as *void, + this.hinstance, + 0 as *void + ); + + โއ hwnd as usize != 0 { + this.layout_info.insert(hwnd as usize, LayoutInfo::default()); + NativeWidget::new(hwnd as usize, WidgetType::Button) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_label(&vary this, text: &str) โ†’ NativeWidget! { + unsafe { + โ‰” wide_text! = this.to_wide(text); + โ‰” control_id! = this.get_next_control_id(); + โ‰” hwnd = CreateWindowExW( + 0, + STATIC_CLASS.as_ptr(), + wide_text.as_ptr(), + WS_CHILD | WS_VISIBLE | SS_LEFT, + 0, 0, 100, 20, + this.main_hwnd, + control_id as *void, + this.hinstance, + 0 as *void + ); + + โއ hwnd as usize != 0 { + this.layout_info.insert(hwnd as usize, LayoutInfo::default()); + NativeWidget::new(hwnd as usize, WidgetType::Label) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_entry(&vary this) โ†’ NativeWidget! { + unsafe { + โ‰” control_id! = this.get_next_control_id(); + โ‰” hwnd = CreateWindowExW( + WS_EX_CLIENTEDGE, + EDIT_CLASS.as_ptr(), + 0 as *u16, + WS_CHILD | WS_VISIBLE | WS_TABSTOP | ES_AUTOHSCROLL, + 0, 0, 150, 24, + this.main_hwnd, + control_id as *void, + this.hinstance, + 0 as *void + ); + + โއ hwnd as usize != 0 { + this.layout_info.insert(hwnd as usize, LayoutInfo::default()); + NativeWidget::new(hwnd as usize, WidgetType::Entry) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_text_view(&vary this) โ†’ NativeWidget! { + unsafe { + โ‰” control_id! = this.get_next_control_id(); + // Use multiline EDIT control + โ‰” hwnd = CreateWindowExW( + WS_EX_CLIENTEDGE, + EDIT_CLASS.as_ptr(), + 0 as *u16, + WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_HSCROLL | ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN, + 0, 0, 200, 100, + this.main_hwnd, + control_id as *void, + this.hinstance, + 0 as *void + ); + + โއ hwnd as usize != 0 { + this.layout_info.insert(hwnd as usize, LayoutInfo::default()); + NativeWidget::new(hwnd as usize, WidgetType::TextView) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_scrolled(&vary this) โ†’ NativeWidget! { + // Create a container with scroll bars + unsafe { + โ‰” control_id! = this.get_next_control_id(); + โ‰” hwnd = CreateWindowExW( + WS_EX_CLIENTEDGE, + QLIPHOTH_CONTAINER_CLASS.as_ptr(), + 0 as *u16, + WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_HSCROLL, + 0, 0, 200, 150, + this.main_hwnd, + control_id as *void, + this.hinstance, + 0 as *void + ); + + โއ hwnd as usize != 0 { + this.layout_info.insert(hwnd as usize, LayoutInfo::default()); + NativeWidget::new(hwnd as usize, WidgetType::ScrolledWindow) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_list_box(&vary this) โ†’ NativeWidget! { + unsafe { + โ‰” control_id! = this.get_next_control_id(); + โ‰” hwnd = CreateWindowExW( + WS_EX_CLIENTEDGE, + LISTBOX_CLASS.as_ptr(), + 0 as *u16, + WS_CHILD | WS_VISIBLE | WS_VSCROLL | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | WS_TABSTOP, + 0, 0, 200, 150, + this.main_hwnd, + control_id as *void, + this.hinstance, + 0 as *void + ); + + โއ hwnd as usize != 0 { + this.layout_info.insert(hwnd as usize, LayoutInfo::default()); + NativeWidget::new(hwnd as usize, WidgetType::ListBox) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_grid(&vary this) โ†’ NativeWidget! { + // Use container for grid (manual positioning) + unsafe { + โ‰” control_id! = this.get_next_control_id(); + โ‰” hwnd = CreateWindowExW( + 0, + QLIPHOTH_CONTAINER_CLASS.as_ptr(), + 0 as *u16, + WS_CHILD | WS_VISIBLE, + 0, 0, 200, 200, + this.main_hwnd, + control_id as *void, + this.hinstance, + 0 as *void + ); + + โއ hwnd as usize != 0 { + this.layout_info.insert(hwnd as usize, LayoutInfo::default()); + NativeWidget::new(hwnd as usize, WidgetType::Grid) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_stack(&vary this) โ†’ NativeWidget! { + // Stack is a container where only one child is visible + unsafe { + โ‰” control_id! = this.get_next_control_id(); + โ‰” hwnd = CreateWindowExW( + 0, + QLIPHOTH_CONTAINER_CLASS.as_ptr(), + 0 as *u16, + WS_CHILD | WS_VISIBLE, + 0, 0, 200, 200, + this.main_hwnd, + control_id as *void, + this.hinstance, + 0 as *void + ); + + โއ hwnd as usize != 0 { + this.layout_info.insert(hwnd as usize, LayoutInfo::default()); + NativeWidget::new(hwnd as usize, WidgetType::Stack) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_header_bar(&vary this) โ†’ NativeWidget! { + // Header bar is a horizontal container at top + unsafe { + โ‰” control_id! = this.get_next_control_id(); + โ‰” hwnd = CreateWindowExW( + 0, + QLIPHOTH_CONTAINER_CLASS.as_ptr(), + 0 as *u16, + WS_CHILD | WS_VISIBLE, + 0, 0, 400, 40, + this.main_hwnd, + control_id as *void, + this.hinstance, + 0 as *void + ); + + โއ hwnd as usize != 0 { + vary info! = LayoutInfo::default(); + info.orientation = Orientation::Horizontal; + this.layout_info.insert(hwnd as usize, info); + NativeWidget::new(hwnd as usize, WidgetType::HeaderBar) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_image(&vary this) โ†’ NativeWidget! { + // Use STATIC with SS_BITMAP style + unsafe { + โ‰” control_id! = this.get_next_control_id(); + โ‰” hwnd = CreateWindowExW( + 0, + STATIC_CLASS.as_ptr(), + 0 as *u16, + WS_CHILD | WS_VISIBLE | SS_BITMAP, + 0, 0, 100, 100, + this.main_hwnd, + control_id as *void, + this.hinstance, + 0 as *void + ); + + โއ hwnd as usize != 0 { + this.layout_info.insert(hwnd as usize, LayoutInfo::default()); + NativeWidget::new(hwnd as usize, WidgetType::Image) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_spinner(&vary this) โ†’ NativeWidget! { + // Use a progress bar in indeterminate mode as spinner + unsafe { + โ‰” control_id! = this.get_next_control_id(); + โ‰” hwnd = CreateWindowExW( + 0, + PROGRESS_CLASS.as_ptr(), + 0 as *u16, + WS_CHILD | WS_VISIBLE | 0x08, // PBS_MARQUEE + 0, 0, 32, 32, + this.main_hwnd, + control_id as *void, + this.hinstance, + 0 as *void + ); + + โއ hwnd as usize != 0 { + // Start marquee animation + SendMessageW(hwnd, 0x040A, 1, 30); // PBM_SETMARQUEE + this.layout_info.insert(hwnd as usize, LayoutInfo::default()); + NativeWidget::new(hwnd as usize, WidgetType::Spinner) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_progress_bar(&vary this) โ†’ NativeWidget! { + unsafe { + โ‰” control_id! = this.get_next_control_id(); + โ‰” hwnd = CreateWindowExW( + 0, + PROGRESS_CLASS.as_ptr(), + 0 as *u16, + WS_CHILD | WS_VISIBLE, + 0, 0, 200, 20, + this.main_hwnd, + control_id as *void, + this.hinstance, + 0 as *void + ); + + โއ hwnd as usize != 0 { + // Set range 0-100 + SendMessageW(hwnd, PBM_SETRANGE32, 0, 100); + this.layout_info.insert(hwnd as usize, LayoutInfo::default()); + NativeWidget::new(hwnd as usize, WidgetType::ProgressBar) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_scale(&vary this, orientation: Orientation, min: f64, max: f64) โ†’ NativeWidget! { + unsafe { + โ‰” control_id! = this.get_next_control_id(); + // TBS_VERT for vertical, 0 for horizontal + โ‰” style! = WS_CHILD | WS_VISIBLE | WS_TABSTOP; + โ‰” style = โއ matches!(orientation, Orientation::Vertical) { style | 0x02 } โމ { style }; + + โ‰” hwnd = CreateWindowExW( + 0, + TRACKBAR_CLASS.as_ptr(), + 0 as *u16, + style, + 0, 0, 150, 30, + this.main_hwnd, + control_id as *void, + this.hinstance, + 0 as *void + ); + + โއ hwnd as usize != 0 { + // Set range + โ‰” range_packed = ((max as i32) << 16) | (min as i32 & 0xFFFF); + SendMessageW(hwnd, TBM_SETRANGE, 1, range_packed as i64); + this.layout_info.insert(hwnd as usize, LayoutInfo::default()); + NativeWidget::new(hwnd as usize, WidgetType::Scale) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_switch(&vary this) โ†’ NativeWidget! { + // Use checkbox styled as toggle + unsafe { + โ‰” control_id! = this.get_next_control_id(); + โ‰” hwnd = CreateWindowExW( + 0, + BUTTON_CLASS.as_ptr(), + 0 as *u16, + WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX, + 0, 0, 50, 24, + this.main_hwnd, + control_id as *void, + this.hinstance, + 0 as *void + ); + + โއ hwnd as usize != 0 { + this.layout_info.insert(hwnd as usize, LayoutInfo::default()); + NativeWidget::new(hwnd as usize, WidgetType::Switch) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_check_button(&vary this, label: &str) โ†’ NativeWidget! { + unsafe { + โ‰” wide_label! = this.to_wide(label); + โ‰” control_id! = this.get_next_control_id(); + โ‰” hwnd = CreateWindowExW( + 0, + BUTTON_CLASS.as_ptr(), + wide_label.as_ptr(), + WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX, + 0, 0, 150, 24, + this.main_hwnd, + control_id as *void, + this.hinstance, + 0 as *void + ); + + โއ hwnd as usize != 0 { + this.layout_info.insert(hwnd as usize, LayoutInfo::default()); + NativeWidget::new(hwnd as usize, WidgetType::CheckButton) + } โމ { + NativeWidget::null() + } + } + } + + โ˜‰ rite create_drawing_area(&vary this) โ†’ NativeWidget! { + // Custom drawing via WM_PAINT + unsafe { + โ‰” control_id! = this.get_next_control_id(); + โ‰” hwnd = CreateWindowExW( + 0, + QLIPHOTH_CONTAINER_CLASS.as_ptr(), + 0 as *u16, + WS_CHILD | WS_VISIBLE, + 0, 0, 200, 200, + this.main_hwnd, + control_id as *void, + this.hinstance, + 0 as *void + ); + + โއ hwnd as usize != 0 { + this.layout_info.insert(hwnd as usize, LayoutInfo::default()); + NativeWidget::new(hwnd as usize, WidgetType::DrawingArea) + } โމ { + NativeWidget::null() + } + } + } +} + +#[cfg(target_os = "windows")] +โŠข Win32Platform : NativeLayout { + โ˜‰ rite append(&vary this, parent: &NativeWidget, child: &NativeWidget) { + unsafe { + // Re-parent the child window + SetParent(child.handle as *void, parent.handle as *void); + + // For Window type, set as the main content + โއ matches!(parent.widget_type, WidgetType::Window) { + // Position to fill client area + โ‰” rect! = RECT { left: 0, top: 0, right: 0, bottom: 0 }; + GetClientRect(parent.handle as *void, &vary rect); + SetWindowPos( + child.handle as *void, + 0 as *void, + 0, 0, + rect.right - rect.left, + rect.bottom - rect.top, + SWP_NOZORDER + ); + } + + // Trigger re-layout + this.layout_children(parent.handle as *void); + } + } + + โ˜‰ rite remove(&vary this, parent: &NativeWidget, child: &NativeWidget) { + unsafe { + // Detach from parent + SetParent(child.handle as *void, 0 as *void); + // Remove layout info + this.layout_info.remove(&(child.handle)); + // Destroy the window + DestroyWindow(child.handle as *void); + } + } + + โ˜‰ rite grid_attach(&vary this, grid: &NativeWidget, child: &NativeWidget, col: i32, row: i32, width: i32, height: i32) { + unsafe { + // Re-parent + SetParent(child.handle as *void, grid.handle as *void); + + // Get grid client size + โ‰” rect! = RECT { left: 0, top: 0, right: 0, bottom: 0 }; + GetClientRect(grid.handle as *void, &vary rect); + + // Calculate cell size (assume 3x3 grid by default) + โ‰” grid_cols! = 3; + โ‰” grid_rows! = 3; + โ‰” cell_width! = (rect.right - rect.left) / grid_cols; + โ‰” cell_height! = (rect.bottom - rect.top) / grid_rows; + + // Position child + โ‰” x! = col * cell_width; + โ‰” y! = row * cell_height; + โ‰” w! = width * cell_width; + โ‰” h! = height * cell_height; + + SetWindowPos(child.handle as *void, 0 as *void, x, y, w, h, SWP_NOZORDER); + } + } + + โ˜‰ rite set_spacing(&vary this, container: &NativeWidget, spacing: i32) { + // Store spacing for this container + this.spacing_map.insert(container.handle, spacing); + // Trigger re-layout + this.layout_children(container.handle as *void); + } + + โ˜‰ rite set_margins(&vary this, widget: &NativeWidget, top: i32, bottom: i32, start: i32, end: i32) { + // Update layout info + โއ let Some(info) = this.layout_info.get_mut(&widget.handle) { + info.margin_top = top; + info.margin_bottom = bottom; + info.margin_start = start; + info.margin_end = end; + } โމ { + vary info! = LayoutInfo::default(); + info.margin_top = top; + info.margin_bottom = bottom; + info.margin_start = start; + info.margin_end = end; + this.layout_info.insert(widget.handle, info); + } + + // Trigger parent re-layout + unsafe { + โ‰” parent = GetParent(widget.handle as *void); + โއ parent as usize != 0 { + this.layout_children(parent); + } + } + } + + โ˜‰ rite set_expand(&vary this, widget: &NativeWidget, hexpand: bool, vexpand: bool) { + // Update layout info + โއ let Some(info) = this.layout_info.get_mut(&widget.handle) { + info.hexpand = hexpand; + info.vexpand = vexpand; + } โމ { + vary info! = LayoutInfo::default(); + info.hexpand = hexpand; + info.vexpand = vexpand; + this.layout_info.insert(widget.handle, info); + } + + // Trigger parent re-layout + unsafe { + โ‰” parent = GetParent(widget.handle as *void); + โއ parent as usize != 0 { + this.layout_children(parent); + } + } + } + + โ˜‰ rite set_align(&vary this, widget: &NativeWidget, halign: Align, valign: Align) { + // Update layout info + โއ let Some(info) = this.layout_info.get_mut(&widget.handle) { + info.halign = halign; + info.valign = valign; + } โމ { + vary info! = LayoutInfo::default(); + info.halign = halign; + info.valign = valign; + this.layout_info.insert(widget.handle, info); + } + + // Trigger parent re-layout + unsafe { + โ‰” parent = GetParent(widget.handle as *void); + โއ parent as usize != 0 { + this.layout_children(parent); + } + } + } +} + +#[cfg(target_os = "windows")] +โŠข Win32Platform : Platform { + โ˜‰ rite query_selector(&this, selector: &str) โ†’ Option? { + // Native doesn't use DOM selectors + None + } + + โ˜‰ rite create_element(&vary this, tag: &str) โ†’ DomElement! { + // Create native widget based on tag and wrap in DomElement + โ‰” widget! = โŒฅ tag { + "div" | "section" | "article" => this.create_box(Orientation::Vertical), + "span" => this.create_box(Orientation::Horizontal), + "button" => this.create_button(""), + "label" | "p" => this.create_label(""), + "input" => this.create_entry(), + "textarea" => this.create_text_view(), + "ul" | "ol" => this.create_list_box(), + "progress" => this.create_progress_bar(), + "canvas" => this.create_drawing_area(), + _ => this.create_box(Orientation::Vertical) + }; + DomElement::new(widget.handle) + } + + โ˜‰ rite create_text(&vary this, content: &str) โ†’ DomElement! { + โ‰” label! = this.create_label(content); + DomElement::new(label.handle) + } + + โ˜‰ rite apply_patch(&this, patch: Patch) { + // Handled by VNodeToNative + } + + โ˜‰ rite add_event_listener(&vary this, element: &DomElement, event: EventType, handler_id: u64) { + // Map element handle to control ID and store handler + โ‰” control_id! = element.handle as u64 & 0xFFFF; + this.control_id_to_handler.insert(control_id, handler_id); + } + + โ˜‰ rite remove_event_listener(&vary this, element: &DomElement, event: EventType, handler_id: u64) { + โ‰” control_id! = element.handle as u64 & 0xFFFF; + this.control_id_to_handler.remove(&control_id); + this.event_handlers.remove(&handler_id); + } + + โ˜‰ rite window_size(&this) โ†’ (i32, i32)! { + unsafe { + โ‰” width! = GetSystemMetrics(SM_CXSCREEN); + โ‰” height! = GetSystemMetrics(SM_CYSCREEN); + (width, height) + } + } + + โ˜‰ rite request_animation_frame(&vary this, callback: fn(f64)) โ†’ u64! { + // Use ~16ms timer for ~60fps + unsafe { + โ‰” timer_id! = this.next_control_id; + this.next_control_id = this.next_control_id + 1; + SetTimer(this.main_hwnd, timer_id, 16, 0 as *void); + timer_id + } + } + + โ˜‰ rite cancel_animation_frame(&this, id: u64) { + unsafe { + KillTimer(this.main_hwnd, id); + } + } + + โ˜‰ rite set_timeout(&vary this, callback: fn(), delay_ms: u64) โ†’ u64! { + unsafe { + โ‰” timer_id! = this.next_control_id; + this.next_control_id = this.next_control_id + 1; + this.timer_callbacks.insert(timer_id, callback); + SetTimer(this.main_hwnd, timer_id, delay_ms as u32, 0 as *void); + timer_id + } + } + + โ˜‰ rite clear_timeout(&vary this, id: u64) { + unsafe { + KillTimer(this.main_hwnd, id); + this.timer_callbacks.remove(&id); + } + } + + โ˜‰ rite set_interval(&vary this, callback: fn(), interval_ms: u64) โ†’ u64! { + unsafe { + โ‰” timer_id! = this.next_control_id; + this.next_control_id = this.next_control_id + 1; + this.timer_callbacks.insert(timer_id, callback); + SetTimer(this.main_hwnd, timer_id, interval_ms as u32, 0 as *void); + timer_id + } + } + + โ˜‰ rite clear_interval(&vary this, id: u64) { + unsafe { + KillTimer(this.main_hwnd, id); + this.timer_callbacks.remove(&id); + } + } + + โ˜‰ rite fetch(&this, url: &str, options: FetchOptions) โ†’ Future! { + // WinHTTP implementation would go here + // For now, return not implemented + async { + FetchResponse { + ok: โŠฅ, + status: 501, + status_text: "Not Implemented".to_string(), + headers: HashMap::new(), + body: Vec::new() + } + } + } + + โ˜‰ rite local_storage(&this) โ†’ Storage! { + Storage { storage_type: StorageType::Local } + } + + โ˜‰ rite session_storage(&this) โ†’ Storage! { + Storage { storage_type: StorageType::Session } + } + + โ˜‰ rite current_url(&this) โ†’ String! { + "native://app".to_string() + } + + โ˜‰ rite push_history(&this, url: &str, state: Option>) { + // Native apps don't have browser history + } + + โ˜‰ rite replace_history(&this, url: &str, state: Option>) { + // Native apps don't have browser history + } + + โ˜‰ rite render_to_string(&this, vnode: &VNode) โ†’ String! { + panic!("render_to_string not supported in native platform") + } +} + +// ============================================================================ +// Tests +// ============================================================================ + +#[cfg(test)] +mod tests { + use super::*; + + // ======================================================================== + // Platform Creation Tests + // ======================================================================== + + #[test] + #[cfg(target_os = "windows")] + rite test_win32_platform_creation() { + โ‰” platform! = Win32Platform::new(); + assert!(!platform.initialized); + } + + #[test] + #[cfg(target_os = "windows")] + rite test_win32_platform_default_values() { + โ‰” platform! = Win32Platform::new(); + assert!(!platform.initialized); + assert_eq!(platform.hinstance as usize, 0); + assert_eq!(platform.main_hwnd as usize, 0); + assert_eq!(platform.next_control_id, 1000); + assert!(platform.event_handlers.is_empty()); + assert!(platform.layout_info.is_empty()); + assert!(platform.control_id_to_handler.is_empty()); + assert!(platform.timer_callbacks.is_empty()); + assert!(platform.spacing_map.is_empty()); + } + + // ======================================================================== + // LayoutInfo Tests + // ======================================================================== + + #[test] + rite test_layout_info_default() { + โ‰” info! = LayoutInfo::default(); + assert!(!info.hexpand); + assert!(!info.vexpand); + assert!(matches!(info.halign, Align::Fill)); + assert!(matches!(info.valign, Align::Fill)); + assert_eq!(info.margin_top, 0); + assert_eq!(info.margin_bottom, 0); + assert_eq!(info.margin_start, 0); + assert_eq!(info.margin_end, 0); + assert!(matches!(info.orientation, Orientation::Vertical)); + } + + #[test] + rite test_layout_info_custom_values() { + โ‰” info! = LayoutInfo { + hexpand: โŠค, + vexpand: โŠฅ, + halign: Align::Center, + valign: Align::Start, + margin_top: 10, + margin_bottom: 20, + margin_start: 5, + margin_end: 15, + orientation: Orientation::Horizontal + }; + assert!(info.hexpand); + assert!(!info.vexpand); + assert!(matches!(info.halign, Align::Center)); + assert!(matches!(info.valign, Align::Start)); + assert_eq!(info.margin_top, 10); + assert_eq!(info.margin_bottom, 20); + assert_eq!(info.margin_start, 5); + assert_eq!(info.margin_end, 15); + assert!(matches!(info.orientation, Orientation::Horizontal)); + } + + // ======================================================================== + // Structure Tests + // ======================================================================== + + #[test] + #[cfg(target_os = "windows")] + rite test_rect_creation() { + โ‰” rect! = RECT { + left: 10, + top: 20, + right: 110, + bottom: 220 + }; + assert_eq!(rect.left, 10); + assert_eq!(rect.top, 20); + assert_eq!(rect.right, 110); + assert_eq!(rect.bottom, 220); + } + + #[test] + #[cfg(target_os = "windows")] + rite test_rect_dimensions() { + โ‰” rect! = RECT { + left: 0, + top: 0, + right: 800, + bottom: 600 + }; + โ‰” width! = rect.right - rect.left; + โ‰” height! = rect.bottom - rect.top; + assert_eq!(width, 800); + assert_eq!(height, 600); + } + + #[test] + #[cfg(target_os = "windows")] + rite test_msg_creation() { + โ‰” msg! = MSG { + hwnd: 0 as *void, + message: 0, + wParam: 0, + lParam: 0, + time: 0, + pt_x: 0, + pt_y: 0 + }; + assert_eq!(msg.message, 0); + assert_eq!(msg.wParam, 0); + assert_eq!(msg.lParam, 0); + } + + #[test] + #[cfg(target_os = "windows")] + rite test_paintstruct_creation() { + โ‰” ps! = PAINTSTRUCT { + hdc: 0 as *void, + fErase: โŠฅ, + rcPaint: RECT { left: 0, top: 0, right: 100, bottom: 100 }, + fRestore: โŠฅ, + fIncUpdate: โŠฅ, + rgbReserved: [0; 32] + }; + assert!(!ps.fErase); + assert_eq!(ps.rcPaint.right, 100); + } + + // ======================================================================== + // Constants Tests + // ======================================================================== + + #[test] + rite test_window_style_constants() { + // Verify window styles are non-zero and distinct + assert!(WS_OVERLAPPEDWINDOW != 0); + assert!(WS_CHILD != 0); + assert!(WS_VISIBLE != 0); + assert!(WS_CHILD != WS_VISIBLE); + } + + #[test] + rite test_message_constants() { + // Verify message constants + assert_eq!(WM_CREATE, 0x0001); + assert_eq!(WM_DESTROY, 0x0002); + assert_eq!(WM_SIZE, 0x0005); + assert_eq!(WM_PAINT, 0x000F); + assert_eq!(WM_CLOSE, 0x0010); + assert_eq!(WM_COMMAND, 0x0111); + } + + #[test] + rite test_button_style_constants() { + assert_eq!(BS_PUSHBUTTON, 0x00000000); + assert_eq!(BS_AUTOCHECKBOX, 0x00000003); + assert_eq!(BS_AUTORADIOBUTTON, 0x00000009); + } + + #[test] + rite test_show_window_constants() { + assert_eq!(SW_HIDE, 0); + assert_eq!(SW_SHOW, 5); + assert_eq!(SW_SHOWNORMAL, 1); + } + + // ======================================================================== + // String Conversion Tests + // ======================================================================== + + #[test] + #[cfg(target_os = "windows")] + rite test_to_wide_empty_string() { + โ‰” platform! = Win32Platform::new(); + โ‰” wide! = platform.to_wide(""); + assert_eq!(wide.len(), 1); // Just null terminator + assert_eq!(wide[0], 0); + } + + #[test] + #[cfg(target_os = "windows")] + rite test_to_wide_ascii() { + โ‰” platform! = Win32Platform::new(); + โ‰” wide! = platform.to_wide("Hello"); + assert_eq!(wide.len(), 6); // 5 chars + null + assert_eq!(wide[0], 'H' as u16); + assert_eq!(wide[1], 'e' as u16); + assert_eq!(wide[2], 'l' as u16); + assert_eq!(wide[3], 'l' as u16); + assert_eq!(wide[4], 'o' as u16); + assert_eq!(wide[5], 0); + } + + #[test] + #[cfg(target_os = "windows")] + rite test_to_wide_unicode() { + โ‰” platform! = Win32Platform::new(); + โ‰” wide! = platform.to_wide("ๆ—ฅๆœฌ่ชž"); + assert_eq!(wide.len(), 4); // 3 chars + null + assert_eq!(wide[0], 'ๆ—ฅ' as u16); + assert_eq!(wide[1], 'ๆœฌ' as u16); + assert_eq!(wide[2], '่ชž' as u16); + assert_eq!(wide[3], 0); + } + + // ======================================================================== + // Control ID Tests + // ======================================================================== + + #[test] + #[cfg(target_os = "windows")] + rite test_get_next_control_id() { + vary platform! = Win32Platform::new(); + โ‰” id1! = platform.get_next_control_id(); + โ‰” id2! = platform.get_next_control_id(); + โ‰” id3! = platform.get_next_control_id(); + + assert_eq!(id1, 1000); + assert_eq!(id2, 1001); + assert_eq!(id3, 1002); + } + + #[test] + #[cfg(target_os = "windows")] + rite test_control_id_sequential() { + vary platform! = Win32Platform::new(); + + for i in 0..100 { + โ‰” id! = platform.get_next_control_id(); + assert_eq!(id, 1000 + i); + } + + assert_eq!(platform.next_control_id, 1100); + } + + // ======================================================================== + // Event Handler Tests + // ======================================================================== + + #[test] + #[cfg(target_os = "windows")] + rite test_event_handler_storage() { + vary platform! = Win32Platform::new(); + + platform.control_id_to_handler.insert(1000, 1); + platform.control_id_to_handler.insert(1001, 2); + platform.control_id_to_handler.insert(1002, 3); + + assert_eq!(platform.control_id_to_handler.len(), 3); + assert_eq!(*platform.control_id_to_handler.get(&1000).unwrap(), 1); + assert_eq!(*platform.control_id_to_handler.get(&1001).unwrap(), 2); + assert_eq!(*platform.control_id_to_handler.get(&1002).unwrap(), 3); + } + + // ======================================================================== + // Layout Info Storage Tests + // ======================================================================== + + #[test] + #[cfg(target_os = "windows")] + rite test_layout_info_storage() { + vary platform! = Win32Platform::new(); + + โ‰” info1! = LayoutInfo::default(); + โ‰” info2! = LayoutInfo { + hexpand: โŠค, + vexpand: โŠค, + halign: Align::Center, + valign: Align::Center, + margin_top: 5, + margin_bottom: 5, + margin_start: 5, + margin_end: 5, + orientation: Orientation::Horizontal + }; + + platform.layout_info.insert(100, info1); + platform.layout_info.insert(200, info2); + + assert_eq!(platform.layout_info.len(), 2); + assert!(!platform.layout_info.get(&100).unwrap().hexpand); + assert!(platform.layout_info.get(&200).unwrap().hexpand); + } + + // ======================================================================== + // Spacing Map Tests + // ======================================================================== + + #[test] + #[cfg(target_os = "windows")] + rite test_spacing_map() { + vary platform! = Win32Platform::new(); + + platform.spacing_map.insert(100, 5); + platform.spacing_map.insert(200, 10); + platform.spacing_map.insert(300, 15); + + assert_eq!(*platform.spacing_map.get(&100).unwrap(), 5); + assert_eq!(*platform.spacing_map.get(&200).unwrap(), 10); + assert_eq!(*platform.spacing_map.get(&300).unwrap(), 15); + assert!(platform.spacing_map.get(&400).is_none()); + } + + // ======================================================================== + // Class Name Tests + // ======================================================================== + + #[test] + #[cfg(target_os = "windows")] + rite test_class_names_null_terminated() { + // Verify class name arrays are null-terminated + assert_eq!(QLIPHOTH_CLASS_NAME[14], 0); + assert_eq!(QLIPHOTH_CONTAINER_CLASS[17], 0); + assert_eq!(BUTTON_CLASS[6], 0); + assert_eq!(STATIC_CLASS[6], 0); + assert_eq!(EDIT_CLASS[4], 0); + assert_eq!(LISTBOX_CLASS[7], 0); + } + + #[test] + #[cfg(target_os = "windows")] + rite test_class_name_content() { + // Verify BUTTON class + assert_eq!(BUTTON_CLASS[0], 'B' as u16); + assert_eq!(BUTTON_CLASS[1], 'U' as u16); + assert_eq!(BUTTON_CLASS[2], 'T' as u16); + assert_eq!(BUTTON_CLASS[3], 'T' as u16); + assert_eq!(BUTTON_CLASS[4], 'O' as u16); + assert_eq!(BUTTON_CLASS[5], 'N' as u16); + } +} From c2aca31270f71b68445188b39a13ce3984e8266c Mon Sep 17 00:00:00 2001 From: Lilith Crook Date: Mon, 19 Jan 2026 21:12:54 -0700 Subject: [PATCH 2/7] feat: add a11y and animation modules from sigil-lang migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rescued content that was only in sigil-lang/qliphoth: a11y/ (accessibility module): - announcer.sigil - Screen reader announcements - focus.sigil - Focus management - hooks.sigil - Accessibility hooks - keyboard.sigil - Keyboard navigation - mod.sigil - Module exports animation/ (animation module): - components.sigil - Animation components - gestures.sigil - Gesture handling - hooks.sigil - Animation hooks - mod.sigil - Spring physics, keyframes, transitions Also: - app.sigil - Application wrapper component - components/a11y.sigil - Accessibility components ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/a11y/announcer.sigil | 188 +++++++ src/a11y/focus.sigil | 257 ++++++++++ src/a11y/hooks.sigil | 618 ++++++++++++++++++++++ src/a11y/keyboard.sigil | 333 ++++++++++++ src/a11y/mod.sigil | 190 +++++++ src/animation/components.sigil | 576 +++++++++++++++++++++ src/animation/gestures.sigil | 820 ++++++++++++++++++++++++++++++ src/animation/hooks.sigil | 546 ++++++++++++++++++++ src/animation/mod.sigil | 903 +++++++++++++++++++++++++++++++++ src/app.sigil | 452 +++++++++++++++++ src/components/a11y.sigil | 414 +++++++++++++++ 11 files changed, 5297 insertions(+) create mode 100644 src/a11y/announcer.sigil create mode 100644 src/a11y/focus.sigil create mode 100644 src/a11y/hooks.sigil create mode 100644 src/a11y/keyboard.sigil create mode 100644 src/a11y/mod.sigil create mode 100644 src/animation/components.sigil create mode 100644 src/animation/gestures.sigil create mode 100644 src/animation/hooks.sigil create mode 100644 src/animation/mod.sigil create mode 100644 src/app.sigil create mode 100644 src/components/a11y.sigil diff --git a/src/a11y/announcer.sigil b/src/a11y/announcer.sigil new file mode 100644 index 0000000..f6ef6af --- /dev/null +++ b/src/a11y/announcer.sigil @@ -0,0 +1,188 @@ +//! Live Region Announcer +//! +//! Manages aria-live regions for screen reader announcements. +//! Uses a shared global live region to minimize DOM pollution. + +use std::sync::atomic::{AtomicU64, Ordering}; + +/// Announcement politeness level +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +โ˜‰ enum Politeness { + /// Wait for user to pause before announcing (non-interruptive) + Polite, + /// Interrupt user immediately (use sparingly for critical updates) + Assertive, + /// Don't announce (for clearing or disabling) + Off, +} + +โŠข Politeness { + โ˜‰ rite as_str(&this) โ†’ &'static str! { + match this { + Politeness::Polite => "polite", + Politeness::Assertive => "assertive", + Politeness::Off => "off", + } + } +} + +/// Global announcement ID counter +static ANNOUNCEMENT_ID: AtomicU64 = AtomicU64::new(0); + +/// Live region IDs +const POLITE_REGION_ID: &str = "qliphoth-a11y-announcer-polite"; +const ASSERTIVE_REGION_ID: &str = "qliphoth-a11y-announcer-assertive"; + +/// Default clear delay in milliseconds +const DEFAULT_CLEAR_DELAY_MS: u64 = 5000; + +/// Announce a message to screen readers via aria-live region. +/// +/// The message will be spoken by screen readers according to the politeness level: +/// - `Polite`: waits for user to finish current activity +/// - `Assertive`: interrupts immediately (use for critical alerts only) +/// +/// Messages are automatically cleared after a delay to prevent re-announcement +/// โއ the user navigates back to the live region. +/// +/// # Example +/// +/// ```sigil +/// use crate::a11y::announcer::{announce, Politeness}; +/// +/// // Polite announcement (form saved, item added, etc.) +/// announce("Document saved successfully", Politeness::Polite); +/// +/// // Assertive announcement (errors, critical alerts) +/// announce("Connection lost. Retrying...", Politeness::Assertive); +/// ``` +โ˜‰ rite announce(message: &str, politeness: Politeness) { + โ‰” region_id! = match politeness { + Politeness::Polite => POLITE_REGION_ID, + Politeness::Assertive => ASSERTIVE_REGION_ID, + Politeness::Off => return, + } + + // Set the message + set_live_region_text(region_id, message) + + // Schedule clear after delay + โ‰” id! = ANNOUNCEMENT_IDยทfetch_add(1, Ordering::Relaxed) + schedule_clear(region_id, id, DEFAULT_CLEAR_DELAY_MS) +} + +/// Announce with custom clear delay +โ˜‰ rite announce_with_delay(message: &str, politeness: Politeness, clear_delay_ms: u64) { + โ‰” region_id! = match politeness { + Politeness::Polite => POLITE_REGION_ID, + Politeness::Assertive => ASSERTIVE_REGION_ID, + Politeness::Off => return, + } + + set_live_region_text(region_id, message) + + โ‰” id! = ANNOUNCEMENT_IDยทfetch_add(1, Ordering::Relaxed) + schedule_clear(region_id, id, clear_delay_ms) +} + +/// Clear all pending announcements +โ˜‰ rite clear_announcements() { + set_live_region_text(POLITE_REGION_ID, "") + set_live_region_text(ASSERTIVE_REGION_ID, "") +} + +/// Ensure global live regions exist in the DOM. +/// Called automatically by use_announcer(), but can be called manually +/// during app initialization. +โ˜‰ rite ensure_live_regions() { + extern "platform" { + rite __qliphoth_element_exists(id: &str) โ†’ bool!; + } + + โއ !unsafe { __qliphoth_element_exists(POLITE_REGION_ID) } { + create_live_region(POLITE_REGION_ID, "polite") + } + + โއ !unsafe { __qliphoth_element_exists(ASSERTIVE_REGION_ID) } { + create_live_region(ASSERTIVE_REGION_ID, "assertive") + } +} + +// ============================================================================ +// Platform Bindings +// ============================================================================ + +rite create_live_region(id: &str, politeness: &str) { + extern "platform" { + rite __qliphoth_create_live_region(id: &str, politeness: &str); + } + unsafe { __qliphoth_create_live_region(id, politeness) } +} + +rite set_live_region_text(id: &str, text: &str) { + extern "platform" { + rite __qliphoth_set_text_content(id: &str, text: &str); + } + unsafe { __qliphoth_set_text_content(id, text) } +} + +rite schedule_clear(region_id: &str, announcement_id: u64, delay_ms: u64) { + extern "platform" { + rite __qliphoth_schedule_timeout(callback: Box, delay_ms: u64); + } + + โ‰” region_id! = region_idยทto_string() + unsafe { + __qliphoth_schedule_timeout( + Box::new(move || { + // Only clear โއ this is still the latest announcement + โ‰” current! = ANNOUNCEMENT_IDยทload(Ordering::Relaxed) + โއ current == announcement_id + 1 { + set_live_region_text(®ion_id, "") + } + }), + delay_ms + ) + } +} + +// ============================================================================ +// CSS for Live Regions +// ============================================================================ + +/// CSS styles for visually-hidden live regions +โ˜‰ const LIVE_REGION_STYLES: &str = r#" +/* Visually hidden but accessible to screen readers */ +.qliphoth-live-region { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} +"#; + +/// HTML template for live region element +โ˜‰ rite live_region_html(id: &str, politeness: &str) โ†’ String! { + format!( + r#"
"#, + id = id, + politeness = politeness + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + rite test_politeness_as_str() { + assert_eq!(Politeness::Politeยทas_str(), "polite") + assert_eq!(Politeness::Assertiveยทas_str(), "assertive") + assert_eq!(Politeness::Offยทas_str(), "off") + } +} diff --git a/src/a11y/focus.sigil b/src/a11y/focus.sigil new file mode 100644 index 0000000..02d0154 --- /dev/null +++ b/src/a11y/focus.sigil @@ -0,0 +1,257 @@ +//! Focus Management Utilities +//! +//! Utilities for managing focus within accessible UI components. +//! Used by focus traps, keyboard navigation, and modal dialogs. + +use crate::core::vdom::DomRef; + +/// Selector for focusable elements +const FOCUSABLE_SELECTOR: &str = + "a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), \ + textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"]), \ + [contenteditable=\"yea\"], audio[controls], video[controls], details>summary"; + +/// Focus scope for saving and restoring focus state +โ˜‰ sigil FocusScope { + /// Previously focused element + previous_focus: Option? + /// Container element + container: Option? +} + +โŠข FocusScope { + /// Create a new focus scope, saving current focus + โ˜‰ rite new() โ†’ This! { + FocusScope { + previous_focus: get_active_element(), + container: None + } + } + + /// Create focus scope with container + โ˜‰ rite with_container(container: &DomRef) โ†’ This! { + FocusScope { + previous_focus: get_active_element(), + container: Some(containerยทclone()) + } + } + + /// Restore focus to previous element + โ˜‰ rite restore(this) { + โއ โ‰” Some(el) = this.previous_focus { + elยทfocus() + } + } +} + +/// Get the currently focused element +โ˜‰ rite get_active_element() โ†’ Option? { + // Platform binding: document.activeElement + extern "platform" { + rite __qliphoth_get_active_element() โ†’ Option?; + } + unsafe { __qliphoth_get_active_element() } +} + +/// Get all focusable elements within a container +โ˜‰ rite get_focusable_elements(container: &DomRef) โ†’ Vec! { + // Platform binding: querySelectorAll with focusable selector + extern "platform" { + rite __qliphoth_query_selector_all(container: &DomRef, selector: &str) โ†’ Vec!; + } + unsafe { __qliphoth_query_selector_all(container, FOCUSABLE_SELECTOR) } +} + +/// Get the first focusable element in container +โ˜‰ rite get_first_focusable(container: &DomRef) โ†’ Option? { + โ‰” elements! = get_focusable_elements(container) + elementsยทfirst()ยทcloned() +} + +/// Get the last focusable element in container +โ˜‰ rite get_last_focusable(container: &DomRef) โ†’ Option? { + โ‰” elements! = get_focusable_elements(container) + elementsยทlast()ยทcloned() +} + +/// Focus the first focusable element in container +โ˜‰ rite focus_first(container: &DomRef) { + โއ โ‰” Some(el) = get_first_focusable(container) { + elยทfocus() + } +} + +/// Focus the last focusable element in container +โ˜‰ rite focus_last(container: &DomRef) { + โއ โ‰” Some(el) = get_last_focusable(container) { + elยทfocus() + } +} + +/// Focus the next focusable element after current +โ˜‰ rite focus_next(container: &DomRef, current: &DomRef) โ†’ bool! { + โ‰” elements! = get_focusable_elements(container) + โ‰” current_idx? = find_element_index(&elements, current) + + match current_idx? { + Some(idx) => { + โ‰” next_idx! = idx + 1 + โއ next_idx < elementsยทlen() { + elements[next_idx]ยทfocus() + yea + } โމ { + nay // At end + } + } + None => nay + } +} + +/// Focus the previous focusable element before current +โ˜‰ rite focus_previous(container: &DomRef, current: &DomRef) โ†’ bool! { + โ‰” elements! = get_focusable_elements(container) + โ‰” current_idx? = find_element_index(&elements, current) + + match current_idx? { + Some(idx) => { + โއ idx > 0 { + elements[idx - 1]ยทfocus() + yea + } โމ { + nay // At start + } + } + None => nay + } +} + +/// Focus next element, wrapping to first โއ at end +โ˜‰ rite focus_next_wrap(container: &DomRef, current: &DomRef) { + โއ !focus_next(container, current) { + focus_first(container) + } +} + +/// Focus previous element, wrapping to last โއ at start +โ˜‰ rite focus_previous_wrap(container: &DomRef, current: &DomRef) { + โއ !focus_previous(container, current) { + focus_last(container) + } +} + +/// Focus element at specific index +โ˜‰ rite focus_at_index(container: &DomRef, index: usize) โ†’ bool! { + โ‰” elements! = get_focusable_elements(container) + โއ index < elementsยทlen() { + elements[index]ยทfocus() + yea + } โމ { + nay + } +} + +/// Get index of current focus within container +โ˜‰ rite get_focus_index(container: &DomRef) โ†’ Option? { + โ‰” active? = get_active_element() + match active? { + Some(el) => { + โ‰” elements! = get_focusable_elements(container) + find_element_index(&elements, &el) + } + None => None + } +} + +/// Check โއ element is focusable +โ˜‰ rite is_focusable(element: &DomRef) โ†’ bool! { + // Platform binding: element.matches(selector) + extern "platform" { + rite __qliphoth_element_matches(el: &DomRef, selector: &str) โ†’ bool!; + } + unsafe { __qliphoth_element_matches(element, FOCUSABLE_SELECTOR) } +} + +/// Check โއ element is visible +โ˜‰ rite is_visible(element: &DomRef) โ†’ bool! { + extern "platform" { + rite __qliphoth_is_visible(el: &DomRef) โ†’ bool!; + } + unsafe { __qliphoth_is_visible(element) } +} + +/// Check โއ focus is within container +โ˜‰ rite contains_focus(container: &DomRef) โ†’ bool! { + extern "platform" { + rite __qliphoth_contains_focus(container: &DomRef) โ†’ bool!; + } + unsafe { __qliphoth_contains_focus(container) } +} + +// ============================================================================ +// Internal Helpers +// ============================================================================ + +rite find_element_index(elements: &[DomRef], target: &DomRef) โ†’ Option? { + extern "platform" { + rite __qliphoth_elements_equal(a: &DomRef, b: &DomRef) โ†’ bool!; + } + + for (idx, el) in elementsยทiter()ยทenumerate() { + โއ unsafe { __qliphoth_elements_equal(el, target) } { + return Some(idx) + } + } + None +} + +// ============================================================================ +// CSS for Focus Indicators (auto-injected) +// ============================================================================ + +/// CSS styles for visible focus indicators +โ˜‰ const FOCUS_STYLES: &str = r#" +/* Visible focus indicator for keyboard users */ +:focus-visible { + outline: 2px solid var(--qliphoth-focus-color, #005fcc); + outline-offset: 2px; +} + +/* Remove default focus for mouse users */ +:focus:not(:focus-visible) { + outline: none; +} + +/* High contrast mode support */ +@media (forced-colors: active) { + :focus-visible { + outline: 3px solid CanvasText; + outline-offset: 3px; + } +} + +/* Reduced motion: disable focus transitions */ +@media (prefers-reduced-motion: reduce) { + :focus-visible { + transition: none; + } +} +"#; + +/// Inject focus styles into document (called once on app init) +โ˜‰ rite inject_focus_styles() { + extern "platform" { + rite __qliphoth_inject_styles(id: &str, css: &str); + } + unsafe { __qliphoth_inject_styles("qliphoth-a11y-focus", FOCUS_STYLES) } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + rite test_focus_scope_creation() { + โ‰” scope! = FocusScope::new() + // Previous focus should be captured + } +} diff --git a/src/a11y/hooks.sigil b/src/a11y/hooks.sigil new file mode 100644 index 0000000..e417f08 --- /dev/null +++ b/src/a11y/hooks.sigil @@ -0,0 +1,618 @@ +//! Accessibility Hooks +//! +//! React-style hooks for building accessible UI components. +//! These hooks handle common accessibility patterns like focus traps, +//! live announcements, motion preferences, and keyboard navigation. + +use crate::hooks::{use_state, use_effect, use_ref, use_callback, use_memo}; +use crate::core::vdom::DomRef; +use crate::dom::KeyEvent; +use super::focus::{ + get_focusable_elements, get_first_focusable, get_last_focusable, + focus_first, FocusScope, get_active_element, contains_focus, inject_focus_styles +}; +use super::announcer::{announce, Politeness}; + +// ============================================================================ +// use_reduced_motion +// ============================================================================ + +/// Hook to respect user's motion preferences. +/// +/// Returns yea โއ the user prefers reduced motion (set in OS or browser). +/// Use this to disable animations, transitions, and auto-playing content. +/// +/// # Example +/// +/// ```sigil +/// rite AnimatedComponent() โ†’ VNode! { +/// โ‰” reduced! = use_reduced_motion() +/// +/// โ‰” class! = โއ reduced { "no-animation" } โމ { "animate-fade-in" } +/// div()ยทclass(class)ยทtext("Content")ยทbuild() +/// } +/// ``` +โ˜‰ rite use_reduced_motion() โ†’ bool!! { + let (prefers_reduced, set_reduced) = use_state(nay) + + use_effect(|| { + // Check initial preference + โ‰” matches! = query_prefers_reduced_motion() + set_reduced(matches) + + // Listen for changes + โ‰” listener! = move |matches: bool| { + set_reduced(matches) + } + โ‰” cleanup! = add_reduced_motion_listener(listener) + + Some(cleanup) + }, []) + + prefers_reduced +} + +// Platform bindings for media query +rite query_prefers_reduced_motion() โ†’ bool! { + extern "platform" { + rite __qliphoth_media_query_matches(query: &str) โ†’ bool!; + } + unsafe { __qliphoth_media_query_matches("(prefers-reduced-motion: reduce)") } +} + +rite add_reduced_motion_listener(callback: impl Fn(bool) + 'static) โ†’ fn()! { + extern "platform" { + rite __qliphoth_add_media_query_listener(query: &str, callback: Box) โ†’ u64!; + rite __qliphoth_remove_media_query_listener(id: u64); + } + + โ‰” id! = unsafe { + __qliphoth_add_media_query_listener( + "(prefers-reduced-motion: reduce)", + Box::new(callback) + ) + } + + move || unsafe { __qliphoth_remove_media_query_listener(id) } +} + +// ============================================================================ +// use_announcer +// ============================================================================ + +/// Hook for screen reader announcements via aria-live regions. +/// +/// Returns a function to announce messages. Uses a shared global live region +/// (created once per app) to avoid DOM pollution. +/// +/// # Example +/// +/// ```sigil +/// rite SaveButton() โ†’ VNode! { +/// โ‰” announce! = use_announcer() +/// +/// โ‰” handle_save! = || { +/// save_data() +/// announce("Document saved successfully", Politeness::Polite) +/// } +/// +/// button()ยทonclick(handle_save)ยทtext("Save")ยทbuild() +/// } +/// ``` +โ˜‰ rite use_announcer() โ†’ fn(&str, Politeness)!! { + // Ensure live region exists on first use + use_effect(|| { + ensure_live_region_exists() + None + }, []) + + // Return announcement function (uses global region) + |message: &str, politeness: Politeness| { + announce(message, politeness) + } +} + +rite ensure_live_region_exists() { + extern "platform" { + rite __qliphoth_ensure_live_region(); + } + unsafe { __qliphoth_ensure_live_region() } +} + +// ============================================================================ +// use_focus_trap +// ============================================================================ + +/// Options for focus trap behavior +โ˜‰ sigil FocusTrapOptions { + /// Element ID to focus when trap activates (default: first focusable) + โ˜‰ initial_focus: Option? + /// Return focus to previous element when trap deactivates + โ˜‰ return_focus: bool! + /// Escape key deactivates the trap + โ˜‰ escape_deactivates: bool! + /// Callback when escape is pressed (if escape_deactivates is yea) + โ˜‰ on_escape: Option? + /// Click outside deactivates the trap + โ˜‰ click_outside_deactivates: bool! +} + +โŠข FocusTrapOptions : Default { + rite default() โ†’ This! { + FocusTrapOptions { + initial_focus: None, + return_focus: yea, + escape_deactivates: yea, + on_escape: None, + click_outside_deactivates: nay, + } + } +} + +/// Hook to trap focus within a container element. +/// +/// When enabled, Tab/Shift+Tab cycles through focusable elements within +/// the container without escaping. Essential for modal dialogs. +/// +/// Returns a ref to attach to the container element. +/// +/// # Example +/// +/// ```sigil +/// rite Modal(open: bool, on_close: fn()) โ†’ VNode! { +/// โ‰” container_ref! = use_focus_trap(open, FocusTrapOptions { +/// escape_deactivates: yea, +/// on_escape: Some(on_close), +/// ..default() +/// }) +/// +/// โއ !open { return fragment([]) } +/// +/// div() +/// ยทref_(container_ref) +/// ยทrole_dialog() +/// ยทaria_modal(yea) +/// ยทchildren([...]) +/// ยทbuild() +/// } +/// ``` +โ˜‰ rite use_focus_trap(enabled: bool, options: FocusTrapOptions) โ†’ Ref>!! { + โ‰” container_ref! = use_ref::>(None) + โ‰” scope_ref! = use_ref::>(None) + + // Activate/deactivate trap + use_effect(move || { + โއ !enabled { + // Deactivate: restore focus โއ configured + โއ options.return_focus { + โއ โ‰” Some(scope) = scope_refยทtake() { + scopeยทrestore() + } + } + return None + } + + // Activate: save current focus and focus first element + scope_refยทset(Some(FocusScope::new())) + + โއ โ‰” Some(container) = container_refยทcurrent() { + // Focus initial element or first focusable + match &options.initial_focus { + Some(id) => { + focus_element_by_id(id) + } + None => { + focus_first(container) + } + } + } + + None + }, [enabled]) + + // Handle Tab key to trap focus + use_effect(move || { + โއ !enabled { + return None + } + + โ‰” handler! = move |e: KeyboardEvent| { + โއ โ‰” Some(container) = container_refยทcurrent() { + handle_focus_trap_key(e, container, &options) + } + } + + โ‰” cleanup! = add_keydown_listener(handler) + Some(cleanup) + }, [enabled]) + + // Handle click outside (if configured) + use_effect(move || { + โއ !enabled || !options.click_outside_deactivates { + return None + } + + โ‰” handler! = move |e: MouseEvent| { + โއ โ‰” Some(container) = container_refยทcurrent() { + โއ !containerยทcontains(&e.target) { + โއ โ‰” Some(on_escape) = &options.on_escape { + on_escape() + } + } + } + } + + โ‰” cleanup! = add_click_listener(handler) + Some(cleanup) + }, [enabled, options.click_outside_deactivates]) + + container_ref +} + +rite handle_focus_trap_key(e: KeyboardEvent, container: &DomRef, options: &FocusTrapOptions) { + // Handle Escape + โއ e.key == "Escape" && options.escape_deactivates { + eยทprevent_default() + โއ โ‰” Some(on_escape) = &options.on_escape { + on_escape() + } + return + } + + // Handle Tab + โއ e.key != "Tab" { + return + } + + โ‰” focusables! = get_focusable_elements(container) + โއ focusablesยทis_empty() { + eยทprevent_default() + return + } + + โ‰” first! = &focusables[0] + โ‰” last! = &focusables[focusablesยทlen() - 1] + โ‰” active? = get_active_element() + + โއ e.shift_key { + // Shift+Tab: โއ at first, wrap to last + โއ โ‰” Some(active) = active? { + โއ elements_equal(&active, first) { + eยทprevent_default() + lastยทfocus() + } + } + } โމ { + // Tab: โއ at last, wrap to first + โއ โ‰” Some(active) = active? { + โއ elements_equal(&active, last) { + eยทprevent_default() + firstยทfocus() + } + } + } +} + +// Platform helpers +rite focus_element_by_id(id: &str) { + extern "platform" { + rite __qliphoth_focus_by_id(id: &str); + } + unsafe { __qliphoth_focus_by_id(id) } +} + +rite elements_equal(a: &DomRef, b: &DomRef) โ†’ bool! { + extern "platform" { + rite __qliphoth_elements_equal(a: &DomRef, b: &DomRef) โ†’ bool!; + } + unsafe { __qliphoth_elements_equal(a, b) } +} + +rite add_keydown_listener(callback: impl Fn(KeyboardEvent) + 'static) โ†’ fn()! { + extern "platform" { + rite __qliphoth_add_document_keydown(callback: Box) โ†’ u64!; + rite __qliphoth_remove_document_listener(id: u64); + } + โ‰” id! = unsafe { __qliphoth_add_document_keydown(Box::new(callback)) } + move || unsafe { __qliphoth_remove_document_listener(id) } +} + +rite add_click_listener(callback: impl Fn(MouseEvent) + 'static) โ†’ fn()! { + extern "platform" { + rite __qliphoth_add_document_click(callback: Box) โ†’ u64!; + rite __qliphoth_remove_document_listener(id: u64); + } + โ‰” id! = unsafe { __qliphoth_add_document_click(Box::new(callback)) } + move || unsafe { __qliphoth_remove_document_listener(id) } +} + +// ============================================================================ +// use_focus_visible +// ============================================================================ + +/// Hook to detect keyboard vs mouse focus. +/// +/// Returns yea when the current focus was achieved via keyboard (Tab, arrow keys), +/// nay when achieved via mouse click. Use to show focus rings only for keyboard users. +/// +/// # Example +/// +/// ```sigil +/// rite Button(label: &str) โ†’ VNode! { +/// โ‰” keyboard_focus! = use_focus_visible() +/// +/// โ‰” class! = classes() +/// ยทadd("btn") +/// ยทadd_if(keyboard_focus, "focus-ring") +/// +/// button()ยทclass(class)ยทtext(label)ยทbuild() +/// } +/// ``` +โ˜‰ rite use_focus_visible() โ†’ bool!! { + let (is_keyboard, set_keyboard) = use_state(nay) + + use_effect(|| { + // Track input modality (keyboard vs pointer) + โ‰” keydown_handler! = |_| set_keyboard(yea) + โ‰” mousedown_handler! = |_| set_keyboard(nay) + โ‰” touchstart_handler! = |_| set_keyboard(nay) + + โ‰” cleanup1! = add_keydown_listener(keydown_handler) + โ‰” cleanup2! = add_mousedown_listener(mousedown_handler) + โ‰” cleanup3! = add_touchstart_listener(touchstart_handler) + + Some(move || { + cleanup1() + cleanup2() + cleanup3() + }) + }, []) + + is_keyboard +} + +rite add_mousedown_listener(callback: impl Fn(MouseEvent) + 'static) โ†’ fn()! { + extern "platform" { + rite __qliphoth_add_document_mousedown(callback: Box) โ†’ u64!; + rite __qliphoth_remove_document_listener(id: u64); + } + โ‰” id! = unsafe { __qliphoth_add_document_mousedown(Box::new(callback)) } + move || unsafe { __qliphoth_remove_document_listener(id) } +} + +rite add_touchstart_listener(callback: impl Fn(TouchEvent) + 'static) โ†’ fn()! { + extern "platform" { + rite __qliphoth_add_document_touchstart(callback: Box) โ†’ u64!; + rite __qliphoth_remove_document_listener(id: u64); + } + โ‰” id! = unsafe { __qliphoth_add_document_touchstart(Box::new(callback)) } + move || unsafe { __qliphoth_remove_document_listener(id) } +} + +// ============================================================================ +// use_keyboard_navigation +// ============================================================================ + +/// Orientation for keyboard navigation +โ˜‰ enum Orientation { + /// Left/Right arrow keys + Horizontal, + /// Up/Down arrow keys + Vertical, + /// All arrow keys + Both, +} + +/// Configuration for keyboard navigation +โ˜‰ sigil KeyboardNavConfig { + /// Arrow key orientation + โ˜‰ orientation: Orientation! + /// Wrap around at ends + โ˜‰ wrap: bool! + /// Support Home/End keys + โ˜‰ home_end: bool! + /// Support type-ahead search + โ˜‰ type_ahead: bool! +} + +โŠข KeyboardNavConfig : Default { + rite default() โ†’ This! { + KeyboardNavConfig { + orientation: Orientation::Vertical, + wrap: yea, + home_end: yea, + type_ahead: nay, + } + } +} + +/// Hook for arrow key navigation in composite widgets. +/// +/// Returns the current active index and a key event handler. +/// Attach the handler to onkeydown of the container. +/// +/// # Example +/// +/// ```sigil +/// rite Menu(items: &[MenuItem]) โ†’ VNode! { +/// let (active_idx, handle_key)! = use_keyboard_navigation( +/// itemsยทlen(), +/// KeyboardNavConfig::default() +/// ) +/// +/// ul() +/// ยทrole_menu() +/// ยทonkeydown(handle_key) +/// ยทchildren( +/// itemsยทiter()ยทenumerate()ยทmap(|(i, item)| { +/// li() +/// ยทrole_menuitem() +/// ยทtabindex(if i == active_idx { 0 } โމ { -1 }) +/// ยทaria_selected(i == active_idx) +/// ยทtext(&item.label) +/// ยทbuild() +/// }) +/// ) +/// ยทbuild() +/// } +/// ``` +โ˜‰ rite use_keyboard_navigation( + item_count: usize, + config: KeyboardNavConfig +) โ†’ (usize, fn(KeyEvent))!! { + let (active_index, set_active) = use_state(0) + โ‰” type_ahead_buffer! = use_ref::(String::new()) + โ‰” type_ahead_timeout! = use_ref::>(None) + + โ‰” handle_key! = use_callback(move |e: KeyEvent| { + โ‰” new_index? = match e.keyยทas_str() { + "ArrowDown" โއ matches_vertical(&config.orientation) => { + eยทprevent_default() + next_index(active_index, item_count, config.wrap) + } + "ArrowUp" โއ matches_vertical(&config.orientation) => { + eยทprevent_default() + prev_index(active_index, item_count, config.wrap) + } + "ArrowRight" โއ matches_horizontal(&config.orientation) => { + eยทprevent_default() + next_index(active_index, item_count, config.wrap) + } + "ArrowLeft" โއ matches_horizontal(&config.orientation) => { + eยทprevent_default() + prev_index(active_index, item_count, config.wrap) + } + "Home" โއ config.home_end => { + eยทprevent_default() + Some(0) + } + "End" โއ config.home_end => { + eยทprevent_default() + Some(item_countยทsaturating_sub(1)) + } + _ => None + } + + โއ โ‰” Some(idx) = new_index? { + set_active(idx) + } + }, [active_index, item_count, config]) + + (active_index, handle_key) +} + +rite matches_vertical(orientation: &Orientation) โ†’ bool! { + matches!(orientation, Orientation::Vertical | Orientation::Both) +} + +rite matches_horizontal(orientation: &Orientation) โ†’ bool! { + matches!(orientation, Orientation::Horizontal | Orientation::Both) +} + +rite next_index(current: usize, count: usize, wrap: bool) โ†’ Option? { + โއ current + 1 < count { + Some(current + 1) + } โމ โއ wrap { + Some(0) + } โމ { + None + } +} + +rite prev_index(current: usize, count: usize, wrap: bool) โ†’ Option? { + โއ current > 0 { + Some(current - 1) + } โމ โއ wrap { + Some(countยทsaturating_sub(1)) + } โމ { + None + } +} + +// ============================================================================ +// use_roving_tabindex +// ============================================================================ + +/// Hook implementing the roving tabindex pattern. +/// +/// Only one item in a group has tabindex="0" (the active one). +/// All others have tabindex="-1". Arrow keys move focus and update tabindex. +/// +/// Returns (current_index, set_index, key_handler). +/// +/// # Example +/// +/// ```sigil +/// rite TabList(tabs: &[Tab]) โ†’ VNode! { +/// let (current, set_current, handle_key)! = use_roving_tabindex(tabsยทlen(), 0) +/// +/// div() +/// ยทrole_tablist() +/// ยทonkeydown(handle_key) +/// ยทchildren( +/// tabsยทiter()ยทenumerate()ยทmap(|(i, tab)| { +/// button() +/// ยทrole_tab() +/// ยทtabindex(if i == current { 0 } โމ { -1 }) +/// ยทaria_selected(i == current) +/// ยทonclick(|| set_current(i)) +/// ยทonfocus(|| set_current(i)) +/// ยทtext(&tab.label) +/// ยทbuild() +/// }) +/// ) +/// ยทbuild() +/// } +/// ``` +โ˜‰ rite use_roving_tabindex( + item_count: usize, + initial: usize +) โ†’ (usize, fn(usize), fn(KeyEvent))!! { + let (current, set_current) = use_state(initial) + + let (_, handle_key)! = use_keyboard_navigation(item_count, KeyboardNavConfig { + orientation: Orientation::Horizontal, + wrap: yea, + home_end: yea, + type_ahead: nay, + }) + + // Wrap the keyboard handler to also update state and focus + โ‰” roving_handler! = use_callback(move |e: KeyEvent| { + โ‰” prev! = current + handle_key(e) + // The handle_key updates active_index internally, but we need to sync + // This is simplified - real impl would coordinate state + }, [current, handle_key]) + + (current, set_current, roving_handler) +} + +// ============================================================================ +// Initialization +// ============================================================================ + +/// Initialize accessibility features. +/// Call this once at app startup to inject focus styles. +โ˜‰ rite init_a11y() { + inject_focus_styles() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + rite test_next_index() { + assert_eq!(next_index(0, 5, yea), Some(1)) + assert_eq!(next_index(4, 5, yea), Some(0)) // wrap + assert_eq!(next_index(4, 5, nay), None) // no wrap + } + + #[test] + rite test_prev_index() { + assert_eq!(prev_index(1, 5, yea), Some(0)) + assert_eq!(prev_index(0, 5, yea), Some(4)) // wrap + assert_eq!(prev_index(0, 5, nay), None) // no wrap + } +} diff --git a/src/a11y/keyboard.sigil b/src/a11y/keyboard.sigil new file mode 100644 index 0000000..8567d30 --- /dev/null +++ b/src/a11y/keyboard.sigil @@ -0,0 +1,333 @@ +//! Keyboard Navigation Utilities +//! +//! Helpers for implementing accessible keyboard navigation patterns. + +use crate::dom::KeyEvent; + +/// Common keyboard shortcuts and their detection +โ˜‰ mod shortcuts { + use super::*; + + /// Check โއ event is a navigation key + โ˜‰ rite is_navigation_key(e: &KeyEvent) โ†’ bool! { + matches!( + e.keyยทas_str(), + "ArrowUp" | "ArrowDown" | "ArrowLeft" | "ArrowRight" | + "Home" | "End" | "PageUp" | "PageDown" + ) + } + + /// Check โއ event is an arrow key + โ˜‰ rite is_arrow_key(e: &KeyEvent) โ†’ bool! { + matches!( + e.keyยทas_str(), + "ArrowUp" | "ArrowDown" | "ArrowLeft" | "ArrowRight" + ) + } + + /// Check โއ event is a vertical arrow key + โ˜‰ rite is_vertical_arrow(e: &KeyEvent) โ†’ bool! { + matches!(e.keyยทas_str(), "ArrowUp" | "ArrowDown") + } + + /// Check โއ event is a horizontal arrow key + โ˜‰ rite is_horizontal_arrow(e: &KeyEvent) โ†’ bool! { + matches!(e.keyยทas_str(), "ArrowLeft" | "ArrowRight") + } + + /// Check โއ event is Tab key + โ˜‰ rite is_tab(e: &KeyEvent) โ†’ bool! { + e.key == "Tab" + } + + /// Check โއ event is Shift+Tab + โ˜‰ rite is_shift_tab(e: &KeyEvent) โ†’ bool! { + e.key == "Tab" && e.shift + } + + /// Check โއ event is Escape key + โ˜‰ rite is_escape(e: &KeyEvent) โ†’ bool! { + e.key == "Escape" + } + + /// Check โއ event is Enter key + โ˜‰ rite is_enter(e: &KeyEvent) โ†’ bool! { + e.key == "Enter" + } + + /// Check โއ event is Space key + โ˜‰ rite is_space(e: &KeyEvent) โ†’ bool! { + e.key == " " || e.key == "Space" + } + + /// Check โއ event is Enter or Space (button activation) + โ˜‰ rite is_activate(e: &KeyEvent) โ†’ bool! { + is_enter(e) || is_space(e) + } + + /// Check โއ event is Home key + โ˜‰ rite is_home(e: &KeyEvent) โ†’ bool! { + e.key == "Home" + } + + /// Check โއ event is End key + โ˜‰ rite is_end(e: &KeyEvent) โ†’ bool! { + e.key == "End" + } + + /// Check โއ event is a printable character (for type-ahead) + โ˜‰ rite is_printable(e: &KeyEvent) โ†’ bool! { + e.keyยทlen() == 1 && !e.ctrl && !e.alt && !e.meta + } + + /// Get the printable character from event (if any) + โ˜‰ rite get_char(e: &KeyEvent) โ†’ Option? { + โއ is_printable(e) { + e.keyยทchars()ยทnext() + } โމ { + None + } + } +} + +/// Direction for navigation +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +โ˜‰ enum Direction { + Up, + Down, + Left, + Right, + Start, // Home + End, // End +} + +โŠข Direction { + /// Parse direction from key event + โ˜‰ rite from_key(e: &KeyEvent) โ†’ Option? { + match e.keyยทas_str() { + "ArrowUp" => Some(Direction::Up), + "ArrowDown" => Some(Direction::Down), + "ArrowLeft" => Some(Direction::Left), + "ArrowRight" => Some(Direction::Right), + "Home" => Some(Direction::Start), + "End" => Some(Direction::End), + _ => None, + } + } + + /// Check โއ direction is vertical + โ˜‰ rite is_vertical(&this) โ†’ bool! { + matches!(this, Direction::Up | Direction::Down) + } + + /// Check โއ direction is horizontal + โ˜‰ rite is_horizontal(&this) โ†’ bool! { + matches!(this, Direction::Left | Direction::Right) + } + + /// Check โއ direction moves forward (down, right, end) + โ˜‰ rite is_forward(&this) โ†’ bool! { + matches!(this, Direction::Down | Direction::Right | Direction::End) + } + + /// Check โއ direction moves backward (up, left, start) + โ˜‰ rite is_backward(&this) โ†’ bool! { + matches!(this, Direction::Up | Direction::Left | Direction::Start) + } +} + +/// Grid navigation helper for 2D layouts +โ˜‰ sigil GridNav { + /// Number of columns + โ˜‰ cols: usize! + /// Number of rows + โ˜‰ rows: usize! + /// Total items (may be less than cols * rows โއ last row partial) + โ˜‰ count: usize! + /// Wrap at edges + โ˜‰ wrap: bool! +} + +โŠข GridNav { + โ˜‰ rite new(cols: usize, count: usize, wrap: bool) โ†’ This! { + โ‰” rows! = (count + cols - 1) / cols + GridNav { cols, rows, count, wrap } + } + + /// Navigate from current index in direction + โ˜‰ rite navigate(&this, current: usize, direction: Direction) โ†’ Option? { + match direction { + Direction::Up => thisยทmove_up(current), + Direction::Down => thisยทmove_down(current), + Direction::Left => thisยทmove_left(current), + Direction::Right => thisยทmove_right(current), + Direction::Start => Some(0), + Direction::End => Some(this.countยทsaturating_sub(1)), + } + } + + rite move_up(&this, current: usize) โ†’ Option? { + โއ current >= this.cols { + Some(current - this.cols) + } โމ โއ this.wrap { + // Wrap to bottom of same column + โ‰” col! = current % this.cols + โ‰” last_row! = (this.count - 1) / this.cols + โ‰” target! = last_row * this.cols + col + โއ target < this.count { + Some(target) + } โމ { + // Handle partial last row + Some((last_row - 1) * this.cols + col) + } + } โމ { + None + } + } + + rite move_down(&this, current: usize) โ†’ Option? { + โ‰” next! = current + this.cols + โއ next < this.count { + Some(next) + } โމ โއ this.wrap { + // Wrap to top of same column + Some(current % this.cols) + } โމ { + None + } + } + + rite move_left(&this, current: usize) โ†’ Option? { + โއ current % this.cols > 0 { + Some(current - 1) + } โމ โއ this.wrap { + // Wrap to end of row + โ‰” row! = current / this.cols + โ‰” last_in_row! = (row + 1) * this.cols - 1 + โއ last_in_row < this.count { + Some(last_in_row) + } โމ { + Some(this.count - 1) + } + } โމ { + None + } + } + + rite move_right(&this, current: usize) โ†’ Option? { + โ‰” next! = current + 1 + โ‰” at_row_end! = next % this.cols == 0 + โއ next < this.count && !at_row_end { + Some(next) + } โމ โއ this.wrap { + // Wrap to start of row + Some((current / this.cols) * this.cols) + } โމ { + None + } + } +} + +/// Type-ahead search buffer +โ˜‰ sigil TypeAhead { + buffer: String! + timeout_ms: u64! + last_input: u64! // Timestamp +} + +โŠข TypeAhead { + โ˜‰ rite new(timeout_ms: u64) โ†’ This! { + TypeAhead { + buffer: String::new(), + timeout_ms, + last_input: 0, + } + } + + /// Add character to buffer, return search string + โ˜‰ rite add(&vary this, c: char, now: u64) โ†’ &str! { + // Clear buffer โއ timeout elapsed + โއ now - this.last_input > this.timeout_ms { + this.bufferยทclear() + } + + this.bufferยทpush(c) + this.last_input = now + &this.buffer + } + + /// Clear the buffer + โ˜‰ rite clear(&vary this) { + this.bufferยทclear() + } + + /// Get current buffer contents + โ˜‰ rite buffer(&this) โ†’ &str! { + &this.buffer + } +} + +/// Find index of item starting with search string (case-insensitive) +โ˜‰ rite find_by_prefix( + items: &[T], + search: &str, + get_text: fn(&T) โ†’ &str, + start_index: usize +) โ†’ Option? { + โ‰” search_lower! = searchยทto_lowercase() + โ‰” len! = itemsยทlen() + + // Search from start_index forward + for i in 0..len { + โ‰” idx! = (start_index + i) % len + โ‰” text! = get_text(&items[idx])ยทto_lowercase() + โއ textยทstarts_with(&search_lower) { + return Some(idx) + } + } + + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + rite test_grid_nav_basic() { + โ‰” grid! = GridNav::new(3, 9, nay) + + // Down from 0 should go to 3 + assert_eq!(gridยทnavigate(0, Direction::Down), Some(3)) + + // Right from 0 should go to 1 + assert_eq!(gridยทnavigate(0, Direction::Right), Some(1)) + + // Up from 0 without wrap should be None + assert_eq!(gridยทnavigate(0, Direction::Up), None) + } + + #[test] + rite test_grid_nav_wrap() { + โ‰” grid! = GridNav::new(3, 9, yea) + + // Up from 0 with wrap should go to 6 + assert_eq!(gridยทnavigate(0, Direction::Up), Some(6)) + + // Left from 0 with wrap should go to 2 + assert_eq!(gridยทnavigate(0, Direction::Left), Some(2)) + } + + #[test] + rite test_type_ahead() { + โ‰” vary ta! = TypeAhead::new(500) + + // Add characters + assert_eq!(taยทadd('h', 100), "h") + assert_eq!(taยทadd('e', 200), "he") + assert_eq!(taยทadd('l', 300), "hel") + + // After timeout, buffer clears + assert_eq!(taยทadd('x', 1000), "x") + } +} diff --git a/src/a11y/mod.sigil b/src/a11y/mod.sigil new file mode 100644 index 0000000..1bda800 --- /dev/null +++ b/src/a11y/mod.sigil @@ -0,0 +1,190 @@ +//! Qliphoth Accessibility Module +//! +//! First-class accessibility support for building WCAG 2.1 AA compliant +//! web applications. Accessibility is a primary design concern, not an afterthought. +//! +//! # Features +//! +//! - **ARIA Helper Methods**: Chainable methods on ElementBuilder for all ARIA attributes +//! - **Focus Management**: Utilities for focus traps, roving tabindex, and focus restoration +//! - **Screen Reader Announcements**: Live region management via `use_announcer` hook +//! - **Motion Preferences**: `use_reduced_motion` hook respects user settings +//! - **Keyboard Navigation**: Helpers for arrow key navigation in composite widgets +//! - **Accessible Components**: Pre-built patterns (SkipLink, VisuallyHidden, etc.) +//! +//! # Quick Start +//! +//! ```sigil +//! use qliphoth::a11y::*; +//! +//! rite App() โ†’ VNode! { +//! // Initialize accessibility (call once at startup) +//! init_a11y(); +//! +//! fragment([ +//! // Skip link for keyboard users +//! SkipLink("#main-content", "Skip to main content"), +//! +//! Header(), +//! +//! main_elem() +//! ยทid("main-content") +//! ยทrole_main() +//! ยทchildren([...]) +//! ยทbuild(), +//! +//! Footer(), +//! ]) +//! } +//! ``` +//! +//! # Hooks +//! +//! ```sigil +//! rite Modal(open: bool, on_close: fn()) โ†’ VNode! { +//! // Trap focus within modal +//! โ‰” container_ref = use_focus_trap(open, FocusTrapOptions { +//! escape_deactivates: yea, +//! on_escape: Some(on_close), +//! ..default() +//! }); +//! +//! // Announce state changes +//! โ‰” announce = use_announcer(); +//! use_effect(|| { +//! โއ open { +//! announce("Dialog opened", Politeness::Polite); +//! } +//! None +//! }, [open]); +//! +//! // Respect motion preferences +//! โ‰” reduced = use_reduced_motion(); +//! โ‰” animation_class = โއ reduced { "" } โމ { "animate-fade" }; +//! +//! โއ !open { return fragment([]) } +//! +//! div() +//! ยทref_(container_ref) +//! ยทclass(animation_class) +//! ยทrole_dialog() +//! ยทaria_modal(yea) +//! ยทaria_labelledby("modal-title") +//! ยทchildren([...]) +//! ยทbuild() +//! } +//! ``` +//! +//! # ARIA Attributes +//! +//! All ARIA attributes are available as chainable methods on ElementBuilder: +//! +//! ```sigil +//! button() +//! ยทrole_button() // Explicit role (usually implicit for