Backup
This commit is contained in:
parent
bf087c798d
commit
43a140c08d
BIN
assets/fonts/fontawesome/fa7_brands_regular_400.otf
Normal file
BIN
assets/fonts/fontawesome/fa7_brands_regular_400.otf
Normal file
Binary file not shown.
BIN
assets/fonts/fontawesome/fa7_free_regular_400.otf
Normal file
BIN
assets/fonts/fontawesome/fa7_free_regular_400.otf
Normal file
Binary file not shown.
BIN
assets/fonts/fontawesome/fa7_freesolid_900.otf
Normal file
BIN
assets/fonts/fontawesome/fa7_freesolid_900.otf
Normal file
Binary file not shown.
@ -3,21 +3,80 @@
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use PHPNative\Framework\Application;
|
||||
use PHPNative\Framework\IconFontRegistry;
|
||||
use PHPNative\Tailwind\Data\Icon as IconName;
|
||||
use PHPNative\Ui\Widget\Button;
|
||||
use PHPNative\Ui\Widget\Container;
|
||||
use PHPNative\Ui\Widget\Icon;
|
||||
use PHPNative\Ui\Widget\Label;
|
||||
use PHPNative\Ui\Widget\Menu;
|
||||
use PHPNative\Ui\Widget\MenuBar;
|
||||
use PHPNative\Ui\Widget\Modal;
|
||||
use PHPNative\Ui\Widget\StatusBar;
|
||||
use PHPNative\Ui\Widget\TabContainer;
|
||||
use PHPNative\Ui\Widget\Table;
|
||||
use PHPNative\Ui\Widget\TextInput;
|
||||
use PHPNative\Ui\Window;
|
||||
|
||||
$iconFontCandidates = [
|
||||
__DIR__ . '/../assets/fonts/fa-solid-900.ttf',
|
||||
__DIR__ . '/../assets/fonts/fontawesome/fa7_freesolid_900.otf',
|
||||
'/usr/share/fonts/truetype/fontawesome-webfont.ttf',
|
||||
'/usr/share/fonts/truetype/fontawesome/fa-solid-900.ttf',
|
||||
'/usr/share/fonts/truetype/fa-solid-900.ttf',
|
||||
];
|
||||
|
||||
$iconFontPath = null;
|
||||
foreach ($iconFontCandidates as $candidate) {
|
||||
if (is_file($candidate)) {
|
||||
$iconFontPath = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($iconFontPath !== null) {
|
||||
IconFontRegistry::setDefaultFontPath($iconFontPath);
|
||||
} else {
|
||||
echo "Hinweis: FontAwesome Font nicht gefunden. Icons werden ohne Symbol dargestellt.\n";
|
||||
}
|
||||
|
||||
$app = new Application();
|
||||
$window = new Window('Windows Application Example', 800, 600);
|
||||
$currentApiKey = '';
|
||||
|
||||
/** @var Label|null $statusLabel */
|
||||
$statusLabel = null;
|
||||
|
||||
// Main container (flex-col: menu, content, status)
|
||||
$mainContainer = new Container('flex flex-col bg-gray-100');
|
||||
|
||||
// Modal dialog setup (hidden by default)
|
||||
$apiKeyInput = new TextInput('API Key', 'w-full border border-gray-300 rounded px-3 py-2 bg-white text-black');
|
||||
|
||||
$modalDialog = new Container('bg-white border border-gray-300 rounded p-6 flex flex-col w-96 gap-3 shadow-lg');
|
||||
$modalDialog->addComponent(new Label('API Einstellungen', 'text-xl font-bold text-black'));
|
||||
$modalDialog->addComponent(new Label(
|
||||
'Bitte gib deinen API Key ein, um externe Dienste zu verbinden.',
|
||||
'text-sm text-gray-700',
|
||||
));
|
||||
|
||||
$fieldContainer = new Container('flex flex-col gap-1');
|
||||
$fieldContainer->addComponent(new Label('API Key', 'text-sm text-gray-600'));
|
||||
$fieldContainer->addComponent($apiKeyInput);
|
||||
$modalDialog->addComponent($fieldContainer);
|
||||
|
||||
$buttonRow = new Container('flex flex-row justify-end gap-2');
|
||||
$cancelButton = new Button('Abbrechen', 'px-4 py-2 bg-gray-200 text-black rounded hover:bg-gray-300');
|
||||
$saveButton = new Button('Speichern', 'px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 flex items-center');
|
||||
$saveIcon = new Icon(IconName::save, 18, 'text-white');
|
||||
$saveButton->setIcon($saveIcon);
|
||||
$buttonRow->addComponent($cancelButton);
|
||||
$buttonRow->addComponent($saveButton);
|
||||
$modalDialog->addComponent($buttonRow);
|
||||
|
||||
$modal = new Modal($modalDialog);
|
||||
$modal->setBackdropAlpha(180);
|
||||
|
||||
// === 1. MenuBar ===
|
||||
$menuBar = new MenuBar();
|
||||
|
||||
@ -37,8 +96,10 @@ $fileMenu->addItem('Beenden', function () use ($app) {
|
||||
|
||||
// Settings Menu
|
||||
$settingsMenu = new Menu(title: 'Einstellungen');
|
||||
$settingsMenu->addItem('Optionen', function () {
|
||||
echo "Optionen clicked\n";
|
||||
$settingsMenu->addItem('Optionen', function () use ($menuBar, $modal, $apiKeyInput, &$currentApiKey) {
|
||||
$menuBar->closeAllMenus();
|
||||
$apiKeyInput->setValue($currentApiKey);
|
||||
$modal->setVisible(true);
|
||||
});
|
||||
$settingsMenu->addItem('Sprache', function () {
|
||||
echo "Sprache clicked\n";
|
||||
@ -105,20 +166,47 @@ $mainContainer->addComponent($tabContainer);
|
||||
// === 3. StatusBar ===
|
||||
$statusBar = new StatusBar();
|
||||
$statusBar->addSegment($statusLabel);
|
||||
$fpsLabel = new Label(
|
||||
text: 'FPS: --',
|
||||
style: 'basis-1/8 text-black border-l',
|
||||
);
|
||||
$statusBar->addSegment(new Label(
|
||||
text: 'Zeilen: 10',
|
||||
style: 'basis-2/8 text-black border-l',
|
||||
)); // Fixed width
|
||||
$statusBar->addSegment($fpsLabel);
|
||||
$statusBar->addSegment(new Label(
|
||||
text: 'Version 1.0',
|
||||
style: 'border-l text-black basis-2/8',
|
||||
));
|
||||
$mainContainer->addComponent($statusBar);
|
||||
|
||||
$cancelButton->setOnClick(function () use ($menuBar, $modal) {
|
||||
$menuBar->closeAllMenus();
|
||||
$modal->setVisible(false);
|
||||
});
|
||||
|
||||
$saveButton->setOnClick(function () use (&$currentApiKey, $apiKeyInput, $menuBar, $modal, &$statusLabel) {
|
||||
$currentApiKey = trim($apiKeyInput->getValue());
|
||||
if ($statusLabel !== null) {
|
||||
$masked = strlen($currentApiKey) > 4
|
||||
? (str_repeat('*', max(0, strlen($currentApiKey) - 4)) . substr($currentApiKey, -4))
|
||||
: $currentApiKey;
|
||||
$statusLabel->setText('API-Key gespeichert: ' . $masked);
|
||||
}
|
||||
$menuBar->closeAllMenus();
|
||||
$modal->setVisible(false);
|
||||
});
|
||||
|
||||
$mainContainer->addComponent($modal);
|
||||
$window->setOnResize(function (Window $window) use (&$statusLabel) {
|
||||
$statusLabel->setText(
|
||||
'Fenster: ' . $window->getViewport()->windowWidth . 'x' . $window->getViewport()->windowHeight,
|
||||
);
|
||||
});
|
||||
$window->setOnFpsChange(function (float $fps) use ($fpsLabel) {
|
||||
$fpsLabel->setText(sprintf('FPS: %d', max(0, (int) round($fps))));
|
||||
});
|
||||
// Set root and run
|
||||
$window->setRoot($mainContainer);
|
||||
$app->addWindow($window);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -909,6 +909,9 @@ const zend_function_entry sdl3_functions[] = {
|
||||
PHP_FE(sdl_poll_event, arginfo_sdl_poll_event)
|
||||
PHP_FE(sdl_wait_event, arginfo_sdl_wait_event)
|
||||
PHP_FE(sdl_wait_event_timeout, arginfo_sdl_wait_event_timeout)
|
||||
PHP_FE(sdl_get_mod_state, arginfo_sdl_get_mod_state)
|
||||
PHP_FE(sdl_get_clipboard_text, arginfo_sdl_get_clipboard_text)
|
||||
PHP_FE(sdl_set_clipboard_text, arginfo_sdl_set_clipboard_text)
|
||||
|
||||
PHP_FE_END
|
||||
};
|
||||
|
||||
@ -55,6 +55,29 @@ void sdl3_events_register_constants(int module_number) {
|
||||
REGISTER_LONG_CONSTANT("SDLK_RIGHT", SDLK_RIGHT, CONST_CS | CONST_PERSISTENT);
|
||||
REGISTER_LONG_CONSTANT("SDLK_UP", SDLK_UP, CONST_CS | CONST_PERSISTENT);
|
||||
REGISTER_LONG_CONSTANT("SDLK_DOWN", SDLK_DOWN, CONST_CS | CONST_PERSISTENT);
|
||||
REGISTER_LONG_CONSTANT("SDLK_HOME", SDLK_HOME, CONST_CS | CONST_PERSISTENT);
|
||||
REGISTER_LONG_CONSTANT("SDLK_END", SDLK_END, CONST_CS | CONST_PERSISTENT);
|
||||
|
||||
// Letter keys for shortcuts
|
||||
REGISTER_LONG_CONSTANT("SDLK_A", SDLK_A, CONST_CS | CONST_PERSISTENT);
|
||||
REGISTER_LONG_CONSTANT("SDLK_C", SDLK_C, CONST_CS | CONST_PERSISTENT);
|
||||
REGISTER_LONG_CONSTANT("SDLK_V", SDLK_V, CONST_CS | CONST_PERSISTENT);
|
||||
REGISTER_LONG_CONSTANT("SDLK_X", SDLK_X, CONST_CS | CONST_PERSISTENT);
|
||||
|
||||
// Key modifiers
|
||||
REGISTER_LONG_CONSTANT("KMOD_NONE", SDL_KMOD_NONE, CONST_CS | CONST_PERSISTENT);
|
||||
REGISTER_LONG_CONSTANT("KMOD_LSHIFT", SDL_KMOD_LSHIFT, CONST_CS | CONST_PERSISTENT);
|
||||
REGISTER_LONG_CONSTANT("KMOD_RSHIFT", SDL_KMOD_RSHIFT, CONST_CS | CONST_PERSISTENT);
|
||||
REGISTER_LONG_CONSTANT("KMOD_LCTRL", SDL_KMOD_LCTRL, CONST_CS | CONST_PERSISTENT);
|
||||
REGISTER_LONG_CONSTANT("KMOD_RCTRL", SDL_KMOD_RCTRL, CONST_CS | CONST_PERSISTENT);
|
||||
REGISTER_LONG_CONSTANT("KMOD_LALT", SDL_KMOD_LALT, CONST_CS | CONST_PERSISTENT);
|
||||
REGISTER_LONG_CONSTANT("KMOD_RALT", SDL_KMOD_RALT, CONST_CS | CONST_PERSISTENT);
|
||||
REGISTER_LONG_CONSTANT("KMOD_LGUI", SDL_KMOD_LGUI, CONST_CS | CONST_PERSISTENT);
|
||||
REGISTER_LONG_CONSTANT("KMOD_RGUI", SDL_KMOD_RGUI, CONST_CS | CONST_PERSISTENT);
|
||||
REGISTER_LONG_CONSTANT("KMOD_CTRL", SDL_KMOD_CTRL, CONST_CS | CONST_PERSISTENT);
|
||||
REGISTER_LONG_CONSTANT("KMOD_SHIFT", SDL_KMOD_SHIFT, CONST_CS | CONST_PERSISTENT);
|
||||
REGISTER_LONG_CONSTANT("KMOD_ALT", SDL_KMOD_ALT, CONST_CS | CONST_PERSISTENT);
|
||||
REGISTER_LONG_CONSTANT("KMOD_GUI", SDL_KMOD_GUI, CONST_CS | CONST_PERSISTENT);
|
||||
}
|
||||
|
||||
PHP_FUNCTION(sdl_poll_event) {
|
||||
@ -173,3 +196,35 @@ PHP_FUNCTION(sdl_wait_event_timeout) {
|
||||
add_assoc_long(return_value, "type", event.type);
|
||||
add_assoc_long(return_value, "timestamp", event.common.timestamp);
|
||||
}
|
||||
|
||||
PHP_FUNCTION(sdl_get_mod_state) {
|
||||
SDL_Keymod mod = SDL_GetModState();
|
||||
RETURN_LONG((zend_long)mod);
|
||||
}
|
||||
|
||||
PHP_FUNCTION(sdl_get_clipboard_text) {
|
||||
char *text = SDL_GetClipboardText();
|
||||
|
||||
if (text == NULL) {
|
||||
RETURN_EMPTY_STRING();
|
||||
}
|
||||
|
||||
RETVAL_STRING(text);
|
||||
SDL_free(text);
|
||||
}
|
||||
|
||||
PHP_FUNCTION(sdl_set_clipboard_text) {
|
||||
char *text;
|
||||
size_t text_len;
|
||||
|
||||
if (zend_parse_parameters(ZEND_NUM_ARGS(), "s", &text, &text_len) == FAILURE) {
|
||||
RETURN_THROWS();
|
||||
}
|
||||
|
||||
if (SDL_SetClipboardText(text) < 0) {
|
||||
php_error_docref(NULL, E_WARNING, "SDL_SetClipboardText failed: %s", SDL_GetError());
|
||||
RETURN_FALSE;
|
||||
}
|
||||
|
||||
RETURN_TRUE;
|
||||
}
|
||||
|
||||
@ -8,6 +8,9 @@
|
||||
PHP_FUNCTION(sdl_poll_event);
|
||||
PHP_FUNCTION(sdl_wait_event);
|
||||
PHP_FUNCTION(sdl_wait_event_timeout);
|
||||
PHP_FUNCTION(sdl_get_mod_state);
|
||||
PHP_FUNCTION(sdl_get_clipboard_text);
|
||||
PHP_FUNCTION(sdl_set_clipboard_text);
|
||||
|
||||
// Argument Info
|
||||
ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_poll_event, 0, 0, 0)
|
||||
@ -20,6 +23,16 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_wait_event_timeout, 0, 0, 1)
|
||||
ZEND_ARG_INFO(0, timeout_ms)
|
||||
ZEND_END_ARG_INFO()
|
||||
|
||||
ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_get_mod_state, 0, 0, 0)
|
||||
ZEND_END_ARG_INFO()
|
||||
|
||||
ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_get_clipboard_text, 0, 0, 0)
|
||||
ZEND_END_ARG_INFO()
|
||||
|
||||
ZEND_BEGIN_ARG_INFO_EX(arginfo_sdl_set_clipboard_text, 0, 0, 1)
|
||||
ZEND_ARG_INFO(0, text)
|
||||
ZEND_END_ARG_INFO()
|
||||
|
||||
// Funktion zum Registrieren von Event-Konstanten
|
||||
void sdl3_events_register_constants(int module_number);
|
||||
|
||||
|
||||
@ -81,7 +81,11 @@ class Application
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$targetFps = 60;
|
||||
$targetFrameDuration = $targetFps > 0 ? (1.0 / $targetFps) : 0.0;
|
||||
|
||||
while ($this->running && count($this->windows) > 0) {
|
||||
$frameStart = microtime(true);
|
||||
// Layout all windows FIRST (sets window references and calculates positions)
|
||||
foreach ($this->windows as $windowId => $window) {
|
||||
$window->layout();
|
||||
@ -178,8 +182,13 @@ class Application
|
||||
}
|
||||
}
|
||||
|
||||
// Limit frame rate to ~60 FPS
|
||||
usleep(16666);
|
||||
if ($targetFrameDuration > 0) {
|
||||
$elapsed = microtime(true) - $frameStart;
|
||||
$timeToSleep = $targetFrameDuration - $elapsed;
|
||||
if ($timeToSleep > 0) {
|
||||
usleep((int) ($timeToSleep * 1_000_000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup remaining windows
|
||||
|
||||
@ -14,4 +14,11 @@ class Style
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
if (is_object($this->style)) {
|
||||
$this->style = clone $this->style;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -53,4 +53,14 @@ class StyleCollection extends TypedCollection
|
||||
|
||||
return $tmp;
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$cloned = [];
|
||||
foreach ($this->items() as $style) {
|
||||
$cloned[] = clone $style;
|
||||
}
|
||||
|
||||
$this->setElements($cloned);
|
||||
}
|
||||
}
|
||||
@ -10,13 +10,24 @@ use PHPNative\Tailwind\Style\Style;
|
||||
|
||||
class StyleParser
|
||||
{
|
||||
private static array $cache = [];
|
||||
|
||||
public static function parse($style): StyleCollection
|
||||
{
|
||||
$computed = new StyleCollection();
|
||||
if($style === null || strlen(trim($style)) === 0) {
|
||||
if($style === null) {
|
||||
return $computed;
|
||||
}
|
||||
$styles = explode(" ", $style);
|
||||
$normalized = trim(preg_replace('/\s+/', ' ', $style));
|
||||
if ($normalized === '') {
|
||||
return $computed;
|
||||
}
|
||||
|
||||
if (isset(self::$cache[$normalized])) {
|
||||
return clone self::$cache[$normalized];
|
||||
}
|
||||
|
||||
$styles = explode(" ", $normalized);
|
||||
|
||||
foreach($styles as $styleStr) {
|
||||
$styleStr = trim($styleStr);
|
||||
@ -39,9 +50,17 @@ class StyleParser
|
||||
}
|
||||
$computed->add($s);
|
||||
}
|
||||
|
||||
self::$cache[$normalized] = clone $computed;
|
||||
|
||||
return $computed;
|
||||
}
|
||||
|
||||
public static function clearCache(): void
|
||||
{
|
||||
self::$cache = [];
|
||||
}
|
||||
|
||||
private static function parseSimpleStyle(string $style): ?Style
|
||||
{
|
||||
if($pd = \PHPNative\Tailwind\Parser\Padding::parse($style)) {
|
||||
|
||||
@ -454,11 +454,11 @@ abstract class Component
|
||||
* @param int $keycode SDL keycode
|
||||
* @return bool True if event was handled
|
||||
*/
|
||||
public function handleKeyDown(int $keycode): bool
|
||||
public function handleKeyDown(int $keycode, int $mod = 0): bool
|
||||
{
|
||||
// Default implementation: propagate to children
|
||||
foreach ($this->children as $child) {
|
||||
if ($child->handleKeyDown($keycode)) {
|
||||
if ($child->handleKeyDown($keycode, $mod)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,9 +130,7 @@ class Container extends Component
|
||||
// Layout overlays separately (they position themselves)
|
||||
foreach ($this->children as $child) {
|
||||
if ($child->isOverlay()) {
|
||||
// Give overlay a viewport based on the window size
|
||||
// The overlay can then adjust its position in its own layout() method
|
||||
if (!isset($child->getViewport()->windowWidth)) {
|
||||
// Overlays always get a full-window viewport; they will decide inner layout themselves
|
||||
$overlayViewport = new Viewport(
|
||||
x: 0,
|
||||
y: 0,
|
||||
@ -142,7 +140,7 @@ class Container extends Component
|
||||
windowHeight: $this->contentViewport->windowHeight,
|
||||
);
|
||||
$child->setViewport($overlayViewport);
|
||||
}
|
||||
$child->setContentViewport(clone $overlayViewport);
|
||||
$child->layout($textRenderer);
|
||||
}
|
||||
}
|
||||
@ -441,6 +439,17 @@ class Container extends Component
|
||||
// at the global level to ensure they appear on top of everything
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the computed content width/height within the container (excludes overlays).
|
||||
*/
|
||||
public function getContentSize(): array
|
||||
{
|
||||
return [
|
||||
'width' => $this->contentWidth,
|
||||
'height' => $this->contentHeight,
|
||||
];
|
||||
}
|
||||
|
||||
private function renderScrollbars(&$renderer, array $overflow): void
|
||||
{
|
||||
$scrollbarColor = [100, 100, 100, 200]; // Gray with some transparency
|
||||
|
||||
176
src/Ui/Widget/Modal.php
Normal file
176
src/Ui/Widget/Modal.php
Normal file
@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace PHPNative\Ui\Widget;
|
||||
|
||||
use PHPNative\Framework\TextRenderer;
|
||||
use PHPNative\Tailwind\Style\Background;
|
||||
use PHPNative\Ui\Component;
|
||||
use PHPNative\Ui\Viewport;
|
||||
|
||||
class Modal extends Container
|
||||
{
|
||||
private Container $content;
|
||||
private int $backdropAlpha;
|
||||
|
||||
public function __construct(
|
||||
Container $content,
|
||||
string $style = 'flex items-center justify-center bg-black',
|
||||
int $backdropAlpha = 160,
|
||||
) {
|
||||
parent::__construct(trim($style));
|
||||
|
||||
$this->content = $content;
|
||||
$this->backdropAlpha = max(0, min(255, $backdropAlpha));
|
||||
|
||||
$this->setOverlay(true);
|
||||
$this->setVisible(false);
|
||||
$this->setZIndex(1000);
|
||||
$this->setUseTextureCache(false);
|
||||
|
||||
$this->addComponent($content);
|
||||
}
|
||||
|
||||
public function layout(null|TextRenderer $textRenderer = null): void
|
||||
{
|
||||
$windowViewport = $this->attachedWindow?->getViewport();
|
||||
if ($windowViewport instanceof Viewport) {
|
||||
$referenceViewport = $windowViewport;
|
||||
} else {
|
||||
$referenceViewport = $this->getParent()?->getViewport() ?? $this->getViewport();
|
||||
}
|
||||
$overlayViewport = new Viewport(
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: $referenceViewport->windowWidth,
|
||||
height: $referenceViewport->windowHeight,
|
||||
windowWidth: $referenceViewport->windowWidth,
|
||||
windowHeight: $referenceViewport->windowHeight,
|
||||
);
|
||||
|
||||
// Set viewport for the modal background (full window)
|
||||
$this->setViewport($overlayViewport);
|
||||
$this->setContentViewport(clone $overlayViewport);
|
||||
|
||||
// Parse styles for the modal background
|
||||
$styleCollection = \PHPNative\Tailwind\StyleParser::parse($this->style);
|
||||
$this->computedStyles = $styleCollection->getValidStyles(
|
||||
\PHPNative\Tailwind\Style\MediaQueryEnum::normal,
|
||||
\PHPNative\Tailwind\Style\StateEnum::normal,
|
||||
);
|
||||
|
||||
if (isset($this->computedStyles[Background::class])) {
|
||||
$this->computedStyles[Background::class]->color->alpha = $this->backdropAlpha;
|
||||
}
|
||||
|
||||
// Calculate max size for content (with padding)
|
||||
$windowWidth = $overlayViewport->width;
|
||||
$windowHeight = $overlayViewport->height;
|
||||
$maxWidth = max(240, $windowWidth - 80);
|
||||
$maxHeight = max(160, $windowHeight - 80);
|
||||
|
||||
// Measure content with max constraints
|
||||
$measurementViewport = new Viewport(
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: $maxWidth,
|
||||
height: $maxHeight,
|
||||
windowWidth: $windowWidth,
|
||||
windowHeight: $windowHeight,
|
||||
);
|
||||
|
||||
$this->content->setViewport($measurementViewport);
|
||||
$this->content->setContentViewport(clone $measurementViewport);
|
||||
$this->content->layout($textRenderer);
|
||||
|
||||
// Get measured size
|
||||
$measuredBounds = $this->content->getViewport();
|
||||
$contentWidth = min($measuredBounds->width, $maxWidth);
|
||||
$contentHeight = min($measuredBounds->height, $maxHeight);
|
||||
|
||||
// Center the content
|
||||
$targetX = (int) max(0, ($windowWidth - $contentWidth) / 2);
|
||||
$targetY = (int) max(0, ($windowHeight - $contentHeight) / 2);
|
||||
|
||||
// Apply final viewport to content
|
||||
$finalViewport = new Viewport(
|
||||
x: $targetX,
|
||||
y: $targetY,
|
||||
width: $contentWidth,
|
||||
height: $contentHeight,
|
||||
windowWidth: $windowWidth,
|
||||
windowHeight: $windowHeight,
|
||||
);
|
||||
|
||||
$this->content->setViewport($finalViewport);
|
||||
$this->content->setContentViewport(clone $finalViewport);
|
||||
$this->content->layout($textRenderer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow replacing the content container.
|
||||
*/
|
||||
public function setContent(Container $content): void
|
||||
{
|
||||
$this->removeChild($this->content);
|
||||
$this->content = $content;
|
||||
$this->addComponent($content);
|
||||
$this->markDirty(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose the content container for further customization.
|
||||
*/
|
||||
public function getContent(): Container
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function setBackdropAlpha(int $alpha): void
|
||||
{
|
||||
$this->backdropAlpha = max(0, min(255, $alpha));
|
||||
$this->markDirty(false);
|
||||
}
|
||||
|
||||
public function handleMouseClick(float $mouseX, float $mouseY, int $button): bool
|
||||
{
|
||||
if (!$this->isVisible()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
parent::handleMouseClick($mouseX, $mouseY, $button);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function handleMouseMove(float $mouseX, float $mouseY): void
|
||||
{
|
||||
if (!$this->isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
parent::handleMouseMove($mouseX, $mouseY);
|
||||
}
|
||||
|
||||
public function handleMouseWheel(float $mouseX, float $mouseY, float $deltaY): bool
|
||||
{
|
||||
if (!$this->isVisible()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
parent::handleMouseWheel($mouseX, $mouseY, $deltaY);
|
||||
return true;
|
||||
}
|
||||
|
||||
private function removeChild(Component $component): void
|
||||
{
|
||||
foreach ($this->children as $index => $child) {
|
||||
if ($child === $component) {
|
||||
if (method_exists($child, 'detachFromWindow')) {
|
||||
$child->detachFromWindow();
|
||||
}
|
||||
$child->setParent(null);
|
||||
unset($this->children[$index]);
|
||||
}
|
||||
}
|
||||
$this->children = array_values($this->children);
|
||||
}
|
||||
}
|
||||
@ -13,6 +13,8 @@ class TextInput extends Container
|
||||
private int $cursorPosition = 0;
|
||||
private float $cursorBlinkTime = 0;
|
||||
private bool $cursorVisible = true;
|
||||
private ?int $selectionStart = null;
|
||||
private ?int $selectionEnd = null;
|
||||
|
||||
public function __construct(
|
||||
string $placeholder = '',
|
||||
@ -98,6 +100,11 @@ class TextInput extends Container
|
||||
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);
|
||||
@ -109,15 +116,69 @@ class TextInput extends Container
|
||||
}
|
||||
}
|
||||
|
||||
public function handleKeyDown(int $keycode): bool
|
||||
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->cursorPosition > 0) {
|
||||
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;
|
||||
@ -129,8 +190,10 @@ class TextInput extends Container
|
||||
}
|
||||
return true;
|
||||
|
||||
case SDLK_DELETE:
|
||||
if ($this->cursorPosition < mb_strlen($this->value)) {
|
||||
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;
|
||||
@ -141,19 +204,79 @@ class TextInput extends Container
|
||||
}
|
||||
return true;
|
||||
|
||||
case SDLK_LEFT:
|
||||
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)) {
|
||||
$this->cursorPosition++;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
case SDLK_RETURN:
|
||||
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;
|
||||
}
|
||||
@ -174,6 +297,53 @@ class TextInput extends Container
|
||||
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
|
||||
@ -204,6 +374,36 @@ class TextInput extends Container
|
||||
'h' => $this->viewport->height - 4,
|
||||
]);
|
||||
|
||||
// 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);
|
||||
$selectionX += $size[0];
|
||||
}
|
||||
|
||||
$selectionWidth = 0;
|
||||
if (!empty($selectedText)) {
|
||||
$size = $textRenderer->measureText($selectedText);
|
||||
$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;
|
||||
|
||||
@ -18,6 +18,10 @@ class Window
|
||||
private float $pixelRatio = 2;
|
||||
private bool $shouldClose = false;
|
||||
private $onResize = null;
|
||||
private $onFpsChange = null;
|
||||
private float $lastFpsUpdate = 0.0;
|
||||
private int $frameCounter = 0;
|
||||
private float $currentFps = 0.0;
|
||||
public function __construct(
|
||||
private string $title,
|
||||
private int $width = 800,
|
||||
@ -78,6 +82,8 @@ class Window
|
||||
height: $this->height,
|
||||
);
|
||||
|
||||
$this->lastFpsUpdate = microtime(true);
|
||||
|
||||
}
|
||||
|
||||
public function setRoot(Component $component): self
|
||||
@ -171,10 +177,11 @@ class Window
|
||||
|
||||
case SDL_EVENT_KEY_DOWN:
|
||||
$keycode = $event['keycode'] ?? 0;
|
||||
$mod = $event['mod'] ?? 0;
|
||||
|
||||
// Propagate key event to root component
|
||||
if ($this->rootComponent) {
|
||||
$this->rootComponent->handleKeyDown($keycode);
|
||||
$this->rootComponent->handleKeyDown($keycode, $mod);
|
||||
}
|
||||
break;
|
||||
|
||||
@ -362,6 +369,8 @@ class Window
|
||||
|
||||
$this->rootComponent->markClean();
|
||||
|
||||
$this->updateFps();
|
||||
|
||||
sdl_render_present($this->renderer);
|
||||
}
|
||||
|
||||
@ -411,4 +420,31 @@ class Window
|
||||
{
|
||||
$this->onResize = $callback;
|
||||
}
|
||||
|
||||
public function setOnFpsChange(?callable $callback): void
|
||||
{
|
||||
$this->onFpsChange = $callback;
|
||||
}
|
||||
|
||||
public function getCurrentFps(): float
|
||||
{
|
||||
return $this->currentFps;
|
||||
}
|
||||
|
||||
private function updateFps(): void
|
||||
{
|
||||
$this->frameCounter++;
|
||||
$now = microtime(true);
|
||||
$elapsed = $now - $this->lastFpsUpdate;
|
||||
|
||||
if ($elapsed >= 1.0) {
|
||||
$this->currentFps = $this->frameCounter / $elapsed;
|
||||
$this->frameCounter = 0;
|
||||
$this->lastFpsUpdate = $now;
|
||||
|
||||
if ($this->onFpsChange !== null) {
|
||||
($this->onFpsChange)($this->currentFps);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user