id = rand(); $this->viewport = new Viewport( x: 0, y: 0, width: 0, height: 0, windowWidth: 800, windowHeight: 600, ); $this->contentViewport = clone $this->viewport; } /** * Destructor - clean up resources */ public function __destruct() { $this->invalidateTextureCache(); } /** * Clean up component resources */ public function cleanup(): void { // Free texture cache $this->invalidateTextureCache(); // Recursively cleanup children foreach ($this->children as $child) { if (method_exists($child, 'cleanup')) { $child->cleanup(); } } } public function setViewport(Viewport $viewport): void { $this->viewport = $viewport; } public function getViewport(): Viewport { return $this->viewport; } public function getContentViewport(): Viewport { return $this->contentViewport; } public function setContentViewport(Viewport $viewport): void { $this->contentViewport = $viewport; } /** * Apply offset to viewport and all child viewports recursively */ public function applyScrollOffset(int $offsetX, int $offsetY): void { // Offset this component's viewports $this->viewport->x = (int) ($this->viewport->x - $offsetX); $this->viewport->y = (int) ($this->viewport->y - $offsetY); $this->contentViewport->x = (int) ($this->contentViewport->x - $offsetX); $this->contentViewport->y = (int) ($this->contentViewport->y - $offsetY); // Recursively apply to all children foreach ($this->children as $child) { $child->applyScrollOffset($offsetX, $offsetY); } } public function setZIndex(int $zIndex): void { $this->zIndex = $zIndex; } public function getZIndex(): int { return $this->zIndex; } public function isVisible(): bool { return $this->visible; } public function setVisible(bool $visible): void { if ($this->visible !== $visible) { $this->visible = $visible; $this->markDirty(true); } } public function setOverlay(bool $overlay): void { if ($this->overlay !== $overlay) { $this->overlay = $overlay; $this->markDirty(false); } } public function isOverlay(): bool { return $this->overlay; } public function invalidateTextureCache(): void { if ($this->cachedTexture !== null) { sdl_destroy_texture($this->cachedTexture); $this->cachedTexture = null; } $this->renderDirty = true; } public function getCachedTexture() { return $this->cachedTexture; } public function setCachedTexture($texture): void { // Free old texture if exists if ($this->cachedTexture !== null) { sdl_destroy_texture($this->cachedTexture); } $this->cachedTexture = $texture; } public function useTextureCache(): bool { return $this->useTextureCache; } public function setUseTextureCache(bool $use): void { $this->useTextureCache = $use; if (!$use) { $this->invalidateTextureCache(); } } public function markClean(): void { $this->layoutDirty = false; $this->renderDirty = false; foreach ($this->children as $child) { $child->markClean(); } } public function setParent(null|Component $parent): void { $this->parent = $parent; } public function getParent(): null|Component { return $this->parent; } public function update(): void { foreach ($this->children as $child) { $child->update(); } } public function layout(null|TextRenderer $textRenderer = null): void { $this->computedStyles = StyleParser::parse($this->style)->getValidStyles( MediaQueryEnum::normal, $this->currentState, ); if (isset($this->computedStyles[Margin::class]) && ($m = $this->computedStyles[Margin::class])) { $this->viewport->x = (int) ($this->viewport->x + $m->left); $this->viewport->width = max(0, ($this->viewport->width - $m->right) - $m->left); $this->viewport->y = (int) ($this->viewport->y + $m->top); $this->viewport->height = max(0, ($this->viewport->height - $m->bottom) - $m->top); } $this->contentViewport = clone $this->viewport; if (isset($this->computedStyles[Padding::class]) && ($p = $this->computedStyles[Padding::class])) { $this->contentViewport->x = (int) ($this->contentViewport->x + $p->left); $this->contentViewport->width = max(0, ($this->contentViewport->width - $p->right) - $p->left); $this->contentViewport->y = (int) ($this->contentViewport->y + $p->top); $this->contentViewport->height = max(0, ($this->contentViewport->height - $p->bottom) - $p->top); } if ($this->useTextureCache) { $this->invalidateTextureCache(); } $this->layoutDirty = false; $this->renderDirty = true; } public function render(&$renderer, null|TextRenderer $textRenderer = null): void { if (!$this->visible) { return; } if ( isset($this->computedStyles[\PHPNative\Tailwind\Style\Background::class]) && ($bg = $this->computedStyles[\PHPNative\Tailwind\Style\Background::class]) ) { if ($this->currentState == StateEnum::hover) { sdl_set_render_draw_color($renderer, $bg->color->red, $bg->color->green, $bg->color->blue, 10); } else { sdl_set_render_draw_color( $renderer, $bg->color->red, $bg->color->green, $bg->color->blue, $bg->color->alpha, ); } if ( isset($this->computedStyles[\PHPNative\Tailwind\Style\Border::class]) && ($border = $this->computedStyles[\PHPNative\Tailwind\Style\Border::class]) ) { // SDL3: sdl_rounded_box_ex uses (x1, y1, x2, y2) instead of (x, y, w, h) $x2 = $this->viewport->x + $this->viewport->width; $y2 = $this->viewport->y + $this->viewport->height; sdl_rounded_box_ex( $renderer, $this->viewport->x, $this->viewport->y, $x2, $y2, $border->roundTopLeft ?? 0, $border->roundTopRight ?? 0, $border->roundBottomRight ?? 0, $border->roundBottomLeft ?? 0, $bg->color->red, $bg->color->green, $bg->color->blue, $bg->color->alpha, ); } else { sdl_render_fill_rect($renderer, [ 'x' => $this->viewport->x, 'y' => $this->viewport->y, 'w' => $this->viewport->width, 'h' => $this->viewport->height, ]); } } 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 { if (!$this->visible) { return; } // Render children foreach ($this->children as $child) { $child->render($renderer, $textRenderer); $child->renderContent($renderer, $textRenderer); } } public function addComponent(Component $component): void { $this->children[] = $component; $component->setParent($this); if ($this->attachedWindow !== null) { $component->attachToWindow($this->attachedWindow); } $this->markDirty(true); // Adding a child means we need to re-layout and re-render } /** * Collect all overlays recursively from this component and all children * @return array Array of overlay components */ public function collectOverlays(): array { $overlays = []; foreach ($this->children as $child) { if ($child->isOverlay()) { $overlays[] = $child; } // Recursively collect from children $overlays = array_merge($overlays, $child->collectOverlays()); } return $overlays; } /** * Handle mouse click event * @return bool True if event was handled */ public function handleMouseClick(float $mouseX, float $mouseY, int $button): bool { // Don't handle events if component is not visible if (!$this->visible) { return false; } // Default implementation: propagate to children foreach ($this->children as $child) { if ($child->handleMouseClick($mouseX, $mouseY, $button)) { return true; } } return false; } /** * Handle mouse move event */ public function handleMouseMove(float $mouseX, float $mouseY): void { // Check if mouse is over this component $isMouseOver = $mouseX >= $this->viewport->x && $mouseX <= ($this->viewport->x + $this->viewport->width) && $mouseY >= $this->viewport->y && $mouseY <= ($this->viewport->y + $this->viewport->height); // Update state based on mouse position $previousState = $this->currentState; $this->currentState = $isMouseOver ? StateEnum::hover : StateEnum::normal; // Recompute styles if state changed if ($previousState !== $this->currentState) { $this->computedStyles = StyleParser::parse($this->style)->getValidStyles( MediaQueryEnum::normal, $this->currentState, ); // Mark as dirty since visual state changed $this->markDirty(false, false); } foreach ($this->children as $child) { $child->handleMouseMove($mouseX, $mouseY); } } /** * Handle mouse button release event */ public function handleMouseRelease(float $mouseX, float $mouseY, int $button): void { // Default implementation: propagate to children foreach ($this->children as $child) { $child->handleMouseRelease($mouseX, $mouseY, $button); } } /** * Handle mouse wheel event * @return bool True if event was handled */ public function handleMouseWheel(float $mouseX, float $mouseY, float $deltaY): bool { // Don't handle events if component is not visible if (!$this->visible) { return false; } // Default implementation: propagate to children foreach ($this->children as $child) { if ($child->handleMouseWheel($mouseX, $mouseY, $deltaY)) { return true; } } return false; } /** * Handle text input event * @param string $text The input text */ public function handleTextInput(string $text): void { // Default implementation: propagate to children foreach ($this->children as $child) { $child->handleTextInput($text); } } /** * Handle key down event * @param int $keycode SDL keycode * @return bool True if event was handled */ public function handleKeyDown(int $keycode, int $mod = 0): bool { // Default implementation: propagate to children foreach ($this->children as $child) { if ($child->handleKeyDown($keycode, $mod)) { return true; } } return false; } public function needsLayout(): bool { return $this->layoutDirty; } public function isDirty(): bool { return $this->renderDirty; } public function markDirty(bool $requiresLayout = false, bool $bubble = true): void { if ($requiresLayout) { $this->layoutDirty = true; $this->renderDirty = true; $this->invalidateTextureCache(); if ($this->attachedWindow !== null) { $this->attachedWindow->setShouldBeReLayouted(true); } } else { $this->renderDirty = true; } if ($bubble && $this->parent !== null) { $this->parent->markDirty($requiresLayout); return; } if (!$bubble && !$requiresLayout) { $ancestor = $this->parent; while ($ancestor !== null && $ancestor->useTextureCache()) { $ancestor->renderDirty = true; $ancestor->invalidateTextureCache(); $ancestor = $ancestor->getParent(); } } } public function attachToWindow(Window $window): void { $this->attachedWindow = $window; foreach ($this->children as $child) { $child->attachToWindow($window); } } public function detachFromWindow(): void { $this->attachedWindow = null; foreach ($this->children as $child) { $child->detachFromWindow(); } } /** * @return array */ public function getChildren(): array { return $this->children; } }