This commit is contained in:
Thomas Peterson 2025-11-13 22:23:20 +01:00
parent 2f34e4d2b2
commit e07c06c7cc
14 changed files with 325 additions and 198 deletions

View File

@ -104,7 +104,9 @@ class ServerListTab
// SFTP Manager Button (handler will be set by SftpManagerTab)
$this->sftpButton = new Button(
'SFTP Manager öffnen',
'w-full border border-gray-300 rounded px-3 py-2 flex shadow-lg/50 shadow-cyan-500 flex-row gap-2 bg-green-300 text-black mb-2',
'rounded-md bg-blue-500 px-3 py-2 text-sm font-semibold text-white shadow-lg shadow-blue-500/50 focus:outline-none flex flex-row gap-2',
null,
'text-white',
);
$sftpIcon = new Icon(IconName::folder, 16, 'text-white');
$this->sftpButton->setIcon($sftpIcon);
@ -114,7 +116,7 @@ class ServerListTab
// SSH Terminal Button
$this->sshTerminalButton = new Button(
'SSH Terminal öffnen',
'w-full border border-gray-300 rounded px-3 py-2 flex flex-row gap-2 bg-lime-300 text-black mb-2',
'w-full border border-gray-300 rounded px-3 py-2 flex shadow-lg/100 shadow-lime-300 flex-row gap-2 bg-lime-300 text-black mb-2',
);
$sshTerminalIcon = new Icon(IconName::terminal, 16, 'text-white');
$this->sshTerminalButton->setIcon($sshTerminalIcon);

View File

