- Create native macOS GUI application using TypePHP and Objective-C++ bridge - Implement thin wrappers around AppKit API for window management and controls - Add support for labels, buttons, and event handling in native macOS interface - Include proper Info.plist configuration and code signing script for app bundles - Provide complete example with application lifecycle and user interaction - Document build requirements and packaging instructions for macOS distributionmaster
parent
6215ecd06d
commit
622d6b0a00
8 changed files with 366 additions and 0 deletions
@ -0,0 +1,5 @@ |
|||||||
|
/build/ |
||||||
|
/build-*/ |
||||||
|
/dist/ |
||||||
|
/*.rsp |
||||||
|
/typephp_macos_hello |
||||||
@ -0,0 +1,26 @@ |
|||||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
||||||
|
<plist version="1.0"> |
||||||
|
<dict> |
||||||
|
<key>CFBundleDevelopmentRegion</key> |
||||||
|
<string>en</string> |
||||||
|
<key>CFBundleExecutable</key> |
||||||
|
<string>typephp-macos-hello</string> |
||||||
|
<key>CFBundleIdentifier</key> |
||||||
|
<string>org.swoole.typephp.macos-hello</string> |
||||||
|
<key>CFBundleInfoDictionaryVersion</key> |
||||||
|
<string>6.0</string> |
||||||
|
<key>CFBundleName</key> |
||||||
|
<string>TypePHP macOS Hello</string> |
||||||
|
<key>CFBundlePackageType</key> |
||||||
|
<string>APPL</string> |
||||||
|
<key>CFBundleShortVersionString</key> |
||||||
|
<string>0.1.0</string> |
||||||
|
<key>CFBundleVersion</key> |
||||||
|
<string>1</string> |
||||||
|
<key>LSMinimumSystemVersion</key> |
||||||
|
<string>11.0</string> |
||||||
|
<key>NSHighResolutionCapable</key> |
||||||
|
<true/> |
||||||
|
</dict> |
||||||
|
</plist> |
||||||
@ -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. |
||||||
@ -0,0 +1,197 @@ |
|||||||
|
#include <AppKit/AppKit.h> |
||||||
|
#include <phpx.h> |
||||||
|
|
||||||
|
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 <NSWindowDelegate> |
||||||
|
|
||||||
|
@property(nonatomic, strong) NSWindow *window; |
||||||
|
@property(nonatomic, strong) NSMutableDictionary<NSNumber *, NSControl *> *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<CGFloat>(width), static_cast<CGFloat>(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<CGFloat>(x), |
||||||
|
static_cast<CGFloat>(y), |
||||||
|
static_cast<CGFloat>(width), |
||||||
|
static_cast<CGFloat>(height)); |
||||||
|
label.font = bold |
||||||
|
? [NSFont systemFontOfSize:static_cast<CGFloat>(fontSize) |
||||||
|
weight:NSFontWeightSemibold] |
||||||
|
: [NSFont systemFontOfSize:static_cast<CGFloat>(fontSize)]; |
||||||
|
label.alignment = NSTextAlignmentCenter; |
||||||
|
label.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin; |
||||||
|
[bridge.window.contentView addSubview:label]; |
||||||
|
return static_cast<php::Int>(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<CGFloat>(x), |
||||||
|
static_cast<CGFloat>(y), |
||||||
|
static_cast<CGFloat>(width), |
||||||
|
static_cast<CGFloat>(height)); |
||||||
|
button.bezelStyle = NSBezelStyleRounded; |
||||||
|
button.keyEquivalent = @"\r"; |
||||||
|
button.autoresizingMask = NSViewMinXMargin | NSViewMaxXMargin | NSViewMinYMargin; |
||||||
|
[bridge.window.contentView addSubview:button]; |
||||||
|
return static_cast<php::Int>(store_control(button)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void php_macos_set_control_text(php::Int controlId, php::Str text) |
||||||
|
{ |
||||||
|
@autoreleasepool { |
||||||
|
NSControl *control = bridge.controls[@(static_cast<NSInteger>(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<php::Int>(controlId); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return -1; |
||||||
|
} |
||||||
@ -0,0 +1,25 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
/** Thin wrappers around the macOS AppKit API. */ |
||||||
|
function macos_app_init(string $applicationName): void {} |
||||||
|
|
||||||
|
function macos_create_window(string $title, int $width, int $height): void {} |
||||||
|
|
||||||
|
function macos_add_label( |
||||||
|
string $text, |
||||||
|
int $x, |
||||||
|
int $y, |
||||||
|
int $width, |
||||||
|
int $height, |
||||||
|
int $fontSize, |
||||||
|
bool $bold |
||||||
|
): int {} |
||||||
|
|
||||||
|
function macos_add_button(string $title, int $x, int $y, int $width, int $height): int {} |
||||||
|
|
||||||
|
function macos_set_control_text(int $controlId, string $text): void {} |
||||||
|
|
||||||
|
function macos_show_window(): void {} |
||||||
|
|
||||||
|
/** Return the activated control ID, or -1 after the window is closed. */ |
||||||
|
function macos_next_event(): int {} |
||||||
@ -0,0 +1,19 @@ |
|||||||
|
#!/bin/sh |
||||||
|
|
||||||
|
set -eu |
||||||
|
|
||||||
|
project_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) |
||||||
|
executable="$project_dir/typephp_macos_hello" |
||||||
|
bundle="$project_dir/dist/TypePHP macOS Hello.app" |
||||||
|
|
||||||
|
if [ ! -x "$executable" ]; then |
||||||
|
echo "Missing $executable; build project.yml first." >&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" |
||||||
@ -0,0 +1,38 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
macos_app_init('TypePHP macOS Hello'); |
||||||
|
macos_create_window('TypePHP macOS Hello', 640, 380); |
||||||
|
|
||||||
|
macos_add_label('TypePHP + Objective-C++', 44, 292, 552, 40, 28, true); |
||||||
|
macos_add_label( |
||||||
|
"The application state and event loop are implemented in TypePHP.\n" . |
||||||
|
'Objective-C++ only provides thin wrappers around AppKit.', |
||||||
|
64, |
||||||
|
178, |
||||||
|
512, |
||||||
|
88, |
||||||
|
15, |
||||||
|
false, |
||||||
|
); |
||||||
|
|
||||||
|
$statusLabel = macos_add_label('Waiting for an AppKit event.', 64, 130, 512, 24, 13, false); |
||||||
|
$button = macos_add_button('Send event to TypePHP', 210, 64, 220, 40); |
||||||
|
$clickCount = 0; |
||||||
|
|
||||||
|
macos_show_window(); |
||||||
|
while (true) { |
||||||
|
$controlId = macos_next_event(); |
||||||
|
if ($controlId < 0) { |
||||||
|
break; |
||||||
|
} |
||||||
|
if ($controlId !== $button) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
$clickCount++; |
||||||
|
$suffix = $clickCount === 1 ? 'time' : 'times'; |
||||||
|
macos_set_control_text($statusLabel, "Button clicked $clickCount $suffix; handled by TypePHP."); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,16 @@ |
|||||||
|
name: objective_c_macos |
||||||
|
version: 0.1.0 |
||||||
|
mode: bin |
||||||
|
output: typephp_macos_hello |
||||||
|
cxx-std: c++17 |
||||||
|
|
||||||
|
sources: |
||||||
|
- php-src |
||||||
|
- objc-src |
||||||
|
|
||||||
|
cxx-flags: |
||||||
|
- -fobjc-arc |
||||||
|
|
||||||
|
ld-flags: |
||||||
|
- -framework AppKit |
||||||
|
- -framework Foundation |
||||||
Loading…
Reference in new issue