addComponent($title); if (!empty($task['note'] ?? '')) { $this->addComponent(new Label($task['note'], 'text-xs text-gray-500')); } } public function handleMouseClick(float $mouseX, float $mouseY, int $button): bool { if ($button !== 1) { return false; } $this->boardView->selectTask($this->boardId, $this->task); return true; } } final class KanbanBoardHeader extends Container { public function __construct( private readonly string $boardId, private KanbanBoardView $boardView, string $title, int $taskCount, ) { parent::__construct('flex flex-col gap-1 cursor-pointer'); $this->addComponent(new Label($title, 'text-lg font-bold text-black')); $this->addComponent(new Label(sprintf('%d Aufgaben', $taskCount), 'text-xs text-gray-700')); $this->addComponent(new Label('Hierhin verschieben (Klick)', 'text-[10] text-gray-500')); } public function handleMouseClick(float $mouseX, float $mouseY, int $button): bool { if ($button !== 1) { return false; } // Nur reagieren, wenn der Klick innerhalb des Header-Viewports liegt $vp = $this->getViewport(); $inside = $mouseX >= $vp->x && $mouseX <= ($vp->x + $vp->width) && $mouseY >= $vp->y && $mouseY <= ($vp->y + $vp->height); if (!$inside) { return false; } if (defined('DEBUG_EVENTS') && DEBUG_EVENTS) { error_log(sprintf('[KanbanHeader] Click on board "%s"', $this->boardId)); } $this->boardView->moveSelectedToBoard($this->boardId); return true; } } final class KanbanBoardView extends Container { private array $boards; private array $boardViews = []; private null|string $selectedTaskId = null; private null|string $selectedBoardId = null; public function __construct( private readonly string $storagePath, array $initialBoards, private readonly Label $statusLabel, ) { parent::__construct('flex flex-row gap-4 flex-1'); $this->boards = $initialBoards; $this->renderBoards(); } public function addBoard(string $title): void { $title = trim($title); if ($title === '') { $this->statusLabel->setText('Board-Namen eingeben, um es hinzuzufügen.'); return; } $this->boards[] = [ 'id' => uniqid('board_', true), 'title' => $title, 'tasks' => [], ]; $this->saveBoards(); $this->renderBoards(); $this->statusLabel->setText('Board hinzugefügt: ' . $title); } public function addTask(string $boardId, string $title): void { $title = trim($title); if ($title === '') { $this->statusLabel->setText('Bitte einen Aufgabentitel eingeben.'); return; } foreach ($this->boards as &$board) { if ($board['id'] === $boardId) { $board['tasks'][] = [ 'id' => uniqid('task_', true), 'title' => $title, 'note' => '', ]; $this->saveBoards(); $this->renderBoards(); $this->statusLabel->setText(sprintf('Aufgabe "%s" zu "%s" hinzugefügt.', $title, $board['title'])); return; } } $this->statusLabel->setText('Board nicht gefunden.'); } public function selectTask(string $boardId, array $task): void { $this->selectedBoardId = $boardId; $this->selectedTaskId = $task['id']; $msg = sprintf('Ausgewählt: "%s" in "%s"', $task['title'], $this->getBoardTitle($boardId)); $this->statusLabel->setText($msg); if (defined('DEBUG_EVENTS') && DEBUG_EVENTS) { error_log('[Kanban] ' . $msg); } $this->renderBoards(); } public function moveSelectedToBoard(string $targetBoardId): void { if ($this->selectedTaskId === null) { if (defined('DEBUG_EVENTS') && DEBUG_EVENTS) { error_log('[Kanban] moveSelectedToBoard called without selectedTaskId'); } $this->statusLabel->setText('Keine Aufgabe ausgewählt.'); return; } if ($this->selectedBoardId === $targetBoardId) { if (defined('DEBUG_EVENTS') && DEBUG_EVENTS) { error_log('[Kanban] moveSelectedToBoard target is same board'); } $this->statusLabel->setText('Aufgabe ist bereits in diesem Board.'); return; } $movedTask = null; foreach ($this->boards as &$board) { if ($board['id'] === $this->selectedBoardId) { foreach ($board['tasks'] as $index => $task) { if ($task['id'] === $this->selectedTaskId) { $movedTask = $task; array_splice($board['tasks'], $index, 1); break 2; } } } } if ($movedTask === null) { if (defined('DEBUG_EVENTS') && DEBUG_EVENTS) { error_log('[Kanban] moveSelectedToBoard: task not found in source board'); } $this->statusLabel->setText('Ausgewählte Aufgabe konnte nicht gefunden werden.'); return; } foreach ($this->boards as &$board) { if ($board['id'] === $targetBoardId) { $board['tasks'][] = $movedTask; break; } } // Nach erfolgreichem Verschieben Auswahl zurücksetzen, // damit weitere Board-Klicks den Task nicht erneut verschieben. $this->selectedBoardId = null; $this->selectedTaskId = null; $this->saveBoards(); $this->renderBoards(); $msg = sprintf('"%s" nach "%s" verschoben.', $movedTask['title'], $this->getBoardTitle($targetBoardId)); $this->statusLabel->setText($msg); if (defined('DEBUG_EVENTS') && DEBUG_EVENTS) { error_log('[Kanban] ' . $msg); } } private function renderBoards(): void { $this->clearChildren(); $this->boardViews = []; foreach ($this->boards as $board) { $defaultStyle = 'flex flex-col w-[340] flex-none bg-white border border-gray-300 rounded-2xl p-5 gap-3'; $highlightStyle = 'flex flex-col w-[340] flex-none bg-blue-50 border-2 border-blue-500 rounded-2xl p-5 gap-3'; $column = new Container($defaultStyle); // Header mit Titel und Task-Anzahl, klickbar als Drop-Ziel $header = new KanbanBoardHeader($board['id'], $this, $board['title'], count($board['tasks'])); $column->addComponent($header); // Task-Karten (Buttons) mit Auswahl-Hervorhebung if (!empty($board['tasks'])) { $taskList = new Container('flex flex-col gap-3 mt-3'); foreach ($board['tasks'] as $task) { $isSelected = $this->selectedTaskId === $task['id']; $style = $isSelected ? 'w-full text-left bg-blue-50 border border-blue-500 rounded-lg px-4 py-3 shadow-md cursor-pointer' : 'w-full text-left bg-white border border-gray-200 rounded-lg px-4 py-3 shadow-md cursor-pointer'; $button = new Button($task['title'], $style); $button->setOnClick(function () use ($board, $task): void { // Debug-Ausgabe in Statuszeile, um Klicks sicher zu sehen $this->statusLabel->setText(sprintf( 'Task-Klick: "%s" in "%s"', $task['title'], $this->getBoardTitle($board['id']), )); $this->selectTask($board['id'], $task); }); $taskList->addComponent($button); } $column->addComponent($taskList); } $this->addComponent($column); $this->boardViews[$board['id']] = [ 'column' => $column, 'defaultStyle' => $defaultStyle, 'highlightStyle' => $highlightStyle, ]; } } private function getBoardTitle(string $boardId): string { foreach ($this->boards as $board) { if ($board['id'] === $boardId) { return $board['title']; } } return $boardId; } private function saveBoards(): void { file_put_contents( $this->storagePath, json_encode($this->boards, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), LOCK_EX, ); } } $storagePath = __DIR__ . '/kanban_data.json'; $boards = []; if (is_file($storagePath)) { $raw = file_get_contents($storagePath); $decoded = json_decode($raw ?: '[]', true); if (is_array($decoded)) { $boards = $decoded; } } if (empty($boards)) { $boards = [ [ 'id' => uniqid('board_', true), 'title' => 'Backlog', 'tasks' => [ ['id' => uniqid('task_', true), 'title' => 'Idee sammeln', 'note' => ''], ['id' => uniqid('task_', true), 'title' => 'Mockups skizzieren', 'note' => ''], ], ], [ 'id' => uniqid('board_', true), 'title' => 'In Arbeit', 'tasks' => [ ['id' => uniqid('task_', true), 'title' => 'API anbinden', 'note' => ''], ], ], [ 'id' => uniqid('board_', true), 'title' => 'Erledigt', 'tasks' => [ ['id' => uniqid('task_', true), 'title' => 'UI Grundlayout', 'note' => ''], ], ], ]; file_put_contents($storagePath, json_encode($boards, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), LOCK_EX); } define('DEBUG_EVENTS', false); $app = new Application(); $window = new Window('Kanban Beispiel', 1200, 800); $root = new Container('flex flex-col h-full w-full bg-gray-100 gap-6 p-6'); $title = new Label('Kanban Board', 'text-3xl font-bold text-black'); $subTitle = new Label( 'Boards hinzufügen, Aufgaben erstellen und per Drag & Drop verschieben.', 'text-base text-gray-700', ); $root->addComponent($title); $root->addComponent($subTitle); $statusLabel = new Label('Bereit.', 'text-sm text-gray-600'); $kanbanView = new KanbanBoardView($storagePath, $boards, $statusLabel); // SDL_Window PixelDensity und DisplayScale im Status anzeigen (falls verfügbar) $densityText = ''; if (function_exists('sdl_get_window_pixel_density')) { $density = sdl_get_window_pixel_density($window->getWindowResource()); $densityText .= sprintf('PixelDensity: %.2f', $density); } if (function_exists('sdl_get_window_display_scale')) { $scale = sdl_get_window_display_scale($window->getWindowResource()); $densityText .= ($densityText !== '' ? ' | ' : '') . sprintf('DisplayScale: %.2f', $scale); } if (function_exists('sdl_get_display_content_scale')) { $contentScale = sdl_get_display_content_scale($window->getWindowResource()); $densityText .= ($densityText !== '' ? ' | ' : '') . sprintf('ContentScale: %.2f', $contentScale); } // Fallback: effektiven Scale aus WindowSize vs. WindowSizeInPixels berechnen if (function_exists('sdl_get_window_size') && function_exists('sdl_get_window_size_in_pixels')) { $logical = sdl_get_window_size($window->getWindowResource()); $pixels = sdl_get_window_size_in_pixels($window->getWindowResource()); if (is_array($logical) && is_array($pixels) && $logical[0] > 0 && $logical[1] > 0) { $scaleX = $pixels[0] / $logical[0]; $scaleY = $pixels[1] / $logical[1]; $densityText .= ($densityText !== '' ? ' | ' : '') . sprintf('EffectiveScale: %.2f x %.2f', $scaleX, $scaleY); } } if (function_exists('sdl_get_current_video_driver')) { $driver = sdl_get_current_video_driver(); if ($driver !== false) { $densityText .= ($densityText !== '' ? ' | ' : '') . 'VideoDriver: ' . $driver; } } if ($densityText !== '') { $statusLabel->setText('Bereit. ' . $densityText); } $boardInput = new TextInput('Neues Board', 'flex-1 border border-gray-300 rounded px-3 py-2 bg-white text-black'); $boardAddButton = new Button('Board hinzufügen', 'px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700'); $boardAddButton->setOnClick(function () use ($kanbanView, $boardInput, $statusLabel): void { $kanbanView->addBoard($boardInput->getValue()); $boardInput->setValue(''); }); $boardRow = new Container('flex flex-row gap-3 items-center'); $boardRow->addComponent($boardInput); $boardRow->addComponent($boardAddButton); $root->addComponent($boardRow); $root->addComponent($kanbanView); $root->addComponent($statusLabel); $window->setRoot($root); $app->addWindow($window); $app->run();