This commit is contained in:
Thomas Peterson 2025-11-13 22:33:48 +01:00
parent e07c06c7cc
commit 15186e0005
14 changed files with 162 additions and 1127 deletions

View File

@ -1,49 +0,0 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use PHPNative\Framework\Application;
use PHPNative\Ui\Widget\Container;
use PHPNative\Ui\Window;
// Create application
$app = new Application();
$window = new Window('Alpha Blending Test', 400, 400);
// Custom container to test alpha blending
class AlphaTestContainer extends Container
{
public function render(&$renderer, ?\PHPNative\Framework\TextRenderer $textRenderer = null): void
{
parent::render($renderer, $textRenderer);
// Enable alpha blending for renderer
sdl_set_render_draw_blend_mode($renderer, SDL_BLENDMODE_BLEND);
// Draw 3 overlapping semi-transparent rectangles
// Red rectangle - alpha 30
sdl_set_render_draw_color($renderer, 255, 0, 0, 30);
sdl_render_fill_rect($renderer, ['x' => 50, 'y' => 100, 'w' => 100, 'h' => 100]);
// Green rectangle - alpha 30
sdl_set_render_draw_color($renderer, 0, 255, 0, 30);
sdl_render_fill_rect($renderer, ['x' => 100, 'y' => 100, 'w' => 100, 'h' => 100]);
// Blue rectangle - alpha 30
sdl_set_render_draw_color($renderer, 0, 0, 255, 30);
sdl_render_fill_rect($renderer, ['x' => 150, 'y' => 100, 'w' => 100, 'h' => 100]);
// Test: 10 black layers with alpha 3
for ($i = 0; $i < 10; $i++) {
sdl_set_render_draw_color($renderer, 0, 0, 0, 3);
sdl_render_fill_rect($renderer, ['x' => 50, 'y' => 250, 'w' => 200 + $i * 10, 'h' => 100]);
}
}
}
$mainContainer = new AlphaTestContainer('flex items-center justify-center bg-gray-200');
// Set window content and run
$window->setRoot($mainContainer);
$app->addWindow($window);
$app->run();

View File

@ -1,62 +0,0 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use PHPNative\Framework\Application;
use PHPNative\Ui\Widget\Button;
use PHPNative\Ui\Widget\Container;
use PHPNative\Ui\Window;
$app = new Application();
$window = new Window('Gap Test', 600, 400);
// Main container
$mainContainer = new Container('flex flex-col p-4 bg-gray-100 gap-4');
// Test 1: Row with gap-2
$test1 = new Container('flex flex-col gap-2');
$test1->addComponent(new \PHPNative\Ui\Widget\Label('Test 1: flex-row gap-2', 'text-lg font-bold'));
$row1 = new Container('flex flex-row gap-2 bg-white p-4');
$row1->addComponent(new Button('Button 1', 'px-4 py-2 bg-blue-600 text-white rounded'));
$row1->addComponent(new Button('Button 2', 'px-4 py-2 bg-green-600 text-white rounded'));
$test1->addComponent($row1);
$mainContainer->addComponent($test1);
// Test 2: Row with gap-4
$test2 = new Container('flex flex-col gap-2');
$test2->addComponent(new \PHPNative\Ui\Widget\Label('Test 2: flex-row gap-4', 'text-lg font-bold'));
$row2 = new Container('flex flex-row gap-4 bg-white p-4');
$row2->addComponent(new Button('Button A', 'px-4 py-2 bg-red-600 text-white rounded'));
$row2->addComponent(new Button('Button B', 'px-4 py-2 bg-purple-600 text-white rounded'));
$test2->addComponent($row2);
$mainContainer->addComponent($test2);
// Test 3: Row with gap-8
$test3 = new Container('flex flex-col gap-2');
$test3->addComponent(new \PHPNative\Ui\Widget\Label('Test 3: flex-row gap-8', 'text-lg font-bold'));
$row3 = new Container('flex flex-row gap-8 bg-white p-4');
$row3->addComponent(new Button('Btn X', 'px-4 py-2 bg-orange-600 text-white rounded'));
$row3->addComponent(new Button('Btn Y', 'px-4 py-2 bg-pink-600 text-white rounded'));
$test3->addComponent($row3);
$mainContainer->addComponent($test3);
// Test 4: Column with gap-4
$test4 = new Container('flex flex-col gap-2');
$test4->addComponent(new \PHPNative\Ui\Widget\Label('Test 4: flex-col gap-4', 'text-lg font-bold'));
$col = new Container('flex flex-col gap-4 bg-white p-4');
$col->addComponent(new Button('Top Button', 'px-4 py-2 bg-teal-600 text-white rounded'));
$col->addComponent(new Button('Bottom Button', 'px-4 py-2 bg-indigo-600 text-white rounded'));
$test4->addComponent($col);
$mainContainer->addComponent($test4);
$window->setRoot($mainContainer);
$app->addWindow($window);
echo "Gap Test Example\n";
echo "You should see:\n";
echo "- Row 1: 2 buttons with 8px gap (gap-2)\n";
echo "- Row 2: 2 buttons with 16px gap (gap-4)\n";
echo "- Row 3: 2 buttons with 32px gap (gap-8)\n";
echo "- Column: 2 buttons stacked with 16px gap (gap-4)\n\n";
$app->run();

View File

