WASM Backend: - src/backend/wasm.zig: Browser backend using extern JS functions - web/zcatgui.js: Canvas 2D rendering bridge (~200 LOC) - web/index.html: Demo page with event handling - examples/wasm_demo.zig: Widget showcase for browser - Output: 18KB WASM binary Android Backend: - src/backend/android.zig: ANativeActivity + ANativeWindow - examples/android_demo.zig: Touch-enabled demo - Touch-to-mouse event mapping - Logging via __android_log_print - Targets: ARM64 (device), x86_64 (emulator) iOS Backend: - src/backend/ios.zig: UIKit bridge via extern C functions - ios/ZcatguiBridge.h: Objective-C header - ios/ZcatguiBridge.m: UIKit implementation (~320 LOC) - CADisplayLink render loop - Touch event queue with @synchronized - Targets: ARM64 (device), ARM64 simulator Build System: - WASM: zig build wasm - Android: zig build android / android-x86 - iOS: zig build ios / ios-sim - Conditional compilation for platform detection Documentation: - docs/MOBILE_WEB_BACKENDS.md: Comprehensive guide (~400 lines) - Updated DEVELOPMENT_PLAN.md with FASE 10 - Updated CLAUDE.md with new commands Stats: 3 backends, ~1500 new LOC, cross-platform ready 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
318 lines
8.6 KiB
Objective-C
318 lines
8.6 KiB
Objective-C
// ZcatguiBridge.m - Objective-C bridge implementation for zcatgui iOS backend
|
|
//
|
|
// This provides the UIKit integration for zcatgui.
|
|
// Add this file to your Xcode iOS project.
|
|
|
|
#import "ZcatguiBridge.h"
|
|
#import <QuartzCore/QuartzCore.h>
|
|
#import <mach/mach_time.h>
|
|
|
|
// Global state
|
|
static ZcatguiView *g_view = nil;
|
|
static NSMutableArray<ZcatguiEvent *> *g_eventQueue = nil;
|
|
static uint32_t g_width = 0;
|
|
static uint32_t g_height = 0;
|
|
static mach_timebase_info_data_t g_timebaseInfo;
|
|
|
|
// Helper to create event
|
|
static ZcatguiEvent *createEvent(ZcatguiEventType type) {
|
|
ZcatguiEvent *event = [[ZcatguiEvent alloc] init];
|
|
event->type = type;
|
|
memset(event->data, 0, sizeof(event->data));
|
|
return event;
|
|
}
|
|
|
|
// =============================================================================
|
|
// ZcatguiEvent wrapper (for NSMutableArray)
|
|
// =============================================================================
|
|
|
|
@interface ZcatguiEventWrapper : NSObject
|
|
@property (nonatomic) ZcatguiEventType type;
|
|
@property (nonatomic) uint8_t data[64];
|
|
@end
|
|
|
|
@implementation ZcatguiEventWrapper
|
|
@end
|
|
|
|
// =============================================================================
|
|
// ZcatguiView Implementation
|
|
// =============================================================================
|
|
|
|
@implementation ZcatguiView {
|
|
CGContextRef _bitmapContext;
|
|
uint32_t *_pixels;
|
|
uint32_t _pixelWidth;
|
|
uint32_t _pixelHeight;
|
|
}
|
|
|
|
- (instancetype)initWithFrame:(CGRect)frame {
|
|
self = [super initWithFrame:frame];
|
|
if (self) {
|
|
self.backgroundColor = [UIColor blackColor];
|
|
self.multipleTouchEnabled = YES;
|
|
self.userInteractionEnabled = YES;
|
|
|
|
_bitmapContext = NULL;
|
|
_pixels = NULL;
|
|
_pixelWidth = 0;
|
|
_pixelHeight = 0;
|
|
|
|
// Initialize event queue
|
|
if (!g_eventQueue) {
|
|
g_eventQueue = [[NSMutableArray alloc] init];
|
|
}
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc {
|
|
if (_bitmapContext) {
|
|
CGContextRelease(_bitmapContext);
|
|
}
|
|
if (_pixels) {
|
|
free(_pixels);
|
|
}
|
|
}
|
|
|
|
- (void)presentPixels:(const uint32_t *)pixels width:(uint32_t)width height:(uint32_t)height {
|
|
// Resize buffer if needed
|
|
if (width != _pixelWidth || height != _pixelHeight) {
|
|
if (_bitmapContext) {
|
|
CGContextRelease(_bitmapContext);
|
|
_bitmapContext = NULL;
|
|
}
|
|
if (_pixels) {
|
|
free(_pixels);
|
|
_pixels = NULL;
|
|
}
|
|
|
|
_pixelWidth = width;
|
|
_pixelHeight = height;
|
|
_pixels = malloc(width * height * 4);
|
|
|
|
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
|
_bitmapContext = CGBitmapContextCreate(
|
|
_pixels,
|
|
width,
|
|
height,
|
|
8,
|
|
width * 4,
|
|
colorSpace,
|
|
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Little
|
|
);
|
|
CGColorSpaceRelease(colorSpace);
|
|
}
|
|
|
|
// Copy pixels
|
|
memcpy(_pixels, pixels, width * height * 4);
|
|
|
|
// Trigger redraw
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self setNeedsDisplay];
|
|
});
|
|
}
|
|
|
|
- (void)drawRect:(CGRect)rect {
|
|
if (!_bitmapContext || !_pixels) {
|
|
return;
|
|
}
|
|
|
|
CGContextRef ctx = UIGraphicsGetCurrentContext();
|
|
if (!ctx) {
|
|
return;
|
|
}
|
|
|
|
// Create image from bitmap context
|
|
CGImageRef image = CGBitmapContextCreateImage(_bitmapContext);
|
|
if (image) {
|
|
// Flip coordinate system
|
|
CGContextTranslateCTM(ctx, 0, self.bounds.size.height);
|
|
CGContextScaleCTM(ctx, 1.0, -1.0);
|
|
|
|
CGContextDrawImage(ctx, self.bounds, image);
|
|
CGImageRelease(image);
|
|
}
|
|
}
|
|
|
|
- (CGSize)framebufferSize {
|
|
return CGSizeMake(_pixelWidth, _pixelHeight);
|
|
}
|
|
|
|
// Touch handling
|
|
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
|
UITouch *touch = [touches anyObject];
|
|
CGPoint location = [touch locationInView:self];
|
|
|
|
ZcatguiEventWrapper *evt = [[ZcatguiEventWrapper alloc] init];
|
|
evt.type = ZcatguiEventTouchDown;
|
|
|
|
int32_t x = (int32_t)location.x;
|
|
int32_t y = (int32_t)location.y;
|
|
memcpy(&evt.data[0], &x, 4);
|
|
memcpy(&evt.data[4], &y, 4);
|
|
|
|
@synchronized(g_eventQueue) {
|
|
[g_eventQueue addObject:evt];
|
|
}
|
|
}
|
|
|
|
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
|
UITouch *touch = [touches anyObject];
|
|
CGPoint location = [touch locationInView:self];
|
|
|
|
ZcatguiEventWrapper *evt = [[ZcatguiEventWrapper alloc] init];
|
|
evt.type = ZcatguiEventTouchMove;
|
|
|
|
int32_t x = (int32_t)location.x;
|
|
int32_t y = (int32_t)location.y;
|
|
memcpy(&evt.data[0], &x, 4);
|
|
memcpy(&evt.data[4], &y, 4);
|
|
|
|
@synchronized(g_eventQueue) {
|
|
[g_eventQueue addObject:evt];
|
|
}
|
|
}
|
|
|
|
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
|
UITouch *touch = [touches anyObject];
|
|
CGPoint location = [touch locationInView:self];
|
|
|
|
ZcatguiEventWrapper *evt = [[ZcatguiEventWrapper alloc] init];
|
|
evt.type = ZcatguiEventTouchUp;
|
|
|
|
int32_t x = (int32_t)location.x;
|
|
int32_t y = (int32_t)location.y;
|
|
memcpy(&evt.data[0], &x, 4);
|
|
memcpy(&evt.data[4], &y, 4);
|
|
|
|
@synchronized(g_eventQueue) {
|
|
[g_eventQueue addObject:evt];
|
|
}
|
|
}
|
|
|
|
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
|
[self touchesEnded:touches withEvent:event];
|
|
}
|
|
|
|
@end
|
|
|
|
// =============================================================================
|
|
// ZcatguiViewController Implementation
|
|
// =============================================================================
|
|
|
|
@implementation ZcatguiViewController {
|
|
CADisplayLink *_displayLink;
|
|
}
|
|
|
|
- (void)viewDidLoad {
|
|
[super viewDidLoad];
|
|
|
|
self.zcatguiView = [[ZcatguiView alloc] initWithFrame:self.view.bounds];
|
|
self.zcatguiView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
|
[self.view addSubview:self.zcatguiView];
|
|
|
|
g_view = self.zcatguiView;
|
|
g_width = (uint32_t)self.view.bounds.size.width;
|
|
g_height = (uint32_t)self.view.bounds.size.height;
|
|
|
|
self.running = YES;
|
|
}
|
|
|
|
- (void)viewDidLayoutSubviews {
|
|
[super viewDidLayoutSubviews];
|
|
|
|
uint32_t newWidth = (uint32_t)self.view.bounds.size.width;
|
|
uint32_t newHeight = (uint32_t)self.view.bounds.size.height;
|
|
|
|
if (newWidth != g_width || newHeight != g_height) {
|
|
g_width = newWidth;
|
|
g_height = newHeight;
|
|
|
|
// Queue resize event
|
|
ZcatguiEventWrapper *evt = [[ZcatguiEventWrapper alloc] init];
|
|
evt.type = ZcatguiEventResize;
|
|
memcpy(&evt.data[0], &newWidth, 4);
|
|
memcpy(&evt.data[4], &newHeight, 4);
|
|
|
|
@synchronized(g_eventQueue) {
|
|
[g_eventQueue addObject:evt];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)startRenderLoop {
|
|
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(renderFrame:)];
|
|
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
|
|
}
|
|
|
|
- (void)stopRenderLoop {
|
|
[_displayLink invalidate];
|
|
_displayLink = nil;
|
|
}
|
|
|
|
- (void)renderFrame:(CADisplayLink *)displayLink {
|
|
// Override this in your subclass to call your Zig frame function
|
|
}
|
|
|
|
- (BOOL)prefersStatusBarHidden {
|
|
return YES;
|
|
}
|
|
|
|
@end
|
|
|
|
// =============================================================================
|
|
// Bridge Functions (called by Zig)
|
|
// =============================================================================
|
|
|
|
void ios_view_init(uint32_t width, uint32_t height) {
|
|
g_width = width;
|
|
g_height = height;
|
|
|
|
// Initialize timebase for timing
|
|
mach_timebase_info(&g_timebaseInfo);
|
|
|
|
NSLog(@"[zcatgui] ios_view_init: %ux%u", width, height);
|
|
}
|
|
|
|
uint32_t ios_view_get_width(void) {
|
|
return g_width;
|
|
}
|
|
|
|
uint32_t ios_view_get_height(void) {
|
|
return g_height;
|
|
}
|
|
|
|
void ios_view_present(const uint32_t *pixels, uint32_t width, uint32_t height) {
|
|
if (g_view) {
|
|
[g_view presentPixels:pixels width:width height:height];
|
|
}
|
|
}
|
|
|
|
uint32_t ios_poll_event(uint8_t *buffer) {
|
|
ZcatguiEventWrapper *evt = nil;
|
|
|
|
@synchronized(g_eventQueue) {
|
|
if (g_eventQueue.count > 0) {
|
|
evt = g_eventQueue.firstObject;
|
|
[g_eventQueue removeObjectAtIndex:0];
|
|
}
|
|
}
|
|
|
|
if (!evt) {
|
|
return ZcatguiEventNone;
|
|
}
|
|
|
|
memcpy(buffer, evt.data, 64);
|
|
return evt.type;
|
|
}
|
|
|
|
void ios_log(const uint8_t *ptr, size_t len) {
|
|
NSString *msg = [[NSString alloc] initWithBytes:ptr length:len encoding:NSUTF8StringEncoding];
|
|
NSLog(@"[zcatgui] %@", msg);
|
|
}
|
|
|
|
uint64_t ios_get_time_ms(void) {
|
|
uint64_t time = mach_absolute_time();
|
|
uint64_t nanos = time * g_timebaseInfo.numer / g_timebaseInfo.denom;
|
|
return nanos / 1000000; // Convert to milliseconds
|
|
}
|