This commit is contained in:
Thomas Peterson 2025-11-15 19:07:20 +01:00
parent 3558819899
commit 37c2a1db67
4 changed files with 123 additions and 15 deletions

View File

@ -37,7 +37,8 @@ final class KanbanTaskCard extends Container
return false; return false;
} }
$this->boardView->selectTask($this->boardId, $this->task); // Start drag operation on click
$this->boardView->beginDrag($this->boardId, $this->task, $this);
return true; return true;
} }
} }
@ -90,6 +91,7 @@ final class KanbanBoardView extends Container
private array $boardViews = []; private array $boardViews = [];
private null|string $selectedTaskId = null; private null|string $selectedTaskId = null;
private null|string $selectedBoardId = null; private null|string $selectedBoardId = null;
private null|array $dragState = null;
public function __construct( public function __construct(
private readonly string $storagePath, private readonly string $storagePath,
@ -158,6 +160,41 @@ final class KanbanBoardView extends Container
$this->renderBoards(); $this->renderBoards();
} }
public function beginDrag(string $boardId, array $task, KanbanTaskCard $card): void
{
$vp = $card->getViewport();
$ghost = new Container('bg-yellow-300 border-2 border-red-600 rounded px-3 py-2 shadow-2xl');
$ghost->addComponent(new Label($task['title'], 'text-sm text-black'));
$ghost->setViewport(clone $vp);
$ghost->setContentViewport(clone $vp);
$ghost->setOverlay(true);
$this->addComponent($ghost);
$this->dragState = [
'fromBoardId' => $boardId,
'task' => $task,
'ghost' => $ghost,
];
$this->statusLabel->setText('Drag gestartet: ' . $task['title']);
}
public function handleDrag(float $mouseX, float $mouseY): void
{
if ($this->dragState === null) {
return;
}
/** @var Container $ghost */
$ghost = $this->dragState['ghost'];
$vp = $ghost->getViewport();
$vp->x = (int) ($mouseX - $vp->width / 2);
$vp->y = (int) ($mouseY - $vp->height / 2);
$ghost->setViewport($vp);
$ghost->setContentViewport(clone $vp);
}
public function moveSelectedToBoard(string $targetBoardId): void public function moveSelectedToBoard(string $targetBoardId): void
{ {
if ($this->selectedTaskId === null) { if ($this->selectedTaskId === null) {
@ -218,6 +255,46 @@ final class KanbanBoardView extends Container
} }
} }
public function handleMouseRelease(float $mouseX, float $mouseY, int $button): void
{
parent::handleMouseRelease($mouseX, $mouseY, $button);
if ($button !== 1 || $this->dragState === null) {
return;
}
// Determine drop target board
$targetBoardId = null;
foreach ($this->boardViews as $boardId => $info) {
$vp = $info['column']->getViewport();
if (
$mouseX >= $vp->x &&
$mouseX <= ($vp->x + $vp->width) &&
$mouseY >= $vp->y &&
$mouseY <= ($vp->y + $vp->height)
) {
$targetBoardId = $boardId;
break;
}
}
// Remove ghost
if (($this->dragState['ghost'] ?? null) instanceof Container) {
$this->dragState['ghost']->setVisible(false);
}
$state = $this->dragState;
$this->dragState = null;
if ($targetBoardId !== null) {
$this->selectedBoardId = $state['fromBoardId'];
$this->selectedTaskId = $state['task']['id'];
$this->moveSelectedToBoard($targetBoardId);
} else {
$this->statusLabel->setText('Drag abgebrochen.');
}
}
private function renderBoards(): void private function renderBoards(): void
{ {
$this->clearChildren(); $this->clearChildren();

View File

@ -7,26 +7,27 @@
"id": "task_691661d89de735.41071535", "id": "task_691661d89de735.41071535",
"title": "Idee sammeln", "title": "Idee sammeln",
"note": "" "note": ""
},
{
"id": "task_691661d89de8e0.86926319",
"title": "UI Grundlayout",
"note": ""
} }
] ]
}, },
{ {
"id": "board_691661d89de806.79123800", "id": "board_691661d89de806.79123800",
"title": "In Arbeit", "title": "In Arbeit",
"tasks": [] "tasks": [
{
"id": "task_691661d89de858.46479539",
"title": "API anbinden",
"note": ""
}
]
}, },
{ {
"id": "board_691661d89de894.20053237", "id": "board_691661d89de894.20053237",
"title": "Erledigt", "title": "Erledigt",
"tasks": [ "tasks": [
{ {
"id": "task_691661d89de858.46479539", "id": "task_691661d89de8e0.86926319",
"title": "API anbinden", "title": "UI Grundlayout",
"note": "" "note": ""
} }
] ]

View File

@ -344,6 +344,19 @@ abstract class Component
} }
} }
/**
* Optional: handle drag (mouse move with pressed button).
* Default: propagate to children.
*/
public function handleDrag(float $mouseX, float $mouseY): void
{
foreach ($this->children as $child) {
if (method_exists($child, 'handleDrag')) {
$child->handleDrag($mouseX, $mouseY);
}
}
}
public function layout(null|TextRenderer $textRenderer = null): void public function layout(null|TextRenderer $textRenderer = null): void
{ {
$this->normalStylesCached = StyleParser::parse($this->style)->getValidStyles( $this->normalStylesCached = StyleParser::parse($this->style)->getValidStyles(

View File

@ -17,7 +17,7 @@ class Window
private Viewport $viewport; private Viewport $viewport;
private bool $shouldBeReLayouted = true; private bool $shouldBeReLayouted = true;
private float $pixelRatio = 1.0; private float $pixelRatio = 1.0;
private float $uiScale = 2.0; private float $uiScale = 1.0;
private bool $shouldClose = false; private bool $shouldClose = false;
private $onResize = null; private $onResize = null;
private $onFpsChange = null; private $onFpsChange = null;
@ -51,15 +51,19 @@ class Window
$flags = SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_RESIZABLE; $flags = SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_RESIZABLE;
$this->window = sdl_create_window($title, $width, $height, $flags); $this->window = sdl_create_window($title, $width, $height, $flags);
if (!$this->window) { if (!$this->window) {
throw new \Exception('Failed to create window: ' . sdl_get_error()); throw new \Exception('Failed to create window: ' . sdl_get_error());
} }
// Get window ID for event routing // Get window ID for event routing
$this->windowId = sdl_get_window_id($this->window); $this->windowId = sdl_get_window_id($this->window);
// Use display scale as UI scale (e.g. 2.0 on HiDPI)
$this->uiScale = sdl_get_window_display_scale($this->window); if (function_exists('sdl_get_window_display_scale')) {
$scale = sdl_get_window_display_scale($this->window);
if ($scale > 0.1 && $scale <= 4.0) {
$this->uiScale = (float) $scale;
}
}
// Enable text input for this window // Enable text input for this window
sdl_start_text_input($this->window); sdl_start_text_input($this->window);
@ -237,6 +241,10 @@ class Window
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;
// Drag handling: mouse move while left button is pressed
if ($this->leftButtonDown && $this->rootComponent) {
$this->rootComponent->handleDrag($this->mouseX, $this->mouseY);
}
break; break;
case SDL_EVENT_KEY_DOWN: case SDL_EVENT_KEY_DOWN:
@ -283,8 +291,9 @@ class Window
break; break;
case SDL_EVENT_MOUSE_MOTION: case SDL_EVENT_MOUSE_MOTION:
$newMouseX = (float) ($event['x'] ?? 0); // Convert physical pixels to logical coordinates using uiScale
$newMouseY = (float) ($event['y'] ?? 0); $newMouseX = (float) ($event['x'] ?? 0) / $this->uiScale;
$newMouseY = (float) ($event['y'] ?? 0) / $this->uiScale;
$this->mouseX = $newMouseX; $this->mouseX = $newMouseX;
$this->mouseY = $newMouseY; $this->mouseY = $newMouseY;
@ -316,6 +325,10 @@ class Window
case SDL_EVENT_MOUSE_BUTTON_DOWN: case SDL_EVENT_MOUSE_BUTTON_DOWN:
$button = $event['button'] ?? 0; $button = $event['button'] ?? 0;
if ($button === 1) {
$this->leftButtonDown = true;
}
// Check overlays first (in reverse z-index order - highest first) // Check overlays first (in reverse z-index order - highest first)
if ($this->rootComponent) { if ($this->rootComponent) {
$overlays = $this->rootComponent->collectOverlays(); $overlays = $this->rootComponent->collectOverlays();
@ -342,6 +355,10 @@ class Window
case SDL_EVENT_MOUSE_BUTTON_UP: case SDL_EVENT_MOUSE_BUTTON_UP:
$button = $event['button'] ?? 0; $button = $event['button'] ?? 0;
if ($button === 1) {
$this->leftButtonDown = false;
}
// Propagate release to root component // Propagate release to root component
if ($this->rootComponent) { if ($this->rootComponent) {
$this->rootComponent->handleMouseRelease($this->mouseX, $this->mouseY, $button); $this->rootComponent->handleMouseRelease($this->mouseX, $this->mouseY, $button);