Compare commits

...

10 Commits

Author SHA1 Message Date
2d8d343011 backup 2025-12-19 17:04:05 +01:00
81d0f5a429 Backup 2025-12-08 10:19:54 +01:00
b991570285 Backup 2025-12-01 11:23:20 +01:00
d4a926ddad Backup 2025-12-01 11:06:53 +01:00
3fa5276aba Backup 2025-11-29 22:28:26 +01:00
bf986acb49 Backup 2025-11-29 22:10:58 +01:00
e617930ca4 Backup 2025-11-27 13:22:59 +01:00
cb148dfb7c Backup 2025-11-20 13:53:13 +01:00
c38bffd4f9 Backup 2025-11-19 12:18:45 +01:00
c567194b1c Backup 2025-11-18 09:01:16 +01:00
45 changed files with 4146 additions and 456 deletions

22
LICENSE Normal file
View File

@ -0,0 +1,22 @@
MIT License
Copyright (c) Thomas Peterson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -9,6 +9,7 @@ use PHPNative\Ui\Widget\Label;
use PHPNative\Ui\Widget\StatusBar; use PHPNative\Ui\Widget\StatusBar;
use PHPNative\Ui\Widget\TabContainer; use PHPNative\Ui\Widget\TabContainer;
use PHPNative\Ui\Window; use PHPNative\Ui\Window;
use ServerManager\UI\KanbanTab;
use ServerManager\UI\MenuBarBuilder; use ServerManager\UI\MenuBarBuilder;
use ServerManager\UI\ServerListTab; use ServerManager\UI\ServerListTab;
use ServerManager\UI\SettingsModal; use ServerManager\UI\SettingsModal;
@ -35,8 +36,11 @@ class App
// Status label (referenced by tabs) // Status label (referenced by tabs)
$statusLabel = new Label( $statusLabel = new Label(
text: 'Fenster: ' . $this->window->getViewport()->windowWidth . 'x' . $this->window->getViewport()->windowHeight, text: 'Fenster: ' .
style: 'basis-4/8 text-black' $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) // Settings variables (simple variables work better with async than object properties)
@ -49,7 +53,13 @@ class App
$mainContainer->addComponent($menuBar); $mainContainer->addComponent($menuBar);
// Create settings modal with the real menu bar // 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()); $mainContainer->addComponent($settingsModal->getModal());
// Build menu bar menus after modal is created // Build menu bar menus after modal is created
@ -59,17 +69,28 @@ class App
$tabContainer = new TabContainer('flex-1'); $tabContainer = new TabContainer('flex-1');
// Create tabs // Create tabs
$kanbanTab = new KanbanTab($this->settings);
$serverListTab = new ServerListTab( $serverListTab = new ServerListTab(
$currentApiKey, $currentApiKey,
$currentPrivateKeyPath, $currentPrivateKeyPath,
$tabContainer, $tabContainer,
$statusLabel, $statusLabel,
$this->settings, $this->settings,
$kanbanTab,
);
$kanbanTab->setServerListTab($serverListTab);
$sftpManagerTab = new SftpManagerTab(
$currentApiKey,
$currentPrivateKeyPath,
$currentRemoteStartDir,
$serverListTab,
$tabContainer,
$statusLabel,
); );
$sftpManagerTab = new SftpManagerTab($currentApiKey, $currentPrivateKeyPath, $currentRemoteStartDir, $serverListTab, $tabContainer, $statusLabel);
// Add tabs // Add tabs
$tabContainer->addTab('Server', $serverListTab->getContainer()); $tabContainer->addTab('Server', $serverListTab->getContainer());
$tabContainer->addTab('Kanban', $kanbanTab->getContainer());
$tabContainer->addTab('SFTP Manager', $sftpManagerTab->getContainer()); $tabContainer->addTab('SFTP Manager', $sftpManagerTab->getContainer());
$mainContainer->addComponent($tabContainer); $mainContainer->addComponent($tabContainer);
@ -86,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('v1.0', 'basis-1/8 text-center text-black border-l border-gray-300'));
$statusBar->addSegment(new Label( $statusBar->addSegment(new Label(
'PHPNative Framework', '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); $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 // Set window content and run
$this->window->setRoot($mainContainer); $this->window->setRoot($mainContainer);
$this->app->addWindow($this->window); $this->app->addWindow($this->window);

View File

@ -32,6 +32,10 @@ class HetznerService
'needs_reboot' => 'unbekannt', 'needs_reboot' => 'unbekannt',
'updates_available' => 'unbekannt', 'updates_available' => 'unbekannt',
'os_version' => 'unbekannt', 'os_version' => 'unbekannt',
'release' => 'unbekannt',
'root' => 'unbekannt',
'data' => 'unbekannt',
'last_backup' => 'unbekannt',
]; ];
} }
@ -63,6 +67,10 @@ class HetznerService
'needs_reboot' => 'nein', 'needs_reboot' => 'nein',
'updates_available' => 'nein', 'updates_available' => 'nein',
'os_version' => 'Ubuntu 22.04 LTS', 'os_version' => 'Ubuntu 22.04 LTS',
'release' => 'v1.0.0',
'root' => '35%',
'data' => '42%',
'last_backup' => 'unbekannt',
]; ];
} }
return $testData; return $testData;

View File

