headerContainer = new Container('flex flex-row w-full bg-gray-200 border-b-2 border-gray-400'); $this->addComponent($this->headerContainer); // Create body container (scrollable) // Use flex-1 to fill available space, overflow-auto to enable scrolling $this->bodyContainer = new Container('flex flex-col w-full overflow-auto flex-1'); $this->addComponent($this->bodyContainer); } public function layout(null|TextRenderer $textRenderer = null): void { $this->headerContainer->layout($textRenderer); $this->bodyContainer->layout($textRenderer); parent::layout($textRenderer); } /** * 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) { $key = $column['key']; $title = $column['title'] ?? $key; $width = $column['width'] ?? null; $style = 'px-4 py-2 text-black font-bold border-r border-gray-300 hover:bg-gray-300 cursor-pointer'; if ($width) { $style .= ' w-' . ((int) ($width / 4)); } else { $style .= ' flex-1'; } $headerLabel = new ClickableHeaderLabel($key, $this, $title, $style); $this->headerContainer->addComponent($headerLabel); } } /** * Set table data * * @param array $data Array of row data (associative arrays) * @param bool $preserveScroll Whether to preserve scroll position */ public function setData(array $data, bool $preserveScroll = false): void { $this->rows = $data; $scrollPosition = null; if ($preserveScroll) { $scrollPosition = $this->bodyContainer->getScrollPosition(); } $this->bodyContainer->clearChildren(); foreach ($data as $rowIndex => $row) { $this->addRow($row, $rowIndex); } if ($preserveScroll && $scrollPosition !== null) { $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, ), ); } /** * 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-400'; if ($isSelected) { $rowStyle .= ' bg-blue-100'; } $rowContainer = new Container($rowStyle); // Check if any column has custom render (interactive components) $hasCustomRender = false; foreach ($this->columns as $column) { if (isset($column['render']) && is_callable($column['render'])) { $hasCustomRender = true; break; } } // Enable texture caching for table rows only if no custom render // Custom renders may contain interactive components (buttons) that need event handling if (!$hasCustomRender) { $rowContainer->setUseTextureCache(true); } foreach ($this->columns as $column) { $key = $column['key']; $value = $rowData[$key] ?? ''; $width = $column['width'] ?? null; $cellStyle = 'px-4 py-2 text-black border-r border-gray-300'; if ($width) { $cellStyle .= ' w-' . ((int) ($width / 4)); } else { $cellStyle .= ' flex-1'; } // Check if column has custom render function if (isset($column['render']) && is_callable($column['render'])) { // Wrap custom cell content in a container so that // width/padding styles are applied consistently. $cellContent = $column['render']($rowData, $rowIndex); $cellContainer = new Container($cellStyle); $cellContainer->addComponent($cellContent); $rowContainer->addComponent($cellContainer); } else { $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 { // First, let children (like buttons) handle the click $handled = parent::handleMouseClick($mouseX, $mouseY, $button); if ($handled) { return true; } // If no child handled it, 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 false; } }; $this->bodyContainer->addComponent($clickHandler); } /** * Sort by column */ public function sortByColumn(string $columnKey): void { // Toggle sort direction if clicking same column if ($this->sortColumn === $columnKey) { $this->sortAscending = !$this->sortAscending; } else { $this->sortColumn = $columnKey; $this->sortAscending = true; } // Sort the data $sortedRows = $this->rows; usort($sortedRows, function ($a, $b) use ($columnKey) { $aVal = $a[$columnKey] ?? ''; $bVal = $b[$columnKey] ?? ''; // Natural sort for strings/numbers $result = strnatcasecmp((string) $aVal, (string) $bVal); return $this->sortAscending ? $result : -$result; }); // Re-render with sorted data (reset scroll) $this->setData($sortedRows, false); } /** * Select a row */ public function selectRow(int $rowIndex): void { // Remember current scroll position of the body so that selection // does not reset scrolling back to the top. $scrollPosition = $this->bodyContainer->getScrollPosition(); $this->selectedRowIndex = $rowIndex; // Trigger callback if ($this->onRowSelect !== null) { ($this->onRowSelect)($rowIndex, $this->rows[$rowIndex] ?? null); } // Re-render rows to update selection but keep scroll $this->setData($this->rows, true); // Restore scroll position $this->bodyContainer->setScrollPosition($scrollPosition['x'], $scrollPosition['y']); } /** * Set row select callback */ public function setOnRowSelect(callable $callback): void { $this->onRowSelect = $callback; } /** * Get selected row index */ public function getSelectedRowIndex(): null|int { return $this->selectedRowIndex; } /** * Get selected row data */ public function getSelectedRow(): null|array { return $this->selectedRowIndex !== null ? ($this->rows[$this->selectedRowIndex] ?? null) : null; } }