@ -1,54 +0,0 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use PHPNative\Tailwind\StyleParser;
// Test shadow parsing
$testStyles = [
'shadow-sm',
'shadow',
'shadow-md',
'shadow-lg',
'shadow-xl',
'shadow-2xl',
'shadow-inner',
'shadow-none',
];
echo "Testing Shadow Parsing:\n";
echo str_repeat('=', 50) . "\n\n";
foreach ($testStyles as $styleString) {
echo "Testing: '{$styleString}'\n";
$styles = StyleParser::parse($styleString);
echo "Parsed StyleCollection:\n";
echo 'Number of styles: ' . $styles->count() . "\n";
foreach ($styles as $style) {
echo ' - Style type: ' . get_class($style->style) . "\n";
if ($style->style instanceof \PHPNative\Tailwind\Style\Shadow) {
echo ' Shadow size: ' . $style->style->size . "\n";
}
}
echo "\n";
}
// Test combined styles
echo "\nTesting combined style:\n";
echo str_repeat('=', 50) . "\n";
$combined = 'px-6 py-3 bg-blue-500 text-white rounded-lg shadow-lg';
echo "Testing: '{$combined}'\n";
$styles = StyleParser::parse($combined);
echo 'Number of styles: ' . $styles->count() . "\n";
foreach ($styles as $style) {
echo ' - ' . get_class($style->style);
if ($style->style instanceof \PHPNative\Tailwind\Style\Shadow) {
echo ' (size: ' . $style->style->size . ')';
}
echo "\n";
}

View File

@ -1,24 +0,0 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use PHPNative\Framework\Application;
use PHPNative\Ui\Widget\Container;
use PHPNative\Ui\Window;
// Create application
$app = new Application();
$window = new Window('Simple Shadow Test', 400, 400);
// Main container with light background
$mainContainer = new Container('flex items-center justify-center bg-gray-200');
// Simple box with shadow - add bg-white so it's visible
$box = new Container('w-48 h-48 bg-white rounded-lg shadow-lg shadow-rose-500');
$mainContainer->addComponent($box);
// Set window content and run
$window->setRoot($mainContainer);
$app->addWindow($window);
$app->run();

View File

@ -1,46 +0,0 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use PHPNative\Framework\Application;
use PHPNative\Ui\Widget\Container;
use PHPNative\Ui\Window;
// Create application
$app = new Application();
$window = new Window('Shadow Color & Opacity Test', 800, 600);
$mainContainer = new Container('flex flex-col gap-6 items-center justify-center bg-gray-100 p-8');
// Test 1: Default shadow (black)
$box1 = new Container('w-48 h-32 bg-white rounded-lg shadow-lg flex items-center justify-center');
// Test 2: Shadow with opacity modifier (50% opacity)
$box2 = new Container('w-48 h-32 bg-white rounded-lg shadow-lg/50 flex items-center justify-center');
// Test 3: Red shadow
$box3 = new Container('w-48 h-32 bg-white rounded-lg shadow-red-500 flex items-center justify-center');
// Test 4: Blue shadow with opacity
$box4 = new Container('w-48 h-32 bg-white rounded-lg shadow-blue-500/30 flex items-center justify-center');
// Test 5: Green shadow with large size
$box5 = new Container('w-48 h-32 bg-white rounded-lg shadow-xl shadow-green-500 flex items-center justify-center');
// Create row containers for better layout
$row1 = new Container('flex gap-6');
$row1->addComponent($box1);
$row1->addComponent($box2);
$row1->addComponent($box3);
$row2 = new Container('flex gap-6');
$row2->addComponent($box4);
$row2->addComponent($box5);
$mainContainer->addComponent($row1);
$mainContainer->addComponent($row2);
// Set window content and run
$window->setRoot($mainContainer);
$app->addWindow($window);
$app->run();

View File

@ -1,33 +0,0 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use PHPNative\Framework\Application;
use PHPNative\Ui\Widget\Container;
use PHPNative\Ui\Widget\Label;
use PHPNative\Ui\Window;
define('DEBUG_RENDERING', true);
$app = new Application();
$window = new Window('Simple Container with Label', 600, 400);
// Main container
$mainContainer = new Container('p-4 bg-gray-100');
$label = new Label(
text: 'Test',
style: 'text-xl m-4 bg-lime-400 p-4',
);
$mainContainer->addComponent($label);
$window->setRoot($mainContainer);
$app->addWindow($window);
echo "TextInput Test started!\n";
echo "- Red container (80px)\n";
echo "- Blue container with TextInput and Button (40px)\n";
echo "- Green container (80px)\n\n";
$app->run();

View File

@ -1,42 +0,0 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use PHPNative\Framework\Application;
use PHPNative\Ui\Window;
use PHPNative\Ui\Widget\Container;
use PHPNative\Ui\Widget\Label;
$app = new Application();
$window = new Window('Flex-Col Test', 800, 600);
// Main container mit flex-col
$mainContainer = new Container('flex flex-col p-4 bg-gray-100');
// Container 1 - Feste Höhe
$container1 = new Container('bg-red-500 h-20 p-4');
$label1 = new Label('Container 1 - Fixed Height (h-20)', 'text-white text-xl');
$container1->addComponent($label1);
$mainContainer->addComponent($container1);
// Container 2 - Mit flex-grow (sollte verfügbaren Platz einnehmen)
$container2 = new Container('flex-grow bg-blue-500 p-4');
$label2 = new Label('Container 2 - Flex Grow (flex-grow)', 'text-white text-xl');
$container2->addComponent($label2);
$mainContainer->addComponent($container2);
// Container 3 - Natürliche Höhe (basierend auf Inhalt)
$container3 = new Container('bg-green-500 p-4');
$label3 = new Label('Container 3 - Natural Height', 'text-white text-xl');
$container3->addComponent($label3);
$mainContainer->addComponent($container3);
$window->setRoot($mainContainer);
$app->addWindow($window);
echo "Flex-Col Test started!\n";
echo "- Red: Fixed height (80px)\n";
echo "- Blue: Should grow to fill available space\n";
echo "- Green: Natural height based on content\n\n";
$app->run();

