365 lines
11 KiB
PHP
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;
|
|
}
|
|
}
|