Fixes
This commit is contained in:
parent
a1fabd3623
commit
6ed95d47e5
119
examples/windows_app_example.php
Normal file
119
examples/windows_app_example.php
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use PHPNative\Framework\Application;
|
||||||
|
use PHPNative\Ui\Widget\Container;
|
||||||
|
use PHPNative\Ui\Widget\Label;
|
||||||
|
use PHPNative\Ui\Widget\MenuBar;
|
||||||
|
use PHPNative\Ui\Widget\StatusBar;
|
||||||
|
use PHPNative\Ui\Widget\TabContainer;
|
||||||
|
use PHPNative\Ui\Widget\Table;
|
||||||
|
use PHPNative\Ui\Window;
|
||||||
|
|
||||||
|
$app = new Application();
|
||||||
|
$window = new Window('Windows Application Example', 800, 600);
|
||||||
|
|
||||||
|
// Main container (flex-col: menu, content, status)
|
||||||
|
$mainContainer = new Container('flex flex-col bg-gray-100');
|
||||||
|
|
||||||
|
// === 1. MenuBar ===
|
||||||
|
$menuBar = new MenuBar();
|
||||||
|
|
||||||
|
// File Menu
|
||||||
|
$fileMenu = $menuBar->addMenu('Datei');
|
||||||
|
$fileMenu->addItem('Neu', function () {
|
||||||
|
echo "Neu clicked\n";
|
||||||
|
});
|
||||||
|
$fileMenu->addItem('Öffnen', function () {
|
||||||
|
echo "Öffnen clicked\n";
|
||||||
|
});
|
||||||
|
$fileMenu->addSeparator();
|
||||||
|
$fileMenu->addItem('Beenden', function () use ($app) {
|
||||||
|
echo "Beenden clicked\n";
|
||||||
|
exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Settings Menu
|
||||||
|
$settingsMenu = $menuBar->addMenu('Einstellungen');
|
||||||
|
$settingsMenu->addItem('Optionen', function () {
|
||||||
|
echo "Optionen clicked\n";
|
||||||
|
});
|
||||||
|
$settingsMenu->addItem('Sprache', function () {
|
||||||
|
echo "Sprache clicked\n";
|
||||||
|
});
|
||||||
|
|
||||||
|
$mainContainer->addComponent($menuBar);
|
||||||
|
|
||||||
|
// === 2. Tab Container (flex-1) ===
|
||||||
|
$tabContainer = new TabContainer('flex-1');
|
||||||
|
|
||||||
|
// Tab 1: Table with data
|
||||||
|
$tab1 = new Container('flex flex-col p-4');
|
||||||
|
$table = new Table();
|
||||||
|
|
||||||
|
$table->setColumns([
|
||||||
|
['key' => 'id', 'title' => 'ID', 'width' => 80],
|
||||||
|
['key' => 'name', 'title' => 'Name'],
|
||||||
|
['key' => 'email', 'title' => 'E-Mail'],
|
||||||
|
['key' => 'status', 'title' => 'Status', 'width' => 120],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$table->setData([
|
||||||
|
['id' => 1, 'name' => 'Max Mustermann', 'email' => 'max@example.com', 'status' => 'Aktiv'],
|
||||||
|
['id' => 2, 'name' => 'Anna Schmidt', 'email' => 'anna@example.com', 'status' => 'Aktiv'],
|
||||||
|
['id' => 3, 'name' => 'Peter Weber', 'email' => 'peter@example.com', 'status' => 'Inaktiv'],
|
||||||
|
['id' => 4, 'name' => 'Lisa Müller', 'email' => 'lisa@example.com', 'status' => 'Aktiv'],
|
||||||
|
['id' => 5, 'name' => 'Tom Klein', 'email' => 'tom@example.com', 'status' => 'Aktiv'],
|
||||||
|
['id' => 6, 'name' => 'Sarah Wagner', 'email' => 'sarah@example.com', 'status' => 'Inaktiv'],
|
||||||
|
['id' => 7, 'name' => 'Michael Becker', 'email' => 'michael@example.com', 'status' => 'Aktiv'],
|
||||||
|
['id' => 8, 'name' => 'Julia Fischer', 'email' => 'julia@example.com', 'status' => 'Aktiv'],
|
||||||
|
['id' => 9, 'name' => 'Daniel Schneider', 'email' => 'daniel@example.com', 'status' => 'Inaktiv'],
|
||||||
|
['id' => 10, 'name' => 'Laura Hoffmann', 'email' => 'laura@example.com', 'status' => 'Aktiv'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Row selection handler
|
||||||
|
$statusBar = null; // Will be set later
|
||||||
|
$table->setOnRowSelect(function ($index, $row) use (&$statusBar) {
|
||||||
|
if ($statusBar && $row) {
|
||||||
|
$statusBar->updateSegment(0, "Selected: {$row['name']} ({$row['email']})");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$tab1->addComponent($table);
|
||||||
|
$tabContainer->addTab('Daten', $tab1);
|
||||||
|
|
||||||
|
// Tab 2: Some info
|
||||||
|
$tab2 = new Container('flex flex-col p-4');
|
||||||
|
$tab2->addComponent(new Label('Dies ist Tab 2', 'text-xl font-bold mb-4'));
|
||||||
|
$tab2->addComponent(new Label('Hier könnte weiterer Inhalt stehen...', ''));
|
||||||
|
$tabContainer->addTab('Info', $tab2);
|
||||||
|
|
||||||
|
// Tab 3: Settings
|
||||||
|
$tab3 = new Container('flex flex-col p-4');
|
||||||
|
$tab3->addComponent(new Label('Einstellungen', 'text-xl font-bold mb-4'));
|
||||||
|
$tab3->addComponent(new Label('Konfigurationsoptionen...', ''));
|
||||||
|
$tabContainer->addTab('Einstellungen', $tab3);
|
||||||
|
|
||||||
|
$mainContainer->addComponent($tabContainer);
|
||||||
|
|
||||||
|
// === 3. StatusBar ===
|
||||||
|
$statusBar = new StatusBar();
|
||||||
|
$statusBar->addSegment('Bereit', '', 0); // Flexible segment
|
||||||
|
$statusBar->addSegment('Zeilen: 10', 'border-l', 200); // Fixed width
|
||||||
|
$statusBar->addSegment('Version 1.0', 'border-l', 150); // Fixed width
|
||||||
|
|
||||||
|
$mainContainer->addComponent($statusBar);
|
||||||
|
|
||||||
|
// Set root and run
|
||||||
|
$window->setRoot($mainContainer);
|
||||||
|
$app->addWindow($window);
|
||||||
|
|
||||||
|
echo "Windows Application Example started!\n";
|
||||||
|
echo "Features:\n";
|
||||||
|
echo "- MenuBar with 'Datei' and 'Einstellungen'\n";
|
||||||
|
echo "- Tab Container with 3 tabs\n";
|
||||||
|
echo "- Scrollable Table in first tab\n";
|
||||||
|
echo "- StatusBar at the bottom\n\n";
|
||||||
|
|
||||||
|
$app->run();
|
||||||
70
examples/windows_menu_example.php
Normal file
70
examples/windows_menu_example.php
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use PHPNative\Framework\Application;
|
||||||
|
use PHPNative\Ui\Widget\Container;
|
||||||
|
use PHPNative\Ui\Widget\Label;
|
||||||
|
use PHPNative\Ui\Widget\MenuBar;
|
||||||
|
use PHPNative\Ui\Widget\StatusBar;
|
||||||
|
use PHPNative\Ui\Widget\TabContainer;
|
||||||
|
use PHPNative\Ui\Widget\Table;
|
||||||
|
use PHPNative\Ui\Window;
|
||||||
|
|
||||||
|
$app = new Application();
|
||||||
|
$window = new Window('Windows Application Example', 800, 600);
|
||||||
|
|
||||||
|
$mainContainer = new Container('bg-gray-100');
|
||||||
|
|
||||||
|
// === 1. MenuBar ===
|
||||||
|
$menuBar = new MenuBar();
|
||||||
|
|
||||||
|
// File Menu
|
||||||
|
$fileMenu = $menuBar->addMenu('Datei');
|
||||||
|
$fileMenu->addItem('Neu', function () {
|
||||||
|
echo "Neu clicked\n";
|
||||||
|
});
|
||||||
|
$fileMenu->addItem('Öffnen', function () {
|
||||||
|
echo "Öffnen clicked\n";
|
||||||
|
});
|
||||||
|
$fileMenu->addSeparator();
|
||||||
|
$fileMenu->addItem('Beenden', function () use ($app) {
|
||||||
|
echo "Beenden clicked\n";
|
||||||
|
exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Settings Menu
|
||||||
|
$settingsMenu = $menuBar->addMenu('Einstellungen');
|
||||||
|
$settingsMenu->addItem('Optionen', function () {
|
||||||
|
echo "Optionen clicked\n";
|
||||||
|
});
|
||||||
|
$settingsMenu->addItem('Sprache', function () {
|
||||||
|
echo "Sprache clicked\n";
|
||||||
|
});
|
||||||
|
|
||||||
|
$mainContainer->addComponent($menuBar);
|
||||||
|
|
||||||
|
$statusBar = new StatusBar();
|
||||||
|
$statusLabel = new Label(
|
||||||
|
text: 'Fenster: ' . $window->getViewport()->windowWidth . 'x' . $window->getViewport()->windowHeight,
|
||||||
|
style: 'basis-2/8 text-black',
|
||||||
|
);
|
||||||
|
$statusBar->addSegment($statusLabel);
|
||||||
|
$statusBar->addSegment(new Label(
|
||||||
|
text: 'Zeilen: 10',
|
||||||
|
style: 'basis-4/8 text-black border-l',
|
||||||
|
)); // Fixed width
|
||||||
|
$statusBar->addSegment(new Label(
|
||||||
|
text: 'Version 1.0',
|
||||||
|
style: 'border-l text-black basis-2/8',
|
||||||
|
));
|
||||||
|
$mainContainer->addComponent($statusBar);
|
||||||
|
$window->setOnResize(function (Window $window) use (&$statusLabel) {
|
||||||
|
$statusLabel->setText(
|
||||||
|
'Fenster: ' . $window->getViewport()->windowWidth . 'x' . $window->getViewport()->windowHeight,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
// Set root and run
|
||||||
|
$window->setRoot($mainContainer);
|
||||||
|
$app->addWindow($window);
|
||||||
|
$app->run();
|
||||||
@ -84,9 +84,7 @@ class Application
|
|||||||
while ($this->running && count($this->windows) > 0) {
|
while ($this->running && count($this->windows) > 0) {
|
||||||
// Layout all windows FIRST (sets window references and calculates positions)
|
// Layout all windows FIRST (sets window references and calculates positions)
|
||||||
foreach ($this->windows as $windowId => $window) {
|
foreach ($this->windows as $windowId => $window) {
|
||||||
if ($window->layout()) {
|
$window->layout();
|
||||||
echo 'Layoutout: ' . PHP_EOL;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// SDL3: Poll all events globally and distribute to the correct windows
|
// SDL3: Poll all events globally and distribute to the correct windows
|
||||||
$events = [];
|
$events = [];
|
||||||
@ -124,12 +122,16 @@ class Application
|
|||||||
// Check if this is a recently closed window (expected)
|
// Check if this is a recently closed window (expected)
|
||||||
if (!in_array($eventWindowId, $this->closedWindowIds)) {
|
if (!in_array($eventWindowId, $this->closedWindowIds)) {
|
||||||
if (defined('DEBUG_EVENTS') && DEBUG_EVENTS) {
|
if (defined('DEBUG_EVENTS') && DEBUG_EVENTS) {
|
||||||
error_log("[Application] WARNING: Event for window_id={$eventWindowId} could not be delivered (window not found)");
|
error_log(
|
||||||
|
"[Application] WARNING: Event for window_id={$eventWindowId} could not be delivered (window not found)",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// This is a recently closed window, events are expected
|
// This is a recently closed window, events are expected
|
||||||
if (defined('DEBUG_EVENTS') && DEBUG_EVENTS) {
|
if (defined('DEBUG_EVENTS') && DEBUG_EVENTS) {
|
||||||
error_log("[Application] Ignoring event for recently closed window_id={$eventWindowId}");
|
error_log(
|
||||||
|
"[Application] Ignoring event for recently closed window_id={$eventWindowId}",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,10 @@ abstract class Component
|
|||||||
|
|
||||||
protected bool $visible = true;
|
protected bool $visible = true;
|
||||||
|
|
||||||
|
protected bool $isOverlay = false;
|
||||||
|
|
||||||
|
protected int $zIndex = 0;
|
||||||
|
|
||||||
protected StateEnum $currentState = StateEnum::normal;
|
protected StateEnum $currentState = StateEnum::normal;
|
||||||
|
|
||||||
protected Viewport $viewport;
|
protected Viewport $viewport;
|
||||||
@ -83,6 +87,36 @@ abstract class Component
|
|||||||
$this->pixelRatio = $pixelRatio;
|
$this->pixelRatio = $pixelRatio;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setVisible(bool $visible): void
|
||||||
|
{
|
||||||
|
$this->visible = $visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isVisible(): bool
|
||||||
|
{
|
||||||
|
return $this->visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setOverlay(bool $isOverlay): void
|
||||||
|
{
|
||||||
|
$this->isOverlay = $isOverlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isOverlay(): bool
|
||||||
|
{
|
||||||
|
return $this->isOverlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setZIndex(int $zIndex): void
|
||||||
|
{
|
||||||
|
$this->zIndex = $zIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getZIndex(): int
|
||||||
|
{
|
||||||
|
return $this->zIndex;
|
||||||
|
}
|
||||||
|
|
||||||
public function update(): void
|
public function update(): void
|
||||||
{
|
{
|
||||||
foreach ($this->children as $child) {
|
foreach ($this->children as $child) {
|
||||||
@ -184,6 +218,26 @@ abstract class Component
|
|||||||
$this->children[] = $component;
|
$this->children[] = $component;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect all overlays recursively from this component and all children
|
||||||
|
* @return array Array of overlay components
|
||||||
|
*/
|
||||||
|
public function collectOverlays(): array
|
||||||
|
{
|
||||||
|
$overlays = [];
|
||||||
|
|
||||||
|
foreach ($this->children as $child) {
|
||||||
|
if ($child->isOverlay()) {
|
||||||
|
$overlays[] = $child;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively collect from children
|
||||||
|
$overlays = array_merge($overlays, $child->collectOverlays());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $overlays;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle mouse click event
|
* Handle mouse click event
|
||||||
* @return bool True if event was handled
|
* @return bool True if event was handled
|
||||||
|
|||||||
@ -22,7 +22,7 @@ class Button extends Container
|
|||||||
// Create label inside button
|
// Create label inside button
|
||||||
$this->label = new Label(
|
$this->label = new Label(
|
||||||
text: $text,
|
text: $text,
|
||||||
style: '',
|
style: 'text-black',
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->addComponent($this->label);
|
$this->addComponent($this->label);
|
||||||
@ -45,6 +45,11 @@ class Button extends Container
|
|||||||
$this->onClick = $onClick;
|
$this->onClick = $onClick;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setStyle(string $style): void
|
||||||
|
{
|
||||||
|
$this->style = $style;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set async click handler that runs in background thread
|
* Set async click handler that runs in background thread
|
||||||
* @param callable $onClickAsync Task to run asynchronously
|
* @param callable $onClickAsync Task to run asynchronously
|
||||||
@ -53,8 +58,8 @@ class Button extends Container
|
|||||||
*/
|
*/
|
||||||
public function setOnClickAsync(
|
public function setOnClickAsync(
|
||||||
callable $onClickAsync,
|
callable $onClickAsync,
|
||||||
?callable $onComplete = null,
|
null|callable $onComplete = null,
|
||||||
?callable $onError = null
|
null|callable $onError = null,
|
||||||
): void {
|
): void {
|
||||||
$this->onClickAsync = [
|
$this->onClickAsync = [
|
||||||
'task' => $onClickAsync,
|
'task' => $onClickAsync,
|
||||||
@ -75,7 +80,7 @@ class Button extends Container
|
|||||||
$this->viewport->x,
|
$this->viewport->x,
|
||||||
$this->viewport->y,
|
$this->viewport->y,
|
||||||
$this->viewport->x + $this->viewport->width,
|
$this->viewport->x + $this->viewport->width,
|
||||||
$this->viewport->y + $this->viewport->height
|
$this->viewport->y + $this->viewport->height,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,8 +106,7 @@ class Button extends Container
|
|||||||
if ($this->onClickAsync['onError'] !== null) {
|
if ($this->onClickAsync['onError'] !== null) {
|
||||||
$task->onError($this->onClickAsync['onError']);
|
$task->onError($this->onClickAsync['onError']);
|
||||||
}
|
}
|
||||||
}
|
} // Call sync onClick callback if set
|
||||||
// Call sync onClick callback if set
|
|
||||||
elseif ($this->onClick !== null) {
|
elseif ($this->onClick !== null) {
|
||||||
($this->onClick)();
|
($this->onClick)();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -103,6 +103,11 @@ class Container extends Component
|
|||||||
$currentY = $this->contentViewport->y;
|
$currentY = $this->contentViewport->y;
|
||||||
|
|
||||||
foreach ($this->children as $child) {
|
foreach ($this->children as $child) {
|
||||||
|
// Skip overlays in normal layout flow
|
||||||
|
if ($child->isOverlay()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Create a viewport for the child with available width but let height be determined by child
|
// Create a viewport for the child with available width but let height be determined by child
|
||||||
$childViewport = new Viewport(
|
$childViewport = new Viewport(
|
||||||
x: $this->contentViewport->x,
|
x: $this->contentViewport->x,
|
||||||
@ -122,6 +127,26 @@ 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)) {
|
||||||
|
$overlayViewport = new Viewport(
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: $this->contentViewport->windowWidth,
|
||||||
|
height: $this->contentViewport->windowHeight,
|
||||||
|
windowWidth: $this->contentViewport->windowWidth,
|
||||||
|
windowHeight: $this->contentViewport->windowHeight,
|
||||||
|
);
|
||||||
|
$child->setViewport($overlayViewport);
|
||||||
|
}
|
||||||
|
$child->layout($textRenderer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate total content size after children are laid out
|
// Calculate total content size after children are laid out
|
||||||
$this->calculateContentSize();
|
$this->calculateContentSize();
|
||||||
|
|
||||||
@ -166,6 +191,10 @@ class Container extends Component
|
|||||||
$usedSpace = 0;
|
$usedSpace = 0;
|
||||||
|
|
||||||
foreach ($this->children as $index => $child) {
|
foreach ($this->children as $index => $child) {
|
||||||
|
// Skip overlays in flex layout
|
||||||
|
if ($child->isOverlay()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// Parse child styles to get basis, width, height, and flex
|
// Parse child styles to get basis, width, height, and flex
|
||||||
$childStyles = \PHPNative\Tailwind\StyleParser::parse($child->style)->getValidStyles(
|
$childStyles = \PHPNative\Tailwind\StyleParser::parse($child->style)->getValidStyles(
|
||||||
\PHPNative\Tailwind\Style\MediaQueryEnum::normal,
|
\PHPNative\Tailwind\Style\MediaQueryEnum::normal,
|
||||||
@ -229,6 +258,11 @@ class Container extends Component
|
|||||||
$currentPosition = $isRow ? $this->contentViewport->x : $this->contentViewport->y;
|
$currentPosition = $isRow ? $this->contentViewport->x : $this->contentViewport->y;
|
||||||
|
|
||||||
foreach ($this->children as $index => $child) {
|
foreach ($this->children as $index => $child) {
|
||||||
|
// Skip overlays in flex layout
|
||||||
|
if ($child->isOverlay()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$childSize = $childSizes[$index];
|
$childSize = $childSizes[$index];
|
||||||
$size = $childSize['flexGrow'] ? $flexGrowSize : $childSize['size'];
|
$size = $childSize['flexGrow'] ? $flexGrowSize : $childSize['size'];
|
||||||
|
|
||||||
@ -284,6 +318,11 @@ class Container extends Component
|
|||||||
$maxY = 0;
|
$maxY = 0;
|
||||||
|
|
||||||
foreach ($this->children as $child) {
|
foreach ($this->children as $child) {
|
||||||
|
// Skip overlays - they don't contribute to content size
|
||||||
|
if ($child->isOverlay()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$childViewport = $child->getViewport();
|
$childViewport = $child->getViewport();
|
||||||
$maxX = max($maxX, ($childViewport->x + $childViewport->width) - $this->contentViewport->x);
|
$maxX = max($maxX, ($childViewport->x + $childViewport->width) - $this->contentViewport->x);
|
||||||
$maxY = max($maxY, ($childViewport->y + $childViewport->height) - $this->contentViewport->y);
|
$maxY = max($maxY, ($childViewport->y + $childViewport->height) - $this->contentViewport->y);
|
||||||
@ -348,11 +387,16 @@ class Container extends Component
|
|||||||
'x' => $scissorX,
|
'x' => $scissorX,
|
||||||
'y' => $scissorY,
|
'y' => $scissorY,
|
||||||
'w' => $scissorW,
|
'w' => $scissorW,
|
||||||
'h' => $scissorH
|
'h' => $scissorH,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Render children with scroll offset
|
// Render children with scroll offset (skip overlays)
|
||||||
foreach ($this->children as $child) {
|
foreach ($this->children as $child) {
|
||||||
|
// Skip overlays - they'll be rendered later
|
||||||
|
if ($child->isOverlay()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Apply scroll offset recursively to child and all its descendants
|
// Apply scroll offset recursively to child and all its descendants
|
||||||
$child->applyScrollOffset((int) $this->scrollX, (int) $this->scrollY);
|
$child->applyScrollOffset((int) $this->scrollX, (int) $this->scrollY);
|
||||||
|
|
||||||
@ -379,9 +423,17 @@ class Container extends Component
|
|||||||
// Render scrollbars
|
// Render scrollbars
|
||||||
$this->renderScrollbars($renderer, $overflow);
|
$this->renderScrollbars($renderer, $overflow);
|
||||||
} else {
|
} else {
|
||||||
// No overflow, render normally
|
// No overflow, render normally (skip overlays)
|
||||||
parent::renderContent($renderer, $textRenderer);
|
foreach ($this->children as $child) {
|
||||||
|
if (!$child->isOverlay()) {
|
||||||
|
$child->render($renderer, $textRenderer);
|
||||||
|
$child->renderContent($renderer, $textRenderer);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: Overlays are NOT rendered here - they will be rendered by Window.php
|
||||||
|
// at the global level to ensure they appear on top of everything
|
||||||
}
|
}
|
||||||
|
|
||||||
private function renderScrollbars(&$renderer, array $overflow): void
|
private function renderScrollbars(&$renderer, array $overflow): void
|
||||||
@ -402,15 +454,12 @@ class Container extends Component
|
|||||||
|
|
||||||
// Track
|
// Track
|
||||||
sdl_set_render_draw_color($renderer, 200, 200, 200, 100);
|
sdl_set_render_draw_color($renderer, 200, 200, 200, 100);
|
||||||
sdl_render_fill_rect(
|
sdl_render_fill_rect($renderer, [
|
||||||
$renderer,
|
'x' => (int) $scrollbarX,
|
||||||
[
|
'y' => (int) $this->contentViewport->y,
|
||||||
'x' => (int) $scrollbarX,
|
'w' => (int) self::SCROLLBAR_WIDTH,
|
||||||
'y' => (int) $this->contentViewport->y,
|
'h' => (int) $scrollbarHeight,
|
||||||
'w' => (int) self::SCROLLBAR_WIDTH,
|
]);
|
||||||
'h' => (int) $scrollbarHeight,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Thumb - using sdl_rounded_box for rounded rectangle
|
// Thumb - using sdl_rounded_box for rounded rectangle
|
||||||
$thumbX = (int) ($scrollbarX + 2);
|
$thumbX = (int) ($scrollbarX + 2);
|
||||||
@ -443,15 +492,12 @@ class Container extends Component
|
|||||||
|
|
||||||
// Track
|
// Track
|
||||||
sdl_set_render_draw_color($renderer, 200, 200, 200, 100);
|
sdl_set_render_draw_color($renderer, 200, 200, 200, 100);
|
||||||
sdl_render_fill_rect(
|
sdl_render_fill_rect($renderer, [
|
||||||
$renderer,
|
'x' => (int) $this->contentViewport->x,
|
||||||
[
|
'y' => (int) $scrollbarY,
|
||||||
'x' => (int) $this->contentViewport->x,
|
'w' => (int) $scrollbarWidth,
|
||||||
'y' => (int) $scrollbarY,
|
'h' => (int) self::SCROLLBAR_WIDTH,
|
||||||
'w' => (int) $scrollbarWidth,
|
]);
|
||||||
'h' => (int) self::SCROLLBAR_WIDTH,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Thumb - using sdl_rounded_box for rounded rectangle
|
// Thumb - using sdl_rounded_box for rounded rectangle
|
||||||
$thumbY = (int) ($scrollbarY + 2);
|
$thumbY = (int) ($scrollbarY + 2);
|
||||||
|
|||||||
100
src/Ui/Widget/Menu.php
Normal file
100
src/Ui/Widget/Menu.php
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PHPNative\Ui\Widget;
|
||||||
|
|
||||||
|
use PHPNative\Framework\TextRenderer;
|
||||||
|
|
||||||
|
class Menu extends Container
|
||||||
|
{
|
||||||
|
private Button $menuButton;
|
||||||
|
private Container $dropdown;
|
||||||
|
private bool $isOpen = false;
|
||||||
|
private array $items = [];
|
||||||
|
|
||||||
|
public function __construct(string $title, callable $onToggle)
|
||||||
|
{
|
||||||
|
parent::__construct('relative');
|
||||||
|
|
||||||
|
// Create menu button
|
||||||
|
$this->menuButton = new Button($title, 'px-4 py-2 hover:bg-gray-200', $onToggle);
|
||||||
|
|
||||||
|
$this->addComponent($this->menuButton);
|
||||||
|
|
||||||
|
// Create dropdown container (initially hidden)
|
||||||
|
$this->dropdown = new Container('flex flex-col absolute bg-white border border-gray-300 shadow-lg z-50');
|
||||||
|
$this->dropdown->setVisible(false);
|
||||||
|
$this->dropdown->setOverlay(true); // Mark as overlay so it doesn't affect layout and renders on top
|
||||||
|
$this->dropdown->setZIndex(100); // Dropdown menus at z-index 100 (modals could use 1000+)
|
||||||
|
$this->addComponent($this->dropdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a menu item
|
||||||
|
*
|
||||||
|
* @param string $text Menu item text
|
||||||
|
* @param callable $onClick Click handler
|
||||||
|
*/
|
||||||
|
public function addItem(string $text, callable $onClick): MenuItem
|
||||||
|
{
|
||||||
|
$menuItem = new MenuItem($text, function () use ($onClick) {
|
||||||
|
// Close menu and execute action
|
||||||
|
$this->close();
|
||||||
|
$onClick();
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->items[] = $menuItem;
|
||||||
|
$this->dropdown->addComponent($menuItem);
|
||||||
|
|
||||||
|
return $menuItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a separator
|
||||||
|
*/
|
||||||
|
public function addSeparator(): void
|
||||||
|
{
|
||||||
|
$separator = new Separator(true, 'my-1');
|
||||||
|
$this->items[] = $separator;
|
||||||
|
$this->dropdown->addComponent($separator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the menu
|
||||||
|
*/
|
||||||
|
public function open(): void
|
||||||
|
{
|
||||||
|
$this->isOpen = true;
|
||||||
|
$this->dropdown->setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the menu
|
||||||
|
*/
|
||||||
|
public function close(): void
|
||||||
|
{
|
||||||
|
$this->isOpen = false;
|
||||||
|
$this->dropdown->setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if menu is open
|
||||||
|
*/
|
||||||
|
public function isOpen(): bool
|
||||||
|
{
|
||||||
|
return $this->isOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function layout(null|TextRenderer $textRenderer = null): void
|
||||||
|
{
|
||||||
|
parent::layout($textRenderer);
|
||||||
|
|
||||||
|
// Get the button's viewport to position dropdown correctly
|
||||||
|
$buttonViewport = $this->menuButton->getViewport();
|
||||||
|
$this->dropdown->getViewport()->x = $buttonViewport->x;
|
||||||
|
$this->dropdown->getViewport()->y = $buttonViewport->y + $buttonViewport->height;
|
||||||
|
$this->dropdown->getViewport()->width = 200; // Fixed width for dropdown
|
||||||
|
$this->dropdown->getViewport()->windowWidth = $buttonViewport->windowWidth;
|
||||||
|
$this->dropdown->getViewport()->windowHeight = $buttonViewport->windowHeight;
|
||||||
|
$this->dropdown->layout($textRenderer);
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/Ui/Widget/MenuBar.php
Normal file
82
src/Ui/Widget/MenuBar.php
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PHPNative\Ui\Widget;
|
||||||
|
|
||||||
|
use PHPNative\Framework\TextRenderer;
|
||||||
|
|
||||||
|
class MenuBar extends Container
|
||||||
|
{
|
||||||
|
private array $menus = [];
|
||||||
|
private null|int $openMenuIndex = null;
|
||||||
|
|
||||||
|
public function __construct(string $style = 'w-full bg-gray-100 border-b border-gray-300')
|
||||||
|
{
|
||||||
|
parent::__construct('flex flex-row ' . $style);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a menu to the menu bar
|
||||||
|
*
|
||||||
|
* @param string $title Menu title
|
||||||
|
* @return Menu The created menu
|
||||||
|
*/
|
||||||
|
public function addMenu(string $title): Menu
|
||||||
|
{
|
||||||
|
$menuIndex = count($this->menus);
|
||||||
|
|
||||||
|
$menu = new Menu($title, function () use ($menuIndex) {
|
||||||
|
$this->toggleMenu($menuIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->menus[] = $menu;
|
||||||
|
$this->addComponent($menu);
|
||||||
|
|
||||||
|
return $menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle a menu open/closed
|
||||||
|
*
|
||||||
|
* @param int $index Menu index
|
||||||
|
*/
|
||||||
|
private function toggleMenu(int $index): void
|
||||||
|
{
|
||||||
|
if ($this->openMenuIndex === $index) {
|
||||||
|
// Close the currently open menu
|
||||||
|
$this->menus[$index]->close();
|
||||||
|
$this->openMenuIndex = null;
|
||||||
|
} else {
|
||||||
|
// Close previously open menu
|
||||||
|
if ($this->openMenuIndex !== null) {
|
||||||
|
$this->menus[$this->openMenuIndex]->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the clicked menu
|
||||||
|
$this->menus[$index]->open();
|
||||||
|
$this->openMenuIndex = $index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close all menus
|
||||||
|
*/
|
||||||
|
public function closeAllMenus(): void
|
||||||
|
{
|
||||||
|
if ($this->openMenuIndex !== null) {
|
||||||
|
$this->menus[$this->openMenuIndex]->close();
|
||||||
|
$this->openMenuIndex = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleMouseClick(float $mouseX, float $mouseY, int $button): bool
|
||||||
|
{
|
||||||
|
$handled = parent::handleMouseClick($mouseX, $mouseY, $button);
|
||||||
|
|
||||||
|
// If click was not handled by any menu, close all menus
|
||||||
|
if (!$handled && $this->openMenuIndex !== null) {
|
||||||
|
$this->closeAllMenus();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $handled;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/Ui/Widget/MenuItem.php
Normal file
11
src/Ui/Widget/MenuItem.php
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PHPNative\Ui\Widget;
|
||||||
|
|
||||||
|
class MenuItem extends Button
|
||||||
|
{
|
||||||
|
public function __construct(string $text, callable $onClick, string $style = '')
|
||||||
|
{
|
||||||
|
parent::__construct($text, 'px-4 text py-2 hover:bg-gray-100 text-left w-full ' . $style, $onClick);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/Ui/Widget/Separator.php
Normal file
52
src/Ui/Widget/Separator.php
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PHPNative\Ui\Widget;
|
||||||
|
|
||||||
|
use PHPNative\Framework\TextRenderer;
|
||||||
|
use PHPNative\Ui\Component;
|
||||||
|
|
||||||
|
class Separator extends Component
|
||||||
|
{
|
||||||
|
private bool $horizontal;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
bool $horizontal = true,
|
||||||
|
string $style = '',
|
||||||
|
) {
|
||||||
|
$this->horizontal = $horizontal;
|
||||||
|
|
||||||
|
// Default styles based on orientation
|
||||||
|
$defaultStyle = $horizontal
|
||||||
|
? 'bg-gray-300 h-0.5 w-full my-1'
|
||||||
|
: 'bg-gray-300 w-0.5 h-full mx-1';
|
||||||
|
|
||||||
|
parent::__construct($defaultStyle . ' ' . $style);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function layout(null|TextRenderer $textRenderer = null): void
|
||||||
|
{
|
||||||
|
parent::layout($textRenderer);
|
||||||
|
|
||||||
|
// Apply explicit height/width from styles
|
||||||
|
$height = $this->computedStyles[\PHPNative\Tailwind\Style\Height::class] ?? null;
|
||||||
|
$width = $this->computedStyles[\PHPNative\Tailwind\Style\Width::class] ?? null;
|
||||||
|
|
||||||
|
if ($this->horizontal) {
|
||||||
|
// For horizontal separator, enforce the height from style (default h-0.5 = 2px)
|
||||||
|
if ($height) {
|
||||||
|
$this->viewport->height = (int) $height->value;
|
||||||
|
} elseif ($this->viewport->height < 1) {
|
||||||
|
$this->viewport->height = 2; // Default 2px
|
||||||
|
}
|
||||||
|
$this->contentViewport->height = $this->viewport->height;
|
||||||
|
} else {
|
||||||
|
// For vertical separator, enforce the width from style (default w-0.5 = 2px)
|
||||||
|
if ($width) {
|
||||||
|
$this->viewport->width = (int) $width->value;
|
||||||
|
} elseif ($this->viewport->width < 1) {
|
||||||
|
$this->viewport->width = 2; // Default 2px
|
||||||
|
}
|
||||||
|
$this->contentViewport->width = $this->viewport->width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/Ui/Widget/StatusBar.php
Normal file
20
src/Ui/Widget/StatusBar.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PHPNative\Ui\Widget;
|
||||||
|
|
||||||
|
use PHPNative\Framework\TextRenderer;
|
||||||
|
|
||||||
|
class StatusBar extends Container
|
||||||
|
{
|
||||||
|
private array $segments = [];
|
||||||
|
|
||||||
|
public function __construct(string $style = 'w-full bg-gray-200 border-t border-gray-300 flex-row')
|
||||||
|
{
|
||||||
|
parent::__construct($style);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addSegment(Label $label): void
|
||||||
|
{
|
||||||
|
$this->addComponent($label);
|
||||||
|
}
|
||||||
|
}
|
||||||
111
src/Ui/Widget/TabContainer.php
Normal file
111
src/Ui/Widget/TabContainer.php
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PHPNative\Ui\Widget;
|
||||||
|
|
||||||
|
use PHPNative\Framework\TextRenderer;
|
||||||
|
|
||||||
|
class TabContainer extends Container
|
||||||
|
{
|
||||||
|
private Container $tabHeaderContainer;
|
||||||
|
private Container $tabContentContainer;
|
||||||
|
private array $tabs = [];
|
||||||
|
private int $activeTabIndex = 0;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
string $style = '',
|
||||||
|
) {
|
||||||
|
parent::__construct('flex flex-col ' . $style);
|
||||||
|
|
||||||
|
// Create header container for tab buttons
|
||||||
|
$this->tabHeaderContainer = new Container('flex flex-row bg-gray-100 border-b border-gray-300');
|
||||||
|
$this->addComponent($this->tabHeaderContainer);
|
||||||
|
|
||||||
|
// Create content container
|
||||||
|
$this->tabContentContainer = new Container('flex-1 overflow-auto');
|
||||||
|
$this->addComponent($this->tabContentContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a tab with title and content
|
||||||
|
*
|
||||||
|
* @param string $title Tab title
|
||||||
|
* @param Container $content Tab content container
|
||||||
|
*/
|
||||||
|
public function addTab(string $title, Container $content): void
|
||||||
|
{
|
||||||
|
$tabIndex = count($this->tabs);
|
||||||
|
$isActive = $tabIndex === $this->activeTabIndex;
|
||||||
|
|
||||||
|
// Create tab button
|
||||||
|
$tabStyle = $isActive
|
||||||
|
? 'bg-white border-t border-l border-r border-gray-300 px-4 py-2'
|
||||||
|
: 'bg-gray-200 hover:bg-gray-300 border-t border-l border-r border-transparent px-4 py-2';
|
||||||
|
|
||||||
|
$tabButton = new Button($title, $tabStyle);
|
||||||
|
$tabButton->setOnClick(function() use ($tabIndex) {
|
||||||
|
$this->setActiveTab($tabIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->tabHeaderContainer->addComponent($tabButton);
|
||||||
|
|
||||||
|
$this->tabs[] = [
|
||||||
|
'title' => $title,
|
||||||
|
'button' => $tabButton,
|
||||||
|
'content' => $content,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add content to container if it's the active tab
|
||||||
|
if ($isActive) {
|
||||||
|
$this->tabContentContainer->clearChildren();
|
||||||
|
$this->tabContentContainer->addComponent($content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the active tab by index
|
||||||
|
*
|
||||||
|
* @param int $index Tab index
|
||||||
|
*/
|
||||||
|
public function setActiveTab(int $index): void
|
||||||
|
{
|
||||||
|
if ($index < 0 || $index >= count($this->tabs)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->activeTabIndex = $index;
|
||||||
|
|
||||||
|
// Update tab button styles
|
||||||
|
foreach ($this->tabs as $i => $tab) {
|
||||||
|
$isActive = $i === $index;
|
||||||
|
$style = $isActive
|
||||||
|
? 'bg-white border-t border-l border-r border-gray-300 px-4 py-2'
|
||||||
|
: 'bg-gray-200 hover:bg-gray-300 border-t border-l border-r border-transparent px-4 py-2';
|
||||||
|
|
||||||
|
$tab['button']->setStyle($style);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update content
|
||||||
|
$this->tabContentContainer->clearChildren();
|
||||||
|
$this->tabContentContainer->addComponent($this->tabs[$index]['content']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the active tab index
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getActiveTabIndex(): int
|
||||||
|
{
|
||||||
|
return $this->activeTabIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total number of tabs
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getTabCount(): int
|
||||||
|
{
|
||||||
|
return count($this->tabs);
|
||||||
|
}
|
||||||
|
}
|
||||||
171
src/Ui/Widget/Table.php
Normal file
171
src/Ui/Widget/Table.php
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PHPNative\Ui\Widget;
|
||||||
|
|
||||||
|
use PHPNative\Framework\TextRenderer;
|
||||||
|
|
||||||
|
class Table extends Container
|
||||||
|
{
|
||||||
|
private array $columns = [];
|
||||||
|
private array $rows = [];
|
||||||
|
private Container $headerContainer;
|
||||||
|
private Container $bodyContainer;
|
||||||
|
private ?int $selectedRowIndex = null;
|
||||||
|
private $onRowSelect = null;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
string $style = '',
|
||||||
|
) {
|
||||||
|
parent::__construct('flex flex-col overflow-auto ' . $style);
|
||||||
|
|
||||||
|
// Create header container
|
||||||
|
$this->headerContainer = new Container('flex flex-row bg-gray-200 border-b-2 border-gray-400');
|
||||||
|
$this->addComponent($this->headerContainer);
|
||||||
|
|
||||||
|
// Create body container (scrollable)
|
||||||
|
$this->bodyContainer = new Container('flex flex-col overflow-auto flex-1');
|
||||||
|
$this->addComponent($this->bodyContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define columns
|
||||||
|
*
|
||||||
|
* @param array $columns Array of column definitions ['key' => string, 'title' => string, 'width' => int|null]
|
||||||
|
*/
|
||||||
|
public function setColumns(array $columns): void
|
||||||
|
{
|
||||||
|
$this->columns = $columns;
|
||||||
|
$this->headerContainer->clearChildren();
|
||||||
|
|
||||||
|
foreach ($columns as $column) {
|
||||||
|
$title = $column['title'] ?? $column['key'];
|
||||||
|
$width = $column['width'] ?? null;
|
||||||
|
|
||||||
|
$style = 'px-4 py-2 font-bold border-r border-gray-300';
|
||||||
|
if ($width) {
|
||||||
|
$style .= ' w-' . ((int)($width / 4));
|
||||||
|
} else {
|
||||||
|
$style .= ' flex-1';
|
||||||
|
}
|
||||||
|
|
||||||
|
$headerLabel = new Label($title, $style);
|
||||||
|
$this->headerContainer->addComponent($headerLabel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set table data
|
||||||
|
*
|
||||||
|
* @param array $data Array of row data (associative arrays)
|
||||||
|
*/
|
||||||
|
public function setData(array $data): void
|
||||||
|
{
|
||||||
|
$this->rows = $data;
|
||||||
|
$this->bodyContainer->clearChildren();
|
||||||
|
|
||||||
|
foreach ($data as $rowIndex => $row) {
|
||||||
|
$this->addRow($row, $rowIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a single row
|
||||||
|
*/
|
||||||
|
private function addRow(array $rowData, int $rowIndex): void
|
||||||
|
{
|
||||||
|
$isSelected = $rowIndex === $this->selectedRowIndex;
|
||||||
|
$rowStyle = 'flex flex-row border-b border-gray-200 hover:bg-gray-100';
|
||||||
|
if ($isSelected) {
|
||||||
|
$rowStyle .= ' bg-blue-100';
|
||||||
|
}
|
||||||
|
|
||||||
|
$rowContainer = new Container($rowStyle);
|
||||||
|
|
||||||
|
foreach ($this->columns as $column) {
|
||||||
|
$key = $column['key'];
|
||||||
|
$value = $rowData[$key] ?? '';
|
||||||
|
$width = $column['width'] ?? null;
|
||||||
|
|
||||||
|
$cellStyle = 'px-4 py-2 border-r border-gray-300';
|
||||||
|
if ($width) {
|
||||||
|
$cellStyle .= ' w-' . ((int)($width / 4));
|
||||||
|
} else {
|
||||||
|
$cellStyle .= ' flex-1';
|
||||||
|
}
|
||||||
|
|
||||||
|
$cellLabel = new Label((string)$value, $cellStyle);
|
||||||
|
$rowContainer->addComponent($cellLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make row clickable
|
||||||
|
$clickHandler = new class($rowContainer, $rowIndex, $this) extends Container {
|
||||||
|
private int $rowIndex;
|
||||||
|
private Table $table;
|
||||||
|
|
||||||
|
public function __construct(Container $rowContainer, int $rowIndex, Table $table)
|
||||||
|
{
|
||||||
|
$this->rowIndex = $rowIndex;
|
||||||
|
$this->table = $table;
|
||||||
|
parent::__construct('');
|
||||||
|
$this->addComponent($rowContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleMouseClick(float $mouseX, float $mouseY, int $button): bool
|
||||||
|
{
|
||||||
|
// Check if click is within row 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->table->selectRow($this->rowIndex);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return parent::handleMouseClick($mouseX, $mouseY, $button);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$this->bodyContainer->addComponent($clickHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a row
|
||||||
|
*/
|
||||||
|
public function selectRow(int $rowIndex): void
|
||||||
|
{
|
||||||
|
$this->selectedRowIndex = $rowIndex;
|
||||||
|
|
||||||
|
// Trigger callback
|
||||||
|
if ($this->onRowSelect !== null) {
|
||||||
|
($this->onRowSelect)($rowIndex, $this->rows[$rowIndex] ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-render rows to update selection
|
||||||
|
$this->setData($this->rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set row select callback
|
||||||
|
*/
|
||||||
|
public function setOnRowSelect(callable $callback): void
|
||||||
|
{
|
||||||
|
$this->onRowSelect = $callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get selected row index
|
||||||
|
*/
|
||||||
|
public function getSelectedRowIndex(): ?int
|
||||||
|
{
|
||||||
|
return $this->selectedRowIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get selected row data
|
||||||
|
*/
|
||||||
|
public function getSelectedRow(): ?array
|
||||||
|
{
|
||||||
|
return $this->selectedRowIndex !== null ? ($this->rows[$this->selectedRowIndex] ?? null) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,7 +10,7 @@ class Window
|
|||||||
private mixed $renderer = null;
|
private mixed $renderer = null;
|
||||||
private int $windowId = 0;
|
private int $windowId = 0;
|
||||||
private null|Component $rootComponent = null;
|
private null|Component $rootComponent = null;
|
||||||
private ?TextRenderer $textRenderer;
|
private null|TextRenderer $textRenderer;
|
||||||
private float $mouseX = 0;
|
private float $mouseX = 0;
|
||||||
private float $mouseY = 0;
|
private float $mouseY = 0;
|
||||||
private Viewport $viewport;
|
private Viewport $viewport;
|
||||||
@ -18,6 +18,7 @@ class Window
|
|||||||
private float $pixelRatio = 2;
|
private float $pixelRatio = 2;
|
||||||
private bool $shouldClose = false;
|
private bool $shouldClose = false;
|
||||||
private bool $hasBeenLaidOut = false;
|
private bool $hasBeenLaidOut = false;
|
||||||
|
private $onResize = null;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private string $title,
|
private string $title,
|
||||||
@ -160,87 +161,89 @@ class Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch ($event['type']) {
|
switch ($event['type']) {
|
||||||
case SDL_EVENT_QUIT:
|
case SDL_EVENT_QUIT:
|
||||||
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
|
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
|
||||||
$this->shouldClose = true;
|
$this->shouldClose = true;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SDL_EVENT_KEY_DOWN:
|
case SDL_EVENT_KEY_DOWN:
|
||||||
$keycode = $event['keycode'] ?? 0;
|
$keycode = $event['keycode'] ?? 0;
|
||||||
|
|
||||||
// Propagate key event to root component
|
// Propagate key event to root component
|
||||||
if ($this->rootComponent) {
|
if ($this->rootComponent) {
|
||||||
$this->rootComponent->handleKeyDown($keycode);
|
$this->rootComponent->handleKeyDown($keycode);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SDL_EVENT_TEXT_INPUT:
|
case SDL_EVENT_TEXT_INPUT:
|
||||||
$text = $event['text'] ?? '';
|
$text = $event['text'] ?? '';
|
||||||
|
|
||||||
// Propagate text input to root component
|
// Propagate text input to root component
|
||||||
if ($this->rootComponent && !empty($text)) {
|
if ($this->rootComponent && !empty($text)) {
|
||||||
$this->rootComponent->handleTextInput($text);
|
$this->rootComponent->handleTextInput($text);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SDL_EVENT_WINDOW_RESIZED:
|
case SDL_EVENT_WINDOW_RESIZED:
|
||||||
// Update window dimensions (from event data)
|
// Update window dimensions (from event data)
|
||||||
$newWidth = $event['data1'] ?? $this->width;
|
$newWidth = $event['data1'] ?? $this->width;
|
||||||
$newHeight = $event['data2'] ?? $this->height;
|
$newHeight = $event['data2'] ?? $this->height;
|
||||||
|
$this->width = $newWidth;
|
||||||
|
$this->height = $newHeight;
|
||||||
|
|
||||||
$this->width = $newWidth;
|
// Update text renderer framebuffer
|
||||||
$this->height = $newHeight;
|
if ($this->textRenderer && $this->textRenderer->isInitialized()) {
|
||||||
|
$this->textRenderer->updateFramebuffer($newWidth, $newHeight);
|
||||||
|
}
|
||||||
|
|
||||||
// Update text renderer framebuffer
|
$this->viewport->x = 0;
|
||||||
if ($this->textRenderer && $this->textRenderer->isInitialized()) {
|
$this->viewport->y = 0;
|
||||||
$this->textRenderer->updateFramebuffer($newWidth, $newHeight);
|
$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;
|
||||||
|
|
||||||
$this->viewport->x = 0;
|
case SDL_EVENT_MOUSE_MOTION:
|
||||||
$this->viewport->y = 0;
|
$this->mouseX = $event['x'] ?? 0;
|
||||||
$this->viewport->windowWidth = $newWidth;
|
$this->mouseY = $event['y'] ?? 0;
|
||||||
$this->viewport->width = $newWidth;
|
|
||||||
$this->viewport->height = $newHeight;
|
|
||||||
$this->viewport->windowHeight = $newHeight;
|
|
||||||
$this->shouldBeReLayouted = true;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SDL_EVENT_MOUSE_MOTION:
|
// Propagate mouse move to root component
|
||||||
$this->mouseX = $event['x'] ?? 0;
|
if ($this->rootComponent) {
|
||||||
$this->mouseY = $event['y'] ?? 0;
|
$this->rootComponent->handleMouseMove($this->mouseX, $this->mouseY);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
// Propagate mouse move to root component
|
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||||
if ($this->rootComponent) {
|
$button = $event['button'] ?? 0;
|
||||||
$this->rootComponent->handleMouseMove($this->mouseX, $this->mouseY);
|
// Propagate click to root component
|
||||||
}
|
if ($this->rootComponent) {
|
||||||
break;
|
$this->rootComponent->handleMouseClick($this->mouseX, $this->mouseY, $button);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
case SDL_EVENT_MOUSE_BUTTON_UP:
|
||||||
$button = $event['button'] ?? 0;
|
$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:
|
// Propagate release to root component
|
||||||
$button = $event['button'] ?? 0;
|
if ($this->rootComponent) {
|
||||||
|
$this->rootComponent->handleMouseRelease($this->mouseX, $this->mouseY, $button);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
// Propagate release to root component
|
case SDL_EVENT_MOUSE_WHEEL:
|
||||||
if ($this->rootComponent) {
|
$deltaY = $event['y'] ?? 0;
|
||||||
$this->rootComponent->handleMouseRelease($this->mouseX, $this->mouseY, $button);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SDL_EVENT_MOUSE_WHEEL:
|
// Propagate wheel to root component
|
||||||
$deltaY = $event['y'] ?? 0;
|
if ($this->rootComponent) {
|
||||||
|
$this->rootComponent->handleMouseWheel($this->mouseX, $this->mouseY, $deltaY);
|
||||||
// Propagate wheel to root component
|
}
|
||||||
if ($this->rootComponent) {
|
break;
|
||||||
$this->rootComponent->handleMouseWheel($this->mouseX, $this->mouseY, $deltaY);
|
}
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -295,6 +298,17 @@ class Window
|
|||||||
if ($this->hasBeenLaidOut && $this->rootComponent) {
|
if ($this->hasBeenLaidOut && $this->rootComponent) {
|
||||||
$this->rootComponent->render($this->renderer, $this->textRenderer);
|
$this->rootComponent->render($this->renderer, $this->textRenderer);
|
||||||
$this->rootComponent->renderContent($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
|
// Present the rendered content
|
||||||
@ -337,4 +351,14 @@ class Window
|
|||||||
{
|
{
|
||||||
$this->shouldBeReLayouted = $shouldBeReLayouted;
|
$this->shouldBeReLayouted = $shouldBeReLayouted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getViewport(): Viewport
|
||||||
|
{
|
||||||
|
return $this->viewport;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setOnResize(callable $callback): void
|
||||||
|
{
|
||||||
|
$this->onResize = $callback;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user