View File

@ -1,29 +0,0 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use PHPNative\Framework\Application;
use PHPNative\Ui\Widget\Button;
use PHPNative\Ui\Widget\Container;
use PHPNative\Ui\Widget\Label;
use PHPNative\Ui\Window;
$app = new Application();
$window = new Window('Simple Test', 400, 300);
$container = new Container('flex flex-col p-4 gap-4');
$label = new Label('Test Label', 'text-lg');
$container->addComponent($label);
$button = new Button('Test Button', 'px-4 py-2 bg-blue-600 text-white rounded');
$button->setOnClick(function() use ($label) {
$label->setText('Button clicked!');
});
$container->addComponent($button);
$window->setRoot($container);
$app->addWindow($window);
echo "Simple test starting...\n";
$app->run();

View File

@ -1,47 +0,0 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use PHPNative\Framework\Application;
use PHPNative\Ui\Widget\Button;
use PHPNative\Ui\Widget\Container;
use PHPNative\Ui\Widget\TextInput;
use PHPNative\Ui\Window;
$app = new Application();
$window = new Window('TextInput Test', 600, 400);
// Main container
$mainContainer = new Container('flex flex-col p-4 bg-gray-100');
// Container 1 - Red background mit fester Höhe
$container1 = new Container('bg-red-500 h-20 mb-4 p-2');
$mainContainer->addComponent($container1);
// Container 2 - Mit TextInput und Button (flex-row)
$inputContainer = new Container('flex flex-row mb-4 bg-blue-200');
$input = new TextInput(
placeholder: 'Type something...',
style: 'flex-1 p-2 border border-gray-300 rounded mr-2 h-10',
);
$button = new Button('Submit', 'bg-green-500 text-white p-2 rounded');
$inputContainer->addComponent($input);
$inputContainer->addComponent($button);
$mainContainer->addComponent($inputContainer);
// Container 3 - Green background mit fester Höhe
$container3 = new Container('bg-green-500 h-20 p-2');
$mainContainer->addComponent($container3);
$window->setRoot($mainContainer);
$app->addWindow($window);
echo "TextInput Test started!\n";
echo "- Red container (80px)\n";
echo "- Blue container with TextInput and Button (40px)\n";
echo "- Green container (80px)\n\n";
$app->run();

View File

@ -1,39 +0,0 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use PHPNative\Framework\Application;
use PHPNative\Ui\Widget\Button;
use PHPNative\Ui\Widget\Container;
use PHPNative\Ui\Widget\TextInput;
use PHPNative\Ui\Window;
$app = new Application();
$window = new Window('TextInput Test', 600, 400);
// Main container
$mainContainer = new Container('flex flex-col p-4 bg-gray-100');
// Container 2 - Mit TextInput und Button (flex-row)
$inputContainer = new Container('flex flex-row p-4 g-blue-200');
$input = new TextInput(
placeholder: 'Type something...',
style: 'flex-1 p-2 border border-gray-300 rounded mr-2',
);
$button = new Button('Submit', 'bg-green-500 text-white p-2 rounded-xl');
$inputContainer->addComponent($input);
$inputContainer->addComponent($button);
$mainContainer->addComponent($inputContainer);
$window->setRoot($mainContainer);
$app->addWindow($window);
echo "TextInput Test started!\n";
echo "- Red container (80px)\n";
echo "- Blue container with TextInput and Button (40px)\n";
echo "- Green container (80px)\n\n";
$app->run();

150
examples/todo_app.php Normal file
View File

