First Scroll

This commit is contained in:
Thomas Peterson 2025-10-23 07:46:13 +02:00
parent 4e510b2ac2
commit 29102c567c
9 changed files with 265 additions and 25 deletions

View File

@ -0,0 +1,63 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use PHPNative\Framework\Application;
use PHPNative\Ui\Widget\Container;
use PHPNative\Ui\Widget\Button;
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('Button Example', 800, 600);
// Main container
$mainContainer = new Container(style: 'p-10 bg-gray-100');
// Title
$title = new Label(
text: 'Button Sizing Example',
style: 'text-xl text-black'
);
$mainContainer->addComponent($title);
// Button with m-10 p-10, should be: 12 (text) + 10 (padding-top) + 10 (padding-bottom) = 32px height
$button1 = new Button(
text: 'Click Me',
style: 'm-10 p-10 bg-blue-500 rounded-lg'
);
$mainContainer->addComponent($button1);
// Button with different padding
$button2 = new Button(
text: 'Another Button',
style: 'm-5 p-15 bg-green-500 rounded-lg'
);
$mainContainer->addComponent($button2);
// Button with no padding
$button3 = new Button(
text: 'No Padding',
style: 'm-10 bg-red-500 rounded-lg'
);
$mainContainer->addComponent($button3);
// Container with multiple labels (should stack)
$labelContainer = new Container(style: 'm-10 p-10 bg-white rounded-lg');
$labelContainer->addComponent(new Label(text: 'Label 1', style: 'text-black'));
$labelContainer->addComponent(new Label(text: 'Label 2', style: 'text-black'));
$labelContainer->addComponent(new Label(text: 'Label 3', style: 'text-black'));
$mainContainer->addComponent($labelContainer);
// Info text
$info = new Label(
text: 'Containers should auto-size to their content',
style: 'text-black m-10'
);
$mainContainer->addComponent($info);
$app->setRoot($mainContainer);
$app->run();

27
examples/TestStack.php Normal file
View File

@ -0,0 +1,27 @@
<?php
use PHPNative\Ui\Widget\Container;
use PHPNative\Ui\Widget\Label;
require_once __DIR__ . '/../vendor/autoload.php';
// 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 \PHPNative\Framework\Application('ToDo Demo', 700, 500);
$scrollContainer = new Container(style: 'bg-red m-4 p-4');
// Add many items to trigger overflow
for ($i = 1; $i <= 5; $i++) {
$item = new Container(style: 'bg-blue-500 m-2 p-3');
$label = new Label(
text: "Item {$i}",
style: 'text-green-500',
);
$item->addComponent($label);
$scrollContainer->addComponent($item);
}
$app->setRoot($scrollContainer);
$app->run();

View File

