First Scroll

This commit is contained in:
Thomas Peterson 2025-10-22 19:25:29 +02:00
parent f3fe8934ef
commit 4e510b2ac2
5 changed files with 508 additions and 12 deletions

View File

@ -0,0 +1,64 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use PHPNative\Framework\Application;
use PHPNative\Ui\Widget\Container;
use PHPNative\Ui\Widget\Label;
// Check PHP version
if (PHP_VERSION_ID < 80100) {
die("This demo requires PHP 8.1+ for Fiber support.\nYour version: " . PHP_VERSION . "\n");
}
$app = new Application('Overflow Scroll Demo', 800, 600);
// Main container
$mainContainer = new Container(style: 'p-4 bg-gray-100');
// Title
$title = new Label(text: 'Overflow Scroll Demo', style: 'text-xl text-black p-2');
$mainContainer->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();

View File

@ -1,5 +1,6 @@
<?php <?php
use PHPNative\Ui\Widget\Container;
use PHPNative\Ui\Widget\Label; use PHPNative\Ui\Widget\Label;
require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../vendor/autoload.php';
@ -24,6 +25,19 @@ $label = new Label(
style: 'text-red-500', style: 'text-red-500',
); );
$containerMenu->addComponent($label); $containerMenu->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); $container->addComponent($containerMenu);
$app->setRoot($container); $app->setRoot($container);
$app->run(); $app->run();

View File

@ -126,7 +126,7 @@ class Application
// Propagate mouse move to root component // Propagate mouse move to root component
if ($this->rootComponent) { if ($this->rootComponent) {
// $this->rootComponent->handleMouseMove($this->mouseX, $this->mouseY); $this->rootComponent->handleMouseMove($this->mouseX, $this->mouseY);
} }
break; break;
@ -138,6 +138,24 @@ class Application
$this->rootComponent->handleMouseClick($this->mouseX, $this->mouseY, $button); $this->rootComponent->handleMouseClick($this->mouseX, $this->mouseY, $button);
} }
break; 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;
} }
} }
} }

View File

@ -29,6 +29,11 @@ abstract class Component
$this->viewport = $viewport; $this->viewport = $viewport;
} }
public function getViewport(): Viewport
{
return $this->viewport;
}
public function setWindow($window): void public function setWindow($window): void
{ {
$this->window = $window; $this->window = $window;
@ -49,18 +54,18 @@ abstract class Component
); );
if (isset($this->computedStyles[Margin::class]) && ($m = $this->computedStyles[Margin::class])) { 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->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->viewport->height -= $m->bottom + $m->top;
} }
$this->contentViewport = clone $this->viewport; $this->contentViewport = clone $this->viewport;
if (isset($this->computedStyles[Padding::class]) && ($p = $this->computedStyles[Padding::class])) { 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->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; $this->contentViewport->height -= $p->bottom + $p->top;
} }
@ -131,4 +136,56 @@ abstract class Component
{ {
$this->children[] = $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;
}
} }

View File

@ -15,6 +15,24 @@ use PHPNative\Ui\Viewport;
class Container extends Component 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 function __construct(
public string $style = '', public string $style = '',
) {} ) {}
@ -25,12 +43,19 @@ class Container extends Component
parent::layout($textRenderer); parent::layout($textRenderer);
// Check if this container has flex layout // Check if this container has flex layout
if (!isset($this->computedStyles[Flex::class])) { if (isset($this->computedStyles[Flex::class])) {
return;
}
$flex = $this->computedStyles[Flex::class]; $flex = $this->computedStyles[Flex::class];
$this->layoutChildren($flex, $textRenderer); $this->layoutChildren($flex, $textRenderer);
} else {
// Non-flex layout: just layout children normally
foreach ($this->children as $child) {
$child->setViewport($this->contentViewport);
$child->layout($textRenderer);
}
}
// Calculate total content size after children are laid out
$this->calculateContentSize();
} }
private function layoutChildren(Flex $flex, null|TextRenderer $textRenderer): void private function layoutChildren(Flex $flex, null|TextRenderer $textRenderer): void
@ -82,7 +107,7 @@ class Container extends Component
// Calculate remaining space for flex-grow items // Calculate remaining space for flex-grow items
$remainingSpace = max(0, $availableSpace - $usedSpace); $remainingSpace = max(0, $availableSpace - $usedSpace);
$flexGrowSize = $flexGrowCount > 0 ? $remainingSpace / $flexGrowCount : 0; $flexGrowSize = $flexGrowCount > 0 ? ($remainingSpace / $flexGrowCount) : 0;
// Second pass: assign sizes and position children // Second pass: assign sizes and position children
$currentPosition = $isRow ? $this->contentViewport->x : $this->contentViewport->y; $currentPosition = $isRow ? $this->contentViewport->x : $this->contentViewport->y;
@ -96,7 +121,7 @@ class Container extends Component
if ($isRow) { if ($isRow) {
// Flex row // Flex row
$childViewport->x = $currentPosition; $childViewport->x = (int) $currentPosition;
$childViewport->y = $this->contentViewport->y; $childViewport->y = $this->contentViewport->y;
$childViewport->width = $size; $childViewport->width = $size;
$childViewport->height = $this->contentViewport->height; $childViewport->height = $this->contentViewport->height;
@ -104,7 +129,7 @@ class Container extends Component
} else { } else {
// Flex column // Flex column
$childViewport->x = $this->contentViewport->x; $childViewport->x = $this->contentViewport->x;
$childViewport->y = $currentPosition; $childViewport->y = (int) $currentPosition;
$childViewport->width = $this->contentViewport->width; $childViewport->width = $this->contentViewport->width;
$childViewport->height = $size; $childViewport->height = $size;
$currentPosition += $size; $currentPosition += $size;
@ -123,4 +148,322 @@ class Container extends Component
Unit::Percent => ($availableSpace * $style->value) / 100, 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;
}
} }