@ -0,0 +1,150 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use PHPNative\Framework\Application;
use PHPNative\Ui\Widget\Button;
use PHPNative\Ui\Widget\Container;
use PHPNative\Ui\Widget\Label;
use PHPNative\Ui\Widget\TextInput;
use PHPNative\Ui\Window;
$storagePath = __DIR__ . '/todo_data.json';
$loadTasks = static function (string $path): array {
if (!is_file($path)) {
return [];
}
$raw = file_get_contents($path);
$data = json_decode($raw ?: '[]', true);
if (!is_array($data)) {
return [];
}
// Normalize task structure
return array_values(array_map(static function ($task) {
return [
'id' => $task['id'] ?? uniqid('task_', true),
'title' => trim((string) ($task['title'] ?? '')),
'done' => (bool) ($task['done'] ?? false),
];
}, $data));
};
$saveTasks = static function (string $path, array $tasks): void {
file_put_contents(
$path,
json_encode($tasks, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE),
LOCK_EX,
);
};
$tasks = $loadTasks($storagePath);
$app = new Application();
$window = new Window('Todo Beispiel', 520, 720);
$statusLabel = new Label('Bereit.', 'text-sm text-gray-600');
$main = new Container('flex flex-col bg-gray-100 gap-4 p-4 h-full w-full');
$title = new Label('Todo Liste', 'text-2xl font-bold text-black');
$main->addComponent($title);
$input = new TextInput('Neue Aufgabe hinzufügen …', 'flex-1 border border-gray-300 rounded px-3 py-2 bg-white text-black');
$addButton = new Button('Hinzufügen', 'px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700');
$inputRow = new Container('flex flex-row gap-3 w-full');
$inputRow->addComponent($input);
$inputRow->addComponent($addButton);
$main->addComponent($inputRow);
$listWrapper = new Container('flex-1 w-full overflow-auto bg-white rounded border border-gray-300 p-2');
$listContainer = new Container('flex flex-col gap-2 w-full');
$listWrapper->addComponent($listContainer);
$main->addComponent($listWrapper);
$main->addComponent($statusLabel);
$renderTasks = null;
$renderTasks = function () use (&$tasks, $listContainer, $statusLabel, $storagePath, $saveTasks, &$renderTasks) {
$listContainer->clearChildren();
if (empty($tasks)) {
$emptyLabel = new Label('Keine Aufgaben vorhanden. Erstelle die erste oben im Feld.', 'text-sm text-gray-500');
$listContainer->addComponent($emptyLabel);
return;
}
foreach ($tasks as $index => $task) {
$row = new Container('flex flex-row items-center gap-3 w-full border border-gray-200 rounded px-3 py-2 bg-white shadow-sm');
$taskLabelStyles = $task['done']
? 'flex-1 text-gray-500 line-through'
: 'flex-1 text-black';
$taskLabel = new Label($task['title'], $taskLabelStyles);
$row->addComponent($taskLabel);
$toggleButton = new Button(
$task['done'] ? 'Reaktivieren' : 'Erledigt',
$task['done']
? 'px-3 py-1 text-sm bg-amber-500 text-white rounded hover:bg-amber-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) {
foreach ($tasks as &$entry) {
if ($entry['id'] === $task['id']) {
$entry['done'] = !$entry['done'];
break;
}
}
$saveTasks($storagePath, $tasks);
$statusLabel->setText(($task['done'] ? 'Aufgabe reaktiviert: ' : 'Aufgabe erledigt: ') . $task['title']);
$renderTasks();
});
$deleteButton = new Button(
'Löschen',
'px-3 py-1 text-sm bg-red-500 text-white rounded hover:bg-red-600',
);
$deleteButton->setOnClick(function () use (&$tasks, $index, $task, $storagePath, $saveTasks, $statusLabel, $renderTasks) {
array_splice($tasks, $index, 1);
$saveTasks($storagePath, $tasks);
$statusLabel->setText('Aufgabe entfernt: ' . $task['title']);
$renderTasks();
});
$row->addComponent($toggleButton);
$row->addComponent($deleteButton);
$listContainer->addComponent($row);
}
};
$renderTasks();
$addButton->setOnClick(function () use (&$tasks, $input, $saveTasks, $storagePath, $statusLabel, $renderTasks) {
$title = trim($input->getValue());
if ($title === '') {
$statusLabel->setText('Bitte zuerst einen Aufgabentext eingeben.');
return;
}
$tasks[] = [
'id' => uniqid('task_', true),
'title' => $title,
'done' => false,
];
$saveTasks($storagePath, $tasks);
$input->setValue('');
$statusLabel->setText('Aufgabe hinzugefügt: ' . $title);
$renderTasks();
});
$window->setRoot($main);
$app->addWindow($window);
$app->run();

12
examples/todo_data.json Normal file
View File

@ -0,0 +1,12 @@
[
{
"id": "task_69164e23f0d356.41043316",
"title": "Test",
"done": false
},
{
"id": "task_69164e28dae205.72302890",
"title": "Geht",
"done": true
}
]

View File

