This commit is contained in:
Thomas Peterson 2025-11-13 17:35:29 +01:00
parent da0d560301
commit 2f34e4d2b2
15 changed files with 427 additions and 167 deletions

View File

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

49
examples/alpha_test.php Normal file
View File

@ -0,0 +1,49 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use PHPNative\Framework\Application;
use PHPNative\Ui\Widget\Container;
use PHPNative\Ui\Window;
// Create application
$app = new Application();
$window = new Window('Alpha Blending Test', 400, 400);
// Custom container to test alpha blending
class AlphaTestContainer extends Container
{
public function render(&$renderer, ?\PHPNative\Framework\TextRenderer $textRenderer = null): void
{
parent::render($renderer, $textRenderer);
// Enable alpha blending for renderer
sdl_set_render_draw_blend_mode($renderer, SDL_BLENDMODE_BLEND);
// Draw 3 overlapping semi-transparent rectangles
// Red rectangle - alpha 30
sdl_set_render_draw_color($renderer, 255, 0, 0, 30);
sdl_render_fill_rect($renderer, ['x' => 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();

View File

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

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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