Skip to content

Commit 6acf14c

Browse files
committed
Add basic macos msgbox support
1 parent b32511c commit 6acf14c

File tree

4 files changed

+157
-1
lines changed

4 files changed

+157
-1
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Boson\Api\MessageBox\Driver\MacOS;
6+
7+
use FFI\Env\Runtime;
8+
9+
/**
10+
* @mixin \FFI
11+
*
12+
* @internal this is an internal library class, please do not use it in your code
13+
* @psalm-internal Boson\Api\MessageBox\Driver
14+
*/
15+
final readonly class LibObjectC
16+
{
17+
private \FFI $ffi;
18+
19+
public function __construct()
20+
{
21+
Runtime::assertAvailable();
22+
23+
$this->ffi = \FFI::cdef(\trim((string) @\file_get_contents(
24+
filename: __FILE__,
25+
offset: __COMPILER_HALT_OFFSET__,
26+
)), 'libobjc.A.dylib');
27+
}
28+
29+
/**
30+
* @param non-empty-string ...$types
31+
* @return callable(CData,CData,...):mixed
32+
*/
33+
public function getMessageSend(string ...$types): callable
34+
{
35+
$signature = \sprintf('id(*)(id, SEL, %s)', \implode(',', $types));
36+
37+
return $this->cast($signature, $this->ffi->objc_msgSend);
38+
}
39+
40+
/**
41+
* @param non-empty-string $name
42+
* @param array<array-key, mixed> $arguments
43+
*/
44+
public function __call(string $name, array $arguments = []): mixed
45+
{
46+
try {
47+
return $this->ffi->$name(...$arguments);
48+
} catch (\Throwable $e) {
49+
throw new \BadMethodCallException($e->getMessage(), previous: $e);
50+
}
51+
}
52+
53+
public function __get(string $name): mixed
54+
{
55+
return $this->ffi->$name;
56+
}
57+
}
58+
59+
__halt_compiler();
60+
61+
typedef void* id;
62+
typedef void* SEL;
63+
64+
id objc_getClass(const char* name);
65+
SEL sel_registerName(const char* str);
66+
void* objc_msgSend(...);
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Boson\Api\MessageBox\Driver;
6+
7+
use Boson\Api\MessageBox\Driver\MacOS\LibObjectC;
8+
use Boson\Api\MessageBox\MessageBoxCreateInfo;
9+
use Boson\Api\MessageBox\MessageBoxExtensionInterface;
10+
use Boson\Api\MessageBox\MessageBoxIcon;
11+
use FFI\CData;
12+
use React\Promise\PromiseInterface;
13+
14+
use function React\Promise\resolve;
15+
16+
final readonly class MacOSMessageBoxExtension implements MessageBoxExtensionInterface
17+
{
18+
private CData $msgSendId;
19+
private CData $msgSendString;
20+
private CData $msgSendLong;
21+
22+
public function __construct(
23+
private LibObjectC $libobjc = new LibObjectC(),
24+
) {
25+
$this->msgSendId = $libobjc->getMessageSend('id');
26+
$this->msgSendString = $libobjc->getMessageSend('const char*');
27+
$this->msgSendLong = $libobjc->getMessageSend('long');
28+
}
29+
30+
public function create(MessageBoxCreateInfo $info): PromiseInterface
31+
{
32+
// NSString *titleStr = [NSString stringWithUTF8String:title]
33+
$titleStr = ($this->msgSendString)(
34+
$this->libobjc->objc_getClass('NSString'),
35+
$this->libobjc->sel_registerName('stringWithUTF8String:'),
36+
$info->title . "\0",
37+
);
38+
39+
// NSString *textStr = [NSString stringWithUTF8String:text]
40+
$textStr = ($this->msgSendString)(
41+
$this->libobjc->objc_getClass('NSString'),
42+
$this->libobjc->sel_registerName('stringWithUTF8String:'),
43+
$info->text . "\0",
44+
);
45+
46+
// NSAlert *alert = [NSAlert new]
47+
$alert = $this->libobjc->objc_msgSend(
48+
$this->libobjc->objc_getClass('NSAlert'),
49+
$this->libobjc->sel_registerName('new'),
50+
);
51+
52+
// [alert setMessageText:textStr];
53+
($this->msgSendId)(
54+
$alert,
55+
$this->libobjc->sel_registerName('setMessageText:'),
56+
$textStr,
57+
);
58+
59+
// [alert setInformativeText:titleStr];
60+
($this->msgSendId)(
61+
$alert,
62+
$this->libobjc->sel_registerName('setInformativeText:'),
63+
$titleStr,
64+
);
65+
66+
// Set alert style based on icon
67+
if ($info->icon !== null) {
68+
// [alert setAlertStyle:alertStyle];
69+
($this->msgSendLong)(
70+
$alert,
71+
$this->libobjc->sel_registerName('setAlertStyle:'),
72+
match ($info->icon) {
73+
MessageBoxIcon::Error => 0, // NSAlertStyleCritical
74+
MessageBoxIcon::Warning => 1, // NSAlertStyleWarning
75+
MessageBoxIcon::Info => 2, // NSAlertStyleInformational
76+
},
77+
);
78+
}
79+
80+
// [alert runModal];
81+
$this->libobjc->objc_msgSend(
82+
$alert,
83+
$this->libobjc->sel_registerName('runModal'),
84+
);
85+
86+
return resolve(null);
87+
}
88+
}

src/Api/MessageBox/Driver/Windows/User32.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* @mixin \FFI
1111
*
1212
* @internal this is an internal library class, please do not use it in your code
13-
* @psalm-internal Boson\Api\MessageBox\Driver\Windows
13+
* @psalm-internal Boson\Api\MessageBox\Driver
1414
*/
1515
final readonly class User32
1616
{

src/Api/MessageBox/MessageBoxExtensionProvider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Boson\Api\MessageBox;
66

7+
use Boson\Api\MessageBox\Driver\MacOSMessageBoxExtension;
78
use Boson\Api\MessageBox\Driver\VoidMessageBoxExtension;
89
use Boson\Api\MessageBox\Driver\WindowsMessageBoxExtension;
910
use Boson\Api\OperatingSystem\OperatingSystemExtensionInterface;
@@ -28,6 +29,7 @@ public function load(IdentifiableInterface $ctx, EventListener $listener): Messa
2829
$os = $ctx->get(OperatingSystemExtensionInterface::class);
2930

3031
return match (true) {
32+
$os->family->is(Family::Darwin) => new MacOSMessageBoxExtension(),
3133
$os->family->is(Family::Windows) => new WindowsMessageBoxExtension(),
3234
default => new VoidMessageBoxExtension(),
3335
};

0 commit comments

Comments
 (0)