@ -1,629 +0,0 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use PHPNative\Framework\Application;
use PHPNative\Framework\HetznerClient;
use PHPNative\Framework\IconFontRegistry;
use PHPNative\Framework\Profiler;
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\FileBrowser;
use PHPNative\Ui\Widget\Icon;
use PHPNative\Ui\Widget\Label;
use PHPNative\Ui\Widget\Menu;
use PHPNative\Ui\Widget\MenuBar;
use PHPNative\Ui\Widget\Modal;
use PHPNative\Ui\Widget\StatusBar;
use PHPNative\Ui\Widget\TabContainer;
use PHPNative\Ui\Widget\Table;
use PHPNative\Ui\Widget\TextInput;
use PHPNative\Ui\Window;
$iconFontCandidates = [
__DIR__ . '/../assets/fonts/fa-solid-900.ttf',
__DIR__ . '/../assets/fonts/fontawesome/fa7_freesolid_900.otf',
'/usr/share/fonts/truetype/fontawesome-webfont.ttf',
'/usr/share/fonts/truetype/fontawesome/fa-solid-900.ttf',
'/usr/share/fonts/truetype/fa-solid-900.ttf',
];
$iconFontPath = null;
foreach ($iconFontCandidates as $candidate) {
if (is_file($candidate)) {
$iconFontPath = $candidate;
break;
}
}
if ($iconFontPath !== null) {
IconFontRegistry::setDefaultFontPath($iconFontPath);
} else {
echo "Hinweis: FontAwesome Font nicht gefunden. Icons werden ohne Symbol dargestellt.\n";
}
// Enable profiler to identify performance bottlenecks (disabled by default)
// Profiler::enable();
$app = new Application();
$window = new Window('Windows Application Example', 800, 600);
// Initialize settings
$settings = new Settings('WindowsAppExample');
$currentApiKey = $settings->get('api_key', '');
$currentPrivateKeyPath = $settings->get('private_key_path', '');
/** @var Label|null $statusLabel */
$statusLabel = null;
// Store selected server for SFTP connection
$selectedServer = null;
// Main container (flex-col: menu, content, status)
$mainContainer = new Container('flex flex-col bg-gray-100');
// Modal dialog setup (hidden by default)
$apiKeyInput = new TextInput('API Key', 'w-full border border-gray-300 rounded px-3 py-2 bg-white text-black');
$privateKeyPathInput = new TextInput(
'Private Key Path',
'w-full border border-gray-300 rounded px-3 py-2 bg-white text-black',
);
$modalDialog = new Container('bg-white rounded-lg p-6 flex flex-col w-96 gap-3');
$modalDialog->addComponent(new Label('API Einstellungen', 'text-xl font-bold text-black'));
$modalDialog->addComponent(new Label(
'Bitte gib deinen API Key und den Pfad zum Private Key für SSH-Verbindungen ein.',
'text-sm text-gray-700',
));
$fieldContainer = new Container('flex flex-col gap-1');
$fieldContainer->addComponent(new Label('API Key', 'text-sm text-gray-600'));
$fieldContainer->addComponent($apiKeyInput);
$modalDialog->addComponent($fieldContainer);
$privateKeyFieldContainer = new Container('flex flex-col gap-1');
$privateKeyFieldContainer->addComponent(new Label('Private Key Pfad', 'text-sm text-gray-600'));
$privateKeyFieldContainer->addComponent($privateKeyPathInput);
$modalDialog->addComponent($privateKeyFieldContainer);
$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');
$saveButton = new Button('Speichern', 'px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 flex items-center');
$saveIcon = new Icon(IconName::save, 18, 'text-white mr-2');
$saveButton->setIcon($saveIcon);
$buttonRow->addComponent($cancelButton);
$buttonRow->addComponent($saveButton);
$modalDialog->addComponent($buttonRow);
$modal = new Modal($modalDialog, 'flex items-center justify-center bg-black', 200);
// === 1. MenuBar ===
$menuBar = new MenuBar();
// File Menu
$fileMenu = new Menu(title: 'Datei');
// Settings Menu
$settingsMenu = new Menu(title: 'Einstellungen');
$menuBar->addMenu($fileMenu);
$menuBar->addMenu($settingsMenu);
$mainContainer->addComponent($menuBar);
// === 2. Tab Container (flex-1) ===
$tabContainer = new TabContainer('flex-1');
// Tab 1: Table with server data (Master-Detail Layout)
$tab1 = new Container('flex flex-row p-4 gap-4'); // Changed to flex-row for side-by-side layout
// Left side: Table with refresh button
$leftSide = new Container('flex flex-col gap-2 flex-1'); // flex-1 to take remaining space
// Refresh button (will be configured after loadServers function is defined)
$refreshButton = new Button(
'Server aktualisieren',
'flex flex-row gap-2 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700',
);
$refreshIcon = new Icon(IconName::search, 16, 'text-white');
$refreshButton->setIcon($refreshIcon);
$leftSide->addComponent($refreshButton);
// Search input field
$searchInput = new TextInput('Suche...', 'w-full border border-gray-300 rounded px-3 py-2 bg-white text-black mb-2');
$leftSide->addComponent($searchInput);
$table = new Table(style: ' flex-1'); // flex-1 to fill available height but not expand infinitely
$table->setColumns([
['key' => 'id', 'title' => 'ID', 'width' => 100],
['key' => 'name', 'title' => 'Name', 'width' => 400],
['key' => 'status', 'title' => 'Status', 'width' => 120],
['key' => 'type', 'title' => 'Typ', 'width' => 120],
['key' => 'ipv4', 'title' => 'IPv4'],
]);
// Test data with 63 entries
$testData = [];
for ($i = 1; $i <= 63; $i++) {
$testData[] = [
'id' => $i,
'name' => "Server-{$i}",
'status' => ($i % 3) === 0 ? 'stopped' : 'running',
'type' => 'cx' . (11 + (($i % 4) * 10)),
'ipv4' => sprintf('192.168.%d.%d', floor($i / 255), $i % 255),
];
}
// Store current server data (will be updated when API loads servers)
$currentServerData = $testData;
$table->setData($currentServerData);
// Add search functionality
$searchInput->setOnChange(function ($value) use ($table, &$currentServerData) {
$searchTerm = strtolower(trim($value));
if (empty($searchTerm)) {
// Show all data if search is empty
$table->setData($currentServerData);
} else {
// Filter by name
$filteredData = array_filter($currentServerData, function ($row) use ($searchTerm) {
return str_contains(strtolower($row['name']), $searchTerm);
});
$table->setData(array_values($filteredData));
}
});
$leftSide->addComponent($table);
$tab1->addComponent($leftSide);
// Right side: Detail panel
$detailPanel = new Container('flex flex-col gap-3 w-80 bg-white border-2 border-gray-300 rounded p-4');
$detailTitle = new Label('Server Details', 'text-xl font-bold text-black mb-2');
$detailPanel->addComponent($detailTitle);
// Detail fields (initially empty)
$detailId = new Label('Bitte einen Server auswählen', 'text-sm text-gray-600');
$detailName = new Label('', 'text-sm text-black');
$detailStatus = new Label('', 'text-sm text-black');
$detailType = new Label('', 'text-sm text-black');
$detailIpv4 = new Label('', 'text-sm text-black');
$detailPanel->addComponent(new Label('ID:', 'text-xs text-gray-500 mt-2'));
$detailPanel->addComponent($detailId);
$detailPanel->addComponent(new Label('Name:', 'text-xs text-gray-500 mt-2'));
$detailPanel->addComponent($detailName);
$detailPanel->addComponent(new Label('Status:', 'text-xs text-gray-500 mt-2'));
$detailPanel->addComponent($detailStatus);
$detailPanel->addComponent(new Label('Typ:', 'text-xs text-gray-500 mt-2'));
$detailPanel->addComponent($detailType);
$detailPanel->addComponent(new Label('IPv4:', 'text-xs text-gray-500 mt-2'));
$detailPanel->addComponent($detailIpv4);
// SFTP Manager Button
$sftpButton = new Button(
'SFTP Manager öffnen',
'w-full mt-4 px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 flex items-center justify-center',
);
$sftpIcon = new Icon(IconName::home, 18, 'text-white mr-2');
$sftpButton->setIcon($sftpIcon);
// Add synchronous click handler to switch tab immediately
$sftpButton->setOnClick(function () use ($tabContainer) {
$tabContainer->setActiveTab(3); // Switch to SFTP Manager tab immediately
});
$detailPanel->addComponent($sftpButton);
// SSH Terminal Button
$sshTerminalButton = new Button(
'SSH Terminal öffnen',
'w-full mt-2 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 flex items-center justify-center',
);
$sshTerminalIcon = new Icon(IconName::home, 18, 'text-white mr-2');
$sshTerminalButton->setIcon($sshTerminalIcon);
// Add click handler to open SSH terminal
$sshTerminalButton->setOnClick(function () use (&$selectedServer, &$currentPrivateKeyPath, &$statusLabel) {
if ($selectedServer === null) {
$statusLabel->setText('Kein Server ausgewählt');
return;
}
if (empty($currentPrivateKeyPath) || !file_exists($currentPrivateKeyPath)) {
$statusLabel->setText('Private Key Pfad nicht konfiguriert');
return;
}
// Build SSH command with private key
$host = $selectedServer['ipv4'];
$keyPath = escapeshellarg($currentPrivateKeyPath);
$sshCommand = "ssh -i {$keyPath} root@{$host}";
// Try to open terminal with SSH connection
// Different terminal emulators for different systems
$terminals = [
'gnome-terminal -- ' . $sshCommand,
'konsole -e ' . $sshCommand,
'xterm -e ' . $sshCommand,
'x-terminal-emulator -e ' . $sshCommand,
];
$opened = false;
foreach ($terminals as $terminalCmd) {
exec($terminalCmd . ' > /dev/null 2>&1 &', $output, $returnCode);
if ($returnCode === 0) {
$opened = true;
$statusLabel->setText('SSH Terminal geöffnet für ' . $selectedServer['name']);
break;
}
}
if (!$opened) {
$statusLabel->setText('Konnte kein Terminal öffnen. SSH Befehl: ' . $sshCommand);
}
});
$detailPanel->addComponent($sshTerminalButton);
$tab1->addComponent($detailPanel);
// Row selection handler - update detail panel
$statusLabel = new Label(
text: 'Fenster: ' . $window->getViewport()->windowWidth . 'x' . $window->getViewport()->windowHeight,
style: 'basis-4/8 text-black',
);
$table->setOnRowSelect(function ($index, $row) use (
&$statusLabel,
&$selectedServer,
$detailId,
$detailName,
$detailStatus,
$detailType,
$detailIpv4,
) {
if ($row) {
$statusLabel->setText("Server: {$row['name']} - {$row['status']} ({$row['ipv4']})");
// Update detail panel
$detailId->setText("#{$row['id']}");
$detailName->setText($row['name']);
$detailStatus->setText($row['status']);
$detailType->setText($row['type']);
$detailIpv4->setText($row['ipv4']);
// Store selected server for SFTP connection
$selectedServer = $row;
}
});
// Function to load servers from Hetzner API (only returns simple data, no objects)
// Now uses HetznerClient class thanks to bootstrap.php in async runtime
$loadServersAsync = function () use ($currentApiKey) {
try {
if (empty($currentApiKey)) {
return ['error' => 'Kein API-Key konfiguriert'];
}
// Use HetznerClient class (autoloader is available in async runtime)
$hetznerClient = new \LKDev\HetznerCloud\HetznerAPIClient($currentApiKey);
$result = ['servers' => []];
foreach ($hetznerClient->servers()->all() as $server) {
$result['servers'][] = [
'id' => $server->id,
'name' => $server->name,
'status' => $server->status,
'type' => $server->serverType->name,
'ipv4' => $server->publicNet->ipv4->ip,
];
}
if (isset($result['servers'])) {
// Return only simple array data
return ['success' => true, 'servers' => $result['servers'], 'count' => count($result['servers'])];
}
return ['error' => 'Unerwartete Antwort'];
} catch (\Exception $e) {
return ['error' => 'Exception: ' . $e->getMessage()];
}
};
// Configure refresh button to load servers asynchronously
$refreshButton->setOnClickAsync(
$loadServersAsync,
function ($result) use ($table, $statusLabel, &$currentServerData, $searchInput) {
// Handle the result in the main thread (can access objects here)
if (is_array($result)) {
if (isset($result['error'])) {
$statusLabel->setText('Fehler: ' . $result['error']);
echo "Error: {$result['error']}\n";
} elseif (isset($result['success'], $result['servers'])) {
// Update current server data
$currentServerData = $result['servers'];
// Check if search is active
$searchTerm = strtolower(trim($searchInput->getValue()));
if (empty($searchTerm)) {
// No search, show all servers
$table->setData($currentServerData);
} else {
// Apply search filter to new data
$filteredData = array_filter($currentServerData, function ($row) use ($searchTerm) {
return str_contains(strtolower($row['name']), $searchTerm);
});
$table->setData(array_values($filteredData));
}
$statusLabel->setText('Server geladen: ' . $result['count'] . ' gefunden');
echo "Success: {$result['count']} servers loaded\n";
}
}
},
function ($error) use ($statusLabel) {
$errorMsg = is_object($error) && method_exists($error, 'getMessage') ? $error->getMessage() : ((string) $error);
$statusLabel->setText('Async Fehler: ' . $errorMsg);
echo "Async error: {$errorMsg}\n";
},
);
$tabContainer->addTab('Server', $tab1);
// Tab 2: SFTP Manager (will be populated dynamically when server is selected)
$sftpTab = new Container('flex flex-row p-4 gap-4 bg-gray-50');
// Left side: Local file browser
$localBrowserContainer = new Container('flex flex-col flex-1 gap-2');
$localBrowserContainer->addComponent(new Label('Lokal', 'text-lg font-bold text-black mb-2'));
$localFileBrowser = new FileBrowser(getcwd(), false, 'flex-1 bg-white border-2 border-gray-300 rounded p-2');
$localBrowserContainer->addComponent($localFileBrowser);
// Right side: Remote file browser
$remoteBrowserContainer = new Container('flex flex-col flex-1 gap-2');
$remoteBrowserContainer->addComponent(new Label('Remote', 'text-lg font-bold text-black mb-2'));
$remoteFileBrowser = new FileBrowser('/', true, 'flex-1 bg-white border-2 border-gray-300 rounded p-2');
$remoteBrowserContainer->addComponent($remoteFileBrowser);
// Connection status label
$connectionStatusLabel = new Label('Nicht verbunden', 'text-sm text-gray-600 italic mb-2');
$remoteBrowserContainer->addComponent($connectionStatusLabel);
$sftpTab->addComponent($localBrowserContainer);
$sftpTab->addComponent($remoteBrowserContainer);
$tabContainer->addTab('SFTP Manager', $sftpTab);
// Remote FileBrowser navigation handler - load directory asynchronously
$remoteFileBrowser->setOnFileSelect(function ($path, $row) use (
$remoteFileBrowser,
&$selectedServer,
&$currentPrivateKeyPath,
&$statusLabel,
) {
if (!isset($row['isDir']) || !$row['isDir']) {
return; // Only handle directories
}
// Load directory asynchronously via Button with async handler
$loadButton = new Button('Load', '');
$loadButton->setOnClickAsync(
function () use ($path, &$selectedServer, &$currentPrivateKeyPath) {
if ($selectedServer === null || empty($currentPrivateKeyPath)) {
return ['error' => 'Not connected'];
}
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'];
}
$files = $sftp->nlist($path);
if ($files === false) {
return ['error' => 'Cannot read directory'];
}
$fileList = [];
foreach ($files as $file) {
if ($file === '.' || $file === '..') {
continue;
}
$fullPath = rtrim($path, '/') . '/' . $file;
$stat = $sftp->stat($fullPath);
$fileList[] = [
'name' => $file,
'path' => $fullPath,
'isDir' => ($stat['type'] ?? 0) === 2,
'size' => $stat['size'] ?? 0,
'mtime' => $stat['mtime'] ?? 0,
];
}
return ['success' => true, 'path' => $path, 'files' => $fileList];
} catch (\Exception $e) {
return ['error' => $e->getMessage()];
}
},
function ($result) use ($remoteFileBrowser, &$statusLabel) {
if (isset($result['error'])) {
$statusLabel->setText('SFTP Fehler: ' . $result['error']);
return;
}
if (isset($result['success'])) {
$remoteFileBrowser->setPath($result['path']);
$remoteFileBrowser->setFileData($result['files']);
}
},
function ($error) use (&$statusLabel) {
$errorMsg = is_string($error) ? $error : 'Unknown error';
$statusLabel->setText('SFTP Error: ' . $errorMsg);
},
);
// Trigger the async load
$loadButton->handleMouseClick(0, 0, 0);
});
// SFTP Button Click Handler - Connect to server and load remote files
$sftpButton->setOnClickAsync(
function () use (&$selectedServer, &$currentPrivateKeyPath) {
// This runs in a separate thread
if ($selectedServer === null) {
return ['error' => 'Kein Server ausgewählt'];
}
if (empty($currentPrivateKeyPath) || !file_exists($currentPrivateKeyPath)) {
return ['error' => 'Private Key Pfad nicht konfiguriert oder Datei nicht gefunden'];
}
try {
$ssh = new \phpseclib3\Net\SSH2($selectedServer['ipv4']);
$key = \phpseclib3\Crypt\PublicKeyLoader::load(file_get_contents($currentPrivateKeyPath));
if (!$ssh->login('root', $key)) {
return ['error' => 'SSH Login fehlgeschlagen'];
}
// Create SFTP connection
$sftp = new \phpseclib3\Net\SFTP($selectedServer['ipv4']);
if (!$sftp->login('root', $key)) {
return ['error' => 'SFTP Login fehlgeschlagen'];
}
// Read root directory
$files = $sftp->nlist('/');
if ($files === false) {
return ['error' => 'Kann Root-Verzeichnis nicht lesen'];
}
$fileList = [];
foreach ($files as $file) {
if ($file === '.' || $file === '..') {
continue;
}
$stat = $sftp->stat('/' . $file);
$fileList[] = [
'name' => $file,
'path' => '/' . $file,
'isDir' => ($stat['type'] ?? 0) === 2,
'size' => $stat['size'] ?? 0,
'mtime' => $stat['mtime'] ?? 0,
];
}
return [
'success' => true,
'server' => $selectedServer,
'files' => $fileList,
];
} catch (\Exception $e) {
return ['error' => 'Verbindung fehlgeschlagen: ' . $e->getMessage()];
}
},
function ($result) use ($tabContainer, $remoteFileBrowser, $connectionStatusLabel, &$statusLabel) {
// This runs in the main thread
if (isset($result['error'])) {
$statusLabel->setText('SFTP Fehler: ' . $result['error']);
return;
}
if (isset($result['success']) && $result['success']) {
$connectionStatusLabel->setText(
'Verbunden mit: ' . $result['server']['name'] . ' (' . $result['server']['ipv4'] . ')',
);
$statusLabel->setText('SFTP Verbindung erfolgreich zu ' . $result['server']['name']);
// Populate remote file browser with the file list
$remoteFileBrowser->setPath('/');
$remoteFileBrowser->setFileData($result['files']);
// Switch to SFTP Manager tab
$tabContainer->setActiveTab(1); // Index 3 = SFTP Manager tab
}
},
function ($error) use (&$statusLabel) {
$errorMsg = is_string($error)
? $error
: (is_object($error) && method_exists($error, 'getMessage') ? $error->getMessage() : 'Unbekannter Fehler');
$statusLabel->setText('SFTP Async Fehler: ' . $errorMsg);
},
);
//$mainContainer->addComponent($tabContainer);
// === 3. StatusBar ===
$statusBar = new StatusBar();
$statusBar->addSegment($statusLabel);
$fpsLabel = new Label(
text: 'FPS: --',
style: 'basis-1/8 text-black border-l',
);
$statusBar->addSegment(new Label(
text: 'Zeilen: 10',
style: 'basis-2/8 text-black border-l',
)); // Fixed width
$statusBar->addSegment($fpsLabel);
$statusBar->addSegment(new Label(
text: 'Version 1.0',
style: 'border-l text-black basis-2/8',
));
//$mainContainer->addComponent($statusBar);
$cancelButton->setOnClick(function () use ($menuBar, $modal) {
$menuBar->closeAllMenus();
$modal->setVisible(false);
});
$saveButton->setOnClick(function () use (
$settings,
&$currentApiKey,
&$currentPrivateKeyPath,
$apiKeyInput,
$privateKeyPathInput,
$menuBar,
$modal,
&$statusLabel,
) {
$currentApiKey = trim($apiKeyInput->getValue());
$currentPrivateKeyPath = trim($privateKeyPathInput->getValue());
// Save to settings
$settings->set('api_key', $currentApiKey);
$settings->set('private_key_path', $currentPrivateKeyPath);
$settings->save();
if ($statusLabel !== null) {
$masked = strlen($currentApiKey) > 4
? (str_repeat('*', max(0, strlen($currentApiKey) - 4)) . substr($currentApiKey, -4))
: $currentApiKey;
$statusLabel->setText('Einstellungen gespeichert: API-Key ' . $masked . ' (' . $settings->getPath() . ')');
}
$menuBar->closeAllMenus();
$modal->setVisible(false);
});
$mainContainer->addComponent($modal);
$window->setOnResize(function (Window $window) use (&$statusLabel) {
$statusLabel->setText(
'Fenster: ' . $window->getViewport()->windowWidth . 'x' . $window->getViewport()->windowHeight,
);
});
$window->setOnFpsChange(function (float $fps) use ($fpsLabel) {
$fpsLabel->setText(sprintf('FPS: %d', max(0, (int) round($fps))));
});
// Set root and run
$window->setRoot($mainContainer);
$app->addWindow($window);
echo "Windows Application Example started!\n";
echo "Features:\n";
echo "- MenuBar with 'Datei' and 'Einstellungen'\n";
echo "- Tab Container with 3 tabs\n";
echo "- Scrollable Table in first tab\n";
echo "- StatusBar at the bottom\n\n";
$app->run();

