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;
}
$this->boardView->selectTask($this->boardId, $this->task);
// Start drag operation on click
$this->boardView->beginDrag($this->boardId, $this->task, $this);
return true;
}
}
@ -90,6 +91,7 @@ final class KanbanBoardView extends Container
private array $boardViews = [];
private null|string $selectedTaskId = null;
private null|string $selectedBoardId = null;
private null|array $dragState = null;
public function __construct(
private readonly string $storagePath,
@ -158,6 +160,41 @@ final class KanbanBoardView extends Container
$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
{
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
{
$this->clearChildren();

View File

@ -7,26 +7,27 @@
"id": "task_691661d89de735.41071535",
"title": "Idee sammeln",
"note": ""
},
{
"id": "task_691661d89de8e0.86926319",
"title": "UI Grundlayout",
"note": ""
}
]
},
{
"id": "board_691661d89de806.79123800",
"title": "In Arbeit",
"tasks": []
"tasks": [
{
"id": "task_691661d89de858.46479539",
"title": "API anbinden",
"note": ""
}
]
},
{
"id": "board_691661d89de894.20053237",
"title": "Erledigt",
"tasks": [
{
"id": "task_691661d89de858.46479539",
"title": "API anbinden",
"id": "task_691661d89de8e0.86926319",
"title": "UI Grundlayout",
"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
{
$this->normalStylesCached = StyleParser::parse($this->style)->getValidStyles(

View File

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