Compare commits
10 Commits
261cc92b19
...
2d8d343011
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d8d343011 | |||
| 81d0f5a429 | |||
| b991570285 | |||
| d4a926ddad | |||
| 3fa5276aba | |||
| bf986acb49 | |||
| e617930ca4 | |||
| cb148dfb7c | |||
| c38bffd4f9 | |||
| c567194b1c |
22
LICENSE
Normal file
22
LICENSE
Normal 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.
|
||||||
|
|
||||||
@ -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);
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
401
examples/ServerManager/UI/KanbanTab.php
Normal file
401
examples/ServerManager/UI/KanbanTab.php
Normal 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
@ -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();
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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';
|
||||||
|
|
||||||
|
|||||||
33
examples/textarea_scroll_test.php
Normal file
33
examples/textarea_scroll_test.php
Normal 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();
|
||||||
@ -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();
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -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.
@ -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 =
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
||||||
|
|
||||||
|
|||||||
@ -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
322
php-sdl3/configure
vendored
@ -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
|
||||||
|
|||||||
@ -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).
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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.
622
php-sdl3/sdl3.c
622
php-sdl3/sdl3.c
@ -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(¶ms[0], idx);
|
||||||
|
|
||||||
|
// Call the PHP callback
|
||||||
|
int result = call_user_function(EG(function_table), NULL, callback, &retval, 1, params);
|
||||||
|
|
||||||
|
if (result == SUCCESS) {
|
||||||
|
zval_ptr_dtor(&retval);
|
||||||
|
} else {
|
||||||
|
php_error_docref(NULL, E_WARNING, "Tray callback %d: call_user_function failed with code %d", (int)idx, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PHP_MINIT_FUNCTION(sdl3) {
|
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, ¢erRect);
|
||||||
|
|
||||||
|
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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 = [];
|
||||||
|
|||||||
@ -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')) {
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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];
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 = [
|
||||||
|
|||||||
@ -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 = '')
|
||||||
|
|||||||
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
84
src/Ui/Widget/ProgressBar.php
Normal file
84
src/Ui/Widget/ProgressBar.php
Normal 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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -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 {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
347
src/Ui/Widget/VirtualListView.php
Normal file
347
src/Ui/Widget/VirtualListView.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
164
src/Ui/Widget/VirtualTable.php
Normal file
164
src/Ui/Widget/VirtualTable.php
Normal 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
70
test_render_drivers.php
Normal 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
67
test_renderer_simple.php
Normal 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
76
test_simple_rect.php
Normal 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
63
test_video_drivers.php
Normal 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
35
test_window.php
Normal 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();
|
||||||
Loading…
Reference in New Issue
Block a user