This commit is contained in:
Thomas Peterson 2025-11-05 18:33:10 +01:00
parent 6ed95d47e5
commit 91ac766f4c
20 changed files with 1047 additions and 211 deletions

View File

@ -0,0 +1,33 @@
<?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

@ -5,6 +5,7 @@ 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;
@ -21,7 +22,7 @@ $mainContainer = new Container('flex flex-col bg-gray-100');
$menuBar = new MenuBar();
// File Menu
$fileMenu = $menuBar->addMenu('Datei');
$fileMenu = new Menu(title: 'Datei');
$fileMenu->addItem('Neu', function () {
echo "Neu clicked\n";
});
@ -35,14 +36,15 @@ $fileMenu->addItem('Beenden', function () use ($app) {
});
// Settings Menu
$settingsMenu = $menuBar->addMenu('Einstellungen');
$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);
// === 2. Tab Container (flex-1) ===
@ -50,7 +52,7 @@ $tabContainer = new TabContainer('flex-1');
// Tab 1: Table with data
$tab1 = new Container('flex flex-col p-4');
$table = new Table();
$table = new Table(style: 'bg-lime-200');
$table->setColumns([
['key' => 'id', 'title' => 'ID', 'width' => 80],
@ -73,10 +75,13 @@ $table->setData([
]);
// Row selection handler
$statusBar = null; // Will be set later
$table->setOnRowSelect(function ($index, $row) use (&$statusBar) {
if ($statusBar && $row) {
$statusBar->updateSegment(0, "Selected: {$row['name']} ({$row['email']})");
$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) {
if ($row) {
$statusLabel->setText("Selected: {$row['name']} ({$row['email']})");
}
});
@ -99,12 +104,21 @@ $mainContainer->addComponent($tabContainer);
// === 3. StatusBar ===
$statusBar = new StatusBar();
$statusBar->addSegment('Bereit', '', 0); // Flexible segment
$statusBar->addSegment('Zeilen: 10', 'border-l', 200); // Fixed width
$statusBar->addSegment('Version 1.0', 'border-l', 150); // Fixed width
$statusBar->addSegment($statusLabel);
$statusBar->addSegment(new Label(
text: 'Zeilen: 10',
style: 'basis-2/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);

View File

@ -5,6 +5,7 @@ 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;
@ -20,7 +21,7 @@ $mainContainer = new Container('bg-gray-100');
$menuBar = new MenuBar();
// File Menu
$fileMenu = $menuBar->addMenu('Datei');
$fileMenu = new Menu(title: 'Datei');
$fileMenu->addItem('Neu', function () {
echo "Neu clicked\n";
});
@ -34,13 +35,15 @@ $fileMenu->addItem('Beenden', function () use ($app) {
});
// Settings Menu
$settingsMenu = $menuBar->addMenu('Einstellungen');
$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);

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -69,6 +69,24 @@ PHP_MINIT_FUNCTION(sdl3) {
REGISTER_LONG_CONSTANT("SDL_WINDOW_MAXIMIZED", SDL_WINDOW_MAXIMIZED, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("SDL_WINDOW_HIGH_PIXEL_DENSITY", SDL_WINDOW_HIGH_PIXEL_DENSITY, CONST_CS | CONST_PERSISTENT);
// SDL Pixel Formats
REGISTER_LONG_CONSTANT("SDL_PIXELFORMAT_RGBA8888", SDL_PIXELFORMAT_RGBA8888, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("SDL_PIXELFORMAT_ARGB8888", SDL_PIXELFORMAT_ARGB8888, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("SDL_PIXELFORMAT_BGRA8888", SDL_PIXELFORMAT_BGRA8888, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("SDL_PIXELFORMAT_ABGR8888", SDL_PIXELFORMAT_ABGR8888, CONST_CS | CONST_PERSISTENT);
// SDL Texture Access
REGISTER_LONG_CONSTANT("SDL_TEXTUREACCESS_STATIC", SDL_TEXTUREACCESS_STATIC, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("SDL_TEXTUREACCESS_STREAMING", SDL_TEXTUREACCESS_STREAMING, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("SDL_TEXTUREACCESS_TARGET", SDL_TEXTUREACCESS_TARGET, CONST_CS | CONST_PERSISTENT);
// SDL Blend Modes
REGISTER_LONG_CONSTANT("SDL_BLENDMODE_NONE", SDL_BLENDMODE_NONE, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("SDL_BLENDMODE_BLEND", SDL_BLENDMODE_BLEND, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("SDL_BLENDMODE_ADD", SDL_BLENDMODE_ADD, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("SDL_BLENDMODE_MOD", SDL_BLENDMODE_MOD, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("SDL_BLENDMODE_MUL", SDL_BLENDMODE_MUL, CONST_CS | CONST_PERSISTENT);
return SUCCESS;
}
@ -231,6 +249,37 @@ PHP_FUNCTION(sdl_render_fill_rect) {
RETURN_THROWS();
}
PHP_FUNCTION(sdl_render_rect) {
zval *ren_res;
SDL_Renderer *ren;
zval *rect_arr;
HashTable *rect_ht;
zval *data;
zend_long x, y, w, h;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "ra", &ren_res, &rect_arr) == FAILURE) {
RETURN_THROWS();
}
ren = (SDL_Renderer *)zend_fetch_resource(Z_RES_P(ren_res), "SDL_Renderer", le_sdl_renderer);
if (!ren) {
RETURN_FALSE;
}
rect_ht = Z_ARRVAL_P(rect_arr);
if (((data = zend_hash_str_find(rect_ht, "x", 1)) != NULL && (x = zval_get_long(data), true)) &&
((data = zend_hash_str_find(rect_ht, "y", 1)) != NULL && (y = zval_get_long(data), true)) &&
((data = zend_hash_str_find(rect_ht, "w", 1)) != NULL && (w = zval_get_long(data), true)) &&
((data = zend_hash_str_find(rect_ht, "h", 1)) != NULL && (h = zval_get_long(data), true))) {
SDL_FRect rect = {(float)x, (float)y, (float)w, (float)h};
SDL_RenderRect(ren, &rect);
RETURN_TRUE;
}
zend_throw_error(NULL, "Invalid rectangle array passed to sdl_render_rect. Expected ['x'=>int, 'y'=>int, 'w'=>int, 'h'=>int]");
RETURN_THROWS();
}
PHP_FUNCTION(sdl_render_present) {
zval *ren_res;
@ -335,6 +384,145 @@ PHP_FUNCTION(sdl_render_texture) {
}
}
PHP_FUNCTION(sdl_create_texture) {
zval *ren_res;
SDL_Renderer *renderer;
zend_long format, access, width, height;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "rllll", &ren_res, &format, &access, &width, &height) == FAILURE) {
RETURN_THROWS();
}
renderer = (SDL_Renderer *)zend_fetch_resource(Z_RES_P(ren_res), "SDL_Renderer", le_sdl_renderer);
if (!renderer) {
RETURN_FALSE;
}
SDL_Texture *texture = SDL_CreateTexture(renderer, (Uint32)format, (int)access, (int)width, (int)height);
if (!texture) {
php_error_docref(NULL, E_WARNING, "Failed to create texture: %s", SDL_GetError());
RETURN_FALSE;
}
RETURN_RES(zend_register_resource(texture, le_sdl_texture));
}
PHP_FUNCTION(sdl_destroy_texture) {
zval *tex_res;
SDL_Texture *texture;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "r", &tex_res) == FAILURE) {
RETURN_THROWS();
}
texture = (SDL_Texture *)zend_fetch_resource(Z_RES_P(tex_res), "SDL_Texture", le_sdl_texture);
if (!texture) {
RETURN_FALSE;
}
// Delete the resource to trigger the destructor
zend_list_close(Z_RES_P(tex_res));
RETURN_TRUE;
}
PHP_FUNCTION(sdl_set_texture_blend_mode) {
zval *tex_res;
SDL_Texture *texture;
zend_long blend_mode;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "rl", &tex_res, &blend_mode) == FAILURE) {
RETURN_THROWS();
}
texture = (SDL_Texture *)zend_fetch_resource(Z_RES_P(tex_res), "SDL_Texture", le_sdl_texture);
if (!texture) {
RETURN_FALSE;
}
if (SDL_SetTextureBlendMode(texture, (SDL_BlendMode)blend_mode) < 0) {
php_error_docref(NULL, E_WARNING, "Failed to set texture blend mode: %s", SDL_GetError());
RETURN_FALSE;
}
RETURN_TRUE;
}
PHP_FUNCTION(sdl_set_texture_alpha_mod) {
zval *tex_res;
SDL_Texture *texture;
zend_long alpha;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "rl", &tex_res, &alpha) == FAILURE) {
RETURN_THROWS();
}
texture = (SDL_Texture *)zend_fetch_resource(Z_RES_P(tex_res), "SDL_Texture", le_sdl_texture);
if (!texture) {
RETURN_FALSE;
}
if (SDL_SetTextureAlphaMod(texture, (Uint8)alpha) < 0) {
php_error_docref(NULL, E_WARNING, "Failed to set texture alpha mod: %s", SDL_GetError());
RETURN_FALSE;
}
RETURN_TRUE;
}
PHP_FUNCTION(sdl_get_render_target) {
zval *ren_res;
SDL_Renderer *renderer;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "r", &ren_res) == FAILURE) {
RETURN_THROWS();
}
renderer = (SDL_Renderer *)zend_fetch_resource(Z_RES_P(ren_res), "SDL_Renderer", le_sdl_renderer);
if (!renderer) {
RETURN_FALSE;
}
SDL_Texture *texture = SDL_GetRenderTarget(renderer);
if (!texture) {
// NULL is valid - means rendering to the screen
RETURN_NULL();
}
// Return the existing texture resource (don't register a new one)
// We just return the texture pointer as a resource
RETURN_RES(zend_register_resource(texture, le_sdl_texture));
}
PHP_FUNCTION(sdl_set_render_target) {
zval *ren_res;
zval *tex_res = NULL;
SDL_Renderer *renderer;
SDL_Texture *texture = NULL;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "r|r!", &ren_res, &tex_res) == FAILURE) {
RETURN_THROWS();
}
renderer = (SDL_Renderer *)zend_fetch_resource(Z_RES_P(ren_res), "SDL_Renderer", le_sdl_renderer);
if (!renderer) {
RETURN_FALSE;
}
if (tex_res != NULL && Z_TYPE_P(tex_res) == IS_RESOURCE) {
texture = (SDL_Texture *)zend_fetch_resource(Z_RES_P(tex_res), "SDL_Texture", le_sdl_texture);
if (!texture) {
RETURN_FALSE;
}
}
if (SDL_SetRenderTarget(renderer, texture) < 0) {
php_error_docref(NULL, E_WARNING, "Failed to set render target: %s", SDL_GetError());
RETURN_FALSE;
}
RETURN_TRUE;
}
PHP_FUNCTION(sdl_rounded_box)
{
zval *ren_res;
@ -569,6 +757,11 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_render_fill_rect, 0, 0, 2)
ZEND_ARG_INFO(0, rect)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_render_rect, 0, 0, 2)
ZEND_ARG_INFO(0, renderer)
ZEND_ARG_INFO(0, rect)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_render_present, 0, 0, 1)
ZEND_ARG_INFO(0, renderer)
ZEND_END_ARG_INFO()
@ -591,6 +784,37 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_render_texture, 0, 0, 2)
ZEND_ARG_INFO(0, dstrect)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_create_texture, 0, 0, 5)
ZEND_ARG_INFO(0, renderer)
ZEND_ARG_INFO(0, format)
ZEND_ARG_INFO(0, access)
ZEND_ARG_INFO(0, width)
ZEND_ARG_INFO(0, height)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_destroy_texture, 0, 0, 1)
ZEND_ARG_INFO(0, texture)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_set_texture_blend_mode, 0, 0, 2)
ZEND_ARG_INFO(0, texture)
ZEND_ARG_INFO(0, blend_mode)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_set_texture_alpha_mod, 0, 0, 2)
ZEND_ARG_INFO(0, texture)
ZEND_ARG_INFO(0, alpha)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_get_render_target, 0, 0, 1)
ZEND_ARG_INFO(0, renderer)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_set_render_target, 0, 0, 1)
ZEND_ARG_INFO(0, renderer)
ZEND_ARG_INFO(0, texture)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_rounded_box, 0, 0, 10)
ZEND_ARG_INFO(0, renderer)
ZEND_ARG_INFO(0, x1)
@ -649,11 +873,18 @@ const zend_function_entry sdl3_functions[] = {
PHP_FE(sdl_set_render_draw_color, arginfo_sdl_set_render_draw_color)
PHP_FE(sdl_render_clear, arginfo_sdl_render_clear)
PHP_FE(sdl_render_fill_rect, arginfo_sdl_render_fill_rect)
PHP_FE(sdl_render_rect, arginfo_sdl_render_rect)
PHP_FE(sdl_render_present, arginfo_sdl_render_present)
PHP_FE(sdl_delay, arginfo_sdl_delay)
PHP_FE(sdl_get_error, arginfo_sdl_get_error)
PHP_FE(sdl_create_texture_from_surface, arginfo_sdl_create_texture_from_surface)
PHP_FE(sdl_render_texture, arginfo_sdl_render_texture)
PHP_FE(sdl_create_texture, arginfo_sdl_create_texture)
PHP_FE(sdl_destroy_texture, arginfo_sdl_destroy_texture)
PHP_FE(sdl_set_texture_blend_mode, arginfo_sdl_set_texture_blend_mode)
PHP_FE(sdl_set_texture_alpha_mod, arginfo_sdl_set_texture_alpha_mod)
PHP_FE(sdl_get_render_target, arginfo_sdl_get_render_target)
PHP_FE(sdl_set_render_target, arginfo_sdl_set_render_target)
PHP_FE(sdl_rounded_box, arginfo_sdl_rounded_box)
PHP_FE(sdl_rounded_box_ex, arginfo_sdl_rounded_box_ex)
PHP_FE(sdl_set_render_clip_rect, arginfo_sdl_set_render_clip_rect)

55
php-sdl3/test_border.php Normal file
View File

@ -0,0 +1,55 @@
<?php
// Teste die neue sdl_render_rect Funktion
if (!sdl_init(SDL_INIT_VIDEO)) {
die("SDL Init failed: " . sdl_get_error());
}
$window = sdl_create_window("Test Border", 640, 480, SDL_WINDOW_RESIZABLE);
if (!$window) {
die("Window creation failed: " . sdl_get_error());
}
$renderer = sdl_create_renderer($window);
if (!$renderer) {
die("Renderer creation failed: " . sdl_get_error());
}
$running = true;
while ($running) {
// Events verarbeiten
while (sdl_poll_event($event)) {
if ($event['type'] === SDL_EVENT_QUIT ||
$event['type'] === SDL_EVENT_WINDOW_CLOSE_REQUESTED) {
$running = false;
}
}
// Hintergrund weiß
sdl_set_render_draw_color($renderer, 255, 255, 255, 255);
sdl_render_clear($renderer);
// Rahmen zeichnen - Blau
sdl_set_render_draw_color($renderer, 0, 0, 255, 255);
sdl_render_rect($renderer, ['x' => 50, 'y' => 50, 'w' => 200, 'h' => 150]);
// Zweiter Rahmen - Rot
sdl_set_render_draw_color($renderer, 255, 0, 0, 255);
sdl_render_rect($renderer, ['x' => 100, 'y' => 100, 'w' => 300, 'h' => 200]);
// Gefülltes Rechteck zum Vergleich - Grün
sdl_set_render_draw_color($renderer, 0, 255, 0, 255);
sdl_render_fill_rect($renderer, ['x' => 300, 'y' => 250, 'w' => 150, 'h' => 100]);
// Rahmen um das gefüllte Rechteck - Schwarz
sdl_set_render_draw_color($renderer, 0, 0, 0, 255);
sdl_render_rect($renderer, ['x' => 300, 'y' => 250, 'w' => 150, 'h' => 100]);
sdl_render_present($renderer);
sdl_delay(16); // ~60 FPS
}
sdl_destroy_renderer($renderer);
sdl_destroy_window($window);
sdl_quit();

View File

@ -150,6 +150,46 @@ class TextRenderer
// Note: Texture and surface are automatically cleaned up by PHP resource destructors
}
/**
* Create an SDL texture for the given text using the current color settings.
*
* @param string $text Text to render into a texture
* @return array|null Returns ['texture' => resource, 'width' => int, 'height' => int] or null on failure
*/
public function createTextTexture(string $text): null|array
{
if (!$this->initialized || !$this->font) {
return null;
}
$r = (int) ($this->colorR * 255);
$g = (int) ($this->colorG * 255);
$b = (int) ($this->colorB * 255);
$surface = ttf_render_text_blended($this->font, $text, $r, $g, $b);
if (!$surface) {
return null;
}
$texture = sdl_create_texture_from_surface($this->renderer, $surface);
if (!$texture) {
return null;
}
$dimensions = ttf_size_text($this->font, $text);
sdl_set_texture_blend_mode($texture, SDL_BLENDMODE_BLEND);
if (\function_exists('sdl_set_texture_alpha_mod')) {
sdl_set_texture_alpha_mod($texture, (int) ($this->colorA * 255));
}
return [
'texture' => $texture,
'width' => (int) $dimensions['w'],
'height' => (int) $dimensions['h'],
];
}
/**
* Set text color
*

View File

@ -6,45 +6,82 @@ use PHPNative\Framework\TextRenderer;
use PHPNative\Tailwind\Style\Margin;
use PHPNative\Tailwind\Style\MediaQueryEnum;
use PHPNative\Tailwind\Style\Padding;
use PHPNative\Tailwind\Style\State;
use PHPNative\Tailwind\Style\StateEnum;
use PHPNative\Tailwind\StyleParser;
abstract class Component
{
protected $children = [];
protected int $id;
protected $pixelRatio;
protected $children = [];
protected bool $visible = true;
protected bool $isOverlay = false;
protected bool $overlay = false;
protected bool $layoutDirty = true;
protected bool $renderDirty = true;
protected int $zIndex = 0;
protected StateEnum $currentState = StateEnum::normal;
protected null|Component $parent = null; // Reference to parent component
protected $cachedTexture = null; // SDL texture cache for this component
protected bool $useTextureCache = false; // Disabled by default, enable per component if needed
protected Viewport $viewport;
protected array $computedStyles = [];
protected Viewport $contentViewport;
protected null|Window $attachedWindow = null;
public function __construct(
protected string $style = '',
) {
// Initialize viewports with default values
// These will be properly set during layout()
$this->id = rand();
$this->viewport = new Viewport(
x: 0,
y: 0,
width: 0,
height: 0,
windowWidth: 800,
windowHeight: 600
windowHeight: 600,
);
$this->contentViewport = clone $this->viewport;
}
/**
* Destructor - clean up resources
*/
public function __destruct()
{
$this->invalidateTextureCache();
}
/**
* Clean up component resources
*/
public function cleanup(): void
{
// Free texture cache
$this->invalidateTextureCache();
// Recursively cleanup children
foreach ($this->children as $child) {
if (method_exists($child, 'cleanup')) {
$child->cleanup();
}
}
}
public function setViewport(Viewport $viewport): void
{
$this->viewport = $viewport;
@ -82,31 +119,6 @@ abstract class Component
}
}
public function setPixelRatio($pixelRatio): void
{
$this->pixelRatio = $pixelRatio;
}
public function setVisible(bool $visible): void
{
$this->visible = $visible;
}
public function isVisible(): bool
{
return $this->visible;
}
public function setOverlay(bool $isOverlay): void
{
$this->isOverlay = $isOverlay;
}
public function isOverlay(): bool
{
return $this->isOverlay;
}
public function setZIndex(int $zIndex): void
{
$this->zIndex = $zIndex;
@ -117,6 +129,88 @@ abstract class Component
return $this->zIndex;
}
public function isVisible(): bool
{
return $this->visible;
}
public function setVisible(bool $visible): void
{
if ($this->visible !== $visible) {
$this->visible = $visible;
$this->markDirty(true);
}
}
public function setOverlay(bool $overlay): void
{
if ($this->overlay !== $overlay) {
$this->overlay = $overlay;
$this->markDirty(false);
}
}
public function isOverlay(): bool
{
return $this->overlay;
}
public function invalidateTextureCache(): void
{
if ($this->cachedTexture !== null) {
sdl_destroy_texture($this->cachedTexture);
$this->cachedTexture = null;
}
$this->renderDirty = true;
}
public function getCachedTexture()
{
return $this->cachedTexture;
}
public function setCachedTexture($texture): void
{
// Free old texture if exists
if ($this->cachedTexture !== null) {
sdl_destroy_texture($this->cachedTexture);
}
$this->cachedTexture = $texture;
}
public function useTextureCache(): bool
{
return $this->useTextureCache;
}
public function setUseTextureCache(bool $use): void
{
$this->useTextureCache = $use;
if (!$use) {
$this->invalidateTextureCache();
}
}
public function markClean(): void
{
$this->layoutDirty = false;
$this->renderDirty = false;
foreach ($this->children as $child) {
$child->markClean();
}
}
public function setParent(null|Component $parent): void
{
$this->parent = $parent;
}
public function getParent(): null|Component
{
return $this->parent;
}
public function update(): void
{
foreach ($this->children as $child) {
@ -145,6 +239,13 @@ abstract class Component
$this->contentViewport->y = (int) ($this->contentViewport->y + $p->top);
$this->contentViewport->height = max(0, ($this->contentViewport->height - $p->bottom) - $p->top);
}
if ($this->useTextureCache) {
$this->invalidateTextureCache();
}
$this->layoutDirty = false;
$this->renderDirty = true;
}
public function render(&$renderer, null|TextRenderer $textRenderer = null): void
@ -160,7 +261,13 @@ abstract class Component
if ($this->currentState == StateEnum::hover) {
sdl_set_render_draw_color($renderer, $bg->color->red, $bg->color->green, $bg->color->blue, 10);
} else {
sdl_set_render_draw_color($renderer, $bg->color->red, $bg->color->green, $bg->color->blue, $bg->color->alpha);
sdl_set_render_draw_color(
$renderer,
$bg->color->red,
$bg->color->green,
$bg->color->blue,
$bg->color->alpha,
);
}
if (
@ -187,17 +294,23 @@ abstract class Component
$bg->color->alpha,
);
} else {
sdl_render_fill_rect(
$renderer,
[
'x' => $this->viewport->x,
'y' => $this->viewport->y,
'w' => $this->viewport->width,
'h' => $this->viewport->height,
]
);
sdl_render_fill_rect($renderer, [
'x' => $this->viewport->x,
'y' => $this->viewport->y,
'w' => $this->viewport->width,
'h' => $this->viewport->height,
]);
}
}
if (defined('DEBUG_RENDERING') && DEBUG_RENDERING) {
sdl_set_render_draw_color($renderer, rand(0, 255), rand(0, 255), rand(0, 255), 10);
sdl_render_rect($renderer, [
'x' => $this->viewport->x,
'y' => $this->viewport->y,
'w' => $this->viewport->width,
'h' => $this->viewport->height,
]);
}
}
public function renderContent(&$renderer, null|TextRenderer $textRenderer = null): void
@ -216,6 +329,11 @@ abstract class Component
public function addComponent(Component $component): void
{
$this->children[] = $component;
$component->setParent($this);
if ($this->attachedWindow !== null) {
$component->attachToWindow($this->attachedWindow);
}
$this->markDirty(true); // Adding a child means we need to re-layout and re-render
}
/**
@ -244,6 +362,11 @@ abstract class Component
*/
public function handleMouseClick(float $mouseX, float $mouseY, int $button): bool
{
// Don't handle events if component is not visible
if (!$this->visible) {
return false;
}
// Default implementation: propagate to children
foreach ($this->children as $child) {
if ($child->handleMouseClick($mouseX, $mouseY, $button)) {
@ -275,6 +398,8 @@ abstract class Component
MediaQueryEnum::normal,
$this->currentState,
);
// Mark as dirty since visual state changed
$this->markDirty(false, false);
}
foreach ($this->children as $child) {
$child->handleMouseMove($mouseX, $mouseY);
@ -298,6 +423,11 @@ abstract class Component
*/
public function handleMouseWheel(float $mouseX, float $mouseY, float $deltaY): bool
{
// Don't handle events if component is not visible
if (!$this->visible) {
return false;
}
// Default implementation: propagate to children
foreach ($this->children as $child) {
if ($child->handleMouseWheel($mouseX, $mouseY, $deltaY)) {
@ -334,4 +464,69 @@ abstract class Component
}
return false;
}
public function needsLayout(): bool
{
return $this->layoutDirty;
}
public function isDirty(): bool
{
return $this->renderDirty;
}
public function markDirty(bool $requiresLayout = false, bool $bubble = true): void
{
if ($requiresLayout) {
$this->layoutDirty = true;
$this->renderDirty = true;
$this->invalidateTextureCache();
if ($this->attachedWindow !== null) {
$this->attachedWindow->setShouldBeReLayouted(true);
}
} else {
$this->renderDirty = true;
}
if ($bubble && $this->parent !== null) {
$this->parent->markDirty($requiresLayout);
return;
}
if (!$bubble && !$requiresLayout) {
$ancestor = $this->parent;
while ($ancestor !== null && $ancestor->useTextureCache()) {
$ancestor->renderDirty = true;
$ancestor->invalidateTextureCache();
$ancestor = $ancestor->getParent();
}
}
}
public function attachToWindow(Window $window): void
{
$this->attachedWindow = $window;
foreach ($this->children as $child) {
$child->attachToWindow($window);
}
}
public function detachFromWindow(): void
{
$this->attachedWindow = null;
foreach ($this->children as $child) {
$child->detachFromWindow();
}
}
/**
* @return array<Component>
*/
public function getChildren(): array
{
return $this->children;
}
}

161
src/Ui/RenderStack.php Normal file
View File

@ -0,0 +1,161 @@
<?php
namespace PHPNative\Ui;
/**
* RenderStack manages layered rendering based on z-index
* Optimizes rendering by only re-rendering dirty layers
*/
class RenderStack
{
private array $layers = [];
private Viewport $savedViewport;
public function __construct(
public $renderer,
public $textRenderer,
) {}
public function beginRender(): void
{
$this->savedViewport = $component->getViewport();
// Create texture for this component
$texture = sdl_create_texture(
$renderer,
SDL_PIXELFORMAT_RGBA8888,
SDL_TEXTUREACCESS_TARGET,
$viewport->width,
$viewport->height,
);
// Enable alpha blending for the texture
sdl_set_texture_blend_mode($texture, SDL_BLENDMODE_BLEND);
// Save current render target
$previousTarget = sdl_get_render_target($renderer);
// Set texture as render target
sdl_set_render_target($renderer, $texture);
// Clear texture with transparent background
sdl_set_render_draw_color($renderer, 0, 0, 0, 0);
sdl_render_clear($renderer);
// Temporarily adjust viewport for texture-relative rendering
$viewport = clone $this->savedViewport;
$originalX = $viewport->x;
$originalY = $viewport->y;
$viewport->x = 0;
$viewport->y = 0;
}
public function endRender(): void
{
}
public function render($renderer, $textRenderer, bool $onlyDirty = true): void
{
}
/**
* Render a single component with texture caching
*/
private function renderComponentWithCache($renderer, $textRenderer, Component $component): void
{
$viewport = $component->getViewport();
// Skip if component has no size
if ($viewport->width <= 0 || $viewport->height <= 0) {
return;
}
// Check if we should use texture caching
if ($component->useTextureCache() && !$component->isDirty() && $component->getCachedTexture() !== null) {
// Use cached texture - just copy it to the screen
$cachedTexture = $component->getCachedTexture();
sdl_render_texture($renderer, $cachedTexture, [
'x' => $viewport->x,
'y' => $viewport->y,
'w' => $viewport->width,
'h' => $viewport->height,
]);
} elseif ($component->useTextureCache() && $component->isDirty()) {
// Component is dirty - render to texture and cache it
$this->renderToTextureAndCache($renderer, $textRenderer, $component);
} else {
// Texture caching disabled - render directly
$component->render($renderer, $textRenderer);
$component->renderContent($renderer, $textRenderer);
}
// Mark as clean after rendering
$component->markClean();
}
/**
* Render component to a texture and cache it
*/
private function renderToTextureAndCache($renderer, $textRenderer, Component $component): void
{
$viewport = $component->getViewport();
// Create texture for this component
$texture = sdl_create_texture(
$renderer,
SDL_PIXELFORMAT_RGBA8888,
SDL_TEXTUREACCESS_TARGET,
$viewport->width,
$viewport->height,
);
if (!$texture) {
// Fallback: render directly if texture creation failed
$component->render($renderer, $textRenderer);
$component->renderContent($renderer, $textRenderer);
return;
}
// Enable alpha blending for the texture
sdl_set_texture_blend_mode($texture, SDL_BLENDMODE_BLEND);
// Save current render target
$previousTarget = sdl_get_render_target($renderer);
// Set texture as render target
sdl_set_render_target($renderer, $texture);
// Clear texture with transparent background
sdl_set_render_draw_color($renderer, 0, 0, 0, 0);
sdl_render_clear($renderer);
// Temporarily adjust viewport for texture-relative rendering
$originalX = $viewport->x;
$originalY = $viewport->y;
$viewport->x = 0;
$viewport->y = 0;
// Render component into texture
$component->render($renderer, $textRenderer);
$component->renderContent($renderer, $textRenderer);
// Restore original viewport position
$viewport->x = $originalX;
$viewport->y = $originalY;
// Restore previous render target
sdl_set_render_target($renderer, $previousTarget);
// Cache the texture
$component->setCachedTexture($texture);
// Now render the cached texture to screen
sdl_render_texture($renderer, $texture, [
'x' => $viewport->x,
'y' => $viewport->y,
'w' => $viewport->width,
'h' => $viewport->height,
]);
}
}

View File

@ -70,6 +70,11 @@ class Button extends Container
public function handleMouseClick(float $mouseX, float $mouseY, int $button): bool
{
// Don't handle events if button is not visible
if (!$this->visible) {
return false;
}
// Debug output
if (defined('DEBUG_EVENTS') && DEBUG_EVENTS) {
error_log(sprintf(

View File

@ -363,6 +363,11 @@ class Container extends Component
];
}
public function render(&$renderer, null|TextRenderer $textRenderer = null): void
{
parent::render($renderer, $textRenderer);
}
public function renderContent(&$renderer, null|TextRenderer $textRenderer = null): void
{
if (!$this->visible) {
@ -519,6 +524,11 @@ class Container extends Component
public function handleMouseClick(float $mouseX, float $mouseY, int $button): bool
{
// Don't handle events if container is not visible
if (!$this->visible) {
return false;
}
// Check if click is on scrollbar
$overflow = $this->hasOverflow();
@ -628,6 +638,11 @@ class Container extends Component
public function handleMouseWheel(float $mouseX, float $mouseY, float $deltaY): bool
{
// Don't handle events if container is not visible
if (!$this->visible) {
return false;
}
$overflow = $this->hasOverflow();
// Check if mouse is over this container

View File

@ -3,6 +3,7 @@
namespace PHPNative\Ui\Widget;
use PHPNative\Framework\TextRenderer;
use PHPNative\Tailwind\Style\Padding;
use PHPNative\Tailwind\Style\Text;
use PHPNative\Ui\Component;
@ -10,6 +11,9 @@ class Label extends Component
{
private int $intrinsicWidth = 0;
private int $intrinsicHeight = 0;
private mixed $textTexture = null;
private int $textWidth = 0;
private int $textHeight = 0;
public function __construct(
public string $text = '',
@ -20,7 +24,13 @@ class Label extends Component
public function setText(string $text): void
{
if ($this->text === $text) {
return;
}
$this->text = $text;
$this->clearTextTexture();
$this->markDirty(true);
}
public function layout(null|TextRenderer $textRenderer = null): void
@ -36,11 +46,20 @@ class Label extends Component
if (!isset($this->computedStyles[\PHPNative\Tailwind\Style\Width::class])) {
$this->viewport->width = $this->intrinsicWidth;
$this->contentViewport->width = $this->intrinsicWidth;
if (isset($this->computedStyles[Padding::class]) && ($pd = $this->computedStyles[Padding::class])) {
$this->viewport->width = $this->intrinsicWidth + $pd->left + $pd->right;
$this->contentViewport->width = $this->intrinsicWidth + $pd->left + $pd->right;
}
}
if (!isset($this->computedStyles[\PHPNative\Tailwind\Style\Height::class])) {
$this->viewport->height = $this->intrinsicHeight;
$this->contentViewport->height = $this->intrinsicHeight;
if (isset($this->computedStyles[Padding::class]) && ($pd = $this->computedStyles[Padding::class])) {
$this->viewport->height = $this->intrinsicHeight + $pd->top + $pd->bottom;
$this->contentViewport->height = $this->intrinsicHeight + $pd->top + $pd->bottom;
}
}
}
}
@ -56,16 +75,62 @@ class Label extends Component
// Set text color
$color = $textStyle->color;
$textRenderer->setColor($color->red / 255, $color->green / 255, $color->blue / 255, $color->alpha / 255);
$red = $color->red >= 0 ? $color->red : 0;
$green = $color->green >= 0 ? $color->green : 0;
$blue = $color->blue >= 0 ? $color->blue : 0;
$alpha = max(0, min(255, $color->alpha));
// Calculate text position based on alignment
$x = $this->contentViewport->x;
$y = $this->contentViewport->y;
$textRenderer->setColor($red / 255, $green / 255, $blue / 255, $alpha / 255);
// Draw the text
$textRenderer->drawText($this->text, (int) $x, (int) $y, $textStyle->size);
if ($this->renderDirty || $this->textTexture === null) {
$this->clearTextTexture();
$textureData = $textRenderer->createTextTexture($this->text);
if ($textureData !== null) {
$this->textTexture = $textureData['texture'];
$this->textWidth = $textureData['width'];
$this->textHeight = $textureData['height'];
if (\function_exists('sdl_set_texture_alpha_mod')) {
sdl_set_texture_alpha_mod($this->textTexture, $alpha);
}
}
}
if ($this->textTexture !== null) {
sdl_render_texture($window, $this->textTexture, [
'x' => (int) $this->contentViewport->x,
'y' => (int) $this->contentViewport->y,
'w' => $this->textWidth,
'h' => $this->textHeight,
]);
} else {
// Fallback: render text directly if texture creation failed
$textRenderer->drawText(
$this->text,
(int) $this->contentViewport->x,
(int) $this->contentViewport->y,
$textStyle->size
);
}
$this->renderDirty = false;
// Call parent to render children if any
parent::renderContent($window, $textRenderer);
}
public function __destruct()
{
$this->clearTextTexture();
parent::__destruct();
}
private function clearTextTexture(): void
{
if ($this->textTexture !== null) {
sdl_destroy_texture($this->textTexture);
$this->textTexture = null;
$this->textWidth = 0;
$this->textHeight = 0;
}
}
}

View File

@ -7,16 +7,16 @@ use PHPNative\Framework\TextRenderer;
class Menu extends Container
{
private Button $menuButton;
private Container $dropdown;
private bool $isOpen = false;
private array $items = [];
private Container $dropdown;
private null|MenuBar $menuBar = null;
public function __construct(string $title, callable $onToggle)
public function __construct(string $title)
{
parent::__construct('relative');
// Create menu button
$this->menuButton = new Button($title, 'px-4 py-2 hover:bg-gray-200', $onToggle);
$this->menuButton = new Button($title, 'px-4 py-2 hover:bg-gray-200', fn() => $this->toggle());
$this->addComponent($this->menuButton);
@ -42,7 +42,6 @@ class Menu extends Container
$onClick();
});
$this->items[] = $menuItem;
$this->dropdown->addComponent($menuItem);
return $menuItem;
@ -54,34 +53,40 @@ class Menu extends Container
public function addSeparator(): void
{
$separator = new Separator(true, 'my-1');
$this->items[] = $separator;
$this->dropdown->addComponent($separator);
}
/**
* Open the menu
*/
public function toggle(): void
{
if ($this->isOpen) {
$this->close();
} else {
$this->open();
}
}
public function open(): void
{
if ($this->isOpen) {
return;
}
$this->isOpen = true;
$this->dropdown->setVisible(true);
if ($this->menuBar !== null) {
$this->menuBar->notifyMenuOpened($this);
}
}
/**
* Close the menu
*/
public function close(): void
public function close(bool $notify = true): void
{
if (!$this->isOpen) {
return;
}
$this->isOpen = false;
$this->dropdown->setVisible(false);
}
/**
* Check if menu is open
*/
public function isOpen(): bool
{
return $this->isOpen;
if ($notify && $this->menuBar !== null) {
$this->menuBar->notifyMenuClosed($this);
}
}
public function layout(null|TextRenderer $textRenderer = null): void
@ -97,4 +102,32 @@ class Menu extends Container
$this->dropdown->getViewport()->windowHeight = $buttonViewport->windowHeight;
$this->dropdown->layout($textRenderer);
}
public function handleMouseMove(float $mouseX, float $mouseY): void
{
parent::handleMouseMove($mouseX, $mouseY);
if ($this->isOpen) {
$this->dropdown->handleMouseMove($mouseX, $mouseY);
}
}
public function render(&$renderer, null|TextRenderer $textRenderer = null): void
{
parent::render($renderer, $textRenderer);
}
public function renderContent(&$renderer, null|TextRenderer $textRenderer = null): void
{
parent::renderContent($renderer, $textRenderer);
}
public function setMenuBar(MenuBar $menuBar): void
{
$this->menuBar = $menuBar;
}
public function isOpen(): bool
{
return $this->isOpen;
}
}

View File

@ -6,77 +6,44 @@ use PHPNative\Framework\TextRenderer;
class MenuBar extends Container
{
/** @var Menu[] */
private array $menus = [];
private null|int $openMenuIndex = null;
private null|Menu $openMenu = null;
public function __construct(string $style = 'w-full bg-gray-100 border-b border-gray-300')
{
parent::__construct('flex flex-row ' . $style);
}
/**
* Add a menu to the menu bar
*
* @param string $title Menu title
* @return Menu The created menu
*/
public function addMenu(string $title): Menu
public function addMenu(Menu $menu): Menu
{
$menuIndex = count($this->menus);
$menu = new Menu($title, function () use ($menuIndex) {
$this->toggleMenu($menuIndex);
});
$this->menus[] = $menu;
$menu->setMenuBar($this);
$this->addComponent($menu);
return $menu;
}
/**
* Toggle a menu open/closed
*
* @param int $index Menu index
*/
private function toggleMenu(int $index): void
{
if ($this->openMenuIndex === $index) {
// Close the currently open menu
$this->menus[$index]->close();
$this->openMenuIndex = null;
} else {
// Close previously open menu
if ($this->openMenuIndex !== null) {
$this->menus[$this->openMenuIndex]->close();
}
// Open the clicked menu
$this->menus[$index]->open();
$this->openMenuIndex = $index;
}
}
/**
* Close all menus
*/
public function closeAllMenus(): void
{
if ($this->openMenuIndex !== null) {
$this->menus[$this->openMenuIndex]->close();
$this->openMenuIndex = null;
foreach ($this->menus as $menu) {
$menu->close(false);
}
$this->openMenu = null;
}
public function handleMouseClick(float $mouseX, float $mouseY, int $button): bool
public function notifyMenuOpened(Menu $menu): void
{
$handled = parent::handleMouseClick($mouseX, $mouseY, $button);
// If click was not handled by any menu, close all menus
if (!$handled && $this->openMenuIndex !== null) {
$this->closeAllMenus();
if ($this->openMenu !== null && $this->openMenu !== $menu) {
$this->openMenu->close(false);
}
$this->openMenu = $menu;
}
return $handled;
public function notifyMenuClosed(Menu $menu): void
{
if ($this->openMenu === $menu) {
$this->openMenu = null;
}
}
}

View File

@ -8,4 +8,9 @@ class MenuItem extends Button
{
parent::__construct($text, 'px-4 text py-2 hover:bg-gray-100 text-left w-full ' . $style, $onClick);
}
public function handleMouseMove(float $mouseX, float $mouseY): void
{
parent::handleMouseMove($mouseX, $mouseY);
}
}

View File

@ -10,12 +10,11 @@ class Table extends Container
private array $rows = [];
private Container $headerContainer;
private Container $bodyContainer;
private ?int $selectedRowIndex = null;
private null|int $selectedRowIndex = null;
private $onRowSelect = null;
public function __construct(
string $style = '',
) {
public function __construct(string $style = '')
{
parent::__construct('flex flex-col overflow-auto ' . $style);
// Create header container
@ -27,6 +26,14 @@ class Table extends Container
$this->addComponent($this->bodyContainer);
}
public function layout(null|TextRenderer $textRenderer = null): void
{
$this->headerContainer->layout($textRenderer);
$this->bodyContainer->layout($textRenderer);
parent::layout($textRenderer);
}
/**
* Define columns
*
@ -41,9 +48,9 @@ class Table extends Container
$title = $column['title'] ?? $column['key'];
$width = $column['width'] ?? null;
$style = 'px-4 py-2 font-bold border-r border-gray-300';
$style = 'px-4 py-2 text-black font-bold border-r border-gray-300';
if ($width) {
$style .= ' w-' . ((int)($width / 4));
$style .= ' w-' . ((int) ($width / 4));
} else {
$style .= ' flex-1';
}
@ -86,14 +93,14 @@ class Table extends Container
$value = $rowData[$key] ?? '';
$width = $column['width'] ?? null;
$cellStyle = 'px-4 py-2 border-r border-gray-300';
$cellStyle = 'px-4 py-2 text-black border-r border-gray-300';
if ($width) {
$cellStyle .= ' w-' . ((int)($width / 4));
$cellStyle .= ' w-' . ((int) ($width / 4));
} else {
$cellStyle .= ' flex-1';
}
$cellLabel = new Label((string)$value, $cellStyle);
$cellLabel = new Label((string) $value, $cellStyle);
$rowContainer->addComponent($cellLabel);
}
@ -115,9 +122,9 @@ class Table extends Container
// Check if click is within row bounds
if (
$mouseX >= $this->viewport->x &&
$mouseX <= ($this->viewport->x + $this->viewport->width) &&
$mouseY >= $this->viewport->y &&
$mouseY <= ($this->viewport->y + $this->viewport->height)
$mouseX <= ($this->viewport->x + $this->viewport->width) &&
$mouseY >= $this->viewport->y &&
$mouseY <= ($this->viewport->y + $this->viewport->height)
) {
$this->table->selectRow($this->rowIndex);
return true;
@ -142,7 +149,7 @@ class Table extends Container
}
// Re-render rows to update selection
$this->setData($this->rows);
// $this->setData($this->rows);
}
/**
@ -156,7 +163,7 @@ class Table extends Container
/**
* Get selected row index
*/
public function getSelectedRowIndex(): ?int
public function getSelectedRowIndex(): null|int
{
return $this->selectedRowIndex;
}
@ -164,7 +171,7 @@ class Table extends Container
/**
* Get selected row data
*/
public function getSelectedRow(): ?array
public function getSelectedRow(): null|array
{
return $this->selectedRowIndex !== null ? ($this->rows[$this->selectedRowIndex] ?? null) : null;
}

View File

@ -17,9 +17,7 @@ class Window
private bool $shouldBeReLayouted = true;
private float $pixelRatio = 2;
private bool $shouldClose = false;
private bool $hasBeenLaidOut = false;
private $onResize = null;
public function __construct(
private string $title,
private int $width = 800,
@ -79,13 +77,18 @@ class Window
width: $this->width,
height: $this->height,
);
}
public function setRoot(Component $component): self
{
if ($this->rootComponent !== null) {
$this->rootComponent->detachFromWindow();
}
$this->rootComponent = $component;
$this->rootComponent->attachToWindow($this);
$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
@ -220,9 +223,27 @@ class Window
case SDL_EVENT_MOUSE_BUTTON_DOWN:
$button = $event['button'] ?? 0;
// Propagate click to root component
// Check overlays first (in reverse z-index order - highest first)
if ($this->rootComponent) {
$this->rootComponent->handleMouseClick($this->mouseX, $this->mouseY, $button);
$overlays = $this->rootComponent->collectOverlays();
usort($overlays, fn($a, $b) => $b->getZIndex() <=> $a->getZIndex());
$handled = false;
foreach ($overlays as $overlay) {
if (
$overlay->isVisible() &&
$overlay->handleMouseClick($this->mouseX, $this->mouseY, $button)
) {
$handled = true;
break;
}
}
// If no overlay handled it, propagate to normal components
if (!$handled) {
$this->rootComponent->handleMouseClick($this->mouseX, $this->mouseY, $button);
}
}
break;
@ -238,9 +259,26 @@ class Window
case SDL_EVENT_MOUSE_WHEEL:
$deltaY = $event['y'] ?? 0;
// Propagate wheel to root component
// Check overlays first (in reverse z-index order - highest first)
if ($this->rootComponent) {
$this->rootComponent->handleMouseWheel($this->mouseX, $this->mouseY, $deltaY);
$overlays = $this->rootComponent->collectOverlays();
usort($overlays, fn($a, $b) => $b->getZIndex() <=> $a->getZIndex());
$handled = false;
foreach ($overlays as $overlay) {
if (
$overlay->isVisible() &&
$overlay->handleMouseWheel($this->mouseX, $this->mouseY, $deltaY)
) {
$handled = true;
break;
}
}
// If no overlay handled it, propagate to normal components
if (!$handled) {
$this->rootComponent->handleMouseWheel($this->mouseX, $this->mouseY, $deltaY);
}
}
break;
}
@ -278,7 +316,7 @@ class Window
$this->rootComponent->setViewport($this->viewport);
$this->rootComponent->layout($this->textRenderer);
$this->shouldBeReLayouted = false;
$this->hasBeenLaidOut = true;
return true;
}
return false;
@ -286,32 +324,44 @@ class Window
/**
* Render the window
*
* Note: With SDL3, we must clear and redraw everything each frame.
* The optimization comes from dirty tracking which prevents unnecessary
* state updates and layout recalculations, not from skipping rendering.
*/
public function render(): void
{
// Clear the window with white background
if ($this->rootComponent === null) {
return;
}
if ($this->shouldBeReLayouted) {
$this->layout();
}
sdl_set_render_draw_color($this->renderer, 255, 255, 255, 255);
sdl_render_clear($this->renderer);
// 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->renderer, $this->textRenderer);
$this->rootComponent->renderContent($this->renderer, $this->textRenderer);
$this->rootComponent->render($this->renderer, $this->textRenderer);
$this->rootComponent->renderContent($this->renderer, $this->textRenderer);
// Render all overlays last (they appear on top of everything)
$overlays = $this->rootComponent->collectOverlays();
usort($overlays, fn($a, $b) => $a->getZIndex() <=> $b->getZIndex());
$overlays = $this->rootComponent->collectOverlays();
if (!empty($overlays)) {
usort($overlays, static fn($a, $b) => $a->getZIndex() <=> $b->getZIndex());
foreach ($overlays as $overlay) {
if ($overlay->isVisible()) {
$overlay->render($this->renderer, $this->textRenderer);
$overlay->renderContent($this->renderer, $this->textRenderer);
if (!$overlay->isVisible()) {
continue;
}
$overlay->render($this->renderer, $this->textRenderer);
$overlay->renderContent($this->renderer, $this->textRenderer);
}
}
// Present the rendered content
$this->rootComponent->markClean();
sdl_render_present($this->renderer);
}

View File

@ -1,43 +0,0 @@
<?php
require_once __DIR__ . '/vendor/autoload.php';
use PHPNative\Framework\Application;
use PHPNative\Ui\Window;
use PHPNative\Ui\Widget\Button;
use PHPNative\Ui\Widget\Container;
define('DEBUG_EVENTS', true); // Enable event debugging
$app = new Application();
// Main window
$mainWindow = new Window('Main Window', 400, 300);
$mainContainer = new Container('flex flex-col p-4 bg-gray-100');
$openButton = new Button('Open Second Window', 'bg-blue-500 text-white p-4 m-2 rounded hover:bg-blue-600');
$openButton->setOnClick(function() use ($app) {
echo "Creating second window...\n";
$secondWindow = new Window('Second Window', 400, 300);
$secondContainer = new Container('flex flex-col p-4 bg-green-100');
$closeButton = new Button('Close This Window', 'bg-red-500 text-white p-4 m-2 rounded hover:bg-red-600');
$closeButton->setOnClick(function() use ($secondWindow) {
echo "Close button clicked in second window (ID: {$secondWindow->getWindowId()})\n";
$secondWindow->close();
});
$secondContainer->addComponent($closeButton);
$secondWindow->setRoot($secondContainer);
$app->addWindow($secondWindow);
echo "Second window created with ID: {$secondWindow->getWindowId()}\n";
});
$mainContainer->addComponent($openButton);
$mainWindow->setRoot($mainContainer);
$app->addWindow($mainWindow);
echo "Main window ID: {$mainWindow->getWindowId()}\n";
$app->run();