diff --git a/examples/ServerManager/App.php b/examples/ServerManager/App.php index 43ee651..945f62e 100644 --- a/examples/ServerManager/App.php +++ b/examples/ServerManager/App.php @@ -36,8 +36,11 @@ class App // Status label (referenced by tabs) $statusLabel = new Label( - text: 'Fenster: ' . $this->window->getViewport()->windowWidth . 'x' . $this->window->getViewport()->windowHeight, - style: 'basis-4/8 text-black' + text: 'Fenster: ' . + $this->window->getViewport()->windowWidth . + 'x' . + $this->window->getViewport()->windowHeight, + style: 'basis-4/8 text-black', ); // Settings variables (simple variables work better with async than object properties) @@ -50,7 +53,13 @@ class App $mainContainer->addComponent($menuBar); // Create settings modal with the real menu bar - $settingsModal = new SettingsModal($this->settings, $menuBar, $currentApiKey, $currentPrivateKeyPath, $currentRemoteStartDir); + $settingsModal = new SettingsModal( + $this->settings, + $menuBar, + $currentApiKey, + $currentPrivateKeyPath, + $currentRemoteStartDir, + ); $mainContainer->addComponent($settingsModal->getModal()); // Build menu bar menus after modal is created @@ -69,6 +78,7 @@ class App $this->settings, $kanbanTab, ); + $kanbanTab->setServerListTab($serverListTab); $sftpManagerTab = new SftpManagerTab( $currentApiKey, $currentPrivateKeyPath, @@ -97,10 +107,21 @@ class App $statusBar->addSegment(new Label('v1.0', 'basis-1/8 text-center text-black border-l border-gray-300')); $statusBar->addSegment(new Label( 'PHPNative Framework', - 'basis-3/8 text-right text-black pr-2 border-l border-gray-300' + 'basis-3/8 text-right text-black pr-2 border-l border-gray-300', )); $mainContainer->addComponent($statusBar); + // Tray disabled for now due to GTK/XKB issues with static builds + // TODO: Re-enable after rebuilding static PHP with new SDL3 tray implementation + if (function_exists('tray_setup')) { + try { + tray_setup('', ['Beenden']); + } catch (\Throwable $e) { + // Tray initialization failed, continue without tray + error_log('Tray setup failed: ' . $e->getMessage()); + } + } + // Set window content and run $this->window->setRoot($mainContainer); $this->app->addWindow($this->window); diff --git a/examples/ServerManager/UI/KanbanTab.php b/examples/ServerManager/UI/KanbanTab.php index 049fe7a..f9a2054 100644 --- a/examples/ServerManager/UI/KanbanTab.php +++ b/examples/ServerManager/UI/KanbanTab.php @@ -24,6 +24,7 @@ class KanbanTab private Container $editBoardButtonsContainer; private string $currentEditingBoard = 'neu'; private null|string $currentEditingTaskId = null; + private null|ServerListTab $serverListTab = null; public function __construct(Settings $settings) { @@ -85,6 +86,11 @@ class KanbanTab $this->renderBoards(); } + public function setServerListTab(ServerListTab $serverListTab): void + { + $this->serverListTab = $serverListTab; + } + public function getContainer(): Container { return $this->tab; @@ -270,6 +276,10 @@ class KanbanTab $this->editModal->setVisible(false); $this->renderBoards(); + + if ($this->serverListTab !== null) { + $this->serverListTab->refreshCurrentServerTasks(); + } } private function renderBoards(): void @@ -352,7 +362,7 @@ class KanbanTab return; } - $tasks = array_values(array_filter( + $tasks = array_values(array_filter( $tasks, static fn($t) => ($t['id'] ?? null) !== $taskId, )); @@ -360,6 +370,10 @@ class KanbanTab $kanbanTab->settings->set('kanban.tasks', $tasks); $kanbanTab->settings->save(); $kanbanTab->renderBoards(); + + if ($kanbanTab->serverListTab !== null) { + $kanbanTab->serverListTab->refreshCurrentServerTasks(); + } }); $headerRow->addComponent($editButton); diff --git a/examples/ServerManager/UI/ServerListTab.php b/examples/ServerManager/UI/ServerListTab.php index 8ca9a91..ff86b54 100644 --- a/examples/ServerManager/UI/ServerListTab.php +++ b/examples/ServerManager/UI/ServerListTab.php @@ -1032,4 +1032,9 @@ class ServerListTab $this->renderTodoList(); } + + public function refreshCurrentServerTasks(): void + { + $this->loadServerTasks(); + } } diff --git a/examples/server_manager.php b/examples/server_manager.php index f50a10d..a22fa36 100644 --- a/examples/server_manager.php +++ b/examples/server_manager.php @@ -2,6 +2,8 @@ declare(strict_types=1); +putenv('XKB_CONFIG_ROOT=/usr/share/X11/xkb'); +putenv('XLOCALEDIR=/usr/share/X11/locale'); // Bootstrap: Load composer autoloader require_once __DIR__ . '/../vendor/autoload.php'; diff --git a/examples/todo_app.php b/examples/todo_app.php index 9d8f208..ff682ec 100644 --- a/examples/todo_app.php +++ b/examples/todo_app.php @@ -36,11 +36,7 @@ $loadTasks = static function (string $path): array { }; $saveTasks = static function (string $path, array $tasks): void { - file_put_contents( - $path, - json_encode($tasks, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), - LOCK_EX, - ); + file_put_contents($path, json_encode($tasks, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), LOCK_EX); }; $tasks = $loadTasks($storagePath); @@ -54,7 +50,10 @@ $main = new Container('flex flex-col bg-gray-100 gap-4 p-4 h-full w-full'); $title = new Label('Todo Liste', 'text-2xl font-bold text-black'); $main->addComponent($title); -$input = new TextInput('Neue Aufgabe hinzufügen …', 'flex-1 border border-gray-300 rounded px-3 py-2 bg-white text-black'); +$input = new TextInput( + 'Neue Aufgabe hinzufügen …', + 'flex-1 border border-gray-300 rounded px-3 py-2 bg-white text-black', +); $addButton = new Button('Hinzufügen', 'px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700'); $inputRow = new Container('flex flex-row gap-3 w-full'); $inputRow->addComponent($input); @@ -78,10 +77,10 @@ $renderTasks = function () use (&$tasks, $listContainer, $statusLabel, $storageP } foreach ($tasks as $index => $task) { - $row = new Container('flex flex-row items-center gap-3 w-full border border-gray-200 rounded px-3 py-2 bg-white shadow-sm'); - $taskLabelStyles = $task['done'] - ? 'flex-1 text-gray-500 line-through' - : 'flex-1 text-black'; + $row = new Container( + 'flex flex-row items-center gap-3 w-full border border-gray-200 rounded px-3 py-2 bg-white shadow-sm', + ); + $taskLabelStyles = $task['done'] ? 'flex-1 text-gray-500 line-through' : 'flex-1 text-black'; $taskLabel = new Label($task['title'], $taskLabelStyles); $row->addComponent($taskLabel); @@ -92,7 +91,14 @@ $renderTasks = function () use (&$tasks, $listContainer, $statusLabel, $storageP : 'px-3 py-1 text-sm bg-emerald-500 text-white rounded hover:bg-emerald-600', ); - $toggleButton->setOnClick(function () use (&$tasks, $task, $storagePath, $saveTasks, $statusLabel, $renderTasks) { + $toggleButton->setOnClick(function () use ( + &$tasks, + $task, + $storagePath, + $saveTasks, + $statusLabel, + $renderTasks, + ) { foreach ($tasks as &$entry) { if ($entry['id'] === $task['id']) { $entry['done'] = !$entry['done']; @@ -105,12 +111,17 @@ $renderTasks = function () use (&$tasks, $listContainer, $statusLabel, $storageP $renderTasks(); }); - $deleteButton = new Button( - 'Löschen', - 'px-3 py-1 text-sm bg-red-500 text-white rounded hover:bg-red-600', - ); + $deleteButton = new Button('Löschen', 'px-3 py-1 text-sm bg-red-500 text-white rounded hover:bg-red-600'); - $deleteButton->setOnClick(function () use (&$tasks, $index, $task, $storagePath, $saveTasks, $statusLabel, $renderTasks) { + $deleteButton->setOnClick(function () use ( + &$tasks, + $index, + $task, + $storagePath, + $saveTasks, + $statusLabel, + $renderTasks, + ) { array_splice($tasks, $index, 1); $saveTasks($storagePath, $tasks); $statusLabel->setText('Aufgabe entfernt: ' . $task['title']); @@ -147,4 +158,64 @@ $addButton->setOnClick(function () use (&$tasks, $input, $saveTasks, $storagePat $window->setRoot($main); $app->addWindow($window); + +// Setup Tray with callbacks +if (function_exists('tray_setup')) { + try { + tray_setup('', [ + [ + 'label' => 'Neue Aufgabe', + 'callback' => function ($idx) use ( + &$tasks, + $input, + $saveTasks, + $storagePath, + $statusLabel, + $renderTasks, + ) { + try { + error_log('Neue Aufgabe Callback called!'); + + $title = 'Tray: Neue Aufgabe'; + + error_log("Adding task: {$title}"); + + $tasks[] = [ + 'id' => uniqid('task_', true), + 'title' => $title, + 'done' => false, + ]; + + error_log('Saving tasks...'); + $saveTasks($storagePath, $tasks); + + error_log('Updating UI...'); + $statusLabel->setText('Aufgabe hinzugefügt: ' . $title); + $renderTasks(); + + error_log('Done!'); + } catch (\Throwable $e) { + error_log('ERROR in Neue Aufgabe callback: ' . $e->getMessage()); + error_log('Trace: ' . $e->getTraceAsString()); + } + }, + ], + [ + 'label' => 'Fenster anzeigen', + 'callback' => function ($idx) use ($window) { + // Show/focus window (TODO: implement window focus/show API) + }, + ], + [ + 'label' => 'Beenden', + 'callback' => function ($idx) use ($app) { + $app->quit(); + }, + ], + ]); + } catch (\Throwable $e) { + error_log('Tray setup failed: ' . $e->getMessage()); + } +} + $app->run(); diff --git a/examples/todo_data.json b/examples/todo_data.json index cef4a93..6e7f310 100644 --- a/examples/todo_data.json +++ b/examples/todo_data.json @@ -1,12 +1,7 @@ [ { - "id": "task_69164e23f0d356.41043316", - "title": "Test", + "id": "task_692840bf6e9035.94502029", + "title": "Tray: Neue Aufgabe", "done": false - }, - { - "id": "task_69164e28dae205.72302890", - "title": "Geht", - "done": true } ] \ No newline at end of file diff --git a/php-sdl3/.libs/sdl3.o b/php-sdl3/.libs/sdl3.o index 1cb718f..bee9ad7 100644 Binary files a/php-sdl3/.libs/sdl3.o and b/php-sdl3/.libs/sdl3.o differ diff --git a/php-sdl3/.libs/sdl3.so b/php-sdl3/.libs/sdl3.so index ab2993a..9794134 100755 Binary files a/php-sdl3/.libs/sdl3.so and b/php-sdl3/.libs/sdl3.so differ diff --git a/php-sdl3/config.m4 b/php-sdl3/config.m4 index 5f4b0ca..82ef4f1 100644 --- a/php-sdl3/config.m4 +++ b/php-sdl3/config.m4 @@ -87,6 +87,8 @@ if test "$PHP_SDL3" != "no"; then AC_MSG_WARN([libnotify not found via pkg-config, desktop_notify() will be disabled]) ]) + dnl SDL3 includes native tray support, no external dependencies needed + SDL_SOURCE_FILES="sdl3.c helper.c sdl3_image.c sdl3_ttf.c sdl3_events.c" PHP_NEW_EXTENSION(sdl3, $SDL_SOURCE_FILES, $ext_shared) diff --git a/php-sdl3/config.nice b/php-sdl3/config.nice index 8553f8e..52d739d 100755 --- a/php-sdl3/config.nice +++ b/php-sdl3/config.nice @@ -3,7 +3,4 @@ # Created by configure './configure' \ -'--with-sdl3' \ -'--with-sdl3-image' \ -'--with-sdl3-ttf' \ "$@" diff --git a/php-sdl3/config.status b/php-sdl3/config.status index 9c67e06..2e07011 100755 --- a/php-sdl3/config.status +++ b/php-sdl3/config.status @@ -413,7 +413,7 @@ $config_headers Report bugs to the package provider." -ac_cs_config='--with-sdl3 --with-sdl3-image --with-sdl3-ttf' +ac_cs_config='' ac_cs_version="\ config.status configured by ./configure, generated by GNU Autoconf 2.72, @@ -494,7 +494,7 @@ if $ac_cs_silent; then fi if $ac_cs_recheck; then - set X /bin/bash './configure' '--with-sdl3' '--with-sdl3-image' '--with-sdl3-ttf' $ac_configure_extra_args --no-create --no-recursion + set X /bin/bash './configure' $ac_configure_extra_args --no-create --no-recursion shift \printf "%s\n" "running CONFIG_SHELL=/bin/bash $*" >&6 CONFIG_SHELL='/bin/bash' diff --git a/php-sdl3/configure b/php-sdl3/configure index 188a98a..f0d50e7 100755 --- a/php-sdl3/configure +++ b/php-sdl3/configure @@ -5247,6 +5247,7 @@ printf "%s\n" "#define HAVE_LIBNOTIFY 1" >>confdefs.h fi + SDL_SOURCE_FILES="sdl3.c helper.c sdl3_image.c sdl3_ttf.c sdl3_events.c" @@ -6102,7 +6103,7 @@ ia64-*-hpux*) ;; *-*-irix6*) # Find out which ABI we are using. - echo '#line 6105 "configure"' > conftest.$ac_ext + echo '#line 6106 "configure"' > conftest.$ac_ext if { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$ac_compile\""; } >&5 (eval $ac_compile) 2>&5 ac_status=$? @@ -7481,7 +7482,7 @@ else case e in #( LDFLAGS="$LDFLAGS -Wl,-exported_symbols_list,conftest.sym" cat > conftest.$ac_ext <&5) + (eval echo "\"configure:7647: $lt_compile\"" >&5) (eval "$lt_compile" 2>conftest.err) ac_status=$? cat conftest.err >&5 - echo "configure:7650: \$? = $ac_status" >&5 + echo "configure:7651: \$? = $ac_status" >&5 if (exit $ac_status) && test -s "$ac_outfile"; then # The compiler can only warn and ignore the option if not recognized # So say no if there are warnings other than the usual output. @@ -7943,11 +7944,11 @@ else case e in #( -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \ -e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \ -e 's:$: $lt_compiler_flag:'` - (eval echo "\"configure:7946: $lt_compile\"" >&5) + (eval echo "\"configure:7947: $lt_compile\"" >&5) (eval "$lt_compile" 2>conftest.err) ac_status=$? cat conftest.err >&5 - echo "configure:7950: \$? = $ac_status" >&5 + echo "configure:7951: \$? = $ac_status" >&5 if (exit $ac_status) && test -s "$ac_outfile"; then # The compiler can only warn and ignore the option if not recognized # So say no if there are warnings other than the usual output. @@ -8051,11 +8052,11 @@ else case e in #( -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \ -e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \ -e 's:$: $lt_compiler_flag:'` - (eval echo "\"configure:8054: $lt_compile\"" >&5) + (eval echo "\"configure:8055: $lt_compile\"" >&5) (eval "$lt_compile" 2>out/conftest.err) ac_status=$? cat out/conftest.err >&5 - echo "configure:8058: \$? = $ac_status" >&5 + echo "configure:8059: \$? = $ac_status" >&5 if (exit $ac_status) && test -s out/conftest2.$ac_objext then # The compiler can only warn and ignore the option if not recognized @@ -8516,7 +8517,7 @@ _LT_EOF # Determine the default libpath from the value encoded in an empty executable. cat > conftest.$ac_ext < conftest.$ac_ext < conftest.$ac_ext < conftest.$ac_ext < conftest.$ac_ext < conftest.$ac_ext <&5) + (eval echo "\"configure:12607: $lt_compile\"" >&5) (eval "$lt_compile" 2>conftest.err) ac_status=$? cat conftest.err >&5 - echo "configure:12610: \$? = $ac_status" >&5 + echo "configure:12611: \$? = $ac_status" >&5 if (exit $ac_status) && test -s "$ac_outfile"; then # The compiler can only warn and ignore the option if not recognized # So say no if there are warnings other than the usual output. @@ -12711,11 +12712,11 @@ else case e in #( -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \ -e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \ -e 's:$: $lt_compiler_flag:'` - (eval echo "\"configure:12714: $lt_compile\"" >&5) + (eval echo "\"configure:12715: $lt_compile\"" >&5) (eval "$lt_compile" 2>out/conftest.err) ac_status=$? cat out/conftest.err >&5 - echo "configure:12718: \$? = $ac_status" >&5 + echo "configure:12719: \$? = $ac_status" >&5 if (exit $ac_status) && test -s out/conftest2.$ac_objext then # The compiler can only warn and ignore the option if not recognized diff --git a/php-sdl3/modules/sdl3.so b/php-sdl3/modules/sdl3.so index ab2993a..9794134 100755 Binary files a/php-sdl3/modules/sdl3.so and b/php-sdl3/modules/sdl3.so differ diff --git a/php-sdl3/sdl3.c b/php-sdl3/sdl3.c index 93b1ad3..0a6c590 100644 --- a/php-sdl3/sdl3.c +++ b/php-sdl3/sdl3.c @@ -11,11 +11,15 @@ #include "sdl3_events.h" #include #include +#include #ifdef HAVE_LIBNOTIFY #include #endif +// SDL3 native tray support +#include + // Resource handles (nicht static, damit sie in anderen Modulen verfügbar sind) int le_sdl_window; int le_sdl_renderer; @@ -45,6 +49,64 @@ static void sdl_texture_dtor(zend_resource *rsrc) { } } +// --- Tray integration state --- +// SDL3 Tray globals +static SDL_Tray *g_sdl_tray = NULL; +static SDL_TrayMenu *g_sdl_tray_menu = NULL; +static SDL_TrayEntry **g_sdl_tray_entries = NULL; +static int g_sdl_tray_entry_count = 0; +static zval *g_tray_callbacks = NULL; // Array of PHP callbacks for each tray entry + +static void SDLCALL php_tray_callback(void *userdata, SDL_TrayEntry *entry) { + intptr_t idx = (intptr_t)userdata; + idx = idx-1; + // Log all callback invocations for debugging + php_error_docref(NULL, E_NOTICE, "Tray callback invoked: userdata=%p (idx=%d), g_tray_callbacks=%p", + userdata, (int)idx, (void*)g_tray_callbacks); + + // userdata can be NULL for events without callbacks (e.g., clicking the tray icon itself) + // This is normal, so just return silently + if (!userdata || !g_tray_callbacks) { + php_error_docref(NULL, E_NOTICE, "Tray callback: skipping (userdata=%p, callbacks=%p)", + userdata, (void*)g_tray_callbacks); + return; + } + + // Check if we have a callback for this index + if (idx < 0 || idx >= g_sdl_tray_entry_count) { + php_error_docref(NULL, E_WARNING, "Tray callback: invalid index %d (max %d)", (int)idx, g_sdl_tray_entry_count); + return; + } + + zval *callback = &g_tray_callbacks[idx]; + + // Only call if callback is set and callable + if (Z_TYPE_P(callback) == IS_UNDEF) { + php_error_docref(NULL, E_WARNING, "Tray callback %d: callback is undefined", (int)idx); + return; + } + + if (!zend_is_callable(callback, 0, NULL)) { + php_error_docref(NULL, E_WARNING, "Tray callback %d: callback is not callable", (int)idx); + return; + } + + zval retval; + zval params[1]; + + // Pass the index as parameter to the callback + ZVAL_LONG(¶ms[0], idx); + + // Call the PHP callback + int result = call_user_function(EG(function_table), NULL, callback, &retval, 1, params); + + if (result == SUCCESS) { + zval_ptr_dtor(&retval); + } else { + php_error_docref(NULL, E_WARNING, "Tray callback %d: call_user_function failed with code %d", (int)idx, result); + } +} + PHP_MINIT_FUNCTION(sdl3) { le_sdl_window = zend_register_list_destructors_ex(sdl_window_dtor, NULL, "SDL_Window", module_number); le_sdl_renderer = zend_register_list_destructors_ex(sdl_renderer_dtor, NULL, "SDL_Renderer", module_number); @@ -168,8 +230,10 @@ PHP_FUNCTION(sdl_get_window_id) { PHP_FUNCTION(sdl_create_renderer) { zval *win_res; SDL_Window *win; + char *renderer_name = NULL; + size_t renderer_name_len = 0; - if (zend_parse_parameters(ZEND_NUM_ARGS(), "r", &win_res) == FAILURE) { + if (zend_parse_parameters(ZEND_NUM_ARGS(), "r|s", &win_res, &renderer_name, &renderer_name_len) == FAILURE) { RETURN_THROWS(); } @@ -178,13 +242,32 @@ PHP_FUNCTION(sdl_create_renderer) { RETURN_FALSE; } - SDL_Renderer *ren = SDL_CreateRenderer(win, NULL); + SDL_Renderer *ren = SDL_CreateRenderer(win, renderer_name_len > 0 ? renderer_name : NULL); if (!ren) { RETURN_FALSE; } RETURN_RES(zend_register_resource(ren, le_sdl_renderer)); } +PHP_FUNCTION(sdl_get_num_render_drivers) { + int num = SDL_GetNumRenderDrivers(); + RETURN_LONG(num); +} + +PHP_FUNCTION(sdl_get_render_driver) { + zend_long index; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "l", &index) == FAILURE) { + RETURN_THROWS(); + } + + const char *name = SDL_GetRenderDriver((int)index); + if (!name) { + RETURN_FALSE; + } + RETURN_STRING(name); +} + PHP_FUNCTION(sdl_set_render_draw_color) { zval *ren_res; SDL_Renderer *ren; @@ -529,6 +612,211 @@ PHP_FUNCTION(sdl_set_texture_alpha_mod) { RETURN_TRUE; } +PHP_FUNCTION(tray_setup) +{ + char *icon_path; + size_t icon_len; + zval *menu_arr = NULL; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "s|a", &icon_path, &icon_len, &menu_arr) == FAILURE) { + RETURN_THROWS(); + } + + // Clean up existing tray if any + if (g_sdl_tray) { + SDL_DestroyTray(g_sdl_tray); + g_sdl_tray = NULL; + g_sdl_tray_menu = NULL; + } + if (g_sdl_tray_entries) { + efree(g_sdl_tray_entries); + g_sdl_tray_entries = NULL; + g_sdl_tray_entry_count = 0; + } + if (g_tray_callbacks) { + // Free old callbacks + for (int i = 0; i < g_sdl_tray_entry_count; i++) { + zval_ptr_dtor(&g_tray_callbacks[i]); + } + efree(g_tray_callbacks); + g_tray_callbacks = NULL; + } + + // Load icon if provided (optional) + SDL_Surface *icon_surface = NULL; + if (icon_len > 0) { + icon_surface = SDL_LoadBMP(icon_path); + // Icon can be NULL, SDL will handle it + } + + // Initialize video subsystem if not already initialized (required for tray) + if (!SDL_WasInit(SDL_INIT_VIDEO)) { + if (SDL_InitSubSystem(SDL_INIT_VIDEO) < 0) { + if (icon_surface) { + SDL_DestroySurface(icon_surface); + } + php_error_docref(NULL, E_WARNING, "Failed to init video subsystem for tray: %s", SDL_GetError()); + RETURN_FALSE; + } + } + + // Check if DISPLAY is set (required for GTK-based tray on Linux) + #ifdef __linux__ + const char *display = getenv("DISPLAY"); + const char *wayland = getenv("WAYLAND_DISPLAY"); + if (!display && !wayland) { + if (icon_surface) { + SDL_DestroySurface(icon_surface); + } + php_error_docref(NULL, E_WARNING, "Cannot create tray: No DISPLAY or WAYLAND_DISPLAY environment variable set"); + RETURN_FALSE; + } + #endif + + // Create SDL3 tray + g_sdl_tray = SDL_CreateTray(icon_surface, "PHP SDL3 Tray"); + + if (icon_surface) { + SDL_DestroySurface(icon_surface); + } + + if (!g_sdl_tray) { + php_error_docref(NULL, E_WARNING, "Failed to create tray: %s", SDL_GetError()); + RETURN_FALSE; + } + + // Create tray menu + g_sdl_tray_menu = SDL_CreateTrayMenu(g_sdl_tray); + if (!g_sdl_tray_menu) { + SDL_DestroyTray(g_sdl_tray); + g_sdl_tray = NULL; + php_error_docref(NULL, E_WARNING, "Failed to create tray menu: %s", SDL_GetError()); + RETURN_FALSE; + } + + // Add menu items if provided + if (menu_arr && Z_TYPE_P(menu_arr) == IS_ARRAY) { + HashTable *ht = Z_ARRVAL_P(menu_arr); + int count = zend_hash_num_elements(ht); + + if (count > 0) { + g_sdl_tray_entries = ecalloc(count, sizeof(SDL_TrayEntry *)); + g_tray_callbacks = ecalloc(count, sizeof(zval)); + g_sdl_tray_entry_count = count; + + int idx = 0; + zval *val; + ZEND_HASH_FOREACH_VAL(ht, val) { + if (idx >= count) { + break; + } + + const char *label = NULL; + zval *callback = NULL; + + // Handle array entries: ['label' => '...', 'callback' => function] + if (Z_TYPE_P(val) == IS_ARRAY) { + zval *label_val = zend_hash_str_find(Z_ARRVAL_P(val), "label", sizeof("label") - 1); + zval *callback_val = zend_hash_str_find(Z_ARRVAL_P(val), "callback", sizeof("callback") - 1); + + if (label_val && Z_TYPE_P(label_val) == IS_STRING) { + label = Z_STRVAL_P(label_val); + } + + if (callback_val && zend_is_callable(callback_val, 0, NULL)) { + callback = callback_val; + } + } + // Handle simple string entries (backward compatibility) + else if (Z_TYPE_P(val) == IS_STRING) { + label = Z_STRVAL_P(val); + } + + // Create entry (NULL label creates separator) + SDL_TrayEntry *entry = SDL_InsertTrayEntryAt( + g_sdl_tray_menu, + -1, + label, + SDL_TRAYENTRY_BUTTON + ); + + if (!entry) { + php_error_docref(NULL, E_WARNING, "Failed to create tray entry %d ('%s'): %s", idx, label ? label : "(null)", SDL_GetError()); + } + + if (entry && label) { + // Set callback with index+1 as userdata (so index 0 doesn't become NULL) + SDL_SetTrayEntryCallback(entry, php_tray_callback, (void *)(intptr_t)(idx + 1)); + php_error_docref(NULL, E_NOTICE, "Registered tray entry %d: '%s' with callback=%s", idx, label, callback ? "YES" : "NO"); + } + + // Store PHP callback if provided + if (callback) { + ZVAL_COPY(&g_tray_callbacks[idx], callback); + } else { + ZVAL_UNDEF(&g_tray_callbacks[idx]); + } + + g_sdl_tray_entries[idx] = entry; + idx++; + } ZEND_HASH_FOREACH_END(); + } + } + + RETURN_TRUE; +} + +PHP_FUNCTION(tray_poll) +{ + zend_bool blocking = 0; + if (zend_parse_parameters(ZEND_NUM_ARGS(), "|b", &blocking) == FAILURE) { + RETURN_THROWS(); + } + + if (!g_sdl_tray) { + RETURN_LONG(-1); + } + + // SDL_UpdateTrays() processes events that were already polled by sdl_poll_event() + // The event polling happens in the Application loop before this is called + // SDL_UpdateTrays() will trigger our C callbacks, which call the PHP callbacks + SDL_UpdateTrays(); + + // Always return -1 (events are handled via callbacks) + RETURN_LONG(-1); +} + +PHP_FUNCTION(tray_exit) +{ + if (zend_parse_parameters_none() == FAILURE) { + RETURN_THROWS(); + } + + if (g_sdl_tray) { + SDL_DestroyTray(g_sdl_tray); + g_sdl_tray = NULL; + g_sdl_tray_menu = NULL; + } + + if (g_sdl_tray_entries) { + efree(g_sdl_tray_entries); + g_sdl_tray_entries = NULL; + } + + if (g_tray_callbacks) { + // Free callbacks + for (int i = 0; i < g_sdl_tray_entry_count; i++) { + zval_ptr_dtor(&g_tray_callbacks[i]); + } + efree(g_tray_callbacks); + g_tray_callbacks = NULL; + } + + g_sdl_tray_entry_count = 0; + + RETURN_TRUE; +} + PHP_FUNCTION(desktop_notify) { char *title, *body; @@ -1105,6 +1393,25 @@ PHP_FUNCTION(sdl_get_current_video_driver) { RETURN_STRING(drv); } +PHP_FUNCTION(sdl_get_num_video_drivers) { + int num = SDL_GetNumVideoDrivers(); + RETURN_LONG(num); +} + +PHP_FUNCTION(sdl_get_video_driver) { + zend_long index; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "l", &index) == FAILURE) { + RETURN_THROWS(); + } + + const char *driver = SDL_GetVideoDriver((int)index); + if (!driver) { + RETURN_FALSE; + } + RETURN_STRING(driver); +} + PHP_FUNCTION(sdl_start_text_input) { zval *win_res; SDL_Window *win; @@ -1167,6 +1474,14 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_create_renderer, 0, 0, 1) ZEND_ARG_INFO(0, window) + ZEND_ARG_INFO(0, renderer_name) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_get_num_render_drivers, 0, 0, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_get_render_driver, 0, 0, 1) + ZEND_ARG_INFO(0, index) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_set_render_draw_color, 0, 0, 5) @@ -1242,6 +1557,18 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_set_texture_alpha_mod, 0, 0, 2) ZEND_ARG_INFO(0, alpha) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_INFO_EX(arginfo_tray_setup, 0, 0, 1) + ZEND_ARG_INFO(0, icon) + ZEND_ARG_ARRAY_INFO(0, menuItems, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_tray_poll, 0, 0, 0) + ZEND_ARG_INFO(0, blocking) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_tray_exit, 0, 0, 0) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_INFO_EX(arginfo_desktop_notify, 0, 0, 2) ZEND_ARG_INFO(0, title) ZEND_ARG_INFO(0, body) @@ -1329,6 +1656,14 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_get_current_video_driver, 0, 0, 0) ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_get_num_video_drivers, 0, 0, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_get_video_driver, 0, 0, 1) + ZEND_ARG_INFO(0, index) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_get_renderer_output_size, 0, 0, 1) ZEND_ARG_INFO(0, renderer) ZEND_END_ARG_INFO() @@ -1350,6 +1685,8 @@ const zend_function_entry sdl3_functions[] = { PHP_FE(sdl_destroy_renderer, arginfo_sdl_destroy_renderer) PHP_FE(sdl_get_window_id, arginfo_sdl_get_window_id) PHP_FE(sdl_create_renderer, arginfo_sdl_create_renderer) + PHP_FE(sdl_get_num_render_drivers, arginfo_sdl_get_num_render_drivers) + PHP_FE(sdl_get_render_driver, arginfo_sdl_get_render_driver) PHP_FE(sdl_set_render_draw_color, arginfo_sdl_set_render_draw_color) PHP_FE(sdl_render_clear, arginfo_sdl_render_clear) PHP_FE(sdl_render_fill_rect, arginfo_sdl_render_fill_rect) @@ -1365,6 +1702,11 @@ const zend_function_entry sdl3_functions[] = { PHP_FE(sdl_update_texture, arginfo_sdl_update_texture) PHP_FE(sdl_set_texture_blend_mode, arginfo_sdl_set_texture_blend_mode) PHP_FE(sdl_set_texture_alpha_mod, arginfo_sdl_set_texture_alpha_mod) + // Tray API + PHP_FE(tray_setup, arginfo_tray_setup) + PHP_FE(tray_poll, arginfo_tray_poll) + PHP_FE(tray_exit, arginfo_tray_exit) + // Desktop notifications PHP_FE(desktop_notify, arginfo_desktop_notify) PHP_FE(sdl_create_box_shadow_texture, arginfo_sdl_create_box_shadow_texture) PHP_FE(sdl_get_render_target, arginfo_sdl_get_render_target) @@ -1378,6 +1720,8 @@ const zend_function_entry sdl3_functions[] = { PHP_FE(sdl_get_window_display_scale, arginfo_sdl_get_window_display_scale) PHP_FE(sdl_get_display_content_scale, arginfo_sdl_get_display_content_scale) PHP_FE(sdl_get_current_video_driver, arginfo_sdl_get_current_video_driver) + PHP_FE(sdl_get_num_video_drivers, arginfo_sdl_get_num_video_drivers) + PHP_FE(sdl_get_video_driver, arginfo_sdl_get_video_driver) PHP_FE(sdl_get_renderer_output_size, arginfo_sdl_get_renderer_output_size) PHP_FE(sdl_start_text_input, arginfo_sdl_start_text_input) PHP_FE(sdl_stop_text_input, arginfo_sdl_stop_text_input) diff --git a/src/Framework/Application.php b/src/Framework/Application.php index 43fc8b9..770f684 100644 --- a/src/Framework/Application.php +++ b/src/Framework/Application.php @@ -86,6 +86,7 @@ class Application while ($this->running && count($this->windows) > 0) { $frameStart = microtime(true); + // Layout all windows FIRST (sets window references and calculates positions) foreach ($this->windows as $windowId => $window) { $window->layout(); @@ -100,6 +101,12 @@ class Application } } + // Process tray events AFTER polling SDL events + // This ensures tray callbacks are triggered + if (function_exists('tray_poll')) { + tray_poll(false); + } + // Coalesce mouse motion events: Only keep the last MouseMotion event per window // This dramatically reduces the number of events to process $coalescedEvents = []; diff --git a/test_render_drivers.php b/test_render_drivers.php new file mode 100644 index 0000000..2271bb8 --- /dev/null +++ b/test_render_drivers.php @@ -0,0 +1,70 @@ + 0) { + echo "Trying with explicit driver names:\n"; + for ($i = 0; $i < $numDrivers; $i++) { + $driverName = sdl_get_render_driver($i); + echo " Trying '{$driverName}'...\n"; + $renderer = sdl_create_renderer($window, $driverName); + if ($renderer) { + echo " SUCCESS with '{$driverName}'\n"; + sdl_destroy_renderer($renderer); + break; + } else { + echo ' FAILED: ' . sdl_get_error() . "\n"; + } + } + } +} else { + echo "Renderer created successfully!\n"; + sdl_destroy_renderer($renderer); +} + +// Cleanup +sdl_destroy_window($window); +sdl_quit(); + +echo "\nTest completed!\n"; diff --git a/test_renderer_simple.php b/test_renderer_simple.php new file mode 100644 index 0000000..d851c3a --- /dev/null +++ b/test_renderer_simple.php @@ -0,0 +1,67 @@ + 300, 'y' => 200, 'w' => 200, 'h' => 150]); + + // Grünes Rechteck zeichnen (Umriss) + sdl_set_render_draw_color($renderer, 0, 255, 0, 255); + sdl_render_rect($renderer, ['x' => 350, 'y' => 250, 'w' => 100, 'h' => 100]); + + // Blaues Rechteck zeichnen (klein, gefüllt) + sdl_set_render_draw_color($renderer, 0, 0, 255, 255); + sdl_render_fill_rect($renderer, ['x' => 100, 'y' => 100, 'w' => 50, 'h' => 50]); + + // Renderer anzeigen + sdl_render_present($renderer); + + // Kleine Pause um CPU zu schonen + sdl_delay(16); // ~60 FPS +} + +// Aufräumen +sdl_destroy_renderer($renderer); +sdl_destroy_window($window); +sdl_quit(); + +echo "Cleanup complete\n"; diff --git a/test_video_drivers.php b/test_video_drivers.php new file mode 100644 index 0000000..4488723 --- /dev/null +++ b/test_video_drivers.php @@ -0,0 +1,63 @@ +