diff --git a/examples/ServerManager/UI/KanbanTab.php b/examples/ServerManager/UI/KanbanTab.php index ef30a1d..049fe7a 100644 --- a/examples/ServerManager/UI/KanbanTab.php +++ b/examples/ServerManager/UI/KanbanTab.php @@ -3,10 +3,14 @@ namespace ServerManager\UI; use PHPNative\Framework\Settings; +use PHPNative\Tailwind\Data\Icon as IconName; use PHPNative\Ui\Widget\Button; use PHPNative\Ui\Widget\Container; +use PHPNative\Ui\Widget\Icon; use PHPNative\Ui\Widget\Label; use PHPNative\Ui\Widget\TextInput; +use PHPNative\Ui\Widget\TextArea; +use PHPNative\Ui\Widget\Modal; class KanbanTab { @@ -14,6 +18,12 @@ class KanbanTab private Settings $settings; private Container $boardsContainer; private TextInput $newBoardInput; + private Modal $editModal; + private TextInput $editTitleInput; + private TextArea $editDetailsArea; + private Container $editBoardButtonsContainer; + private string $currentEditingBoard = 'neu'; + private null|string $currentEditingTaskId = null; public function __construct(Settings $settings) { @@ -69,6 +79,9 @@ class KanbanTab $kanbanTab->renderBoards(); }); + // Edit modal for tasks + $this->createEditModal(); + $this->renderBoards(); } @@ -92,6 +105,173 @@ class KanbanTab } } + private function rebuildBoardButtons(): void + { + if (!isset($this->editBoardButtonsContainer)) { + return; + } + + $this->editBoardButtonsContainer->clearChildren(); + + $boards = $this->settings->get('kanban.boards', []); + if (!is_array($boards) || empty($boards)) { + $boards = ['neu', 'in arbeit', 'fertig']; + } + + foreach ($boards as $boardName) { + $isSelected = ($boardName === $this->currentEditingBoard); + + $style = 'px-2 py-1 rounded text-xs border '; + if ($isSelected) { + $style .= 'bg-blue-600 text-white border-blue-600'; + } else { + $style .= 'bg-white text-gray-700 border-gray-300 hover:bg-gray-100'; + } + + $button = new Button($boardName, $style); + + $kanbanTab = $this; + $button->setOnClick(function () use ($kanbanTab, $boardName) { + $kanbanTab->currentEditingBoard = $boardName; + $kanbanTab->rebuildBoardButtons(); + }); + + $this->editBoardButtonsContainer->addComponent($button); + } + } + + private function createEditModal(): void + { + $content = new Container('flex flex-col bg-white rounded-lg shadow-xl p-4 gap-3 w-[480] max-h-[420]'); + $content->setUseTextureCache(false); + + $content->addComponent(new Label('Task bearbeiten', 'text-lg font-bold text-black')); + + // Title + $content->addComponent(new Label('Titel', 'text-xs text-gray-600')); + $this->editTitleInput = new TextInput( + 'Titel', + 'w-full border border-gray-300 rounded px-3 py-2 bg-white text-black text-sm', + ); + $content->addComponent($this->editTitleInput); + + // Details + $content->addComponent(new Label('Details', 'text-xs text-gray-600')); + $this->editDetailsArea = new TextArea( + '', + 'Details...', + 'flex-1 border border-gray-300 rounded px-3 py-2 bg-white text-black text-xs', + ); + $this->editDetailsArea->setUseTextureCache(false); + $content->addComponent($this->editDetailsArea); + + // Board selection (selectbox-artig) + $content->addComponent(new Label('Board', 'text-xs text-gray-600')); + $this->editBoardButtonsContainer = new Container('flex flex-row flex-wrap gap-1'); + $this->rebuildBoardButtons(); + $content->addComponent($this->editBoardButtonsContainer); + + // Buttons + $buttonRow = new Container('flex flex-row justify-end gap-2 mt-2'); + $cancelButton = new Button( + 'Abbrechen', + 'px-3 py-2 bg-gray-300 rounded hover:bg-gray-400', + null, + 'text-black text-sm', + ); + $saveButton = new Button( + 'Speichern', + 'px-3 py-2 bg-green-600 rounded hover:bg-green-700', + null, + 'text-white text-sm', + ); + + $kanbanTab = $this; + $cancelButton->setOnClick(function () use ($kanbanTab) { + $kanbanTab->editModal->setVisible(false); + }); + $saveButton->setOnClick(function () use ($kanbanTab) { + $kanbanTab->saveEditedTask(); + }); + + $buttonRow->addComponent($cancelButton); + $buttonRow->addComponent($saveButton); + $content->addComponent($buttonRow); + + $this->editModal = new Modal($content); + $this->tab->addComponent($this->editModal); + } + + private function openEditModal(array $task): void + { + $this->currentEditingTaskId = $task['id'] ?? null; + if ($this->currentEditingTaskId === null) { + return; + } + + $title = (string) ($task['title'] ?? ''); + $details = (string) ($task['details'] ?? ''); + $board = (string) ($task['board'] ?? 'neu'); + + $this->editTitleInput->setValue($title); + $this->editDetailsArea->setValue($details); + $this->currentEditingBoard = $board; + $this->rebuildBoardButtons(); + + $this->editModal->setVisible(true); + } + + private function saveEditedTask(): void + { + if ($this->currentEditingTaskId === null) { + $this->editModal->setVisible(false); + return; + } + + $title = trim($this->editTitleInput->getValue()); + $details = trim($this->editDetailsArea->getValue()); + $board = trim($this->currentEditingBoard); + + if ($title === '') { + $title = 'Ohne Titel'; + } + + if ($board === '') { + $board = 'neu'; + } + + $tasks = $this->settings->get('kanban.tasks', []); + if (!is_array($tasks)) { + $tasks = []; + } + + foreach ($tasks as &$task) { + if (($task['id'] ?? null) === $this->currentEditingTaskId) { + $task['title'] = $title; + $task['details'] = $details; + $task['board'] = $board; + break; + } + } + unset($task); + + // Ensure board exists + $boards = $this->settings->get('kanban.boards', []); + if (!is_array($boards)) { + $boards = []; + } + if (!in_array($board, $boards, true)) { + $boards[] = $board; + $this->settings->set('kanban.boards', $boards); + } + + $this->settings->set('kanban.tasks', $tasks); + $this->settings->save(); + + $this->editModal->setVisible(false); + $this->renderBoards(); + } + private function renderBoards(): void { $this->boardsContainer->clearChildren(); @@ -127,11 +307,66 @@ class KanbanTab $title = (string) ($task['title'] ?? ''); $serverId = $task['server_id'] ?? null; $serverLabel = $serverId !== null ? ('Server #' . $serverId) : 'Kein Server'; + $taskId = $task['id'] ?? null; $card = new Container( 'flex flex-col gap-1 px-3 py-2 bg-white border border-gray-200 rounded shadow-sm', ); - $card->addComponent(new Label($title, 'text-xs text-gray-900')); + + // Header row with title and action icons + $headerRow = new Container('flex flex-row items-center gap-1'); + $headerRow->addComponent(new Label($title, 'text-xs text-gray-900 flex-1')); + + // Edit icon button + $editButton = new Button( + '', + 'p-1 rounded hover:bg-blue-100', + null, + 'text-blue-600', + ); + $editIcon = new Icon(IconName::edit, 12, 'text-blue-600'); + $editButton->setIcon($editIcon); + + $kanbanTab = $this; + $editButton->setOnClick(function () use ($kanbanTab, $task) { + $kanbanTab->openEditModal($task); + }); + + // Delete icon button + $deleteButton = new Button( + '', + 'p-1 rounded hover:bg-red-100', + null, + 'text-red-600', + ); + $deleteIcon = new Icon(IconName::trash, 12, 'text-red-600'); + $deleteButton->setIcon($deleteIcon); + + $deleteButton->setOnClick(function () use ($kanbanTab, $taskId) { + if ($taskId === null) { + return; + } + + $tasks = $kanbanTab->settings->get('kanban.tasks', []); + if (!is_array($tasks)) { + return; + } + + $tasks = array_values(array_filter( + $tasks, + static fn($t) => ($t['id'] ?? null) !== $taskId, + )); + + $kanbanTab->settings->set('kanban.tasks', $tasks); + $kanbanTab->settings->save(); + $kanbanTab->renderBoards(); + }); + + $headerRow->addComponent($editButton); + $headerRow->addComponent($deleteButton); + $card->addComponent($headerRow); + + // Server label $card->addComponent(new Label($serverLabel, 'text-[10px] text-gray-500')); $columnBody->addComponent($card); diff --git a/examples/ServerManager/UI/ServerListTab.php b/examples/ServerManager/UI/ServerListTab.php index d2b3e9d..9160e0e 100644 --- a/examples/ServerManager/UI/ServerListTab.php +++ b/examples/ServerManager/UI/ServerListTab.php @@ -82,13 +82,13 @@ class ServerListTab // Bulk action buttons for selected servers $rebootButton = new Button( 'Reboot ausgewählte', - 'px-3 py-2 bg-red-600 rounded hover:bg-red-700', + 'px-3 shadow-lg/50 py-2 bg-red-600 rounded hover:bg-red-700', null, 'text-white', ); $updateButton = new Button( 'Update ausgewählte', - 'px-3 py-2 bg-amber-600 rounded hover:bg-amber-700', + 'px-3 shadow-lg/50 hover:shadow-red-600 py-2 bg-amber-600 rounded hover:bg-amber-700', null, 'text-white', ); @@ -260,12 +260,7 @@ class ServerListTab 'Neue Task...', 'flex-1 border border-gray-300 rounded px-3 py-2 bg-white text-black', ); - $addTodoButton = new Button( - '+', - 'px-3 py-2 bg-blue-600 rounded hover:bg-blue-700', - null, - 'text-white', - ); + $addTodoButton = new Button('+', 'px-3 py-2 bg-blue-600 rounded hover:bg-blue-700', null, 'text-white'); $todoInputRow->addComponent($this->todoInput); $todoInputRow->addComponent($addTodoButton); $detailPanel->addComponent($todoInputRow); @@ -306,7 +301,7 @@ class ServerListTab // Load per-server settings (API key, todos) $serverId = $row['id'] ?? null; - $serverListTab->currentServerId = is_numeric($serverId) ? (int) $serverId : null; + $serverListTab->currentServerId = is_numeric($serverId) ? ((int) $serverId) : null; if ($serverListTab->currentServerId !== null) { $settingsKeyBase = 'servers.' . $serverListTab->currentServerId; @@ -388,20 +383,14 @@ class ServerListTab echo "Error: {$result['error']}\n"; } elseif (isset($result['success'], $result['servers'])) { // Basisdaten setzen, Docker-Status initial auf "pending" - $serverListTab->currentServerData = array_map( - static fn($row) => array_merge( - [ - 'docker_status' => 'pending', - 'docker' => null, - 'docker_error' => null, - 'needs_reboot' => 'unbekannt', - 'updates_available' => 'unbekannt', - 'os_version' => 'unbekannt', - ], - $row, - ), - $result['servers'], - ); + $serverListTab->currentServerData = array_map(static fn($row) => array_merge([ + 'docker_status' => 'pending', + 'docker' => null, + 'docker_error' => null, + 'needs_reboot' => 'unbekannt', + 'updates_available' => 'unbekannt', + 'os_version' => 'unbekannt', + ], $row), $result['servers']); $serverListTab->table->setData($serverListTab->currentServerData, false); $serverListTab->statusLabel->setText('Server geladen: ' . $result['count'] . ' gefunden'); @@ -788,9 +777,8 @@ class ServerListTab $serverListTab->settings->set($settingsKeyBase . '.api_key', $apiKey); $serverListTab->settings->save(); - $serverListTab->statusLabel->setText( - 'Server-Einstellungen gespeichert für Server #' . $serverListTab->currentServerId, - ); + $serverListTab->statusLabel->setText('Server-Einstellungen gespeichert für Server #' . + $serverListTab->currentServerId); }); // Add TODO for current server @@ -918,9 +906,7 @@ class ServerListTab $this->todoListContainer->clearChildren(); if (empty($this->currentServerTodos)) { - $this->todoListContainer->addComponent( - new Label('Keine Tasks', 'text-xs text-gray-500 italic'), - ); + $this->todoListContainer->addComponent(new Label('Keine Tasks', 'text-xs text-gray-500 italic')); return; } @@ -929,10 +915,7 @@ class ServerListTab $board = (string) ($task['board'] ?? 'neu'); $row = new Container('flex flex-row items-center gap-2'); - $row->addComponent(new Label( - '- ' . $title . ' [' . $board . ']', - 'text-xs text-gray-800 flex-1', - )); + $row->addComponent(new Label('- ' . $title . ' [' . $board . ']', 'text-xs text-gray-800 flex-1')); $removeButton = new Button( 'x', @@ -953,10 +936,7 @@ class ServerListTab return; } - $tasks = array_values(array_filter( - $tasks, - static fn($t) => ($t['id'] ?? null) !== $taskId, - )); + $tasks = array_values(array_filter($tasks, static fn($t) => ($t['id'] ?? null) !== $taskId)); $serverListTab->settings->set('kanban.tasks', $tasks); $serverListTab->settings->save(); @@ -1010,12 +990,9 @@ class ServerListTab $tasks = []; } - $this->currentServerTodos = array_values(array_filter( - $tasks, - function ($task) { - return (int) ($task['server_id'] ?? 0) === (int) $this->currentServerId; - }, - )); + $this->currentServerTodos = array_values(array_filter($tasks, function ($task) { + return ((int) ($task['server_id'] ?? 0)) === ((int) $this->currentServerId); + })); $this->renderTodoList(); } diff --git a/php-sdl3/.libs/sdl3.o b/php-sdl3/.libs/sdl3.o index c70678f..dee1ae3 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 862d6ca..8f7238d 100755 Binary files a/php-sdl3/.libs/sdl3.so and b/php-sdl3/.libs/sdl3.so differ diff --git a/php-sdl3/config.nice b/php-sdl3/config.nice index 52d739d..5b88163 100755 --- a/php-sdl3/config.nice +++ b/php-sdl3/config.nice @@ -3,4 +3,5 @@ # Created by configure './configure' \ +'--enable-sdl3' \ "$@" diff --git a/php-sdl3/config.status b/php-sdl3/config.status index 801a253..883d91d 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='' +ac_cs_config='--enable-sdl3' 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' $ac_configure_extra_args --no-create --no-recursion + set X /bin/bash './configure' '--enable-sdl3' $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/modules/sdl3.so b/php-sdl3/modules/sdl3.so index 862d6ca..8f7238d 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 2763acb..66ac501 100644 --- a/php-sdl3/sdl3.c +++ b/php-sdl3/sdl3.c @@ -526,6 +526,153 @@ PHP_FUNCTION(sdl_set_texture_alpha_mod) { RETURN_TRUE; } +PHP_FUNCTION(sdl_create_box_shadow_texture) { + zval *ren_res; + SDL_Renderer *renderer; + zend_long width, height, blurRadius, alpha, r, g, b; + + if (zend_parse_parameters( + ZEND_NUM_ARGS(), + "rlllllll", + &ren_res, + &width, + &height, + &blurRadius, + &alpha, + &r, + &g, + &b) == FAILURE) { + RETURN_THROWS(); + } + + renderer = (SDL_Renderer *)zend_fetch_resource(Z_RES_P(ren_res), "SDL_Renderer", le_sdl_renderer); + if (!renderer) { + RETURN_FALSE; + } + + if (width <= 0 || height <= 0) { + RETURN_FALSE; + } + + int w = (int) width; + int h = (int) height; + int radius = (int) blurRadius; + if (radius < 0) { + radius = 0; + } + if (alpha < 0) { + alpha = 0; + } + if (alpha > 255) { + alpha = 255; + } + + int size = w * h; + Uint8 *alphaMap = emalloc(size * sizeof(Uint8)); + if (!alphaMap) { + RETURN_FALSE; + } + + memset(alphaMap, 0, size * sizeof(Uint8)); + + int margin = radius + 2; + for (int y = margin; y < h - margin; y++) { + int rowOffset = y * w; + for (int x = margin; x < w - margin; x++) { + alphaMap[rowOffset + x] = (Uint8) alpha; + } + } + + if (radius > 0) { + Uint8 *temp = emalloc(size * sizeof(Uint8)); + if (!temp) { + efree(alphaMap); + RETURN_FALSE; + } + + // Horizontal blur + for (int y = 0; y < h; y++) { + int rowOffset = y * w; + for (int x = 0; x < w; x++) { + int sum = 0; + int count = 0; + int xStart = x - radius; + int xEnd = x + radius; + if (xStart < 0) xStart = 0; + if (xEnd >= w) xEnd = w - 1; + for (int xi = xStart; xi <= xEnd; xi++) { + sum += alphaMap[rowOffset + xi]; + count++; + } + temp[rowOffset + x] = count > 0 ? (Uint8)(sum / count) : 0; + } + } + + // Vertical blur + for (int x = 0; x < w; x++) { + for (int y = 0; y < h; y++) { + int sum = 0; + int count = 0; + int yStart = y - radius; + int yEnd = y + radius; + if (yStart < 0) yStart = 0; + if (yEnd >= h) yEnd = h - 1; + for (int yi = yStart; yi <= yEnd; yi++) { + sum += temp[yi * w + x]; + count++; + } + alphaMap[y * w + x] = count > 0 ? (Uint8)(sum / count) : 0; + } + } + + efree(temp); + } + + SDL_Texture *texture = SDL_CreateTexture( + renderer, + SDL_PIXELFORMAT_ARGB8888, + SDL_TEXTUREACCESS_STATIC, + w, + h); + + if (!texture) { + efree(alphaMap); + php_error_docref(NULL, E_WARNING, "Failed to create shadow texture: %s", SDL_GetError()); + RETURN_FALSE; + } + + Uint32 *pixels = emalloc(size * sizeof(Uint32)); + if (!pixels) { + efree(alphaMap); + SDL_DestroyTexture(texture); + RETURN_FALSE; + } + + Uint8 red = (Uint8) r; + Uint8 green = (Uint8) g; + Uint8 blue = (Uint8) b; + + for (int i = 0; i < size; i++) { + Uint8 a = alphaMap[i]; + pixels[i] = ((Uint32)a << 24) | ((Uint32)red << 16) | ((Uint32)green << 8) | (Uint32)blue; + } + + if (SDL_UpdateTexture(texture, NULL, pixels, w * 4) < 0) { + efree(alphaMap); + efree(pixels); + SDL_DestroyTexture(texture); + php_error_docref(NULL, E_WARNING, "Failed to update shadow texture: %s", SDL_GetError()); + RETURN_FALSE; + } + + efree(alphaMap); + efree(pixels); + + SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND); + + RETURN_RES(zend_register_resource(texture, le_sdl_texture)); +} + PHP_FUNCTION(sdl_get_render_target) { zval *ren_res; SDL_Renderer *renderer; @@ -994,6 +1141,17 @@ 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_sdl_create_box_shadow_texture, 0, 0, 8) + ZEND_ARG_INFO(0, renderer) + ZEND_ARG_INFO(0, width) + ZEND_ARG_INFO(0, height) + ZEND_ARG_INFO(0, blurRadius) + ZEND_ARG_INFO(0, alpha) + ZEND_ARG_INFO(0, r) + ZEND_ARG_INFO(0, g) + ZEND_ARG_INFO(0, b) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_get_render_target, 0, 0, 1) ZEND_ARG_INFO(0, renderer) ZEND_END_ARG_INFO() @@ -1101,6 +1259,7 @@ 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) + PHP_FE(sdl_create_box_shadow_texture, arginfo_sdl_create_box_shadow_texture) PHP_FE(sdl_get_render_target, arginfo_sdl_get_render_target) PHP_FE(sdl_set_render_target, arginfo_sdl_set_render_target) PHP_FE(sdl_rounded_box, arginfo_sdl_rounded_box) diff --git a/src/Ui/Component.php b/src/Ui/Component.php index 8a9fdeb..fbfca5b 100644 --- a/src/Ui/Component.php +++ b/src/Ui/Component.php @@ -931,6 +931,22 @@ abstract class Component return null; } + // If the optimized C helper is available, use it. + if (function_exists('sdl_create_box_shadow_texture')) { + return sdl_create_box_shadow_texture( + $renderer, + $width, + $height, + $blurRadius, + $alpha, + $r, + $g, + $b, + ); + } + + // Fallback: existing PHP implementation + // Create alpha map (single channel) - start with transparent $alphaMap = array_fill(0, $width * $height, 0);