sdl3/src/Ui/Component.php
2025-10-26 22:47:12 +01:00

338 lines
10 KiB
PHP

<?php
namespace PHPNative\Ui;
use PHPNative\Framework\TextRenderer;
use PHPNative\Tailwind\Style\Margin;
use PHPNative\Tailwind\Style\MediaQueryEnum;
use PHPNative\Tailwind\Style\Padding;
use PHPNative\Tailwind\Style\State;
use PHPNative\Tailwind\Style\StateEnum;
use PHPNative\Tailwind\StyleParser;
abstract class Component
{
protected $children = [];
protected $pixelRatio;
protected bool $visible = true;
protected bool $isOverlay = false;
protected int $zIndex = 0;
protected StateEnum $currentState = StateEnum::normal;
protected Viewport $viewport;
protected array $computedStyles = [];
protected Viewport $contentViewport;
public function __construct(
protected string $style = '',
) {
// Initialize viewports with default values
// These will be properly set during layout()
$this->viewport = new Viewport(
x: 0,
y: 0,
width: 0,
height: 0,
windowWidth: 800,
windowHeight: 600
);
$this->contentViewport = clone $this->viewport;
}
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 setPixelRatio($pixelRatio): void
{
$this->pixelRatio = $pixelRatio;
}
public function setVisible(bool $visible): void
{
$this->visible = $visible;
}
public function isVisible(): bool
{
return $this->visible;
}
public function setOverlay(bool $isOverlay): void
{
$this->isOverlay = $isOverlay;
}
public function isOverlay(): bool
{
return $this->isOverlay;
}
public function setZIndex(int $zIndex): void
{
$this->zIndex = $zIndex;
}
public function getZIndex(): int
{
return $this->zIndex;
}
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);
}
}
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,
]
);
}
}
}
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;
}
/**
* 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
{
// 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,
);
}
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
{
// 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): bool
{
// Default implementation: propagate to children
foreach ($this->children as $child) {
if ($child->handleKeyDown($keycode)) {
return true;
}
}
return false;
}
}