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

View File

@ -5,6 +5,7 @@ require_once __DIR__ . '/../vendor/autoload.php';
use PHPNative\Framework\Application; use PHPNative\Framework\Application;
use PHPNative\Ui\Widget\Container; use PHPNative\Ui\Widget\Container;
use PHPNative\Ui\Widget\Label; use PHPNative\Ui\Widget\Label;
use PHPNative\Ui\Widget\Menu;
use PHPNative\Ui\Widget\MenuBar; use PHPNative\Ui\Widget\MenuBar;
use PHPNative\Ui\Widget\StatusBar; use PHPNative\Ui\Widget\StatusBar;
use PHPNative\Ui\Widget\TabContainer; use PHPNative\Ui\Widget\TabContainer;
@ -20,7 +21,7 @@ $mainContainer = new Container('bg-gray-100');
$menuBar = new MenuBar(); $menuBar = new MenuBar();
// File Menu // File Menu
$fileMenu = $menuBar->addMenu('Datei'); $fileMenu = new Menu(title: 'Datei');
$fileMenu->addItem('Neu', function () { $fileMenu->addItem('Neu', function () {
echo "Neu clicked\n"; echo "Neu clicked\n";
}); });
@ -34,13 +35,15 @@ $fileMenu->addItem('Beenden', function () use ($app) {
}); });
// Settings Menu // Settings Menu
$settingsMenu = $menuBar->addMenu('Einstellungen'); $settingsMenu = new Menu(title: 'Einstellungen');
$settingsMenu->addItem('Optionen', function () { $settingsMenu->addItem('Optionen', function () {
echo "Optionen clicked\n"; echo "Optionen clicked\n";
}); });
$settingsMenu->addItem('Sprache', function () { $settingsMenu->addItem('Sprache', function () {
echo "Sprache clicked\n"; echo "Sprache clicked\n";
}); });
$menuBar->addMenu($fileMenu);
$menuBar->addMenu($settingsMenu);
$mainContainer->addComponent($menuBar); $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_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); 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; return SUCCESS;
} }
@ -231,6 +249,37 @@ PHP_FUNCTION(sdl_render_fill_rect) {
RETURN_THROWS(); 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) { PHP_FUNCTION(sdl_render_present) {
zval *ren_res; 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) PHP_FUNCTION(sdl_rounded_box)
{ {
zval *ren_res; 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_ARG_INFO(0, rect)
ZEND_END_ARG_INFO() 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_BEGIN_ARG_INFO_EX(arginfo_sdl_render_present, 0, 0, 1)
ZEND_ARG_INFO(0, renderer) ZEND_ARG_INFO(0, renderer)
ZEND_END_ARG_INFO() 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_ARG_INFO(0, dstrect)
ZEND_END_ARG_INFO() 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_BEGIN_ARG_INFO_EX(arginfo_sdl_rounded_box, 0, 0, 10)
ZEND_ARG_INFO(0, renderer) ZEND_ARG_INFO(0, renderer)
ZEND_ARG_INFO(0, x1) 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_set_render_draw_color, arginfo_sdl_set_render_draw_color)
PHP_FE(sdl_render_clear, arginfo_sdl_render_clear) PHP_FE(sdl_render_clear, arginfo_sdl_render_clear)
PHP_FE(sdl_render_fill_rect, arginfo_sdl_render_fill_rect) 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_render_present, arginfo_sdl_render_present)
PHP_FE(sdl_delay, arginfo_sdl_delay) PHP_FE(sdl_delay, arginfo_sdl_delay)
PHP_FE(sdl_get_error, arginfo_sdl_get_error) 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_create_texture_from_surface, arginfo_sdl_create_texture_from_surface)
PHP_FE(sdl_render_texture, arginfo_sdl_render_texture) 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, arginfo_sdl_rounded_box)
PHP_FE(sdl_rounded_box_ex, arginfo_sdl_rounded_box_ex) PHP_FE(sdl_rounded_box_ex, arginfo_sdl_rounded_box_ex)
PHP_FE(sdl_set_render_clip_rect, arginfo_sdl_set_render_clip_rect) 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 // 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 * Set text color
* *

View File

