diff --git a/examples/objective-c-macos/.gitignore b/examples/objective-c-macos/.gitignore new file mode 100644 index 00000000..da2cb165 --- /dev/null +++ b/examples/objective-c-macos/.gitignore @@ -0,0 +1,5 @@ +/build/ +/build-*/ +/dist/ +/*.rsp +/typephp_macos_hello diff --git a/examples/objective-c-macos/Info.plist b/examples/objective-c-macos/Info.plist new file mode 100644 index 00000000..a8541b7c --- /dev/null +++ b/examples/objective-c-macos/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + typephp-macos-hello + CFBundleIdentifier + org.swoole.typephp.macos-hello + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + TypePHP macOS Hello + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.1.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + 11.0 + NSHighResolutionCapable + + + diff --git a/examples/objective-c-macos/README.md b/examples/objective-c-macos/README.md new file mode 100644 index 00000000..ab1692a9 --- /dev/null +++ b/examples/objective-c-macos/README.md @@ -0,0 +1,40 @@ +# TypePHP macOS AppKit example + +This is a native macOS GUI application built from TypePHP and Objective-C++. +TypePHP defines the window layout, application state, click behavior and event +loop. Objective-C++ remains a thin platform bridge that exposes primitive AppKit +operations. Clicking the native button returns a control ID to TypePHP, which +updates the status label. + +Requirements: + +- macOS with Xcode Command Line Tools +- PHP 8.4 or 8.5 built with the embed SAPI (`libphp.dylib`) +- a matching PHPX build +- `PHPX_HOME` pointing to the PHPX source tree + +Build from the TypePHP repository root: + +```sh +export PHPX_HOME=/path/to/phpx +php bin/tpc.php examples/objective-c-macos/project.yml --no-progress +``` + +Run the native executable: + +```sh +./examples/objective-c-macos/typephp_macos_hello +``` + +To create a standard `.app` bundle that can be launched from Finder: + +```sh +sh examples/objective-c-macos/package-app.sh +open "examples/objective-c-macos/dist/TypePHP macOS Hello.app" +``` + +The project uses an `.mm` source because PHPX exposes a C++ API. Its project +configuration enables ARC and links both AppKit and Foundation. This example is +macOS-only; an iOS application would additionally require Xcode project and +code-signing integration, so it is intentionally not mixed into this desktop +example. diff --git a/examples/objective-c-macos/objc-src/macos_bridge.mm b/examples/objective-c-macos/objc-src/macos_bridge.mm new file mode 100644 index 00000000..d5764885 --- /dev/null +++ b/examples/objective-c-macos/objc-src/macos_bridge.mm @@ -0,0 +1,197 @@ +#include +#include + +static NSString *to_ns_string(const php::Str &value) +{ + NSString *result = [[NSString alloc] initWithBytes:value.data() + length:value.length() + encoding:NSUTF8StringEncoding]; + return result == nil ? @"" : result; +} + +@interface TypePHPAppKitBridge : NSObject + +@property(nonatomic, strong) NSWindow *window; +@property(nonatomic, strong) NSMutableDictionary *controls; +@property(nonatomic) NSInteger nextControlId; +@property(nonatomic) NSInteger pendingControlId; +@property(nonatomic) BOOL running; + +- (void)controlActivated:(NSControl *)sender; + +@end + +@implementation TypePHPAppKitBridge + +- (instancetype)init +{ + self = [super init]; + if (self != nil) { + self.controls = [[NSMutableDictionary alloc] init]; + self.nextControlId = 1; + } + return self; +} + +- (void)controlActivated:(NSControl *)sender +{ + self.pendingControlId = sender.tag; +} + +- (void)windowWillClose:(NSNotification *)notification +{ + (void) notification; + self.running = NO; +} + +@end + +static __strong TypePHPAppKitBridge *bridge = nil; + +static void install_application_menu(NSString *applicationName) +{ + NSMenu *menuBar = [[NSMenu alloc] init]; + NSMenuItem *applicationMenuItem = [[NSMenuItem alloc] init]; + [menuBar addItem:applicationMenuItem]; + NSApp.mainMenu = menuBar; + + NSMenu *applicationMenu = [[NSMenu alloc] init]; + NSString *quitTitle = [@"Quit " stringByAppendingString:applicationName]; + NSMenuItem *quitItem = [[NSMenuItem alloc] initWithTitle:quitTitle + action:@selector(terminate:) + keyEquivalent:@"q"]; + [applicationMenu addItem:quitItem]; + applicationMenuItem.submenu = applicationMenu; +} + +static NSInteger store_control(NSControl *control) +{ + const NSInteger controlId = bridge.nextControlId++; + control.tag = controlId; + bridge.controls[@(controlId)] = control; + return controlId; +} + +void php_macos_app_init(php::Str applicationName) +{ + @autoreleasepool { + NSApplication *application = NSApplication.sharedApplication; + application.activationPolicy = NSApplicationActivationPolicyRegular; + bridge = [[TypePHPAppKitBridge alloc] init]; + install_application_menu(to_ns_string(applicationName)); + [application finishLaunching]; + } +} + +void php_macos_create_window(php::Str title, php::Int width, php::Int height) +{ + @autoreleasepool { + const NSRect frame = NSMakeRect(0, 0, static_cast(width), static_cast(height)); + const NSWindowStyleMask style = NSWindowStyleMaskTitled | + NSWindowStyleMaskClosable | + NSWindowStyleMaskMiniaturizable | + NSWindowStyleMaskResizable; + bridge.window = [[NSWindow alloc] initWithContentRect:frame + styleMask:style + backing:NSBackingStoreBuffered + defer:NO]; + bridge.window.releasedWhenClosed = NO; + bridge.window.delegate = bridge; + bridge.window.title = to_ns_string(title); + bridge.window.minSize = NSMakeSize(480, 300); + [bridge.window center]; + } +} + +php::Int php_macos_add_label( + php::Str text, + php::Int x, + php::Int y, + php::Int width, + php::Int height, + php::Int fontSize, + php::Bool bold) +{ + @autoreleasepool { + NSTextField *label = [NSTextField wrappingLabelWithString:to_ns_string(text)]; + label.frame = NSMakeRect( + static_cast(x), + static_cast(y), + static_cast(width), + static_cast(height)); + label.font = bold + ? [NSFont systemFontOfSize:static_cast(fontSize) + weight:NSFontWeightSemibold] + : [NSFont systemFontOfSize:static_cast(fontSize)]; + label.alignment = NSTextAlignmentCenter; + label.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin; + [bridge.window.contentView addSubview:label]; + return static_cast(store_control(label)); + } +} + +php::Int php_macos_add_button( + php::Str title, + php::Int x, + php::Int y, + php::Int width, + php::Int height) +{ + @autoreleasepool { + NSButton *button = [NSButton buttonWithTitle:to_ns_string(title) + target:bridge + action:@selector(controlActivated:)]; + button.frame = NSMakeRect( + static_cast(x), + static_cast(y), + static_cast(width), + static_cast(height)); + button.bezelStyle = NSBezelStyleRounded; + button.keyEquivalent = @"\r"; + button.autoresizingMask = NSViewMinXMargin | NSViewMaxXMargin | NSViewMinYMargin; + [bridge.window.contentView addSubview:button]; + return static_cast(store_control(button)); + } +} + +void php_macos_set_control_text(php::Int controlId, php::Str text) +{ + @autoreleasepool { + NSControl *control = bridge.controls[@(static_cast(controlId))]; + if (control != nil) { + control.stringValue = to_ns_string(text); + } + } +} + +void php_macos_show_window() +{ + @autoreleasepool { + [bridge.window makeKeyAndOrderFront:nil]; + bridge.running = YES; + [NSApp activateIgnoringOtherApps:YES]; + } +} + +php::Int php_macos_next_event() +{ + while (bridge.running) { + @autoreleasepool { + NSEvent *event = [NSApp nextEventMatchingMask:NSEventMaskAny + untilDate:NSDate.distantFuture + inMode:NSDefaultRunLoopMode + dequeue:YES]; + if (event != nil) { + [NSApp sendEvent:event]; + [NSApp updateWindows]; + } + + if (bridge.pendingControlId != 0) { + const NSInteger controlId = bridge.pendingControlId; + bridge.pendingControlId = 0; + return static_cast(controlId); + } + } + } + return -1; +} diff --git a/examples/objective-c-macos/objc-src/macos_bridge.stub.php b/examples/objective-c-macos/objc-src/macos_bridge.stub.php new file mode 100644 index 00000000..10224237 --- /dev/null +++ b/examples/objective-c-macos/objc-src/macos_bridge.stub.php @@ -0,0 +1,25 @@ +&2 + exit 1 +fi + +mkdir -p "$bundle/Contents/MacOS" +cp "$project_dir/Info.plist" "$bundle/Contents/Info.plist" +cp "$executable" "$bundle/Contents/MacOS/typephp-macos-hello" +codesign --force --deep --sign - "$bundle" + +echo "Created $bundle" diff --git a/examples/objective-c-macos/php-src/main.php b/examples/objective-c-macos/php-src/main.php new file mode 100644 index 00000000..cd196ff7 --- /dev/null +++ b/examples/objective-c-macos/php-src/main.php @@ -0,0 +1,38 @@ +