View File

@ -1,73 +0,0 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use PHPNative\Framework\Application;
use PHPNative\Ui\Widget\Container;
use PHPNative\Ui\Widget\Label;
use PHPNative\Ui\Widget\Menu;
use PHPNative\Ui\Widget\MenuBar;
use PHPNative\Ui\Widget\StatusBar;
use PHPNative\Ui\Widget\TabContainer;
use PHPNative\Ui\Widget\Table;
use PHPNative\Ui\Window;
$app = new Application();
$window = new Window('Windows Application Example', 800, 600);
$mainContainer = new Container('bg-gray-100');
// === 1. MenuBar ===
$menuBar = new MenuBar();
// File Menu
$fileMenu = new Menu(title: 'Datei');
$fileMenu->addItem('Neu', function () {
echo "Neu clicked\n";
});
$fileMenu->addItem('Öffnen', function () {
echo "Öffnen clicked\n";
});
$fileMenu->addSeparator();
$fileMenu->addItem('Beenden', function () use ($app) {
echo "Beenden clicked\n";
exit(0);
});
// Settings Menu
$settingsMenu = new Menu(title: 'Einstellungen');
$settingsMenu->addItem('Optionen', function () {
echo "Optionen clicked\n";
});
$settingsMenu->addItem('Sprache', function () {
echo "Sprache clicked\n";
});
$menuBar->addMenu($fileMenu);
$menuBar->addMenu($settingsMenu);
$mainContainer->addComponent($menuBar);
$statusBar = new StatusBar();
$statusLabel = new Label(
text: 'Fenster: ' . $window->getViewport()->windowWidth . 'x' . $window->getViewport()->windowHeight,
style: 'basis-2/8 text-black',
);
$statusBar->addSegment($statusLabel);
$statusBar->addSegment(new Label(
text: 'Zeilen: 10',
style: 'basis-4/8 text-black border-l',
)); // Fixed width
$statusBar->addSegment(new Label(
text: 'Version 1.0',
style: 'border-l text-black basis-2/8',
));
$mainContainer->addComponent($statusBar);
$window->setOnResize(function (Window $window) use (&$statusLabel) {
$statusLabel->setText(
'Fenster: ' . $window->getViewport()->windowWidth . 'x' . $window->getViewport()->windowHeight,
);
});
// Set root and run
$window->setRoot($mainContainer);
$app->addWindow($window);
$app->run();