diff --git a/examples/ServerManager/UI/ServerListTab.php b/examples/ServerManager/UI/ServerListTab.php index 9ce9e26..ec6f5dc 100644 --- a/examples/ServerManager/UI/ServerListTab.php +++ b/examples/ServerManager/UI/ServerListTab.php @@ -50,7 +50,7 @@ class ServerListTab // Refresh button $this->refreshButton = new Button( 'Server aktualisieren', - 'flex flex-row gap-2 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700', + 'flex shadow-lg/50 flex-row gap-2 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700', ); $refreshIcon = new Icon(IconName::sync, 16, 'text-white'); $this->refreshButton->setIcon($refreshIcon); @@ -104,7 +104,7 @@ class ServerListTab // SFTP Manager Button (handler will be set by SftpManagerTab) $this->sftpButton = new Button( 'SFTP Manager öffnen', - 'w-full border border-gray-300 rounded px-3 py-2 flex shadow-lg flex-row gap-2 bg-green-300 text-black mb-2', + 'w-full border border-gray-300 rounded px-3 py-2 flex shadow-lg/50 shadow-cyan-500 flex-row gap-2 bg-green-300 text-black mb-2', ); $sftpIcon = new Icon(IconName::folder, 16, 'text-white'); $this->sftpButton->setIcon($sftpIcon); diff --git a/examples/alpha_test.php b/examples/alpha_test.php new file mode 100644 index 0000000..b822a5d --- /dev/null +++ b/examples/alpha_test.php @@ -0,0 +1,49 @@ + 50, 'y' => 100, 'w' => 100, 'h' => 100]); + + // Green rectangle - alpha 30 + sdl_set_render_draw_color($renderer, 0, 255, 0, 30); + sdl_render_fill_rect($renderer, ['x' => 100, 'y' => 100, 'w' => 100, 'h' => 100]); + + // Blue rectangle - alpha 30 + sdl_set_render_draw_color($renderer, 0, 0, 255, 30); + sdl_render_fill_rect($renderer, ['x' => 150, 'y' => 100, 'w' => 100, 'h' => 100]); + + // Test: 10 black layers with alpha 3 + for ($i = 0; $i < 10; $i++) { + sdl_set_render_draw_color($renderer, 0, 0, 0, 3); + sdl_render_fill_rect($renderer, ['x' => 50, 'y' => 250, 'w' => 200 + $i * 10, 'h' => 100]); + } + } +} + +$mainContainer = new AlphaTestContainer('flex items-center justify-center bg-gray-200'); + +// Set window content and run +$window->setRoot($mainContainer); +$app->addWindow($window); +$app->run(); diff --git a/examples/shadow_simple.php b/examples/shadow_simple.php index adc9917..51fa0fe 100644 --- a/examples/shadow_simple.php +++ b/examples/shadow_simple.php @@ -13,8 +13,8 @@ $window = new Window('Simple Shadow Test', 400, 400); // Main container with light background $mainContainer = new Container('flex items-center justify-center bg-gray-200'); -// Simple box with shadow -$box = new Container('w-48 h-48 bg-white rounded-lg shadow-lg'); +// Simple box with shadow - add bg-white so it's visible +$box = new Container('w-48 h-48 bg-white rounded-lg shadow-lg shadow-rose-500'); $mainContainer->addComponent($box); diff --git a/examples/shadow_test.php b/examples/shadow_test.php index d69b237..1b0d240 100644 --- a/examples/shadow_test.php +++ b/examples/shadow_test.php @@ -3,70 +3,42 @@ require_once __DIR__ . '/../vendor/autoload.php'; use PHPNative\Framework\Application; -use PHPNative\Ui\Widget\Button; use PHPNative\Ui\Widget\Container; -use PHPNative\Ui\Widget\Label; use PHPNative\Ui\Window; // Create application $app = new Application(); -$window = new Window('Shadow Test', 800, 600); +$window = new Window('Shadow Color & Opacity Test', 800, 600); -// Main container -$mainContainer = new Container('flex flex-col gap-4 p-8'); +$mainContainer = new Container('flex flex-col gap-6 items-center justify-center bg-gray-100 p-8'); -// Title -$title = new Label('Tailwind Shadow Test', 'text-2xl font-bold text-black mb-4'); -$mainContainer->addComponent($title); +// Test 1: Default shadow (black) +$box1 = new Container('w-48 h-32 bg-white rounded-lg shadow-lg flex items-center justify-center'); -// Container for buttons -$buttonContainer = new Container('flex flex-col gap-6 items-center'); +// Test 2: Shadow with opacity modifier (50% opacity) +$box2 = new Container('w-48 h-32 bg-white rounded-lg shadow-lg/50 flex items-center justify-center'); -// Test different shadow sizes -$shadows = [ - 'shadow-sm' => 'Small Shadow', - 'shadow' => 'Base Shadow', - 'shadow-md' => 'Medium Shadow', - 'shadow-lg' => 'Large Shadow', - 'shadow-xl' => 'Extra Large Shadow', - 'shadow-2xl' => '2X Large Shadow', - 'shadow-inner' => 'Inner Shadow', -]; +// Test 3: Red shadow +$box3 = new Container('w-48 h-32 bg-white rounded-lg shadow-red-500 flex items-center justify-center'); -foreach ($shadows as $shadowClass => $label) { - // Container for each example - $exampleContainer = new Container('flex flex-row gap-4 items-center w-full'); +// Test 4: Blue shadow with opacity +$box4 = new Container('w-48 h-32 bg-white rounded-lg shadow-blue-500/30 flex items-center justify-center'); - // Label - $labelWidget = new Label($label, 'w-48 text-black text-sm'); - $exampleContainer->addComponent($labelWidget); +// Test 5: Green shadow with large size +$box5 = new Container('w-48 h-32 bg-white rounded-lg shadow-xl shadow-green-500 flex items-center justify-center'); - // Button with shadow - $button = new Button('Button with ' . $shadowClass, 'px-6 py-3 text-white ' . $shadowClass); - $exampleContainer->addComponent($button); +// Create row containers for better layout +$row1 = new Container('flex gap-6'); +$row1->addComponent($box1); +$row1->addComponent($box2); +$row1->addComponent($box3); - // Card with shadow - $card = new Container('px-6 py-4 ' . $shadowClass); - $cardLabel = new Label('Card', 'text-black text-sm'); - $card->addComponent($cardLabel); - $exampleContainer->addComponent($card); +$row2 = new Container('flex gap-6'); +$row2->addComponent($box4); +$row2->addComponent($box5); - $buttonContainer->addComponent($exampleContainer); -} - -// Add no shadow example -$noShadowContainer = new Container('flex flex-row gap-4 items-center w-full'); -$noShadowLabel = new Label('No Shadow', 'w-48 text-black text-sm'); -$noShadowContainer->addComponent($noShadowLabel); -$noShadowButton = new Button('Button without shadow', 'px-6 py-3 bg-blue-500 text-white rounded-lg shadow-none'); -$noShadowContainer->addComponent($noShadowButton); -$noShadowCard = new Container('px-6 py-4 bg-white rounded-lg shadow-none'); -$noShadowCardLabel = new Label('Card', 'text-black text-sm'); -$noShadowCard->addComponent($noShadowCardLabel); -$noShadowContainer->addComponent($noShadowCard); -$buttonContainer->addComponent($noShadowContainer); - -$mainContainer->addComponent($buttonContainer); +$mainContainer->addComponent($row1); +$mainContainer->addComponent($row2); // Set window content and run $window->setRoot($mainContainer); diff --git a/php-sdl3/.libs/sdl3.o b/php-sdl3/.libs/sdl3.o index 991b1e1..47165e3 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 cd9adfd..9a67a5a 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 cd9adfd..9a67a5a 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 74675ae..93c2dd7 100644 --- a/php-sdl3/sdl3.c +++ b/php-sdl3/sdl3.c @@ -425,6 +425,63 @@ PHP_FUNCTION(sdl_destroy_texture) { RETURN_TRUE; } +PHP_FUNCTION(sdl_update_texture) { + zval *tex_res, *rect_arr, *pixels_arr; + SDL_Texture *texture; + SDL_Rect *rect = NULL, tmp_rect; + zend_long pitch; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "ra!al", &tex_res, &rect_arr, &pixels_arr, &pitch) == FAILURE) { + RETURN_THROWS(); + } + + texture = (SDL_Texture *)zend_fetch_resource(Z_RES_P(tex_res), "SDL_Texture", le_sdl_texture); + if (!texture) { + RETURN_FALSE; + } + + // Parse rect if provided + if (rect_arr != NULL && Z_TYPE_P(rect_arr) == IS_ARRAY) { + HashTable *rect_ht = Z_ARRVAL_P(rect_arr); + zval *val; + + if ((val = zend_hash_str_find(rect_ht, "x", sizeof("x")-1)) != NULL) { + tmp_rect.x = (int)zval_get_long(val); + } + if ((val = zend_hash_str_find(rect_ht, "y", sizeof("y")-1)) != NULL) { + tmp_rect.y = (int)zval_get_long(val); + } + if ((val = zend_hash_str_find(rect_ht, "w", sizeof("w")-1)) != NULL) { + tmp_rect.w = (int)zval_get_long(val); + } + if ((val = zend_hash_str_find(rect_ht, "h", sizeof("h")-1)) != NULL) { + tmp_rect.h = (int)zval_get_long(val); + } + rect = &tmp_rect; + } + + // Convert PHP array to pixel data + HashTable *pixels_ht = Z_ARRVAL_P(pixels_arr); + int pixel_count = zend_hash_num_elements(pixels_ht); + Uint32 *pixels = emalloc(pixel_count * sizeof(Uint32)); + + zval *pixel_val; + int i = 0; + ZEND_HASH_FOREACH_VAL(pixels_ht, pixel_val) { + pixels[i++] = (Uint32)zval_get_long(pixel_val); + } ZEND_HASH_FOREACH_END(); + + // Update texture + if (SDL_UpdateTexture(texture, rect, pixels, (int)pitch) < 0) { + efree(pixels); + php_error_docref(NULL, E_WARNING, "Failed to update texture: %s", SDL_GetError()); + RETURN_FALSE; + } + + efree(pixels); + RETURN_TRUE; +} + PHP_FUNCTION(sdl_set_texture_blend_mode) { zval *tex_res; SDL_Texture *texture; @@ -786,6 +843,13 @@ 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_update_texture, 0, 0, 4) + ZEND_ARG_INFO(0, texture) + ZEND_ARG_INFO(0, rect) + ZEND_ARG_ARRAY_INFO(0, pixels, 0) + ZEND_ARG_INFO(0, pitch) +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) @@ -871,6 +935,7 @@ const zend_function_entry sdl3_functions[] = { 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_update_texture, arginfo_sdl_update_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) diff --git a/src/Tailwind/Model/StyleCollection.php b/src/Tailwind/Model/StyleCollection.php index a0b6db8..6c9a33f 100644 --- a/src/Tailwind/Model/StyleCollection.php +++ b/src/Tailwind/Model/StyleCollection.php @@ -46,6 +46,8 @@ class StyleCollection extends TypedCollection \PHPNative\Tailwind\Parser\Text::merge($tmp[$style->style::class], $style->style); }elseif(isset($tmp[$style->style::class]) && $style->style::class === Flex::class) { \PHPNative\Tailwind\Parser\Flex::merge($tmp[$style->style::class], $style->style); + }elseif(isset($tmp[$style->style::class]) && $style->style::class === \PHPNative\Tailwind\Style\Shadow::class) { + \PHPNative\Tailwind\Parser\Shadow::merge($tmp[$style->style::class], $style->style); }else{ $tmp[$style->style::class] = $style->style; } diff --git a/src/Tailwind/Parser/Shadow.php b/src/Tailwind/Parser/Shadow.php index dae444e..5cb803f 100644 --- a/src/Tailwind/Parser/Shadow.php +++ b/src/Tailwind/Parser/Shadow.php @@ -8,14 +8,49 @@ class Shadow implements Parser { public static function parse(string $style): ?\PHPNative\Tailwind\Style\Shadow { - // shadow-sm, shadow, shadow-md, shadow-lg, shadow-xl, shadow-2xl, shadow-inner, shadow-none - if (preg_match('/\bshadow-(sm|md|lg|xl|2xl|inner|none)\b/', $style, $matches)) { - return new \PHPNative\Tailwind\Style\Shadow($matches[1]); + $size = null; + $color = new \PHPNative\Tailwind\Style\Color(); + $opacity = null; + + // Parse shadow size with optional opacity (e.g., shadow-lg/50, shadow-xl/75) + if (preg_match('/\bshadow-(sm|md|lg|xl|2xl|inner|none)(?:\/(\d+))?\b/', $style, $matches)) { + $size = $matches[1]; + if (isset($matches[2])) { + $opacity = (int)$matches[2]; + } + } + // shadow (base shadow) with optional opacity (e.g., shadow/50) + elseif (preg_match('/\bshadow(?:\/(\d+))?\b/', $style, $matches)) { + // Only match plain 'shadow' or 'shadow/XX', not 'shadow-color-XXX' + if (!preg_match('/\bshadow-[a-z]+-\d+/', $style)) { + $size = 'base'; + if (isset($matches[1])) { + $opacity = (int)$matches[1]; + } + } } - // shadow (base shadow) - if (preg_match('/\bshadow\b/', $style)) { - return new \PHPNative\Tailwind\Style\Shadow('base'); + // Parse shadow color (e.g., shadow-red-500, shadow-blue-300) + if (preg_match('/\bshadow-([a-z]+)-(\d+)(?:\/(\d+))?\b/', $style, $matches)) { + $colorStr = $matches[1] . '-' . $matches[2]; + $color = Color::parse($colorStr); + if (isset($matches[3])) { + $opacity = (int)$matches[3]; + } + // If color is specified but no size was found, use base shadow + if ($size === null) { + $size = 'base'; + } + } + + // If we still don't have a size, but have color or opacity, default to 'none' + if ($size === null) { + $size = 'none'; + } + + // Return shadow style if we found a valid shadow class + if ($size !== 'none' || $opacity !== null || $color->red >= 0) { + return new \PHPNative\Tailwind\Style\Shadow($size, $color, $opacity); } return null; @@ -23,8 +58,24 @@ class Shadow implements Parser public static function merge(\PHPNative\Tailwind\Style\Shadow $class, \PHPNative\Tailwind\Style\Shadow $style) { - if ($style->size !== 'none') { + // Only merge size if it's not 'base' (which is the default for color-only shadows) + // or if the existing size is 'none' + if ($style->size !== 'none' && $style->size !== 'base') { $class->size = $style->size; + } elseif ($style->size === 'base' && $class->size === 'none') { + // Only set to 'base' if current size is 'none' + $class->size = $style->size; + } + + if ($style->color->red >= 0 || $style->color->green >= 0 || $style->color->blue >= 0) { + // Copy color values instead of reference + $class->color->red = $style->color->red; + $class->color->green = $style->color->green; + $class->color->blue = $style->color->blue; + $class->color->alpha = $style->color->alpha; + } + if ($style->opacity !== null) { + $class->opacity = $style->opacity; } } } diff --git a/src/Tailwind/Style/Shadow.php b/src/Tailwind/Style/Shadow.php index 71e2257..0e45d7c 100644 --- a/src/Tailwind/Style/Shadow.php +++ b/src/Tailwind/Style/Shadow.php @@ -9,5 +9,6 @@ class Shadow implements Style public function __construct( public string $size = 'none', // none, sm, base, md, lg, xl, 2xl, inner public Color $color = new Color(), + public ?int $opacity = null, // 0-100, null means use default ) {} } diff --git a/src/Tailwind/StyleParser.php b/src/Tailwind/StyleParser.php index 98c20bd..f51229b 100644 --- a/src/Tailwind/StyleParser.php +++ b/src/Tailwind/StyleParser.php @@ -63,6 +63,10 @@ class StyleParser private static function parseSimpleStyle(string $style): ?Style { + // Shadow must be parsed before Width to avoid "shadow-*" being matched as "w-*" + if($s = \PHPNative\Tailwind\Parser\Shadow::parse($style)) { + return $s; + } if($pd = \PHPNative\Tailwind\Parser\Padding::parse($style)) { return $pd; } @@ -96,9 +100,6 @@ class StyleParser if($b = \PHPNative\Tailwind\Parser\Border::parse($style)) { return $b; } - if($s = \PHPNative\Tailwind\Parser\Shadow::parse($style)) { - return $s; - } return null; } diff --git a/src/Ui/Component.php b/src/Ui/Component.php index e72a4f8..9f53f5d 100644 --- a/src/Ui/Component.php +++ b/src/Ui/Component.php @@ -37,6 +37,9 @@ abstract class Component protected bool $useTextureCache = false; protected bool $textureCacheValid = false; + protected $cachedShadowTexture = null; // Cached shadow texture + protected bool $shadowCacheValid = false; + protected Viewport $viewport; protected array $computedStyles = []; @@ -174,7 +177,12 @@ abstract class Component sdl_destroy_texture($this->cachedHoverTexture); $this->cachedHoverTexture = null; } + if ($this->cachedShadowTexture !== null) { + sdl_destroy_texture($this->cachedShadowTexture); + $this->cachedShadowTexture = null; + } $this->textureCacheValid = false; + $this->shadowCacheValid = false; $this->renderDirty = true; } @@ -840,7 +848,105 @@ abstract class Component } /** - * Render shadow effect for the component + * Render shadow if this component has one + * Called by parent container before rendering the component itself + */ + public function renderShadowIfPresent(&$renderer): void + { + if (isset($this->computedStyles[\PHPNative\Tailwind\Style\Shadow::class])) { + $shadow = $this->computedStyles[\PHPNative\Tailwind\Style\Shadow::class]; + if ($shadow instanceof \PHPNative\Tailwind\Style\Shadow) { + $this->renderShadow($renderer, $shadow); + } + } + } + + /** + * Box blur algorithm for shadow texture + */ + private function boxBlur(array &$pixels, int $width, int $height, int $radius): void + { + $temp = array_fill(0, $width * $height, 0); + + for ($y = 0; $y < $height; $y++) { + for ($x = 0; $x < $width; $x++) { + $r = 0; $g = 0; $b = 0; $a = 0; $count = 0; + + for ($dy = -$radius; $dy <= $radius; $dy++) { + for ($dx = -$radius; $dx <= $radius; $dx++) { + $nx = $x + $dx; + $ny = $y + $dy; + + if ($nx >= 0 && $nx < $width && $ny >= 0 && $ny < $height) { + $pixel = $pixels[$ny * $width + $nx]; + $r += ($pixel >> 16) & 0xFF; + $g += ($pixel >> 8) & 0xFF; + $b += $pixel & 0xFF; + $a += ($pixel >> 24) & 0xFF; + $count++; + } + } + } + + $r = (int)($r / $count); + $g = (int)($g / $count); + $b = (int)($b / $count); + $a = (int)($a / $count); + + $temp[$y * $width + $x] = ($a << 24) | ($r << 16) | ($g << 8) | $b; + } + } + + $pixels = $temp; + } + + /** + * Create a blurred shadow texture + */ + private function createShadowTexture(&$renderer, int $width, int $height, int $blurRadius, int $alpha, int $r = 0, int $g = 0, int $b = 0): mixed + { + // Create pixel array (ARGB8888 format) - start with transparent + $pixels = array_fill(0, $width * $height, 0x00000000); + + // Create color value in ARGB format + $color = ($alpha << 24) | ($r << 16) | ($g << 8) | $b; + + // Fill only the inner rectangle with semi-transparent color + // Leave a border for the blur to spread into + $margin = $blurRadius + 2; + for ($y = $margin; $y < $height - $margin; $y++) { + for ($x = $margin; $x < $width - $margin; $x++) { + $pixels[$y * $width + $x] = $color; + } + } + + // Apply box blur - this will spread the alpha to the edges + $this->boxBlur($pixels, $width, $height, $blurRadius); + + // Create texture + $texture = sdl_create_texture( + $renderer, + SDL_PIXELFORMAT_ARGB8888, + SDL_TEXTUREACCESS_STATIC, + $width, + $height + ); + + if (!$texture) { + return null; + } + + // Update texture with pixel data + sdl_update_texture($texture, null, $pixels, $width * 4); + + // Set blend mode for alpha blending + sdl_set_texture_blend_mode($texture, SDL_BLENDMODE_BLEND); + + return $texture; + } + + /** + * Render shadow effect for the component using texture-based approach */ private function renderShadow(&$renderer, \PHPNative\Tailwind\Style\Shadow $shadow): void { @@ -849,114 +955,66 @@ abstract class Component } // Define shadow properties based on size - // Using fewer layers with proper alpha distribution + // Alpha values much lower for transparent shadows (0-255 range) $shadowProps = match ($shadow->size) { - 'sm' => ['blur' => 3, 'spread' => 1, 'offsetX' => 0, 'offsetY' => 1, 'alpha' => 40], - 'base' => ['blur' => 4, 'spread' => 2, 'offsetX' => 0, 'offsetY' => 2, 'alpha' => 45], - 'md' => ['blur' => 6, 'spread' => 3, 'offsetX' => 0, 'offsetY' => 4, 'alpha' => 50], - 'lg' => ['blur' => 8, 'spread' => 5, 'offsetX' => 0, 'offsetY' => 6, 'alpha' => 55], - 'xl' => ['blur' => 10, 'spread' => 7, 'offsetX' => 0, 'offsetY' => 10, 'alpha' => 60], - '2xl' => ['blur' => 15, 'spread' => 10, 'offsetX' => 0, 'offsetY' => 15, 'alpha' => 70], - 'inner' => ['blur' => 4, 'spread' => 2, 'offsetX' => 0, 'offsetY' => 0, 'alpha' => 45, 'inner' => true], - default => ['blur' => 4, 'spread' => 2, 'offsetX' => 0, 'offsetY' => 2, 'alpha' => 45], + 'sm' => ['blur' => 2, 'offsetX' => 0, 'offsetY' => 1, 'alpha' => 25], + 'base' => ['blur' => 3, 'offsetX' => 0, 'offsetY' => 2, 'alpha' => 35], + 'md' => ['blur' => 4, 'offsetX' => 0, 'offsetY' => 4, 'alpha' => 45], + 'lg' => ['blur' => 6, 'offsetX' => 0, 'offsetY' => 10, 'alpha' => 60], + 'xl' => ['blur' => 10, 'offsetX' => 0, 'offsetY' => 20, 'alpha' => 75], + '2xl' => ['blur' => 15, 'offsetX' => 0, 'offsetY' => 25, 'alpha' => 90], + 'inner' => ['blur' => 3, 'offsetX' => 0, 'offsetY' => 0, 'alpha' => 35, 'inner' => true], + default => ['blur' => 3, 'offsetX' => 0, 'offsetY' => 2, 'alpha' => 35], }; - // Get shadow color (default to black/gray if not specified) - $shadowColor = $shadow->color->isNotSet() - ? $shadow->color - : new \PHPNative\Tailwind\Style\Color(0, 0, 0, $shadowProps['alpha']); + // Apply opacity modifier if specified (0-100 range -> 0-255 range) + $alpha = $shadowProps['alpha']; + if ($shadow->opacity !== null) { + // Convert percentage to 0-255 range and apply to base alpha + $alpha = (int)(($shadow->opacity / 100) * 255); + } - // Override alpha with shadow-specific alpha if color alpha is default - if ($shadowColor->alpha === 255) { - $shadowColor = new \PHPNative\Tailwind\Style\Color( - $shadowColor->red, - $shadowColor->green, - $shadowColor->blue, - $shadowProps['alpha'], + // Get shadow color (default to black if not specified) + $r = $shadow->color->red >= 0 ? $shadow->color->red : 0; + $g = $shadow->color->green >= 0 ? $shadow->color->green : 0; + $b = $shadow->color->blue >= 0 ? $shadow->color->blue : 0; + + // Use cached shadow texture if available and valid + if ($this->shadowCacheValid && $this->cachedShadowTexture !== null) { + $shadowTexture = $this->cachedShadowTexture; + } else { + // Create shadow texture with blur + $shadowTexture = $this->createShadowTexture( + $renderer, + (int)$this->viewport->width, + (int)$this->viewport->height, + $shadowProps['blur'], + $alpha, + $r, + $g, + $b ); + + if (!$shadowTexture) { + return; + } + + // Cache the shadow texture + $this->cachedShadowTexture = $shadowTexture; + $this->shadowCacheValid = true; } - // Get border radius if present for rounded shadows - $borderRadius = 0; - $border = null; - if (isset($this->computedStyles[\PHPNative\Tailwind\Style\Border::class])) { - $border = $this->computedStyles[\PHPNative\Tailwind\Style\Border::class]; - $borderRadius = $border->roundTopLeft ?? 0; - } + // Render shadow texture with offset + $shadowRect = [ + 'x' => $this->viewport->x + $shadowProps['offsetX'], + 'y' => $this->viewport->y + $shadowProps['offsetY'], + 'w' => $this->viewport->width, + 'h' => $this->viewport->height, + ]; - // Render shadow layers (simulate blur with expanding semi-transparent layers) - // Note: Alpha blending is automatic when using alpha values < 255 in sdl_set_render_draw_color - // Render from outermost to innermost for proper blending - for ($i = $shadowProps['blur'] - 1; $i >= 0; $i--) { - // Calculate alpha that decreases towards outer layers (blur effect) - // Innermost layer (i=0) has full alpha, outermost has minimal alpha - $progress = 1.0 - ($i / $shadowProps['blur']); - $layerAlpha = (int) ($shadowProps['alpha'] * $progress); + sdl_render_texture($renderer, $shadowTexture, $shadowRect); - // Ensure minimum visibility - if ($layerAlpha < 5) { - $layerAlpha = 5; - } - - // Calculate expansion for blur effect - $expansion = $shadowProps['spread'] * ($i / $shadowProps['blur']); - - if (isset($shadowProps['inner']) && $shadowProps['inner']) { - // Inner shadow - render inside the element, shrinking inwards - $shadowX = (int) ($this->viewport->x + $expansion); - $shadowY = (int) ($this->viewport->y + $expansion); - $shadowWidth = (int) max(0, $this->viewport->width - ($expansion * 2)); - $shadowHeight = (int) max(0, $this->viewport->height - ($expansion * 2)); - } else { - // Outer shadow - expand outwards from element - $shadowX = (int) ($this->viewport->x + $shadowProps['offsetX'] - $expansion); - $shadowY = (int) ($this->viewport->y + $shadowProps['offsetY'] - $expansion); - $shadowWidth = (int) ($this->viewport->width + ($expansion * 2)); - $shadowHeight = (int) ($this->viewport->height + ($expansion * 2)); - } - - // Skip if shadow is too small or alpha is too low - if ($shadowWidth <= 0 || $shadowHeight <= 0 || $layerAlpha < 1) { - continue; - } - - if ($borderRadius > 0 && $border !== null) { - // Render rounded shadow with expanding border radius - $x2 = $shadowX + $shadowWidth; - $y2 = $shadowY + $shadowHeight; - $radiusExpansion = $expansion / 2; - - sdl_rounded_box_ex( - $renderer, - $shadowX, - $shadowY, - $x2, - $y2, - (int) (($border->roundTopLeft ?? 0) + $radiusExpansion), - (int) (($border->roundTopRight ?? 0) + $radiusExpansion), - (int) (($border->roundBottomRight ?? 0) + $radiusExpansion), - (int) (($border->roundBottomLeft ?? 0) + $radiusExpansion), - $shadowColor->red, - $shadowColor->green, - $shadowColor->blue, - $layerAlpha, - ); - } else { - // Render rectangular shadow - sdl_set_render_draw_color( - $renderer, - $shadowColor->red, - $shadowColor->green, - $shadowColor->blue, - $layerAlpha, - ); - sdl_render_fill_rect($renderer, [ - 'x' => $shadowX, - 'y' => $shadowY, - 'w' => $shadowWidth, - 'h' => $shadowHeight, - ]); - } - } + // Clean up texture only if it's not cached + // If cached, it will be destroyed in invalidateTextureCache() } } diff --git a/src/Ui/RenderStack.php b/src/Ui/RenderStack.php index b8a97b3..5873621 100644 --- a/src/Ui/RenderStack.php +++ b/src/Ui/RenderStack.php @@ -9,14 +9,68 @@ namespace PHPNative\Ui; class RenderStack { private array $layers = []; - - private Viewport $savedViewport; + private int $nextLayerId = 0; public function __construct( public $renderer, public $textRenderer, ) {} + /** + * Add a render layer to the stack + * + * @param int $zIndex Lower values render first (behind), higher values render last (in front) + * @param callable $renderCallback Function that performs the rendering + * @param array $bounds ['x' => float, 'y' => float, 'w' => float, 'h' => float] + * @return int Layer ID + */ + public function addLayer(int $zIndex, callable $renderCallback, array $bounds): int + { + $layerId = $this->nextLayerId++; + + $this->layers[$layerId] = [ + 'id' => $layerId, + 'zIndex' => $zIndex, + 'callback' => $renderCallback, + 'bounds' => $bounds, + ]; + + return $layerId; + } + + /** + * Remove a layer from the stack + */ + public function removeLayer(int $layerId): void + { + unset($this->layers[$layerId]); + } + + /** + * Clear all layers + */ + public function clear(): void + { + $this->layers = []; + } + + /** + * Render all layers sorted by z-index + */ + public function renderAll(): void + { + // Sort layers by z-index (lower first) + $sortedLayers = $this->layers; + usort($sortedLayers, function ($a, $b) { + return $a['zIndex'] <=> $b['zIndex']; + }); + + // Render each layer + foreach ($sortedLayers as $layer) { + ($layer['callback'])($this->renderer, $this->textRenderer); + } + } + public function beginRender(): void { $this->savedViewport = $component->getViewport(); diff --git a/src/Ui/Widget/Container.php b/src/Ui/Widget/Container.php index 3d83fe0..d2c234d 100644 --- a/src/Ui/Widget/Container.php +++ b/src/Ui/Widget/Container.php @@ -399,6 +399,13 @@ class Container extends Component return; } + // First pass: Render shadows of all children (behind them) + foreach ($this->children as $child) { + if (!$child->isOverlay() && method_exists($child, 'renderShadowIfPresent')) { + $child->renderShadowIfPresent($renderer); + } + } + $overflow = $this->hasOverflow(); // Save original viewport