Backup
This commit is contained in:
parent
e617930ca4
commit
bf986acb49
@ -530,7 +530,25 @@ class ServerListTab
|
|||||||
$serverListTab->currentServerData[$i]['domains'] = $domains;
|
$serverListTab->currentServerData[$i]['domains'] = $domains;
|
||||||
}
|
}
|
||||||
$serverListTab->currentServerData[$i]['docker'] = $dockerResult['docker'];
|
$serverListTab->currentServerData[$i]['docker'] = $dockerResult['docker'];
|
||||||
|
$searchTerm = $serverListTab->searchInput->getValue();
|
||||||
|
if (empty($searchTerm)) {
|
||||||
$serverListTab->table->setData($serverListTab->currentServerData, true);
|
$serverListTab->table->setData($serverListTab->currentServerData, true);
|
||||||
|
} else {
|
||||||
|
$filteredData = array_filter(
|
||||||
|
$serverListTab->currentServerData,
|
||||||
|
function ($row) use ($searchTerm) {
|
||||||
|
return (
|
||||||
|
str_contains(strtolower($row['name']), $searchTerm) ||
|
||||||
|
count(array_filter($row['domains'], function ($item) use (
|
||||||
|
$searchTerm,
|
||||||
|
) {
|
||||||
|
return str_contains(strtolower($item), $searchTerm);
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
$serverListTab->table->setData(array_values($filteredData), true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map system status into human-readable table fields
|
// Map system status into human-readable table fields
|
||||||
@ -664,20 +682,18 @@ class ServerListTab
|
|||||||
if ($result['success']) {
|
if ($result['success']) {
|
||||||
$serverListTab->statusLabel->setText('Reboot ausgelöst für ' . $name);
|
$serverListTab->statusLabel->setText('Reboot ausgelöst für ' . $name);
|
||||||
if (function_exists('desktop_notify')) {
|
if (function_exists('desktop_notify')) {
|
||||||
desktop_notify(
|
desktop_notify('Reboot gestartet', 'Reboot ausgelöst für ' . $name, [
|
||||||
'Reboot gestartet',
|
'timeout' => 4000,
|
||||||
'Reboot ausgelöst für ' . $name,
|
'urgency' => 'normal',
|
||||||
['timeout' => 4000, 'urgency' => 'normal'],
|
]);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} elseif (isset($result['error'])) {
|
} elseif (isset($result['error'])) {
|
||||||
$serverListTab->statusLabel->setText('Reboot Fehler bei ' . $name . ': ' . $result['error']);
|
$serverListTab->statusLabel->setText('Reboot Fehler bei ' . $name . ': ' . $result['error']);
|
||||||
if (function_exists('desktop_notify')) {
|
if (function_exists('desktop_notify')) {
|
||||||
desktop_notify(
|
desktop_notify('Reboot fehlgeschlagen', 'Fehler bei ' . $name . ': ' . $result['error'], [
|
||||||
'Reboot fehlgeschlagen',
|
'timeout' => 6000,
|
||||||
'Fehler bei ' . $name . ': ' . $result['error'],
|
'urgency' => 'critical',
|
||||||
['timeout' => 6000, 'urgency' => 'critical'],
|
]);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -773,27 +789,39 @@ class ServerListTab
|
|||||||
$needsReboot = (bool) $result['needs_reboot'];
|
$needsReboot = (bool) $result['needs_reboot'];
|
||||||
$serverListTab->currentServerData[$index]['needs_reboot'] = $needsReboot ? 'ja' : 'nein';
|
$serverListTab->currentServerData[$index]['needs_reboot'] = $needsReboot ? 'ja' : 'nein';
|
||||||
}
|
}
|
||||||
|
$searchTerm = $serverListTab->searchInput->getValue();
|
||||||
|
if (empty($searchTerm)) {
|
||||||
$serverListTab->table->setData($serverListTab->currentServerData, true);
|
$serverListTab->table->setData($serverListTab->currentServerData, true);
|
||||||
|
} else {
|
||||||
|
$filteredData = array_filter($serverListTab->currentServerData, function ($row) use (
|
||||||
|
$searchTerm,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
str_contains(strtolower($row['name']), $searchTerm) ||
|
||||||
|
count(array_filter($row['domains'], function ($item) use ($searchTerm) {
|
||||||
|
return str_contains(strtolower($item), $searchTerm);
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
$serverListTab->table->setData(array_values($filteredData), false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($result['success']) {
|
if ($result['success']) {
|
||||||
$serverListTab->statusLabel->setText('Updates ausgeführt für ' . $name);
|
$serverListTab->statusLabel->setText('Updates ausgeführt für ' . $name);
|
||||||
if (function_exists('desktop_notify')) {
|
if (function_exists('desktop_notify')) {
|
||||||
desktop_notify(
|
desktop_notify('Update erfolgreich', 'Updates ausgeführt für ' . $name, [
|
||||||
'Update erfolgreich',
|
'timeout' => 5000,
|
||||||
'Updates ausgeführt für ' . $name,
|
'urgency' => 'normal',
|
||||||
['timeout' => 5000, 'urgency' => 'normal'],
|
]);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} elseif (isset($result['error'])) {
|
} elseif (isset($result['error'])) {
|
||||||
$serverListTab->statusLabel->setText('Update Fehler bei ' . $name . ': ' . $result['error']);
|
$serverListTab->statusLabel->setText('Update Fehler bei ' . $name . ': ' . $result['error']);
|
||||||
if (function_exists('desktop_notify')) {
|
if (function_exists('desktop_notify')) {
|
||||||
desktop_notify(
|
desktop_notify('Update fehlgeschlagen', 'Fehler bei ' . $name . ': ' . $result['error'], [
|
||||||
'Update fehlgeschlagen',
|
'timeout' => 6000,
|
||||||
'Fehler bei ' . $name . ': ' . $result['error'],
|
'urgency' => 'critical',
|
||||||
['timeout' => 6000, 'urgency' => 'critical'],
|
]);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -31,6 +31,8 @@ class SftpManagerTab
|
|||||||
private Label $deleteConfirmLabel;
|
private Label $deleteConfirmLabel;
|
||||||
private null|array $currentLocalSelection = null;
|
private null|array $currentLocalSelection = null;
|
||||||
private null|array $currentRemoteSelection = null;
|
private null|array $currentRemoteSelection = null;
|
||||||
|
private ?string $lastRemoteClickPath = null;
|
||||||
|
private float $lastRemoteClickTime = 0.0;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
string &$apiKey,
|
string &$apiKey,
|
||||||
@ -45,6 +47,7 @@ class SftpManagerTab
|
|||||||
$currentRemoteStartDir = &$remoteStartDir;
|
$currentRemoteStartDir = &$remoteStartDir;
|
||||||
|
|
||||||
// Left side: Local file browser
|
// Left side: Local file browser
|
||||||
|
// Lokaler Browser: Spalte, Scrollen übernimmt der FileBrowser selbst
|
||||||
$localBrowserContainer = new Container('flex flex-col flex-1 gap-2');
|
$localBrowserContainer = new Container('flex flex-col flex-1 gap-2');
|
||||||
$localBrowserContainer->addComponent(new Label('Lokal', 'text-lg font-bold text-black mb-2'));
|
$localBrowserContainer->addComponent(new Label('Lokal', 'text-lg font-bold text-black mb-2'));
|
||||||
$this->localFileBrowser = new FileBrowser(
|
$this->localFileBrowser = new FileBrowser(
|
||||||
@ -64,6 +67,7 @@ class SftpManagerTab
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Right side: Remote file browser
|
// Right side: Remote file browser
|
||||||
|
// Remote-Browser: Spalte, Scrollen übernimmt der FileBrowser selbst
|
||||||
$remoteBrowserContainer = new Container('flex flex-col flex-1 gap-2');
|
$remoteBrowserContainer = new Container('flex flex-col flex-1 gap-2');
|
||||||
|
|
||||||
// Header with title and new file button
|
// Header with title and new file button
|
||||||
@ -191,6 +195,25 @@ class SftpManagerTab
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Require double click for navigation:
|
||||||
|
// first click selektiert nur, zweiter Klick (innerhalb kurzer Zeit)
|
||||||
|
// öffnet das Verzeichnis.
|
||||||
|
$now = microtime(true);
|
||||||
|
$doubleClickThreshold = 0.4; // Sekunden
|
||||||
|
|
||||||
|
if (
|
||||||
|
$sftpTab->lastRemoteClickPath !== $path ||
|
||||||
|
($now - $sftpTab->lastRemoteClickTime) > $doubleClickThreshold
|
||||||
|
) {
|
||||||
|
$sftpTab->lastRemoteClickPath = $path;
|
||||||
|
$sftpTab->lastRemoteClickTime = $now;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zweiter Klick innerhalb des Zeitfensters -> als Doppelklick werten
|
||||||
|
$sftpTab->lastRemoteClickPath = null;
|
||||||
|
$sftpTab->lastRemoteClickTime = 0.0;
|
||||||
|
|
||||||
$loadButton = new Button('Load', '');
|
$loadButton = new Button('Load', '');
|
||||||
$loadButton->setOnClickAsync(
|
$loadButton->setOnClickAsync(
|
||||||
function () use ($path, &$currentPrivateKeyPath, &$selectedServerRef) {
|
function () use ($path, &$currentPrivateKeyPath, &$selectedServerRef) {
|
||||||
@ -343,7 +366,9 @@ class SftpManagerTab
|
|||||||
|
|
||||||
if (isset($result['success']) && $result['success']) {
|
if (isset($result['success']) && $result['success']) {
|
||||||
// Switch to SFTP tab on successful connection
|
// Switch to SFTP tab on successful connection
|
||||||
$tabContainer->setActiveTab(1);
|
// Tab-Reihenfolge in App.php:
|
||||||
|
// 0 = Server, 1 = Kanban, 2 = SFTP Manager
|
||||||
|
$tabContainer->setActiveTab(2);
|
||||||
|
|
||||||
$sftpTab->connectionStatusLabel->setText(
|
$sftpTab->connectionStatusLabel->setText(
|
||||||
'Verbunden mit: ' . $result['server']['name'] . ' (' . $result['server']['ipv4'] . ')',
|
'Verbunden mit: ' . $result['server']['name'] . ' (' . $result['server']['ipv4'] . ')',
|
||||||
|
|||||||
33
examples/textarea_scroll_test.php
Normal file
33
examples/textarea_scroll_test.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use PHPNative\Framework\Application;
|
||||||
|
use PHPNative\Ui\Widget\TextArea;
|
||||||
|
use PHPNative\Ui\Window;
|
||||||
|
|
||||||
|
$app = new Application();
|
||||||
|
$window = new Window('TextArea Scroll Test', 800, 600);
|
||||||
|
|
||||||
|
// Root container: nur Hintergrund, TextArea soll den ganzen Bereich nutzen
|
||||||
|
$root = new \PHPNative\Ui\Widget\Container('bg-gray-100');
|
||||||
|
|
||||||
|
// Viel Text erzeugen
|
||||||
|
$lines = [];
|
||||||
|
for ($i = 1; $i <= 200; $i++) {
|
||||||
|
$lines[] = sprintf('Zeile %03d: Dies ist eine Testzeile zum Scrollen in der TextArea.', $i);
|
||||||
|
}
|
||||||
|
$longText = implode("\n", $lines);
|
||||||
|
|
||||||
|
$textArea = new TextArea($longText, 'Scroll-Test', 'w-full h-full border border-gray-300 bg-white text-black font-mono text-sm');
|
||||||
|
|
||||||
|
// Optional: kein Texture-Cache, damit Änderungen sofort sichtbar sind
|
||||||
|
$textArea->setUseTextureCache(false);
|
||||||
|
|
||||||
|
$root->addComponent($textArea);
|
||||||
|
|
||||||
|
$window->setRoot($root);
|
||||||
|
$app->addWindow($window);
|
||||||
|
$app->run();
|
||||||
@ -8,6 +8,8 @@ namespace PHPNative\Framework;
|
|||||||
|
|
||||||
class TextRenderer
|
class TextRenderer
|
||||||
{
|
{
|
||||||
|
private const MAX_TEXTURE_SIZE = 16000;
|
||||||
|
|
||||||
private $renderer;
|
private $renderer;
|
||||||
private bool $initialized = false;
|
private bool $initialized = false;
|
||||||
private string $fontPath = '';
|
private string $fontPath = '';
|
||||||
@ -103,6 +105,58 @@ class TextRenderer
|
|||||||
return $this->loadFont($size);
|
return $this->loadFont($size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure that rendered text does not exceed the maximum supported
|
||||||
|
* texture size by truncating very long strings.
|
||||||
|
*/
|
||||||
|
private function truncateToMaxTextureSize(string $text, $font): string
|
||||||
|
{
|
||||||
|
if ($text === '') {
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dimensions = ttf_size_text($font, $text);
|
||||||
|
$width = (int) ($dimensions['w'] ?? 0);
|
||||||
|
$height = (int) ($dimensions['h'] ?? 0);
|
||||||
|
|
||||||
|
if ($width <= self::MAX_TEXTURE_SIZE && $height <= self::MAX_TEXTURE_SIZE) {
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
$length = \function_exists('mb_strlen') ? mb_strlen($text) : strlen($text);
|
||||||
|
if ($length <= 1) {
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Estimate how much we can keep based on width ratio
|
||||||
|
$scale = self::MAX_TEXTURE_SIZE / max($width, 1);
|
||||||
|
$targetLength = max(1, (int) floor($length * $scale));
|
||||||
|
|
||||||
|
$substr = \function_exists('mb_substr') ? 'mb_substr' : 'substr';
|
||||||
|
$text = $substr($text, 0, $targetLength);
|
||||||
|
|
||||||
|
// Safety: if still too large, iteratively shrink
|
||||||
|
for ($i = 0; $i < 3; $i++) {
|
||||||
|
$dimensions = ttf_size_text($font, $text);
|
||||||
|
$width = (int) ($dimensions['w'] ?? 0);
|
||||||
|
$height = (int) ($dimensions['h'] ?? 0);
|
||||||
|
|
||||||
|
if ($width <= self::MAX_TEXTURE_SIZE && $height <= self::MAX_TEXTURE_SIZE) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$length = \function_exists('mb_strlen') ? mb_strlen($text) : strlen($text);
|
||||||
|
if ($length <= 1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetLength = max(1, (int) floor($length * 0.7));
|
||||||
|
$text = $substr($text, 0, $targetLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
private function getScaledFontSize(int $size): int
|
private function getScaledFontSize(int $size): int
|
||||||
{
|
{
|
||||||
if ($this->pixelRatio <= 1.0) {
|
if ($this->pixelRatio <= 1.0) {
|
||||||
@ -178,7 +232,14 @@ class TextRenderer
|
|||||||
if (strlen($text) < 1) {
|
if (strlen($text) < 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$surface = ttf_render_text_blended($renderFont, $text, $r, $g, $b);
|
// Truncate extremely long text so that the resulting texture
|
||||||
|
// stays within the GPU's max texture size.
|
||||||
|
$safeText = $this->truncateToMaxTextureSize($text, $renderFont);
|
||||||
|
if ($safeText === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$surface = ttf_render_text_blended($renderFont, $safeText, $r, $g, $b);
|
||||||
if (!$surface) {
|
if (!$surface) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -190,7 +251,7 @@ class TextRenderer
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get text size
|
// Get text size
|
||||||
$textSize = ttf_size_text($baseFont, $text);
|
$textSize = ttf_size_text($baseFont, $safeText);
|
||||||
|
|
||||||
// Render texture
|
// Render texture
|
||||||
sdl_render_texture($this->renderer, $texture, [
|
sdl_render_texture($this->renderer, $texture, [
|
||||||
@ -230,7 +291,14 @@ class TextRenderer
|
|||||||
if (strlen($text) < 1) {
|
if (strlen($text) < 1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
$surface = ttf_render_text_blended($renderFont, $text, $r, $g, $b);
|
// Truncate extremely long text so that the resulting texture
|
||||||
|
// stays within the GPU's max texture size.
|
||||||
|
$safeText = $this->truncateToMaxTextureSize($text, $renderFont);
|
||||||
|
if ($safeText === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$surface = ttf_render_text_blended($renderFont, $safeText, $r, $g, $b);
|
||||||
if (!$surface) {
|
if (!$surface) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -240,7 +308,7 @@ class TextRenderer
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$dimensions = ttf_size_text($baseFont, $text);
|
$dimensions = ttf_size_text($baseFont, $safeText);
|
||||||
|
|
||||||
sdl_set_texture_blend_mode($texture, SDL_BLENDMODE_BLEND);
|
sdl_set_texture_blend_mode($texture, SDL_BLENDMODE_BLEND);
|
||||||
if (\function_exists('sdl_set_texture_alpha_mod')) {
|
if (\function_exists('sdl_set_texture_alpha_mod')) {
|
||||||
|
|||||||
@ -30,7 +30,7 @@ class Container extends Component
|
|||||||
private float $scrollStartY = 0;
|
private float $scrollStartY = 0;
|
||||||
|
|
||||||
// Scrollbar dimensions
|
// Scrollbar dimensions
|
||||||
private const SCROLLBAR_WIDTH = 12;
|
private const SCROLLBAR_WIDTH = 16;
|
||||||
private const SCROLLBAR_MIN_SIZE = 20;
|
private const SCROLLBAR_MIN_SIZE = 20;
|
||||||
|
|
||||||
public function __construct(string $style = '')
|
public function __construct(string $style = '')
|
||||||
|
|||||||
@ -12,10 +12,15 @@ class FileBrowser extends Container
|
|||||||
private $onRenameFile = null;
|
private $onRenameFile = null;
|
||||||
private $onDeleteFile = null;
|
private $onDeleteFile = null;
|
||||||
private bool $isRemote = false;
|
private bool $isRemote = false;
|
||||||
|
private ?string $lastClickPath = null;
|
||||||
|
private float $lastClickTime = 0.0;
|
||||||
|
|
||||||
public function __construct(string $initialPath = '.', bool $isRemote = false, string $style = '')
|
public function __construct(string $initialPath = '.', bool $isRemote = false, string $style = '')
|
||||||
{
|
{
|
||||||
parent::__construct('w-full flex flex-col gap-2 ' . $style);
|
// Root-Container füllt die verfügbare Höhe im Eltern-Layout (flex-1),
|
||||||
|
// damit die Tabelle unten eine klar begrenzte Höhe bekommt und
|
||||||
|
// ihr Body sinnvoll scrollen kann.
|
||||||
|
parent::__construct('w-full flex flex-col flex-1 gap-2 ' . $style);
|
||||||
|
|
||||||
$this->currentPath = $initialPath;
|
$this->currentPath = $initialPath;
|
||||||
$this->isRemote = $isRemote;
|
$this->isRemote = $isRemote;
|
||||||
@ -24,8 +29,10 @@ class FileBrowser extends Container
|
|||||||
$this->pathLabel = new Label($initialPath, 'px-3 py-2 bg-gray-200 text-black rounded text-sm font-mono');
|
$this->pathLabel = new Label($initialPath, 'px-3 py-2 bg-gray-200 text-black rounded text-sm font-mono');
|
||||||
$this->addComponent($this->pathLabel);
|
$this->addComponent($this->pathLabel);
|
||||||
|
|
||||||
// File table with explicit flex-1 for scrolling
|
// File table, die den verbleibenden Platz im FileBrowser nutzt.
|
||||||
$this->fileTable = new Table('');
|
// Das eigentliche Scrollen passiert im Body-Container der Table
|
||||||
|
// (overflow-auto dort).
|
||||||
|
$this->fileTable = new Table(' flex-1');
|
||||||
$this->fileTable->setColumns([
|
$this->fileTable->setColumns([
|
||||||
['key' => 'type', 'title' => 'Typ', 'width' => 60],
|
['key' => 'type', 'title' => 'Typ', 'width' => 60],
|
||||||
['key' => 'name', 'title' => 'Name'],
|
['key' => 'name', 'title' => 'Name'],
|
||||||
@ -106,13 +113,39 @@ class FileBrowser extends Container
|
|||||||
// Handle row selection
|
// Handle row selection
|
||||||
$fileBrowser = $this;
|
$fileBrowser = $this;
|
||||||
$this->fileTable->setOnRowSelect(function ($index, $row) use ($fileBrowser) {
|
$this->fileTable->setOnRowSelect(function ($index, $row) use ($fileBrowser) {
|
||||||
if ($row && isset($row['isDir']) && $row['isDir'] && !empty($row['path'])) {
|
if (!$row || empty($row['path'])) {
|
||||||
// Navigate to directory
|
return;
|
||||||
$fileBrowser->loadDirectory($row['path']);
|
|
||||||
} elseif ($row && isset($row['path']) && !empty($row['path']) && $fileBrowser->onFileSelect !== null) {
|
|
||||||
// File selected
|
|
||||||
($fileBrowser->onFileSelect)($row['path'], $row);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$path = $row['path'];
|
||||||
|
|
||||||
|
// Immer Auswahl-Callback auslösen (z.B. für Upload)
|
||||||
|
if ($fileBrowser->onFileSelect !== null) {
|
||||||
|
($fileBrowser->onFileSelect)($path, $row);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nur Verzeichnisse navigieren – per Doppelklick
|
||||||
|
if (!($row['isDir'] ?? false)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = microtime(true);
|
||||||
|
$doubleClickThreshold = 0.4; // Sekunden
|
||||||
|
|
||||||
|
if (
|
||||||
|
$fileBrowser->lastClickPath !== $path ||
|
||||||
|
($now - $fileBrowser->lastClickTime) > $doubleClickThreshold
|
||||||
|
) {
|
||||||
|
// Erster Klick: nur merken
|
||||||
|
$fileBrowser->lastClickPath = $path;
|
||||||
|
$fileBrowser->lastClickTime = $now;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zweiter Klick innerhalb des Zeitfensters -> als Doppelklick werten
|
||||||
|
$fileBrowser->lastClickPath = null;
|
||||||
|
$fileBrowser->lastClickTime = 0.0;
|
||||||
|
$fileBrowser->loadDirectory($path);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,10 @@ class Table extends Container
|
|||||||
|
|
||||||
public function __construct(string $style = '')
|
public function __construct(string $style = '')
|
||||||
{
|
{
|
||||||
parent::__construct('flex flex-col w-full' . $style);
|
// Table selbst ist kein Flex-Container; sie wird als Kind
|
||||||
|
// in einem flex-Layout benutzt (z.B. flex-1), aber intern
|
||||||
|
// werden Header und Body ganz normal vertikal gestapelt.
|
||||||
|
parent::__construct('w-full' . $style);
|
||||||
|
|
||||||
// Create header container
|
// Create header container
|
||||||
$this->headerContainer = new Container('flex flex-row w-full bg-gray-200 border-b-2 border-gray-400');
|
$this->headerContainer = new Container('flex flex-row w-full bg-gray-200 border-b-2 border-gray-400');
|
||||||
@ -88,6 +91,18 @@ class Table extends Container
|
|||||||
if ($preserveScroll && $scrollPosition !== null) {
|
if ($preserveScroll && $scrollPosition !== null) {
|
||||||
$this->bodyContainer->setScrollPosition($scrollPosition['x'], $scrollPosition['y']);
|
$this->bodyContainer->setScrollPosition($scrollPosition['x'], $scrollPosition['y']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debug-Ausgabe: Größe des Tabellen-Body und Viewport
|
||||||
|
$bodySize = $this->bodyContainer->getContentSize();
|
||||||
|
$bodyViewport = $this->bodyContainer->getContentViewport();
|
||||||
|
error_log(
|
||||||
|
sprintf(
|
||||||
|
'Table body debug: rows=%d, contentHeight=%d, viewportHeight=%d',
|
||||||
|
count($data),
|
||||||
|
(int) ($bodySize['height'] ?? 0),
|
||||||
|
(int) $bodyViewport->height,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -10,6 +10,9 @@ use PHPNative\Tailwind\Style\Text;
|
|||||||
|
|
||||||
class TextArea extends Container
|
class TextArea extends Container
|
||||||
{
|
{
|
||||||
|
private const SCROLLBAR_WIDTH = 12;
|
||||||
|
private const SCROLLBAR_MIN_SIZE = 20;
|
||||||
|
|
||||||
private array $lines = [''];
|
private array $lines = [''];
|
||||||
private int $cursorLine = 0;
|
private int $cursorLine = 0;
|
||||||
private int $cursorCol = 0;
|
private int $cursorCol = 0;
|
||||||
@ -23,6 +26,10 @@ class TextArea extends Container
|
|||||||
private int $selectionEndLine = -1;
|
private int $selectionEndLine = -1;
|
||||||
private int $selectionEndCol = -1;
|
private int $selectionEndCol = -1;
|
||||||
|
|
||||||
|
private bool $isDraggingScrollbar = false;
|
||||||
|
private float $dragStartY = 0.0;
|
||||||
|
private int $scrollStartOffsetY = 0;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public string $value = '',
|
public string $value = '',
|
||||||
public string $placeholder = '',
|
public string $placeholder = '',
|
||||||
@ -173,6 +180,48 @@ class TextArea extends Container
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle click on scrollbar (if present)
|
||||||
|
$contentHeight = count($this->lines) * $this->lineHeight;
|
||||||
|
$viewportHeight = $this->contentViewport->height;
|
||||||
|
|
||||||
|
if ($contentHeight > $viewportHeight) {
|
||||||
|
$scrollbarX = ($this->contentViewport->x + $this->contentViewport->width) - self::SCROLLBAR_WIDTH;
|
||||||
|
$scrollbarY = $this->contentViewport->y;
|
||||||
|
|
||||||
|
if (
|
||||||
|
$mouseX >= $scrollbarX &&
|
||||||
|
$mouseX <= ($scrollbarX + self::SCROLLBAR_WIDTH) &&
|
||||||
|
$mouseY >= $scrollbarY &&
|
||||||
|
$mouseY <= ($scrollbarY + $viewportHeight)
|
||||||
|
) {
|
||||||
|
$scrollbarHeight = $viewportHeight;
|
||||||
|
$thumbHeight = max(
|
||||||
|
self::SCROLLBAR_MIN_SIZE,
|
||||||
|
($viewportHeight / $contentHeight) * $scrollbarHeight,
|
||||||
|
);
|
||||||
|
$maxScroll = $contentHeight - $viewportHeight;
|
||||||
|
|
||||||
|
if ($maxScroll > 0) {
|
||||||
|
$thumbRange = $scrollbarHeight - $thumbHeight;
|
||||||
|
|
||||||
|
// Position scroll to where user clicked (center thumb on click)
|
||||||
|
$clickPos = $mouseY - $scrollbarY - ($thumbHeight / 2);
|
||||||
|
$clickPos = max(0, min($thumbRange, $clickPos));
|
||||||
|
$scrollRatio = $thumbRange > 0 ? ($clickPos / $thumbRange) : 0;
|
||||||
|
$this->scrollOffsetY = (int) max(0, min($maxScroll, $scrollRatio * $maxScroll));
|
||||||
|
// Nur neu rendern, kein neues Layout nötig
|
||||||
|
$this->markDirty(false, false);
|
||||||
|
|
||||||
|
// Start drag
|
||||||
|
$this->isDraggingScrollbar = true;
|
||||||
|
$this->dragStartY = $mouseY;
|
||||||
|
$this->scrollStartOffsetY = $this->scrollOffsetY;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
$mouseX >= $this->viewport->x &&
|
$mouseX >= $this->viewport->x &&
|
||||||
$mouseX <= ($this->viewport->x + $this->viewport->width) &&
|
$mouseX <= ($this->viewport->x + $this->viewport->width) &&
|
||||||
@ -205,6 +254,95 @@ class TextArea extends Container
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function handleMouseMove(float $mouseX, float $mouseY): void
|
||||||
|
{
|
||||||
|
parent::handleMouseMove($mouseX, $mouseY);
|
||||||
|
|
||||||
|
if (!$this->visible || !$this->isDraggingScrollbar) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$contentHeight = count($this->lines) * $this->lineHeight;
|
||||||
|
$viewportHeight = $this->contentViewport->height;
|
||||||
|
|
||||||
|
if ($contentHeight <= $viewportHeight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scrollbarHeight = $viewportHeight;
|
||||||
|
$thumbHeight = max(
|
||||||
|
self::SCROLLBAR_MIN_SIZE,
|
||||||
|
($viewportHeight / $contentHeight) * $scrollbarHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
$maxScroll = $contentHeight - $viewportHeight;
|
||||||
|
$thumbRange = $scrollbarHeight - $thumbHeight;
|
||||||
|
|
||||||
|
if ($thumbRange <= 0 || $maxScroll <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$deltaY = $mouseY - $this->dragStartY;
|
||||||
|
$scrollRatioDelta = $deltaY / $thumbRange;
|
||||||
|
$newScroll = $this->scrollStartOffsetY + ($scrollRatioDelta * $maxScroll);
|
||||||
|
|
||||||
|
$newOffset = (int) max(0, min($maxScroll, $newScroll));
|
||||||
|
|
||||||
|
if ($newOffset !== $this->scrollOffsetY) {
|
||||||
|
$this->scrollOffsetY = $newOffset;
|
||||||
|
// Nur neu rendern, kein neues Layout nötig
|
||||||
|
$this->markDirty(false, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleMouseRelease(float $mouseX, float $mouseY, int $button): void
|
||||||
|
{
|
||||||
|
parent::handleMouseRelease($mouseX, $mouseY, $button);
|
||||||
|
$this->isDraggingScrollbar = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleMouseWheel(float $mouseX, float $mouseY, float $deltaY): bool
|
||||||
|
{
|
||||||
|
if (!$this->visible) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
$mouseX < $this->contentViewport->x ||
|
||||||
|
$mouseX > ($this->contentViewport->x + $this->contentViewport->width) ||
|
||||||
|
$mouseY < $this->contentViewport->y ||
|
||||||
|
$mouseY > ($this->contentViewport->y + $this->contentViewport->height)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$contentHeight = count($this->lines) * $this->lineHeight;
|
||||||
|
$maxScroll = max(0, $contentHeight - $this->contentViewport->height);
|
||||||
|
|
||||||
|
if ($maxScroll <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schneller scrollen: mehrere Zeilen pro Tick
|
||||||
|
$scrollStep = max($this->lineHeight * 3, 20);
|
||||||
|
|
||||||
|
$oldOffset = $this->scrollOffsetY;
|
||||||
|
$this->scrollOffsetY = (int) max(
|
||||||
|
0,
|
||||||
|
min($maxScroll, $this->scrollOffsetY + ($deltaY * $scrollStep)),
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($this->scrollOffsetY === $oldOffset) {
|
||||||
|
// Keine tatsächliche Bewegung – parent darf evtl. weiter scrollen
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nur neu rendern, kein neues Layout nötig
|
||||||
|
$this->markDirty(false, false);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public function layout(null|TextRenderer $textRenderer = null): void
|
public function layout(null|TextRenderer $textRenderer = null): void
|
||||||
{
|
{
|
||||||
parent::layout($textRenderer);
|
parent::layout($textRenderer);
|
||||||
@ -274,6 +412,42 @@ class TextArea extends Container
|
|||||||
$this->renderCursor($window, $textRenderer);
|
$this->renderCursor($window, $textRenderer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw simple vertical scrollbar if content is higher than viewport
|
||||||
|
$contentHeight = count($this->lines) * $this->lineHeight;
|
||||||
|
$viewportHeight = $this->contentViewport->height;
|
||||||
|
|
||||||
|
if ($contentHeight > $viewportHeight) {
|
||||||
|
$scrollbarWidth = self::SCROLLBAR_WIDTH;
|
||||||
|
$scrollbarX = ($this->contentViewport->x + $this->contentViewport->width) - $scrollbarWidth;
|
||||||
|
$scrollbarY = $this->contentViewport->y;
|
||||||
|
|
||||||
|
// Track
|
||||||
|
sdl_set_render_draw_color($window, 220, 220, 220, 255);
|
||||||
|
sdl_render_fill_rect($window, [
|
||||||
|
'x' => (int) $scrollbarX,
|
||||||
|
'y' => (int) $scrollbarY,
|
||||||
|
'w' => (int) $scrollbarWidth,
|
||||||
|
'h' => (int) $viewportHeight,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Thumb
|
||||||
|
$thumbHeight = max(
|
||||||
|
20,
|
||||||
|
($viewportHeight / $contentHeight) * $viewportHeight,
|
||||||
|
);
|
||||||
|
$maxScroll = $contentHeight - $viewportHeight;
|
||||||
|
$scrollRatio = $maxScroll > 0 ? ($this->scrollOffsetY / $maxScroll) : 0;
|
||||||
|
$thumbY = $scrollbarY + ($scrollRatio * ($viewportHeight - $thumbHeight));
|
||||||
|
|
||||||
|
sdl_set_render_draw_color($window, 120, 120, 120, 230);
|
||||||
|
sdl_render_fill_rect($window, [
|
||||||
|
'x' => (int) ($scrollbarX + 1),
|
||||||
|
'y' => (int) $thumbY,
|
||||||
|
'w' => (int) ($scrollbarWidth - 2),
|
||||||
|
'h' => (int) $thumbHeight,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
// Update cursor blink
|
// Update cursor blink
|
||||||
$this->cursorBlinkTimer++;
|
$this->cursorBlinkTimer++;
|
||||||
if ($this->cursorBlinkTimer >= 30) {
|
if ($this->cursorBlinkTimer >= 30) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user