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; } }