Fixed Scroll

This commit is contained in:
Thomas Peterson 2025-10-23 08:56:22 +02:00
parent 29102c567c
commit 5700809299
4 changed files with 113 additions and 38 deletions

View File

@ -11,10 +11,11 @@ if (PHP_VERSION_ID < 80100) {
}
$app = new \PHPNative\Framework\Application('ToDo Demo', 700, 500);
$scrollContainer = new Container(style: 'bg-red m-4 p-4');
$container = new Container('m-10 p-10 bg-lime-500');
$scrollContainer = new Container(style: 'overflow-y-auto bg-red m-4 p-4');
// Add many items to trigger overflow
for ($i = 1; $i <= 5; $i++) {
for ($i = 1; $i <= 200; $i++) {
$item = new Container(style: 'bg-blue-500 m-2 p-3');
$label = new Label(
text: "Item {$i}",
@ -23,5 +24,6 @@ for ($i = 1; $i <= 5; $i++) {
$item->addComponent($label);
$scrollContainer->addComponent($item);
}
$app->setRoot($scrollContainer);
$container->addComponent($scrollContainer);
$app->setRoot($container);
$app->run();

View File

@ -151,7 +151,6 @@ class Application
case RGFW_mouseScroll:
$deltaY = $event[1] ?? 0;
var_dump($event);
// Propagate wheel to root component
if ($this->rootComponent) {

View File

@ -35,6 +35,33 @@ abstract class Component
return $this->viewport;
}
public function getContentViewport(): Viewport
{
return $this->contentViewport;
}
public function setContentViewport(Viewport $viewport): void
{
$this->contentViewport = $viewport;
}
/**
* Apply offset to viewport and all child viewports recursively
*/
public function applyScrollOffset(int $offsetX, int $offsetY): void
{
// Offset this component's viewports
$this->viewport->x = (int) ($this->viewport->x - $offsetX);
$this->viewport->y = (int) ($this->viewport->y - $offsetY);
$this->contentViewport->x = (int) ($this->contentViewport->x - $offsetX);
$this->contentViewport->y = (int) ($this->contentViewport->y - $offsetY);
// Recursively apply to all children
foreach ($this->children as $child) {
$child->applyScrollOffset($offsetX, $offsetY);
}
}
public function setWindow($window): void
{
$this->window = $window;

View File

@ -45,6 +45,17 @@ class Container extends Component
$flexStyle = $this->computedStyles[Flex::class] ?? null;
$hasFlexGrow = $flexStyle && $flexStyle->type !== FlexTypeEnum::none;
// Check if overflow is set (if yes, container should not auto-expand)
$overflow = $this->computedStyles[\PHPNative\Tailwind\Style\Overflow::class] ?? null;
$hasOverflowX = $overflow && in_array($overflow->x, [
\PHPNative\Tailwind\Style\OverflowEnum::scroll,
\PHPNative\Tailwind\Style\OverflowEnum::auto,
]);
$hasOverflowY = $overflow && in_array($overflow->y, [
\PHPNative\Tailwind\Style\OverflowEnum::scroll,
\PHPNative\Tailwind\Style\OverflowEnum::auto,
]);
// A container is a flex container if it has flex/flex-row/flex-col in style
// (but not flex-1, flex-auto, flex-none which make it a flex child)
$isFlexContainer =
@ -55,10 +66,17 @@ class Container extends Component
preg_match('/(?<![a-z-])flex(?![a-z-])/', $this->style)
);
// If overflow is set, treat it as explicit size (don't auto-expand)
$hasExplicitWidth =
isset($this->computedStyles[Width::class]) || isset($this->computedStyles[Basis::class]) || $hasFlexGrow;
isset($this->computedStyles[Width::class]) ||
isset($this->computedStyles[Basis::class]) ||
$hasFlexGrow ||
$hasOverflowX;
$hasExplicitHeight =
isset($this->computedStyles[Height::class]) || isset($this->computedStyles[Basis::class]) || $hasFlexGrow;
isset($this->computedStyles[Height::class]) ||
isset($this->computedStyles[Basis::class]) ||
$hasFlexGrow ||
$hasOverflowY;
// Check if this container has flex layout
if ($isFlexContainer) {
@ -69,11 +87,14 @@ class Container extends Component
foreach ($this->children as $child) {
// Create a viewport for the child with available width but let height be determined by child
$childViewport = new Viewport();
$childViewport->x = $this->contentViewport->x;
$childViewport->y = (int) $currentY;
$childViewport->width = $this->contentViewport->width; // Maximum available width
$childViewport->height = $this->contentViewport->height; // Maximum available height
$childViewport = new Viewport(
x: $this->contentViewport->x,
y: (int) $currentY,
width: $this->contentViewport->width,
height: $this->contentViewport->height,
windowWidth: $this->contentViewport->windowWidth,
windowHeight: $this->contentViewport->windowHeight
);
$child->setViewport($childViewport);
$child->layout($textRenderer);
@ -171,21 +192,27 @@ class Container extends Component
$size = $childSize['flexGrow'] ? $flexGrowSize : $childSize['size'];
// Create viewport for child
$childViewport = new Viewport();
if ($isRow) {
// Flex row
$childViewport->x = (int) $currentPosition;
$childViewport->y = $this->contentViewport->y;
$childViewport->width = $size;
$childViewport->height = $this->contentViewport->height;
$childViewport = new Viewport(
x: (int) $currentPosition,
y: $this->contentViewport->y,
width: $size,
height: $this->contentViewport->height,
windowWidth: $this->contentViewport->windowWidth,
windowHeight: $this->contentViewport->windowHeight
);
$currentPosition += $size;
} else {
// Flex column
$childViewport->x = $this->contentViewport->x;
$childViewport->y = (int) $currentPosition;
$childViewport->width = $this->contentViewport->width;
$childViewport->height = $size;
$childViewport = new Viewport(
x: $this->contentViewport->x,
y: (int) $currentPosition,
width: $this->contentViewport->width,
height: $size,
windowWidth: $this->contentViewport->windowWidth,
windowHeight: $this->contentViewport->windowHeight
);
$currentPosition += $size;
}
@ -270,32 +297,52 @@ class Container extends Component
// 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,
);
$scissorX = (int) $this->contentViewport->x;
$scissorY = (int) $this->contentViewport->y;
$scissorW = (int) $this->contentViewport->width;
$scissorH = (int) $this->contentViewport->height;
// Render children with scroll offset
// IMPORTANT: RSGL uses batch rendering!
// We need to flush the batch before/after scissor to ensure correct clipping
// Step 1: Flush any pending draw calls before scissor
rsgl_render($this->window);
// Step 2: Enable scissor test for clipping
$windowHeight = $this->contentViewport->windowHeight;
$invertedY = $windowHeight - ($scissorY + $scissorH);
rsgl_scissorStart($this->window, $scissorX, $invertedY, $scissorW, $scissorH);
// Step 3: Render children with scroll offset (batches draw calls)
foreach ($this->children as $child) {
$child->setWindow($this->window);
// Apply scroll offset
// Apply scroll offset recursively to child and all its descendants
$child->applyScrollOffset((int) $this->scrollX, (int) $this->scrollY);
// Performance optimization: skip completely invisible children
$childViewport = $child->getViewport();
$childViewport->x = (int) ($childViewport->x - $this->scrollX);
$childViewport->y = (int) ($childViewport->y - $this->scrollY);
$isVisible =
$childViewport->x + $childViewport->width > $scissorX &&
$childViewport->x < $scissorX + $scissorW &&
$childViewport->y + $childViewport->height > $scissorY &&
$childViewport->y < $scissorY + $scissorH;
$child->render($textRenderer);
$child->renderContent($textRenderer);
if ($isVisible) {
// Render - batches draw calls
$child->render($textRenderer);
$child->renderContent($textRenderer);
}
// Restore position
$childViewport->x = (int) ($childViewport->x + $this->scrollX);
$childViewport->y = (int) ($childViewport->y + $this->scrollY);
// Restore by applying negative offset
$child->applyScrollOffset((int) -$this->scrollX, (int) -$this->scrollY);
}
// Disable scissor test
// Step 4: Flush the batch while scissor is still active
rsgl_render($this->window);
// Step 5: Disable scissor test
rsgl_scissorEnd($this->window);
// Render scrollbars