- 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