338 lines
10 KiB
PHP
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;
|
|
}
|
|
}
|