This commit is contained in:
Thomas Peterson 2025-10-24 11:39:17 +02:00
parent 899499f2e5
commit 6b021058c6
9 changed files with 378 additions and 40 deletions

View File

@ -0,0 +1,135 @@
<?php
// Enable event debugging (optional - set to false to disable)
define('DEBUG_EVENTS', true);
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;
echo "Starting two-window test application...\n";
echo 'DEBUG_EVENTS: ' . (DEBUG_EVENTS ? 'enabled' : 'disabled') . "\n";
// Create application
$app = new Application();
// Frame counter for window 1
$window1FrameCount = 0;
echo "Creating Window 1...\n";
$window1 = $app->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";

View File

@ -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,18 +87,17 @@ class Application
// Update all windows
foreach ($this->windows as $windowId => $window) {
if (!$window->shouldClose()) {
$window->update();
}
// Layout all windows
foreach ($this->windows as $windowId => $window) {
$window->layout();
}
// Render all windows
// Render all windows (skip windows that are closing)
foreach ($this->windows as $windowId => $window) {
if (!$window->shouldClose()) {
$window->render();
}
}
// Remove closed windows
foreach ($this->windows as $windowId => $window) {

View File

@ -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);
}

View File

@ -24,15 +24,19 @@ 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];
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];
if (isset($data[$color][$variant])) {
[$red, $green, $blue] = sscanf($data[$color][$variant], "#%02x%02x%02x");
}
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}

75
test_queue_events.php Normal file
View File

@ -0,0 +1,75 @@
<?php
/* Test multi-window with event queue mode */
// Enable event queue mode
rgfw_setQueueEvents(true);
$win1 = rgfw_createWindow("Window 1", 100, 100, 400, 300, 0);
$win2 = rgfw_createWindow("Window 2", 550, 100, 400, 300, 0);
rsgl_init($win1);
rsgl_init($win2);
echo "Event queue mode enabled. Testing with 2 windows...\n";
echo "Try clicking in each window to test mouse events\n";
$running = true;
$frameCount = 0;
while ($running) {
// Poll events for all windows
rgfw_pollEvents();
// Check events for window 1
while ($event = rgfw_window_checkQueuedEvent($win1)) {
echo "Window 1 - Event type: {$event['type']}\n";
if ($event['type'] == RGFW_quit) {
$running = false;
} elseif ($event['type'] == RGFW_mouseButtonPressed) {
echo " Window 1 - Mouse clicked! Button: {$event['button']}\n";
} elseif ($event['type'] == RGFW_mousePosChanged) {
echo " Window 1 - Mouse moved to: {$event[0]}, {$event[1]}\n";
}
}
// Check events for window 2
while ($event = rgfw_window_checkQueuedEvent($win2)) {
echo "Window 2 - Event type: {$event['type']}\n";
if ($event['type'] == RGFW_quit) {
$running = false;
} elseif ($event['type'] == RGFW_mouseButtonPressed) {
echo " Window 2 - Mouse clicked! Button: {$event['button']}\n";
} elseif ($event['type'] == RGFW_mousePosChanged) {
echo " Window 2 - Mouse moved to: {$event[0]}, {$event[1]}\n";
}
}
// Render window 1 (red background)
rsgl_clear($win1, 255, 0, 0, 255);
rsgl_drawRectF($win1, 150, 100, 100, 100);
rgfw_window_swapBuffers($win1);
// Render window 2 (blue background)
rsgl_clear($win2, 0, 0, 255, 255);
rsgl_drawRectF($win2, 150, 100, 100, 100);
rgfw_window_swapBuffers($win2);
$frameCount++;
if ($frameCount % 100 == 0) {
echo "Frame $frameCount - Both windows running\n";
}
// Check if windows should close
if (rgfw_window_shouldClose($win1) || rgfw_window_shouldClose($win2)) {
$running = false;
}
usleep(16000); // ~60 FPS
}
echo "Test finished after $frameCount frames\n";
rgfw_window_close($win1);
rgfw_window_close($win2);