@ -0,0 +1,401 @@
<?php
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\Modal;
use PHPNative\Ui\Widget\TextArea;
use PHPNative\Ui\Widget\TextInput;
class KanbanTab
{
private Container $tab;
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;
private null|ServerListTab $serverListTab = null;
public function __construct(Settings $settings)
{
$this->settings = $settings;
$this->ensureDefaultBoards();
$this->tab = new Container('flex flex-col p-4 gap-4 bg-gray-50');
$headerRow = new Container('flex flex-row items-center gap-2');
$headerRow->addComponent(new Label('Kanban Tasks', 'text-xl font-bold text-black flex-1'));
$this->newBoardInput = new TextInput(
'Neues Board...',
'w-60 border border-gray-300 rounded px-3 py-2 bg-white text-black text-sm',
);
$addBoardButton = new Button(
'Board hinzufügen',
'px-3 py-2 bg-blue-600 rounded hover:bg-blue-700',
null,
'text-white text-sm',
);
$headerRow->addComponent($this->newBoardInput);
$headerRow->addComponent($addBoardButton);
$this->tab->addComponent($headerRow);
$this->boardsContainer = new Container(
'flex flex-row gap-4 flex-1 overflow-auto bg-gray-100 rounded border border-gray-300 p-3',
);
$this->tab->addComponent($this->boardsContainer);
$kanbanTab = $this;
$addBoardButton->setOnClick(function () use ($kanbanTab) {
$name = trim($kanbanTab->newBoardInput->getValue());
if ($name === '') {
return;
}
$boards = $kanbanTab->settings->get('kanban.boards', []);
if (!is_array($boards)) {
$boards = [];
}
if (!in_array($name, $boards, true)) {
$boards[] = $name;
$kanbanTab->settings->set('kanban.boards', $boards);
$kanbanTab->settings->save();
}
$kanbanTab->newBoardInput->setValue('');
$kanbanTab->renderBoards();
});
// Edit modal for tasks
$this->createEditModal();
$this->renderBoards();
}
public function setServerListTab(ServerListTab $serverListTab): void
{
$this->serverListTab = $serverListTab;
}
public function getContainer(): Container
{
return $this->tab;
}
public function refresh(): void
{
$this->renderBoards();
}
private function ensureDefaultBoards(): void
{
$boards = $this->settings->get('kanban.boards', null);
if (!is_array($boards) || empty($boards)) {
$boards = ['neu', 'in arbeit', 'fertig'];
$this->settings->set('kanban.boards', $boards);
$this->settings->save();
}
}
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();
if ($this->serverListTab !== null) {
$this->serverListTab->refreshCurrentServerTasks();
}
}
private function renderBoards(): void
{
$this->boardsContainer->clearChildren();
$boards = $this->settings->get('kanban.boards', []);
if (!is_array($boards)) {
$boards = [];
}
$tasks = $this->settings->get('kanban.tasks', []);
if (!is_array($tasks)) {
$tasks = [];
}
// Map server IDs to names if the server list tab is available
$serverNames = [];
if ($this->serverListTab !== null) {
foreach ($this->serverListTab->currentServerData as $row) {
$id = $row['id'] ?? null;
$name = $row['name'] ?? null;
if ($id !== null && $name !== null) {
$serverNames[(int) $id] = (string) $name;
}
}
}
foreach ($boards as $boardName) {
$column = new Container('flex flex-col bg-white rounded shadow-md w-128 max-h-full');
$columnHeader = new Container('px-3 py-2 border-b border-gray-300 bg-gray-100');
$columnHeader->addComponent(new Label($boardName, 'text-sm font-semibold text-gray-800'));
$column->addComponent($columnHeader);
$columnBody = new Container('flex flex-col gap-2 p-2 overflow-auto');
$boardTasks = array_values(array_filter(
$tasks,
static fn($task) => ($task['board'] ?? 'neu') === $boardName,
));
if (empty($boardTasks)) {
$columnBody->addComponent(new Label('Keine Tasks', 'text-xs text-gray-400 italic'));
} else {
foreach ($boardTasks as $task) {
$title = (string) ($task['title'] ?? '');
$serverId = $task['server_id'] ?? null;
$serverName = null;
if ($serverId !== null && isset($serverNames[(int) $serverId])) {
$serverName = $serverNames[(int) $serverId];
}
if ($serverName !== null) {
$serverLabel = $serverName . ' (#' . $serverId . ')';
} else {
$serverLabel = $serverId !== null ? ('Server #' . $serverId) : 'Kein Server';
}
$taskId = $task['id'] ?? null;
$card = new Container(
'flex flex-col gap-1 px-3 py-2 bg-lime-100 border border-red-500 rounded-sm shadow-lg',
);
// 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();
if ($kanbanTab->serverListTab !== null) {
$kanbanTab->serverListTab->refreshCurrentServerTasks();
}
});
$headerRow->addComponent($editButton);
$headerRow->addComponent($deleteButton);
$card->addComponent($headerRow);
// Server label
$card->addComponent(new Label($serverLabel, 'text-sm text-gray-500'));
$columnBody->addComponent($card);
}
}
$column->addComponent($columnBody);
$this->boardsContainer->addComponent($column);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -18,6 +18,7 @@ class SettingsModal
private TextInput $apiKeyInput; private TextInput $apiKeyInput;
private TextInput $privateKeyPathInput; private TextInput $privateKeyPathInput;
private TextInput $remoteStartDirInput; private TextInput $remoteStartDirInput;
private TextInput $doneBoardInput;
public function __construct(Settings $settings, MenuBar $menuBar, string &$apiKey, string &$privateKeyPath, string &$remoteStartDir) public function __construct(Settings $settings, MenuBar $menuBar, string &$apiKey, string &$privateKeyPath, string &$remoteStartDir)
{ {
@ -31,11 +32,17 @@ class SettingsModal
'Remote Start Directory', 'Remote Start Directory',
'w-full border border-gray-300 rounded px-3 py-2 bg-white text-black' 'w-full border border-gray-300 rounded px-3 py-2 bg-white text-black'
); );
$this->doneBoardInput = new TextInput(
'Fertig-Board (z.B. fertig)',
'w-full border border-gray-300 rounded px-3 py-2 bg-white text-black'
);
// Set initial values // Set initial values
$this->apiKeyInput->setValue($apiKey); $this->apiKeyInput->setValue($apiKey);
$this->privateKeyPathInput->setValue($privateKeyPath); $this->privateKeyPathInput->setValue($privateKeyPath);
$this->remoteStartDirInput->setValue($remoteStartDir); $this->remoteStartDirInput->setValue($remoteStartDir);
$doneBoard = (string) $settings->get('kanban.done_board', 'fertig');
$this->doneBoardInput->setValue($doneBoard);
// Create modal dialog // Create modal dialog
$modalDialog = new Container('bg-white rounded-lg p-6 flex flex-col w-96 gap-3'); $modalDialog = new Container('bg-white rounded-lg p-6 flex flex-col w-96 gap-3');
@ -63,6 +70,15 @@ class SettingsModal
$remoteStartDirFieldContainer->addComponent($this->remoteStartDirInput); $remoteStartDirFieldContainer->addComponent($this->remoteStartDirInput);
$modalDialog->addComponent($remoteStartDirFieldContainer); $modalDialog->addComponent($remoteStartDirFieldContainer);
// Done-board field
$doneBoardFieldContainer = new Container('flex flex-col gap-1');
$doneBoardFieldContainer->addComponent(new Label(
'Board-Name für erledigte Tasks (z.B. "fertig")',
'text-sm text-gray-600'
));
$doneBoardFieldContainer->addComponent($this->doneBoardInput);
$modalDialog->addComponent($doneBoardFieldContainer);
// Buttons // Buttons
$buttonRow = new Container('flex flex-row gap-2 justify-end'); $buttonRow = new Container('flex flex-row gap-2 justify-end');
$cancelButton = new Button('Abbrechen', 'px-4 py-2 bg-gray-200 text-black rounded hover:bg-gray-300'); $cancelButton = new Button('Abbrechen', 'px-4 py-2 bg-gray-200 text-black rounded hover:bg-gray-300');
@ -80,20 +96,37 @@ class SettingsModal
$apiKeyInputRef = $this->apiKeyInput; $apiKeyInputRef = $this->apiKeyInput;
$privateKeyPathInputRef = $this->privateKeyPathInput; $privateKeyPathInputRef = $this->privateKeyPathInput;
$remoteStartDirInputRef = $this->remoteStartDirInput; $remoteStartDirInputRef = $this->remoteStartDirInput;
$doneBoardInputRef = $this->doneBoardInput;
$cancelButton->setOnClick(function () use ($menuBar, $settingsModal) { $cancelButton->setOnClick(function () use ($menuBar, $settingsModal) {
$menuBar->closeAllMenus(); $menuBar->closeAllMenus();
$settingsModal->hide(); $settingsModal->hide();
}); });
$saveButton->setOnClick(function () use ($menuBar, $settingsModal, &$apiKey, &$privateKeyPath, &$remoteStartDir, $settings, $apiKeyInputRef, $privateKeyPathInputRef, $remoteStartDirInputRef) { $saveButton->setOnClick(function () use (
$menuBar,
$settingsModal,
&$apiKey,
&$privateKeyPath,
&$remoteStartDir,
$settings,
$apiKeyInputRef,
$privateKeyPathInputRef,
$remoteStartDirInputRef,
$doneBoardInputRef
) {
$apiKey = trim($apiKeyInputRef->getValue()); $apiKey = trim($apiKeyInputRef->getValue());
$privateKeyPath = trim($privateKeyPathInputRef->getValue()); $privateKeyPath = trim($privateKeyPathInputRef->getValue());
$remoteStartDir = trim($remoteStartDirInputRef->getValue()); $remoteStartDir = trim($remoteStartDirInputRef->getValue());
$doneBoard = trim($doneBoardInputRef->getValue());
if ($doneBoard === '') {
$doneBoard = 'fertig';
}
$settings->set('api_key', $apiKey); $settings->set('api_key', $apiKey);
$settings->set('private_key_path', $privateKeyPath); $settings->set('private_key_path', $privateKeyPath);
$settings->set('remote_start_dir', $remoteStartDir); $settings->set('remote_start_dir', $remoteStartDir);
$settings->set('kanban.done_board', $doneBoard);
$settings->save(); $settings->save();
$menuBar->closeAllMenus(); $menuBar->closeAllMenus();

View File

@ -10,6 +10,7 @@ use PHPNative\Ui\Widget\Label;
use PHPNative\Ui\Widget\Modal; use PHPNative\Ui\Widget\Modal;
use PHPNative\Ui\Widget\TextArea; use PHPNative\Ui\Widget\TextArea;
use PHPNative\Ui\Widget\TextInput; use PHPNative\Ui\Widget\TextInput;
use PHPNative\Ui\Widget\ProgressBar;
class SftpManagerTab class SftpManagerTab
{ {
@ -25,12 +26,29 @@ class SftpManagerTab
private TextInput $filenameInput; private TextInput $filenameInput;
private Modal $renameModal; private Modal $renameModal;
private TextInput $renameInput; private TextInput $renameInput;
private Label $renamePathLabel;
private string $currentRenameFilePath = ''; private string $currentRenameFilePath = '';
private string $currentRenameMode = 'remote';
private Modal $deleteConfirmModal; private Modal $deleteConfirmModal;
private string $currentDeleteFilePath = ''; private string $currentDeleteFilePath = '';
private Label $deleteConfirmLabel; private Label $deleteConfirmLabel;
private null|array $currentLocalSelection = null; private null|array $currentLocalSelection = null;
private null|array $currentRemoteSelection = null; private null|array $currentRemoteSelection = null;
private ?string $lastRemoteClickPath = null;
private float $lastRemoteClickTime = 0.0;
private ProgressBar $transferProgressBar;
private Label $transferInfoLabel;
private Label $transferBytesLabel;
private array $pendingUploadQueue = [];
private int $totalUploadFiles = 0;
private int $completedUploadFiles = 0;
private int $totalUploadBytes = 0;
private int $completedUploadBytes = 0;
private array $pendingDownloadQueue = [];
private int $totalDownloadFiles = 0;
private int $completedDownloadFiles = 0;
private int $totalDownloadBytes = 0;
private int $completedDownloadBytes = 0;
public function __construct( public function __construct(
string &$apiKey, string &$apiKey,
@ -45,6 +63,7 @@ class SftpManagerTab
$currentRemoteStartDir = &$remoteStartDir; $currentRemoteStartDir = &$remoteStartDir;
// Left side: Local file browser // Left side: Local file browser
// Lokaler Browser: Spalte, Scrollen übernimmt der FileBrowser selbst
$localBrowserContainer = new Container('flex flex-col flex-1 gap-2'); $localBrowserContainer = new Container('flex flex-col flex-1 gap-2');
$localBrowserContainer->addComponent(new Label('Lokal', 'text-lg font-bold text-black mb-2')); $localBrowserContainer->addComponent(new Label('Lokal', 'text-lg font-bold text-black mb-2'));
$this->localFileBrowser = new FileBrowser( $this->localFileBrowser = new FileBrowser(
@ -64,6 +83,7 @@ class SftpManagerTab
}); });
// Right side: Remote file browser // Right side: Remote file browser
// Remote-Browser: Spalte, Scrollen übernimmt der FileBrowser selbst
$remoteBrowserContainer = new Container('flex flex-col flex-1 gap-2'); $remoteBrowserContainer = new Container('flex flex-col flex-1 gap-2');
// Header with title and new file button // Header with title and new file button
@ -84,19 +104,28 @@ class SftpManagerTab
$remoteBrowserContainer->addComponent($this->connectionStatusLabel); $remoteBrowserContainer->addComponent($this->connectionStatusLabel);
// Middle: Transfer buttons (Upload/Download) // Middle: Transfer buttons (Upload/Download)
$transferContainer = new Container('flex flex-col justify-center items-center gap-2'); $transferContainer = new Container('flex flex-col justify-center items-center gap-2 w-[180]');
$uploadButton = new Button( $uploadButton = new Button(
'Hochladen →', 'Hochladen →',
'px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700', 'w-full px-3 py-2 bg-green-600 text-white rounded hover:bg-green-700',
); );
$downloadButton = new Button( $downloadButton = new Button(
'← Herunterladen', '← Herunterladen',
'px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700', 'w-full px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700',
); );
$transferContainer->addComponent($uploadButton); $transferContainer->addComponent($uploadButton);
$transferContainer->addComponent($downloadButton); $transferContainer->addComponent($downloadButton);
// Transfer-Info + ProgressBar
$this->transferInfoLabel = new Label('Kein Transfer aktiv', 'text-xs text-gray-600 mt-2');
$this->transferBytesLabel = new Label('', 'text-xs text-gray-600');
$this->transferProgressBar = new ProgressBar('mt-1');
$this->transferProgressBar->setValue(0.0);
$transferContainer->addComponent($this->transferInfoLabel);
$transferContainer->addComponent($this->transferBytesLabel);
$transferContainer->addComponent($this->transferProgressBar);
$this->tab->addComponent($localBrowserContainer); $this->tab->addComponent($localBrowserContainer);
$this->tab->addComponent($transferContainer); $this->tab->addComponent($transferContainer);
$this->tab->addComponent($remoteBrowserContainer); $this->tab->addComponent($remoteBrowserContainer);
@ -119,6 +148,11 @@ class SftpManagerTab
// Create delete confirmation modal // Create delete confirmation modal
$this->createDeleteConfirmModal($currentPrivateKeyPath, $serverListTab, $statusLabel); $this->createDeleteConfirmModal($currentPrivateKeyPath, $serverListTab, $statusLabel);
// Setup local rename handler
$this->localFileBrowser->setOnRenameFile(function ($path, $row) use ($sftpTab, $statusLabel) {
$sftpTab->handleLocalRename($path, $row, $statusLabel);
});
// Setup transfer button handlers // Setup transfer button handlers
$uploadButton->setOnClick(function () use ($sftpTab, &$currentPrivateKeyPath, $serverListTab, $statusLabel) { $uploadButton->setOnClick(function () use ($sftpTab, &$currentPrivateKeyPath, $serverListTab, $statusLabel) {
$sftpTab->handleUpload($currentPrivateKeyPath, $serverListTab, $statusLabel); $sftpTab->handleUpload($currentPrivateKeyPath, $serverListTab, $statusLabel);
@ -191,6 +225,25 @@ class SftpManagerTab
return; return;
} }
// Require double click for navigation:
// first click selektiert nur, zweiter Klick (innerhalb kurzer Zeit)
// öffnet das Verzeichnis.
$now = microtime(true);
$doubleClickThreshold = 0.4; // Sekunden
if (
$sftpTab->lastRemoteClickPath !== $path ||
($now - $sftpTab->lastRemoteClickTime) > $doubleClickThreshold
) {
$sftpTab->lastRemoteClickPath = $path;
$sftpTab->lastRemoteClickTime = $now;
return;
}
// Zweiter Klick innerhalb des Zeitfensters -> als Doppelklick werten
$sftpTab->lastRemoteClickPath = null;
$sftpTab->lastRemoteClickTime = 0.0;
$loadButton = new Button('Load', ''); $loadButton = new Button('Load', '');
$loadButton->setOnClickAsync( $loadButton->setOnClickAsync(
function () use ($path, &$currentPrivateKeyPath, &$selectedServerRef) { function () use ($path, &$currentPrivateKeyPath, &$selectedServerRef) {
@ -343,7 +396,9 @@ class SftpManagerTab
if (isset($result['success']) && $result['success']) { if (isset($result['success']) && $result['success']) {
// Switch to SFTP tab on successful connection // Switch to SFTP tab on successful connection
$tabContainer->setActiveTab(1); // Tab-Reihenfolge in App.php:
// 0 = Server, 1 = Kanban, 2 = SFTP Manager
$tabContainer->setActiveTab(2);
$sftpTab->connectionStatusLabel->setText( $sftpTab->connectionStatusLabel->setText(
'Verbunden mit: ' . $result['server']['name'] . ' (' . $result['server']['ipv4'] . ')', 'Verbunden mit: ' . $result['server']['name'] . ' (' . $result['server']['ipv4'] . ')',
@ -751,16 +806,6 @@ class SftpManagerTab
return; return;
} }
if (($localRow['isDir'] ?? false) === true) {
$statusLabel->setText('Ordner-Upload wird noch nicht unterstützt');
return;
}
if (!is_file($localPath) || !is_readable($localPath)) {
$statusLabel->setText('Lokale Datei ist nicht lesbar');
return;
}
if ($serverListTab->selectedServer === null) { if ($serverListTab->selectedServer === null) {
$statusLabel->setText('Kein Server ausgewählt'); $statusLabel->setText('Kein Server ausgewählt');
return; return;
@ -776,13 +821,184 @@ class SftpManagerTab
$remoteDir = '/'; $remoteDir = '/';
} }
$remotePath = rtrim($remoteDir, '/') . '/' . basename($localPath); // Upload-Queue inkl. Byte-Größen vorbereiten
$this->pendingUploadQueue = $this->buildUploadQueue($localPath, $localRow, $remoteDir);
$this->totalUploadFiles = count($this->pendingUploadQueue);
$this->completedUploadFiles = 0;
$this->totalUploadBytes = 0;
$this->completedUploadBytes = 0;
foreach ($this->pendingUploadQueue as $item) {
$this->totalUploadBytes += (int) ($item['size'] ?? 0);
}
$this->transferProgressBar->setValue(0.0);
$this->transferInfoLabel->setText(
sprintf(
'Upload: %d Dateien',
$this->totalUploadFiles,
),
);
$this->transferBytesLabel->setText(
sprintf(
'0.00 / %.2f MB',
$this->totalUploadBytes > 0 ? ($this->totalUploadBytes / (1024 * 1024)) : 0,
),
);
if ($this->totalUploadFiles <= 0) {
$this->transferProgressBar->setValue(1.0);
$this->transferInfoLabel->setText('Keine Dateien zum Hochladen');
return;
}
// Referenz auf ausgewählten Server für spätere Reloads
$this->startNextUploadTask($currentPrivateKeyPath, $serverListTab, $statusLabel);
}
/**
* Lokale Upload-Statistik (Dateien/Ordner/Bytes) rekursiv berechnen.
*/
private function calculateLocalUploadStats(string $path, array $row): array
{
$files = 0;
$dirs = 0;
$bytes = 0;
$isDir = (bool) ($row['isDir'] ?? false);
if (!$isDir) {
if (is_file($path) && is_readable($path)) {
$files = 1;
$bytes = (int) @filesize($path);
}
return ['files' => $files, 'dirs' => $dirs, 'bytes' => $bytes];
}
// Ordner: rekursiv zählen
$dirs++;
$iterator = @scandir($path);
if ($iterator === false) {
return ['files' => $files, 'dirs' => $dirs, 'bytes' => $bytes];
}
foreach ($iterator as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}
$childPath = $path . DIRECTORY_SEPARATOR . $entry;
if (is_dir($childPath)) {
$childStats = $this->calculateLocalUploadStats($childPath, ['isDir' => true]);
$files += $childStats['files'];
$dirs += $childStats['dirs'];
$bytes += $childStats['bytes'];
} elseif (is_file($childPath) && is_readable($childPath)) {
$files++;
$bytes += (int) @filesize($childPath);
}
}
return ['files' => $files, 'dirs' => $dirs, 'bytes' => $bytes];
}
/**
* Queue für Datei-Uploads aufbauen (Datei- oder Ordner-Auswahl).
*
* @return array<int,array{local:string,remote:string}>
*/
private function buildUploadQueue(string $localPath, array $row, string $remoteDir): array
{
$queue = [];
$remoteBase = rtrim($remoteDir, '/');
if ($remoteBase === '') {
$remoteBase = '/';
}
$isDir = (bool) ($row['isDir'] ?? false);
if (!$isDir) {
$queue[] = [
'local' => $localPath,
'remote' => $remoteBase . '/' . basename($localPath),
'size' => is_file($localPath) ? (int) @filesize($localPath) : 0,
];
return $queue;
}
// Wurzelordner auf Remote
$rootRemote = $remoteBase . '/' . basename($localPath);
$this->collectUploadFilesRecursive($localPath, $rootRemote, $queue);
return $queue;
}
/**
* Rekursive Sammlung aller Dateien eines Ordners.
*
* @param array<int,array{local:string,remote:string}> $queue
*/
private function collectUploadFilesRecursive(string $localDir, string $remoteDir, array &$queue): void
{
$entries = @scandir($localDir);
if ($entries === false) {
return;
}
foreach ($entries as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}
$localChild = $localDir . DIRECTORY_SEPARATOR . $entry;
$remoteChild = rtrim($remoteDir, '/') . '/' . $entry;
if (is_dir($localChild)) {
$this->collectUploadFilesRecursive($localChild, $remoteChild, $queue);
} elseif (is_file($localChild) && is_readable($localChild)) {
$queue[] = [
'local' => $localChild,
'remote' => $remoteChild,
'size' => is_file($localChild) ? (int) @filesize($localChild) : 0,
];
}
}
}
/**
* Nächste Datei aus der Upload-Queue hochladen und ProgressBar aktualisieren.
*/
private function startNextUploadTask(
string &$currentPrivateKeyPath,
ServerListTab $serverListTab,
Label $statusLabel,
): void {
if ($this->totalUploadFiles <= 0) {
$this->transferProgressBar->setValue(1.0);
$this->transferInfoLabel->setText('Keine Dateien zum Hochladen');
return;
}
if (empty($this->pendingUploadQueue)) {
// Alle Dateien hochgeladen
$this->transferProgressBar->setValue(1.0);
$this->transferInfoLabel->setText('Upload abgeschlossen');
$selectedServerRef = &$serverListTab->selectedServer;
$this->reloadCurrentDirectory($currentPrivateKeyPath, $selectedServerRef, $statusLabel);
return;
}
$item = array_shift($this->pendingUploadQueue);
$localPath = $item['local'];
$remotePath = $item['remote'];
$fileSize = (int) ($item['size'] ?? 0);
// Use reference to selected server for async operation
$selectedServerRef = &$serverListTab->selectedServer; $selectedServerRef = &$serverListTab->selectedServer;
$sftpTab = $this; $sftpTab = $this;
$uploadAsyncButton = new Button('Upload', ''); $uploadAsyncButton = new Button('UploadChunk', '');
$uploadAsyncButton->setOnClickAsync( $uploadAsyncButton->setOnClickAsync(
function () use (&$currentPrivateKeyPath, &$selectedServerRef, $localPath, $remotePath) { function () use (&$currentPrivateKeyPath, &$selectedServerRef, $localPath, $remotePath) {
if ($selectedServerRef === null || empty($currentPrivateKeyPath)) { if ($selectedServerRef === null || empty($currentPrivateKeyPath)) {
@ -799,7 +1015,23 @@ class SftpManagerTab
return ['error' => 'SFTP Login failed']; return ['error' => 'SFTP Login failed'];
} }
$result = $sftp->put($remotePath, $localPath, \phpseclib3\Net\SFTP::SOURCE_LOCAL_FILE); // Zielverzeichnis sicherstellen
$remoteDir = dirname($remotePath);
if (!$sftp->is_dir($remoteDir)) {
if (!$sftp->mkdir($remoteDir, -1, true)) {
return ['error' => 'Kann Remote-Verzeichnis nicht anlegen'];
}
}
if (!is_file($localPath) || !is_readable($localPath)) {
return ['error' => 'Lokale Datei ist nicht lesbar'];
}
$result = $sftp->put(
$remotePath,
$localPath,
\phpseclib3\Net\SFTP::SOURCE_LOCAL_FILE,
);
if ($result === false) { if ($result === false) {
return ['error' => 'Upload fehlgeschlagen']; return ['error' => 'Upload fehlgeschlagen'];
} }
@ -809,19 +1041,53 @@ class SftpManagerTab
return ['error' => $e->getMessage()]; return ['error' => $e->getMessage()];
} }
}, },
function ($result) use ($sftpTab, $statusLabel, &$currentPrivateKeyPath, &$selectedServerRef) { function ($result) use ($sftpTab, &$currentPrivateKeyPath, $serverListTab, $statusLabel, $fileSize) {
if (isset($result['error'])) { if (isset($result['error'])) {
$sftpTab->transferInfoLabel->setText('Upload fehlgeschlagen: ' . $result['error']);
$statusLabel->setText('Fehler beim Hochladen: ' . $result['error']); $statusLabel->setText('Fehler beim Hochladen: ' . $result['error']);
return; return;
} }
if (isset($result['success'])) { if (isset($result['success'])) {
$statusLabel->setText('Datei erfolgreich hochgeladen'); $sftpTab->completedUploadFiles++;
$sftpTab->reloadCurrentDirectory($currentPrivateKeyPath, $selectedServerRef, $statusLabel); $sftpTab->completedUploadBytes += $fileSize;
$progress = 0.0;
if ($sftpTab->totalUploadBytes > 0) {
$progress = $sftpTab->completedUploadBytes / $sftpTab->totalUploadBytes;
} elseif ($sftpTab->totalUploadFiles > 0) {
$progress = $sftpTab->completedUploadFiles / $sftpTab->totalUploadFiles;
} else {
$progress = 1.0;
}
$sftpTab->transferProgressBar->setValue($progress);
$sftpTab->transferInfoLabel->setText(
sprintf(
'Upload: %d / %d Dateien',
$sftpTab->completedUploadFiles,
$sftpTab->totalUploadFiles,
),
);
$sftpTab->transferBytesLabel->setText(
sprintf(
'%.2f / %.2f MB',
$sftpTab->completedUploadBytes > 0
? ($sftpTab->completedUploadBytes / (1024 * 1024))
: 0,
$sftpTab->totalUploadBytes > 0
? ($sftpTab->totalUploadBytes / (1024 * 1024))
: 0,
),
);
// Nächste Datei starten
$sftpTab->startNextUploadTask($currentPrivateKeyPath, $serverListTab, $statusLabel);
} }
}, },
function ($error) use ($statusLabel) { function ($error) use ($sftpTab, $statusLabel) {
$errorMsg = is_string($error) ? $error : 'Unknown error'; $errorMsg = is_string($error) ? $error : 'Unknown error';
$sftpTab->transferInfoLabel->setText('Upload fehlgeschlagen: ' . $errorMsg);
$statusLabel->setText('Fehler beim Hochladen: ' . $errorMsg); $statusLabel->setText('Fehler beim Hochladen: ' . $errorMsg);
}, },
); );
@ -829,6 +1095,125 @@ class SftpManagerTab
$uploadAsyncButton->handleMouseClick(0, 0, 0); $uploadAsyncButton->handleMouseClick(0, 0, 0);
} }
/**
* Nächste Datei aus der Download-Queue holen und Fortschritt aktualisieren.
*/
private function startNextDownloadTask(
string &$currentPrivateKeyPath,
ServerListTab $serverListTab,
Label $statusLabel,
string $localRootDir,
): void {
if ($this->totalDownloadFiles <= 0) {
$this->transferProgressBar->setValue(1.0);
$this->transferInfoLabel->setText('Keine Dateien zum Herunterladen');
return;
}
if (empty($this->pendingDownloadQueue)) {
// Alle Dateien heruntergeladen
$this->transferProgressBar->setValue(1.0);
$this->transferInfoLabel->setText('Download abgeschlossen');
$this->localFileBrowser->loadDirectory($localRootDir);
return;
}
$item = array_shift($this->pendingDownloadQueue);
$remotePath = $item['remote'];
$localPath = $item['local'];
$fileSize = (int) ($item['size'] ?? 0);
$selectedServerRef = &$serverListTab->selectedServer;
$sftpTab = $this;
$downloadAsyncButton = new Button('DownloadChunk', '');
$downloadAsyncButton->setOnClickAsync(
function () use (&$currentPrivateKeyPath, &$selectedServerRef, $remotePath, $localPath) {
if ($selectedServerRef === null || empty($currentPrivateKeyPath)) {
return ['error' => 'Not connected'];
}
$selectedServer = $selectedServerRef;
try {
$sftp = new \phpseclib3\Net\SFTP($selectedServer['ipv4']);
$key = \phpseclib3\Crypt\PublicKeyLoader::load(file_get_contents($currentPrivateKeyPath));
if (!$sftp->login('root', $key)) {
return ['error' => 'SFTP Login failed'];
}
$localDir = dirname($localPath);
if (!is_dir($localDir)) {
if (!mkdir($localDir, 0777, true) && !is_dir($localDir)) {
return ['error' => 'Lokales Zielverzeichnis kann nicht erstellt werden'];
}
}
$result = $sftp->get($remotePath, $localPath);
if ($result === false) {
return ['error' => 'Download fehlgeschlagen'];
}
return ['success' => true];
} catch (\Exception $e) {
return ['error' => $e->getMessage()];
}
},
function ($result) use ($sftpTab, &$currentPrivateKeyPath, $serverListTab, $statusLabel, $localRootDir, $fileSize) {
if (isset($result['error'])) {
$sftpTab->transferInfoLabel->setText('Download fehlgeschlagen: ' . $result['error']);
$statusLabel->setText('Fehler beim Herunterladen: ' . $result['error']);
return;
}
if (isset($result['success'])) {
$sftpTab->completedDownloadFiles++;
$sftpTab->completedDownloadBytes += $fileSize;
$progress = 0.0;
if ($sftpTab->totalDownloadBytes > 0) {
$progress = $sftpTab->completedDownloadBytes / $sftpTab->totalDownloadBytes;
} elseif ($sftpTab->totalDownloadFiles > 0) {
$progress = $sftpTab->completedDownloadFiles / $sftpTab->totalDownloadFiles;
} else {
$progress = 1.0;
}
$sftpTab->transferProgressBar->setValue($progress);
$sftpTab->transferInfoLabel->setText(
sprintf(
'Download: %d / %d Dateien',
$sftpTab->completedDownloadFiles,
$sftpTab->totalDownloadFiles,
),
);
$sftpTab->transferBytesLabel->setText(
sprintf(
'%.2f / %.2f MB',
$sftpTab->completedDownloadBytes > 0
? ($sftpTab->completedDownloadBytes / (1024 * 1024))
: 0,
$sftpTab->totalDownloadBytes > 0
? ($sftpTab->totalDownloadBytes / (1024 * 1024))
: 0,
),
);
// Nächste Datei herunterladen
$sftpTab->startNextDownloadTask($currentPrivateKeyPath, $serverListTab, $statusLabel, $localRootDir);
}
},
function ($error) use ($sftpTab, $statusLabel) {
$errorMsg = is_string($error) ? $error : 'Unknown error';
$sftpTab->transferInfoLabel->setText('Download fehlgeschlagen: ' . $errorMsg);
$statusLabel->setText('Fehler beim Herunterladen: ' . $errorMsg);
},
);
$downloadAsyncButton->handleMouseClick(0, 0, 0);
}
private function handleDownload( private function handleDownload(
string &$currentPrivateKeyPath, string &$currentPrivateKeyPath,
ServerListTab $serverListTab, ServerListTab $serverListTab,
@ -847,11 +1232,6 @@ class SftpManagerTab
return; return;
} }
if (($remoteRow['isDir'] ?? false) === true) {
$statusLabel->setText('Ordner-Download wird noch nicht unterstützt');
return;
}
if ($serverListTab->selectedServer === null) { if ($serverListTab->selectedServer === null) {
$statusLabel->setText('Kein Server ausgewählt'); $statusLabel->setText('Kein Server ausgewählt');
return; return;
@ -872,15 +1252,16 @@ class SftpManagerTab
return; return;
} }
$localPath = rtrim($localDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . basename($remotePath); $this->transferProgressBar->setValue(0.0);
$this->transferInfoLabel->setText('Download wird vorbereitet …');
// Use reference to selected server for async operation // Async-Scan der Remote-Struktur, um eine Download-Queue aufzubauen
$selectedServerRef = &$serverListTab->selectedServer; $selectedServerRef = &$serverListTab->selectedServer;
$sftpTab = $this; $sftpTab = $this;
$downloadAsyncButton = new Button('Download', ''); $scanButton = new Button('ScanDownload', '');
$downloadAsyncButton->setOnClickAsync( $scanButton->setOnClickAsync(
function () use (&$currentPrivateKeyPath, &$selectedServerRef, $remotePath, $localPath) { function () use (&$currentPrivateKeyPath, &$selectedServerRef, $remotePath, $remoteRow, $localDir) {
if ($selectedServerRef === null || empty($currentPrivateKeyPath)) { if ($selectedServerRef === null || empty($currentPrivateKeyPath)) {
return ['error' => 'Not connected']; return ['error' => 'Not connected'];
} }
@ -895,34 +1276,132 @@ class SftpManagerTab
return ['error' => 'SFTP Login failed']; return ['error' => 'SFTP Login failed'];
} }
$result = $sftp->get($remotePath, $localPath); $queue = [];
if ($result === false) { $files = 0;
return ['error' => 'Download fehlgeschlagen']; $dirs = 0;
$totalBytes = 0;
$isDir = (bool) ($remoteRow['isDir'] ?? false);
if (!$isDir) {
$stat = $sftp->stat($remotePath);
$size = (int) ($stat['size'] ?? 0);
$queue[] = [
'remote' => $remotePath,
'local' => rtrim($localDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . basename($remotePath),
'size' => $size,
];
$files = 1;
$totalBytes = $size;
} else {
$rootLocal = rtrim($localDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . basename($remotePath);
$collect = null;
$collect = function (string $srcDir, string $dstDir) use (&$collect, $sftp, &$queue, &$files, &$dirs, &$totalBytes) {
$dirs++;
$entries = $sftp->nlist($srcDir);
if ($entries === false) {
return;
} }
return ['success' => true]; foreach ($entries as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}
$remoteChild = rtrim($srcDir, '/') . '/' . $entry;
$localChild = rtrim($dstDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $entry;
$stat = $sftp->stat($remoteChild);
$isDirChild = ($stat['type'] ?? 0) === 2;
if ($isDirChild) {
$collect($remoteChild, $localChild);
} else {
$size = (int) ($stat['size'] ?? 0);
$queue[] = [
'remote' => $remoteChild,
'local' => $localChild,
'size' => $size,
];
$files++;
$totalBytes += $size;
}
}
};
$collect($remotePath, $rootLocal);
}
return [
'success' => true,
'queue' => $queue,
'files' => $files,
'dirs' => $dirs,
'localDir' => $localDir,
'totalBytes' => $totalBytes,
];
} catch (\Exception $e) { } catch (\Exception $e) {
return ['error' => $e->getMessage()]; return ['error' => $e->getMessage()];
} }
}, },
function ($result) use ($sftpTab, $statusLabel, $localDir) { function ($result) use ($sftpTab, $statusLabel, &$currentPrivateKeyPath, $serverListTab) {
if (isset($result['error'])) { if (isset($result['error'])) {
$statusLabel->setText('Fehler beim Herunterladen: ' . $result['error']); $statusLabel->setText('Fehler beim Vorbereiten des Downloads: ' . $result['error']);
$sftpTab->transferInfoLabel->setText('Download fehlgeschlagen');
return; return;
} }
if (isset($result['success'])) { if (!isset($result['success']) || !$result['success']) {
$statusLabel->setText('Datei erfolgreich heruntergeladen'); $statusLabel->setText('Unbekannter Fehler beim Vorbereiten des Downloads');
$sftpTab->localFileBrowser->loadDirectory($localDir); $sftpTab->transferInfoLabel->setText('Download fehlgeschlagen');
return;
} }
$queue = $result['queue'] ?? [];
$files = (int) ($result['files'] ?? 0);
$dirs = (int) ($result['dirs'] ?? 0);
$totalBytes = (int) ($result['totalBytes'] ?? 0);
$sftpTab->pendingDownloadQueue = $queue;
$sftpTab->totalDownloadFiles = count($queue);
$sftpTab->completedDownloadFiles = 0;
$sftpTab->totalDownloadBytes = $totalBytes;
$sftpTab->completedDownloadBytes = 0;
if ($sftpTab->totalDownloadFiles <= 0) {
$sftpTab->transferProgressBar->setValue(1.0);
$sftpTab->transferInfoLabel->setText('Keine Dateien zum Herunterladen');
$sftpTab->transferBytesLabel->setText('');
return;
}
$sftpTab->transferProgressBar->setValue(0.0);
$sftpTab->transferInfoLabel->setText(
sprintf(
'Download: %d Dateien, %d Ordner',
$files,
$dirs,
),
);
$sftpTab->transferBytesLabel->setText(
sprintf(
'0.00 / %.2f MB',
$totalBytes > 0 ? ($totalBytes / (1024 * 1024)) : 0,
),
);
$localDir = $result['localDir'] ?? '';
$sftpTab->startNextDownloadTask($currentPrivateKeyPath, $serverListTab, $statusLabel, $localDir);
}, },
function ($error) use ($statusLabel) { function ($error) use ($sftpTab, $statusLabel) {
$errorMsg = is_string($error) ? $error : 'Unknown error'; $errorMsg = is_string($error) ? $error : 'Unknown error';
$statusLabel->setText('Fehler beim Herunterladen: ' . $errorMsg); $statusLabel->setText('Fehler beim Vorbereiten des Downloads: ' . $errorMsg);
$sftpTab->transferInfoLabel->setText('Download fehlgeschlagen');
}, },
); );
$downloadAsyncButton->handleMouseClick(0, 0, 0); $scanButton->handleMouseClick(0, 0, 0);
} }
private function createRenameModal( private function createRenameModal(
@ -934,13 +1413,14 @@ class SftpManagerTab
$modalContent = new Container('flex flex-col bg-white rounded-lg shadow-lg p-4 gap-3 w-96'); $modalContent = new Container('flex flex-col bg-white rounded-lg shadow-lg p-4 gap-3 w-96');
// Title // Title
$modalContent->addComponent(new Label('Datei umbenennen', 'text-lg font-bold text-black')); $modalContent->addComponent(new Label('Umbenennen', 'text-lg font-bold text-black'));
// Path info
$this->renamePathLabel = new Label('', 'text-xs text-gray-600 font-mono break-words');
$modalContent->addComponent($this->renamePathLabel);
// Filename input // Filename input
$this->renameInput = new TextInput( $this->renameInput = new TextInput('Neuer Name', 'w-full border-2 border-gray-300 rounded px-3 py-2 bg-white text-black');
'Neuer Dateiname',
'w-full border-2 border-gray-300 rounded px-3 py-2 bg-white text-black',
);
$modalContent->addComponent($this->renameInput); $modalContent->addComponent($this->renameInput);
// Button container // Button container
@ -965,12 +1445,25 @@ class SftpManagerTab
} }
$oldPath = $sftpTab->currentRenameFilePath; $oldPath = $sftpTab->currentRenameFilePath;
$directory = dirname($oldPath); $separator = $sftpTab->currentRenameMode === 'local' ? DIRECTORY_SEPARATOR : '/';
$newPath = $directory . '/' . $newFilename; $directory = $sftpTab->normalizeDirectory(dirname($oldPath), $separator);
$newPath = $sftpTab->joinPath($directory, $newFilename, $separator);
// Close rename modal // Close rename modal
$sftpTab->renameModal->setVisible(false); $sftpTab->renameModal->setVisible(false);
if ($sftpTab->currentRenameMode === 'local') {
$succeeded = @rename($oldPath, $newPath);
if ($succeeded === false) {
$statusLabel->setText('Lokales Umbenennen fehlgeschlagen');
return;
}
$statusLabel->setText('Lokal umbenannt: ' . basename($newPath));
$sftpTab->localFileBrowser->loadDirectory($directory);
return;
}
// Perform rename via SFTP // Perform rename via SFTP
$selectedServerRef = &$serverListTab->selectedServer; $selectedServerRef = &$serverListTab->selectedServer;
@ -1134,9 +1627,25 @@ class SftpManagerTab
return; return;
} }
$this->currentRenameMode = 'remote';
$this->currentRenameFilePath = $path; $this->currentRenameFilePath = $path;
$filename = basename($path); $filename = basename($path);
$this->renameInput->setValue($filename); $this->renameInput->setValue($filename);
$this->renamePathLabel->setText('Remote: ' . $path);
$this->renameModal->setVisible(true);
}
private function handleLocalRename(string $path, array $row, Label $statusLabel): void
{
if (!file_exists($path)) {
$statusLabel->setText('Lokale Datei/Ordner nicht gefunden');
return;
}
$this->currentRenameMode = 'local';
$this->currentRenameFilePath = $path;
$this->renameInput->setValue(basename($path));
$this->renamePathLabel->setText('Lokal: ' . $path);
$this->renameModal->setVisible(true); $this->renameModal->setVisible(true);
} }
@ -1157,4 +1666,23 @@ class SftpManagerTab
$this->deleteConfirmLabel->setText('Möchten Sie die Datei "' . $filename . '" wirklich löschen?'); $this->deleteConfirmLabel->setText('Möchten Sie die Datei "' . $filename . '" wirklich löschen?');
$this->deleteConfirmModal->setVisible(true); $this->deleteConfirmModal->setVisible(true);
} }
private function normalizeDirectory(string $directory, string $separator): string
{
$normalized = rtrim($directory, $separator);
if ($normalized === '') {
return $separator;
}
return $normalized;
}
private function joinPath(string $directory, string $filename, string $separator): string
{
if ($directory === $separator) {
return $directory . $filename;
}
return $directory . $separator . $filename;
}
} }

