diff --git a/examples/simple_container.php b/examples/simple_container.php new file mode 100644 index 0000000..40ab82e --- /dev/null +++ b/examples/simple_container.php @@ -0,0 +1,33 @@ +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(); diff --git a/examples/windows_app_example.php b/examples/windows_app_example.php index b8b5a8e..6b2ad6f 100644 --- a/examples/windows_app_example.php +++ b/examples/windows_app_example.php @@ -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); diff --git a/examples/windows_menu_example.php b/examples/windows_menu_example.php index 1a1fbac..c81edb4 100644 --- a/examples/windows_menu_example.php +++ b/examples/windows_menu_example.php @@ -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); diff --git a/php-sdl3/.libs/sdl3.o b/php-sdl3/.libs/sdl3.o index dc00efc..7e992ee 100644 Binary files a/php-sdl3/.libs/sdl3.o and b/php-sdl3/.libs/sdl3.o differ diff --git a/php-sdl3/.libs/sdl3.so b/php-sdl3/.libs/sdl3.so index dd5a381..83caf72 100755 Binary files a/php-sdl3/.libs/sdl3.so and b/php-sdl3/.libs/sdl3.so differ diff --git a/php-sdl3/modules/sdl3.so b/php-sdl3/modules/sdl3.so index dd5a381..83caf72 100755 Binary files a/php-sdl3/modules/sdl3.so and b/php-sdl3/modules/sdl3.so differ diff --git a/php-sdl3/sdl3.c b/php-sdl3/sdl3.c index f32270f..9ea202c 100644 --- a/php-sdl3/sdl3.c +++ b/php-sdl3/sdl3.c @@ -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) diff --git a/php-sdl3/test_border.php b/php-sdl3/test_border.php new file mode 100644 index 0000000..0ad5901 --- /dev/null +++ b/php-sdl3/test_border.php @@ -0,0 +1,55 @@ + 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(); diff --git a/src/Framework/TextRenderer.php b/src/Framework/TextRenderer.php index 4d7ff44..47a3b81 100644 --- a/src/Framework/TextRenderer.php +++ b/src/Framework/TextRenderer.php @@ -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 * diff --git a/src/Ui/Component.php b/src/Ui/Component.php index 2d23332..adf734c 100644 --- a/src/Ui/Component.php +++ b/src/Ui/Component.php @@ -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 + */ + public function getChildren(): array + { + return $this->children; + } } diff --git a/src/Ui/RenderStack.php b/src/Ui/RenderStack.php new file mode 100644 index 0000000..b8a97b3 --- /dev/null +++ b/src/Ui/RenderStack.php @@ -0,0 +1,161 @@ +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, + ]); + } +} diff --git a/src/Ui/Widget/Button.php b/src/Ui/Widget/Button.php index 5ba36fb..dfe27c1 100644 --- a/src/Ui/Widget/Button.php +++ b/src/Ui/Widget/Button.php @@ -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( diff --git a/src/Ui/Widget/Container.php b/src/Ui/Widget/Container.php index b12a9e5..952801d 100644 --- a/src/Ui/Widget/Container.php +++ b/src/Ui/Widget/Container.php @@ -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 diff --git a/src/Ui/Widget/Label.php b/src/Ui/Widget/Label.php index 3131007..1dceec1 100644 --- a/src/Ui/Widget/Label.php +++ b/src/Ui/Widget/Label.php @@ -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; + } + } } diff --git a/src/Ui/Widget/Menu.php b/src/Ui/Widget/Menu.php index 30f6af5..48acda3 100644 --- a/src/Ui/Widget/Menu.php +++ b/src/Ui/Widget/Menu.php @@ -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; + } } diff --git a/src/Ui/Widget/MenuBar.php b/src/Ui/Widget/MenuBar.php index f7d934f..d76715d 100644 --- a/src/Ui/Widget/MenuBar.php +++ b/src/Ui/Widget/MenuBar.php @@ -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; + } } } diff --git a/src/Ui/Widget/MenuItem.php b/src/Ui/Widget/MenuItem.php index 90cb68f..a5ffe83 100644 --- a/src/Ui/Widget/MenuItem.php +++ b/src/Ui/Widget/MenuItem.php @@ -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); + } } diff --git a/src/Ui/Widget/Table.php b/src/Ui/Widget/Table.php index b3c2449..827c965 100644 --- a/src/Ui/Widget/Table.php +++ b/src/Ui/Widget/Table.php @@ -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; } diff --git a/src/Ui/Window.php b/src/Ui/Window.php index e705b02..75d706d 100644 --- a/src/Ui/Window.php +++ b/src/Ui/Window.php @@ -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); } diff --git a/test_multi_window.php b/test_multi_window.php deleted file mode 100644 index 0b5700a..0000000 --- a/test_multi_window.php +++ /dev/null @@ -1,43 +0,0 @@ -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();