zcatgui/ios/ZcatguiBridge.m
reugenio 6889474327 feat: zcatgui v0.15.0 - Mobile & Web Backends
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>
2025-12-09 18:20:13 +01:00

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
}