@ -6,45 +6,82 @@ use PHPNative\Framework\TextRenderer;
use PHPNative\Tailwind\Style\Margin; use PHPNative\Tailwind\Style\Margin;
use PHPNative\Tailwind\Style\MediaQueryEnum; use PHPNative\Tailwind\Style\MediaQueryEnum;
use PHPNative\Tailwind\Style\Padding; use PHPNative\Tailwind\Style\Padding;
use PHPNative\Tailwind\Style\State;
use PHPNative\Tailwind\Style\StateEnum; use PHPNative\Tailwind\Style\StateEnum;
use PHPNative\Tailwind\StyleParser; use PHPNative\Tailwind\StyleParser;
abstract class Component abstract class Component
{ {
protected $children = []; protected int $id;
protected $pixelRatio; protected $children = [];
protected bool $visible = true; 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 int $zIndex = 0;
protected StateEnum $currentState = StateEnum::normal; 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 Viewport $viewport;
protected array $computedStyles = []; protected array $computedStyles = [];
protected Viewport $contentViewport; protected Viewport $contentViewport;
protected null|Window $attachedWindow = null;
public function __construct( public function __construct(
protected string $style = '', protected string $style = '',
) { ) {
// Initialize viewports with default values // Initialize viewports with default values
// These will be properly set during layout() // These will be properly set during layout()
$this->id = rand();
$this->viewport = new Viewport( $this->viewport = new Viewport(
x: 0, x: 0,
y: 0, y: 0,
width: 0, width: 0,
height: 0, height: 0,
windowWidth: 800, windowWidth: 800,
windowHeight: 600 windowHeight: 600,
); );
$this->contentViewport = clone $this->viewport; $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 public function setViewport(Viewport $viewport): void
{ {
$this->viewport = $viewport; $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 public function setZIndex(int $zIndex): void
{ {
$this->zIndex = $zIndex; $this->zIndex = $zIndex;
@ -117,6 +129,88 @@ abstract class Component
return $this->zIndex; 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 public function update(): void
{ {
foreach ($this->children as $child) { foreach ($this->children as $child) {
@ -145,6 +239,13 @@ abstract class Component
$this->contentViewport->y = (int) ($this->contentViewport->y + $p->top); $this->contentViewport->y = (int) ($this->contentViewport->y + $p->top);
$this->contentViewport->height = max(0, ($this->contentViewport->height - $p->bottom) - $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 public function render(&$renderer, null|TextRenderer $textRenderer = null): void
@ -160,7 +261,13 @@ abstract class Component
if ($this->currentState == StateEnum::hover) { if ($this->currentState == StateEnum::hover) {
sdl_set_render_draw_color($renderer, $bg->color->red, $bg->color->green, $bg->color->blue, 10); sdl_set_render_draw_color($renderer, $bg->color->red, $bg->color->green, $bg->color->blue, 10);
} else { } 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 ( if (
@ -187,17 +294,23 @@ abstract class Component
$bg->color->alpha, $bg->color->alpha,
); );
} else { } else {
sdl_render_fill_rect( sdl_render_fill_rect($renderer, [
$renderer,
[
'x' => $this->viewport->x, 'x' => $this->viewport->x,
'y' => $this->viewport->y, 'y' => $this->viewport->y,
'w' => $this->viewport->width, 'w' => $this->viewport->width,
'h' => $this->viewport->height, '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 public function renderContent(&$renderer, null|TextRenderer $textRenderer = null): void
@ -216,6 +329,11 @@ abstract class Component
public function addComponent(Component $component): void public function addComponent(Component $component): void
{ {
$this->children[] = $component; $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 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 // Default implementation: propagate to children
foreach ($this->children as $child) { foreach ($this->children as $child) {
if ($child->handleMouseClick($mouseX, $mouseY, $button)) { if ($child->handleMouseClick($mouseX, $mouseY, $button)) {
@ -275,6 +398,8 @@ abstract class Component
MediaQueryEnum::normal, MediaQueryEnum::normal,
$this->currentState, $this->currentState,
); );
// Mark as dirty since visual state changed
$this->markDirty(false, false);
} }
foreach ($this->children as $child) { foreach ($this->children as $child) {
$child->handleMouseMove($mouseX, $mouseY); $child->handleMouseMove($mouseX, $mouseY);
@ -298,6 +423,11 @@ abstract class Component
*/ */
public function handleMouseWheel(float $mouseX, float $mouseY, float $deltaY): bool 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 // Default implementation: propagate to children
foreach ($this->children as $child) { foreach ($this->children as $child) {
if ($child->handleMouseWheel($mouseX, $mouseY, $deltaY)) { if ($child->handleMouseWheel($mouseX, $mouseY, $deltaY)) {
@ -334,4 +464,69 @@ abstract class Component
} }
return false; 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 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 // Debug output
if (defined('DEBUG_EVENTS') && DEBUG_EVENTS) { if (defined('DEBUG_EVENTS') && DEBUG_EVENTS) {
error_log(sprintf( 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 public function renderContent(&$renderer, null|TextRenderer $textRenderer = null): void
{ {
if (!$this->visible) { if (!$this->visible) {
@ -519,6 +524,11 @@ class Container extends Component
public function handleMouseClick(float $mouseX, float $mouseY, int $button): bool 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 // Check if click is on scrollbar
$overflow = $this->hasOverflow(); $overflow = $this->hasOverflow();
@ -628,6 +638,11 @@ class Container extends Component
public function handleMouseWheel(float $mouseX, float $mouseY, float $deltaY): bool 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(); $overflow = $this->hasOverflow();
// Check if mouse is over this container // Check if mouse is over this container

View File

@ -3,6 +3,7 @@
namespace PHPNative\Ui\Widget; namespace PHPNative\Ui\Widget;
use PHPNative\Framework\TextRenderer; use PHPNative\Framework\TextRenderer;
use PHPNative\Tailwind\Style\Padding;
use PHPNative\Tailwind\Style\Text; use PHPNative\Tailwind\Style\Text;
use PHPNative\Ui\Component; use PHPNative\Ui\Component;
@ -10,6 +11,9 @@ class Label extends Component
{ {
private int $intrinsicWidth = 0; private int $intrinsicWidth = 0;
private int $intrinsicHeight = 0; private int $intrinsicHeight = 0;
private mixed $textTexture = null;
private int $textWidth = 0;
private int $textHeight = 0;
public function __construct( public function __construct(
public string $text = '', public string $text = '',
@ -20,7 +24,13 @@ class Label extends Component
public function setText(string $text): void public function setText(string $text): void
{ {
if ($this->text === $text) {
return;
}
$this->text = $text; $this->text = $text;
$this->clearTextTexture();
$this->markDirty(true);
} }
public function layout(null|TextRenderer $textRenderer = null): void 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])) { if (!isset($this->computedStyles[\PHPNative\Tailwind\Style\Width::class])) {
$this->viewport->width = $this->intrinsicWidth; $this->viewport->width = $this->intrinsicWidth;
$this->contentViewport->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])) { if (!isset($this->computedStyles[\PHPNative\Tailwind\Style\Height::class])) {
$this->viewport->height = $this->intrinsicHeight; $this->viewport->height = $this->intrinsicHeight;
$this->contentViewport->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 // Set text color
$color = $textStyle->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 $textRenderer->setColor($red / 255, $green / 255, $blue / 255, $alpha / 255);
$x = $this->contentViewport->x;
$y = $this->contentViewport->y;
// Draw the text if ($this->renderDirty || $this->textTexture === null) {
$textRenderer->drawText($this->text, (int) $x, (int) $y, $textStyle->size); $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 // Call parent to render children if any
parent::renderContent($window, $textRenderer); 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 class Menu extends Container
{ {
private Button $menuButton; private Button $menuButton;
private Container $dropdown;
private bool $isOpen = false; 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'); parent::__construct('relative');
// Create menu button // 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); $this->addComponent($this->menuButton);
@ -42,7 +42,6 @@ class Menu extends Container
$onClick(); $onClick();
}); });
$this->items[] = $menuItem;
$this->dropdown->addComponent($menuItem); $this->dropdown->addComponent($menuItem);
return $menuItem; return $menuItem;
@ -54,34 +53,40 @@ class Menu extends Container
public function addSeparator(): void public function addSeparator(): void
{ {
$separator = new Separator(true, 'my-1'); $separator = new Separator(true, 'my-1');
$this->items[] = $separator;
$this->dropdown->addComponent($separator); $this->dropdown->addComponent($separator);
} }
/** public function toggle(): void
* Open the menu {
*/ if ($this->isOpen) {
$this->close();
} else {
$this->open();
}
}
public function open(): void public function open(): void
{ {
if ($this->isOpen) {
return;
}
$this->isOpen = true; $this->isOpen = true;
$this->dropdown->setVisible(true); $this->dropdown->setVisible(true);
if ($this->menuBar !== null) {
$this->menuBar->notifyMenuOpened($this);
}
} }
/** public function close(bool $notify = true): void
* Close the menu
*/
public function close(): void
{ {
if (!$this->isOpen) {
return;
}
$this->isOpen = false; $this->isOpen = false;
$this->dropdown->setVisible(false); $this->dropdown->setVisible(false);
if ($notify && $this->menuBar !== null) {
$this->menuBar->notifyMenuClosed($this);
} }
/**
* Check if menu is open
*/
public function isOpen(): bool
{
return $this->isOpen;
} }
public function layout(null|TextRenderer $textRenderer = null): void public function layout(null|TextRenderer $textRenderer = null): void
@ -97,4 +102,32 @@ class Menu extends Container
$this->dropdown->getViewport()->windowHeight = $buttonViewport->windowHeight; $this->dropdown->getViewport()->windowHeight = $buttonViewport->windowHeight;
$this->dropdown->layout($textRenderer); $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 class MenuBar extends Container
{ {
/** @var Menu[] */
private array $menus = []; 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') public function __construct(string $style = 'w-full bg-gray-100 border-b border-gray-300')
{ {
parent::__construct('flex flex-row ' . $style); parent::__construct('flex flex-row ' . $style);
} }
/** public function addMenu(Menu $menu): Menu
* Add a menu to the menu bar
*
* @param string $title Menu title
* @return Menu The created menu
*/
public function addMenu(string $title): Menu
{ {
$menuIndex = count($this->menus);
$menu = new Menu($title, function () use ($menuIndex) {
$this->toggleMenu($menuIndex);
});
$this->menus[] = $menu; $this->menus[] = $menu;
$menu->setMenuBar($this);
$this->addComponent($menu); $this->addComponent($menu);
return $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 public function closeAllMenus(): void
{ {
if ($this->openMenuIndex !== null) { foreach ($this->menus as $menu) {
$this->menus[$this->openMenuIndex]->close(); $menu->close(false);
$this->openMenuIndex = null;
} }
$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 ($this->openMenu !== null && $this->openMenu !== $menu) {
$this->openMenu->close(false);
// If click was not handled by any menu, close all menus }
if (!$handled && $this->openMenuIndex !== null) { $this->openMenu = $menu;
$this->closeAllMenus();
} }
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); 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 array $rows = [];
private Container $headerContainer; private Container $headerContainer;
private Container $bodyContainer; private Container $bodyContainer;
private ?int $selectedRowIndex = null; private null|int $selectedRowIndex = null;
private $onRowSelect = null; private $onRowSelect = null;
public function __construct( public function __construct(string $style = '')
string $style = '', {
) {
parent::__construct('flex flex-col overflow-auto ' . $style); parent::__construct('flex flex-col overflow-auto ' . $style);
// Create header container // Create header container
@ -27,6 +26,14 @@ class Table extends Container
$this->addComponent($this->bodyContainer); $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 * Define columns
* *
@ -41,7 +48,7 @@ class Table extends Container
$title = $column['title'] ?? $column['key']; $title = $column['title'] ?? $column['key'];
$width = $column['width'] ?? null; $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) { if ($width) {
$style .= ' w-' . ((int) ($width / 4)); $style .= ' w-' . ((int) ($width / 4));
} else { } else {
@ -86,7 +93,7 @@ class Table extends Container
$value = $rowData[$key] ?? ''; $value = $rowData[$key] ?? '';
$width = $column['width'] ?? null; $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) { if ($width) {
$cellStyle .= ' w-' . ((int) ($width / 4)); $cellStyle .= ' w-' . ((int) ($width / 4));
} else { } else {
@ -142,7 +149,7 @@ class Table extends Container
} }
// Re-render rows to update selection // 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 * Get selected row index
*/ */
public function getSelectedRowIndex(): ?int public function getSelectedRowIndex(): null|int
{ {
return $this->selectedRowIndex; return $this->selectedRowIndex;
} }
@ -164,7 +171,7 @@ class Table extends Container
/** /**
* Get selected row data * Get selected row data
*/ */
public function getSelectedRow(): ?array public function getSelectedRow(): null|array
{ {
return $this->selectedRowIndex !== null ? ($this->rows[$this->selectedRowIndex] ?? null) : null; return $this->selectedRowIndex !== null ? ($this->rows[$this->selectedRowIndex] ?? null) : null;
} }

View File

@ -17,9 +17,7 @@ class Window
private bool $shouldBeReLayouted = true; private bool $shouldBeReLayouted = true;
private float $pixelRatio = 2; private float $pixelRatio = 2;
private bool $shouldClose = false; private bool $shouldClose = false;
private bool $hasBeenLaidOut = false;
private $onResize = null; private $onResize = null;
public function __construct( public function __construct(
private string $title, private string $title,
private int $width = 800, private int $width = 800,
@ -79,13 +77,18 @@ class Window
width: $this->width, width: $this->width,
height: $this->height, height: $this->height,
); );
} }
public function setRoot(Component $component): self public function setRoot(Component $component): self
{ {
if ($this->rootComponent !== null) {
$this->rootComponent->detachFromWindow();
}
$this->rootComponent = $component; $this->rootComponent = $component;
$this->rootComponent->attachToWindow($this);
$this->shouldBeReLayouted = true; $this->shouldBeReLayouted = true;
$this->hasBeenLaidOut = false;
// Layout immediately to prevent black screen on first render // Layout immediately to prevent black screen on first render
// This is especially important for windows created during event handling // This is especially important for windows created during event handling
@ -220,10 +223,28 @@ class Window
case SDL_EVENT_MOUSE_BUTTON_DOWN: case SDL_EVENT_MOUSE_BUTTON_DOWN:
$button = $event['button'] ?? 0; $button = $event['button'] ?? 0;
// Propagate click to root component
// Check overlays first (in reverse z-index order - highest first)
if ($this->rootComponent) { if ($this->rootComponent) {
$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); $this->rootComponent->handleMouseClick($this->mouseX, $this->mouseY, $button);
} }
}
break; break;
case SDL_EVENT_MOUSE_BUTTON_UP: case SDL_EVENT_MOUSE_BUTTON_UP:
@ -238,10 +259,27 @@ class Window
case SDL_EVENT_MOUSE_WHEEL: case SDL_EVENT_MOUSE_WHEEL:
$deltaY = $event['y'] ?? 0; $deltaY = $event['y'] ?? 0;
// Propagate wheel to root component // Check overlays first (in reverse z-index order - highest first)
if ($this->rootComponent) { if ($this->rootComponent) {
$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); $this->rootComponent->handleMouseWheel($this->mouseX, $this->mouseY, $deltaY);
} }
}
break; break;
} }
} }
@ -278,7 +316,7 @@ class Window
$this->rootComponent->setViewport($this->viewport); $this->rootComponent->setViewport($this->viewport);
$this->rootComponent->layout($this->textRenderer); $this->rootComponent->layout($this->textRenderer);
$this->shouldBeReLayouted = false; $this->shouldBeReLayouted = false;
$this->hasBeenLaidOut = true;
return true; return true;
} }
return false; return false;
@ -286,32 +324,44 @@ class Window
/** /**
* Render the 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 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_set_render_draw_color($this->renderer, 255, 255, 255, 255);
sdl_render_clear($this->renderer); 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->render($this->renderer, $this->textRenderer);
$this->rootComponent->renderContent($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(); $overlays = $this->rootComponent->collectOverlays();
usort($overlays, fn($a, $b) => $a->getZIndex() <=> $b->getZIndex());
if (!empty($overlays)) {
usort($overlays, static fn($a, $b) => $a->getZIndex() <=> $b->getZIndex());
foreach ($overlays as $overlay) { foreach ($overlays as $overlay) {
if ($overlay->isVisible()) { if (!$overlay->isVisible()) {
continue;
}
$overlay->render($this->renderer, $this->textRenderer); $overlay->render($this->renderer, $this->textRenderer);
$overlay->renderContent($this->renderer, $this->textRenderer); $overlay->renderContent($this->renderer, $this->textRenderer);
} }
} }
}
// Present the rendered content $this->rootComponent->markClean();
sdl_render_present($this->renderer); 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();