View File

@ -2,6 +2,8 @@
declare(strict_types=1); declare(strict_types=1);
putenv('XKB_CONFIG_ROOT=/usr/share/X11/xkb');
putenv('XLOCALEDIR=/usr/share/X11/locale');
// Bootstrap: Load composer autoloader // Bootstrap: Load composer autoloader
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use PHPNative\Framework\Application;
use PHPNative\Ui\Widget\TextArea;
use PHPNative\Ui\Window;
$app = new Application();
$window = new Window('TextArea Scroll Test', 800, 600);
// Root container: nur Hintergrund, TextArea soll den ganzen Bereich nutzen
$root = new \PHPNative\Ui\Widget\Container('bg-gray-100');
// Viel Text erzeugen
$lines = [];
for ($i = 1; $i <= 200; $i++) {
$lines[] = sprintf('Zeile %03d: Dies ist eine Testzeile zum Scrollen in der TextArea.', $i);
}
$longText = implode("\n", $lines);
$textArea = new TextArea($longText, 'Scroll-Test', 'w-full h-full border border-gray-300 bg-white text-black font-mono text-sm');
// Optional: kein Texture-Cache, damit Änderungen sofort sichtbar sind
$textArea->setUseTextureCache(false);
$root->addComponent($textArea);
$window->setRoot($root);
$app->addWindow($window);
$app->run();

View File

