placeholder = $placeholder; $this->onChange = $onChange; } public function layout(null|TextRenderer $textRenderer = null): void { // Parse styles first to get computed styles $this->computedStyles = \PHPNative\Tailwind\StyleParser::parse($this->style)->getValidStyles( \PHPNative\Tailwind\Style\MediaQueryEnum::normal, \PHPNative\Tailwind\Style\StateEnum::normal, ); // Call parent layout parent::layout($textRenderer); // If no explicit height is set, calculate based on text size + padding if (!isset($this->computedStyles[\PHPNative\Tailwind\Style\Height::class])) { $padding = $this->computedStyles[\PHPNative\Tailwind\Style\Padding::class] ?? null; $paddingY = $padding ? ($padding->top + $padding->bottom) : 0; $textStyle = $this->computedStyles[\PHPNative\Tailwind\Style\Text::class] ?? new TextStyle(); $fontSize = $textStyle->size; // Calculate text height $textHeight = 16; // Default font size if ($textRenderer !== null && $textRenderer->isInitialized()) { $displayText = empty($this->value) ? ($this->placeholder ?: 'A') : 'A'; $size = $textRenderer->measureText($displayText, $fontSize); $textHeight = $size[1] ?? 16; } // Set height to text height + padding + borders (2px * 2) $this->viewport->height = (int) ($textHeight + $paddingY + 4); $this->contentViewport->height = max(0, (int) ($textHeight + $paddingY)); } } public function getValue(): string { return $this->value; } public function setValue(string $value): void { $this->value = $value; $this->cursorPosition = mb_strlen($value); if ($this->onChange !== null) { ($this->onChange)($value); } } public function setOnChange(callable $onChange): void { $this->onChange = $onChange; } public function handleMouseClick(float $mouseX, float $mouseY, int $button): bool { // Check if click is within bounds if ( $mouseX >= $this->viewport->x && $mouseX <= ($this->viewport->x + $this->viewport->width) && $mouseY >= $this->viewport->y && $mouseY <= ($this->viewport->y + $this->viewport->height) ) { $this->focused = true; return true; } else { $this->focused = false; return false; } } public function handleTextInput(string $text): void { if (!$this->focused) { return; } // Delete selection if any if ($this->hasSelection()) { $this->deleteSelection(); } // Insert text at cursor position $before = mb_substr($this->value, 0, $this->cursorPosition); $after = mb_substr($this->value, $this->cursorPosition); $this->value = $before . $text . $after; $this->cursorPosition += mb_strlen($text); if ($this->onChange !== null) { ($this->onChange)($this->value); } } public function handleKeyDown(int $keycode, int $mod = 0): bool { if (!$this->focused) { return false; } // Check for modifier keys $ctrlPressed = ($mod & \KMOD_CTRL) !== 0; $shiftPressed = ($mod & \KMOD_SHIFT) !== 0; // Handle Ctrl+A (Select All) if ($ctrlPressed && $keycode === \SDLK_A) { $this->selectionStart = 0; $this->selectionEnd = mb_strlen($this->value); return true; } // Handle Ctrl+C (Copy) if ($ctrlPressed && $keycode === \SDLK_C) { if ($this->hasSelection()) { $selectedText = $this->getSelectedText(); \sdl_set_clipboard_text($selectedText); } return true; } // Handle Ctrl+X (Cut) if ($ctrlPressed && $keycode === \SDLK_X) { if ($this->hasSelection()) { $selectedText = $this->getSelectedText(); \sdl_set_clipboard_text($selectedText); $this->deleteSelection(); } return true; } // Handle Ctrl+V (Paste) if ($ctrlPressed && $keycode === \SDLK_V) { $clipboardText = \sdl_get_clipboard_text(); if (!empty($clipboardText)) { // Delete selection if any if ($this->hasSelection()) { $this->deleteSelection(); } // Insert clipboard text at cursor $before = mb_substr($this->value, 0, $this->cursorPosition); $after = mb_substr($this->value, $this->cursorPosition); $this->value = $before . $clipboardText . $after; $this->cursorPosition += mb_strlen($clipboardText); if ($this->onChange !== null) { ($this->onChange)($this->value); } } return true; } switch ($keycode) { case \SDLK_BACKSPACE: if ($this->hasSelection()) { $this->deleteSelection(); } elseif ($this->cursorPosition > 0) { $before = mb_substr($this->value, 0, $this->cursorPosition - 1); $after = mb_substr($this->value, $this->cursorPosition); $this->value = $before . $after; $this->cursorPosition--; if ($this->onChange !== null) { ($this->onChange)($this->value); } } return true; case \SDLK_DELETE: if ($this->hasSelection()) { $this->deleteSelection(); } elseif ($this->cursorPosition < mb_strlen($this->value)) { $before = mb_substr($this->value, 0, $this->cursorPosition); $after = mb_substr($this->value, $this->cursorPosition + 1); $this->value = $before . $after; if ($this->onChange !== null) { ($this->onChange)($this->value); } } return true; case \SDLK_LEFT: if ($this->cursorPosition > 0) { if ($shiftPressed) { // Start or extend selection if (!$this->hasSelection()) { $this->selectionStart = $this->cursorPosition; } $this->cursorPosition--; $this->selectionEnd = $this->cursorPosition; } else { // Clear selection and move cursor if ($this->hasSelection()) { // Move cursor to start of selection $this->cursorPosition = min($this->selectionStart, $this->selectionEnd); $this->clearSelection(); } else { $this->cursorPosition--; } } } return true; case \SDLK_RIGHT: if ($this->cursorPosition < mb_strlen($this->value)) { if ($shiftPressed) { // Start or extend selection if (!$this->hasSelection()) { $this->selectionStart = $this->cursorPosition; } $this->cursorPosition++; $this->selectionEnd = $this->cursorPosition; } else { // Clear selection and move cursor if ($this->hasSelection()) { // Move cursor to end of selection $this->cursorPosition = max($this->selectionStart, $this->selectionEnd); $this->clearSelection(); } else { $this->cursorPosition++; } } } return true; case \SDLK_HOME: if ($shiftPressed) { // Extend selection to start if (!$this->hasSelection()) { $this->selectionStart = $this->cursorPosition; } $this->cursorPosition = 0; $this->selectionEnd = $this->cursorPosition; } else { $this->cursorPosition = 0; $this->clearSelection(); } return true; case \SDLK_END: if ($shiftPressed) { // Extend selection to end if (!$this->hasSelection()) { $this->selectionStart = $this->cursorPosition; } $this->cursorPosition = mb_strlen($this->value); $this->selectionEnd = $this->cursorPosition; } else { $this->cursorPosition = mb_strlen($this->value); $this->clearSelection(); } return true; case \SDLK_RETURN: // Enter key - can be handled by parent return false; } return false; } public function isFocused(): bool { return $this->focused; } public function focus(): void { $this->focused = true; } public function blur(): void { $this->focused = false; $this->clearSelection(); } private function hasSelection(): bool { return $this->selectionStart !== null && $this->selectionEnd !== null && $this->selectionStart !== $this->selectionEnd; } private function getSelectedText(): string { if (!$this->hasSelection()) { return ''; } $start = min($this->selectionStart, $this->selectionEnd); $end = max($this->selectionStart, $this->selectionEnd); return mb_substr($this->value, $start, $end - $start); } private function deleteSelection(): void { if (!$this->hasSelection()) { return; } $start = min($this->selectionStart, $this->selectionEnd); $end = max($this->selectionStart, $this->selectionEnd); $before = mb_substr($this->value, 0, $start); $after = mb_substr($this->value, $end); $this->value = $before . $after; $this->cursorPosition = $start; $this->clearSelection(); if ($this->onChange !== null) { ($this->onChange)($this->value); } } private function clearSelection(): void { $this->selectionStart = null; $this->selectionEnd = null; } public function render(&$renderer, null|TextRenderer $textRenderer = null): void { // Render background with focus indicator if ($this->focused) { // Focused: blue border sdl_set_render_draw_color($renderer, 59, 130, 246, 255); // Blue-500 } else { // Not focused: gray border sdl_set_render_draw_color($renderer, 209, 213, 219, 255); // Gray-300 } // Draw border sdl_render_fill_rect($renderer, [ 'x' => $this->viewport->x, 'y' => $this->viewport->y, 'w' => $this->viewport->width, 'h' => $this->viewport->height, ]); // Draw white background inside sdl_set_render_draw_color($renderer, 255, 255, 255, 255); sdl_render_fill_rect($renderer, [ 'x' => $this->viewport->x + 2, 'y' => $this->viewport->y + 2, 'w' => $this->viewport->width - 4, 'h' => $this->viewport->height - 4, ]); $textStyle = $this->computedStyles[\PHPNative\Tailwind\Style\Text::class] ?? new TextStyle(); $fontSize = $textStyle->size; // Render selection highlight if ($this->hasSelection() && $textRenderer !== null && $textRenderer->isInitialized()) { $start = min($this->selectionStart, $this->selectionEnd); $end = max($this->selectionStart, $this->selectionEnd); $textBeforeSelection = mb_substr($this->value, 0, $start); $selectedText = mb_substr($this->value, $start, $end - $start); $selectionX = $this->viewport->x + 6; if (!empty($textBeforeSelection)) { $size = $textRenderer->measureText($textBeforeSelection, $fontSize); $selectionX += $size[0]; } $selectionWidth = 0; if (!empty($selectedText)) { $size = $textRenderer->measureText($selectedText, $fontSize); $selectionWidth = $size[0]; } // Draw selection background (blue) sdl_set_render_draw_color($renderer, 59, 130, 246, 100); // Blue with transparency sdl_render_fill_rect($renderer, [ 'x' => $selectionX, 'y' => $this->viewport->y + 4, 'w' => $selectionWidth, 'h' => $this->viewport->height - 8, ]); } // Render text or placeholder if ($textRenderer !== null && $textRenderer->isInitialized()) { $displayText = empty($this->value) ? $this->placeholder : $this->value; $textX = $this->viewport->x + 6; $textY = $this->viewport->y + 6; // Set color: gray for placeholder, black for text if (empty($this->value)) { $textRenderer->setColor(156/255, 163/255, 175/255, 1.0); // Gray-400 } else { $textRenderer->setColor(0, 0, 0, 1.0); // Black } $textRenderer->drawText($displayText, (int) $textX, (int) $textY, $fontSize); } // Render cursor if focused if ($this->focused && $this->cursorVisible && $textRenderer) { // Calculate cursor position $textBeforeCursor = mb_substr($this->value, 0, $this->cursorPosition); $cursorX = $this->viewport->x + 6; // Left padding if (!empty($textBeforeCursor)) { $size = $textRenderer->measureText($textBeforeCursor, $fontSize); $cursorX += $size[0]; } // Draw cursor sdl_set_render_draw_color($renderer, 0, 0, 0, 255); sdl_render_fill_rect($renderer, [ 'x' => $cursorX, 'y' => $this->viewport->y + 4, 'w' => 2, 'h' => $this->viewport->height - 8, ]); } // Don't render children - we handle text rendering directly } }