@ -1,129 +0,0 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use PHPNative\Framework\Application;
use PHPNative\Ui\Widget\Button;
use PHPNative\Ui\Widget\Checkbox;
use PHPNative\Ui\Widget\Container;
use PHPNative\Ui\Widget\Label;
use PHPNative\Ui\Widget\TextInput;
use PHPNative\Ui\Window;
$app = new Application();
$window = new Window('Todo App', 600, 500);
// Main container
$mainContainer = new Container('flex flex-col p-4 bg-gray-50');
// Title
$title = new Label('My Todo List', 'text-2xl font-bold mb-4 text-gray-800');
$mainContainer->addComponent($title);
// Input section
$inputContainer = new Container('flex flex-row mb-4');
$input = new TextInput(
placeholder: 'What needs to be done?',
style: 'flex-1 p-2 border border-gray-300 rounded mr-2',
);
$addButton = new Button('Add', 'bg-blue-500 text-white p-2 rounded-lg hover:bg-blue-600');
$inputContainer->addComponent($input);
$inputContainer->addComponent($addButton);
$mainContainer->addComponent($inputContainer);
// Todo list container
$todoListContainer = new Container('flex flex-col');
$mainContainer->addComponent($todoListContainer);
// Stats
$statsLabel = new Label('0 items', 'mt-4 text-gray-600 text-sm h-10');
$mainContainer->addComponent($statsLabel);
// Todos array
$todos = [];
// Function to update stats
$updateStats = function () use (&$todos, $statsLabel) {
$total = count($todos);
$completed = count(array_filter($todos, fn($t) => $t['completed']));
$statsLabel->setText("{$total} items ({$completed} completed)");
};
// Function to render todos - use a reference so it can be used in closures
$renderTodos = null;
$renderTodos = function () use (&$todos, $todoListContainer, $updateStats, &$renderTodos, $window) {
// Clear existing todos
$todoListContainer->clearChildren();
foreach ($todos as $index => $todo) {
$todoItem = new Container('flex flex-row items-center p-2 mb-2 bg-white border border-gray-200 rounded h-12');
// Checkbox
$checkbox = new Checkbox(
label: '',
checked: $todo['completed'],
style: 'mr-2 w-6 h-6',
);
$checkbox->setOnChange(function ($checked) use (&$todos, $index, &$renderTodos, $updateStats) {
$todos[$index]['completed'] = $checked;
$updateStats();
});
// Text
$textStyle = $todo['completed'] ? 'flex-1 text-gray-400 line-through' : 'flex-1 text-gray-800';
$todoText = new Label($todo['text'], $textStyle);
// Delete button
$deleteButton = new Button('Delete', 'bg-red-500 text-white px-2 py-1 rounded text-sm hover:bg-red-600');
$deleteButton->setOnClick(function () use (&$todos, $index, &$renderTodos, $updateStats) {
array_splice($todos, $index, 1);
$renderTodos();
$updateStats();
});
$todoItem->addComponent($checkbox);
$todoItem->addComponent($todoText);
$todoItem->addComponent($deleteButton);
$todoListContainer->addComponent($todoItem);
}
$updateStats();
// Trigger re-layout after adding new components
$window->setShouldBeReLayouted(true);
};
// Add button click handler
$addButton->setOnClick(function () use ($input, &$todos, &$renderTodos) {
$text = trim($input->getValue());
if (!empty($text)) {
$todos[] = [
'text' => $text,
'completed' => false,
];
$input->setValue(''); // Clear input
$renderTodos();
}
});
// Handle Enter key in input
$input->setOnChange(function ($value) {
// Could add live validation here
});
$window->setRoot($mainContainer);
$app->addWindow($window);
echo "Todo App started!\n";
echo "- Type in the input field and click 'Add' or press Enter\n";
echo "- Click checkboxes to mark todos as complete\n";
echo "- Click 'Delete' to remove a todo\n\n";
$app->run();

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -702,6 +702,28 @@ PHP_FUNCTION(sdl_set_render_clip_rect) {
RETURN_THROWS();
}
PHP_FUNCTION(sdl_set_render_scale) {
zval *ren_res;
SDL_Renderer *ren;
double scale_x, scale_y;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "rdd", &ren_res, &scale_x, &scale_y) == FAILURE) {
RETURN_THROWS();
}
ren = (SDL_Renderer *)zend_fetch_resource(Z_RES_P(ren_res), "SDL_Renderer", le_sdl_renderer);
if (!ren) {
RETURN_FALSE;
}
if (SDL_SetRenderScale(ren, (float)scale_x, (float)scale_y) < 0) {
php_error_docref(NULL, E_WARNING, "Failed to set render scale: %s", SDL_GetError());
RETURN_FALSE;
}
RETURN_TRUE;
}
PHP_FUNCTION(sdl_get_window_size) {
zval *win_res;
SDL_Window *win;
@ -723,6 +745,54 @@ PHP_FUNCTION(sdl_get_window_size) {
add_index_long(return_value, 1, h);
}
PHP_FUNCTION(sdl_get_window_size_in_pixels) {
zval *win_res;
SDL_Window *win;
int w, h;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "r", &win_res) == FAILURE) {
RETURN_THROWS();
}
win = (SDL_Window *)zend_fetch_resource(Z_RES_P(win_res), "SDL_Window", le_sdl_window);
if (!win) {
RETURN_FALSE;
}
if (SDL_GetWindowSizeInPixels(win, &w, &h) < 0) {
php_error_docref(NULL, E_WARNING, "Failed to get window size in pixels: %s", SDL_GetError());
RETURN_FALSE;
}
array_init(return_value);
add_index_long(return_value, 0, w);
add_index_long(return_value, 1, h);
}
PHP_FUNCTION(sdl_get_renderer_output_size) {
zval *ren_res;
SDL_Renderer *ren;
int w, h;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "r", &ren_res) == FAILURE) {
RETURN_THROWS();
}
ren = (SDL_Renderer *)zend_fetch_resource(Z_RES_P(ren_res), "SDL_Renderer", le_sdl_renderer);
if (!ren) {
RETURN_FALSE;
}
if (SDL_GetRenderOutputSize(ren, &w, &h) < 0) {
php_error_docref(NULL, E_WARNING, "Failed to get renderer output size: %s", SDL_GetError());
RETURN_FALSE;
}
array_init(return_value);
add_index_long(return_value, 0, w);
add_index_long(return_value, 1, h);
}
PHP_FUNCTION(sdl_start_text_input) {
zval *win_res;
SDL_Window *win;
@ -903,10 +973,24 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_set_render_clip_rect, 0, 0, 1)
ZEND_ARG_INFO(0, rect)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_set_render_scale, 0, 0, 3)
ZEND_ARG_INFO(0, renderer)
ZEND_ARG_INFO(0, scaleX)
ZEND_ARG_INFO(0, scaleY)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_get_window_size, 0, 0, 1)
ZEND_ARG_INFO(0, window)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_get_window_size_in_pixels, 0, 0, 1)
ZEND_ARG_INFO(0, window)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_get_renderer_output_size, 0, 0, 1)
ZEND_ARG_INFO(0, renderer)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_start_text_input, 0, 0, 1)
ZEND_ARG_INFO(0, window)
ZEND_END_ARG_INFO()
@ -929,6 +1013,7 @@ const zend_function_entry sdl3_functions[] = {
PHP_FE(sdl_render_fill_rect, arginfo_sdl_render_fill_rect)
PHP_FE(sdl_render_rect, arginfo_sdl_render_rect)
PHP_FE(sdl_render_present, arginfo_sdl_render_present)
PHP_FE(sdl_set_render_scale, arginfo_sdl_set_render_scale)
PHP_FE(sdl_delay, arginfo_sdl_delay)
PHP_FE(sdl_get_error, arginfo_sdl_get_error)
PHP_FE(sdl_create_texture_from_surface, arginfo_sdl_create_texture_from_surface)
@ -944,6 +1029,8 @@ const zend_function_entry sdl3_functions[] = {
PHP_FE(sdl_rounded_box_ex, arginfo_sdl_rounded_box_ex)
PHP_FE(sdl_set_render_clip_rect, arginfo_sdl_set_render_clip_rect)
PHP_FE(sdl_get_window_size, arginfo_sdl_get_window_size)
PHP_FE(sdl_get_window_size_in_pixels, arginfo_sdl_get_window_size_in_pixels)
PHP_FE(sdl_get_renderer_output_size, arginfo_sdl_get_renderer_output_size)
PHP_FE(sdl_start_text_input, arginfo_sdl_start_text_input)
PHP_FE(sdl_stop_text_input, arginfo_sdl_stop_text_input)

View File

@ -9,14 +9,16 @@ namespace PHPNative\Framework;
class TextRenderer
{
private $renderer;
private $font = null;
private bool $initialized = false;
private string $fontPath;
private int $fontSize;
private string $fontPath = '';
private int $defaultFontSize = 16;
/** @var array<int, resource> */
private array $fonts = [];
private float $colorR = 1.0;
private float $colorG = 1.0;
private float $colorB = 1.0;
private float $colorA = 1.0;
private float $pixelRatio = 1.0;
/**
* Constructor
@ -54,11 +56,10 @@ class TextRenderer
}
$this->fontPath = $fontPath;
$this->fontSize = $fontSize;
$this->defaultFontSize = $fontSize;
$this->font = ttf_open_font($fontPath, $fontSize);
if (!$this->font) {
$font = $this->loadFont($fontSize);
if ($font === null) {
error_log("Failed to open font: {$fontPath}");
$this->initialized = false;
return false;
@ -72,6 +73,50 @@ class TextRenderer
return true;
}
private function loadFont(int $size): mixed
{
$size = max(1, (int) round($size));
if (isset($this->fonts[$size])) {
return $this->fonts[$size];
}
if ($this->fontPath === '') {
return null;
}
$font = ttf_open_font($this->fontPath, $size);
if (!$font) {
return null;
}
$this->fonts[$size] = $font;
return $font;
}
private function getFont(int $size): mixed
{
if (!$this->initialized) {
return null;
}
return $this->loadFont($size);
}
private function getScaledFontSize(int $size): int
{
if ($this->pixelRatio <= 1.0) {
return $size;
}
return max(1, (int) round($size * $this->pixelRatio));
}
public function setPixelRatio(float $ratio): void
{
$this->pixelRatio = max(1.0, $ratio);
}
/**
* Find a system font
*
@ -111,13 +156,18 @@ class TextRenderer
*/
public function drawText(string $text, int $x, int $y, null|int $size = null): void
{
if (!$this->initialized || !$this->font) {
if (!$this->initialized) {
return;
}
// SDL_ttf doesn't support variable font sizes easily
// We use the font size that was set when the font was opened
// For different sizes, you'd need to open multiple font instances
$fontSize = $size ?? $this->defaultFontSize;
$baseFont = $this->getFont($fontSize);
if ($baseFont === null) {
return;
}
$renderFontSize = $this->getScaledFontSize($fontSize);
$renderFont = $this->getFont($renderFontSize) ?? $baseFont;
// Convert float color (0.0-1.0) to int (0-255)
$r = (int) ($this->colorR * 255);
@ -128,7 +178,7 @@ class TextRenderer
if (strlen($text) < 1) {
return;
}
$surface = ttf_render_text_blended($this->font, $text, $r, $g, $b);
$surface = ttf_render_text_blended($renderFont, $text, $r, $g, $b);
if (!$surface) {
return;
}
@ -140,7 +190,7 @@ class TextRenderer
}
// Get text size
$textSize = ttf_size_text($this->font, $text);
$textSize = ttf_size_text($baseFont, $text);
// Render texture
sdl_render_texture($this->renderer, $texture, [
@ -158,19 +208,28 @@ class TextRenderer
* @param string $text Text to render into a texture
* @return array|null Returns ['texture' => resource, 'width' => int, 'height' => int] or null on failure
*/
public function createTextTexture(string $text): null|array
public function createTextTexture(string $text, null|int $size = null): null|array
{
if (!$this->initialized || !$this->font) {
if (!$this->initialized) {
return null;
}
$fontSize = $size ?? $this->defaultFontSize;
$baseFont = $this->getFont($fontSize);
if ($baseFont === null) {
return null;
}
$renderFontSize = $this->getScaledFontSize($fontSize);
$renderFont = $this->getFont($renderFontSize) ?? $baseFont;
$r = (int) ($this->colorR * 255);
$g = (int) ($this->colorG * 255);
$b = (int) ($this->colorB * 255);
if (strlen($text) < 1) {
return null;
}
$surface = ttf_render_text_blended($this->font, $text, $r, $g, $b);
$surface = ttf_render_text_blended($renderFont, $text, $r, $g, $b);
if (!$surface) {
return null;
}
@ -180,7 +239,7 @@ class TextRenderer
return null;
}
$dimensions = ttf_size_text($this->font, $text);
$dimensions = ttf_size_text($baseFont, $text);
sdl_set_texture_blend_mode($texture, SDL_BLENDMODE_BLEND);
if (\function_exists('sdl_set_texture_alpha_mod')) {
@ -219,12 +278,17 @@ class TextRenderer
*/
public function measureText(string $text, null|int $size = null): array
{
if (!$this->initialized || !$this->font) {
if (!$this->initialized) {
return [0, 0];
}
// Get text size using TTF
$dimensions = ttf_size_text($this->font, $text);
$fontSize = $size ?? $this->defaultFontSize;
$font = $this->getFont($fontSize);
if ($font === null) {
return [0, 0];
}
$dimensions = ttf_size_text($font, $text);
return [(int) $dimensions['w'], (int) $dimensions['h']];
}
@ -245,11 +309,16 @@ class TextRenderer
*/
public function free(): void
{
if ($this->initialized && $this->font) {
ttf_close_font($this->font);
$this->font = null;
$this->initialized = false;
if (!$this->initialized) {
return;
}
foreach ($this->fonts as $font) {
ttf_close_font($font);
}
$this->fonts = [];
$this->initialized = false;
}
/**

View File

@ -862,42 +862,45 @@ abstract class Component
}
/**
* Box blur algorithm for shadow texture
* Box blur algorithm for the alpha channel of the shadow texture
* Keeps RGB constant so tinted shadows don't desaturate while fading out.
*/
private function boxBlur(array &$pixels, int $width, int $height, int $radius): void
private function boxBlurAlpha(array &$alphaMap, int $width, int $height, int $radius): void
{
if ($radius <= 0) {
return;
}
$temp = array_fill(0, $width * $height, 0);
for ($y = 0; $y < $height; $y++) {
for ($x = 0; $x < $width; $x++) {
$r = 0; $g = 0; $b = 0; $a = 0; $count = 0;
$alphaSum = 0;
$count = 0;
for ($dy = -$radius; $dy <= $radius; $dy++) {
$ny = $y + $dy;
if ($ny < 0 || $ny >= $height) {
continue;
}
$rowOffset = $ny * $width;
for ($dx = -$radius; $dx <= $radius; $dx++) {
$nx = $x + $dx;
$ny = $y + $dy;
if ($nx >= 0 && $nx < $width && $ny >= 0 && $ny < $height) {
$pixel = $pixels[$ny * $width + $nx];
$r += ($pixel >> 16) & 0xFF;
$g += ($pixel >> 8) & 0xFF;
$b += $pixel & 0xFF;
$a += ($pixel >> 24) & 0xFF;
$count++;
if ($nx < 0 || $nx >= $width) {
continue;
}
$alphaSum += $alphaMap[$rowOffset + $nx];
$count++;
}
}
$r = (int)($r / $count);
$g = (int)($g / $count);
$b = (int)($b / $count);
$a = (int)($a / $count);
$temp[$y * $width + $x] = ($a << 24) | ($r << 16) | ($g << 8) | $b;
$temp[$y * $width + $x] = $count > 0 ? (int) ($alphaSum / $count) : 0;
}
}
$pixels = $temp;
$alphaMap = $temp;
}
/**
@ -905,23 +908,28 @@ abstract class Component
*/
private function createShadowTexture(&$renderer, int $width, int $height, int $blurRadius, int $alpha, int $r = 0, int $g = 0, int $b = 0): mixed
{
// Create pixel array (ARGB8888 format) - start with transparent
$pixels = array_fill(0, $width * $height, 0x00000000);
// Create color value in ARGB format
$color = ($alpha << 24) | ($r << 16) | ($g << 8) | $b;
// Create alpha map (single channel) - start with transparent
$alphaMap = array_fill(0, $width * $height, 0);
// Fill only the inner rectangle with semi-transparent color
// Leave a border for the blur to spread into
$margin = $blurRadius + 2;
for ($y = $margin; $y < $height - $margin; $y++) {
for ($x = $margin; $x < $width - $margin; $x++) {
$pixels[$y * $width + $x] = $color;
$alphaMap[$y * $width + $x] = $alpha;
}
}
// Apply box blur - this will spread the alpha to the edges
$this->boxBlur($pixels, $width, $height, $blurRadius);
// Apply box blur on alpha to spread transparency without washing out color
$this->boxBlurAlpha($alphaMap, $width, $height, $blurRadius);
// Build final pixel buffer with constant RGB and varying alpha
$pixels = [];
$totalPixels = $width * $height;
for ($i = 0; $i < $totalPixels; $i++) {
$pixelAlpha = $alphaMap[$i];
$pixels[$i] = ($pixelAlpha << 24) | ($r << 16) | ($g << 8) | $b;
}
// Create texture
$texture = sdl_create_texture(

View File

@ -19,6 +19,7 @@ class Button extends Container
public string $text = '',
string $style = '',
null|callable $onClick = null,
string $buttonStyle = '',
) {
parent::__construct($style);
@ -29,7 +30,7 @@ class Button extends Container
// Create label inside button
$this->label = new Label(
text: $text,
style: 'text-black',
style: 'text-black ' . $buttonStyle,
);
$this->addComponent($this->label);

View File

@ -3,6 +3,7 @@
namespace PHPNative\Ui\Widget;
use PHPNative\Framework\TextRenderer;
use PHPNative\Tailwind\Style\Text as TextStyle;
use PHPNative\Ui\Component;
class Checkbox extends Component
@ -102,9 +103,10 @@ class Checkbox extends Component
if (!empty($this->labelText) && $textRenderer !== null && $textRenderer->isInitialized()) {
$textX = $this->viewport->x + $checkboxSize + 8; // Checkbox + margin
$textY = $this->viewport->y + 2;
$textStyle = $this->computedStyles[\PHPNative\Tailwind\Style\Text::class] ?? new TextStyle();
$textRenderer->setColor(0, 0, 0, 1.0); // Black
$textRenderer->drawText($this->labelText, (int) $textX, (int) $textY);
$textRenderer->drawText($this->labelText, (int) $textX, (int) $textY, $textStyle->size);
}
}
}

View File

@ -224,13 +224,30 @@ class Container extends Component
$width = $childStyles[Width::class] ?? null;
$height = $childStyles[Height::class] ?? null;
// Track explicit cross-axis sizing so we don't stretch components unintentionally.
$crossSize = null;
$hasExplicitCrossSize = false;
if ($isRow && $height) {
$crossSize = $this->calculateSize($height, $this->contentViewport->height);
$hasExplicitCrossSize = true;
} elseif (!$isRow && $width) {
$crossSize = $this->calculateSize($width, $this->contentViewport->width);
$hasExplicitCrossSize = true;
}
$size = 0;
$hasExplicitSize = false;
// Check if child has flex-grow
if ($childFlex && $childFlex->type !== FlexTypeEnum::none) {
$flexGrowCount++;
$childSizes[$index] = ['size' => 0, 'flexGrow' => true, 'natural' => false];
$childSizes[$index] = [
'size' => 0,
'flexGrow' => true,
'natural' => false,
'crossSize' => $crossSize,
'crossExplicit' => $hasExplicitCrossSize,
];
} else {
// Calculate fixed size from basis, width, or height
if ($basis) {
@ -264,7 +281,13 @@ class Container extends Component
}
$usedSpace += $size;
$childSizes[$index] = ['size' => $size, 'flexGrow' => false, 'natural' => !$hasExplicitSize];
$childSizes[$index] = [
'size' => $size,
'flexGrow' => false,
'natural' => !$hasExplicitSize,
'crossSize' => $crossSize,
'crossExplicit' => $hasExplicitCrossSize,
];
}
}
@ -284,6 +307,8 @@ class Container extends Component
$childSize = $childSizes[$index];
$size = $childSize['flexGrow'] ? $flexGrowSize : $childSize['size'];
$crossSize = $childSize['crossSize'] ?? null;
$hasExplicitCrossSize = $childSize['crossExplicit'] ?? false;
// Add gap before this child (except for first child)
if ($childIndex > 0 && $gapSize > 0) {
@ -293,21 +318,29 @@ class Container extends Component
// Create viewport for child
if ($isRow) {
// Flex row
$childHeight = $hasExplicitCrossSize && $crossSize !== null
? min($crossSize, $this->contentViewport->height)
: $this->contentViewport->height;
$childViewport = new Viewport(
x: (int) $currentPosition,
y: $this->contentViewport->y,
width: $size,
height: $this->contentViewport->height,
height: $childHeight,
windowWidth: $this->contentViewport->windowWidth,
windowHeight: $this->contentViewport->windowHeight,
);
$currentPosition += $size;
} else {
$childWidth = $hasExplicitCrossSize && $crossSize !== null
? min($crossSize, $this->contentViewport->width)
: $this->contentViewport->width;
// Flex column
$childViewport = new Viewport(
x: $this->contentViewport->x,
y: (int) $currentPosition,
width: $this->contentViewport->width,
width: $childWidth,
height: $size,
windowWidth: $this->contentViewport->windowWidth,
windowHeight: $this->contentViewport->windowHeight,

View File

@ -83,7 +83,7 @@ class Label extends Component
if ($this->renderDirty || $this->textTexture === null) {
$this->clearTextTexture();
$textureData = $textRenderer->createTextTexture($this->text);
$textureData = $textRenderer->createTextTexture($this->text, $textStyle->size);
if ($textureData !== null) {
$this->textTexture = $textureData['texture'];
$this->textWidth = $textureData['width'];

View File

@ -3,6 +3,7 @@
namespace PHPNative\Ui\Widget;
use PHPNative\Framework\TextRenderer;
use PHPNative\Tailwind\Style\Text as TextStyle;
class TextInput extends Container
{
@ -42,12 +43,14 @@ class TextInput extends Container
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);
$size = $textRenderer->measureText($displayText, $fontSize);
$textHeight = $size[1] ?? 16;
}
@ -374,6 +377,9 @@ class TextInput extends Container
'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);
@ -384,13 +390,13 @@ class TextInput extends Container
$selectionX = $this->viewport->x + 6;
if (!empty($textBeforeSelection)) {
$size = $textRenderer->measureText($textBeforeSelection);
$size = $textRenderer->measureText($textBeforeSelection, $fontSize);
$selectionX += $size[0];
}
$selectionWidth = 0;
if (!empty($selectedText)) {
$size = $textRenderer->measureText($selectedText);
$size = $textRenderer->measureText($selectedText, $fontSize);
$selectionWidth = $size[0];
}
@ -417,7 +423,7 @@ class TextInput extends Container
$textRenderer->setColor(0, 0, 0, 1.0); // Black
}
$textRenderer->drawText($displayText, (int) $textX, (int) $textY);
$textRenderer->drawText($displayText, (int) $textX, (int) $textY, $fontSize);
}
// Render cursor if focused
@ -427,7 +433,7 @@ class TextInput extends Container
$cursorX = $this->viewport->x + 6; // Left padding
if (!empty($textBeforeCursor)) {
$size = $textRenderer->measureText($textBeforeCursor);
$size = $textRenderer->measureText($textBeforeCursor, $fontSize);
$cursorX += $size[0];
}

View File

@ -16,7 +16,7 @@ class Window
private float $mouseY = 0;
private Viewport $viewport;
private bool $shouldBeReLayouted = true;
private float $pixelRatio = 2;
private float $pixelRatio = 1.0;
private bool $shouldClose = false;
private $onResize = null;
private $onFpsChange = null;
@ -46,9 +46,8 @@ class Window
$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;
// Create window with resizable + high pixel density flags to get crisp rendering on HiDPI displays
$flags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY;
$this->window = sdl_create_window($title, $width, $height, $flags);
if (!$this->window) {
@ -72,6 +71,7 @@ class Window
if (!$this->textRenderer->init()) {
error_log('Warning: Failed to initialize text renderer. Text rendering will not be available.');
}
$this->updatePixelRatio();
// Get actual window size
$size = sdl_get_window_size($this->window);
@ -87,6 +87,52 @@ class Window
$this->lastFpsUpdate = microtime(true);
}
private function updatePixelRatio(): void
{
$this->pixelRatio = 1.0;
$windowSize = sdl_get_window_size($this->window);
$pixelSize = null;
if (function_exists('sdl_get_window_size_in_pixels')) {
$pixelSize = sdl_get_window_size_in_pixels($this->window);
}
if (
(!is_array($pixelSize) || count($pixelSize) < 2) &&
function_exists('sdl_get_renderer_output_size')
) {
$pixelSize = sdl_get_renderer_output_size($this->renderer);
}
if (is_array($windowSize) && is_array($pixelSize)) {
$logicalWidth = max(1, (int) ($windowSize[0] ?? 1));
$logicalHeight = max(1, (int) ($windowSize[1] ?? 1));
$pixelWidth = max(1, (int) ($pixelSize[0] ?? 1));
$pixelHeight = max(1, (int) ($pixelSize[1] ?? 1));
$ratioX = $pixelWidth / $logicalWidth;
$ratioY = $pixelHeight / $logicalHeight;
$computed = max($ratioX, $ratioY);
if ($computed > 0) {
$this->pixelRatio = max(1.0, $computed);
}
}
if ($this->textRenderer) {
$this->textRenderer->setPixelRatio($this->pixelRatio);
}
if (function_exists('sdl_set_render_scale')) {
sdl_set_render_scale($this->renderer, $this->pixelRatio, $this->pixelRatio);
}
}
public function getPixelRatio(): float
{
return $this->pixelRatio;
}
public function setRoot(Component $component): self
{
if ($this->rootComponent !== null) {
@ -207,6 +253,8 @@ class Window
$this->textRenderer->updateFramebuffer($newWidth, $newHeight);
}
$this->updatePixelRatio();
$this->viewport->x = 0;
$this->viewport->y = 0;
$this->viewport->windowWidth = $newWidth;