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

365 lines
11 KiB
PHP

<?php
namespace PHPNative\Ui;
use PHPNative\Framework\TextRenderer;
class Window
{
private mixed $window = null;
private mixed $renderer = null;
private int $windowId = 0;
private null|Component $rootComponent = null;
private null|TextRenderer $textRenderer;
private float $mouseX = 0;
private float $mouseY = 0;
private Viewport $viewport;
private bool $shouldBeReLayouted = true;
private float $pixelRatio = 2;
private bool $shouldClose = false;
private bool $hasBeenLaidOut = false;
private $onResize = null;
public function __construct(
private string $title,
private int $width = 800,
private int $height = 600,
private int $x = 100,
private int $y = 100,
) {
// Initialize SDL if not already done
static $sdlInitialized = false;
if (!$sdlInitialized) {
if (!sdl_init(SDL_INIT_VIDEO)) {
throw new \Exception('Failed to initialize SDL: ' . sdl_get_error());
}
// Initialize TTF
if (!ttf_init()) {
throw new \Exception('Failed to initialize TTF: ' . sdl_get_error());
}
$sdlInitialized = true;
}
// Create window with resizable flag for normal window decorations
// SDL_WINDOW_RESIZABLE gives you the standard window controls (close, minimize, maximize)
$flags = SDL_WINDOW_RESIZABLE;
$this->window = sdl_create_window($title, $width, $height, $flags);
if (!$this->window) {
throw new \Exception('Failed to create window: ' . sdl_get_error());
}
// Get window ID for event routing
$this->windowId = sdl_get_window_id($this->window);
// Enable text input for this window
sdl_start_text_input($this->window);
// Create renderer
$this->renderer = sdl_create_renderer($this->window);
if (!$this->renderer) {
throw new \Exception('Failed to create renderer: ' . sdl_get_error());
}
// Initialize text renderer
$this->textRenderer = new TextRenderer($this->renderer);
if (!$this->textRenderer->init()) {
error_log('Warning: Failed to initialize text renderer. Text rendering will not be available.');
}
// Get actual window size
$size = sdl_get_window_size($this->window);
$this->width = $size[0];
$this->height = $size[1];
$this->viewport = new Viewport(
windowWidth: $this->width,
windowHeight: $this->height,
width: $this->width,
height: $this->height,
);
}
public function setRoot(Component $component): self
{
$this->rootComponent = $component;
$this->shouldBeReLayouted = true;
$this->hasBeenLaidOut = false;
// Layout immediately to prevent black screen on first render
// This is especially important for windows created during event handling
$this->layout();
return $this;
}
public function getRoot(): null|Component
{
return $this->rootComponent;
}
public function getTitle(): string
{
return $this->title;
}
public function getWidth(): int
{
return $this->width;
}
public function getHeight(): int
{
return $this->height;
}
public function shouldClose(): bool
{
return $this->shouldClose;
}
public function close(): void
{
$this->shouldClose = true;
}
public function getWindowResource(): mixed
{
return $this->window;
}
public function getRendererResource(): mixed
{
return $this->renderer;
}
public function getWindowId(): int
{
return $this->windowId;
}
/**
* Process a single event for this window
* Called by Application with pre-filtered events
*/
public function processEvent(array $event): void
{
// Debug output - can be removed later
if (defined('DEBUG_EVENTS') && DEBUG_EVENTS) {
$eventTypes = [
SDL_EVENT_QUIT => 'QUIT',
SDL_EVENT_WINDOW_CLOSE_REQUESTED => 'WINDOW_CLOSE_REQUESTED',
SDL_EVENT_KEY_DOWN => 'KEY_DOWN',
SDL_EVENT_MOUSE_BUTTON_DOWN => 'MOUSE_BUTTON_DOWN',
SDL_EVENT_MOUSE_BUTTON_UP => 'MOUSE_BUTTON_UP',
SDL_EVENT_MOUSE_MOTION => 'MOUSE_MOTION',
SDL_EVENT_WINDOW_RESIZED => 'WINDOW_RESIZED',
];
$typeName = $eventTypes[$event['type']] ?? ('UNKNOWN(' . $event['type'] . ')');
error_log("[{$this->title}] (ID:{$this->windowId}) Event: {$typeName}");
}
switch ($event['type']) {
case SDL_EVENT_QUIT:
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
$this->shouldClose = true;
break;
case SDL_EVENT_KEY_DOWN:
$keycode = $event['keycode'] ?? 0;
// Propagate key event to root component
if ($this->rootComponent) {
$this->rootComponent->handleKeyDown($keycode);
}
break;
case SDL_EVENT_TEXT_INPUT:
$text = $event['text'] ?? '';
// Propagate text input to root component
if ($this->rootComponent && !empty($text)) {
$this->rootComponent->handleTextInput($text);
}
break;
case SDL_EVENT_WINDOW_RESIZED:
// Update window dimensions (from event data)
$newWidth = $event['data1'] ?? $this->width;
$newHeight = $event['data2'] ?? $this->height;
$this->width = $newWidth;
$this->height = $newHeight;
// Update text renderer framebuffer
if ($this->textRenderer && $this->textRenderer->isInitialized()) {
$this->textRenderer->updateFramebuffer($newWidth, $newHeight);
}
$this->viewport->x = 0;
$this->viewport->y = 0;
$this->viewport->windowWidth = $newWidth;
$this->viewport->width = $newWidth;
$this->viewport->height = $newHeight;
$this->viewport->windowHeight = $newHeight;
$this->shouldBeReLayouted = true;
if ($this->onResize) {
($this->onResize)($this);
}
break;
case SDL_EVENT_MOUSE_MOTION:
$this->mouseX = $event['x'] ?? 0;
$this->mouseY = $event['y'] ?? 0;
// Propagate mouse move to root component
if ($this->rootComponent) {
$this->rootComponent->handleMouseMove($this->mouseX, $this->mouseY);
}
break;
case SDL_EVENT_MOUSE_BUTTON_DOWN:
$button = $event['button'] ?? 0;
// Propagate click to root component
if ($this->rootComponent) {
$this->rootComponent->handleMouseClick($this->mouseX, $this->mouseY, $button);
}
break;
case SDL_EVENT_MOUSE_BUTTON_UP:
$button = $event['button'] ?? 0;
// Propagate release to root component
if ($this->rootComponent) {
$this->rootComponent->handleMouseRelease($this->mouseX, $this->mouseY, $button);
}
break;
case SDL_EVENT_MOUSE_WHEEL:
$deltaY = $event['y'] ?? 0;
// Propagate wheel to root component
if ($this->rootComponent) {
$this->rootComponent->handleMouseWheel($this->mouseX, $this->mouseY, $deltaY);
}
break;
}
}
/**
* Update mouse position and hover states
* This is called every frame to ensure hover works even without focus
*/
public function updateMousePosition(): void
{
// With SDL3, mouse position is tracked through SDL_EVENT_MOUSE_MOTION events
// This method is kept for compatibility but does nothing
// Hover states are updated in handleEvents() via SDL_EVENT_MOUSE_MOTION
}
/**
* Update window state
*/
public function update(): void
{
if ($this->rootComponent) {
$this->rootComponent->update();
}
}
/**
* Layout components
*/
public function layout(): bool
{
if ($this->rootComponent && $this->shouldBeReLayouted) {
// Always layout for now - we can optimize this later by tracking what changed
// The shouldBeReLayouted flag was preventing proper layout updates
$this->rootComponent->setViewport($this->viewport);
$this->rootComponent->layout($this->textRenderer);
$this->shouldBeReLayouted = false;
$this->hasBeenLaidOut = true;
return true;
}
return false;
}
/**
* Render the window
*/
public function render(): void
{
// Clear the window with white background
sdl_set_render_draw_color($this->renderer, 255, 255, 255, 255);
sdl_render_clear($this->renderer);
// Only render content if window has been laid out
// This can happen when windows are created during async callbacks
if ($this->hasBeenLaidOut && $this->rootComponent) {
$this->rootComponent->render($this->renderer, $this->textRenderer);
$this->rootComponent->renderContent($this->renderer, $this->textRenderer);
// Render all overlays last (they appear on top of everything)
$overlays = $this->rootComponent->collectOverlays();
usort($overlays, fn($a, $b) => $a->getZIndex() <=> $b->getZIndex());
foreach ($overlays as $overlay) {
if ($overlay->isVisible()) {
$overlay->render($this->renderer, $this->textRenderer);
$overlay->renderContent($this->renderer, $this->textRenderer);
}
}
}
// Present the rendered content
sdl_render_present($this->renderer);
}
/**
* Clean up resources
*/
public function cleanup(): void
{
if ($this->textRenderer) {
$this->textRenderer->free();
$this->textRenderer = null; // Prevent destructor from running again
}
}
/**
* Destroy the window and all resources
* This explicitly closes the SDL window
*/
public function destroy(): void
{
// First clean up text renderer
$this->cleanup();
// Explicitly destroy SDL renderer and window
if ($this->renderer) {
sdl_destroy_renderer($this->renderer);
$this->renderer = null;
}
if ($this->window) {
sdl_destroy_window($this->window);
$this->window = null;
}
}
public function setShouldBeReLayouted(bool $shouldBeReLayouted): void
{
$this->shouldBeReLayouted = $shouldBeReLayouted;
}
public function getViewport(): Viewport
{
return $this->viewport;
}
public function setOnResize(callable $callback): void
{
$this->onResize = $callback;
}
}