diff --git a/examples/advanced_multi_window_example.php b/examples/advanced_multi_window_example.php new file mode 100644 index 0000000..b37d575 --- /dev/null +++ b/examples/advanced_multi_window_example.php @@ -0,0 +1,189 @@ +createWindow('Advanced Multi-Window Demo', 700, 500, 100, 100); + +$windowCounter = 1; + +// Main UI +$mainContainer = new Container('flex flex-col p-6 gap-4 bg-gray-50'); + +$title = new Label( + text: 'Advanced Multi-Window Demo', + style: 'text-2xl text-gray-900' +); + +$subtitle = new Label( + text: 'Windows mit asynchronen Operationen', + style: 'text-base text-gray-600' +); + +// Button to create window with async data loading +$createAsyncWindowButton = new Button( + text: 'Window mit Async-Datenladung', + style: 'bg-purple-500 hover:bg-purple-700 text-white p-4 rounded-lg' +); + +$createAsyncWindowButton->setOnClickAsync( + onClickAsync: function() use (&$windowCounter) { + // Simulate loading data in background + sleep(1); + + return [ + 'windowId' => $windowCounter++, + 'data' => 'Daten erfolgreich geladen!', + 'timestamp' => date('H:i:s'), + 'items' => ['Item 1', 'Item 2', 'Item 3'] + ]; + }, + + onComplete: function($result) use ($app) { + // Create window with loaded data + $windowId = $result['windowId']; + $newWindow = $app->createWindow( + "Data Window #$windowId", + 500, + 350, + 150 + ($windowId * 30), + 150 + ($windowId * 30) + ); + + $container = new Container('flex flex-col p-6 gap-3 bg-green-50'); + + $titleLabel = new Label( + text: "Data Window #$windowId", + style: 'text-xl text-gray-900' + ); + + $dataLabel = new Label( + text: $result['data'], + style: 'text-base text-green-700 p-2 bg-white rounded' + ); + + $timeLabel = new Label( + text: "Geladen um: " . $result['timestamp'], + style: 'text-sm text-gray-600' + ); + + $itemsLabel = new Label( + text: "Items:\n" . implode("\n", $result['items']), + style: 'text-sm text-gray-700 p-2 bg-white rounded' + ); + + $closeButton = new Button( + text: 'Schließen', + style: 'bg-red-500 hover:bg-red-700 text-white p-3 rounded' + ); + + $closeButton->setOnClick(function() use ($newWindow) { + $newWindow->close(); + }); + + $container->addComponent($titleLabel); + $container->addComponent($dataLabel); + $container->addComponent($timeLabel); + $container->addComponent($itemsLabel); + $container->addComponent($closeButton); + + $newWindow->setRoot($container); + }, + + onError: function($error) { + error_log("Error creating window: " . $error->getMessage()); + } +); + +// Button to create multiple windows at once +$createMultipleButton = new Button( + text: '3 Windows auf einmal erstellen', + style: 'bg-orange-500 hover:bg-orange-700 text-white p-4 rounded-lg' +); + +$createMultipleButton->setOnClick(function() use ($app, &$windowCounter) { + for ($i = 0; $i < 3; $i++) { + $currentId = $windowCounter++; + $offset = $currentId * 40; + + $newWindow = $app->createWindow( + "Batch Window #$currentId", + 400, + 250, + 200 + $offset, + 200 + ($i * 50) + ); + + $container = new Container('flex flex-col p-4 gap-2 bg-yellow-50'); + + $label = new Label( + text: "Batch Window #$currentId", + style: 'text-lg text-gray-900' + ); + + $info = new Label( + text: "Teil einer Batch-Erstellung ($i/3)", + style: 'text-sm text-gray-600' + ); + + $closeButton = new Button( + text: 'X', + style: 'bg-red-500 hover:bg-red-700 text-white p-2 rounded' + ); + + $closeButton->setOnClick(function() use ($newWindow) { + $newWindow->close(); + }); + + $container->addComponent($label); + $container->addComponent($info); + $container->addComponent($closeButton); + + $newWindow->setRoot($container); + } +}); + +// Window count label +$windowCountLabel = new Label( + text: 'Offene Windows: 1', + style: 'text-sm text-blue-600 p-2 bg-white rounded' +); + +// Update window count periodically +$mainContainer->addComponent($title); +$mainContainer->addComponent($subtitle); +$mainContainer->addComponent($windowCountLabel); +$mainContainer->addComponent($createAsyncWindowButton); +$mainContainer->addComponent($createMultipleButton); + +// Info +$info = new Label( + text: 'Die UI bleibt responsive während des Ladens!', + style: 'text-xs text-gray-500 italic mt-4' +); +$mainContainer->addComponent($info); + +// Quit button +$quitButton = new Button( + text: 'Beenden', + style: 'bg-red-600 hover:bg-red-800 text-white p-4 rounded-lg' +); + +$quitButton->setOnClick(function() use ($app) { + $app->quit(); +}); + +$mainContainer->addComponent($quitButton); + +$mainWindow->setRoot($mainContainer); + +// Run application +$app->run(); diff --git a/examples/multi_window_example.php b/examples/multi_window_example.php new file mode 100644 index 0000000..046ce76 --- /dev/null +++ b/examples/multi_window_example.php @@ -0,0 +1,161 @@ +createWindow('Main Window - Multi-Window Demo', 600, 400, 100, 100); + +// Counter for new windows +$windowCounter = 1; + +// Create main window UI +$mainContainer = new Container('flex flex-col p-6 gap-4 bg-gray-100'); + +$title = new Label( + text: 'Multi-Window Demo', + style: 'text-2xl text-gray-900' +); + +$description = new Label( + text: 'Klicken Sie auf die Buttons, um neue Windows zu erstellen!', + style: 'text-base text-gray-700' +); + +$windowCountLabel = new Label( + text: 'Offene Windows: 1', + style: 'text-sm text-blue-600 p-2 bg-white rounded' +); + +// Button to create a simple new window +$createSimpleButton = new Button( + text: 'Neues Fenster erstellen', + style: 'bg-blue-500 hover:bg-blue-700 text-white p-4 rounded-lg' +); + +$createSimpleButton->setOnClick(function() use ($app, &$windowCounter, $windowCountLabel) { + // Create new window with offset position + $offset = $windowCounter * 30; + $newWindow = $app->createWindow( + "Window #$windowCounter", + 500, + 300, + 150 + $offset, + 150 + $offset + ); + + // Create UI for new window + $container = new Container('flex flex-col p-6 gap-3 bg-gradient-to-br from-purple-400 to-pink-400'); + + $label = new Label( + text: "Dies ist Window #$windowCounter", + style: 'text-xl text-white' + ); + + $closeButton = new Button( + text: 'Fenster schließen', + style: 'bg-red-500 hover:bg-red-700 text-white p-3 rounded' + ); + + $closeButton->setOnClick(function() use ($newWindow) { + $newWindow->close(); + }); + + $container->addComponent($label); + $container->addComponent($closeButton); + $newWindow->setRoot($container); + + $windowCounter++; + + // Update counter + $windowCountLabel->setText('Offene Windows: ' . $app->getWindowCount()); +}); + +// Button to create a window with interactive content +$createInteractiveButton = new Button( + text: 'Interaktives Fenster erstellen', + style: 'bg-green-500 hover:bg-green-700 text-white p-4 rounded-lg' +); + +$createInteractiveButton->setOnClick(function() use ($app, &$windowCounter, $windowCountLabel) { + $offset = $windowCounter * 30; + $newWindow = $app->createWindow( + "Interactive Window #$windowCounter", + 600, + 400, + 150 + $offset, + 150 + $offset + ); + + // Create interactive UI + $container = new Container('flex flex-col p-6 gap-3 bg-blue-50'); + + $title = new Label( + text: "Interaktives Window #$windowCounter", + style: 'text-xl text-gray-900' + ); + + $counter = 0; + $counterLabel = new Label( + text: "Zähler: $counter", + style: 'text-lg text-blue-600 p-2 bg-white rounded' + ); + + $incrementButton = new Button( + text: 'Zähler erhöhen', + style: 'bg-blue-500 hover:bg-blue-700 text-white p-3 rounded' + ); + + $incrementButton->setOnClick(function() use (&$counter, $counterLabel) { + $counter++; + $counterLabel->setText("Zähler: $counter"); + }); + + $closeButton = new Button( + text: 'Fenster schließen', + style: 'bg-red-500 hover:bg-red-700 text-white p-3 rounded' + ); + + $closeButton->setOnClick(function() use ($newWindow) { + $newWindow->close(); + }); + + $container->addComponent($title); + $container->addComponent($counterLabel); + $container->addComponent($incrementButton); + $container->addComponent($closeButton); + $newWindow->setRoot($container); + + $windowCounter++; + $windowCountLabel->setText('Offene Windows: ' . $app->getWindowCount()); +}); + +// Button to quit application +$quitButton = new Button( + text: 'Alle Fenster schließen und beenden', + style: 'bg-red-600 hover:bg-red-800 text-white p-4 rounded-lg mt-4' +); + +$quitButton->setOnClick(function() use ($app) { + $app->quit(); +}); + +// Add all components to main container +$mainContainer->addComponent($title); +$mainContainer->addComponent($description); +$mainContainer->addComponent($windowCountLabel); +$mainContainer->addComponent($createSimpleButton); +$mainContainer->addComponent($createInteractiveButton); +$mainContainer->addComponent($quitButton); + +$mainWindow->setRoot($mainContainer); + +// Run application +$app->run(); diff --git a/examples/simple_window_example.php b/examples/simple_window_example.php new file mode 100644 index 0000000..03f532a --- /dev/null +++ b/examples/simple_window_example.php @@ -0,0 +1,65 @@ +createWindow('Simple Window Example', 800, 600); + +// Create UI +$container = new Container('flex flex-col p-6 gap-4 bg-gradient-to-br from-blue-400 to-purple-500'); + +$title = new Label( + text: 'Willkommen zu PHPNative!', + style: 'text-3xl text-white' +); + +$description = new Label( + text: 'Dies ist ein einfaches Beispiel mit einem Window.', + style: 'text-lg text-white' +); + +$button = new Button( + text: 'Klick mich!', + style: 'bg-white hover:bg-gray-100 text-blue-600 p-4 rounded-lg' +); + +$statusLabel = new Label( + text: '', + style: 'text-base text-white' +); + +$clickCount = 0; +$button->setOnClick(function() use ($statusLabel, &$clickCount) { + $clickCount++; + $statusLabel->setText("Button wurde $clickCount mal geklickt!"); +}); + +$quitButton = new Button( + text: 'Beenden', + style: 'bg-red-500 hover:bg-red-700 text-white p-4 rounded-lg mt-4' +); + +$quitButton->setOnClick(function() use ($app) { + $app->quit(); +}); + +// Add components +$container->addComponent($title); +$container->addComponent($description); +$container->addComponent($button); +$container->addComponent($statusLabel); +$container->addComponent($quitButton); + +// Set window root +$window->setRoot($container); + +// Run application +$app->run(); diff --git a/src/Framework/Application.php b/src/Framework/Application.php index 7b0b215..713dd92 100644 --- a/src/Framework/Application.php +++ b/src/Framework/Application.php @@ -4,229 +4,129 @@ namespace PHPNative\Framework; use PHPNative\Async\TaskManager; use PHPNative\Ui\Component; -use PHPNative\Ui\Viewport; +use PHPNative\Ui\Window; class Application { + private static ?Application $instance = null; protected array $windows = []; protected int $nextWindowId = 0; + protected bool $running = true; - protected $window; - protected $windowWidth; - protected $windowHeight; - protected $title; - protected $running = true; - protected $rootComponent = null; - private TextRenderer $textRenderer; - private $mouseX; - private $mouseY; - private Viewport $viewport; - private bool $shouldBeReLayouted = true; - private float $pixelRatio = 2; - - public function __construct(string $title, int $width = 800, int $height = 600) + public function __construct() { - $this->title = $title; - $this->windowWidth = $width; - $this->windowHeight = $height; - - // Create window - $this->window = rgfw_createWindow($title, 100, 100, $width, $height, RGFW_CENTER); - if (!$this->window) { - throw new \Exception('Failed to create window'); - } - - // Initialize RSGL renderer - if (!rsgl_init($this->window)) { - throw new \Exception('Failed to initialize RSGL renderer'); - } - - // Initialize text renderer - $this->textRenderer = new TextRenderer($this->window); - if (!$this->textRenderer->init()) { - error_log('Warning: Failed to initialize text renderer. Text rendering will not be available.'); - } - - // Get actual window size - $size = rgfw_window_getSize($this->window); - $this->windowWidth = $size[0]; - $this->windowHeight = $size[1]; - $this->viewport = new Viewport( - windowWidth: $this->windowWidth, - windowHeight: $this->windowHeight, - width: $this->windowWidth, - height: $this->windowHeight, - ); + self::$instance = $this; } - public function createWindow(): void + /** + * Get singleton instance + */ + public static function getInstance(): ?Application { + return self::$instance; } + /** + * Create a new window + * + * @param string $title Window title + * @param int $width Window width + * @param int $height Window height + * @param int $x Window X position (default: 100) + * @param int $y Window Y position (default: 100) + * @return Window The created window instance + */ + public function createWindow( + string $title, + int $width = 800, + int $height = 600, + int $x = 100, + int $y = 100 + ): Window { + $window = new Window($title, $width, $height, $x, $y); + $windowId = $this->nextWindowId++; + $this->windows[$windowId] = $window; + + return $window; + } + + /** + * Get all windows + */ + public function getWindows(): array + { + return $this->windows; + } + + /** + * Get window count + */ + public function getWindowCount(): int + { + return count($this->windows); + } + + /** + * Main application loop + */ public function run(): void { - while ($this->running && !rgfw_window_shouldClose($this->window)) { - $this->handleEvents(); - $this->update(); - $this->layout(); - $this->render(); + while ($this->running && count($this->windows) > 0) { + // Handle events for all windows + foreach ($this->windows as $windowId => $window) { + $window->handleEvents(); + } + + // Update async tasks (global) + TaskManager::getInstance()->update(); + + // Update all windows + foreach ($this->windows as $windowId => $window) { + $window->update(); + } + + // Layout all windows + foreach ($this->windows as $windowId => $window) { + $window->layout(); + } + + // Render all windows + foreach ($this->windows as $windowId => $window) { + $window->render(); + } + + // Remove closed windows + foreach ($this->windows as $windowId => $window) { + if ($window->shouldClose()) { + $window->cleanup(); + unset($this->windows[$windowId]); + } + } // Limit frame rate to ~60 FPS usleep(16666); } + // Cleanup remaining windows $this->cleanup(); } /** - * Handle window and input events - */ - protected function handleEvents(): void - { - while ($event = rgfw_window_checkEvent($this->window)) { - switch ($event['type']) { - case RGFW_quit: - $this->running = false; - break; - - case RGFW_keyPressed: - $keyCode = $event['keyCode'] ?? 0; - if ($keyCode == RGFW_Escape) { - $this->running = false; - } - break; - - case RGFW_windowResized: - // Update window dimensions (from event data) - $newWidth = $event[0] ?? $this->windowWidth; - $newHeight = $event[1] ?? $this->windowHeight; - - $this->windowWidth = $newWidth; - $this->windowHeight = $newHeight; - - // Update RSGL renderer size and viewport - // This ensures the renderer is properly configured for the new window size - rsgl_updateRendererSize($this->window); - - // Update text renderer framebuffer - if ($this->textRenderer && $this->textRenderer->isInitialized()) { - $this->textRenderer->updateFramebuffer($newWidth, $newHeight); - } - - $this->viewport->x = 0; - $this->viewport->y = 0; - $this->viewport->windowWidth = $newWidth; - $this->viewport->width = $newWidth; - $this->viewport->height = $newHeight; - $this->viewport->windowHeight = $newHeight; - $this->shouldBeReLayouted = true; - break; - - case RGFW_mousePosChanged: - $this->mouseX = $event[0] ?? 0; - $this->mouseY = $event[1] ?? 0; - - // Propagate mouse move to root component - if ($this->rootComponent) { - $this->rootComponent->handleMouseMove($this->mouseX, $this->mouseY); - } - break; - - case RGFW_mouseButtonPressed: - $button = $event['button'] ?? 0; - // Propagate click to root component - if ($this->rootComponent) { - $this->rootComponent->handleMouseClick($this->mouseX, $this->mouseY, $button); - } - break; - - case RGFW_mouseButtonReleased: - $button = $event['button'] ?? 0; - - // Propagate release to root component - if ($this->rootComponent) { - $this->rootComponent->handleMouseRelease($this->mouseX, $this->mouseY, $button); - } - break; - - case RGFW_mouseScroll: - $deltaY = $event[1] ?? 0; - - // Propagate wheel to root component - if ($this->rootComponent) { - $this->rootComponent->handleMouseWheel($this->mouseX, $this->mouseY, $deltaY); - } - break; - } - } - } - - /** - * Update application state - */ - protected function update(): void - { - // Update async tasks - TaskManager::getInstance()->update(); - - if ($this->rootComponent) { - $this->rootComponent->update(); - } - } - - /** - * Render the application - */ - protected function render(): void - { - rsgl_clear($this->window, 255, 255, 255, 0); - - // Render root component tree - if ($this->rootComponent) { - $this->rootComponent->render($this->textRenderer); - $this->rootComponent->renderContent($this->textRenderer); - } - - // Render to screen - rsgl_render($this->window); - rgfw_window_swapBuffers($this->window); - } - - protected function layout(): void - { - // Render root component tree - - if ($this->rootComponent && $this->shouldBeReLayouted) { - $this->rootComponent->setViewport($this->viewport); - $this->rootComponent->setWindow($this->window); - $this->rootComponent->layout($this->textRenderer); - $this->shouldBeReLayouted = false; - } - } - - /** - * Clean up resources + * Clean up all resources */ protected function cleanup(): void { - if ($this->textRenderer) { - $this->textRenderer->free(); + foreach ($this->windows as $window) { + $window->cleanup(); } - rsgl_close($this->window); - rgfw_window_close($this->window); + $this->windows = []; } /** - * Stop the application + * Stop the application (close all windows) */ public function quit(): void { $this->running = false; } - - public function setRoot(Component $component): self - { - $this->rootComponent = $component; - return $this; - } } diff --git a/src/Tailwind/StyleParser.php b/src/Tailwind/StyleParser.php index 1e43c44..60e5cab 100644 --- a/src/Tailwind/StyleParser.php +++ b/src/Tailwind/StyleParser.php @@ -22,6 +22,12 @@ class StyleParser $styleStr = trim($styleStr); $style = self::parseSimpleStyle($styleStr); + + // Skip if style is not recognized + if($style === null) { + continue; + } + $s = new \PHPNative\Tailwind\Model\Style($style); $mq = \PHPNative\Tailwind\Parser\MediaQuery::parse($styleStr); if($mq) { diff --git a/src/Ui/Window.php b/src/Ui/Window.php index a0357f9..1328863 100644 --- a/src/Ui/Window.php +++ b/src/Ui/Window.php @@ -2,6 +2,230 @@ namespace PHPNative\Ui; +use PHPNative\Framework\TextRenderer; + class Window { + private mixed $window = null; + private ?Component $rootComponent = null; + private TextRenderer $textRenderer; + private float $mouseX = 0; + private float $mouseY = 0; + private Viewport $viewport; + private bool $shouldBeReLayouted = true; + private float $pixelRatio = 2; + private bool $shouldClose = false; + + public function __construct( + private string $title, + private int $width = 800, + private int $height = 600, + private int $x = 100, + private int $y = 100 + ) { + // Create window + $this->window = rgfw_createWindow($title, $x, $y, $width, $height, 0); + if (!$this->window) { + throw new \Exception('Failed to create window'); + } + + // Initialize RSGL renderer + if (!rsgl_init($this->window)) { + throw new \Exception('Failed to initialize RSGL renderer'); + } + + // Initialize text renderer + $this->textRenderer = new TextRenderer($this->window); + if (!$this->textRenderer->init()) { + error_log('Warning: Failed to initialize text renderer. Text rendering will not be available.'); + } + + // Get actual window size + $size = rgfw_window_getSize($this->window); + $this->width = $size[0]; + $this->height = $size[1]; + $this->viewport = new Viewport( + windowWidth: $this->width, + windowHeight: $this->height, + width: $this->width, + height: $this->height, + ); + } + + public function setRoot(Component $component): self + { + $this->rootComponent = $component; + $this->shouldBeReLayouted = true; + return $this; + } + + public function getRoot(): ?Component + { + return $this->rootComponent; + } + + public function getTitle(): string + { + return $this->title; + } + + public function getWidth(): int + { + return $this->width; + } + + public function getHeight(): int + { + return $this->height; + } + + public function shouldClose(): bool + { + return $this->shouldClose || rgfw_window_shouldClose($this->window); + } + + public function close(): void + { + $this->shouldClose = true; + } + + public function getWindowResource(): mixed + { + return $this->window; + } + + /** + * Handle window and input events + */ + public function handleEvents(): void + { + while ($event = rgfw_window_checkEvent($this->window)) { + switch ($event['type']) { + case RGFW_quit: + $this->shouldClose = true; + break; + + case RGFW_keyPressed: + $keyCode = $event['keyCode'] ?? 0; + if ($keyCode == RGFW_Escape) { + $this->shouldClose = true; + } + break; + + case RGFW_windowResized: + // Update window dimensions (from event data) + $newWidth = $event[0] ?? $this->width; + $newHeight = $event[1] ?? $this->height; + + $this->width = $newWidth; + $this->height = $newHeight; + + // Update RSGL renderer size and viewport + rsgl_updateRendererSize($this->window); + + // Update text renderer framebuffer + if ($this->textRenderer && $this->textRenderer->isInitialized()) { + $this->textRenderer->updateFramebuffer($newWidth, $newHeight); + } + + $this->viewport->x = 0; + $this->viewport->y = 0; + $this->viewport->windowWidth = $newWidth; + $this->viewport->width = $newWidth; + $this->viewport->height = $newHeight; + $this->viewport->windowHeight = $newHeight; + $this->shouldBeReLayouted = true; + break; + + case RGFW_mousePosChanged: + $this->mouseX = $event[0] ?? 0; + $this->mouseY = $event[1] ?? 0; + + // Propagate mouse move to root component + if ($this->rootComponent) { + $this->rootComponent->handleMouseMove($this->mouseX, $this->mouseY); + } + break; + + case RGFW_mouseButtonPressed: + $button = $event['button'] ?? 0; + // Propagate click to root component + if ($this->rootComponent) { + $this->rootComponent->handleMouseClick($this->mouseX, $this->mouseY, $button); + } + break; + + case RGFW_mouseButtonReleased: + $button = $event['button'] ?? 0; + + // Propagate release to root component + if ($this->rootComponent) { + $this->rootComponent->handleMouseRelease($this->mouseX, $this->mouseY, $button); + } + break; + + case RGFW_mouseScroll: + $deltaY = $event[1] ?? 0; + + // Propagate wheel to root component + if ($this->rootComponent) { + $this->rootComponent->handleMouseWheel($this->mouseX, $this->mouseY, $deltaY); + } + break; + } + } + } + + /** + * Update window state + */ + public function update(): void + { + if ($this->rootComponent) { + $this->rootComponent->update(); + } + } + + /** + * Layout components + */ + public function layout(): void + { + if ($this->rootComponent && $this->shouldBeReLayouted) { + $this->rootComponent->setViewport($this->viewport); + $this->rootComponent->setWindow($this->window); + $this->rootComponent->layout($this->textRenderer); + $this->shouldBeReLayouted = false; + } + } + + /** + * Render the window + */ + public function render(): void + { + rsgl_clear($this->window, 255, 255, 255, 0); + + // Render root component tree + if ($this->rootComponent) { + $this->rootComponent->render($this->textRenderer); + $this->rootComponent->renderContent($this->textRenderer); + } + + // Render to screen + rsgl_render($this->window); + rgfw_window_swapBuffers($this->window); + } + + /** + * Clean up resources + */ + public function cleanup(): void + { + if ($this->textRenderer) { + $this->textRenderer->free(); + } + rsgl_close($this->window); + rgfw_window_close($this->window); + } }