@ -25,14 +25,14 @@ $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'); $scrollContainer = new Container(style: 'overflow-y-auto bg-red m-4 p-4');
// Add many items to trigger overflow // Add many items to trigger overflow
for ($i = 1; $i <= 20; $i++) { for ($i = 1; $i <= 100; $i++) {
$item = new Container(style: 'bg-blue-500 m-2 p-3 rounded-lg'); $item = new Container(style: 'bg-blue-500 m-2 p-3');
$label = new Label( $label = new Label(
text: "Item {$i} - Scroll vertically with mouse wheel or drag the scrollbar", text: "Item {$i}",
style: 'text-white', style: 'text-green-500',
); );
$item->addComponent($label); $item->addComponent($label);
$scrollContainer->addComponent($item); $scrollContainer->addComponent($item);

View File

@ -21,6 +21,7 @@ class Application
private $mouseY; private $mouseY;
private Viewport $viewport; private Viewport $viewport;
private bool $shouldBeReLayouted = true; private bool $shouldBeReLayouted = true;
private float $pixelRatio = 2;
public function __construct(string $title, int $width = 800, int $height = 600) public function __construct(string $title, int $width = 800, int $height = 600)
{ {
@ -150,6 +151,7 @@ class Application
case RGFW_mouseScroll: case RGFW_mouseScroll:
$deltaY = $event[1] ?? 0; $deltaY = $event[1] ?? 0;
var_dump($event);
// Propagate wheel to root component // Propagate wheel to root component
if ($this->rootComponent) { if ($this->rootComponent) {
@ -191,6 +193,7 @@ class Application
protected function layout(): void protected function layout(): void
{ {
// Render root component tree // Render root component tree
if ($this->rootComponent && $this->shouldBeReLayouted) { if ($this->rootComponent && $this->shouldBeReLayouted) {
$this->rootComponent->setViewport($this->viewport); $this->rootComponent->setViewport($this->viewport);
$this->rootComponent->setWindow($this->window); $this->rootComponent->setWindow($this->window);

View File

@ -131,6 +131,35 @@ class TextRenderer
rfont_setColor($this->window, $r, $g, $b, $a); rfont_setColor($this->window, $r, $g, $b, $a);
} }
/**
* Measure text dimensions
*
* @param string $text Text to measure
* @param int|null $size Font size
* @return array [width, height]
*/
public function measureText(string $text, null|int $size = null): array
{
if (!$this->initialized) {
return [0, 0];
}
if ($size === null) {
$size = $this->fontSize;
}
// Try to use rfont_getTextSize if available, otherwise estimate
if (function_exists('rfont_getTextSize')) {
$dimensions = rfont_getTextSize($this->window, $text, $size);
return [(int) $dimensions[0], (int) $dimensions[1]];
}
// Fallback: estimate based on font size
$width = strlen($text) * ($size * 0.6); // Rough estimate
$height = $size * 1.2; // Line height
return [(int) $width, (int) $height];
}
/** /**
* Update framebuffer size (call on window resize) * Update framebuffer size (call on window resize)
* *

View File

@ -14,6 +14,7 @@ abstract class Component
protected $children = []; protected $children = [];
protected $window; protected $window;
protected $pixelRatio;
protected string $styles = ''; protected string $styles = '';
@ -39,6 +40,11 @@ abstract class Component
$this->window = $window; $this->window = $window;
} }
public function setPixelRatio($pixelRatio): void
{
$this->pixelRatio = $pixelRatio;
}
public function update(): void public function update(): void
{ {
foreach ($this->children as $child) { foreach ($this->children as $child) {
@ -55,23 +61,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 = (int) ($this->viewport->x + $m->left); $this->viewport->x = (int) ($this->viewport->x + $m->left);
$this->viewport->width -= $m->right + $m->left; $this->viewport->width = max(0, ($this->viewport->width - $m->right) - $m->left);
$this->viewport->y = (int) ($this->viewport->y + $m->top); $this->viewport->y = (int) ($this->viewport->y + $m->top);
$this->viewport->height -= $m->bottom + $m->top; $this->viewport->height = max(0, ($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 = (int) ($this->contentViewport->x + $p->left); $this->contentViewport->x = (int) ($this->contentViewport->x + $p->left);
$this->contentViewport->width -= $p->right + $p->left; $this->contentViewport->width = max(0, ($this->contentViewport->width - $p->right) - $p->left);
$this->contentViewport->y = (int) ($this->contentViewport->y + $p->top); $this->contentViewport->y = (int) ($this->contentViewport->y + $p->top);
$this->contentViewport->height -= $p->bottom + $p->top; $this->contentViewport->height = max(0, ($this->contentViewport->height - $p->bottom) - $p->top);
}
foreach ($this->children as $child) {
$child->setViewport($this->contentViewport);
$child->layout($textRenderer);
} }
} }

36
src/Ui/Widget/Button.php Normal file
View File

@ -0,0 +1,36 @@
<?php
namespace PHPNative\Ui\Widget;
use PHPNative\Framework\TextRenderer;
class Button extends Container
{
private Label $label;
public function __construct(
public string $text = '',
public string $style = '',
) {
parent::__construct($style);
// Create label inside button
$this->label = new Label(
text: $text,
style: ''
);
$this->addComponent($this->label);
}
public function setText(string $text): void
{
$this->text = $text;
$this->label->setText($text);
}
public function getText(): string
{
return $this->text;
}
}

View File

@ -41,21 +41,75 @@ class Container extends Component
{ {
// Call parent to compute styles and setup viewports // Call parent to compute styles and setup viewports
parent::layout($textRenderer); parent::layout($textRenderer);
// Check if container has explicit width/height or is a flex child
$flexStyle = $this->computedStyles[Flex::class] ?? null;
$hasFlexGrow = $flexStyle && $flexStyle->type !== FlexTypeEnum::none;
// 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 =
$flexStyle &&
(
preg_match('/\bflex-row\b/', $this->style) ||
preg_match('/\bflex-col\b/', $this->style) ||
preg_match('/(?<![a-z-])flex(?![a-z-])/', $this->style)
);
$hasExplicitWidth =
isset($this->computedStyles[Width::class]) || isset($this->computedStyles[Basis::class]) || $hasFlexGrow;
$hasExplicitHeight =
isset($this->computedStyles[Height::class]) || isset($this->computedStyles[Basis::class]) || $hasFlexGrow;
// Check if this container has flex layout // Check if this container has flex layout
if (isset($this->computedStyles[Flex::class])) { if ($isFlexContainer) {
$flex = $this->computedStyles[Flex::class]; $this->layoutChildren($flexStyle, $textRenderer);
$this->layoutChildren($flex, $textRenderer);
} else { } else {
// Non-flex layout: just layout children normally // Non-flex layout: stack children vertically
$currentY = $this->contentViewport->y;
foreach ($this->children as $child) { foreach ($this->children as $child) {
$child->setViewport($this->contentViewport); // 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
$child->setViewport($childViewport);
$child->layout($textRenderer); $child->layout($textRenderer);
// Get the actual height after layout
$actualHeight = $child->getViewport()->height;
$currentY += $actualHeight;
} }
} }
// Calculate total content size after children are laid out // Calculate total content size after children are laid out
$this->calculateContentSize(); $this->calculateContentSize();
// Adjust viewport to content size if no explicit size
// But limit to available parent size (for proper overflow/scrolling)
$padding = $this->computedStyles[\PHPNative\Tailwind\Style\Padding::class] ?? null;
$paddingX = $padding ? ($padding->left + $padding->right) : 0;
$paddingY = $padding ? ($padding->top + $padding->bottom) : 0;
// Store original available size before adjustment
$availableWidth = $this->viewport->width;
$availableHeight = $this->viewport->height;
if (!$hasExplicitWidth) {
// Set viewport to min(contentSize + padding, availableSize)
$desiredWidth = $this->contentWidth + $paddingX;
$this->viewport->width = (int) min($desiredWidth, $availableWidth);
$this->contentViewport->width = max(0, $this->viewport->width - ((int) $paddingX));
}
if (!$hasExplicitHeight) {
// Set viewport to min(contentSize + padding, availableSize)
$desiredHeight = $this->contentHeight + $paddingY;
$this->viewport->height = (int) min($desiredHeight, $availableHeight);
$this->contentViewport->height = max(0, $this->viewport->height - ((int) $paddingY));
}
} }
private function layoutChildren(Flex $flex, null|TextRenderer $textRenderer): void private function layoutChildren(Flex $flex, null|TextRenderer $textRenderer): void
@ -152,8 +206,9 @@ class Container extends Component
private function calculateContentSize(): void private function calculateContentSize(): void
{ {
if (empty($this->children)) { if (empty($this->children)) {
$this->contentWidth = $this->contentViewport->width; // No children: content size is 0
$this->contentHeight = $this->contentViewport->height; $this->contentWidth = 0;
$this->contentHeight = 0;
return; return;
} }
@ -166,8 +221,9 @@ class Container extends Component
$maxY = max($maxY, ($childViewport->y + $childViewport->height) - $this->contentViewport->y); $maxY = max($maxY, ($childViewport->y + $childViewport->height) - $this->contentViewport->y);
} }
$this->contentWidth = max($this->contentViewport->width, $maxX); // Content size is the actual space used by children within contentViewport
$this->contentHeight = max($this->contentViewport->height, $maxY); $this->contentWidth = $maxX;
$this->contentHeight = $maxY;
} }
private function hasOverflow(): array private function hasOverflow(): array
@ -291,7 +347,7 @@ class Container extends Component
(int) (self::SCROLLBAR_WIDTH - 4), (int) (self::SCROLLBAR_WIDTH - 4),
(int) $thumbHeight, (int) $thumbHeight,
4, 4,
4 4,
); );
} }
@ -332,7 +388,7 @@ class Container extends Component
(int) $thumbWidth, (int) $thumbWidth,
(int) (self::SCROLLBAR_WIDTH - 4), (int) (self::SCROLLBAR_WIDTH - 4),
4, 4,
4 4,
); );
} }
} }

View File

@ -8,6 +8,9 @@ use PHPNative\Ui\Component;
class Label extends Component class Label extends Component
{ {
private int $intrinsicWidth = 0;
private int $intrinsicHeight = 0;
public function __construct( public function __construct(
public string $text = '', public string $text = '',
public string $style = '', public string $style = '',
@ -18,6 +21,28 @@ class Label extends Component
$this->text = $text; $this->text = $text;
} }
public function layout(null|TextRenderer $textRenderer = null): void
{
// Call parent to compute styles and setup viewports
parent::layout($textRenderer);
// Measure text to get intrinsic size
if ($textRenderer !== null && $textRenderer->isInitialized()) {
$textStyle = $this->computedStyles[Text::class] ?? new Text();
[$this->intrinsicWidth, $this->intrinsicHeight] = $textRenderer->measureText($this->text, $textStyle->size);
// Adjust viewport to fit text if no explicit size
if (!isset($this->computedStyles[\PHPNative\Tailwind\Style\Width::class])) {
$this->viewport->width = $this->intrinsicWidth;
$this->contentViewport->width = $this->intrinsicWidth;
}
if (!isset($this->computedStyles[\PHPNative\Tailwind\Style\Height::class])) {
$this->viewport->height = $this->intrinsicHeight;
$this->contentViewport->height = $this->intrinsicHeight;
}
}
}
public function renderContent(null|TextRenderer $textRenderer = null): void public function renderContent(null|TextRenderer $textRenderer = null): void
{ {
if (!$this->visible || $textRenderer === null || !$textRenderer->isInitialized()) { if (!$this->visible || $textRenderer === null || !$textRenderer->isInitialized()) {