@ -36,11 +36,7 @@ $loadTasks = static function (string $path): array {
}; };
$saveTasks = static function (string $path, array $tasks): void { $saveTasks = static function (string $path, array $tasks): void {
file_put_contents( file_put_contents($path, json_encode($tasks, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), LOCK_EX);
$path,
json_encode($tasks, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE),
LOCK_EX,
);
}; };
$tasks = $loadTasks($storagePath); $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'); $title = new Label('Todo Liste', 'text-2xl font-bold text-black');
$main->addComponent($title); $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'); $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 = new Container('flex flex-row gap-3 w-full');
$inputRow->addComponent($input); $inputRow->addComponent($input);
@ -78,10 +77,10 @@ $renderTasks = function () use (&$tasks, $listContainer, $statusLabel, $storageP
} }
foreach ($tasks as $index => $task) { 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'); $row = new Container(
$taskLabelStyles = $task['done'] 'flex flex-row items-center gap-3 w-full border border-gray-200 rounded px-3 py-2 bg-white shadow-sm',
? 'flex-1 text-gray-500 line-through' );
: 'flex-1 text-black'; $taskLabelStyles = $task['done'] ? 'flex-1 text-gray-500 line-through' : 'flex-1 text-black';
$taskLabel = new Label($task['title'], $taskLabelStyles); $taskLabel = new Label($task['title'], $taskLabelStyles);
$row->addComponent($taskLabel); $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', : '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) { foreach ($tasks as &$entry) {
if ($entry['id'] === $task['id']) { if ($entry['id'] === $task['id']) {
$entry['done'] = !$entry['done']; $entry['done'] = !$entry['done'];
@ -105,12 +111,17 @@ $renderTasks = function () use (&$tasks, $listContainer, $statusLabel, $storageP
$renderTasks(); $renderTasks();
}); });
$deleteButton = new Button( $deleteButton = new Button('Löschen', 'px-3 py-1 text-sm bg-red-500 text-white rounded hover:bg-red-600');
'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); array_splice($tasks, $index, 1);
$saveTasks($storagePath, $tasks); $saveTasks($storagePath, $tasks);
$statusLabel->setText('Aufgabe entfernt: ' . $task['title']); $statusLabel->setText('Aufgabe entfernt: ' . $task['title']);
@ -147,4 +158,64 @@ $addButton->setOnClick(function () use (&$tasks, $input, $saveTasks, $storagePat
$window->setRoot($main); $window->setRoot($main);
$app->addWindow($window); $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(); $app->run();

View File

@ -1,12 +1,7 @@
[ [
{ {
"id": "task_69164e23f0d356.41043316", "id": "task_692840bf6e9035.94502029",
"title": "Test", "title": "Tray: Neue Aufgabe",
"done": false "done": false
},
{
"id": "task_69164e28dae205.72302890",
"title": "Geht",
"done": true
} }
] ]

View File

@ -14,7 +14,7 @@ library_names='sdl3.so sdl3.so sdl3.so'
old_library='' old_library=''
# Libraries that this one depends upon. # Libraries that this one depends upon.
dependency_libs=' -L/usr/local/lib -lSDL3_gfx -lSDL3_image -lSDL3_ttf -lSDL3' dependency_libs=' -L/usr/local/lib -lSDL3_image -lSDL3_ttf -lSDL3 -lharfbuzz -latomic -lsysprof-capture-4 -lpcre2-8 -lgraphite2 -lfreetype -lbz2 -lpng16 -lm -lz -lbrotlidec -lbrotlicommon -lnotify -lgdk_pixbuf-2.0 -lgio-2.0 -lgobject-2.0 -lglib-2.0'
# Version information for sdl3. # Version information for sdl3.
current=0 current=0

Binary file not shown.

Binary file not shown.

View File

@ -18,7 +18,7 @@ exec_prefix = $(prefix)
libdir = ${exec_prefix}/lib libdir = ${exec_prefix}/lib
phpincludedir = /usr/local/include/php phpincludedir = /usr/local/include/php
CC = cc CC = cc
CFLAGS = -g -O2 -I/usr/local/include -I/usr/local/include -I/usr/local/include -I/usr/local/include -I/usr/include/harfbuzz -I/usr/include/freetype2 -I/usr/include/libpng16 -I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include -I/usr/include/sysprof-6 -pthread CFLAGS = -g -O2 -I/usr/local/include -I/usr/local/include -I/usr/local/include -I/usr/include/harfbuzz -I/usr/include/freetype2 -I/usr/include/libpng16 -I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include -I/usr/include/sysprof-6 -pthread -I/usr/include/gdk-pixbuf-2.0 -I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include -I/usr/include/sysprof-6 -I/usr/include/libpng16 -I/usr/include/x86_64-linux-gnu -I/usr/include/webp -I/usr/include/libmount -I/usr/include/blkid -pthread
CFLAGS_CLEAN = $(CFLAGS) -D_GNU_SOURCE CFLAGS_CLEAN = $(CFLAGS) -D_GNU_SOURCE
CPP = cc -E CPP = cc -E
CPPFLAGS = -DHAVE_CONFIG_H CPPFLAGS = -DHAVE_CONFIG_H
@ -30,7 +30,7 @@ PHP_EXECUTABLE = /usr/local/bin/php
EXTRA_LDFLAGS = EXTRA_LDFLAGS =
EXTRA_LIBS = EXTRA_LIBS =
INCLUDES = -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib INCLUDES = -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib
LDFLAGS = -L/usr/local/lib -Wl,-rpath,/usr/local/lib -Wl,--enable-new-dtags -lSDL3 -L/usr/local/lib -lSDL3_gfx -Wl,-rpath,/usr/local/lib -Wl,--enable-new-dtags -lSDL3 -L/usr/local/lib -lSDL3_image -Wl,-rpath,/usr/local/lib -Wl,--enable-new-dtags -lSDL3 -L/usr/local/lib -lSDL3_ttf -Wl,-rpath,/usr/local/lib -Wl,--enable-new-dtags -lSDL3 LDFLAGS = -L/usr/local/lib -Wl,-rpath,/usr/local/lib -Wl,--enable-new-dtags -lSDL3 -pthread -lm -L/usr/local/lib -lSDL3_image -Wl,-rpath,/usr/local/lib -Wl,--enable-new-dtags -lSDL3 -pthread -lm -L/usr/local/lib -lSDL3_ttf -Wl,-rpath,/usr/local/lib -Wl,--enable-new-dtags -lSDL3 -pthread -lm -lharfbuzz -pthread -lm -lz -lz -lm -lz -lbrotlicommon -lglib-2.0 -latomic -lm -pthread -lsysprof-capture-4 -pthread -lpcre2-8 -lgraphite2 -lfreetype -lz -lbz2 -lpng16 -lz -lm -lz -lbrotlidec -lbrotlicommon -lnotify -lgdk_pixbuf-2.0 -lgio-2.0 -lgobject-2.0 -lglib-2.0
LIBTOOL = $(SHELL) $(top_builddir)/libtool LIBTOOL = $(SHELL) $(top_builddir)/libtool
SHELL = /bin/bash SHELL = /bin/bash
INSTALL_HEADERS = INSTALL_HEADERS =

View File

@ -10,6 +10,9 @@
/* Define to 1 if you have the <inttypes.h> header file. */ /* Define to 1 if you have the <inttypes.h> header file. */
#define HAVE_INTTYPES_H 1 #define HAVE_INTTYPES_H 1
/* Enable libnotify for desktop_notify() */
#define HAVE_LIBNOTIFY 1
/* Define to 1 if you have the <stdint.h> header file. */ /* Define to 1 if you have the <stdint.h> header file. */
#define HAVE_STDINT_H 1 #define HAVE_STDINT_H 1

View File

@ -9,6 +9,9 @@
/* Define to 1 if you have the <inttypes.h> header file. */ /* Define to 1 if you have the <inttypes.h> header file. */
#undef HAVE_INTTYPES_H #undef HAVE_INTTYPES_H
/* Enable libnotify for desktop_notify() */
#undef HAVE_LIBNOTIFY
/* Define to 1 if you have the <stdint.h> header file. */ /* Define to 1 if you have the <stdint.h> header file. */
#undef HAVE_STDINT_H #undef HAVE_STDINT_H

View File

@ -2,10 +2,6 @@ PHP_ARG_WITH(sdl3, [for sdl3 support], [
AS_HELP_STRING([--with-sdl3[=DIR]], [Enable sdl3 support. DIR is the prefix for SDL3 installation.]) AS_HELP_STRING([--with-sdl3[=DIR]], [Enable sdl3 support. DIR is the prefix for SDL3 installation.])
]) ])
PHP_ARG_WITH(sdl3_gfx, [for sdl3_gfx support], [
AS_HELP_STRING([--with-sdl3-gfx[=DIR]], [Enable sdl3_gfx support. DIR is the prefix for SDL3_gfx installation.])
])
PHP_ARG_WITH(sdl3_image, [for sdl3_image support], [ PHP_ARG_WITH(sdl3_image, [for sdl3_image support], [
AS_HELP_STRING([--with-sdl3-image[=DIR]], [Enable sdl3_image support. DIR is the prefix for SDL3_image installation.]) AS_HELP_STRING([--with-sdl3-image[=DIR]], [Enable sdl3_image support. DIR is the prefix for SDL3_image installation.])
]) ])
@ -21,22 +17,19 @@ if test "$PHP_SDL3" != "no"; then
PKG_CHECK_MODULES([SDL3], [sdl3 >= 3.0.0], [ PKG_CHECK_MODULES([SDL3], [sdl3 >= 3.0.0], [
CFLAGS="$CFLAGS $SDL3_CFLAGS" CFLAGS="$CFLAGS $SDL3_CFLAGS"
LDFLAGS="$LDFLAGS $SDL3_LIBS"
],[ ],[
AC_MSG_ERROR([SDL3 not found. Please check your installation or use --with-sdl3=/path/to/sdl3]) AC_MSG_ERROR([SDL3 not found. Please check your installation or use --with-sdl3=/path/to/sdl3])
]) ])
if test "$PHP_SDL3_GFX" != "no"; then dnl Prefer static SDL3 libs if available
if test -d "$PHP_SDL3_GFX"; then AC_MSG_CHECKING([for static SDL3 libs])
PKG_CONFIG_PATH="$PHP_SDL3_GFX/lib/pkgconfig:$PHP_SDL3_GFX/share/pkgconfig:$PKG_CONFIG_PATH" SDL3_STATIC_LIBS=`$PKG_CONFIG --libs --static sdl3 2>/dev/null`
fi if test "x$SDL3_STATIC_LIBS" != "x"; then
AC_MSG_RESULT([$SDL3_STATIC_LIBS])
PKG_CHECK_MODULES([SDL3_GFX], [sdl3-gfx >= 1.0.0], [ LDFLAGS="$LDFLAGS $SDL3_STATIC_LIBS"
CFLAGS="$CFLAGS $SDL3_GFX_CFLAGS" else
LDFLAGS="$LDFLAGS $SDL3_GFX_LIBS" AC_MSG_RESULT([not found, using shared SDL3 libs])
],[ LDFLAGS="$LDFLAGS $SDL3_LIBS"
AC_MSG_ERROR([SDL3_gfx not found. Please check your installation or use --with-sdl3-gfx=/path/to/sdl3_gfx])
])
fi fi
if test "$PHP_SDL3_IMAGE" != "no"; then if test "$PHP_SDL3_IMAGE" != "no"; then
@ -46,10 +39,20 @@ if test "$PHP_SDL3" != "no"; then
PKG_CHECK_MODULES([SDL3_IMAGE], [sdl3-image >= 3.0.0], [ PKG_CHECK_MODULES([SDL3_IMAGE], [sdl3-image >= 3.0.0], [
CFLAGS="$CFLAGS $SDL3_IMAGE_CFLAGS" CFLAGS="$CFLAGS $SDL3_IMAGE_CFLAGS"
LDFLAGS="$LDFLAGS $SDL3_IMAGE_LIBS"
],[ ],[
AC_MSG_ERROR([SDL3_image not found. Please check your installation or use --with-sdl3-image=/path/to/sdl3_image]) AC_MSG_ERROR([SDL3_image not found. Please check your installation or use --with-sdl3-image=/path/to/sdl3_image])
]) ])
dnl Prefer static SDL3_image libs if available
AC_MSG_CHECKING([for static SDL3_image libs])
SDL3_IMAGE_STATIC_LIBS=`$PKG_CONFIG --libs --static sdl3-image 2>/dev/null`
if test "x$SDL3_IMAGE_STATIC_LIBS" != "x"; then
AC_MSG_RESULT([$SDL3_IMAGE_STATIC_LIBS])
LDFLAGS="$LDFLAGS $SDL3_IMAGE_STATIC_LIBS"
else
AC_MSG_RESULT([not found, using shared SDL3_image libs])
LDFLAGS="$LDFLAGS $SDL3_IMAGE_LIBS"
fi
fi fi
if test "$PHP_SDL3_TTF" != "no"; then if test "$PHP_SDL3_TTF" != "no"; then
@ -59,11 +62,32 @@ if test "$PHP_SDL3" != "no"; then
PKG_CHECK_MODULES([SDL3_TTF], [sdl3-ttf >= 3.0.0], [ PKG_CHECK_MODULES([SDL3_TTF], [sdl3-ttf >= 3.0.0], [
CFLAGS="$CFLAGS $SDL3_TTF_CFLAGS" CFLAGS="$CFLAGS $SDL3_TTF_CFLAGS"
LDFLAGS="$LDFLAGS $SDL3_TTF_LIBS"
],[ ],[
AC_MSG_ERROR([SDL3_ttf not found. Please check your installation or use --with-sdl3-ttf=/path/to/sdl3_ttf]) AC_MSG_ERROR([SDL3_ttf not found. Please check your installation or use --with-sdl3-ttf=/path/to/sdl3_ttf])
]) ])
dnl Prefer static SDL3_ttf libs if available
AC_MSG_CHECKING([for static SDL3_ttf libs])
SDL3_TTF_STATIC_LIBS=`$PKG_CONFIG --libs --static sdl3-ttf 2>/dev/null`
if test "x$SDL3_TTF_STATIC_LIBS" != "x"; then
AC_MSG_RESULT([$SDL3_TTF_STATIC_LIBS])
LDFLAGS="$LDFLAGS $SDL3_TTF_STATIC_LIBS"
else
AC_MSG_RESULT([not found, using shared SDL3_ttf libs])
LDFLAGS="$LDFLAGS $SDL3_TTF_LIBS"
fi fi
fi
dnl Optional libnotify support for desktop notifications
PKG_CHECK_MODULES([LIBNOTIFY], [libnotify], [
AC_DEFINE([HAVE_LIBNOTIFY], [1], [Enable libnotify for desktop_notify()])
CFLAGS="$CFLAGS $LIBNOTIFY_CFLAGS"
LDFLAGS="$LDFLAGS $LIBNOTIFY_LIBS"
], [
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" SDL_SOURCE_FILES="sdl3.c helper.c sdl3_image.c sdl3_ttf.c sdl3_events.c"

View File

@ -570,6 +570,7 @@ D["PACKAGE_VERSION"]=" \"\""
D["PACKAGE_STRING"]=" \"\"" D["PACKAGE_STRING"]=" \"\""
D["PACKAGE_BUGREPORT"]=" \"\"" D["PACKAGE_BUGREPORT"]=" \"\""
D["PACKAGE_URL"]=" \"\"" D["PACKAGE_URL"]=" \"\""
D["HAVE_LIBNOTIFY"]=" 1"
D["COMPILE_DL_SDL3"]=" 1" D["COMPILE_DL_SDL3"]=" 1"
D["HAVE_STDIO_H"]=" 1" D["HAVE_STDIO_H"]=" 1"
D["HAVE_STDLIB_H"]=" 1" D["HAVE_STDLIB_H"]=" 1"

322
php-sdl3/configure vendored
View File

@ -802,12 +802,12 @@ RANLIB
AR AR
ECHO ECHO
LN_S LN_S
LIBNOTIFY_LIBS
LIBNOTIFY_CFLAGS
SDL3_TTF_LIBS SDL3_TTF_LIBS
SDL3_TTF_CFLAGS SDL3_TTF_CFLAGS
SDL3_IMAGE_LIBS SDL3_IMAGE_LIBS
SDL3_IMAGE_CFLAGS SDL3_IMAGE_CFLAGS
SDL3_GFX_LIBS
SDL3_GFX_CFLAGS
SDL3_LIBS SDL3_LIBS
SDL3_CFLAGS SDL3_CFLAGS
SHLIB_DL_SUFFIX_NAME SHLIB_DL_SUFFIX_NAME
@ -887,7 +887,6 @@ with_libdir
with_php_config with_php_config
enable_ enable_
with_sdl3 with_sdl3
with_sdl3_gfx
with_sdl3_image with_sdl3_image
with_sdl3_ttf with_sdl3_ttf
enable_shared enable_shared
@ -912,12 +911,12 @@ CPPFLAGS
CPP CPP
SDL3_CFLAGS SDL3_CFLAGS
SDL3_LIBS SDL3_LIBS
SDL3_GFX_CFLAGS
SDL3_GFX_LIBS
SDL3_IMAGE_CFLAGS SDL3_IMAGE_CFLAGS
SDL3_IMAGE_LIBS SDL3_IMAGE_LIBS
SDL3_TTF_CFLAGS SDL3_TTF_CFLAGS
SDL3_TTF_LIBS' SDL3_TTF_LIBS
LIBNOTIFY_CFLAGS
LIBNOTIFY_LIBS'
# Initialize some variables set by options. # Initialize some variables set by options.
@ -1550,10 +1549,6 @@ Extension:
installation. installation.
--with-sdl3-gfx=DIR Enable sdl3_gfx support. DIR is the prefix for
SDL3_gfx installation.
--with-sdl3-image=DIR Enable sdl3_image support. DIR is the prefix for --with-sdl3-image=DIR Enable sdl3_image support. DIR is the prefix for
SDL3_image installation. SDL3_image installation.
@ -1589,10 +1584,6 @@ Some influential environment variables:
CPP C preprocessor CPP C preprocessor
SDL3_CFLAGS C compiler flags for SDL3, overriding pkg-config SDL3_CFLAGS C compiler flags for SDL3, overriding pkg-config
SDL3_LIBS linker flags for SDL3, overriding pkg-config SDL3_LIBS linker flags for SDL3, overriding pkg-config
SDL3_GFX_CFLAGS
C compiler flags for SDL3_GFX, overriding pkg-config
SDL3_GFX_LIBS
linker flags for SDL3_GFX, overriding pkg-config
SDL3_IMAGE_CFLAGS SDL3_IMAGE_CFLAGS
C compiler flags for SDL3_IMAGE, overriding pkg-config C compiler flags for SDL3_IMAGE, overriding pkg-config
SDL3_IMAGE_LIBS SDL3_IMAGE_LIBS
@ -1601,6 +1592,10 @@ Some influential environment variables:
C compiler flags for SDL3_TTF, overriding pkg-config C compiler flags for SDL3_TTF, overriding pkg-config
SDL3_TTF_LIBS SDL3_TTF_LIBS
linker flags for SDL3_TTF, overriding pkg-config linker flags for SDL3_TTF, overriding pkg-config
LIBNOTIFY_CFLAGS
C compiler flags for LIBNOTIFY, overriding pkg-config
LIBNOTIFY_LIBS
linker flags for LIBNOTIFY, overriding pkg-config
Use these variables to override the choices made by 'configure' or to help Use these variables to override the choices made by 'configure' or to help
it to find libraries and programs with nonstandard names/locations. it to find libraries and programs with nonstandard names/locations.
@ -4777,57 +4772,6 @@ printf "%s\n" "$ext_output" >&6; }
php_with_sdl3_gfx=no
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for sdl3_gfx support" >&5
printf %s "checking for sdl3_gfx support... " >&6; }
# Check whether --with-sdl3_gfx was given.
if test ${with_sdl3_gfx+y}
then :
withval=$with_sdl3_gfx; PHP_SDL3_GFX=$withval
else case e in #(
e)
PHP_SDL3_GFX=no
test "$PHP_ENABLE_ALL" && PHP_SDL3_GFX=$PHP_ENABLE_ALL
;;
esac
fi
ext_output="yes, shared"
ext_shared=yes
case $PHP_SDL3_GFX in
shared,*)
PHP_SDL3_GFX=$(echo "$PHP_SDL3_GFX"|$SED 's/^shared,//')
;;
shared)
PHP_SDL3_GFX=yes
;;
no)
ext_output=no
ext_shared=no
;;
*)
ext_output=yes
ext_shared=no
;;
esac
ext_output="yes, shared"
ext_shared=yes
test "$PHP_SDL3_GFX" = "no" && PHP_SDL3_GFX=yes
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ext_output" >&5
printf "%s\n" "$ext_output" >&6; }
php_with_sdl3_image=no php_with_sdl3_image=no
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for sdl3_image support" >&5 { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for sdl3_image support" >&5
@ -5009,93 +4953,20 @@ else
printf "%s\n" "yes" >&6; } printf "%s\n" "yes" >&6; }
CFLAGS="$CFLAGS $SDL3_CFLAGS" CFLAGS="$CFLAGS $SDL3_CFLAGS"
fi
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for static SDL3 libs" >&5
printf %s "checking for static SDL3 libs... " >&6; }
SDL3_STATIC_LIBS=`$PKG_CONFIG --libs --static sdl3 2>/dev/null`
if test "x$SDL3_STATIC_LIBS" != "x"; then
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $SDL3_STATIC_LIBS" >&5
printf "%s\n" "$SDL3_STATIC_LIBS" >&6; }
LDFLAGS="$LDFLAGS $SDL3_STATIC_LIBS"
else
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: not found, using shared SDL3 libs" >&5
printf "%s\n" "not found, using shared SDL3 libs" >&6; }
LDFLAGS="$LDFLAGS $SDL3_LIBS" LDFLAGS="$LDFLAGS $SDL3_LIBS"
fi
if test "$PHP_SDL3_GFX" != "no"; then
if test -d "$PHP_SDL3_GFX"; then
PKG_CONFIG_PATH="$PHP_SDL3_GFX/lib/pkgconfig:$PHP_SDL3_GFX/share/pkgconfig:$PKG_CONFIG_PATH"
fi
pkg_failed=no
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for sdl3-gfx >= 1.0.0" >&5
printf %s "checking for sdl3-gfx >= 1.0.0... " >&6; }
if test -n "$SDL3_GFX_CFLAGS"; then
pkg_cv_SDL3_GFX_CFLAGS="$SDL3_GFX_CFLAGS"
elif test -n "$PKG_CONFIG"; then
if test -n "$PKG_CONFIG" && \
{ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"sdl3-gfx >= 1.0.0\""; } >&5
($PKG_CONFIG --exists --print-errors "sdl3-gfx >= 1.0.0") 2>&5
ac_status=$?
printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
test $ac_status = 0; }; then
pkg_cv_SDL3_GFX_CFLAGS=`$PKG_CONFIG --cflags "sdl3-gfx >= 1.0.0" 2>/dev/null`
test "x$?" != "x0" && pkg_failed=yes
else
pkg_failed=yes
fi
else
pkg_failed=untried
fi
if test -n "$SDL3_GFX_LIBS"; then
pkg_cv_SDL3_GFX_LIBS="$SDL3_GFX_LIBS"
elif test -n "$PKG_CONFIG"; then
if test -n "$PKG_CONFIG" && \
{ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"sdl3-gfx >= 1.0.0\""; } >&5
($PKG_CONFIG --exists --print-errors "sdl3-gfx >= 1.0.0") 2>&5
ac_status=$?
printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
test $ac_status = 0; }; then
pkg_cv_SDL3_GFX_LIBS=`$PKG_CONFIG --libs "sdl3-gfx >= 1.0.0" 2>/dev/null`
test "x$?" != "x0" && pkg_failed=yes
else
pkg_failed=yes
fi
else
pkg_failed=untried
fi
if test $pkg_failed = yes; then
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
printf "%s\n" "no" >&6; }
if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then
_pkg_short_errors_supported=yes
else
_pkg_short_errors_supported=no
fi
if test $_pkg_short_errors_supported = yes; then
SDL3_GFX_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "sdl3-gfx >= 1.0.0" 2>&1`
else
SDL3_GFX_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "sdl3-gfx >= 1.0.0" 2>&1`
fi
# Put the nasty error message in config.log where it belongs
echo "$SDL3_GFX_PKG_ERRORS" >&5
as_fn_error $? "SDL3_gfx not found. Please check your installation or use --with-sdl3-gfx=/path/to/sdl3_gfx" "$LINENO" 5
elif test $pkg_failed = untried; then
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
printf "%s\n" "no" >&6; }
as_fn_error $? "SDL3_gfx not found. Please check your installation or use --with-sdl3-gfx=/path/to/sdl3_gfx" "$LINENO" 5
else
SDL3_GFX_CFLAGS=$pkg_cv_SDL3_GFX_CFLAGS
SDL3_GFX_LIBS=$pkg_cv_SDL3_GFX_LIBS
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
printf "%s\n" "yes" >&6; }
CFLAGS="$CFLAGS $SDL3_GFX_CFLAGS"
LDFLAGS="$LDFLAGS $SDL3_GFX_LIBS"
fi
fi fi
if test "$PHP_SDL3_IMAGE" != "no"; then if test "$PHP_SDL3_IMAGE" != "no"; then
@ -5178,9 +5049,21 @@ else
printf "%s\n" "yes" >&6; } printf "%s\n" "yes" >&6; }
CFLAGS="$CFLAGS $SDL3_IMAGE_CFLAGS" CFLAGS="$CFLAGS $SDL3_IMAGE_CFLAGS"
LDFLAGS="$LDFLAGS $SDL3_IMAGE_LIBS"
fi fi
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for static SDL3_image libs" >&5
printf %s "checking for static SDL3_image libs... " >&6; }
SDL3_IMAGE_STATIC_LIBS=`$PKG_CONFIG --libs --static sdl3-image 2>/dev/null`
if test "x$SDL3_IMAGE_STATIC_LIBS" != "x"; then
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $SDL3_IMAGE_STATIC_LIBS" >&5
printf "%s\n" "$SDL3_IMAGE_STATIC_LIBS" >&6; }
LDFLAGS="$LDFLAGS $SDL3_IMAGE_STATIC_LIBS"
else
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: not found, using shared SDL3_image libs" >&5
printf "%s\n" "not found, using shared SDL3_image libs" >&6; }
LDFLAGS="$LDFLAGS $SDL3_IMAGE_LIBS"
fi
fi fi
if test "$PHP_SDL3_TTF" != "no"; then if test "$PHP_SDL3_TTF" != "no"; then
@ -5263,10 +5146,107 @@ else
printf "%s\n" "yes" >&6; } printf "%s\n" "yes" >&6; }
CFLAGS="$CFLAGS $SDL3_TTF_CFLAGS" CFLAGS="$CFLAGS $SDL3_TTF_CFLAGS"
LDFLAGS="$LDFLAGS $SDL3_TTF_LIBS"
fi fi
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for static SDL3_ttf libs" >&5
printf %s "checking for static SDL3_ttf libs... " >&6; }
SDL3_TTF_STATIC_LIBS=`$PKG_CONFIG --libs --static sdl3-ttf 2>/dev/null`
if test "x$SDL3_TTF_STATIC_LIBS" != "x"; then
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $SDL3_TTF_STATIC_LIBS" >&5
printf "%s\n" "$SDL3_TTF_STATIC_LIBS" >&6; }
LDFLAGS="$LDFLAGS $SDL3_TTF_STATIC_LIBS"
else
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: not found, using shared SDL3_ttf libs" >&5
printf "%s\n" "not found, using shared SDL3_ttf libs" >&6; }
LDFLAGS="$LDFLAGS $SDL3_TTF_LIBS"
fi fi
fi
pkg_failed=no
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for libnotify" >&5
printf %s "checking for libnotify... " >&6; }
if test -n "$LIBNOTIFY_CFLAGS"; then
pkg_cv_LIBNOTIFY_CFLAGS="$LIBNOTIFY_CFLAGS"
elif test -n "$PKG_CONFIG"; then
if test -n "$PKG_CONFIG" && \
{ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"libnotify\""; } >&5
($PKG_CONFIG --exists --print-errors "libnotify") 2>&5
ac_status=$?
printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
test $ac_status = 0; }; then
pkg_cv_LIBNOTIFY_CFLAGS=`$PKG_CONFIG --cflags "libnotify" 2>/dev/null`
test "x$?" != "x0" && pkg_failed=yes
else
pkg_failed=yes
fi
else
pkg_failed=untried
fi
if test -n "$LIBNOTIFY_LIBS"; then
pkg_cv_LIBNOTIFY_LIBS="$LIBNOTIFY_LIBS"
elif test -n "$PKG_CONFIG"; then
if test -n "$PKG_CONFIG" && \
{ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"libnotify\""; } >&5
($PKG_CONFIG --exists --print-errors "libnotify") 2>&5
ac_status=$?
printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
test $ac_status = 0; }; then
pkg_cv_LIBNOTIFY_LIBS=`$PKG_CONFIG --libs "libnotify" 2>/dev/null`
test "x$?" != "x0" && pkg_failed=yes
else
pkg_failed=yes
fi
else
pkg_failed=untried
fi
if test $pkg_failed = yes; then
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
printf "%s\n" "no" >&6; }
if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then
_pkg_short_errors_supported=yes
else
_pkg_short_errors_supported=no
fi
if test $_pkg_short_errors_supported = yes; then
LIBNOTIFY_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "libnotify" 2>&1`
else
LIBNOTIFY_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "libnotify" 2>&1`
fi
# Put the nasty error message in config.log where it belongs
echo "$LIBNOTIFY_PKG_ERRORS" >&5
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: libnotify not found via pkg-config, desktop_notify() will be disabled" >&5
printf "%s\n" "$as_me: WARNING: libnotify not found via pkg-config, desktop_notify() will be disabled" >&2;}
elif test $pkg_failed = untried; then
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
printf "%s\n" "no" >&6; }
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: libnotify not found via pkg-config, desktop_notify() will be disabled" >&5
printf "%s\n" "$as_me: WARNING: libnotify not found via pkg-config, desktop_notify() will be disabled" >&2;}
else
LIBNOTIFY_CFLAGS=$pkg_cv_LIBNOTIFY_CFLAGS
LIBNOTIFY_LIBS=$pkg_cv_LIBNOTIFY_LIBS
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
printf "%s\n" "yes" >&6; }
printf "%s\n" "#define HAVE_LIBNOTIFY 1" >>confdefs.h
CFLAGS="$CFLAGS $LIBNOTIFY_CFLAGS"
LDFLAGS="$LDFLAGS $LIBNOTIFY_LIBS"
fi
SDL_SOURCE_FILES="sdl3.c helper.c sdl3_image.c sdl3_ttf.c sdl3_events.c" SDL_SOURCE_FILES="sdl3.c helper.c sdl3_image.c sdl3_ttf.c sdl3_events.c"
@ -6123,7 +6103,7 @@ ia64-*-hpux*)
;; ;;
*-*-irix6*) *-*-irix6*)
# Find out which ABI we are using. # Find out which ABI we are using.
echo '#line 6126 "configure"' > conftest.$ac_ext echo '#line 6106 "configure"' > conftest.$ac_ext
if { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$ac_compile\""; } >&5 if { { eval echo "\"\$as_me\":${as_lineno-$LINENO}: \"$ac_compile\""; } >&5
(eval $ac_compile) 2>&5 (eval $ac_compile) 2>&5
ac_status=$? ac_status=$?
@ -7502,7 +7482,7 @@ else case e in #(
LDFLAGS="$LDFLAGS -Wl,-exported_symbols_list,conftest.sym" LDFLAGS="$LDFLAGS -Wl,-exported_symbols_list,conftest.sym"
cat > conftest.$ac_ext <<EOF cat > conftest.$ac_ext <<EOF
#line 7505 "configure" #line 7485 "configure"
#include "confdefs.h" #include "confdefs.h"
int main(void) { int main(void) {
; return 0; } ; return 0; }
@ -7664,11 +7644,11 @@ else case e in #(
-e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \ -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \
-e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \ -e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \
-e 's:$: $lt_compiler_flag:'` -e 's:$: $lt_compiler_flag:'`
(eval echo "\"configure:7667: $lt_compile\"" >&5) (eval echo "\"configure:7647: $lt_compile\"" >&5)
(eval "$lt_compile" 2>conftest.err) (eval "$lt_compile" 2>conftest.err)
ac_status=$? ac_status=$?
cat conftest.err >&5 cat conftest.err >&5
echo "configure:7671: \$? = $ac_status" >&5 echo "configure:7651: \$? = $ac_status" >&5
if (exit $ac_status) && test -s "$ac_outfile"; then if (exit $ac_status) && test -s "$ac_outfile"; then
# The compiler can only warn and ignore the option if not recognized # The compiler can only warn and ignore the option if not recognized
# So say no if there are warnings other than the usual output. # So say no if there are warnings other than the usual output.
@ -7964,11 +7944,11 @@ else case e in #(
-e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \ -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \
-e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \ -e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \
-e 's:$: $lt_compiler_flag:'` -e 's:$: $lt_compiler_flag:'`
(eval echo "\"configure:7967: $lt_compile\"" >&5) (eval echo "\"configure:7947: $lt_compile\"" >&5)
(eval "$lt_compile" 2>conftest.err) (eval "$lt_compile" 2>conftest.err)
ac_status=$? ac_status=$?
cat conftest.err >&5 cat conftest.err >&5
echo "configure:7971: \$? = $ac_status" >&5 echo "configure:7951: \$? = $ac_status" >&5
if (exit $ac_status) && test -s "$ac_outfile"; then if (exit $ac_status) && test -s "$ac_outfile"; then
# The compiler can only warn and ignore the option if not recognized # The compiler can only warn and ignore the option if not recognized
# So say no if there are warnings other than the usual output. # So say no if there are warnings other than the usual output.
@ -8072,11 +8052,11 @@ else case e in #(
-e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \ -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \
-e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \ -e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \
-e 's:$: $lt_compiler_flag:'` -e 's:$: $lt_compiler_flag:'`
(eval echo "\"configure:8075: $lt_compile\"" >&5) (eval echo "\"configure:8055: $lt_compile\"" >&5)
(eval "$lt_compile" 2>out/conftest.err) (eval "$lt_compile" 2>out/conftest.err)
ac_status=$? ac_status=$?
cat out/conftest.err >&5 cat out/conftest.err >&5
echo "configure:8079: \$? = $ac_status" >&5 echo "configure:8059: \$? = $ac_status" >&5
if (exit $ac_status) && test -s out/conftest2.$ac_objext if (exit $ac_status) && test -s out/conftest2.$ac_objext
then then
# The compiler can only warn and ignore the option if not recognized # The compiler can only warn and ignore the option if not recognized
@ -8537,7 +8517,7 @@ _LT_EOF
# Determine the default libpath from the value encoded in an empty executable. # Determine the default libpath from the value encoded in an empty executable.
cat > conftest.$ac_ext <<EOF cat > conftest.$ac_ext <<EOF
#line 8540 "configure" #line 8520 "configure"
#include "confdefs.h" #include "confdefs.h"
int main(void) { int main(void) {
; return 0; } ; return 0; }
@ -8579,7 +8559,7 @@ if test -z "$aix_libpath"; then aix_libpath="/usr/lib:/lib"; fi
# Determine the default libpath from the value encoded in an empty executable. # Determine the default libpath from the value encoded in an empty executable.
cat > conftest.$ac_ext <<EOF cat > conftest.$ac_ext <<EOF
#line 8582 "configure" #line 8562 "configure"
#include "confdefs.h" #include "confdefs.h"
int main(void) { int main(void) {
; return 0; } ; return 0; }
@ -10160,7 +10140,7 @@ else
lt_dlunknown=0; lt_dlno_uscore=1; lt_dlneed_uscore=2 lt_dlunknown=0; lt_dlno_uscore=1; lt_dlneed_uscore=2
lt_status=$lt_dlunknown lt_status=$lt_dlunknown
cat > conftest.$ac_ext <<EOF cat > conftest.$ac_ext <<EOF
#line 10163 "configure" #line 10143 "configure"
#include "confdefs.h" #include "confdefs.h"
#if HAVE_DLFCN_H #if HAVE_DLFCN_H
@ -10259,7 +10239,7 @@ else
lt_dlunknown=0; lt_dlno_uscore=1; lt_dlneed_uscore=2 lt_dlunknown=0; lt_dlno_uscore=1; lt_dlneed_uscore=2
lt_status=$lt_dlunknown lt_status=$lt_dlunknown
cat > conftest.$ac_ext <<EOF cat > conftest.$ac_ext <<EOF
#line 10262 "configure" #line 10242 "configure"
#include "confdefs.h" #include "confdefs.h"
#if HAVE_DLFCN_H #if HAVE_DLFCN_H
@ -11328,7 +11308,7 @@ case $host_os in
# Determine the default libpath from the value encoded in an empty executable. # Determine the default libpath from the value encoded in an empty executable.
cat > conftest.$ac_ext <<EOF cat > conftest.$ac_ext <<EOF
#line 11331 "configure" #line 11311 "configure"
#include "confdefs.h" #include "confdefs.h"
int main(void) { int main(void) {
; return 0; } ; return 0; }
@ -11371,7 +11351,7 @@ if test -z "$aix_libpath"; then aix_libpath="/usr/lib:/lib"; fi
# Determine the default libpath from the value encoded in an empty executable. # Determine the default libpath from the value encoded in an empty executable.
cat > conftest.$ac_ext <<EOF cat > conftest.$ac_ext <<EOF
#line 11374 "configure" #line 11354 "configure"
#include "confdefs.h" #include "confdefs.h"
int main(void) { int main(void) {
; return 0; } ; return 0; }
@ -12624,11 +12604,11 @@ else case e in #(
-e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \ -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \
-e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \ -e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \
-e 's:$: $lt_compiler_flag:'` -e 's:$: $lt_compiler_flag:'`
(eval echo "\"configure:12627: $lt_compile\"" >&5) (eval echo "\"configure:12607: $lt_compile\"" >&5)
(eval "$lt_compile" 2>conftest.err) (eval "$lt_compile" 2>conftest.err)
ac_status=$? ac_status=$?
cat conftest.err >&5 cat conftest.err >&5
echo "configure:12631: \$? = $ac_status" >&5 echo "configure:12611: \$? = $ac_status" >&5
if (exit $ac_status) && test -s "$ac_outfile"; then if (exit $ac_status) && test -s "$ac_outfile"; then
# The compiler can only warn and ignore the option if not recognized # The compiler can only warn and ignore the option if not recognized
# So say no if there are warnings other than the usual output. # So say no if there are warnings other than the usual output.
@ -12732,11 +12712,11 @@ else case e in #(
-e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \ -e 's:.*FLAGS}\{0,1\} :&$lt_compiler_flag :; t' \
-e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \ -e 's: [^ ]*conftest\.: $lt_compiler_flag&:; t' \
-e 's:$: $lt_compiler_flag:'` -e 's:$: $lt_compiler_flag:'`
(eval echo "\"configure:12735: $lt_compile\"" >&5) (eval echo "\"configure:12715: $lt_compile\"" >&5)
(eval "$lt_compile" 2>out/conftest.err) (eval "$lt_compile" 2>out/conftest.err)
ac_status=$? ac_status=$?
cat out/conftest.err >&5 cat out/conftest.err >&5
echo "configure:12739: \$? = $ac_status" >&5 echo "configure:12719: \$? = $ac_status" >&5
if (exit $ac_status) && test -s out/conftest2.$ac_objext if (exit $ac_status) && test -s out/conftest2.$ac_objext
then then
# The compiler can only warn and ignore the option if not recognized # The compiler can only warn and ignore the option if not recognized

View File

@ -2,7 +2,6 @@
#define PHP_SDL3_HELPER #define PHP_SDL3_HELPER
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <SDL3_gfx/SDL3_gfxPrimitives.h>
#include "math.h" #include "math.h"
// Zeichnet einen gefüllten Viertel-Kreis mit Anti-Aliasing (filled quarter circle). // Zeichnet einen gefüllten Viertel-Kreis mit Anti-Aliasing (filled quarter circle).

View File

@ -85,7 +85,7 @@ AR_FLAGS="cru"
LTCC="cc" LTCC="cc"
# LTCC compiler flags. # LTCC compiler flags.
LTCFLAGS="-g -O2 -I/usr/local/include -I/usr/local/include -I/usr/local/include -I/usr/local/include -I/usr/include/harfbuzz -I/usr/include/freetype2 -I/usr/include/libpng16 -I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include -I/usr/include/sysprof-6 -pthread " LTCFLAGS="-g -O2 -I/usr/local/include -I/usr/local/include -I/usr/local/include -I/usr/include/harfbuzz -I/usr/include/freetype2 -I/usr/include/libpng16 -I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include -I/usr/include/sysprof-6 -pthread -I/usr/include/gdk-pixbuf-2.0 -I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include -I/usr/include/sysprof-6 -I/usr/include/libpng16 -I/usr/include/x86_64-linux-gnu -I/usr/include/webp -I/usr/include/libmount -I/usr/include/blkid -pthread "
# A language-specific compiler. # A language-specific compiler.
CC="cc" CC="cc"

View File

@ -14,7 +14,7 @@ library_names='sdl3.so sdl3.so sdl3.so'
old_library='' old_library=''
# Libraries that this one depends upon. # Libraries that this one depends upon.
dependency_libs=' -L/usr/local/lib -lSDL3_gfx -lSDL3_image -lSDL3_ttf -lSDL3' dependency_libs=' -L/usr/local/lib -lSDL3_image -lSDL3_ttf -lSDL3 -lharfbuzz -latomic -lsysprof-capture-4 -lpcre2-8 -lgraphite2 -lfreetype -lbz2 -lpng16 -lm -lz -lbrotlidec -lbrotlicommon -lnotify -lgdk_pixbuf-2.0 -lgio-2.0 -lgobject-2.0 -lglib-2.0'
# Version information for sdl3. # Version information for sdl3.
current=0 current=0

Binary file not shown.

View File

@ -10,8 +10,15 @@
#include "sdl3_ttf.h" #include "sdl3_ttf.h"
#include "sdl3_events.h" #include "sdl3_events.h"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <SDL3_gfx/SDL3_gfxPrimitives.h>
#include <math.h> #include <math.h>
#include <string.h>
#ifdef HAVE_LIBNOTIFY
#include <libnotify/notify.h>
#endif
// SDL3 native tray support
#include <SDL3/SDL_tray.h>
// Resource handles (nicht static, damit sie in anderen Modulen verfügbar sind) // Resource handles (nicht static, damit sie in anderen Modulen verfügbar sind)
int le_sdl_window; int le_sdl_window;
@ -42,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(&params[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) { PHP_MINIT_FUNCTION(sdl3) {
le_sdl_window = zend_register_list_destructors_ex(sdl_window_dtor, NULL, "SDL_Window", module_number); 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); le_sdl_renderer = zend_register_list_destructors_ex(sdl_renderer_dtor, NULL, "SDL_Renderer", module_number);
@ -165,8 +230,10 @@ PHP_FUNCTION(sdl_get_window_id) {
PHP_FUNCTION(sdl_create_renderer) { PHP_FUNCTION(sdl_create_renderer) {
zval *win_res; zval *win_res;
SDL_Window *win; 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(); RETURN_THROWS();
} }
@ -175,13 +242,32 @@ PHP_FUNCTION(sdl_create_renderer) {
RETURN_FALSE; RETURN_FALSE;
} }
SDL_Renderer *ren = SDL_CreateRenderer(win, NULL); SDL_Renderer *ren = SDL_CreateRenderer(win, renderer_name_len > 0 ? renderer_name : NULL);
if (!ren) { if (!ren) {
RETURN_FALSE; RETURN_FALSE;
} }
RETURN_RES(zend_register_resource(ren, le_sdl_renderer)); 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) { PHP_FUNCTION(sdl_set_render_draw_color) {
zval *ren_res; zval *ren_res;
SDL_Renderer *ren; SDL_Renderer *ren;
@ -526,6 +612,420 @@ PHP_FUNCTION(sdl_set_texture_alpha_mod) {
RETURN_TRUE; 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;
size_t title_len, body_len;
zval *options = NULL;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "ss|a", &title, &title_len, &body, &body_len, &options) == FAILURE) {
RETURN_THROWS();
}
#ifdef HAVE_LIBNOTIFY
if (!notify_is_initted()) {
if (!notify_init("PHPNative")) {
php_error_docref(NULL, E_WARNING, "Failed to initialize libnotify");
RETURN_FALSE;
}
}
NotifyNotification *n = notify_notification_new(title, body, NULL);
if (!n) {
php_error_docref(NULL, E_WARNING, "Failed to create notification");
RETURN_FALSE;
}
if (options && Z_TYPE_P(options) == IS_ARRAY) {
zval *timeout = zend_hash_str_find(Z_ARRVAL_P(options), "timeout", sizeof("timeout") - 1);
if (timeout && Z_TYPE_P(timeout) == IS_LONG) {
notify_notification_set_timeout(n, (int) Z_LVAL_P(timeout));
}
zval *urgency = zend_hash_str_find(Z_ARRVAL_P(options), "urgency", sizeof("urgency") - 1);
if (urgency && Z_TYPE_P(urgency) == IS_STRING) {
const char *u = Z_STRVAL_P(urgency);
if (strcmp(u, "low") == 0) {
notify_notification_set_urgency(n, NOTIFY_URGENCY_LOW);
} else if (strcmp(u, "critical") == 0) {
notify_notification_set_urgency(n, NOTIFY_URGENCY_CRITICAL);
} else {
notify_notification_set_urgency(n, NOTIFY_URGENCY_NORMAL);
}
}
}
GError *error = NULL;
gboolean res = notify_notification_show(n, &error);
if (!res) {
if (error) {
php_error_docref(NULL, E_WARNING, "Notification error: %s", error->message);
g_error_free(error);
}
g_object_unref(G_OBJECT(n));
RETURN_FALSE;
}
g_object_unref(G_OBJECT(n));
RETURN_TRUE;
#else
php_error_docref(NULL, E_WARNING, "desktop_notify() not available (libnotify not found at build time)");
RETURN_FALSE;
#endif
}
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) { PHP_FUNCTION(sdl_get_render_target) {
zval *ren_res; zval *ren_res;
SDL_Renderer *renderer; SDL_Renderer *renderer;
@ -596,10 +1096,46 @@ PHP_FUNCTION(sdl_rounded_box)
RETURN_FALSE; RETURN_FALSE;
} }
if (roundedBoxRGBA(ren, (Sint16)x1, (Sint16)y1, (Sint16)x2, (Sint16)y2, (Sint16)rad, (Uint8)r, (Uint8)g, (Uint8)b, (Uint8)a) == 0) { // Zeichne eine Box mit gleichen Radien an allen Ecken (Wrapper um die erweiterte Variante)
SDL_SetRenderDrawColor(ren, (Uint8)r, (Uint8)g, (Uint8)b, (Uint8)a);
int halfw = ((Sint16)x2 - (Sint16)x1) / 2;
int halfh = ((Sint16)y2 - (Sint16)y1) / 2;
int rad_tl = (int)rad;
int rad_tr = (int)rad;
int rad_br = (int)rad;
int rad_bl = (int)rad;
if (rad_tl > halfw) rad_tl = halfw; if (rad_tl > halfh) rad_tl = halfh;
if (rad_tr > halfw) rad_tr = halfw; if (rad_tr > halfh) rad_tr = halfh;
if (rad_br > halfw) rad_br = halfw; if (rad_br > halfh) rad_br = halfh;
if (rad_bl > halfw) rad_bl = halfw; if (rad_bl > halfh) rad_bl = halfh;
SDL_FRect topRect = { x1 + rad_tl, y1, x2 - x1 - rad_tl - rad_tr, rad_tl > rad_tr ? rad_tl : rad_tr };
if (topRect.w > 0 && topRect.h > 0) SDL_RenderFillRect(ren, &topRect);
int maxBottomRad = rad_bl > rad_br ? rad_bl : rad_br;
SDL_FRect bottomRect = { x1 + rad_bl, y2 - maxBottomRad, x2 - x1 - rad_bl - rad_br, maxBottomRad };
if (bottomRect.w > 0 && bottomRect.h > 0) SDL_RenderFillRect(ren, &bottomRect);
SDL_FRect leftRect = { x1, y1 + rad_tl, rad_tl > rad_bl ? rad_tl : rad_bl, y2 - y1 - rad_tl - rad_bl };
if (leftRect.w > 0 && leftRect.h > 0) SDL_RenderFillRect(ren, &leftRect);
int maxRightRad = rad_tr > rad_br ? rad_tr : rad_br;
SDL_FRect rightRect = { x2 - maxRightRad, y1 + rad_tr, maxRightRad, y2 - y1 - rad_tr - rad_br };
if (rightRect.w > 0 && rightRect.h > 0) SDL_RenderFillRect(ren, &rightRect);
int maxLeftRad = rad_tl > rad_bl ? rad_tl : rad_bl;
maxRightRad = rad_tr > rad_br ? rad_tr : rad_br;
SDL_FRect centerRect = { x1 + maxLeftRad, y1, x2 - x1 - maxLeftRad - maxRightRad, y2 - y1 };
if (centerRect.w > 0 && centerRect.h > 0) SDL_RenderFillRect(ren, &centerRect);
if (rad_tl > 0) filled_quarter_circle(ren, x1 + rad_tl, y1 + rad_tl, rad_tl, 0);
if (rad_tr > 0) filled_quarter_circle(ren, x2 - rad_tr - 1, y1 + rad_tr, rad_tr, 1);
if (rad_br > 0) filled_quarter_circle(ren, x2 - rad_br - 1, y2 - rad_br - 1, rad_br, 2);
if (rad_bl > 0) filled_quarter_circle(ren, x1 + rad_bl, y2 - rad_bl - 1, rad_bl, 3);
RETURN_TRUE; RETURN_TRUE;
}
RETURN_FALSE;
} }
#include <math.h> #include <math.h>
@ -857,6 +1393,25 @@ PHP_FUNCTION(sdl_get_current_video_driver) {
RETURN_STRING(drv); 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) { PHP_FUNCTION(sdl_start_text_input) {
zval *win_res; zval *win_res;
SDL_Window *win; SDL_Window *win;
@ -919,6 +1474,14 @@ ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_create_renderer, 0, 0, 1) ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_create_renderer, 0, 0, 1)
ZEND_ARG_INFO(0, window) 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_END_ARG_INFO()
ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_set_render_draw_color, 0, 0, 5) ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_set_render_draw_color, 0, 0, 5)
@ -994,6 +1557,34 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_set_texture_alpha_mod, 0, 0, 2)
ZEND_ARG_INFO(0, alpha) ZEND_ARG_INFO(0, alpha)
ZEND_END_ARG_INFO() 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)
ZEND_ARG_ARRAY_INFO(0, options, 0)
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_BEGIN_ARG_INFO_EX(arginfo_sdl_get_render_target, 0, 0, 1)
ZEND_ARG_INFO(0, renderer) ZEND_ARG_INFO(0, renderer)
ZEND_END_ARG_INFO() ZEND_END_ARG_INFO()
@ -1065,6 +1656,14 @@ ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_get_current_video_driver, 0, 0, 0) ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_get_current_video_driver, 0, 0, 0)
ZEND_END_ARG_INFO() 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_BEGIN_ARG_INFO_EX(arginfo_sdl_get_renderer_output_size, 0, 0, 1)
ZEND_ARG_INFO(0, renderer) ZEND_ARG_INFO(0, renderer)
ZEND_END_ARG_INFO() ZEND_END_ARG_INFO()
@ -1086,6 +1685,8 @@ const zend_function_entry sdl3_functions[] = {
PHP_FE(sdl_destroy_renderer, arginfo_sdl_destroy_renderer) PHP_FE(sdl_destroy_renderer, arginfo_sdl_destroy_renderer)
PHP_FE(sdl_get_window_id, arginfo_sdl_get_window_id) PHP_FE(sdl_get_window_id, arginfo_sdl_get_window_id)
PHP_FE(sdl_create_renderer, arginfo_sdl_create_renderer) 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_set_render_draw_color, arginfo_sdl_set_render_draw_color)
PHP_FE(sdl_render_clear, arginfo_sdl_render_clear) PHP_FE(sdl_render_clear, arginfo_sdl_render_clear)
PHP_FE(sdl_render_fill_rect, arginfo_sdl_render_fill_rect) PHP_FE(sdl_render_fill_rect, arginfo_sdl_render_fill_rect)
@ -1101,6 +1702,13 @@ const zend_function_entry sdl3_functions[] = {
PHP_FE(sdl_update_texture, arginfo_sdl_update_texture) 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_blend_mode, arginfo_sdl_set_texture_blend_mode)
PHP_FE(sdl_set_texture_alpha_mod, arginfo_sdl_set_texture_alpha_mod) 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) 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_set_render_target, arginfo_sdl_set_render_target)
PHP_FE(sdl_rounded_box, arginfo_sdl_rounded_box) PHP_FE(sdl_rounded_box, arginfo_sdl_rounded_box)
@ -1112,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_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_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_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_get_renderer_output_size, arginfo_sdl_get_renderer_output_size)
PHP_FE(sdl_start_text_input, arginfo_sdl_start_text_input) PHP_FE(sdl_start_text_input, arginfo_sdl_start_text_input)
PHP_FE(sdl_stop_text_input, arginfo_sdl_stop_text_input) PHP_FE(sdl_stop_text_input, arginfo_sdl_stop_text_input)

