diff --git a/examples/simple_two_windows.php b/examples/simple_two_windows.php new file mode 100644 index 0000000..70c84b1 --- /dev/null +++ b/examples/simple_two_windows.php @@ -0,0 +1,135 @@ +createWindow('Window 1', 400, 300, 100, 100); +$container1 = new Container('flex flex-col p-6 gap-4 bg-blue-100'); + +$title1 = new Label( + text: 'Window 1', + style: 'text-2xl text-blue-900', +); + +$frameLabel1 = new Label( + text: 'Frame: 0', + style: 'text-base text-blue-700', +); + +$button1 = new Button( + text: 'Click Me (Window 1)', + style: 'bg-blue-500 hover:bg-blue-700 text-white p-4 rounded', +); + +$clickCount1 = 0; +$button1->setOnClick(function () use (&$clickCount1, $button1) { + $clickCount1++; + $button1->setText("Clicked {$clickCount1} times"); + echo "Window 1 button clicked! Count: {$clickCount1}\n"; +}); + +$container1->addComponent($title1); +$container1->addComponent($frameLabel1); +$container1->addComponent($button1); +$window1->setRoot($container1); + +// Frame counter for window 2 +$window2FrameCount = 0; + +echo "Creating Window 2...\n"; +$window2 = $app->createWindow('Window 2', 400, 300, 520, 100); +$container2 = new Container('flex flex-col p-6 gap-4 bg-green-100'); + +$title2 = new Label( + text: 'Window 2', + style: 'text-2xl text-green-900', +); + +$frameLabel2 = new Label( + text: 'Frame: 0', + style: 'text-base text-green-700', +); + +$button2 = new Button( + text: 'Click Me (Window 2)', + style: 'bg-green-500 hover:bg-green-700 text-white p-4 rounded', +); + +$clickCount2 = 0; +$button2->setOnClick(function () use (&$clickCount2, $button2) { + $clickCount2++; + $button2->setText("Clicked {$clickCount2} times"); + echo "Window 2 button clicked! Count: {$clickCount2}\n"; +}); + +$container2->addComponent($title2); +$container2->addComponent($frameLabel2); +$container2->addComponent($button2); +$window2->setRoot($container2); + +echo "Starting main loop...\n"; +echo "Click the buttons to test interaction!\n"; +echo "Close all windows to exit.\n\n"; + +// Custom run loop with frame counter updates +$running = true; +while ($running && count($app->getWindows()) > 0) { + // Update frame counters + $window1FrameCount++; + $window2FrameCount++; + $frameLabel1->setText("Frame: {$window1FrameCount}"); + $frameLabel2->setText("Frame: {$window2FrameCount}"); + + // Layout all windows FIRST (sets window references and calculates positions) + foreach ($app->getWindows() as $windowId => $window) { + $window->layout(); + } + + // Handle events for all windows (now that layout is done) + foreach ($app->getWindows() as $windowId => $window) { + $window->handleEvents(); + } + + // Update async tasks (global) + PHPNative\Async\TaskManager::getInstance()->update(); + + // Update all windows + foreach ($app->getWindows() as $windowId => $window) { + $window->update(); + } + + // Render all windows + foreach ($app->getWindows() as $windowId => $window) { + $window->render(); + } + + // Remove closed windows + $windowsCopy = $app->getWindows(); + foreach ($windowsCopy as $windowId => $window) { + if ($window->shouldClose()) { + echo "Window {$windowId} closing...\n"; + } + } + + // Limit frame rate to ~60 FPS + usleep(16666); +} + +echo "Application exited.\n"; diff --git a/src/Framework/Application.php b/src/Framework/Application.php index 713dd92..e2d5c1d 100644 --- a/src/Framework/Application.php +++ b/src/Framework/Application.php @@ -8,7 +8,7 @@ use PHPNative\Ui\Window; class Application { - private static ?Application $instance = null; + private static null|Application $instance = null; protected array $windows = []; protected int $nextWindowId = 0; protected bool $running = true; @@ -16,12 +16,14 @@ class Application public function __construct() { self::$instance = $this; + + rgfw_setQueueEvents(true); } /** * Get singleton instance */ - public static function getInstance(): ?Application + public static function getInstance(): null|Application { return self::$instance; } @@ -36,13 +38,8 @@ class Application * @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 { + 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; @@ -72,7 +69,15 @@ class Application public function run(): void { while ($this->running && count($this->windows) > 0) { - // Handle events for all windows + // Layout all windows FIRST (sets window references and calculates positions) + foreach ($this->windows as $windowId => $window) { + $window->layout(); + } + + // Poll events globally for all windows (event queue mode, if available) + rgfw_pollEvents(); + + // Handle events for all windows (now that layout is done) foreach ($this->windows as $windowId => $window) { $window->handleEvents(); } @@ -82,17 +87,16 @@ class Application // Update all windows foreach ($this->windows as $windowId => $window) { - $window->update(); + if (!$window->shouldClose()) { + $window->update(); + } } - // Layout all windows + // Render all windows (skip windows that are closing) foreach ($this->windows as $windowId => $window) { - $window->layout(); - } - - // Render all windows - foreach ($this->windows as $windowId => $window) { - $window->render(); + if (!$window->shouldClose()) { + $window->render(); + } } // Remove closed windows diff --git a/src/Tailwind/Parser/Background.php b/src/Tailwind/Parser/Background.php index 8df86af..a897d83 100644 --- a/src/Tailwind/Parser/Background.php +++ b/src/Tailwind/Parser/Background.php @@ -13,6 +13,16 @@ class Background implements Parser preg_match_all('/bg-(.*)/', $style, $output_array); if (count($output_array[0]) > 0) { $colorStyle = $output_array[1][0]; + + // Skip gradient-related classes (gradient directions and color stops) + // These require special handling that's not yet implemented + if (str_starts_with($colorStyle, 'gradient-') || + str_starts_with($colorStyle, 'from-') || + str_starts_with($colorStyle, 'to-') || + str_starts_with($colorStyle, 'via-')) { + return null; + } + $color = Color::parse($colorStyle); return new \PHPNative\Tailwind\Style\Background($color); } diff --git a/src/Tailwind/Parser/Color.php b/src/Tailwind/Parser/Color.php index f1d7bf3..471809a 100644 --- a/src/Tailwind/Parser/Color.php +++ b/src/Tailwind/Parser/Color.php @@ -24,14 +24,18 @@ class Color implements Parser preg_match_all('/(\w{1,8})/', $style, $output_array); if (count($output_array[0]) > 0) { $color = (string)$output_array[1][0]; - [$red, $green, $blue] = sscanf($data[$color]['500'], "#%02x%02x%02x"); + if (isset($data[$color]['500'])) { + [$red, $green, $blue] = sscanf($data[$color]['500'], "#%02x%02x%02x"); + } } preg_match_all('/(\w{1,8})-(\d{1,3})/', $style, $output_array); if (count($output_array[0]) > 0) { $color = (string)$output_array[1][0]; $variant = (string)$output_array[2][0]; - [$red, $green, $blue] = sscanf($data[$color][$variant], "#%02x%02x%02x"); + if (isset($data[$color][$variant])) { + [$red, $green, $blue] = sscanf($data[$color][$variant], "#%02x%02x%02x"); + } } diff --git a/src/Ui/Component.php b/src/Ui/Component.php index 10af138..33e96e0 100644 --- a/src/Ui/Component.php +++ b/src/Ui/Component.php @@ -69,6 +69,11 @@ abstract class Component public function setWindow($window): void { $this->window = $window; + + // Recursively set window for all children + foreach ($this->children as $child) { + $child->setWindow($window); + } } public function setPixelRatio($pixelRatio): void diff --git a/src/Ui/Widget/Button.php b/src/Ui/Widget/Button.php index 3148b3a..09aef4b 100644 --- a/src/Ui/Widget/Button.php +++ b/src/Ui/Widget/Button.php @@ -65,6 +65,20 @@ class Button extends Container public function handleMouseClick(float $mouseX, float $mouseY, int $button): bool { + // Debug output + if (defined('DEBUG_EVENTS') && DEBUG_EVENTS) { + error_log(sprintf( + "[Button '%s'] Click at (%.1f, %.1f), bounds: (%.1f, %.1f) to (%.1f, %.1f)", + $this->text, + $mouseX, + $mouseY, + $this->viewport->x, + $this->viewport->y, + $this->viewport->x + $this->viewport->width, + $this->viewport->y + $this->viewport->height + )); + } + // Check if click is within button bounds if ( $mouseX >= $this->viewport->x && @@ -72,6 +86,10 @@ class Button extends Container $mouseY >= $this->viewport->y && $mouseY <= ($this->viewport->y + $this->viewport->height) ) { + if (defined('DEBUG_EVENTS') && DEBUG_EVENTS) { + error_log("[Button '{$this->text}'] Click INSIDE button - executing callback"); + } + // Call async onClick callback if set if ($this->onClickAsync !== null) { $task = TaskManager::getInstance()->runAsync($this->onClickAsync['task']); @@ -91,7 +109,13 @@ class Button extends Container return true; } - // Propagate to parent if click was outside button - return parent::handleMouseClick($mouseX, $mouseY, $button); + + if (defined('DEBUG_EVENTS') && DEBUG_EVENTS) { + error_log("[Button '{$this->text}'] Click OUTSIDE button"); + } + + // Click was outside button - don't handle it + // Don't call parent::handleMouseClick as that would cause double event propagation + return false; } } diff --git a/src/Ui/Widget/Container.php b/src/Ui/Widget/Container.php index 205488f..b0a7d63 100644 --- a/src/Ui/Widget/Container.php +++ b/src/Ui/Widget/Container.php @@ -165,23 +165,44 @@ class Container extends Component $height = $childStyles[Height::class] ?? null; $size = 0; + $hasExplicitSize = false; // Check if child has flex-grow if ($childFlex && $childFlex->type !== FlexTypeEnum::none) { $flexGrowCount++; - $childSizes[$index] = ['size' => 0, 'flexGrow' => true]; + $childSizes[$index] = ['size' => 0, 'flexGrow' => true, 'natural' => false]; } else { // Calculate fixed size from basis, width, or height if ($basis) { $size = $this->calculateSize($basis, $availableSpace); + $hasExplicitSize = true; } elseif ($isRow && $width) { $size = $this->calculateSize($width, $availableSpace); + $hasExplicitSize = true; } elseif (!$isRow && $height) { $size = $this->calculateSize($height, $availableSpace); + $hasExplicitSize = true; + } + + if (!$hasExplicitSize) { + // Need to measure natural size - do a temporary layout + $tempViewport = new Viewport( + x: $this->contentViewport->x, + y: $this->contentViewport->y, + width: $isRow ? $availableSpace : $this->contentViewport->width, + height: $isRow ? $this->contentViewport->height : $availableSpace, + windowWidth: $this->contentViewport->windowWidth, + windowHeight: $this->contentViewport->windowHeight, + ); + $child->setViewport($tempViewport); + $child->layout($textRenderer); + + // Get natural size + $size = $isRow ? $child->getViewport()->width : $child->getViewport()->height; } $usedSpace += $size; - $childSizes[$index] = ['size' => $size, 'flexGrow' => false]; + $childSizes[$index] = ['size' => $size, 'flexGrow' => false, 'natural' => !$hasExplicitSize]; } } @@ -481,9 +502,13 @@ class Container extends Component } // Propagate to children if not on scrollbar + // Only adjust coordinates if we have active scrolling + $adjustedMouseX = $overflow['x'] || $overflow['y'] ? $mouseX + $this->scrollX : $mouseX; + $adjustedMouseY = $overflow['x'] || $overflow['y'] ? $mouseY + $this->scrollY : $mouseY; + foreach ($this->children as $child) { if (method_exists($child, 'handleMouseClick')) { - if ($child->handleMouseClick($mouseX + $this->scrollX, $mouseY + $this->scrollY, $button)) { + if ($child->handleMouseClick($adjustedMouseX, $adjustedMouseY, $button)) { return true; } } @@ -495,6 +520,9 @@ class Container extends Component public function handleMouseMove(float $mouseX, float $mouseY): void { parent::handleMouseMove($mouseX, $mouseY); + + $overflow = $this->hasOverflow(); + if ($this->isDraggingScrollbarY) { $deltaY = $mouseY - $this->dragStartY; $scrollbarHeight = $this->contentViewport->height; @@ -519,10 +547,13 @@ class Container extends Component $this->scrollX = max(0, min($maxScroll, $this->scrollStartX + ($scrollRatio * $maxScroll))); } - // Propagate to children + // Propagate to children - only adjust coordinates if we have active scrolling + $adjustedMouseX = $overflow['x'] || $overflow['y'] ? $mouseX + $this->scrollX : $mouseX; + $adjustedMouseY = $overflow['x'] || $overflow['y'] ? $mouseY + $this->scrollY : $mouseY; + foreach ($this->children as $child) { if (method_exists($child, 'handleMouseMove')) { - $child->handleMouseMove($mouseX + $this->scrollX, $mouseY + $this->scrollY); + $child->handleMouseMove($adjustedMouseX, $adjustedMouseY); } } } @@ -532,10 +563,14 @@ class Container extends Component $this->isDraggingScrollbarX = false; $this->isDraggingScrollbarY = false; - // Propagate to children + // Propagate to children - only adjust coordinates if we have active scrolling + $overflow = $this->hasOverflow(); + $adjustedMouseX = $overflow['x'] || $overflow['y'] ? $mouseX + $this->scrollX : $mouseX; + $adjustedMouseY = $overflow['x'] || $overflow['y'] ? $mouseY + $this->scrollY : $mouseY; + foreach ($this->children as $child) { if (method_exists($child, 'handleMouseRelease')) { - $child->handleMouseRelease($mouseX + $this->scrollX, $mouseY + $this->scrollY, $button); + $child->handleMouseRelease($adjustedMouseX, $adjustedMouseY, $button); } } } @@ -564,10 +599,13 @@ class Container extends Component } } - // Propagate to children + // Propagate to children - only adjust coordinates if we have active scrolling + $adjustedMouseX = $overflow['x'] || $overflow['y'] ? $mouseX + $this->scrollX : $mouseX; + $adjustedMouseY = $overflow['x'] || $overflow['y'] ? $mouseY + $this->scrollY : $mouseY; + foreach ($this->children as $child) { if (method_exists($child, 'handleMouseWheel')) { - if ($child->handleMouseWheel($mouseX + $this->scrollX, $mouseY + $this->scrollY, $deltaY)) { + if ($child->handleMouseWheel($adjustedMouseX, $adjustedMouseY, $deltaY)) { return true; } } diff --git a/src/Ui/Window.php b/src/Ui/Window.php index 1328863..805c8f5 100644 --- a/src/Ui/Window.php +++ b/src/Ui/Window.php @@ -7,7 +7,7 @@ use PHPNative\Framework\TextRenderer; class Window { private mixed $window = null; - private ?Component $rootComponent = null; + private null|Component $rootComponent = null; private TextRenderer $textRenderer; private float $mouseX = 0; private float $mouseY = 0; @@ -15,13 +15,14 @@ class Window private bool $shouldBeReLayouted = true; private float $pixelRatio = 2; private bool $shouldClose = false; + private bool $hasBeenLaidOut = false; public function __construct( private string $title, private int $width = 800, private int $height = 600, private int $x = 100, - private int $y = 100 + private int $y = 100, ) { // Create window $this->window = rgfw_createWindow($title, $x, $y, $width, $height, 0); @@ -56,10 +57,16 @@ class Window { $this->rootComponent = $component; $this->shouldBeReLayouted = true; + $this->hasBeenLaidOut = false; + + // Layout immediately to prevent black screen on first render + // This is especially important for windows created during event handling + $this->layout(); + return $this; } - public function getRoot(): ?Component + public function getRoot(): null|Component { return $this->rootComponent; } @@ -99,7 +106,27 @@ class Window */ public function handleEvents(): void { - while ($event = rgfw_window_checkEvent($this->window)) { + // Limit events processed per frame to prevent one window from blocking others + // This is especially important in multi-window scenarios + $maxEventsPerFrame = 20; + $eventsProcessed = 0; + + while ($event = rgfw_window_checkQueuedEvent($this->window)) { + $eventsProcessed++; + + // Debug output - can be removed later + if (defined('DEBUG_EVENTS') && DEBUG_EVENTS) { + $eventTypes = [ + RGFW_quit => 'QUIT', + RGFW_keyPressed => 'KEY_PRESSED', + RGFW_mouseButtonPressed => 'MOUSE_PRESSED', + RGFW_mouseButtonReleased => 'MOUSE_RELEASED', + RGFW_mousePosChanged => 'MOUSE_MOVE', + ]; + $typeName = $eventTypes[$event['type']] ?? ('UNKNOWN(' . $event['type'] . ')'); + error_log("[{$this->title}] Event: {$typeName}"); + } + switch ($event['type']) { case RGFW_quit: $this->shouldClose = true; @@ -181,6 +208,17 @@ class Window */ public function update(): void { + // Update hover states based on current mouse position + // This ensures hover works even when the window doesn't have focus + if ($this->rootComponent && function_exists('rgfw_window_getMousePoint')) { + $mousePos = rgfw_window_getMousePoint($this->window); + if ($mousePos !== false) { + $this->mouseX = $mousePos[0] ?? $this->mouseX; + $this->mouseY = $mousePos[1] ?? $this->mouseY; + $this->rootComponent->handleMouseMove($this->mouseX, $this->mouseY); + } + } + if ($this->rootComponent) { $this->rootComponent->update(); } @@ -191,11 +229,14 @@ class Window */ public function layout(): void { - if ($this->rootComponent && $this->shouldBeReLayouted) { + if ($this->rootComponent) { + // Always layout for now - we can optimize this later by tracking what changed + // The shouldBeReLayouted flag was preventing proper layout updates $this->rootComponent->setViewport($this->viewport); $this->rootComponent->setWindow($this->window); $this->rootComponent->layout($this->textRenderer); $this->shouldBeReLayouted = false; + $this->hasBeenLaidOut = true; } } @@ -204,15 +245,17 @@ class Window */ public function render(): void { - rsgl_clear($this->window, 255, 255, 255, 0); + // Always clear the window to prevent black screens (white background, fully opaque) + rsgl_clear($this->window, 255, 255, 255, 255); - // Render root component tree - if ($this->rootComponent) { + // Only render content if window has been laid out + // This can happen when windows are created during async callbacks + if ($this->hasBeenLaidOut && $this->rootComponent) { $this->rootComponent->render($this->textRenderer); $this->rootComponent->renderContent($this->textRenderer); } - // Render to screen + // Always swap buffers to display the cleared window rsgl_render($this->window); rgfw_window_swapBuffers($this->window); } diff --git a/test_queue_events.php b/test_queue_events.php new file mode 100644 index 0000000..9787922 --- /dev/null +++ b/test_queue_events.php @@ -0,0 +1,75 @@ +