From 4e510b2ac28dd0fd71152204a2a35acd61d0c48f Mon Sep 17 00:00:00 2001 From: Thomas Peterson Date: Wed, 22 Oct 2025 19:25:29 +0200 Subject: [PATCH] First Scroll --- examples/OverflowScroll.php | 64 ++++++ examples/Todo.php | 14 ++ src/Framework/Application.php | 20 +- src/Ui/Component.php | 65 ++++++- src/Ui/Widget/Container.php | 357 +++++++++++++++++++++++++++++++++- 5 files changed, 508 insertions(+), 12 deletions(-) create mode 100644 examples/OverflowScroll.php diff --git a/examples/OverflowScroll.php b/examples/OverflowScroll.php new file mode 100644 index 0000000..44c2270 --- /dev/null +++ b/examples/OverflowScroll.php @@ -0,0 +1,64 @@ +addComponent($title); + +// Example 1: Vertical scroll with overflow-y-auto +$scrollContainer = new Container(style: 'overflow-y-auto bg-white m-4 p-4 h-200'); + +// Add many items to trigger overflow +for ($i = 1; $i <= 20; $i++) { + $item = new Container(style: 'bg-blue-500 m-2 p-3 rounded-lg'); + $label = new Label( + text: "Item {$i} - Scroll vertically with mouse wheel or drag the scrollbar", + style: 'text-white' + ); + $item->addComponent($label); + $scrollContainer->addComponent($item); +} + +$mainContainer->addComponent($scrollContainer); + +// Example 2: Horizontal scroll with overflow-x-auto +$label2 = new Label(text: 'Horizontal Scroll:', style: 'text-black p-2'); +$mainContainer->addComponent($label2); + +$horizontalScroll = new Container(style: 'flex flex-row overflow-x-auto bg-white m-4 p-4 h-100'); + +for ($i = 1; $i <= 10; $i++) { + $box = new Container(style: 'w-150 bg-green-500 m-2 p-3 rounded-lg'); + $boxLabel = new Label(text: "Box {$i}", style: 'text-white'); + $box->addComponent($boxLabel); + $horizontalScroll->addComponent($box); +} + +$mainContainer->addComponent($horizontalScroll); + +// Instructions +$instructions = new Container(style: 'bg-yellow-200 p-4 m-4 rounded-lg'); +$instructionText = new Label( + text: 'Use mouse wheel to scroll. Click and drag scrollbars.', + style: 'text-black' +); +$instructions->addComponent($instructionText); +$mainContainer->addComponent($instructions); + +$app->setRoot($mainContainer); +$app->run(); diff --git a/examples/Todo.php b/examples/Todo.php index 83220db..9438db2 100644 --- a/examples/Todo.php +++ b/examples/Todo.php @@ -1,5 +1,6 @@ addComponent($label); +$scrollContainer = new Container(style: 'overflow-y-auto bg-white m-4 p-4'); + +// Add many items to trigger overflow +for ($i = 1; $i <= 20; $i++) { + $item = new Container(style: 'bg-blue-500 m-2 p-3 rounded-lg'); + $label = new Label( + text: "Item {$i} - Scroll vertically with mouse wheel or drag the scrollbar", + style: 'text-white', + ); + $item->addComponent($label); + $scrollContainer->addComponent($item); +} +$containerMenu->addComponent($scrollContainer); $container->addComponent($containerMenu); $app->setRoot($container); $app->run(); diff --git a/src/Framework/Application.php b/src/Framework/Application.php index 8829f34..3bcc435 100644 --- a/src/Framework/Application.php +++ b/src/Framework/Application.php @@ -126,7 +126,7 @@ class Application // Propagate mouse move to root component if ($this->rootComponent) { - // $this->rootComponent->handleMouseMove($this->mouseX, $this->mouseY); + $this->rootComponent->handleMouseMove($this->mouseX, $this->mouseY); } break; @@ -138,6 +138,24 @@ class Application $this->rootComponent->handleMouseClick($this->mouseX, $this->mouseY, $button); } break; + + case RGFW_mouseButtonReleased: + $button = $event['button'] ?? 0; + + // Propagate release to root component + if ($this->rootComponent) { + $this->rootComponent->handleMouseRelease($this->mouseX, $this->mouseY, $button); + } + break; + + case RGFW_mouseScroll: + $deltaY = $event[1] ?? 0; + + // Propagate wheel to root component + if ($this->rootComponent) { + $this->rootComponent->handleMouseWheel($this->mouseX, $this->mouseY, $deltaY); + } + break; } } } diff --git a/src/Ui/Component.php b/src/Ui/Component.php index 8049c63..124e276 100644 --- a/src/Ui/Component.php +++ b/src/Ui/Component.php @@ -29,6 +29,11 @@ abstract class Component $this->viewport = $viewport; } + public function getViewport(): Viewport + { + return $this->viewport; + } + public function setWindow($window): void { $this->window = $window; @@ -49,18 +54,18 @@ abstract class Component ); if (isset($this->computedStyles[Margin::class]) && ($m = $this->computedStyles[Margin::class])) { - $this->viewport->x += $m->left; + $this->viewport->x = (int) ($this->viewport->x + $m->left); $this->viewport->width -= $m->right + $m->left; - $this->viewport->y += $m->top; + $this->viewport->y = (int) ($this->viewport->y + $m->top); $this->viewport->height -= $m->bottom + $m->top; } $this->contentViewport = clone $this->viewport; if (isset($this->computedStyles[Padding::class]) && ($p = $this->computedStyles[Padding::class])) { - $this->contentViewport->x += $p->left; + $this->contentViewport->x = (int) ($this->contentViewport->x + $p->left); $this->contentViewport->width -= $p->right + $p->left; - $this->contentViewport->y += $p->top; + $this->contentViewport->y = (int) ($this->contentViewport->y + $p->top); $this->contentViewport->height -= $p->bottom + $p->top; } @@ -131,4 +136,56 @@ abstract class Component { $this->children[] = $component; } + + /** + * Handle mouse click event + * @return bool True if event was handled + */ + public function handleMouseClick(float $mouseX, float $mouseY, int $button): bool + { + // Default implementation: propagate to children + foreach ($this->children as $child) { + if ($child->handleMouseClick($mouseX, $mouseY, $button)) { + return true; + } + } + return false; + } + + /** + * Handle mouse move event + */ + public function handleMouseMove(float $mouseX, float $mouseY): void + { + // Default implementation: propagate to children + foreach ($this->children as $child) { + $child->handleMouseMove($mouseX, $mouseY); + } + } + + /** + * Handle mouse button release event + */ + public function handleMouseRelease(float $mouseX, float $mouseY, int $button): void + { + // Default implementation: propagate to children + foreach ($this->children as $child) { + $child->handleMouseRelease($mouseX, $mouseY, $button); + } + } + + /** + * Handle mouse wheel event + * @return bool True if event was handled + */ + public function handleMouseWheel(float $mouseX, float $mouseY, float $deltaY): bool + { + // Default implementation: propagate to children + foreach ($this->children as $child) { + if ($child->handleMouseWheel($mouseX, $mouseY, $deltaY)) { + return true; + } + } + return false; + } } diff --git a/src/Ui/Widget/Container.php b/src/Ui/Widget/Container.php index f273710..ee95b4e 100644 --- a/src/Ui/Widget/Container.php +++ b/src/Ui/Widget/Container.php @@ -15,6 +15,24 @@ use PHPNative\Ui\Viewport; class Container extends Component { + // Scroll state + private float $scrollX = 0; + private float $scrollY = 0; + private float $contentWidth = 0; + private float $contentHeight = 0; + + // Scrollbar state + private bool $isDraggingScrollbarX = false; + private bool $isDraggingScrollbarY = false; + private float $dragStartX = 0; + private float $dragStartY = 0; + private float $scrollStartX = 0; + private float $scrollStartY = 0; + + // Scrollbar dimensions + private const SCROLLBAR_WIDTH = 12; + private const SCROLLBAR_MIN_SIZE = 20; + public function __construct( public string $style = '', ) {} @@ -25,12 +43,19 @@ class Container extends Component parent::layout($textRenderer); // Check if this container has flex layout - if (!isset($this->computedStyles[Flex::class])) { - return; + if (isset($this->computedStyles[Flex::class])) { + $flex = $this->computedStyles[Flex::class]; + $this->layoutChildren($flex, $textRenderer); + } else { + // Non-flex layout: just layout children normally + foreach ($this->children as $child) { + $child->setViewport($this->contentViewport); + $child->layout($textRenderer); + } } - $flex = $this->computedStyles[Flex::class]; - $this->layoutChildren($flex, $textRenderer); + // Calculate total content size after children are laid out + $this->calculateContentSize(); } private function layoutChildren(Flex $flex, null|TextRenderer $textRenderer): void @@ -82,7 +107,7 @@ class Container extends Component // Calculate remaining space for flex-grow items $remainingSpace = max(0, $availableSpace - $usedSpace); - $flexGrowSize = $flexGrowCount > 0 ? $remainingSpace / $flexGrowCount : 0; + $flexGrowSize = $flexGrowCount > 0 ? ($remainingSpace / $flexGrowCount) : 0; // Second pass: assign sizes and position children $currentPosition = $isRow ? $this->contentViewport->x : $this->contentViewport->y; @@ -96,7 +121,7 @@ class Container extends Component if ($isRow) { // Flex row - $childViewport->x = $currentPosition; + $childViewport->x = (int) $currentPosition; $childViewport->y = $this->contentViewport->y; $childViewport->width = $size; $childViewport->height = $this->contentViewport->height; @@ -104,7 +129,7 @@ class Container extends Component } else { // Flex column $childViewport->x = $this->contentViewport->x; - $childViewport->y = $currentPosition; + $childViewport->y = (int) $currentPosition; $childViewport->width = $this->contentViewport->width; $childViewport->height = $size; $currentPosition += $size; @@ -123,4 +148,322 @@ class Container extends Component Unit::Percent => ($availableSpace * $style->value) / 100, }; } + + private function calculateContentSize(): void + { + if (empty($this->children)) { + $this->contentWidth = $this->contentViewport->width; + $this->contentHeight = $this->contentViewport->height; + return; + } + + $maxX = 0; + $maxY = 0; + + foreach ($this->children as $child) { + $childViewport = $child->getViewport(); + $maxX = max($maxX, ($childViewport->x + $childViewport->width) - $this->contentViewport->x); + $maxY = max($maxY, ($childViewport->y + $childViewport->height) - $this->contentViewport->y); + } + + $this->contentWidth = max($this->contentViewport->width, $maxX); + $this->contentHeight = max($this->contentViewport->height, $maxY); + } + + private function hasOverflow(): array + { + $overflow = $this->computedStyles[\PHPNative\Tailwind\Style\Overflow::class] ?? null; + if (!$overflow) { + return ['x' => false, 'y' => false]; + } + + $needsScrollX = $this->contentWidth > $this->contentViewport->width; + $needsScrollY = $this->contentHeight > $this->contentViewport->height; + + return [ + 'x' => + + $needsScrollX && + in_array($overflow->x, [ + \PHPNative\Tailwind\Style\OverflowEnum::scroll, + \PHPNative\Tailwind\Style\OverflowEnum::auto, + ]) + , + 'y' => + + $needsScrollY && + in_array($overflow->y, [ + \PHPNative\Tailwind\Style\OverflowEnum::scroll, + \PHPNative\Tailwind\Style\OverflowEnum::auto, + ]) + , + ]; + } + + public function renderContent(null|TextRenderer $textRenderer = null): void + { + if (!$this->visible) { + return; + } + + $overflow = $this->hasOverflow(); + + // Save original viewport + $originalViewport = $this->contentViewport; + + // Apply scroll offset to children + if ($overflow['x'] || $overflow['y']) { + // Enable scissor test for clipping + rsgl_scissorStart( + $this->window, + (int) $this->contentViewport->x, + (int) $this->contentViewport->y, + (int) $this->contentViewport->width, + (int) $this->contentViewport->height, + ); + + // Render children with scroll offset + foreach ($this->children as $child) { + $child->setWindow($this->window); + + // Apply scroll offset + $childViewport = $child->getViewport(); + $childViewport->x = (int) ($childViewport->x - $this->scrollX); + $childViewport->y = (int) ($childViewport->y - $this->scrollY); + + $child->render($textRenderer); + $child->renderContent($textRenderer); + + // Restore position + $childViewport->x = (int) ($childViewport->x + $this->scrollX); + $childViewport->y = (int) ($childViewport->y + $this->scrollY); + } + + // Disable scissor test + rsgl_scissorEnd($this->window); + + // Render scrollbars + $this->renderScrollbars($overflow); + } else { + // No overflow, render normally + parent::renderContent($textRenderer); + } + } + + private function renderScrollbars(array $overflow): void + { + $scrollbarColor = [100, 100, 100, 200]; // Gray with some transparency + + // Vertical scrollbar + if ($overflow['y']) { + $scrollbarHeight = $this->contentViewport->height; + $thumbHeight = max( + self::SCROLLBAR_MIN_SIZE, + ($this->contentViewport->height / $this->contentHeight) * $scrollbarHeight, + ); + $maxScroll = $this->contentHeight - $this->contentViewport->height; + $thumbY = $this->contentViewport->y + (($this->scrollY / $maxScroll) * ($scrollbarHeight - $thumbHeight)); + + $scrollbarX = ($this->contentViewport->x + $this->contentViewport->width) - self::SCROLLBAR_WIDTH; + + // Track + rsgl_setColor($this->window, 200, 200, 200, 100); + rsgl_drawRectF( + $this->window, + (int) $scrollbarX, + (int) $this->contentViewport->y, + (int) self::SCROLLBAR_WIDTH, + (int) $scrollbarHeight, + ); + + // Thumb + rsgl_setColor( + $this->window, + $scrollbarColor[0], + $scrollbarColor[1], + $scrollbarColor[2], + $scrollbarColor[3], + ); + rsgl_drawRoundRectF( + $this->window, + (int) ($scrollbarX + 2), + (int) $thumbY, + (int) (self::SCROLLBAR_WIDTH - 4), + (int) $thumbHeight, + 4, + 4 + ); + } + + // Horizontal scrollbar + if ($overflow['x']) { + $scrollbarWidth = $this->contentViewport->width; + $thumbWidth = max( + self::SCROLLBAR_MIN_SIZE, + ($this->contentViewport->width / $this->contentWidth) * $scrollbarWidth, + ); + $maxScroll = $this->contentWidth - $this->contentViewport->width; + $thumbX = $this->contentViewport->x + (($this->scrollX / $maxScroll) * ($scrollbarWidth - $thumbWidth)); + + $scrollbarY = ($this->contentViewport->y + $this->contentViewport->height) - self::SCROLLBAR_WIDTH; + + // Track + rsgl_setColor($this->window, 200, 200, 200, 100); + rsgl_drawRectF( + $this->window, + (int) $this->contentViewport->x, + (int) $scrollbarY, + (int) $scrollbarWidth, + (int) self::SCROLLBAR_WIDTH, + ); + + // Thumb + rsgl_setColor( + $this->window, + $scrollbarColor[0], + $scrollbarColor[1], + $scrollbarColor[2], + $scrollbarColor[3], + ); + rsgl_drawRoundRectF( + $this->window, + (int) $thumbX, + (int) ($scrollbarY + 2), + (int) $thumbWidth, + (int) (self::SCROLLBAR_WIDTH - 4), + 4, + 4 + ); + } + } + + public function handleMouseClick(float $mouseX, float $mouseY, int $button): bool + { + // Check if click is on scrollbar + $overflow = $this->hasOverflow(); + + if ($overflow['y']) { + $scrollbarX = ($this->contentViewport->x + $this->contentViewport->width) - self::SCROLLBAR_WIDTH; + if ( + $mouseX >= $scrollbarX && + $mouseX <= ($scrollbarX + self::SCROLLBAR_WIDTH) && + $mouseY >= $this->contentViewport->y && + $mouseY <= ($this->contentViewport->y + $this->contentViewport->height) + ) { + $this->isDraggingScrollbarY = true; + $this->dragStartY = $mouseY; + $this->scrollStartY = $this->scrollY; + return true; + } + } + + if ($overflow['x']) { + $scrollbarY = ($this->contentViewport->y + $this->contentViewport->height) - self::SCROLLBAR_WIDTH; + if ( + $mouseY >= $scrollbarY && + $mouseY <= ($scrollbarY + self::SCROLLBAR_WIDTH) && + $mouseX >= $this->contentViewport->x && + $mouseX <= ($this->contentViewport->x + $this->contentViewport->width) + ) { + $this->isDraggingScrollbarX = true; + $this->dragStartX = $mouseX; + $this->scrollStartX = $this->scrollX; + return true; + } + } + + // Propagate to children if not on scrollbar + foreach ($this->children as $child) { + if (method_exists($child, 'handleMouseClick')) { + if ($child->handleMouseClick($mouseX + $this->scrollX, $mouseY + $this->scrollY, $button)) { + return true; + } + } + } + + return false; + } + + public function handleMouseMove(float $mouseX, float $mouseY): void + { + if ($this->isDraggingScrollbarY) { + $deltaY = $mouseY - $this->dragStartY; + $scrollbarHeight = $this->contentViewport->height; + $thumbHeight = max( + self::SCROLLBAR_MIN_SIZE, + ($this->contentViewport->height / $this->contentHeight) * $scrollbarHeight, + ); + $maxScroll = $this->contentHeight - $this->contentViewport->height; + $scrollRatio = $deltaY / ($scrollbarHeight - $thumbHeight); + $this->scrollY = max(0, min($maxScroll, $this->scrollStartY + ($scrollRatio * $maxScroll))); + } + + if ($this->isDraggingScrollbarX) { + $deltaX = $mouseX - $this->dragStartX; + $scrollbarWidth = $this->contentViewport->width; + $thumbWidth = max( + self::SCROLLBAR_MIN_SIZE, + ($this->contentViewport->width / $this->contentWidth) * $scrollbarWidth, + ); + $maxScroll = $this->contentWidth - $this->contentViewport->width; + $scrollRatio = $deltaX / ($scrollbarWidth - $thumbWidth); + $this->scrollX = max(0, min($maxScroll, $this->scrollStartX + ($scrollRatio * $maxScroll))); + } + + // Propagate to children + foreach ($this->children as $child) { + if (method_exists($child, 'handleMouseMove')) { + $child->handleMouseMove($mouseX + $this->scrollX, $mouseY + $this->scrollY); + } + } + } + + public function handleMouseRelease(float $mouseX, float $mouseY, int $button): void + { + $this->isDraggingScrollbarX = false; + $this->isDraggingScrollbarY = false; + + // Propagate to children + foreach ($this->children as $child) { + if (method_exists($child, 'handleMouseRelease')) { + $child->handleMouseRelease($mouseX + $this->scrollX, $mouseY + $this->scrollY, $button); + } + } + } + + public function handleMouseWheel(float $mouseX, float $mouseY, float $deltaY): bool + { + $overflow = $this->hasOverflow(); + + // Check if mouse is over this container + if ( + $mouseX >= $this->contentViewport->x && + $mouseX <= ($this->contentViewport->x + $this->contentViewport->width) && + $mouseY >= $this->contentViewport->y && + $mouseY <= ($this->contentViewport->y + $this->contentViewport->height) + ) { + if ($overflow['y']) { + $maxScroll = $this->contentHeight - $this->contentViewport->height; + $this->scrollY = max(0, min($maxScroll, $this->scrollY + ($deltaY * 20))); + return true; + } + + if ($overflow['x']) { + $maxScroll = $this->contentWidth - $this->contentViewport->width; + $this->scrollX = max(0, min($maxScroll, $this->scrollX + ($deltaY * 20))); + return true; + } + } + + // Propagate to children + foreach ($this->children as $child) { + if (method_exists($child, 'handleMouseWheel')) { + if ($child->handleMouseWheel($mouseX + $this->scrollX, $mouseY + $this->scrollY, $deltaY)) { + return true; + } + } + } + + return false; + } }