View File

@ -14,7 +14,7 @@ library_names='sdl3.so sdl3.so sdl3.so'
old_library='' old_library=''
# Libraries that this one depends upon. # Libraries that this one depends upon.
dependency_libs=' -L/usr/local/lib -lSDL3_gfx -lSDL3_image -lSDL3_ttf -lSDL3' dependency_libs=' -L/usr/local/lib -lSDL3_image -lSDL3_ttf -lSDL3 -lharfbuzz -latomic -lsysprof-capture-4 -lpcre2-8 -lgraphite2 -lfreetype -lbz2 -lpng16 -lm -lz -lbrotlidec -lbrotlicommon -lnotify -lgdk_pixbuf-2.0 -lgio-2.0 -lgobject-2.0 -lglib-2.0'
# Version information for sdl3. # Version information for sdl3.
current=0 current=0

View File

@ -86,6 +86,7 @@ class Application
while ($this->running && count($this->windows) > 0) { while ($this->running && count($this->windows) > 0) {
$frameStart = microtime(true); $frameStart = microtime(true);
// Layout all windows FIRST (sets window references and calculates positions) // Layout all windows FIRST (sets window references and calculates positions)
foreach ($this->windows as $windowId => $window) { foreach ($this->windows as $windowId => $window) {
$window->layout(); $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 // Coalesce mouse motion events: Only keep the last MouseMotion event per window
// This dramatically reduces the number of events to process // This dramatically reduces the number of events to process
$coalescedEvents = []; $coalescedEvents = [];

View File

@ -8,6 +8,8 @@ namespace PHPNative\Framework;
class TextRenderer class TextRenderer
{ {
private const MAX_TEXTURE_SIZE = 16000;
private $renderer; private $renderer;
private bool $initialized = false; private bool $initialized = false;
private string $fontPath = ''; private string $fontPath = '';
@ -103,6 +105,58 @@ class TextRenderer
return $this->loadFont($size); return $this->loadFont($size);
} }
/**
* Ensure that rendered text does not exceed the maximum supported
* texture size by truncating very long strings.
*/
private function truncateToMaxTextureSize(string $text, $font): string
{
if ($text === '') {
return $text;
}
$dimensions = ttf_size_text($font, $text);
$width = (int) ($dimensions['w'] ?? 0);
$height = (int) ($dimensions['h'] ?? 0);
if ($width <= self::MAX_TEXTURE_SIZE && $height <= self::MAX_TEXTURE_SIZE) {
return $text;
}
$length = \function_exists('mb_strlen') ? mb_strlen($text) : strlen($text);
if ($length <= 1) {
return $text;
}
// Estimate how much we can keep based on width ratio
$scale = self::MAX_TEXTURE_SIZE / max($width, 1);
$targetLength = max(1, (int) floor($length * $scale));
$substr = \function_exists('mb_substr') ? 'mb_substr' : 'substr';
$text = $substr($text, 0, $targetLength);
// Safety: if still too large, iteratively shrink
for ($i = 0; $i < 3; $i++) {
$dimensions = ttf_size_text($font, $text);
$width = (int) ($dimensions['w'] ?? 0);
$height = (int) ($dimensions['h'] ?? 0);
if ($width <= self::MAX_TEXTURE_SIZE && $height <= self::MAX_TEXTURE_SIZE) {
break;
}
$length = \function_exists('mb_strlen') ? mb_strlen($text) : strlen($text);
if ($length <= 1) {
break;
}
$targetLength = max(1, (int) floor($length * 0.7));
$text = $substr($text, 0, $targetLength);
}
return $text;
}
private function getScaledFontSize(int $size): int private function getScaledFontSize(int $size): int
{ {
if ($this->pixelRatio <= 1.0) { if ($this->pixelRatio <= 1.0) {
@ -178,7 +232,14 @@ class TextRenderer
if (strlen($text) < 1) { if (strlen($text) < 1) {
return; return;
} }
$surface = ttf_render_text_blended($renderFont, $text, $r, $g, $b); // Truncate extremely long text so that the resulting texture
// stays within the GPU's max texture size.
$safeText = $this->truncateToMaxTextureSize($text, $renderFont);
if ($safeText === '') {
return;
}
$surface = ttf_render_text_blended($renderFont, $safeText, $r, $g, $b);
if (!$surface) { if (!$surface) {
return; return;
} }
@ -190,7 +251,7 @@ class TextRenderer
} }
// Get text size // Get text size
$textSize = ttf_size_text($baseFont, $text); $textSize = ttf_size_text($baseFont, $safeText);
// Render texture // Render texture
sdl_render_texture($this->renderer, $texture, [ sdl_render_texture($this->renderer, $texture, [
@ -230,7 +291,14 @@ class TextRenderer
if (strlen($text) < 1) { if (strlen($text) < 1) {
return null; return null;
} }
$surface = ttf_render_text_blended($renderFont, $text, $r, $g, $b); // Truncate extremely long text so that the resulting texture
// stays within the GPU's max texture size.
$safeText = $this->truncateToMaxTextureSize($text, $renderFont);
if ($safeText === '') {
return null;
}
$surface = ttf_render_text_blended($renderFont, $safeText, $r, $g, $b);
if (!$surface) { if (!$surface) {
return null; return null;
} }
@ -240,7 +308,7 @@ class TextRenderer
return null; return null;
} }
$dimensions = ttf_size_text($baseFont, $text); $dimensions = ttf_size_text($baseFont, $safeText);
sdl_set_texture_blend_mode($texture, SDL_BLENDMODE_BLEND); sdl_set_texture_blend_mode($texture, SDL_BLENDMODE_BLEND);
if (\function_exists('sdl_set_texture_alpha_mod')) { if (\function_exists('sdl_set_texture_alpha_mod')) {

View File

@ -36,20 +36,29 @@ class StyleCollection extends TypedCollection
$tmp = []; $tmp = [];
foreach($styles as $style) { foreach($styles as $style) {
if(isset($tmp[$style->style::class]) && $style->style::class === Padding::class) { $className = $style->style::class;
\PHPNative\Tailwind\Parser\Padding::merge($tmp[$style->style::class], $style->style);
}elseif(isset($tmp[$style->style::class]) && $style->style::class === Margin::class) { // Ensure we always work on cloned styles in the merged result
\PHPNative\Tailwind\Parser\Margin::merge($tmp[$style->style::class], $style->style); if (!isset($tmp[$className])) {
}elseif(isset($tmp[$style->style::class]) && $style->style::class === Border::class) { $tmp[$className] = clone $style->style;
\PHPNative\Tailwind\Parser\Border::merge($tmp[$style->style::class], $style->style); continue;
}elseif(isset($tmp[$style->style::class]) && $style->style::class === Text::class) { }
\PHPNative\Tailwind\Parser\Text::merge($tmp[$style->style::class], $style->style);
}elseif(isset($tmp[$style->style::class]) && $style->style::class === Flex::class) { if ($className === Padding::class) {
\PHPNative\Tailwind\Parser\Flex::merge($tmp[$style->style::class], $style->style); \PHPNative\Tailwind\Parser\Padding::merge($tmp[$className], $style->style);
}elseif(isset($tmp[$style->style::class]) && $style->style::class === \PHPNative\Tailwind\Style\Shadow::class) { } elseif ($className === Margin::class) {
\PHPNative\Tailwind\Parser\Shadow::merge($tmp[$style->style::class], $style->style); \PHPNative\Tailwind\Parser\Margin::merge($tmp[$className], $style->style);
}else{ } elseif ($className === Border::class) {
$tmp[$style->style::class] = $style->style; \PHPNative\Tailwind\Parser\Border::merge($tmp[$className], $style->style);
} elseif ($className === Text::class) {
\PHPNative\Tailwind\Parser\Text::merge($tmp[$className], $style->style);
} elseif ($className === Flex::class) {
\PHPNative\Tailwind\Parser\Flex::merge($tmp[$className], $style->style);
} elseif ($className === \PHPNative\Tailwind\Style\Shadow::class) {
\PHPNative\Tailwind\Parser\Shadow::merge($tmp[$className], $style->style);
} else {
// Default: overwrite with cloned instance
$tmp[$className] = clone $style->style;
} }
} }

View File

@ -10,7 +10,9 @@ class Background implements Parser
{ {
$color = new \PHPNative\Tailwind\Style\Color(); $color = new \PHPNative\Tailwind\Style\Color();
preg_match_all('/bg-(.*)/', $style, $output_array); // Nur den ersten bg-* Token ohne nachfolgende Klassen extrahieren
// Beispiel: "bg-lime-300 border ..." -> "lime-300"
preg_match_all('/bg-([^\s]+)/', $style, $output_array);
if (count($output_array[0]) > 0) { if (count($output_array[0]) > 0) {
$colorStyle = $output_array[1][0]; $colorStyle = $output_array[1][0];

View File

@ -10,6 +10,7 @@ class Border implements Parser
{ {
$color = new \PHPNative\Tailwind\Style\Color(); $color = new \PHPNative\Tailwind\Style\Color();
// Rounded per side: rounded-t-sm / rounded-b-lg / rounded-l-md / rounded-r-xl
preg_match_all('/rounded-(t|b|l|r)-(none|sm|md|lg|xl|2xl|3xl)/', $style, $output_array); preg_match_all('/rounded-(t|b|l|r)-(none|sm|md|lg|xl|2xl|3xl)/', $style, $output_array);
if (count($output_array[0]) > 0) { if (count($output_array[0]) > 0) {
$size = match ((string) $output_array[2][0]) { $size = match ((string) $output_array[2][0]) {
@ -41,6 +42,7 @@ class Border implements Parser
}; };
} }
// Rounded all corners: rounded-sm / rounded-lg / ...
preg_match_all('/rounded-(none|sm|md|lg|xl|2xl|3xl)/', $style, $output_array); preg_match_all('/rounded-(none|sm|md|lg|xl|2xl|3xl)/', $style, $output_array);
if (count($output_array[0]) > 0) { if (count($output_array[0]) > 0) {
$size = match ((string) $output_array[1][0]) { $size = match ((string) $output_array[1][0]) {
@ -61,7 +63,8 @@ class Border implements Parser
); );
} }
preg_match_all('/rounded/', $style, $output_array); // Generic rounded (no size) -> small radius
preg_match_all('/(?<![a-z-])rounded(?![a-z-])/', $style, $output_array);
if (count($output_array[0]) > 0) { if (count($output_array[0]) > 0) {
$size = 4; $size = 4;
@ -73,24 +76,41 @@ class Border implements Parser
); );
} }
preg_match_all('/border-([tblr])-(.*)/', $style, $output_array); // Directional borders with explicit numeric width, e.g. border-t-2
if (count($output_array[0]) > 0) { if (preg_match('/^border-(t|b|l|r)-(\d+)$/', $style, $m)) {
return match ((string) $output_array[1][0]) { $w = (int) $m[2];
't' => new \PHPNative\Tailwind\Style\Border(true, top: (int) $output_array[2][0]), return match ($m[1]) {
'b' => new \PHPNative\Tailwind\Style\Border(true, bottom: (int) $output_array[2][0]), 't' => new \PHPNative\Tailwind\Style\Border(true, top: $w),
'r' => new \PHPNative\Tailwind\Style\Border(true, right: (int) $output_array[2][0]), 'b' => new \PHPNative\Tailwind\Style\Border(true, bottom: $w),
'l' => new \PHPNative\Tailwind\Style\Border(true, left: (int) $output_array[2][0]), 'r' => new \PHPNative\Tailwind\Style\Border(true, right: $w),
'l' => new \PHPNative\Tailwind\Style\Border(true, left: $w),
}; };
} }
preg_match_all('/border-(.*)/', $style, $output_array); // Directional borders without width -> default 1px, e.g. border-r
if (count($output_array[0]) > 0) { if (preg_match('/^border-(t|b|l|r)$/', $style, $m)) {
$colorStyle = $output_array[1][0]; $w = 1;
$color = Color::parse($colorStyle); return match ($m[1]) {
return new \PHPNative\Tailwind\Style\Border(false, $color); 't' => new \PHPNative\Tailwind\Style\Border(true, top: $w),
'b' => new \PHPNative\Tailwind\Style\Border(true, bottom: $w),
'r' => new \PHPNative\Tailwind\Style\Border(true, right: $w),
'l' => new \PHPNative\Tailwind\Style\Border(true, left: $w),
};
} }
preg_match_all('/border/', $style, $output_array); // Color-only border: border-red-500 / border-lime-300
if (preg_match('/^border-(.+)$/', $style, $output_array)) {
$colorStyle = $output_array[1];
$color = Color::parse($colorStyle);
// Nur Farbe setzen, keine Breiten Breiten kommen von "border" oder "border-r" etc.
return new \PHPNative\Tailwind\Style\Border(
enabled: false,
color: $color,
);
}
// Plain "border" -> 1px Rahmen rundherum, Standardfarbe
preg_match_all('/(?<![a-z-])border(?![a-z-])/', $style, $output_array);
if (count($output_array[0]) > 0) { if (count($output_array[0]) > 0) {
return new \PHPNative\Tailwind\Style\Border( return new \PHPNative\Tailwind\Style\Border(
enabled: true, enabled: true,
@ -111,41 +131,41 @@ class Border implements Parser
if ($style2->enabled && !$style1->enabled) { if ($style2->enabled && !$style1->enabled) {
$style1->enabled = true; $style1->enabled = true;
} }
if ($style2->color->red != null) {
// Farbe nur übernehmen, wenn im zweiten Style wirklich gesetzt ist
if ($style2->color->isNotSet()) {
$style1->color->red = $style2->color->red; $style1->color->red = $style2->color->red;
}
if ($style2->color->green != null) {
$style1->color->green = $style2->color->green; $style1->color->green = $style2->color->green;
}
if ($style2->color->blue != null) {
$style1->color->blue = $style2->color->blue; $style1->color->blue = $style2->color->blue;
} }
if ($style2->color->alpha != null) { if ($style2->color->alpha !== null) {
$style1->color->alpha = $style2->color->alpha; $style1->color->alpha = $style2->color->alpha;
} }
if ($style2->top != null) {
if ($style2->top !== null) {
$style1->top = $style2->top; $style1->top = $style2->top;
} }
if ($style2->bottom != null) { if ($style2->bottom !== null) {
$style1->bottom = $style2->bottom; $style1->bottom = $style2->bottom;
} }
if ($style2->left != null) { if ($style2->left !== null) {
$style1->left = $style2->left; $style1->left = $style2->left;
} }
if ($style2->right != null) { if ($style2->right !== null) {
$style1->right = $style2->right; $style1->right = $style2->right;
} }
if ($style2->roundTopLeft != null) { if ($style2->roundTopLeft !== null) {
$style1->roundTopLeft = $style2->roundTopLeft; $style1->roundTopLeft = $style2->roundTopLeft;
} }
if ($style2->roundTopRight != null) { if ($style2->roundTopRight !== null) {
$style1->roundTopRight = $style2->roundTopRight; $style1->roundTopRight = $style2->roundTopRight;
} }
if ($style2->roundBottomLeft != null) { if ($style2->roundBottomLeft !== null) {
$style1->roundBottomLeft = $style2->roundBottomLeft; $style1->roundBottomLeft = $style2->roundBottomLeft;
} }
if ($style2->roundBottomRight != null) { if ($style2->roundBottomRight !== null) {
$style1->roundBottomRight = $style2->roundBottomRight; $style1->roundBottomRight = $style2->roundBottomRight;
} }
} }
} }

View File

@ -11,4 +11,9 @@ class Shadow implements Style
public Color $color = new Color(), public Color $color = new Color(),
public ?int $opacity = null, // 0-100, null means use default public ?int $opacity = null, // 0-100, null means use default
) {} ) {}
public function __clone()
{
$this->color = clone $this->color;
}
} }

View File

@ -37,8 +37,10 @@ abstract class Component
protected bool $useTextureCache = false; protected bool $useTextureCache = false;
protected bool $textureCacheValid = false; protected bool $textureCacheValid = false;
protected $cachedShadowTexture = null; // Cached shadow texture protected $cachedShadowTexture = null; // Cached shadow texture (normal state)
protected $cachedShadowHoverTexture = null; // Cached shadow texture (hover state)
protected bool $shadowCacheValid = false; protected bool $shadowCacheValid = false;
protected bool $shadowHoverCacheValid = false;
protected Viewport $viewport; protected Viewport $viewport;
@ -191,8 +193,13 @@ abstract class Component
sdl_destroy_texture($this->cachedShadowTexture); sdl_destroy_texture($this->cachedShadowTexture);
$this->cachedShadowTexture = null; $this->cachedShadowTexture = null;
} }
if ($this->cachedShadowHoverTexture !== null) {
sdl_destroy_texture($this->cachedShadowHoverTexture);
$this->cachedShadowHoverTexture = null;
}
$this->textureCacheValid = false; $this->textureCacheValid = false;
$this->shadowCacheValid = false; $this->shadowCacheValid = false;
$this->shadowHoverCacheValid = false;
$this->renderDirty = true; $this->renderDirty = true;
} }
@ -471,6 +478,75 @@ abstract class Component
]); ]);
} }
} }
// Optional border stroke (separate von der Hintergrundfüllung)
if (isset($this->computedStyles[\PHPNative\Tailwind\Style\Border::class])) {
/** @var \PHPNative\Tailwind\Style\Border $border */
$border = $this->computedStyles[\PHPNative\Tailwind\Style\Border::class];
$top = (int) ($border->top ?? 0);
$bottom = (int) ($border->bottom ?? 0);
$left = (int) ($border->left ?? 0);
$right = (int) ($border->right ?? 0);
$hasStroke = $border->enabled || $top > 0 || $bottom > 0 || $left > 0 || $right > 0;
if ($hasStroke) {
$color = $border->color;
// Fallback-Farbe, wenn keine explizite gesetzt ist
$red = ($color->red >= 0) ? $color->red : 160;
$green = ($color->green >= 0) ? $color->green : 160;
$blue = ($color->blue >= 0) ? $color->blue : 160;
$alpha = $color->alpha;
sdl_set_render_draw_color($renderer, $red, $green, $blue, $alpha);
$x = (int) $this->viewport->x;
$y = (int) $this->viewport->y;
$w = (int) $this->viewport->width;
$h = (int) $this->viewport->height;
// Obere Kante
if ($top > 0) {
sdl_render_fill_rect($renderer, [
'x' => $x,
'y' => $y,
'w' => $w,
'h' => $top,
]);
}
// Untere Kante
if ($bottom > 0) {
sdl_render_fill_rect($renderer, [
'x' => $x,
'y' => $y + $h - $bottom,
'w' => $w,
'h' => $bottom,
]);
}
// Linke Kante
if ($left > 0) {
sdl_render_fill_rect($renderer, [
'x' => $x,
'y' => $y,
'w' => $left,
'h' => $h,
]);
}
// Rechte Kante
if ($right > 0) {
sdl_render_fill_rect($renderer, [
'x' => $x + $w - $right,
'y' => $y,
'w' => $right,
'h' => $h,
]);
}
}
}
if (defined('DEBUG_RENDERING') && DEBUG_RENDERING) { if (defined('DEBUG_RENDERING') && DEBUG_RENDERING) {
sdl_set_render_draw_color($renderer, rand(0, 255), rand(0, 255), rand(0, 255), 10); sdl_set_render_draw_color($renderer, rand(0, 255), rand(0, 255), rand(0, 255), 10);
sdl_render_rect($renderer, [ sdl_render_rect($renderer, [
@ -931,6 +1007,22 @@ abstract class Component
return null; 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 // Create alpha map (single channel) - start with transparent
$alphaMap = array_fill(0, $width * $height, 0); $alphaMap = array_fill(0, $width * $height, 0);
@ -1004,11 +1096,15 @@ abstract class Component
$g = $shadow->color->green >= 0 ? $shadow->color->green : 0; $g = $shadow->color->green >= 0 ? $shadow->color->green : 0;
$b = $shadow->color->blue >= 0 ? $shadow->color->blue : 0; $b = $shadow->color->blue >= 0 ? $shadow->color->blue : 0;
// Use cached shadow texture if available and valid // Choose cache based on current state (normal vs hover)
if ($this->shadowCacheValid && $this->cachedShadowTexture !== null) { $isHover = $this->currentState === \PHPNative\Tailwind\Style\StateEnum::hover;
$shadowTexture = $this->cachedShadowTexture; $cacheTexture = $isHover ? $this->cachedShadowHoverTexture : $this->cachedShadowTexture;
$cacheValid = $isHover ? $this->shadowHoverCacheValid : $this->shadowCacheValid;
if ($cacheValid && $cacheTexture !== null) {
$shadowTexture = $cacheTexture;
} else { } else {
// Create shadow texture with blur // Create shadow texture with blur based on current state's styles
$shadowTexture = $this->createShadowTexture( $shadowTexture = $this->createShadowTexture(
$renderer, $renderer,
(int) $this->viewport->width, (int) $this->viewport->width,
@ -1024,10 +1120,15 @@ abstract class Component
return; return;
} }
// Cache the shadow texture // Cache per state
if ($isHover) {
$this->cachedShadowHoverTexture = $shadowTexture;
$this->shadowHoverCacheValid = true;
} else {
$this->cachedShadowTexture = $shadowTexture; $this->cachedShadowTexture = $shadowTexture;
$this->shadowCacheValid = true; $this->shadowCacheValid = true;
} }
}
// Render shadow texture with offset // Render shadow texture with offset
$shadowRect = [ $shadowRect = [

View File

@ -30,7 +30,7 @@ class Container extends Component
private float $scrollStartY = 0; private float $scrollStartY = 0;
// Scrollbar dimensions // Scrollbar dimensions
private const SCROLLBAR_WIDTH = 12; private const SCROLLBAR_WIDTH = 16;
private const SCROLLBAR_MIN_SIZE = 20; private const SCROLLBAR_MIN_SIZE = 20;
public function __construct(string $style = '') public function __construct(string $style = '')

View File

@ -4,18 +4,23 @@ namespace PHPNative\Ui\Widget;
class FileBrowser extends Container class FileBrowser extends Container
{ {
private Table $fileTable; private VirtualListView $fileTable;
private Label $pathLabel; private Label $pathLabel;
private Label $summaryLabel;
private string $currentPath; private string $currentPath;
private $onFileSelect = null; private $onFileSelect = null;
private $onEditFile = null; private $onEditFile = null;
private $onRenameFile = null; private $onRenameFile = null;
private $onDeleteFile = null; private $onDeleteFile = null;
private bool $isRemote = false; private bool $isRemote = false;
private null|string $lastClickPath = null;
private float $lastClickTime = 0.0;
public function __construct(string $initialPath = '.', bool $isRemote = false, string $style = '') public function __construct(string $initialPath = '.', bool $isRemote = false, string $style = '')
{ {
parent::__construct('w-full flex flex-col gap-2 ' . $style); // Root-Container füllt die verfügbare Höhe im Eltern-Layout (flex-1)
// und enthält eine VirtualListView für performantes Scrollen.
parent::__construct('w-full flex flex-col flex-1 gap-2 ' . $style);
$this->currentPath = $initialPath; $this->currentPath = $initialPath;
$this->isRemote = $isRemote; $this->isRemote = $isRemote;
@ -24,8 +29,9 @@ class FileBrowser extends Container
$this->pathLabel = new Label($initialPath, 'px-3 py-2 bg-gray-200 text-black rounded text-sm font-mono'); $this->pathLabel = new Label($initialPath, 'px-3 py-2 bg-gray-200 text-black rounded text-sm font-mono');
$this->addComponent($this->pathLabel); $this->addComponent($this->pathLabel);
// File table with explicit flex-1 for scrolling // VirtualListView rendert nur die sichtbaren Zeilen und
$this->fileTable = new Table(''); // bleibt damit auch bei großen Verzeichnissen flüssig.
$this->fileTable = new VirtualListView(' flex-1');
$this->fileTable->setColumns([ $this->fileTable->setColumns([
['key' => 'type', 'title' => 'Typ', 'width' => 60], ['key' => 'type', 'title' => 'Typ', 'width' => 60],
['key' => 'name', 'title' => 'Name'], ['key' => 'name', 'title' => 'Name'],
@ -36,6 +42,10 @@ class FileBrowser extends Container
$this->addComponent($this->fileTable); $this->addComponent($this->fileTable);
// Summary label: zeigt Anzahl Ordner/Dateien im aktuellen Verzeichnis
$this->summaryLabel = new Label('0 Ordner, 0 Dateien', 'text-xs text-gray-600');
$this->addComponent($this->summaryLabel);
// Load initial directory (only if local) // Load initial directory (only if local)
if (!$isRemote) { if (!$isRemote) {
$this->loadDirectory($initialPath); $this->loadDirectory($initialPath);
@ -102,17 +112,41 @@ class FileBrowser extends Container
} }
$this->fileTable->setData($files); $this->fileTable->setData($files);
$this->updateSummaryFromFiles($files);
// Handle row selection // Handle row selection
$fileBrowser = $this; $fileBrowser = $this;
$this->fileTable->setOnRowSelect(function ($index, $row) use ($fileBrowser) { $this->fileTable->setOnRowSelect(function ($index, $row) use ($fileBrowser) {
if ($row && isset($row['isDir']) && $row['isDir'] && !empty($row['path'])) { if (!$row || empty($row['path'])) {
// Navigate to directory return;
$fileBrowser->loadDirectory($row['path']);
} elseif ($row && isset($row['path']) && !empty($row['path']) && $fileBrowser->onFileSelect !== null) {
// File selected
($fileBrowser->onFileSelect)($row['path'], $row);
} }
$path = $row['path'];
// Immer Auswahl-Callback auslösen (z.B. für Upload)
if ($fileBrowser->onFileSelect !== null) {
($fileBrowser->onFileSelect)($path, $row);
}
// Nur Verzeichnisse navigieren per Doppelklick
if (!($row['isDir'] ?? false)) {
return;
}
$now = microtime(true);
$doubleClickThreshold = 0.4; // Sekunden
if ($fileBrowser->lastClickPath !== $path || ($now - $fileBrowser->lastClickTime) > $doubleClickThreshold) {
// Erster Klick: nur merken
$fileBrowser->lastClickPath = $path;
$fileBrowser->lastClickTime = $now;
return;
}
// Zweiter Klick innerhalb des Zeitfensters -> als Doppelklick werten
$fileBrowser->lastClickPath = null;
$fileBrowser->lastClickTime = 0.0;
$fileBrowser->loadDirectory($path);
}); });
} }
@ -184,6 +218,7 @@ class FileBrowser extends Container
} }
$this->fileTable->setData($files); $this->fileTable->setData($files);
$this->updateSummaryFromFiles($files);
// Handle row selection // Handle row selection
$fileBrowser = $this; $fileBrowser = $this;
@ -250,16 +285,22 @@ class FileBrowser extends Container
public function renderActionsCell(array $rowData, int $rowIndex): Container public function renderActionsCell(array $rowData, int $rowIndex): Container
{ {
// Match the cell style from Table (100px width for icon buttons) // Match the cell style from Table (100px width for icon buttons)
$container = new Container( $container = new Container('w-25 flex flex-row items-center justify-center gap-1');
'w-25 py-1 border-r border-gray-300 flex flex-row items-center justify-center gap-1',
); $isDir = (bool) ($rowData['isDir'] ?? false);
$hasPath = !empty($rowData['path']);
$isParentEntry = ($rowData['name'] ?? '') === '..';
// Skip action buttons for invalid rows or parent navigation
if (!$hasPath || $isParentEntry) {
return $container;
}
// Only show action buttons for files (not directories)
if (!($rowData['isDir'] ?? false) && !empty($rowData['path'])) {
$fileBrowser = $this; $fileBrowser = $this;
// Edit button // Edit button only for files
$editButton = new Button('', 'p-1 text-blue-500 hover:text-blue-600 flex items-center justify-center'); if (!$isDir) {
$editButton = new Button('', 'text-blue-500 hover:text-blue-600 flex items-center justify-center');
$editIcon = new Icon(\PHPNative\Tailwind\Data\Icon::edit, 16, 'text-blue-500'); $editIcon = new Icon(\PHPNative\Tailwind\Data\Icon::edit, 16, 'text-blue-500');
$editButton->setIcon($editIcon); $editButton->setIcon($editIcon);
$editButton->setOnClick(function () use ($fileBrowser, $rowData) { $editButton->setOnClick(function () use ($fileBrowser, $rowData) {
@ -268,9 +309,10 @@ class FileBrowser extends Container
} }
}); });
$container->addComponent($editButton); $container->addComponent($editButton);
}
// Rename button // Rename button for files and directories
$renameButton = new Button('', 'p-1 text-amber-500 hover:text-amber-600 flex items-center justify-center'); $renameButton = new Button('', 'text-amber-500 hover:text-amber-600 flex items-center justify-center');
$renameIcon = new Icon(\PHPNative\Tailwind\Data\Icon::pen, 16, 'text-amber-500'); $renameIcon = new Icon(\PHPNative\Tailwind\Data\Icon::pen, 16, 'text-amber-500');
$renameButton->setIcon($renameIcon); $renameButton->setIcon($renameIcon);
$renameButton->setOnClick(function () use ($fileBrowser, $rowData) { $renameButton->setOnClick(function () use ($fileBrowser, $rowData) {
@ -280,8 +322,9 @@ class FileBrowser extends Container
}); });
$container->addComponent($renameButton); $container->addComponent($renameButton);
// Delete button // Delete button only for files
$deleteButton = new Button('', 'p-1 text-red-500 hover:text-red-600 flex items-center justify-center'); if (!$isDir) {
$deleteButton = new Button('', 'text-red-500 hover:text-red-600 flex items-center justify-center');
$deleteIcon = new Icon(\PHPNative\Tailwind\Data\Icon::trash, 16, 'text-red-500'); $deleteIcon = new Icon(\PHPNative\Tailwind\Data\Icon::trash, 16, 'text-red-500');
$deleteButton->setIcon($deleteIcon); $deleteButton->setIcon($deleteIcon);
$deleteButton->setOnClick(function () use ($fileBrowser, $rowData) { $deleteButton->setOnClick(function () use ($fileBrowser, $rowData) {
@ -349,6 +392,9 @@ class FileBrowser extends Container
$this->fileTable->setData($tableData); $this->fileTable->setData($tableData);
// Update summary from original file list (without '..' Eintrag)
$this->updateSummaryFromFiles($files);
// Set up row selection handler AFTER data is set (for remote browsers) // Set up row selection handler AFTER data is set (for remote browsers)
// This needs to be done every time because setData might reset handlers // This needs to be done every time because setData might reset handlers
$this->setupRemoteNavigationHandler(); $this->setupRemoteNavigationHandler();
@ -378,4 +424,27 @@ class FileBrowser extends Container
} }
}); });
} }
/**
* Update summary label based on list of files (with isDir flag).
*/
private function updateSummaryFromFiles(array $files): void
{
$dirCount = 0;
$fileCount = 0;
foreach ($files as $file) {
if (!isset($file['isDir'])) {
continue;
}
if ($file['isDir']) {
$dirCount++;
} else {
$fileCount++;
}
}
$this->summaryLabel->setText(sprintf('%d Ordner, %d Dateien', $dirCount, $fileCount));
}
} }

View File

@ -0,0 +1,84 @@
<?php
namespace PHPNative\Ui\Widget;
use PHPNative\Framework\TextRenderer;
use PHPNative\Ui\Component;
class ProgressBar extends Component
{
private float $value = 0.0; // 0.0 - 1.0
public function __construct(string $style = '')
{
// Basis: volle Breite, kleine Höhe
$defaultStyle = 'w-full h-[8]';
parent::__construct(trim($defaultStyle . ' ' . $style));
}
public function setValue(float $value): void
{
$clamped = max(0.0, min(1.0, $value));
if ($this->value === $clamped) {
return;
}
$this->value = $clamped;
$this->markDirty(false);
}
public function getValue(): float
{
return $this->value;
}
public function layout(null|TextRenderer $textRenderer = null): void
{
parent::layout($textRenderer);
// Mindesthöhe sicherstellen
if ($this->viewport->height < 4) {
$this->viewport->height = 4;
$this->contentViewport->height = 4;
}
}
public function renderContent(&$renderer, null|TextRenderer $textRenderer = null): void
{
if (!$this->visible) {
return;
}
$x = (int) $this->contentViewport->x;
$y = (int) $this->contentViewport->y;
$w = (int) $this->contentViewport->width;
$h = (int) $this->contentViewport->height;
if ($w <= 0 || $h <= 0) {
return;
}
// Hintergrund (Track)
sdl_set_render_draw_color($renderer, 220, 220, 220, 255);
sdl_render_fill_rect($renderer, [
'x' => $x,
'y' => $y,
'w' => $w,
'h' => $h,
]);
// Fortschrittsbalken
$fillWidth = (int) floor($w * $this->value);
if ($fillWidth <= 0) {
return;
}
sdl_set_render_draw_color($renderer, 59, 130, 246, 255); // Tailwind blue-500
sdl_render_fill_rect($renderer, [
'x' => $x,
'y' => $y,
'w' => $fillWidth,
'h' => $h,
]);
}
}

View File

@ -17,7 +17,10 @@ class Table extends Container
public function __construct(string $style = '') public function __construct(string $style = '')
{ {
parent::__construct('flex flex-col w-full' . $style); // Table selbst ist kein Flex-Container; sie wird als Kind
// in einem flex-Layout benutzt (z.B. flex-1), aber intern
// werden Header und Body ganz normal vertikal gestapelt.
parent::__construct('w-full' . $style);
// Create header container // Create header container
$this->headerContainer = new Container('flex flex-row w-full bg-gray-200 border-b-2 border-gray-400'); $this->headerContainer = new Container('flex flex-row w-full bg-gray-200 border-b-2 border-gray-400');
@ -52,7 +55,7 @@ class Table extends Container
$title = $column['title'] ?? $key; $title = $column['title'] ?? $key;
$width = $column['width'] ?? null; $width = $column['width'] ?? null;
$style = 'px-4 py-2 text-black font-bold border-r border-gray-300 hover:bg-gray-300 cursor-pointer'; $style = 'w-full px-4 py-2 text-black font-bold border-r border-gray-300 hover:bg-gray-300 cursor-pointer';
if ($width) { if ($width) {
$style .= ' w-' . ((int) ($width / 4)); $style .= ' w-' . ((int) ($width / 4));
} else { } else {
@ -123,7 +126,7 @@ class Table extends Container
$value = $rowData[$key] ?? ''; $value = $rowData[$key] ?? '';
$width = $column['width'] ?? null; $width = $column['width'] ?? null;
$cellStyle = 'px-4 py-2 text-black border-r border-gray-300'; $cellStyle = 'w-full px-4 py-2 text-black border-r border-gray-300';
if ($width) { if ($width) {
$cellStyle .= ' w-' . ((int) ($width / 4)); $cellStyle .= ' w-' . ((int) ($width / 4));
} else { } else {

View File

@ -10,6 +10,9 @@ use PHPNative\Tailwind\Style\Text;
class TextArea extends Container class TextArea extends Container
{ {
private const SCROLLBAR_WIDTH = 12;
private const SCROLLBAR_MIN_SIZE = 20;
private array $lines = ['']; private array $lines = [''];
private int $cursorLine = 0; private int $cursorLine = 0;
private int $cursorCol = 0; private int $cursorCol = 0;
@ -23,6 +26,10 @@ class TextArea extends Container
private int $selectionEndLine = -1; private int $selectionEndLine = -1;
private int $selectionEndCol = -1; private int $selectionEndCol = -1;
private bool $isDraggingScrollbar = false;
private float $dragStartY = 0.0;
private int $scrollStartOffsetY = 0;
public function __construct( public function __construct(
public string $value = '', public string $value = '',
public string $placeholder = '', public string $placeholder = '',
@ -173,6 +180,48 @@ class TextArea extends Container
return false; return false;
} }
// Handle click on scrollbar (if present)
$contentHeight = count($this->lines) * $this->lineHeight;
$viewportHeight = $this->contentViewport->height;
if ($contentHeight > $viewportHeight) {
$scrollbarX = ($this->contentViewport->x + $this->contentViewport->width) - self::SCROLLBAR_WIDTH;
$scrollbarY = $this->contentViewport->y;
if (
$mouseX >= $scrollbarX &&
$mouseX <= ($scrollbarX + self::SCROLLBAR_WIDTH) &&
$mouseY >= $scrollbarY &&
$mouseY <= ($scrollbarY + $viewportHeight)
) {
$scrollbarHeight = $viewportHeight;
$thumbHeight = max(
self::SCROLLBAR_MIN_SIZE,
($viewportHeight / $contentHeight) * $scrollbarHeight,
);
$maxScroll = $contentHeight - $viewportHeight;
if ($maxScroll > 0) {
$thumbRange = $scrollbarHeight - $thumbHeight;
// Position scroll to where user clicked (center thumb on click)
$clickPos = $mouseY - $scrollbarY - ($thumbHeight / 2);
$clickPos = max(0, min($thumbRange, $clickPos));
$scrollRatio = $thumbRange > 0 ? ($clickPos / $thumbRange) : 0;
$this->scrollOffsetY = (int) max(0, min($maxScroll, $scrollRatio * $maxScroll));
// Nur neu rendern, kein neues Layout nötig
$this->markDirty(false, false);
// Start drag
$this->isDraggingScrollbar = true;
$this->dragStartY = $mouseY;
$this->scrollStartOffsetY = $this->scrollOffsetY;
}
return true;
}
}
if ( if (
$mouseX >= $this->viewport->x && $mouseX >= $this->viewport->x &&
$mouseX <= ($this->viewport->x + $this->viewport->width) && $mouseX <= ($this->viewport->x + $this->viewport->width) &&
@ -205,6 +254,95 @@ class TextArea extends Container
return false; return false;
} }
public function handleMouseMove(float $mouseX, float $mouseY): void
{
parent::handleMouseMove($mouseX, $mouseY);
if (!$this->visible || !$this->isDraggingScrollbar) {
return;
}
$contentHeight = count($this->lines) * $this->lineHeight;
$viewportHeight = $this->contentViewport->height;
if ($contentHeight <= $viewportHeight) {
return;
}
$scrollbarHeight = $viewportHeight;
$thumbHeight = max(
self::SCROLLBAR_MIN_SIZE,
($viewportHeight / $contentHeight) * $scrollbarHeight,
);
$maxScroll = $contentHeight - $viewportHeight;
$thumbRange = $scrollbarHeight - $thumbHeight;
if ($thumbRange <= 0 || $maxScroll <= 0) {
return;
}
$deltaY = $mouseY - $this->dragStartY;
$scrollRatioDelta = $deltaY / $thumbRange;
$newScroll = $this->scrollStartOffsetY + ($scrollRatioDelta * $maxScroll);
$newOffset = (int) max(0, min($maxScroll, $newScroll));
if ($newOffset !== $this->scrollOffsetY) {
$this->scrollOffsetY = $newOffset;
// Nur neu rendern, kein neues Layout nötig
$this->markDirty(false, false);
}
}
public function handleMouseRelease(float $mouseX, float $mouseY, int $button): void
{
parent::handleMouseRelease($mouseX, $mouseY, $button);
$this->isDraggingScrollbar = false;
}
public function handleMouseWheel(float $mouseX, float $mouseY, float $deltaY): bool
{
if (!$this->visible) {
return false;
}
if (
$mouseX < $this->contentViewport->x ||
$mouseX > ($this->contentViewport->x + $this->contentViewport->width) ||
$mouseY < $this->contentViewport->y ||
$mouseY > ($this->contentViewport->y + $this->contentViewport->height)
) {
return false;
}
$contentHeight = count($this->lines) * $this->lineHeight;
$maxScroll = max(0, $contentHeight - $this->contentViewport->height);
if ($maxScroll <= 0) {
return false;
}
// Schneller scrollen: mehrere Zeilen pro Tick
$scrollStep = max($this->lineHeight * 3, 20);
$oldOffset = $this->scrollOffsetY;
$this->scrollOffsetY = (int) max(
0,
min($maxScroll, $this->scrollOffsetY + ($deltaY * $scrollStep)),
);
if ($this->scrollOffsetY === $oldOffset) {
// Keine tatsächliche Bewegung parent darf evtl. weiter scrollen
return false;
}
// Nur neu rendern, kein neues Layout nötig
$this->markDirty(false, false);
return true;
}
public function layout(null|TextRenderer $textRenderer = null): void public function layout(null|TextRenderer $textRenderer = null): void
{ {
parent::layout($textRenderer); parent::layout($textRenderer);
@ -274,6 +412,42 @@ class TextArea extends Container
$this->renderCursor($window, $textRenderer); $this->renderCursor($window, $textRenderer);
} }
// Draw simple vertical scrollbar if content is higher than viewport
$contentHeight = count($this->lines) * $this->lineHeight;
$viewportHeight = $this->contentViewport->height;
if ($contentHeight > $viewportHeight) {
$scrollbarWidth = self::SCROLLBAR_WIDTH;
$scrollbarX = ($this->contentViewport->x + $this->contentViewport->width) - $scrollbarWidth;
$scrollbarY = $this->contentViewport->y;
// Track
sdl_set_render_draw_color($window, 220, 220, 220, 255);
sdl_render_fill_rect($window, [
'x' => (int) $scrollbarX,
'y' => (int) $scrollbarY,
'w' => (int) $scrollbarWidth,
'h' => (int) $viewportHeight,
]);
// Thumb
$thumbHeight = max(
20,
($viewportHeight / $contentHeight) * $viewportHeight,
);
$maxScroll = $contentHeight - $viewportHeight;
$scrollRatio = $maxScroll > 0 ? ($this->scrollOffsetY / $maxScroll) : 0;
$thumbY = $scrollbarY + ($scrollRatio * ($viewportHeight - $thumbHeight));
sdl_set_render_draw_color($window, 120, 120, 120, 230);
sdl_render_fill_rect($window, [
'x' => (int) ($scrollbarX + 1),
'y' => (int) $thumbY,
'w' => (int) ($scrollbarWidth - 2),
'h' => (int) $thumbHeight,
]);
}
// Update cursor blink // Update cursor blink
$this->cursorBlinkTimer++; $this->cursorBlinkTimer++;
if ($this->cursorBlinkTimer >= 30) { if ($this->cursorBlinkTimer >= 30) {

View File

@ -0,0 +1,347 @@
<?php
namespace PHPNative\Ui\Widget;
use PHPNative\Framework\TextRenderer;
use PHPNative\Ui\Viewport;
class VirtualListView extends Container
{
private array $columns = [];
private array $rows = [];
private float $rowHeight = 0.0;
private float $scrollY = 0.0;
private int $firstVisibleRow = 0;
private int $lastVisibleRow = -1;
private null|int $selectedRowIndex = null;
private $onRowSelect = null;
private const SCROLLBAR_WIDTH = 16;
private const SCROLLBAR_MIN_SIZE = 20;
private const VISIBLE_BUFFER = 5;
public function __construct(string $style = '')
{
parent::__construct('w-full ' . $style);
}
public function setColumns(array $columns): void
{
$this->columns = $columns;
$this->markDirty(true);
}
public function setData(array $rows): void
{
$this->rows = array_values($rows);
$this->scrollY = 0.0;
$this->firstVisibleRow = 0;
$this->lastVisibleRow = -1;
$this->selectedRowIndex = null;
$this->markDirty(true);
}
public function setOnRowSelect(callable $callback): void
{
$this->onRowSelect = $callback;
}
public function getSelectedRow(): null|array
{
if ($this->selectedRowIndex === null) {
return null;
}
return $this->rows[$this->selectedRowIndex] ?? null;
}
public function layout(null|TextRenderer $textRenderer = null): void
{
parent::layout($textRenderer);
if (empty($this->rows) || $textRenderer === null || !$textRenderer->isInitialized()) {
$this->clearChildren();
return;
}
if ($this->rowHeight <= 0) {
$this->rowHeight = $this->measureRowHeight($textRenderer);
}
$maxScroll = $this->getMaxScroll();
$this->scrollY = max(0.0, min($this->scrollY, $maxScroll));
$this->updateVisibleRows($textRenderer);
}
private function measureRowHeight(TextRenderer $textRenderer): float
{
if (empty($this->rows)) {
return 24.0;
}
$testRow = $this->buildRowContainer($this->rows[0], 0);
$viewport = new Viewport(
x: $this->contentViewport->x,
y: $this->contentViewport->y,
width: $this->contentViewport->width,
height: $this->contentViewport->height,
windowWidth: $this->contentViewport->windowWidth,
windowHeight: $this->contentViewport->windowHeight,
uiScale: $this->contentViewport->uiScale,
);
$testRow->setViewport($viewport);
$testRow->setContentViewport(clone $viewport);
$testRow->layout($textRenderer);
$rowViewport = $testRow->getViewport();
$height = max(1.0, (float) $rowViewport->height);
return $height;
}
private function updateVisibleRows(null|TextRenderer $textRenderer): void
{
$this->clearChildren();
if ($this->rowHeight <= 0 || empty($this->rows) || $textRenderer === null) {
return;
}
$viewportHeight = $this->contentViewport->height;
if ($viewportHeight <= 0) {
return;
}
$rowCount = count($this->rows);
$first = (int) floor($this->scrollY / $this->rowHeight);
$visibleCount = ((int) ceil($viewportHeight / $this->rowHeight)) + self::VISIBLE_BUFFER;
$last = min($rowCount - 1, ($first + $visibleCount) - 1);
$this->firstVisibleRow = $first;
$this->lastVisibleRow = $last;
for ($i = $first; $i <= $last; $i++) {
$rowData = $this->rows[$i];
$rowContainer = $this->buildRowContainer($rowData, $i);
$rowY = ($this->contentViewport->y + ($i * $this->rowHeight)) - $this->scrollY;
$rowViewport = new Viewport(
x: $this->contentViewport->x,
y: (int) $rowY,
width: $this->contentViewport->width,
height: $this->rowHeight,
windowWidth: $this->contentViewport->windowWidth,
windowHeight: $this->contentViewport->windowHeight,
uiScale: $this->contentViewport->uiScale,
);
$rowContainer->setViewport($rowViewport);
$rowContainer->setContentViewport(clone $rowViewport);
$rowContainer->layout($textRenderer);
$this->addComponent($rowContainer);
}
}
private function buildRowContainer(array $rowData, int $rowIndex): Container
{
$isSelected = $this->selectedRowIndex !== null && $rowIndex === $this->selectedRowIndex;
$rowStyle = 'flex flex-row border-b border-gray-200 hover:bg-gray-400';
if ($isSelected) {
$rowStyle .= ' bg-blue-100';
}
$rowContainer = new VirtualListRow($this, $rowIndex, $rowStyle);
foreach ($this->columns as $column) {
$key = $column['key'];
$value = $rowData[$key] ?? '';
$width = $column['width'] ?? null;
$cellStyle = 'w-full px-4 py-2 text-black border-r border-gray-300';
if ($width) {
$cellStyle .= ' w-' . ((int) ($width / 4));
} else {
$cellStyle .= ' flex-1';
}
if (isset($column['render']) && is_callable($column['render'])) {
$cellContent = $column['render']($rowData, $rowIndex);
$cellContainer = new Container($cellStyle);
$cellContainer->addComponent($cellContent);
$rowContainer->addComponent($cellContainer);
} else {
$cellLabel = new Label((string) $value, $cellStyle);
$rowContainer->addComponent($cellLabel);
}
}
return $rowContainer;
}
private function getMaxScroll(): float
{
if ($this->rowHeight <= 0) {
return 0.0;
}
$totalHeight = count($this->rows) * $this->rowHeight;
$viewportHeight = $this->contentViewport->height;
return max(0.0, $totalHeight - $viewportHeight);
}
public function handleMouseWheel(float $mouseX, float $mouseY, float $deltaY): bool
{
if (!$this->visible) {
return false;
}
if (
$mouseX < $this->contentViewport->x ||
$mouseX > ($this->contentViewport->x + $this->contentViewport->width) ||
$mouseY < $this->contentViewport->y ||
$mouseY > ($this->contentViewport->y + $this->contentViewport->height)
) {
return parent::handleMouseWheel($mouseX, $mouseY, $deltaY);
}
$maxScroll = $this->getMaxScroll();
if ($maxScroll <= 0.0) {
return false;
}
$scrollStep = max($this->rowHeight * 3, 20.0);
$oldScroll = $this->scrollY;
$this->scrollY = max(0.0, min($maxScroll, $this->scrollY + ($deltaY * $scrollStep)));
if ($this->scrollY === $oldScroll) {
return false;
}
$this->markDirty(true, false);
return true;
}
public function renderContent(&$renderer, null|TextRenderer $textRenderer = null): void
{
if (!$this->visible || $textRenderer === null || !$textRenderer->isInitialized()) {
return;
}
$clipRect = [
'x' => (int) $this->contentViewport->x,
'y' => (int) $this->contentViewport->y,
'w' => (int) $this->contentViewport->width,
'h' => (int) $this->contentViewport->height,
];
sdl_set_render_clip_rect($renderer, $clipRect);
foreach ($this->children as $child) {
$child->render($renderer, $textRenderer);
$child->renderContent($renderer, $textRenderer);
}
sdl_set_render_clip_rect($renderer, null);
$this->renderScrollbar($renderer);
}
private function renderScrollbar(&$renderer): void
{
if ($this->rowHeight <= 0 || empty($this->rows)) {
return;
}
$totalHeight = count($this->rows) * $this->rowHeight;
$viewportHeight = $this->contentViewport->height;
if ($totalHeight <= $viewportHeight) {
return;
}
$scrollbarHeight = $viewportHeight;
$thumbHeight = max(self::SCROLLBAR_MIN_SIZE, ($viewportHeight / $totalHeight) * $scrollbarHeight);
$maxScroll = $this->getMaxScroll();
if ($maxScroll <= 0.0) {
return;
}
$thumbRange = $scrollbarHeight - $thumbHeight;
$scrollRatio = $this->scrollY / $maxScroll;
$thumbY = $this->contentViewport->y + ($scrollRatio * $thumbRange);
$scrollbarX = ($this->contentViewport->x + $this->contentViewport->width) - self::SCROLLBAR_WIDTH;
sdl_set_render_draw_color($renderer, 220, 220, 220, 255);
sdl_render_fill_rect($renderer, [
'x' => (int) $scrollbarX,
'y' => (int) $this->contentViewport->y,
'w' => (int) self::SCROLLBAR_WIDTH,
'h' => (int) $scrollbarHeight,
]);
sdl_set_render_draw_color($renderer, 120, 120, 120, 230);
sdl_render_fill_rect($renderer, [
'x' => (int) ($scrollbarX + 2),
'y' => (int) $thumbY,
'w' => (int) (self::SCROLLBAR_WIDTH - 4),
'h' => (int) $thumbHeight,
]);
}
public function selectRow(int $rowIndex): void
{
if ($rowIndex < 0 || $rowIndex >= count($this->rows)) {
return;
}
$this->selectedRowIndex = $rowIndex;
if ($this->onRowSelect !== null) {
($this->onRowSelect)($rowIndex, $this->rows[$rowIndex]);
}
$this->markDirty(true);
}
}
class VirtualListRow extends Container
{
private int $rowIndex;
private VirtualListView $table;
public function __construct(VirtualListView $table, int $rowIndex, string $style = '')
{
$this->rowIndex = $rowIndex;
$this->table = $table;
parent::__construct($style);
}
public function handleMouseClick(float $mouseX, float $mouseY, int $button): bool
{
$handled = parent::handleMouseClick($mouseX, $mouseY, $button);
if ($handled) {
return true;
}
if (
$mouseX >= $this->viewport->x &&
$mouseX <= ($this->viewport->x + $this->viewport->width) &&
$mouseY >= $this->viewport->y &&
$mouseY <= ($this->viewport->y + $this->viewport->height)
) {
$this->table->selectRow($this->rowIndex);
return true;
}
return false;
}
}

View File

@ -0,0 +1,164 @@
<?php
namespace PHPNative\Ui\Widget;
use PHPNative\Framework\TextRenderer;
class VirtualTable extends Container
{
private Table $innerTable;
private array $rows = [];
private array $columns = [];
private int $pageSize = 200;
private int $currentPage = 0;
private int $totalPages = 1;
private $onRowSelect = null;
private Label $pageInfoLabel;
private Button $prevButton;
private Button $nextButton;
private bool $prevEnabled = false;
private bool $nextEnabled = false;
public function __construct(string $style = '')
{
parent::__construct('flex flex-col w-full' . $style);
$this->innerTable = new Table(' flex-1');
$this->addComponent($this->innerTable);
$pagination = new Container('flex flex-row items-center justify-end gap-2 mt-1');
$this->pageInfoLabel = new Label('', 'text-xs text-gray-600');
$this->prevButton = new Button('←', 'px-2 py-1 text-xs bg-gray-200 rounded hover:bg-gray-300');
$this->nextButton = new Button('→', 'px-2 py-1 text-xs bg-gray-200 rounded hover:bg-gray-300');
$virtualTable = $this;
$this->prevButton->setOnClick(function () use ($virtualTable) {
$virtualTable->goToPreviousPage();
});
$this->nextButton->setOnClick(function () use ($virtualTable) {
$virtualTable->goToNextPage();
});
$pagination->addComponent($this->pageInfoLabel);
$pagination->addComponent($this->prevButton);
$pagination->addComponent($this->nextButton);
$this->addComponent($pagination);
}
public function layout(null|TextRenderer $textRenderer = null): void
{
parent::layout($textRenderer);
$this->updatePaginationLabel();
}
public function setColumns(array $columns): void
{
$this->columns = $columns;
$this->innerTable->setColumns($columns);
}
public function setData(array $data): void
{
$this->rows = array_values($data);
$this->currentPage = 0;
$this->recalculateTotalPages();
$this->updatePageData();
}
public function setOnRowSelect(callable $callback): void
{
$this->onRowSelect = $callback;
$virtualTable = $this;
$this->innerTable->setOnRowSelect(function ($index, $row) use ($virtualTable) {
if ($virtualTable->onRowSelect !== null) {
$globalIndex = ($virtualTable->currentPage * $virtualTable->pageSize) + $index;
($virtualTable->onRowSelect)($globalIndex, $row);
}
});
}
public function getSelectedRow(): null|array
{
$selected = $this->innerTable->getSelectedRow();
if ($selected === null) {
return null;
}
return $selected;
}
private function recalculateTotalPages(): void
{
$rowCount = count($this->rows);
$this->totalPages = max(1, (int) ceil($rowCount / $this->pageSize));
if ($this->currentPage >= $this->totalPages) {
$this->currentPage = $this->totalPages - 1;
}
}
private function updatePageData(): void
{
$offset = $this->currentPage * $this->pageSize;
$pageRows = array_slice($this->rows, $offset, $this->pageSize);
$this->innerTable->setData($pageRows, false);
$this->updatePaginationLabel();
}
private function updatePaginationLabel(): void
{
$totalRows = count($this->rows);
if ($totalRows === 0) {
$this->pageInfoLabel->setText('Keine Einträge');
$this->prevEnabled = false;
$this->nextEnabled = false;
$this->prevButton->setStyle('px-2 py-1 text-xs bg-gray-100 rounded text-gray-400');
$this->nextButton->setStyle('px-2 py-1 text-xs bg-gray-100 rounded text-gray-400');
return;
}
$start = ($this->currentPage * $this->pageSize) + 1;
$end = min($totalRows, ($this->currentPage + 1) * $this->pageSize);
$this->pageInfoLabel->setText(sprintf('Zeige %d%d von %d', $start, $end, $totalRows));
$this->prevEnabled = $this->currentPage > 0;
$this->nextEnabled = ($this->currentPage + 1) < $this->totalPages;
$this->prevButton->setStyle(
$this->prevEnabled
? 'px-2 py-1 text-xs bg-gray-200 rounded hover:bg-gray-300'
: 'px-2 py-1 text-xs bg-gray-100 rounded text-gray-400'
);
$this->nextButton->setStyle(
$this->nextEnabled
? 'px-2 py-1 text-xs bg-gray-200 rounded hover:bg-gray-300'
: 'px-2 py-1 text-xs bg-gray-100 rounded text-gray-400'
);
}
private function goToPreviousPage(): void
{
if ($this->currentPage <= 0 || !$this->prevEnabled) {
return;
}
$this->currentPage--;
$this->updatePageData();
$this->markDirty(true);
}
private function goToNextPage(): void
{
if (($this->currentPage + 1) >= $this->totalPages || !$this->nextEnabled) {
return;
}
$this->currentPage++;
$this->updatePageData();
$this->markDirty(true);
}
}

70
test_render_drivers.php Normal file
View File

@ -0,0 +1,70 @@
<?php
// Test script for SDL_GetNumRenderDrivers and SDL_GetRenderDriver
echo "SDL3 Render Drivers Test\n";
echo "========================\n\n";
// Initialize SDL
if (!sdl_init(SDL_INIT_VIDEO)) {
die('Failed to initialize SDL: ' . sdl_get_error() . "\n");
}
echo "SDL initialized successfully\n\n";
// Get number of render drivers
$numDrivers = \sdl_get_num_render_drivers();
echo "Number of available render drivers: {$numDrivers}\n\n";
// List all available render drivers
echo "Available render drivers:\n";
for ($i = 0; $i < $numDrivers; $i++) {
$driver = sdl_get_render_driver($i);
if ($driver !== false) {
echo " [{$i}] {$driver}\n";
} else {
echo " [{$i}] Error getting driver\n";
}
}
echo "\n";
// Try creating a window and renderer
echo "Creating a window...\n";
$window = sdl_create_window('Render Driver Test', 800, 600, SDL_WINDOW_HIDDEN);
if (!$window) {
die('Failed to create window: ' . sdl_get_error() . "\n");
}
echo "Window created successfully\n\n";
// Try creating a renderer with default (null) name
echo "Attempting to create renderer with default settings...\n";
$renderer = sdl_create_renderer($window);
if (!$renderer) {
echo 'Failed to create renderer: ' . sdl_get_error() . "\n\n";
// Try with explicit driver names
if ($numDrivers > 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";

67
test_renderer_simple.php Normal file
View File

@ -0,0 +1,67 @@
<?php
putenv('XLOCALEDIR=/usr/share/X11/locale');
echo "Simple SDL3 Renderer Test\n";
echo "=========================\n\n";
// Initialize SDL
if (!sdl_init(SDL_INIT_VIDEO)) {
die('Failed to initialize SDL: ' . sdl_get_error() . "\n");
}
echo "SDL initialized successfully\n";
// Get current video driver
$videoDriver = sdl_get_current_video_driver();
echo 'Current video driver: ' . ($videoDriver ?: 'none') . "\n\n";
// Create window with different flags to test
$flags = SDL_WINDOW_HIDDEN;
echo "Creating window with HIDDEN flag...\n";
$window = sdl_create_window('Renderer Test', 800, 600, $flags);
if (!$window) {
die('Failed to create window: ' . sdl_get_error() . "\n");
}
echo "Window created successfully\n\n";
// Get video driver after window creation
$videoDriver = sdl_get_current_video_driver();
echo 'Video driver after window: ' . ($videoDriver ?: 'none') . "\n\n";
// Try to create renderer
echo "Attempting to create renderer...\n";
$renderer = sdl_create_renderer($window);
if (!$renderer) {
$error = sdl_get_error();
echo "FAILED to create renderer: {$error}\n\n";
// Try with a visible window instead
echo "Destroying window and trying with visible window...\n";
sdl_destroy_window($window);
$window = sdl_create_window('Renderer Test', 800, 600, 0);
if (!$window) {
die('Failed to create visible window: ' . sdl_get_error() . "\n");
}
echo "Visible window created\n";
echo "Attempting to create renderer on visible window...\n";
$renderer = sdl_create_renderer($window);
if (!$renderer) {
echo 'FAILED again: ' . sdl_get_error() . "\n";
} else {
echo "SUCCESS on visible window!\n";
sdl_destroy_renderer($renderer);
}
sdl_destroy_window($window);
} else {
echo "SUCCESS! Renderer created\n";
sdl_destroy_renderer($renderer);
sdl_destroy_window($window);
}
sdl_quit();
echo "\nTest completed\n";

76
test_simple_rect.php Normal file
View File

@ -0,0 +1,76 @@
<?php
// SDL initialisieren
if (!sdl_init(SDL_INIT_VIDEO)) {
die("sdl_init failed: " . sdl_get_error() . "\n");
}
echo "SDL initialized successfully\n";
// Fenster erstellen
$window = sdl_create_window(
"Simple Rectangle Test",
800, 600,
SDL_WINDOW_RESIZABLE
);
if (!$window) {
die("sdl_create_window failed: " . sdl_get_error() . "\n");
}
echo "Window created successfully\n";
// Renderer erstellen
$renderer = sdl_create_renderer($window, null);
if (!$renderer) {
sdl_destroy_window($window);
die("sdl_create_renderer failed: " . sdl_get_error() . "\n");
}
echo "Renderer created successfully\n";
// Event Loop
$running = true;
while ($running) {
// Events verarbeiten
while ($event = sdl_poll_event()) {
if ($event['type'] === SDL_EVENT_QUIT) {
$running = false;
} elseif ($event['type'] === SDL_EVENT_KEY_DOWN) {
if ($event['keycode'] === SDLK_ESCAPE || $event['keycode'] === SDLK_Q) {
$running = false;
}
}
}
// Bildschirm löschen (schwarz)
sdl_set_render_draw_color($renderer, 0, 0, 0, 255);
sdl_render_clear($renderer);
// Rotes Rechteck zeichnen (gefüllt)
sdl_set_render_draw_color($renderer, 255, 0, 0, 255);
sdl_render_fill_rect($renderer, ['x' => 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";

63
test_video_drivers.php Normal file
View File

@ -0,0 +1,63 @@
<?php
// Test script for SDL_GetNumVideoDrivers and SDL_GetVideoDriver
echo "SDL3 Video Drivers Test\n";
echo "========================\n\n";
// Initialize SDL
if (!sdl_init(SDL_INIT_VIDEO)) {
die("Failed to initialize SDL: " . sdl_get_error() . "\n");
}
echo "SDL initialized successfully\n\n";
// Get number of video drivers
$numDrivers = sdl_get_num_video_drivers();
echo "Number of available video drivers: $numDrivers\n\n";
// List all available video drivers
echo "Available video drivers:\n";
for ($i = 0; $i < $numDrivers; $i++) {
$driver = sdl_get_video_driver($i);
if ($driver !== false) {
echo " [$i] $driver\n";
} else {
echo " [$i] Error getting driver\n";
}
}
echo "\n";
// Get current video driver
$currentDriver = sdl_get_current_video_driver();
if ($currentDriver !== false) {
echo "Current video driver: $currentDriver\n";
} else {
echo "No video driver initialized yet (window not created)\n";
}
echo "\n";
// Create a window to initialize video driver
echo "Creating a window to initialize video driver...\n";
$window = sdl_create_window("Video Driver Test", 800, 600, SDL_WINDOW_HIDDEN);
if (!$window) {
die("Failed to create window: " . sdl_get_error() . "\n");
}
echo "Window created successfully\n\n";
// Now get the current driver again
$currentDriver = sdl_get_current_video_driver();
if ($currentDriver !== false) {
echo "Current video driver (after window creation): $currentDriver\n";
} else {
echo "Failed to get current video driver\n";
}
// Cleanup
sdl_destroy_window($window);
sdl_quit();
echo "\nTest completed successfully!\n";

35
test_window.php Normal file
View File

@ -0,0 +1,35 @@
<?php
// Fenster und Renderer erstellen
// In SDL3 ist ein Fenster standardmäßig sichtbar, daher verwenden wir 0 oder andere Flags
$window = sdl_create_window('Server Manager', 800, 600, 0);
$renderer = sdl_create_renderer($window, null);
// Event-Loop
$running = true;
while ($running) {
// Events verarbeiten
while ($event = sdl_poll_event()) {
if ($event['type'] === SDL_EVENT_QUIT) {
$running = false;
}
// Weitere Events (Tastatur, Maus, etc.)
}
// Rendern
sdl_set_render_draw_color($renderer, 0, 0, 0, 255);
sdl_render_clear($renderer);
// Dein UI-Code hier
sdl_render_present($renderer);
// Kleine Pause
sdl_delay(16); // ~60 FPS
}
// Cleanup
sdl_destroy_renderer($renderer);
sdl_destroy_window($window);
sdl_quit();